問題現象
前幾天線上新上線一個Kafka Java Consumer程式,出現一個異常的問題,那就通過檢視日誌,資料寫入到了Elasticsearch索引裡面,但是前端查詢不到資料。
最終通過和開發一起定位,是因為我們業務上的原因,預設資料時間戳問題,預設需要使用UTC TimeZone
;但當運維用date
命令看的時候,預設是UTC時區啊,為啥還是寫錯了呢?
因為我們線上維護的是/etc/localtime
檔案來保證時區問題,而且也是UTC
時區,但是還是寫入資料時間對不上,之後上線操作的同事說把/etc/timezone
檔案刪除,然後重啟消費者程式好了。
好了,這是為啥,雖然知道刪除/etc/timezone
檔案後,業務資料寫入正常了,但是這是為什麼呢,下面我們就來一探究竟。
尋找真相
通常我遇到這種之前沒有遇到的問題,都會藉助Google搜尋一把,搜尋完成後,得到JVM載入時區檔案順序如下:
- 如果系統環境變數有TZ設定,則優先取變數TZ的值;
- 如果在檔案
/etc/sysconfig/clock
檔案中可以找到"ZONE"的值,注意ZONE的值要帶雙引號,如ZONE="Asia/Shanghai" - 如果沒有找到找到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開發規範,會不會在啟動指令碼中指點時區都不重要,重要的是作為一個運維需要主動去溝通,問問開發他們的程式對時區和編碼是否有要求,然後主動把這些引數在啟動指令碼中內設好,增強自己的運維主觀意識,減少線上執行程式對系統環境的依賴,來規避一些問題。