Tomcat 6.0集群/会话复制配置

官网配置:http://tomcat.apache.org/tomcat-6.0-doc/cluster-howto.html
本文为译文,属个人英语学习文,如误导了你,先说声抱歉。

  1. 前言
    添加Cluster到<Engine>或者<Host>元素中
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>

增加上面配置后会激活all-to-all 的会话复制(由DeltaManager实现会话复制)。
all-to-all 意思为所有会话都会复制到集群中的其他节点上。
此模式适用于小的集群模式,不适合大的集群模式(有许多的tomcat节点)。
DeltaManager模式会将会话复制到所有节点上,即使这节点Tomcat并没有部署应用。为了避免这一问题,你可以使用BackupManager,BackupManager只会将会话复制到有部署应用的节点上。但是BackupManager有一个缺点,它的实用性没有DeltaManager好。
重要默认信息
1、组播地址是228.0.0.4
2、组播端口是45564(端口和地址组合确定集群成员)
3、IP地址获取方式java.net.InetAddress.getLocalHost()
.getHostAddress()(确保IP非127.0.0.1,这是一种常见的错误)
4、TCP端口监听复制消息可用的端口范围:4000 - 4100
5、两个侦听器需要配置ClusterSessionListener
和 JvmRouteSessionIDBinderListener
6、两个拦截器需要配置TcpFailureDetector
和MessageDispatch15Interceptor
以下是集群的默认配置

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">
  <Manager className="org.apache.catalina.ha.session.DeltaManager"
                   expireSessionsOnShutdown="false"
                   notifyListenersOnReplication="true"/>
  <Channel className="org.apache.catalina.tribes.group.GroupChannel">
            <Membership className="org.apache.catalina.tribes.membership.McastService"
                        address="228.0.0.4"
                        port="45564"
                        frequency="500"
                        dropTime="3000"/>
            <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                      address="auto"
                      port="4000"
                      autoBind="100"
                      selectorTimeout="5000"
                      maxThreads="6"/>
            <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
              <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
            </Sender>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
          </Channel>
          <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
                 filter=""/>
          <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
          <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                    tempDir="/tmp/war-temp/"
                    deployDir="/tmp/war-deploy/"
                    watchDir="/tmp/war-listen/"
                    watchEnabled="false"/>
          <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
          <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
        </Cluster>    
  1. 集群基础知识
    在Tomcat6.0上运用会话复制,需遵循以下规则:
    •你所有的session属性必须实现java.io.serializable
    •去掉server.xml中Cluster部分注释
    •如你有自定义Cluster valve,请确保在server.xml中 replicationvalve定义为在Cluster元素后
    •如你Tomcat实例运行在同一台机器,确保tcplistenport属性是的唯一性,通常情况下,Tomcat是足够自动检测的端口范围是4000-4100
    •确保你的web.xml有<distributable/> 元素
    •如果您使用的mod_jk,确保jvmroute属性在<Engine name="Catalina" jvmRoute="node01" >中,并且,jvmroute属性值需和workers.properties中work name 匹配
    •确保所有节点时钟是同步的
    •确保负载均衡配置成粘性会话模式。
    负载均衡的实现方式有很多种,可参考 负载均衡章节。
    注意:你的会话用cookie进行跟踪,因此你需要确保URL的一致性,否则一个新的session就会创建。
    注意:集群当前支持的版本是JDK1.5以以上
    集群模块采用Tomcat的JULI日志框架,因此你可以在logging.properties中配置日志规则。跟踪messages,可开启配置中的:org.apache.catalina.tribes.MESSAGES
  2. 概述
    在Tomcat中启用会话复制,有以下三种不同的实现方式:
    1:使用会话持久性,并保存会话至共享文件系统(使用PersistenceManager + FileStore)
    2:使用会话持久性,并保存会话至共享数据库(使用PersistenceManager + JDBCStore)
    3:使用内存复制+Tomcat6附带的SimpleTcpCluster(lib/catalina-tribes.jar + lib/catalina-ha.jar)
    在这个发布的会话复制版本中,Tomcat可以使用DeltaManager来实现会话复制all-to-all或用备份复制来实现会话共享(仅在一个节点上使用BackupManager)。all-to-all会话复制模式适用于小的集群模式。对于大集群模式可用primary-secondary来实现-会话会基于BackupManager的方式-存储在一台备份服务器上。
    当前,你使用 domain workerattribute(mod_jk1.2.8以上)来构建集群划分。这种构建方式可以解决潜在的系统构建的伸缩问题。为了确保网络通讯,你可以将集群分成若干小组。他们可以通过不同的组播地址来区分。
    这种简单的结构如下所示:
        DNS Round Robin
               |
         Load Balancer
          /           \
      Cluster1      Cluster2
      /     \        /     \
  Tomcat1 Tomcat2  Tomcat3 Tomcat4

需注意session的复制只是集群开始,更重要的是:一个流程的集群: farming,如你在一个服务器上部署了应用,然后集群会把该应用分别部署在集群中的各个节点。可通过研究FarmWarDeployer来获取相应信息 (集群例子在server.xml)。

  1. 集群信息
    节点成员通过组播心跳建立,如你想细分你的集群,可以通过修改<Membership>元素的组播地址或端口来实现。
    心跳包含Tomcat节点的IP地址和tomcat监听session复制的TCP端口。所有的数据通信通过TCP协议实现。
    ReplicationValve 用于找出已完成的请求和启动复制。当session发生变化时数据才会被复制(在session中调用setAttribute或 removeAttribute 会改变session数据)
    一个重要性能考虑因素就是同步复制还是异步复制的,同步复制模式是所有请求需等所有会话都已复制到其他节点时才会返回。同步和异步都采用 channelSendOptions标记(INT数据类型),SimpleTcpCluster/DeltaManagerd的默认值为8时它是异步的。你可以通过阅读send flag(overview)send flag(javadoc)了解更多信息。
    在异步复制模式中,请求会先于会话复制完成前返回,异步复制的请求时间相对应同步复制会更短,同步处理保证请求返回前,session已经同步完成。
  2. 节点crash后的故障节点转移-绑定session
    如你使用的是mod_jk和非sticky session,或者是因某些原因sticky session不工作,更或者是简单tomcat崩溃,session的id将要修改它以前的worker的ID,也就是之前tomcat的ID(定义在 Engine元素的jvmRoute属性)。为解决这个问题,我们需要使用 JvmRouteBinderValve。

Jvmroutebindervalve重写会话ID,以确保崩溃后下一个请求进来时能被粘住(不会随机返回节点-之前的节点已不可用)。该配置会重写cookies中的JSESSIONID值和会话ID操持一直,如没有这值,在崩溃后很难使用mod_jk就能保证session的sticky。

默认情况下,没有配置valve,那么就会被Jvmroutebindervalve添加。
集群消息侦听器(JvmRouteSessionIDBinderListener)实际为默认配置,在故障发生时 用来重写集群中跟其他节点session id。记住,当你添加的valve或在server.xml中配置集群监听器,默认的Jvmroutebindervalve就会变得无效,因此请确定你在所有节点中添加了正确的valve和监听器。
提示:
sessionIdAttribute属性是可在request中更改名称-包含旧的session id. 默认属性名称为
org.apache.catalina.cluster.session.JvmRouteOrignalSessionID。
技巧:
在所有备份节点中删除一个节点时,你可以启用mod_jk调动via JMX!
在所有JvmRouteBinderValve备份中设置为true,禁止mod_jk工作,删除节点,并重启。
再次启用mod_jk和禁止JvmRouteBinderValve。
这个用例意味着只有请求的会话会被迁移。

  1. 配置示例
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
                 channelSendOptions="6">
          <Manager className="org.apache.catalina.ha.session.BackupManager"
                   expireSessionsOnShutdown="false"
                   notifyListenersOnReplication="true"
                   mapSendOptions="6"/>
          <!--
          <Manager className="org.apache.catalina.ha.session.DeltaManager"
                   expireSessionsOnShutdown="false"
                   notifyListenersOnReplication="true"/>
          -->        
          <Channel className="org.apache.catalina.tribes.group.GroupChannel">
            <Membership className="org.apache.catalina.tribes.membership.McastService"
                        address="228.0.0.4"
                        port="45564"
                        frequency="500"
                        dropTime="3000"/>
            <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                      address="auto"
                      port="5000"
                      selectorTimeout="100"
                      maxThreads="6"/>
            <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
              <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
            </Sender>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/>
          </Channel>
          <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
                 filter=".*\.gif;.*\.js;.*\.jpg;.*\.png;.*\.htm;.*\.html;.*\.css;.*\.txt;"/>
          <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                    tempDir="/tmp/war-temp/"
                    deployDir="/tmp/war-deploy/"
                    watchDir="/tmp/war-listen/"
                    watchEnabled="false"/>
          <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
        </Cluster>

配置详解

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
                 channelSendOptions="6">

Cluster是主要的元素,集群的配置的所有细节都在这个元素中。channelSendOpentions是一个标记,连接所有SimpleTcpCluster发送的信息,或者任何调用SimpleTcpCluster.send方法的对象。标记描述定义在javadoc site
DelatManager使用SimpleTcpCluster.send方法发送消息,而backup manager则是通过通道直接发送消息。
更多参考信息,请阅读引用文档

<Manager className="org.apache.catalina.ha.session.BackupManager" 
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"
mapSendOptions="6"/> 
<!-- 
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"/> 
-->

这个是manager 配置的模板,当context元素中无manager配置时默认使用。在tomcat5.x中,每一个app应用必须用同一个manager,但在该版本的tomcat中,你可以为每一个webapp定义manager。所以,你可以在你的集群中混合配置manager。
很显然,每一个节点中的manager需要同集群中其他节点的manager保持一致。如果没有为webapp配置manager,并且webapp被标记成<distributable/>,tomcat将会采用这个manager的配置并且生成一个manager的实例。
更多参考信息,请阅读引用文档

<Channel className="org.apache.catalina.tribes.group.GroupChannel">

Channel是一个部落,一个在tomcat内部的组通信框架。
该元素将所有的通信相关以及节点之间关系封装起来。
更多参考信息,请阅读引用文档

 <Membership className="org.apache.catalina.tribes.membership.McastService"
                        address="228.0.0.4"
                        port="45564"
                        frequency="500"
                        dropTime="3000"/>

membership是通过组播的。
请注意,部落也支持静态成员,如你想扩展节点可用staticmembershipinterceptor实现。
地址属性使用组播地址,端口使用组播端口,两者共同组成了集群的分割。
如你想要一个质量保证集群和一个生产集群,最简单的配置方式是将QA集群做成一个独立的组播地址和端口然后和生产集群组合。
Membership组成的广播TCP地址和端口能到其他的节点,因此节点间可以通过TCP进行通信。
请注意,地址是一个广播的receiver.address属性。
更多参考信息,请阅读引用文档

<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                      address="auto"
                      port="5000"
                      selectorTimeout="100"
                      maxThreads="6"/>

在Tribes(有组通信能力的消息传递框架)中的发送和接收数据已被分解成两大组件。
接收,顾名思义:负责接收消息。
由于Tribes的堆栈线程较少, (现在已采用其他框架所流行的方式进行改进), 有一个线程池,该组件具有一个maxthreads和minthreads设置。 地址属性的host地址将通过membership组件广播给其他节点。

<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
              <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
            </Sender>

发送组件,顾名思义:负责将消息传递给其他节点。
发送组件有一个shell组件,复制传输等具体要做的事情在子组件完成。Tribes有一个发送池,支持并行发送消息,如果使用NIO发送者,你也可以同时发送消息。
同时意味着一个消息到多个发送者的并行和多个消息到多个发送者的并行。

<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/>
</Channel>

Tribes通过堆栈来发送消息。
堆栈中的每个元素被称为一个拦截器,工作模式同Tomcat servlet容器。使用拦截器,可将逻辑分解成更易于管理的代码块。
上面的拦截器配置是:
TcpFailureDetector(TCP故障检测器)——通过TCP验证节点crashed。如果组播数据包被丢弃,拦截器为防止误报,会将节点标记为崩溃,即使它仍然是活着的。
MessageDispatch15Interceptor——分配线程(线程池)用于同步发送消息。
ThroughputInterceptor——打印出简单的信息通信记录。
请注意,拦截器的顺序是很重要的。他们在服务器中定义的方式。xml是他们在通道堆栈的方式。认为这是一个链表,头被第一个拦截器和尾最后一次。
他们被定义在server.xmlzhong,表示为通道堆栈。把它作为一个链表,头是第一个最拦截和尾部的最后一个。

<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
                 filter=".*\.gif;.*\.js;.*\.jpg;.*\.png;.*\.htm;.*\.html;.*\.css;.*\.txt;"/>

集群使用valve来跟踪到web应用的请求,我们之前提到了ReplicationValve和JvmRouteBinderValve。
<Cluster>元素不是Tomcat元素,相反是集群将valve添加到父容器中。当<Cluster>元素在<Engine>元素中被配置,valve将被添加到引擎中等等。

<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                    tempDir="/tmp/war-temp/"
                    deployDir="/tmp/war-deploy/"
                    watchDir="/tmp/war-listen/"
                    watchEnabled="false"/>

默认的Tomcat集群支持farmed deployment,如:集群能将部署和取消部署其他节点的应用。
该组件当前不稳定但很快会解决这一问题。
Tomcat 5.0 和 5.5 版本相比,在部署算法上有一点变化。
组件的逻辑改变到部署目录必须与应用目录相匹配。

 <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>

因为 SimpleTcpCluster 本身既是 Channel 对象的发送者,又是接受者,所以组件可以将它们自身注册成SimpleTcpCluster的侦听器。
上面这个侦听器 ClusterSessionListener 将侦听 DeltaManager 复制的消息,并将会话变更应用到 manager 上,反过来应用到会话上。
集群架构

Server
           |
         Service
           |
         Engine
           |  \ 
           |  --- Cluster --*
           |
         Host
           |
         ------
        /      \
        Cluster    Context(1-N)                 
        |             \
        |             -- Manager
        |                   \
        |                   -- DeltaManager
        |                   -- BackupManager
        |
        |---------------------------
        |                       \
        Channel                    \
        ----------------------------- \
        |                          \
        Interceptor_1 ..               \
        |                            \
        Interceptor_N                    \
        -----------------------------      \
        |          |         |             \
        Receiver    Sender   Membership       \
                                         -- Valve
                                         |      \
                                         |       -- ReplicationValve
                                         |       -- JvmRouteBinderValve 
                                         |
                                         -- LifecycleListener 
                                         |
                                         -- ClusterListener 
                                         |      \
                                         |       -- ClusterSessionListener
                                         |       -- JvmRouteSessionIDBinderListener
                                         |
                                         -- Deployer 
                                                \
                                                 -- FarmWarDeployer

工作原理
为了易于理解集群的工作原理,我们通过模拟场景来加深理解。
在这场景中我们需用到两个tomcat实例:Tomcat A和Tomcat B,我们会按顺序模拟以下事件:
1、Tomcat A 启动。
2、Tomcat B 启动(在Tomcat A启动成功之后)
3、Tomcat A 接收一个请求,建立session S1.
4、Tomcat A crash
5、Tomcat B 接收到一个session为S1 的请求
6、Tomcat A 启动
7、Tomcat A 接收一个请求,调用session为S1的invalidate
8、Tomcat B 接收一个请求,新Session S2
9、Tomcat A session S2 由于不活跃而超时
介绍完了事件序列,下面详细剖析一下在会话复制代码中到底发生了什么。

  1. Tomcat A 启动
    Tomcat 使用标准启动顺序来启动。
    Host 对象创建好之后,会关联一个 Cluster 对象。
    在解析上下文时,如果 web.xml 中包含 distributable 元素,
    Tomcat 就会让 Cluster 类(在该例中是 SimpleTcpCluster)创建复制的上下文的管理器。
    启用了集群并在 web.xml 中设置了 distributable 元素后,Tomcat 会为该上下文创建一个 DeltaManager(而不是 StandardManager)。
    Cluster 类会启动一个成员服务(组播)和一个复制服务(TCP 单播)。下文将会介绍更多的架构细节。
  1. Tomcat B 启动
    Tomcat B 启动时,采取的顺序与 Tomcat A 基本一样。集群启动,建立成员(Tomcat A 与 Tomcat B)。
    Tomcat B 会请求集群中已有服务器(本例中是 Tomcat A)的会话状态。
    如果 Tomcat A 响应该请求,那么在 Tomcat B 开始侦听 HTTP 请求之前,Tomcat A 会将会话状态传到 Tomcat B那里;
    如果 Tomcat A 没有响应该请求,Tomcat B 会等待 60 秒后,记录日志信息。
    该会话状态会发送到每一个在 web.xml 中设置了 distributable 元素的应用。
    注意:为了有效地使用会话复制,所有的 Tomcat 实例都必须拥有相同的配置。
  2. Tomcat A 接收一个请求,建立session S1
    Tomcat A 接收到请求的处理方式,与没有会话复制时的处理方式完全相同。
    请求完成时会触发相应行为,ReplicationValve 会在响应返回用户之前拦截请求。
    如发现会话已经更改,则使用 TCP 将会话复制到 Tomcat B 上。
    一旦序列化的数据被转交给操作系统的 TCP 逻辑,请求就会重新通过 valve 管道返回给用户。
    对于每一个请求,都将复制所有的会话,这样做就有利于复制那些在会话中修改属性的代码,
    使其即使不必调用 setAttribute 或 removeAttribute,也能被复制。
    另外,使用 useDirtyFlag 配置参数也可以优化会话的复制次数。
  3. Tomcat A crash
    当 Tomcat A 崩溃时,Tomcat B 会接到通知,得知 Tomcat A 已被移出集群,随即 Tomcat B 就在其成员列表中也将 Tomcat A 移除,Tomcat B 不再收到关于 Tomcat A 的任何通知。
    负载均衡器会把 Tomcat A 中请求全部重定向到Tomcat B ,所有的会话都将保持现有的状态。
  4. Tomcat B 接收到一个session为S1 的请求
    无意外的,Tomcat B 将会像按处理任何请求的方式来处理该请求。
  5. Tomcat A 启动
    在 Tomcat A 开始接收新的请求之前,将会根据上面(1)(2)两条所所说明的启动序列来启动。
    Tomcat A 会加入集群,联系 Tomcat B 并获取所有的会话状态。
    一旦接收到会话状态,就会完成加载,并打开 HTTP/mod_jk 端口。
    所以,除非 Tomcat A 从 Tomcat B 那里接收到了会话变更,否则没有发给 Tomcat A 的请求。
  6. Tomcat A 接收一个请求,调用session为S1的invalidate
    拦截器会拦截invalidate 的调用, 并且 session 会被加入失效会话队列。
    在请求完成时,不会发送会话改变消息,而是发送一个 “到期” 消息给 Tomcat B,Tomcat B 也会让此会话失效。
  7. Tomcat B 接收一个请求,新Session S2
    同步骤3.
  8. Tomcat A session S2 由于不活跃而超时
    invalidate 调用会被拦截,当一个会话被用户标记失效时,该会话就会加入到无效会话队列。
    此时,失效的会话不会被复制,直到另一个请求通过系统并检查无效会话队列。

Membership 集群成员是通过非常简单的组播 ping 命令来实现的。每个 Tomcat 实例都会定期发送一个组播 ping,ping 消息中包含 Tomcat 实例自身的 IP 和配置的 TCP 监听端口。如果实例在一个给定的时间内没有收到这样的 ping 信息,就会认为那个成员已经崩溃了。非常简洁高效!当然,您需要在系统上启用组播。
TCP 复制 一旦收到一个多播 ping 包,在下一个复制请求时成员被添加到集群,发送实例将使用的主机和端口信息,以及建立TCP socket。使用该TCP socket发送序列化的数据。选择TCP socket,是因为它建立有流量控制和保证发送的机制。因此我知道我想发送的数据它能发送到指定位置。
分布式锁定与页面使用框架 Tomcat 在跨集群同步不保持会话实例。这种逻辑的实现将开销大,并导致各种各样的问题。
如果客户端用同一个会话同时发送多个请求,那么最后的请求将会覆盖集群中的其他会话。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容