從Docker鏡像構(gòu)建演化史來了解多階段構(gòu)建的影響

現(xiàn)在很多開發(fā)者都會慢慢習(xí)慣在開發(fā)環(huán)境通過Docker來構(gòu)建開發(fā)環(huán)境姥卢,有時(shí)候可能會有環(huán)境移植的問題摔蓝,所以需要我們寫好一套Dockerfile來構(gòu)建相關(guān)的開發(fā)鏡像,既然說到鏡像盏触,那我想問問大家了解Docker鏡像的演變史嗎渗蟹?我們現(xiàn)在就來回顧一下吧。

自從2013年dotCloud公司(現(xiàn)已改名為Docker Inc)發(fā)布Docker容器技術(shù)以來赞辩,到目前為止已經(jīng)有四年多的時(shí)間了雌芽。這期間Docker技術(shù)飛速發(fā)展,并催生出一個生機(jī)勃勃的辨嗽、以輕量級容器技術(shù)為基礎(chǔ)的龐大的容器平臺生態(tài)圈世落。作為Docker三大核心技術(shù)之一的鏡像技術(shù)Docker的快速發(fā)展之路上可謂功不可沒:鏡像讓容器真正插上了翅膀,實(shí)現(xiàn)了容器自身的重用和標(biāo)準(zhǔn)化傳播糟需,使得開發(fā)屉佳、交付谷朝、運(yùn)維流水線上的各個角色真正圍繞同一交付物,“test what you write, ship what you test”成為現(xiàn)實(shí)武花。

對于已經(jīng)接納和使用Docker技術(shù)在日常開發(fā)工作中的開發(fā)者而言圆凰,構(gòu)建Docker鏡像已經(jīng)是家常便飯。但如何更高效地構(gòu)建以及構(gòu)建出Size更小的鏡像卻是很多Docker技術(shù)初學(xué)者心中常見的疑問髓堪,甚至是一些老手都未曾細(xì)致考量過的問題送朱。本文將從一個Docker用戶角度來闡述Docker鏡像構(gòu)建的演化史,希望能起到一定的解惑作用干旁。

一驶沼、鏡像:繼承中的創(chuàng)新

談鏡像構(gòu)建之前,我們先來簡要說下鏡像争群。

Docker技術(shù)本質(zhì)上并不是新技術(shù)回怜,而是將已有技術(shù)進(jìn)行了更好地整合和包裝。內(nèi)核容器技術(shù)以一種完整形態(tài)最早出現(xiàn)在Sun公司Solaris操作系統(tǒng)上换薄,Solaris是當(dāng)時(shí)最先進(jìn)的服務(wù)器操作系統(tǒng)玉雾。2005年Sun發(fā)布了Solaris Container技術(shù),從此開啟了內(nèi)核容器之門轻要。

2008年复旬,以Google公司開發(fā)人員為主導(dǎo)實(shí)現(xiàn)的Linux Container(即LXC)功能在被merge到Linux內(nèi)核中。LXC是一種內(nèi)核級虛擬化技術(shù)冲泥,主要基于NamespacesCgroups技術(shù)驹碍,實(shí)現(xiàn)共享一個操作系統(tǒng)內(nèi)核前提下的進(jìn)程資源隔離,為進(jìn)程提供獨(dú)立的虛擬執(zhí)行環(huán)境凡恍,這樣的一個虛擬的執(zhí)行環(huán)境就是一個容器志秃。本質(zhì)上說,LXC容器與現(xiàn)在的Docker所提供容器是一樣的嚼酝。Docker也是基于Namespaces和Cgroups技術(shù)之上實(shí)現(xiàn)的浮还,Docker的創(chuàng)新之處在于其基于Union File System技術(shù)定義了一套容器打包規(guī)范,真正將容器中的應(yīng)用及其運(yùn)行的所有依賴都封裝到一種特定格式的文件中去闽巩,而這種文件就被稱為鏡像(即image)钧舌,原理見下圖(引自Docker官網(wǎng)):

docker-image-layers-and-container.png

圖1:Docker鏡像原理

鏡像是容器的“序列化”標(biāo)準(zhǔn),這一創(chuàng)新為容器的存儲涎跨、重用和傳輸?shù)於嘶A(chǔ)洼冻。并且“坐上了巨輪”的容器鏡像可以傳播到世界每一個角落,這無疑助力了容器技術(shù)的飛速發(fā)展六敬。

Solaris Container碘赖、LXC等早期內(nèi)核容器技術(shù)不同,Docker為開發(fā)者提供了開發(fā)者體驗(yàn)良好的工具集,這其中就包括了用于鏡像構(gòu)建的Dockerfile以及一種用于編寫Dockerfile領(lǐng)域特定語言普泡。采用Dockerfile方式構(gòu)建成為鏡像構(gòu)建的標(biāo)準(zhǔn)方法播掷,其可重復(fù)、可自動化撼班、可維護(hù)以及分層精確控制等特點(diǎn)是采用傳統(tǒng)采用docker commit命令提交的鏡像所不能比擬的歧匈。

二、“鏡像是個筐”:初學(xué)者的認(rèn)知

“鏡像是個筐砰嘁,什么都往里面裝” – 這句俏皮話可能是大部分Docker初學(xué)者對鏡像最初認(rèn)知的真實(shí)寫照件炉。這里我們用一個例子來生動地展示一下。我們將httpserver.go這個源文件編譯為httpd程序并通過鏡像發(fā)布矮湘,考慮到被編譯的源碼并非本文重點(diǎn)斟冕,這里使用了一個極簡的demo代碼:

//httpserver.go

package main

import (
        "fmt"
        "net/http"
)

func main() {
        fmt.Println("http daemon start")
        fmt.Println("  -> listen on port:8080")
        http.ListenAndServe(":8080", nil)
}

接下來,我們來編寫一個用于構(gòu)建目標(biāo)imageDockerfile

From ubuntu:14.04

RUN apt-get update \
      && apt-get install -y software-properties-common \
      && add-apt-repository ppa:gophers/archive \
      && apt-get update \
      && apt-get install -y golang-1.9-go \
                            git \
      && rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"

COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
      && chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

構(gòu)建這個Image:

# docker build -t repodemo/httpd:latest .
//...構(gòu)建輸出這里省略...

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB

整個鏡像的構(gòu)建過程因環(huán)境而定缅阳。如果您的網(wǎng)絡(luò)速度一般磕蛇,這個構(gòu)建過程可能會花費(fèi)你10多分鐘甚至更多。最終如我們所愿十办,基于repodemo/httpd:latest這個鏡像的容器可以正常運(yùn)行:

# docker run repodemo/httpd
http daemon start
  -> listen on port:8080

一個Dockerfile最終生產(chǎn)出一個鏡像秀撇。Dockerfile由若干Command組成,每個Command執(zhí)行結(jié)果都會單獨(dú)形成一個layer向族。我們來探索一下構(gòu)建出來的鏡像:

# docker history 183dbef8eba6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B
27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0B
a9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB
... ...
aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       && apt-get...   356MB
.... ...
<missing>           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB

我們?nèi)コ裟切㏒ize為0或很小的layer呵燕,我們看到三個size占比較大的layer,見下圖:

docker-image-history-2.png

圖2:Docker鏡像分層探索

雖然Docker引擎利用r緩存機(jī)制可以讓同主機(jī)下非首次的鏡像構(gòu)建執(zhí)行得很快件相,但是在Docker技術(shù)熱情催化下的這種構(gòu)建思路讓docker鏡像在存儲和傳輸方面的優(yōu)勢蕩然無存再扭,要知道一個ubuntu-server 16.04的虛擬機(jī)ISO文件的大小也就不過600多MB而已。

三适肠、”理性的回歸”:builder模式的崛起

Docker使用者在新技術(shù)接觸初期的熱情“冷卻”之后迎來了“理性的回歸”霍衫。根據(jù)上面分層鏡像的圖示候引,我們發(fā)現(xiàn)最終鏡像中包含構(gòu)建環(huán)境是多余的侯养,我們只需要在最終鏡像中包含足夠支撐httpd運(yùn)行的運(yùn)行環(huán)境即可,而base image自身就可以滿足澄干。于是我們應(yīng)該去除不必要的中間層:

docker-image-history-3-1.png

圖3:去除不必要的分層

現(xiàn)在問題來了逛揩!如果不在同一鏡像中完成應(yīng)用構(gòu)建,那么在哪里麸俘、由誰來構(gòu)建應(yīng)用呢辩稽?至少有兩種方法:

  1. 在本地構(gòu)建并COPY到鏡像中;
  2. 借助構(gòu)建者鏡像(builder image)構(gòu)建从媚。

不過方法1本地構(gòu)建有很多局限性逞泄,比如:本地環(huán)境無法復(fù)用、無法很好融入持續(xù)集成/持續(xù)交付流水線等。借助builder image進(jìn)行構(gòu)建已經(jīng)成為Docker社區(qū)的一個最佳實(shí)踐喷众,Docker官方為此也推出了各種主流編程語言的官方base image各谚,比如:gojava到千、node昌渤、python以及ruby等。借助builder image進(jìn)行鏡像構(gòu)建的流程原理如下圖:

docker-image-history-3-2.png

圖4:借助builder image進(jìn)行鏡像構(gòu)建的流程圖

通過原理圖憔四,我們可以看到整個目標(biāo)鏡像的構(gòu)建被分為了兩個階段:

第一階段:構(gòu)建負(fù)責(zé)編譯源碼的構(gòu)建者鏡像膀息;
第二階段:將第一階段的輸出作為輸入,構(gòu)建出最終的目標(biāo)鏡像了赵。
我們選擇golang:1.9.2作為builder base image潜支,構(gòu)建者鏡像的Dockerfile.build如下:

// Dockerfile.build

FROM golang:1.9.2

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

執(zhí)行構(gòu)建:

# docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .

構(gòu)建好的應(yīng)用程序httpd放在了鏡像repodemo/httpd-builder中的/go/src目錄下,我們需要一些“膠水”命令來連接兩個構(gòu)建階段柿汛,這些命令將httpd從構(gòu)建者鏡像中取出并作為下一階段構(gòu)建的輸入:

# docker create --name extract-httpserver repodemo/httpd-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-builder

通過上面的命令毁腿,我們將編譯好的httpd程序拷貝到了本地。下面是目標(biāo)鏡像的Dockerfile:

//Dockerfile.target
From ubuntu:14.04

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

接下來我們來構(gòu)建目標(biāo)鏡像:

# docker build -t repodemo/httpd:latest -f Dockerfile.target .

我們來看看這個鏡像的“體格”:

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB

200MB苛茂!目標(biāo)鏡像的Size降為原來的 1/2 還多已烤。

四、“像賽車那樣減去所有不必要的東西”:追求最小鏡像

前面我們構(gòu)建出的鏡像的Size已經(jīng)縮小到200MB妓羊,但這還不夠胯究。200MB的“體格”在我們的網(wǎng)絡(luò)環(huán)境下緩存和傳輸仍然很難令人滿意。我們要為鏡像進(jìn)一步減重躁绸,減到盡可能的小裕循,就像賽車那樣,為了能減輕重量將所有不必要的東西都拆除掉:我們僅保留能支撐我們的應(yīng)用運(yùn)行的必要庫净刮、命令剥哑,其余的一律不納入目標(biāo)鏡像。當(dāng)然不僅僅是Size上的原因淹父,小鏡像還有額外的好處株婴,比如:內(nèi)存占用小,啟動速度快暑认,更加高效困介;不會因其他不必要的工具、庫的漏洞而被攻擊蘸际,減少了“攻擊面”座哩,更加安全。

docker-image-history-4-1.png

圖5:目標(biāo)鏡像還能更小些嗎粮彤?

一般應(yīng)用開發(fā)者不會從scratch鏡像從頭構(gòu)建自己的base image以及目標(biāo)鏡像的根穷,開發(fā)者會挑選適合的base image姜骡。一些“蠅量級”甚至是“草量級”的官方base image的出現(xiàn)為這種情況提供了條件。

docker-image-size.png

圖6:一些base image的Size比較(來自imagelayers.io截圖)

從圖中看屿良,我們有兩個選擇:busyboxalpine溶浴。

單從image的size上來說,busybox更小管引。不過busybox默認(rèn)的libc實(shí)現(xiàn)是uClibc士败,而我們通常運(yùn)行環(huán)境使用的libc實(shí)現(xiàn)都是glibc,因此我們要么選擇靜態(tài)編譯程序褥伴,要么使用busybox:glibc鏡像作為base image谅将。

而 alpine image 是另外一種蠅量級 base image,它使用了比 glibc 更小更安全的 musl libc 庫重慢。 不過和 busybox image 相比饥臂,alpine image 體積還是略大。除了因?yàn)?musl比uClibc 大一些之外似踱,alpine還在鏡像中添加了自己的包管理系統(tǒng)apk隅熙,開發(fā)者可以使用apk在基于alpine的鏡像中添 加需要的包或工具。因此核芽,對于普通開發(fā)者而言囚戚,alpine image顯然是更佳的選擇。不過alpine使用的libc實(shí)現(xiàn)為musl轧简,與基于glibc上編譯出來的應(yīng)用程序不兼容驰坊。如果直接將前面構(gòu)建出的httpd應(yīng)用塞入alpine,在容器啟動時(shí)會遇到下面錯誤哮独,因?yàn)榧虞d器找不到glibc這個動態(tài)共享庫文件:

standard_init_linux.go:185: exec user process caused "no such file or directory"

對于Go應(yīng)用來說拳芙,我們可以采用靜態(tài)編譯的程序,但一旦采用靜態(tài)編譯皮璧,也就意味著我們將失去一些libc提供的原生能力舟扎,比如:在linux上,你無法使用系統(tǒng)提供的DNS解析能力悴务,只能使用Go自實(shí)現(xiàn)的DNS解析器睹限。

我們還可以采用基于alpine的builder image,golang base image就提供了alpine 版本惨寿。 我們就用這種方式構(gòu)建出一個基于alpine base image的極小目標(biāo)鏡像邦泄。

docker-image-history-4-2.png

圖7:借助 alpine builder image 進(jìn)行鏡像構(gòu)建的流程圖

我們新建兩個用于 alpine 版本目標(biāo)鏡像構(gòu)建的 Dockerfile:Dockerfile.build.alpine 和Dockerfile.target.alpine:

//Dockerfile.build.alpine
FROM golang:alpine

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

// Dockerfile.target.alpine
From alpine

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

構(gòu)建builder鏡像:

#  docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
repodemo/httpd-alpine-builder    latest              d5b5f8813d77        About a minute ago   275MB

執(zhí)行“膠水”命令:

# docker create --name extract-httpserver repodemo/httpd-alpine-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-alpine-builder

構(gòu)建目標(biāo)鏡像:

# docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-alpine            latest              895de7f785dd        13 seconds ago      16.2MB

16.2MB删窒!目標(biāo)鏡像的Size降為不到原來的十分之一裂垦。我們得到了預(yù)期的結(jié)果。

五肌索、“要有光蕉拢,于是便有了光”:對多階段構(gòu)建的支持

至此,雖然我們實(shí)現(xiàn)了目標(biāo)Image的最小化,但是整個構(gòu)建過程卻是十分繁瑣晕换,我們需要準(zhǔn)備兩個Dockerfile午乓、需要準(zhǔn)備“膠水”命令、需要清理中間產(chǎn)物等闸准。作為Docker用戶益愈,我們希望用一個Dockerfile就能解決所有問題,于是就有了Docker引擎對多階段構(gòu)建(multi-stage build)的支持夷家。注意:這個特性非常新蒸其,只有Docker 17.05.0-ce及以后的版本才能支持。

現(xiàn)在我們就按照“多階段構(gòu)建”的語法將上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一個Dockerfile中:

//Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o httpd ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd

ENTRYPOINT ["/root/httpd"]

Dockerfile的語法還是很簡明和易理解的库快。即使是你第一次看到這個語法也能大致猜出六成含義摸袁。與之前Dockefile最大的不同在于在支持多階段構(gòu)建的Dockerfile中我們可以寫多個“From baseimage”的語句了,每個From語句開啟一個構(gòu)建階段义屏,并且可以通過“as”語法為此階段構(gòu)建命名(比如這里的builder)靠汁。我們還可以通過COPY命令在兩個階段構(gòu)建產(chǎn)物之間傳遞數(shù)據(jù),比如這里傳遞的httpd應(yīng)用闽铐,這個工作之前我們是使用“膠水”代碼完成的蝶怔。

構(gòu)建目標(biāo)鏡像:

# docker build -t repodemo/httpd-multi-stage .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-multi-stage       latest              35e494aa5c6f        2 minutes ago       16.2MB

我們看到通過多階段構(gòu)建特性構(gòu)建的Docker Image與我們之前通過builder模式構(gòu)建的鏡像在效果上是等價(jià)的。

六兄墅、來到現(xiàn)實(shí)

沿著時(shí)間的軌跡添谊,Docker 鏡像構(gòu)建走到了今天。追求又快又小的鏡像已成為了 Docker 社區(qū) 的共識察迟。社區(qū)在自創(chuàng) builder 鏡像構(gòu)建的最佳實(shí)踐后終于迎來了多階段構(gòu)建這柄利器斩狱,從此構(gòu)建 出極簡的鏡像將不再困難。

七扎瓶、總結(jié)

所以所踊,我們看過了Docker鏡像構(gòu)建的這個過程,也了解到了我們?nèi)绾我徊讲綁嚎s鏡像體積的方法概荷,不過對于正式環(huán)境來說秕岛,鏡像越小并不意味著越好,因?yàn)樾◇w積的鏡像是有可能會導(dǎo)致一些語言的適配性不是很好误证,這點(diǎn)還是需要大量測試之后才能正式使用继薛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市愈捅,隨后出現(xiàn)的幾起案子遏考,更是在濱河造成了極大的恐慌,老刑警劉巖蓝谨,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灌具,死亡現(xiàn)場離奇詭異青团,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)咖楣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進(jìn)店門督笆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诱贿,你說我怎么就攤上這事娃肿。” “怎么了珠十?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵咸作,是天一觀的道長。 經(jīng)常有香客問我宵睦,道長记罚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任壳嚎,我火速辦了婚禮桐智,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘烟馅。我一直安慰自己说庭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布郑趁。 她就那樣靜靜地躺著刊驴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寡润。 梳的紋絲不亂的頭發(fā)上捆憎,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天,我揣著相機(jī)與錄音梭纹,去河邊找鬼躲惰。 笑死,一個胖子當(dāng)著我的面吹牛变抽,可吹牛的內(nèi)容都是我干的础拨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼绍载,長吁一口氣:“原來是場噩夢啊……” “哼诡宗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起击儡,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤塔沃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后曙痘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芳悲,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡立肘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年边坤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了名扛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡茧痒,死狀恐怖肮韧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旺订,我是刑警寧澤弄企,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站区拳,受9級特大地震影響拘领,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜樱调,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一约素、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笆凌,春花似錦圣猎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至爪模,卻和暖如春欠啤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背屋灌。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工跪妥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人声滥。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓眉撵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親落塑。 傳聞我的和親對象是個殘疾皇子纽疟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,435評論 2 348

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