第十五节 分布式配置中心
Spring Cloud Config是Spring Cloud团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密/解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。Spring Cloud Config实现了对服务端和客户端中环境变量和属性配置的抽象映射,所以它除了适用于Spring构建的应用程序之外,也可以在任何其他语言运行的应用程序中使用。由于Spring Cloud Config实现的配置中心默认采用Git来存储配置信息,所以使用Spring Cloud Config构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过Git客户端工具来方便的管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如:SVN仓库、本地化文件系统。
准备配置仓库
准备一个git仓库,可以在码云或Github上创建都可以。比如本文准备的仓库示例:
http://git.oschina.net/jiaozhiguang/config-repo-demo
· 假设我们读取配置中心的应用名为config-client,那么我们可以在git仓库中该项目的默认配置文件config-client.yml:
info: profile: default |
|
· 为了演示加载不同环境的配置,我们可以在git仓库中再创建一个针对dev环境的配置文件config-client-dev.yml:
info: profile: dev
构建配置中心 通过Spring Cloud Config来构建一个分布式配置中心非常简单,只需要三步: · 创建一个基础的Spring Boot工程,命名为:config-server-git,并在pom.xml中引入下面的依赖(省略了parent和dependencyManagement部分):
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.SR1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> · 创建Spring Boot的程序主类,并添加@EnableConfigServer注解,开启Spring Cloud Config的服务端功能。 @EnableConfigServer
public static void main(String[] args) { SpringApplication.run(ConfigServerGitApplication.class, args); } · 在application.yml中添加配置服务的基本信息以及Git仓库的相关信息,例如: spring: cloud: server:
到这里,使用一个通过Spring Cloud Config实现,并使用Git管理配置内容的分布式配置中心就已经完成了。我们可以将该应用先启动起来,确保没有错误产生,然后再尝试下面的内容。
如果我们的Git仓库需要权限访问,那么可以通过配置下面的两个属性来实现; spring.cloud.config.server.git.username:访问Git仓库的用户名 spring.cloud.config.server.git.password:访问Git仓库的用户密码
完成了这些准备工作之后,我们就可以通过浏览器、POSTMAN或CURL等工具直接来访问到我们的配置内容了。访问配置信息的URL与配置文件的映射关系如下: · /{application}/{profile}[/{label}] · /{application}-{profile}.yml · /{label}/{application}-{profile}.yml · /{application}-{profile}.properties · /{label}/{application}-{profile}.properties 上面的url会映射{application}-{profile}.properties对应的配置文件,其中{label}对应Git上不同的分支,默认为master。我们可以尝试构造不同的url来访问不同的配置内容,比如,要访问master分支,config-client应用的dev环境,就可以访问这个url:http://localhost:1201/config-client/dev/master,并获得如下返回:
我们可以看到该Json中返回了应用名:config-client,环境名:dev,分支名:master,以及default环境和dev环境的配置内容。
构建客户端 在完成了上述验证之后,确定配置服务中心已经正常运作,下面我们尝试如何在微服务应用中获取上述的配置信息。 · 创建一个Spring Boot应用,命名为config-client,并在pom.xml中引入下述依赖: <dependency>
<dependencyManagement>
· 创建bootstrap.yml配置,来指定获取配置文件的config-server-git位置,例如: spring: cloud: # profile: default label: master #server: · spring.application.name:对应配置文件规则中的{application}部分 · spring.cloud.config.profile:对应配置文件规则中的{profile}部分 · spring.cloud.config.label:对应配置文件规则中的{label}部分 · spring.cloud.config.uri:配置中心config-server的地址 这里需要格外注意:上面这些属性必须配置在bootstrap.properties中,这样config-server中的配置信息才能被正确加载。 在完成了上面你的代码编写之后,读者可以将config-server-git、config-client都启动起来,然后访问http://localhost:2001/info ,我们可以看到该端点将会返回从git仓库中获取的配置信息: { "profile": "default" } 另外,我们也可以修改config-client的profile为dev来观察加载配置的变化。 |
|
第十六节 配置中心服务化高可用
随着线上项目变的日益庞大,每个项目都散落着各种配置文件,如果采用分布式的开发模式,需要的配置文件随着服务增加而不断增多。某一个基础服务信息变更,都会引起一系列的更新和重启,运维苦不堪言也容易出错。配置中心便是解决此类问题的灵丹妙药。
市面上开源的配置中心有很多,BAT每家都出过,360的QConf、淘宝的diamond、百度的disconf都是解决这类问题。国外也有很多开源的配置中心Apache Commons Configuration、owner、cfg4j等等。这些开源的软件以及解决方案都很优秀,但是我最钟爱的却是Spring Cloud Config,因为它功能全面强大,可以无缝的和spring体系相结合,够方便够简单颜值高我喜欢。
Spring Cloud Config
在我们了解spring cloud config之前,我可以想想一个配置中心提供的核心功能应该有什么
· 提供服务端和客户端支持
· 集中管理各环境的配置文件
· 配置文件修改之后,可以快速的生效
· 可以进行版本管理
· 支持大的并发查询
· 支持各种语言
Spring Cloud Config可以完美的支持以上所有的需求。
Spring Cloud Config项目是一个解决分布式系统的配置管理方案。它包含了Client和Server两个部分,server提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client通过接口获取数据、并依据此数据初始化自己的应用。Spring cloud使用git或svn存放配置文件,默认情况下使用git
改造
客户端都是直接调用配置中心的server端来获取配置文件信息。这样就存在了一个问题,客户端和服务端的耦合性太高,如果server端要做集群,客户端只能通过原始的方式来路由,server端改变IP地址的时候,客户端也需要修改配置,不符合springcloud服务治理的理念。springcloud提供了这样的解决方案,我们只需要将server端当做一个服务注册到eureka中,client端去eureka中去获取配置中心server端的服务既可。
这篇文章我们基于配置中心git版本的内容来改造
server端改造
需要多引入spring-cloud-starter-eureka包,来添加对eureka的支持。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
增加了eureka注册中心的配置
eureka:
client:
serviceUrl:
defaultZone: http://localhost:2002/eureka/ # 注册中心eurka地址
启动类添加@EnableDiscoveryClient激活对配置中心的支持
这样server端的改造就完成了。先启动eureka注册中心,在启动server端,在浏览器中访问:http://localhost:2002/就会看到server端已经注册了到注册中心了。
客户端改造
1、添加依赖
需要多引入spring-cloud-starter-eureka包,来添加对eureka的支持。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
2、配置文件
主要是去掉了spring.cloud.config.uri直接指向server端地址的配置,增加了最后的三个配置:
· spring.cloud.config.discovery.enabled :开启Config服务发现支持
· spring.cloud.config.discovery.serviceId :指定server端的name,也就是server端spring.application.name的值
· eureka.client.serviceUrl.defaultZone :指向配置中心的地址
spring:
application:
name: config-client
cloud:
config:
#uri: http://localhost:1201/
# profile: default
profile: dev
label: master
discovery:
enabled: true
serviceId: config-server
#server:
# port: 2001
eureka:
client:
serviceUrl:
defaultZone: http://user:password@localhost:2002/eureka/ # 注册中心eurka地址
3、启动类
启动类添加@EnableDiscoveryClient激活对配置中心的支持为了模拟生产集群环境,我们改动server端的端口为8003,再启动一个server端来做服务的负载,提供高可用的server端支持。
在微服务架构中,我们将系统拆分成了一个个的服务单元,各单元应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,最终导致自身服务的瘫痪,进一步甚至出现故障的蔓延最终导致整个系统的瘫痪。如果这样的架构存在如此严重的隐患,那么相较传统架构就更加的不稳定。为了解决这样的问题,因此产生了断路器等一系列的服务保护机制。
针对上述问题,在Spring Cloud Hystrix中实现了线程隔离、断路器等一系列的服务保护功能。它也是基于Netflix的开源框架 Hystrix实现的,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。
动手试一试
在开始使用Spring Cloud Hystrix实现断路器之前,我们先拿之前实现的一些内容作为基础,其中包括:
· eureka-server工程:服务注册中心,端口:2002
· eureka-client工程:服务提供者,两个实例启动端口分别为2001
下面我们可以复制一下之前实现的一个服务消费者:eureka-consumer-ribbon,命名为eureka-consumer-ribbon-hystrix。下面我们开始对其进行改在:
第一步:pom.xml的dependencies节点中引入spring-cloud-starter-hystrix依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
第二步:在应用主类中使用@EnableCircuitBreaker或@EnableHystrix注解开启Hystrix的使用:
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(EurekaConsumerApplication.class, args);
}
}
注意:这里我们还可以使用Spring Cloud应用中的@SpringCloudApplication注解来修饰应用主类,该注解的具体定义如下所示。我们可以看到该注解中包含了上我们所引用的三个注解,这也意味着一个Spring Cloud标准应用应包含服务发现以及断路器。
第三步:改造服务消费方式,新增ConsumerService类,然后将在Controller中的逻辑迁移过去。最后,在为具体执行逻辑的函数上增加@HystrixCommand注解来指定服务降级方法,比如:
@RestController
public class DcController {
@Autowired
ConsumerService consumerService;
@GetMapping("/consumer")
public String dc() {
return consumerService.consumer();
}
@Service
class ConsumerService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "fallback")
public String consumer() {
return restTemplate.getForObject("http://eureka-client/dc", String.class);
}
public String fallback() {
return "fallback";
}
}
}
下面我们来验证一下上面Hystrix带来的一些基础功能。我们先把涉及的服务都启动起来,然后访问localhost:2101/consumer,此时可以获取正常的返回,比如:Services: [eureka-consumer-ribbon-hystrix, eureka-client]。
为了触发服务降级逻辑,我们可以将服务提供者eureka-client的逻辑加一些延迟,比如:
@RestController
public class DcController {
@Autowired
DiscoveryClient discoveryClient;
@GetMapping("/dc")
public String dc() throws InterruptedException {
Thread.sleep(5000L);
String services = "Services: " + discoveryClient.getServices();
System.out.println(services);
return services;
}
}
重启eureka-client之后,再尝试访问localhost:2101/consumer,此时我们将获得的返回结果为:fallback。我们从eureka-client的控制台中,可以看到服务提供方输出了原本要返回的结果,但是由于返回前延迟了5秒,而服务消费方触发了服务请求超时异常,服务消费者就通过HystrixCommand注解中指定的降级逻辑进行执行,因此该请求的结果返回了fallback。这样的机制,对自身服务起到了基础的保护,同时还为异常情况提供了自动的服务降级切换机制。
我们已经体验了如何使用@HystrixCommand来为一个依赖资源定义服务降级逻辑。实现方式非常简单,同时对于降级逻辑还能实现一些更加复杂的级联降级等策略。之前对于使用Hystrix来实现服务容错保护时,除了服务降级之外,我们还提到过线程隔离、断路器等功能。那么在本篇中我们就来具体说说线程隔离。
依赖隔离
“舱壁模式”对于熟悉Docker的读者一定不陌生,Docker通过“舱壁模式”实现进程的隔离,使得容器与容器之间不会互相影响。而Hystrix则使用该模式实现线程池的隔离,它会为每一个Hystrix命令创建一个独立的线程池,这样就算某个在Hystrix命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。
通过对依赖服务的线程池隔离实现,可以带来如下优势:
· 应用自身得到完全的保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的额其余部分。
· 可以有效的降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响到应用其他的请求。
· 当依赖的服务从失效恢复正常后,它的线程池会被清理并且能够马上恢复健康的服务,相比之下容器级别的清理恢复速度要慢得多。
· 当依赖的服务出现配置错误的时候,线程池会快速的反应出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时,我们可以在不影响应用功能的情况下通过实时的动态属性刷新(后续会通过Spring Cloud Config与Spring Cloud Bus的联合使用来介绍)来处理它。
· 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,此时线程池的监控指标信息会反映出这样的变化。同时,我们也可以通过实时动态刷新自身应用对依赖服务的阈值进行调整以适应依赖方的改变。
· 除了上面通过线程池隔离服务发挥的优点之外,每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步的访问。
总之,通过对依赖服务实现线程池隔离,让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。
虽然线程池隔离的方案带了如此多的好处,但是很多使用者可能会担心为每一个依赖服务都分配一个线程池是否会过多地增加系统的负载和开销。对于这一点,使用者不用过于担心,因为这些顾虑也是大部分工程师们会考虑到的,Netflix在设计Hystrix的时候,认为线程池上的开销相对于隔离所带来的好处是无法比拟的。同时,Netflix也针对线程池的开销做了相关的测试,以证明和打消Hystrix实现对性能影响的顾虑。
下图是Netflix Hystrix官方提供的一个Hystrix命令的性能监控,该命令以每秒60个请求的速度(QPS)向一个单服务实例进行访问,该服务实例每秒运行的线程数峰值为350个。
从图中的统计我们可以看到,使用线程池隔离与不使用线程池隔离的耗时差异如下表所示:
比较情况 | 未使用线程池隔离 | 使用了线程池隔离 | 耗时差距 |
中位数 | 2ms | 2ms | 2ms |
90百分位 | 5ms | 8ms | 3ms |
99百分位 | 28ms | 37ms | 9ms |
在99%的情况下,使用线程池隔离的延迟有9ms,对于大多数需求来说这样的消耗是微乎其微的,更何况为系统在稳定性和灵活性上所带来的巨大提升。虽然对于大部分的请求我们可以忽略线程池的额外开销,而对于小部分延迟本身就非常小的请求(可能只需要1ms),那么9ms的延迟开销还是非常昂贵的。实际上Hystrix也为此设计了另外的一个解决方案:信号量。
Hystrix中除了使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销要远比线程池的开销小得多,但是它不能设置超时和实现异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。在HystrixCommand和HystrixObservableCommand中2处支持信号量的使用:
· 命令执行:如果隔离策略参数execution.isolation.strategy设置为SEMAPHORE,Hystrix会使用信号量替代线程池来控制依赖服务的并发控制。
· 降级逻辑:当Hystrix尝试降级逻辑时候,它会在调用线程中使用信号量。
信号量的默认值为10,我们也可以通过动态刷新配置的方式来控制并发线程的数量。对于信号量大小的估算方法与线程池并发度的估算类似。仅访问内存数据的请求一般耗时在1ms以内,性能可以达到5000rps,这样级别的请求我们可以将信号量设置为1或者2,我们可以按此标准并根据实际请求耗时来设置信号量。
如何使用
说了那么多依赖隔离的好处,那么我们如何使用Hystrix来实现依赖隔离呢?其实,我们在上一篇定义服务降级的时候,已经自动的实现了依赖隔离。
在上一篇的示例中,我们使用了@HystrixCommand来将某个函数包装成了Hystrix命令,这里除了定义服务降级之外,Hystrix框架就会自动的为这个函数实现调用的隔离。所以,依赖隔离、服务降级在使用时候都是一体化实现的,这样利用Hystrix来实现服务容错保护在编程模型上就非常方便的,并且考虑更为全面。除了依赖隔离、服务降级之外,还有一个重要元素:断路器。我们将在下一篇做详细的介绍,这三个重要利器构成了Hystrix实现服务容错保护的强力组合拳。
断路器
断路器模式源于Martin Fowler的Circuit Breaker一文。“断路器”本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时的切断故障电路,防止发生过载、发热、甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),直接切断原来的主逻辑调用。但是,在Hystrix中的断路器除了切断主逻辑的功能之外,还有更复杂的逻辑,下面我们来看看它更为深层次的处理逻辑。
以在《Spring Cloud构建微服务架构:服务容错保护(Hystrix服务降级)》一文中实现的服务降级例子为示例,我们来说说断路器的工作原理。当我们把服务提供者eureka-client中加入了模拟的时间延迟之后,在服务消费端的服务降级逻辑因为hystrix命令调用依赖服务超时,触发了降级逻辑,但是即使这样,受限于Hystrix超时时间的问题,我们的调用依然很有可能产生堆积。
这个时候断路器就会发挥作用,那么断路器是在什么情况下开始起作用呢?这里涉及到断路器的三个重要参数:快照时间窗、请求总数下限、错误百分比下限。这个参数的作用分别是:
· 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
· 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格根据熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用此时不足20次,即时所有的请求都超时或其他原因失败,断路器都不会打开。
· 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,如果在这30次调用中,有16次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开。
那么当断路器打开之后会发生什么呢?我们先来说说断路器未打开之前,对于之前那个示例的情况就是每个请求都会在当hystrix超时之后返回fallback,每个请求时间延迟就是近似hystrix的超时时间,如果设置为5秒,那么每个请求就都要延迟5秒才会返回。当熔断器在10秒内发现请求总数超过20,并且错误百分比超过50%,这个时候熔断器打开。打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,那么原来的主逻辑要如何恢复呢?对于这一问题,hystrix也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
通过上面的一系列机制,hystrix的断路器实现了对依赖资源故障的端口、对降级策略的自动切换以及对主逻辑的自动恢复机制。这使得我们的微服务在依赖外部服务或资源的时候得到了非常好的保护,同时对于一些具备降级逻辑的业务需求可以实现自动化的切换与恢复,相比于设置开关由监控和运维来进行切换的传统实现方式显得更为智能和高效。