YARN基本架构和设计理念

YARN 是在 MRv1 基础上演化而来的,它克服了 MRv1 中的各种局限性。在正式介绍 YARN 之前,我们先要了解 MRv1 的一些局限性,这可概括为以下几个方面:

  • 扩展性差。在 MRv1 中,JobTracker 同时兼备了资源管理和作业控制两个功能,这 成为系统的一个最大瓶颈,严重制约了 Hadoop 集群扩展性。

  • 可靠性差。MRv1 采用了 master/slave 结构,其中,master 存在单点故障问题,一旦 它出现故障将导致整个集群不可用。

  • 资源利用率低。MRv1 采用了基于槽位的资源分配模型,槽位是一种粗粒度的资源 划分单位,通常一个任务不会用完槽位对应的资源,且其他任务也无法使用这些空 闲资源。此外,Hadoop 将槽位分为 Map Slot 和 Reduce Slot 两种,且不允许它们之 间共享,常常会导致一种槽位资源紧张而另外一种闲置(比如一个作业刚刚提交时, 只会运行 Map Task,此时 Reduce Slot 闲置)。

  • 无法支持多种计算框架。随着互联网高速发展,MapReduce 这种基于磁盘的离线计 算框架已经不能满足应用要求,从而出现了一些新的计算框架,包括内存计算框架、 流式计算框架和迭代式计算框架等,而 MRv1 不能支持多种计算框架并存。

为了克服以上几个缺点,Apache 开始尝试对 Hadoop 进行升级改造,进而诞生了更加 先进的下一代 MapReduce 计算框架 MRv2。正是由于 MRv2 将资源管理功能抽象成了一个 独立的通用系统 YARN,直接导致下一代 MapReduce 的核心从单一的计算框架 MapReduce转移为通用的资源管理系统 YARN。

轻量级弹性计算平台

随着互联网的高速发展,基于数据密集型应用的计算框架不断出现,从支持离线处理 的 MapReduce,到支持在线处理的 Storm,从迭代式计算框架 Spark 到流式处理框架 S4, 各种框架诞生于不同的公司或者实验室,它们各有所长,各自解决了某一类应用问题。而 在大部分互联网公司中,这几种框架可能同时被采用。比如在搜索引擎公司中,一种可能 的技术方案如下:网页建立索引采用 MapReduce 框架,自然语言处理 / 数据挖掘采用 Spark
(如网页 PageRank 计算、聚类分类算法等),对性能要求很高的数据挖掘算法用 MPI 等。 考虑到资源利用率、运维成本、数据共享等因素,公司一般希望将所有这些框架都部署到 一个公共的集群中,让它们共享集群的资源,并对资源进行统一使用,同时采用某种资源 隔离方案(如轻量级 cgroups)对各个任务进行隔离,这样便诞生了轻量级弹性计算平台, 如图 2-2 所示。YARN 便是弹性计算平台的典型代表。

image.png

从上面分析可知,YARN 实际上是一个弹性计算平台,它的目标已经不再局限于支持 MapReduce 一种计算框架,而是朝着对多种框架进行统一管理的方向发展。
相比于“一种计算框架一个集群”的模式,共享集群的模式存在多种好处:

  • 资源利用率高。如图 2-3 所示,如果每个框架一个集群,则往往由于应用程序数量 和资源需求的不均衡性,使得在某段时间内,有些计算框架的集群资源紧张,而另 外一些集群资源空闲。共享集群模式则通过多种框架共享资源,使得集群中的资源得到更加充分的利用。
  • 运维成本低。如果采用“一个框架一个集群”的模式,则可能需要多个管理员管理
    这些集群,进而增加运维成本,而共享模式通常需要少数管理员即可完成多个框架 的统一管理。
  • 数据共享。随着数据量的暴增,跨集群间的数据移动不仅需花费更长的时间,且硬 件成本也会大大增加,而共享集群模式可让多种框架共享数据和硬件资源,将大大 减小数据移动带来的成本。

术语解释

(1)Hadoop 1.0

Hadoop 1.0 即第一代 Hadoop,由分布式存储系统 HDFS 和分布式计算框架 MapReduce
组成,其中,HDFS 由一个 NameNode 和多个 DataNode 组成,MapReduce 由一个 JobTracker 和多个 TaskTracker 组成,对应 Hadoop 版本为 Apache Hadoop 0.20.x、1.x、0.21.X、0.22.x 和 CDH3 。

(2)Hadoop 2.0
Hadoop 2.0 即第二代 Hadoop,为克服 Hadoop 1.0 中 HDFS 和 MapReduce 存在的各种问题而提出的。如图 2-4 所示,针对 Hadoop 1.0 中的单 NameNode 制约 HDFS 的扩展性问 题,提出了 HDFS Federation,它让多个 NameNode 分管不同的目录进而实现访问隔离和横 向扩展,同时它彻底解决了 NameNode 单点故障问题 ;针对 Hadoop 1.0 中的 MapReduce 在扩展性和多框架支持等方面的不足,它将 JobTracker 中的资源管理和作业控制功能分 开, 分 别 由 组件ResourceManager 和 ApplicationMaster 实 现, 其 中,ResourceManager 负责所有应用程序的资源分配,而 ApplicationMaster 仅负责管理一个应用程序,进而诞 生了全新的通用资源管理框架 YARN。基于 YARN,用户可以运行各种类型的应用程序(不再像 1.0 那样仅局限于 MapReduce 一类应用),从离线计算的 MapReduce 到在线计算 (流式处理)的 Storm 等。Hadoop 2.0 对应 Hadoop 版本为 Apache Hadoop 0.23.x、2.x 和CDH4 。

image.png

(3)MapReduce 1.0 或 MRv1

MapReduce 1.0 计算框架主要由三部分组成,分别是编程模型、数据处理引擎和运行时环境。它的基本编程模型是将问题抽象成 Map 和 Reduce 两个阶段,其中 Map 阶段将输 入数据解析成 key/value,迭代调用 map() 函数处理后,再以 key/value 的形式输出到本地 目录,而 Reduce 阶段则将 key 相同的 value 进行规约处理,并将最终结果写到 HDFS 上 ; 它的数据处理引擎由 MapTask 和 ReduceTask 组成,分别负责 Map 阶段逻辑和 Reduce 阶段逻辑的处理 ;它的运行时环境由(一个)JobTracker 和(若干个)TaskTracker 两类服务 组成,其中,JobTracker 负责资源管理和所有作业的控制,而 TaskTracker 负责接收来自 JobTracker 的命令并执行它。该框架在扩展性、容错性和多框架支持等方面存在不足,这也 促使了 MRv2 的产生。
(4)MRv2
MRv2 具有与 MRv1 相同的编程模型和数据处理引擎,唯一不同的是运行时环境。MRv2是在 MRv1 基础上经加工之后,运行于资源管理框架 YARN 之上的计算框架 MapReduce。 它的运行时环境不再由 JobTracker 和 TaskTracker 等服务组成,而是变为通用资源管理 系统 YARN 和作业控制进程 ApplicationMaster,其中,YARN 负责资源管理和调度,而 ApplicationMaster 仅负责一个作业的管理。简言之,MRv1 仅是一个独立的离线计算框架, 而 MRv2 则是运行于 YARN 之上的 MapReduce。
(5)YARN
YARN 是 Hadoop 2.0 中的资源管理系统,它是一个通用的资源管理模块,可为各类应用程序进行资源管理和调度。YARN 不仅限于 MapReduce 一种框架使用,也可以供其他框 架使用,比如 Tez、Spark、Storm。由于 YARN 的通用性,下一代 MapReduce 的核心已经从简单的支持单一应用的计算框架 MapReduce 转移到通用的资源管理系统 YARN。

(6)HDFS Federation
Hadoop 2.0 中对 HDFS 进行了改进,使 NameNode 可以横向扩展成多个,每个 NameNode分管一部分目录,进而产生了 HDFS Federation,该机制的引入不仅增强了 HDFS 的扩展性, 也使 HDFS 具备了隔离性。

YARN 基本设计思想

在 Hadoop 1.0 中,JobTracker 由资源管理(由 TaskScheduler 模块实现)和作业控制(由 JobTracker 中多个模块共同实现)两部分组成,具体如图 2-6 所示。当前 Hadoop MapReduce 之所以在可扩展性、资源利用率和多框架支持等方面存在不足,正是由于 Hadoop 对 JobTracker 赋予的功能过多而造成负载过重。此外,从设计角度上看,Hadoop 未能够将资 源管理相关的功能与应用程序相关的功能分开,造成 Hadoop 难以支持多种计算框架。

image.png

下一代 MapReduce 框架的基本设计思想是将 JobTracker 的两个主要功能,即资源管理和作业控制(包括作业监控、容错等),分拆成两独立的进程,如图 2-7 所示。资源管理进 程与具体应用程序无关,它负责整个集群的资源(内存、CPU、磁盘等)管理,而作业控 制进程则是直接与应用程序相关的模块,且每个作业控制进程只负责管理一个作业。这样, 通过将原有 JobTracker 中与应用程序相关和无关的模块分开,不仅减轻了 JobTracker 负载, 也使得 Hadoop 支持更多的计算框架。

image.png

从资源管理角度看,下一代 MapReduce 框架实际上衍生出了一个资源统一管理平台 YARN,它使得 Hadoop 不再局限于仅支持 MapReduce 一种计算模型,而是可无限融入多 种计算框架,且对这些框架进行统一管理和调度。

编程模型对比

前面提到 MRv1 主要由编程模型(由新旧 API 组成)、数据处理引擎(由 MapTask 和 ReduceTask 组成)和运行时环境(由一个 JobTracker 和若干个 TaskTracker 组成)三部分 组成,为了保证编程模型的向后兼容性,MRv2 重用了 MRv1 中的编程模型和数据处理引 擎,但运行时环境被完全重写。

MRv1 的运行时环境主要由两类服务组成,分别是 JobTracker 和
TaskTracker。其中,JobTracker 负责资源和任务的管理与调度,TaskTracker 负责单 个节点的资源管理和任务执行。MRv1 将资源管理和应用程序管理两部分混杂在一 起,使得它在扩展性、容错性和多框架支持等方面存在明显缺陷。而 MRv2 则通过 将资源管理和应用程序管理两部分剥离开,分别由 YARN 和 ApplicationMaster 负 责,其中,YARN 专管资源管理和调度,而 ApplicationMaster 则负责与具体应用程 序相关的任务切分、任务调度和容错等,具体如图 2-8 所示。


image.png
YARN 基本架构

YARN 是 Hadoop 2.0 中的资源管理系统,它的基本设计思想是将 MRv1 中的 JobTracker 拆 分 成 了 两 个 独 立 的服务 :一个全局的资源管理器 ResourceManager 和每个应用程序特有的 ApplicationMaster。其中 ResourceManager 负责整个系统的资源管理和分配,而 ApplicationMaster 负责单个应用程序的管理。

YARN 基本组成结构
YARN 总体上仍然是 Master/Slave 结构,在整个资源管理框架中,ResourceManager 为 Master,NodeManager 为 Slave,ResourceManager 负责对各个 NodeManager 上的资源进行 统一管理和调度。当用户提交一个应用程序时,需要提供一个用以跟踪和管理这个程序的 ApplicationMaster,它负责向 ResourceManager 申请资源,并要求 NodeManger 启动可以占 用一定资源的任务。由于不同的 ApplicationMaster 被分布到不同的节点上,因此它们之间 不会相互影响。
图 2-9 描述了 YARN 的基本组成结构,YARN 主要由 ResourceManager、NodeManager、 ApplicationMaster(图中给出了 MapReduce 和 MPI 两种计算框架的 ApplicationMaster,分 别为 MR AppMstr 和 MPI AppMstr)和 Container 等几个组件构成。

image.png

  1. ResourceManager(RM)
    RM 是一个全局的资源管理器,负责整个系统的资源管理和分配。它主要由两个组件 构成:调度器(Scheduler)和应用程序管理器(Applications Manager,ASM)。

(1)调度器
调度器根据容量、队列等限制条件(如每个队列分配一定的资源,最多执行一定数量的作业等),将系统中的资源分配给各个正在运行的应用程序。需要注意的是,该调度器是 一个“纯调度器”,它不再从事任何与具体应用程序相关的工作,比如不负责监控或者跟踪 应用的执行状态等,也不负责重新启动因应用执行失败或者硬件故障而产生的失败任务, 这些均交由应用程序相关的 ApplicationMaster 完成。调度器仅根据各个应用程序的资源需 求进行资源分配,而资源分配单位用一个抽象概念“资源容器”(Resource Container,简 称 Container)表示,Container 是一个动态资源分配单位,它将内存、CPU、磁盘、网络等 资源封装在一起,从而限定每个任务使用的资源量。此外,该调度器是一个可插拔的组件, 用户可根据自己的需要设计新的调度器,YARN 提供了多种直接可用的调度器,比如 Fair Scheduler 和 Capacity Scheduler 等。

(2)应用程序管理器
应用程序管理器负责管理整个系统中所有应用程序,包括应用程序提交、与调度器协商资源以启动 ApplicationMaster、监控 ApplicationMaster 运行状态并在失败时重新启动它等。

  1. ApplicationMaster(AM)
    用户提交的每个应用程序均包含一个 AM,主要功能包括:
  • 与 RM 调度器协商以获取资源(用 Container 表示);
  • 与 NM 通信以启动 / 停止任务;
  • 监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务。

当 前 YARN 自 带 了 两 个 AM 实 现, 一 个 是 用 于 演 示 AM 编 写 方 法 的 实 例 程 序distributedshell,它可以申请一定数目的 Container 以并行运行一个 Shell 命令或者 Shell 脚本 ; 另一个是运行 MapReduce 应用程序的 AM— MRAppMaster。

  1. NodeManager(NM)

NM 是每个节点上的资源和任务管理器,一方面,它会定时地向 RM 汇报本节点上的 资源使用情况和各个 Container 的运行状态;另一方面,它接收并处理来自 AM 的 Container 启动 / 停止等各种请求。

  1. Container
    Container 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、 CPU、磁盘、网络等,当 AM 向 RM 申请资源时,RM 为 AM 返回的资源便是用 Container 表示的。YARN 会为每个任务分配一个 Container,且该任务只能使用该 Container 中描述的 资源。需要注意的是,Container 不同于 MRv1 中的 slot,它是一个动态资源划分单位,是 根据应用程序的需求动态生成的。目前YARN 仅支持 CPU 和内存两种资源, 且使用了轻量级资源隔离机制 Cgroups 进行资源隔离
YARN 通信协议

RPC 协议是连接各个组件的“大动脉”,了解不同组件之间的 RPC 协议有助于我们更 深入地学习 YARN 框架。在 YARN 中,任何两个需相互通信的组件之间仅有一个 RPC 协 议,而对于任何一个 RPC 协议,通信双方有一端是 Client,另一端为 Server,且 Client 总 是主动连接 Server 的,因此,YARN 实际上采用的是拉式(pull-based)通信模型。如图 2-10 所示,箭头指向的组件是 RPC Server,而箭头尾部的组件是 RPC Client,YARN 主要由以 下几个 RPC 协议组成

  • JobClient(作业提交客户端)与 RM 之间的协议 — ApplicationClientProtocol : JobClient 通过该 RPC 协议提交应用程序、查询应用程序状态等。
  • Admin(管理员)与 RM 之间的通信协议— ResourceManagerAdministrationProtocol : Admin 通过该 RPC 协议更新系统配置文件,比如节点黑白名单、用户队列权限等。
  • AM 与 RM 之间的协议— ApplicationMasterProtocol :AM 通过该 RPC 协议向 RM 注册和撤销自己,并为各个任务申请资源。
  • AM 与 NM 之间的协议 — ContainerManagementProtocol :AM 通过该 RPC 要求 NM 启动或者停止 Container,获取各个 Container 的使用状态等信息。
  • NM 与 RM 之间的协议— ResourceTracker :NM 通过该 RPC 协议向 RM 注册,并 定时发送心跳信息汇报当前节点的资源使用情况和 Container 运行情况。
image.png

为了提高 Hadoop 的向后兼容性和不同版本之间的兼容性,YARN 中的序列化框架采用 了 Google 开源的 Protocol Buffers。Protocol Buffers 的引入使得 YARN 具有协议向后兼容性。

YARN 工作流程

运行在 YARN 上的应用程序主要分为两类 :短应用程序和长应用程序,其中,短应用程序是指一定时间内(可能是秒级、分钟级或小时级,尽管天级别或者更长时间的也存在,但非常少)可运行完成并正常退出的应用程序,比如 MapReduce 作业,长应用程序是指不出意外,永不终止运行的应用程序,通常是一些服务,比如 Storm Service,HBase Service(包括 Hmaster 和 RegionServer 两类服务)等。尽管这两类应用程序作用不同,一类直接运行数据处理
程序,一类用于部署服务(服务之上再运行数据处理程序),但运行在 YARN 上的流程是相同的。

当用户向 YARN 中提交一个应用程序后,YARN 将分两个阶段运行该应用程序 :第一 个阶段是启动 ApplicationMaster ;第二个阶段是由 ApplicationMaster 创建应用程序,为它 申请资源,并监控它的整个运行过程,直到运行完成。如图 2-11 所示,YARN 的工作流程 分为以下几个步骤:
步骤 1 用户向 YARN 中提交应用程序,其中包括 ApplicationMaster 程序、启动 ApplicationMaster 的命令、用户程序等。
步骤 2 ResourceManager 为该应用程序分配第一个 Container,并与对应的 Node- Manager 通信,要求它在这个 Container 中启动应用程序的 ApplicationMaster。
步骤 3 ApplicationMaster 首先向 ResourceManager 注册,这样用户可以直接通过 ResourceManage 查看应用程序的运行状态,然后它将为各个任务申请资源,并监控它的运 行状态,直到运行结束,即重复步骤 4~7。
步骤 4 ApplicationMaster 采用轮询的方式通过 RPC 协议向 ResourceManager 申请和 领取资源。
步骤 5 一旦 ApplicationMaster 申请到资源后,便与对应的 NodeManager 通信,要求 它启动任务。
步骤 6 NodeManager 为任务设置好运行环境(包括环境变量、JAR 包、二进制程序 等)后,将任务启动命令写到一个脚本中,并通过运行该脚本启动任务。
步骤 7 各个任务通过某个 RPC 协议向 ApplicationMaster 汇报自己的状态和进度,以 让 ApplicationMaster 随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。
在应用程序运行过程中,用户可随时通过 RPC 向 ApplicationMaster 查询应用程序的当 前运行状态。
步骤 8 应用程序运行完成后,ApplicationMaster 向 ResourceManager 注销并关闭自己。

image.png
多角度理解 YARN

并行编程
在单机程序设计中,为了快速处理一个大的数据集,通常采用多线程并行编程,如 图 2-12 所示,大体流程如下 :先由操作系统启动一个主线程,由它负责数据切分、任务分 配、子线程启动和销毁等工作,而各个子线程只负责计算自己的数据,当所有子线程处理 完数据后,主线程再退出。类比理解,YARN 上的应用程序运行过程与之非常相近,只不 过它是集群上的分布式并行编程。可将 YARN 看做一个云操作系统,它负责为应用程序启 动 ApplicationMaster(相当于主线程),然后再由 ApplicationMaster 负责数据切分、任务分配、 启动和监控等工作,而由 ApplicationMaster 启动的各个 Task(相当于子线程)仅负责自己的计 算任务。当所有任务计算完成后,ApplicationMaster 认为应用程序运行完成,然后退出。

image.png

资源管理系统
资源管理系统的主要功能是对集群中各类资源进行抽象,并根据各种应用程序或者服 务的要求,按照一定的调度策略,将资源分配给它们使用,同时需采用一定的资源隔离机 制防止应用程序或者服务之间因资源抢占而相互干扰。YARN 正是一个资源管理系统, 它的出现弱化了计算框架之争,引入 YARN 这一层后,各种计算框架可各自发挥自己的优势, 并由 YARN 进行统一管理,进而运行在一个大集群上。

云计算
普遍认为,云计算包括以下几个层次的服务 :IaaS、PaaS 和 SaaS。这里所谓的层次, 是分层体系架构意义上的“层次”。IaaS、PaaS、SaaS 分别实现在基础设施层、软件开放运 行平台层、应用软件层。

IaaS(Infrastructure-as-a-Service) :基础设施即服务。消费者通过 Internet 可以从完善的 计算机基础设施获得服务。Iaas 通过网络向用户提供计算机(物理机和虚拟机)、存储空间、 网络连接、负载均衡和防火墙等基本计算资源 ;用户在此基础上部署和运行各种软件,包 括操作系统和应用程序等。

PaaS(Platform-as-a-Service) :平台即服务。PaaS 是将软件研发的平台作为一种服务, 以 SaaS 的模式提交给用户。平台通常包括操作系统、编程语言的运行环境、数据库和 Web 服务器等,用户可以在平台上部署和运行自己的应用。通常而言,用户不能管理和控制底 层的基础设施,只能控制自己部署的应用。

SaaS(Software-as-a-Service) :软件即服务。它是一种通过 Internet 提供软件的模式,用 户无需购买软件,而是向提供商租用基于 Web 的软件,来管理企业经营活动。云提供商在 云端安装和运行应用软件,云用户通过云客户端(比如 Web 浏览器)使用软件。

从云计算分层概念上讲,YARN 可看做 PAAS 层,它能够为不同类型的应用程序提供 统一的管理和调度。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容