sprincloud源碼之eureka服務註冊

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代表此次註冊來源於集羣同步的註冊,代表此次註冊不要再進行集羣同步,避免無限註冊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章