首页 > 代码库 > SpringCloud 分布式配置

SpringCloud 分布式配置

前言

在单体式应用中,我们通常的做法是将配置文件和代码放在一起,这没有什么不妥。当你的应用变得越来越大从而不得不进行服务化拆分的时候,会发现各种provider实例越来越多,修改某一项配置越来越麻烦,你常常不得不为修改某一项配置而重启某个服务所有的provider实例,甚至为了灰度上线需要更新部分provider的配置。这个时候有一套配置文件集中管理方案就变得十分重要,SpringCloudConfig和SpringCloudBus就是这种问题的解决方案之一,业界也有些知名的同类开源产品,比如百度的disconf。

相比较同类产品,SpringCloudConfig最大的优势是和Spring无缝集成,支持Spring里面EnvironmentPropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上是完全一致,结合SpringBoot可使你的项目有更加统一的标准(包括依赖版本和约束规范),避免了应为集成不同开软件源造成的依赖版本冲突。

一. 简介

SpringCloudConfig就是我们通常意义上的配置中心,把应用原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布能力。SpringCloudConfig分服务端和客户端,服务端负责将git(svn)中存储的配置文件发布成REST接口,客户端可以从服务端REST接口获取配置。但客户端并不能主动感知到配置的变化,从而主动去获取新的配置,这需要每个客户端通过POST方法触发各自的/refresh

SpringCloudBus通过一个轻量级消息代理连接分布式系统的节点。这可以用于广播状态更改(如配置更改)或其他管理指令。SpringCloudBus提供了通过POST方法访问的endpoint/bus/refresh,这个接口通常由git的钩子功能调用,用以通知各个SpringCloudConfig的客户端去服务端更新配置。

下图是SpringCloudConfig结合SpringCloudBus实现分布式配置的工作流

技术分享

注意:这是工作的流程图,实际的部署中SpringCloudBus并不是一个独立存在的服务,这里单列出来是为了能清晰的显示出工作流程。

二. SpringCloudConfig Server

SpringCloudConfig提供基于以下3个维度的配置管理:

  • 应用
    • 这个比较好理解,每个配置都是属于某一个应用的
  • 环境
    • 每个配置都是区分环境的,如dev, test, prod等
  • 版本
    • 这个可能是一般的配置中心所缺乏的,就是对同一份配置的不同版本管理
    • Spring Cloud Config提供版本的支持,也就是说对于一个应用的不同部署实例,可以从服务端获取到不同版本的配置,这对于一些特殊场景如:灰度发布,A/B测试等提供了很好的支持。

2.1 ConfigServer 配置

服务端要在pom中依赖spring-cloud-config-serverspring-cloud-starter-bus-kafka

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-monitor</artifactId>
</dependency>

application.properties中要配置仓库描述和消息队列地址,如果是私有项目还需要配置用户名密码

spring.cloud.config.server.git.uri=https://github.com/seagrape/SpringCloudConfig.git
spring.cloud.config.server.git.searchPaths=alan-config-repo
#spring.cloud.config.server.git.username=sihan2
#spring.cloud.config.server.git.password=MYPASSWORD

spring.cloud.stream.kafka.binder.brokers=10.79.96.52:9092
spring.cloud.stream.kafka.binder.zk-nodes=10.79.96.52:2182

启动类中要有@EnableConfigServer注解

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

2.2 ConfigServer 启动

Server端启动后,提供了如下的接口地址,参数说明

  • application:应用名
  • profile:环境
  • label:版本
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

NOTE:是配置文件的名字一般是有两部分组成,举个例子感受下,alan-provider-data-config-dev.properties,其中alan-provider-data-config是第一部分,这部分建议通过命名规则能让你知道是哪一个项目的配置,并且客户端要配置spring.cloud.config.name=alan-provider-data-config,才能让客户端知道自己要去服务端找哪一个配置文件。dev是第二部分,这部分用以区别配置文件应用的场景,是开发环境、测试环境或者生产环境

接口返回样例 curl http://localhost:8888/alan-provider-data-config/dev/master

{
    "name": "alan-provider-data-config",
    "profiles": ["dev"],
    "label": "master",
    "version": "78dce2b71473749a5298e11ef0d004ffa8d26bd1",
    "propertySources": [{
        "name": "https://github.com/seagrape/SpringCloudConfig.git/alan-config-repo/alan-provider-data-config-dev.properties",
        "source": {
            "spring.datasource.driver-class-name": "com.mysql.jdbc.Driver",
            "spring.datasource.username": "username",
            "spring.datasource.password": "password",
            "spring.datasource.url": "jdbc:mysql://DEVIP:PORT/DBNAME?characterEncoding=UTF-8"
        }
    }]
}

接口返回样例 curl http://localhost:8888/alan-provider-data-config-dev.properties

spring.datasource.driver-class-name: com.mysql.jdbc.Driver
spring.datasource.password: password
spring.datasource.url: jdbc:mysql://DEVIP:PORT/DBNAME?characterEncoding=UTF-8
spring.datasource.username: username

2.3 ConfigServer 文件系统

GIT做文件系统,文件都会被clone到本地文件系统中,默认这些文件会被放置到以config-repo-为前缀的系统临时目录,在 linux 上应该是 /tmp/config-repo-目录,如果你遇到了不可预知的问题出现,你可以通过设置spring.cloud.config.server.git.basedir参数值为非系统临时目录。

Config Server中,还有一种从本地classpath 或文件系统中加载配置文件的方式,可以通过spring.cloud.config.server.native.searchLocations进行设置。但如果你连GIT环境都没有,你还是回去喝奶吧......

三. SpringCloudConfig Client

3.1 ConfigClient 配置

客户端要在pom中依赖spring-cloud-starter-configspring-cloud-starter-bus-kafka

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

bootstrap.properties中配置配置中心地址和消息队列地址

1.特别注意 配置中心的地址一定要放在bootstrap.properties中,这个配置文件是由“根”上下文优先加载,可以保证程序启动之初就感知到远程配置中心的存在,并从远程获取配置,随后继续启动系统,这点十分重要。 2.而application.properties是由子上下文加载,加载顺序低于前者,如果配置中心地址放在这里,并且你远程配置了一些启动相关的必要参数,那么,你的程序很可能由于缺少参数而启动失败。 3.下面这段代码,最关键的是第一行,第二行如果不配置系统默认读取spring.application.name,第三行如果不配置,系统默认default,即:${spring.application.name}.properties 4.我们一般的做法是,在系统启动的时候,用命令行传入--spring.cloud.config.profile=dev|prod|test的方式,因为在启动的时候,我们是明确知道我要获取哪套配置的。 5.bus相关的配置(本例中用的kafka)完全可以放在远程。

spring.cloud.config.uri=http://127.0.0.1:${config.port:8888}
spring.cloud.config.name=alan-provider-data-config
spring.cloud.config.profile=${config.profile:dev}

spring.cloud.stream.kafka.binder.brokers=10.79.96.52:9092
spring.cloud.stream.kafka.binder.zk-nodes=10.79.96.52:2182

引用配置的类要加@RefreshScope注解

@SpringBootApplication
@RestController
@RefreshScope
public class ConfigClientApplication {

    @Value("${spring.datasource.username}")
    String name = "World";

    @RequestMapping("/")
    public String home() {
        System.out.println(name);
        return name;
    }   

    public static void main(String[] args) {

        SpringApplication.run(ConfigClientApplication.class, args);

    }
}

3.2 RefreshScope 注解

我们知道Spring原生提供了一些scope,如singleton,prototype,request等。 为了实现配置更新后,已经注入bean的值也能更新的目的,Spring Cloud提供了一个新的scope - RefreshScope。

Spring Cloud对RefreshScope的定义如下:

A Scope implementation that allows for beans to be refreshed dynamically at runtime (see refresh(String) and refreshAll()). If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a new instance is created.

所以,对于那些有注入值的bean,我们可以把它们标记为RefreshScope,这样当运行时发现有配置更新的时候,通过调用RefreshScope.refresh(beanName)或RefreshScope.refreshAll(),从而下次这些bean被使用时会被重新初始化,进而会被重新注入值,所以也就达到了更新的目的。

3.3 ConfigClient 启动顺序

ConfigClient最好要在ConfigServer之后启动,Spring加载配置文件是有顺序的,靠前的配置文件会覆盖靠后的配置文件中相同键的值,如果ConfigServer先启动可以保证ConfigClient将远程的配置文件加载到最前面,如果使用中没有注意到这一点,有可能导致你本地的配置文件先于远程的加载,导致本地的配置覆盖远程配置。当然,你也可以让本地配置和远程配置完全不重复,这样也可以避免键/值覆盖的问题。

后面会进一步说明这部分相关的知识点。

四. 背景知识

4.1 Spring中的Environment和PropertySource

  • Environment
    • Spring的ApplicationContext会包含一个Environment
    • Environment自身包含了很多个PropertySource
  • PropertySource
    • 属性源
    • 可以理解为很多个Key - Value的属性配置

在运行时的结构形如:

技术分享

需要注意的是,PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,那么在前面的property source优先。所以对上图的例子:

  • env.getProperty(“key1”) -> value1
  • env.getProperty(“key2”) -> value2
  • env.getProperty(“key3”) -> value4

在ConfigClient启动阶段,从ConfigServer获取配置,然后组装成PropertySource并插入到第一个,在随后的获取配置过程中,来自Config Server的配置和其它本地的配置对使用者而言是没有任何差别的,从而实现了无缝集成。

技术分享

需要注意的是,如果ConfigServer在ConfigClient之后启动,那远程配置将被加载到最后,这点在使用中需要特别注意。 下图是可以看到远程配置被优先加载

技术分享

4.2 /env

/env 是 spring-boot-starter-actuator提供的一个接口,GET方法调用可以查看系统环境变量,POST调用可以更改环境变量的值,并且通过这种方式修改的变量值具有最最高优先级。通过观察了解这个接口数据的变化,对学习SpringCloudConfig有帮助。

curl -X POST http://localhost:8080/env -d spring.datasource.username=wsh

如果要使上述修改生效,还需要利用/refresh重新载入所有@RefreshScope修饰的bean类。

curl -X POST http://localhost:8080/refresh

如果要重置这些修改

curl -X POST http://localhost:8080/env/reset

4.3 /refresh

使@RefreshScope修饰的bean类在下次调用时重新载入配置。

4.4 /bus/env

作用同/env,区别是会对所有节点生效

curl -X POST http://localhost:8888/bus/env -d spring.datasource.username=wsh

4.5 /bus/refresh

作用同/refresh,区别是会对所有节点生效

向消息broker发送一条信息,所有监听这个broker的应用会获得上述消息,并各自开始更新配置。每个SpringCloudBus的节点都有这个接口,并且这些接口是等效的,调用任何一个都可以起到相同的效果。但通常我们会调用在ConfigServer上配置的Bus,这样从流程上更符合人们的理解习惯。

curl -X POST http://localhost:8888/bus/refresh

五. SpringCloudBus

SpringCloudBus并不是一个独立的服务,他配置在每个ConfigClient,并通过消息队列使所有节点感知到状态变化。SpringCloudConfig没有直接集成bus的功能是有好处的,bus是可插拔设计并且目前并不完美,如果有个性需求完全可以用自己的方案替换bus,这个剥离bus成本几乎等于零。

目前Bus有两种实现spring-cloud-starter-bus-amqpspring-cloud-starter-bus-kafka,官网的例子是基于amqp,需要运行RabbitMQ,本文的例子用的是Kafka。

现在我们已经有能力在无需重启的情况下对应用程序配置进行更新了。

技术分享

六. 项目代码

SpringCloudConfig GitHub

七. 问题

  1. 当代码仓库访问速度慢的时候,读取配置的速度也会慢,要考虑代码仓库的可用性
  2. 为了增强Config Server 的高可靠性,需要按比例增加Config Server的数量
  3. [待完善,随时补充......]

八. 参考资料

  • Dive into Spring Cloud Config

九. 扩展阅读

  • SpringCloudBus官方指南中文翻译

SpringCloud 分布式配置