4-Zookeeper 原理介紹

Zookeeper 原理

Zookeeper的基本概念

角色

Zookeeper中的角色主要有以下三類,如下表所示:

image.png

系統(tǒng)模型如圖所示:

image.png

設計目的

  1. 最終一致性:client不論連接到哪個Server泽示,展示給它都是同一個視圖往枷,這是zookeeper最重要的性能更耻。

  2. 可靠性:具有簡單自沧、健壯纵柿、良好的性能序矩,如果消息m被到一臺服務器接受鸯绿,那么它將被所有的服務器接受。

  3. 實時性:Zookeeper保證客戶端將在一個時間間隔范圍內(nèi)獲得服務器的更新信息簸淀,或者服務器失效的信息瓶蝴。但由于網(wǎng)絡延時等原因,Zookeeper不能保證兩個客戶端能同時得到剛更新的數(shù)據(jù)租幕,如果需要最新數(shù)據(jù)舷手,應該在讀數(shù)據(jù)之前調(diào)用sync()接口。

  4. 等待無關(wait-free):慢的或者失效的client不得干預快速的client的請求令蛉,使得每個client都能有效的等待聚霜。

  5. 原子性:更新只能成功或者失敗狡恬,沒有中間狀態(tài)。

  6. 順序性:包括全局有序和偏序兩種:全局有序是指如果在一臺服務器上消息a在消息b前發(fā)布蝎宇,則在所有Server上消息a都將在消息b前被發(fā)布弟劲;偏序是指如果一個消息b在消息a后被同一個發(fā)送者發(fā)布,a必將排在b前面姥芥。

ZooKeeper的工作原理

Zookeeper的核心是原子廣播兔乞,這個機制保證了各個Server之間的同步。實現(xiàn)這個機制的協(xié)議叫做Zab協(xié)議凉唐。Zab協(xié)議有兩種模式庸追,它們分別是恢復模式(選主)和廣播模式(同步)。當服務啟動或者在領導者崩潰后台囱,Zab就進入了恢復模式淡溯,當領導者被選舉出來,且大多數(shù)Server完成了和leader的狀態(tài)同步以后簿训,恢復模式就結(jié)束了咱娶。狀態(tài)同步保證了leader和Server具有相同的系統(tǒng)狀態(tài)。

為了保證事務的順序一致性强品,zookeeper采用了遞增的事務id號(zxid)來標識事務膘侮。所有的提議(proposal)都在被提出的時候加上了zxid。實現(xiàn)中zxid是一個64位的數(shù)字的榛,它高32位是epoch用來標識leader關系是否改變琼了,每次一個leader被選出來,它都會有一個新的epoch夫晌,標識當前屬于那個leader的統(tǒng)治時期雕薪。低32位用于遞增計數(shù)。

每個Server在工作過程中有三種狀態(tài):

  • LOOKING:當前Server不知道leader是誰慷丽,正在搜尋

  • LEADING:當前Server即為選舉出來的leader

  • FOLLOWING:leader已經(jīng)選舉出來蹦哼,當前Server與之同步

選主流程

當leader崩潰或者leader失去大多數(shù)的follower,這時候zk進入恢復模式要糊,恢復模式需要重新選舉出一個新的leader纲熏,讓所有的Server都恢復到一個正確的狀態(tài)。Zk的選舉算法有兩種:一種是基于basic paxos實現(xiàn)的锄俄,另外一種是基于fast paxos算法實現(xiàn)的局劲。系統(tǒng)默認的選舉算法為fast paxos。先介紹basic paxos流程:

  1. 選舉線程由當前Server發(fā)起選舉的線程擔任奶赠,其主要功能是對投票結(jié)果進行統(tǒng)計鱼填,并選出推薦的Server;

  2. 選舉線程首先向所有Server發(fā)起一次詢問(包括自己)毅戈;

  3. 選舉線程收到回復后苹丸,驗證是否是自己發(fā)起的詢問 (驗證zxid是否一致)愤惰,然后獲取對方的id (myid),并存儲到當前詢問對象列表中赘理,最后獲取對方提議的leader相關信息 (id,zxid)宦言,并將這些信息存儲到當次選舉的投票記錄表中;

  4. 收到所有Server回復以后商模,就計算出zxid最大的那個Server奠旺,并將這個Server相關信息設置成下一次要投票的Server;

  5. 線程將當前zxid最大的Server設置為當前Server要推薦的Leader施流,如果此時獲勝的Server獲得n/2 + 1的Server票數(shù)响疚,設置當前推薦的leader為獲勝的Server,將根據(jù)獲勝的Server相關信息設置自己的狀態(tài)瞪醋,否則忿晕,繼續(xù)這個過程,直到leader被選舉出來银受。

通過流程分析我們可以得出:要使Leader獲得多數(shù)Server的支持杏糙,則Server總數(shù)必須是奇數(shù)2n+1,且存活的Server的數(shù)目不得少于n+1蚓土。

每個Server啟動后都會重復以上流程。在恢復模式下赖淤,如果是剛從崩潰狀態(tài)恢復的或者剛啟動的server還會從磁盤快照中恢復數(shù)據(jù)和會話信息蜀漆,zk會記錄事務日志并定期進行快照,方便在恢復時進行狀態(tài)恢復咱旱。選主的具體流程圖如下所示:

image.png

fast paxos流程是在選舉過程中确丢,某Server首先向所有Server提議自己要成為leader,當其它Server收到提議以后吐限,解決epoch和zxid的沖突鲜侥,并接受對方的提議,然后向?qū)Ψ桨l(fā)送接受提議完成的消息诸典,重復這個流程描函,最后一定能選舉出Leader。其流程圖如下所示:

image.png

同步流程

選完leader以后狐粱,zk就進入狀態(tài)同步過程舀寓。

  1. leader等待server連接;

  2. Follower連接leader肌蜻,將最大的zxid發(fā)送給leader互墓;

  3. Leader根據(jù)follower的zxid確定同步點;

  4. 完成同步后通知follower 已經(jīng)成為uptodate狀態(tài)蒋搜;

  5. Follower收到uptodate消息后篡撵,又可以重新接受client的請求進行服務了判莉。

流程圖如下所示:

image.png

工作流程

Leader工作流程

Leader主要有三個功能:

  1. 恢復數(shù)據(jù);

  2. 維持與Learner的心跳育谬,接收Learner請求并判斷Learner的請求消息類型券盅;

  3. Learner的消息類型主要有PING消息、REQUEST消息斑司、ACK消息渗饮、REVALIDATE消息,根據(jù)不同的消息類型宿刮,進行不同的處理互站。

PING消息是指Learner的心跳信息;REQUEST消息是Follower發(fā)送的提議信息僵缺,包括寫請求及同步請求胡桃;ACK消息是Follower的對提議的回復,超過半數(shù)的Follower通過磕潮,則commit該提議翠胰;REVALIDATE消息是用來延長SESSION有效時間。

Leader的工作流程簡圖如下所示自脯,在實際實現(xiàn)中之景,流程要比下圖復雜得多,啟動了三個線程來實現(xiàn)功能:

image.png
Follower工作流程

Follower主要有四個功能:

  1. 向Leader發(fā)送請求(PING消息膏潮、REQUEST消息锻狗、ACK消息、REVALIDATE消息)焕参;

  2. 接收Leader消息并進行處理轻纪;

  3. 接收Client的請求,如果為寫請求叠纷,發(fā)送給Leader進行投票刻帚;

  4. 返回Client結(jié)果。

Follower的消息循環(huán)處理如下幾種來自Leader的消息:

  1. PING消息: 心跳消息涩嚣;

  2. PROPOSAL消息:Leader發(fā)起的提案崇众,要求Follower投票;

  3. COMMIT消息:服務器端最新一次提案的信息缓艳;

  4. UPTODATE消息:表明同步完成校摩;

  5. REVALIDATE消息:根據(jù)Leader的REVALIDATE結(jié)果,關閉待revalidate的session還是允許其接受消息阶淘;

  6. SYNC消息:返回SYNC結(jié)果到客戶端衙吩,這個消息最初由客戶端發(fā)起,用來強制得到最新的更新溪窒。

Follower的工作流程簡圖如下所示坤塞,在實際實現(xiàn)中冯勉,F(xiàn)ollower是通過5個線程來實現(xiàn)功能的。

image.png

對于observer的流程不再敘述摹芙,observer流程和Follower的唯一不同的地方就是observer不會參加leader發(fā)起的投票灼狰。

ZooKeeper在大型分布式系統(tǒng)中的應用

前面已經(jīng)介紹了ZooKeeper的典型應用場景。本節(jié)將以常見的大數(shù)據(jù)產(chǎn)品Hadoop和HBase為例來介紹ZooKeeper在其中的應用浮禾,幫助大家更好地理解ZooKeeper的分布式應用場景交胚。

ZooKeeper在Hadoop中的應用

在Hadoop中,ZooKeeper主要用于實現(xiàn)HA (Hive Availability)盈电,包括HDFS的NamaNode和YARN的ResourceManager的HA蝴簇。同時,在YARN中匆帚,ZooKeepr還用來存儲應用的運行狀態(tài)熬词。HDFS的NamaNode和YARN的ResourceManager利用ZooKeepr實現(xiàn)HA的原理是一樣的,所以本節(jié)以YARN為例來介紹吸重。

image.png

從上圖可以看出互拾,YARN主要由ResourceManager(RM)、NodeManager(NM)嚎幸、ApplicationMaster(AM)和Container四部分組成颜矿。其中最核心的就是ResourceManager。

ResourceManager負責集群中所有資源的統(tǒng)一管理和分配嫉晶,同時接收來自各個節(jié)點(NodeManager)的資源匯報信息或衡,并把這些信息按照一定的策略分配給各個應用程序(Application Manager),其內(nèi)部維護了各個應用程序的ApplicationMaster信息车遂、NodeManager信息以及資源使用信息等。

為了實現(xiàn)HA斯辰,必須有多個ResourceManager并存(一般就兩個)舶担,并且只有一個ResourceManager處于Active狀態(tài),其他的則處于Standby狀態(tài)彬呻,當Active節(jié)點無法正常工作(如機器宕機或重啟)時衣陶,處于Standby的就會通過競爭選舉產(chǎn)生新的Active節(jié)點。

主備切換

下面我們就來看看YARN是如何實現(xiàn)多個ResourceManager之間的主備切換的闸氮。

1.創(chuàng)建鎖節(jié)點

在ZooKeeper上會有一個/yarn-leader-election/appcluster-yarn的鎖節(jié)點剪况,所有的ResourceManager在啟動的時候,都會去競爭寫一個Lock子節(jié)點:/yarn-leader-election/appcluster-yarn/ActiveBreadCrumb蒲跨,該節(jié)點是臨時節(jié)點。ZooKeepr能夠為我們保證最終只有一個ResourceManager能夠創(chuàng)建成功。創(chuàng)建成功的那個ResourceManager就切換為Active狀態(tài)去扣,沒有成功的那些ResourceManager則切換為Standby狀態(tài)。

[zk: localhost:2181(CONNECTED) 16] get /yarn-leader-election/appcluster-yarn/ActiveBreadCrumb

appcluster-yarnrm2
cZxid = 0x1b00133dc0
ctime = Tue Jan 03 15:44:42 CST 2017
mZxid = 0x1f00000540
mtime = Sat Jan 07 00:50:20 CST 2017
pZxid = 0x1b00133dc0
cversion = 0
dataVersion = 28
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 22
numChildren = 0

可以看到此時集群中ResourceManager2為Active堪唐。

2.注冊Watcher監(jiān)聽

所有Standby狀態(tài)的ResourceManager都會向/yarn-leader-election/appcluster-yarn/ActiveBreadCrumb節(jié)點注冊一個節(jié)點變更的Watcher監(jiān)聽,利用臨時節(jié)點的特性翎蹈,能夠快速感知到Active狀態(tài)的ResourceManager的運行情況淮菠。

3.主備切換

當Active狀態(tài)的ResourceManager出現(xiàn)諸如宕機或重啟的異常情況時,其在ZooKeeper上連接的客戶端會話就會失效荤堪,因此/yarn-leader-election/appcluster-yarn/ActiveBreadCrumb節(jié)點就會被刪除合陵。此時其余各個Standby狀態(tài)的ResourceManager就都會接收到來自ZooKeeper服務端的Watcher事件通知,然后會重復進行步驟1的操作澄阳。

以上就是利用ZooKeeper來實現(xiàn)ResourceManager的主備切換的過程拥知,實現(xiàn)了ResourceManager的HA。

HDFS中NameNode的HA的實現(xiàn)原理跟YARN中ResourceManager的HA的實現(xiàn)原理相同寇荧。其鎖節(jié)點為/hadoop-ha/mycluster/ActiveBreadCrumb举庶。

ResourceManager狀態(tài)存儲

在 ResourceManager 中,RMStateStore 能夠存儲一些 RM 的內(nèi)部狀態(tài)信息揩抡,包括 Application 以及它們的 Attempts 信息户侥、Delegation Token 及 Version Information 等。需要注意的是峦嗤,RMStateStore 中的絕大多數(shù)狀態(tài)信息都是不需要持久化存儲的蕊唐,因為很容易從上下文信息中將其重構(gòu)出來,如資源的使用情況烁设。在存儲的設計方案中替梨,提供了三種可能的實現(xiàn),分別如下装黑。

  • 基于內(nèi)存實現(xiàn)副瀑,一般是用于日常開發(fā)測試。
  • 基于文件系統(tǒng)的實現(xiàn)恋谭,如HDFS糠睡。
  • 基于ZooKeeper實現(xiàn)。

由于這些狀態(tài)信息的數(shù)據(jù)量都不是很大疚颊,因此Hadoop官方建議基于ZooKeeper來實現(xiàn)狀態(tài)信息的存儲狈孔。在ZooKeepr上,ResourceManager 的狀態(tài)信息都被存儲在/rmstore這個根節(jié)點下面材义。

[zk: localhost:2181(CONNECTED) 28] ls /rmstore/ZKRMStateRoot
[RMAppRoot, AMRMTokenSecretManagerRoot, EpochNode, RMDTSecretManagerRoot, RMVersionNode]

RMAppRoot 節(jié)點下存儲的是與各個 Application 相關的信息均抽,RMDTSecretManagerRoot 存儲的是與安全相關的 Token 等信息。每個 Active 狀態(tài)的 ResourceManager 在初始化階段都會從 ZooKeeper 上讀取到這些狀態(tài)信息其掂,并根據(jù)這些狀態(tài)信息繼續(xù)進行相應的處理油挥。

小結(jié):

ZooKeepr在Hadoop中的應用主要有:

  1. HDFS中NameNode的HA和YARN中ResourceManager的HA。
  1. 存儲RMStateStore狀態(tài)信息

ZooKeeper在HBase中的應用

HBase主要用ZooKeeper來實現(xiàn)HMaster選舉與主備切換、系統(tǒng)容錯喘漏、RootRegion管理护蝶、Region狀態(tài)管理和分布式SplitWAL任務管理等。

HMaster選舉與主備切換

HMaster選舉與主備切換的原理和HDFS中NameNode及YARN中ResourceManager的HA原理相同翩迈。

系統(tǒng)容錯

當HBase啟動時持灰,每個RegionServer都會到ZooKeeper的/hbase/rs節(jié)點下創(chuàng)建一個信息節(jié)點(下文中,我們稱該節(jié)點為"rs狀態(tài)節(jié)點")负饲,例如/hbase/rs/[Hostname]堤魁,同時,HMaster會對這個節(jié)點注冊監(jiān)聽返十。當某個 RegionServer 掛掉的時候妥泉,ZooKeeper會因為在一段時間內(nèi)無法接受其心跳(即 Session 失效),而刪除掉該 RegionServer 服務器對應的 rs 狀態(tài)節(jié)點洞坑。與此同時盲链,HMaster 則會接收到 ZooKeeper 的 NodeDelete 通知,從而感知到某個節(jié)點斷開迟杂,并立即開始容錯工作刽沾。

HBase為什么不直接讓HMaster來負責RegionServer的監(jiān)控呢?如果HMaster直接通過心跳機制等來管理RegionServer的狀態(tài)排拷,隨著集群越來越大侧漓,HMaster的管理負擔會越來越重,另外它自身也有掛掉的可能监氢,因此數(shù)據(jù)還需要持久化布蔗。在這種情況下,ZooKeeper就成了理想的選擇浪腐。

RootRegion管理

對應HBase集群來說纵揍,數(shù)據(jù)存儲的位置信息是記錄在元數(shù)據(jù)region,也就是RootRegion上的议街。每次客戶端發(fā)起新的請求骡男,需要知道數(shù)據(jù)的位置,就會去查詢RootRegion傍睹,而RootRegion自身位置則是記錄在ZooKeeper上的(默認情況下,是記錄在ZooKeeper的/hbase/meta-region-server節(jié)點中)犹菱。當RootRegion發(fā)生變化拾稳,比如Region的手工移動、重新負載均衡或RootRegion所在服務器發(fā)生了故障等是腊脱,就能夠通過ZooKeeper來感知到這一變化并做出一系列相應的容災措施访得,從而保證客戶端總是能夠拿到正確的RootRegion信息。

Region管理

HBase里的Region會經(jīng)常發(fā)生變更,這些變更的原因來自于系統(tǒng)故障悍抑、負載均衡鳄炉、配置修改、Region分裂與合并等搜骡。一旦Region發(fā)生移動拂盯,它就會經(jīng)歷下線(offline)和重新上線(online)的過程。

在下線期間數(shù)據(jù)是不能被訪問的记靡,并且Region的這個狀態(tài)變化必須讓全局知曉谈竿,否則可能會出現(xiàn)事務性的異常。對于大的HBase集群來說摸吠,Region的數(shù)量可能會多達十萬級別空凸,甚至更多,這樣規(guī)模的Region狀態(tài)管理交給ZooKeeper來做也是一個很好的選擇寸痢。

分布式SplitWAL任務管理

當某臺RegionServer服務器掛掉時呀洲,由于總有一部分新寫入的數(shù)據(jù)還沒有持久化到HFile中,因此在遷移該RegionServer的服務時啼止,一個重要的工作就是從WAL中恢復這部分還在內(nèi)存中的數(shù)據(jù)道逗,而這部分工作最關鍵的一步就是SplitWAL,即HMaster需要遍歷該RegionServer服務器的WAL族壳,并按Region切分成小塊移動到新的地址下憔辫,并進行日志的回放(replay)。

由于單個RegionServer的日志量相對龐大(可能有上千個Region仿荆,上GB的日志)贰您,而用戶又往往希望系統(tǒng)能夠快速完成日志的恢復工作。因此一個可行的方案是將這個處理WAL的任務分給多臺RegionServer服務器來共同處理拢操,而這就又需要一個持久化組件來輔助HMaster完成任務的分配锦亦。當前的做法是,HMaster會在ZooKeeper上創(chuàng)建一個SplitWAL節(jié)點(默認情況下令境,是/hbase/SplitWAL節(jié)點)杠园,將"哪個RegionServer處理哪個Region"這樣的信息以列表的形式存放到該節(jié)點上,然后由各個RegionServer服務器自行到該節(jié)點上去領取任務并在任務執(zhí)行成功或失敗后再更新該節(jié)點的信息舔庶,以通知HMaster繼續(xù)進行后面的步驟抛蚁。ZooKeeper在這里擔負起了分布式集群中相互通知和信息持久化的角色。

小結(jié):

以上就是一些HBase中依賴ZooKeeper完成分布式協(xié)調(diào)功能的典型場景惕橙。但事實上瞧甩,HBase對ZooKeepr的依賴還不止這些,比如HMaster還依賴ZooKeeper來完成Table的enable/disable狀態(tài)記錄弥鹦,以及HBase中幾乎所有的元數(shù)據(jù)存儲都是放在ZooKeeper上的肚逸。

由于ZooKeeper出色的分布式協(xié)調(diào)能力及良好的通知機制,HBase在各版本的演進過程中越來越多地增加了ZooKeeper的應用場景,從趨勢上來看兩者的交集越來越多朦促。HBase中所有對ZooKeeper的操作都封裝在了org.apache.hadoop.hbase.zookeeper這個包中膝晾,感興趣的同學可以自行研究。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末务冕,一起剝皮案震驚了整個濱河市血当,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洒疚,老刑警劉巖歹颓,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異油湖,居然都是意外死亡巍扛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門乏德,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撤奸,“玉大人,你說我怎么就攤上這事喊括‰使希” “怎么了?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵郑什,是天一觀的道長府喳。 經(jīng)常有香客問我,道長蘑拯,這世上最難降的妖魔是什么钝满? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮申窘,結(jié)果婚禮上弯蚜,老公的妹妹穿的比我還像新娘。我一直安慰自己剃法,他們只是感情好碎捺,可當我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贷洲,像睡著了一般收厨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上优构,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天诵叁,我揣著相機與錄音,去河邊找鬼俩块。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的玉凯。 我是一名探鬼主播势腮,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼漫仆!你這毒婦竟也來了捎拯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤盲厌,失蹤者是張志新(化名)和其女友劉穎署照,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吗浩,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡建芙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了懂扼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禁荸。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖阀湿,靈堂內(nèi)的尸體忽然破棺而出赶熟,到底是詐尸還是另有隱情,我是刑警寧澤陷嘴,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布映砖,位于F島的核電站,受9級特大地震影響灾挨,放射性物質(zhì)發(fā)生泄漏邑退。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一涨醋、第九天 我趴在偏房一處隱蔽的房頂上張望瓜饥。 院中可真熱鬧,春花似錦浴骂、人聲如沸乓土。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽趣苏。三九已至,卻和暖如春梯轻,著一層夾襖步出監(jiān)牢的瞬間食磕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工喳挑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留彬伦,地道東北人滔悉。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像单绑,于是被迫代替她去往敵國和親回官。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,566評論 2 349

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