HBase存储及分布式架构

 本文主要从HBase存储系统的分布式架构设计、HBase存储机制、逻辑存储与物理存储等方面系统讲解了HBase存储系统。以及根据HBase系统的特性讲述了常用的应用场景、开发过程中需要注意的事项、优化方式(最佳实践)等。
 笔者在实际开发过程中,根据HBase存储的特性及业务实际应用场景,将HBase作为用户画像的底层存储,在生产中亿级用户量,日活超千万的量级下,HBase很好的满足了性能与存储的业务需求。
 希望通过这边文章与大家分享HBase存储方面的知识,若有不正确或者有出入的地方,欢迎大家指正,在此感谢!

1.HBase系统架构

HBase整体系统架构如下图所示:


HBase存储及分布式架构.jpg

组件简介

Client

HBase访问客户端,包含访问HBase的接口;Client维护着一些Cache来加快对HBase的访问,比如Region的位置信息。

主要职责:
1.使用HBase RPC机制与HMaster和HRegionServer进行通信。
2.Client与HMaster进行通信管理类操作。
3.Client与HRegionServer进行数据读写类操作。

HRegion

1.HBase中分布式存储的最小单元;但不是存储的最小单元。
2.HBase表在行的方向上分隔为多个Region。不同的Region可以分别在不同的Region Server上,但同一个Region是不会拆分到多个Server上。Region按大小分隔,当Region的某个列族达到一个阈值(默认256M)时就会分成两个新的Region。

HRegion Server

Region Server 负责处理数据的读写请求,客户端请求数据时直接和 Region Server 交互。

主要职责:
1.用来维护Master分配给他的Region,处理对这些Region的IO请求;
2.负责切分正在运行过程中变的过大的Region。

HMaster

1.HRegionServer管理者/协调者。
2.数据的读写操作与他没有关系,它挂了之后,集群照样运行。但是Master 也不能宕机太久,有很多必要的操作,比如创建表、修改列族配置等DDL跟Region的分割与合并都需要它的操作。

主要职责:
1.负责启动的时候分配Region到具体的 RegionServer。
2.发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上。
3.管理HRegion服务器的负载均衡,调整HRegion分布。
4.在HRegion分裂后,负责新HRegion的分配。

 HBase 中可以启动多个Master,通过 Zookeeper 的 Master Election(选主机制) 机制保证总有一个 Master 运行。

ZooKeeper

1.HBase 通过 Zookeeper 来做 Master 的高可用。
2.RegionServer 的监控、元数据的入口以及集群配置的维护等工作。

主要职责:  
1.保证任何时候,集群中只有一个Master。
2.存储所有Region的寻址入口。
3.实时监控Region Server的上线和下线信息,并实时通知给Master。
4.存储HBase的Schema和Table元数据。

2.HMaster、HRegion Server、ZooKeeper协作原理

协作基本原理

1.HMaster 与 HRegion Server 之间通过ZooKeeper的 发布与订阅 机制进行感知与管理。
2.HMaster集群通过ZooKeeper的竞争选举机制来维护HMaster集群的高可用性。

HRegion Server 上下线

上线

1.HMaster通过Zookeeper来追踪HRegion Server的状态。
2.HRegion Server 上线时,首先在Zookeeper的Server目录中创建自己的文件,并取得文件的独占锁。
3.由于HMaser订阅了Server目录,当目录下有文件增加或者删除时,HMaster能收到来自Zookeeper的实时通知,因此当HRegion Server上线时HMaster能马上得到消息。

下线

1.HRegion Server下线时,它断掉了Zookeeper的通讯,Zookeeper便会释放代表Server的文件的独占锁。
2.HMaster轮询Zookeeper Server目录下文件的独占锁。 当HMaster发现某个Region Server丢失了自己的独占锁(或者HMaster与HRegion server连续几次通讯都不成功), HMaster将尝试获取该文件的读写锁,一旦获取成功,说明:
  2.1 该HRegion server与Zookeeper通讯已经断开
  2.2 该HResion server挂了

 无论哪种情况,HMaster将删除Server目录下代表该Server的文件,并将该Server的所有Region,并将其分配给其他活着的Server。 如果HRegion Server因为临时网络断开丢失了锁,并很快恢复与Zookeeper的通讯,只要代表其的文件没有被删除,它会继续尝试或许该文件的锁,一旦获取成功,它就可以接着服务

HMaster 上下线

上线

HMaster启动时:
1.从 Zookeeper中获取一个代表HMaster的锁,用以阻止其他Master成为Master(竞争选主机制)。
2.扫描Zookeeper中的Server目录,获取HRegion Server的List。
3.与2中获取的Server 通讯,获取已分配的Region和Region Server的对应关系。
4.扫描hbase:meta表,记录尚未分配的Region的信息,并添加到待分配的Region列表中。

下线

1.HMaster下线时,由于它不参与Client的IO操作,所以这些操作不受影响。
2.HMaster下线仅导致元数据的操作(比如无法创建表,无法修改表结构,无法进行负载均衡,无法进行region的合并,但是split可以进行,因为split只有HRegion Server参与)受影响,用户的IO操作可以继续进行。

PS:所以短时间内的HMaster下线对HBase集群影响不大。

小结

 HMaster 和 HRegion Server通过在Zookeeper中创建的Ephemeral Node(临时节点)来完成注册、监听、集群管理等工作,具体原理如下:
 1.活跃的HMaster监听Region Server的信息,并在其下线后重新分配Region server来恢复相应的服务。
 2.不活跃的HMaster监听活跃HMaster的信息,并在起下线后重新选出活跃的HMaster进行服务。

3.HRegion工作原理

HRegionServer架构及原理.png

组件简介

WAL

即 Write Ahead Log,是HDFS上的一个文件,用以存储尚未进行持久化的数据。顺序写入。
所有写操作都会先保证将数据写入这个Log文件后,才会真正去更新MemStore,最后写入HFile文件中。

WAL的优点:
1.采用这种模式,可以保证HRegionServer宕机后,依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。
2.这个Log文件会定期Roll出新的文件而删除旧的文件 - 已经持久化到HFile中的Log可以删除。

Block Cache

是一个读缓存。在内存中存放经常读的数据,提升读的性能。
当缓存满的时候,最近最少使用的数据(Least Recently Used data)被踢出。

MemStore

是一个写缓存。存储的是按键排好序的待写入硬盘的数据。在Region中每个Column Family对应一个HStore,每个HStore有一个MemStore和多个HFile文件。

HFile

HBase中的健值数据对存储在HFile中。

 当MemStore中积累到指定大小的数据后(默认128M),会Flush到一个HFile文件中(每次Flush都会生成一个新的HFile);当HFile增长到一定阈值后(默认8块),会触发Compact合并操作,使多个HFile合并成一个HFile。当合并的HFile数据大小超过512M时,会对HFile进行拆分。同时将当前Region拆分成2个Region,原来的Region会下线,新拆分出的2个子Region会被HMaster分配到相应的HRegionServer上,使得原先1个Redion的压力得以分流到2个Region上。

工作原理

请详解 4.HBase 读写流程

4.HBase 读写流程

读流程

HRegionServer读写数据流程.png
流程如下:
1)Client 先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server。
2)访问对应的 Region Server,获取 hbase:meta 表,根据读请求的 namespace:table/rowkey,查询出目标数据位于哪个 Region Server中的哪个Region 中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。
3)与目标 Region Server 进行通讯;
4)分别在 Block Cache(读缓存),MemStore 和 Store File(HFile)中查询目标数据,并将查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不同的类型(Put/Delete)。
5)将从文件中查询到的数据块(Block,HFile 数据存储单元,默认大小为 64KB)缓存到Block Cache。
6)将合并后的最终结果返回给客户端。

写流程

HBase写流程.png
流程如下:
1)Client 先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server。
2)访问对应的 Region Server,获取 hbase:meta 表,根据读请求的 namespace:table/rowkey,查询出目标数据位于哪个 Region Server 中的哪个 Region 中。并将该 table 的 region 信息以及 meta 表的位置信息缓存在客户端的 meta cache,方便下次访问。
3)与目标 Region Server 进行通讯;
4)将数据顺序写入(追加)到 WAL;
5)将数据写入对应的 MemStore,数据会在 MemStore 进行排序;
6)向客户端发送 ack;
7)等达到 MemStore 的刷写时机后,将数据刷写到 HFile。

小结

第一次读和写操作:
 有一个特殊的 HBase Catalog 表叫 Meta Table(它其实是一张特殊的 HBase 表),包含了集群中所有 Regions 的位置信息。Zookeeper 保存了这个 Meta table 的位置。

当HBase第一次读或者写操作到来时:
1.客户端从 Zookeeper 那里获取是哪一台 Region Server 负责管理 Meta table。   
2.客户端会查询那台管理 Meta table 的 Region Server,进而获知是哪一台 Region Server 负责管理本次数据请求所需要的rowkey。客户端会缓存这个信息,以及Meta table 的位置信息本身。   
3.然后客户端回去访问那台 Region Server,获取数据。

 对于以后的的读请求,客户端从可以缓存中直接获取 Meta table 的位置信息(在哪一台 Region Server 上),以及之前访问过的 rowkey 的位置信息(哪一台 Region Server 上),除非因为 Region 被迁移了导致缓存失效。这时客户端会重复上面的步骤,重新获取相关位置信息并更新缓存。

PS1:
户端读写数据,实际上分了两步:
第一步是定位,从 Meta table 获取 rowkey 属于哪个 Region Server 管理;
第二步再去相应的 Region Server 读写数据。这里涉及到了两个 Region Server,要理解它们各自的角色功能。

PS2:
HBase 保证 hbase:meta 表始终只有一个 Region,这是为了确保 meta 表多次操作的原子性。

5.HBase 存储机制

Flush 机制

Flush机制.png

MemStore刷写时机及过程

1.当某个MemStore的大小达到了hbase.hregion.memstore.flush.size(默认值 128M),其所在 region 的所有 memstore (对应的列簇)都会刷写。
  当达到128M的时候会触发flush memstore,当达到128M * n还没法触发flush时候会抛异常来拒绝写入。
  两个相关参数的默认值如下:
    hbase.hregion.memstore.flush.size=128M(默认)
    hbase.hregion.memstore.block.multiplier=4(默认)

2.当 region server 中 memstore 的总大小达到
     java_heapsize(应用的堆内存)
    *hbase.regionserver.global.memstore.size(默认值 0.4) 
    *hbase.regionserver.global.memstore.size.lower.limit(默认值 0.95),
  region 会按照其所有 memstore 的大小顺序(由大到小)依次进行刷写。直到 region server中所有 memstore 的总大小减小到上述值以下。  当 region server 中 memstore 的总大小达到java_heapsize*hbase.regionserver.global.memstore.size(默认值 0.4)时,会阻止继续往所有的 memstore 写数据。

3.到达自动刷写的时间,也会触发 memstore flush。自动刷新的时间间隔由该属性进行配置 hbase.regionserver.optionalcacheflushinterval(默认 1 小时)。

4.当 WAL 文件的数量超过 hbase.regionserver.maxlogs,region 会按照时间顺序依次进行刷写,直到 WAL 文件数量减小到 hbase.regionserver.maxlogs 以下(该属性名已经废弃,现无需手动设置,最大值为 32)。

StoreFile Compaction 机制

HBase StoreFile Compaction机制.png

HBase StoreFile Compaction机制:
 由于memstore每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(Put/Delete)有可能分布在不同的HFile中,因此查询时需要遍历所有的HFile。为了减少HFile的个数,以及清除掉过期和删除的数据,会进行StoreFile Compaction。

 Compaction分为两种,分别时Minor CompactionMajor Compaction

  • Minor Compaction 会将临时的若干较小的HFile合并成一个较大的HFile,但不会清理过期和删除的数据。
  • Major Compaction 会将一个Store下的所有HFile合并为一个大HFile,并且会清理掉过期和删除的数据。

Region Split 机制

HBase Region Split机制.png

Region Split机制:
 默认情况下,每个 Table 起初只有一个 Region,随着数据的不断写入,Region 会自动进行拆分。刚拆分时,两个子 Region 都位于当前的 Region Server,但处于负载均衡的考虑,HMaster 有可能会将某个 Region 转移给其他的 Region Server。

Region Split 时机:
1)当1个region中的某个Store下所有StoreFile的总大小超过hbase.hregion.max.filesize,该 Region 就会进行拆分(0.94 版本之前)。
2)当 1 个 region 中 的 某 个 Store 下所有 StoreFile 的 总 大 小 超 过 Min(R^2 *"hbase.hregion.memstore.flush.size",hbase.hregion.max.filesize"),该 Region 就会进行拆分,其中 R 为当前 Region Server 中属于该 Table 的个数(0.94 版本之后)。

6.HBase逻辑表 与 物理存储表

逻辑表

HBase逻辑表.png

物理存储
HBase物理存储.png

数据模型(相关术语)


1)Name Space
命名空间,类似于关系型数据库的database概念,每个命名空间下有多个表。HBase两个自带的命名空间,分别是hbase和default,hbase中存放的是HBase内置的表,default表是用户默认使用的命名空间。
一个表可以自由选择是否有命名空间,如果创建表的时候加上了命名空间后,这个表名字以<Namespace>:<Table>作为区分!

2)Table
类似于关系型数据库的表概念。不同的是,HBase定义表时只需要声明列族即可,数据属性,比如超时时间(TTL),压缩算法(COMPRESSION)等,都在列族的定义中定义,不需要声明具体的列。
这意味着,往HBase写入数据时,字段可以动态、按需指定。因此,和关系型数据库相比,HBase能够轻松应对字段变更的场景。

3)Row
HBase表中的每行数据都由一个RowKey和多个Column(列)组成。一个行包含了多个列,这些列通过列族来分类,行中的数据所属列族只能从该表所定义的列族中选取,不能定义这个表中不存在的列族,否则报错NoSuchColumnFamilyException。

4) RowKey
Rowkey由用户指定的一串不重复的字符串定义,是一行的唯一标识!数据是按照RowKey的字典顺序存储的,并且查询数据时只能根据RowKey进行检索,所以RowKey的设计十分重要。
如果使用了之前已经定义的RowKey,那么会将之前的数据更新掉!

5)Column Family
列族是多个列的集合。一个列族可以动态地灵活定义多个列。表的相关属性大部分都定义在列族上,同一个表里的不同列族可以有完全不同的属性配置,但是同一个列族内的所有列都会有相同的属性。
列族存在的意义是HBase会把相同列族的列尽量放在同一台机器上,所以说,如果想让某几个列被放到一起,你就给他们定义相同的列族。
官方建议一张表的列族定义的越少越好,列族太多会极大程度地降低数据库性能,且目前版本Hbase的架构,容易出BUG。

6) Column Qualifier
Hbase中的列是可以随意定义的,一个行中的列不限名字、不限数量,只限定列族。因此列必须依赖于列族存在!列的名称前必须带着其所属的列族!例如info:name,info:age。
因为HBase中的列全部都是灵活的,可以随便定义的,因此创建表的时候并不需要指定列!列只有在你插入第一条数据的时候才会生成。其他行有没有当前行相同的列是不确定,只有在扫描数据的时候才能得知!

7)TimeStamp
用于标识数据的不同版本(version)。时间戳默认由系统指定,也可以由用户显式指定。
在读取单元格的数据时,版本号可以省略,如果不指定,Hbase默认会获取最后一个版本的数据返回!

8)Cell
一个列中可以存储多个版本的数据。而每个版本就称为一个单元格(Cell)。
Cell由{rowkey, column Family:column Qualifier, time Stamp}确定。
Cell中的数据是没有类型的,全部是字节码形式存贮。

9)Region
Region由一个表的若干行组成!在Region中行的排序按照行键(rowkey)字典排序。
Region不能跨RegionSever,且当数据量大的时候,HBase会拆分Region。
Region由RegionServer进程管理。HBase在进行负载均衡的时候,一个Region有可能会从当前RegionServer移动到其他RegionServer上。
Region是基于HDFS的,它的所有数据存取操作都是调用了HDFS的客户端接口来实现的。

7.HBase特性 及 常用场景

特性

  • 千万级高并发
  • PB级存储(千亿数据量)
  • 非结构化存储(列式数据库)
  • 动态列,稀疏列
  • 支持二级索引
  • 强一致性,可靠性,扩展性

场景

  1. 写密集型应用,每天写入量巨大,而相对读数量较小的应用
  2. 不需要复杂查询条件来查询数据的应用
    • 使用rowkey,单条记录或者小范围的查询性能不错,大范围的查询由于分布式的原因,可能在性能上有点影响。
    • 使用HBase的过滤器的话性能比较差。
  3. 不需要关联的场景,HBase为NoSQL无法支持join
  4. 可靠性要求高
    • master支持主备热切。
    • regionServer宕机,region会分配给在线的机器。
    • 数据持久化在HDFS,默认3份,HDFS保证数据可靠性。
    • 内存的数据若丢失可以通过Wal预写日志恢复。
  5. 数据量较大,而且增长量无法预估的应用
    • HBase支持在线扩展,即使在一段时间内数据量呈井喷式增长,也可以通过HBase横向扩展来满足功能。

应用

  • 对象存储系统
     HBase MOB(Medium Object Storage),中等对象存储是hbase-2.0.0版本引入的新特性,用于解决hbase存储中等文件(0.1m~10m)性能差的问题。这个特性适合将图片、文档、PDF、小视频存储到Hbase中。
  • OLAP的存储
     Kylin的底层用的是HBase的存储,看中的是它的高并发和海量存储能力。kylin构建cube的过程会产生大量的预聚合中间数据,数据膨胀率高,对数据库的存储能力有很高要求。
     Phoenix是构建在HBase上的一个SQL引擎,通过phoenix可以直接调用JDBC接口操作Hbase,虽然有upsert操作,但是更多的是用在OLAP场景,缺点是非常不灵活。
  • 时序型数据
     openTsDB应用,记录以及展示指标在各个时间点的数值,一般用于监控的场景,是HBase上层的一个应用。
  • 用户画像系统
     动态列,稀疏列的特性。用于描述用户特征的维度数是不定的且可能会动态增长的(比如爱好,性别,住址等); 不是每个特征维度都会有数据;
  • 消息/订单系统
     强一致性,良好的读性能
  • feed流系统存储

8.HBase与关系型数据库的区别

指标 传统关系数据库 HBase
数据类型 有丰富的数据类型 字符串
数据操作 丰富操作,复杂联表查询 简单CRUD
存储模式 基于行存储 基于列存储
数据索引 复杂的多个索引 只有RowKey索引
数据维护 新覆盖旧 多版本
可伸缩性 难实现横向扩展 性能动态伸缩

9.HBase最佳实践

RowKey 的设计原则

1.RowKey 长度原则

 二进制码流RowKey 最大长度 64Kb,实际应用中一般为 10-100bytes,以 byte[] 形式保存,一般设计定长。建议越短越好,因为HFile是按照KV存储的Key太大浪费空间。

2.RowKey 散列原则

 RowKey 在设计时候要尽可能的实现可以将数据均衡的分布在每个 RegionServer 上,防止热点Region的产生。
Region热点问题解决

  • Reverse反转
    针对固定长度的Rowkey反转后存储,这样可以使Rowkey中经常改变的部分放在最前面,可以有效的随机Rowkey。
  • Salt加盐
    Salt是将每一个Rowkey加一个前缀,前缀使用一些随机字符,使得数据分散在多个不同的Region,达到Region负载均衡的目标。
  • Hash散列或者Mod
    用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推断。

3.排序原则

 HBase的Rowkey是按照ASCII有序设计的,我们在设计Rowkey时要充分利用这点。

4.RowKey 唯一原则

 RowKey 必须在设计上保证其唯一性,RowKey 是按照字典顺序排序存储的,因此设计 RowKey 时可以将将经常读取的数据存储到一块。

HBase 优化方法

减少调整

 HBase中有几个内容会动态调整,如Region(分区)、HFile。通过一些方法可以减少这些会带来I/O开销的调整。

Region

 没有预建分区的话,随着Region中条数的增加,Region会进行分裂,这将增加I/O开销,所以解决方法就是根据你的RowKey设计来进行预建分区,减少Region的动态分裂。

HFile

 MemStore执行flush会生成HFile,同时HFile过多时候也会进行Merge, 为了减少这样的无谓的I/O开销,建议估计项目数据量大小,给HFile设定一个合适的值。

减少启停

 数据库事务机制就是为了更好地实现批量写入,较少数据库的开启关闭带来的开销,那么HBase中也存在频繁开启关闭带来的问题。

关闭 Compaction

 HBase 中自动化的Minor Compaction和Major Compaction会带来极大的I/O开销,为了避免这种不受控制的意外发生,建议关闭自动Compaction,在闲时进行compaction。

减少数据量

 开启过滤,提高查询速度
 开启BloomFilter,BloomFilter是列族级别的过滤,在生成一个StoreFile同时会生成一个MetaBlock,用于查询时过滤数据。

使用压缩

 一般推荐使用Snappy和LZO压缩

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

推荐阅读更多精彩内容