sprincloud源碼之eureka服務註冊
前言
eureka是netflix公司基於jersey框架寫的一個註冊中心,提供了服務註冊,服務下架,服務續約,集羣同步等功能。
jersey是一個類似於springmvc的框架,只不過mvc是基於servlet的,jersey是基於filter的,二者在使用上也很類似,mvc發請求被servlet攔截到反射調用controller,而jersey是被filter攔截到調用resource, 二者的原理基本一致。
sprincloud整合eureka
@EnableEurekaServer
@EnableEurekaServer----->>
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}
----->>>
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {
}
}
上面代碼的意思就是加了@EnableEurekaServer就會往spring容器注入一個Marker對象
EurekaServerAutoConfiguration
spring.factories裏的內容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
這是springboot自動配置的原理,springboot在初始化的時候會把spring.factories定義的類也初始化,可認爲是spi
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
public class EurekaServerAutoConfiguration
這裏我把其他註解省略了,可以看到EurekaServerAutoConfiguration初始化的前提是spring容器得有Marker類,
也就是說加上@EnableEurekaServer就是eureka服務註冊中心
jersey過濾器的初始化
springmvc自動配置的時候是在DispatcherServletAutoConfiguration初始化DispatcherServlet,
現在也是如此,在EurekaServerAutoConfiguration初始化一個filter,代碼如下
@Bean
public FilterRegistrationBean<?> jerseyFilterRegistration(
javax.ws.rs.core.Application eurekaJerseyApp) {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<Filter>();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
bean.setUrlPatterns(
Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*"));
return bean;
}
filter初始化後就可以攔截eureka-client的請求轉發給eureka-server處理了
eureka服務註冊
源碼在ApplicationResource的addInstance方法
ApplicationResource.addInstance()---->>
this.registry.register()---->>
InstanceRegistry.register(){
//springcloud發佈一個事件EurekaInstanceRegisteredEvent
handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
//調用父類的register
super.register(info, isReplication);
}
我們先暫停一下如上代碼講解,先看看InstanceRegistry的繼承關係圖:
InstanceRegistry是springcloud對於eureka的一個擴展,此類的唯一作用就是發佈事件
PeerAwareInstanceRegistrImpl是專門做集羣同步的
AbstractInstanceRegistry纔是做服務註冊的
這裏採取了一種責任鏈模式,先發布事件,在服務註冊最後集羣同步,每一個類只做一件事
下面我們接着代碼說:
InstanceRegistry.register 發佈事件
InstanceRegistry.register(){
//springcloud發佈一個事件EurekaInstanceRegisteredEvent
handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
//調用父類的register
super.register(info, isReplication);
}
PeerAwareInstanceRegistrImpl.register
public void register(InstanceInfo info, boolean isReplication) {
//leaseExpirationDurationInSeconds: 30 #Eureka服務器在接收到實例的最後一次發出的心跳後,需要等待多久纔可以將此實例刪除,默認爲90秒
int leaseDuration = 90;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
//服務註冊
super.register(info, leaseDuration, isReplication);
//集羣同步
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);
}
AbstractInstanceRegistry.register 服務註冊
在看服務註冊的代碼前我們先看this.registry和租債器Lease
this.registry
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();
registry的數據結構如上等價於<applicationName, Map<instanceId, Lease>>
如圖所示有三個order微服務,applicationName配置的都是order,說明三個微服務是屬於同一個微服務組,而三個微服務的id各不相同,代表這是同一個order組的三個微服務,這樣user微服務來調用order模塊就知道如何負載均衡了。
租債器Lease
註釋的代碼是我修正netflix的代碼
public class Lease<T> {
//Eureka服務器在接收到實例的最後一次發出的心跳後,需要等待多久纔可以將此實例刪除,默認爲90秒
public static final int DEFAULT_DURATION_IN_SECS = 90;
private T holder;//微服務對象
private long evictionTimestamp;//服務剔除時間戳
private long registrationTimestamp;//服務註冊時間戳
private long serviceUpTimestamp;//
private volatile long lastUpdateTimestamp;//最後一次更新的時間,註冊下架續約都是更新操作
private long duration;//duration ms後沒有心跳剔除服務
//private long expireTimestamp;//過期時間
/**
* @param r 微服務實例
* @param durationInSecs Eureka服務器在接收到實例的最後一次發出的心跳後,需要等待多久纔可以將此實例刪除
* eureka.instance.leaseExpirationDurationInSeconds=30s,後一次發出的心跳後,30s後還沒有新的心跳剔除服務
* eureka.instance.leaseRenewalIntervalInSeconds=10s心跳間隔時間
*/
public Lease(T r, int durationInSecs) {
this.holder = r;
this.registrationTimestamp = System.currentTimeMillis();//註冊時間
this.lastUpdateTimestamp = this.registrationTimestamp;//更新時間=註冊時間
this.duration = (long)(durationInSecs * 1000);//duration ms後沒有心跳剔除服務
}
//續期操作
public void renew() {
this.lastUpdateTimestamp = System.currentTimeMillis() + this.duration;
// this.lastUpdateTimestamp = System.currentTimeMillis();
// this.expireTimestamp = System.currentTimeMillis() + this.duration;
}
//服務下架/剔除操作
public void cancel() {
if (this.evictionTimestamp <= 0L) {
this.evictionTimestamp = System.currentTimeMillis();
}
}
public void serviceUp() {
if (this.serviceUpTimestamp == 0L) {
this.serviceUpTimestamp = System.currentTimeMillis();
}
}
public boolean isExpired() {
return this.isExpired(0L);
}
public boolean isExpired(long additionalLeaseMs) {
return this.evictionTimestamp > 0L || System.currentTimeMillis() > this.lastUpdateTimestamp + this.duration + additionalLeaseMs;
}
// public boolean isExpired(long additionalLeaseMs) {
// return this.evictionTimestamp > 0L || System.currentTimeMillis() > this.expireTimestamp + additionalLeaseMs;
// }
public static void main(String[] args) throws InterruptedException {
Lease lease = new Lease(new User(1,"lry"),5);
while(true){
System.out.println(lease.isExpired());
Thread.sleep(1000);
}
}
static class User{
private int id;
private String name;
public User(int id,String name){
this.id = id;
this.name = name;
}
}
}
上面的代碼netflix公司表示有bug,具體是如下兩個方法
//續期
//把最後更新時間改爲當前時間+duration
public void renew() {
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
//對象是否過期
//System.currentTimeMillis() > lastUpdateTimestamp + duration
//判斷是否過期的時候又加了一個duration 相當於是2*duration 後纔算是過期
public boolean isExpired(long additionalLeaseMs) {
return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}
bug描述
Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to +duration
more than what it should be, the expiry will actually be 2 * duration. This is a minor bug and
should only affect instances that ungracefully shutdown. Due to possible wide ranging impact
to existing usage, this will not be fixed.
大致意思就是renew多加了一個duration導致判斷isExpire實際是2*duration
服務註冊代碼
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
//Lease可以理解成一個微服務實例,它內部封裝一個微服務和各種時間來實現對對象的過期與否控制
//可以理解lease==InstanceInfo
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
//registrant.getAppName()微服務組內沒有微服務
if (gMap == null) {
ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
//嘗試通過id拿到一個微服務實例,一般情況下都拿不到,除非以下兩種情況
//情況一:有兩臺application name 和instance id都一樣微服務
//情況二:斷點調試,這種情況衝突是因爲客戶端註冊有超時重試機制
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
//如果拿到了
if (existingLease != null && (existingLease.getHolder() != null)) {
//已經存在的微服務實例最後修改時間
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
//要註冊的微服務實例最後修改時間
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
//如果已存的微服務時間>要註冊的(時間越大說明操作越新),用已存的覆蓋要註冊的
//即如果出現衝突的話拿最新的微服務實例
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
registrant = existingLease.getHolder();
}
}
//沒有拿到,一般都是進這裏
else {
synchronized (lock) {
//期待發送心跳的客戶端數量
if (this.expectedNumberOfClientsSendingRenews > 0) {
//要註冊進來了,期待值+1
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
//更新客戶端每分鐘發送心跳數的閾值,這個方法較重要
updateRenewsPerMinThreshold();
}
}
}
//不管如何都new一個Lease
Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
//如果if(gMap == null)都沒有進,說明微服務組內已經有微服務了,直接put(id,instance)即可
((Map)gMap).put(registrant.getId(), lease);
//最近註冊隊列添加此微服務
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
//標記微服務實例ADDED
registrant.setActionType(ActionType.ADDED);
//最近改變隊列添加此微服務,此隊列會保存近三分鐘有改動的微服務,用於增量更新
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
//設置最後更新的時間戳
registrant.setLastUpdatedTimestamp();
}
updateRenewsPerMinThreshold()
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
expectedNumberOfClientsSendingRenews:期待發送心跳的客戶端數量
ExpectedClientRenewalIntervalSeconds:期待客戶端發送心跳的間隔秒數
RenewalPercentThreshold:續期的百分比閾值85%
numberOfRenewsPerMinThreshold:客戶端每分鐘發送心跳數的閾值,如果server在一分鐘內沒有收到這麼多的心跳數就會觸發自我保護機制,以後博客應該會說到
舉個例子就明白了:
假設有10個客戶端,發送心跳間隔爲30s,那麼一分鐘如果全部正常的話server收到的心跳應該是20次,
如果server一分鐘收到的心跳<20*85%,即17個觸發自我保護機制
PeerAwareInstanceRegistrImpl.replicateToPeers 集羣同步
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info ,
InstanceStatus newStatus, boolean isReplication) {
//沒有同伴,或者是isReplication=true則不要集羣同步
//isReplication=true是這種情況,比如有兩臺eureka-server server1和server2組成了server集羣
//此時有一個eureka-client user要註冊到集羣中,首先user註冊到server1上(isReplication=false),
//server1會把user註冊進registry這個map中然後集羣同步將user註冊給server2,此時
//isReplication就是true,這樣server2註冊好user後發現isReplication是true就不會再做集羣同步,
//即不會給server1發送註冊user的指令,避免了無限註冊
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
//跳過自己
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
}
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus,
PeerEurekaNode node) {
switch (action) {
case Cancel:
node.cancel(appName, id);
break;
case Heartbeat:
InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
case Register:
node.register(info);
break;
case StatusUpdate:
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.statusUpdate(appName, id, newStatus, infoFromRegistry);
break;
case DeleteStatusOverride:
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.deleteStatusOverride(appName, id, infoFromRegistry);
break;
}
}
從集羣同步的代碼大致可以推斷出流程如上圖
集羣中有三個server,當user模塊註冊到集羣中的時候會先註冊到一臺server上,然後該server會遍歷其他集羣節點把user註冊上去
總結
1:@EnableEurekaServer注入一個Marker類,說明是一個註冊中心
2:EurekaServerAutoConfiguration注入一個filter,來攔截jersey請求轉發給resource
3:服務註冊,就是把信息存到一個ConcurrentHashMap<name,Map<id,Lease>>
4:對於註冊衝突拿最新的微服務實例
5:server每分鐘內收到的心跳數低於理應收到的85%就會觸發自我保護機制
6:Lease的renew bug, duration多加了一次,理應加一個expireTime表示過期時間
7:集羣同步:先註冊到一臺server,然後遍歷其他的集羣的其他server節點調用register註冊到其他server,
isReplication=true代表此次註冊來源於集羣同步的註冊,代表此次註冊不要再進行集羣同步,避免無限註冊