系列二:次时代Kafka与Pulsar该如何选择?

编者荐语:

感谢大家支持,我是国栋,目前新书已上架各大线上平台!!

多谢PowerData社区对此的支持。感谢机械工业出版社编辑老师长期的指导。感谢Tencent同事们的指点与陪伴。


作者简介

国栋,腾讯软件工程师,Apache Pulsar、Apache Flink 等项目的贡献者,杭州电子科技大学硕士。

曾参与某大型数据中台建设项目,以及消息队列服务(Pulsar、Kafka)及其相关数据总线服务的开发与建设工作。在 Apache Pulsar、Apache Kafka、Apache Flink 落地实践方面具有丰富经验。

本文章部分内容,节选自作者新书《Apache Pulsar 原理解析与应用实践》,机械工业出版社。

引言

Kafka 自 2011 年被捐献给 Apache 基金会,至今已发展为消息队列事实标准。作为一个优秀的分布式消息系统,Kafka 被许多企业采用并成为其大数据架构中不可或缺的一部分。目前 Kafka 也不局限于分布式消息队列,而在向“集成分发、存储和计算的流式数据平台”发展。

与此同时,大数据"新秀"Pulsar 是站在 Kafka 与 RocketMQ 的肩膀上,针对 Kafka 使用与维护过程中的痛点问题,结合云原生、存算分离的架构趋势,所提出新的架构思想的新一代消息队列架构。(站在巨人肩膀上,在开源软件与计算机发展的历史上,参考现有软件架构,有针对性地现有架构的痛点,提出新一代的解决方案是促进行业发展的巨大贡献。)

作者在某一线互联网公司负责维护数据总线与数据集成服务,其在 Kafka 与 Plusar 的基础之上,又抽象了一层数据管道的概念,使两种消息队列可以插拔式嵌入服务中,并在业务场景下进行灵活切换。因此,笔者可以同时深入使用 Kafka 与 Plusar 这两种消息队列。

Kafka 经历了这么多复杂业务场景的考验,作为大数据信息队列的事实标准,即使放在现在依旧是大数据场景下的首要选择。

Pulsar 在前人基础上,从架构底层重新定义大数据消息队列,结合其本身云原生、多租户、存算分离的特性,在很多业务场景下可以更加轻松的面对各种调整。

在当前云原生迅速发展的时代,我们应该如何抉择 Kafka 与 Plusar, 本文从多个角度进行对比,给出参考。

前 Kafka 时代

哼,一个能打的都没有!

Kafka 是 2009 年底 LinkedIn 研发的一套流数据处理平台。LinkedIn 发现当下 MQ 系统有两个通用缺陷:一是当消费者出现消费不及时的时候,数据就会丢掉;二是可延展性问题,MQ 系统不能很好地配合数据的波峰或波谷。

这些需求正好对应当时的消息队列系统不能解决的一些问题:横向拓展、持久化、高吞吐、高性能、甚至是低成本。因此 Kafka 这一流处理系统出现后,瞬间成为大数据流处理系统的事实标准。

Kafka 设计理念非常简单,就是一个 以 append-only 日志作为核心的数据存储结构。简单来说,就是我们把数据以日志的方式进行组织,所有对于日志的写操作,都提交在日志的最末端,读取时也只能顺序读取。

从当下的角度看待当时的 Kafka 设计确实简单又十分优雅!Kafka 能实现上述的:横向拓展、持久化、高吞吐、高性能都得益于这个基础设计。

顺序读写,高吞吐:HDD 的随机读取和写入因为其本身原因会非常慢,但其实如果能够把所有的读和写都按照顺序来进行操作,会发现它几乎可以媲美内存的随机访问。Kafka 利用 append-only 日志作为核心的数据存储结构,只会对文件进行顺序的磁盘读写操作,完美利用了磁盘顺序读写快速的优点。

【机械硬盘的连续读写性能很好,但随机读写性能很差,这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停地移动,时间都浪费在了磁头寻址上,所以性能较低。】

多分区扩展:在 Kafka 中,Topic 只是一个逻辑的概念。每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点,因此可以充分利用集群优势,实现机器间的并行处理。同时由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘优势。

多副本下的高可用:每个 broker 中的 partition 都会设置有 replication(副本)的个数,生产者写入的时候首先根据分发策略(有 partition 按 partition,有 key 按 key,都没有轮询)写入到 leader 中,follower(副本)再跟 leader 同步数据,这样有了备份,也可以保证消息数据的不丢失。

低成本:Kafka 的日志存储持久化到磁盘,在部署时可以使用成本较低的 HDD 磁盘。

页缓存加速:顺序写文件时,读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据,页缓存会将一些写操作重新按顺序排好,从而减少磁盘头的移动时间,并将连续的小块写组装成大块的物理写从而提高性能。

【Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能】

零拷贝,充分利用 IO:Kafka 在这里采用的方案是通过 NIO 的 transferTo/transferFrom 调用操作系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝。

【零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载】

从当下的角度看待 Kafka 的设计还是那么简洁和优雅,Kafka 能实现上述的:横向拓展、持久化、高吞吐、高性能都得益于以 append-only 日志作为核心的数据存储结构这个基础设计。但是,这种简单的设计也有一些弊端,但是在 Kafka 刚出的那个时候,这个设计和功能确实太优秀了。

后 Kafka 时代

何为后 Kafka 时代?

Kafka 作为一个老的基础组件,很多读者都已经对其设计和原理十分熟悉,在后起之秀 Pulsar 的冲击下,很多人或许会犹豫究竟要选择哪个技术。不禁让人思考:Kafka 老矣,尚能饭否?

Kafka 使用痛点

首先先说结论,后 Kafka 时代下,Kafka 的某些设计已经比较落后了。在运营/运维 Kafka 的过程中,其实遇到了很多问题。

而很多问题是在基础架构确定之后,就决定了会有这样的结果。

单机器的分区上限问题

虽然 Kafka 的 topic partition 是顺序写入,但是当 broker 上有成百上千个 topic partition 时,从磁盘角度看就变成了随机写入,此时磁盘读写性能会随着 topic partition 数量的增加而降低,因此 Kafka broker 上存储的 topic partition 数量是有限制的。在微服务与云原生场景下,需要创建大量 Topic 用于进程间通信,导致 Kafka 吞吐性能低下。

【Kafka 的设计是每个 Topic 包括若干个 Partition,每个 Partition 同一时刻只会写一个存储文件。当存储文件写满后(例如 1G),再写下一个存储文件。这样的存储机制在 Topic 比较少的情况下并不会有问题,大数据场景下通常 Topic 不需要设置太多。而用在大规模微服务的场景下由于业务的需求,需要设置很多 Topic,通常几百甚至上千个。当 Kafka 设置了几百个 Topic 后,由于其特有的存储模型,每个 Broker 节点会创建数百个文件,而众多的文件在被读取时,部分数据会被加载到操作系统的 Page Cache 中,使用过多的 Page Cache 会令系统极其不稳定。这也是为什么 Kafka 在多 Topic 时,性能表现很差的原因。】

非存算分离架构带来的扩容问题

Kafka 并不是一个存储与计算分离的架构,因此无法单独从存储和计算的维度进行扩容。

单分区文件目录绑定单台 broker 带来的 IO 瓶颈

Kafka 中根据设置的保留期来删除消息。有可能消息没被消费,过期后被删除。不支持 TTL。

但是这其中的本质问题来自于:一个分区只能归属于一台 Broker 机器,如果想要扩容的话,只能扩分区,拆分区

在极端情况下,如果原有 Kafka 集群负载到达 50%,流量这时如果翻三四倍,这对 Kafka 的运维来说简直是个灾难!

运维成本高

如果某个机器磁盘满了,需要显式使用工具迁移分区(Kafka-reassign-partitions.sh)

数据存储和消息队列服务绑定,集群扩缩容/分区均衡需要大量拷贝数据,会带来了很大的运维成本。

随着 Kafka 集群规模的增长,Kakfa 集群的运维成本急剧增长,需要投入大量的人力进行日常运维。在某互联网公司中,扩容一台机器到 Kafka 集群并进行分区均衡,需要 0.5 人/天;缩容一台机器需要 1 人/天。

PageCache 污染问题,造成读写性能下降

Kafka 对 page cache 需求巨大。根据经验值,为 Kafka 分配 6~8GB 的堆内存就已经足足够用了,将剩下的系统内存都作为 page cache 空间,可以最大化 I/O 效率。

另一个需要特别注意的问题是 lagging consumer,即那些消费速率慢、明显落后的 consumer。它们要读取的数据有较大概率不在 broker page cache 中,因此会增加很多不必要的读盘操作。

比这更坏的是,lagging consumer 读取的“冷”数据仍然会进入 page cache,污染了多数正常 consumer 要读取的“热”数据,连带着正常 consumer 的性能变差。在生产环境中,这个问题尤为重要。

在 catch-up 读场景下,容易出现 PageCache 污染,造成读写性能下降。虽然 Kafka 可以利用 PageCache 进行读取加速,在一些场景下实践效果不佳。

不支持读写分离

在 Kafka 中,为了使 consumer 读取数据能够保持一致,只允许 consumer 读取 leader 副本的数据的。follower replica 只用作备份数据。

生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从而实现的是一种主写主读的生产消费模型。Kafka 并不支持主写从读

其实 Kafka 的主写主读也是有一些优点的:

可以简化代码的实现逻辑,减少出错的可能;

将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;

没有延时的影响;

在副本稳定的情况下,不会出现数据不一致的情况。

但是这些也不能算是完全的优点,只是在当前 Kafka 架构下,做到读写分离的收益不如主写主读方案。

Kafka 中 IO 不隔离,因此消费者在清除 Backlog 时会影响其他生产者和消费者

【Backlog 是指已经被写入但尚未被消费的消息队列,消费者消费消息时需要从 Backlog 中读取数据。如果一个消费者清除了 Backlog,那么其他消费者就无法再读取这些消息。

此外,如果消费者消费消息的速度较慢,而生产者持续发送消息,导致消息堆积,这时候清除 Backlog 可以帮助消费者快速消费积压的消息。但是,清除 Backlog 也可能导致生产者发送的消息被删除,这会影响到其他消费者读取消息的顺序和完整性。

因此,在清除 Backlog 之前,需要谨慎考虑其影响,并确保所有相关方都已经做好了相应的准备。在实际使用中,可以采用多个消费者订阅同一个 Topic 的方式来分摊消息的消费压力,以提高系统的稳定性和可靠性。】

Kafka2.4.0 的 follower replica fetch 功能允许使用者从最近的副本(非 leader)中获取数据, 这像是朝着读写分离方向前进,但是阅读 release 日志会发现:Kafka 能够从 follower 副本读数据,这个功能并不是为了提升读取性能。

那推出 follower replica fetch 功能的背景是什么呢?举个比较常见的场景,Kafka 存在多个数据中心,不同数据中心存在于不同的机房,当其中一个数据中心需要向另一个数据中心同步数据的时候,由于只能从 leader replica 消费数据,那么它不得不进行跨机房获取数据,而这些流量带宽通常是比较昂贵的(尤其是云服务器)。即无法利用本地性来减少昂贵的跨机房流量。所以 Kafka 推出这一个功能,就是帮助类似这种场景,节约流量资源。这种功能还可以和新推出的 mirror maker2 相互配合,实现多个数据源的数据同步。



Pulsar 的优势

开源领域中有诸多优秀开源消息队列,例如 RabbitMQ、Apache RocketMQ、Apache ActiveMQ 和 Apache Kafka。

在前人的基础上,Pulsar 实现了很多上一代消息系统或者上一代流数据系统没有实现的功能和特性,比如云原生、多租户、存储与计算分离、分层存储这些特性。针对之前消息队列系统的痛点,Pulsar 做了很多针对化的解决措施。

什么是 Pulsar?

(1)Pulsar 是一个分布式消息平台,可以同时处理流式数据和异构系统这两类问题。Pulsar 具有非常灵活的消息传递模型,为了实现更加丰富的消费模式,Pulsar 提出了订阅的概念。订阅是一个数据消费的规则,它决定了如何将消息传递给消费者,通过不同的订阅模式决定多个消费者在消费时的不同行为。

(2)Pulsar 是一个集消息传递、消息存储、轻量化函数式计算为一体的流数据平台。Pulsar 不仅提供了数据的存储与消费能力,还提供了一定的流处理能力。

(3)Pulsar 是一个分布式可扩展的流式存储系统,并在数据存储的基础上构建了消息队列和流服务的统一模型。这使得 Pulsar 不仅有消息队列的功能(类似 RabbitMQ 和 RocketMQ 在业务系统中的使用),还是一个数据流的处理模型(类似 Kafka 在大数据系统中的定位)。

云原生架构

云原生是一种在云计算时代构建和运行应用程序的方法,可以充分利用和发挥云平台的弹性自动化优势。云原生应用程序在云上以最佳方式运行,让业务系统的可用性、敏捷性和可扩展性得到大幅提升。

Pulsar 是在消息队列领域里基于云原生基础架构设计的产品,拥有诸多云原生应用特性,如无状态计算层、计算与存储分离,可以很好地利用云的弹性(伸缩能力),保证具有足够的可扩容性和容错性,能够很好地在容器化环境中运行。

Pulsar 的存储与计算分离架构在云原生环境中有着更大的价值。Pulsar 实例中的存储节点可以由一组 Bookie 容器去负责,计算节点又是由另外一组 Broker 容器去负责。存储与计算节点可以完全独立扩缩容,通过 Kubernetes 这样的容器编排工具,业务方可以很快地构建弹性扩缩容的云原生消息队列。

存储与计算分离

Pulsar 是一个存储与计算分离的消息队列,其中提供的计算服务的角色被称为 Broker,提供存储服务的角色被称为 Bookie。Broker 是服务端的一个无状态组件,主要负责两类职能:数据的生产消费与 Pulsar 管理。真正扛起存储重任的是 BookKeeper。

Broker 服务是无状态的,在计算资源不足时可独立进行扩容。Bookie 是有状态的存储服务,Pulsar 中的数据会以数据块的形式分配在不同的 Bookie 节点中。当存储资源不够时,可通过增加 Bookie 节点的形式进行扩容。Pulsar 会感知 Bookie 集群的变化,并在合适的时机使用新增加的 Bookie 节点进行存储,避免了人为迁移数据的运维操作。Broker 服务与 Bookie 服务的扩容相互独立,避免了资源浪费,提供了 Pulsar 的可维护性。

分层存储

BookKeeper 集群可以使用廉价的机械硬盘作为存储介质。但是在部署 BookKeeper 集群的过程中,为了最大程度提升写入与读取能力,有可能会选择带有固态硬盘(SSD)的机器,这些机器成本较为昂贵。

Pulsar 是一个存储与计算分离的消息队列,消息最初会存储在 BookKeeper 集群中,通过内部管理组件进行抽象管理。数据管理的基本单位是数据段,其中数据删除与创建的基本单位也是数据段。Pulsar 社区提供了分层存储的能力,在服务端提供了数据卸载功能,可以将每个逻辑上的数据段从 BookKeeper 存储切换为其他类型的存储。

Pulsar 的分层存储功能允许将较旧的积压数据卸载到长期存储中,例如 Hadoop HDFS 或 Amazon S3 存储,从而释放 BookKeeper 中的空间并降低存储成本。通过分层存储可以实现消息队列中的冷数据与热数据分离,让成本更加可控。

可拔插协议处理

在 Pulsar 中支持可插拔的协议处理机制,Pulsar 可以在运行时动态加载额外的协议处理程序并支持其他消息协议。基于消息队列协议层,目前 Pulsar 已经支持了 Kafka、RocketMQ、AMQP 和 MQTT 等多种协议。基于消息队列协议层,Pulsar 可以将自身云原生、分层存储、自动负载管理等诸多特性推广至更多的消息队列系统中

Pulsar 协议层支持的 Kafka 项目为 KafkaOn Pulsar(KoP)。将 KoP 协议部署在现有的 Pulsar 集群中,用户可以在 Pulsar 集群中继续使用原生 Kafka 协议,同时能够利用 Pulsar 的强大功能,完善存量 Kafka 应用的使用体验。

数据可靠性

Pulsar 可以通过幂等性生产者在单个分区上写入数据,并保证其可靠性。通过客户端的自增序列 ID、重试机制与服务端的去重机制,幂等生产者可以保证发送到单个分区的每条消息只会被持久化一次,且不会丢失数据。

另外,Pulsar 事务中的所有生产或消费操作都作为一个单元提交。一个事务中的所有操作要么全部提交,要么全部失败。Pulsar 保障每条消息只写入或处理一次,即使发生故障也不会丢失数据或重复。如果事务中止,则该事务中的所有写入和确认都将自动回滚。综上所述,可以发现 Kafka 与 Pulsar 的事务功能都是为了支持精确一次语义的。

丰富的生态支持

当用户在使用消息队列或者流式服务时,有时遇到的应用场景仅是对消息进行搬运,或者进行一些简单的统计、过滤、汇总等操作。通过 Pulsar Function 就可以原生支持这些功能。官方提供了多种导入数据与导出数据的连接器。通过 Pulsar I/O 提供的能力可以通过简单的配置,灵活地将 Pulsar 与外部系统相结合,例如关系型数据库、非关系型数据库(如 MongoDB)、数据湖、Hadoop 生态等。

Kafka 依旧优秀

我的字典里,没有老这个字。

不屈的灵魂,不灭的斗志,不老的心。

在笔者遇到的业务挑战中,即使是国内的头部游戏业务冲击下,在合理的配置与使用中,Kafka 依旧十分稳健。

什么样的场景下可以继续用 Kafka? 大部分情况下都可以毫不犹豫的选择 Kafka

在集群内 topic 不多或 topic 增长速度不是特别快的情况下,Kafka 依旧是很好的选择。

不需要复杂的企业级场景的时候,Kafka 仍旧是首选。例如不需要多租户与云原始等特性时,不需要应对特别复杂的吞吐量挑战时,不需要分层存储等特性时,

Kafka 原生的集群模式使用简单,能满足大部分业务的需要。Kafka 生态更加完善,国内外的资料与先驱者更加多,在遇到 Kafka 问题时,求取解决方案的途径会更加简单。

不可否认,复杂的架构势必会带来新的优势,但是也会带来复杂性的提高,进而导致出现问题的概率会增加。使用 Pulsar 早期版本时,有时会遇到一些奇怪的 bug,需要开发与维护人员更多的知识储备与问题处理能力!

《Apache Pulsar 原理解析与应用实践》

机械工业出版社《 Apache Pulsar原理解析与应用实践》》

本书从项目背景、基本概念、架构设计和工程实践等多角度出发,全面解读 Pulsar 的核心原理与应用方法。作为云原生的分布式消息队列和流数据平台,Pulsar 不仅支持云原生、多租户、跨区域数据复制等高级功能,还支持消息队列事务、分层存储、可插拔的消息队列协议、Pulsar Function、Pulsar I/O、Pulsar SQL 等拓展功能,且可与 Apache Spark、Apache Flink 等计算引擎,及 Apache Flume、Apache Kafka、Logstash 等社区生态相结合。所以,通过 Pulsar 可以轻松构建出一整套的数据服务。本书对这些内容均进行了详细介绍。

本书包括 3 篇 11 章。

基础篇(第 1~4 章)首先对 Pulsar 的背景进行简单介绍,并对多种消息队列进行重点比较分析;然后对 Pulsar 的基本概念和基本架构进行分析,让读者对 Pulsar 有一个总体的了解;接着分享了 Pulsar 安装与部署的方法,以方便读者快速上手并构建自己的服务;最后深度解读了 Pulsar 的基本使用方法。

原理篇(第 5~7 章)首先深度解读了 Pulsar 的核心组件 Broker、Bookie、ManagedLedger、主题管理等的原理;然后分析了构建在这些核心组件之上的高级特性,如事务管理、消息协议拓展、分层存储设计、消息延迟传递与主题压缩;最后对 Pulsar 提供的轻量化流数据处理引擎 Pulsar Function 及 I/O 功能进行剖析。

应用篇(第 8~11 章)首先分享了 Pulsar 在结构化数据查询与实时处理引擎技术方面的实践,介绍了 Pulsar 如何与 Trino、Flink、Spark 等引擎相结合;接着对 Pulsar 安全配置、服务管理、服务监控等进行讨论;最后介绍了 Pulsar 服务的应用模式,以及 Pulsar 在数据集成、动态数据捕获和高可靠性配置等方面的实践。

JD链接

https://item.jd.com/13728745.html?bbtf=1

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

推荐阅读更多精彩内容