Docker 中的應(yīng)用為什么沒有 Graceful Shutdown

下面是兩個 Dockerfile 文件,我們來看看他們之間的區(qū)別是什么私痹?會對運行中的容器產(chǎn)生什么樣的影響侄榴?

第一個癞蚕,執(zhí)行入口以 exec 形式啟動一個 Spring Boot 應(yīng)用程序桦山。

FROM frolvlad/alpine-java:jdk8-slim

RUN set -eux && mkdir -p /home/
RUN set -eux && mkdir -p /home/auth-server
RUN set -eux && mkdir -p /opt/logs/auth-server
RUN set -eux && touch /opt/logs/auth-server/auth-server.log
ADD auth-server.jar /home/auth-server/auth-server.jar

ENV TZ=Asia/Shanghai
ENV JAVA_ENV="-Denv=docker"
ENV JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"
ENTRYPOINT [ "sh", "-c", "java $JAVA_ENV $JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar" ]

第二個会放,執(zhí)行入口以 exec 形式執(zhí)行一個 shell 腳本咧最,因為容器中可能還需要運行一些日志收集矢沿、鏈路監(jiān)控的程序捣鲸。所以容器運行時通過執(zhí)行一個 shell 腳本來一起啟動這些程序栽惶。

FROM frolvlad/alpine-java:jdk8-slim

RUN set -eux && mkdir -p /home/
RUN set -eux && mkdir -p /home/auth-server
RUN set -eux && mkdir -p /opt/logs/auth-server
RUN set -eux && touch /opt/logs/auth-server/auth-server.log
ADD auth-server.jar /home/auth-server/auth-server.jar

COPY entrypoint.sh /home/auth-server/entrypoint.sh
RUN chmod +x /home/auth-server/entrypoint.sh

ENTRYPOINT ["/home/auth-server/entrypoint.sh"]

entrypoint.sh

#!/bin/sh
ENV="-Denv=docker"

export JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"
export JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom"

java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar

echo "java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar"
echo "start success"

區(qū)別就在于 Dockerfile 中的執(zhí)行入口 ENTRYPOINT 的參數(shù)不同代承,通過這兩個 Dockerfile 制作的鏡像次泽,在容器運行時又有什么區(qū)別呢?下面是兩個鏡像玖像,啟動容器后里面的進程信息。

  • ENTRYPOINT 執(zhí)行 java -jar 啟動應(yīng)用程序
/ # ps aux
PID   USER   TIME  COMMAND
    1 root   0:10 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
   25 root   0:00 /bin/sh
   34 root   0:00 ps aux
  • ENTRYPOINT 執(zhí)行 shell 腳本啟動應(yīng)用程序
/ # ps aux
PID   USER   TIME  COMMAND
    1 root   0:00 /bin/sh /home/auth-server/entrypoint.sh
    6 root   0:15 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
   76 root   0:00 /bin/sh
   81 root   0:00 ps aux

區(qū)別就在于誰是容器里的 1 號進程祖驱。 容器里的 1 號進程和非 1 號進程又有什么區(qū)別握恳?

如果使用過 Kubernetes,應(yīng)該知道 Kubernetes 并沒有提供重啟 Pod 的命令捺僻,只能通過 kubectl apply 來重建 Pod乡洼,而一般研發(fā)的操作發(fā)布入口崇裁,都是通過 Jenkins 工具自動構(gòu)建一個新的鏡像到 Harbor,然后再自動發(fā)布到 Kubernetes 平臺來重建應(yīng)用程序束昵。

想重啟一下應(yīng)用程序拔稳,而且使用的是單進程模式,應(yīng)用進程就是容器里的 1 號進程锹雏,在不讓運維介入的情況下礁遵,那你可能必須走上面的 Jenkins 發(fā)布流程晰赞。

如果你們的 Dockerfile 鏡像模板使用的是上述第二種方式,也就是說應(yīng)用進程并不是容器中的 1 號進程褐墅,則還有另外一種方式逝钥,就是通過在 Dashboard 界面進入到 Pod 里蜘欲,手動 kill 掉應(yīng)用進程,這時配合 Kubernetes 的存活探針 livenessProbe 可以達到重啟 Pod 的效果,而且這個 Pod 的 IP 不會變化箱硕。

livenessProbe:
  tcpSocket:
    port: 9096
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3
  successThreshold: 1
  timeoutSeconds: 10   

Liveness 指針是存活探針惠昔,它用來判斷容器是否存活、判斷 pod 是否 running啦扬。如果 Liveness 指針判斷容器不健康勤晚,此時會通過 kubelet 殺掉相應(yīng)的 pod揉忘,并根據(jù)重啟策略來判斷是否重啟這個容器狂丝。如果默認不配置 Liveness 指針,則默認情況下認為它這個探測默認返回是成功的哈蝇。

單進程模式下性芬,應(yīng)用進程就是容器中的 1 號進程鸡挠,不能通過 kill 1 來實現(xiàn)嗎姓惑?你可以嘗試一下,不管是通過 kill -9 還是 kill -15踊兜,這個 1 號進程都是殺不死的筷频。

生產(chǎn)環(huán)境建議容器都是單進程,應(yīng)用進程既是容器的主進程(1號進程)示罗。

上述兩種容器運行的方式禽额,對于 Kubernetes 平臺中 Pod 的滾動更新,或者僅僅用 Docker 時馍佑,容器的 Restart 會對服務(wù)產(chǎn)生什么樣的影響雏亚?

先來介紹下在 linux 中兩個終止進程的命令:kill -9 pid 和 kill -15 pid遣铝,代表兩種信號 SIGKILL 和 SIGTERM。

[root@ ~]# kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
......略

SIGTERM 是軟終止莉擒,SIGKILL 用于立即終止進程酿炸,他們都可以用于終止程序,但是是有區(qū)別的:

  1. SIGTERM 優(yōu)雅的終止進程涨冀,而 SIGKILL 會立即終止進程填硕。
  2. SIGTERM 信號可以處理、忽略和阻止,而 SIGKILL 不能被處理或阻止扁眯。
  3. SIGTERM 不會殺死子進程壮莹。SIGKILL 會殺死子進程。

我們以一個 Spring Cloud 服務(wù)舉例姻檀,服務(wù)啟動后注冊到 Eureka Server命满,當服務(wù)停止時通知 Eureka Server 下線注銷實例(這個有一個前提,服務(wù)必須是優(yōu)雅停止 graceful shutdown)绣版,服務(wù)下線通知是怎么實現(xiàn)的胶台。如下:

@Singleton
public class DiscoveryClient implements EurekaClient {

   /**
     * Shuts down Eureka Client. Also sends a deregistration request to the
     * eureka server.
     */
    @PreDestroy
    @Override
    public synchronized void shutdown() {
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");

            ......
            logger.info("Completed shut down of DiscoveryClient");
        }
    }
}

就是通過 @PreDestroy 注解,在 Bean 實例銷毀之前做一些操作杂抽,對于 DiscoverClient 來說就是在 shutdown 時發(fā)送一個 HTTP 請求Sending request: DELETE /eureka/apps/API-GATEWAY/{instance-id} HTTP/1.1 主動通知一下 Eureka Server 自己要下線了诈唬。接下來經(jīng)過多次同步之后,其它客戶端感知到服務(wù)下線缩麸。

如果通過 kill -9 來強制殺死應(yīng)用讯榕,Spring Boot 應(yīng)用就來不及做這些善后工作,直接被終止了匙睹。

Docker 提供了有兩種方式來停止容器:docker stop 和 docker kill

  • docker stop:容器內(nèi)的主進程(PID為1的進程)將收到 SIGTERM 信號愚屁,如果在寬限時間后(默認 10s)進程還沒有退出,將發(fā)送 SIGKILL 信號痕檬。使用 docker stop 時霎槐,docker 守護進程在發(fā)送 SIGKILL 信號之前等待的秒數(shù)是可以控制的,參數(shù)如下:

    Name, shorthand Default Description
    --time , -t 10 Seconds to wait for stop before killing it
  • docker kill:默認向容器內(nèi)的主進程(1號進程)發(fā)送 SIGKILL 信號梦谜,或者用 --signal 選項指定的信號丘跌。也就是說默認情況下,docker kill 命令不會給容器進程一個優(yōu)雅地退出的機會唁桩,它只是發(fā)出一個 SIGKILL 信號來終止容器闭树。但是,它有一個 --signal 入?yún)⒒脑瑁梢韵蛉萜鬟M程發(fā)送 SIGKILL 以外的信號报辱。

    Name, shorthand Default Description
    --signal , -s KILL Signal to send to the container

對于 Kubernetes 平臺來說,Pod 銷毀的寬限時間默認是 30s单山。通過 terminationGracePeriodSeconds: 30 參數(shù)設(shè)置 碍现。如果容器在寬限期后仍在運行,SIGKILL 將強制移除 Pod米奸,終止操作完成昼接。

下面通過命令看下上面兩種 Dockerfile 文件構(gòu)建的鏡像,在容器停止時悴晰,容器內(nèi)進程接收到的信號有什么區(qū)別慢睡。

使用 docker top auth-server 命令可以查看容器內(nèi)進程在宿主機上的 PID 號负拟。

第一種方式啟動的容器:應(yīng)用進程在宿主機上的 PID = 6184

[root@ dockerfile]# docker top auth-server
UID    PID   PPID    C    STIME    TTY   TIME       CMD
root  6184   6168    22   00:26    ?     00:00:15   java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar

第二種腳本方式啟動的容器:應(yīng)用進程在宿主機上的 PID = 6716

[root@ dockerfile]# docker top auth-server
UID    PID    PPID   C    STIME    TTY   TIME       CMD
root   6698   6682   0    00:30    ?     00:00:00   /bin/sh /home/eureka-server/entrypoint.sh
root   6716   6698   95   00:30    ?     00:00:11   java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar

docker stop auth-server 在停止容器的同時出吹,使用 strace -p PID 來觀察容器內(nèi)進程接收到的信號情況多搀。

  • strace -p 6184(ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Dfile......")
[root@ eureka]# strace -p 6184
strace: Process 6184 attached
futex(0x7fbd15b2a9d0, FUTEX_WAIT, 6, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
futex(0x7fbd14ef1580, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn({mask=[]})                 = 202
futex(0x7fbd15b2a9d0, FUTEX_WAIT, 6, NULL <unfinished ...>
+++ exited with 143 +++
  • strace -p 6698(ENTRYPOINT ["/home/auth-server/entrypoint.sh"])
[root@ eureka-server]# strace -p 6698
strace: Process 6698 attached
wait4(-1, 0x7fffd3f084cc, 0, NULL)      = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
wait4(-1, 0x7fffd3f084cc, 0, NULL)      = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
+++ killed by SIGKILL +++
  • strace -p 6716(ENTRYPOINT ["/home/auth-server/entrypoint.sh"])
[root@ eureka]# strace -p 6716
strace: Process 6716 attached
futex(0x7fa3569749d0, FUTEX_WAIT, 7, NULL <unfinished ...>
+++ killed by SIGKILL +++
PID SIGTERM SIGKILL
6184(容器內(nèi) PID = 1)
6698(容器內(nèi) PID = 1 )/bin/sh /home/auth-server/entrypoint.sh
6716(容器內(nèi) PID = 6)

6184 接收到了 SIGTERM 信號彼水,而 6716 只接收到了 SIGKILL 信號被強制殺死翔曲,應(yīng)用進程沒有機會去做一些@PreDestroy 的善后操作搀突;但是 6698 先是接收到了 SIGTERM 而后通過 SIGKILL 被殺死萎攒。

也就是說 docker stop 向容器發(fā)送信號時让歼,SIGTERM 信號僅發(fā)送到 PID = 1 的容器進程饲帅。

由于 /bin/sh 不將信號轉(zhuǎn)發(fā)給任何子進程复凳,應(yīng)用進程接收不到 docker stop <container>發(fā)出的 SIGTERM 信號,只能等寬限時間結(jié)束后被強制終止灶泵,這樣我們的應(yīng)用程序就不能 graceful shutdown 優(yōu)雅終止育八。</container>

對于 Eureka Client 來說,會造成在更嚴重的下線通知延遲赦邻。

在非 graceful shutdown 情況下髓棋,客戶端不會調(diào)用 Eureka API 來更新 registry 注冊列表,而是只能等 Eureka Server 定時清理無效節(jié)點惶洲,這個周期默認是 60s按声,續(xù)約超時的時間默認是 90s,也就是說服務(wù)下線后恬吕,可能需要延遲 150s 之后签则,Eureka Server 中的 registry 對象才會被更新。而后還要經(jīng)過多輪同步铐料,客戶端才能感知到渐裂。

如果不可避免的要在容器里運行多個進程,能讓 1 號 init 進程在收到 SIGTERM 信號的同時钠惩,轉(zhuǎn)發(fā)給其它進程柒凉,就可以解決應(yīng)用非 graceful shutdown 的問題。

下面介紹一種方法來解決上面這個問題篓跛,既要保證應(yīng)用進程可以接收到 SIGTERM 信號膝捞,還要可以在容器內(nèi)手動 kill 掉應(yīng)用進程(這樣可以配合 Kubernetes 的存活探針達到重啟 Pod 的效果)。

使用 Tini 作為 init 進程愧沟。tini 會把它接收到的所有信號都轉(zhuǎn)發(fā)給它的子進程绑警,這正是我們想要的。

將 Dockerfile 文件 和 entrypoint.sh 腳本稍微改造一下:

Dcokerfile 中添加安裝 tini 語句央渣,ENTRYPOINT 使用 Tini 作為 init 進程计盒。

FROM frolvlad/alpine-java:jdk8-slim

RUN set -eux && mkdir -p /home/
RUN set -eux && mkdir -p /home/auth-server
RUN set -eux && mkdir -p /opt/logs/auth-server
RUN set -eux && touch /opt/logs/auth-server/auth-server.log
ADD auth-server.jar /home/auth-server/auth-server.jar

COPY entrypoint.sh /home/auth-server/entrypoint.sh
RUN chmod +x /home/auth-server/entrypoint.sh

ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini

ENTRYPOINT ["/tini", "--", "/home/auth-server/entrypoint.sh"]

使用 exec 方式啟動可執(zhí)行程序,它會替換掉當前 /bin/sh 進程芽丹,并保持 PID 不變北启。

#!/bin/sh
ENV="-Denv=docker"

export JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"
export JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom"

exec java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar

echo "java ${ENV} $JAVA_OPTS -jar /home/auth-server/auth-server.jar"
echo "start success"

下面是改造之后的容器內(nèi)的進程信息:

[root@ dockerfile]# docker exec -it auth-server /bin/sh
/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /tini -- /home/auth-server/entrypoint.sh
    6 root      0:12 java -Denv=docker -server -Xmx512m -Xms512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/auth-server/auth-server.jar
   30 root      0:00 /bin/sh
   35 root      0:00 ps aux

再用 strace 監(jiān)測下 docker stop auth-server 時應(yīng)用進程收到的信號,下面可以看到應(yīng)用進程收到了 SIGTERM 信號。

[root@iZ2zece2l8yr2f8qhrnr3lZ ~]# strace -p 1336
strace: Process 1336 attached
futex(0x7f56967149d0, FUTEX_WAIT, 7, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=1, si_uid=0} ---
futex(0x7f5695adb580, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn({mask=[]})                 = 202
futex(0x7f56967149d0, FUTEX_WAIT, 7, NULL <unfinished ...>
+++ exited with 143 +++

應(yīng)用收到 SIGTERM 信號后咕村,就會做一些終止時的善后操作场钉,下面就是通知 Eureka Server 服務(wù)下線。

INFO  [c.n.eureka.DefaultEurekaServerContext  ] - Shutting down ...
INFO  [c.n.eureka.DefaultEurekaServerContext  ] - Shut down
INFO  [com.netflix.discovery.DiscoveryClient  ] - Shutting down DiscoveryClient ...
INFO  [com.netflix.discovery.DiscoveryClient  ] - Completed shut down of DiscoveryClient

很多開源項目的官方鏡像中都使用了這種方式懈涛,例如:

使用 tini 的基礎(chǔ)鏡像:

注意: 編寫 shell 腳本時需注意逛万,要讓腳本始終處于運行狀態(tài),因為 Docker 容器僅在 1 號進程運行時才保持運行 批钠,1 號進程退出宇植,Docker 容器也將退出。如果配置了 restart: always埋心,你會發(fā)現(xiàn)容器一直在嘗試重啟指郁。

微服務(wù)演示項目中:auth-server 服務(wù)我是采用 tini 作為 init 進程來構(gòu)建的鏡像,你可以在 Kubernetes 平臺或者 Docker Compose 中嘗試上面所描述的問題拷呆。

~ END ~闲坎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者茬斧。
  • 序言:七十年代末腰懂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子项秉,更是在濱河造成了極大的恐慌悯恍,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伙狐,死亡現(xiàn)場離奇詭異涮毫,居然都是意外死亡,警方通過查閱死者的電腦和手機贷屎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門罢防,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唉侄,你說我怎么就攤上這事咒吐。” “怎么了属划?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵恬叹,是天一觀的道長。 經(jīng)常有香客問我同眯,道長绽昼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任须蜗,我火速辦了婚禮硅确,結(jié)果婚禮上目溉,老公的妹妹穿的比我還像新娘。我一直安慰自己菱农,他們只是感情好缭付,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著循未,像睡著了一般陷猫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上的妖,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天绣檬,我揣著相機與錄音,去河邊找鬼羔味。 笑死,一個胖子當著我的面吹牛钠右,可吹牛的內(nèi)容都是我干的赋元。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼飒房,長吁一口氣:“原來是場噩夢啊……” “哼搁凸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起狠毯,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤护糖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嚼松,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫡良,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年献酗,在試婚紗的時候發(fā)現(xiàn)自己被綠了寝受。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡罕偎,死狀恐怖很澄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颜及,我是刑警寧澤甩苛,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站俏站,受9級特大地震影響讯蒲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肄扎,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一爱葵、第九天 我趴在偏房一處隱蔽的房頂上張望施戴。 院中可真熱鬧,春花似錦萌丈、人聲如沸赞哗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肪笋。三九已至,卻和暖如春度迂,著一層夾襖步出監(jiān)牢的瞬間藤乙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工惭墓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坛梁,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓腊凶,卻偏偏與公主長得像划咐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钧萍,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

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