东八区转为0时区_踩坑记 | Flink 天级别窗口中存在的时区问题
本系列每篇文章都是從一些實際的 case 出發,分析一些生產環境中經常會遇到的問題,拋磚引玉,以幫助小伙伴們解決一些實際問題。本文介紹 Flink 時間以及時區問題,分析了在天級別的窗口時會遇到的時區問題,如果對小伙伴有幫助的話,歡迎點贊 + 再看~
?本文主要分為兩部分:
第一部分(第 1 - 3 節)的分析主要針對 flink,分析了 flink 天級別窗口的中存在的時區問題以及解決方案。
第二部分(第 4 節)的分析可以作為所有時區問題的分析思路,主要以解決方案中的時區偏移量為什么是加 8 小時為案例做了通用的深度解析。
為了讓讀者能對本文探討的問題有一個大致了解,本文先給出問題 sql,以及解決方案。后文給出詳細的分析~
1.問題以及解決方案
問題 sql
sql 很簡單,用來統計當天累計 uv。
---------------?偽代碼?---------------INSERT?INTO
??kafka_sink_table
SELECT
??--?窗口開始時間
??CAST(
????TUMBLE_START(proctime,?INTERVAL?'1'?DAY)?AS?bigint
??)?AS?window_start,
??--?當前記錄處理的時間
??cast(max(proctime)?AS?BIGINT)?AS?current_ts,
??--?每個桶內的?uv
??count(DISTINCT?id)?AS?part_daily_full_uv
FROM
??kafka_source_table
GROUP?BY
??mod(id,?bucket_number),
??--?bucket_number?為常數,根據具體場景指定具體數值
??TUMBLE(proctime,?INTERVAL?'1'?DAY)
---------------?偽代碼?---------------
你是否能一眼看出這個 sql 所存在的問題?(PS:數據源以及數據匯時區都為東八區)
「沒錯,天級別窗口所存在的時區問題,即這段代碼統計的不是樓主所在東八區一整天數據的 uv,這段代碼統計的一整天的范圍在東八區是第一天早 8 點至第二天早 8 點。」
解決方案
樓主目前所處時區為東八區,解決方案如下:
---------------?偽代碼?---------------CREATE?VIEW?view_table?AS
SELECT
???id,
???--?通過注入時間解決
???--?加上東八區的時間偏移量,設置注入時間為時間戳列
???CAST(CURRENT_TIMESTAMP?AS?BIGINT)?*?1000?+?8?*?60?*?60?*?1000?as?ingest_time
FROM?
???source_table;
INSERT?INTO
??target_table
SELECT
??CAST(
????TUMBLE_START(ingest_time,?INTERVAL?'1'?DAY)?AS?bigint
??)?AS?window_start,
??cast(max(ingest_time)?AS?BIGINT)?-?8?*?3600?*?1000?AS?current_ts,
??count(DISTINCT?id)?AS?part_daily_full_uv
FROM
??view_table
GROUP?BY
??mod(id,?1024),
???--?根據注入時間劃分天級別窗口
??TUMBLE(ingest_time,?INTERVAL?'1'?DAY)
---------------?偽代碼?---------------
通過上述方案,就可以將統計的數據時間范圍調整為東八區的今日 0 點至明日 0 點。下文詳細說明整個需求場景以及解決方案的實現和分析過程。
2.需求場景以及實現方案
需求場景
coming,需求場景比較簡單,就是消費上游的一個埋點日志數據源,根據埋點中的 id 統計當天 0 點至當前時刻的累計 uv,按照分鐘級別產出到下游 OLAP 引擎中進行簡單的聚合,最后在 BI 看板進行展示,沒有任何維度字段(感動到哭?)。
數據鏈路以及組件選型
客戶端用戶行為埋點日志 -> logServer -> kafka -> flink(sql) -> kafka -> druid -> BI 看板。
實現方案以及具體的實現方式很多,這次使用的是 sql API。
flink sql schema
source 和 sink 表 schema 如下(只保留關鍵字段):
---------------?偽代碼?---------------CREATE?TABLE?kafka_sink_table?(
??--?天級別窗口開始時間
??window_start?BIGINT,
??--?當前記錄處理的時間
??current_ts?BIGINT,
??--?每個桶內的?uv(處理過程對?id?進行了分桶)
??part_daily_full_uv?BIGINT
)?WITH?(
?--?...?
);
CREATE?TABLE?kafka_source_table?(
??--?...?
??--?需要進行?uv?計算的?id
??id?BIGINT,
??--?處理時間
??proctime?AS?PROCTIME()
)?WITH?(
??--?...?
);
---------------?偽代碼?---------------
flink sql transform
---------------?偽代碼?---------------INSERT?INTO
??kafka_sink_table
SELECT
??--?窗口開始時間
??CAST(
????TUMBLE_START(proctime,?INTERVAL?'1'?DAY)?AS?bigint
??)?AS?window_start,
??--?當前記錄處理的時間
??cast(max(proctime)?AS?BIGINT)?AS?current_ts,
??--?每個桶內的?uv
??count(DISTINCT?id)?AS?part_daily_full_uv
FROM
??kafka_source_table
GROUP?BY
??mod(id,?bucket_number),
??--?bucket_number?為常數,根據具體場景指定具體數值
??TUMBLE(proctime,?INTERVAL?'1'?DAY)
---------------?偽代碼?---------------
使用 early-fire 機制(同 DataStream API 中的 ContinuousProcessingTimeTrigger),并設定觸發間隔為 60 s。
在上述實現 sql 中,我們對 id 進行了分桶,那么每分鐘輸出的數據條數即為 bucket_number 條,最終在 druid 中按照分鐘粒度將所有桶的數據進行 sum 聚合,即可得到從當天 0 點累計到當前分鐘的全量 uv。
時區問題
?18???:
「頭文字 ∩ 技術小哥哥」:使用 sql,easy game,閑坐摸魚...
「頭文字 ∩ 技術小哥哥」:等到 「00:00」 時,發現指標還在不停地往上漲,難道是 sql 邏輯錯了,不應該啊,試過分鐘,小時級別窗口都木有這個問題
「頭文字 ∩ 技術小哥哥」:摳頭ing,算了,稍后再分析這個問題吧,現在還有正事要干?
「頭文字 ∩ 技術小哥哥」:到了早上,瞅了一眼配置的時間序列報表,發現在 「08:00」 點的時候指標歸零,重新開始累計。woc,想法一閃而過,東八區?(當時為啥沒 format 下 sink 數據中的 window_start...)
?3.問題定位
問題說明
flink 在使用時間的這個概念的時候是基于 java 時間紀元(即格林威治 1970/01/01 00:00:00,也即 Unix 時間戳為 0)概念的,窗口對齊以及觸發也是基于 java 時間紀元[1]。
問題場景復現
可以通過直接查看 sink 數據的 window_start 得出上述結論。
但為了還原整個過程,我們按照如下 source 和 sink 數據進行整個問題的復現:
source 數據如下:
sink 數據(「為了方便理解,直接按照 druid 聚合之后的數據展示」):
從上述數據可以發現,天級別窗口「開始時間」在 UTC + 8(北京)的時區是每天早上 8 點,即 UTC + 0(格林威治)的凌晨 0 點。
「下文先給出解決方案,然后詳細解析各個時間以及時區概念~」
解決方案
- 「框架層面解決」:Blink Planner 支持時區設置[2]
- 「sql層面解決」:從 sql 實現層面給出解決方案
sql 層面解決方案
---------------?偽代碼?---------------CREATE?VIEW?view_table?AS
SELECT
???id,
???--?通過注入時間解決
???--?加上東八區的時間偏移量,設置注入時間為時間戳列
???CAST(CURRENT_TIMESTAMP?AS?BIGINT)?*?1000?+?8?*?60?*?60?*?1000?as?ingest_time
FROM?
???source_table;
INSERT?INTO
??target_table
SELECT
??CAST(
????TUMBLE_START(ingest_time,?INTERVAL?'1'?DAY)?AS?bigint
??)?AS?window_start,
??cast(max(ingest_time)?AS?BIGINT)?-?8?*?3600?*?1000?AS?current_ts,
??count(DISTINCT?id)?AS?part_daily_full_uv
FROM
??view_table
GROUP?BY
??mod(id,?1024),
???--?根據注入時間劃分天級別窗口
??TUMBLE(ingest_time,?INTERVAL?'1'?DAY)
---------------?偽代碼?---------------
我目前所屬的時區是東八區(北京時間),通過上述 sql,設置注入時間,并對注入時間加上 8 小時的偏移量進行天級別窗口的劃分,就可以對此問題進行解決(也可以在 create table 時,在 schema 中根據計算列添加對應的注入時間戳進行解決)。如果你在 sql 層面有更好的解決方案,歡迎討論~
?Notes:
- 「東 n 區的解決方案就是時間戳 +n * 3600 秒的偏移量,西 n 區的解決方案就是時間戳 -n * 3600 秒的偏移量」
- 「DataStream API 存在相同的天級別窗口時區問題」
這里提出一個問題,為什么東八區是需要在時間戳上加 8 小時偏移量進行天級別窗口計算,而不是減 8 小時或是加上 32(24 + 8) 小時,小伙伴們有詳細分析過嘛~
根據上述問題,引出本文的第二大部分,即深度解析時區偏移量問題,這部分可以作為所有時區問題的分析思路。
4.為什么東八區是加 8 小時?
時間和時區基本概念
「時區[3]」:由于世界各國家與地區經度不同,地方時也有所不同,因此會劃分為不同的時區。
「Unix 時間戳(Unix timestamp)[4]」:Unix 時間戳(Unix timestamp),或稱 Unix 時間(Unix time)、POSIX 時間(POSIX time),是一種時間表示方式,定義為從格林威治時間 1970 年 01 月 01 日 00 時 00 分 00 秒(UTC/GMT的午夜)起至現在的總秒數。Unix 時間戳不僅被使用在 Unix 系統、類 Unix 系統中,也在許多其他操作系統中被廣泛采用。
「GMT」:Greenwich Mean Time 格林威治標準時間。這是以英國格林威治天文臺觀測結果得出的時間,這是英國格林威治當地時間,這個地方的當地時間過去被當成世界標準的時間。
「UT」:Universal Time 世界時。根據原子鐘計算出來的時間。
「UTC」:Coordinated Universal Time 協調世界時。因為地球自轉越來越慢,每年都會比前一年多出零點幾秒,每隔幾年協調世界時組織都會給世界時 +1 秒,讓基于原子鐘的世界時和基于天文學(人類感知)的格林威治標準時間相差不至于太大。并將得到的時間稱為 UTC,這是現在使用的世界標準時間。協調世界時不與任何地區位置相關,也不代表此刻某地的時間,所以在說明某地時間時要加上時區也就是說 GMT 并不等于 UTC,而是等于 UTC + 0,只是格林威治剛好在 0 時區上。
白話時間和時區
當時看完這一系列的時間以及時區說明之后我大腦其實是一片空白。...ojbk...,我用自己現在的一些理解,嘗試將上述所有涉及到時間的概念解釋一下。
- 「GMT」:格林威治標準時間。
- 「UTC」:基于原子鐘協調之后的世界標準時間。可以認為 UTC 時間和格林威治標準時間一致。即 GMT = UTC + 0,其中 0 代表格林威治為 0 時區。
- 「時區」:逆向思維來解釋下(只從技術層面解釋,不從其他復雜層面解釋),沒有時區劃分代表著全世界都是同一時區,那么同一時刻看到的外顯時間是一樣的。舉個?:假如全世界都按照格林威治時間作為統一時間,在格林威治時間 0 點時,對于北京和加拿大的兩個同學來說,這兩個同學感知到的是北京是太陽剛剛升起(清晨),加拿大是太陽剛剛落下(傍晚)。但是由于沒有時區劃分,這兩個同學看到的時間都是 0 點,因此這是不符合人類對「感知到的時間」和自己「看到的時間」的理解的。所以劃分時區之后,可以滿足北京(東八區 UTC + 8)同學看到的時間是上午 8 點,加拿大(西四區 UTC - 4)同學看到的時間是下午 8 點。注意時區的劃分是和 UTC 綁定的。東八區即 UTC + 8。
- 「flink 時間」:flink 使用的時間基于 java 時間紀元(GMT 1970/01/01 00:00:00,UTC + 0 1970/01/01 00:00:00)。
- 「Unix 時間戳」:世界上任何一個地方,同時接收到的數據的對應的 Unix 時間戳都是相同的,類似時區中我們舉的不分時區的?,全世界同一時刻的 Unix 時間戳一致。
- 「Unix 時間戳為 0」:對應的格林威治時間:1970-01-01 00:00:00,對應的北京時間(東八區):1970-01-01 08:00:00**
概念關系如圖所示:
為什么東八區是加 8 小時?
下述表格只對一些重要的時間進行了標注:
拿第一條數據解釋下,其代表在北京時間 1970/01/01 00:00:00 時,生成的一條數據所攜帶的 Unix 時間戳為 -8 * 3600。
根據需求和上圖和上述表格內容,我們可以得到如下推導過程:
需求場景是統計一個整天的 uv,即天級別窗口,比如統計北京時間 1970/01/01 00:00:00 - 1970/01/02 00:00:00 范圍的數據時,這個日期范圍內的數據所攜帶的 Unix 時間戳范圍為 -8 * 3600 到 16 * 3600
對于 flink 來說,默認情況下它所能統計的一個整天的 Unix 時間戳的范圍是 0 到 24 * 3600
所以當我們想通過 flink 實現正確統計北京時間(1970/01/01 00:00:00 - 1970/01/02 00:00:00)范圍內的數據時,即統計 Unix 時間戳為 -8 * 3600 到 16 * 3600 的數據時,就需要對時間戳做個映射。
映射方法如下,就是將整體范圍內的時間戳做在時間軸上做平移映射,就是把 -8 * 3600 映射到 0,16 * 3600 映射到 24 * 3600。相當于是對北京時間的 Unix 時間戳整體加 8 * 3600。
最后在產出的時間戳上把加上的 8 小時再減掉(因為外顯時間會自動按照時區對 Unix 時間戳進行格式化)。
Notes:
- 「可以加 32 小時嗎?答案是可以。在東八區,對于天級別窗口的劃分,加 8 小時和加 8 + n * 24(其中 n 為整數)小時后進行的天級別窗口劃分和計算的效果是一樣的,flink 都會將東八區的整一天內的數據劃分到一個天級別窗口內。所以加 32(8 + 24),56(8 + 48),-16(8 - 24)小時效果都相同,上述例子只是選擇了時間軸平移最小的距離,即 8 小時。注意某些系統的 Unix 時間戳為負值時會出現異常。」
- 「此推理過程適用于所有遇到時區問題的場景,如果你也有其他應用場景有這個問題,也可以按照上述方式解決」
Appendix
求輸入 Unix 時間戳對應的東八區每天 0 點的 Unix 時間戳。
public?static?final?long?ONE_DAY_MILLS?=?24?*?60?*?60?*?1000L;public?static?long?transform(long?timestamp)?{
????return?timestamp?-?(timestamp?+?8?*?60?*?60?*?1000)?%?ONE_DAY_MILLS;
}
5.總結
本文首先介紹了直接給出了我們的問題 sql 和解決方案。
第二節從需求場景以及整個數據鏈路的實現方案出發,解釋了我們怎樣使用 flink sql 進行了需求實現,并進而引出了 sql 中天級別窗口存在的時區問題。
第三節確認了天級別窗口時區問題原因,引出了 flink 使用了 java 時間紀元,并針對此問題給出了引擎層面和 sql 層面的解決方案。也進而提出了一個問題:為什么我們的解決方案是加 8 小時偏移量?
第四節針對加 8 小時偏移量的原因進行了分析,并詳細闡述了時區,UTC,GMT,Unix 時間戳之間的關系。
最后一節對本文進行了總結。
如果你有更方便的時區偏移量理解方式,歡迎留言~
Reference
[1]java 時間紀元: https://cloud.tencent.com/developer/article/1447368
[2]Blink Planner 時區設置: https://www.alibabacloud.com/help/zh/doc-detail/96910.htm?spm=a2c63.p38356.b99.48.28613830DXb6FQ
[3]時區: https://baike.baidu.com/item/%E6%97%B6%E5%8C%BA/491122?fr=aladdin
[4]Unix 時間戳(Unix timestamp): https://baike.baidu.com/item/unix%E6%97%B6%E9%97%B4%E6%88%B3/2078227?fr=aladdin
?踩坑記 | Flink 事件時間語義下數據亂序丟數更多 Flink 實時大數據分析相關技術博文,視頻。后臺回復?“flink”?獲取。
點個贊+在看,少個 bug?? 與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的东八区转为0时区_踩坑记 | Flink 天级别窗口中存在的时区问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: js循环判断有无重复值_JavaScri
- 下一篇: wincc无法修改服务器名称_WinCC