Spring Cloud Alibaba Sentinel组件

什么是服务雪崩效应

服务雪崩效应是一种因“服务提供者服务的不可用”(原因)导致“服务调用者服务不可用”(结果),并将不可用逐渐放大的现象。如下图所示


image.png

形成原因

服务雪崩的过程可以分为三个阶段:

  • 服务提供者不可用;
  • 重试加大请求流量;
  • 服务调用者不可用;
    服务雪崩的每个阶段都可能由不同的原因造成,总结如下:


    image.png

应对策略

常见容错方案:

1、超时
2、限流
3、舱壁模式(如每个controller都有自己独立的线程池,之间互不干扰)
4、断路器模式

image.png

全面应对策略:

image.png

Sentinel 是什么

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

大家可能会问:Sentinel 和之前常用的熔断降级库 Netflix Hystrix 有什么异同呢?Sentinel官网有一个对比的文章,这里摘抄一个总结的表格,具体的对比可以点此 链接 查看。

对比内容 Sentinel Hystrix
隔离策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于响应时间或失败比率 基于失败比率
实时指标实现 滑动窗口 滑动窗口(基于 RxJava)
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于 QPS,支持基于调用关系的限流 不支持
流量整形 支持慢启动、匀速器模式 不支持
系统负载保护 支持 不支持
控制台 开箱即用,可配置规则、查看秒级监控、机器发现等 不完善
常见框架的适配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix

从对比的表格可以看到,Sentinel比Hystrix在功能性上还要强大一些,本文让我们一起来了解下Sentinel的源码,揭开Sentinel的神秘面纱。

Sentinel功能特点

1、丰富的应用场景:例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等
2、完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
3、广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
4、完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

image.png

开源生态

image.png

Sentinel 分为两个部分:

核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

为应用整合Sentinel

引入maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
    <!--sentinel整合之后会暴露出/actuator/sentinel-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <!--整合Spring Cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合Spring Cloud Alibaba-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>0.9.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

加入配置暴露/actuator/sentinel端点

#添加sentinel依赖后 暴露/actuator/sentinel端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

启动服务访问http://localhost:port/actuator/sentinel会返回json信息,说明已经整合好了Sentinel

搭建Sentinel控制台

下载Sentinel控制台:https://github.com/alibaba/Sentinel/releases

为服务整合Sentinel控制台:

spring:
    cloud:
        sentinel:
        filter:
            #打开/关闭掉对Spring MVC端点的保护
            enabled: true
        transport:
            port: 8719
            #指定sentinel控制台的地址
            dashboard: localhost:8080
image.png

定义资源:也就是对哪个资源进行流量控制,现在已经提供了注解形式,所以新的接入直接用注解,@SentinelResource

  • 关于SentinelResource注解,这里列出几个好用和必填的参数,具体参考这里

Sentinel控制台配置流控规则

流控模式

  • 直接:当QPS超过阈值就进行限流。
  • 关联:当关联的资源达到阈值,就限流自己。
    • 适用场景:查询和修改同一表的数据,如果是高并发的应用,查询接口的流量过大,就会影响修改接口的性能,反之同理,这就可以根据业务需求,去衡量希望优先读还是优先写。
    • 关联其实是一种保护关联资源的设计。
  • 链路:只记录指定链路上的流量,即指定资源从入口资源进来的流量如果达到阈值就限流。
    • 链路其实是一种细粒度的针对来源,而编辑流控规则中的针对来源输入框是微服务级别的,可以指定指定微服务过来的流量达到阈值就限流。
    • 而链路是api级别的,指定的是api的调用流量达到阈值就限流。
流控模式-关联
流控模式-链路

流控效果

  • 快速失败:直接失败,抛异常
    相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
  • Warm Up(预热):根据codeFactor(冷加载因子,默认值为3),从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
    即如果阈值为100,冷加载因子为3,预热时长为10秒,那么就会用100 / 3作为最初的阈值,经过10秒之后才会将阈值达到100,进而进行限流,意思就是让允许通过的流量缓慢增加,在达到一定的时间之后才达到阈值这样会更好的保护微服务
  • 排队等待:匀速排队,让请求以均匀的速度通过,阈值类型必须设置成QPS,否则无效。此种模式可适用于应对突发流量的场景
    相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
image.png
流控效果-排队等待

Sentinel控制台配置降级规则,即断路器模式,Sentinel目前只有断路器三态中的打开和关闭,没有半开状态

降级策略

  • RT:平均响应时间
    注意:Sentinel默认RT最大时间为4900毫秒,可通过-Dcsp.sentinel.statistic.max.rt=xxx修改


    image.png
  • 异常比例


    image.png
  • 异常数
    注意:时间窗口<60秒可能会出问题
    相关源码:com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule


    image.png

    image.png

Sentinel控制台配置热点规则,是一种特殊的流控规则,支持对特定的参数和参数的值限流

适用于存在热点参数(某些参数QPS很高),并希望提升API可用性的场景
注意:参数必须是基本类型或者String
相关源码:com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowChecker#passCheck()
加入代码

@GetMapping("/test-hot")
@SentinelResource("hot")
public String testHot(@RequestParam(required = false) String a,
                      @RequestParam(required = false)String b){
    return a + ":" + b;
}
image.png

Sentinel控制台配置系统规则

阈值类型

  • LOAD
    当系统Load1(1分钟的load)超过阈值,且并发线程数超过系统容量时触发,建议配置为CPU核心数*2.5。(仅对Linux/Unix-like 机器生效)
    系统容量 = maxQps * minRt
    maxQps:秒级统计出来的最大QPS
    minRt:秒级统计出来的最小响应时间
    相关源码:com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkBbr()
  • RT:所有入口流量的平均RT达到阈值触发
  • 线程数:所有入口流量的并发线程数达到阈值触发
  • 入口QPS:所有入口流量的QPS达到阈值触发
    相关源码:com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkSystem()


    image.png

Sentinel控制台配置授权规则

  • 白名单:资源名里的资源只允许为白名单里面的流控应用访问。
  • 黑名单:资源名里的资源不允许为黑名单里面的流控应用访问。
  • 授权规则通过调用来源从而实现对服务消费者的授权或者限制。
image.png

Sentinel使用Java代码方式配置规则

请点击:
Alibaba Sentinel 规则参数总结
Alibaba Sentinel 配置项总结

Sentinel与控制台通信原理剖析

image.png

微服务注册到Sentinel控制台和发送心跳源码:com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender
微服务和Sentinel控制台通信API源码:com.alibaba.csp.sentinel.command.CommandHandler接口的实现类

修改控制台规则是如何通知客户端的?
看sentinel-transport-simple-http包中的HttpEventTask类,它开启了一个线程,专门用来做为socket连接,控制台通过socket请求通知客户端,从而更新客户端规则,更改规则核心代码如下:

long start = System.currentTimeMillis();
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream(), SentinelConfig.charset()));
OutputStream outputStream = this.socket.getOutputStream();
printWriter = new PrintWriter(new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset())));
String line = in.readLine();
CommandCenterLog.info("[SimpleHttpCommandCenter] socket income: " + line + "," + this.socket.getInetAddress(), new Object[0]);
CommandRequest request = this.parseRequest(line);
String commandName = HttpCommandUtils.getTarget(request);
if (!StringUtil.isBlank(commandName)) {
    CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
    if (commandHandler != null) {
        CommandResponse<?> response = commandHandler.handle(request);
        this.handleResponse(response, printWriter, outputStream);
    } else {
        this.badRequest(printWriter, "Unknown command `" + commandName + '`');
    }

    printWriter.flush();
    long cost = System.currentTimeMillis() - start;
    CommandCenterLog.info("[SimpleHttpCommandCenter] Deal a socket task: " + line + ", address: " + this.socket.getInetAddress() + ", time cost: " + cost + " ms", new Object[0]);
    return;
}

this.badRequest(printWriter, "Invalid command");

Sentinel配置项

微服务应用端连接Sentinel控制台配置项

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

Sentinel控制台配置项

配置项 默认值 最小值 描述
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 - dashboard登录账号
sentinel.dashboard.auth.password[1.6] sentinel - dashboard登录密码
server.servlet.session.timeout[1.6] 30分钟 - 登录session过期时间 配置为
7200表示7200秒 配置为60m表示为60分钟

Sentinel API

  • Sphu:定义资源,让资源受到监控并保护资源。
  • Tracer:可以对我们想要的异常进行统计。
  • ContextUtil:可以实现调用来源,还可以标记调用。
@GetMapping("/test-sentinel-api")
public String testSentinelAPI(@RequestParam(required = false) String a){
    String resourceName = "test-sentinel-api";
    ContextUtil.enter(resourceName,"test-wfw");

    Entry entry = null;
    try {
        //定义一个sentinel保护的资源 名称是test-sentinel-api
        entry = SphU.entry(resourceName);
        //被保护的业务逻辑
        if(StringUtils.isEmpty(a)){
            throw new IllegalArgumentException("a不能为空");
        }
        return a;
    } catch (BlockException e) {
        //如果被保护的资源被限流或者降级了,就会抛BlockException
        log.warn("限流,或者降级了...",e);
        return "限流,或者降级了...";
    }catch (IllegalArgumentException e) {
        //统计IllegalArgumentException发生的次数,发生的占比等
        Tracer.trace(e);
        return "参数非法...";
    } finally {
        if(entry != null){
            //退出entry
            entry.exit();
        }
        ContextUtil.exit();
    }
}
流控规则-针对来源

Sentinel @SentinelResource详解

@SentinelResource使用方式一

//使用blockHandler属性,blockHandler的方法必须和资源在同一类中,并且有相同的参数和返回值
@PostMapping
@SentinelResource(value = "createOrder",blockHandler = "doOnBlock")
public OrderInfo create(@RequestBody OrderInfo order, @AuthenticationPrincipal String username){
    log.info("用户名为:username={}",username);
    PriceInfo priceInfo = priceFeignClient.getPrice(order.getProductId());
    log.info("商品价格为,priceInfo={}",priceInfo);
    return order;
}

public OrderInfo doOnBlock(@RequestBody OrderInfo order, @AuthenticationPrincipal String username, BlockException exception){
    log.info("blocked by :blockException={}",exception.getClass().getSimpleName());
    return order;
}

@SentinelResource使用方式二

@PostMapping
@SentinelResource(value = "createOrder",blockHandler = "doOnBlock",blockHandlerClass = SentinelBlockHandler.class)
public OrderInfo create(@RequestBody OrderInfo order, @AuthenticationPrincipal String username){
    log.info("用户名为:username={}",username);
    PriceInfo priceInfo = priceFeignClient.getPrice(order.getProductId());
    log.info("商品价格为,priceInfo={}",priceInfo);
    return order;
}

@Slf4j
public class SentinelBlockHandler {

    public static OrderInfo doOnBlock(@RequestBody OrderInfo order, @AuthenticationPrincipal String username, BlockException exception){
        log.info("blocked by :blockException={}",exception.getClass().getSimpleName());

        return order;
    }
}

@SentinelResource其与属性和新增属性

属性 作用 是否必须
value 资源名称
entryType entry类型,标记流量的方向,取值IN/OUT,默认是OUT
blockHandler 处理BlockException的函数名称。函数要求:
1. 必须是 public
2.返回类型与原方法一致
3. 参数类型需要和原方法相匹配,并在最后加 BlockException 类型
的参数。
4. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置
blockHandlerClass ,并指定blockHandlerClass里面的方法。
blockHandlerClass 存放blockHandler的类。对应的处理函数必须static修饰,
否则无法解析,其他要求:同blockHandler。
fallback 用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对
所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)
进行处理。函数要求:
1. 返回类型与原方法一致
2. 参数类型需要和原方法相匹配,Sentinel 1.6开始,
也可在方法最后加 Throwable 类型的参数。
3.默认需和原方法在同一个类中。若希望使用其他类的函数,
可配置 fallbackClass ,并指定fallbackClass里面的方法。
fallbackClass【1.6】 存放fallback的类。对应的处理函数必须static修饰,
否则无法解析,其他要求:同fallback。
defaultFallback【1.6】 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的
异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。
若同时配置了 fallback 和defaultFallback,以fallback为准。
函数要求:1. 返回类型与原方法一致
2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,
可配置 fallbackClass ,并指定 fallbackClass 里面的方法。
exceptionsToIgnore【1.6】 指定排除掉哪些异常。排除的异常不会计入异常统计,
也不会进入fallback逻辑,而是原样抛出。
exceptionsToTrace 需要trace的异常 Throwable

@SentinelResource相关源码

com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect
com.alibaba.csp.sentinel.annotation.aspectj.AbstractSentinelAspectSupport

RestTemplate整合Sentinel

@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate RestTemplate(){

    return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
}

resttemplate:
  sentinel:
    #false 关闭@SentinelRestTemplate注解,在做开发调试的时候可以关闭此注解,专注于功能的实现
    enabled: true

相关源码:org.springframework.cloud.alibaba.sentinel.custom.SentinelBeanPostProcessor

Feign整合Sentinel

feign:
  sentinel:
    #为feign整合Sentinel
    enabled: true

发生限流降级时,自定义处理逻辑

使用@FeignClient的fallback属性

@FeignClient(value="priceServer",fallback = PriceFeignClientFallback.class)
public interface PriceFeignClient {

    @GetMapping(value = "/prices/{id}")
    PriceInfo getPrice(@PathVariable("id") Integer id);
}

/**
 * 发生限流降级时,自定义处理逻辑
 *
 * 一旦PriceFeignClient中远程调用的getPrice()方法被流控了或发生异常了,就会进入此方法
 * 这就相当于一个兜底的行为,保证了服务的可用
 */

@Component
public class PriceFeignClientFallback implements PriceFeignClient {

    @Override
    public PriceInfo getPrice(Integer id) {
        PriceInfo priceInfo = new PriceInfo();
        priceInfo.setId(id);
        priceInfo.setPrice(new BigDecimal(id));
        return priceInfo;
    }
}

使用@FeignClient的fallbackFactory属性,推荐使用这种方案

@FeignClient(value="priceServer",fallbackFactory = PriceFeignClientFallbackFactory.class)
public interface PriceFeignClient {

    @GetMapping(value = "/prices/{id}")
    PriceInfo getPrice(@PathVariable("id") Integer id);
}

/**
 * 发生限流降级时,自定义处理逻辑
 * 一旦PriceFeignClient中远程调用的getPrice()方法被流控了或发生异常了,就会进入此方法
 * 这就相当于一个兜底的行为,保证了服务的可用
 * 相比于FeignClient中的fallback属性而言,fallbackFactory属性在fallback的基础上可以拿到异常信息
 */

@Component
@Slf4j
public class PriceFeignClientFallbackFactory implements FallbackFactory<PriceFeignClient> {

    @Override
    public PriceFeignClient create(Throwable throwable) {
        return new PriceFeignClient() {
            @Override
            public PriceInfo getPrice(Integer id) {
                log.error("远程调用被限流或降级了,throwable={}",throwable);
                PriceInfo priceInfo = new PriceInfo();
                priceInfo.setId(id);
                priceInfo.setPrice(new BigDecimal(id));
                return priceInfo;
            }
        };
    }
}

相关源码:org.springframework.cloud.alibaba.sentinel.feign.SentinelFeign

Sentinel使用方式总结

使用方式 使用方式 使用方法
编码方式 API try...catch...finally
注解方式 @SentinelResource blockHandler / fallback
RestTemplate @SentinelRestTemplate blockHandler / fallback
Feign feign.sentinel.enabled=true fallback / fallbackFactory

Sentinel规则持久化方案推荐:

Alibaba Sentinel规则持久化-推模式-手把手教程【基于Nacos】

https://github.com/eacdy/Sentinel-Dashboard-Nacos/releases

https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel#pull模式

参考:

https://www.jianshu.com/p/0e218ef7f505

https://blog.csdn.net/sheinenggaosuwo/article/details/86592893

https://blog.csdn.net/xudawenfighting/article/details/80127279

阿里熔断限流Sentinel研究

阿里sentinel源码研究深入

https://www.jianshu.com/p/ed57014e1abb

Sentinel-开源版本Dashboard集成Apollo配置中心

阿里Sentinel控制台源码修改-对接Apollo规则持久化

Alibaba Sentinel 配置项总结

SentinelResource注解 属性总结

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

推荐阅读更多精彩内容