Springcloud EurekaClient 底層實現(一)

最近接手一個springcloud項目,需要優雅地註銷服務後再重啓,這樣就不會因爲ribbon負載均衡到重啓的服務上從而丟失請求,那麼我們先來研究下怎麼手動註銷服務。

以下所有代碼都是基於springboot2.0.9 RELEASE和springcloud Finchley.RELEASE

如何優雅的手動註銷服務

1.eurekaserver:發送delete請求

比如說現在想註銷下圖中的PRODUCER服務
在這裏插入圖片描述
那麼拼接url http://39.96.0.32:8000/eureka/apps/producer/DESKTOP-UJN399N:producer:8081 發送delete請求
在這裏插入圖片描述
發現eureka註冊中心不再顯示producer服務了,說明我們已成功從eureka註冊表中移除了producer服務。
在這裏插入圖片描述
好戲來了,過一段時間後,發現producer自動回來了。
在這裏插入圖片描述
仔細想想也應該是這樣,因爲eureka-server作爲註冊中心,自然要不停地獲取各client發送來的心跳,既然Producer程序沒關,自然會給eureka-server發送心跳,這樣就重新註冊了。
下圖是producer服務重新註冊自身服務的日誌。
在這裏插入圖片描述

顯然這種方式註銷服務談不上優雅,因爲很可能你剛註銷服務,剛好下1s服務就發送了次心跳給server,又重新給註冊上了。

2.eureka server:發送PUT請求

PUT請求 http://39.96.0.32:8000/eureka/apps/producer/DESKTOP-UJN399N:producer:8080/status?value=UP
服務上線:UP 服務下線 OUT_OF_SERVICE or DOWN
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
服務重新上線 UP
在這裏插入圖片描述
在這裏插入圖片描述

PUT 請求eureka server設置status,可以改變服務的狀態(如OUT_OF_SERVICE,DOWN,UP等),這樣更優雅!

3.client端:寫web接口,執行shutDownComponent命令

用戶調用Producer服務中自定義的 /offline 端口,即可實現從eureka-server註冊表移除該服務。

/** 用戶自定義 TestController中 **/
 @RequestMapping(value = "/offline", method = RequestMethod.GET)
    public void offLine(){
        DiscoveryManager.getInstance().shutdownComponent();
    }

/** com.netflix.discovery.DisconveryManager **/
    public void shutdownComponent() {
        if (discoveryClient != null) {
            try {
                discoveryClient.shutdown();
                discoveryClient = null;
            } catch (Throwable th) {
                logger.error("Error in shutting down client", th);
            }
        }
    }

shutDown()直接關閉了發送心跳的定時器,如果想重新上線,可以向 http://39.96.0.32:8001/eureka/apps/CONSUMER 發送Post請求,同時帶有instanceInfo的json字符串作爲BODY,但是就算是上線了,由於heartbeat的定時器一直處於關閉狀態,過段時間server還是會將該服務下線。目前我還沒找到重新啓動定時器的方法。

4.client端:調用actuator/service-registry接口

pom中引用spring-boot-starter-actuator,在yml裏放開actuator的所有端口,啓動程序後會發現多了很多actuator端口。

management:
  endpoints:
    web:
      exposure:
        # 默認只放出health,info等接口,現全部開放
        include: "*"

在這裏插入圖片描述
通過POST請求 /actuator/service-registry 接口,可以使當前服務變爲DOWN和UP狀態。

  • POST status=“DOWN”
    在這裏插入圖片描述
  • eureka server 顯示consumer服務狀態爲DOWN
    在這裏插入圖片描述
  • 使服務重新上線,post status=“UP”
    在這裏插入圖片描述
  • eureka server顯示consumer服務處於up狀態
    在這裏插入圖片描述

這種方式非常優雅,set status=“DOWN”等流量都沒有進來後(這可能需要一段時間),可以選擇重啓該服務,這樣不會丟失請求。也可以down掉幾個其他服務,將請求負載均衡到本地服務,方便DEBUG,然後再將其他服務UP起來
後面會講到,POST請求 /actuator/service-registry 接口實質上還是PUT調用 http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777/status?value=UP

註冊服務底層代碼

啓動consumer client服務,eureka server就會很快顯示consumer服務已經上線,這是爲什麼呢?
向eureka server發送了DELETE consumer的請求,過段時間consumer又會重新上線,這又是爲什麼呢?

1.準備工作

  • pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.huoli</groupId>
    <artifactId>consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>consumer</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  • application.yml
eureka:
  client:
    # 客戶端從eureka獲取applications到本地緩存的時間間隔,默認30s
    # DiscoveryClient 中啓動 registry cache refresh timer 定時任務,每3s從server獲取一次註冊服務列表,但獲取的信息並不是實時的狀態
    registry-fetch-interval-seconds: 3
    service-url:
      defaultZone: http://39.96.0.32:8000/eureka/,http://39.96.0.32:8001/eureka/,http://39.96.0.32:8002/eureka/
  instance:
    # 表示eureka server至上一次收到client的心跳之後,等待下一次心跳的超時時間,在這個時間內若沒收到下一次心跳,則將移除該instance,默認90s
    lease-expiration-duration-in-seconds: 90
    # 表示eureka client發送心跳給server端的頻率。如果在leaseExpirationDurationInSeconds後,server端沒有收到client的心跳,則將摘除該instance,默認30s
    # DiscoveryClient 中啓動 Heartbeat timer 定時任務,每5s發送次請求給server,告訴自己處於上線狀態
    lease-renewal-interval-in-seconds: 5
server:
  port: 7777
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
  application:
    name: consumer
logging:
  level:
    root: info
# 一定要寫這句話,否則hystrix無法檢測到請求
feign:
  hystrix:
    enabled: true
  client:
    config:
      default:
        connectTimeout: 7000
        readTimeout: 7000
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        timeout:
          #如果enabled設置爲false,則請求超時交給ribbon控制
          enabled: true
        isolation:
          thread:
            timeoutInMilliseconds: 180000
management:
  endpoints:
    web:
      exposure:
        # 默認只放出health,info等接口,查看localhost:7777/acurator/loggers 可看到各包對應的日誌等級
        # 在bootstrap.properties中配置包的日誌等級,方便定位問題
        include: 'loggers'        
  • bootstrap.properties
    爲了方便理解代碼,在bootstrap.properties中將DiscoveryClient和loadbalancer的日誌等級設爲DEBUG。
# 設置指定包的日誌級別
logging.level.com.netflix.discovery.DiscoveryClient=DEBUG
logging.level.com.netflix.loadbalancer=DEBUG
  • java
/**
* APPLICATION
*/
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}


/**
* FeignClient
*/
@FeignClient(name = "producer")
public interface ProducerRemote {

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public String hello(@RequestParam("name") String name);

    @RequestMapping("/test")
    public List<Object> test(@RequestBody Map<String,String> map);

}

/**
* TestController 
*/
@RestController
public class TestController {
    @Autowired
    ProducerRemote producerRemote;

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @RequestMapping("/hello")
    public String hello(){
        return producerRemote.hello("abc");
    }
}

2.啓動服務

啓動服務後,console打印如下日誌,先大致瀏覽一遍,記住打印日誌的類是DiscoveryClient
在這裏插入圖片描述
瀏覽器輸入 http://localhost:7777/actuator/loggers,看見DiscoveryClient的日誌級別是DEBUG,說明bootstrap.properties中的配置生效。
在這裏插入圖片描述

註冊服務步驟

  • EurekaClientAutoConfiguration中static class RefreshableEurekaClientConfiguration,@Bean生成EurekaClient的實例
    在這裏插入圖片描述
  • DiscoveryClient作爲EurekaClient的繼承類開始實例化,調用方法initScheduledTasks(),該方法生成了2個定時器,分別是
  1. 間隔registryFetchIntervalSeconds時間執行**CacheRefreshThread()**方法,從eureka server獲取註冊信息,存儲在本地緩存
  2. 間隔renewalIntervalInSecs時間執行**HeartbeatThread()**方法,向eureka server發送heart beat,保持自身服務在線狀態
    在這裏插入圖片描述

至此,註冊服務流程講解完畢,該服務會間隔renewalIntervalInSecs時間,反覆執行renew()的代碼,第一次先register註冊服務,再然後不停地跟sendHeartBeat說自己處於UP狀態。
其中renewalIntervalInSecs可在yml中配置,下圖中配置的是5s,當然這個數值大小還是根據實際情況去設置。
在這裏插入圖片描述

DiscoveryClient.shutDown()


unregister()最後調用的核心代碼是 EurekaHttpClient.cancle(),即向eureka server發送DELETE請求 。
在這裏插入圖片描述

總結

至此介紹完了如何優雅的關閉和啓動eureka client服務,以及底層的代碼實現。
需要記住的名詞有

  • DiscoveryClient (父類爲EurekaClient) 操作和eureka server連接的工具類
  • initScheduledTasks 啓動2個定時器,一個發送heartbeat,一個從server拉取註冊信息(fetch registry)
  • renewalIntervalInSecs 發送heartbeat的時間間隔

eureka server的rest接口可參閱 https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
這裏我們自己總結下規律,注意sendHeartBeat和statusUpdate之間的不同

EurekaHttpClient中的方法 請求方式 請求地址 其他
register post http://39.96.0.32:8001/eureka/apps/CONSUMER InstanceInfo的json對象
cancel delete http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777
sendHeartBeat put http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777?status=UP&lastDirtyTimestamp=1562759993141
statusUpdate put http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777/status?value=UP&lastDirtyTimestamp=1562759993141 status=UP,OUT_OF_SERVICE,DOWN等等

下一章將介紹EurekaClient是如何獲取服務列表,然後負載均衡去調用其他服務的。

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