许久没有做过学习笔记了,之前挖的坑也要因为手头的事情暂告一段落。
由于技术上的需求,最近一段时间对微服务的架构学习了许多。粗略的学习过后决定一探其中源码,究其本质在于探索一些spirng springboot springcloud中的注解以及这些注解在配置项中起到的作用。偶尔也会涉及比如hystrix的设计模式的探讨与启发。
该博客不是科普微服务的博客,所以认为读者已经对微服务的基本架构和用法有所了解,这里只是对源码有一些自己的拙见。
众所周知,spring cloud以eureka组件作为其服务的注册中心。服务注册中心起到了注册服务,分发服务的作用。提供方和消费方都不知道其中的细节。这个组件的工作方式是怎么样的呢,让我们来窥视一下源代码:
我们从服务的客户端开始探究这个问题,道理很简单,从提供方注册服务、消费方获取服务的这个过程对我们来说比较直观。
/**
* Convenience annotation for clients to enable Eureka discovery configuration
* (specifically). Use this (optionally) in case you want discovery and know for sure that
* it is Eureka you want. All it does is turn on discovery and let the autoconfiguration
* find the eureka classes if they are available (i.e. you need Eureka on the classpath as
* well).
*
* @author Dave Syer
* @author Spencer Gibb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableDiscoveryClient
public @interface EnableEurekaClient {
}
这是@EnableEurekaClient注解的源代码 。
[充电站]@Inherited注解:使子类可以继承父类的注解。
在这个注释中没有实际的代码,但是通过阅读作者留下的说明,我们大概了解到了有了这个注解,就会去加载eureka客户端的配置并且尝试发现eureka服务。
[充电站]@EnalbeDiscoveryClient注解:允许应用发现服务,这个注解和@EnableEurekaClient的区别就在于,除了Eureka还有很多的注册中心可以使用,例如大名鼎鼎的zk(zookeeper)。
我们通过DiscoveryClient类去深入了解其中的机制:
/**
* The class that is instrumental for interactions with Eureka Server.
*
*
* Eureka Client is responsible for a) Registering the
* instance with Eureka Server b) Renewalof the lease with
* Eureka Server c) Cancellation of the lease from
* Eureka Server during shutdown
*
* d) Querying the list of services/instances registered with
* Eureka Server
*
*
*
* Eureka Client needs a configured list of Eureka Server
* {@link java.net.URL}s to talk to.These {@link java.net.URL}s are typically amazon elastic eips
* which do not change. All of the functions defined above fail-over to other
* {@link java.net.URL}s specified in the list in the case of failure.
*
*
* @author Karthik Ranganathan, Greg Kim
* @author Spencer Gibb
*
*/
秉承一贯的特点,我们先看作者留下的说明文档:这个类是为了与eureka服务端做交互的。这个类起如下的作用:向服务中心注册实例、与服务中心续约、服务崩溃时解除与服务中心的注册关系、查询在服务中心已注册的可用服务清单。客户端会从配置文件中找到服务中心的地址,当且仅当所有的服务请求都失败了,才会认为这一次的请求是失败的。
从说明文档里已经很明白的了解到了类起到的作用。
接下来我们要去了解更为细节的东西,首先是获取配置的方式,至于配置的设计,在这里先不多说。(有部分方法已经被标注为过时方法,可以通过@link寻找到EndpointUtils类的相同方法)
public static List getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
List orderedUrls = new ArrayList();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.
getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[1];
availZones[0] = DEFAULT_ZONE;
}
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
List serviceUrls = clientConfig.
getEurekaServerServiceUrls(availZones[myZoneOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
int currentOffset = myZoneOffset ==
(availZones.length - 1) ? 0 : (myZoneOffset + 1);
while (currentOffset != myZoneOffset) {
serviceUrls = clientConfig.
getEurekaServerServiceUrls(availZones[currentOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
if (currentOffset == (availZones.length - 1)) {
currentOffset = 0;
} else {
currentOffset++;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl
specified!");
}
return orderedUrls;
}
一上来就有一个region的概念,从config对象中获取了region的信息,如果没有设置region,将会采用默认的region,也就是defaultRegion,从方法的返回值可以知道,region应该只能配置一个。
接下来就是一个zone的概念,从方法的返回值也可以知道zone是可以有多个的,并且是在这个region下的若干zone,如果没有配置,将会采用默认值也就是defaultZone。注意这里,如果设置了prefersamezone,将会有限寻找到相同的zone的偏移量,并且返回的有序列表中,该zone下的url将会在列表前段。
之后的事情就比较简单,遍历所有的zone,并且获得url,将其放到有序列表中。
看这个方法:
public List getEurekaServerServiceUrls(String myZone) {
String serviceUrls = configInstance.getStringProperty(
namespace + CONFIG_EUREKA_SERVER_SERVICE_URL_PREFIX + "." + myZone,
null).get();
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = configInstance.getStringProperty(
namespace + CONFIG_EUREKA_SERVER_SERVICE_URL_PREFIX + ".default",
null).get();
}
if (serviceUrls != null) {
return Arrays.asList(serviceUrls.split(","));
}
return new ArrayList();
}
配置文件中的配置,本质上由来于它解析的方式,eureka.client.serviceUrls.defaultZone就是这样的。
因此我们也可以配置自己的region、zone以及相应的url:
eureka:
client:
register-with-eureka: true
fetch-registry: true
prefer-same-zone-eureka: true
region: beijing
availability-zones:
beijing: zone-1,zone-2
service-url:
zone-1: http://localhost:30000/eureka/
zone-2: http://localhost:30001/eureka/
在我们使用Ribbon实现服务调用时,对于zone的设置可以在负载均衡时实现区域亲和:Ribbon的默认策略会优先访问和客户端在同一个zone里的服务端实例,只有当同一个zone里没有可用服务实例才会去访问其他zone。在这点上,我们可以设计出对区域性故障的容灾集群。
再来看看服务的注册:
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
分析第一段:clientConfig.shouldFetchRegistry()条件告诉我们,只有我们设置了registry-with-eureka=true才会触发这一段内容,其中涉及了两个参数值:registryFetchIntervalSeconds是从eureka服务端获取注册信息的时间间隔,默认30s。
cacheRefreshExecutorExponentialBackOffBound是缓存刷新重试延迟时间的最大乘数,不是特别重要。
那么这个计划将会在每30秒去做一次服务的注册,有注册就会有续约和获取,我们先看续约:
boolean renew() {
EurekaHttpResponse httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
if (httpResponse.getStatusCode() == 404) {
REREGISTER_COUNTER.increment();
logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
return register();
}
return httpResponse.getStatusCode() == 200;
} catch (Throwable e) {
logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
return false;
}
}
可以发现,本质上是一个http请求,像中心发送请求,其实服务的获取、注册等操作也是以rest请求做成的。
本章分析完后,将会自己制作一个简单的类eureka服务注册机制,以进一步体会其中的设计和参数必要性。