Kafka學(xué)習(xí)筆記

原文:InfoQ 作者 郭俊

簡介

Kafka是一種分布式的,基于發(fā)布/訂閱的消息系統(tǒng)非洲。使用Scala編寫,它以可水平擴(kuò)展和高吞吐率而被廣泛使用。

Kafka架構(gòu)

Terminoliogy

  • Broker:Kafka集群包含一個(gè)或多個(gè)服務(wù)器拐纱,這種服務(wù)器被稱為broker 代理、中介者

  • Topic:每條發(fā)布到Kafka集群的消息都有一個(gè)類別哥倔,這個(gè)類別被稱為 Topic

    物理上不同Topic的消息分開存儲(chǔ)秸架,邏輯上一個(gè)Topic的消息雖然保存于一個(gè)或多個(gè)broker上但用戶只需指定消息的Topic即可生產(chǎn)或消費(fèi)數(shù)據(jù)而不必關(guān)心數(shù)據(jù)存于何處主題

  • Partition:Parition是物理上的概念,每個(gè)Topic包含一個(gè)或多個(gè)Partition 分割咆蒿、分區(qū)

  • Producer:負(fù)責(zé)發(fā)布消息到Kafka broker

  • Consumer:消息消費(fèi)者东抹,向Kafka broker讀取消息的客戶端蚂子。

  • Consumer Group:每個(gè)Consumer屬于一個(gè)特定的Consumer Group

    可為每個(gè)Consumer指定group name,若不指定group name則屬于默認(rèn)的group

Kafka拓?fù)浣Y(jié)構(gòu)

Kafka拓?fù)浣Y(jié)構(gòu)

如上圖所示缭黔,一個(gè)典型的Kafka集群中包含若干Producer(可以是web前端產(chǎn)生的Page View食茎,或者是服務(wù)器日志,系統(tǒng)CPU馏谨、Memory等)别渔,若干broker(Kafka支持水平擴(kuò)展,一般broker數(shù)量越多惧互,集群吞吐率越高)哎媚,若干Consumer Group,以及一個(gè)Zookeeper集群喊儡。Kafka通過Zookeeper管理集群配置抄伍,選舉leader,以及在Consumer Group發(fā)生變化時(shí)進(jìn)行rebalance管宵。Producer使用push模式將消息發(fā)布到broker截珍,Consumer使用pull模式從broker訂閱并消費(fèi)消息。

Topic & Partition

Topic =Queue

img
img

Topic在邏輯上可以被認(rèn)為是一個(gè)queue箩朴,每條消費(fèi)都必須指定它的Topic岗喉,可以簡單理解為必須指明把這條消息放進(jìn)哪個(gè)queue里。為了使得Kafka的吞吐率可以線性提高炸庞,物理上把Topic分成一個(gè)或多個(gè) Partition钱床,每個(gè)Partition在物理上對(duì)應(yīng)一個(gè)文件夾,該文件夾下存儲(chǔ)這個(gè)Partition的所有消息和索引文件埠居。若創(chuàng)建topic1和topic2兩個(gè)topic查牌,且分別有13個(gè)和19個(gè)分區(qū),則整個(gè)集群上會(huì)相應(yīng)會(huì)生成共32個(gè)文件夾(本文所用集群共8個(gè)節(jié)點(diǎn)滥壕,此處topic1和topic2 replication-factor均為1)纸颜,如下圖所示。

img

每個(gè)日志文件都是一個(gè)log entrie序列绎橘,每個(gè)log entrie包含一個(gè)4字節(jié)整型數(shù)值(值為N+5)胁孙,1個(gè)字節(jié)的"magic value",4個(gè)字節(jié)的CRC校驗(yàn)碼称鳞,其后跟N個(gè)字節(jié)的消息體涮较。每條消息都有一個(gè)當(dāng)前Partition下唯一的64字節(jié)的offset,它指明了這條消息的起始位置冈止。磁盤上存儲(chǔ)的消息格式如下:

message length : 4 bytes (value: 1+4+n)
"magic" value : 1 byte 
crc : 4 bytes 
payload : n bytes 

這個(gè)log entries并非由一個(gè)文件構(gòu)成狂票,而是分成多個(gè)segment,每個(gè)segment以該segment第一條消息的offset命名并以“.kafka”為后綴熙暴。另外會(huì)有一個(gè)索引文件闺属,它標(biāo)明了每個(gè)segment下包含的log entry的offset范圍慌盯,如下圖所示。

img

因?yàn)槊織l消息都被append到該P(yáng)artition中屋剑,屬于順序?qū)懘疟P润匙,因此效率非常高(經(jīng)驗(yàn)證,順序?qū)懘疟P效率比隨機(jī)寫內(nèi)存還要高唉匾,這是Kafka高吞吐率的一個(gè)很重要的保證)孕讳。

img

對(duì)于傳統(tǒng)的message queue而言,一般會(huì)刪除已經(jīng)被消費(fèi)的消息巍膘,而Kafka集群會(huì)保留所有的消息厂财,無論其被消費(fèi)與否。當(dāng)然峡懈,因?yàn)榇疟P限制璃饱,不可能永久保留所有數(shù)據(jù)(實(shí)際上也沒必要),因此Kafka提供兩種策略刪除舊數(shù)據(jù)肪康。一是基于時(shí)間荚恶,二是基于Partition文件大小。例如可以通過配置$KAFKA_HOME/config/server.properties磷支,讓Kafka刪除一周前的數(shù)據(jù)谒撼,也可在Partition文件超過1GB時(shí)刪除舊數(shù)據(jù),配置如下所示雾狈。

# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according to the retention policies
log.retention.check.interval.ms=300000
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction.
log.cleaner.enable=false

這里要注意廓潜,因?yàn)镵afka讀取特定消息的時(shí)間復(fù)雜度為O(1),即與文件大小無關(guān)善榛,所以這里刪除過期文件與提高Kafka性能無關(guān)辩蛋。選擇怎樣的刪除策略只與磁盤以及具體的需求有關(guān)冯袍。另外菇夸,Kafka會(huì)為每一個(gè)Consumer Group保留一些metadata信息——當(dāng)前消費(fèi)的消息的position,也即offset袱耽。這個(gè)offset由Consumer控制味滞。正常情況下Consumer會(huì)在消費(fèi)完一條消息后遞增該offset樱蛤。當(dāng)然,Consumer也可將offset設(shè)成一個(gè)較小的值剑鞍,重新消費(fèi)一些消息。因?yàn)閛ffet由Consumer控制爽醋,所以Kafka broker是無狀態(tài)的蚁署,它不需要標(biāo)記哪些消息被哪些消費(fèi)過,也不需要通過broker去保證同一個(gè)Consumer Group只有一個(gè)Consumer能消費(fèi)某一條消息蚂四,因此也就不需要鎖機(jī)制晌杰,這也為Kafka的高吞吐率提供了有力保障。

備注:
Kafka讀取特定消息的時(shí)間復(fù)雜度為O(1)肋演,按順序讀取,即不受文件大小約束
Kafka broker是無狀態(tài)的梗夸,broker沒有鎖機(jī)制,提高了高吞吐率

Producer消息路由

Producer發(fā)送消息到broker時(shí),會(huì)根據(jù)Paritition機(jī)制選擇將其存儲(chǔ)到哪一個(gè)Partition该酗。如果Partition機(jī)制設(shè)置合理莱衩,所有消息可以均勻分布到不同的Partition里睹晒,這樣就實(shí)現(xiàn)了負(fù)載均衡。如果一個(gè)Topic對(duì)應(yīng)一個(gè)文件,那這個(gè)文件所在的機(jī)器I/O將會(huì)成為這個(gè)Topic的性能瓶頸呆盖,而有了Partition后,不同的消息可以并行寫入不同broker的不同Partition里尤筐,極大的提高了吞吐率奖磁★跽可以在$KAFKA_HOME/config/server.properties中通過配置項(xiàng)num.partitions來指定新建Topic的默認(rèn)Partition數(shù)量吞彤,也可在創(chuàng)建Topic時(shí)通過參數(shù)指定,同時(shí)也可以在Topic創(chuàng)建之后通過Kafka提供的工具修改破加。

備注:不同的消息可以并行寫入不同broker的不同Partition里

在發(fā)送一條消息時(shí)锭环,可以指定這條消息的key吱七,Producer根據(jù)這個(gè)key和Partition機(jī)制來判斷應(yīng)該將這條消息發(fā)送到哪個(gè)Parition。Paritition機(jī)制可以通過指定Producer的paritition. class這一參數(shù)來指定,該class必須實(shí)現(xiàn)kafka.producer.Partitioner接口。本例中如果key可以被解析為整數(shù)則將對(duì)應(yīng)的整數(shù)與Partition總數(shù)取余幕帆,該消息會(huì)被發(fā)送到該數(shù)對(duì)應(yīng)的Partition获搏。(每個(gè)Parition都會(huì)有個(gè)序號(hào),序號(hào)從0開始)

//發(fā)送消息時(shí)失乾,按照此機(jī)制將消息發(fā)送到對(duì)應(yīng)的Parition上 (key---partition)
import kafka.producer.Partitioner;
import kafka.utils.VerifiableProperties;

public class JasonPartitioner<T> implements Partitioner 
{
    public JasonPartitioner(VerifiableProperties verifiableProperties)
    {
      
    }
    @Override
    public int partition(Object key, int numPartitions) 
    {
        try 
        {
            int partitionNum = Integer.parseInt((String) key);
            return Math.abs(Integer.parseInt((String) key) % numPartitions);
        } 
        catch (Exception e) 
        {
            return Math.abs(key.hashCode() % numPartitions);
        }
    }
}

如果將上例中的類作為partition.class碱茁,并通過如下代碼發(fā)送20條消息(key分別為0聋袋,1,2干毅,3)至topic3(包含4個(gè)Partition)。

//將20條信息發(fā)送到topic3對(duì)應(yīng)的4個(gè)Partition中
public void sendMessage() throws InterruptedException
{
  for(int i = 1; i <= 5; i++)
  {
        List messageList = new ArrayList<KeyedMessage<String, String>>();
        for(int j = 0; j < 4; j++)//4個(gè)partition
          {
            //第i個(gè)消息給第j個(gè)paitition
            messageList.add(new KeyedMessage<String, String>
                            ("topic2", j+"", "The " + i + message for key " + j));
        }
        producer.send(messageList);
    }
  producer.close();
}

key相同的消息會(huì)被發(fā)送并存儲(chǔ)到同一個(gè)partition里泼返,而且key的序號(hào)正好和Partition序號(hào)相同渠鸽。(Partition序號(hào)從0開始叫乌,本例中的key也從0開始)。下圖所示是通過Java程序調(diào)用Consumer后打印出的消息列表徽缚。

img

Consumer Group

(本節(jié)所有描述都是基于Consumer hight level API而非low level API)憨奸。

使用Consumer high level API時(shí),同一Topic的一條消息只能被同一個(gè)Consumer Group內(nèi)的一個(gè)Consumer消費(fèi)凿试,但多個(gè)Consumer Group可同時(shí)消費(fèi)這一消息排宰。

img
廣播:不同的Consumer Group
單播:在同一個(gè)Consumer Group 
離線處理:Hadoop 
實(shí)時(shí)處理:Storm

這是Kafka用來實(shí)現(xiàn)一個(gè)Topic消息的廣播(發(fā)給所有的Consumer)和單播(發(fā)給某一個(gè)Consumer)的手段。一個(gè)Topic可以對(duì)應(yīng)多個(gè)Consumer Group那婉。如果需要實(shí)現(xiàn)廣播板甘,只要每個(gè)Consumer有一個(gè)獨(dú)立的Group就可以了。要實(shí)現(xiàn)單播只要所有的Consumer在同一個(gè)Group里详炬。用Consumer Group還可以將Consumer進(jìn)行自由的分組而不需要多次發(fā)送消息到不同的Topic盐类。

實(shí)際上,Kafka的設(shè)計(jì)理念之一就是同時(shí)提供離線處理實(shí)時(shí)處理痕寓。根據(jù)這一特性傲醉,可以使用Storm這種實(shí)時(shí)流處理系統(tǒng)對(duì)消息進(jìn)行實(shí)時(shí)在線處理,同時(shí)使用Hadoop這種批處理系統(tǒng)進(jìn)行離線處理呻率,還可以同時(shí)將數(shù)據(jù)實(shí)時(shí)備份到另一個(gè)數(shù)據(jù)中心硬毕,只需要保證這三個(gè)操作所使用的Consumer屬于不同的Consumer Group即可。下圖是Kafka在Linkedin的一種簡化部署示意圖礼仗。

img

下面這個(gè)例子更清晰地展示了Kafka Consumer Group的特性吐咳。首先創(chuàng)建一個(gè)Topic (名為topic1,包含3個(gè)Partition)元践,然后創(chuàng)建一個(gè)屬于group1的Consumer實(shí)例韭脊,并創(chuàng)建三個(gè)屬于group2的Consumer實(shí)例,最后通過Producer向topic1發(fā)送key分別為1单旁,2沪羔,3的消息。結(jié)果發(fā)現(xiàn)屬于group1的Consumer收到了所有的這三條消息象浑,同時(shí)group2中的3個(gè)Consumer分別收到了key為1蔫饰,2,3的消息愉豺。如下圖所示篓吁。

img

Push vs. Pull

作為一個(gè)消息系統(tǒng),Kafka遵循了傳統(tǒng)的方式蚪拦,選擇由Producer向broker push消息并由Consumer從broker pull消息杖剪。一些logging-centric system冻押,比如Facebook的Scribe和Cloudera的Flume,采用push模式盛嘿。事實(shí)上洛巢,push模式和pull模式各有優(yōu)劣。

push模式很難適應(yīng)消費(fèi)速率不同的消費(fèi)者孩擂,因?yàn)橄l(fā)送速率是由broker決定的狼渊。push模式的目標(biāo)是盡可能以最快速度傳遞消息,但是這樣很容易造成Consumer來不及處理消息类垦,典型的表現(xiàn)就是拒絕服務(wù)以及網(wǎng)絡(luò)擁塞。而pull模式則可以根據(jù)Consumer的消費(fèi)能力以適當(dāng)?shù)乃俾氏M(fèi)消息城须。

對(duì)于Kafka而言蚤认,pull模式更合適。pull模式可簡化broker的設(shè)計(jì)糕伐,Consumer可自主控制消費(fèi)消息的速率砰琢,同時(shí)Consumer可以自己控制消費(fèi)方式——即可批量消費(fèi)也可逐條消費(fèi),同時(shí)還能選擇不同的提交方式從而實(shí)現(xiàn)不同的傳輸語義良瞧。

Kafka delivery guarantee

有這么幾種可能的delivery guarantee:

  • At most once 消息可能會(huì)丟陪汽,但絕不會(huì)重復(fù)傳輸

  • At least one 消息絕不會(huì)丟,但可能會(huì)重復(fù)傳輸

  • Exactly once 每條消息肯定會(huì)被傳輸一次且僅傳輸一次褥蚯,很多時(shí)候這是用戶所想要的挚冤。

    當(dāng)Producer向broker發(fā)送消息時(shí),一旦這條消息被commit赞庶,因數(shù)replication的存在训挡,它就不會(huì)丟。但是如果Producer發(fā)送數(shù)據(jù)給broker后歧强,遇到網(wǎng)絡(luò)問題而造成通信中斷澜薄,那Producer就無法判斷該條消息是否已經(jīng)commit。雖然Kafka無法確定網(wǎng)絡(luò)故障期間發(fā)生了什么摊册,但是Producer可以生成一種類似于主鍵的東西肤京,發(fā)生故障時(shí)冪等性的重試多次,這樣就做到了Exactly once茅特。截止到目前(Kafka 0.8.2版本忘分,2015-03-04),這一Feature還并未實(shí)現(xiàn)温治,有希望在Kafka未來的版本中實(shí)現(xiàn)饭庞。(所以目前默認(rèn)情況下一條消息從Producer到broker是確保了At least once,可通過設(shè)置Producer異步發(fā)送實(shí)現(xiàn)At most once)熬荆。

    接下來討論的是消息從broker到Consumer的delivery guarantee語義舟山。(僅針對(duì)Kafka consumer high level API)。Consumer在從broker讀取消息后,可以選擇commit累盗,該操作會(huì)在Zookeeper中保存該Consumer在該P(yáng)artition中讀取的消息的offset寒矿。該Consumer下一次再讀該P(yáng)artition時(shí)會(huì)從下一條開始讀取。如未commit若债,下一次讀取的開始位置會(huì)跟上一次commit之后的開始位置相同符相。當(dāng)然可以將Consumer設(shè)置為autocommit,即Consumer一旦讀到數(shù)據(jù)立即自動(dòng)commit蠢琳。如果只討論這一讀取消息的過程啊终,那Kafka是確保了Exactly once。但實(shí)際使用中應(yīng)用程序并非在Consumer讀取完數(shù)據(jù)就結(jié)束了傲须,而是要進(jìn)行進(jìn)一步處理蓝牲,而數(shù)據(jù)處理與commit的順序在很大程度上決定了消息從broker和consumer的delivery guarantee semantic。

  • 讀完消息先commit再處理消息泰讽。這種模式下例衍,如果Consumer在commit后還沒來得及處理消息就crash了,下次重新開始工作后就無法讀到剛剛已提交而未處理的消息已卸,這就對(duì)應(yīng)于At most once

  • 讀完消息先處理再commit佛玄。這種模式下,如果在處理完消息之后commit之前Consumer crash了累澡,下次重新開始工作時(shí)還會(huì)處理剛剛未commit的消息梦抢,實(shí)際上該消息已經(jīng)被處理過了。這就對(duì)應(yīng)于At least once永乌。在很多使用場景下惑申,消息都有一個(gè)主鍵,所以消息的處理往往具有冪等性翅雏,即多次處理這一條消息跟只處理一次是等效的圈驼,那就可以認(rèn)為是Exactly once。(筆者認(rèn)為這種說法比較牽強(qiáng)望几,畢竟它不是Kafka本身提供的機(jī)制绩脆,主鍵本身也并不能完全保證操作的冪等性。而且實(shí)際上我們說delivery guarantee 語義是討論被處理多少次橄抹,而非處理結(jié)果怎樣靴迫,因?yàn)樘幚矸绞蕉喾N多樣,我們不應(yīng)該把處理過程的特性——如是否冪等性楼誓,當(dāng)成Kafka本身的Feature)

  • 如果一定要做到Exactly once玉锌,就需要協(xié)調(diào)offset和實(shí)際操作的輸出。精典的做法是引入兩階段提交疟羹。如果能讓offset和操作輸入存在同一個(gè)地方主守,會(huì)更簡潔和通用禀倔。這種方式可能更好,因?yàn)樵S多輸出系統(tǒng)可能不支持兩階段提交参淫。比如救湖,Consumer拿到數(shù)據(jù)后可能把數(shù)據(jù)放到HDFS,如果把最新的offset和數(shù)據(jù)本身一起寫到HDFS涎才,那就可以保證數(shù)據(jù)的輸出和offset的更新要么都完成鞋既,要么都不完成,間接實(shí)現(xiàn)Exactly once耍铜。(目前就high level API而言邑闺,offset是存于Zookeeper中的,無法存于HDFS业扒,而low level API的offset是由自己去維護(hù)的检吆,可以將之存于HDFS中)

總之,Kafka默認(rèn)保證At least once程储,并且允許通過設(shè)置Producer異步提交來實(shí)現(xiàn)At most once。而Exactly once要求與外部存儲(chǔ)系統(tǒng)協(xié)作臂寝,幸運(yùn)的是Kafka提供的offset可以非常直接非常容易得使用這種方式章鲤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市咆贬,隨后出現(xiàn)的幾起案子败徊,更是在濱河造成了極大的恐慌,老刑警劉巖掏缎,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皱蹦,死亡現(xiàn)場離奇詭異,居然都是意外死亡眷蜈,警方通過查閱死者的電腦和手機(jī)沪哺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酌儒,“玉大人辜妓,你說我怎么就攤上這事〖稍酰” “怎么了籍滴?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長榴啸。 經(jīng)常有香客問我孽惰,道長,這世上最難降的妖魔是什么鸥印? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任勋功,我火速辦了婚禮坦报,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酝润。我一直安慰自己燎竖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布要销。 她就那樣靜靜地躺著构回,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疏咐。 梳的紋絲不亂的頭發(fā)上纤掸,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音浑塞,去河邊找鬼借跪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛酌壕,可吹牛的內(nèi)容都是我干的掏愁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼卵牍,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼果港!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起糊昙,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤辛掠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后释牺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萝衩,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年没咙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了猩谊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镜撩,死狀恐怖预柒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袁梗,我是刑警寧澤宜鸯,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站遮怜,受9級(jí)特大地震影響淋袖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锯梁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一即碗、第九天 我趴在偏房一處隱蔽的房頂上張望焰情。 院中可真熱鬧,春花似錦剥懒、人聲如沸内舟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽验游。三九已至,卻和暖如春保檐,著一層夾襖步出監(jiān)牢的瞬間耕蝉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工夜只, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留垒在,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓扔亥,卻偏偏與公主長得像场躯,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子旅挤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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