摘要:本文总结几类项目中跟时区相关的问题,给大家分享一些基本的时区知识,以及如何在软件开发和测试中注意考虑时区因素,以避免因时区而导致系统功能的问题。

场景一

“小D,我发现调整不同的timezone(时区),咱们的KPI计算结果可能会有一天的误差…”

“啊!我看看什么原因。”

“嗯啦,KPI务必是准确的,可不能有误差…”

“小Q,我知道啦!咱们的工单任务完成时间记录的是UTC时间,是考虑了timezone转换的,但是缺失信息的记录并没有考虑… ”

“为什么缺失信息不记录UTC时间呢?”

“因为缺失信息只记录日期,并没有时间。”

蓝鲸项目的小Q和小D说的是啥呢?下面先来借助维基百科的解释来介绍时区和UTC的概念:

  1. 时区(timezone)
    时区是地球上的区域使用同一个时间的定义。以前,人们通过观察太阳的位置决定时间,这就使得不同的地方的时间有所不同(地方时)。1863年,首次使用时区的概念,通过设立一个区域的标准时间部分地解决了这个问题。
  2. UTC
    协调世界时(英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC)是世界上调节时钟和时间的主要时间标准,它与0度经线的平太阳时相差不超过1秒。协调世界时是最接近格林威治标准时间(GMT)的几个替代时间系统之一。对于大多数用途来说,UTC时间被认为能与GMT时间互换,但GMT时间已不再被科学界所确定。
世界时区分布(图片来自维基百科)

原来小Q和小D所在的蓝鲸项目正在开发一个全球性的系统,用户处于世界各地的不同时区。

系统的工单处理流程中每个任务的完成因为有先后依赖关系,当时记录的是完成时刻的UTC时间,以防不同时区的用户完成依赖任务的时候产生冲突。

比如:处于UTC+08:00时区的用户S在当地时间“2019-02-28 09:30”处理了任务1,系统记录的时间是“UTC 2019-02-28 01:30”,接下来处于UTC-0800的用户G在当地时间“2019-02-27 18:20”来处理任务2(必须晚于任务1完成),系统记录的是“UTC 2019-02-28 02:20”,这个时间就不会有问题。

工单任务处理

在处理某个任务的时候如果发现有信息缺失,需要记录发现和收集到缺失信息的时间(可以是过去时间,由用户输入),而这个发现和收集完成的时间一般都是同一个用户来记录,本身不会有时区问题,另外这个时间跟工单处理流程中的任务完成时间并没有特别直接的关系,就对缺失信息部分只记录由用户输入的日期,而且是直接记录用户当地时间的日期,并没有记录时分秒,也没法根据UTC进行转换。

这样,同一个日期,比如,在东部时区的“2019-04-28”可能就是西部时区的“2019-04-27”,在西部时区的“2019-04-28”可能就是东部时区的“2019-04-29”,前后有相差一天的可能性。

但是,在做到KPI功能的时候,计算KPI需要结合工单任务处理时间和缺失信息记录时间,由于没有考虑时区问题,不同时区可能会造成KPI有一天的误差。

场景二

“我们这里有个线上用户报过来一个bug,合同签订日期签完以后打开编辑,在页面上变成了一个后一天的日期,没法保存”,R说。

“我们的合同签订日期最晚是签订当天的日期,的确不能用将来日期,这个业务上是这么要求的”,小B说。

“我们前端也有校验不允许存储将来日期。可是为什么会变成后一天的日期呢”,小D一脸迷惑。

“会不会还是timezone的问题?!” 小Q由于前一阵做KPI的时候测试时区相关问题花了不少精力去搞明白各种case,对时区也有了不少的研究,对此比较敏感。

“那个报bug的用户设置是什么timezone的?” 小B问。

“是东14区(UTC+14:00)!” R回答。

“竟然还有东14区,我一直以为都在正负12时区之间。”小Q很惊讶。

“那就对了!的确是timezone引发的问题!”小D沉思了一会,激动的跳起来。

这又是为什么呢?大家都在焦急地等待小D的解释。

原来,对于这种没有时分秒要求的时间,我们系统统一用当天中午UTC的12:00存入DB,这样做的原因是保证-12~+12时区内都不会有问题,换算以后都是当天,但是没有考虑到正负13、14区的情况。

下面我们来举例说明为什么东14区的会有问题。

假设用户A和用户B分别处于东12区和东14区,A和B分别在当地时间的2019年04月28日的上午9:00签订了合同,那么系统记录的时间都是“UTC 2019-04-28 12:00”,在用户A的页面上显示的日期是“2019-04-28”(“UTC 2019-04-28 12:00”转换东12区的时间是“2019-04-28 24:00”>,这个时间的日期还是4月28日),而在用户B的页面上显示的日期应该是“2019-04-29”(“UTC 2019-04-28 12:00”转换东14区的时间是“2019-04-28 26:00”,也就是“2019-04-29 02:00”,这个时间的日期变成了4月29日)。

如下表,用户A、B、C、D的合同签订时间都是当地时间2019年04月28日的上午9:00:

同理,用户C和D处于西部时区,从上表我们可以看到处于-12的C的时间是跟实际日期一样,而处于-13的用户D的时间则比实际时间早了一天,也是有问题的。

至此,我们明白了为什么东14区会引起系统功能有问题。但是,一直以来以为时区都在正负12之间,为什么会有大于+12的时区呢?原来小D是早就知道的,他给我们解释了是下面两个原因:

  1. 有些跨国际日期变更线的地区,为了保证该地区所有地方的时间都在同一天,就把时区设置成了大于+12的,比如基里巴斯。
  2. 有些处于+12区的地区有夏令时,夏令时的时候时区会变成+13,比如新西兰。

其实,时区还有很多有意思的,有偏移量是半个小时的(如印度),还有45分钟的(如尼泊尔),不一定都是整点。更多详情可以参考维基百科的时区列表

场景三

客户P发过来一封邮件:
“自2017年初以来,土耳其和白俄罗斯政府不再遵守夏令时(DST),因此这将改变这些国家的时区为GMT+3,而不是GMT+2。 目前,在我们系统伊斯坦布尔和明斯克都显示为GMT+2。”

这个问题听起来很简单,直接在DB里把对应的时区改一下就ok了。可是,正当小D准备去改数据的时候,发现了一个崩溃的事情:伊斯坦布尔和明斯克还跟另外两个地区的时区是绑定在一起的,见下图。当前系统中已经设置好的一些会议,没法判断真正需要的是哪个地区对应的时区… 已有数据无法修复!

伊斯坦布尔和明斯克

在此先不解释如何修复的数据问题。

当时,我们正好要把时区引入到另外一个新系统,考虑到避免再出现类似的情况,采用了一个新的库,那就是每个地区对应一个时区。比如:GMT+08:00分别有上海、乌鲁木齐、重庆、香港、新加坡等时区。

场景四

“这个新的timezone里怎么有一些乱七八糟的时区?”

“乱七八糟的时区?”

“Etc什么的,好像还正负矛盾的。比如:‘(GMT+08:00)Etc/GMT-8’” 对时区敏感的小Q觉得这些时区不太顺眼。

“这个是咱们新的时区库引入的,不过的确挺奇怪的,不知道这个Etc时区是啥。” 小B也不是很明白。

原来,这个时区表示法里时区名字都是用“区域/位置”来表示,比如“Asia/Shanghai”,而前面的“(GMT+08:00)”是表示相对于GMT的一个偏移量。前面对话中提到的“Etc/GMT-8”只是时区名字而已。那为什么叫这么奇怪的名字呢?

Etc时区

下面引用维基百科的解释来说明:

区域分为大陆,海洋或“Etc”三类。 目前使用的大陆和海洋是非洲,美洲,南极洲,北极,亚洲,大西洋,澳大利亚,欧洲,印度和太平洋。

包括海洋的原因是一些岛屿很难连接到某个大陆,有些地理位置与一个大陆相连,在政治上却与另一个大陆相连。

“Etc”属于特殊区域,用于某些管理区域,特别是用于表示协调世界时的“Etc/UTC”。 为了符合POSIX样式,以“Etc/GMT”开头的区域名称的标志与标准ISO 8601惯例相反。 在“Etc”区域,格林威治标准时间以西的区域有一个正号,而以东区域的名称有一个负号(例如“Etc/GMT-14”比GMT早14个小时。)

总结

对于国际化的软件系统来说,时区还是需要特别关注的。根据所经历项目出现的时区相关问题,尝试总结以下几点供大家参考。

实现方面

  1. 对于具体操作执行时间,这种都是包含时分秒的具体时刻,存储全部采用UTC时间,在显示的时候根据时区偏移量转换为对应的当地时间。不同模块之间传递的时间参数要以UTC格式。
  2. 对于没有精确时间点的、由用户输入的日期,这种一般对时间要求没有那么精确,可以加上操作时候的时间戳存储到数据库。当然还需要结合业务对时间的具体需求来定不同方案。
  3. 时区数据库单独一个位置作为一个时区的方式可以更方便修改对应位置的时区,比如:“(GMT+08:00)Asia/Chongqing”,而不是“(GMT+08:00) Beijing,Chongqing,Hong Kong,Urumqi”
  4. 关于UTC时间的获取:可以用第三方工具获取客观的UTC时间,但是出于安全考虑,一般不用;而是直接从服务器获取UTC时间,所以需要保证服务器的客观时间点是正确的,如果服务器的客观时间点有错,将会导致计算出的时间有出入。
  5. 有时候需要自定义一些时间段供用户使用,这种需要特别注意夏令时的影响。

测试方面

  1. 对于过去时间、未来时间的测试,一定要测不同时区跨度看是否满足条件能够正确使用。
  2. 显示时间:本地,不同时区切换能正确显示相应时间;输入时间:不同时区对应的日期边界值要重点考虑。如果只能输入未来时间,要考虑一些负时区的情况,比如 UTC-12, UTC-13等;如果只能是过去时间,要考虑UTC+12, UTC+14等。
  3. 定时任务:如果设置成固定时间段(如:每隔一小时)执行,与时区无关;每天定点执行,要考虑不存在的或者多出的时间段。
  4. 日志文件:时间戳命名的文件,不会由于夏令时导致文件名重复;文件内容涉及时间部分,不同时区显示正确。
  5. 夏令时跳变时刻系统功能测试,对于多出来的一小时或者减少的一小时系统行为是否正常。

大家如果还有遇到过其他时区方面的bug,欢迎留言,谢谢!