Flink在實時在實時計算平臺和實時數倉中的企業級應用小結

點擊上方藍色字體,選擇“設爲星標

回覆”資源“獲取更多資源


大數據領域自 2010 年開始,以 Hadoop、Hive 爲代表的離線計算開始進入各大公司的視野。大數據領域開始瞭如火如荼的發展。我個人在學校期間就開始關注大數據領域的技術迭代和更新,並且有幸在畢業後成爲大數據領域的開發者。

在過去的這幾年時間裏,以 Storm、Spark、Flink 爲代表的實時計算技術接踵而至。2019 年阿里巴巴內部 Flink 正式開源。整個實時計算領域風起雲湧,一些普通的開發者因爲業務需要或者個人興趣開始接觸Flink。
Apache Flink(以下簡稱 Flink)一改過去實時計算領域爲人詬病的缺陷,以其強大的計算能力和先進的設計理念,迅速成爲實時計算領域先進生產力的代表。各大小公司紛紛開始在 Flink 的應用上進行探索,其中最引人矚目的兩個方向便是:實時計算平臺和實時數據倉庫。

Flink 實時計算

如果你是一位大數據領域的開發人員或者你是一名後端的開發者,那麼你對下面這些需求場景應該不會陌生:
我是抖音主播,我想看帶貨銷售情況的排行?我是運營,我想看到我們公司銷售商品的 TOP10?我是開發,我想看到我們公司所有生產環境中服務器的運行情況?...... 
在 Hadoop 時代,我們通常的做法是將數據批量存儲到 HDFS 中,在用 Hive 產出離線的報表。或者我們使用類似 ClickHouse 或者 PostgreSQL 這樣的數據庫存儲生產數據,用 SQL 直接進行彙總查看。
那麼這樣的方式有什麼問題呢?
第一種,基於 Hive 的離線報表形式。大部分公司隨着業務場景的不斷豐富,同時在業界經過多年的實踐檢驗,基於 Hadoop 的離線存儲體系已經足夠成熟。但是離線計算天然時效性不強,一般都是隔天級別的滯後,業務數據隨着實踐的推移,本身的價值就會逐漸減少。越來越多的場景需要使用實時計算,在這種背景下實時計算平臺的需求應運而生。
第二種,基於 ClickHouse 或者 PostgreSQL 直接進行彙總查詢。這種情況在一些小規模的公司使用非常常見,原因只有一個就是數據量不夠大。在我們常用的具有 OLAP 特性的數據庫的使用過程中,如果在一定的數據量下直接用複雜的 SQL 查詢,一條複雜的 SQL 足以引起數據庫的劇烈抖動,甚至直接宕機,對生產環境產生毀滅性的影響。這種查詢在大公司是堅決不能進行的操作。
因此基於 Flink 強大實時計算能力消費實時數據的需求便應運而生。在實時數據平臺中,Flink 會承擔實時數據的採集、計算和發送到下游。

Flink 實時數據倉庫

數據倉庫最初是指的我們存儲的 Hive 中的表的集合。按照業務需求一般會分爲原始層、明細層、彙總層、業務層。各個公司根據實際業務需要會有更爲細緻的劃分。
傳統的離線數據倉庫的做法一般是將數據按天離線集中存儲後,按照固定的計算邏輯進行數據的清洗、轉換和加載。最終在根據業務需求進行報表產出或者提供給其他的應用使用。我們很明顯的可以看到,數據在這中間有了至少 T+1 天的延遲,數據的時效性大打折扣。
這時,實時數據倉庫應運而生。一個典型的實時數據倉庫架構圖如下:


技術選型

這一部分作者結合自身在阿里巴巴這樣的公司生產環境中的技術選擇和實際應用的中一些經驗,來講解實時計算平臺和實時數據倉庫的各個部分是如何進行技術選型的。

實時計算引擎

我們在上面提到,實時計算解決的最重要的問題就是實時性和穩定性。
實時計算對數據有非常高的穩定性和精確性要求,特別是面向公衆第三方的數據大屏,同時要求高吞吐、低延遲、極高的穩定性和絕對零誤差。隨時電商大促的成交記錄一次次被刷新,背後是下單、支付、發貨高達幾萬甚至十幾萬的峯值 QPS。
你可以想象這樣的場景嗎?天貓雙十一,萬衆矚目下的實時成交金額大屏突然卡住沒有反應。我估計所有開發人員都要被開除了…
我們以一個最常見和經典的實時計算大屏幕來舉例。
在面向實際運營的數據大屏中,需要提供高達幾十種維度的數據,每秒的數據量高達千萬甚至億級別,這對於我們的實時計算架構提出了相當高的要求。那麼我們的大屏背後的實時處理在這種數據量規模如何才能達到高吞吐、低延遲、極高的穩定性和絕對零誤差的呢?


在上圖的架構圖中,涉及幾個關鍵的技術選型,我們下面一一進行講解。
業務庫 Binlog 同步利器 - Canal
我們的實時計算架構一般是基於業務數據進行的,但無論是實時計算大屏還是常規的數據分析報表,都不能影響業務的正常進行,所以這裏需要引入消息中間件或增量同步框架 Canal。
我們生產環境中的業務數據絕大多數都是基於 MySQL 的,所以需要一個能夠實時監控 MySQL 業務數據變化的工具。Canal 是阿里巴巴開源的數據庫 Binlog 日誌解析框架,主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據訂閱和消費。


Canal 的原理也非常簡單,它會僞裝成一個數據庫的從庫,來讀取 Binlog 並進行解析。Canal 在阿里巴巴內部有大規模的應用,因爲阿里有衆多的業務是跨機房部署,大量業務需要進行業務同步,Canal 功能強大,性能也很穩定。
解耦和海量數據支持 - Kafka
在實時大屏的技術架構下,我們的數據源絕大多數情況下都是消息。我們需要一個強大的消息中間件來支撐高達幾十萬 QPS,同時支持海量數據存儲。
首先,我們爲什麼需要引入消息中間件?主要是下面三個目的:
  • 同步變異步

  • 應用解耦

  • 流量削峯

在我們的架構中,爲了和業務數據互相隔離,需要使用消息中間件進行解耦從而互不影響。另外在雙十一等大促場景下,交易峯值通常出現在某一個時間段,這個時間段系統壓力陡增,數據量暴漲,消息中間件還起到了削峯的作用。
爲什麼選擇 Kafka?
Kafka 是最初由 Linkedin 公司開發,是一個分佈式、高吞吐、多分區的消息中間件。Kafka 經過長時間的迭代和實踐檢驗,因爲其獨特的優點已經成爲目前主流的分佈式消息引擎,經常被用作企業的消息總線、實時數據存儲等。
Kafka 從衆多的消息中間件中脫穎而出,主要是因爲高吞吐、低延遲的特點;另外基於 Kafka 的生態越來越完善,各個實時處理框架包括 Flink 在消息處理上都會優先進行支持。並且 Flink 和 Kafka 結合可以實現端到端精確一次語義的原理。
Kafka 作爲大數據生態系統中已經必不可少的一員,主要的特性如下所示。
  • 高吞吐:

    可以滿足每秒百萬級別消息的生產和消費,並且可以通過橫向擴展,保證數據處理能力可以得到線性擴展。

  • 低延遲:

    以時間複雜度爲 O(1) 的方式提供消息持久化能力,即使對 TB 級以上數據也能保證常數時間複雜度的訪問性能。

  • 高容錯:

    Kafka 允許集羣的節點出現失敗。

  • 可靠性:

    消息可以根據策略進行磁盤的持久化,並且讀寫效率都很高。

  • 生態豐富:

    Kafka 周邊生態極其豐富,與各個實時處理框架結合緊密。

實時計算服務 - Flink
Flink 在當前的架構中主要承擔了消息消費、維表關聯、消息發送等。在實時計算領域,Flink 的優勢主要包括:
  • 強大的狀態管理

    Flink 使用 State 存儲中間狀態和結果,並且有強大的容錯能力;

  • 非常豐富的 API

    Flink 提供了包含 DataSet API、DataStream API、Flink SQL 等等強大的API;

  • 生態支持完善

    Flink 支持多種數據源(Kafka、MySQL等)和存儲(HDFS、ES 等),並且和其他的大數據領域的框架結合完善;

  • 批流一體

    Flink 已經在將流計算和批計算的 API 進行統一,並且支持直接寫入 Hive。


對於 Flink 的一些特點我們不做過多展開了。這裏需要注意的是,Flink 在消費完成後一般會把計算結果數據發往三個方向:
  • 高度彙總,高度彙總指標一般存儲在 Redis、HBase 中供前端直接查詢使用。

  • 明細數據,在一些場景下,我們的運營和業務人員需要查詢明細數據,有一些明細數據極其重要,比如雙十一派送的包裹中會有一些丟失和破損。

  • 實時消息,Flink 在計算完成後,有一個下游是發往消息系統,這裏的作用主要是提供給其他業務複用;

    另外,在一些情況下,我們計算好明細數據也需要再次經過消息系統才能落庫,將原來直接落庫拆成兩步,方便我們進行問題定位和排查。


百花齊放 - OLAP 數據庫選擇
OLAP 的選擇是當前實時架構中最有爭議和最困難的。目前市面上主流的開源 OLAP 引擎包含但不限於:Hive、Hawq、Presto、Kylin、Impala、SparkSQL、Druid、Clickhouse、Greeplum 等,可以說目前沒有一個引擎能在數據量,靈活程度和性能上做到完美,用戶需要根據自己的需求進行選型。
我曾經在之前的一篇文章  《實時數倉 | 你需要的是一款強大的 OLAP 引擎》 用了兩萬字分析了目前市面上主流的 OLAP 數據庫的選型問題,這裏直接給出結論:
  • Hive、Hawq、Impala:

    基於 SQL on Hadoop

  • Presto 和 Spark SQL 類似:

    基於內存解析 SQL 生成執行計劃

  • Kylin:

    用空間換時間、預計算

  • Druid:

    數據實時攝入加實時計算

  • ClickHouse:

    OLAP 領域的 HBase,單表查詢性能優勢巨大

  • Greenpulm:

    OLAP 領域的 PostgreSQL


如果你的場景是基於 HDFS 的離線計算任務,那麼 Hive、Hawq 和 Imapla 就是你的調研目標。
如果你的場景解決分佈式查詢問題,有一定的實時性要求,那麼 Presto 和 SparkSQL 可能更符合你的期望。
如果你的彙總維度比較固定,實時性要求較高,可以通過用戶配置的維度 + 指標進行預計算,那麼不妨嘗試 Kylin 和 Druid。
ClickHouse 則在單表查詢性能上獨領風騷,遠超過其他的 OLAP 數據庫。
Greenpulm 作爲關係型數據庫產品,性能可以隨着集羣的擴展線性增長,更加適合進行數據分析。

Flink 實時數據倉庫

實時數據倉庫的發展經歷了從離線到實時的發展,一個典型的實時數據倉庫架構如下如圖所示:



一般實時數據倉庫的設計也借鑑了離線數倉的理念,不但要提高我們模型的複用率,也要考慮實時數倉的穩定性和易用性。
在實時數據倉庫的技術選型中,用到的核心技術包括:Kafka、Flink、Hbase 等。
其中 Kafka 和 Flink 的優勢我們在上述實時數據平臺的技術選型中已經做過詳細的介紹。這其中還有兩個關鍵的指標存儲系統:Hbase 和 Redis。
其中 Hbase 是典型的列式分佈式存儲系統,Redis 是緩存系統中首選,他們的主要優勢包括:
  • 強一致性

  • 自動故障轉移和容錯性

  • 極高的讀寫 QPS,非常適合存儲 K-V 形式的指標


批流一體是未來
隨着 Flink 1.12 版本的發佈,Flink 與 Hive 的集成達到了一個全新的高度,Flink 可以很方便的對 Hive 直接進行讀寫。
這代表了什麼?
只要我們還在使用實時數據倉庫,那麼我們可以直接對 Hive 進行讀寫,Flink 成爲了 Hive 上的一個處理引擎,既可以通過批的方式也可以通過流的方式。從 Flink 1.12 開始會有大批的離線實時一體的數據倉庫出現。
我們數據倉庫架構就變成了:

其中 Flink SQL 統一了實時和離線的邏輯,避免出現離線和實時需要兩套架構和代碼支撐,也基本解決了離線和實時數據對不齊的尷尬局面。

大廠的實時計算平臺和實時數倉技術方案

這部分小編結合自身在實際生產環境中的經驗,參考了市面上幾個大公司在實時計算平臺和實時數倉設計中,選出了其中最穩妥也是最常用的技術方案,奉獻給大家。

作者的經驗

在我們的實時計算架構中採用的是典型的 Kappa 架構,我們的業務難點和重點主要集中在:
  • 數據源過多

我們的實時消息來源多達幾十個,分佈在各大生產系統中,這些系統中的消息數據格式不一。
  • 數據源之間時間 GAP 巨大

我們業務數據之間需要互相等待,舉個最簡單的例子。用戶下單後,可能 7 天以後後還會進行操作,這就導致一個問題,我們在建設實時數倉時中間狀態 State 巨大,直接使用 Flink 原生的狀態會導致任務資源消耗巨大,非常不穩定。
  • 離線數據和實時數據要求強一致性

我們的數據最終會以考覈的形式下發,直接指導一線員工的工資和獎金髮放。要求數據強一致性保障,否則會引起投訴甚至輿情。
基於以上的考慮,我們的實時數據倉庫架構如下:


幾個關鍵的技術點如下:
第一,我們使用了 Hbase 作爲中間狀態的存儲。我們在上面提到,因爲在 Flink SQL 中進行計算需要存儲中間狀態,而我們的數據源過多,且時間差距過大,那麼實時計算的狀態存儲變得異常巨大,在大數據量的衝擊下,任務變得非常不穩定。另外如果任務發生 Fail-over,狀態會丟失,結果嚴重失真。所以我們所有的數據都會存儲在 Hbase。
第二,實時數據觸發模式計算。在 Flink SQL 的邏輯裏,Hbase 的變更消息發出,我們只需要接受其中的 rowkey 信息,然後所有的數據都是反查 Hbase。我們在上面的文章中講到過,Hbase 因爲極高的讀寫 QPS 被各大公司普遍應用在實時存儲和高頻查詢中。
第三,雙寫 ADB 和 Hologres。ADB 和 Hologres 是阿里雲提供的強大的 OLAP 引擎。我們在 Flink SQL 計算完畢後將結果雙寫,前端查詢可以進行分流和負載均衡。
第四,離線數據同步。這裏我們採用的是直接將消息通過中間件進行同步,在離線數倉中有一套一樣的邏輯將數據寫入 Hive 中。在 Flink 1.12 後,離線和實時的計算邏輯統一爲一套,完全避免了離線和實時消息的不一致難題。
但是,客觀的說這套數據架構有沒有什麼問題呢?
  • 這套數據架構引入了 Hbase 作爲中間存儲,數據鏈路變長。導致運維成本大量增加,整個架構的實時性能受制於 Hbase 的變更信息能不能及時發送。

  • 指標沒有分層,會導致 ADB 和 Hologres 成爲查詢瓶頸。在這套數據架構中,我們完全拋棄了中間指標層,完全依賴 SQL 直接彙總查詢。一方面得益於省略中間層後指標的準確性,另一方面因爲 SQL 直接查詢會對 ADB 有巨大的查詢壓力,使得 ADB 消耗了巨大的資源和成本。

在未來的規劃中,我們希望對業務 SQL 進行分級。高優先級、實時性極高的指標和數據直接查詢數據庫。非高優先級和極高實時性的指標可以通過歷史數據加實時數據結合的方式組裝結果,減少對數據庫的查詢壓力。

騰訊看點的實時數據系統設計

騰訊看點數據中心承接了騰訊 QQ 看點、小程序、瀏覽器、快報等等業務的開發取數、看數的需求。騰訊看點一天上報的數據量可以達到萬億級規模,對低延遲、亞秒級的實時計算和多維查詢帶來了巨大的技術挑戰。
首先,我們來看一下騰訊看點的實時數據系統的架構設計:


上圖是騰訊看點的整體的實時架構設計圖。我們可以看到整體的架構分爲三層:
  • 數據採集層

在這層中,騰訊看點完全使用消息隊列 Kafka 進行了解耦操作,避免直接讀取業務系統數據。

  • 實時數據倉庫層

在這一層中騰訊看點使用 Flink 分別做分鐘級別的聚合和中度聚合,大大減輕了實時 SQL 查詢的壓力。
  • 實時數據存儲層


騰訊看點使用 ClickHouse 和 MySQL 作爲實時數據存儲,我們在下面會分析 ClickHouse 作爲實時數據存儲的優勢和特點。
關於數據選型,實時數倉的整體架構騰訊看點選擇了 Lambda 架構,主要是因爲高靈活性、高容錯性、高成熟度、極低的遷移成本。
在實時計算上,騰訊看點選擇了 Flink 作爲計算引擎,Flink 受到青睞的原因包括 Exactly-once 語義支持,輕量級的快照機制以及極高的吞吐性。另一一個很重要的原因就是 Flink 高效的維表關聯,支持了實時數據流 (百萬級/s) 關聯 HBase 維度表。
在數據存儲上,騰訊看點重度使用 ClickHouse。ClickHouse 的優勢包括:
  • 多核 CPU 並行計算

  • SIMD 並行計算加速

  • 分佈式水平擴展集羣

  • 稀疏索引、列式存儲、數據壓縮

  • 聚合分析優化

最終騰訊看點的實時數據系統支撐了亞秒級響應多維條件查詢請求:
  • 過去 30 分鐘內容的查詢,99% 的請求耗時在1秒內

  • 過去 24 小時內容的查詢,90% 的請求耗時在5秒內,99% 的請求耗時在 10 秒內


阿里巴巴批流一體數據倉庫建設

我們在上面介紹了 Flink 的優勢,尤其是在 Flink 1.12 版本後,Flink 與 Hive 的集成達到了一個全新的高度,Flink 可以很方便的對 Hive 直接進行讀寫。
阿里巴巴率先在業務實現了批流一體的實時數據倉庫,根據公開的資料顯示,阿里巴巴在批流一體上的探索主要包含三個方面:
  • 統一元數據管理

Flink 從 1.11 版本開始簡化了連接 Hive 的方式,Flink 通過一套簡單的 Hive Catelog API 打通了與 Hive 的通信。使得訪問 Hive 變得輕而易舉。
  • 統一計算引擎

在我們傳統的實時數倉的建設中,基於離線和實時引擎的不同,需要編寫兩套 SQL 進行計算和數據入庫操作。Flink 高效解決了這個問題,基於 ANSI-SQL 標準提供了批與流統一的語法,並且使用 Flink 引擎執行可以同時讀寫 Hive 與其他的 OLAP 數據庫。
  • 統一數據存儲

在這個架構下,離線數據成爲了實時數據的歷史備份,離線數據也可以作爲數據源被實時攝入,批量計算的場景變成了實時調度,不在依賴定時調度任務。
基於以上的工作,基於 Flink 和 Hive 的批流一體實時數倉應運而生,整體的架構如下:


我們可以看到,原來的離線和實時雙寫鏈路演變成了單一通道,一套代碼即可完成離線和實時的計算操作。並且基於 Flink 對 SQL 的支撐,代碼開發變得異常簡潔,阿里巴巴的批流一體數據倉庫在 2020 年落地並且投入使用,效果顯著,支撐了雙十一的數據需求。

實戰案例

這部分我們我們將以一個實時統計項目爲背景,介紹實時計算中的架構設計和技術選型以及最終的實現。其中涉及了日誌數據埋點、日誌數據採集、清洗、最終的指標計算等等。

架構設計

我們以統計網站的 PV 和 UV 爲例,涉及到幾個關鍵的處理步驟:

  • 日誌數據上報

  • 日誌數據清洗

  • 實時計算程序

  • 結果存儲

基於以上的業務處理流程,我們常用的實時處理技術選型和架構如下圖所示:

整體的代碼開發包括:
  • Flume 和 Kafka 整合和部署

  • Kafka 模擬數據生成和發送

  • Flink 和 Kafka 整合時間窗口設計

  • Flink 計算 PV、UV 代碼實現

  • Flink 和 Redis 整合以及 Redis Sink 實現


Flume 和 Kafka 整合和部署

我們可以在 Flume 的官網下載安裝包,在這裏下載一個 1.8.0 的穩定版本,然後進行解壓:
  
  
  
tar zxf apache-flume-1.8.0-bin.tar.gz
可以看到有幾個關鍵的目錄,其中 conf/ 目錄則是我們存放配置文件的目錄。
接下來我們整合 Flume 和 Kafka。整體整合思路爲,我們的兩個 Flume Agent 分別部署在兩臺 Web 服務器上,用來採集兩臺服務器的業務日誌,並且 Sink 到另一臺 Flume Agent 上,然後將數據 Sink 到 Kafka 集羣。在這裏需要配置三個 Flume Agent。
首先在 Flume Agent 1 和 Flume Agent 2 上創建配置文件,修改 source、channel 和 sink 的配置,vim log_kafka.conf 代碼如下:
# 定義這個 agent 中各組件的名字a1.sources = r1a1.sinks = k1a1.channels = c1
# source的配置,監聽日誌文件中的新增數據a1.sources.r1.type = execa1.sources.r1.command = tail -F /home/logs/access.log
#sink配置,使用avro日誌做數據的消費a1.sinks.k1.type = avroa1.sinks.k1.hostname = flumeagent03a1.sinks.k1.port = 9000
#channel配置,使用文件做數據的臨時緩存a1.channels.c1.type = filea1.channels.c1.checkpointDir = /home/temp/flume/checkpointa1.channels.c1.dataDirs = /home/temp/flume/data
#描述和配置 source channel sink 之間的連接關係a1.sources.r1.channels = c1a1.sinks.k1.channel = c
上述配置會監聽 /home/logs/access.log 文件中的數據變化,並且將數據 Sink 到 flumeagent03 的 9000 端口。
然後我們分別啓動 Flume Agent 1 和 Flume Agent 2,命令如下:
$ flume-ng agent -c conf -n a1 -f conf/log_kafka.conf >/dev/null 2>&1 &
第三個 Flume Agent 用來接收上述兩個 Agent 的數據,並且發送到 Kafka。我們需要啓動本地 Kafka,並且創建一個名爲 log_kafka 的 Topic。
然後,我們創建 Flume 配置文件,修改 source、channel 和 sink 的配置,vim flume_kafka.conf 代碼如下:


# 定義這個 agent 中各組件的名字a1.sources = r1a1.sinks = k1a1.channels = c1
#source配置a1.sources.r1.type = avroa1.sources.r1.bind = 0.0.0.0a1.sources.r1.port = 9000
#sink配置a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSinka1.sinks.k1.topic = log_kafkaa1.sinks.k1.brokerList = 127.0.0.1:9092a1.sinks.k1.requiredAcks = 1a1.sinks.k1.batchSize = 20
#channel配置a1.channels.c1.type = memorya1.channels.c1.capacity = 1000a1.channels.c1.transactionCapacity = 100
#描述和配置 source channel sink 之間的連接關係a1.sources.r1.channels = c1a1.sinks.k1.channel = c1    
配置完成後,我們啓動該 Flume Agent:
$ flume-ng agent -c conf -n a1 -f conf/flume_kafka.conf >/dev/null 2>&1 &
當 Flume Agent 1 和 2 中監聽到新的日誌數據後,數據就會被 Sink 到 Kafka 指定的 Topic,我們就可以消費 Kafka 中的數據了。
我們現在需要消費 Kafka Topic 信息,並且把序列化的消息轉化爲用戶的行爲對象:
  
  
  
public class UserClick {
private String userId; private Long timestamp; private String action;
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
public String getAction() { return action; }
public void setAction(String action) { this.action = action; }
public UserClick(String userId, Long timestamp, String action) { this.userId = userId; this.timestamp = timestamp; this.action = action; }}
enum UserAction{ //點擊 CLICK("CLICK"), //購買 PURCHASE("PURCHASE"), //其他 OTHER("OTHER");
private String action; UserAction(String action) { this.action = action; }}

在計算 PV 和 UV 的業務場景中,我們選擇使用消息中自帶的事件時間作爲時間特徵,代碼如下:


StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);// 檢查點配置,如果要用到狀態後端,那麼必須配置env.setStateBackend(new MemoryStateBackend(true));
Properties properties = new Properties();properties.setProperty("bootstrap.servers", "127.0.0.1:9092");
properties.setProperty(FlinkKafkaConsumerBase.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS, "10");FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>("log_user_action", new SimpleStringSchema(), properties);//設置從最早的offset消費consumer.setStartFromEarliest();
DataStream<UserClick> dataStream = env .addSource(consumer) .name("log_user_action") .map(message -> { JSONObject record = JSON.parseObject(message); return new UserClick( record.getString("user_id"), record.getLong("timestamp"), record.getString("action") ); })        .returns(TypeInformation.of(UserClick.class));
由於我們的用戶訪問日誌可能存在亂序,所以使用 BoundedOutOfOrdernessTimestampExtractor  來處理亂序消息和延遲時間,我們指定消息的亂序時間 30 秒,具體代碼如下:
SingleOutputStreamOperator<UserClick> userClickSingleOutputStreamOperator = dataStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<UserClick>(Time.seconds(30)) {    @Override    public long extractTimestamp(UserClick element) {        return element.getTimestamp();    }});
到目前爲止,我們已經通過讀取 Kafka 中的數據,序列化爲用戶點擊事件的 DataStream,並且完成了水印和時間戳的設計和開發。
接下來,按照業務需要,我們需要開窗並且進行一天內用戶點擊事件的 PV、UV 計算。這裏我們使用 Flink 提供的滾動窗口,並且使用 ContinuousProcessingTimeTrigger 來週期性的觸發窗口階段性計算。
dataStream     .windowAll(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8))).trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(20)))
爲了減少窗口內緩存的數據量,我們可以根據用戶的訪問時間戳所在天進行分組,然後將數據分散在各個窗口內進行計算,接着在 State 中進行彙總。
首先,我們把 DataStream 按照用戶的訪問時間所在天進行分組:
userClickSingleOutputStreamOperator         .keyBy(new KeySelector<UserClick, String>() {            @Override            public String getKey(UserClick value) throws Exception {                return DateUtil.timeStampToDate(value.getTimestamp());            }        })        .window(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8)))        .trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(20)))        .evictor(TimeEvictor.of(Time.seconds(0), true))        ...
然後根據用戶的訪問時間所在天進行分組並且調用了 evictor 來剔除已經計算過的數據。其中的 DateUtil 是獲取時間戳的年月日:

  
  
  
public class DateUtil {    public static String timeStampToDate(Long timestamp){        ThreadLocal<SimpleDateFormat> threadLocal                = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));        String format = threadLocal.get().format(new Date(timestamp));        return format.substring(0,10);    }}
接下來我們實現自己的 ProcessFunction:


public class MyProcessWindowFunction extends ProcessWindowFunction<UserClick,Tuple3<String,String, Integer>,String,TimeWindow>{
private transient MapState<String, String> uvState; private transient ValueState<Integer> pvState;
@Override public void open(Configuration parameters) throws Exception {
super.open(parameters); uvState = this.getRuntimeContext().getMapState(new MapStateDescriptor<>("uv", String.class, String.class)); pvState = this.getRuntimeContext().getState(new ValueStateDescriptor<Integer>("pv", Integer.class)); }
@Override public void process(String s, Context context, Iterable<UserClick> elements, Collector<Tuple3<String, String, Integer>> out) throws Exception {
Integer pv = 0; Iterator<UserClick> iterator = elements.iterator(); while (iterator.hasNext()){ pv = pv + 1; String userId = iterator.next().getUserId(); uvState.put(userId,null); } pvState.update(pvState.value() + pv);
Integer uv = 0; Iterator<String> uvIterator = uvState.keys().iterator(); while (uvIterator.hasNext()){ String next = uvIterator.next(); uv = uv + 1; }
Integer value = pvState.value(); if(null == value){ pvState.update(pv); }else { pvState.update(value + pv); }
out.collect(Tuple3.of(s,"uv",uv)); out.collect(Tuple3.of(s,"pv",pvState.value())); }}
我們在主程序中可以直接使用自定義的 ProcessFunction :
userClickSingleOutputStreamOperator        .keyBy(new KeySelector<UserClick, String>() {            @Override            public String getKey(UserClick value) throws Exception {                return value.getUserId();            }        })        .window(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8)))        .trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(20)))        .evictor(TimeEvictor.of(Time.seconds(0), true))        .process(new MyProcessWindowFunction());
到此爲止,我們已經計算出來了 PV 和 UV,下面我們分別講解 Flink 和 Redis 是如何整合實現 Flink Sink 的。
在這裏我們直接使用開源的 Redis 實現,首先新增 Maven 依賴如下:
  
  
  
<dependency>    <groupId>org.apache.flink</groupId>    <artifactId>flink-connector-redis_2.11</artifactId>    <version>1.1.5</version></dependency>
可以通過實現 RedisMapper 來自定義 Redis Sink,在這裏我們使用 Redis 中的 HASH 作爲存儲結構,Redis 中的 HASH 相當於 Java 語言裏面的 HashMap:
  
  
  
public class MyRedisSink implements RedisMapper<Tuple3<String,String, Integer>>{
/** * 設置redis數據類型 */ @Override public RedisCommandDescription getCommandDescription() { return new RedisCommandDescription(RedisCommand.HSET,"flink_pv_uv"); }
//指定key @Override public String getKeyFromData(Tuple3<String, String, Integer> data) { return data.f1; } //指定value @Override public String getValueFromData(Tuple3<String, String, Integer> data) { return data.f2.toString(); }}
上面實現了 RedisMapper 並覆寫了其中的 getCommandDescription、getKeyFromData、getValueFromData 3 種方法,其中 getCommandDescription 定義了存儲到 Redis 中的數據格式。這裏我們定義的 RedisCommand 爲 HSET,使用 Redis 中的 HASH 作爲數據結構;getKeyFromData 定義了 HASH 的 Key;getValueFromData 定義了 HASH 的值。
然後我們直接調用 addSink 函數即可:
...userClickSingleOutputStreamOperator            .keyBy(new KeySelector<UserClick, String>() {                @Override                public String getKey(UserClick value) throws Exception {                    return value.getUserId();                }            })            .window(TumblingProcessingTimeWindows.of(Time.days(1), Time.hours(-8)))            .trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(20)))            .evictor(TimeEvictor.of(Time.seconds(0), true))            .process(new MyProcessWindowFunction())            .addSink(new RedisSink<>(conf,new MyRedisSink()));...
到此爲止,我們就會將結果存進了 Redis 中,我們在實際業務中可以選擇使用不同的目標庫例如:Hbase 或者 MySQL 等等。

總結

以 Flink 爲代表的實時計算技術還是飛速發展中,衆多的新特性例如 Flink Hive Connector、CDC 增量同步等持續湧現,我們有理由相信基於 Flink 的實時計算平臺和實時數據倉庫的發展未來會大放異彩,解決掉業界在實時計算和實時數倉領域的痛點,成爲大數據領域先進生產力的代表。

歡迎點贊+收藏+轉發朋友圈素質三連

文章不錯?點個【在看】吧!

本文分享自微信公衆號 - 大數據技術與架構(import_bigdata)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章