Kafka筆記

一、背景知識

Kafka定義

傳統(tǒng)定義:Kafka 是一個分布式的基于發(fā)布/訂閱模式的消息隊列,主要應(yīng)用于大數(shù)據(jù)實時處理領(lǐng)域。

最新定義:Kafka 是一個開源的分布式事件流平臺,被數(shù)千家公司用于高性能數(shù)據(jù)管道、流分析、數(shù)據(jù)集成和關(guān)鍵任務(wù)應(yīng)用。

消息隊列

傳統(tǒng)的消息隊列的主要應(yīng)用場景包括: 緩存/消峰、 解耦和異步通信。目前企業(yè)中比較常見的消息隊列產(chǎn)品主要有 ActiveMQ、RabbitMQ、RocketMQ、Kafka 等。

消息隊列的兩種模式:

  • 點對點模式:一對一,消費者主動拉取數(shù)據(jù),消息收到后消息清除。該模式使用較少
  • 發(fā)布/ 訂閱模式:一對多,消息生產(chǎn)者將消息發(fā)布到 topic 中,同時有多個消費者消費該消息,消費之后不會清除消息

二、Kafka架構(gòu)

Kafka架構(gòu)
  1. Producer:消息生產(chǎn)者,就是向 kafka broker 發(fā)消息的客戶端
  2. Consumer:消息消費者,向 kafka broker 取消息的客戶端
  3. Consumer Group:消費者組,由多個 consumer 組成。消費者組內(nèi)每個消費者負(fù)責(zé)消費不同分區(qū)的數(shù)據(jù),一個分區(qū)只能由一個組內(nèi)消費者消費;消費者組之間互不影響,所有的消費者都屬于某個消費者組,即消費者組是邏輯上的一個訂閱者
  4. Broker:一臺 kafka 服務(wù)器就是一個 broker,一個集群由多個 broker 組成。一個 broker可以容納多個 topic
  5. Topic:可以理解為一個隊列,生產(chǎn)者和消費者面向的都是一個 topic
  6. Partition:為了實現(xiàn)擴(kuò)展性,一個非常大的 topic 可以分布到多個 broker(即服務(wù)器)上,一個 topic 可以分為多個 partition,每個 partition 是一個有序的隊列
  7. Replica:副本,為保證集群中的某個節(jié)點發(fā)生故障時,該節(jié)點上的 partition 數(shù)據(jù)不丟失,且 kafka 仍然能夠繼續(xù)工作,kafka 提供了副本機(jī)制,一個 topic 的每個分區(qū)都有若干個副本,其中有一個 leader 和若干個 follower
  8. Leader:每個分區(qū)多個副本的主,生產(chǎn)者發(fā)送數(shù)據(jù)的對象,以及消費者消費數(shù)據(jù)的對象都是 leader。由 zk 記錄誰是 leader,2.8.0 版本以后也可以配置不使用 zk
  9. Follower:每個分區(qū)多個副本中的從,實時從 leader 中同步數(shù)據(jù),保持和 leader 數(shù)據(jù)的同步。leader 發(fā)生故障時,某個 follower 會成為新的 follower。

三、生產(chǎn)者

3.1 消息發(fā)送流程

在消息發(fā)送的過程中,涉及到了兩個線程:main 線程和 sender 線程。在 main 線程中創(chuàng)建了一個雙端隊列 RecordAccumulator。Main 線程將消息發(fā)送給 RecordAccumulator,sender 線程不斷從 RecordAccumulator 中拉取消息發(fā)送到 broker。

消息發(fā)送流程

幾個重要參數(shù):

  • buffer.memory:RecordAccumulator 緩沖區(qū)總大小,默認(rèn) 32m
  • batch.size:緩沖區(qū)一批數(shù)據(jù)最大值,默認(rèn)16k。適當(dāng)增加該值,可以提高吞吐量,但是如果該值設(shè)置太大,會導(dǎo)致數(shù)據(jù)傳輸延遲增加
  • linger.ms:如果數(shù)據(jù)遲遲未達(dá)到 batch.size, sender 等待 linger.time 之后就會發(fā)送數(shù)據(jù)。單位 ms,默認(rèn)值是 0ms, 表示沒有延遲。生產(chǎn)環(huán)境建議該值大小為 5-100ms 之間
  • acks:Kafka 提供了三種可靠性級別,用戶根據(jù)對可靠性和延遲的要求進(jìn)行權(quán)衡,選擇以下的配置:
    0:生產(chǎn)者發(fā)送過來的數(shù)據(jù),不需要等數(shù)據(jù)落盤應(yīng)答
    1:生產(chǎn)者發(fā)送過來的數(shù)據(jù),leader 收到數(shù)據(jù)后應(yīng)答
    -1(all):生產(chǎn)者發(fā)送過來的數(shù)據(jù),leader 和 ISR(和 leader 保持同步的 follower 集合) 隊列里面的所有節(jié)點收齊數(shù)據(jù)后應(yīng)答。 默認(rèn)值是-1,-1 和 all 是等價的
  • compression.type:生產(chǎn)者發(fā)送的所有數(shù)據(jù)的壓縮方式。默認(rèn)是 none,也就是不壓縮。支持壓縮類型:none、gzip、snappy、lz4 和 zstd
  • max.in.flight.requests.per.connection:允許最多沒有返回 ack 的次數(shù),默認(rèn)為 5,開啟冪等性要保證該值是 1-5 的數(shù)字

幾種消息發(fā)送方式:

  • 普通異步發(fā)送
  • 帶回調(diào)函數(shù)的異步 api
  • 同步 api

3.2 分區(qū)

分區(qū)的好處:

  • 方便在集群中擴(kuò)展,每個 partition 可以通過調(diào)整以適應(yīng)它所在的機(jī)器,而一個 topic 又可以有多個 Partition 組成,因此整個集群就可以適應(yīng)任意大小的數(shù)據(jù)了
  • 可以提高并發(fā),因為可以以 partition 為單位生產(chǎn)/消費數(shù)據(jù)了

生產(chǎn)者發(fā)送消息的分區(qū)策略:

  1. 指明 partition 的情況下,直接將指明的值直接作為 partiton 值
  2. 沒有指明 partition 值但有 key 的情況下,將 key 的 hash 值與 topic 的 partition 數(shù)進(jìn)行取余得到 partition 值
  3. 既沒有 partition 值又沒有 key 值的情況下,第一次調(diào)用時隨機(jī)生成一個整數(shù)(后面每次調(diào)用在這個整數(shù)上自增),將這個值與 topic 可用的 partition 總數(shù)取余得到 partition 值,也就是常說的 round-robin 輪詢算法

3.3 生產(chǎn)經(jīng)驗

生產(chǎn)者如何提高吞吐量

  1. 調(diào)整批次大小:如將 batch.size 由16k調(diào)整為32k
  2. 調(diào)整Sender線程等待時間:如將 linger.ms 由0調(diào)整為5-100ms
  3. 壓縮策略:如將 compression.type 設(shè)為 snappy
  4. 調(diào)整緩存大小:如將 buffer.memory 由32m調(diào)整為64m

數(shù)據(jù)可靠性

Ack應(yīng)答級別:

  • acks=0,生產(chǎn)者發(fā)送數(shù)據(jù)后就不管了,可靠性差,效率高
  • acks=1,生產(chǎn)者發(fā)送數(shù)據(jù)后 leader 應(yīng)答即可,可靠性中等,效率中等
  • acks=-1,生產(chǎn)者發(fā)送數(shù)據(jù)后 leader 和 ISR 隊列中所有 follower 應(yīng)答才行,可靠性高,效率低

生產(chǎn)環(huán)境中,acks=0 很少使用;acks=1,一般用于傳輸普通日志,允許丟失個別數(shù)據(jù);acks=-1,一般用于傳輸和交易相關(guān)等對可靠性要求較高的場景。

數(shù)據(jù)完全可靠條件 = ACK級別為-1 + 分區(qū)副本大于等于2 + ISR里應(yīng)答的最小副本數(shù)大于等于2

數(shù)據(jù)重復(fù)性

至少一次(At Least Once)= ACK級別為1 + 分區(qū)副本大于等于2 + ISR里應(yīng)答的最小副本數(shù)大于等于2。不能保證數(shù)據(jù)不重復(fù)。

最多一次(At Most Once)= ACK級別為0。不能保證數(shù)據(jù)不丟失。

精確一次(Exactly Once)= 冪等性 + 至少一次。冪等性默認(rèn)開啟,但只能保證在單分區(qū)單會話內(nèi)不重復(fù),如果需要全局嚴(yán)格一致,則需要開啟事務(wù)(開啟事務(wù)的前提是開啟冪等性)。

數(shù)據(jù)順序

單分區(qū)內(nèi),可以配置為有序:多分區(qū),分區(qū)與分區(qū)間無序。

單分區(qū)有序的條件:

  • 1.x 版本之前:max.in.flight.requests.per.connection = 1
  • 1.x 及之后版本:
    (1)若未開啟冪等性
    配置 max.in.flight.requests.per.connection = 1
    (2)若開啟冪等性
    配置 max.in.flight.requests.per.connection <= 5。其原理是 1.x 版本后,如果開啟冪等,kafka 服務(wù)端會緩存生產(chǎn)者發(fā)來的最近5個 requests 的元數(shù)據(jù),因此可以保證最近5個 requests 的數(shù)據(jù)是有序的。

四、Broker

4.1 Broker啟動流程

Kafka 集群中有一個 broker 的 controller 會被選舉為 controller leader,負(fù)責(zé)管理集群 broker 的上下線、所有 topic 的分區(qū)副本分配和 leader 選舉等工作。Controller 的信息同步工作是依賴于 zookeeper 的(2.8.0 版本以后也可以不依賴)。

Broker啟動流程

4.2 副本與故障處理

副本

副本的作用是提高數(shù)據(jù)可靠性,Kafka 默認(rèn)副本1個,生產(chǎn)環(huán)境一般配置為2個,保證數(shù)據(jù)可靠性;太多副本會增加磁盤存儲空間,增加網(wǎng)絡(luò)上數(shù)據(jù)傳輸,降低效率。

Kafka 中副本分為:leader 和 follower。Kafka 生產(chǎn)者只會把數(shù)據(jù)發(fā)往 leader,
然后 follower 找 leader 進(jìn)行同步數(shù)據(jù)。

幾個重要概念:

  • AR:Kafka 分區(qū)中的所有副本統(tǒng)稱為(Assigned Repllicas)。AR = ISR + OSR
  • ISR:表示和 leader 保持同步的 follower集合。如果 follower 長時間未向 leader 發(fā)送通信請求或同步數(shù)據(jù),則該 follower 將被踢出 ISR。該時間閾值由 replica.lag.time.max.ms 參數(shù)設(shè)定,默認(rèn)30s。Leader 發(fā)生故障之后,就會從 ISR 中選舉新的 leader
  • OSR:表示 follower 與 leader 副本同步時,延遲過多的副本
  • LEO:Log End Offset,每個副本的最新的 offset + 1
  • HW:High Watermart,所有副本中最小的 LEO

Follower 故障

  1. Follower 發(fā)生故障后會被臨時提出 ISR
  2. 這個期間 leader 和 follower 繼續(xù)接受數(shù)據(jù)
  3. 待該 follower 恢復(fù)后,follower 會讀取本地磁盤記錄的上次的 HW,并將 log 文件高于 HW 的部分截取掉,從 HW 開始向 leader 進(jìn)行同步
  4. 等該 follower 的 LEO 大于等于該分區(qū)的 HW,即 follower 追上 leader 之后,就可以重新加入 ISR 了

Leader 故障

  1. Leader 發(fā)生故障之后,會從 ISR 中選出一個新的 leader
  2. 為保證多個副本之間的數(shù)據(jù)一致性,其余的 follower 會先將各自的 log 文件高于 HW 的部分截掉,然后從新的 leader 同步數(shù)據(jù)

注意: 這只能保證副本之間的數(shù)據(jù)一致性,并不能保證數(shù)據(jù)不丟失或者不重復(fù)。如何保證?見上一節(jié)數(shù)據(jù)可靠性。

4.3 文件存儲

Topic 是邏輯上的概念,而 partition 是物理上的概念,每個 partition 對應(yīng)一個 log 文件,該文件中存儲的就是 producer 生產(chǎn)的數(shù)據(jù)。Producer 生產(chǎn)的數(shù)據(jù)會不斷追加到該 log 文件末端。為防止 log 文件過大導(dǎo)致數(shù)據(jù)定位效率低下,kafka 采取了分片和索引機(jī)制,將每個 partition 分為多個 segment。每個 segment 包括:.index 文件、.log 文件和 .timeindex 等文件,這些文件位于一個文件夾下,該文件夾命名規(guī)則:topic 名稱 + 分區(qū)序號,例如:first-0。

文件存儲機(jī)制

兩個重要參數(shù):

  • log.segment.bytes:log 日志劃分成塊(即 segment)的大小,默認(rèn)值1G
  • log.index.interval.bytes:默認(rèn)4kb,每當(dāng)寫入了4kb大小的日志(.log),然后就往 index 文件里面記錄一個索引(稀疏索引)

Log 文件和 Index 文件示例

文件示例

高效讀寫數(shù)據(jù)

Kafka 如何做到高效讀寫數(shù)據(jù)?

  1. Kafka 本身是分布式集群,可以采用分區(qū)技術(shù),并行度高
  2. 讀數(shù)據(jù)采用稀疏索引,可以快速定位要消費的數(shù)據(jù)
  3. 順序?qū)懘疟P,生產(chǎn)者數(shù)據(jù)是一直追加到 log 文件末端的順序?qū)懀樞驅(qū)?600M/s vs 隨機(jī)寫 100K/s)
  4. 零拷貝+頁緩存技術(shù)
    零拷貝:Kafka 的數(shù)據(jù)加工處理由生產(chǎn)者和消費者處理,broker 應(yīng)用層不關(guān)心存儲的數(shù)據(jù),所以就不用了走應(yīng)用層,傳輸效率高。
    頁緩存:操作系統(tǒng)提供,當(dāng)上層由寫操作時,操作系統(tǒng)只是將數(shù)據(jù)寫入 PageCache;讀操作時先從 PageCache 中查找,找不到再去磁盤中獲取。

關(guān)于零拷貝和頁緩存,具體可以參考:https://zhuanlan.zhihu.com/p/258513662

五、消費者

5.1 消費方式

Consumer 采用 pull(拉)模式從 broker 中讀取數(shù)據(jù);因為 push (推)模式很難適應(yīng)消費速率不同的消費者。

Pull 模式不足之處是,如果 kafka 沒有數(shù)據(jù),消費者可能會陷入循環(huán)中,一直返回空數(shù)據(jù)。針對這一點,kafka 的消費者在消費數(shù)據(jù)時會傳入一個時長參數(shù) timeout,如果當(dāng)前沒有數(shù)據(jù)可供消費,consumer 會等待一段時間之后再返回,這段時長即為 timeout。

5.2 消費者組

消費者組(Consumer Group,CG)由多個 consumer 組成。形成一個消費者組的條件,是所有消費者的 groupid 相同。消費者組內(nèi)每個消費者負(fù)責(zé)消費不同分區(qū)的數(shù)據(jù),一個分區(qū)只能由一個組內(nèi)消費者消費;消費者組之間互不影響,所有的消費者都屬于某個消費者組,即消費者組是邏輯上的一個訂閱者。

消費者組初始化流程:


消費者組初始化流程

消費者組消費流程:


消費者組消費流程

5.3 分區(qū)的分配與再平衡

一個消費者組中有多個 consumer,一個 topic 有多個 partition,所以必然會涉及到 partition 的分配問題,即確定那個 partition 由哪個 consumer 來消費。當(dāng)消費者組里面的消費者個數(shù)發(fā)生改變的時候,也會觸發(fā)再平衡。

Kafka 有四種分配策略,可以通過參數(shù) partition.assignment.strategy 來配置,默認(rèn) Range + CooperativeSticky。

  • Range:針對每個 topic。將 topic 中的分區(qū)與消費者排序,通過分區(qū)數(shù)/消費者數(shù)決定每個消費者消費幾個分區(qū),若除不盡則前面幾個消費者會多消費1個分區(qū)。注意,如果有N個 topic,容易產(chǎn)生數(shù)據(jù)傾斜
  • RoundRobin:針對集群中的所有 topic。把所有分區(qū)和所有的消費者都列出來,然后按照 hashcode 進(jìn)行排序,最后通過輪訓(xùn)算法來分配分區(qū)給到各個消費者
  • Sticky:粘性分區(qū)從 0.11.x 版本開始引入,首先會盡量均衡的放置分區(qū)到消費者上面,在出現(xiàn)同一消費者組內(nèi)消費者出現(xiàn)問題的時候,會盡量保持原有分配的分
    區(qū)不變化
  • CooperativeSticky:和 sticky 類似只是支持了cooperative 的 再平衡

5.4 Offset

由于 consumer 在消費過程中可能會出現(xiàn)斷電宕機(jī)等故障,consumer 恢復(fù)后,需要從故障前的位置的繼續(xù)消費,所以 consumer 需要實時記錄自己消費到了哪個 offset,以便故障恢復(fù)后繼續(xù)消費。

Kafka 0.9版本之前,consumer 默認(rèn)將 offset 保存在 zookeeper 中;從 0.9 版本開始,默認(rèn)將 offset 保存在 kafka 一個內(nèi)置的 topic 中,該 topic 為__consumer_offsets。__consumer_offsets 主題里面采用 key 和 value 的方式存儲數(shù)據(jù)。Key 是 group.id+topic+分區(qū)號,value 就是當(dāng)前 offset 的值。 每隔一段時間,kafka 內(nèi)部會對這個 topic 進(jìn)行 compact,也就是每個 group.id+topic+分區(qū)號 就保留最新數(shù)據(jù)。

提交 offset

  • 自動提交:為了使用戶專注自己的業(yè)務(wù)邏輯,kafka 提供了自動提交 offset 的功能,相關(guān)參數(shù):
    enable.auto.commit:是否開啟自動提交,默認(rèn) true
    auto.commit.inteval.ms:自動提交的時間間隔,默認(rèn)5s
  • 手動提交:包括兩種方式,同步提交(commitSync)和異步提交(commitAsync)

重復(fù)消費: 已經(jīng)消費了數(shù)據(jù),但是 offset 沒提交。
漏消費: 先提交 offset 后消費,有可能會造成數(shù)據(jù)的漏消費。

如何避免漏消費和重復(fù)消費,做到精準(zhǔn)一次消費呢?這依賴于消費者事務(wù),要求消費端將消費過程和提交 offset 過程做原子綁定,也就是說需要將 offset 保存到支持事務(wù)的自定義介質(zhì)(如 Mysql)。

指定 offset 消費

當(dāng) kafka 中沒有初始偏移量(消費者組第一次消費)或服務(wù)器上不再存在當(dāng)前偏移量時(例如該數(shù)據(jù)已被刪除),該怎么辦?有以下幾種配置:

  • earliest:自動將偏移量重置為最早的偏移量
  • latest(默認(rèn)值):自動將偏移量重置為最新偏移量
  • none:如果未找到消費者組的先前偏移量,則向消費者拋出異常
  • 任意指定 offset 位移開始消費

5.5 生產(chǎn)經(jīng)驗

如何提高吞吐量(避免數(shù)據(jù)積壓)

  • 如果是消費能力不足,可以考慮增加 topic 的分區(qū)數(shù),并提升消費者組的消費者數(shù)量,使消費者數(shù) = 分區(qū)數(shù)
  • 如果是下游的數(shù)據(jù)處理不及時,可以提高每批次拉取的數(shù)量。如果拉取數(shù)據(jù)/處理時間 < 生產(chǎn)速度,即處理的數(shù)據(jù)小于生產(chǎn)的數(shù)據(jù),也會造成數(shù)據(jù)積壓

六、Kafka-Kraft 模式

kafka架構(gòu)

左圖為 kafka 原有架構(gòu),元數(shù)據(jù)在 zookeeper 中,運行時動態(tài)選舉 controller,由 controller 進(jìn)行 kafka 集群管理。右圖為 kraft 模式架構(gòu)(實驗性),不再依賴 zookeeper 集群,而是用三臺 controller 節(jié)點代替 zookeeper,元數(shù)據(jù)保存在 controller 中,由 controller 直接進(jìn)行 kafka 集群管理。這樣做的好處有以下幾個:

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