JVM載入TimeZone讀取檔案優先順序實戰分析

諾克大叔發表於2018-06-25

問題現象

前幾天線上新上線一個Kafka Java Consumer程式,出現一個異常的問題,那就通過檢視日誌,資料寫入到了Elasticsearch索引裡面,但是前端查詢不到資料。

最終通過和開發一起定位,是因為我們業務上的原因,預設資料時間戳問題,預設需要使用UTC TimeZone;但當運維用date命令看的時候,預設是UTC時區啊,為啥還是寫錯了呢?

因為我們線上維護的是/etc/localtime檔案來保證時區問題,而且也是UTC時區,但是還是寫入資料時間對不上,之後上線操作的同事說把/etc/timezone 檔案刪除,然後重啟消費者程式好了。

好了,這是為啥,雖然知道刪除/etc/timezone檔案後,業務資料寫入正常了,但是這是為什麼呢,下面我們就來一探究竟。

尋找真相

通常我遇到這種之前沒有遇到的問題,都會藉助Google搜尋一把,搜尋完成後,得到JVM載入時區檔案順序如下:

JVM載入TimeZone讀取檔案優先順序實戰分析

  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開發規範,會不會在啟動指令碼中指點時區都不重要,重要的是作為一個運維需要主動去溝通,問問開發他們的程式對時區和編碼是否有要求,然後主動把這些引數在啟動指令碼中內設好,增強自己的運維主觀意識,減少線上執行程式對系統環境的依賴,來規避一些問題。

JVM載入TimeZone讀取檔案優先順序實戰分析

相關文章