服务治理是微服务架构中的核心模块,主要用来实现各个微服务的自动化注册和发现。随着业务的增长和微服务实例的增长,服务治理可以大大减少手动配置的工作和手动配置错误,并且结合其他中间件实现服务的负载均衡。一个微服务治理框架一般包含三个核心要素:
- 服务注册中心:提供服务注册和发现的功能
- 服务提供者:服务提供者向服务注册中心注册自己的信息,如服务名、IP地址、端口号等信息
- 服务消费者:服务消费者从服务注册中心获取服务列表,从而消费者可以知道去何处调用其所需要的服务
在实际场景中,一般服务注册中心是单独的微服务(在高可用环境下会是集群),而服务提供者可能也是服务消费者,服务消费者也可能是服务提供者。
Spring Cloud Eurek是Spring Cloud社区提供的微服务中间件,使用Netflix Eureka来实现服务的注册和发现。其包含两部分:
- Eureka服务端(Eureka Server),即服务注册中心,支持集群式部署
- Eureka客户端(Eureka Client),主要处理服务的注册和发现,周期性的向Eureka服务端发送心跳信息来更新它的服务租约,当服务下线时通知Eureka服务端及时下线服务
1. Eureka服务治理架构
在实际使用中,Eureka的服务治理架构一般如下图所示:
从图可以看出在这个架构中,可以看到:
- 有2个角色,即Eureka Server和Eureka Client
- 每个区域有一个Eureka集群,并且每个区域至少有一个eureka服务器可以处理区域故障,以防服务器瘫痪
- Eureka Server间相互同步注册信息
- Eureka Client分为Applicaton Service和Application Client,即服务提供者何服务消费者
- Applicaton Service向Eureka Server注册、续约和下线和获得注册表信息
- Application Client获得注册信息并调用服务
在分布式环境下需要考虑单点故障问题,因此需要在生产环境下为各个服务部署多个服务结点,以提高服务的可用性, 对Eureka服务注册中心同样如此。
接下来介绍一下多个Eureka服务注册中心的的关键配置,基于Spring Boot搭建Eureka服务的步骤请参考Eureka帮助文档。当工程初始化好后,新建两个配置文件applicaiton-peer1.yml和application-peer2.yml,内容分别是:
application-peer1.yml
spring:
application:
name: eureka-server
server:
port: 9000
eureka:
instance:
hostname: peer1
client:
service-url:
defaultZone: http://peer2:9001/eureka
aplication-peer2.yml
spring:
application:
name: eureka-server
server:
port: 9001
eureka:
instance:
hostname: peer2
client:
service-url:
defaultZone: http://peer1:9000/eureka
并在/etc/host中添加如下配置:
127.0.0.1 peer1
127.0.0.1 peer2
然后执行如下Maven命令启动两个Eureka注册服务中心:
mvn spring-boot:run -Dspring-boot.run.profiles=peer1&
mvn spring-boot:run -Dspring-boot.run.profiles=peer2&
在浏览器访问http://peer1:9000,可以看到如下结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x7HL0OX4-1582968212541)(/Users/liulijun/Library/Application Support/typora-user-images/image-20200225131016057.png)]
服务消费者和服务提供者需要将eureka.client.service-url.defaultZone修改为
:
eureka:
client:
service-url:
defaultZone: http://peer1:9000/eureka, http://peer2:9001/eureka
重启服务提供者和服务消费者,此时就可以在peer1和peer2中都能看到服务消费者和服务提供者的信息。
通过配置多个Eureka注册中心可以看到只要有一个Eureka服务注册中心可用,服务消费者和服务提供者就可以通过Eureka服务注册中心找到其要消费的服务,从而达到了Eureka服务高可用。
接下来将进入Eureka源码的分析,由于Eureka的源码包含服务端和客户端模块,文章将分开分析并先从Eureka客户端代码开始分析。
2. Eureka客户端装配
在应用中一般通过@EnableDiscoveryClient注解开启Eureka客户端功能,从注释可以看到该注解的作用是开启DiscoveryClient
实现,但是深入到@EnableDiscoveryClient源码中去看,该注解并没有做什么初始化操作。
尝试在应用中去掉@EnableDiscoveryClient注解,重启应用后发现服务依然注册到了Eureka注册中心,由此说明Eureka客户端的装配并不是由@EnableDiscoveryClient注解触发的,那么Eureka客户端是在哪里装配的呢?
在Spring中有一种类似于Java SPI的加载机制,它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。因此,看下spring-cloud-netflix-eureka-client下的spring.factories文件,发现其中有如下配置:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
可以判断Eureka客户端是在EurekaDiscoveryClientConfigServiceBootstrapConfiguration
类中装配的。看下该类的源码:
@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
@Import({
(1)EurekaDiscoveryClientConfiguration.class, // this emulates
// @EnableDiscoveryClient, the import
// selector doesn't run before the
// bootstrap phase
(2)EurekaClientAutoConfiguration.class,
EurekaReactiveDiscoveryClientConfiguration.class,
ReactiveCommonsClientAutoConfiguration.class })
public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration {
}
(1) EurekaDiscoveryClientConfiguration
注入了三个bean
discoveryClient
:该Bean是最Eureka的重点,实现类为EurekaDiscoveryClient
eurekaHealthCheckHandler
:Eureka健康检查的处理类
(2) EurekaClientAutoConfiguration
是最复杂的装配类,其依赖的装配非常多,需要用到时再讲解
EurekaReactiveDiscoveryClientConfiguration
和ReactiveCommonsClientAutoConfiguration
用于Spring响应式编程,这里不做介绍。
3. Eureka客户端启动流程
上一节中说了步骤(1)中注册的discoveryClient
是整个Eureka的重点,因此接下来将重点分析这个Bean,首先看一下该Bean的注册代码,位于EurekaDiscoveryClientConfiguration
类中:
@Bean
@ConditionalOnMissingBean
(3)public EurekaDiscoveryClient discoveryClient(EurekaClient client,
EurekaClientConfig clientConfig) {
return new EurekaDiscoveryClient(client, clientConfig);
}
(3)可以看到注册的discoverClient
为EurekaDiscoveryClient
类的实例,其需要另外两个Bean进行初始化,即类型分别为EurekaClient
和EurekaClientConfig
的Bean。这两个Bean是在上一节步骤(2)中介绍的EurekaClientAutoConfiguration
类中注册的,代码如下:
@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class,
search = SearchStrategy.CURRENT)
(4)public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
EurekaClientConfigBean client = new EurekaClientConfigBean();
......
return client;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnRefreshScope
protected static class RefreshableEurekaClientConfiguration {
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class,
search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config, EurekaInstanceConfig instance,
@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
......
(5)CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
config, this.optionalArgs, this.context);
cloudEurekaClient.registerHealthCheck(healthCheckHandler);
return cloudEurekaClient;
}
......
}
(4) 注册类型为EurekaClientConfig
的Bean,该类是EurekaClientConfig
接口的实现类,其对应配置文件中以**eureka.client
**开头的配置
(5) 注册类型为CloudEurekaClient
的Bean
通过步骤(3)-(5),此便可以梳理出EurekaDiscoveryClient
类的关系,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JnwvgE1J-1582968212542)(/Users/liulijun/Library/Application Support/typora-user-images/image-20200227132851820.png)]
图分为三个部分,分别用不同的颜色标记出来:
- 左边是Spring Cloud定义的接口,定义了用来发现服务的常用方法,Spring Cloud通过该接口可以方便的切换不同的服务治理框架
- 右边的所有接口和类都是Netflix Eureka开源包的实现,主要定义了针对Eureka的服务发现的抽象方法
- 中间则是对Netflix Eureka服务的的封装并实现了Spring Cloud的
DiscoveryClient
接口
沿着CloudEurekaClient
类的继承关系看,可以发现在Netflix Eureka开源包中DiscoveryClient
是服务发现的主要实现类,从基注释中知道其主要有四个功能:
- 向Eureka Server(即服务注册中心)注册服务实例(服务提供者)
- 向Eureka Server续约服务
- 服务关闭时,向Eureka Server取消租约
- 查询Eureka Server中注册的服务列表
接着进入DiscoveryClient
的构造函数(只摘取比较重要的操作):
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
......
fetchRegistryGeneration = new AtomicLong(0);
remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
//<!--------------------(6)--------------------------
if (config.shouldFetchRegistry()) {
this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
if (config.shouldRegisterWithEureka()) {
this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
// -------------------------------------------------!>
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
(7)if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
......
return; // no need to setup up an network tasks and we are done
}
try {
// default size of 2 - 1 each for heartbeat and cacheRefresh
(8)scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
(9)heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
(10)cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
//<!--------------------(11)--------------------------
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);
//----------------------(11)--------------------------!>
(12)if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
// call and execute the pre registration handler before all background tasks (inc registration) is started
(13)if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
(14)if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
if (!register() ) {
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
(15)initScheduledTasks();
try {
(16) Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
}
(6) 初始化监视器
(7) 当前实例不需要注册到Eureka Server和从Eureka Server摘取服务列表时,构造方法到此结束
(8) 初始化调度器,该调度器用于执行心跳线程和缓存刷新线程
(9) 初始化心跳线程池(用于向服务注册中心续约)
(10) 初始化缓存刷新线程池(用于从服务注册中心摘取服务列表)
(11) 初始化EurekaTransport,该类封装的属性用于Eureka Client和Eureka Server通信
(12) 从Eureka Server拉取服务列表,fetchRegistry
是第一次拉取注册信息,如果拉取不成功的话则执行fetchRegistryFromBackup
从备份注册中心获取
(13) 注册之前的扩展点,转为为null
(14) 向Eureka Server发起注册,由于clientConfig.shouldEnforceRegistrationAtInit()
默认为false,因此不执行该注册逻辑,而实际的服务注册是在步骤(15)完成的
(15) 初始化定时任务和向Eureka Server注册服务
(16) 向监视器注册该类
以下就是Eureka客户端的装配和启动流程,Eureka服务注册的流程将在下一篇文章中分析。