🐍 Python

[Python] influxdb 삽입 시 timezone 주의사항 (feat. datetime.now()를 지양하자)

iseunghan 2024. 2. 23. 15:37
반응형

Intro

파이썬으로 InfluxDB에 데이터를 저장시키는 모듈을 개발하다 이상한점이 생겼습니다. KST로 넣었는데 이게 그대로 UTC로 들어가 있거나 분명 UTC로 저장시켰는데 KST로 저장되어있거나.. 희한한 현상이 발생했습니다. 그래서 파이썬 코드를 전부 까보며 influxDB 저장할 때 시간을 변환하는지와 서버 타임존과 관련이 있는지 알아보도록 하겠습니다.

서버의 타임존이 다르면, influxdb에는 시간이 어떻게 찍힐까?

TL;DR

💡 서버 시간에 따라서 `datetime.timestamp()` 함수가 UTC 시간으로 보정하여 변환한다. 그렇기 때문에 서버 타임존에 따라서 timestamp()의 결과가 달라질 뿐, influxdb_client 라이브러리를 통해 데이터 삽입 시에는 따로 시간이 변환되지 않는다. 입력 시간을 무조건 UTC로 간주한다.

공통 테스트 코드

client = InfluxDBClient(url='http://localhost:8086', token='my-secret-token', org="myorg")

now = datetime.now()

utc = pytz.timezone('UTC')
seoul = pytz.timezone('Asia/Seoul')

now_utc = datetime.utcnow()
now_kst = datetime.now(tz=seoul)
print("------------------------------------------")
print('UTC tz: ', now_utc)
print('KST tz: ', now_kst)
print("------------------------------------------")

utc_nano = int((now_utc.timestamp()) * 10 ** 9)
kst_nano = int((now_kst.timestamp()) * 10 ** 9)
print("UTC-NS: ", utc_nano)
print("KST-NS: ", kst_nano)
print("------------------------------------------")

utc_body = Point(f"test-measure-{str(utc_nano)[-3:-1]}").time(utc_nano).field('utc', utc_nano)
kst_body = Point(f"test-measure-{str(utc_nano)[-3:-1]}").time(kst_nano).field('kst', kst_nano)

write_api = client.write_api(write_options=SYNCHRONOUS)
write_api.write(bucket='test-bucket', org='myorg', record=kst_body)
write_api.write(bucket='test-bucket', org='myorg', record=utc_body)
write_api.close()
print("UTC-Influx-Point --> ", utc_body)
print("KST-Influx-Point --> ", kst_body)
print("------------------------------------------")

client.close()

Case 1) 서버 시간: KST 설정

/ # date
Fri Feb 23 12:22:05 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 03:22:10.295505
KST tz:  2024-02-23 12:22:10.295507+09:00
------------------------------------------
UTC-NS:  1708626130295505152 # -9시간 됨, **2024-02-22T18:22:10.295Z**
KST-NS:  1708658530295506944 # -9시간 됨, **2024-02-23T03:22:10.295Z**
------------------------------------------
UTC-Influx-Point -->  test-measure-15 utc=1708626130295505152i 1708626130295505152
KST-Influx-Point -->  test-measure-15 kst=1708658530295506944i 1708658530295506944
------------------------------------------

influxdb 결과

  • UTC, _time: 2024-02-22T18:22:10.295Z, _value: 1708626130295505200 ,
    결론: UTC지만, 서버시간이 KST라 -9시간 보정됨

  • KST, _time: 2024-02-23T03:22:10.295Z, _value: 1708658530295507000 ,
    결론: KST이고, 서버가 KST라 -9시간 보정됨

Case 2) 서버 시간: UTC 설정

/ # date
Fri Feb 23 03:32:49 UTC 2024

/ # python3 Test.py
------------------------------------------
UTC-pytz:  UTC
Seoul-pytz:  Asia/Seoul
------------------------------------------
UTC tz:  2024-02-23 03:32:53.333882
KST tz:  2024-02-23 12:32:53.333884+09:00
------------------------------------------
UTC-NS:  1708659173333882112 # -0시간 됨, **2024-02-23T03:32:53.333Z**
KST-NS:  1708659173333883904 # -9시간 됨, **2024-02-23T03:32:53.333Z**
------------------------------------------
UTC-Influx-Point -->  test-measure-11 utc=1708659173333882112i 1708659173333882112
KST-Influx-Point -->  test-measure-11 kst=1708659173333883904i 1708659173333883904
------------------------------------------

  • UTC, _time: 2024-02-23T03:32:53.333Z, _value: 1708659173333882000 , 결론: UTC이므로 그대로 저장됨

  • KST, _time: 2024-02-23T03:32:53.333Z, _value: 1708659173333884000 , 결론: KST이므로 -9시간 보정됨

서버 시간이 다르면 왜 시간이 다르게 저장될까? - 문제는 timestamp()이다

서버 시간이 다를 때, 왜 시간이 다르게 보정됐을까요?

문제는 바로 datetime.timestamp() 함수에 있습니다.

보통 datetime.now()를 사용하면, 기본적으로 tzinfo가 None으로 들어갑니다.

@classmethod
def now(cls, tz=None):
    "Construct a datetime from time.time() and optional time zone info."
    t = _time.time()
    return cls.fromtimestamp(t, tz)

그래서 아래 timestamp 부분을 살펴보면, tzinfo가 None일 때 _mktime()을 호출합니다.

def timestamp(self):
      "Return POSIX timestamp as float"
      if self._tzinfo is None:
          s = self._mktime()
          return s + self.microsecond / 1e6
      else:
          return (self - _EPOCH).total_seconds()

_mktime 함수를 살펴보면, time.localtime(second) 함수를 호출하여, 시스템의 타임존에 따라 변환된 시간을 가지고 그 시간들을 계산해서 보정하는 것 같습니다.

def _mktime(self):
    """Return integer POSIX timestamp."""
    epoch = datetime(1970, 1, 1)
    max_fold_seconds = 24 * 3600
    t = (self - epoch) // timedelta(0, 1)
    def local(u):
        y, m, d, hh, mm, ss = _time.localtime(u)[:6]
        return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1)

    # Our goal is to solve t = local(u) for u.
    a = local(t) - t
    u1 = t - a
    t1 = local(u1)
    if t1 == t:
        # We found one solution, but it may not be the one we need.
        # Look for an earlier solution (if `fold` is 0), or a
        # later one (if `fold` is 1).
        u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self.fold]
        b = local(u2) - u2
        if a == b:
            return u1
    else:
        b = t1 - u1
        assert a != b
    u2 = t - b
    t2 = local(u2)
    if t2 == t:
        return u2
    if t1 == t:
        return u1
    # We have found both offsets a and b, but neither t - a nor t - b is
    # a solution.  This means t is in the gap.
    return (max, min)[self.fold](u1, u2)

결론 - datetime.now()를 지양하고 datetime.now(tz)를 지향하자

KST 타임존 설정된 서버에서 UTC 시간과 KST 둘 다 -9시간 보정된게 뭔가 이상했습니다. UTC는 그냥 냅둬야 되는게 아닌가? 라고 생각으로 다시 코드를 살펴봤습니다.

문제는 바로 datetime.utcnow() 이 녀석입니다.

now_utc = datetime.utcnow()
# UTC tz:  2024-02-23 03:20:29.093107

utcnow 함수를 들어가봅시다.

@classmethod
def utcnow(cls):
    "Construct a UTC datetime from time.time()."
    t = _time.time()
    return cls.utcfromtimestamp(t)

@classmethod
def utcfromtimestamp(cls, t):
    """Construct a naive UTC datetime from a POSIX timestamp."""
    return cls._fromtimestamp(t, True, None)

@classmethod
def _fromtimestamp(cls, t, utc, tz):
    """Construct a datetime from a POSIX timestamp (like time.time()).

    A timezone info object may be passed in as well.
    """
        ...

네 바로 utc를 생성할 때, tz에 None을 주는 것입니다.

그래서 utcnow()로 생성한 datetime의 tzinfo를 호출해보면 None으로 나옵니다.

>>> print(datetime.utcnow().tzinfo)
None

그렇기 때문에 tzinfo가 None이므로 얘가 UTC인지 KST인지에 대한 정보가 없으므로 그냥 -9시간 보정이 들어간 것 입니다.

datetime.now() 의 사용을 지양하고, datetime.now(tz=timezone.utc) 를 지향하자 입니다.

dt.datetime.now(dt.timezone.utc)
datetime.datetime(2024, 2, 23, 5, 34, 16, 146937, tzinfo=datetime.timezone.utc)

timezone 정보가 없다면 이 시간이 어떤 시간대인지 알기 어렵습니다. timezone을 설정해주고 다시 테스트를 해보겠습니다.

/ # date
Fri Feb 23 14:24:12 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 05:24:15.156322+00:00
KST tz:  2024-02-23 14:24:15.156330+09:00
------------------------------------------
UTC-NS:  1708665855156322048
KST-NS:  1708665855156329984
------------------------------------------
UTC-Influx-Point -->  test-measure-04 utc=1708665855156322048i 1708665855156322048
KST-Influx-Point -->  test-measure-04 kst=1708665855156329984i 1708665855156329984
------------------------------------------

보이시나요? 기존에 없었던 UTC tz: 2024-02-23 05:24:15.156322+00:00 뒤에 +00:00 이 붙었습니다. 바로 GMT+0이라는 정보가 추가된 것인데요. 이렇게 되면 KST 타임존을 가진 서버에서 timestamp() 함수를 호출했을 때 -9시간을 하지 않습니다. 그래서 결국 동일한 시간이 들어간 것을 알 수 있습니다.

번외 - 문자열로 저장한다면 꼭 timezone을 포함시키자

influxdb에 문자열 형태로 넣고 싶으실 수 있습니다. UTC 시간에 Z를 붙여 UTC라는 표현을 해주고, KST로 변환한 시간을 넣어주는 코드입니다.

client = InfluxDBClient(url='http://localhost:8086', token='my-secret-token', org="myorg")

now = datetime.now()

utc = pytz.timezone('UTC')
seoul = pytz.timezone('Asia/Seoul')

now_utc = datetime.now(tz=utc)
now_kst = datetime.now(tz=seoul)
print("------------------------------------------")
print('UTC tz: ', now_utc)
print('KST tz: ', now_kst)
print("------------------------------------------")

utc = now_utc.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
kst = now_kst.strftime('%Y-%m-%dT%H:%M:%S.%f')
print(f"UTC-strftime: {utc}")
print(f"KST-strftime: {kst}")
print("------------------------------------------")

utc_body = Point(f"test-measure-1").time(utc).field('utc', 100)
kst_body = Point(f"test-measure-1").time(kst).field('kst', 100)

write_api = client.write_api(write_options=SYNCHRONOUS)
write_api.write(bucket='test-bucket', org='in2wise', record=kst_body)
write_api.write(bucket='test-bucket', org='in2wise', record=utc_body)
write_api.close()

print("UTC-Influx-Point --> ", utc_body)
print("KST-Influx-Point --> ", kst_body)
print("------------------------------------------")

client.close()
/ # date
Fri Feb 23 15:02:50 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 06:31:50.391616+00:00
KST tz:  2024-02-23 15:31:50.391623+09:00
------------------------------------------
UTC-strftime: 2024-02-23T06:31:50.391616Z
KST-strftime: 2024-02-23T15:31:50.391623
------------------------------------------
UTC-Influx-Point -->  test-measure-111 utc=100i 1708669910391616000 # GMT: 2024년 February 23일 Friday AM 6:31:50.391
KST-Influx-Point -->  test-measure-111 kst=100i 1708702310391623000 # GMT: 2024년 February 23일 Friday PM 3:31:50.391
------------------------------------------

utc 로우에는 실제 utc 시간을 넣었고, kst 로우에는 kst 시간을 넣었지만 client에서 따로 kst를 utc로 변환하거나 하는 작업은 없었습니다. 여기서 Z는 의미가 없어보입니다. 타임존에 대한 정보가 없으니 라이브러리는 time에 들어가는 시간을 utc로 간주하고 그대로 저장시킵니다.

타임존을 꼭 포함시키자

공식문서를 참고해서 타임존을 포함시키도록 합니다. 

utc = now_utc.strftime('%Y-%m-%dT%H:%M:%S.%f %Z') # 또는 %z를 사용
kst = now_kst.strftime('%Y-%m-%dT%H:%M:%S.%f %Z')
/ # date
Fri Feb 23 15:02:50 KST 2024

/ # python3 Test.py
------------------------------------------
UTC tz:  2024-02-23 06:34:43.474180+00:00
KST tz:  2024-02-23 15:34:43.474191+09:00
------------------------------------------
UTC-strftime: 2024-02-23T06:34:43.474180 UTC
KST-strftime: 2024-02-23T15:34:43.474191 KST
------------------------------------------
UTC-Influx-Point -->  test-measure-111 utc=100i 1708670083474180000 # GMT: 2024년 February 23일 Friday AM 06:34:43.474
KST-Influx-Point -->  test-measure-111 kst=100i 1708670083474191000 # GMT: 2024년 February 23일 Friday AM 06:34:43.474
------------------------------------------

시간 뒤에 타임존에 대한 정보가 있으니, influxdb-client 라이브러리가 UTC로 변환하여 저장시키는 것을 확인할 수 있습니다. 이 테스트로 저희는 다음과 같은 결론을 내릴 수 있습니다.

결론

influxdb Client는 time에 넣은 시간이 따로 타임존이 없다면(Z의 경우 포함) 무조건 UTC로 본다. 만약 타임존이 포함된 문자열이라면, 라이브러리는 UTC로 변환하여 저장시킨다.

REFERENCES

 

datetime — Basic date and time types

Source code: Lib/datetime.py The datetime module supplies classes for manipulating dates and times. While date and time arithmetic is supported, the focus of the implementation is on efficient attr...

docs.python.org

 

datetime — Basic date and time types

Source code: Lib/datetime.py The datetime module supplies classes for manipulating dates and times. While date and time arithmetic is supported, the focus of the implementation is on efficient attr...

docs.python.org

 

Epoch Converter

Convert Unix Timestamps (and many other date formats) to regular dates.

www.epochconverter.com

반응형