Flink 面試通關手冊

概述

2019 年是大數(shù)據(jù)實時計算領域最不平凡的一年,2019 年 1 月阿里巴巴 Blink (內(nèi)部的 Flink 分支版本)開源邑贴,大數(shù)據(jù)領域一夜間從 Spark 獨步天下走向了兩強爭霸的時代采够。Flink 因為其天然的流式計算特性以及強大的處理性能成為炙手可熱的大數(shù)據(jù)處理框架。

時至今日盏浇,F(xiàn)link 已經(jīng)發(fā)展到 1.9 版本璃谨,在大數(shù)據(jù)開發(fā)領域,面試中對于 Flink 的考察已經(jīng)是大數(shù)據(jù)開發(fā)求職者必須面對的桐款,本文結(jié)合自己作為面試官過程中的經(jīng)驗詳細總結(jié)了近 50 個關于 Flink 的面試考察點咸这。

在本場 Chat 中,分為以下幾個部分:

第一部分:Flink 中的核心概念和基礎篇鲁僚,包含了 Flink 的整體介紹炊苫、核心概念、算子等考察點冰沙。

第二部分:Flink 進階篇侨艾,包含了 Flink 中的數(shù)據(jù)傳輸、容錯機制拓挥、序列化唠梨、數(shù)據(jù)熱點、反壓等實際生產(chǎn)環(huán)境中遇到的問題等考察點侥啤。

第三部分:Flink 源碼篇当叭,包含了 Flink 的核心代碼實現(xiàn)、Job 提交流程盖灸、數(shù)據(jù)交換蚁鳖、分布式快照機制、Flink SQL 的原理等考察點赁炎。

第一部分:Flink 中的核心概念和基礎考察

**一醉箕、 簡單介紹一下 Flink **

Flink 是一個框架和分布式處理引擎,用于對無界和有界數(shù)據(jù)流進行有狀態(tài)計算徙垫。并且 Flink 提供了數(shù)據(jù)分布讥裤、容錯機制以及資源管理等核心功能。

Flink提供了諸多高抽象層的API以便用戶編寫分布式任務:

  • DataSet API姻报, 對靜態(tài)數(shù)據(jù)進行批處理操作己英,將靜態(tài)數(shù)據(jù)抽象成分布式的數(shù)據(jù)集,用戶可以方便地使用Flink提供的各種操作符對分布式數(shù)據(jù)集進行處理吴旋,支持Java损肛、Scala和Python。

  • DataStream API荣瑟,對數(shù)據(jù)流進行流處理操作治拿,將流式的數(shù)據(jù)抽象成分布式的數(shù)據(jù)流,用戶可以方便地對分布式數(shù)據(jù)流進行各種操作褂傀,支持Java和Scala忍啤。

  • Table API,對結(jié)構(gòu)化數(shù)據(jù)進行查詢操作,將結(jié)構(gòu)化數(shù)據(jù)抽象成關系表同波,并通過類SQL的DSL對關系表進行各種查詢操作鳄梅,支持Java和Scala。

此外未檩,F(xiàn)link 還針對特定的應用領域提供了領域庫戴尸,例如:
Flink ML,F(xiàn)link 的機器學習庫冤狡,提供了機器學習Pipelines API并實現(xiàn)了多種機器學習算法孙蒙。
Gelly,F(xiàn)link 的圖計算庫悲雳,提供了圖計算的相關API及多種圖計算算法實現(xiàn)挎峦。

根據(jù)官網(wǎng)的介紹,F(xiàn)link 的特性包含:

支持高吞吐合瓢、低延遲坦胶、高性能的流處理
支持帶有事件時間的窗口 (Window) 操作
支持有狀態(tài)計算的 Exactly-once 語義
支持高度靈活的窗口 (Window) 操作,支持基于 time晴楔、count顿苇、session 以及 data-driven 的窗口操作
支持具有 Backpressure 功能的持續(xù)流模型
支持基于輕量級分布式快照(Snapshot)實現(xiàn)的容錯
一個運行時同時支持 Batch on Streaming 處理和 Streaming 處理
Flink 在 JVM 內(nèi)部實現(xiàn)了自己的內(nèi)存管理
支持迭代計算
支持程序自動優(yōu)化:避免特定情況下 Shuffle、排序等昂貴操作税弃,中間結(jié)果有必要進行緩存

二纪岁、 Flink 相比傳統(tǒng)的 Spark Streaming 有什么區(qū)別?

這個問題是一個非常宏觀的問題,因為兩個框架的不同點非常之多则果。但是在面試時有非常重要的一點一定要回答出來:Flink 是標準的實時處理引擎幔翰,基于事件驅(qū)動。而 Spark Streaming 是微批(Micro-Batch)的模型短条。

下面我們就分幾個方面介紹兩個框架的主要區(qū)別:

1. 架構(gòu)模型

Spark Streaming 在運行時的主要角色包括:Master导匣、Worker才菠、Driver茸时、Executor,F(xiàn)link 在運行時主要包含:Jobmanager赋访、Taskmanager和Slot可都。

2. 任務調(diào)度

Spark Streaming 連續(xù)不斷的生成微小的數(shù)據(jù)批次,構(gòu)建有向無環(huán)圖DAG蚓耽,Spark Streaming 會依次創(chuàng)建 DStreamGraph渠牲、JobGenerator、JobScheduler步悠。

Flink 根據(jù)用戶提交的代碼生成 StreamGraph签杈,經(jīng)過優(yōu)化生成 JobGraph,然后提交給 JobManager進行處理,JobManager 會根據(jù) JobGraph 生成 ExecutionGraph答姥,ExecutionGraph 是 Flink 調(diào)度最核心的數(shù)據(jù)結(jié)構(gòu)铣除,JobManager 根據(jù) ExecutionGraph 對 Job 進行調(diào)度。

3. 時間機制

Spark Streaming 支持的時間機制有限鹦付,只支持處理時間尚粘。
Flink 支持了流處理程序在時間上的三個定義:處理時間、事件時間敲长、注入時間郎嫁。同時也支持 watermark 機制來處理滯后數(shù)據(jù)。

4. 容錯機制

對于 Spark Streaming 任務祈噪,我們可以設置 checkpoint泽铛,然后假如發(fā)生故障并重啟,我們可以從上次 checkpoint 之處恢復辑鲤,但是這個行為只能使得數(shù)據(jù)不丟失厚宰,可能會重復處理,不能做到恰一次處理語義遂填。

Flink 則使用兩階段提交協(xié)議來解決這個問題铲觉。

三、 Flink 的組件棧有哪些吓坚?

根據(jù) Flink 官網(wǎng)描述撵幽,F(xiàn)link 是一個分層架構(gòu)的系統(tǒng),每一層所包含的組件都提供了特定的抽象礁击,用來服務于上層組件盐杂。

file

圖片來源于:https://flink.apache.org

自下而上,每一層分別代表:
Deploy 層:該層主要涉及了Flink的部署模式哆窿,在上圖中我們可以看出链烈,F(xiàn)link 支持包括local、Standalone挚躯、Cluster强衡、Cloud等多種部署模式。
Runtime 層:Runtime層提供了支持 Flink 計算的核心實現(xiàn)码荔,比如:支持分布式 Stream 處理漩勤、JobGraph到ExecutionGraph的映射、調(diào)度等等缩搅,為上層API層提供基礎服務越败。
API層:API 層主要實現(xiàn)了面向流(Stream)處理和批(Batch)處理API,其中面向流處理對應DataStream API硼瓣,面向批處理對應DataSet API究飞,后續(xù)版本,F(xiàn)link有計劃將DataStream和DataSet API進行統(tǒng)一。
Libraries層:該層稱為Flink應用框架層亿傅,根據(jù)API層的劃分霉祸,在API層之上構(gòu)建的滿足特定應用的實現(xiàn)計算框架,也分別對應于面向流處理和面向批處理兩類袱蜡。面向流處理支持:CEP(復雜事件處理)丝蹭、基于SQL-like的操作(基于Table的關系操作);面向批處理支持:FlinkML(機器學習庫)坪蚁、Gelly(圖處理)奔穿。

四、Flink 的運行必須依賴 Hadoop組件嗎敏晤?

Flink可以完全獨立于Hadoop贱田,在不依賴Hadoop組件下運行。
但是做為大數(shù)據(jù)的基礎設施嘴脾,Hadoop體系是任何大數(shù)據(jù)框架都繞不過去的男摧。Flink可以集成眾多Hadooop 組件,例如Yarn译打、Hbase耗拓、HDFS等等。例如奏司,F(xiàn)link可以和Yarn集成做資源調(diào)度乔询,也可以讀寫HDFS,或者利用HDFS做檢查點韵洋。

五竿刁、你們的Flink集群規(guī)模多大?

大家注意搪缨,這個問題看起來是問你實際應用中的Flink集群規(guī)模食拜,其實還隱藏著另一個問題:Flink可以支持多少節(jié)點的集群規(guī)模?

在回答這個問題時候副编,可以將自己生產(chǎn)環(huán)節(jié)中的集群規(guī)模负甸、節(jié)點、內(nèi)存情況說明齿桃,同時說明部署模式(一般是Flink on Yarn)惑惶,除此之外煮盼,用戶也可以同時在小集群(少于5個節(jié)點)和擁有 TB 級別狀態(tài)的上千個節(jié)點上運行 Flink 任務短纵。

** 六、Flink的基礎編程模型了解嗎僵控? **

file

上圖是來自Flink官網(wǎng)的運行流程圖香到。
通過上圖我們可以得知,F(xiàn)link 程序的基本構(gòu)建是數(shù)據(jù)輸入來自一個 Source,Source 代表數(shù)據(jù)的輸入端悠就,經(jīng)過 Transformation 進行轉(zhuǎn)換千绪,然后在一個或者多個Sink接收器中結(jié)束。數(shù)據(jù)流(stream)就是一組永遠不會停止的數(shù)據(jù)記錄流梗脾,而轉(zhuǎn)換(transformation)是將一個或多個流作為輸入荸型,并生成一個或多個輸出流的操作。執(zhí)行時炸茧,F(xiàn)link程序映射到 streaming dataflows瑞妇,由流(streams)和轉(zhuǎn)換操作(transformation operators)組成。

** 七梭冠、Flink集群有哪些角色辕狰?各自有什么作用? **

file

Flink 程序在運行時主要有 TaskManager控漠,JobManager蔓倍,Client三種角色。
其中JobManager扮演著集群中的管理者Master的角色盐捷,它是整個集群的協(xié)調(diào)者偶翅,負責接收Flink Job,協(xié)調(diào)檢查點碉渡,F(xiàn)ailover 故障恢復等倒堕,同時管理Flink集群中從節(jié)點TaskManager。

TaskManager是實際負責執(zhí)行計算的Worker爆价,在其上執(zhí)行Flink Job的一組Task垦巴,每個TaskManager負責管理其所在節(jié)點上的資源信息,如內(nèi)存铭段、磁盤骤宣、網(wǎng)絡,在啟動的時候?qū)①Y源的狀態(tài)向JobManager匯報序愚。

Client是Flink程序提交的客戶端憔披,當用戶提交一個Flink程序時,會首先創(chuàng)建一個Client爸吮,該Client首先會對用戶提交的Flink程序進行預處理芬膝,并提交到Flink集群中處理,所以Client需要從用戶提交的Flink程序配置中獲取JobManager的地址形娇,并建立到JobManager的連接锰霜,將Flink Job提交給JobManager。

** 八桐早、說說 Flink 資源管理中 Task Slot 的概念**

file

在Flink架構(gòu)角色中我們提到癣缅,TaskManager是實際負責執(zhí)行計算的Worker厨剪,TaskManager 是一個 JVM 進程,并會以獨立的線程來執(zhí)行一個task或多個subtask友存。為了控制一個 TaskManager 能接受多少個 task祷膳,F(xiàn)link 提出了 Task Slot 的概念。

簡單的說屡立,TaskManager會將自己節(jié)點上管理的資源分為不同的Slot:固定大小的資源子集直晨。
這樣就避免了不同Job的Task互相競爭內(nèi)存資源,但是需要主要的是膨俐,Slot只會做內(nèi)存的隔離抡秆。沒有做CPU的隔離。

** 九吟策、說說 Flink 的常用算子儒士? **

Flink 最常用的常用算子包括:
Map:DataStream → DataStream,輸入一個參數(shù)產(chǎn)生一個參數(shù)檩坚,map的功能是對輸入的參數(shù)進行轉(zhuǎn)換操作着撩。
Filter:過濾掉指定條件的數(shù)據(jù)。
KeyBy:按照指定的key進行分組匾委。
Reduce:用來進行結(jié)果匯總合并相赁。
Window:窗口函數(shù)摊鸡,根據(jù)某些特性將每個key的數(shù)據(jù)進行分組(例如:在5s內(nèi)到達的數(shù)據(jù))

** 十、說說你知道的Flink分區(qū)策略? **

什么要搞懂什么是分區(qū)策略抒抬。
分區(qū)策略是用來決定數(shù)據(jù)如何發(fā)送至下游睬隶。目前 Flink 支持了8中分區(qū)策略的實現(xiàn)惭缰。

file

上圖是整個Flink實現(xiàn)的分區(qū)策略繼承圖:

GlobalPartitioner
數(shù)據(jù)會被分發(fā)到下游算子的第一個實例中進行處理议纯。

ShufflePartitioner
數(shù)據(jù)會被隨機分發(fā)到下游算子的每一個實例中進行處理。

RebalancePartitioner
數(shù)據(jù)會被循環(huán)發(fā)送到下游的每一個實例中進行處理浅役。

RescalePartitioner
這種分區(qū)器會根據(jù)上下游算子的并行度斩松,循環(huán)的方式輸出到下游算子的每個實例。
這里有點難以理解觉既,假設上游并行度為2惧盹,編號為A和B。下游并行度為4瞪讼,編號為1钧椰,2,3符欠,4嫡霞。
那么A則把數(shù)據(jù)循環(huán)發(fā)送給1和2,B則把數(shù)據(jù)循環(huán)發(fā)送給3和4背亥。
假設上游并行度為4秒际,編號為A悬赏,B狡汉,C娄徊,D。下游并行度為2盾戴,編號為1寄锐,2。那么A和B則把數(shù)據(jù)發(fā)送給1尖啡,C和D則把數(shù)據(jù)發(fā)送給2橄仆。

BroadcastPartitioner
廣播分區(qū)會將上游數(shù)據(jù)輸出到下游算子的每個實例中。適合于大數(shù)據(jù)集和小數(shù)據(jù)集做Jion的場景衅斩。

ForwardPartitioner
ForwardPartitioner 用于將記錄輸出到下游本地的算子實例盆顾。它要求上下游算子并行度一樣。
簡單的說畏梆,F(xiàn)orwardPartitioner用來做數(shù)據(jù)的控制臺打印您宪。

KeyGroupStreamPartitioner
Hash分區(qū)器。會將數(shù)據(jù)按 Key 的 Hash 值輸出到下游算子實例中奠涌。

CustomPartitionerWrapper
用戶自定義分區(qū)器宪巨。需要用戶自己實現(xiàn)Partitioner接口,來定義自己的分區(qū)邏輯溜畅。
例如:

static class CustomPartitioner implements Partitioner<String> {
      @Override
      public int partition(String key, int numPartitions) {
          switch (key){
              case "1":
                  return 1;
              case "2":
                  return 2;
              case "3":
                  return 3;
              default:
                  return 4;
          }
      }
  }

** 十一捏卓、Flink的并行度了解嗎?Flink的并行度設置是怎樣的慈格? **

Flink中的任務被分為多個并行任務來執(zhí)行怠晴,其中每個并行的實例處理一部分數(shù)據(jù)。這些并行實例的數(shù)量被稱為并行度浴捆。

我們在實際生產(chǎn)環(huán)境中可以從四個不同層面設置并行度:

  • 操作算子層面(Operator Level)
  • 執(zhí)行環(huán)境層面(Execution Environment Level)
  • 客戶端層面(Client Level)
  • 系統(tǒng)層面(System Level)

需要注意的優(yōu)先級:算子層面>環(huán)境層面>客戶端層面>系統(tǒng)層面龄寞。

** 十二、Flink的Slot和parallelism有什么區(qū)別汤功?**

官網(wǎng)上十分經(jīng)典的圖:

file

slot是指taskmanager的并發(fā)執(zhí)行能力物邑,假設我們將 taskmanager.numberOfTaskSlots 配置為3
那么每一個 taskmanager 中分配3個 TaskSlot, 3個 taskmanager 一共有9個TaskSlot。

file

parallelism是指taskmanager實際使用的并發(fā)能力滔金。假設我們把 parallelism.default 設置為1色解,那么9個 TaskSlot 只能用1個,有8個空閑餐茵。

** 十三科阎、Flink有沒有重啟策略?說說有哪幾種忿族?**

Flink 實現(xiàn)了多種重啟策略锣笨。

  • 固定延遲重啟策略(Fixed Delay Restart Strategy)
  • 故障率重啟策略(Failure Rate Restart Strategy)
  • 沒有重啟策略(No Restart Strategy)
  • Fallback重啟策略(Fallback Restart Strategy)

** 十四蝌矛、用過Flink中的分布式緩存嗎?如何使用错英? **

Flink實現(xiàn)的分布式緩存和Hadoop有異曲同工之妙入撒。目的是在本地讀取文件,并把他放在 taskmanager 節(jié)點中椭岩,防止task重復拉取茅逮。

val env = ExecutionEnvironment.getExecutionEnvironment

// register a file from HDFS
env.registerCachedFile("hdfs:///path/to/your/file", "hdfsFile")

// register a local executable file (script, executable, ...)
env.registerCachedFile("file:///path/to/exec/file", "localExecFile", true)

// define your program and execute
...
val input: DataSet[String] = ...
val result: DataSet[Integer] = input.map(new MyMapper())
...
env.execute()

** 十五、說說Flink中的廣播變量判哥,使用時需要注意什么献雅? **

我們知道Flink是并行的,計算過程可能不在一個 Slot 中進行塌计,那么有一種情況即:當我們需要訪問同一份數(shù)據(jù)挺身。那么Flink中的廣播變量就是為了解決這種情況。

我們可以把廣播變量理解為是一個公共的共享變量锌仅,我們可以把一個dataset 數(shù)據(jù)集廣播出去章钾,然后不同的task在節(jié)點上都能夠獲取到,這個數(shù)據(jù)在每個節(jié)點上只會存在一份技扼。

** 十六伍玖、說說Flink中的窗口? **

來一張官網(wǎng)經(jīng)典的圖:

file

Flink 支持兩種劃分窗口的方式剿吻,按照time和count窍箍。如果根據(jù)時間劃分窗口,那么它就是一個time-window 如果根據(jù)數(shù)據(jù)劃分窗口丽旅,那么它就是一個count-window椰棘。

flink支持窗口的兩個重要屬性(size和interval)

如果size=interval,那么就會形成tumbling-window(無重疊數(shù)據(jù))
如果size>interval,那么就會形成sliding-window(有重疊數(shù)據(jù))
如果size< interval, 那么這種窗口將會丟失數(shù)據(jù)。比如每5秒鐘榄笙,統(tǒng)計過去3秒的通過路口汽車的數(shù)據(jù)邪狞,將會漏掉2秒鐘的數(shù)據(jù)。

通過組合可以得出四種基本窗口:

  • time-tumbling-window 無重疊數(shù)據(jù)的時間窗口茅撞,設置方式舉例:timeWindow(Time.seconds(5))
  • time-sliding-window 有重疊數(shù)據(jù)的時間窗口帆卓,設置方式舉例:timeWindow(Time.seconds(5), Time.seconds(3))
  • count-tumbling-window無重疊數(shù)據(jù)的數(shù)量窗口,設置方式舉例:countWindow(5)
  • count-sliding-window 有重疊數(shù)據(jù)的數(shù)量窗口米丘,設置方式舉例:countWindow(5,3)

** 十七剑令、說說Flink中的狀態(tài)存儲? **

Flink在做計算的過程中經(jīng)常需要存儲中間狀態(tài)拄查,來避免數(shù)據(jù)丟失和狀態(tài)恢復吁津。選擇的狀態(tài)存儲策略不同,會影響狀態(tài)持久化如何和 checkpoint 交互堕扶。

Flink提供了三種狀態(tài)存儲方式:MemoryStateBackend碍脏、FsStateBackend梭依、RocksDBStateBackend。

** 十八典尾、Flink 中的時間有哪幾類 **

Flink 中的時間和其他流式計算系統(tǒng)的時間一樣分為三類:事件時間役拴,攝入時間,處理時間三種急黎。

如果以 EventTime 為基準來定義時間窗口將形成EventTimeWindow,要求消息本身就應該攜帶EventTime扎狱。
如果以 IngesingtTime 為基準來定義時間窗口將形成 IngestingTimeWindow,以 source 的systemTime為準侧到。
如果以 ProcessingTime 基準來定義時間窗口將形成 ProcessingTimeWindow勃教,以 operator 的systemTime 為準。

** 十九匠抗、Flink 中水印是什么概念故源,起到什么作用? **

Watermark 是 Apache Flink 為了處理 EventTime 窗口計算提出的一種機制, 本質(zhì)上是一種時間戳汞贸。
一般來講Watermark經(jīng)常和Window一起被用來處理亂序事件绳军。

** 二十、Flink Table & SQL 熟悉嗎矢腻?TableEnvironment這個類有什么作用 **

TableEnvironment是Table API和SQL集成的核心概念门驾。

這個類主要用來:

  • 在內(nèi)部catalog中注冊表
  • 注冊外部catalog
  • 執(zhí)行SQL查詢
  • 注冊用戶定義(標量,表或聚合)函數(shù)
  • 將DataStream或DataSet轉(zhuǎn)換為表
  • 持有對ExecutionEnvironment或StreamExecutionEnvironment的引用

** 二十多柑、Flink SQL的實現(xiàn)原理是什么奶是? 是如何實現(xiàn) SQL 解析的呢? **

首先大家要知道 Flink 的SQL解析是基于Apache Calcite這個開源框架竣灌。

file

基于此聂沙,一次完整的SQL解析過程如下:

  • 用戶使用對外提供Stream SQL的語法開發(fā)業(yè)務應用
  • 用calcite對StreamSQL進行語法檢驗,語法檢驗通過后初嘹,轉(zhuǎn)換成calcite的邏輯樹節(jié)點及汉;最終形成calcite的邏輯計劃
  • 采用Flink自定義的優(yōu)化規(guī)則和calcite火山模型、啟發(fā)式模型共同對邏輯樹進行優(yōu)化屯烦,生成最優(yōu)的Flink物理計劃
  • 對物理計劃采用janino codegen生成代碼坷随,生成用低階API DataStream 描述的流應用,提交到Flink平臺執(zhí)行

第二部分:Flink 面試進階篇

** 一驻龟、Flink是如何支持批流一體的温眉? **

file

本道面試題考察的其實就是一句話:Flink的開發(fā)者認為批處理是流處理的一種特殊情況。批處理是有限的流處理迅脐。Flink 使用一個引擎支持了DataSet API 和 DataStream API芍殖。

** 二、Flink是如何做到高效的數(shù)據(jù)交換的谴蔑? **

在一個Flink Job中豌骏,數(shù)據(jù)需要在不同的task中進行交換龟梦,整個數(shù)據(jù)交換是有 TaskManager 負責的,TaskManager 的網(wǎng)絡組件首先從緩沖buffer中收集records窃躲,然后再發(fā)送计贰。Records 并不是一個一個被發(fā)送的,二是積累一個批次再發(fā)送蒂窒,batch 技術可以更加高效的利用網(wǎng)絡資源躁倒。

** 三、Flink是如何做容錯的洒琢? **

Flink 實現(xiàn)容錯主要靠強大的CheckPoint機制和State機制秧秉。Checkpoint 負責定時制作分布式快照、對程序中的狀態(tài)進行備份衰抑;State 用來存儲計算過程中的中間狀態(tài)象迎。

** 四、Flink 分布式快照的原理是什么呛踊? **

Flink的分布式快照是根據(jù)Chandy-Lamport算法量身定做的砾淌。簡單來說就是持續(xù)創(chuàng)建分布式數(shù)據(jù)流及其狀態(tài)的一致快照。

file

核心思想是在 input source 端插入 barrier谭网,控制 barrier 的同步來實現(xiàn) snapshot 的備份和 exactly-once 語義汪厨。

** 五、Flink 是如何保證Exactly-once語義的愉择? **

Flink通過實現(xiàn)兩階段提交和狀態(tài)保存來實現(xiàn)端到端的一致性語義劫乱。
分為以下幾個步驟:

  • 開始事務(beginTransaction)創(chuàng)建一個臨時文件夾,來寫把數(shù)據(jù)寫入到這個文件夾里面
  • 預提交(preCommit)將內(nèi)存中緩存的數(shù)據(jù)寫入文件并關閉
  • 正式提交(commit)將之前寫完的臨時文件放入目標目錄下薄辅。這代表著最終的數(shù)據(jù)會有一些延遲
  • 丟棄(abort)丟棄臨時文件

若失敗發(fā)生在預提交成功后要拂,正式提交前≌境可以根據(jù)狀態(tài)來提交預提交的數(shù)據(jù)脱惰,也可刪除預提交的數(shù)據(jù)。

** 六窿春、Flink 的 kafka 連接器有什么特別的地方拉一? **

Flink源碼中有一個獨立的connector模塊,所有的其他connector都依賴于此模塊旧乞,F(xiàn)link 在1.9版本發(fā)布的全新kafka連接器蔚润,摒棄了之前連接不同版本的kafka集群需要依賴不同版本的connector這種做法,只需要依賴一個connector即可尺栖。

** 七嫡纠、說說 Flink的內(nèi)存管理是如何做的? **

Flink 并不是將大量對象存在堆上,而是將對象都序列化到一個預分配的內(nèi)存塊上。此外除盏,F(xiàn)link大量的使用了堆外內(nèi)存叉橱。如果需要處理的數(shù)據(jù)超出了內(nèi)存限制,則會將部分數(shù)據(jù)存儲到硬盤上者蠕。
Flink 為了直接操作二進制數(shù)據(jù)實現(xiàn)了自己的序列化框架窃祝。

理論上Flink的內(nèi)存管理分為三部分:

  • Network Buffers:這個是在TaskManager啟動的時候分配的,這是一組用于緩存網(wǎng)絡數(shù)據(jù)的內(nèi)存踱侣,每個塊是32K粪小,默認分配2048個,可以通過“taskmanager.network.numberOfBuffers”修改
  • Memory Manage pool:大量的Memory Segment塊抡句,用于運行時的算法(Sort/Join/Shuffle等)探膊,這部分啟動的時候就會分配。下面這段代碼玉转,根據(jù)配置文件中的各種參數(shù)來計算內(nèi)存的分配方法突想。(heap or off-heap殴蹄,這個放到下節(jié)談)究抓,內(nèi)存的分配支持預分配和lazy load,默認懶加載的方式袭灯。
  • User Code刺下,這部分是除了Memory Manager之外的內(nèi)存用于User code和TaskManager本身的數(shù)據(jù)結(jié)構(gòu)。

** 八稽荧、說說 Flink的序列化如何做的? **

Java本身自帶的序列化和反序列化的功能橘茉,但是輔助信息占用空間比較大,在序列化對象時記錄了過多的類信息姨丈。

Apache Flink摒棄了Java原生的序列化方法畅卓,以獨特的方式處理數(shù)據(jù)類型和序列化,包含自己的類型描述符蟋恬,泛型類型提取和類型序列化框架翁潘。

TypeInformation 是所有類型描述符的基類。它揭示了該類型的一些基本屬性歼争,并且可以生成序列化器拜马。TypeInformation 支持以下幾種類型:

  • BasicTypeInfo: 任意Java 基本類型或 String 類型
  • BasicArrayTypeInfo: 任意Java基本類型數(shù)組或 String 數(shù)組
  • WritableTypeInfo: 任意 Hadoop Writable 接口的實現(xiàn)類
  • TupleTypeInfo: 任意的 Flink Tuple 類型(支持Tuple1 to Tuple25)。Flink tuples 是固定長度固定類型的Java Tuple實現(xiàn)
  • CaseClassTypeInfo: 任意的 Scala CaseClass(包括 Scala tuples)
  • PojoTypeInfo: 任意的 POJO (Java or Scala)沐绒,例如俩莽,Java對象的所有成員變量,要么是 public 修飾符定義乔遮,要么有 getter/setter 方法
  • GenericTypeInfo: 任意無法匹配之前幾種類型的類

針對前六種類型數(shù)據(jù)集扮超,F(xiàn)link皆可以自動生成對應的TypeSerializer,能非常高效地對數(shù)據(jù)集進行序列化和反序列化。

** 九出刷、 Flink中的Window出現(xiàn)了數(shù)據(jù)傾斜蝉衣,你有什么解決辦法? **

window產(chǎn)生數(shù)據(jù)傾斜指的是數(shù)據(jù)在不同的窗口內(nèi)堆積的數(shù)據(jù)量相差過多巷蚪。本質(zhì)上產(chǎn)生這種情況的原因是數(shù)據(jù)源頭發(fā)送的數(shù)據(jù)量速度不同導致的病毡。出現(xiàn)這種情況一般通過兩種方式來解決:

  • 在數(shù)據(jù)進入窗口前做預聚合
  • 重新設計窗口聚合的key

** 十、 Flink中在使用聚合函數(shù) GroupBy屁柏、Distinct啦膜、KeyBy 等函數(shù)時出現(xiàn)數(shù)據(jù)熱點該如何解決? **

數(shù)據(jù)傾斜和數(shù)據(jù)熱點是所有大數(shù)據(jù)框架繞不過去的問題淌喻。處理這類問題主要從3個方面入手:

  • 在業(yè)務上規(guī)避這類問題

例如一個假設訂單場景僧家,北京和上海兩個城市訂單量增長幾十倍,其余城市的數(shù)據(jù)量不變裸删。這時候我們在進行聚合的時候八拱,北京和上海就會出現(xiàn)數(shù)據(jù)堆積,我們可以單獨數(shù)據(jù)北京和上海的數(shù)據(jù)涯塔。

  • Key的設計上

把熱key進行拆分肌稻,比如上個例子中的北京和上海,可以把北京和上海按照地區(qū)進行拆分聚合匕荸。

  • 參數(shù)設置

Flink 1.9.0 SQL(Blink Planner) 性能優(yōu)化中一項重要的改進就是升級了微批模型爹谭,即 MiniBatch。原理是緩存一定的數(shù)據(jù)后再觸發(fā)處理榛搔,以減少對State的訪問诺凡,從而提升吞吐和減少數(shù)據(jù)的輸出量。

** 十一践惑、Flink任務延遲高腹泌,想解決這個問題,你會如何入手尔觉? **

在Flink的后臺任務管理中凉袱,我們可以看到Flink的哪個算子和task出現(xiàn)了反壓。最主要的手段是資源調(diào)優(yōu)和算子調(diào)優(yōu)穷娱。資源調(diào)優(yōu)即是對作業(yè)中的Operator的并發(fā)數(shù)(parallelism)绑蔫、CPU(core)、堆內(nèi)存(heap_memory)等參數(shù)進行調(diào)優(yōu)泵额。作業(yè)參數(shù)調(diào)優(yōu)包括:并行度的設置配深,State的設置,checkpoint的設置嫁盲。

十二篓叶、Flink是如何處理反壓的烈掠?

Flink 內(nèi)部是基于 producer-consumer 模型來進行消息傳遞的,F(xiàn)link的反壓設計也是基于這個模型缸托。Flink 使用了高效有界的分布式阻塞隊列左敌,就像 Java 通用的阻塞隊列(BlockingQueue)一樣。下游消費者消費變慢俐镐,上游就會受到阻塞矫限。

十三、Flink的反壓和Strom有哪些不同佩抹?

Storm 是通過監(jiān)控 Bolt 中的接收隊列負載情況叼风,如果超過高水位值就會將反壓信息寫到 Zookeeper ,Zookeeper 上的 watch 會通知該拓撲的所有 Worker 都進入反壓狀態(tài)棍苹,最后 Spout 停止發(fā)送 tuple无宿。

Flink中的反壓使用了高效有界的分布式阻塞隊列,下游消費變慢會導致發(fā)送端阻塞枢里。

二者最大的區(qū)別是Flink是逐級反壓孽鸡,而Storm是直接從源頭降速。

十四栏豺、 Operator Chains(算子鏈)這個概念你了解嗎彬碱?

為了更高效地分布式執(zhí)行,F(xiàn)link會盡可能地將operator的subtask鏈接(chain)在一起形成task冰悠。每個task在一個線程中執(zhí)行堡妒。將operators鏈接成task是非常有效的優(yōu)化:它能減少線程之間的切換,減少消息的序列化/反序列化溉卓,減少數(shù)據(jù)在緩沖區(qū)的交換,減少了延遲的同時提高整體的吞吐量搬泥。這就是我們所說的算子鏈桑寨。

十五、 Flink什么情況下才會把Operator chain在一起形成算子鏈忿檩?

兩個operator chain在一起的的條件:

  • 上下游的并行度一致
  • 下游節(jié)點的入度為1 (也就是說下游節(jié)點沒有來自其他節(jié)點的輸入)
  • 上下游節(jié)點都在同一個 slot group 中(下面會解釋 slot group)
  • 下游節(jié)點的 chain 策略為 ALWAYS(可以與上下游鏈接尉尾,map、flatmap燥透、filter等默認是ALWAYS)
  • 上游節(jié)點的 chain 策略為 ALWAYS 或 HEAD(只能與下游鏈接沙咏,不能與上游鏈接,Source默認是HEAD)
  • 兩個節(jié)點間數(shù)據(jù)分區(qū)方式是 forward(參考理解數(shù)據(jù)流的分區(qū))
  • 用戶沒有禁用 chain

十六班套、 說說Flink1.9的新特性肢藐?

  • 支持hive讀寫,支持UDF
  • Flink SQL TopN和GroupBy等優(yōu)化
  • Checkpoint跟savepoint針對實際業(yè)務場景做了優(yōu)化
  • Flink state查詢

十七吱韭、消費kafka數(shù)據(jù)的時候吆豹,如何處理臟數(shù)據(jù)?

可以在處理前加一個fliter算子,將不符合規(guī)則的數(shù)據(jù)過濾出去痘煤。

第三部分:Flink 面試源碼篇

** 一凑阶、Flink Job的提交流程 **
用戶提交的Flink Job會被轉(zhuǎn)化成一個DAG任務運行,分別是:StreamGraph衷快、JobGraph宙橱、ExecutionGraph,F(xiàn)link中JobManager與TaskManager蘸拔,JobManager與Client的交互是基于Akka工具包的养匈,是通過消息驅(qū)動。整個Flink Job的提交還包含著ActorSystem的創(chuàng)建都伪,JobManager的啟動呕乎,TaskManager的啟動和注冊。

二陨晶、Flink所謂"三層圖"結(jié)構(gòu)是哪幾個"圖"猬仁?

一個Flink任務的DAG生成計算圖大致經(jīng)歷以下三個過程:

  • StreamGraph
    最接近代碼所表達的邏輯層面的計算拓撲結(jié)構(gòu),按照用戶代碼的執(zhí)行順序向StreamExecutionEnvironment添加StreamTransformation構(gòu)成流式圖先誉。
  • JobGraph
    從StreamGraph生成湿刽,將可以串聯(lián)合并的節(jié)點進行合并,設置節(jié)點之間的邊褐耳,安排資源共享slot槽位和放置相關聯(lián)的節(jié)點诈闺,上傳任務所需的文件,設置檢查點配置等铃芦。相當于經(jīng)過部分初始化和優(yōu)化處理的任務圖雅镊。
  • ExecutionGraph
    由JobGraph轉(zhuǎn)換而來,包含了任務具體執(zhí)行所需的內(nèi)容刃滓,是最貼近底層實現(xiàn)的執(zhí)行圖仁烹。

** 三、JobManger在集群中扮演了什么角色咧虎? **

JobManager 負責整個 Flink 集群任務的調(diào)度以及資源的管理卓缰,從客戶端中獲取提交的應用,然后根據(jù)集群中 TaskManager 上 TaskSlot 的使用情況砰诵,為提交的應用分配相應的 TaskSlot 資源并命令 TaskManager 啟動從客戶端中獲取的應用征唬。

JobManager 相當于整個集群的 Master 節(jié)點,且整個集群有且只有一個活躍的 JobManager 茁彭,負責整個集群的任務管理和資源管理总寒。

JobManager 和 TaskManager 之間通過 Actor System 進行通信,獲取任務執(zhí)行的情況并通過 Actor System 將應用的任務執(zhí)行情況發(fā)送給客戶端尉间。

同時在任務執(zhí)行的過程中偿乖,F(xiàn)link JobManager 會觸發(fā) Checkpoint 操作击罪,每個 TaskManager 節(jié)點 收到 Checkpoint 觸發(fā)指令后,完成 Checkpoint 操作贪薪,所有的 Checkpoint 協(xié)調(diào)過程都是在 Fink JobManager 中完成媳禁。

當任務完成后,F(xiàn)link 會將任務執(zhí)行的信息反饋給客戶端画切,并且釋放掉 TaskManager 中的資源以供下一次提交任務使用竣稽。

** 四、JobManger在集群啟動過程中起到什么作用霍弹? **

JobManager的職責主要是接收Flink作業(yè)毫别,調(diào)度Task,收集作業(yè)狀態(tài)和管理TaskManager典格。它包含一個Actor岛宦,并且做如下操作:

  • RegisterTaskManager: 它由想要注冊到JobManager的TaskManager發(fā)送。注冊成功會通過AcknowledgeRegistration消息進行Ack耍缴。
  • SubmitJob: 由提交作業(yè)到系統(tǒng)的Client發(fā)送砾肺。提交的信息是JobGraph形式的作業(yè)描述信息。
  • CancelJob: 請求取消指定id的作業(yè)防嗡。成功會返回CancellationSuccess变汪,否則返回CancellationFailure。
  • UpdateTaskExecutionState: 由TaskManager發(fā)送蚁趁,用來更新執(zhí)行節(jié)點(ExecutionVertex)的狀態(tài)裙盾。成功則返回true,否則返回false他嫡。
  • RequestNextInputSplit: TaskManager上的Task請求下一個輸入split番官,成功則返回NextInputSplit,否則返回null涮瞻。
  • JobStatusChanged: 它意味著作業(yè)的狀態(tài)(RUNNING, CANCELING, FINISHED,等)發(fā)生變化鲤拿。這個消息由ExecutionGraph發(fā)送。

** 五署咽、TaskManager在集群中扮演了什么角色?**

TaskManager 相當于整個集群的 Slave 節(jié)點生音,負責具體的任務執(zhí)行和對應任務在每個節(jié)點上的資源申請和管理宁否。

客戶端通過將編寫好的 Flink 應用編譯打包,提交到 JobManager缀遍,然后 JobManager 會根據(jù)已注冊在 JobManager 中 TaskManager 的資源情況慕匠,將任務分配給有資源的 TaskManager節(jié)點,然后啟動并運行任務域醇。

TaskManager 從 JobManager 接收需要部署的任務台谊,然后使用 Slot 資源啟動 Task蓉媳,建立數(shù)據(jù)接入的網(wǎng)絡連接,接收數(shù)據(jù)并開始數(shù)據(jù)處理锅铅。同時 TaskManager 之間的數(shù)據(jù)交互都是通過數(shù)據(jù)流的方式進行的酪呻。

可以看出,F(xiàn)link 的任務運行其實是采用多線程的方式盐须,這和 MapReduce 多 JVM 進行的方式有很大的區(qū)別玩荠,F(xiàn)link 能夠極大提高 CPU 使用效率,在多個任務和 Task 之間通過 TaskSlot 方式共享系統(tǒng)資源贼邓,每個 TaskManager 中通過管理多個 TaskSlot 資源池進行對資源進行有效管理阶冈。

** 六、TaskManager在集群啟動過程中起到什么作用塑径? **

TaskManager的啟動流程較為簡單:
啟動類:org.apache.flink.runtime.taskmanager.TaskManager
核心啟動方法 : selectNetworkInterfaceAndRunTaskManager
啟動后直接向JobManager注冊自己女坑,注冊完成后,進行部分模塊的初始化统舀。

七匆骗、Flink 計算資源的調(diào)度是如何實現(xiàn)的?

TaskManager中最細粒度的資源是Task slot绑咱,代表了一個固定大小的資源子集绰筛,每個TaskManager會將其所占有的資源平分給它的slot。

通過調(diào)整 task slot 的數(shù)量描融,用戶可以定義task之間是如何相互隔離的铝噩。每個 TaskManager 有一個slot,也就意味著每個task運行在獨立的 JVM 中窿克。每個 TaskManager 有多個slot的話骏庸,也就是說多個task運行在同一個JVM中。

而在同一個JVM進程中的task年叮,可以共享TCP連接(基于多路復用)和心跳消息具被,可以減少數(shù)據(jù)的網(wǎng)絡傳輸,也能共享一些數(shù)據(jù)結(jié)構(gòu)只损,一定程度上減少了每個task的消耗一姿。
每個slot可以接受單個task,也可以接受多個連續(xù)task組成的pipeline跃惫,如下圖所示叮叹,F(xiàn)latMap函數(shù)占用一個taskslot,而key Agg函數(shù)和sink函數(shù)共用一個taskslot:

file

八爆存、簡述Flink的數(shù)據(jù)抽象及數(shù)據(jù)交換過程蛉顽?

Flink 為了避免JVM的固有缺陷例如java對象存儲密度低,F(xiàn)GC影響吞吐和響應等先较,實現(xiàn)了自主管理內(nèi)存携冤。MemorySegment就是Flink的內(nèi)存抽象悼粮。默認情況下,一個MemorySegment可以被看做是一個32kb大的內(nèi)存塊的抽象曾棕。這塊內(nèi)存既可以是JVM里的一個byte[]扣猫,也可以是堆外內(nèi)存(DirectByteBuffer)。

在MemorySegment這個抽象之上睁蕾,F(xiàn)link在數(shù)據(jù)從operator內(nèi)的數(shù)據(jù)對象在向TaskManager上轉(zhuǎn)移苞笨,預備被發(fā)給下個節(jié)點的過程中,使用的抽象或者說內(nèi)存對象是Buffer子眶。

對接從Java對象轉(zhuǎn)為Buffer的中間對象是另一個抽象StreamRecord瀑凝。

九、Flink 中的分布式快照機制是如何實現(xiàn)的臭杰?

Flink的容錯機制的核心部分是制作分布式數(shù)據(jù)流和操作算子狀態(tài)的一致性快照粤咪。 這些快照充當一致性checkpoint,系統(tǒng)可以在發(fā)生故障時回滾渴杆。 Flink用于制作這些快照的機制在“分布式數(shù)據(jù)流的輕量級異步快照”中進行了描述寥枝。 它受到分布式快照的標準Chandy-Lamport算法的啟發(fā),專門針對Flink的執(zhí)行模型而定制磁奖。

file

barriers在數(shù)據(jù)流源處被注入并行數(shù)據(jù)流中囊拜。快照n的barriers被插入的位置(我們稱之為Sn)是快照所包含的數(shù)據(jù)在數(shù)據(jù)源中最大位置比搭。例如冠跷,在Apache Kafka中,此位置將是分區(qū)中最后一條記錄的偏移量身诺。 將該位置Sn報告給checkpoint協(xié)調(diào)器(Flink的JobManager)蜜托。

然后barriers向下游流動。當一個中間操作算子從其所有輸入流中收到快照n的barriers時霉赡,它會為快照n發(fā)出barriers進入其所有輸出流中橄务。 一旦sink操作算子(流式DAG的末端)從其所有輸入流接收到barriers n,它就向checkpoint協(xié)調(diào)器確認快照n完成穴亏。在所有sink確認快照后蜂挪,意味快照著已完成。

一旦完成快照n嗓化,job將永遠不再向數(shù)據(jù)源請求Sn之前的記錄锅劝,因為此時這些記錄(及其后續(xù)記錄)將已經(jīng)通過整個數(shù)據(jù)流拓撲,也即是已經(jīng)被處理結(jié)束蟆湖。

十、簡單說說FlinkSQL的是如何實現(xiàn)的玻粪?

Flink 將 SQL 校驗莫绣、SQL 解析以及 SQL 優(yōu)化交給了Apache Calcite所禀。Calcite 在其他很多開源項目里也都應用到了牍戚,譬如 Apache Hive, Apache Drill, Apache Kylin, Cascading。Calcite 在新的架構(gòu)中處于核心的地位结窘,如下圖所示。

file

構(gòu)建抽象語法樹的事情交給了 Calcite 去做充蓝。SQL query 會經(jīng)過 Calcite 解析器轉(zhuǎn)變成 SQL 節(jié)點樹隧枫,通過驗證后構(gòu)建成 Calcite 的抽象語法樹(也就是圖中的 Logical Plan)。另一邊谓苟,Table API 上的調(diào)用會構(gòu)建成 Table API 的抽象語法樹官脓,并通過 Calcite 提供的 RelBuilder 轉(zhuǎn)變成 Calcite 的抽象語法樹。然后依次被轉(zhuǎn)換成邏輯執(zhí)行計劃和物理執(zhí)行計劃涝焙。

在提交任務后會分發(fā)到各個 TaskManager 中運行卑笨,在運行時會使用 Janino 編譯器編譯代碼后運行。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仑撞,一起剝皮案震驚了整個濱河市赤兴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隧哮,老刑警劉巖桶良,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沮翔,居然都是意外死亡陨帆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門鉴竭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來歧譬,“玉大人,你說我怎么就攤上這事搏存」宀剑” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵璧眠,是天一觀的道長缩焦。 經(jīng)常有香客問我,道長责静,這世上最難降的妖魔是什么袁滥? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮灾螃,結(jié)果婚禮上题翻,老公的妹妹穿的比我還像新娘。我一直安慰自己腰鬼,他們只是感情好嵌赠,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布塑荒。 她就那樣靜靜地躺著,像睡著了一般姜挺。 火紅的嫁衣襯著肌膚如雪齿税。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天炊豪,我揣著相機與錄音凌箕,去河邊找鬼。 笑死词渤,一個胖子當著我的面吹牛牵舱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播掖肋,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼仆葡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了志笼?” 一聲冷哼從身側(cè)響起沿盅,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纫溃,沒想到半個月后腰涧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡紊浩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年窖铡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坊谁。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡费彼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出口芍,到底是詐尸還是另有隱情箍铲,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布鬓椭,位于F島的核電站颠猴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏小染。R本人自食惡果不足惜翘瓮,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望裤翩。 院中可真熱鬧资盅,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至择份,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間烫堤,已是汗流浹背荣赶。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸽斟,地道東北人拔创。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像富蓄,于是被迫代替她去往敵國和親剩燥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354