Spring Cloud Alibaba之服务容错组件 - Sentinel [基础篇]

[TOC]


常见容错方案

在微服务等分布式架构中,服务容错是老生常谈的问题了,我们都知道在微服务架构中会存在多个微服务,而绝大部分微服务之间都会存在调用关系,若由于某个底层服务不可用从而产生连锁反应,导致一系列的上层服务崩溃、故障,这种现象被称为雪崩效应或级联故障。如下图所示:


image.png

所以在微服务等分布式架构中,能够防御服务雪崩效应的容错方案是必不可少的,常见的容错方案如下:

1、超时:

设置请求超时时间,让请求线程在等待超过一定的时间后就判定为请求失败从而释放请求线程,在某些场景下线程释放得够快的话,就不会因为不断创建线程而导致资源耗尽引起的服务崩溃

2、限流:

例如,上图中的服务A只能承受1k左右的QPS,那么就设置一个最大请求数量阈值,当QPS达到1k时就拒绝在这之后的请求。就像是我只能吃一碗饭,就算给我三碗我也只吃一碗

3、舱壁模式:

舱壁模式实际上就是借鉴于现实生活中的船舱结构而设计,一艘船想要不那么容易沉也需要具备有一定的”容错“能力,而早期的船由于设计上的欠缺,只要一个地方进水了,那么水就会逐渐漫进整个船舱,这种结构的船几乎没有“容错”能力,所以就比较容易沉。于是此时就有人想到将原本一体的船舱分隔成一个个独立的船舱,船舱之间都使用钢板焊死隔开,这些钢板就是所谓的舱壁了。采用这种设计后,就算当其中一个两个船舱进水了,也不会影响到其他船舱,这艘船依旧能够正常行驶。

在软件层面上借鉴这种思想,我们可以让每个服务都运行在自己独立的线程池中,线程池之间是互不干扰的,服务A的线程池资源耗尽也不会影响到服务B。此时线程池就像船舱的舱壁一样将不同的服务资源隔离开来,这样某个服务挂掉也不会影响其他服务的运行

4、断路器模式:

断路器模式的思想实际上和家里的断路器一样,在软件层面大致就是对某个服务的API进行监控,若在一定时间内调用的失败率或失败次数达到指定的阈值就认为该API是不可用的从而触发“跳闸”,即此时断路器就处于打开状态。过了一段时间后断路器会处于一个半开状态,若在半开状态时尝试调用该API成功后就会关闭断路器,否则依旧认为不可用让断路器继续处于打开状态

断路器三态转换如下图:


image.png

断路器模式原文:CircuitBreaker

而Spring Cloud已经提供了相关的服务容错组件,组件里已经整合了这些常用的方案,不需要我们手动去实现。在此之前Spring Cloud提供的唯一服务容错组件是Hystrix,不过现在多了一个选择,那就是Spring Cloud Alibaba的Sentinel组件。关于Hystrix可以参考如下文章,本文主要介绍Sentinel:


Sentinel简介及整合

Sentinel 是什么,官方描述如下:

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的轻量级流量控制、熔断降级框架,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助您保护服务的稳定性。

官方GitHub仓库地址如下:

https://github.com/alibaba/Sentinel

现在我们来为项目整合Sentinel,第一步添加如下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- actuator,用于暴露监控端点 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Tips:该项目使用的Spring Cloud版本为Greenwich.SR1,Spring Cloud Alibaba版本为0.9.0.RELEASE

第二步配置actuator:

# 暴露所有端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

完成以上两步后,启动项目,使用浏览器访问http://localhost:8080/actuator/sentinel,返回如下结果代表整合成功:

image.png


搭建Sentinel Dashboard控制台

在上一小节中,我们已经为项目成功整合了Sentinel,但这也只不过是完成了第一步。因为此时没有一个可视化的界面能让我们看到Sentinel具体的监控信息,所以还需要搭建官方提供的可视化Sentinel控制台,然后在控制台中整合项目的监控信息。

Sentinel控制台的下载地址如下:

https://github.com/alibaba/Sentinel/releases

Sentinel Dashboard有多个release版本,应该选择哪个呢?如果你是用在生产环境则选择与项目中sentinel-core版本对应的即可,如下:


image.png

若只是学习或测试使用那就可以随便选择了,只要能用就行,所以我这里选择最新版本1.6.3,注意这里选择jar包进行下载:


image.png

下载完成后,存放到一个你觉得ok的目录下,然后打开cmd,通过命令运行该jar包。如下:

E:\Spring Cloud Alibaba\Sentinel>java -jar sentinel-dashboard-1.6.3.jar

启动成功,监听的端口是8080:


image.png

使用浏览器访问http://localhost:8080进入到登录页面,默认的账户密码都是sentinel:

image.png

登录成功,此时控制台上是空白的,因为还没有监控任何的项目:


image.png

所以接着到项目中整合一下Sentinel Dashboard的请求地址,在配置文件中添加如下配置:

spring:
  cloud:
    sentinel:
      transport:
        # 配置sentinel控制台的地址
        dashboard: 127.0.0.1:8080

配置完成启动项目后需要先访问一下该项目的接口,因为Sentinel Dashboard是懒加载的,只有监控的项目被访问后才会收集监控信息。这样才能看到下图的实时监控信息,我这里的服务名是content-center:


image.png

Sentinel 相关配置项小结

客户端(微服务)连接控制台相关配置项:

spring:
  cloud:
    sentinel:
      transport:
        #指定控制台的地址
        dashboard: localhost:8080
        #指定和控制台通信的IP
        #如不配置,会自动选择一个IP注册
        client-ip:  127.0.0.1
        #指定和控制台通信的端口,默认值8719
        #如不设置,会自动从8719开始扫描,依次+1,直到找到未被占用的端口
        port: 8719
        #心跳发送周期,默认值null
        #但在S impleHttpHeartbeatSender会用默认值10秒
        heartbeat- interval-ms :  10000

控制台相关配置项:

配置项 默认值 最小值 描述
sentinel.dashboard.app.hideAppNoMachineMillis 0 60000 是否隐藏无健康节点的应用,距离最近一次主机心跳时间的毫秒数,默认关闭
sentinel.dashboard.removeAppNoMachineMillis 0 120000 是否自动删除无健康节点的应用,距离最近一次其下节点的心跳时间毫秒数,默认关闭
sentinel.dashboard.unhealthyMachineMillis 60000 30000 主机失联判定,不可关闭
sentinel.dashboard.autoRemoveMachineMillis 0 300000 距离最近心跳时间超过指定时间是否自动删除失联节点,默认关闭
server.port 8080 - 指定端口
csp.sentinel.dashboard.server localhost:8080 - 指定地址
project.name - - 指定程序的名称
sentinel.dashboard.auth.username [1.6版本支持] sentinel - Sentinel Dashboard登录账号
sentinel.dashboard.auth.password [1.6版本支持] sentinel - Sentinel Dashboard登录密码
server.servlet.session.timeout [1.6版本支持] 30分钟 - 登录Session过期时间。配置为7200表示7200秒;配置为60m表示60分钟

控制台配置项需在启动命令中指定,例如指定账户密码,如下:

java -jar -Dsentinel.dashboard.auth.username=admin -Dsentinel.dashboard.auth.password=123456 sentinel-dashboard-1.6.3.jar

流控规则

我们可以在Sentinel控制台中给某个接口添加流控规则,点击簇点链路,可以看到该服务曾经被访问过的路径:


image.png

然后点击接口右边的流控按钮就可以添加流控规则:


image.png

添加成功:


image.png

此时访问该服务的接口,QPS超过设定的阈值1,就会返回如下信息:


image.png

关于流控规则中的流控模式:

  • 直接:当前资源的QPS达到设定的阈值,就触发限流
  • 关联:当关联的资源的QPS达到设定的阈值,就触发限流。例如,/shares/1关联了/query,那么/query达到阈值,就会对/shares/1限流
  • 链路:只记录指定链路上的流量,这种模式是针对接口级别的来源进行限流

链路模式稍微有些抽象,这里举个简单的例子说明一下。下图中有两个调用链路,图中的/test-b/test-a实际就是两个接口,它们都调用了同一个common资源,所以/test-b/test-a就称为common的入口资源:

image.png

此时我为common添加一个限流规则如下:

image.png

可以看到流控模式选择链路后,需要填写一个入口资源,我这里填的是/test-a,那么这意味着什么呢?意味着当/test-a的QPS达到该规则的阈值后,就会对/test-a限流,同时/test-b不会受到任何影响。说明这种流控模式可以针对接口级别的来源进行限流,而“针对来源”则是对微服务级别的来源进行限流。

关于流控规则中的监控效果:

  • 快速失败:直接失败,抛出异常,不做任何额外的处理,是最简单的效果
    • 相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
  • Warm Up(预热):会根据codeFactor(默认3)的值,从阈值除以codeFactor,经过预热时长,才到达设置的QPS阈值。适用于将突然增大的流量转换为缓步增长的场景
    • 相关的官方文档:限流 - 冷启动
    • 相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
  • 排队等待:匀速排队,让请求以均匀的速度通过,若请求等待时间超过设置的超时时间则抛弃该请求,阈值类型必须设置成QPS,否则无效。适用于突发流量的场景
    • 相关的官方文档:限流 - 匀速器
    • 相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

关于流控的官方文档:


降级规则

服务降级实际就是断路器模式的应用,相对于流控规则,降级规则要简单一些。降级规则可以在“簇点链路”或“降级规则”中添加:


image.png

例如,这里给/shares/1添加降级规则,降级策略先以RT为例:

image.png

该降级规则的含义如下图:


image.png

此时访问/shares/1接口,秒级平均响应时间超出阈值1,并且在时间窗口内通过的请求大于等于5,就会返回如下信息:

image.png

关于RT这种降级策略需要注意的点:

  • RT默认最大为4900ms,所以即便设置的值大于4900ms也依旧会按照4900ms计算
    • 可以通过参数修改:-Dcsp.sentinel.statistic.max.rt=xxx

若将降级策略改为异常比例,则含义如下:


image.png

若将降级策略改为异常数,则含义如下:


image.png

关于异常数这种降级策略需要注意的点:

  • 若将时间窗口的值设置小于60秒则可能会出问题,因为异常数的统计是分钟级别的,时间窗口小于60秒就有可能不断进入降级状态

降级规则的相关源码:

  • com.alibaba.csp.sentinel.slots.block.degradeDegradeRule#passCheck(对降级的判断都在这个方法里完成)

在文章的开头我们介绍过断路器有三个状态,所以这里需要提及一下的是目前Sentinel的降级断路器是不支持半开状态的,只有打开和关闭两个状态,据官方人员描述说是会预计在未来添加半开的支持。

关于降级的官方文档:


热点规则

热点规则全称是热点参数限流规则,从名称可以得知,需要有参数的接口才能够使用热点规则。例如,有一个接口的代码如下:

@GetMapping("/test-hot")
@SentinelResource("hot")  // 该注解用于声明是Sentinel需要监控的资源
public String testHot(@RequestParam(required = false) String a,
                      @RequestParam(required = false) String b) {
    return a + " " + b;
}

在控制台中为hot添加热点规则,如下:


image.png

image.png
  • Tips:参数索引从0开始,对应到代码中的话,则参数a的索引为0,参数b的索引为1,所以该规则是作用于参数a

添加完该规则后,此时访问这个接口,两个参数都传值,当QPS达到阈值时,就会抛出如下异常信息:


image.png

如果不传参数a,仅传参数b的话,则不会受到该规则的限流,如下:


image.png

说明该规则表达的含义是:在时间窗口内,一旦该规则指定的索引参数QPS达到了阈值,则会触发限流

除此之外,还有高级选项,在这里可以添加参数例外项,如下示例:


image.png

image.png

添加完成后,此时将参数a的值设置为5,然后频繁发送请求,会发现即便QPS超过1也不会触发限流:


image.png

这是因为参数a的值设置为5时,限流阈值是1000,设置为其他值时,限流阈值才是1。这就是所谓的参数例外项了,即参数的为某个特定的值时,只受参数例外项里的限流阈值影响。

热点规则适用的场景:

  • 适用于存在热点参数并希望提升API可用性的场景,即某个特定请求参数QPS偏高于其他请求参数时,仅对该参数的请求限流,使用其他请求参数则可以正常响应,这样可以提高一定的可用性

使用热点规则需要注意的点:

  • 参数必须是基本类型或者String类型,否则将不会生效

热点规则相关源码:

  • com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowChecker#passCheck(对热点参数规则的判断逻辑都在这个方法里)

系统规则

系统规则全称为系统保护规则,从名称可以得知该规则是用于保护系统、防止系统负载过高而崩溃的,所以触发系统规则后会对整个系统限流。添加系统规则如下图所示:


image.png

设置系统规则比较简单,选择一个合适的阈值类型并填写阈值即可:


image.png

关于阈值类型:

  • LOAD(负载):当系统load1(1分钟的load)超过阈值,且并发线程数超过系统容量时触发,建议设置为CPU核心数 * 2.5(注意:仅对 Linux/Unit-like 机器生效)。例如CPU核心数为4,4 * 2.5 = 10
    • 系统容量 = maxQPS * minRT;(由Sentinel计算 )
      • maxQPS:秒级统计出来的最大QPS
      • minRT:秒级统计出来的最小响应时间
    • 相关源码:com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkBbr
  • RT:所有入口流量的平均RT达到阈值时触发
  • 线程数:所有入口流量的并发线程数达到阈值时触发
  • 入口QPS:所有入口流量的QPS达到阈值时触发
  • CPU使用率:系统CPU使用率达到阈值时触发

系统规则的判断逻辑所在的源码如下:

  • com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkSystem

授权规则

授权规则用于限制某个资源仅允许哪个服务访问,所以通常用于对服务消费者的访问权进行控制。我们可以在簇点链路中为某个接口添加授权规则,这里以/shares/1接口为例,如下:

image.png

新增授权规则:


image.png
  • 该授权规则的含义为:仅允许test服务访问/shares/1接口,如果授权类型设置为黑名单则表示/shares/1接口不允许test服务访问。即白名单是授权某个服务访问,黑名单则是限制某个服务访问,从而实现访问控制的效果。

代码配置规则

上面几个关于规则的小节中已经介绍了如何在Sentinel控制台中配置各种规则,除此之外,Sentinel还支持在代码中配置这些规则,所以本小节将简单介绍一下如何在代码中进行配置。

代码如下(Tips:代码基于sentinel-core 1.5.2版本):

package com.zj.node.contentcenter.controller.content;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 添加Sentinel规则
 *
 * @author 01
 * @date 2019-07-31
 **/
@Slf4j
@RestController
public class SentinelRuleController {

    /**
     * 测试添加流控规则
     */
    @PostMapping("/test-add-flow-rule")
    public String testAddFlowRile(String resourceName) {
        log.info("add flow rule. resourceName is {}", resourceName);
        addFlowQpsRule(resourceName);

        return "add flow rule success!";
    }

    /**
     * 添加流控规则
     *
     * @param resourceName 资源名称
     */
    private void addFlowQpsRule(String resourceName) {
        // 规则列表
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule(resourceName);
        // 针对来源
        rule.setLimitApp("default");
        // 设置阈值类型为QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 单机阈值
        rule.setCount(20);
        // 将规则添加到规则列表
        rules.add(rule);
        // 加载规则列表
        FlowRuleManager.loadRules(rules);
    }

    /**
     * 添加降级规则
     *
     * @param resourceName 资源名称
     */
    private void addDegradeRule(String resourceName) {
        List<DegradeRule> rules = new ArrayList<>();
        DegradeRule rule = new DegradeRule(resourceName);
        // 设置降级策略为 RT
        rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
        // set threshold RT, 10 ms(设置RT时间阈值)
        rule.setCount(10);
        // 时间窗口
        rule.setTimeWindow(10);
        rules.add(rule);
        DegradeRuleManager.loadRules(rules);
    }

    /**
     * 添加热点规则
     *
     * @param resourceName 资源名称
     */
    private void addHotRule(String resourceName) {
        ParamFlowRule rule = new ParamFlowRule(resourceName);
        // 参数索引
        rule.setParamIdx(0);
        // 单机阈值
        rule.setCount(5);

        // 添加参数例外项
        ParamFlowItem item = new ParamFlowItem();
        // 参数类型
        item.setClassType(int.class.getName());
        // 参数值
        item.setObject("5");
        // 限流阈值
        item.setCount(10);
        rule.setParamFlowItemList(Collections.singletonList(item));

        ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
    }

    /**
     * 添加系统规则
     */
    private void addSystemRule() {
        List<SystemRule> rules = new ArrayList<>();
        SystemRule rule = new SystemRule();
        // 设置系统最高负载阈值
        rule.setHighestSystemLoad(10);
        rules.add(rule);
        SystemRuleManager.loadRules(rules);
    }

    /**
     * 添加授权规则
     *
     * @param resourceName 资源名称
     * @param limitApp     流控应用(指调用方,多个调用方名称使用英文逗号分隔)
     */
    private void addAuthorityRule(String resourceName, String limitApp) {
        AuthorityRule rule = new AuthorityRule();
        // 资源名称
        rule.setResource(resourceName);
        // 流控应用
        rule.setLimitApp(limitApp);
        // 设置授权类型为白名单
        rule.setStrategy(RuleConstant.AUTHORITY_WHITE);

        AuthorityRuleManager.loadRules(Collections.singletonList(rule));
    }
}

我们来测试添加流控规则,使用postman访问测试接口,如下:


image.png

添加成功后,到Sentinel控制台中,查看是否存在该规则:


image.png

从上图中可以看到该流控规则已经成功添加到Sentinel中了,证明测试成功。至于其他的规则也可以使用类似的方式添加,并且也都给出了代码,这里就不一一去演示了。


Sentinel规则参数总结

下面总结一下Alibaba Sentinel各种规则的参数,并且提供了官方文档的链接,若未来本文不再适用,可以自行点击链接前往官方文档查看

1、流控规则:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 或线程数模式 QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 根据资源本身
controlBehavior 流控效果(直接拒绝 / 排队等待 / 慢启动模式) 直接拒绝

官方文档:


2、降级规则:

Field 说明 默认值
resource 资源名,即限流规则的作用对象
count 阈值
grade 降级模式,根据 RT 降级还是根据异常比例或异常数降级 RT
timeWindow 降级的时间,单位为 s

官方文档:


3、热点规则:

Field 说明 默认值
resource 资源名,即热点规则的作用对象
count 限流阈值,必填
grade 限流模式 QPS 模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

官方文档:


4、系统规则:

Field 说明 默认值
highestSystemLoad 最大的 load1,参考值 -1 (不生效)
avgRt 所有入口流量的平均响应时间 -1 (不生效)
maxThread 入口流量的最大并发数 -1 (不生效)
qps 所有入口资源的 QPS -1 (不生效)

官方文档:


5、授权规则:

Field 说明 默认值
resource 资源名,即授权规则的作用对象
limitApp 流控应用(指调用方,即服务消费者),对应的黑名单/白名单,不同 origin 用英文逗号(,)分隔,如 appA,appB
strategy 限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式 AUTHORITY_WHITE

官方文档:

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

推荐阅读更多精彩内容