2019独角兽企业重金招聘Python工程师标准>>>
1. 问题现象
最近发现某些服务器上运行的java程序选择的时区是Asia/Harbin
或Asia/Chungking
,而不是我们常见的Asia/Shanghai
。由此导致与外部对接方交互时因为时区不同而发生问题。
通过jinfo我们可以查看java程序使用的时区:
[root@centos6 ~]# jinfo 12104 | grep user.timezone
user.timezone = Asia/Harbin
让我们再来看看CentOS的时区:
[root@centos6 ~]# date +"%Z %::z"
CST +08:00:00
2. 原因分析
首先我们来看下Java程序是怎样取得时区信息的。通过Oracle的官方文档,我们可以知道其默认时区的获取方式:
- Use the user.timezone property value as the default time zone ID if it's available.
- Detect the platform time zone ID. The source of the platform time zone and ID mapping may vary with implementation.
- Use GMT as the last resort if the given or detected time zone ID is unknown.
我们可以写一个简单的程序来测试:
DefaultTimeZone.java
public class DefaultTimeZone {
public static void main(String[] args) {
System.out.println(java.util.TimeZone.getDefault());
}
}
[root@centos6 ~]# javac DefaultTimeZone.java
[root@centos6 ~]# java DefaultTimeZone
sun.util.calendar.ZoneInfo[id="Asia/Harbin",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null]
果然得到的值是Asia/Harbin
。那这个值我们通过date命令看到的CST有什么区别和联系呢? 查阅资料发现,Linux和Java都是通过IANA提供的时区数据库获取时区信息的。我们得到的CST,称为时区简写名称,并且与时区名称是一对多的关系。一个CST可能会对应多个时区名称:
Abbreviation | Time zone name | Location | Offset |
---|---|---|---|
CST | Central Standard Time | North America | UTC -6 |
CST | CT - Central Time | Central America | |
CST | NACST - North American Central Standard Time | ||
CST | CST - Tiempo Central Estándar (Spanish) | ||
CST | HNC - Heure Normale du Centre (French) | ||
CST | China Standard Time | Asia | UTC +8 |
CST | Cuba Standard Time | Caribbean | UTC -5 |
(数据来源:https://www.timeanddate.com/time/zones/)
我们上面通过date命令得到的CST,显然是我们的China Standard Time。
而Asia/Harbin
或Asia/Shanghai
则是IANA时区数据库中提供的时区ID,这个时区ID很有趣,居然与上面国际标准的时区名称也是一对多的关系。这一点我们可以通过保存在Linux系统中的时区数据库(/usr/share/zoneinfo/ 目录内)证实:
[root@localhost ~]# zdump PRC Asia/Shanghai Asia/Chungking Asia/Harbin Asia/Chongqing
PRC Thu Aug 23 15:22:29 2018 CST
Asia/Shanghai Thu Aug 23 15:22:29 2018 CST
Asia/Chungking Thu Aug 23 15:22:29 2018 CST
Asia/Harbin Thu Aug 23 15:22:29 2018 CST
Asia/Chongqing Thu Aug 23 15:22:29 2018 CST
[root@localhost ~]# ls -li /usr/share/zoneinfo/{PRC,Asia/Shanghai,Asia/Chungking,Asia/Harbin,Asia/Chongqing}
2093683 -rw-r--r-- 5 root root 388 May 10 01:41 /usr/share/zoneinfo/Asia/Chongqing
2093683 -rw-r--r-- 5 root root 388 May 10 01:41 /usr/share/zoneinfo/Asia/Chungking
2093683 -rw-r--r-- 5 root root 388 May 10 01:41 /usr/share/zoneinfo/Asia/Harbin
2093683 -rw-r--r-- 5 root root 388 May 10 01:41 /usr/share/zoneinfo/Asia/Shanghai
2093683 -rw-r--r-- 5 root root 388 May 10 01:41 /usr/share/zoneinfo/PRC
IANA的时区数据库中,PRC、Asia/Chongqing、Asia/Chungking、Asia/Harbin、Asia/Shanghai这5个时区ID都是CST时区,并且这些文件其实是同一个文件(相同的inode)。
但是同属中国的Asia/Urumuqi
和Asia/Kashgar
采用的却是新疆时间(UTC+6)。
IANA采用这些名称作为ID,应该是有历史的原因在里面,这一点我们不深究。但是我们无法理解为什么Java采用的时区ID是Asia/Harbin
,而不是我们熟悉的Asia/Shanghai
。
由于本人对Linux代码不熟,我无法深入研究其应用的方式,但是从CentOS 7的一个manual文档(是的CentOS 6并没有这个文档)可以发现一些信息:
man 5 localtime
LOCALTIME(5) localtime LOCALTIME(5)
NAME
localtime - Local timezone configuration file
SYNOPSIS
/etc/localtime -> ../usr/share/zoneinfo/...
DESCRIPTION
The /etc/localtime file configures the system-wide timezone of the local system that is used by applications for presentation to the user. It should be an absolute or
relative symbolic link pointing to /usr/share/zoneinfo/, followed by a timezone identifier such as "Europe/Berlin" or "Etc/UTC". The resulting link should lead to the
corresponding binary tzfile(5) timezone data for the configured timezone.
Because the timezone identifier is extracted from the symlink target name of /etc/localtime, this file may not be a normal file or hardlink.
The timezone may be overridden for individual programs by using the $TZ environment variable. See environ(7).
You may use timedatectl(1) to change the settings of this file from the command line during runtime. Use systemd-firstboot(1) to initialize the time zone on mounted (but
not booted) system images.
SEE ALSO
systemd(1), tzset(3), localtime(3), timedatectl(1), systemd-timedated.service(8), systemd-firstboot(1)
systemd 219 LOCALTIME(5)
对于使用systemd的CentOS 7来说,其建议/etc/localtime
这个文件是一个到时区数据库的软链接:
Because the timezone identifier is extracted from the symlink target name of /etc/localtime, this file may not be a normal file or hardlink.
在CentOS 7下,也确实如此:
[root@centos7 ~]# ls -li /etc/localtime
33554503 lrwxrwxrwx. 1 root root 35 May 4 13:30 /etc/localtime -> ../usr/share/zoneinfo/Asia/Shanghai
也就是说, 程序会根据软链接的名称来确定时区ID。
回到我们CentOS 6,我们发现其是一个普通的文件,而不是软链接:
[root@centos6 ~]# ls -li /etc/localtime
523326 -rw-r--r--. 1 root root 388 Jan 21 2016 /etc/localtime
让我们改成软链接试试:
[root@centos6 ~]# mv /etc/localtime /etc/localtime.backup && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
[root@centos6 ~]# jinfo 13994 | grep timezone
user.timezone = Asia/Shanghai
看起来问题解决了。
3. 解决方案
从上面分析的情况来看,我们可以从几个方面去解决这个问题:
- 修改操作系统/etc/localtime。这个我们上面已经提及,只需要软链接到/usr/share/zoneinfo/下正确的时区文件就可以。
- 启动java时设置环境变量TZ,比如TZ=Asia/Shanghai。
- 启动java时设置一个全局的变量:
java -Duser.timezone=Asia/Shanghai ....
你可以选择最合适的方式来设置时区。
PS:对于操作系统维护者或者镜像管理者来说,我建议在制作系统镜像时可以将/etc/localtime
改为软链接的方式。