最近参加公司Hackthon,自己写的项目,同事写的项目,都踩了时间的坑。以前升级Celery到4.1.0之后定时任务同样出了时区的BUG。时间/时区这块,大家经常用,但是却没有系统的整理总结,遂写此文。
日期(Date)、时间(Time)和时间戳(Timestamp)
互联网这么大,覆盖了全球这么多时区。如果没有统一的标准来规定怎么表示时间,那么一定是混乱的,无法正常运行的。我们需要一个统一的标准,来规范统一不同语言、不同时区对同一个时刻的表示。
国际标准组织(ISO)制定了ISO 8601,我们的中华人民共和国国家标准GB/T 7408-2005《数据元和交换格式 信息交换 日期和时间表示法》与ISO 8601:2000等效采用。
上面两个标准,对开发者而言,太过于严肃。其实有一份更加简单易读的文件 RFC3339 Date and Time on the Internet: Timestamps。本文之后的讨论,都基于 RFC3339。
RFC3339 比 ISO 8601 有一个很一个明显的限制,这里提一下:ISO允许24点,而 RFC3339 为了减少混淆,限制小时必须在0至23之间。23:59过1分钟,是第二天的0:00。
阅读RFC3339,我主要明确下面三个定义:
- 时间戳
- 本地时间
- 标准时间
时间戳
时间戳是一个数字,定义为格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。注意,同一时刻,不同时区获得的时间戳是相同的。以前很多用来记录时间的字段,在数据库中往往不会存储为Datetime类型,而是直接存储为无符号整形,存放时间戳的值。
Python获取时间戳的代码为
1 | import time |
本地时间
当前时区的本地时间
1 | import datetime |
上面的输出值为
2017-12-08 11:50:03.537506
标准时间
本地时间只包括当前的时间,不包含任何时区信息。同一时刻,东八区的本地时间比零时区的本地时间快了8个小时。在不同时区之间交换时间数据,除了用纯数字的时间戳,还有一种更方便人类阅读的表示方式:标准时间的偏移量表示方法。
RFC3339详细定义了互联网上日期/时间的偏移量表示:
2017-12-08T00:00:00.00Z
这个代表了UTC时间的2017年12月08日零时
2017-12-08T08:00:00.00+08:00
这个代表了同一时刻的,东八区北京时间(CST)表示的方法
上面两个时间的时间戳是等价的。两个的区别,就是在本地时间后面增加了时区信息。Z表示零时区。+08:00表示UTC时间增加8小时。
这种表示方式容易让人疑惑的点是从标准时间换算UTC时间。以CST转换UTC为例,没有看文档的情况下,根据 +08:00 的结尾,很容易根据直觉在本地时间再加上8小时。正确的计算方法是本地时间减去多增加的8小时。+08:00减去8小时才是UTC时间,-08:00加上8小时才是UTC时间。
为什么我们会踩坑
了解了本地时间和标准时间之后,很容易想到为什么我们会在这里出问题。
很自然的,开发者编写代码处理用户输入的时间类型数据,处理的都是本地时间的格式。而计算机系统处理逻辑,依赖标准时间。从用户输入的本地时间,转化成系统能处理的标准时间,这个过程是系统隐式的自动转换。当系统的时区设置和用户所处的时区不一致的时候,时间经过一次输入,处理之后再输出,就有了差异。
为了避免在时间处理上出现问题,核心就是统一输入输出时间的转化。因此解决的思路也是两个:
- 强制用户输入的时间类型是标准的,使用时间戳/标准时间。如果你的系统是给全球用户使用的,应该采用这种功能方法。这是最准确,最不容易出错的。
- 系统的时区和用户的时区严格一致。项目规模小,全部都是一个时区的用户。采用这种方案,一旦用户跨时区了,还是会存在问题。不是很推荐。
时区的名称可以查维基百科,也可以查官方的数据库 Time Zone Database。
踩过的坑和解决方法
避免踩时间坑,总的改进方式:
- 系统最好都配好NTP,保证时间准确。
- 所有的输入数据,最好转换成标准时间,再交给业务逻辑处理。
- 在天朝,还是把系统时区设置成北京时间比较好。
下面是一些踩过的坑:
Docker 设置时区
很多Docker镜像都是UTC时区,在上海使用镜像启用容器,容器里面获取的本地时间都会慢8个小时。可以在Dockerfile里面设置时区。参考这个问题的回答
1 | ENV TZ=Asia/Shanghai |
Ubuntu 设置时区
Docker里面大部分镜像是基于Debian的,Docker这么解决,Ubuntu上思路类似,直接执行命令就好了。
1 | TZ=Asia/Shanghai |
可以执行date
命令查看时间,CST是中国标准时间。
➜ ~ date
Fri Dec 8 14:12:46 CST 2017
Linux 和 Windows 双系统时间差
以前写过修复Ubuntu和Win10双系统时间差。
Celery 4.1.0 时区计算错误
这个PR Correct calculation of application current time with timezone 已经修复了,但是现在还没发布新版本。要么回退4.0.2,要么等下个版本。
Python 怎么从本地时间转换成标准时间
Python 里面的 datetime.datetime.now() 拿到的是本地时间。如果用这个保存时间,请务必确保系统的时区设置正确,统一。
Python中间的时间戳、datetime之间的转换,最好强制写清楚时区。这是个例子
1 | # 时间戳转换到CST时间 |