深入理解Apache Flink核心技術(shù)

Apache Flink(下簡(jiǎn)稱Flink)項(xiàng)目是大數(shù)據(jù)處理領(lǐng)域最近冉冉升起的一顆新星列肢,其不同于其他大數(shù)據(jù)項(xiàng)目的諸多特性吸引了越來越多人的關(guān)注退疫。本文將深入分析Flink的一些關(guān)鍵技術(shù)與特性验游,希望能夠幫助讀者對(duì)Flink有更加深入的了解苫幢,對(duì)其他大數(shù)據(jù)系統(tǒng)開發(fā)者也能有所裨益职员。本文假設(shè)讀者已對(duì)MapReduce碧信、Spark及Storm等大數(shù)據(jù)處理框架有所了解赊琳,同時(shí)熟悉流處理與批處理的基本概念。

Flink簡(jiǎn)介

Flink核心是一個(gè)流式的數(shù)據(jù)流執(zhí)行引擎砰碴,其針對(duì)數(shù)據(jù)流的分布式計(jì)算提供了數(shù)據(jù)分布躏筏、數(shù)據(jù)通信以及容錯(cuò)機(jī)制等功能〕释鳎基于流執(zhí)行引擎趁尼,F(xiàn)link提供了諸多更高抽象層的API以便用戶編寫分布式任務(wù):

DataSet API埃碱, 對(duì)靜態(tài)數(shù)據(jù)進(jìn)行批處理操作,將靜態(tài)數(shù)據(jù)抽象成分布式的數(shù)據(jù)集酥泞,用戶可以方便地使用Flink提供的各種操作符對(duì)分布式數(shù)據(jù)集進(jìn)行處理砚殿,支持Java、Scala和Python芝囤。

DataStream API似炎,對(duì)數(shù)據(jù)流進(jìn)行流處理操作,將流式的數(shù)據(jù)抽象成分布式的數(shù)據(jù)流悯姊,用戶可以方便地對(duì)分布式數(shù)據(jù)流進(jìn)行各種操作羡藐,支持Java和Scala。

Table API悯许,對(duì)結(jié)構(gòu)化數(shù)據(jù)進(jìn)行查詢操作仆嗦,將結(jié)構(gòu)化數(shù)據(jù)抽象成關(guān)系表,并通過類SQL的DSL對(duì)關(guān)系表進(jìn)行各種查詢操作先壕,支持Java和Scala欧啤。

此外,F(xiàn)link還針對(duì)特定的應(yīng)用領(lǐng)域提供了領(lǐng)域庫(kù)启上,例如:

Flink ML邢隧,F(xiàn)link的機(jī)器學(xué)習(xí)庫(kù),提供了機(jī)器學(xué)習(xí)Pipelines API并實(shí)現(xiàn)了多種機(jī)器學(xué)習(xí)算法冈在。

Gelly倒慧,F(xiàn)link的圖計(jì)算庫(kù),提供了圖計(jì)算的相關(guān)API及多種圖計(jì)算算法實(shí)現(xiàn)包券。

Flink的技術(shù)棧如圖1所示:


圖1 Flink技術(shù)棧

此外纫谅,F(xiàn)link也可以方便地和Hadoop生態(tài)圈中其他項(xiàng)目集成,例如Flink可以讀取存儲(chǔ)在HDFS或HBase中的靜態(tài)數(shù)據(jù)溅固,以Kafka作為流式的數(shù)據(jù)源付秕,直接重用MapReduce或Storm代碼,或是通過YARN申請(qǐng)集群資源等侍郭。

統(tǒng)一的批處理與流處理系統(tǒng)

在大數(shù)據(jù)處理領(lǐng)域询吴,批處理任務(wù)與流處理任務(wù)一般被認(rèn)為是兩種不同的任務(wù),一個(gè)大數(shù)據(jù)項(xiàng)目一般會(huì)被設(shè)計(jì)為只能處理其中一種任務(wù)亮元,例如Apache Storm猛计、Apache Smaza只支持流處理任務(wù),而Aapche MapReduce爆捞、Apache Tez奉瘤、Apache Spark只支持批處理任務(wù)。Spark Streaming是Apache Spark之上支持流處理任務(wù)的子系統(tǒng)煮甥,看似一個(gè)特例盗温,實(shí)則不然——Spark Streaming采用了一種micro-batch的架構(gòu)藕赞,即把輸入的數(shù)據(jù)流切分成細(xì)粒度的batch,并為每一個(gè)batch數(shù)據(jù)提交一個(gè)批處理的Spark任務(wù)卖局,所以Spark Streaming本質(zhì)上還是基于Spark批處理系統(tǒng)對(duì)流式數(shù)據(jù)進(jìn)行處理找默,和Apache Storm、Apache Smaza等完全流式的數(shù)據(jù)處理方式完全不同吼驶。通過其靈活的執(zhí)行引擎,F(xiàn)link能夠同時(shí)支持批處理任務(wù)與流處理任務(wù)店煞。

在執(zhí)行引擎這一層坠敷,流處理系統(tǒng)與批處理系統(tǒng)最大不同在于節(jié)點(diǎn)間的數(shù)據(jù)傳輸方式醉旦。對(duì)于一個(gè)流處理系統(tǒng),其節(jié)點(diǎn)間數(shù)據(jù)傳輸?shù)臉?biāo)準(zhǔn)模型是:當(dāng)一條數(shù)據(jù)被處理完成后,序列化到緩存中澎媒,然后立刻通過網(wǎng)絡(luò)傳輸?shù)较乱粋€(gè)節(jié)點(diǎn),由下一個(gè)節(jié)點(diǎn)繼續(xù)處理帅矗。而對(duì)于一個(gè)批處理系統(tǒng)吗蚌,其節(jié)點(diǎn)間數(shù)據(jù)傳輸?shù)臉?biāo)準(zhǔn)模型是:當(dāng)一條數(shù)據(jù)被處理完成后,序列化到緩存中囤萤,并不會(huì)立刻通過網(wǎng)絡(luò)傳輸?shù)较乱粋€(gè)節(jié)點(diǎn)昼窗,當(dāng)緩存寫滿,就持久化到本地硬盤上涛舍,當(dāng)所有數(shù)據(jù)都被處理完成后澄惊,才開始將處理后的數(shù)據(jù)通過網(wǎng)絡(luò)傳輸?shù)较乱粋€(gè)節(jié)點(diǎn)。這兩種數(shù)據(jù)傳輸模式是兩個(gè)極端富雅,對(duì)應(yīng)的是流處理系統(tǒng)對(duì)低延遲的要求和批處理系統(tǒng)對(duì)高吞吐量的要求掸驱。Flink的執(zhí)行引擎采用了一種十分靈活的方式,同時(shí)支持了這兩種數(shù)據(jù)傳輸模型没佑。Flink以固定的緩存塊為單位進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)傳輸毕贼,用戶可以通過緩存塊超時(shí)值指定緩存塊的傳輸時(shí)機(jī)。如果緩存塊的超時(shí)值為0蛤奢,則Flink的數(shù)據(jù)傳輸方式類似上文所提到流處理系統(tǒng)的標(biāo)準(zhǔn)模型鬼癣,此時(shí)系統(tǒng)可以獲得最低的處理延遲。如果緩存塊的超時(shí)值為無限大啤贩,則Flink的數(shù)據(jù)傳輸方式類似上文所提到批處理系統(tǒng)的標(biāo)準(zhǔn)模型扣溺,此時(shí)系統(tǒng)可以獲得最高的吞吐量。同時(shí)緩存塊的超時(shí)值也可以設(shè)置為0到無限大之間的任意值瓜晤。緩存塊的超時(shí)閾值越小锥余,則Flink流處理執(zhí)行引擎的數(shù)據(jù)處理延遲越低,但吞吐量也會(huì)降低痢掠,反之亦然驱犹。通過調(diào)整緩存塊的超時(shí)閾值嘲恍,用戶可根據(jù)需求靈活地權(quán)衡系統(tǒng)延遲和吞吐量。


圖2 Flink執(zhí)行引擎數(shù)據(jù)傳輸模式

在統(tǒng)一的流式執(zhí)行引擎基礎(chǔ)上雄驹,F(xiàn)link同時(shí)支持了流計(jì)算和批處理佃牛,并對(duì)性能(延遲、吞吐量等)有所保障医舆。相對(duì)于其他原生的流處理與批處理系統(tǒng)俘侠,并沒有因?yàn)榻y(tǒng)一執(zhí)行引擎而受到影響從而大幅度減輕了用戶安裝、部署蔬将、監(jiān)控爷速、維護(hù)等成本。

Flink流處理的容錯(cuò)機(jī)制

對(duì)于一個(gè)分布式系統(tǒng)來說霞怀,單個(gè)進(jìn)程或是節(jié)點(diǎn)崩潰導(dǎo)致整個(gè)Job失敗是經(jīng)常發(fā)生的事情惫东,在異常發(fā)生時(shí)不會(huì)丟失用戶數(shù)據(jù)并能自動(dòng)恢復(fù)才是分布式系統(tǒng)必須支持的特性之一。本節(jié)主要介紹Flink流處理系統(tǒng)任務(wù)級(jí)別的容錯(cuò)機(jī)制毙石。

批處理系統(tǒng)比較容易實(shí)現(xiàn)容錯(cuò)機(jī)制廉沮,由于文件可以重復(fù)訪問,當(dāng)某個(gè)任務(wù)失敗后徐矩,重啟該任務(wù)即可滞时。但是到了流處理系統(tǒng),由于數(shù)據(jù)源是無限的數(shù)據(jù)流滤灯,從而導(dǎo)致一個(gè)流處理任務(wù)執(zhí)行幾個(gè)月的情況漂洋,將所有數(shù)據(jù)緩存或是持久化,留待以后重復(fù)訪問基本上是不可行的力喷。Flink基于分布式快照與可部分重發(fā)的數(shù)據(jù)源實(shí)現(xiàn)了容錯(cuò)刽漂。用戶可自定義對(duì)整個(gè)Job進(jìn)行快照的時(shí)間間隔,當(dāng)任務(wù)失敗時(shí)弟孟,F(xiàn)link會(huì)將整個(gè)Job恢復(fù)到最近一次快照贝咙,并從數(shù)據(jù)源重發(fā)快照之后的數(shù)據(jù)。Flink的分布式快照實(shí)現(xiàn)借鑒了Chandy和Lamport在1985年發(fā)表的一篇關(guān)于分布式快照的論文拂募,其實(shí)現(xiàn)的主要思想如下:

按照用戶自定義的分布式快照間隔時(shí)間庭猩,F(xiàn)link會(huì)定時(shí)在所有數(shù)據(jù)源中插入一種特殊的快照標(biāo)記消息,這些快照標(biāo)記消息和其他消息一樣在DAG中流動(dòng)陈症,但是不會(huì)被用戶定義的業(yè)務(wù)邏輯所處理蔼水,每一個(gè)快照標(biāo)記消息都將其所在的數(shù)據(jù)流分成兩部分:本次快照數(shù)據(jù)和下次快照數(shù)據(jù)。


圖3 Flink包含快照標(biāo)記消息的消息流

快照標(biāo)記消息沿著DAG流經(jīng)各個(gè)操作符录肯,當(dāng)操作符處理到快照標(biāo)記消息時(shí)趴腋,會(huì)對(duì)自己的狀態(tài)進(jìn)行快照,并存儲(chǔ)起來。當(dāng)一個(gè)操作符有多個(gè)輸入的時(shí)候优炬,F(xiàn)link會(huì)將先抵達(dá)的快照標(biāo)記消息及其之后的消息緩存起來颁井,當(dāng)所有的輸入中對(duì)應(yīng)該次快照的快照標(biāo)記消息全部抵達(dá)后,操作符對(duì)自己的狀態(tài)快照并存儲(chǔ)蠢护,之后處理所有快照標(biāo)記消息之后的已緩存消息雅宾。操作符對(duì)自己的狀態(tài)快照并存儲(chǔ)可以是異步與增量的操作,并不需要阻塞消息的處理葵硕。分布式快照的流程如圖4所示:


圖4 Flink分布式快照流程圖

當(dāng)所有的Data Sink(終點(diǎn)操作符)都收到快照標(biāo)記信息并對(duì)自己的狀態(tài)快照和存儲(chǔ)后眉抬,整個(gè)分布式快照就完成了,同時(shí)通知數(shù)據(jù)源釋放該快照標(biāo)記消息之前的所有消息懈凹。若之后發(fā)生節(jié)點(diǎn)崩潰等異常情況時(shí)蜀变,只需要恢復(fù)之前存儲(chǔ)的分布式快照狀態(tài),并從數(shù)據(jù)源重發(fā)該快照以后的消息就可以了蘸劈。

Exactly-Once是流處理系統(tǒng)需要支持的一個(gè)非常重要的特性,它保證每一條消息只被流處理系統(tǒng)處理一次尊沸,許多流處理任務(wù)的業(yè)務(wù)邏輯都依賴于Exactly-Once特性威沫。相對(duì)于At-Least-Once或是At-Most-Once, Exactly-Once特性對(duì)流處理系統(tǒng)的要求更為嚴(yán)格,實(shí)現(xiàn)也更加困難洼专。Flink基于分布式快照實(shí)現(xiàn)了Exactly-Once特性棒掠。

相對(duì)于其他流處理系統(tǒng)的容錯(cuò)方案,F(xiàn)link基于分布式快照的方案在功能和性能方面都具有很多優(yōu)點(diǎn)屁商,包括:

低延遲烟很。由于操作符狀態(tài)的存儲(chǔ)可以異步,所以進(jìn)行快照的過程基本上不會(huì)阻塞消息的處理蜡镶,因此不會(huì)對(duì)消息延遲產(chǎn)生負(fù)面影響雾袱。

高吞吐量。當(dāng)操作符狀態(tài)較少時(shí)官还,對(duì)吞吐量基本沒有影響芹橡。當(dāng)操作符狀態(tài)較多時(shí),相對(duì)于其他的容錯(cuò)機(jī)制望伦,分布式快照的時(shí)間間隔是用戶自定義的林说,所以用戶可以權(quán)衡錯(cuò)誤恢復(fù)時(shí)間和吞吐量要求來調(diào)整分布式快照的時(shí)間間隔。

與業(yè)務(wù)邏輯的隔離屯伞。Flink的分布式快照機(jī)制與用戶的業(yè)務(wù)邏輯是完全隔離的腿箩,用戶的業(yè)務(wù)邏輯不會(huì)依賴或是對(duì)分布式快照產(chǎn)生任何影響。

錯(cuò)誤恢復(fù)代價(jià)劣摇。分布式快照的時(shí)間間隔越短珠移,錯(cuò)誤恢復(fù)的時(shí)間越少,與吞吐量負(fù)相關(guān)。

Flink流處理的時(shí)間窗口

對(duì)于流處理系統(tǒng)來說剑梳,流入的消息不存在上限唆貌,所以對(duì)于聚合或是連接等操作,流處理系統(tǒng)需要對(duì)流入的消息進(jìn)行分段垢乙,然后基于每一段數(shù)據(jù)進(jìn)行聚合或是連接锨咙。消息的分段即稱為窗口,流處理系統(tǒng)支持的窗口有很多類型追逮,最常見的就是時(shí)間窗口酪刀,基于時(shí)間間隔對(duì)消息進(jìn)行分段處理。本節(jié)主要介紹Flink流處理系統(tǒng)支持的各種時(shí)間窗口钮孵。

對(duì)于目前大部分流處理系統(tǒng)來說骂倘,時(shí)間窗口一般是根據(jù)Task所在節(jié)點(diǎn)的本地時(shí)鐘進(jìn)行切分,這種方式實(shí)現(xiàn)起來比較容易巴席,不會(huì)產(chǎn)生阻塞历涝。但是可能無法滿足某些應(yīng)用需求,比如:

消息本身帶有時(shí)間戳漾唉,用戶希望按照消息本身的時(shí)間特性進(jìn)行分段處理荧库。

由于不同節(jié)點(diǎn)的時(shí)鐘可能不同,以及消息在流經(jīng)各個(gè)節(jié)點(diǎn)的延遲不同赵刑,在某個(gè)節(jié)點(diǎn)屬于同一個(gè)時(shí)間窗口處理的消息分衫,流到下一個(gè)節(jié)點(diǎn)時(shí)可能被切分到不同的時(shí)間窗口中,從而產(chǎn)生不符合預(yù)期的結(jié)果般此。

Flink支持3種類型的時(shí)間窗口蚪战,分別適用于用戶對(duì)于時(shí)間窗口不同類型的要求:

Operator Time。根據(jù)Task所在節(jié)點(diǎn)的本地時(shí)鐘來切分的時(shí)間窗口铐懊。

Event Time邀桑。消息自帶時(shí)間戳,根據(jù)消息的時(shí)間戳進(jìn)行處理科乎,確保時(shí)間戳在同一個(gè)時(shí)間窗口的所有消息一定會(huì)被正確處理概漱。由于消息可能亂序流入Task,所以Task需要緩存當(dāng)前時(shí)間窗口消息處理的狀態(tài)喜喂,直到確認(rèn)屬于該時(shí)間窗口的所有消息都被處理瓤摧,才可以釋放,如果亂序的消息延遲很高會(huì)影響分布式系統(tǒng)的吞吐量和延遲玉吁。

Ingress Time照弥。有時(shí)消息本身并不帶有時(shí)間戳信息,但用戶依然希望按照消息而不是節(jié)點(diǎn)時(shí)鐘劃分時(shí)間窗口进副,例如避免上面提到的第二個(gè)問題这揣,此時(shí)可以在消息源流入Flink流處理系統(tǒng)時(shí)自動(dòng)生成增量的時(shí)間戳賦予消息悔常,之后處理的流程與Event Time相同。Ingress Time可以看成是Event Time的一個(gè)特例给赞,由于其在消息源處時(shí)間戳一定是有序的机打,所以在流處理系統(tǒng)中,相對(duì)于Event Time片迅,其亂序的消息延遲不會(huì)很高残邀,因此對(duì)Flink分布式系統(tǒng)的吞吐量和延遲的影響也會(huì)更小。

Event Time時(shí)間窗口的實(shí)現(xiàn)

Flink借鑒了Google的MillWheel項(xiàng)目柑蛇,通過WaterMark來支持基于Event Time的時(shí)間窗口芥挣。

當(dāng)操作符通過基于Event Time的時(shí)間窗口來處理數(shù)據(jù)時(shí),它必須在確定所有屬于該時(shí)間窗口的消息全部流入此操作符后才能開始數(shù)據(jù)處理耻台。但是由于消息可能是亂序的空免,所以操作符無法直接確認(rèn)何時(shí)所有屬于該時(shí)間窗口的消息全部流入此操作符。WaterMark包含一個(gè)時(shí)間戳盆耽,F(xiàn)link使用WaterMark標(biāo)記所有小于該時(shí)間戳的消息都已流入蹋砚,F(xiàn)link的數(shù)據(jù)源在確認(rèn)所有小于某個(gè)時(shí)間戳的消息都已輸出到Flink流處理系統(tǒng)后,會(huì)生成一個(gè)包含該時(shí)間戳的WaterMark摄杂,插入到消息流中輸出到Flink流處理系統(tǒng)中坝咐,F(xiàn)link操作符按照時(shí)間窗口緩存所有流入的消息,當(dāng)操作符處理到WaterMark時(shí)匙姜,它對(duì)所有小于該WaterMark時(shí)間戳的時(shí)間窗口數(shù)據(jù)進(jìn)行處理并發(fā)送到下一個(gè)操作符節(jié)點(diǎn)畅厢,然后也將WaterMark發(fā)送到下一個(gè)操作符節(jié)點(diǎn)冯痢。

為了保證能夠處理所有屬于某個(gè)時(shí)間窗口的消息氮昧,操作符必須等到大于這個(gè)時(shí)間窗口的WaterMark之后才能開始對(duì)該時(shí)間窗口的消息進(jìn)行處理,相對(duì)于基于Operator Time的時(shí)間窗口浦楣,F(xiàn)link需要占用更多內(nèi)存袖肥,且會(huì)直接影響消息處理的延遲時(shí)間。對(duì)此振劳,一個(gè)可能的優(yōu)化措施是椎组,對(duì)于聚合類的操作符,可以提前對(duì)部分消息進(jìn)行聚合操作历恐,當(dāng)有屬于該時(shí)間窗口的新消息流入時(shí)寸癌,基于之前的部分聚合結(jié)果繼續(xù)計(jì)算,這樣的話弱贼,只需緩存中間計(jì)算結(jié)果即可蒸苇,無需緩存該時(shí)間窗口的所有消息。

對(duì)于基于Event Time時(shí)間窗口的操作符來說吮旅,流入WaterMark的時(shí)間戳與當(dāng)前節(jié)點(diǎn)的時(shí)鐘一致是最簡(jiǎn)單理想的狀況溪烤,但是在實(shí)際環(huán)境中是不可能的,由于消息的亂序以及前面節(jié)點(diǎn)處理效率的不同,總是會(huì)有某些消息流入時(shí)間大于其本身的時(shí)間戳檬嘀,真實(shí)WaterMark時(shí)間戳與理想情況下WaterMark時(shí)間戳的差別稱為Time Skew槽驶,如圖5所示:


圖5 WaterMark的Time Skew圖

Time Skew決定了該WaterMark與上一個(gè)WaterMark之間的時(shí)間窗口所有數(shù)據(jù)需要緩存的時(shí)間,Time Skew時(shí)間越長(zhǎng)鸳兽,該時(shí)間窗口數(shù)據(jù)的延遲越長(zhǎng)掂铐,占用內(nèi)存的時(shí)間也越長(zhǎng),同時(shí)會(huì)對(duì)流處理系統(tǒng)的吞吐量產(chǎn)生負(fù)面影響贸铜。

基于時(shí)間戳的排序

在流處理系統(tǒng)中堡纬,由于流入的消息是無限的,所以對(duì)消息進(jìn)行排序基本上被認(rèn)為是不可行的蒿秦。但是在Flink流處理系統(tǒng)中烤镐,基于WaterMark,F(xiàn)link實(shí)現(xiàn)了基于時(shí)間戳的全局排序棍鳖。排序的實(shí)現(xiàn)思路如下:排序操作符緩存所有流入的消息炮叶,當(dāng)其接收到WaterMark時(shí),對(duì)時(shí)間戳小于該WaterMark的消息進(jìn)行排序渡处,并發(fā)送到下一個(gè)節(jié)點(diǎn)镜悉,在此排序操作符中釋放所有時(shí)間戳小于該WaterMark的消息,繼續(xù)緩存流入的消息医瘫,等待下一個(gè)WaterMark觸發(fā)下一次排序侣肄。

由于WaterMark保證了在其之后不會(huì)出現(xiàn)時(shí)間戳比它小的消息,所以可以保證排序的正確性醇份。需要注意的是稼锅,如果排序操作符有多個(gè)節(jié)點(diǎn),只能保證每個(gè)節(jié)點(diǎn)的流出消息是有序的僚纷,節(jié)點(diǎn)之間的消息不能保證有序矩距,要實(shí)現(xiàn)全局有序,則只能有一個(gè)排序操作符節(jié)點(diǎn)怖竭。

通過支持基于Event Time的消息處理锥债,F(xiàn)link擴(kuò)展了其流處理系統(tǒng)的應(yīng)用范圍,使得更多的流處理任務(wù)可以通過Flink來執(zhí)行痊臭。

定制的內(nèi)存管理

Flink項(xiàng)目基于Java及Scala等JVM語言哮肚,JVM本身作為一個(gè)各種類型應(yīng)用的執(zhí)行平臺(tái),其對(duì)Java對(duì)象的管理也是基于通用的處理策略广匙,其垃圾回收器通過估算Java對(duì)象的生命周期對(duì)Java對(duì)象進(jìn)行有效率的管理允趟。

針對(duì)不同類型的應(yīng)用,用戶可能需要針對(duì)該類型應(yīng)用的特點(diǎn)艇潭,配置針對(duì)性的JVM參數(shù)更有效率的管理Java對(duì)象拼窥,從而提高性能戏蔑。這種JVM調(diào)優(yōu)的黑魔法需要用戶對(duì)應(yīng)用本身及JVM的各參數(shù)有深入了解,極大地提高了分布式計(jì)算平臺(tái)的調(diào)優(yōu)門檻鲁纠。Flink框架本身了解計(jì)算邏輯每個(gè)步驟的數(shù)據(jù)傳輸总棵,相比于JVM垃圾回收器,其了解更多的Java對(duì)象生命周期改含,從而為更有效率地管理Java對(duì)象提供了可能情龄。

JVM存在的問題

Java對(duì)象開銷

相對(duì)于c/c++等更加接近底層的語言,Java對(duì)象的存儲(chǔ)密度相對(duì)偏低捍壤,例如[1]骤视,“abcd”這樣簡(jiǎn)單的字符串在UTF-8編碼中需要4個(gè)字節(jié)存儲(chǔ),但采用了UTF-16編碼存儲(chǔ)字符串的Java則需要8個(gè)字節(jié)鹃觉,同時(shí)Java對(duì)象還有header等其他額外信息专酗,一個(gè)4字節(jié)字符串對(duì)象在Java中需要48字節(jié)的空間來存儲(chǔ)。對(duì)于大部分的大數(shù)據(jù)應(yīng)用盗扇,內(nèi)存都是稀缺資源祷肯,更有效率地內(nèi)存存儲(chǔ),意味著CPU數(shù)據(jù)訪問吞吐量更高疗隶,以及更少磁盤落地的存在佑笋。

對(duì)象存儲(chǔ)結(jié)構(gòu)引發(fā)的cache miss

為了緩解CPU處理速度與內(nèi)存訪問速度的差距[2],現(xiàn)代CPU數(shù)據(jù)訪問一般都會(huì)有多級(jí)緩存斑鼻。當(dāng)從內(nèi)存加載數(shù)據(jù)到緩存時(shí)蒋纬,一般是以cache line為單位加載數(shù)據(jù),所以當(dāng)CPU訪問的數(shù)據(jù)如果是在內(nèi)存中連續(xù)存儲(chǔ)的話坚弱,訪問的效率會(huì)非常高蜀备。如果CPU要訪問的數(shù)據(jù)不在當(dāng)前緩存所有的cache line中,則需要從內(nèi)存中加載對(duì)應(yīng)的數(shù)據(jù)史汗,這被稱為一次cache miss琼掠。當(dāng)cache miss非常高的時(shí)候拒垃,CPU大部分的時(shí)間都在等待數(shù)據(jù)加載停撞,而不是真正的處理數(shù)據(jù)。Java對(duì)象并不是連續(xù)的存儲(chǔ)在內(nèi)存上悼瓮,同時(shí)很多的Java數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)聚集性也不好戈毒。

大數(shù)據(jù)的垃圾回收

Java的垃圾回收機(jī)制一直讓Java開發(fā)者又愛又恨,一方面它免去了開發(fā)者自己回收資源的步驟横堡,提高了開發(fā)效率埋市,減少了內(nèi)存泄漏的可能,另一方面垃圾回收也是Java應(yīng)用的不定時(shí)炸彈命贴,有時(shí)秒級(jí)甚至是分鐘級(jí)的垃圾回收極大影響了Java應(yīng)用的性能和可用性道宅。在時(shí)下數(shù)據(jù)中心食听,大容量?jī)?nèi)存得到了廣泛的應(yīng)用,甚至出現(xiàn)了單臺(tái)機(jī)器配置TB內(nèi)存的情況污茵,同時(shí)樱报,大數(shù)據(jù)分析通常會(huì)遍歷整個(gè)源數(shù)據(jù)集,對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換泞当、清洗迹蛤、處理等步驟。在這個(gè)過程中襟士,會(huì)產(chǎn)生海量的Java對(duì)象盗飒,JVM的垃圾回收?qǐng)?zhí)行效率對(duì)性能有很大影響。通過JVM參數(shù)調(diào)優(yōu)提高垃圾回收效率需要用戶對(duì)應(yīng)用和分布式計(jì)算框架以及JVM的各參數(shù)有深入了解陋桂,而且有時(shí)候這也遠(yuǎn)遠(yuǎn)不夠逆趣。

OOM問題

OutOfMemoryError是分布式計(jì)算框架經(jīng)常會(huì)遇到的問題,當(dāng)JVM中所有對(duì)象大小超過分配給JVM的內(nèi)存大小時(shí)嗜历,就會(huì)出現(xiàn)OutOfMemoryError錯(cuò)誤汗贫,JVM崩潰,分布式框架的健壯性和性能都會(huì)受到影響秸脱。通過JVM管理內(nèi)存落包,同時(shí)試圖解決OOM問題的應(yīng)用,通常都需要檢查Java對(duì)象的大小摊唇,并在某些存儲(chǔ)Java對(duì)象特別多的數(shù)據(jù)結(jié)構(gòu)中設(shè)置閾值進(jìn)行控制咐蝇。但是JVM并沒有提供官方檢查Java對(duì)象大小的工具,第三方的工具類庫(kù)可能無法準(zhǔn)確通用地確定Java對(duì)象大小[6]巷查。侵入式的閾值檢查也會(huì)為分布式計(jì)算框架的實(shí)現(xiàn)增加很多額外與業(yè)務(wù)邏輯無關(guān)的代碼有序。

Flink的處理策略

為了解決以上提到的問題,高性能分布式計(jì)算框架通常需要以下技術(shù):

定制的序列化工具岛请。顯式內(nèi)存管理的前提步驟就是序列化旭寿,將Java對(duì)象序列化成二進(jìn)制數(shù)據(jù)存儲(chǔ)在內(nèi)存上(on heap或是off-heap)。通用的序列化框架崇败,如Java默認(rèn)使用java.io.Serializable將Java對(duì)象及其成員變量的所有元信息作為其序列化數(shù)據(jù)的一部分盅称,序列化后的數(shù)據(jù)包含了所有反序列化所需的信息。這在某些場(chǎng)景中十分必要后室,但是對(duì)于Flink這樣的分布式計(jì)算框架來說缩膝,這些元數(shù)據(jù)信息可能是冗余數(shù)據(jù)。定制的序列化框架岸霹,如Hadoop的org.apache.hadoop.io.Writable需要用戶實(shí)現(xiàn)該接口疾层,并自定義類的序列化和反序列化方法。這種方式效率最高贡避,但需要用戶額外的工作痛黎,不夠友好予弧。

顯式的內(nèi)存管理。一般通用的做法是批量申請(qǐng)和釋放內(nèi)存湖饱,每個(gè)JVM實(shí)例有一個(gè)統(tǒng)一的內(nèi)存管理器桌肴,所有內(nèi)存的申請(qǐng)和釋放都通過該內(nèi)存管理器進(jìn)行。這可以避免常見的內(nèi)存碎片問題琉历,同時(shí)由于數(shù)據(jù)以二進(jìn)制的方式存儲(chǔ)坠七,可以大大減輕垃圾回收壓力。

緩存友好的數(shù)據(jù)結(jié)構(gòu)和算法旗笔。對(duì)于計(jì)算密集的數(shù)據(jù)結(jié)構(gòu)和算法彪置,直接操作序列化后的二進(jìn)制數(shù)據(jù),而不是將對(duì)象反序列化后再進(jìn)行操作蝇恶。同時(shí)拳魁,只將操作相關(guān)的數(shù)據(jù)連續(xù)存儲(chǔ),可以最大化的利用L1/L2/L3緩存撮弧,減少Cache miss的概率潘懊,提升CPU計(jì)算的吞吐量。以排序?yàn)槔哐埽捎谂判虻闹饕僮魇菍?duì)Key進(jìn)行對(duì)比授舟,如果將所有排序數(shù)據(jù)的Key與Value分開并對(duì)Key連續(xù)存儲(chǔ),那么訪問Key時(shí)的Cache命中率會(huì)大大提高贸辈。

定制的序列化工具

分布式計(jì)算框架可以使用定制序列化工具的前提是要待處理數(shù)據(jù)流通常是同一類型释树,由于數(shù)據(jù)集對(duì)象的類型固定,從而可以只保存一份對(duì)象Schema信息擎淤,節(jié)省大量的存儲(chǔ)空間奢啥。同時(shí),對(duì)于固定大小的類型嘴拢,也可通過固定的偏移位置存取桩盲。在需要訪問某個(gè)對(duì)象成員變量時(shí),通過定制的序列化工具席吴,并不需要反序列化整個(gè)Java對(duì)象赌结,而是直接通過偏移量,從而只需要反序列化特定的對(duì)象成員變量抢腐。如果對(duì)象的成員變量較多時(shí)姑曙,能夠大大減少Java對(duì)象的創(chuàng)建開銷襟交,以及內(nèi)存數(shù)據(jù)的拷貝大小迈倍。Flink數(shù)據(jù)集都支持任意Java或是Scala類型,通過自動(dòng)生成定制序列化工具捣域,既保證了API接口對(duì)用戶友好(不用像Hadoop那樣數(shù)據(jù)類型需要繼承實(shí)現(xiàn)org.apache.hadoop.io.Writable接口)啼染,也達(dá)到了和Hadoop類似的序列化效率宴合。

Flink對(duì)數(shù)據(jù)集的類型信息進(jìn)行分析,然后自動(dòng)生成定制的序列化工具類迹鹅。Flink支持任意的Java或是Scala類型卦洽,通過Java Reflection框架分析基于Java的Flink程序UDF(User Define Function)的返回類型的類型信息,通過Scala Compiler分析基于Scala的Flink程序UDF的返回類型的類型信息斜棚。類型信息由TypeInformation類表示阀蒂,這個(gè)類有諸多具體實(shí)現(xiàn)類,例如:

BasicTypeInfo任意Java基本類型(裝包或未裝包)和String類型弟蚀。

BasicArrayTypeInfo任意Java基本類型數(shù)組(裝包或未裝包)和String數(shù)組蚤霞。

WritableTypeInfo任意Hadoop的Writable接口的實(shí)現(xiàn)類。

TupleTypeInfo任意的Flink tuple類型(支持Tuple1 to Tuple25)义钉。 Flink tuples是固定長(zhǎng)度固定類型的Java Tuple實(shí)現(xiàn)昧绣。

CaseClassTypeInfo任意的 Scala CaseClass(包括 Scala tuples)。

PojoTypeInfo任意的POJO (Java or Scala)捶闸,例如Java對(duì)象的所有成員變量夜畴,要么是public修飾符定義,要么有g(shù)etter/setter方法删壮。

GenericTypeInfo任意無法匹配之前幾種類型的類贪绘。

前6種類型數(shù)據(jù)集幾乎覆蓋了絕大部分的Flink程序,針對(duì)前6種類型數(shù)據(jù)集央碟,F(xiàn)link皆可以自動(dòng)生成對(duì)應(yīng)的TypeSerializer定制序列化工具兔簇,非常有效率地對(duì)數(shù)據(jù)集進(jìn)行序列化和反序列化。對(duì)于第7種類型硬耍,F(xiàn)link使用Kryo進(jìn)行序列化和反序列化垄琐。此外,對(duì)于可被用作Key的類型经柴,F(xiàn)link還同時(shí)自動(dòng)生成TypeComparator狸窘,用來輔助直接對(duì)序列化后的二進(jìn)制數(shù)據(jù)直接進(jìn)行compare、hash等操作坯认。對(duì)于Tuple翻擒、CaseClass、Pojo等組合類型牛哺,F(xiàn)link自動(dòng)生成的TypeSerializer陋气、TypeComparator同樣是組合的,并把其成員的序列化/反序列化代理給其成員對(duì)應(yīng)的TypeSerializer引润、TypeComparator巩趁,如圖6所示:


圖6 Flink組合類型序列化

此外如有需要,用戶可通過集成TypeInformation接口定制實(shí)現(xiàn)自己的序列化工具淳附。

顯式的內(nèi)存管理

垃圾回收是JVM內(nèi)存管理回避不了的問題议慰,JDK8的G1算法改善了JVM垃圾回收的效率和可用范圍蠢古,但對(duì)于大數(shù)據(jù)處理實(shí)際環(huán)境還遠(yuǎn)遠(yuǎn)不夠。這也和現(xiàn)在分布式框架的發(fā)展趨勢(shì)有所沖突别凹,越來越多的分布式計(jì)算框架希望盡可能多地將待處理數(shù)據(jù)集放入內(nèi)存草讶,而對(duì)于JVM垃圾回收來說,內(nèi)存中Java對(duì)象越少炉菲、存活時(shí)間越短堕战,其效率越高。通過JVM進(jìn)行內(nèi)存管理的話拍霜,OutOfMemoryError也是一個(gè)很難解決的問題践啄。同時(shí),在JVM內(nèi)存管理中沉御,Java對(duì)象有潛在的碎片化存儲(chǔ)問題(Java對(duì)象所有信息可能在內(nèi)存中連續(xù)存儲(chǔ))屿讽,也有可能在所有Java對(duì)象大小沒有超過JVM分配內(nèi)存時(shí),出現(xiàn)OutOfMemoryError問題吠裆。Flink將內(nèi)存分為3個(gè)部分伐谈,每個(gè)部分都有不同用途:

Network buffers: 一些以32KB Byte數(shù)組為單位的buffer,主要被網(wǎng)絡(luò)模塊用于數(shù)據(jù)的網(wǎng)絡(luò)傳輸试疙。

Memory Manager pool大量以32KB Byte數(shù)組為單位的內(nèi)存池诵棵,所有的運(yùn)行時(shí)算法(例如Sort/Shuffle/Join)都從這個(gè)內(nèi)存池申請(qǐng)內(nèi)存,并將序列化后的數(shù)據(jù)存儲(chǔ)其中祝旷,結(jié)束后釋放回內(nèi)存池履澳。

Remaining (Free) Heap主要留給UDF中用戶自己創(chuàng)建的Java對(duì)象,由JVM管理怀跛。

Network buffers在Flink中主要基于Netty的網(wǎng)絡(luò)傳輸距贷,無需多講。Remaining Heap用于UDF中用戶自己創(chuàng)建的Java對(duì)象吻谋,在UDF中忠蝗,用戶通常是流式的處理數(shù)據(jù),并不需要很多內(nèi)存漓拾,同時(shí)Flink也不鼓勵(lì)用戶在UDF中緩存很多數(shù)據(jù)阁最,因?yàn)檫@會(huì)引起前面提到的諸多問題。Memory Manager pool(以后以內(nèi)存池代指)通常會(huì)配置為最大的一塊內(nèi)存骇两,接下來會(huì)詳細(xì)介紹速种。

在Flink中,內(nèi)存池由多個(gè)MemorySegment組成低千,每個(gè)MemorySegment代表一塊連續(xù)的內(nèi)存配阵,底層存儲(chǔ)是byte[],默認(rèn)32KB大小。MemorySegment提供了根據(jù)偏移量訪問數(shù)據(jù)的各種方法闸餐,如get/put int饱亮、long矾芙、float舍沙、double等,MemorySegment之間數(shù)據(jù)拷貝等方法和java.nio.ByteBuffer類似剔宪。對(duì)于Flink的數(shù)據(jù)結(jié)構(gòu)拂铡,通常包括多個(gè)向內(nèi)存池申請(qǐng)的MemeorySegment,所有要存入的對(duì)象通過TypeSerializer序列化之后葱绒,將二進(jìn)制數(shù)據(jù)存儲(chǔ)在MemorySegment中感帅,在取出時(shí)通過TypeSerializer反序列化。數(shù)據(jù)結(jié)構(gòu)通過MemorySegment提供的set/get方法訪問具體的二進(jìn)制數(shù)據(jù)地淀。Flink這種看起來比較復(fù)雜的內(nèi)存管理方式帶來的好處主要有:

二進(jìn)制的數(shù)據(jù)存儲(chǔ)大大提高了數(shù)據(jù)存儲(chǔ)密度失球,節(jié)省了存儲(chǔ)空間。

所有的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)和算法只能通過內(nèi)存池申請(qǐng)內(nèi)存帮毁,保證了其使用的內(nèi)存大小是固定的实苞,不會(huì)因?yàn)檫\(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)和算法而發(fā)生OOM。對(duì)于大部分的分布式計(jì)算框架來說烈疚,這部分由于要緩存大量數(shù)據(jù)最有可能導(dǎo)致OOM黔牵。

內(nèi)存池雖然占據(jù)了大部分內(nèi)存,但其中的MemorySegment容量較大(默認(rèn)32KB)爷肝,所以內(nèi)存池中的Java對(duì)象其實(shí)很少猾浦,而且一直被內(nèi)存池引用,所有在垃圾回收時(shí)很快進(jìn)入持久代灯抛,大大減輕了JVM垃圾回收的壓力金赦。

Remaining Heap的內(nèi)存雖然由JVM管理,但是由于其主要用來存儲(chǔ)用戶處理的流式數(shù)據(jù)对嚼,生命周期非常短素邪,速度很快的Minor GC就會(huì)全部回收掉,一般不會(huì)觸發(fā)Full GC猪半。

Flink當(dāng)前的內(nèi)存管理在最底層是基于byte[]兔朦,所以數(shù)據(jù)最終還是on-heap,最近Flink增加了off-heap的內(nèi)存管理支持磨确。Flink off-heap的內(nèi)存管理相對(duì)于on-heap的優(yōu)點(diǎn)主要在于:

啟動(dòng)分配了大內(nèi)存(例如100G)的JVM很耗費(fèi)時(shí)間沽甥,垃圾回收也很慢。如果采用off-heap乏奥,剩下的Network buffer和Remaining heap都會(huì)很小摆舟,垃圾回收也不用考慮MemorySegment中的Java對(duì)象了。

更有效率的IO操作。在off-heap下恨诱,將MemorySegment寫到磁盤或是網(wǎng)絡(luò)可以支持zeor-copy技術(shù)媳瞪,而on-heap的話則至少需要一次內(nèi)存拷貝。

off-heap可用于錯(cuò)誤恢復(fù)照宝,比如JVM崩潰蛇受,在on-heap時(shí)數(shù)據(jù)也隨之丟失,但在off-heap下厕鹃,off-heap的數(shù)據(jù)可能還在兢仰。此外,off-heap上的數(shù)據(jù)還可以和其他程序共享剂碴。

緩存友好的計(jì)算

磁盤IO和網(wǎng)絡(luò)IO之前一直被認(rèn)為是Hadoop系統(tǒng)的瓶頸把将,但是隨著Spark、Flink等新一代分布式計(jì)算框架的發(fā)展忆矛,越來越多的趨勢(shì)使得CPU/Memory逐漸成為瓶頸察蹲,這些趨勢(shì)包括:

更先進(jìn)的IO硬件逐漸普及。10GB網(wǎng)絡(luò)和SSD硬盤等已經(jīng)被越來越多的數(shù)據(jù)中心使用催训。

更高效的存儲(chǔ)格式洽议。Parquet,ORC等列式存儲(chǔ)被越來越多的Hadoop項(xiàng)目支持瞳腌,其非常高效的壓縮性能大大減少了落地存儲(chǔ)的數(shù)據(jù)量绞铃。

更高效的執(zhí)行計(jì)劃。例如很多SQL系統(tǒng)執(zhí)行計(jì)劃優(yōu)化器的Fliter-Push-Down優(yōu)化會(huì)將過濾條件盡可能的提前嫂侍,甚至提前到Parquet的數(shù)據(jù)訪問層儿捧,使得在很多實(shí)際的工作負(fù)載中并不需要很多的磁盤IO。

由于CPU處理速度和內(nèi)存訪問速度的差距挑宠,提升CPU的處理效率的關(guān)鍵在于最大化的利用L1/L2/L3/Memory菲盾,減少任何不必要的Cache miss。定制的序列化工具給Flink提供了可能各淀,通過定制的序列化工具懒鉴,F(xiàn)link訪問的二進(jìn)制數(shù)據(jù)本身,因?yàn)檎加脙?nèi)存較小碎浇,存儲(chǔ)密度比較大临谱,而且還可以在設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)和算法時(shí)盡量連續(xù)存儲(chǔ),減少內(nèi)存碎片化對(duì)Cache命中率的影響奴璃,甚至更進(jìn)一步悉默,F(xiàn)link可以只是將需要操作的部分?jǐn)?shù)據(jù)(如排序時(shí)的Key)連續(xù)存儲(chǔ),而將其他部分的數(shù)據(jù)存儲(chǔ)在其他地方苟穆,從而最大可能地提升Cache命中的概率抄课。

以Flink中的排序?yàn)槔牵判蛲ǔJ欠植际接?jì)算框架中一個(gè)非常重的操作,F(xiàn)link通過特殊設(shè)計(jì)的排序算法獲得了非常好的性能跟磨,其排序算法的實(shí)現(xiàn)如下:

將待排序的數(shù)據(jù)經(jīng)過序列化后存儲(chǔ)在兩個(gè)不同的MemorySegment集中间聊。數(shù)據(jù)全部的序列化值存放于其中一個(gè)MemorySegment集中。數(shù)據(jù)序列化后的Key和指向第一個(gè)MemorySegment集中值的指針存放于第二個(gè)MemorySegment集中抵拘。

對(duì)第二個(gè)MemorySegment集中的Key進(jìn)行排序哎榴,如需交換Key位置,只需交換對(duì)應(yīng)的Key+Pointer的位置仑濒,第一個(gè)MemorySegment集中的數(shù)據(jù)無需改變叹话。 當(dāng)比較兩個(gè)Key大小時(shí)偷遗,TypeComparator提供了直接基于二進(jìn)制數(shù)據(jù)的對(duì)比方法墩瞳,無需反序列化任何數(shù)據(jù)。

排序完成后氏豌,訪問數(shù)據(jù)時(shí)喉酌,按照第二個(gè)MemorySegment集中Key的順序訪問,并通過Pointer值找到數(shù)據(jù)在第一個(gè)MemorySegment集中的位置泵喘,通過TypeSerializer反序列化成Java對(duì)象返回泪电。


圖7 Flink排序算法

這樣實(shí)現(xiàn)的好處有:

通過Key和Full data分離存儲(chǔ)的方式盡量將被操作的數(shù)據(jù)最小化,提高Cache命中的概率纪铺,從而提高CPU的吞吐量。

移動(dòng)數(shù)據(jù)時(shí),只需移動(dòng)Key+Pointer览徒,而無須移動(dòng)數(shù)據(jù)本身扯键,大大減少了內(nèi)存拷貝的數(shù)據(jù)量。

TypeComparator直接基于二進(jìn)制數(shù)據(jù)進(jìn)行操作芜繁,節(jié)省了反序列化的時(shí)間旺隙。

通過定制的內(nèi)存管理,F(xiàn)link通過充分利用內(nèi)存與CPU緩存骏令,大大提高了CPU的執(zhí)行效率蔬捷,同時(shí)由于大部分內(nèi)存都由框架自己控制,也很大程度提升了系統(tǒng)的健壯性榔袋,減少了OOM出現(xiàn)的可能周拐。

總結(jié)

本文主要介紹了Flink項(xiàng)目的一些關(guān)鍵特性,F(xiàn)link是一個(gè)擁有諸多特色的項(xiàng)目凰兑,包括其統(tǒng)一的批處理和流處理執(zhí)行引擎妥粟,通用大數(shù)據(jù)計(jì)算框架與傳統(tǒng)數(shù)據(jù)庫(kù)系統(tǒng)的技術(shù)結(jié)合,以及流處理系統(tǒng)的諸多技術(shù)創(chuàng)新等聪黎,因?yàn)槠邢藓比荩現(xiàn)link還有一些其他很有意思的特性沒有詳細(xì)介紹备恤,比如DataSet API級(jí)別的執(zhí)行計(jì)劃優(yōu)化器,原生的迭代操作符等锦秒,感興趣的讀者可以通過Flink官網(wǎng)了解更多Flink的詳細(xì)內(nèi)容露泊。希望通過本文的介紹能夠讓讀者對(duì)Flink有更多的了解,也讓更多的人使用甚至參與到Flink項(xiàng)目中去旅择。? ?大數(shù)據(jù)學(xué)習(xí)群 724693112惭笑,歡迎大家一起來進(jìn)行技術(shù)和資料分享交流。想了解大數(shù)據(jù)的朋友也可以一起來聊聊啊生真。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沉噩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子柱蟀,更是在濱河造成了極大的恐慌川蒙,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件长已,死亡現(xiàn)場(chǎng)離奇詭異畜眨,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)术瓮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門康聂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胞四,你說我怎么就攤上這事恬汁。” “怎么了辜伟?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵氓侧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我游昼,道長(zhǎng)甘苍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任烘豌,我火速辦了婚禮载庭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘廊佩。我一直安慰自己囚聚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布标锄。 她就那樣靜靜地躺著顽铸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪料皇。 梳的紋絲不亂的頭發(fā)上谓松,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天星压,我揣著相機(jī)與錄音,去河邊找鬼鬼譬。 笑死娜膘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的优质。 我是一名探鬼主播竣贪,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼巩螃!你這毒婦竟也來了演怎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤避乏,失蹤者是張志新(化名)和其女友劉穎爷耀,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淑际,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡畏纲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年扇住,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了春缕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艘蹋,死狀恐怖锄贼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情女阀,我是刑警寧澤宅荤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站浸策,受9級(jí)特大地震影響冯键,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜庸汗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一惫确、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚯舱,春花似錦改化、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兄裂,卻和暖如春句旱,著一層夾襖步出監(jiān)牢的瞬間阳藻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工谈撒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稚配,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓港华,卻偏偏與公主長(zhǎng)得像道川,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子立宜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容