1. 程式人生 > >SpringCloud--Eureka服務註冊和發現

SpringCloud--Eureka服務註冊和發現

Eureka是SpringCloud家族中的一個元件,因為它的有服務註冊和發現的機制,所以很適合用於做註冊中心。Eureka有服務端和客戶端,註冊中心作為服務端,我們提供的服務作為客戶端註冊到服務端上,由Eureka統一管理。

作為註冊中心,它內部執行機制是什麼樣的?下面我就帶著下面這些問題來學習Eureka。
1.如何去開發一個整合spring cloud eureka程式?
2.服務提供者怎麼註冊到服務中心的?
3.服務中心怎麼接收註冊請求?
4.服務中心怎麼儲存?
5.服務中心自身是怎麼實現高可用的?
6.服務叢集之間怎麼同步資訊?如何去重?
7.服務中心如何檢查服務提供者是否正常?
8.服務提供者如果下架服務?

1.如何去開發一個整合spring cloud eureka程式?

下面就開發一個偽叢集(在單機上)的Eureka程式:
服務端pom.xml檔案主要配置:

 <!-- spring boot 封裝spring
    starter封裝、自動配置autoconfiguration
    -->
    <parent>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-parent</artifactId
>
<version>Edgware.SR2</version> <relativePath /> </parent> <!-- 後面引用不用加版本號 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId
>
spring-cloud-dependencies</artifactId> <version>Edgware.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- spring-boot-starter-web web專案,整合容器tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- spring-boot-starter-actuator 管理工具/web 檢視堆疊,動態重新整理配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- cloud eureka元件 註冊中心 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies>

bootstrap.yml檔案:

# 應用名稱
spring:
  application:
    name: eureka-server

application.yml檔案:

# 上下文初始化載入
info:
  name: Eureka server
  contact: wenthkim

spring:
  profiles:
    active: eureka1
spring:
  profiles: eureka1
server:
  port: 8761
eureka:
  client:
    # 是否註冊到eurekaserver 
    registerWithEureka: false
    # 是否拉取資訊
    fetchRegistry: true
    # eureka server地址
    serviceUrl:
      defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
  server:
    # false 關閉自我保護,不管如何都要剔除心跳檢測異常的服務
    enableSelfPreservation: true
    # updatePeerEurekaNodes執行間隔
    peerEurekaNodesUpdateIntervalMs: 10000000
    waitTimeInMsWhenSyncEmpty: 0
  instance:
    hostname: eureka1
    metadataMap: 
      instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

啟動java類:

@SpringBootApplication
@EnableEurekaServer
public class EurekaApp {
    public static void main(String[] args) {
        new SpringApplicationBuilder(EurekaApp.class).web(true).run(args);
    }
}

下面把bootstrap.yml檔案的port和hostname(需要在自己電腦C:\Windows\System32\drivers\etc下的hosts檔案配置對應的域名)分別改成8761,8762,8763和eureka1,eureka2,eureka3,分別啟動Eureka程式即可搭建一個高可用的註冊中心。
啟動成功後,瀏覽器輸入http://localhost:8761/即看到成功介面
這裡寫圖片描述

2.服務提供者怎麼註冊到服務中心的?

註冊到服務中心的主要都有哪些資訊:
ip、port(埠)、instance_id(例項id)、name(服務名)等。
當eureka客戶端啟動的時候,EurekaClientConfiguration讀書客戶端配置資訊,建立一個InstanceInfo例項,然後交給ApplicationInfoManager管理,下面看段程式碼:

    @Configuration
    @ConditionalOnMissingRefreshScope
    protected static class EurekaClientConfiguration {

        @Autowired
        private ApplicationContext context;

        @Autowired(required = false)
        private DiscoveryClientOptionalArgs optionalArgs;

        @Bean(destroyMethod = "shutdown")
        @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
        public EurekaClient eurekaClient(ApplicationInfoManager manager,
                EurekaClientConfig config) {
            return new CloudEurekaClient(manager, config, this.optionalArgs,
                    this.context);
        }

        @Bean
        @ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
        public ApplicationInfoManager eurekaApplicationInfoManager(
                EurekaInstanceConfig config) {
                //獲取配置 建立例項 
            InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
            return new ApplicationInfoManager(config, instanceInfo);
        }
    }

在例項化過程中,DiscoveryClient的HeartbeatThread定時任務會不斷掃描,找到未註冊的例項,並註冊到服務中心,下面DiscoveryClient類的某段程式碼:

 /**
     * The heartbeat task that renews the lease in the given intervals.
     * 心跳,註冊定時任務
     */
    private class HeartbeatThread implements Runnable {

        public void run() {
            if (renew()) {
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }

     /**
     * Renew with the eureka service by making the appropriate REST call
     */
    boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            //向註冊中心傳送一個註冊請求
            httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
            logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            //當註冊中心返回404,證明這個例項沒註冊,則進行註冊,否則返回註冊成功
            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;
        }
    }

       /**
     * Register with the eureka service by making the appropriate REST call.
     * 向服務端註冊方法
     */
    boolean register() throws Throwable {
        logger.info(PREFIX + appPathIdentifier + ": registering service...");
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == 204;
    }

3.服務中心怎麼接收註冊請求?

通過rest介面接收,spring cloud 整合eureka server原生包中的Jersey RESTful介面,JerseyApi
這裡寫圖片描述
下面跟著原始碼檢視接收流程(eureka server是通過filter攔截請求的),程式碼如下:
入口EurekaServerAutoConfiguration類下的jerseyFilterRegistration會增加一個filter用於攔截註冊請求

    @Bean
    public FilterRegistrationBean jerseyFilterRegistration(Application eurekaJerseyApp) {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new ServletContainer(eurekaJerseyApp));
        bean.setOrder(2147483647);
        //攔截上圖客戶端註冊請求
        bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
        return bean;
    }

由ApplicationResource類下的addInstance受理請求,程式碼如下 :

    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        if (this.isBlank(info.getId())) {
            return Response.status(400).entity("Missing instanceId").build();
        } else if (this.isBlank(info.getHostName())) {
            return Response.status(400).entity("Missing hostname").build();
        } else if (this.isBlank(info.getAppName())) {
            return Response.status(400).entity("Missing appName").build();
        } else if (!this.appName.equals(info.getAppName())) {
            return Response.status(400).entity("Mismatched appName, expecting " + this.appName + " but was " + info.getAppName()).build();
        } else if (info.getDataCenterInfo() == null) {
            return Response.status(400).entity("Missing dataCenterInfo").build();
        } else if (info.getDataCenterInfo().getName() == null) {
            return Response.status(400).entity("Missing dataCenterInfo Name").build();
        } else {
            DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
            if (dataCenterInfo instanceof UniqueIdentifier) {
                String dataCenterInfoId = ((UniqueIdentifier)dataCenterInfo).getId();
                if (this.isBlank(dataCenterInfoId)) {
                    boolean experimental = "true".equalsIgnoreCase(this.serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                    if (experimental) {
                        String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                        return Response.status(400).entity(entity).build();
                    }

                    if (dataCenterInfo instanceof AmazonInfo) {
                        AmazonInfo amazonInfo = (AmazonInfo)dataCenterInfo;
                        String effectiveId = amazonInfo.get(MetaDataKey.instanceId);
                        if (effectiveId == null) {
                            amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId());
                        }
                    } else {
                        logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                    }
                }
            }
            //進行註冊,註冊成功返回204,isReplication防止迴圈傳播
            this.registry.register(info, "true".equals(isReplication));
            return Response.status(204).build();
        }
    }

呼叫InstanceRegistry類的register方法註冊成功併發布EurekaInstanceRegisteredEvent註冊事件,程式碼如下:

 /**
 * 註冊併發布註冊事件
 */
 public void register(InstanceInfo info, boolean isReplication) {
        this.handleRegistration(info, this.resolveInstanceLeaseDuration(info), isReplication);
        super.register(info, isReplication);
    }

/**
* 釋出註冊事件
*/
  private void handleRegistration(InstanceInfo info, int leaseDuration, boolean isReplication) {
        this.log("register " + info.getAppName() + ", vip " + info.getVIPAddress() + ", leaseDuration " + leaseDuration + ", isReplication " + isReplication);
        this.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
    }

4.服務中心怎麼儲存?

檢視AbstractInstanceRegistry類下的register方法可以清楚地看到客戶端資訊是存在ConcurrentHashMap裡面的。

5.服務中心自身是怎麼實現高可用的?

通過對等的eureka server例項,當eureka服務端啟動時,會找到配置檔案所填其它服務端地址,相互註冊。例子,比如現在有eureka1,eureka2,eureka3。當有服務註冊到eureka1時,eureka1會發出注冊事件,此時eureka2,eureka3會同步這個例項資訊。

6.服務叢集之間怎麼同步資訊?如何去重?

eurekaserver初始化時,維護了一個PeerEurekaNodes.peerEurekaNodes列表,
當需要同步更新資訊的時候, 遍歷所有節點,並傳播資訊,會呼叫PeerAwareInstanceRegistryImpl類的這幾個方法,cancel/register/renew,程式碼如下:

public boolean cancel(String appName, String id, boolean isReplication) {
        if (super.cancel(appName, id, isReplication)) {
            this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Cancel, appName, id, (InstanceInfo)null, (InstanceStatus)null, isReplication);
            Object var4 = this.lock;
            synchronized(this.lock) {
                if (this.expectedNumberOfRenewsPerMin > 0) {
                    this.expectedNumberOfRenewsPerMin -= 2;
                    this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin * this.serverConfig.getRenewalPercentThreshold());
                }

                return true;
            }
        } else {
            return false;
        }
    }

    public void register(InstanceInfo info, boolean isReplication) {
        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);
    }

    public boolean renew(String appName, String id, boolean isReplication) {
        if (super.renew(appName, id, isReplication)) {
            this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Heartbeat, appName, id, (InstanceInfo)null, (InstanceStatus)null, isReplication);
            return true;
        } else {
            return false;
        }
    }
    /**
    * 遍歷所有節點
    */
    private void replicateToPeers(PeerAwareInstanceRegistryImpl.Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();

        try {
            if (isReplication) {
                this.numberOfReplicationsLastMin.increment();
            }

            if (this.peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            Iterator var8 = this.peerEurekaNodes.getPeerEurekaNodes().iterator();

            while(var8.hasNext()) {
                PeerEurekaNode node = (PeerEurekaNode)var8.next();
                if (!this.peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    this.replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
                }
            }
        } finally {
            tracer.stop();
        }

    }
/**
* 對不同的事件進行處理
*/
private void replicateInstanceActionsToPeers(PeerAwareInstanceRegistryImpl.Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
        try {
            InstanceInfo infoFromRegistry = null;
            CurrentRequestVersion.set(Version.V2);
            switch(action) {
            case Cancel:
                node.cancel(appName, id);
                break;
            case Heartbeat:
                InstanceStatus overriddenStatus = (InstanceStatus)this.overriddenInstanceStatusMap.get(id);
                infoFromRegistry = this.getInstanceByAppAndId(appName, id, false);
                node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                break;
            case Register:
                node.register(info);
                break;
            case StatusUpdate:
                infoFromRegistry = this.getInstanceByAppAndId(appName, id, false);
                node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                break;
            case DeleteStatusOverride:
                infoFromRegistry = this.getInstanceByAppAndId(appName, id, false);
                node.deleteStatusOverride(appName, id, infoFromRegistry);
            }
        } catch (Throwable var9) {
            logger.error("Cannot replicate information to {} for action {}", new Object[]{node.getServiceUrl(), action.name(), var9});
        }

    }

7.服務中心如何檢查服務提供者是否正常?

服務剔除: 長時間沒有給心跳 預設90秒
當eureka啟動初始化上下文的時候,會啟動一個定時任務EvictionTask(檢測異常服務)。
程式碼有點多就不一一貼出來了。程式碼入口為EurekaServerInitializerConfiguration類的start方法,最後調到AbstractInstanceRegistry類的postInit啟動EvictionTask。
當eureka server開啟自我保護的情況下,不會剔除任何服務。即eureka.server.enableSelfPreservation=true的時候。自我保護例子:
當一個服務有10個例項,預設提交心跳時間為30秒。
此時閥值為0.8,
那麼每分鐘需要收到心跳包的個數為:10*0.8*2=16,
如果一分鐘內,eureka收到的心跳請求沒達到16個,eureka則懷疑是網路抖動,此時不會剔除任何服務。

8.服務提供者如果下架服務?

當spring上下文關閉時下架eureka服務,程式入口EurekaDiscoveryClientConfiguration類的onApplicationEvent方法,最終呼叫DiscoveryClient類的shutdown,unregister,cancel方法下架服務,程式碼如下:

    @EventListener(ContextClosedEvent.class)
    public void onApplicationEvent(ContextClosedEvent event) {
        // register in case meta data changed
        stop();
        this.eurekaClient.shutdown();
    }
     /**
     * Shuts down Eureka Client. Also sends a deregistration request to the
     * eureka server.
     */
    @PreDestroy
    @Override
    public synchronized void shutdown() {
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");

            if (statusChangeListener != null && applicationInfoManager != null) {
                applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
            }

            cancelScheduledTasks();

            // If APPINFO was registered
            if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
                applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                unregister();
            }

            if (eurekaTransport != null) {
                eurekaTransport.shutdown();
            }

            heartbeatStalenessMonitor.shutdown();
            registryStalenessMonitor.shutdown();

            logger.info("Completed shut down of DiscoveryClient");
        }
    }

    /**
     * unregister w/ the eureka service.
     */
    void unregister() {
        // It can be null if shouldRegisterWithEureka == false
        if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
            try {
                logger.info("Unregistering ...");
                EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
                logger.info(PREFIX + appPathIdentifier + " - deregister  status: " + httpResponse.getStatusCode());
            } catch (Exception e) {
                logger.error(PREFIX + appPathIdentifier + " - de-registration failed" + e.getMessage(), e);
            }
        }
    }

從此開始微服務的學習之旅,本人正在入門學習,如理解有誤歡迎指正,歡迎有興趣的小夥伴一起交流。