HBase系列 - 内部机制 MemstoreFlush、StoreFile Compaction、Region Split详解

前言

HBase以高并发、搞可靠、高性能而闻名,而Compact和Split功能贯穿了hbase的整个写入过程,而熟悉Compact和Split内部逻辑以及控制参数才能根据具体的实际业务场景来调整参数满足业务需要,对HBase优化以及问题排查是至关重要的。

本文对 MemStore 的 Flush 进行说明,包括哪几种条件会触发 Memstore Flush 及目前常见的刷写策略(FlushPolicy)以及 Memstore Flush 后可能引起HBase的StoreFile Compaction 和 Region Split。

当使用HBase到了瓶颈(内存、cpu、IO)的时候,熟悉其内部机制可以更好的针对场景去做trade-off。大多数情况下,都是磁盘IO存在问题。熟悉Memstore Flush、StoreFile Compaction、Region split 的话可以更加灵活的使用内存和cpu去换去紧张的IO资源以及如何去避免被阻塞导致性能问题。

HBase 数据写入总流程.png

如上图所示:

当RegionServer接收写入请求后整个存储过程的三个阶段

  1. 当达到一定的条件后Memstore会Flush生成Hfile
  2. 随着Memstore Flush的HFile文件越来越多,可能严重影响到HBase的读取性能。Flush文件时满足条件的话会触发Compaction操作。
  3. 随着数据的不断写入,Region越来越膨胀,如果RegionSplitPolicy满足之后也会自动进行拆分和迁移,在RegionServer之间负载均衡。

1.MemStore Flush

HBase MemStore Flush.png

Memstore中的数据在一定条件下会进行刷写操作,使数据持久化到相应的存储设备上。

1.1 MemStore刷写条件

1.1.1 整个 RegionServer 的 MemStore 占用内存总和大于相关阈值

在该种情况下,RegionServer中所有region的单个memstore内存占用都没达到刷盘条件,但整体的内存消耗已经到一个非常危险的范围,如果持续下去,很有可能造成RS的OOM,这个时候,需要进行memstore的刷盘,从而释放内存。

hbase.regionserver.global.memstore.size.lower.limit(默认值0.95)
hbase.regionserver.global.memstore.size(默认值0.4)

注意:0.99.0 之前以上两个参数分别对应的是
hbase.regionserver.global.memstore.lowerLimit(默认值0.95)
hbase.regionserver.global.memstore.upperLimit(默认值0.4)

HBase 为 RegionServer 的 MemStore 分配了一定的写缓存,大小等于 hbase_heapsize(RegionServer 占用的堆内存大小)* hbase.regionserver.global.memstore.size,也就是说写缓存大概占用 RegionServer 整个 JVM 内存使用量的 40%。

如果整个 RegionServer 的 MemStore 占用内存总和大于hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize 的时候,将会触发 MemStore 的刷写。

举个例子,如果我们 HBase 堆内存总共是 32G,按照默认的比例,那么触发 RegionServer 级别的 Flush 是 RS 中所有的 MemStore 占用写内存为:32 * 0.4 * 0.95 = 12.16G。

RegionServer 级别的 Flush 策略是按照其所有memstore的大小顺序(由大到小)依次进行刷写。直到region server中所有memstore的总大小减小到(hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize)以下才会停止。

注意:如果达到了 RegionServer 级别的 Flush,那么当前 RegionServer 的所有写操作将会被阻塞,而且这个阻塞可能会持续到分钟级别。

1.1.2 Region 中一个 MemStore 占用的内存超过相关阈值

当Region中一个memstroe的大小达到了hbase.hregion.memstore.flush.size,其所在region的所有memstore都会刷写。我们每次调用 put、delete 等操作都会检查的这个条件的。

hbase.hregion.memstore.flush.size(默认值128M)
hbase.hregion.memstore.block.multiplier(默认值为4)

但是如果我们的数据增加得很快,达到了hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier 的大小,也就是1284=512MB的时候,那么除了触发 MemStore 刷写之外,HBase 还会在刷写的时候同时阻塞*所有写入该 Store 的写请求。这时候如果你往对应的 Store 写数据,会出现 RegionTooBusyException 异常。

1.1.3 WAL文件的数量超过相关阈值

当WAL文件的数量超过hbase.regionserver.max.logs,region会按照时间顺序依次进行刷写,直到WAL文件数量减小到hbase.regionserver.max.log以下。这个阈值(maxLogs)的计算公式

this.blocksize = WALUtil.getWALBlockSize(this.conf, this.fs, this.walDir);
float multiplier = conf.getFloat("hbase.regionserver.logroll.multiplier", 0.5f);
this.logrollsize = (long)(this.blocksize * multiplier);
this.maxLogs = conf.getInt("hbase.regionserver.maxlogs",
      Math.max(32, calculateMaxLogFiles(conf, logrollsize)));
 
public static long getWALBlockSize(Configuration conf, FileSystem fs, Path dir)
      throws IOException {
    return conf.getLong("hbase.regionserver.hlog.blocksize",
        CommonFSUtils.getDefaultBlockSize(fs, dir) * 2);
}
 
private int calculateMaxLogFiles(Configuration conf, long logRollSize) {
    Pair<Long, MemoryType> globalMemstoreSize = MemorySizeUtil.getGlobalMemStoreSize(conf);
    return (int) ((globalMemstoreSize.getFirst() * 2) / logRollSize);
}

也就是说,如果设置了 hbase.regionserver.maxlogs,那就是这个参数的值;否则是 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize)。如果某个 RegionServer 的 WAL 数量大于 maxLogs 就会触发 MemStore 的刷写。

WAL 数量触发的刷写策略是,找到最旧的 un-archived WAL 文件,并找到这个 WAL 文件对应的 Regions, 然后对这些 Regions 进行刷写。

1.1.4 到达自动刷写的时间

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

hbase.regionserver.optionalcacheflushinterval 默认3600000

如果设定为0,则意味着关闭定时自动刷写。

为了防止一次性有过多的 MemStore 刷写,定期自动刷写会有 0 ~ 5 分钟的延迟,具体参见 PeriodicMemStoreFlusher 类的实现。

1.1.5 数据更新超过一定阈值

内存中的更新数量已经足够多,比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为30000000,那么也是会触发刷写的。

1.1.6 手动触发刷写

​ 手动触发刷写的两个方式

  1. 调用 Admin 接口提供 的API 来触发 MemStore 的刷写操作

    void flush(TableName tableName) throws IOException;
    void flushRegion(byte[] regionName) throws IOException;
    void flushRegionServer(ServerName serverName) throws IOException;
    
  2. hbase shell

    hbase> flush 'TABLENAME'
    hbase> flush 'REGIONNAME'
    hbase> flush 'ENCODED_REGIONNAME'
    hbase> flush 'REGION_SERVER_NAME'
    

1.2. 什么操作会触发刷写条件判断

常见的 put、delete、append、increment、调用 flush 命令、Region 分裂、Region Merge、bulkLoad HFiles 以及给表做快照操作都会对上面的相关条件做检查,以便判断要不要做刷写操作。

1.3. MemStore 刷写策略(FlushPolicy)

在 HBase 1.1 之前,MemStore 刷写是 Region 级别的。就是说,如果要刷写某个 MemStore ,MemStore 所在的 Region 中其他 MemStore 也是会被一起刷写的。HBASE-10201/HBASE-3149引入列族级别的刷写。我们可以通过 hbase.regionserver.flush.policy 参数选择不同的刷写策略。

目前 HBase 2.0.2 的刷写策略全部都是实现 FlushPolicy 抽象类的。并且自带三种刷写策略:

FlushAllLargeStoresPolicyFlushNonSloppyStoresFirstPolicy 以及 FlushAllStoresPolicy

1.3.1 FlushAllStoresPolicy

返回当前 Region 对应的所有 MemStore。也就是每次刷写都是对 Region 里面所有的 MemStore 进行的,这个行为和 HBase 1.1 之前是一样的。

1.3.2 FlushAllLargeStoresPolicy

在 HBase 2.0 之前版本是 FlushLargeStoresPolicy,后面被拆分成分 FlushAllLargeStoresPolicyFlushNonSloppyStoresFirstPolicy,参见 HBASE-14920

//region.getMemStoreFlushSize() / familyNumber
//就是 hbase.hregion.memstore.flush.size 参数的值除以相关表列族的个数
flushSizeLowerBound = max(region.getMemStoreFlushSize() / familyNumber, hbase.hregion.percolumnfamilyflush.size.lower.bound.min)
 
//如果设置了 hbase.hregion.percolumnfamilyflush.size.lower.bound
flushSizeLowerBound = hbase.hregion.percolumnfamilyflush.size.lower.bound

这种策略会先判断 Region 中每个 MemStore 的使用内存(OnHeap + OffHeap)是否大于某个阀值,大于这个阀值的 MemStore 将会被刷写。阀值的计算是由 hbase.hregion.percolumnfamilyflush.size.lower.boundhbase.hregion.percolumnfamilyflush.size.lower.bound.min 以及 hbase.hregion.memstore.flush.size 参数决定的

hbase.hregion.percolumnfamilyflush.size.lower.bound.min 默认值16MB
hbase.hregion.percolumnfamilyflush.size.lower.bound 默认没有设置。

比如当前表有3个列族,其他用默认的值,那么 flushSizeLowerBound = max((long)128 / 3, 16) = 42

如果当前 Region 中没有 MemStore 的使用内存大于上面的阀值,FlushAllLargeStoresPolicy 策略就退化成 FlushAllStoresPolicy 策略了,也就是会对 Region 里面所有的 MemStore 进行 Flush。

1.3.3 FlushNonSloppyStoresFirstPolicy

HBase 2.0 引入了 in-memory compaction,参见 HBASE-13408

如果我们对相关列族 hbase.hregion.compacting.memstore.type 参数的值不是 NONE,那么这个 MemStore 的 isSloppyMemStore 值就是 true,否则就是 false。

FlushNonSloppyStoresFirstPolicy 策略将 Region 中的 MemStore 按照 isSloppyMemStore 分到两个 HashSet 里面(sloppyStoresregularStores)。然后

  • 判断 regularStores 里面是否有 MemStore 内存占用大于相关阀值的 MemStore ,有的话就会对这些 MemStore 进行刷写,其他的不做处理,这个阀值计算和 FlushAllLargeStoresPolicy 的阀值计算逻辑一致。
  • 如果 regularStores 里面没有 MemStore 内存占用大于相关阀值的 MemStore,这时候就开始在 sloppyStores 里面寻找是否有 MemStore 内存占用大于相关阀值的 MemStore,有的话就会对这些 MemStore 进行刷写,其他的不做处理。
  • 如果上面 sloppyStoresregularStores 都没有满足条件的 MemStore 需要刷写,这时候就 FlushNonSloppyStoresFirstPolicy 策略久退化成 FlushAllStoresPolicy 策略了。
HBase memstore flush from ali.jpeg

(图片来源:阿里开发者社区

2. StoreFile Compaction

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

Compaction与Flush不同之处在于:Flush是针对一个Region整体执行操作,而Compaction操作是针对Region上的一个Store而言,从逻辑上看,Flush操作粒度较大。

2.1 Minor Compaction 、Major Compaction

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

Minor Compaction会将临近的若干个较小的HFile合并成一个较大的HFile,但不会清理过期和删除的数据。

Major Compaction会将一个Store下的所有的HFile合并成一个大HFile,并且会清理掉过期和删除的数据。

HBase StoreFile Compaction.png

2.2 Compaction 触发条件

2.2.1 Memstore Flush

在进行Memstore Flush前后都会进行判断是否触发Compact。

flush之前,先判断该region上是否有store中的hfile文件个数大于hbase.hstore.blockingStoreFiles,有则触发compact操作。

flush之后,对当前store中的文件数进行判断,是否满足规定的触发条件,满足则触发compaction操作。

具体的代码查看:MemStoreFlusher.flushRegion()方法。

2.2.2 定期检查线程

周期性检查是否需要进行compaction操作,周期为:hbase.server.thread.wakefrequency*hbase.server.compactchecker.interval.interval.multiplier

参数hbase.server.thread.wakefrequency默认值 10000 即 10s,是HBase服务端线程唤醒时间间隔,用于log roller、memstore flusher等操作周期性检查;

参数 hbase.server.compactchecker.interval.multiplier默认值1000,是compaction操作周期性检查乘数因子。

具体的代码查看:CompactionChecker.chore()方法。

2.2.3 手动执行Compaction命令

手动触发大多都是major compaction,避开业务高峰期进行major compaction,或修改了表的属性需要立即生效,或需要物理删除已删除的数据和过期的数据。

HBase Shell、Master UI者HBase API 执行 compact、major_compact等命令。

2.3 Compaction 参数解析

为了达到选择尽量多的合并小文件的同时也减少IO,同时合并这些文件之后对读的性能会有显著的提升。

compaction源码入口可以从CompactSplitThread.requestCompactionInternal方法进行查看。

首先都会对store上的hfile进行逐一排查,排除不满足条件的文件,条件如下:

​ a. 排除正在进行compaction的文件以及比这些文件更新的文件。

​ b. 排除hfile大小大于hbase.hstore.compaction.max.size或在高峰期时大于hbase.hstore.compaction.max.size.offpeak的hfile。

满足了条件的文件再通过策略去判断是否满足以下的Compaction 相关参数,再作是否需要compact。

2.3.1 Major Compaction 参数

Major Compaction涉及的参数比较少,主要有大合并时间间隔与一个抖动参数因子

hbase.hregion.majorcompaction 默认值604800000,单位ms Major compaction周期性时间间隔 
HBase 0.96.x及之前默认为1天 设置为 0 时表示禁用自动触发major compaction

hbase.hregion.majorcompaction.jitter 默认值0.5 抖动参数
为了避免major compaction同时在各个regionserver上同时发生,避免此操作给集群带来很大压力

2.3.2 Minor Compaction 参数

Minor compaction涉及的参数比major compaction要多,各个参数的目标是为了选择合适的HFile

hbase.hstore.compaction.min :默认值 3 早期参数名称为 hbase.hstore.compactionthreshold。
表示进行compaction最少的hfile文件数。

hbase.hstore.compaction.max : 默认值 10
一次minor compaction最多合并的HFile数量。

hbase.hstore.compaction.min.size :默认值 128M(memstore flush size)
进行compaction的最小hfile大小,小于该值的hfile会直接被放入候选文件中。

hbase.hstore.compaction.max.size :默认值Long.MAX_VALUE
进行compaction的最大hfile大小,大于该值的hfile不会被放入候选文件中。

hbase.hstore.compaction.ratio :默认值1.2
进行compaction的ratio。

hbase.hstore.compaction.ratio.offpeak : 默认值5.0
非高峰期的ratio控制
这个参数受另外两个参数 hbase.offpeak.start.hour 与 hbase.offpeak.end.hour 控制,这两个参数值为[0, 23]的整数,用于定义非高峰期时间段,默认值均为-1表示禁用非高峰期ratio设置。

2.4 Compaction 策略介绍

HBase的compaction policy准确的说有4种,分别是RatioBasedCompactionPolicyExploringCompactionPolicy、FIFOCompactionPolicy 以及 StripeCompactionPolicy。

其中,HBase使用的压缩策略主要就是前两种,HBase 0.96.x版本之前,默认的压缩策略是RatioBasedCompactionPolicy,HBase 0.96.x以及更新版本中,默认为ExploringCompactionPolicy。

ExploringCompactionPoliy要比旧版本中的RatioBasedCompactionPolicy 性能更高,因此一般情况下也不建议改变默认配置。

2.5 Compaction对于读写操作的影响

2.5.1 存储上的写入放大

HBase Compaction会带来写入放大,特别是在写多读少的场景下,写入放大就会比较明显。

下图简单示意了写入放大的效果。

(图片来源:https://mmbiz.qpic.cn/mmbiz_png/licvxR9ib9M6D6sDjXPZxHR1ic4LDKyicf2qfx417fJ8QHmfn82uBhSS1fC4mDqSB67JHzGs6kyqHiccrQEu2ryPKJA/640

随着minor compaction以及major Compaction的发生,可以看到,这条数据被反复读取/写入了多次,这是导致写放大的一个关键原因,这里的写放大,涉及到网络IO与磁盘IO,因为数据在HDFS中默认有三个副本。

2.5.2 读路径上的延时毛刺

HBase执行compaction操作结果会使文件数基本稳定,进而IO Seek次数相对稳定,延迟就会稳定在一定范围。然而,compaction操作会带来很大的带宽压力以及短时间IO压力。因此compaction就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。这种短时间的压力就会造成读请求在延时上会有比较大的毛刺。

下图是一张示意图,可见读请求延时有很大毛刺,但是总体趋势基本稳定。

读路径上的延时毛刺.png

(图片来源:http://img0.tuicool.com/3m2aIjv.png

2.5.3 写请求上的短暂阻塞

Compaction对写请求也会有比较大的影响。主要体现在HFile比较多的场景下,HBase会限制写请求的速度。如果底层HFile数量超过hbase.hstore.blockingStoreFiles 配置值,默认10,flush操作将会受到阻塞,阻塞时间为hbase.hstore.blockingWaitTime,默认90000,即1.5分钟,在这段时间内,如果compaction操作使得HFile下降到blockingStoreFiles配置值,则停止阻塞。另外阻塞超过时间后,也会恢复执行flush操作。这样做可以有效地控制大量写请求的速度,但同时这也是影响写请求速度的主要原因之一。

2.6 Compaction 总结

HBase Compaction操作是为了数据读取做的优化,总的来说是以牺牲磁盘io来换取读性能的基本稳定。Compaction操作分为minor compaction与major compaction,其中major compaction消耗资源较大、对读写请求有一定影响,因此一般是禁用自动周期性执行而选择业务低峰期时手动执行。

HBase column family HStore compact from ali.jpeg

(图片来源:阿里开发者社区

3. Region Split

默认情况下,每个Table起初只有一个Region,随着数据的不断写入,达到一定的大小Region会自动进行Split。刚拆分时,两个子Region都位于当前的Region Server,但处于负载均衡的考虑,HMaster有可能会将某个Region转移给其他的Region Server。table在region中是按照row key来排序的,并且一个row key所对应的行只会存储在一个region中,这一点保证了Hbase的强一致性 。

在一个region中有一个或多个stroe,每个stroe对应一个column families(列族)。一个store中包含一个memstore 和 0 或 多个store files。每个column family 是分开存放和分开访问的。

注意,Region Split 是针对所有的列族进行的,这样做的目的是同一行的数据即使在 Split 后也是存在同一个 Region 的。

3.1 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版本之后)。

3.2 split的三种方式

3.2.1 Pre-splitting

当一个table刚被创建的时候,Hbase默认的分配一个region给table。也就是说这个时候,所有的读写请求都会访问到同一个regionServer的同一个region中,这个时候就达不到负载均衡的效果了,集群中的其他regionServer就可能会处于比较空闲的状态。解决这个问题可以用pre-splitting,在创建table的时候就配置好,生成多个region。

Hbase自带了两种pre-split的算法,分别是 HexStringSplitUniformSplit 。如果我们的row key是十六进制的字符串作为前缀的,就比较适合用HexStringSplit,作为pre-split的算法。例如,我们使用HexHash(prefix)作为row key的前缀,其中Hexhash为最终得到十六进制字符串的hash算法。我们也可以用我们自己的split算法。

hbase shell

hbase> hbase org.apache.hadoop.hbase.util.RegionSplitter pre_split_table HexStringSplit -c 10 -f f1

-c 10 的意思为,最终的region数目为10个;-f  f1为创建一个那么为f1的 column family.
hbase> scan 'hbase:meta'
scan hbase_meta.png

只截取了meta表中的2个region的记录(一共10个region),分别是rowkey范围是 '' ''~19999999 和19999999~33333332的region。

也可以自定义切分点,例如在hbase shell下使用如下命令:

hbase> create 't1', 'f1', {SPLITS => ['10', '20', '30', '40']}

或者

$ echo -e  "anbnc" >/tmp/splits
hbase(main):015:0> create 'test_table', 'f1', SPLITSFILE=>'/tmp/splits'

3.2.2 自动splitting

当一个reion达到一定的大小,他会自动split称两个region。

Hbase版本是0.94 ,那么默认的有三种自动split的策略,ConstantSizeRegionSplitPolicy,IncreasingToUpperBoundRegionSplitPolicy还有 KeyPrefixRegionSplitPolicy

在0.94版本之前ConstantSizeRegionSplitPolicy 是默认和唯一的split策略。当某个store(对应一个column family)的大小大于配置值 ‘hbase.hregion.max.filesize’的时候(默认10G)region就会自动分裂。

而0.94版本中,IncreasingToUpperBoundRegionSplitPolicy 是默认的split策略。

这个策略中,最小的分裂大小和table的某个RegionServer的region 个数有关,当store file的大小大于如下公式得出的值的时候就会split,公式如下

Min (R的立方^2 * “hbase.hregion.memstore.flush.size”, “hbase.hregion.max.filesize”)  

R为同一个table中在同一个region server中region的个数。

可以通过配置hbase.regionserver.region.split.policy来指定split策略,我们也可以写我们自己的split策略。

3.2.3 强制split

Hbase 允许客户端强制执行split,在hbase shell中执行以下命令:

 hbase> split 'forced_table', 'b' 
 
 //其中forced_table 为要split的table , ‘b’ 为split 点

HBase region splitfrom ali.jpeg

(图片来源:阿里开发者社区

MemStore Flush参考

HBASE-10201

HBASE-3149

HBASE-5349

http://lxw1234.com/archives/2016/09/719.htm

[过往记忆]](https://www.iteblog.com/archives/2497.html#_MemStore)

compaction参考

https://www.jianshu.com/p/eef5dc6f3cf4

https://blog.csdn.net/u011598442/article/details/90632702

http://hbase.apache.org/book.html#compaction
https://mp.weixin.qq.com/s/ctnCm3uLCotgRpozbXmVMg
https://blog.csdn.net/cangencong/article/details/72763265
https://blog.csdn.net/shenshouniu/article/details/83902291

Region Splitting 参考

https://blog.cloudera.com/apache-hbase-region-splitting-and-merging/

参考 https://developer.aliyun.com/article/73490

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