Hive原理及SQL優化

1.Hive原理

Hive是構建在Hadoop上的數據倉庫軟件框架,支持使用SQL來讀,寫和管理大規模數據集合。Hive入門非常簡單,功能非常強大,所以非常流行。

通常來說,Hive只支持數據查詢和加載,但后面的版本也支持了插入,更新和刪除以及流式api。Hive具有目前Hadoop上最豐富最全的SQL語法,也擁有最慢最穩定的執行。是目前Hadoop上幾乎標準的ETL和數據倉庫工具。

Hive這個特點與其它AdHoc查詢工具如Impala(拉式獲取數據\利用內存\漸進輸出結果),Spark SQL或者Presto有著應用場景的區別,也就是雖然都是即席查詢工具,前者適用與穩定作業執行,調度以及ETL,或者更傾向于交戶式。一個典型的場景是分析師使用Impala去探測數據,驗證想法,并把數據產品部署在Hive上執行。

1.1Hadoop基本原理

在我們講Hive原理和查詢優化前,讓我們先回顧一下Hadoop基本原理。

Hadoop是一個分布式系統,有HDFS和Yarn。HDFS用于執行存儲,Yarn用于資源調度和計算。MapReduce是跑在Yarn上的一種計算作業,此外還有Spark等。

Hive通常意義上來說,是把一個SQL轉化成一個分布式作業,如MapReduce,Spark或者Tez。無論Hive的底層執行框架是MapReduce、Spark還是Tez,其原理基本都類似。

而目前,由于MapReduce穩定,容錯性好,大量數據情況下使用磁盤,能處理的數據量大,所以目前Hive的主流執行框架是MapReduce,但性能相比Spark和Tez也就較低,等下講到Group By和JOIN原理時會解釋這方面的原因。

目前的Hive除了支持在MapReduce上執行,還支持在Spark和Tez 上執行。我們以MapReduce為例來說明的Hive的原理。先回顧一下 MapReduce 原理。


兩個Mapper各自輸入一塊數據,由鍵值對構成,對它進行加工(加上了個字符n),然后按加工后的數據的鍵進行分組,相同的鍵到相同的機器。這樣的話,第一臺機器分到了鍵nk1和nk3,第二臺機器分到了鍵nk2。

接下來再在這些Reducers上執行聚合操作(這里執行的是是count),輸出就是nk1出現了2次,nk3出現了1次,nk2出現了3次。從全局上來看,MapReduce就是一個分布式的GroupBy的過程。

從上圖可以看到,Global Shuffle左邊,兩臺機器執行的是Map。Global Shuffle右邊,兩臺機器執行的是Reduce。所以Hive,實際上就是一個編譯器,一個翻譯機。把SQL翻譯成MapReduce之類的作業。

1.2Hive架構


下面這個舊一點的圖片來自Facebook


從架構圖上可以很清楚地看出Hive和Hadoop(MapReduce,HDFS)的關系。

Hive是最上層,即客戶端層或者作業提交層。?

MapReduce/Yarn是中間層,也就是計算層。?

HDFS是底層,也就是存儲層。

從Facebook的圖上可以看出,Hive主要有QL,MetaStore和Serde三大核心組件構成。QL就是編譯器,也是Hive中最核心的部分。Serde就是Serializer和Deserializer的縮寫,用于序列化和反序列化數據,即讀寫數據。MetaStore對外暴露Thrift API,用于元數據的修改。比如表的增刪改查,分區的增刪改查,表的屬性的修改,分區的屬性的修改等。

1.3Hive的數據模型

Hive的數據存儲在HDFS上,基本存儲單位是表或者分區,Hive內部把表或者分區稱作SD,即Storage Descriptor。一個SD通常是一個HDFS路徑,或者其它文件系統路徑。SD的元數據信息存儲在Hive MetaStore中,如文件路徑,文件格式,列,數據類型,分隔符。Hive默認的分格符有三種,分別是^A、^B和^C,即ASCii碼的1、2和3,分別用于分隔列,分隔列中的數組元素,和元素Key-Value對中的Key和Value。

還記得大明湖畔暴露Thrift API的MetaStore么?嗯,是她,就是它!所有的數據能不能認得出來全靠它!

Hive的核心是Driver,Driver的核心是SemanticAnalyzer。 Hive實際上是一個SQL到Hadoop作業的編譯器。 Hadoop上最流行的作業就是MapReduce,當然還有其它,比如Tez和Spark。Hive目前支持MapReduce, Tez, Spark三種作業,其原理和剛才回顧的MapReduce過程類似,只是在執行優化上有區別。

1.4Hive的編譯流程

Hive作業的執行過程實際上是SQL翻譯成作業的過程?那么,它是怎么翻譯的?


一條SQL,進入的Hive。經過上述的過程,其實也是一個比較典型的編譯過程變成了一個作業。


首先,Driver會輸入一個字符串SQL,然后經過Parser變成AST(abstract syntax tree 抽象語法樹),這個變成AST的過程是通過Antlr(antlr是指可以根據輸入自動生成語法樹并可視化的顯示出來的開源語法分析器)來完成的,也就是Anltr根據語法文件來將SQL變成AST。

AST進入SemanticAnalyzer(核心)變成QB,也就是所謂的QueryBlock。一個最簡的查詢塊,通常來講,一個From子句會生成一個QB。生成QB是一個遞歸過程,生成的 QB經過GenLogicalPlan過程,變成了一個Operator圖,也是一個有向無環圖。

OP DAG經過邏輯優化器,對這個圖上的邊或者結點進行調整,順序修訂,變成了一個優化后的有向無環圖。這些優化過程可能包括謂詞下推(Predicate Push Down),分區剪裁(Partition Prunner),關聯排序(Join Reorder)等等

經過了邏輯優化,這個有向無環圖還要能夠執行。所以有了生成物理執行計劃的過程。GenTasks。Hive的作法通常是碰到需要分發的地方,切上一刀,生成一道MapReduce作業。如Group By切一刀,Join切一刀,Distribute By切一刀,Distinct切一刀。

這么很多刀砍下去之后,剛才那個邏輯執行計劃,也就是那個邏輯有向無環圖,就被切成了很多個子圖,每個子圖構成一個結點。這些結點又連成了一個執行計劃圖,也就是Task Tree.

把這些個Task Tree 還可以有一些優化,比如基于輸入選擇執行路徑,增加備份作業等。進行調整。這個優化就是由Physical Optimizer來完成的。經過Physical Optimizer,這每一個結點就是一個MapReduce作業或者本地作業,就可以執行了。

這就是一個SQL如何變成MapReduce作業的過程。要想觀查這個過程的最終結果,可以打開Hive,輸入Explain + 語句,就能夠看到。


1.5group by的執行任務

Hive最重要的部分是Group By和Join。下面分別講解一下:

首先是Group By

例如我們有一條SQL語句:

INSERT INTO TABLE pageid_age_sum

SELECT pageid, age, count(1)

FROM pv_users

GROUP BY pageid, age;


把每個網頁的閱讀數按年齡進行分組統計。由于前面介紹了,MapReduce就是一個Group By的過程,這個SQL翻譯成MapReduce就是相對簡單的。

我們在Map端,每一個Map讀取一部分表的數據,通常是64M或者256M,然后按需要Group By的Key分發到Reduce端。經過Shuffle Sort,每一個Key再在Reduce端進行聚合(這里是Count),然后就輸出了最終的結果。

1.6 distinct的執行任務

值得一提的是,Distinct在實現原理上與Group By類似。當Group By遇上 Distinct……例如:SELECT pageid, COUNT(DISTINCT userid) FROM page_view GROUP BY pageid

Hive 實現成MapReduce的原理如下:

也就是說Map分發到Reduce的時候,會使用pageid和userid作為聯合分發鍵,再去聚合(Count),輸出結果。

介紹了這么多原理,重點還是為了使用,為了適應場景和業務,為了優化。從原理上可以看出,當遇到Group By的查詢時,會按Group By 鍵進行分發?如果鍵很多,撐爆了機器會怎么樣?

對于Impala,或Spark,為了快,key在內存中,爆是經常的。爆了就失敗了。對于Hive,Key在硬盤,本身就比Impala, Spark的處理能力大上幾萬倍。但……不幸的是,硬盤也有可能爆。

1.7 join的執行任務

例如這樣一個查詢:INSERT INTO TABLE pv_users

SELECT pv.pageid, u.age

FROM page_view pv JOIN user u ON (pv.userid = u.userid);

把訪問和用戶表進行關聯,生成訪問用戶表。Hive的Join也是通過MapReduce來完成的。

就上面的查詢,在MapReduce的Join的實現過程如下:

Map端會分別讀入各個表的一部分數據,把這部分數據進行打標,例如pv表標1,user表標2.

Map讀取是分布式進行的。標完完后分發到Reduce端,Reduce 端根據Join Key,也就是關聯鍵進行分組。然后按打的標進行排序,也就是圖上的Shuffle Sort。

在每一個Reduce分組中,Key為111的在一起,也就是一臺機器上。同時,pv表的數據在這臺機器的上端,user表的數據在這臺機器的下端。

這時候,Reduce把pv表的數據讀入到內存里,然后逐條與硬盤上user表的數據做Join就可以了。

從這個實現可以看出,我們在寫Hive Join的時候,應該盡可能把小表(分布均勻的表)寫在左邊,大表(或傾斜表)寫在右邊。這樣可以有效利用內存和硬盤的關系,增強Hive的處理能力。

同時由于使用Join Key進行分發, Hive也只支持等值Join,不支持非等值Join。由于Join和Group By一樣存在分發,所以也同樣存在著傾斜的問題。所以Join也要對抗傾斜數據,提升查詢執行性能。

1.8 Map join的執行任務

通常,有一種執行非常快的Join叫Map Join 。

手動的Map Join SQL如下(pv是小表):

INSERT INTO TABLE pv_users

SELECT /*+ MAPJOIN(pv) */ pv.pageid, u.age

FROM page_view pv JOIN user u

ON (pv.userid = u.userid);

還是剛才的例子,用Map Join執行

Map Join通常只適用于一個大表和一個小表做關聯的場景,例如事實表和維表的關聯

原理如上圖,用戶可以手動指定哪個表是小表,然后在客戶端把小表打成一個哈希表序列化文件的壓縮包,通過分布式緩存均勻分發到作業執行的每一個結點上。然后在結點上進行解壓,在內存中完成關聯。

Map Join全過程不會使用Reduce,非常均勻,不會存在數據傾斜問題。默認情況下,小表不應該超過25M。在實際使用過程中,手動判斷是不是應該用Map Join太麻煩了,而且小表可能來自于子查詢的結果。

Hive有一種稍微復雜一點的機制,叫Auto Map Join

還記得原理中提到的物理優化器?Physical Optimizer么?它的其中一個功能就是把Join優化成Auto Map Join

圖上左邊是優化前的,右邊是優化后的

優化過程是把Join作業前面加上一個條件選擇器ConditionalTask和一個分支。左邊的分支是MapJoin,右邊的分支是Common Join(Reduce Join)

看看左邊的分支是不是和我們上上一張圖很像?

這個時候,我們在執行的時候,就由這個Conditional Task 進行實時路徑選擇,遇到小于25兆走左邊,大于25兆走右邊。所謂,男的走左邊,女的走右邊,人妖走中間。

在比較新版的Hive中,Auto Mapjoin是默認開啟的。如果沒有開啟,可以使用一個開關, set hive.auto.convert.join=true 開啟。

當然,Join也會遇到和上面的Group By一樣的傾斜問題。


Hive 也可以通過像Group By一樣兩道作業的模式單獨處理一行或者多行傾斜的數據。

hive 中設定

set hive.optimize.skewjoin = true;

set hive.skewjoin.key = skew_key_threshold (default = 100000)

其原理是就在Reduce Join過程,把超過十萬條的傾斜鍵的行寫到文件里,回頭再起一道Join單行的Map Join作業來單獨收拾它們。最后把結果取并集就是了。如上圖所示。

1.9Hive適合做什么?

由于多年積累,Hive比較穩定,幾乎是Hadoop上事實的SQL標準。 Hive適合離線ETL,適合大數據離線Ad-Hoc查詢。適合特大規模數據集合需要精確結果的查詢。對于交互式Ad-Hoc查詢,通常還會有別的解決方案,比如Impala, Presto等等。

特大規模的離線數據處理,尤其是大表關聯,特大規模數據聚集,很適合使用Hive。講了這么多原理,最重要的還是應用,還是創造價值。

對Hive來說,數據量再大,都不怕。數據傾斜,是大難題。但有很多優化方法和業務改進方法可以避過。Hive執行穩定,函數多,擴展性強,數據吞吐量大,了解原理,有助于用好和選型。

2.SQL優化

HIVE優化可以從兩方面入手,減少計算和加速計算。

減少計算包括:減少數據量、數據復用,避免重復計算、多粒度逐步聚合和業務裁剪。

加速計算包括:使用內存計算、并發計算。

2.1 limit優化

limit不啟用優化的情況

a.語句中帶有join,group by,列as別名,order by,where等的,不啟用優化。

b.子查詢中的limit不會做優化,會掃描所有數據,掃描完了以后取limit指定的條數。

c. 查詢視圖 limit不會啟用優化

?對于a這種情況,都會用MR處理,并且優化會比較復雜,因為group by,order by這樣的,必須掃描所有數據,要保證結果正確又盡量少計算,會讓查詢優化器變得非常復雜。對于b這種情況,與a類似,都要走MR,另外一方面,子查詢中帶limit本身用法比較少見,所以也沒有優化。

推薦使用:

(?這些語句會直接讀取HDFS文件,不走MR,會很快)

select * from taba? limit 1;

?select a from taba partition(p) limit 1;

2.2使用分區/列修剪

? ? ? 做好列裁剪和filter操作,尤其是只讀取需要的分區。支持分區自動修剪,因此,一般情況下,只要分區字段在where子句中,TDW就會自動過濾掉不需要的分區。但是,如果分區字段位于in以及其他函數中,那么分區自動修剪將失效。另外,between、or、and都支持分區自動修剪。

推薦使用:

(tdbank_imp_date 為表的分區字段,對分區字段不要使用函數,不要用substr):?

where tdbank_imp_date?in('2018030100','2018030101','2018030102')?

where tdbank_imp_date='2018030100'

where tdbank_imp_date between? '2018030100'??and '2018043023'

2.3禁止出現笛卡爾積

笛卡爾積只有1個reduce任務(一個整的大文件),會導致計算超慢,甚至可能計算不出來或者導致節點掛掉。

以下3種形式的SQL會導致笛卡爾積:

select * from gbk,utf8?where?gbk.key= utf8.key?and gbk.key > 10;

select * from gbk?join?utf8?where?gbk.key= utf8.key?and gbk.key > 10;

?tablea?join?tableb?join?tablec?join?...?on?tablea.col1 = tableb.col2 and ...

推薦使用:

(1) select * from gbk?join?utf8?on gbk.key= utf8.key?where gbk.key > 10;

(2) tablea?join?tableb?on?(tablea.col1 = tableb.col2 and ...?)?join?tablec?on?...join?...?on?...

(3) select * from?

(select * from gbk where gbk.key>10) gbk?

join?

(select * from utf8)

on gbk.key=utf8.key?

2.4表關聯的優化

2.4.1有小表且數據條數不超過2w行

?推薦使用Map Join

map join的必要條件:

?????? a.參與連接的小表的行數,以不超過2萬條為宜。

?????? b.連接類型是inner join、right outer join(小表不能是右表)、left outer join(小表不能是左表)、left semi join。

使用方法示例:

例如(其中pv是小表,會把pv表生成hash表,壓縮):

????? SELECT /*+ MAPJOIN(pv) */ ??

??????????????????? pv.pageid, u.age ????????????????????????????????

????? FROM page_view pv

???????????????? JOIN user u

???????????????? ON (pv.userid = u.userid);

注:當大表存在數據傾斜時,如果小表符合map join的要求,使用map join會極大加速計算。

2.4.2小表連接大表

? ? 將較大的表放在join操作符的右邊,這樣生成的查詢計劃效率較高,執行速度快,不容易出錯;

? ?在Join 操作的Reduce 階段,位于Join 操作符左邊的表的內容會首先被加載進內存(容器滿后存入硬盤),然后對右側表進行流式處理,將條目少的表放在左邊,可以有效減少發生磁盤IO和OOM 錯誤的幾率。

推薦使用:

將較大的表放在join操作符的右邊??

2.5數據傾斜的優化

? ? ?常見的數據傾斜問題,一般發生在group by?或者join操作上,表現為一個或幾個reduce一直沒辦法做完,原因是key分布不均,某個或某幾個key的數據特別大。可以對key的數據量排序來檢驗是否有數據傾斜問題:

SELECT? key1,key2, count(1) as cnt

FROM test_tatble

GROUP BY key1,key2

ORDER BY cnt DESC LIMIT 50

2.5.1有小表使用MAPJOIN

當需要join的表有一個小表時就很適合用內存計算來完成,也即使用MAPJOIN。

2.5.2 去除數據傾斜數據,數據為臟數據

特別留意關聯的key 里面有大量0、空值;做過濾 ,然后再distinct輸出

2.5.3單獨計算導致數據傾斜的key再合并數據

根據key值拆開成2個集合,然后再union all 起來?

2.5.4 groupby時的數據傾斜,參數設置

set hive.groupby.skewindata=true;

{:problematic sql}

set hive.groupby.skewindata=false;

查詢計劃會生成兩個MR Job,其中在第一個MR Job中,Map的輸出結果會隨機分配到Reduce端,每個Reduce做部分聚合計算,然后輸出結果,從而達到負載均衡的目的;在第二MR Job中,根據預處理的數據結果按照分組 Key 將數據分配到相應的Reduce任務中,完成最終的聚合計算。

2.5.5設置reduce數,使得reducekey分散

通過設置reduce任務數提高并行度來加速執行:

set mapred.reduce.tasks=N; //執行語句之前

set mapred.reduce.tasks=-1; //執行語句之后恢復原狀

注意:請合理設置N的大小,最好設置為上述參數的大小。一定不要超過999!

2.5.6 將數據傾斜的key隨機化

將空值key轉換成一個字符串加上隨機數,從而將傾斜的數據分到不同的Reduce任務上

select*fromtable1 a left join table2 b on case when a.vopenid is null then concat('random',rand()) else a.vopenid end = b.vopenid;

2.6先聚合后連接

這個原則很簡單,因為join key可能存在傾斜,因此,只要可能,最好先對join key進行處理一下再進行join操作,避免數據傾斜。

一般來說,join后面會跟著一些聚合函數操作,這個原則是盡量將聚合操作提前做,使得在做join的時候join key可以是單一的。

聚合有數據壓縮的作用。“先聚合,后連接”可以減少聚合和連接時的數據量

2.8開啟并行執行

? ?支持并行執行機制,以下功能都可以通過并行執行加速:union all、join(參與join的是復雜查詢)、cube、rollup、groupingsets、with等。可見,很多常用的功能都可以加速。默認該并行開關是關閉的,若需要并行執行,可以設置?set hive.exec.parallel=true;打開開關,但消耗資源成本會增加。

2.9任務分析常用語句

(1)想知道分區修剪是否起作用么?

答案:用explain語句吧,看看要讀取哪些目錄就知道了。另外,通過這個語句的執行結果,還可以檢查你的查詢計劃是否合理。

例句:

Explain select t.col1,t.col3 from dbname::tablename t where t.ftime=‘20130104’

(2)show rowcount的作用

Show rowcount dbname::tablename //顯示整個表有多少行

通過這個命令可以知道一個表有多少行記錄,有了它的幫助就可以在連接時基本保證小表連接大表,也可以知道是否適合使用map join。

Show rowcount extended dbname::tablename? //按分區顯示每個分區有多少行

這個命令更有用的形式是show rowcount extended tablename,這個命令可以按分區來顯示每個分區有多少行記錄。

注意:本命令和下一條命令只對結構化存儲文件生效。如對文本文件執行該命令將報錯。

(3)show tablesize的作用

Show tablesize dbname::tablename

通過這個命令可以知道一個表有多大,單位是字節。

擴展形式:

Show tablesize extended dbname::tablename

說明:

通過該命令的結果,可以估算出大約需要多少個map任務,現在一般256MB/512MB一個map任務。如果你想知道join的時候需要多少個map任務,只需要把每個表需要的map任務數求和就可以了。

需要的map數太多可不是好事,通常map數超過1萬就是較大的任務了。否則,你需要耐心等待。

2.10臨時表的應用

多次需要用到的數據,最好使用臨時表保存起來


總結:

用集合的思維分解問題 , 用同類型key做關聯;

能少join就盡量少join,想辦法實現數據復用 ;?

盡最大能力限制子查詢輸入/輸出的數據量 ;

盡量避免udf函數的使用,盡量利用內存機制;

遇到數據傾斜,先清除臟數據,再做優化;


3.常見sql的標準寫法

(1) 日志數據提取

需要注意分區/列修剪

select filed1,filed2,filed3 from tableA where tdbank_imp_date between '2018092700' and '2018092723' and ....

(2) 去重統計

先distinct 減少輸出的數據量

select count(*) from (

select distinct? openid from? tablelog where?tdbank_imp_date between '2018092700' and '2018092723' and ....

)

(3) 查詢最近一個月既玩了A游戲也玩了B游戲的用戶數量

能一個查詢搞定的 盡量一個查詢搞定

select count(suin) from iplat group by suin having (count(case when vgameappid='xxx'? then 1 end )>0 and count(case when vgameappid='yyy' then 1 end)>0);


參考資料:

https://blog.csdn.net/LW_GHY/article/details/51469753?utm_source=copy

http://km.oa.com/group/2430/articles/show/127863

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

推薦閱讀更多精彩內容