ZooKeeper 简介

分布式应用程序的分布式协调服务

ZooKeeper是一个分布式、开源的分布式应用程序协调服务。它公开了一组简单的原语,分布式应用程序可以在这些原语的基础上实现更高级别的服务,用于同步、配置维护、组和命名。它被设计为易于编程,并使用了一个数据模型,其风格类似于文件系统的目录树结构。它运行在Java中,并具有Java和C的绑定。
众所周知,协调服务很难做好。它们特别容易出现竞争条件和死锁等错误。

ZooKeeper的初心就是减轻分布式应用程序从零开始实现协调服务的责任。

设计目标

简洁

ZooKeeper允许分布式进程通过与标准文件系统组织类似的共享层次命名空间相互协调。名称空间由数据寄存器(用ZooKeeper的话说,称为znodes)组成,这些寄存器类似于文件和目录。

与设计用于存储的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐和低延迟。

ZooKeeper实现非常重视高性能、高可用性和严格有序的访问。

  • ZooKeeper的性能方面意味着它可以用于大型分布式系统。
  • 可靠性方面使其不会成为单点故障。
  • 严格的顺序意味着可以在客户机上实现复杂的同步原语。

复制

和它所协调的分布式进程一样,ZooKeeper本身也打算在一组称为合集的主机上进行复制。组成ZooKeeper服务的服务器必须相互了解。它们在持久存储中维护状态的内存映像以及事务日志和快照。只要大多数服务器可用,ZooKeeper服务就可用。
客户机连接到单个ZooKeeper服务器。客户机维护一个TCP连接,通过它发送请求、获取响应、获取监视事件和发送心跳。如果到服务器的TCP连接中断,客户机将连接到另一台服务器。

有序的

ZooKeeper给每个更新贴上一个数字,这个数字反映了所有ZooKeeper事务的顺序。后续操作可以使用该顺序实现更高级别的抽象,比如同步原语。

高速的

在“以读取为主”的工作负载中,它尤其快。ZooKeeper应用程序运行在数千台机器上,当读操作比写操作更常见时,它的性能最好,比率约为10:1。

数据模型和层次命名空间

ZooKeeper提供的名称空间很像标准文件系统。名称是由斜杠(/)分隔的路径元素序列。ZooKeeper名称空间中的每个节点都由一条路径标识。

节点和短暂节点

不同于标准文件系统,在ZooKeeper的命名空间的每个节点都可以具有数据跟孩子节点一样。 (ZooKeeper是用来存储协调数据的:状态信息,配置,位置信息等,所以每个节点存储的数据都很小,在几个字节到千级别字节范围),我们使用术语Znode,使之清楚,我们 正在谈论的ZooKeeper数据节点。

Znodes有一个stat结构,包含对数据的变更的版本号,ACL的变化,和时间戳,允许缓存验证和协调的更新。 每当Znode的数据发生变更,其版本号也会增加。例如,当客户端检索数据时也会检索数据的版本号。

存储在一个命名空间中的每个Znode的数据读取和写入都是原子的。读取获取与Znode节点相关联的所有数据字节和写入替换所有的数据。每个节点都有限制谁可以做什么的访问控制列表(ACL)。

ZooKeeper的也有短暂的节点的概念。这些znodes存在,只要创建的Znode的会话处于活动状态。 当会话结束时,Znode被删除。 当你想实现[TBD]短暂的节点是有用的。

条件更新和监控

Zookeeper支持监控功能,客户端可以在节点上设置一个监控,当节点变更的时候,监控可以被触发和移除。当一个监控被触发,客户端接受到一个数据包,标识znode节点发生了变更。如果客户端和服务端的链接断开了,客户端将收到一个本地通知.

保证

Zookeeper非常快和简洁,但是,由于它的目标是作为构建更复杂服务(比如同步)的基础,所以它提供了一组保证。如下:

  • 顺序一致性——来自客户机的更新将按发送顺序应用。
  • 原子性——更新成功或失败。没有部分结果。
  • 单个系统映像——无论服务连接到哪个服务器,客户机都将看到相同的服务视图。
  • 可靠性——一旦应用了更新,它将从那时起一直持续到客户机覆盖更新为止。
  • 及时性——保证系统的客户端视图在一定的时间范围内是最新的。

相关接口

ZooKeeper的设计目标之一是提供一个非常简单的编程接口。因此,它只支持这些操作

  • create:树中的一个位置创建一个节点
  • delete:删除一个节点
  • exists:测试一个位置上的节点是否存在
  • get data:从节点上读取数据
  • set data:写数据到节点上
  • get children:检索节点上的子节点
  • sync:等待要传播的数据

实现

除了请求处理器之外,除请求处理器外,构成ZooKeeper服务的每个服务器都复制其自己的每个组件副本。

复制数据库是包含整个数据树中的内存数据库。将更新记录到磁盘以确保可恢复性,并且将写入操作序列化到磁盘之后再将其应用于内存数据库。

每个ZooKeeper服务器都为客户端提供服务。 客户端连接到确定的某一个服务器提交请求。 读取请求从每个服务器数据库的本地副本提供服务。 改变服务的状态、写请求的请求,由一个一致性协议处理。

作为一致性协议的一部分,所有来自客户端的写请求都被转发到一个名为leader的服务器上。其他的ZooKeeper服务器(称为followers)接收来自leader的消息建议,并就消息传递达成一致。消息层负责在失败时替换leader,并将followers与leader同步。

ZooKeeper使用自定义原子消息传递协议。因为消息层是原子的,所以ZooKeeper可以保证本地副本不会偏离。当leader接收到写请求时,它会计算要应用写时系统的状态,并将其转换为捕获这个新状态的事务。

单机模式

安装单机模式的Zookeeper很简单,服务器包含着单个jar文件中,所以,安装包内包含创建一个配置文件,一旦下载下了Zookeeper,解压他,并cd到根目录下.为了启动Zookeeper,需要创建一个配置文件,下面的示例很简单,创建一个conf/zoo.cfg,内容如下:

tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181

根据实际情况,dataDir修改成存在的目录,下面介绍一下几个配置项的含义:

  • tickTime:基础时间单元,单位为微秒,用于心跳和最小session超时时长的2倍
  • dataDir:内存数据库快照的存储位置,除非另有说明,否则事务日志将更新到数据库
  • clientPort:客户端连接的监听端口

创建配置文件完成后,即可开启Zookeeper
bin/zkServer.sh start

Zookeeper日志系统使用的是log4j.你可以通过log4j配置来使日志输出到控制台打印或者日志文件中。
到目前位置,Zookeeper都是运行在单机模式下。没有副本,如果Zookeeper进程挂了,整个服务将暂定, 仅适用于开发者环境

管理Zookeeper存储

对于生产环境下,Zookeeper长时间运行。数据存储需要明确管理。

连接Zookeeper

bin/zkCli.sh -server 127.0.0.1:2181

就像是文件操作一样,一但你成功连接,可以看到如下信息:

Connecting to localhost:2181
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.ZooKeeper).
log4j:WARN Please initialize the log4j system properly.
Welcome to ZooKeeper!
JLine support is enabled
[zkshell: 0]

通过Shell,键入help,可以获得客户端可以执行的命令列表。如下:

[zkshell: 0] help
ZooKeeper host:port cmd args
    get path [watch]
    ls path [watch]
    set path data [version]
    delquota [-n|-b] path
    quit
    printwatches on|off
    create path data acl
    stat path [watch]
    listquota path
    history
    setAcl path acl
    getAcl path
    sync path
    redo cmdno
    addauth scheme auth
    delete path [version]
    deleteall path
    setquota -n|-b val path

到这里,你可以尝试输入一个简单的命令,ls

[zkshell: 8] ls /
[zookeeper]

接下来,通过create /zk_test mydata创建一个新的znode,这将创建一个新的znode并关联了字符串"my_data".可以看到:

[zkshell: 9] create /zk_test my_data
Created /zk_test

再次键入:

[zkshell: 11] ls /
[zookeeper, zk_test]

表明zk_test目录已经创建成功。
接下来,验证数据关联节点,通过get命名:

[zkshell: 12] get /zk_test
my_data
cZxid = 5
ctime = Fri Jun 05 13:57:06 PDT 2009
mZxid = 5
mtime = Fri Jun 05 13:57:06 PDT 2009
pZxid = 5
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0
dataLength = 7
numChildren = 0

我们可以变更zk_test关联的数据,通过set命令:

[zkshell: 14] set /zk_test junk
cZxid = 5
ctime = Fri Jun 05 13:57:06 PDT 2009
mZxid = 6
mtime = Fri Jun 05 14:01:52 PDT 2009
pZxid = 5
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0
dataLength = 4
numChildren = 0
[zkshell: 15] get /zk_test
junk
cZxid = 5
ctime = Fri Jun 05 13:57:06 PDT 2009
mZxid = 6
mtime = Fri Jun 05 14:01:52 PDT 2009
pZxid = 5
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0
dataLength = 4
numChildren = 0

最后,可以通过delete命令删除节点:

[zkshell: 16] delete /zk_test
[zkshell: 17] ls /
[zookeeper]
[zkshell: 18]

副本模式Zookeeper

单机模式的Zookeeper仅用于开发测试,生产环境下,你需要使用副本模式,同一应用程序中的复制服务器组称为quorum,并处于复制模式,quorum中的所有服务器都具有相同配置文件的副本。


注:在副本模式中,最小数目的服务器数量为3个,强烈建议,需要一个奇数数目的服务器,如果你只有两个服务器,那么当一个挂机后,没有足够数量的机器来仲裁选举,两台服务器本质上比一台服务器更不稳定,因为有两个单点故障。


这种模式下的配置文件,与单机模式下的配置文件,大同小异:

tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
initLimit=5
syncLimit=2
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

新的配置项:

  • initLimit: 集群中的follower服务器(F)与leader服务器(L)之间 初始连接 时能容忍的最多心跳数(tickTime的数量)。

此配置表示,允许 follower (相对于 leader 而言的“客户端”)连接 并同步到 leader 的初始化连接时间,它以 tickTime 的倍数来表示。当超过设置倍数的 tickTime 时间,则连接失败。

  • syncLimit: 标识参数设定了允许一个跟随者与一个领导者进行同步的时间,如果在设定的时间段内,跟随者未完成同步,它将会被集群丢弃。所有关联到这个跟随者的客户端将连接到另外一个跟随着。

server.X列表列出了组成Zookeeper服务的服务器列表。当服务启动后,通过文件的myid查找哪个服务器。这个文件包含ASCII形式的服务编码。

最后,每个服务器名后面,有两个端口,这个是两个端用于连接通信的端口。

Zookeeper数据模型

Zookeeper有一个命名空间模型,就像是分布式的文件系统一样。唯一的不同是,命名空间中的每一个节点都可以有与之关联的数据,和子节点一样。就像是文件系统中允许一个文件可以是一个目录一样。节点的路径总是表示为规范的、绝对的、斜杠分隔的路径。没有相对路径。
任何unicode字符都可以在受以下约束的路径中使用:

  • null字符(\u0000)不能作为路径的一部分。
  • 以下字符不能用,因为他们不能被很好的展示,或则以一种疑惑的方式:(\u0001-\u001F)和\u007F-\u009F
  • 下面的字符也不被允许\ud800-\uF8FF,\uFFF0-\uFFFF
  • .字符可以被用于名称的一部分,但是...不能单独用于在路径中表明一个节点。因为Zookeeper不能使用相对路径。所以下面的路径是非法的/a/b/./c/a/b/../c
  • zookeeper是保留字

ZNodes

在Zookeeper树中,每个节点作为一个znode。Znode维护一个包含数据更改、acl更改的版本号的stat结构。该结构也包含时间戳。版本号和时间戳,允许ZooKeeper验证缓存并协调更新。
每次节点的数据变更,版本号都会自动,例如,每次客户端检索数据,也会检索数据的版本号。当客户端进行一个更新或者删除操作时,需要提供变更的数据的版本号。如果版本号不匹配实际的数据版本号,更新操作失败。


注:Zookeeper中,znode标识数据节点,Servers标识组成Zookeeper服务的机器。 quorum peers指组成服务的一个整体。client指任何使用Zookeeper服务的主机或者进程

Znodes是开发者访问的主要实体,需要留意的点很多。

Watches

客户端可以在znodes上设置监控。节点上的变更将触发监控并清除监控。当一个监控被触发后,Zookeeper会发送一个通知在客户端上。

数据访问

一个命名空间下的每个节点的读和写都是自动的。读获取节点关联的所有数据,写替换节点关联的数据。每个节点都有一个控制列表(ACL)来限制谁可以做这些。

Zookeeper不是设计用来作为一个常规的数据库或者大对象的存储。相反,它管理协调数据。这些数据可以以配置、状态信息、集合等形式出现。各种形式的协调数据的一个共同特性是它们相对较小:以千字节为单位测量。ZooKeeper客户机和服务器实现都进行了完整性检查,以确保znode的数据少于1M,但平均数据应该比这少得多。在相对较大的数据大小上操作将导致某些操作比其他操作花费更多的时间,并且会影响某些操作的延迟,因为需要额外的时间将更多的数据通过网络转移到存储介质上。如果需要大数据存储,通常处理此类数据的模式是将其存储在一个大存储系统上,比如NFS或HDFS,并将指向ZooKeeper中存储位置的指针存储。

临时节点

ZooKeeper也有临时节点的概念。只要创建znode的会话处于活动状态,这些znode就会存在。当会话结束时,删除znode。由于这种行为,临时znode不允许有子节点。

序列节点——惟一命名

在创建znode时,您还可以请求ZooKeeper在路径的末尾添加一个单调递增的计数器。

容器节点(3.5.3版本增加)

ZooKeeper有容器znode的概念。容器znode是一种特殊用途的znode,可用于菜谱,如leader、lock等。当删除容器的最后一个子元素时,容器将成为将来某个时候服务器要删除的候选对象。给定此属性,您应该准备获取KeeperException。在容器znode内创建子节点时使用NoNodeException。例如,当在容器znodes内创建子znode时,总是检查KeeperException。发生时重新创建容器znode。

TTL 节点(3.5.3版本增加)

在创建持久或持久顺序znode时,可以选择为znode设置一个以毫秒为单位的TTL。如果znode没有在TTL中进行修改,并且没有子节点,那么它将成为将来某个时候由服务器删除的候选节点。注意:TTL节点必须通过系统属性启用,因为它们在默认情况下是禁用的。有关详细信息,请参阅管理员指南。如果您试图创建没有适当系统属性集的TTL节点,服务器将抛出KeeperException.UnimplementedException。

Time in ZooKeeper

Zookeeper使用多种方式记录时间:

  • Zxid:对ZooKeeper状态的每次更改都会收到一个zxid (ZooKeeper事务Id)形式的戳记,这将向ZooKeeper公开所有更改的总顺序。每个更改都有一个惟一的zxid,如果zxid1小于zxid2,则zxid1发生在zxid2之前。
  • Version numbers:对节点的每一次更改都会导致该节点的版本号之一增加。这三个版本号是version(对znode数据的更改数量)、cversion(对znode子节点的更改数量)和hate(对znode ACL的更改数量)。
  • Ticks:当使用多服务器ZooKeeper时,服务器使用ticks来定义事件的时间,例如状态上传、会话超时、对等点之间的连接超时等。Tick时间仅通过最小会话超时(Tick时间的2倍)间接公开;如果客户机请求的会话超时小于最小会话超时,服务器将告诉客户机会话超时实际上是最小会话超时。
  • Real time:ZooKeeper根本不使用实时或时钟时间,除了在znode创建和修改时将时间戳放入stat结构之外。

ZooKeeper Stat Structure

每个节点的stat结构都由一下字段组成:

  • czxid:节点创建的zxid变化
  • mzxid:节点最后修改的zxid的变化
  • pzxid:子节点最后修改的zxid的变化
  • ctime:节点创建的微秒形式
  • mtime:节点最后修改时间的微秒形式
  • version:节点数据变更的版本号
  • cversion:子节点变更的版本号
  • aversion:节点Acl变更的版本号
  • ephemeralOwner:如果节点是临时节点,该字段为节点的session id,如果是非临时节点,为零。
  • dataLength:节点数据字段的长度
  • numChildren:子节点的数据

Zookeeper Sessions

ZooKeeper客户端通过使用语言绑定创建服务句柄来与ZooKeeper服务建立会话.创建好句柄后,句柄将以连接状态启动,客户端库将尝试连接到构成ZooKeeper服务的服务器之一,此时它将切换到连接状态。在正常操作期间,客户端句柄将处于这两种状态之一。如果出现不可恢复的错误,比如会话过期或身份验证失败,或者应用程序显式关闭句柄,句柄将移动到关闭状态。

要创建一个客户端会话,应用程序代码必须提供一个连接字符串,其中包含一个逗号分隔的host:port对列表,每个host:port对对应于ZooKeeper服务器(例如“127.0.0.1:4545”或“127.0.0.1:3000、127.0.0.1:3001 127.0.0.1:3002”)。ZooKeeper客户端库将选择一个任意的服务器并尝试连接到它。如果这个连接失败,或者客户机由于任何原因与服务器断开连接,客户机将自动尝试列表中的下一个服务器,直到(重新)建立连接。

我们允许客户端通过提供一个新的逗号分隔的host:port对列表来更新连接字符串,每个host:port对对应于ZooKeeper服务器。该函数调用一个概率负载平衡算法,该算法可能导致客户端与其当前主机断开连接,目标是在新列表中实现每个服务器预期的统一连接数。如果客户端连接到的当前主机不在新列表中,此调用将始终导致连接被删除。否则,决策将基于服务器的数量是增加了还是减少了.

例如,如果以前的连接字符串包含3台主机,而现在列表包含这3台主机和另外2台主机,那么连接到这3台主机的40%的客户端将移动到其中一台新主机,以平衡负载。该算法将导致客户端断开与当前主机的连接,该连接的概率为0.4,在本例中,将导致客户端连接到随机选择的两个新主机之一。

ZooKeeper Watches

Zookeeper中所有的读操作,如getData(),getChildren()和exists()-都有一个设置监控的副作用选项。Zookeeper中的监控有三种:one-time trigger,sent to the client 和the data for which the watch was set.
设置监控的三个要点:

  • one time trigger:当数据变更,一个监控事件将被发送到客户端,例如:如果一个客户端做了一个getData("/zsnode1", true),之后/zsnode1节点的数据发生了变更或者删除,客户端将收到一个监听事件。如果再次发生变更,就没有监听事件发送了,除非客户端做了其他的读操作设置里新的监控。
  • sent to the client这意味着一个事件正在发送到客户机的路上,但是可能在成功地将更改操作的返回代码发送到发起更改的客户机之前无法到达客户机。手表以异步方式发送给观察者。ZooKeeper提供了一个订购保证:客户端在第一次看到watch事件之前,永远不会看到设置了手表的更改。网络延迟或其他因素可能导致不同的客户端在不同的时间看到手表并从更新中返回代码。关键是不同客户看到的所有东西都有一个一致的顺序。
  • The data for which the watch was set:这指的是节点更改的不同方式。将ZooKeeper看作是维护两个表列表是有帮助的:数据表和子表。getData()和exists()设置数据手表。getChildren()设置子手表。或者,可以考虑根据返回的数据类型设置手表。getData()和exists()返回关于节点数据的信息,而getChildren()返回子节点的列表。因此,setData()将触发正在设置的znode的数据监视(假设设置成功)。成功的create()将触发

手表在客户机连接到的ZooKeeper服务器上本地维护。这使得手表的设置、维护和分派都很轻量。当客户端连接到新服务器时,将为任何会话事件触发手表。当与服务器断开连接时,将不会接收到手表。当客户端重新连接时,任何以前注册的手表都将重新注册并在需要时触发。一般来说,这一切都是透明的。有一种情况下可能会错过一个手表:如果创建了znode,则会错过一个尚未创建znode的手表

监控的定义

我们可以使用读取ZooKeeper状态的三个调用来设置手表:exists、getData和getChildren。下面的列表详细说明了手表可以触发的事件以及启用这些事件的调用:

  • created 事件: 调用exists方法会启用
  • deleted 事件: 调用exists,getData,getChildren方法会启用
  • changed 事件: 调用exists,getData方法会启用
  • child 事件: 调用getChildren方法会启用

监控去除

可以使用removeWatches方法去除节点上注册的监控,此外,即使没有服务器连接,ZooKeeper客户机也可以通过将local标志设置为true来在本地删除手表。下面的列表详细说明了成功删除手表后将触发的事件。

  • Child Remove 事件:调用getChildren方法添加的监控
  • Data Remove 事件:调用exists方法和getData方法添加的监控

Zookeeper监控的作用

关于监控,Zookeeper维护一下保证:

  • 根据其他事件、其他监控和异步响应对监控。ZooKeeper客户端库确保按顺序分派所有内容。进行排序。ZooKeeper客户端库确保按顺序分派所有内容。
  • 在看到与该znode对应的新数据之前,客户机将看到它正在监视的znode的监视事件。
  • 来自ZooKeeper的监控事件的顺序对应于ZooKeeper服务所看到的更新的顺序。

监控事件需要注意的点

  • 监控是一次性的,如果你得到一个监控事件,并且在未来有变更的时候得到通知,你必须设置其他的监控。
  • 因为手表是一次性触发器,并且在获取事件和发送获取手表的新请求之间存在延迟,所以您不能可靠地查看ZooKeeper中节点发生的每个更改。准备处理znode在获取事件和再次设置手表之间多次更改的情况。
  • 对于给定的通知,监视对象或功能/上下文对仅触发一次。例如,如果为exists注册了相同的watch对象,并且为相同的文件调用了getData,然后删除了该文件,那么使用该文件的删除通知只会调用该watch对象一次。
  • 当您断开与服务器的连接时(例如,当服务器失败时),在重新建立连接之前,您不会得到任何监控。因此,会话事件被发送给所有未完成的监控处理程序。使用会话事件进入安全模式:断开连接时不会接收事件,因此您的流程应该在该模式下谨慎地工作。

Zookeeper 使用Acls访问控制

Zookeeper使用Acls来进行节点的访问控制,其Acl实现和Unix文件权限访问非常相似,一个Zookeeper节点,它使用许可位来允许/禁止针对节点的各种操作以及这些位所应用的范围。不同于标准的Unix权限控制,一个Zookeeper节点node不受user(文件所有者)、group和world(其他)三个标准范围的限制。ZooKeeper没有znode的所有者的概念。相反,ACL指定与这些id关联的id和权限集。

还要注意,ACL只适用于特定的znode。尤其是它不适用于儿童。例如,如果/app只能由ip:172.16.16.1读取,并且/app/status是世界可读的,则任何人都可以读取/app/status;ACL不是递归的。

ZooKeeper支持可插入的身份验证方案。id使用表单scheme:expression指定,其中scheme是id对应的身份验证方案。该方案定义了一组有效表达式。例如,ip:172.16.16.1是使用ip方案的地址为172.16.16.1的主机的id,而digest:bob:password是使用摘要方案的用户的id,其名称为bob。

当一个客户端连接到ZooKeeper并进行身份验证时,ZooKeeper会将对应于该客户端的所有id与客户端连接相关联。当客户端试图访问节点时,将对照z node的ACl检查这些id。ACL由成对的(方案:表达式、置换)组成。表达式的格式特定于方案。例如,该对(ip:19.22.0.0/16,READ)向以19.22开头的任何客户端授予READ权限。

ACL 权限

Zookeeper支持一下权限

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

推荐阅读更多精彩内容