2020年了还不知道配置中心?

Hi,好久不见,我是你们的walking****,在这个高燃BGM下又和大家见面了😜。今天分享一个分布式、微服务架构中重要的一个组件:配置中心。

也许你们现在的项目不是分布式、微服务架构,没有用到配置中心,也没听说过配置中心,对它完全很陌生,那你就很有必要阅读本文了。

本文将从

1)什么是配置、什么是配置中心以及配置中心的诞生讲起,

2)然后简单介绍几个不错的开源配置中心产品,

3)接着会重点介绍携程开源的分布式配置中心Apollo的架构以及基本的搭建与使用。

好了废话不多说,开始...🌝🌝

什么是配置?

众所周知,应用程序在启动和运行的时候会去读取一些「配置信息」,比如:数据库连接参数、启动参数、接口的超时时间、应用程序的端口等。「配置」,基本上伴随着应用程序的整个生命周期。

不知道大家有没有注意,配置其实它有一些比较明显的特点,如下:

1、独立于程序的只读变量

1)配置是独立于应用程序的,并且同一个程序在不同的配置下会有不同的行为;

2)其次,配置对于程序是只读的,程序通过读取配置来改变自己的行为,程序不应该去改变配置

2、伴随应用的整个生命周期

1)配置贯穿于应用的整个生命周期,包括启动和运行。应用在启动时通过读取配置来初始化,在运行时根据配置调整行为。比如:启动时需要读取服务的端口号、系统在运行过程中需要读取定时策略执行定时任务、根据配置调整日志级别等。

3、多种加载方式

1)常见的加载方式有程序内部硬编码、配置文件、环境变量、启动参数、基于数据库等,最常用的是配置文件和环境变量这两种方式。

4、需要治理

1)权限控制:由于配置能改变程序的行为,不正确的配置甚至会造成灾难,所以对配置的修改必须有比较完善的权限控制体系来治理;

2)对不同环境、集群进行配置管理:同一个程序在不同的环境(开发,测试,预生产、生产)、不同的集群(如不同的数据中心、服务器在全国很多地方都有机房部署集群的情况)经常需要有不同的配置,所以需要有完善的环境、集群配置管理的能力。

5、支持持久化

1)配置信息需要支持持久化,永久保存,重启后依然能够找到之前的配置,配置一旦生成就永久存在除非人为的删除。

我们最常用的就是在项目的resources下创建各种properties文件放我们的各种配置

配置中心的诞生

配置中心的诞生和项目架构的演进有着密切的联系。传统单体应用存在一些潜在缺陷,如随着规模的扩大,部署效率降低,团队协作效率差,系统可靠性变差,维护困难,新功能上线周期长等,所以迫切需要一种新的架构去解决这些问题,而微服务( microservices )架构正是当下一种流行的解决方案。

不过,解决一个问题的同时,往往会面临很多新的问题,所以微服务化的过程中伴随着很多的挑战,其中一个挑战就是有关服务(应用)配置的。

1)当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,各个服务都有自己的配置,随着项目需求的不断壮大发展,配置会越来越多,到最后繁琐的配置文件会让你越来越崩溃,稍不注意出个错配置错了就得修改配置重新打包部署,特别麻烦。

2)在集群部署的情况下,如果新版本的配置会给系统带来很大的影响,我们往往会选择灰度发布,即先发布部分服务器,进行测试,稳定后再将配置同步到所有服务器,如果说还用传统的方式,那么我们就需要将配置文件一个个的修改然后重启服务,虽然不需要我们开发自己去做,有运维,那也挺烦人的,运维发布完了,我们还得检查他改的是不是正确,费时费力。

3)而且在系统不断的迭代的过程中有些配置在多个服务之间都是相同或相近的,就会有很大的冗余。

所以在分布式、微服务这种大环境下,传统的项目配置方式的弊端就慢慢的凸显出来了,这个问题变得非常棘手,亟待一种管理配置、治理配置的解决方案。这时,配置中心就应运而生了。

什么是配置中心

配置中心,顾名思义,将配置中心化,说白了就是将配置从应用中抽取出来,统一管理,优雅的解决了配置的动态变更、权限管理、持久化、运维成本等问题。应用程序自身只是从配置中心拿到自己想要的配置,既不需要去添加管理配置的接口,也不需要自己去实现配置的持久化,更不需要去关心配置何时变化。配置与应用程序隔离开,单独管理配置。

总得来说,配置中心就是一种统一管理各种应用配置的基础服务组件。

在系统架构中,配置中心是整个微服务基础架构体系中的一个组件,如下图,它的功能看上去并不起眼,无非就是配置的管理和存取,但它是整个微服务架构中不可或缺的一环。

image

集中管理配置,那么就要将应用的配置作为一个单独的服务抽离出来了,同理也需要解决新的问题,比如:版本管理(为了支持回滚),权限管理等。

总结一下,在传统巨型单体应用纷纷转向细粒度微服务架构的历史进程中,配置中心是微服务化不可缺少的一个系统组件,在这种背景下中心化的配置服务即配置中心应运而生,一个合格的配置中心需要满足一下功能:

  • 对配置项的读取和修改提供简单易用的API和操作界面

  • 添加新配置要够简单、直接、方便

  • 支持不同管理员对配置的修改的不同权限,以把控风险

  • 可以查看配置修改的历史记录,以便回滚

  • 不同部署环境相互隔离,互不影响

主流配置中心简介

目前市面上用的比较多的配置中心有:(按开源时间排序)

简介

1、Disconf

2014年7月百度开源的配置管理中心,专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」。目前已经不再维护更新。

https://github.com/knightliao/disconf

2、Spring Cloud Config

2014年9月开源,Spring Cloud 生态组件,可以和Spring Cloud体系无缝整合。

https://github.com/spring-cloud/spring-cloud-config

3、Apollo

2016年5月,携程开源的配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

https://github.com/ctripcorp/apollo

4、Nacos

2018年6月,阿里开源的配置中心,也可以做DNS和RPC的服务发现。

https://github.com/alibaba/nacos

功能特性对比

由于Disconf不再维护,下面主要对比一下Spring Cloud Config、Apollo和Nacos。


image.png

总的来看,Apollo和Nacos相对于Spring Cloud Config的生态支持更广,在配置管理流程上做的更好。Apollo相对于Nacos在配置管理做的更加全面,Nacos则使用起来相对比较简洁,在对性能要求比较高的大规模场景更适合。但对于一个开源项目的选型,项目上的人力投入(迭代进度、文档的完整性)、社区的活跃度(issue的数量和解决速度、Contributor数量、社群的交流频次等),这些因素也比较关键,考虑到Nacos开源时间不长和社区活跃度,所以从目前来看Apollo应该是最合适的配置中心选型。

Apollo

Apollo特性

基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提供了以下的特性:

1、统一管理不同环境、不同集群的配置

  • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。

  • 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等

  • 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖

2、配置修改实时生效(热发布)

  • 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序

3、版本发布管理

  • 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚

4、灰度发布

  • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例

5、权限管理、发布审核、操作审计

  • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。

  • 所有的操作都有审计日志,可以方便地追踪问题

6、客户端配置信息监控

  • 可以在界面上方便地看到配置在被哪些实例使用

7、提供Java和.Net原生客户端

  • 提供了Java和.Net的原生客户端,方便应用集成

  • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)

  • 同时提供了Http接口,非Java和.Net应用也可以方便地使用

8、提供开放平台API

  • Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等

  • 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制

执行流程

先来看一个简单的工作流程图,如下:

image

操作流程如下:

1、管理员通过可视化操作界面在Apollo配置中心修改、发布配置

2、应用程序通过Apollo客户端从配置中心拉取配置信息

管理员通过Apollo配置中心修改或发布配置后,会有两种机制来保证应用程序获取最新配置:

1)Apollo配置中心主动向客户端推送最新的配置;

2)Apollo客户端会定时从Apollo配置中心拉取最新的配置。

通过以上推、拉的两种机制共同来保证应用程序能及时获取到配置。

Apollo的安装部署

下载地址:https://github.com/ctripcorp/apollo/releases

image

两种下载方式,一是直接下载上图的画框的三个分别是adminservice、configservice、portal;二是下载source code源码自己打包,zip 和 tar.gz 都可以。这里使用的第二种方式。

解释一下为啥第一种下载方式会有三个包,这其实和Apollo的架构有关。portal 是一个可视化界面会调用 adminservice 进行配置管理和发布,可以认为是 adminservice 的 client 端;configservice 和 Apollo 的 client 保持长连接,configservice 服务于 Apollo client 进行配置获取,configservice 和 client 通过一种推拉结合的方式,实现配置实时更新的同时,保证配置更新不丢失。所以不管是第一种方式还是第二种方式,我们都需要运行三个jar程序。

采用第二种方式,下载好源码并解压后倒导入idea,如下

image
image.gif

然后找到scripts文件夹并打开,有一个 sql 文件夹,里面有两个 SQL 文件

image
image.gif

在mysql里执行这两个SQL 文件,成功后会创建两个数据库

image
image.gif

然后注意到 scripts 目录下有两个文件,build.bat 和 build.sh 文件,分别是Windows 和 linux用的。

需要打开对应的文件更改一些参数,如数据库配置参数URL,用户名密码,改成你自己的。

下面这些是Erueka的不同环境的地址,我们就本地dev的不用改了

set dev_meta="http://localhost:8080"
set fat_meta="http://someIp:8080"
set uat_meta="http://anotherIp:8080"
set pro_meta="http://yetAnotherIp:8080"
image.gif

然后,运行就直接就是用maven打包了,打完之后在各自的target目录下即可看到

image
image.gif

image
image.gif

image
image.gif

我是把这三个jar复制出来,放到一个专门的目录,方便运行。

然后,依次启动三个jar

java -jar apollo-configservice-1.6.1.jar
java -jar apollo-adminservice-1.6.1.jar
java -jar apollo-portal-1.6.1.jar
image.gif

都启动后,访问http://localhost:8070/ 进入Apollo的配置管理中心,可以创建项目(walking是我自己创建的),稍后再讲

image

然后访问 http://localhost:8080/ 出现如下画面,Apollo安装部署成功。

image

创建项目

点击创建项目,选择部门,Apollo默认的有两个样例,当然你可以在右上角管理员工具里添加,稍后再说。

然后填写AppId,这是全局唯一的,客户端调用的时候要用这个。应用名称就随意写了。

应用负责人,默认的是Apollo,也可以在右上角管理员工具里增加。

项目管理员,可以额外添加该项目的管理员

image

然后提交就行了。

然后就会进入到该项目,默认会有一个application 的命名空间

image

可以添加命名空间

image

我们可以点击新增配置来添加参数

image

新添加的参数是未发布状态,可点击发布按钮使其生效

image
image
image

然后就发布成功了。

Apollo工作原理

下图是Apollo架构模块的概览

apollo架构图 by walking

各模块职责

上图简要描述了Apollo的总体设计,我们可以从下往上看:

  • Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端

  • Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)

  • Eureka提供服务注册和发现,为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的

  • Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳

  • 在Eureka之上架了一层Meta Server用于封装Eureka的服务发现接口

  • Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试

  • Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试

  • 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

分步执行流程

  1. Apollo启动后,Config/Admin Service会自动注册到Eureka服务注册中心,并定期发送保活心跳。

  2. Apollo Client和Portal管理端通过配置的Meta Server的域名地址经由Software Load Balancer(软件负载均衡器)进行负载均衡后分配到某一个Meta Server

  3. Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client

  4. Meta Server获取Config Service和Admin Service(IP+Port)失败后会进行重试

  5. 获取到正确的Config Service和Admin Service的服务信息后,Apollo Client通过Config Service为应用提供配置获取、实时更新等功能;Apollo Portal管理端通过Admin Service提供配置新增、修改、发布等功能

核心概念

application (应用)

这个很好理解,就是实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置

关键字:appId

environment (环境)

配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置

关键字:env

cluster (集群)

一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。

关键字:cluster

namespace (命名空间)

一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等

关键字:namespaces

它们的关系如下图所示:

image

Spring Boot整合Apollo

Apollo搭建好之后,我们通过一个简单的SpringBoot项目来去从它上面获取配置,进行一个测试。

入门案例

1、添加Apollo client依赖

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>${apollo.client.version}</version>
</dependency>
image.gif

2、application.yml添加配置

server:
  port: 8761
app:
  id: walking #AppId是应用的身份信息,是配置中心获取配置的一个重要信息
apollo:
  meta: http://localhost:8080 #eureka地址
  bootstrap:
    enabled: true #在应用启动阶段,向Spring容器注入被托管的application.properties文件的配置信息

3、启动类上增加 @EnableApolloConfig 注解

@RestController
public class ApolloController {
    @Value("${test_key}")
    String testApollo;
    @GetMapping("testApollo")
    public Map<String,Object> testApollo() {
        Map<String,Object> result = new HashMap<>();
        result.put("testApollo",testApollo);
        return result;
    }
}

4、访问接口,拿到了配置value值

image

访问多个命名空间

1、修改application.yml,增加 namespaces 选项,填写命名空间名称,application可以不用写。

server:
  port: 8761
app:
  id: walking #AppId是应用的身份信息,是配置中心获取配置的一个重要信息
apollo:
  meta: http://localhost:8080 #eureka地址
  bootstrap:
    enabled: true #在应用启动阶段,向Spring容器注入被托管的application.properties文件的配置信息
    namespaces: TEST1.datasource-mysql.config

2、获取另外一个命名空间内的参数 mysql.url

@RestController
public class ApolloController {
    @Value("${test_key}")
    String testApollo;
    @Value("${mysql.url}")
    String mysqlUrl;
    @GetMapping("testApollo")
    public Map<String,Object> testApollo() {
        Map<String,Object> result = new HashMap<String,Object>();
        result.put("testApollo",testApollo);
        result.put("mysqlUrl",mysqlUrl);
        return result;
    }
}

3、测试,拿到了

image

动态修改日志级别

日志模块是每个项目中必须的,用来记录程序运行中的相关信息。一般在开发环境下使用DEBUG级别的日志输出,为了方便查看问题,而在线上一般都使用INFO或者ERROR级别的日志,主要记录业务操作或者错误的日志。

那么问题来了,当线上环境出现问题希望输出DEBUG日志信息辅助排查的时候怎么办呢?以前确实是这么做的:修改配置文件,重新打包然后上传重启线上环境。很麻烦,也很慢。

所以我们想通过把日志级别参数部署到 Apollo 上,然后监听参数变化后从 Apollo 上获取日志级别,再修改日志级别,来达到热更新的效果。

1、引入日志包,这里使用的是lombok,比较方便演示

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
    <scope>provided</scope>
</dependency>

2、application.yml​​​​​​​

server:
  port: 8761
app:
  id: walking #AppId是应用的身份信息,是配置中心获取配置的一个重要信息
apollo:
  meta: http://localhost:8080
  bootstrap:
    enabled: true #在应用启动阶段,向Spring容器注入被托管的application.properties文件的配置信息。
    namespaces: TEST1.datasource-mysql.config
   eagerLoad:
      enabled: true #将Apollo配置的加载提到初始化日志系统之前
logging:
  level:
    com:
      walking:
        controller: info #根据包名调整controller包的日志级别,为了后面演示在配置中心动态配置日志级别

3、增加了如下配置,用于将 Apollo 配置的加载提到初始化日志系统之前,以及配置com.walking.controller包的日志级别为 debug

   eagerLoad:
      enabled: true #将Apollo配置的加载提到初始化日志系统之前
logging:
  level:
    com:
      walking:
        controller: debug #根据包名调整controller包的日志级别,为了后面演示在配置中心动态配置日志级别      

4、然后增加这样一个参数用于部署日志级别

image

5、然后通过@ApolloConfigChangeListener注解监听配置的变化,监听到配置变化时重新设置该目录的日志级别​​​​​​​

@Slf4j
@Configuration
public class LoggerConfig {
    private static final String LOGGER_TAG = "logging.level.";
    @Autowired
    private LoggingSystem loggingSystem;
    /*
     * 将Apollo服务端的中的配置注入这个类中。
     */
    @ApolloConfig
    private Config config;
    /*
     * @ApolloConfigChangeListener
     * value 指定监听哪个 namespace,默认是 application
     * interestedKeys 指定监听哪些key 如interestedKeys={"abc","123"},
     * interestedKeyPrefixes 指定监听的key(模糊匹配)
     * 监听配置中心配置的更新事件,若该事件发生,则调用refreshLoggingLevels方法,处理该事件。
     * ConfigChangeEvent参数:可以获取被修改配置项的key集合,以及被修改配置项的新值、旧值和修改类型等信息。
     */
    @ApolloConfigChangeListener(value="application",interestedKeyPrefixes = {LOGGER_TAG})
    private void configChangeListter(ConfigChangeEvent changeEvent) {
        refreshLoggingLevels(changeEvent);
    }
    private void refreshLoggingLevels(ConfigChangeEvent changeEvent) {
        log.info("apollo config changed,LoggerConfig 更新日志级别");
        Set<String> keyNames = changeEvent.changedKeys();
        for (String key : keyNames) {
            if (StringUtils.containsIgnoreCase(key, LOGGER_TAG)) {
                ConfigChange change = changeEvent.getChange(key);
                String newLevel = change.getNewValue();
                String oldLevel = change.getOldValue();
                LogLevel level = LogLevel.valueOf(newLevel.toUpperCase());
                String packageName = key.replace(LOGGER_TAG, "");
                loggingSystem.setLogLevel(packageName, level);
                log.info("package logLevel changed==>key:{},package:{},oldLevel:{},newLevel:{},changeType:{}",
                        key, packageName, oldLevel,newLevel,change.getChangeType());
            }
        }
        log.info("apollo config changed,LoggerConfig 更新完毕");
    }
}

6、我们在 controller 的接口里增加如下代码,便于观察日志级别是否改变
​​​​​

log.debug("debug...");
log.info("info...");
log.error("error...");
log.warn("warn...");

7、首先启动spring boot项目,我们访问测试接口,查看日志。

image

正是我们在 application.yml 上设置的 debug 日志级别。

8、接下来我们在 Apollo 上修改日志级别为 warn,然后发布。

日志里已经看到我们的应用已经监听到配置的变化,并更新了日志级别

image

9、访问一下测试接口,可看到已经生效了。

image


这就实现了日志级别的热修改。

如我们可以完全依赖 Apollo 上的日志级别,就可以直接把 application.yml 的日志级别去掉了。

但是,我们需要修改一下我们的 LoggerConfig 类了,因为目前这个类的修改日志级别的功能是在监听到 Apollo 的配置变化了之后修改的,当我们的应用刚部署时是没有修改配置的,所以就没有触发执行这个设置日志级别的方法。

我们只需要增加一个init方法即可,并在其上添加@PostConstruct 注解即可,这个注解就是初始化时就调用该方法,和 init method 作用一样,这样在我们的系统启动时就可以从 Apollo 上拿到配置参数从而设置日志级别。

@PostConstruct //加载这个类时就执行
private void init(){
    log.info("初始化日志级别...");
    Set<String> keyNames = config.getPropertyNames();
    for (String key : keyNames) {
        if (StringUtils.containsIgnoreCase(key, LOGGER_TAG)) {
            String strLevel = config.getProperty(key, "info");
            LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
            String packageName = key.replace(LOGGER_TAG, "");
            loggingSystem.setLogLevel(packageName, level);
            log.info("init package logLevel==>key:{},package:{},level:{}", key, packageName, strLevel);
        }
    }
    log.info("初始化日志级别完毕");
}

启动

image

测试一下是不是warn级别

访问测试接口,可以看到正是 warn级别。

image


再去修改为info,客户端应用监听到修改并重新设置了日志级别

image

访问测试接口,已修改为info级别。

image

Apollo还支持,灰度发布、分环境、分集群配置,篇幅的原因关于Apollo应用的介绍今天就先到这里啦。

觉得有帮助的话请点赞吧❤~

热文:

Redis的各种数据类型到底能玩出什么花儿?

Redis分布式锁实战

什么是消息队列啊?

联合索引在B+树上的存储结构及数据查找方式

唠叨~

欢迎关注公众号,编程大道,之前整理的 redis 和 MQ 的知识点思维导图分享给大家

image
image

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