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

我们上面通过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改为软链接的方式。