传统应用配置问题
- 静态配置
- 传统应用的配置,都是静态配置,写在配置文件中,运行时无法动态修改,如果修改之后,就需要重启应用
- 配置格式不统一
- 开发人员习惯不同,使用XML、properties、DB存储配置
- 易引起事故
- 在上线时,有时会忘记修改配置文件,将测试环境的变量变成线上
- 配置修改麻烦,周期长
- 在部署多个服务器时,修改配置费时费力
- 缺少安全审计和版本控制
- 没有版本控制,在出现问题时,无法及时进行回滚。
配置中心解决方法
- 静态配置
- 集中式配置,所有的配置信息都保存到配置中心
- 配置格式不统一
- 配置中心统一管理格式,开发人员不用关心格式,通过界面管理
- 易引发事故
- 环境隔离,不同的环境使用不同的配置,互不干扰
- 配置修改之后,可以生效
- 配置麻烦,周期长
- 配置集中一次修改,实时通知到所有客户端
- 缺少安全审计和版本控制
- 所有修改都有历史记录,方便查找修改人和时间
- 可以按需退回到历史版本
配置基本概念
配置定义
- 可独立于程序的可配变量
- 同一份程序在不同配置下会有不同行为
- 应用场景:连接字符串、应用配置、业务配置
配置形态
- 程序内部硬编码(坚决反对)
- 配置文件:将配置写入xml、properties文件中
- 环境变量:根据环境不同,传递到程序中
- 启动参数:将参数传递到程序中
- 基于数据库:将配置写入到数据库
配置治理
- 权限控制和审计:要保留用户的操作记录和权限
- 不同环境、集群配置管理:根据不同环境,获取不同的配置
- 框架类组件配置管理:获取配置,是否开启功能
- 灰度发布:进行公测,环境隔离
配置分类和场景
动态配置
- 应用配置
- 请求超时、线程池、队列、缓存、数据库连接池的容量、日志级别、限流熔断阈值、黑白名单
- 功能开关
- 蓝绿发布、灰度开关、降级开关、HA高可用开关、DB迁移
- 业务配置
- 促销规律、贷款额度、利率等业务参数、A/B测试
静态配置
- 安全配置
用户名、密码、令牌、许可证户等- 环境相关
- 数据库/中间件/其他服务的连接字符串
开关驱动开发原理
优点
- 新功能和代码发布分离,减轻发布风险
- 迭代速度快,快速创新实现
- 可定时高级A/B测试
- 相对复杂发布系统,投入成本相对低
- 没有分支开发的合并冲突问题
缺点
- 代码侵入、需要定期清理
- 需要开关配置中心配合
- 需要DevOps文化和流程配合
Apollo 功能亮点
- 统一管理不同环境、不同集群的配置
- Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。
- 同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等
- 通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖
- 配置修改实时生效(热发布)
- 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序。
- 版本发布管理
- 所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。
- 灰度发布
- 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。
- 权限管理、发布审核、操作审计
- 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
- 所有的操作都有审计日志,可以方便的追踪问题。
- 客户端配置信息监控
- 可以方便的看到配置在被哪些实例使用
- 提供Java和.Net原生客户端
- 提供了Java和.Net的原生客户端,方便应用集成
- 支持Spring Placeholder,Annotation和Spring Boot的ConfigurationProperties,方便应用使用
- 同时提供了Http接口,非Java和.Net应用也可以方便的使用
- 提供开放平台API
- Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。
- 不过Apollo出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够保存。
- 配置可能会有比较复杂的格式,如xml, json,需要对格式做校验。
- 还有一些使用方如DAL,不仅有特定的格式,而且对输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配。
- 对于这类应用,Apollo支持应用方通过开放接口在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
- 部署简单
- 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
- 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来
- Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数
Apollo 架构
核心概念
概念 | 应用 | 环境 | 集群 | 配置项 |
---|---|---|---|---|
解释 | 应用程序的唯一标识,存储位置为 /META-INF/app.properties |
功能不同,不同环境,存储位置为 /opt/settings/server.properties |
一个应用不同实例的分组,不同集群, 拥有不同的配置 |
支持xml、properties、json格式 |
注意:配置项存放位置:(1)私有配置env+app+cluster+namespace+item_key;(2)共有配置:env+cluster+namespace+item_key。
命名空间
一个应用不同配置的分组,默认的命名空间为application。
命名空间类型
- 私有类型:只能被所属应用获取
- 共有类型:环境变量必须全局唯一
- 关联类型:私有继承公开并覆盖、定制公共组建配置场景
基础模型
基础模型的流程:
1.用户登陆Apollo系统之后,在配置中心修改配置并发布
2.在用户发布配置之后,会同知道Apollo客户端更新配置
3.Apollo客户端会定时从配置中心拉取最新的配置、更新本地配置并通知到应用
架构模块介绍
Config Server
- 提供配置获取接口
- 提供配置更新推送接口(基于Http long polling)
- 服务端使用 Spring DeferredResult实现异步化,从而大大增加长连接数量
- 目前使用的tomcat embed默认配置是最多10000个连接
- 接口服务对象为Apollo客户端
Admin Service
- 提供配置管理接口
- 提供配置修改、发布等接口
- 接口服务对象为Portal
Meta Server
- Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port)
- Client通过域名访问Meta Server获取Config Service服务列表(IP+Port)
- Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client
- 增设一个Meta Server的角色主要是为了封装服务发现的细节,对Portal和Client而言,永远通过一个Http接口获取Admin Service和Config Service的服务信息,而不需要关心背后实际的服务注册和发现组件
- Meta Server只是一个逻辑角色,在部署时和Config Service是在一个JVM进程中的,所以IP、端口和Config Service一致
Eureka
- 基于Eureka和Spring Cloud Netflix提供服务注册和发现
- Config Service和Admin Service会向Eureka注册服务,并保持心跳
- 为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的(通过Spring Cloud Netflix)
Portal
- 提供Web界面供用户管理配置
- 通过Meta Server获取Admin Service服务列表(IP+Port),通过IP+Port访问服务
- 在Portal侧做load balance、错误重试
Client
- Apollo提供的客户端程序,为应用提供配置获取、实时更新等功能
- 通过Meta Server获取Config Service服务列表(IP+Port),通过IP+Port访问服务
- 在Client侧做load balance、错误重试
框架如何保证实时更新
用户在Portal发布配置之后,AdminService会将配置写入到ReleaseMessage表中,ConfigServer会定期扫描ReleaeMessage表,会实时通知客户端。(长连接,通过Http Long Polling实现)
客户端还会定时从Apollo配置中心拉取配置(推拉结合,Spring DeferredResult、定期拉配置)
高可用
1.Config/Admin/Portal 都是用负载均衡技术,来保证高可用。
2.服务端的实时推送技术、客户端定时拉取配置,保证及时性,如果拉取不到,客户端使用本地缓存的配置,保证高可用。
为什么使用Eureka
- 它提供了完整的Service Registry和Service Discovery实现
+首先是提供了完整的实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心。- 和Spring Cloud无缝集成
- 同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,使用起来非常方便。
- Eureka支持在应用自身的容器中启动,也就是说应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。
+为了提高配置中心的可用性和降低部署复杂度,我们需要尽可能地减少外部依赖。- 开源
+由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题
Apollo 于Spring Boot 结合
pom依赖
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.8.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
yml文件
app:
id: SampleApp
apollo:
bootstrap:
enabled: true
meta: https://localhost:8080
function:
switch:
open: false
current:
version: v1
server:
port: 9000
添加Apollo配置
package com.edu.apollo.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
@Data
public class DemoGlobalConfig {
@Value("${current.version}")
public String currentVersion;
@Value("${function.switch.open}")
public boolean switchFlag;
}
package com.edu.apollo.config;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableApolloConfig
public class DemoApolloConfig {
@Bean
public DemoGlobalConfig globalConfig() {
return new DemoGlobalConfig();
}
}
相对应的Controller
package com.edu.apollo.controller;
import com.edu.apollo.config.DemoGlobalConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class ApolloConfigController {
@Autowired
DemoGlobalConfig demoGlobalConfig;
@Autowired
Environment environment;
@CrossOrigin
@RequestMapping(value = "/config", method = RequestMethod.GET)
public ResponseEntity<String> getApolloConfig() {
String currentVersion = demoGlobalConfig.getCurrentVersion();
return new ResponseEntity<String>(currentVersion,HttpStatus.OK);
}
//check whether the parameter is valid or not
@CrossOrigin
@RequestMapping(value = "/parameter", method = RequestMethod.GET)
public ResponseEntity<String> getApolloConfigByParameter(@RequestParam String parameter) {
String result = environment.getProperty(parameter);
if (result == null){
result = "defaultValue";
}
return new ResponseEntity<String>(result,HttpStatus.OK);
}
}
添加开关
package com.edu.apollo.service;
public interface SwitchService {
public String getSwitchData();
}
package com.edu.apollo.service.impl;
import com.edu.apollo.config.DemoGlobalConfig;
import com.edu.apollo.service.SwitchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SwitchServiceImpl implements SwitchService {
@Autowired
private DemoGlobalConfig demoGlobalConfig;
@Override
public String getSwitchData() {
if (demoGlobalConfig.isSwitchFlag()) {
return "new";
} else {
return "old";
}
}
}
package com.edu.apollo.controller;
import com.edu.apollo.service.SwitchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SwitchController {
@Autowired
private SwitchService switchService;
@GetMapping(value = "/switch")
public ResponseEntity<String> getSwitchOpen(){
return new ResponseEntity<String>(switchService.getSwitchData(),HttpStatus.OK);
}
}
在启动程序的时候,需要添加环境变量:-Dapollo.configService=http://localhost:8080
Apollo启动过程
start config service for client
start admin service for portal 8090
start portal service 8070
eureka 8080
apollo cache:/opt/data/{appid}/config-cache 要有读写权限
运行步骤
校验是否启动成功
在程序启动之后,需要进行校验: http://localhost:9000/config
返回值为v1
校验Apollo是否生效
在Apollo界面上更改配置为current.version=v2
在程序启动之后,需要进行校验: http://localhost:9000/config
返回值为v2
开关功能校验
在程序启动之后,需要进行校验: http://localhost:9000/switch
返回值为old
在Apollo界面上更改配置为function.switch.open=true
在程序启动之后,需要进行校验: http://localhost:9000/config
返回值为v2