raft 系列解讀(2) 之 測試用例

raft 系列解讀(2) 之 測試用例

基于mit的6.824課程,github代碼地址:https://github.com/zhuanxuhit/distributed-system

case1:TestInitialElection

測試中3個server,然后啟動,驗(yàn)證在同一個任期(term)內(nèi)是否只有一個leader,并且在2 * RaftElectionTimeout后,由于心跳的存在,不會發(fā)生重選。

在代碼實(shí)現(xiàn)中,主要有以下幾點(diǎn):

  • 實(shí)現(xiàn)AppendEntiesRequestVote兩個rpc部分功能
  • 實(shí)現(xiàn)Make新建Raft

我們來看下其中主要的關(guān)鍵點(diǎn):

程序整體組織上是在Make中啟動了一個goroutine,是一個無限循環(huán),根據(jù)不同的狀態(tài)進(jìn)行不同的處理,結(jié)構(gòu)如下:

圖片

follower

先講第一個狀態(tài)follower的處理
所有的server重啟后第一個狀態(tài)都是follower,如果在election timeout時間內(nèi),既沒有收到leader的heartbeat,也沒有收到RequestVote請求,那么開啟選舉過程,此時狀態(tài)將轉(zhuǎn)換為candidate,代碼如下:

        rf.resetElectionTimeout()
        // 等待心跳,如果心跳未到,但是選舉超時了,則開始新一輪選舉
        select {
        case <-rf.heartbeatChan:
        case <-time.After(rf.randomizedElectionTimeout):
        // 開始重新選舉
            log.Println("election timeout:", rf.randomizedElectionTimeout)
            if rf.status != STATUS_FOLLOWER {
                // panic
                log.Fatal("status not right when in follower and after randomizedElectionTimeout:", rf.randomizedElectionTimeout)
            }
            rf.convertToCandidate()
        }

candidate

接著開始第二個狀態(tài)candidate的處理:

  • 第一步,新增本地任期和投票
  • 第二步,重置 election timer 并開始廣播
  • 第三步等待結(jié)果
    • 1)他自己贏得了選舉;
    • 2)收到AppendEntries得知另外一個服務(wù)器確立他為Leader,轉(zhuǎn)變?yōu)閒ollower
      1. 一個周期時間過去但是沒有任何人贏得選舉,開始新的選舉

結(jié)構(gòu)大致如下:


圖片

leader

如果此時贏得了選舉,則進(jìn)入第3個狀態(tài)leader的處理:目前l(fā)eader只實(shí)現(xiàn)了一個功能,周期性的發(fā)送心跳,功能非常簡單,此處不再貼代碼了。

rpc

剩下就是兩個rpc的發(fā)送和接收處理了,其中需要特別注意的點(diǎn)如下:

  • 所有rpc處理中:如果收到的請求或者響應(yīng)中,包含的term大于當(dāng)前的currentTerm,設(shè)置currentTerm=term,然后變?yōu)閒ollower
  • 所有rpc處理中:判斷任期是否小于currentTerm,小于的都丟棄

在完成第一個測試的過程中:AppendEnties只需要處理心跳請求即可。

最后給出代碼的地址:https://github.com/zhuanxuhit/distributed-system,tag是:lab3-raft-case1

case2:TestReElection

有3個server,選舉出來一個leader后,模擬leader故障,重新選舉出一個leader,然后再模擬older leader故障恢復(fù)重新加入,此時也只會有一個leader,再模擬3個2個都故障了,那理論上就不會有l(wèi)eader出現(xiàn)了,此時再逐個加入故障的server,都只會有一個leader

直接運(yùn)行測試

go test -v -run ReElection
  1. leader故障,新的leader選出來
  2. 老的leader加入,不影響只有一個leader
  3. 兩個server故障,不會有新的leader
  4. 恢復(fù)一個server,出現(xiàn)leader
  5. 再次恢復(fù)一個server,出現(xiàn)leader

先看第1個,出現(xiàn)的調(diào)試信息:

2016/10/10 18:44:46 follower: 0 election timeout: 1.287113937s
2016/10/10 18:44:46 now I begin to candidate,index: 0
2016/10/10 18:44:47 follower: 2 election timeout: 1.54916732s
2016/10/10 18:44:47 now I begin to candidate,index: 2

可以看到0開始選舉后,不知道為什么2沒有投票,去看代碼,發(fā)現(xiàn)問題是:

  • 當(dāng)發(fā)現(xiàn)遠(yuǎn)端term大于本地term后,直接轉(zhuǎn)換為follower,并更新當(dāng)前的currentTerm和voteFor

修改后即可通過測試,接著馬上又出現(xiàn)另一個問題:

2016/10/10 18:54:50 candidate: 0 'slog is not at least as up-to-date as receiver’s log

但是我們現(xiàn)在做的是沒有日志的,查看代碼發(fā)現(xiàn)問題是:

  • (args.LastLogIndex < rf.commitIndex || args.LastLogTerm < currentTerm),因?yàn)閏urrentTerm增加了,但是LastLogTerm是0,所以要考慮rf.commitIndex == 0表示還沒有日志,則沒必要檢查

修改完后,再次運(yùn)行case,這次是兩個server故障,不會有新的leader出問題了,選舉不出來,接著查原因:

在處理投票的時候,往heartbeatChan寫的時候阻塞了,rf.heartbeatChan = make(chan bool, 1)是有一個緩沖的channel,那為什么會阻塞呢,我們看下有幾個地方會寫,幾個地方會去讀

有兩個地方會去寫:

  1. AppendEnties中收到心跳會去寫,當(dāng)去寫的時候,說明是已經(jīng)有l(wèi)eader了,自己會轉(zhuǎn)變?yōu)閒ollower
  2. RequestVote中收到投票也會去寫

讀的地方也有兩個

  1. 在狀態(tài)follower中,去讀heartbeatChan,如果選舉超時內(nèi)沒收到心跳,則開始candidate
  2. 在candidate狀態(tài),去讀去讀heartbeatChan,表示已經(jīng)有新的leader產(chǎn)生了

于是就發(fā)現(xiàn)了問題:

  • 在實(shí)現(xiàn)leader任務(wù)的時候,沒有一個點(diǎn)去觸發(fā)退出心跳
  • 選舉失敗,應(yīng)該等待超時,然后重新開始新一輪選舉,而不是馬上開始新一輪選舉,這樣子造成彼此都不成功

修改代碼后,通過case2

case3:TestBasicAgree

這個case開始要做提交了,實(shí)現(xiàn)Start()函數(shù)了,這個case主要測試是:有5個server,沒提交前檢查沒有提交的log,然后提交后,測試該log是否已經(jīng)被每個server都存儲了。

在實(shí)現(xiàn)start中,其做的步驟是:

// 客戶端的一次日志請求操作觸發(fā)
// 1)Leader將該請求記錄到自己的日志之中;
// 2)Leader將請求的日志以并發(fā)的形式,發(fā)送AppendEntries RCPs給所有的服務(wù)器;
// 3)Leader等待獲取多數(shù)服務(wù)器的成功回應(yīng)之后(如果總共5臺,那么只要收到另外兩臺回應(yīng)),
// 將該請求的命令應(yīng)用到狀態(tài)機(jī)(也就是提交),更新自己的commitIndex 和 lastApplied值;
// 4)Leader在與Follower的下一個AppendEntries RPCs通訊中,
// 就會使用更新后的commitIndex,Follower使用該值更新自己的commitIndex;
// 5)Follower發(fā)現(xiàn)自己的 commitIndex > lastApplied
// 則將日志commitIndex的條目應(yīng)用到自己的狀態(tài)機(jī)(這里就是Follower提交條目的時機(jī))

實(shí)現(xiàn)的關(guān)鍵點(diǎn):在Start函數(shù)中,一旦判斷出當(dāng)前server是leader,馬上開啟一個goroutine,開始異步進(jìn)行agree工作,然后立即返回,代碼如下:

圖片

此處第4步和第5步需要在另外的地方完成,一個是heartbeat中,另一個是follower在處理AppendEntries過程中

還有就是在成為leader的時候,需要初始化nextIndex,matchIndex

圖片

而在發(fā)送heartbeat中,判斷l(xiāng)og的最大index ≥ nextIndex,如果大于,需要發(fā)送從nextIndex開始的log,在發(fā)送完后需要判斷成功與否,成功則更新nextIndex,matchIndex,失敗則減少nextIndex,并重試
圖片

還有最重要的一點(diǎn):為了通過測試,記住要在日志提交后,發(fā)送消息ApplyMsgapplymsg,這樣才能通過測試

好了到此為止,寫的代碼剛好通過第三個測試,繼續(xù)下一關(guān)的!

case4:TestFailAgree

測試的內(nèi)容是:有3個server,其中一個follower故障,發(fā)的命令只有2個能收到,當(dāng)恢復(fù)故障后,發(fā)的命令都能收到

出現(xiàn)的問題:由于每個command真正提交都是通過goroutine來執(zhí)行的,因此每個goroutine之間并發(fā)執(zhí)行,怎么保證前一個agree了,下一個才能agree成功呢?
現(xiàn)在出現(xiàn)的問題是:
map[3:103 5:104 1:101 2:102],亂序,即4還沒有提交了,5就提交成功了

現(xiàn)在的問題是:誰也不服誰,當(dāng)follower恢復(fù)后,大家都競選,但是沒有一個成功,查明原因后發(fā)現(xiàn)是因?yàn)闆]有處理一個概念:
>如果候選人的日志至少和大多數(shù)的服務(wù)器節(jié)點(diǎn)一樣新

這個一樣新通過:比較兩份日志中最后一條日志條目的索引值和任期號定義誰的日志比較新。如果兩份日志最后的條目的任期號不同,那么任期號大的日志更加新。如果兩份日志最后的條目任期號相同,那么日志比較長的那個就更加新。

進(jìn)行到這,發(fā)現(xiàn)已經(jīng)很難調(diào)試了,代碼太亂,邏輯混亂,于是準(zhǔn)備開始重構(gòu)

現(xiàn)有代碼的問題:

  • 臨界區(qū)的混亂,到底哪里加鎖,哪里不加
  • 各個goroutine之間交互的混亂
  • 代碼功能組織的問題

重構(gòu)的代碼最重要的一點(diǎn)是:抽象出了狀態(tài)機(jī),在里面去更新

case5:FailNoAgree

測試內(nèi)容是:5個server,3個follow故障,此時提交的命令將不會Committed,然后恢復(fù)3個follower,此時發(fā)送第3個命令,會忘記第2個沒有確認(rèn)的命令,此時第3個命令的index應(yīng)該還是2

現(xiàn)在出現(xiàn)的問題是:
follow的日志沒更新,但是leader的nextIndex確更新了!

2016/10/13 10:44:20 leader is 4
2016/10/13 10:44:22 server:0,currentTerm:3,role:candidate
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:22 server:1,currentTerm:3,role:candidate
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:22 server:2,currentTerm:3,role:candidate
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:22 server:3,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:22 server:4,currentTerm:2,role:leader
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[2 2 2 3 3]
matchIndex is:[1 1 1 2 0]

2016/10/13 10:44:22 恢復(fù)3個server
2016/10/13 10:44:25 LeaderId: 4 has big term: 5 than follower: 3 currentTerm: 4
2016/10/13 10:44:25 server 3 len(rf.log) 3 args.PrevLogIndex 1
2016/10/13 10:44:26 重新選舉后leader is 4
2016/10/13 10:44:26 server:0,currentTerm:5,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:26 server:1,currentTerm:5,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:26 server:2,currentTerm:5,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {2 10 1}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:26 server:3,currentTerm:5,role:follower
commitIndex:2,lastApplied:2
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[0 0 0 0 0]
matchIndex is:[0 0 0 0 0]

2016/10/13 10:44:26 server:4,currentTerm:5,role:leader
commitIndex:2,lastApplied:2
log is:[{0 <nil> 0} {2 10 1} {2 20 2}]
nextIndex is:[3 3 3 3 3]
matchIndex is:[2 2 2 2 0]

看重新選舉后,leader4:matchIndex is:[2 2 2 2 0],但是其他的follower確沒有收到新的日志,怎么回事呢?看代碼什么情況下回去更新matchIndex呢?

問題在于發(fā)送心跳的時候返回了reply=true了,確沒有去檢查日志是否是最新的

此處記住appendEntries如果返回true,則一定表示是日志一樣新了!

true if follower contained entry matching prevLogIndex and prevLogTerm

case6:ConcurrentStarts

這個case測試的是:
同時發(fā)送5個命令,然后測試5個命令能夠被順序的提交
測試中的修改是:


圖片

將紅色框中的內(nèi)容移動到了鎖里面,為了防止并發(fā)訪問的時候,index得到相同。

case7:Rejoin

測試重新加入直接通過了,之前的代碼就能實(shí)現(xiàn)
測試內(nèi)容是:3個server,leader故障,然后向故障的leader發(fā)送命令,同時向新選舉出來的leader發(fā)送命令,大致如下圖,最后能統(tǒng)一


圖片

case8:Backup

類似case7:不同在于此處有5個server,然后命令更多,測試也是網(wǎng)絡(luò)分區(qū)后出現(xiàn)多l(xiāng)eader,然后恢復(fù)網(wǎng)絡(luò)后,再重新同步數(shù)據(jù)
不用修改,直接通過

case9:Count

case9主要是性能測試,測試rpc的次數(shù)不能太多

case10-12:Persist1-3

持久化的邏輯一直沒有加上,此處加上的

先看需要持久化哪些數(shù)據(jù),然后持久化的時機(jī)是什么時候?

需要持久化哪些日志?

    e.Encode(rf.currentTerm) // 當(dāng)前任期
    e.Encode(rf.log) // 收到的日志
    e.Encode(rf.votedFor) // 投票的
    e.Encode(rf.commitIndex) // 已經(jīng)確認(rèn)的一致性日志,之后的日志表示還沒有確認(rèn)是否可以同步,一旦確認(rèn)的日志都不會改變了

既然這幾個需要同步,那就是發(fā)生改變的時候把數(shù)據(jù)持久化下來就可以了

需要調(diào)用persist()函數(shù)的地方有:

  • leader向各個follower發(fā)送完日志,確認(rèn)提交的時候
  • follower處理AppendEnties有新日志或者commiIndex更新的時候

case13:Figure8

測試主要測試的是下面的這張圖:


圖片

描述的問題是:為什么領(lǐng)導(dǎo)人無法通過老的日志的任期號來判斷其提交狀態(tài)。

  • (a) S1 是領(lǐng)導(dǎo)者,部分的復(fù)制了索引位置 2 的日志條目
  • (b) S1 崩潰了,然后 S5 在任期 3 里通過 S3、S4 和自己的選票贏得選舉,然后從客戶端接收了一條不一樣的日志條目放在了索引2 處
  • (c) S5 又崩潰了;S1 重新啟動,選舉成功,開始復(fù)制日志。在這時,來自任期 2 的那條日志已經(jīng)被復(fù)制到了集群中的大多數(shù)機(jī)器上,但是還沒有被提交
  • (d) S1 又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然后覆蓋了他們在索引 2 處的日志。但是,在崩潰之前,如果 S1 在自己的任期里復(fù)制了日志條目到大多數(shù)機(jī)器上
  • (e) 然后這個條目就會被提交(S5 就不可能選舉成功)。 在這個時候,之前的所有日志就會被正常提交處理

Raft采用計算副本數(shù)的方式,使得永遠(yuǎn)不會提交前前 面紀(jì)元的日志條目,

現(xiàn)在出現(xiàn)的問題是commit了不同的值?
即在沒有達(dá)成一致的情況下就就行了提交!

Test: Figure 8 ...
2016/10/13 20:38:35 server:0,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 1752890841475247006 1}]
nextIndex is:[1 1 1 1 1]
matchIndex is:[0 0 0 0 0]

2016/10/13 20:38:35 server:2,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 1752890841475247006 1}]
nextIndex is:[1 1 1 1 1]
matchIndex is:[0 0 0 0 0]

2016/10/13 20:38:35 server:4,currentTerm:2,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 1752890841475247006 1}]
nextIndex is:[1 1 1 1 1]
matchIndex is:[0 0 0 0 0]

2016/10/13 20:38:35 apply error: commit index=2 server=1 4541014630978635374 != server=3 8558661384468427932

到這就得加上之前忘記的一個策略

如果存在以個N滿足 N>commitIndex,多數(shù)的matchIndex[i] >= N,并且 log[N].term == currentTerm:設(shè)置commitIndex = N

主要是指:leader只會提交本紀(jì)元的日志

case14:UnreliableAgree

模擬網(wǎng)絡(luò)不可靠,在不可靠的情況下cfg.setunreliable(false),則有概率還是丟棄請求,在這種情況下測試協(xié)議最后還能達(dá)成一致

case15:Figure8Unreliable

通過設(shè)置cfg.setlongreordering(true),在labrpc中會直接睡眠一段時間,模擬這次情況下協(xié)議還是達(dá)成一致

ms := 200 + rand.Intn(1 + rand.Intn(2000))
time.Sleep(time.Duration(ms) * time.Millisecond)
2016/10/14 14:51:11 server:4,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:3,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:2,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:1,currentTerm:31,role:follower
commitIndex:3,lastApplied:3
2016/10/14 14:51:11 server:0,currentTerm:31,role:leader
commitIndex:3,lastApplied:3
nextIndex is:[186 53 58 51 62]
matchIndex is:[185 0 0 0 0]
2016/10/14 16:09:45 check log type: raft.AppendEntiesArgs value: {6 1 1 1 1 [{1 4411 2} {2 9540 3} {4 3863 4} {6 2769 5}]}
2016/10/14 16:09:45 error log indexserver:0,currentTerm:6,role:follower
commitIndex:1,lastApplied:1
log is:[{0 <nil> 0} {1 606 1} {1 4411 2} {4 3863 4} {6 2769 5}]
nextIndex is:[84 0 0 3 2]
matchIndex is:[83 1 1 2 1]

錯誤日志,由于沒有很好的傳遞日志,代碼bug

case16-17:TestReliableChurn,UnreliableChurn

測試通過

下一篇的計劃是結(jié)合代碼再次看下關(guān)鍵實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,347評論 2 377

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