分布式全链路监控 -- opentracing小试

前言

最近公司在考虑做全链路监控的事儿,主要是一个请求进来从服务网关到各个业务战队会流转到很多个战队的业务模块,如果一个业务中出现问题就会影响到整个调用链路的结果(响应时间、响应结果、异常处理等等)。因此我们需要考虑一个全链路监控机制来完成请求的全链路监控。最开始考虑直接基于pinpoint的注入插件来做,但是后来发现由于采样率等关系,无法应用到生产上,所以考虑自己做一套自己的标准。同时我们也想引入opentracing的标准,因此有了这一篇文章所覆盖的内容。

opentracing简单介绍

opentracing介绍

opentracing的具体信息大家可以参照前言里的相关链接到opentracing的官网和github上的去看看,这里只是做一些简单地介绍。opentracing里主要包含以下几个组件:

Span

表示分布式调用链条中的一个调用单元,比方说某个dubbo的调用provider,或者是个http调用的服务提供方,他的边界包含一个请求进到服务内部再由某种途径(http/dubbo等)从当前服务出去。一个span一般会记录这个调用单元内部的一些信息,例如:

  1. 日志信息
  2. 标签信息
  3. 开始/结束时间

SpanContext

表示一个span对应的上下文,span和spanContext基本上是一一对应的关系,上下文存储的是一些需要跨越边界的一些信息,例如:

  1. spanId 当前这个span的id
  2. traceId 这个span所属的traceId(也就是这次调用链的唯一id)
  3. baggage 其他的能过跨越多个调用单元的信息
    这个SpanContext可以通过某些媒介和方式传递给调用链的下游来做一些处理(例如子Span的id生成、信息的继承打印日志等等)

Tracer

tracer表示的是一个通用的接口,它相当于是opentracing标准的枢纽,它有以下的职责:

  1. 建立和开启一个span
  2. 从某种媒介中提取和注入一个spanContext

Carrier

表示的是一个承载spanContext的媒介,比方说在http调用场景中会有HttpCarrier,在dubbo调用场景中也会有对应的DubboCarrier。

Formatter

这个接口负责了具体场景中序列化反序列化上下文的具体逻辑,例如在HttpCarrier使用中通常就会有一个对应的HttpFormatter。Tracer的注入和提取就是委托给了Formatter

ScopeManager

这个类是0.30版本之后新加入的组件,这个组件的作用是能够通过它获取当前线程中启用的Span信息,并且可以启用一些处于未启用状态的span。在一些场景中,我们在一个线程中可能同时建立多个span,但是同一时间统一线程只会有一个span在启用,其他的span可能处在下列的状态中:

  1. 等待子span完成
  2. 等待某种阻塞方法
  3. 创建并未开始

除了上述组件之外,我们在实现一个分布式全链路监控框架的时候,还需要有一个reporter组件,通过它来打印或者上报一些关键链路信息(例如span创建和结束),只有把这些信息进行处理之后我们才能对全链路信息进行可视化和真正的监控。

简单实现思路

这篇文章先介绍一些关键组件(涵盖Span、SpanContext、Tracer和ScopeManager)关键逻辑的实现,也借鉴了一点sofa-tracer的实现思路(比方说spanId生成规则、traceId生成规则等,关于这些信息大家可以移步到sofa-tracer来查看)。我们的项目叫星图(StarAtlas),因此我们的组件都是以这个为前缀的,这里省去我们的包名作者日期等注释信息。
先来看Span:

import io.opentracing.Span;
import io.opentracing.SpanContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * StarAtlasSpan
 * <p>
 * the implementation of span
 *
 */
public class StarAtlasSpan implements Span {

    private StarAtlasTracer starAtlasTracer;

    private long startTime;

    private List<StarAtlasSpanReferenceRelationship> spanReferences;

    private String operationName;

    private StarAtlasSpanContext spanContext;

    private Logger logger = LoggerFactory.getLogger(this.getClass());


    public StarAtlasSpan(StarAtlasTracer starAtlasTracer, long startTime,
                         List<StarAtlasSpanReferenceRelationship> spanReferences,
                         String operationName, StarAtlasSpanContext spanContext,
                         Map<String, ?> tags) {
        AssertUtils.notNull(starAtlasTracer);
        AssertUtils.notNull(spanContext);
        this.starAtlasTracer = starAtlasTracer;
        this.startTime = startTime;
        this.spanReferences = spanReferences != null ? new ArrayList<StarAtlasSpanReferenceRelationship>(
                spanReferences) : null;
        this.operationName = operationName;
        this.spanContext = spanContext;
        //tags
        this.setTags(tags);

        // report extention to be implement
        //SpanExtensionFactory.logStartedSpan(this);
    }

    @Override
    public SpanContext context() {
        return this.spanContext;
    }

    @Override
    public Span setTag(String s, String s1) {
        return null;
    }

    @Override
    public Span setTag(String s, boolean b) {
        return null;
    }

    @Override
    public Span setTag(String s, Number number) {
        return null;
    }

    @Override
    public Span log(Map<String, ?> map) {
        return null;
    }

    @Override
    public Span log(long l, Map<String, ?> map) {
        return null;
    }

    @Override
    public Span log(String s) {
        return null;
    }

    @Override
    public Span log(long l, String s) {
        return null;
    }

    @Override
    public Span setBaggageItem(String s, String s1) {
        return null;
    }

    @Override
    public String getBaggageItem(String s) {
        return null;
    }

    @Override
    public Span setOperationName(String s) {
        return null;
    }

    @Override
    public void finish() {

    }

    @Override
    public void finish(long l) {

    }

    private void setTags(Map<String, ?> tags) {
        if (tags == null || tags.size() <= 0) {
            return;
        }
        for (Map.Entry<String, ?> entry : tags.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.isBlank(key)) {
                continue;
            }
            Object value = entry.getValue();
            if (value == null) {
                continue;
            }
            if (value instanceof String) {
                //初始化时候,tags也可以作为 client 和 server 的判断依据
                this.setTag(key, (String) value);
            } else if (value instanceof Boolean) {
                this.setTag(key, (Boolean) value);
            } else if (value instanceof Number) {
                this.setTag(key, (Number) value);
            } else {
                logger.error("Span tags unsupported type [" + value.getClass() + "]");
            }
        }
    }
}

这里比较简单,就是创建一个Span,并且注入一些信息,这里注释了一些打印日志的代码。在构建函数里面有个StarAtlasSpanReferenceRelationship的list,这个类实际上是标识了这个Span和其他Span之间的关系,用于创建Span的时候维护父子从属关系。
我们再来看看SpanContext:

import io.opentracing.SpanContext;

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * StarAtlasSpanContext
 *
 * the span context implementation to store span information
 *
 */
public class StarAtlasSpanContext implements SpanContext {

    //spanId 分隔符
    public static final String        RPC_ID_SEPARATOR       = ".";

    //======================== 以下为序列化数据的 key ========================

    private static final String       TRACE_ID_KET           = "tcid";

    private static final String       SPAN_ID_KET            = "spid";

    private static final String       PARENT_SPAN_ID_KET     = "pspid";

    private static final String       SAMPLE_KET             = "sample";

    private AtomicInteger childContextIndex = new AtomicInteger(0);

    private String spanId;

    private String traceId;

    private String parentId;

    /***
     * 默认不会采样
     */
    private boolean isSampled = false;

    public StarAtlasSpanContext(String traceId, String spanId, String parentId) {
        //默认不会采样
        this(traceId, spanId, parentId, false);
    }

    public StarAtlasSpanContext(String traceId, String spanId, String parentId, boolean isSampled) {
        this.traceId = traceId;
        this.spanId = spanId;
        this.parentId = StringUtils.isBlank(parentId) ? this.genParentSpanId(spanId) : parentId;
        this.isSampled = isSampled;
    }

    @Override
    public Iterable<Map.Entry<String, String>> baggageItems() {
        return null;
    }

    /**
     * 获取下一个子上下文的 ID
     *
     * @return 下一个 spanId
     */
    public String nextChildContextId() {
        return this.spanId + RPC_ID_SEPARATOR + childContextIndex.incrementAndGet();
    }

    public String getSpanId() {
        return spanId;
    }

    public void setSpanId(String spanId) {
        this.spanId = spanId;
    }

    public String getTraceId() {
        return traceId;
    }

    public void setTraceId(String traceId) {
        this.traceId = traceId;
    }

    public String getParentId() {
        return parentId;
    }

    public void setParentId(String parentId) {
        this.parentId = parentId;
    }

    public boolean isSampled() {
        return isSampled;
    }

    public void setSampled(boolean sampled) {
        isSampled = sampled;
    }

    private String genParentSpanId(String spanId) {
        return (StringUtils.isBlank(spanId) || spanId.lastIndexOf(RPC_ID_SEPARATOR) < 0) ? StringUtils.EMPTY_STRING
                : spanId.substring(0, spanId.lastIndexOf(RPC_ID_SEPARATOR));
    }
}

这个类跟Span类似,也是存储了一些spanId、traceId和baggage等信息,另外有几个比较特别的函数,包括获取当前上下文的父级spanId,生成下一级的子span的id。
接下来再看看Scope和ScopeManager:

import io.opentracing.Scope;
import io.opentracing.ScopeManager;
import io.opentracing.Span;

/**
 * StarAtlasScopeManager
 * <p>
 * the scope manager to store and manage the scope information within a thread
 *
 */
public class StarAtlasScopeManager implements ScopeManager {

    /**
     * the thread local store for the active scope
     */
    final ThreadLocal<StarAtlasScope> scopeThreadLocal = new ThreadLocal<>();

    /**
     * singleton method
     *
     * @return
     */
    public static StarAtlasScopeManager getInstance() {
        return StarAtlasScopeManagerSingletonHolder.INSTANCE;
    }

    private StarAtlasScopeManager() {

    }

    /**
     * the method to active a span
     *
     * @param span
     * @param finishOnClose
     * @return
     */
    @Override
    public Scope activate(Span span, boolean finishOnClose) {
        if (!checkCanActivate(span)) {
            throw new IllegalStateException("a span cannot be activated more than once");
        }
        return new StarAtlasScope(this, span, finishOnClose);
    }

    /**
     * the method to get the current active span
     *
     * @return
     */
    @Override
    public Scope active() {
        return this.scopeThreadLocal.get();
    }

    /**
     * check if the span can be activate
     * if the span exists in the recover chain of the current active scope
     * then we know that the span has been activate before.
     *
     * @param span
     * @return
     */
    private boolean checkCanActivate(Span span) {
        StarAtlasScope scope = (StarAtlasScope) this.active();
        while (scope != null) {
            if (scope.span() == span) {
                return false;
            }
            scope = scope.scopeToRecover;
        }
        return true;
    }

    private static class StarAtlasScopeManagerSingletonHolder {
        private static final StarAtlasScopeManager INSTANCE = new StarAtlasScopeManager();
    }
}

这里ScopeManage主要通过一个ThreadLocal来存储当前Span的信息(用一个Scope来包装)。然后实现了三个方法:

  • activate 在当前线程中激活一个span,并返回一个scope封装当前激活的span
  • active 返回当前线程激活的scope
  • checkCanActivate 这是自行实现的一个方法,我们激活一个span封装scope的时候会把激活前线程中激活的scope以scopeToRecover变量存储在新激活的scope中(具体可参考接下来scope的代码)。这样我们就可以根据当前激活的scope以scopeToRecover来不断地追溯到最初,因此当我们激活一个span的时候,我们就可以通过这个span在不在追溯的链路上来判断是否这个span被重复激活了。

Scope代码如下:

import io.opentracing.Scope;
import io.opentracing.Span;

/**
 * StarAtlasScope
 * <p>
 * StarAtlasScope is a wrap class for span
 * It represents a active span in current thread.
 * And it support close function to deactivate a span
 *
 */
public class StarAtlasScope implements Scope {

    /**
     * finish the span or not when we close the scope
     */
    private final boolean finishOnClose;

    /**
     * the wrapped span
     */
    private final Span span;

    /**
     * scope manager
     */
    private final StarAtlasScopeManager scopeManager;

    /**
     * the scope to recover on close
     */
    final StarAtlasScope scopeToRecover;

    StarAtlasScope(StarAtlasScopeManager scopeManager, Span span, boolean finishOnClose) {
        this.finishOnClose = finishOnClose;
        this.span = span;
        this.scopeManager = scopeManager;
        // store the previous scope to recover
        this.scopeToRecover = this.scopeManager.scopeThreadLocal.get();
        // push the current scope into thread local
        // may extract into a package level method in StarAtlasScopeManager
        this.scopeManager.scopeThreadLocal.set(this);
    }

    /**
     * call close means the active period for the current thread and scope comes to an end
     */
    @Override
    public void close() {
        // if the current active scope does not equal to this
        // the close operation can not continue
        if (scopeManager.active() != this) {
            throw new IllegalStateException("can not call scope close in an unexpected way");
        }
        if (finishOnClose) {
            span.finish();
        }
        // recover the scope
        this.scopeManager.scopeThreadLocal.set(this.scopeToRecover);
    }

    @Override
    public Span span() {
        return span;
    }

}

Scope的实现基本就是封装了一个span,并且在创建的时候把之前激活的scope存下来(印证了之前的说法),支持两个方法:

  • close 关闭当前的scope,也连带的把封装的span关闭,并且恢复线程中激活的scope到之前。
  • span 返回封装的span

最后我们再来看看Tracer:

import io.opentracing.*;
import io.opentracing.propagation.Format;

import java.util.*;

/**
 */
public class StarAtlasTracer implements Tracer {

    /**
     * traceID的KEY
     */
    public static final String KEY_TRACEID = "SA-TRACEID";

    /**
     * 正常 TRACE 开始的 spanId
     */
    public static final String  ROOT_SPAN_ID = "0";

    @Override
    public ScopeManager scopeManager() {
        return StarAtlasScopeManager.getInstance();
    }

    @Override
    public Span activeSpan() {
        return this.scopeManager().active().span();
    }

    @Override
    public SpanBuilder buildSpan(String operationName) {
        return new StarAtlasSpanBuilder(operationName);
    }

    @Override
    public <C> void inject(SpanContext spanContext, Format<C> format, C c) {

    }

    @Override
    public <C> SpanContext extract(Format<C> format, C c) {
        return null;
    }

    /**
     * the implementation of span builder
     */
    private class StarAtlasSpanBuilder implements SpanBuilder {

        private String operationName = StringUtils.EMPTY_STRING;

        private long startTime = -1;

        private List<StarAtlasSpanReferenceRelationship> references = Collections.emptyList();

        private final Map<String, Object> tags          = new HashMap<String, Object>();

        private boolean ignoreActiveSpan = false;

        public StarAtlasSpanBuilder(String operationName){
            this.operationName = operationName;
        }

        @Override
        public SpanBuilder asChildOf(SpanContext parentContext) {
            return addReference(References.CHILD_OF, parentContext);
        }

        @Override
        public SpanBuilder asChildOf(Span parentSpan) {
            if(parentSpan == null){
                return this;
            }
            return asChildOf(parentSpan.context());
        }

        @Override
        public SpanBuilder addReference(String referenceType, SpanContext referencedContext) {
            if (referencedContext == null) {
                return this;
            }
            if (!(referencedContext instanceof StarAtlasSpanContext)) {
                return this;
            }
            if (!References.CHILD_OF.equals(referenceType)
                    && !References.FOLLOWS_FROM.equals(referenceType)) {
                return this;
            }
            if (references.isEmpty()) {
                // Optimization for 99% situations, when there is only one parent
                references = Collections.singletonList(new StarAtlasSpanReferenceRelationship(
                        (StarAtlasSpanContext) referencedContext, referenceType));
            } else {
                if (references.size() == 1) {
                    //要保证有顺序
                    references = new ArrayList<StarAtlasSpanReferenceRelationship>(references);
                }
                references.add(new StarAtlasSpanReferenceRelationship(
                        (StarAtlasSpanContext) referencedContext, referenceType));
            }
            return this;
        }

        @Override
        public SpanBuilder ignoreActiveSpan() {
            throw new UnsupportedOperationException("unsupport ignore active span right now");
        }

        @Override
        public SpanBuilder withTag(String key, String value) {
            this.tags.put(key, value);
            return this;
        }

        @Override
        public SpanBuilder withTag(String key, boolean value) {
            this.tags.put(key, value);
            return this;
        }

        @Override
        public SpanBuilder withTag(String key, Number value) {
            this.tags.put(key, value);
            return this;
        }

        @Override
        public SpanBuilder withStartTimestamp(long startTime) {
            this.startTime = startTime;
            return this;
        }

        @Override
        public Scope startActive(boolean finishOnClose) {
            Span span = this.start();
            return StarAtlasTracer.this.scopeManager().activate(span, finishOnClose);
        }

        @Override
        public Span startManual() {
            return null;
        }

        @Override
        public Span start() {
            StarAtlasSpanContext spanContext = null;
            if(this.references.size() > 0){
                // there is a parent context
                spanContext = createChildContext();
            }else if (!this.ignoreActiveSpan
                    && StarAtlasTracer.this.scopeManager().active() != null){
                // use the current span as default parent;
                Scope currentScope = StarAtlasTracer.this.scopeManager().active();
                this.asChildOf(currentScope.span());
                spanContext = createChildContext();
            }else {
                // it should be the root
                spanContext = createRootSpanContext();
            }
            long begin = this.startTime > 0 ? this.startTime : System.currentTimeMillis();
            StarAtlasSpan span = new StarAtlasSpan(StarAtlasTracer.this, begin,
                    this.references, this.operationName, spanContext, this.tags);
            return span;
        }

        private StarAtlasSpanContext createRootSpanContext(){
            String traceId = TraceIdGenerator.generate();
            return new StarAtlasSpanContext(traceId, ROOT_SPAN_ID, StringUtils.EMPTY_STRING);
        }

        private StarAtlasSpanContext createChildContext() {
            StarAtlasSpanContext preferredReference = preferredReference();

            StarAtlasSpanContext sofaTracerSpanContext = new StarAtlasSpanContext(
                    preferredReference.getTraceId(), preferredReference.nextChildContextId(),
                    preferredReference.getSpanId(), preferredReference.isSampled());
            return sofaTracerSpanContext;
        }

        /**
         * choose the preferred reference
         * @return
         */
        private StarAtlasSpanContext preferredReference() {
            StarAtlasSpanReferenceRelationship preferredReference = references.get(0);
            for (StarAtlasSpanReferenceRelationship reference : references) {
                // childOf takes precedence as a preferred parent
                String referencedType = reference.getReferenceType();
                if (References.CHILD_OF.equals(referencedType)
                        && !References.CHILD_OF.equals(preferredReference.getReferenceType())) {
                    preferredReference = reference;
                    break;
                }
            }
            return preferredReference.getSpanContext();
        }
    }
}

这里借鉴了一些sofa-tracer里面的实现。主要逻辑就是实现了SpanBuilder来完成创建Span的逻辑,并且提供了激活span的接口。

测试

完成了这些功能之后,我们可以编写下列单元测试代码来进行测试:

import io.opentracing.Scope;
import io.opentracing.Span;
import org.junit.Assert;
import org.junit.Test;

/**
 * StarAtlasTracerTest
 *
 */
public class StarAtlasTracerTest {
    /**
     * 测试仅生成root
     */
    @Test
    public void generateRoot(){
        StarAtlasTracer starAtlasTracer = new StarAtlasTracer();
        Span root = starAtlasTracer.buildSpan("root").start();
        Assert.assertNotNull(root);
        StarAtlasSpanContext context = (StarAtlasSpanContext) root.context();
        Assert.assertEquals(context.getSpanId(), "0");
        Assert.assertEquals(context.getParentId(), "");
        Assert.assertFalse(StringUtils.isBlank(context.getTraceId()));
        Assert.assertNull(starAtlasTracer.scopeManager().active());
    }

    /**
     * 测试生成root并activate
     */
    @Test
    public void generateRootAndActivate(){
        StarAtlasTracer starAtlasTracer = new StarAtlasTracer();
        Scope rootScope = starAtlasTracer.buildSpan("root").startActive(true);
        Assert.assertNotNull(rootScope);
        StarAtlasSpanContext context = (StarAtlasSpanContext) rootScope.span().context();
        Assert.assertEquals(context.getSpanId(), "0");
        Assert.assertEquals(context.getParentId(), "");
        Assert.assertNotNull(starAtlasTracer.scopeManager().active());
        Assert.assertEquals(rootScope, starAtlasTracer.scopeManager().active());
        rootScope.close();
        Assert.assertNull(starAtlasTracer.scopeManager().active());
    }

    /**
     * 测试生成child并activate
     */
    @Test
    public void generateChildAndActivate(){
        StarAtlasTracer starAtlasTracer = new StarAtlasTracer();
        Scope rootScope = starAtlasTracer.buildSpan("root").startActive(true);
        StarAtlasSpanContext rootContext = (StarAtlasSpanContext) rootScope.span().context();
        Assert.assertNotNull(rootScope);
        Span child = starAtlasTracer.buildSpan("child").asChildOf(rootScope.span()).start();
        StarAtlasSpanContext context = (StarAtlasSpanContext)child.context();
        Assert.assertEquals(context.getSpanId(), "0.1");
        Assert.assertEquals(context.getTraceId(), rootContext.getTraceId());
        Assert.assertEquals(rootScope, starAtlasTracer.scopeManager().active());
        Scope childScope = starAtlasTracer.scopeManager().activate(child, true);
        Assert.assertEquals(childScope, starAtlasTracer.scopeManager().active());
        childScope.close();
        Assert.assertEquals(rootScope, starAtlasTracer.scopeManager().active());
        rootScope.close();
    }

    /**
     * 测试重复激活span
     */
    @Test
    public void testDuplicatedActivate(){
        StarAtlasTracer starAtlasTracer = new StarAtlasTracer();
        Span root = starAtlasTracer.buildSpan("root").start();
        Scope rootScope = starAtlasTracer.scopeManager().activate(root, true);
        Span child = starAtlasTracer.buildSpan("child").start();
        Scope childScope = starAtlasTracer.scopeManager().activate(child, true);
        try{
            starAtlasTracer.scopeManager().activate(root, true);
        } catch (Exception e){
            System.out.println(e.getMessage());
            Assert.assertTrue(e instanceof IllegalStateException);
        }
        childScope.close();
        rootScope.close();
    }
}

具体测试场景在注释中都有,有兴趣的同学可以自行泡一下。

后记

本篇文章讲解了一下opentracing中的基本概念,并提供了一个基本的实现和测试。后续有时间和精力的情况下有可能会有后续文章讨论一下如何介入dubbo/http等场景。有问题的同学可以通过评论来讨论。

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

推荐阅读更多精彩内容