服務治理: Spring Cloud Eureka
一、簡介
Spring cloud eureka
是Spring cloud netfilx
中的一部分,它基於Netflix Eureka
做了二次封裝,主要職責完成Eureka 中的服務治理功能
本篇主要探討如下:
- 服務治理和Eureka簡介
- 構建服務注冊中心
- 服務注冊與服務發現
- Eureka 基礎架構
- Eureka 的服務治理機制
- Eureka 的配置
二、 功能概述
服務治理
服務治理可以是說微服務架構中最為核心的基礎模塊,它主要用來實現各個微服務實現的自動化注冊與發現。在開始的時候微服務系統的服務可能並不多,我們需要一些配置來完成服務的調用。
-
服務注冊
: 在服務治理框架中,通常會構建一個注冊中心,由各個服務提供者來向注冊中心登記並提供服務,將主機與端口號、版本號、通信協議等一些附加信息告知注冊中心,注冊中心按照服務名分類組織服務清單。服務名 位置 服務A 192.168.1.101:8000, 192.168.1.102:8000 服務B 192.168.1.103:9000,192.168.1.104:9000,192.168.1.105:9000 比如我們有兩個提供服務A 的進程分別位於192.168.1.101:8000, 192.168.1.102:8000 上,另外還有三個提供服務B 的進程分別位於192.168.1.103:9000,192.168.1.104:9000,192.168.1.105:9000 進程上,那么你向服務中心注冊過后,服務中心就會有一個這樣的服務列表,服務中心向各個注冊的服務發送心跳機制,來檢驗服務是否可用,若不可用就會把服務剔除,來達到故障排除的效果。
-
服務發現
: 由於在服務治理框架下運作,服務間的調用不再通過指定的Ip:端口號
這種方式來實現 ,而是向服務名發起請求實現。所以,在服務調用方在調用服務提供方接口的時候,並不知道具體服務的位置。因此,服務調用方需要向服務中心
獲取服務列表,以實現對具體服務
的訪問。比如一個服務調用者C想要獲取服務A的ip來完成接口的調用,那么他首先應該去服務中心發起咨詢你服務的請求,由注冊中心的服務列表將A的位置發送給調用者C,如果按照上面服務A地址的話,那么調用者C會由兩個服務A的地址來提供服務,當服務C需要調用的時候,便從服務A中的清單中采用輪詢的方式取出一個位置來服務調用,這個過程也被稱為負載均衡。
Netflix Eureka
Eureka
是Netflix開發的服務發現框架
,本身是一個基於REST的服務
,主要用於定位運行在AWS域中的中間層服務,以達到負載均衡和中間層服務故障轉移的目的。SpringCloud將它集成在其子項目spring-cloud-netflix中,以實現SpringCloud的服務發現功能。Eureka
包含兩個組件:Eureka Server
和Eureka Client
Eureka Server
簡稱Eureka 服務端, 主要提供服務注冊功能,其實也就相當於是注冊中心,和其他服務注冊中心一樣,提供高可用的配置,同時也支持集群部署
,當集群中某一個節點發生故障時,那么Eureka就會進入自我保護模式,它允許故障的節點繼續提供服務的發現與注冊,當故障分片恢復運行時,集群中的其他分片會把他們的狀態再同步回來。Eureka Client
:簡稱Eureka 客戶端,主要處理服務的注冊與發現。客戶端通過注解和參數配置的方式,Eureka 客戶端向注冊中心注冊自身的服務並周期性的發送心跳機制來更新服務租約。同時,它也能從服務端查詢當前注冊的服務信息並把它們緩存到本地並周期性地刷新服務狀態。
搭建服務注冊中心
Spring Cloud Eureka 是采用SpringBoot 進行項目的快速搭建的,如果不太了解SpringBoot的話,可以了解一下SpringBoot 入門實例。
- 首先創建SpringBoot工程,命名為Eureka-server,也就是
Eureka服務端
,創建完成后在pom.xml文件中增加如下maven依賴,完整的文件如下:
<?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>
<groupId>com.eureka.server</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
- 在SpringBoot啟動類,也就是
@SpringBootApplication
修飾的主方法中加入如下注解@EnableEurekaServer
。
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
加入這個注解也就標識着這是一個Eureka的服務端,可以啟動服務了,但是啟動服務會報錯,因為你沒有添加注冊中心的相關配置。
- 在
application.properties
文件中加入如下內容
server.port=8000
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
server.port 就代表着注冊中心的端口號
eureka.client.service-url.defaultZone :eureka客戶端默認服務url
eureka.client.register-with-eureka : 表示注冊中心是否向其他注冊中心注冊自己,單節點注冊中心不需要,設置為false
eureka.client.fetch-registry: 表示注冊中心是否主動去檢索服務,並不需要檢索服務,設置為false
其他配置:
# 項目contextPath,一般在正式發布版本中,我們不配置
# 避免加上更目錄:Cannot execute request on any known server
# 加上根目錄也需要在注冊地址上加入根
server.context-path=/eureka81
# 錯誤頁,指定發生錯誤時,跳轉的URL。請查看BasicErrorController源碼便知
server.error.path=/error
# 通過spring.application.name屬性,我們可以指定微服務的名稱后續在調用的時候只需要使用該名稱就可以進行服務的訪問。
spring.application.name=eureka-server
# eureka是默認使用hostname進行注冊,可通過一下項自動獲取注冊服務IP或者直接通過eureka.instance.ip-address指定IP
# eureka.instance.prefer-ip-address=true
# SpringBoot 在啟動的時候會讀配置文件,會把prefer-ip-address 默認轉換為preferIpAddress駝峰命名
eureka.instance.preferIpAddress=true
# 設為false,關閉自我保護
eureka.server.enable-self-preservation=false
# 清理間隔(單位毫秒,默認是60*1000
eureka.server.eviction-interval-timer-in-ms=6000
# 開啟健康檢查(需要spring-boot-starter-actuator依賴)
eureka.client.healthcheck.enabled=false
# 續約更新時間間隔(默認30秒)
eureka.instance.lease-renewal-interval-in-seconds=10
# 續約到期時間(默認90秒)
eureka.instance.lease-expiration-duration-in-seconds=30
沒有加入 eureka.instance.preferIpAddress=true
之前,默認本地為注冊中心
加入 eureka.instance.preferIpAddress=true
之后,圈出來的ip即為eureka.client.service-url.defaultZone指定的 ip。
在完成了上述配置之后,應用程序啟動並訪問http://localhost:1111/ 可以看到如下圖所示的信息版,其中Instances curently registered with Eureka 是空的,表明還沒有任何服務提供者提供服務。
注冊服務提供者
在完成了上述搭建之后,接下來我們嘗試將一個既有的SpringBoot應用加入Eureka服務治理體系去。
使用上一小節的快速入門工程進行改造,將其作為一個微服務應用向服務注冊中心發布注冊自己
pom.xml配置如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 配置完
pom.xml
,我們需要在啟動類上加入@EnableDiscoverClient
注解,用於開啟eureka-client客戶端 - 在
application.properties
中加入如下內容
# 這個名字就是Eureka注冊中新的實例名稱
spring.application.name=server-provider
# 向注冊中心注冊自己
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
- 配置完上面兩個之后,在package文件夾下新建HelloController類,具體代碼如下
@RestController
public class HelloController {
private final Logger log = LoggerFactory.getLogger(HelloController.class);
@Resource
private DiscoveryClient discoveryClient;
@RequestMapping(value = "hello", method = RequestMethod.GET)
public String hello(){
ServiceInstance instance = discoveryClient.getLocalServiceInstance();
log.info("instance.host = " + instance.getHost() + " instance.service = " + instance.getServiceId()
+ " instance.port = " + instance.getPort());
return "Hello World";
}
}
- 啟動服務提供者,啟動完成后,會出現如下表示啟動成功。
- 訪問http://localhost:1111/ ,主頁上顯示
eureka-provider
注冊到了注冊中心
此處的Status 中的內容也就包括上面配置的spring.application.name=server-provider
-
在主頁訪問 http://localhost:8080/hello ,發現頁面上 輸出了
Hello World
,控制台打印出來了c.s.provider.controller.HelloController : instance.host = macliu instance.service = server-provider instance.port = 8080
注意事項
- 上面注冊到注冊中心的圖,你會發現這樣一行紅色的文字
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
這是Eureka的一種自我保護機制,Eureka Server在運行期間,會統計心跳失敗的比例在15分鍾之內是否低於85%,如果出現低於的情況(在單機調試的時候很容易滿足,實際在生產環境上通常是由於網絡不穩定導致),Eureka Server會將當前的實例注冊信息保護起來,同時提示這個警告。
Eureka server和client之間每隔30秒會進行一次心跳通信,告訴server,client還活着
- 把上面的server-provider服務停止之后,會出現如下狀態
這個表示server-provider 已經標記為下線,也就是 DOWN 狀態,再次重新上線后,發現Status又變為了UP狀態。
- 把上面的配置文件中自我保護功能關閉后,出現如下狀態
高可用配置中心
在微服務架構這樣的分布式環境中,需要充分考慮到發生故障的情況,所以在生產環境中必須對各個組件進行高可用部署,對於微服務是如此,對於注冊中心也一樣。
Eureka Server
的設計就充分考慮了高可用問題,在Eureka的服務治理體系中,所有的節點既是服務提供方,也是服務的消費者,服務注冊中心也不例外,不同的注冊中心在向其他注冊中心提供節點列表的時候,也在向其他注冊中心獲取節點列表。
高可用的配置中心就是向其他注冊中心注冊自己,同時把服務列表提供給其他注冊中心,從而達到注冊中心列表同步,達到高可用的效果。通過下面兩個配置來實現
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
下面就在單節點的基礎之上創建一下高可用
的配置中心(雙節點注冊中心
)
-
首先,創建兩個配置文件,分別是
application-peer1.properties
和application-peer2.properties
,內容分別如下-
application-peer1.properties
spring.application.name=eureka-server server.port=1111 eureka.instance.hostname=peer1 eureka.client.register-with-eureka=true eureka.client.fetch-registry=true eureka.client.service-url.defaultZone=http://peer2:1112/eureka/
-
application-peer2.properties
spring.application.name=eureka-server server.port=1112 eureka.instance.hostname=peer2 eureka.client.register-with-eureka=true eureka.client.fetch-registry=true eureka.client.service-url.defaultZone=http://peer1:1111/eureka/
-
-
在本地修改配置文件/etc/hosts ,Windows下面是C:\Winows\System32\drivers\etc\hosts。
添加如下內容
127.0.0.1 peerl 127.0.0.1 peer2
如下:
-
首先在idea 或者eclipse 使用mvn clean 和 mvn install命令,會直接打包,這里注意,一定要在pom.xml中配置如下,否則使用
java -jar
會報沒有主清單屬性的錯誤。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
-
打包完成后,切換到
eureka-server
項目,再切換到target目錄下,此時有mvn install 的jar包,使用java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
和java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
兩個命令,啟動兩個例程。起來過后分別訪問 http://localhost:peer1/eureka/ 和 http://localhost:peer2/eureka/ 主頁,發現對應的注冊中心分別注冊進去了,而且分片也處於可用分片狀態。
到現在為止,我們已經讓兩個注冊中心分別注冊各自的服務了,還記得上面還有一個server-provider
服務嗎?我們也讓server-provider分別注冊到這兩個注冊中心。
在server-provider
中修改對應的配置文件
eureka.client.service-url.defaultZone=http://peer1:1111/eureka, http://peer2:1112/eureka/
啟動程序,發現http://localhost:1111/ 和 http://localhost:1112/ 中都注冊了server-provider
服務
訪問http://localhost:8080/hello,你會發現頁面上顯示出來
hello world
,斷開其中的任意一個注冊中心,hello world
也能夠顯示出來。也就是說,server-provider 分別對兩個注冊中心分別注冊了各自的服務,由兩個注冊中心以輪詢的方式提供服務。斷開其中一個注冊中心,還有另外一個注冊中心可以提供服務,這也是Eureka 高可用的體現。
注意事項
- 如果
application-peer1.properties
和application-peer2.properties
中的eureka.instance.hostname
與 本地hosts文件中的名稱不一致的話,那么注冊中心啟動后,會使分片處於不可用的狀態,spring.application.name
表示的是實例的名稱,也就是如下的地方
-
當
server-provider
注冊進來的時候,高可用配置的注冊中心會以輪詢的方式提供服務,每次提供服務是哪個注冊中心是不可預知的。 -
如我們不想使用主機名來定義注冊中心的地址,也可以使用IP地址的形式, 但是需要在配置文件中增加配置參數eureka.instance.prefer-ip-address=true, 該值默認為false。
服務發現與消費
通過上述的內容介紹與實踐,我們已經搭建起來微服務架構中的核心組件— 服務注冊中心(包括單節點模式和高可用模式)。並用server-provider
注冊雙節點,在頁面上發起一個url請求時,注冊中心找到server-provider
,並有兩個節點以輪詢的方式提供服務。
下面就來構建一個消費者
,它主要完成兩個目標:發現服務
和消費服務
。其中,服務發現的任務由Eureka
客戶端完成,消費服務
的任務由Ribbon
來完成。
先來認識一下什么是Ribbon
:Ribbon是客戶端負載均衡器
,可以讓您對HTTP和TCP客戶端的行為進行控制。 Feign已經使用了Ribbon,如果你使用了@FeignClient
,那么Ribbon也適用。
Ribbon
可以在通過客戶端中配置的ribbonServerList
服務端列表去輪詢訪問以達到負載均衡的效果。當ribbon
與Eureka
聯合使用時,Ribbon
的服務實例清單RibbonServerList
會被DiscoveryEnabledNIWSServerList
重寫,擴展成從Eureka注冊中心中獲取服務端列表。同時它也會用NIWSDiscoveryPing來取代Ping,它將職責委托給Eureka來確定服務端是否啟動,我們目前不細致探討Ribbon的細節問題。
下面通過一個簡單的實例,看看Eureka的服務治理體系下如何實現服務的發現與消費。
- 首先,先做一些准備工作,啟動之前實現的服務注冊中心
eureka-server
以及server-provider
服務,為了實現ribbon的負載均衡功能,我們通過java -jar
命令行的方式來啟動兩個不同端口的server-provider
- 啟動一個eureka-server即可
- 使用
java -jar service-provider-0.0.1-SNAPSHOT.jar --server.port=8081
和java -jar service-provider-0.0.1-SNAPSHOT.jar --server.port=8082
來啟動兩個server-provider 進程
- 啟動完成后,可見注冊中心注冊了兩個server-provider 實例
- 新創建一個SpringBoot 工程,命名為ribbon-consumer,相較於之前pom.xml,我們新增了spring-cloud-starter-ribbon
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 創建完pom.xml,在ribbon-consumer 啟動類加上
@EnableDiscoveryClient
注解,讓該注解注冊為Eureka客戶端,以獲得服務發現的能力,同時,創建RestTemplate
對象,加上@LoadBalance
注解開啟負載均衡。
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
}
- 在src目錄下新建一個
RibbonController
類,注入@RestTemplate
,構造一個方法來調用server-provider
中的/hello 方法。代碼如下
@RestController
public class RibbonController {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET)
public String helloConsumer(){
return restTemplate.getForEntity("http://server-provider/hello",String.class).getBody();
}
}
在helloConsumer方法上面采用Restful 風格的編碼方式,這個方法遠程調用了
server-provider
中的hello方法,在這里不像是http://ip:端口號這種書寫方式,而是直接采用 服務名/方法名來直接調用方法,因為你不知道具體的方法在哪個ip的機器上,需要由Eureka進行調用,這樣消費者就不用關心由誰提供了服務,只要提供了服務即可,這也是面向對象方法的一種體現,同時也能很好的解耦。
- 在
application.properties
配置如下
spring.application.name=ribbon-consumer
server.port=9000
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
因為ribbon-consumer需要由客戶端來主動調用方法,所以需要提供實例名稱,端口號,並在注冊中心注冊ribbon-consumer服務
- 啟動服務,訪問Eureka主頁發現Ribbon-consumer的服務也注冊進來了。
- 通過在url 請求地址 http://localhost:9000/ribbon-consumer發起GET請求,成功返回了Hello World,此時,我們在控制台看到了如下的信息
INFO 29397 --- [nio-9000-exec-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client server-provider initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=server-provider,current list of Servers=[macliu:8082, macliu:8081],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:macliu:8082; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
, [Server:macliu:8081; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@3dc2c2eb
再嘗試刷新幾次url,因為我們實在后台啟動的程序,在終端會看到如下的信息
INFO 28929 --- [nio-8082-exec-3] c.s.provider.controller.HelloController : instance.host = macliuinstance.service = server-providerinstance.port = 8082
因為開了兩個終端,一個是8081
端口,一個是8082
端口,多刷新幾次頁面后,會發現終端在循環輸出上面的信息,來判斷使用ribbon 實現了負載均衡。
歡迎關注Java建設者:一起學習交流