Apache Flink在唯品會的實踐

Ververica發表於2019-05-10

作者:王新春

整理:郭旭策

本文來自於王新春在2018年7月29日 Flink China社群線下 Meetup·上海站的分享。王新春目前在唯品會負責實時平臺相關內容,主要包括實時計算框架和提供實時基礎資料,以及機器學習平臺的工作。之前在美團點評,也是負責大資料平臺工作。他已經在大資料實時處理方向積累了豐富的工作經驗。

本文主要內容主要包括以下幾個方面:

  1. 唯品會實時平臺現狀

  2. Apache Flink(以下簡稱Flink)在唯品會的實踐

  3. Flink On K8S

  4. 後續規劃

一、唯品會實時平臺現狀

目前在唯品會實時平臺並不是一個統一的計算框架,而是包括 Storm,Spark,Flink 在內的三個主要計算框架。由於歷史原因,當前在 Storm 平臺上的 job 數量是最多的,但是從去年開始,業務重心逐漸切換到 Flink 上面,所以今年在 Flink 上面的應用數量有了大幅增加。

實時平臺的核心業務包含八大部分:實時推薦作為電商的重點業務,包含多個實時特徵;大促看板,包含各種維度的統計指標(例如:各種維度的訂單、UV、轉化率、漏斗等),供領導層、運營、產品決策使用;實時資料清洗,從使用者埋點收集來資料,進行實時清洗和關聯,為下游的各個業務提供更好的資料;此外還有網際網路金融、安全風控、與友商比價等業務,以及 Logview、Mercury、Titan 作為內部服務的監控系統、VDRC 實時資料同步系統等。

Apache Flink在唯品會的實踐
實時平臺的職責主要包括實時計算平臺和實時基礎資料。實時計算平臺在 Storm、Spark、Flink 等計算框架的基礎上,為監控、穩定性提供了保障,為業務開發提供了資料的輸入與輸出。實時基礎資料包含對上游埋點的定義和規範化,對使用者行為資料、MySQL 的 Binlog 日誌等資料進行清洗、打寬等處理,為下游提供質量保證的資料。

在架構設計上,包括兩大資料來源。一種是在App、微信、H5等應用上的埋點資料,原始資料收集後傳送到在kafka中;另一種是線上實時資料的 MySQL Binlog 日誌。資料在計算框架裡面做清洗關聯,把原始的資料通過實時ETL為下游的業務應用(包括離線寬表等)提供更易於使用的資料。

Apache Flink在唯品會的實踐

二、Flink在唯品會的實踐

場景一:Dataeye實時看板

Dataeye 實時看板是支援需要對所有的埋點資料、訂單資料等進行實時計算時,具有資料量大的特點,並且需要統計的維度有很多,例如全站、二級平臺、部類、檔期、人群、活動、時間維度等,提高了計算的複雜程度,統計的資料輸出指標每秒鐘可以達到幾十萬。

以 UV 計算為例,首先對 Kafka 內的埋點資料進行清洗,然後與Redis資料進行關聯,關聯好的資料寫入Kafka中;後續 Flink 計算任務消費 Kafka 的關聯資料。通常任務的計算結果的量也很大(由於計算維度和指標特別多,可以達到上千萬),資料輸出通過也是通過 Kafka 作為緩衝,最終使用同步任務同步到 HBase 中,作為實時資料展示。同步任務會對寫入 HBase 的資料限流和同型別的指標合併,保護 HBase。與此同時還有另一路計算方案作為容災。

Apache Flink在唯品會的實踐

在以 Storm 進行計算引擎中進行計算時,需要使用 Redis 作為中間狀態的儲存,而切換到 Flink 後,Flink 自身具備狀態儲存,節省了儲存空間;由於不需要訪問 Redis,也提升了效能,整體資源消耗降低到了原來的1/3。

在將計算任務從 Storm 逐步遷移到Flink的過程中,對兩路方案先後進行遷移,同時將計算任務和同步任務分離,緩解了資料寫入 HBase 的壓力。

切換到 Flink 後也需要對一些問題進行追蹤和改進。對於 FlinkKafkaConsumer,由於業務原因對 kafka 中的 Aotu Commit 進行修改,以及對 offset 的設定,需要自己實現支援 kafka 叢集切換的功能。對不帶 window 的state 資料需要手動清理。還有計算框架的通病——資料傾斜問題需要處理。同時對於同步任務追數問題,Storm可以從 Redis 中取值,Flink 只能等待。

場景二:Kafka資料落地HDFS

之前都是通過 Spark Streaming 的方式去實現,現在正在逐步切換到 Flink 上面,通過 OrcBucketingTableSink 將埋點資料落地到 HDFS上 的 Hive 表中。在 Flink 處理中單 Task Write 可達到3.5K/s左右,使用 Flink 後資源消耗降低了90%,同時將延遲30s降低到了3s以內。目前還在做 Flink 對 Spark Bucket Table 的支援。

場景三:實時的ETL

對於 ETL 處理工作而言,存在的一個痛點就是字典表儲存在 HDFS 中,並且是不斷變化的,而實時的資料流需要與字典表進行 join。字典表的變化是由離線批處理任務引起的,目前的做法是使用ContinuousFileMonitoringFunctionContinuousFileReaderOperator 定時監聽 HDFS 資料變化,不斷地將新資料刷入,使用最新的資料去做 join 實時資料。

我們計劃做更加通用的方式,去支援 Hive 表和 stream 的 join,實現Hive表資料變化之後,資料自動推送的效果。

三、Flink On K8S

在唯品會內部有一些不同的計算框架,有實時計算的,有機器學習的,還有離線計算的,所以需要一個統一的底層框架來進行管理,因此將 Flink 遷移到了 K8S 上。

在 K8S 上使用了思科的網路元件,每個docker容器都有獨立的 ip,對外也是可見的。實時平臺的融合器整體架構如下圖所示。

Apache Flink在唯品會的實踐

唯品會在K8S上的實現方案與 Flink 社群提供的方案差異還是很大的。唯品會使用 K8S StatefulSet 模式部署,內部實現了cluster相關的一些介面。一個job對應一個mini cluster,並且支援HA。對於Flink來說,使用 StatefulSet 的最大的原因是 pod 的 hostname 是有序的;這樣潛在的好處有:

1.hostname為-0和-1的pod可以直接指定為jobmanager;可以使用一個statefulset啟動一個cluster,而deployment必須2個;Jobmanager和TaskManager分別獨立的deployment。

  1. pod由於各種原因fail後,由於StatefulSet重新拉起的pod的hostname不變,叢集recover的速度理論上可以比deployment更快(deployment每次主機名隨機)。

容器的entrypoint

由於要由主機名來判斷是啟動jobmanager還是taskmanager,因此需要在entrypoint中去匹配設定的jobmanager的主機名是否有一致。 傳入引數為:"cluster ha";則自動根據主機名判斷啟動那個角色;也可以直接指定角色名稱 docker-entrypoint.sh的指令碼內容如下:

#!/bin/sh

# If unspecified, the hostname of the container is taken as the JobManager address

ACTION_CMD="$1"

# if use cluster model, pod ${JOB_CLUSTER_NAME}-0,${JOB_CLUSTER_NAME}-1 as jobmanager
if [ ${ACTION_CMD} == "cluster" ]; then
  jobmanagers=(${JOB_MANGER_HOSTS//,/ })
  ACTION_CMD="taskmanager"
  for i in ${!jobmanagers[@]}
  do
      if [ "$(hostname -s)" == "${jobmanagers[i]}" ]; then
          ACTION_CMD="jobmanager"
          echo "pod hostname match jobmanager config host, change action to jobmanager."
      fi
  done
fi

# if ha model, replace ha configuration
if [ "$2" == "ha" ]; then
  sed -i -e "s|high-availability.cluster-id: cluster-id|high-availability.cluster-id: ${FLINK_CLUSTER_IDENT}|g" "$FLINK_CONF_DIR/flink-conf.yaml"
  sed -i -e "s|high-availability.zookeeper.quorum: localhost:2181|high-availability.zookeeper.quorum: ${FLINK_ZK_QUORUM}|g" "$FLINK_CONF_DIR/flink-conf.yaml"
  sed -i -e "s|state.backend.fs.checkpointdir: checkpointdir|state.backend.fs.checkpointdir: hdfs:///user/flink/flink-checkpoints/${FLINK_CLUSTER_IDENT}|g" "$FLINK_CONF_DIR/flink-conf.yaml"
  sed -i -e "s|high-availability.storageDir: hdfs:///flink/ha/|high-availability.storageDir: hdfs:///user/flink/ha/${FLINK_CLUSTER_IDENT}|g" "$FLINK_CONF_DIR/flink-conf.yaml"
fi

if [ ${ACTION_CMD} == "help" ]; then
    echo "Usage: $(basename "$0") (cluster ha|jobmanager|taskmanager|local|help)"
    exit 0
elif [ ${ACTION_CMD} == "jobmanager" ]; then
    JOB_MANAGER_RPC_ADDRESS=${JOB_MANAGER_RPC_ADDRESS:-$(hostname -f)}
    echo "Starting Job Manager"
    sed -i -e "s/jobmanager.rpc.address: localhost/jobmanager.rpc.address: ${JOB_MANAGER_RPC_ADDRESS}/g" "$FLINK_CONF_DIR/flink-conf.yaml"
    sed -i -e "s/jobmanager.heap.mb: 1024/jobmanager.heap.mb: ${JOB_MANAGER_HEAP_MB}/g" "$FLINK_CONF_DIR/flink-conf.yaml"

    echo "config file: " && grep '^[^\n#]' "$FLINK_CONF_DIR/flink-conf.yaml"
    exec "$FLINK_HOME/bin/jobmanager.sh" start-foreground cluster

elif [ ${ACTION_CMD} == "taskmanager" ]; then
    TASK_MANAGER_NUMBER_OF_TASK_SLOTS=${TASK_MANAGER_NUMBER_OF_TASK_SLOTS:-$(grep -c ^processor /proc/cpuinfo)}
    echo "Starting Task Manager"

    sed -i -e "s/taskmanager.heap.mb: 1024/taskmanager.heap.mb: ${TASK_MANAGER_HEAP_MB}/g" "$FLINK_CONF_DIR/flink-conf.yaml"
    sed -i -e "s/taskmanager.numberOfTaskSlots: 1/taskmanager.numberOfTaskSlots: $TASK_MANAGER_NUMBER_OF_TASK_SLOTS/g" "$FLINK_CONF_DIR/flink-conf.yaml"

    echo "config file: " && grep '^[^\n#]' "$FLINK_CONF_DIR/flink-conf.yaml"
    exec "$FLINK_HOME/bin/taskmanager.sh" start-foreground
elif [ ${ACTION_CMD} == "local" ]; then
    echo "Starting local cluster"
    exec "$FLINK_HOME/bin/jobmanager.sh" start-foreground local
fi

exec "$@"
複製程式碼

entrypoint變數說明

映象的docker entrypoint指令碼里面需要設定的環境變數設定說明:

環境變數名稱 引數 示例****內容 說明
JOB_MANGER_HOSTS StatefulSet.name-0,StatefulSet.name-1 flink-cluster-0,flink-cluster-1 JM的主機名,短主機名;可以不用FQDN
FLINK_CLUSTER_IDENT namespace/StatefulSet.name default/flink-cluster 用來做zk ha設定和hdfs checkpiont的根目錄
TASK_MANAGER_NUMBER_OF_TASK_SLOTS containers.resources.cpu.limits 2 TM的slot數量,根據resources.cpu.limits來設定
FLINK_ZK_QUORUM env:FLINK_ZK_QUORUM ...:2181 HA ZK的地址
JOB_MANAGER_HEAP_MB env:JOB_MANAGER_HEAP_MBvalue:containers.resources.memory.limit -1024 4096 JM的Heap大小,由於存在堆外記憶體,需要小於container.resources.memory.limits;否則容易OOM kill
TASK_MANAGER_HEAP_MB env:TASK_MANAGER_HEAP_MB value: containers.resources.memory.limit -1024 4096 TM的Heap大小,由於存在Netty的堆外記憶體,需要小於container.resources.memory.limits;否則容易OOM kill

使用ConfigMap維護配置

對應 Flink 叢集所依賴的 HDFS 等其他配置,則通過建立 configmap 來管理和維護。

kubectl create configmap hdfs-conf --from-file=hdfs-site.xml --from-file=core-site.xml

[hadoop@flink-jm-0 hadoop]$ ll /home/vipshop/conf/hadoop
total 0
lrwxrwxrwx. 1 root root 20 Apr  9 06:54 core-site.xml -> ..data/core-site.xml
lrwxrwxrwx. 1 root root 20 Apr  9 06:54 hdfs-site.xml -> ..data/hdfs-site.xml
複製程式碼

四、後續計劃

當前實時系統,機器學習平臺要處理的資料分佈在各種資料儲存元件中,如Kafka、Redis、Tair和HDFS等,如何方便高效的訪問,處理,共享這些資料是一個很大的挑戰,對於當前的資料訪問和解析常常需要耗費很多的精力,主要的痛點包括:

  1. 對於Kafka,Redis,Tair中的 binary(PB/Avro等格式)資料,使用者無法快速直接的瞭解資料的 schema 與資料內容,採集資料內容及與寫入者的溝通成本很高。

  2. 由於缺少獨立的統一資料系統服務,對Kafka,Redis,Tair等中的binary資料訪問需要依賴寫入者提供的資訊,如proto生成類,資料格式wiki定義等,維護成本高,容易出錯。

  3. 缺乏 relational schema 使得使用者無法直接基於更高效易用的 SQL 或 LINQ 層 API 開發業務。

  4. 無法通過一個獨立的服務方便的釋出和共享資料。

  5. 實時資料無法直接提供給Batch SQL引擎使用。

  6. 此外,對於當前大部分的資料來源的訪問也缺少審計,許可權管理,訪問監控,跟蹤等特性。

UDM(統一資料管理系統) 包括 Location Manager, Schema Metastore 以及 Client Proxy 等模組,主要的功能包括:

  1. 提供從名字到地址的對映服務,使用者通過抽象名字而不是具體地址訪問資料。

  2. 使用者可以方便的通過Web GUI介面方便的檢視資料Schema,探查資料內容。

  3. 提供支援審計,監控,溯源等附加功能的Client API Proxy。

  4. 在Spark/Flink/Storm等框架中,以最適合使用的形式提供這些資料來源的封裝。

UDM的整體架構如下圖所示。

Apache Flink在唯品會的實踐

UDM的使用者包括實時,機器學習以及離線平臺中資料的生產者和使用者。在使用Sql API或Table API的時候,首先完成Schema的註冊,之後使用Sql進行開發,降低了開發程式碼量。

以Spark訪問Kafka PB資料的時序圖來說明UDM的內部流程

Apache Flink在唯品會的實踐

在Flink中,使用UDMExternalCatalog來打通Flink計算框架和UDM之間的橋樑,通過實現ExternalCatalog的各個介面,以及實現各自資料來源的TableSourceFactory,完成Schema和接入管控等各項功能。同時增強Flink的SQL Client的各項功能,可以通過呼叫API查詢UDM的Schema,完成SQL任務的生成和提交。

相關文章