Flink 應用示例

https://ci.apache.org/projects/flink/flink-docs-release-1.10/zh/getting-started/examples/

 

  https://www.infoworld.com/article/3293426/how-to-build-stateful-streaming-applications-with-apache-flink.html

在本文中,我將給出兩個常見的狀態流處理用例的示例,並討論如何使用Flink實現它們。 第一個用例是事件驅動的應用程序,即攝取連續的事件流並將某些業務邏輯應用於這些事件的應用程序。 第二個是流分析用例,其中我將介紹兩個用Flink的SQLAPI實現的分析查詢,這些查詢實時聚合流數據。 我們在DataArtisans中提供了公共GitHub存儲庫中所有示例的源代碼。

在深入研究示例的細節之前,我將介紹示例應用程序接收的事件流,並解釋如何運行我們提供的代碼。

一連串的出租車乘車活動

我們的示例應用程序基於2013年在紐約市發生的關於出租車乘車的公共數據集。 2015年DEBS(ACM分佈式事件基礎系統國際會議)大挑戰的組織者重新安排了原始數據集,並將其轉換爲一個CSV文件,我們正在從中閱讀以下九個字段。

獎章-出租車的MD5和ID

Hack_license-出租車牌照的MD5和ID

Pickup_datetime-乘客被接走的時間

Dropoff_datetime-乘客下車的時間

Pickup_longitude-拾取位置的經度

Pickup_latitude-取車地點的緯度

Dropoff_longitude-降落地點的經度

Dropoff_latitude-降落地點的緯度

Total_amount-以美元支付的總額

CSV文件按其下拉時間屬性的升序存儲記錄。 因此,該文件可以被視爲旅行結束時發佈的事件的有序日誌。 爲了運行我們在GitHub上提供的示例,您需要從GoogleDrive下載DEBS挑戰的數據集。

所有示例應用程序都順序地讀取CSV文件,並將其作爲出租車乘坐事件流來接收。 從那時起,應用程序就像任何其他流一樣處理事件,即像從基於日誌的發佈訂閱系統(如Apache Kafka或Kinesis)接收的流。 事實上,讀取文件(或任何其他類型的持久化數據)並將其視爲流是Flink統一批處理和流處理方法的基石。

運行Flink示例

如前所述,我們在GitHub存儲庫中發佈了示例應用程序的源代碼。 我們鼓勵您分叉並克隆存儲庫。 這些示例可以很容易地從您選擇的IDE中執行;您不需要設置和配置Flink集羣來運行它們。 首先,導入示例的源代碼作爲Maven項目。 然後,執行應用程序的主類,並提供數據文件的存儲位置(下載數據的鏈接見上文)作爲程序參數。

一旦您啓動了應用程序,它將在應用程序的JVM進程中啓動一個本地的嵌入式Flink實例,並提交應用程序來執行它。 當Flink啓動和作業的任務正在安排時,您將看到一堆日誌語句。 一旦應用程序運行,其輸出將被寫入標準輸出。

 

在Flink中構建事件驅動應用程序

現在,讓我們討論我們的第一個用例,這是一個事件驅動的應用程序。 事件驅動的應用程序接收事件流,在接收事件時執行計算,並可能發出新事件或觸發外部操作。 多個事件驅動的應用程序可以通過事件日誌系統將它們連接在一起,類似於大型系統可以由微服務組成。 事件驅動的應用程序、事件日誌和應用程序狀態快照(在Flink中稱爲保存點)包含一個非常強大的設計模式,因爲您可以重置它們的狀態並重放它們的輸入以從失敗中恢復、修復錯誤或將應用程序遷移到不同的集羣。

 

在本文中,我們將檢查一個事件驅動的應用程序,支持一個服務,它監視出租車司機的工作時間。 在2016年,紐約市出租車和豪華轎車委員會決定將出租車司機的工作時間限制在12小時輪班,並要求在下一班開始之前至少休息8小時。 輪班從第一次乘車開始。 從那時起,司機可以在12小時內開始新的乘車。 我們的應用程序跟蹤司機的乘車情況,標記他們12小時窗口的結束時間(即他們可能開始最後一次乘車的時間),以及違反規定的標誌乘車。 您可以在GitHub存儲庫中找到此示例的完整源代碼。

我們的應用程序是用Flink的數據流API和密鑰處理函數實現的。 數據流API是一種基於類型數據流概念的功能API。 數據流 DataStream<T>  是T類型的事件流的邏輯表示。流通過向它應用一個函數來處理,該函數產生另一個數據流,可能是不同類型的數據流。 Flink通過將事件分配到流分區並將不同的函數實例應用到每個分區來並行處理流。

下面的代碼片段顯示了我們監控應用程序的高級流..

// ingest stream of taxi rides.//獲取出租車數據流
DataStream<TaxiRide> rides = TaxiRides.getRides(env, inputPath);

DataStream<Tuple2<String, String>> notifications = rides
   // partition stream by the driver’s license id //根據出租車司機的id進行分區
   .keyBy(r -> r.licenseId)
   // monitor ride events and generate notifications//監控乘車事件並生成通知
   .process(new MonitorWorkTime());

// print notifications// 打印通知
notifications.print();

該應用程序開始接收一系列出租車乘車事件。 在我們的示例中,這些事件是從文本文件中讀取、解析並存儲在出租車騎行POJO對象中。 實際應用程序通常會從消息隊列或事件日誌中攝取事件,例如Apache Kafka或Pravega。 下一步是通過司機的駕駛執照ID來鎖定出租車駕駛事件。 密鑰By操作在聲明的字段上劃分流,這樣所有具有相同密鑰的事件都由以下函數的相同並行實例處理。 在我們的例子中,我們在許可證ID字段上進行分區,因爲我們希望監視每個驅動程序的工作時間。

 

接下來,我們將監視器工作時間函數應用於分區出租車騎行事件。 該功能跟蹤每個司機的乘車情況,並監視他們的班次和休息時間。 它發出Tuple2<String, String>;類型的事件,其中每個元組表示由驅動程序的許可證ID和消息組成的通知。 最後,我們的應用程序通過將消息打印到標準輸出來發出消息。 現實世界的應用程序會將通知寫入外部消息或存儲系統,如Apache Kafka、HDFS或數據庫系統,或者觸發外部調用立即將它們推送出去。

現在我們已經討論了應用程序的總體流程,讓我們看看 MonitorWorkTime函數,它包含了應用程序的大多數實際業務邏輯。 監視器工作時間函數是一個狀態鍵處理函數,它接收出租車騎行事件併發出 Tuple2<String, String>;記錄。 鍵處理函數接口具有處理數據的兩種方法: processElement()  和onTimer()。 每個到達事件都調用 processElement() 方法。 當先前註冊的計時器觸發時,調用onTimer()方法。 下面的片段顯示了監視器工作時間函數的骨架以及在處理方法之外聲明的所有內容。

public static class MonitorWorkTime
    extends KeyedProcessFunction<String, TaxiRide, Tuple2<String, String>> {

  // time constants in milliseconds
  private static final long ALLOWED_WORK_TIME = 12 * 60 * 60 * 1000; // 12 hours
  private static final long REQ_BREAK_TIME = 8 * 60 * 60 * 1000;     // 8 hours
  private static final long CLEAN_UP_INTERVAL = 28 * 60 * 60 * 1000; // 24 hours

 private transient DateTimeFormatter formatter;

  // state handle to store the starting time of a shift
  ValueState<Long> shiftStart;

  @Override
  public void open(Configuration conf) {
    // register state handle
    shiftStart = getRuntimeContext().getState(
      new ValueStateDescriptor<>(“shiftStart”, Types.LONG));
    // initialize time formatter
    this.formatter = DateTimeFormat.forPattern(“yyyy-MM-dd HH:mm:ss”);
  }

  // processElement() and onTimer() are discussed in detail below.
}

該函數聲明瞭幾個以毫秒爲單位的時間間隔常量、一個時間格式化程序和一個由Flink管理的鍵狀態的狀態句柄。 管理狀態是定期檢查指定和自動恢復,以防失敗。 鍵狀態是按每個鍵組織的,這意味着函數將保持每個句柄和鍵的一個值。 在我們的例子中,監視器工作時間函數爲每個密鑰保持一個長值,即每個許可證ID。 移位啓動狀態存儲驅動程序移位的起始時間。 狀態句柄在open()方法中初始化,在處理第一個事件之前調用一次。

現在,讓我們看看processElement() 方法。

@Override
public void processElement(
    TaxiRide ride,
    Context ctx,
    Collector<Tuple2<String, String>> out) throws Exception {

  // look up start time of the last shift
  Long startTs = shiftStart.value();

  if (startTs == null ||
    startTs < ride.pickUpTime - (ALLOWED_WORK_TIME + REQ_BREAK_TIME)) {

    // this is the first ride of a new shift.
    startTs = ride.pickUpTime;
    shiftStart.update(startTs);
    long endTs = startTs + ALLOWED_WORK_TIME;
    out.collect(Tuple2.of(ride.licenseId,
      “You are allowed to accept new passengers until “ + formatter.print(endTs)));

    // register timer to clean up the state in 24h
    ctx.timerService().registerEventTimeTimer(startTs + CLEAN_UP_INTERVAL);
  } else if (startTs < ride.pickUpTime - ALLOWED_WORK_TIME) {
    // this ride started after the allowed work time ended.
    // it is a violation of the regulations!
    out.collect(Tuple2.of(ride.licenseId,
      “This ride violated the working time regulations.”));
  }
}

 

對每個出租車乘車事件調用進程元素()方法。 首先,該方法從狀態句柄中獲取驅動程序移位的開始時間。 如果狀態不包含開始時間(開始TS==NULL),或者如果最後一次移位啓動超過20小時(ALLOWED_WORK_TIME) REQ_BREAK_TIME)早於當前乘坐,當前乘坐是新班次的第一次乘坐.. 在任何一種情況下,該函數通過更新班次的開始時間到當前乘坐的開始時間來啓動新班次,以新班次的結束時間向駕駛員發出消息,並在24小時內註冊一個計時器來清理狀態。

如果當前乘坐不是新班次的第一次乘坐,則功能檢查是否違反工作時間規定,即是否比司機當前班次的開始晚超過12小時。 如果是這樣的話,該函數會發出一條消息來通知驅動程序違規的情況。

 

The processElement() method of the MonitorWorkTime function registers a timer to clean up the state 24 hours after the start of a shift.

監視器工作時間函數的過程元素()方法在輪班開始後24小時註冊一個計時器來清理狀態。

Removing state that is no longer needed is important to prevent growing state sizes due to leaking state.

移除不再需要的狀態對於防止由於泄漏狀態而增加狀態大小非常重要。

A timer fires when the time of the application passes the timer’s timestamp.

當應用程序的時間通過計時器的時間戳時,計時器就會觸發。

At that point, the onTimer() method is called.

此時調用onTimer()方法..

Similar to state, timers are maintained per key, and the function is put into the context of the associated key before the onTimer() method is called.

類似於狀態,每個鍵維護定時器,在調用onTimer()方法之前,將函數放入關聯密鑰的上下文中。

Hence, all state access is directed to the key that was active when the timer was registered.

因此,所有狀態訪問都指向計時器註冊時處於活動狀態的密鑰。

@Override
public void onTimer(
    long timerTs,
    OnTimerContext ctx,
    Collector<Tuple2<String, String>> out) throws Exception {

  // remove the shift state if no new shift was started already.
  Long startTs = shiftStart.value();
  if (startTs == timerTs - CLEAN_UP_INTERVAL) {
    shiftStart.clear();
  }
}

The processElement() method registers timers for 24 hours after a shift started to clean up state that is no longer needed.

進程元素()方法在一次輪班開始清理不再需要的狀態後24小時註冊計時器。

Cleaning up the state is the only logic that the onTimer() method implements.

清理狀態是onTimer()方法實現的唯一邏輯。

When a timer fires, we check if the driver started a new shift in the meantime, i.

當計時器啓動時,我們檢查司機是否在此期間開始了新的輪班。

e.

[醫]屈光正常; 正視眼; 眼; 實驗者; 能量

, whether the shift starting time changed.

班次開始時間是否改變。

If that is not the case, we clear the shift state for the driver.

如果不是這樣,我們爲司機清除輪班狀態。

The implementation of the working hour monitoring example demonstrates how Flink applications operate with state and time, the core ingredients of any slightly advanced stream processing application.

工作時間監控示例的實現演示了Flink應用程序如何以狀態和時間運行,這是任何稍微先進的流處理應用程序的核心組成部分。

Flink provides many features for working with of state and time, such as support for processing and event time.

Flink爲處理狀態和時間提供了許多功能,例如支持處理和事件時間。

In addition, Flink provides several strategies for dealing with late records, different types of state, an efficient checkpointing mechanism to guarantee exactly-once state consistency, and many unique features centered around the concept of “savepoints,” just to name a few.

此外,Flink提供了幾種策略來處理延遲記錄、不同類型的狀態、一種有效的檢查點機制來保證狀態的一致性,以及許多圍繞“保存點”概念的獨特特性,僅舉幾個例子。

The combination of all of these features results in a stream processor that provides the flexibility that is required to build very sophisticated event-driven applications and run them at scale and with operational ease.

所有這些特性的結合導致流處理器提供了構建非常複雜的事件驅動應用程序所需的靈活性,並以規模和操作方便的方式運行它們。

 

 

Flink中使用SQL分析數據流

前面的示例顯示了在使用Flink的數據流API和過程函數實現應用程序時,您手頭的表現力和低級控制。 然而,很大一部分應用程序(和更高級應用程序的子任務)具有非常相似的要求,不需要這種表達能力。 它們可以更簡潔和方便地使用SQL來定義,SQL是數據處理和分析的標準語言。

Flink具有對批處理和流數據的統一SQL支持。 對於給定的查詢,Flink計算相同的結果,而不管它是在連續攝取的記錄流上執行的,還是在有界記錄集上執行的,因爲流和數據集提供相同的數據。 Flink支持ANSI SQL的語法和語義;它沒有定義一種看起來類似於SQL的語言,並且帶有專有語法和語義。

 

SELECT
  toCellId(dropOffLon, dropOffLat) AS area,
  TUMBLE_START(dropOffTime, INTERVAL ‘1’ HOUR) AS t,
  AVG(total) AS avgTotal
FROM Rides
GROUP BY
  toCellId(dropOffLon, dropOffLat),
  TUMBLE(dropOffTime, INTERVAL ‘1’ HOUR)

The query looks and behaves just like a regular SELECT-FROM-GROUP-BY query.

However, there are three functions that we need to explain:

  1. TUMBLE is a function to assign records to windows of a fixed length, here one hour.
  2. TUMBLE_START is a function that returns the start timestamp of a window, e.g., TUMBLE_START will return “2018-06-01 13:00:00.000” for the window that starts at 1 p.m. and ends at 2 p.m. on June 1, 2018.
  3. toCellId is a user-defined function that computes a cell ID, representing an area of 250 by 250 meters, from a pair of longitude-latitude values.

he query will return a result similar to this:

8> 29126,2013-01-04 11:00:00.0,44.0
4> 46551,2013-01-04 11:00:00.0,14.4625
1> 44565,2013-01-04 12:00:00.0,8.188571
3> 57031,2013-01-04 12:00:00.0,13.066667
8> 46847,2013-01-04 12:00:00.0,20.5
7> 56781,2013-01-04 12:00:00.0,7.5

https://www.elastic.co/cn/blog/building-real-time-dashboard-applications-with-apache-flink-elasticsearch-and-kibana

 

 

 

  • 來自 Ververica 的 Flink 學習網站也有許多示例。你可以從中選取能夠親自實踐的部分,並加以練習。

 

 

 

 

 

 

 

 

 

 

 

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