前几天线上新上线一个Kafka Java Consumer程序,出现一个异常的问题,那就通过查看日志,数据写入到了Elasticsearch索引里面,但是前端查询不到数据。
最终通过和开发一起定位,是因为我们业务上的原因,默认数据时间戳问题,默认需要使用UTC TimeZone
;但当运维用date
命令看的时候,默认是UTC时区啊,为啥还是写错了呢?
因为我们线上维护的是/etc/localtime
文件来保证时区问题,而且也是UTC
时区,但是还是写入数据时间对不上,之后上线操作的同事说把/etc/timezone
文件删除,然后重启消费者程序好了。
好了,这是为啥,虽然知道删除/etc/timezone
文件后,业务数据写入正常了,但是这是为什么呢,下面我们就来一探究竟。
通常我遇到这种之前没有遇到的问题,都会借助Google搜索一把,搜索完成后,得到JVM加载时区文件顺序如下:
/etc/sysconfig/clock
文件中可以找到"ZONE"的值,注意ZONE的值要带双引号,如ZONE="Asia/Shanghai"那按照搜索到的结果,跟我的情况不对啊,我们线上删除/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开发规范,会不会在启动脚本中指点时区都不重要,重要的是作为一个运维需要主动去沟通,问问开发他们的程序对时区和编码是否有要求,然后主动把这些参数在启动脚本中内设好,增强自己的运维主观意识,减少线上运行程序对系统环境的依赖,来规避一些问题。