JVM加载TimeZone读取文件优先级实战分析

in 互联网技术 with 0 comment  访问: 272 次

问题现象

前几天线上新上线一个Kafka Java Consumer程序,出现一个异常的问题,那就通过查看日志,数据写入到了Elasticsearch索引里面,但是前端查询不到数据。

最终通过和开发一起定位,是因为我们业务上的原因,默认数据时间戳问题,默认需要使用UTC TimeZone;但当运维用date命令看的时候,默认是UTC时区啊,为啥还是写错了呢?

因为我们线上维护的是/etc/localtime文件来保证时区问题,而且也是UTC时区,但是还是写入数据时间对不上,之后上线操作的同事说把/etc/timezone 文件删除,然后重启消费者程序好了。

好了,这是为啥,虽然知道删除/etc/timezone文件后,业务数据写入正常了,但是这是为什么呢,下面我们就来一探究竟。

寻找真相

通常我遇到这种之前没有遇到的问题,都会借助Google搜索一把,搜索完成后,得到JVM加载时区文件顺序如下:
jvmTimeZone.png

  1. 如果系统环境变量有TZ设置,则优先取变量TZ的值;
  2. 如果在文件/etc/sysconfig/clock 文件中可以找到"ZONE"的值,注意ZONE的值要带双引号,如ZONE="Asia/Shanghai"
  3. 如果没有找到找到ZONE的值,就会读取/etc/localtime的内容和/usr/hsare/zoneinfo下的时区文件进行匹配,如果找到匹配的,就返回对应的路径

中文参考链接:https://blog.csdn.net/zj380475045/article/details/72765936 http://www.360doc.com/content/12/1011/17/110467_240881174.shtml
英文参考链接:https://bugs.java.com/view_bug.do?bug_id=6456628

那按照搜索到的结果,跟我的情况不对啊,我们线上删除/etc/timezone文件就好了,所以肯定跟文件/etc/timezone有关啊,所以我感觉肯定跟操作系统和JAVA版本有关,SO我觉得实践一把,一定要把谜底揭开。

揭开谜底

环境 操作系统 JAVA版本
aliyun Centos6.5 1.8.0_25

如上表格是我线上环境情况,实践过程如下。

Java测试代码如下:

[root@Labhost2 src]# cat TimeTest.java 
import java.util.Date;
import java.util.TimeZone;

public class TimeTest {

    public static void main(String args[]) {
    long time = System.currentTimeMillis();
    String millis = Long.toString(time);
    Date date = new Date(time);
    System.out.println("Current time in milliseconds = " + millis + " => " + date.toString());
    System.out.println("Current time zone: " + TimeZone.getDefault().getID());
    }
}

[root@Labhost2 src]# javac TimeTest.java   # 生成测试类
[root@Labhost2 src]# ls
TimeTest.class  TimeTest.java

从搜索我们知道JVM读取时区跟系统变量TZ和文件/etc/sysconfig/clock/etc/localtime 有关,我这里在加上我们删除的文件/etc/timezone 一起来实践,验证过程如下:

[root@Labhost2 src]# export TZ="Pacific/Honolulu"
[root@Labhost2 src]# cat /etc/sysconfig/clock
ZONE="America/Los_Angeles"
UTC=false 
ARC=false
[root@Labhost2 src]# ll /etc/localtime 
lrwxrwxrwx 1 root root 23 4月  18 09:23 /etc/localtime -> /usr/share/zoneinfo/UTC
[root@Labhost2 src]# cat /etc/timezone
Asia/Shanghai

从上信息我们总结一下状态:

测试项 时区值
TZ Pacific/Honolulu
/etc/sysconfig/clock America/Los_Angeles
/etc/localtime UTC
/etc/timezone Asia/Shanghai

上面状态设置好了之后,测试输出验证如下:

[root@Labhost2 src]# java TimeTest
Current time in milliseconds = 1524275592096 => Fri Apr 20 15:53:12 HST 2018
Current time zone: Pacific/Honolulu
[root@Labhost2 src]# unset TZ
[root@Labhost2 src]# java TimeTest
Current time in milliseconds = 1524275606924 => Sat Apr 21 09:53:26 CST 2018
Current time zone: Asia/Shanghai
[root@Labhost2 src]# rm -rf /etc/timezone 
[root@Labhost2 src]# java TimeTest
Current time in milliseconds = 1524275627626 => Sat Apr 21 01:53:47 UTC 2018
Current time zone: UTC
[root@Labhost2 src]# rm -rf /etc/localtime 
[root@Labhost2 src]# java TimeTest
Current time in milliseconds = 1524275640872 => Sat Apr 21 01:54:00 GMT 2018
Current time zone: GMT

从上面测试结果可知,在我这种环境下,JVM读取时区文件顺序依次为:$TZ > /etc/timezone > /etc/localtime > 默认GMT , 所以跟搜索到的情况不一样,跟文件/etc/sysconfig/clock 无关。

好了,到这里得到了正确的答案了,终于明白了,可以解释我们线上的情况了,我们线上删除文件/etc/timezone 后,就去读取文件 /etc/localtime了,我们线上文件/etc/localtime默认维护设置的就是UTC时区,正好符合我们业务需求,这就解释了。

默认GMT说明:java.util.TimeZone类中getDefault方法的源代码显示,它最终是会调用sun.util.calendar.ZoneInfo类的getTimeZone 方法。这个方法为需要的时间区域返回一个作为ID的String参数。这个默认的时间区域ID是从 user.timezone (system)属性那里得到。如果user.timezone没有定义,它就会尝试从user.country和java.home (System)属性来得到ID。 如果它没有成功找到一个时间区域ID,它就会使用一个"fallback" 的GMT值。换句话说, 如果它没有计算出你的时间区域ID,它将使用GMT作为你默认的时间区域。

总结

要避免这种问题最好的方式如下:

[推荐]Java程序在发布后的启动脚本中,可通过JVM参数指定应用的时区、编码, 比如 java -Duser.timezone=Asia/Shanghai -Dfile.encoding=utf8 DateTest

不管你们公司的研发人员有没有相应的Java开发规范,会不会在启动脚本中指点时区都不重要,重要的是作为一个运维需要主动去沟通,问问开发他们的程序对时区和编码是否有要求,然后主动把这些参数在启动脚本中内设好,增强自己的运维主观意识,减少线上运行程序对系统环境的依赖,来规避一些问题。

WeZan
Responses