BookKeeper 工作原理

BookKeeper 工作原理

參考文檔:

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-1-high-level-6dce62269125

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-2-writes-359ffc17c497

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-3-reads-31637b118bf

https://medium.com/splunk-maas/apache-bookkeeper-internals-part-4-back-pressure-7847bd6d1257

https://mp.weixin.qq.com/s/L0IPBZDEI31mOrvLIwaqAA

BookKeeper是一個(gè)高性能的追加寫(xiě)(append-only)存儲(chǔ)服務(wù),主要用在Pulsar中哄啄,單個(gè)BookKeeper節(jié)點(diǎn)也叫Bookie歇竟。本文試圖來(lái)解釋一些BookKeeper中的架構(gòu)势似。

BookKeeper總體架構(gòu)

BookKeeper是一個(gè)單進(jìn)程服務(wù)低矮,多個(gè)進(jìn)程提供對(duì)等的服務(wù)內(nèi)容婿滓。其總體架構(gòu)如下菜皂,

20220114-BookKeeper 工作原理-2022-01-17-10-20-35.png

最上層是Netty燕侠,用來(lái)處理網(wǎng)絡(luò)請(qǐng)求IO棒掠。BookKeeper內(nèi)部主要有2塊Entry處理的服務(wù)孵构,Journal和DbLedgerStorage。Journal是BookKeeper的WAL烟很,DbLedgerStorage是處理Write Cache颈墅,并從后臺(tái)將Entry數(shù)據(jù)刷入到Entry Log文件中。

BookKeeper的線程模型

BookKeeper的線程模型如下圖雾袱,

20220114-BookKeeper 工作原理-2022-01-17-10-20-53.png

不同顏色的SyncThread恤筛、DbStorageThread是處理后臺(tái)任務(wù)線程,其他是是處理實(shí)時(shí)任務(wù)的線程芹橡。介紹其中幾個(gè)線程毒坛,

  1. Netty Threadpool,Netty線程林说,主要處理網(wǎng)絡(luò)IO煎殷,消息解析;
  2. Long Pool Threadpool腿箩,長(zhǎng)輪詢線程豪直,當(dāng)寫(xiě)入線程發(fā)生相關(guān)寫(xiě)入的時(shí)候,觸發(fā)該線程(沒(méi)有理解這個(gè)線程的具體作用)珠移;
  3. Write Threadpool弓乙,寫(xiě)線程,處理寫(xiě)入請(qǐng)求的任務(wù)剑梳;
  4. Read Threadpool唆貌,讀線程,處理讀取請(qǐng)求的任務(wù)垢乙;
  5. High Priority Threadpool锨咙,高優(yōu)先級(jí)線程,對(duì)請(qǐng)求添加高優(yōu)先級(jí)標(biāo)簽追逮,主要是Pulsar的Fencing和恢復(fù)場(chǎng)景會(huì)用到酪刀,正常情況下不會(huì)用;

值得注意的是钮孵,write, read, long poll 和 high priority這4類(lèi)線程都是OrderedExecutor類(lèi)的實(shí)例骂倘,這些線程組成一個(gè)大的線程組來(lái)提供服務(wù),并與Ledger id綁定巴席。Netty根據(jù)Ledger id來(lái)分發(fā)請(qǐng)求到響應(yīng)的線程組進(jìn)行處理历涝。部分線程的默認(rèn)線程數(shù)量,

  • serverNumIOThreads (Netty threads, defaults to 2xCPU threads)
  • numAddWorkerThreads (defaults to 1)
  • numReadWorkerThreads (defaults to 8)
  • numLongPollWorkerThreads (defaults to 0 表示Long Poll讀入消息后就提交給讀線程)
  • numHighPriorityWorkerThreads (defaults to 8)
  • numJournalCallbackThreads (defaults to 1)

在處理Journal和DbLedger的時(shí)候,線程管理的整體結(jié)構(gòu)荧库,如下圖堰塌,

20220114-BookKeeper 工作原理-2022-01-17-10-21-44.png
20220114-BookKeeper 工作原理-2022-01-17-10-21-57.png

BookKeeper的存儲(chǔ)是分開(kāi)管理的,Journal和DbLedger存儲(chǔ)在不同的地方分衫,同時(shí)场刑,可以配置多個(gè)Journal和多個(gè)DbLedger(參數(shù)journalDirectories、ledgerDirectories)蚪战,最大可能利用存儲(chǔ)牵现。對(duì)每個(gè)Journal,專(zhuān)有的Journal線程組實(shí)例處理邀桑;對(duì)每個(gè)DbLedger瞎疼,也有專(zhuān)有的SingleDirectoryDbLedgerStorage線程組實(shí)例來(lái)處理,包括Write Cache概漱、Read Cache, Ledger Entry Log File丑慎、RockDB index file等。

例如瓤摧,對(duì)于一個(gè)讀請(qǐng)求竿裂,Netty根據(jù)Ledger id轉(zhuǎn)發(fā)到對(duì)應(yīng)的DbLedger實(shí)例進(jìn)行處理,如下照弥,

20220114-BookKeeper 工作原理-2022-01-17-10-22-15.png

對(duì)于一個(gè)寫(xiě)請(qǐng)求腻异,Netty根據(jù)Ledger id轉(zhuǎn)發(fā)到對(duì)應(yīng)的Journal和DbLedger實(shí)例進(jìn)行處理,如下这揣,

20220114-BookKeeper 工作原理-2022-01-17-10-22-29.png

BookKeeper寫(xiě)入請(qǐng)求分析

BookKeeper處理寫(xiě)入Entry請(qǐng)求的整體流程如下悔常,

20220114-BookKeeper 工作原理-2022-01-17-10-22-46.png

寫(xiě)入線程

首先,Netty線程收到寫(xiě)入請(qǐng)求给赞,解析机打,并把請(qǐng)求轉(zhuǎn)給寫(xiě)入線程。寫(xiě)入線程通常只有1個(gè)片迅,因?yàn)橐瓿傻氖虑楹苌俨醒?xiě)入線程先把Entry寫(xiě)入Write Cache,然后再發(fā)起一個(gè)寫(xiě)入Journal的請(qǐng)求進(jìn)入Entry Log內(nèi)存隊(duì)列柑蛇,就完成了所有工作芥挣。整個(gè)過(guò)程都是內(nèi)存操作,消耗很少耻台,因此不需要很多線程空免。

Journal線程

Journal線程在Entry Log內(nèi)存隊(duì)列的另一端等待,當(dāng)有寫(xiě)入線程寫(xiě)入消息時(shí)盆耽,就把消息中的Entry寫(xiě)入到磁盤(pán)蹋砚。但Journal的寫(xiě)入并不是同步寫(xiě)(fsync)扼菠,因此只保證寫(xiě)入到了系統(tǒng)緩存中,Journal線程本身并不發(fā)起同步寫(xiě)的系統(tǒng)請(qǐng)求都弹。同時(shí)娇豫,Journal線程周期性的發(fā)起強(qiáng)制寫(xiě)入請(qǐng)求,并將請(qǐng)求寫(xiě)入Force Write內(nèi)存隊(duì)列畅厢。強(qiáng)制寫(xiě)入請(qǐng)求的發(fā)起時(shí)機(jī)有以下幾處,

  • 達(dá)到預(yù)設(shè)的最大等待時(shí)間(配置journalMaxGroupWaitMSec氮昧,默認(rèn)2 ms)
  • 達(dá)到累積寫(xiě)入字節(jié)大小上限(配置journalBufferedWritesThreshold框杜,默認(rèn)512Kb)
  • 達(dá)到累積寫(xiě)入Entry條數(shù)上限(配置journalBufferedEntriesThreshold,默認(rèn)0袖肥,不啟用)
  • 當(dāng)Entry Log內(nèi)存隊(duì)列消費(fèi)完咪辱,從不空到空(配置journalFlushWhenQueueEmpty,默認(rèn)false)

也就是說(shuō)椎组,默認(rèn)啟用前2個(gè)選項(xiàng)油狂。

注意:Journal線程只有1個(gè),不會(huì)出現(xiàn)多個(gè)線程同時(shí)寫(xiě)一個(gè)Entry Log文件的情況寸癌。

強(qiáng)制寫(xiě)線程

Force Write線程等待Force Write內(nèi)存隊(duì)列的消息专筷,收到請(qǐng)求后,就發(fā)起fsync系統(tǒng)調(diào)用蒸苇,強(qiáng)制將數(shù)據(jù)寫(xiě)入磁盤(pán)Journal文件磷蛹。當(dāng)數(shù)據(jù)持久化完成之后,調(diào)用Journal Callback線程溪烤。

注意:處理強(qiáng)制寫(xiě)請(qǐng)求的時(shí)候味咳,可能有新的Entry寫(xiě)入,實(shí)際刷盤(pán)的時(shí)候檬嘀,刷入的Entry可能比發(fā)起請(qǐng)求時(shí)的Entry要多槽驶。

Journal Callback線程

該線程是回調(diào)線程,發(fā)送響應(yīng)給客戶端鸳兽。寫(xiě)入Journal的步驟到此結(jié)束掂铐。

DbStorage線程

DbStorage線程主要處理,當(dāng)Write Cache寫(xiě)滿之后贸铜,將數(shù)據(jù)寫(xiě)入到Ledger中堡纬,一個(gè)DbLedgerStorage對(duì)應(yīng)一個(gè)DbStorage線程。DbStorage線程是一個(gè)很復(fù)雜的線程蒿秦,不僅要負(fù)責(zé)管理Write Cache烤镐,還需要負(fù)責(zé)Entry存儲(chǔ)寫(xiě)入。

Write Cache在內(nèi)存中有兩份棍鳖,但同時(shí)只有一個(gè)Write Cache是活躍的(Active Write Cache)炮叶,用于實(shí)時(shí)任務(wù)的寫(xiě)入碗旅;另一個(gè)Write Cache是寫(xiě)入存儲(chǔ)時(shí)候用的(Flushed Write Cache)。當(dāng)寫(xiě)入存儲(chǔ)完成的時(shí)候镜悉,就把Flushed Write Cache清空祟辟。當(dāng)寫(xiě)入線程發(fā)現(xiàn)Active Write Cache已滿的時(shí)候,就觸發(fā)DbStorage線程進(jìn)行寫(xiě)入存儲(chǔ)侣肄。如果當(dāng)時(shí)Flushed Write Cache是已經(jīng)清空的旧困,說(shuō)明之前的寫(xiě)入任務(wù)已經(jīng)完成,DbStorage線程交換兩個(gè)Write Cache稼锅,將空的Write Cache變?yōu)锳ctive Write Cache吼具,供寫(xiě)入線程使用;然后矩距,開(kāi)始執(zhí)行寫(xiě)入任務(wù)拗盒,將Flushed Write Cache寫(xiě)入存儲(chǔ)。

這是理想情況锥债,如果Active Write Cache已滿的時(shí)候陡蝇,F(xiàn)lushed Write Cache尚未清空,說(shuō)明之前的寫(xiě)入任務(wù)還沒(méi)有完成哮肚。此時(shí)登夫,不能交換2個(gè)Write Cache,寫(xiě)入線程會(huì)阻擋寫(xiě)入請(qǐng)求一小段時(shí)間绽左,等待寫(xiě)入任務(wù)完成悼嫉。這個(gè)時(shí)間參數(shù)是dbStorage_maxThrottleTimeMs,默認(rèn)10秒拼窥。直到Flushed Write Cache全部寫(xiě)入完成戏蔑,交換2個(gè)Write Cache,寫(xiě)入線程就被釋放鲁纠。

默認(rèn)情況下总棵,Write Cache的大小設(shè)置為可用直接內(nèi)存的25%(應(yīng)該就是機(jī)器內(nèi)存的25%),也可以通過(guò)參數(shù)dbStorage_writeCacheMaxSizeMb來(lái)設(shè)置改含。因?yàn)閃rite Cache有2份情龄,所以實(shí)際的Write Cache大小會(huì)翻倍。假定Write Cache設(shè)置為250MB捍壤,有2個(gè)Ledger目錄骤视,每個(gè)Ledger有2個(gè)Write Cache,總共就有4份Write Cache鹃觉,共計(jì)消耗內(nèi)存1GB专酗。

當(dāng)DbLedgerStorage存儲(chǔ)寫(xiě)入時(shí),會(huì)先按Ledger id和Entry id對(duì)所有Entry進(jìn)行排序盗扇,然后將Entry寫(xiě)入Entry Log文件祷肯,并將文件的偏移量寫(xiě)入Entry索引沉填,即RocksDB。

DbLedgerStorage存儲(chǔ)寫(xiě)入佑笋,不僅可以由DbStorage線程完成翼闹,也可以由Sync線程在產(chǎn)生檢查點(diǎn)時(shí)完成。

注意蒋纬,Entry Log文件中猎荠,每次寫(xiě)入的Entry是來(lái)自于多個(gè)Ledger的,同一個(gè)存儲(chǔ)中有多個(gè)Ledger的數(shù)據(jù)混雜在一起颠锉。經(jīng)過(guò)排序法牲,同一個(gè)Ledger的Entry會(huì)聚合在一起。在讀取的時(shí)候琼掠,當(dāng)前Entry的前后是同一個(gè)Ledger的概率高。如圖停撞,

20220114-BookKeeper 工作原理-2022-01-17-10-24-29.png

Sync線程

Sync線程是一個(gè)守護(hù)線程瓷蛙,不在Journal線程池和DbLedgerStorage線程池中,主要負(fù)責(zé)定期生成檢查點(diǎn)(checkpoint)戈毒。檢查點(diǎn)要完成以下任務(wù)艰猬,

  1. 將Ledger數(shù)據(jù)刷入存儲(chǔ);
  2. 標(biāo)記刷入磁盤(pán)的Journal位置(日志標(biāo)記)埋市,并持久化冠桃,表示這個(gè)位置之前的Entry都已經(jīng)安全的寫(xiě)入存儲(chǔ)了。這個(gè)過(guò)程是通過(guò)寫(xiě)磁盤(pán)上一個(gè)單獨(dú)的日志標(biāo)記文件來(lái)完成的道宅;
  3. 清理不在需要的舊Journal文件食听;

寫(xiě)入瓶頸

通常情況下,BookKeeper的瓶頸都是磁盤(pán)IO造成的污茵,但Journal IO瓶頸和DbLedgerStorage IO瓶頸的現(xiàn)象是不一樣的樱报。

如果是Journal的瓶頸,會(huì)發(fā)現(xiàn)寫(xiě)入線程很平穩(wěn)的拒絕某些請(qǐng)求泞当,同時(shí)迹蛤,如果有火焰圖工具的話,可以看到寫(xiě)入線程非常繁忙襟士。如果是DbLedgerStorage的瓶頸盗飒,會(huì)發(fā)現(xiàn)寫(xiě)入線程拒絕所有寫(xiě)入請(qǐng)求,等待10秒(默認(rèn))后就開(kāi)始正常接收請(qǐng)求陋桂,然后發(fā)生擁堵逆趣,又開(kāi)始拒絕請(qǐng)求。

當(dāng)然章喉,也可能是CPU瓶頸汗贫,不太常見(jiàn)身坐,一般情況都是IO是瓶頸。即使發(fā)生了落包,也不難發(fā)現(xiàn)部蛇,CPU占用率很高,直接增加CPU資源處理Netty請(qǐng)求即可咐蝇。

BookKeeper讀取請(qǐng)求分析

BookKeeper的讀取請(qǐng)求涯鲁,主要是由DbLedgerStorage的getEntry(long ledgerId, long entryId)方法完成。架構(gòu)圖如下有序,

20220114-BookKeeper 工作原理-2022-01-17-10-25-05.png

讀取的流程如下抹腿,

  1. 檢查Write Cache中是否有數(shù)據(jù),有則返回旭寿;
  2. 檢查Read Cache中是否有數(shù)據(jù)警绩,有則返回,無(wú)則說(shuō)明數(shù)據(jù)在磁盤(pán)上盅称;
  3. 從Entry索引(RocksDB)中獲取到Entry的位置(哪個(gè)文件的哪個(gè)偏移量)肩祥;
  4. 根據(jù)文件和偏移量,定位特定的Entry缩膝;
  5. 執(zhí)行預(yù)讀然旌荨;
  6. 將預(yù)讀取的Entry加載到Read Cache疾层;
  7. 返回當(dāng)前Entry将饺;

預(yù)讀取的假設(shè)是,讀取了當(dāng)前Entry痛黎,也很可能會(huì)讀取之后的Entry予弧,因此預(yù)先加載這些Entry到內(nèi)存,也因?yàn)閷?xiě)入的時(shí)候舅逸,相同Ledger的數(shù)據(jù)被排序放在了一起桌肴,因此,預(yù)讀取是磁盤(pán)的順序讀琉历,性能較好坠七。預(yù)讀取的邊界是,

  1. 達(dá)到單次預(yù)讀取數(shù)量上限旗笔,默認(rèn)1000彪置,Pulsar場(chǎng)景;
  2. 讀到當(dāng)前文件結(jié)束蝇恶;
  3. 讀到另一個(gè)Ledger的Entry拳魁;

Read Cache每個(gè)DbLedgerStorage上有1個(gè),默認(rèn)是可用直接內(nèi)存的25%撮弧。

關(guān)于讀取的一些其他問(wèn)題

Broker粘滯讀取潘懊,也就是說(shuō)姚糊,Broker如果在某個(gè)Bookie上讀取到了數(shù)據(jù),那么下次統(tǒng)一客戶端的讀取請(qǐng)求還是發(fā)送到同一個(gè)Bookie授舟。同樣是基于鄰近讀取的假設(shè)救恨。如果沒(méi)有粘滯的話,可能每個(gè)Bookie上都需要預(yù)讀取加載相同的數(shù)據(jù)释树。這也說(shuō)明肠槽,Broker在讀取Bookie數(shù)據(jù)的時(shí)候并不是對(duì)等對(duì)待每個(gè)Bookie的。Broker粘滯讀取需要由 Broker 來(lái)完成實(shí)現(xiàn)奢啥。

Read Cache有多個(gè)分段(Segment)秸仙,每個(gè)Segment的數(shù)據(jù)結(jié)構(gòu)是一個(gè)環(huán)形隊(duì)列(Ring Buffer)。內(nèi)存預(yù)先分配桩盲,新的Entry覆蓋舊的Entry寂纪,每個(gè)Cache有索引指向?qū)?yīng)位置,以方便查找赌结。

Read Cache緩存抖動(dòng)弊攘,主要出現(xiàn)在Read Cache大小不足的時(shí)候。假設(shè)Read Cache可以容納2000個(gè)Entry姑曙,讀請(qǐng)求先讀取Ledger A,預(yù)讀取1000個(gè)Entry在Cache中迈倍;又讀取Ledger B伤靠,預(yù)讀取1000個(gè)Entry在Cache中,此時(shí)Read Cache已滿啼染;再讀取Ledger C宴合,預(yù)讀取1000個(gè)Entry在Cache中,覆蓋掉Ledger A的1000個(gè)迹鹅;此時(shí)A又來(lái)讀取下一個(gè)Entry卦洽,緩存無(wú)法命中,繼續(xù)預(yù)讀刃迸铩阀蒂;如果之后,A弟蚀、B蚤霞、C依次讀取,那么一次Cache也無(wú)法命中义钉,性能急劇下降昧绣。造成這樣現(xiàn)象的原因是Read Cache大小不足,所有的緩存類(lèi)應(yīng)用都有類(lèi)似現(xiàn)象捶闸。解決方案有夜畴,增大Read Cache拖刃,或者降低預(yù)讀取的上限(如改為預(yù)讀取500條即可)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贪绘,一起剝皮案震驚了整個(gè)濱河市兑牡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兔簇,老刑警劉巖发绢,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異垄琐,居然都是意外死亡边酒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)狸窘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)墩朦,“玉大人,你說(shuō)我怎么就攤上這事翻擒∶セ粒” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵陋气,是天一觀的道長(zhǎng)劳吠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)巩趁,這世上最難降的妖魔是什么痒玩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮议慰,結(jié)果婚禮上蠢古,老公的妹妹穿的比我還像新娘。我一直安慰自己别凹,他們只是感情好草讶,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著炉菲,像睡著了一般堕战。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颁督,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天践啄,我揣著相機(jī)與錄音,去河邊找鬼沉御。 笑死屿讽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伐谈,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼烂完,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了诵棵?” 一聲冷哼從身側(cè)響起抠蚣,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎履澳,沒(méi)想到半個(gè)月后嘶窄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡距贷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年柄冲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忠蝗。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡现横,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阁最,到底是詐尸還是另有隱情戒祠,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布速种,位于F島的核電站姜盈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏配阵。R本人自食惡果不足惜贩据,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闸餐。 院中可真熱鬧,春花似錦矾芙、人聲如沸舍沙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拂铡。三九已至,卻和暖如春葱绒,著一層夾襖步出監(jiān)牢的瞬間感帅,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工地淀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留失球,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像实苞,于是被迫代替她去往敵國(guó)和親豺撑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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