前言

在分布式系统中,我们的配置管理起来是很麻烦的一件事儿,特别是在如果没有配置中心的时候。在我们java开发中,一般而言会将敏感配置信息(如DB的user,password),数据库的地址,redis地址等等, 一系列配置信息和项目代码解耦,根据每个环境单独配置,这样一来是方便不需要改代码,也不需要在项目中冗余多份,二来是更加的安全。那么分离之后我们将面临的问题是怎么读取这个配置文件:

  1. 首先我们可以通过在不同环境的机器上的约定位置防止这个配置文件,应用读取即可,但是这个操作会随着节点的增多,配置的不断修改,你可能需要为此单独写一个工具,并且由于没有git版本控制回滚到上一个配置版本很困难。
  2. 其次我们也可以利用kubernetes提供的configmap机制来进行配置,然而门槛较高和kubernetes绑定在一起,也较为麻烦。
  3. 为解决这个问题,config-service应运而生,他的原理是通过git管理配置文件(这一点十分的优雅),然后通过一个http-server转接git服务为配置服务。

config service的架构图如下:

配置仓库

在编码之前我们需要先在我们的git server上建立一个项目存放我们的配置信息。配置文件我们采用properties格式,文件名规范如下格式:

1
{spring.application.name}-{profile}.properties

列如我有应用

1
2
3
spring:
  application:
    name: user-service

并有四个环境default,test,uat,prod四个环境,那么将会有四个对应的配置文件:

1
2
3
4
user-service-default.properties
user-service-test.properties
user-service-uat.properties
user-service-prod.properties

依赖

当前最新的spring boot版本为2.1.7 我们将他作为我们的parent。主要是方便三方jar的版本管理和maven插件的使用。

1
2
3
4
5
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
</parent>

加入spring cloud的pom依赖,方便spring cloud 提供的相关jar的版本管理。这里需要将此依赖放入dependencyManagement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

添加 spring cloud config server 依赖,并排除默认的tomcat服务器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

添加undertow依赖,由于配出了tomcat服务器这里我们使用undertow做为内嵌服务器。

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

由于我们需要注册到eureka中,所以还需要添加eureka-client的依赖。这里我们排除掉自带的jersey http client,spring cloud会自动激活一个RestTemplate作为调用client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>jersey-apache-client4</artifactId>
            <groupId>com.sun.jersey.contribs</groupId>
        </exclusion>
        <exclusion>
            <artifactId>jersey-client</artifactId>
            <groupId>com.sun.jersey</groupId>
        </exclusion>
        <exclusion>
            <artifactId>jersey-core</artifactId>
            <groupId>com.sun.jersey</groupId>
        </exclusion>
    </exclusions>
</dependency>

pom

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
    </parent>
    <groupId>io.qingmu</groupId>
    <artifactId>config-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>config-service</name>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>jersey-apache-client4</artifactId>
                    <groupId>com.sun.jersey.contribs</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>jersey-client</artifactId>
                    <groupId>com.sun.jersey</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>jersey-core</artifactId>
                    <groupId>com.sun.jersey</groupId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <configuration>
                    <imageName>
                        freemanliu/config-service:v1.0.0
                    </imageName>
                    <registryUrl></registryUrl>
                    <workdir>/work</workdir>
                    <rm>true</rm>
                    <env>
                        <TZ>Asia/Shanghai</TZ>
                        <JAVA_OPTS>
                            -XX:+UnlockExperimentalVMOptions \
                            -XX:+UseCGroupMemoryLimitForHeap \
                            -XX:MaxRAMFraction=2 \
                            -XX:CICompilerCount=8 \
                            -XX:ActiveProcessorCount=8 \
                            -XX:+UseG1GC \
                            -XX:+AggressiveOpts \
                            -XX:+UseFastAccessorMethods \
                            -XX:+UseStringDeduplication \
                            -XX:+UseCompressedOops \
                            -XX:+OptimizeStringConcat
                        </JAVA_OPTS>
                    </env>
                    <baseImage>freemanliu/openjre:8.212</baseImage>
                    <cmd>
                        /sbin/tini java ${JAVA_OPTS} -jar ${project.build.finalName}.jar
                    </cmd>
                    <!--是否推送image-->
                    <pushImage>true</pushImage>
                    <resources>
                        <resource>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                    <serverId>docker-hub</serverId>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

bootstrap.yml

由于我们需要注册到eureka注册中心,这里需要写入eureka相关配置信息。EUREKA_SERVER是为了方便打包好的jar替换eureka服务器的地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_SERVER:http://127.0.0.1:8761/eureka/}
  instance:
    prefer-ip-address: true
    # 表示 Eureka Client 向 Eureka Server 发送心跳的频率(默认 30 秒)
    # 如果在lease-expiration-duration-in-seconds指定的时间内未收到心跳,则移除该实例
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 10
    instance-id: ${spring.application.name}:${server.port}@${random.long(1000000,9999999)}

application.yml

undertow服务器设置

1
2
3
4
5
server:
  port: 8881
  undertow:
    io-threads: 2
    worker-threads: 50

spring cloud config 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

spring:
  application:
    name: config-service
  cloud:
    config:
      label: master
      server:
        git:
          uri: ${GIT_SERVER}
          username: ${GIT_USERNAME}
          password: ${GIT_PASSWORD}

完整的application.yaml如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
  port: 8881
  undertow:
    io-threads: 2
    worker-threads: 50
spring:
  application:
    name: config-service
  cloud:
    config:
      label: master
      server:
        git:
          uri: ${GIT_SERVER}
          username: ${GIT_USERNAME}
          password: ${GIT_PASSWORD}

management:
  endpoints:
    web:
      exposure:
        include: "*"

ConfigServiceApplication.java

编写配置中心的服务类,注解@EnableConfigServer表示启动config server,@EnableDiscoveryClient 启用服务发现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.qingmu.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServiceApplication.class, args);
    }
}

完整的项目结构如下:

到此编码完成,我们在idea中启动ConfigServiceApplication服务类。

访问eureka注册中心。可以看到config-service已经注册上来了。

打包发布镜像

1
mvn clean package 

部署到kubernetes

由于configserver是无状态的服务,所以这里我们采用deployment部署即可。这里我们部署两个副本集的yaml文件如下: 需要你相关的git信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# config-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: config-service
  namespace: default
spec:
  revisionHistoryLimit: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 25%
      maxSurge: 25%
  replicas: 2
  selector:
    matchLabels:
      app: config-service
  template:
    metadata:
      labels:
        app: config-service
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - config-service
              weight: 1
      containers:
        - name: config-service
          image: freemanliu/config-service:v1.0.0
          imagePullPolicy: IfNotPresent
          lifecycle:
            preStop:
              httpGet:
                port: 8881
                path: /spring/shutdown
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: 8881
            periodSeconds: 5
            timeoutSeconds: 10
            successThreshold: 1
            failureThreshold: 5
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8881
            periodSeconds: 5
            timeoutSeconds: 10
            successThreshold: 1
            failureThreshold: 5
          resources:
            requests:
              memory: 2Gi
            limits:
              memory: 2Gi
          ports:
            - containerPort: 8881
          env:
            - name: EUREKA_SERVER
              value: "http://eureka-0.eureka:8761/eureka/,http://eureka-1.eureka:8761/eureka/,http://eureka-2.eureka:8761/eureka/"
            - name: GIT_SERVER
              value: "补全你的git信息"
            - name: GIT_USERNAME
              value: "补全你的git信息"
            - name: GIT_PASSWORD
              value: "补全你的git信息"

使用kubectl部署到集群中。

1
kubectl apply -f config-service-deployment.yaml

查看部署状态

1
2
3
$ kubectl get pods -owide | grep config-service
config-service-67c7bb5bfb-g8wk2            1/1     Running   0          3m37s   172.224.6.245   node4               <none>           <none>
config-service-67c7bb5bfb-mzfgq            1/1     Running   0          4m12s   172.224.5.43    node3               <none>           <none>

查看eureka配置中心

我们来扩容一下config-service

1
2
$ kubectl scale --replicas=3 deploy/config-service
deployment.extensions/config-service scaled

再次查看eureka,可以发现服务副本 已经变成三份了。

我们来缩减一下config-service,再次查看eureka的时候,已经变成1了。

1
2
$ kubectl scale --replicas=1 deploy/config-service
deployment.extensions/config-service scaled

Client使用

在使用config-server配置中心时,调用者不需要关系configserver的具体地址,可通过eureka注册中心自动发现。 在 bootstrap.yml 添加如下配置,注意这里的serviceId 需要和配置中心中配置的spring.application.name保持一致。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
spring:
  cloud:
    config:
      label: master
      discovery:
        enabled: true
        serviceId: config-service
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_SERVER:http://eureka-k8s:8761/eureka/}
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 10
    instance-id: ${spring.application.name}:${server.port}@${random.long(1000000,9999999)}

Github 地址

完整的代码我上传到了github 你可以从这个https://github.com/qingmuio/config-service 获取到代码。