zookeeper技术内幕

【简介】zookeeper是为分布式应用所设计的高可用、高性能且一致的开源协调服务,是大数据分布式集群的基础组件,为集群提供分布式锁服务。同时,用户可以在分布式锁的基础上开发其他功能,例如配置维护、分布式通知/协调、组服务、分布式消息队列等。

一. 典型应用场景

ZooKeeper是一个高可用的分布式数据管理与协调框架,基于对ZAB算法的实现,该框架能够很好地保证分布式环境中数据的一致性。

1.1 数据发布/订阅

数据发布/订阅系统,即所谓的配置中心,发布者将数据发布到ZooKeeper的一个或多个节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是有客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。而ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知后,需要主动到服务端获取最新的数据。

1.2 命名服务

通过调用ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。在ZooKeeper中,每一个数据节点都能维护一份子节点的顺序序列,当客户端对其创建一个顺序子节点的时候,ZooKeeper会自动以后缀的形式在其子节点上添加一个序号。

全局唯一ID生成的节点示意图.PNG

1.3 分布式协调/通知

ZooKeeper的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。基于ZooKeeper实现分布式协调与通知功能,通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发生变化,所有订阅的客户端都能够接收到相应的Watcher通知,并做出相应的处理。

1.4 Master选举

ZooKeeper的强一致性能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZooKeeper将会保证客户端无法重复创建一个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会注册一个子节点变更的Watcher,用于监控当前Master机器是否存活,一旦发现当前的Master挂了,其余的客户端将会重新进行Master选举。

1.5 分布式锁

分布式锁:控制分布式系统之间同步访问共享资源的一种方式,如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性。
排他锁:所有客户端都会试图通过调用create接口创建临时子节点,ZooKeeper会保证所有的客户端中最终只有一个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端需要注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。当获取锁的客户端机器宕机或政策执行完业务逻辑后,客户端会主动删除自己创建的临时节点。其他客户端再次重新发起获取锁。
共享锁:所有客户端在需要获取共享锁时创建一个临时顺序节点(节点名区分读写请求)。根据共享锁的定义,不同的事务都可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何读取操作的情况下进行。基于以上原则,则可以通过ZooKeeper的节点来确定分布式读写顺序。

共享锁节点示意图.PNG

1.6 大型分布式系统常见应用场景

HDFS-NameNode: Active/Standby选举 < /hadoop-ha>;
YARN-ResourceManager: Active/Standby选举< /yarn-leader-election>;
Hive-HiveServer2: HA高可用配置< /hiveserver2>;
HBase:Master Active/Standby选举,基础元数据管理 < /hbase-unsecure>;
kafka: 元数据管理,消费索引维护 < /brokers, /consumers>;
...

二. 数据模型

ZooKeeper采用树形层次结构,树中的每个节点被称为—Znode:

ZK数据模型.PNG

Znode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。

2.1 Znode结构

Znode包含文件和目录两种特性,既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分。每个Znode由3部分组成:
① stat:维护Znode的状态信息;
② data:该Znode关联的数据;
③ acl:访问控制列表,用于控制Znode的访问权限(读写、创建、删除等);

stat存储的状态信息:
cZxid:创建Znode的事务ID;
ctime:Znode的创建时间;
mZxid:最后一次修改Znode的事务ID;
mtime:最近一次修改Znode的时间;
pZxid: Znode添加或删除子节点操作的事务ID;
cversion:Znode子节点更改次数;
dataVersion:Znode的数据更改次数;
aclVersion:Znode ACL的更改次数;
ephemeralOwner:临时节点所有者的session id,如果此节点为持久节点,则值为0;
dataLength:Znode数据长度;
numChildren:Znode子节点个数;

事务ID:能够改变zookeeper服务器状态的操作称为事务操作,一般包括数据节点创建和删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务操作,zookeeper都会为其分配一个全局唯一的事务ID,用Zxid来表示,通常是一个64位数字。每一个Zxid对应一次更新操作,从这些Zxid中可以间接地之别出zookeeper出力这些更新操作请求的全局顺序。

2.2 节点类型

持久节点(PERSISTENT): 节点创建后一直存在,只能被客户端显式删除;
持久连续节点(PERSISTENT_SEQUENTIAL):同持久节点,且该节点创建子节点时,自动为子节点的命名末尾添加递增编号,用于记录下每个节点创建的先后顺序;
临时节点(EPHEMERAL):生命周期和客户端的会话绑定,客户端断开连接后自动删除节点;
临时顺序节点(EPHEMERAL_SEQUENTIAL):同临时节点,且节点命名末尾自动添加递增编号;
容器节点(CONTAINER):如果节点中最后一个子Znode被删除,将会触发删除该Znode;
持久定时节点(PERSISTENT_WITH_TTL):客户端断开连接后不会自动删除Znode,如果该Znode没有子Znode且在给定TTL时间内无修改,该Znode将会被删除;
持久顺序定时节点(PERSISTENT_SEQUENTIAL_WITH_TTL):同PERSISTENT_WITH_TTL,且Znode命名末尾自动添加递增编号;

临时节点:生命周期依赖于创建它们的会话,一旦会话结束,临时节点将被删除(也可以在会话未结束时手动删除)。这一特性也决定了临时节点不能包含子节点。
永久节点:节点的生命周期不依赖于会话,只能被客户端显式删除。
连续节点:Znode命名结尾添加一个递增的计数,这个计数对于此节点的父节点来说是唯一的,它的格式为"%10d"(10位数字,没有数值的数位用0补充,例如"0000000001")。当计数值大于232-1时,计数器将溢出。

2.3 Znode基本操作

create: 创建Znode (父Znode 必须已经存在);
delete: 删除Znode(该节点必须不包含任何子Znode );
exists: 测试Znode是否存在,如果存在则获取Znode状态信息;
getACL/setACL: 获取/设置Znode ACL权限;
getChildren: 获取子Znode的列表;
getData/setData: 获取/设置Znode data;
sync: 同步client和zookeeper的znode信息;

三. 监听器

ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。
典型应用场景:定义一个一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。

watcher机制.PNG

概述:客户端在向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。

接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知的逻辑,包含KeeperState和EventType两个枚举类。分别代表了通知状态和事件类型。同一个事件类型在不同的通知状态中代表的含义有所不同。

回调方法process()

process()方法是Watcher接口中的一个回调方法,当ZooKeeper想客户端发送一个Watcher事件通知后,客户端就会对相应的process()方法进行回调,从而实现对事件的处理。process()方法的定义如下:

abstract public void process(WatchedEvent event);

WatchedEvent包含了每一个事件的三个基本属性:

public class WatchedEvent {
  final private KeeperState keeperState;   //通知状态
  final private EventType eventType;   //事件类型
  private String path;  //节点路径

  ...

    /**
     *  Convert WatchedEvent to type that can be sent over network
     */
    public WatcherEvent getWrapper() {
        return new WatcherEvent(eventType.getIntValue(), 
                                keeperState.getIntValue(), 
                                path);
    }
}

WatchedEvent与WatcherEvent的区别:
WatchedEvent类路径:org.apache.zookeeper.WatchedEvent;
WatcherEvent类路径:org.apache.zookeeper.proto.WatcherEvent;

public class WatcherEvent implements Record{
  private int type;
  private int state;
  private String path;

  public void serialize(OutputArchive a_, String tag) throws IOException{ ... }
  public void deserialize(InputArchive a_, String tag) throws IOException{ ... }
}

笼统地讲,两者表示的是同一个事物,都是对一个服务端事件的封装。不同的是,WatchedEvent是一个逻辑事件,用于服务端和客户端程序执行过程中所需的逻辑对象,而WatcherEvent因为实现了序列化接口,因此可以用于网络传输。
服务端在生成WatchedEvent事件之后,会调用getWrapper方法将自己包装成一个可序列化的WatcherEvent事件,以便通过网络传输到客户端。客户端在接收到服务端的这个事件对象后,首先会将WatcherEvent事件还原成一个WatchedEvent事件,并传递给process方法处理。

四. ACL

ACL(Access Control List) : ZooKeeper作为一个分布式协调框架,器内部存储的都是一些关乎分布式系统运行时状态的元数据,尤其是一些涉及分布式锁、Master选举和分布式协调等应用场景的数据,会直接影响基于ZooKeeper进行构建的分布式系统的运行状态。因此,ZooKeeper提供了一套完善的ACL权限控制机制来保障数据的安全。
一个有效的ACL信息:scheme(权限模式):id(授权对象):permission(权限)

4.1 Scheme

权限模式用来确定权限验证过程中使用的检验策略,ZooKeeper中包括以下四种权限模式:
IP: IP模式通过IP地址来进行权限控制,例如配置了“ip:192.168.0.110”,即标签权限控制都是针对这个IP地址的(IP模式也支持按照网段的方式进行配置);
Digest:以"username:password"形式的权限标识来精选权限配置,便于区分不同应用来进行权限控制;
World:数据节点的访问权限对所有用户开放,即所有用户都可以在不进行任何权限校验的情况下操作ZooKeeper上的数据,它只有一个权限标识,即"world:anyone";
Super: 超级用户,可以对ZooKeeper上任意的数据节点进行任何操作;

备注:ZooKeeper 除以上4种默认权限模式外,还提供了特殊的权限控制插件体系,允许开发人员通过指定方式对ZooKeeper的权限精选扩展。

4.2 ID

授权对象指的是权限赋予的用户或一个指定实体,例如IP地址或是机器等。在不同的权限模式下,授权对象是不同的。

4.3 Permission

权限就是指那些通过权限检查后可以被允许执行的操作,所有对数据的操作权限分为以下五类:

CREATE(c): 数据节点的创建权限,允许授权对象在该数据节点下创建子节点;
DELETE(d): 子节点的删除权限,允许授权对象删除该数据节点的子节点;
READ(r): 数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等;
WRITE(w): 数据节点的更新权限, 允许授权对象对该数据节点进行更新操作;
ADMIN(a): 数据节点的管理权限,允许授权对象对该数据节点进行ACL相关的设置操作;

4.4 设置ACL

在数据节点创建时进行ACL权限的设置:

create [-s] [-e] path data acl
create -e /zk-book init digest:foo: MiGs3Eiy1pP4rvH1Q1N1wbP+oUF8=:cdrwa

对已经创建的数据节点进行ACL权限的设置:

set path acl
set /zk-book init digest:foo: MiGs3Eiy1pP4rvH1Q1N1wbP+oUF8=:cdrwa

查看数据节点的ACL设置:

getAcl path
getAcl /zk-book

五. Leader&Follower&Observer

ZooKeeper服务器角色:
Leader:① 事务请求的唯一调度和处理者,保证集群事务处理的顺序性; ② 集群内部各服务器的调度者;
Follower:①处理客户端非事务请求,转发事务请求给Leader服务器;②参与事务请求Proposal的投票;③参与Leader选举投票;
Observer:①处理客户端非事务请求,转发事务请求给Leader服务器; ②参与事务请求Proposal的投票;

服务器状态:
LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,需要进入Leader选举流程;
FOLLOWING:跟随者状态,表明当前服务器角色是Follower;
LEADING:领导者状态,表明当前服务器角色是Leader;
OBSERVING:观察者状态,表明当前服务器角色是Observer;

5.1Leader选举概述

当超过一台ZooKeeper服务器启动,且服务器之间已经能够进行互相通信,每台服务器都试图找到一个Leader时,便需要进入Leader选举流程。ZooKeeper集群正常运行过程中,一旦选举出了Leader,那么所有服务器的集群角色一般不会发生变化(即使集群中有非Leader角色的服务器挂了或者有新机器加入到集群)。但是当Leader服务器挂了,那么整个集群将无法对外提供服务,直到新一轮的Leader选举完毕。服务器启动时期的Leader选举与运行期间的Leader选举过程基本一致。参考代码: org.apache.zookeeper.server.quorum.FastLeaderElection

Leader选举流程示意图.PNG

当服务器检测到当前服务器状态为LOOKING时,就会调用FastLeaderElection.lookForLeader方法来进行Leader的重新选举:
1. 自增选举轮次
FastLeaderElection.logicalclock用于标识当前Leader的选举次数,ZooKeeper规定了所有有效的投票都必须在同一轮次中。ZooKeeper在开始新一轮的投票(调用FastLeaderElection.lookForLeader方法)时,会首先对logicalclock进行自增操作。
2. 初始化选票

this.id = id;     //唯一标识一台ZooKeeper服务器(sid),与myid值一致(server.id)
this.zxid = zxid;    //事务ID,用来唯一标识一次服务器状态的变更
this.electionEpoch = -1;    //当前服务器的选举轮次
this.peerEpoch = peerEpoch;    //被推举的服务器的选举轮次
this.state = ServerState.LOOKING;

3. 发送初始化投票
每台ZooKeeper服务器都会发起第一次投票(投给自己),然后将初始化投票放入sendqueue队列中。

LinkedBlockingQueue<ToSend> sendqueue:选票发送队列,用于保存待发送的选票;
LinkedBlockingQueue<Notification> recvqueue:选票接收队列,用于保存接收到的外部选票;
WorkerSender ws:选票发送器,后台线程;
WorkerReceiver wr:选票接收器,后台线程;

4. 接收外部投票
每台ZooKeeper服务器都会不断从recvqueue队列中获取外部投票,如果服务器发现无法获取任何外部投票,那么就会立即确认是否和集群中其他服务器保持有效连接。

if (n == null) {
   if (manager.haveDelivered()) { sendNotifications(); }   //如果已经建立连接,则再次发送自己当前内部投票
   else { manager.connectAll(); }   //如果连接未建立则重连
} 

5. 判断选举轮次
发送完初始化选票后,开始处理外部投票(其他服务器发送的投票)。ZooKeeper规定了所有有效的投票都必须在同一选举轮次中,在处理外部投票时,会根据选举轮次进行不同的处理。
外部投票的选举轮次大于内部投票
立即跟新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的选票来进行PK是否变更内部投票(服务器自身当前的投票),最终将内部投票发送出去。
外部投票的选举轮次小于内部投票
忽略该外部投票,不做任何处理,继续处理下一个外部投票。
外部投票的选举轮次与内部投票一致
开始进行选票PK。
6. 选票PK
确定当前服务器是否需要变更投票(FastLeaderElection.totalOrderPredicate)

  protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
        LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" +
                Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid));
        if(self.getQuorumVerifier().getWeight(newId) == 0){
            return false;
        }

        /*
         * We return true if one of the following three cases hold:
         * 1- New epoch is higher
         * 2- New epoch is the same as current epoch, but new zxid is higher
         * 3- New epoch is the same as current epoch, new zxid is the same
         *  as current zxid, but server id is higher.
         */

        return ((newEpoch > curEpoch) ||
                ((newEpoch == curEpoch) &&
                ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
    }

规则1:如果外部投票被推举的Leader服务器选举轮次大于内部投票(newEpoch > curEpoch),需要变更投票;
规则2:如果如果选举轮次一致,则比较两者ZXID(newZxid > curZxid),如果外部投票的ZXID大于内部投票,则需要变更投票;
规则3:如果两者的ZXID也一致,则比较两者的SID,如果外部投票的SID大于内部投票,则需要变更投票;
7. 变更投票
选票PK后,如果确定了外部投票所推举的服务器更适合成为Leader,那么就需要变更投票——使用外部投票的选票信息来覆盖内部投票。变更完成后,需要将变更后的内部投票再次发送出去。
8. 选票归档并统计
无论是否进行投票变更,都会将刚刚处理的那份外部投票放入”投票集合“recvset(Map<Long, Vote> recvset = new HashMap<Long, Vote>();)进行归档(recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));)。归档后开始本次投票统计,如果集群中有过半(n/2+1)服务器认可了当前的投票termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch)),则更新服务器状态并终止投票。
9. 更新服务器状态
统计投票后,如果确定有过半(n/2+1)服务器认可了当前的投票,则需要更新服务器状态。

//更新服务器状态
self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState());
//最终的选票
Vote endVote = new Vote(proposedLeader,proposedZxid, proposedEpoch);
leaveInstance(endVote);    //清空recvqueue队列的选票

六. 会话

ZooKeeper客户端与服务端创建完成连接创建后,就建立了一个会话,该具有全局唯一的会话ID(Session ID)。客户端和服务器之间维持的是一个长连接,会话创建时会配置一个会话超时时间,客户端会在会话超时时间过期范围内,不断向服务端发送PING请求来保持会话的有效性(心跳检测),服务端接收到这个心跳检测后会重新激活对应的客户端会话(会话激活的过程不仅能够使服务端检测到对应客户端的存活性,也能让客户端自己保持连接状态)。
ZooKeeper将所有的会话都分配在不同的区块中(分桶管理),分配的原则是每个会话的“下次超时时间点”(ExpirationTime,会话最近一次可能超时的时间点),将ExpirationTime相同的会话放在同一区块中进行管理。ExpirationTime计算公式如下:

long now = Time.currentElapsedTime();
//timeout=SessionTimeout,该会话设置的超时时间
 Long newExpiryTime = roundToNextInterval(now + timeout);  
//expirationInterval=tickTime,默认2000毫秒
  private long roundToNextInterval(long time) {
        return (time / expirationInterval + 1) * expirationInterval;
    }

session的分桶管理策略.PNG

每一次会话激活过程(心跳检测)都会将会话从老的区块中取出,放入next_ExpirationTime对应的新区块中,会话激活的过程就是一次会话迁移org.apache.zookeeper.server.SessionTrackerImpl

客户端激活会话流程图.PNG

七. 事务日志与数据快照

7.1 事务日志

存储路径:${dataDIr}/version-2/log.***
文件命名:log.${写入该事务日志文件第一条事务记录ZXID的十六进制}
日志内容:每一次事务操作的内容
查看方式:java -classpath .:${jar_path}/slf4j-api-1.6.1.jar:${jar_path}/zookeeper-3.4.5.jar org.apache.zookeeper.server.LogFormatter log.***

7.2 数据快照(snapshot)

存储路径:${dataDIr}/version-2/snapshot.***
文件命名:snapshot.${本次数据快照开始时刻服务器最新ZXID的十六进制}
日志内容:某一时刻的全量内存数据内容
查看方式:java -classpath .:${jar_path}/slf4j-api-1.6.1.jar:${jar_path}/zookeeper-3.4.5.jar org.apache.zookeeper.server.SnapshotFormatter log.***

八. 四字命令

conf    查看当前ZooKeeper服务器的配置信息;
cons    查看当前这台服务器上所有客户端连接的详细信息;
crst    重置所有的客户端连接统计信息;
dump    查看当前集群的所有会话信息;
envi    查看ZooKeeper所在服务器运行时的环境信息;
ruok    查看当前ZooKeeper服务器是否正在运行;
stat    查看ZooKeeper服务器的运行时状态信息;
srvr    同stat,区别于srvr不会将客户端的连接情况输出;
srst    重置所有服务器的统计信息;
wchs    查看当前服务器上管理的Watcher的概要信息;
wchc    同wchs,区别于以会话为单位进行归组,同时列出被该会话注册了Watcher的节点路径;
wchp    同wchc,区别于wchp以节点路径为单位进行归组;
mntr    同stat,区别于mntr输出内容更详细;

echo cons | nc 127.0.0.1 2181

参考资料:<<从PAXOS到ZOOKEEPER分布式一致性原理与实践>>
博客主页:https://www.jianshu.com/u/e97bb429f278

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

推荐阅读更多精彩内容