Spring Cloud 接入 EDAS 之服務發現篇
目前 EDAS 已經完全支持 Spring Cloud 應用的部署了,你可以將你的Spring Cloud 應用部署到 EDAS 中。
同時,為了更好地將阿裏中間件的功能以雲服務的方式提供給大家,我們也對 Spring Cloud 中的一些組件進行了加強或替換的工作。
讓我們先來聊聊服務發現。
我們知道原生的 Spring Cloud 支持多種服務注冊與發現的方式,Eureka 、 Consul 、 Zookeeper 等,目前使用最多最廣的就是 Eureka了,那我們就先從一個簡單的 Eureka Demo 說起。
Eureka Demo
創建服務注冊中心
創建一個基礎的 Spring Cloud 工程,命名為 eureka-server,並在 pom.xml 中引入需要的依賴內容:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<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>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
通過 @EnableEurekaServer 注解來啟動一個服務注冊中心。隻需要在一個普通的 Spring Boot 應用中添加這個注解就能開啟此功能,代碼如下:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
這樣啟動時,應用將完全隻用默認配置,如果我們想給服務命名,或者是修改監聽端口,可以在 resource/application.properties 中進行如下配置。
由於我們自己就是唯一的一個 EurekaServer ,這裏就不向自己注冊自己了,將 register-with-eureka 設置成 false。
spring.application.name=eureka-server
server.port=8761
eureka.client.register-with-eureka=false
隻需要直接運行 EurekaServerApplication 的 main 函數,eureka server 即可啟動成功。
啟動成功後,可以在 https://localhost:8761 頁麵查看詳情。
頁麵打開成功,表明服務已經啟動,目前 instances 為空,表明還沒有服務注冊上來。
創建服務提供者
創建一個 Spring Cloud 工程,命名為 service-provider。同樣,首先在 pom.xml 中引入需要的依賴內容:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<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>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
接著是服務提供端的代碼,其中 @EnableDiscoveryClient 注解表明此應用需開啟服務注冊與發現功能。
@SpringBootApplication
@EnableDiscoveryClient
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
既然是服務提供者,所以我們還需要提供一個簡單的服務
@RestController
public class PersonController {
@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
public String echo(@PathVariable String string) {
return string;
}
}
最後同樣是配置,出去配置應用名與監聽端口外,我們還得告訴應用,服務注冊中心的地址。
spring.application.name=service-provider
server.port=18081
eureka.client.serviceUrl.defaultZone=https://localhost:8761/eureka/
啟動 service-provider 服務,在 Eureka 頁麵查看服務是否已經注冊成功.
可以看到 instances 中已經存在的實例有了,service-provider,端口是 18081。
創建服務消費者
這個例子中,我們將不僅僅是演示服務發現的功能,同時還將演示 Eureka 服務發現 與 RestTemplate、AsyncRestTemplate、FeignClient這三個客戶端是如何結合的。因為實際使用中,我們更多使用的是用這三個客戶端進行服務調用。
創建一個 Spring Cloud 工程,命名為 service-consumer。同樣,首先在 pom.xml 中引入需要的依賴內容:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
因為在這裏我們要演示 FeignClient 的使用,所以與 service-provider 相比,我們的依賴增加了一個 spring-cloud-starter-feign。
配置好依賴後,我們首先在啟動函數裏完成三件事:啟用服務注冊與發現,激活 FeignClients,將 RestTemplate 與 AsyncRestTemplate 與服務發現結合。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@LoadBalanced
@Bean
public AsyncRestTemplate asyncRestTemplate(){
return new AsyncRestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
在使用 FeignClient 之前,我們還需要完善它的配置,其中代碼如下
@FeignClient(name = "service-provider")
public interface EchoService {
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
String echo(@PathVariable("str") String str);
}
在這三個客戶端都配置好,並與 Eureka Client 結合後,我們可以在 Controller 中直接使用他們。
@RestController
public class Controller {
@Autowired
private RestTemplate restTemplate;
@Autowired
private AsyncRestTemplate asyncRestTemplate;
@Autowired
private EchoService echoService;
@RequestMapping(value = "/echo-rest/{str}", method = RequestMethod.GET)
public String rest(@PathVariable String str) {
return restTemplate.getForObject("https://service-provider/echo/" + str, String.class);
}
@RequestMapping(value = "/echo-async-rest/{str}", method = RequestMethod.GET)
public String asyncRest(@PathVariable String str) throws Exception{
ListenableFuture<ResponseEntity<String>> future = asyncRestTemplate.
getForEntity("https://service-provider/echo/"+str, String.class);
return future.get().getBody();
}
@RequestMapping(value = "/echo-feign/{str}", method = RequestMethod.GET)
public String feign(@PathVariable String str) {
return echoService.echo(str);
}
}
最後,還是不能忘了配置,特別是服務注冊中心的地址
spring.application.name=service-consumer
server.port=18082
eureka.client.serviceUrl.defaultZone=https://localhost:8761/eureka/
啟動服務,開始我們的調用吧,可以看到調用都成功了,
注意:AsyncRestTemplate 接入 服務發現的時間比較晚,需要在 Dalston 之後的版本才能使用,具體詳情參見此 pull request
Eureka 的煩惱
前麵的例子在本機工作起來是很方便的,但是很遺憾,這隻是一個 demo ,實際部署中我們可能都踩過坑或者有這麼一些不爽。
- 隻有一個服務注冊中心,顯然這不符合高可用的原則,高可用就得加機器,維護成本太高了。
- 實際生產中,不會將服務注冊中心與業務服務部署在同一台機器上。如果上雲的時候,我們 eureka server 的地址變化了,我還得修改配置文件裏 eureka 服務的地址,太麻煩了。
- 實際使用中,我的服務注冊中心肯定是需要加密的,總不能隨隨便便一個人就來隨便注冊和看看我都有哪些服務吧,我還得自己實現服務注冊的安全。
- 為什麼我注冊上去的服務,不是一個 ip ,而是一個奇怪的名字,通過這個名字我怎麼能調通服務。其實隻是因為沒有增加 eureka.instance.prefer-ip-address=true這個配置。依舊需要添加配置。
- eureka 因為緩存設計的原因,使得服務注冊上去之後,最遲需要兩分鍾後才能發現。
或許你希望有人提供一個 安全、穩定、高可用、高性能、簡單易用的服務注冊中心。
然後,我不想配置那麼一大堆地址了
最後,我也不想修改我的代碼
是的,EDAS 服務注冊中心,就是這樣一個解決方案。
隻需要修改兩行代碼以及 pom 依賴,無縫將服務注冊中心從 Eureka 切換到 EDAS 服務注冊中心。
你將得到
- 穩定高可用的服務注冊中心
- 安全的服務注冊、服務發現
- 秒級的服務發現機製
- 無需再關心服務注冊中心的地址
EDAS 服務注冊中心
如何接入
源碼的修改,隻有兩行,需要在 main 函數中添加兩行,修改之後的 service-provider 的 main 函數如下。
public static void main(String[] args) {
PandoraBootstrap.run(args);
SpringApplication.run(ServerApplication.class, args);
PandoraBootstrap.markStartupAndWait();
}
pom.xml 的修改有兩點
首先就是將原來的 eureka的starter 替換成 EDAS 服務注冊中心的starter,並加入 pandora 的依賴。
修改之後的 service-provider 的 dependences 依賴如下。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vipclient</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-pandora</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
在 build 的 plugins 中,也需要修改成 EDAS 的方式,修改後的內容如下,版本號後續可能會升級。
<build>
<plugins>
<plugin>
<groupId>com.taobao.pandora</groupId>
<artifactId>pandora-boot-maven-plugin</artifactId>
<version>2.1.7.8</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
service-consumer 的修改方式與 service-provider 的修改方式完全一樣。
使用方式
使用方式?使用方式已經在接入方式裏了,其他方式和原生的完全一樣。
如果你習慣了 Eureka 頁麵查看服務狀態的方式,EDAS 控製台同樣也提供了相關的功能。
本地調試
本地調試時,需要下載輕量級配置中心,並將其啟動,詳情參見 輕量級配置中心
最後在應用的啟動時,配置 JVM 參數,配置如下。
-Dvipserver.client.port=8080
Demo 下載
工作原理
換了一個依賴就把 Eureka 替換成了 EDAS 服務注冊中心,雖然方便,但是這對於我來說相當於是一個黑盒啊,黑盒總是讓人很沒有安全感。
下麵我們將從 服務注冊中心尋址、服務注冊與下線、客戶端結合、高可用、安全等多個方麵來分析原理。
服務注冊中心尋址
既然不需要在配置文件裏配置服務注冊中心的地址了,那麼客戶端是如何找到服務中心的呢?
其實是通過一個 http 請求來實現的,https://jmenv.tbsite.net/vipserver/serverlist。
不僅僅是客戶端,服務端也是通過這個地址來互相發現的。
而在 EDAS 的機器上, jmenv.tbsite.net 是自動配置的。如果在結合我們的輕量級配置中心做本地的開發調試時,還是需要做一點額外配置。
服務注冊與下線
服務注冊的通信協議是 HTTP 協議,默認注冊的應用名是 spring.application.name ,如果有需要將某個應用發布成多個服務名的話,可以試試在 /resource/application.properties
中配置 vipserver.register.doms
的方式來實現,多個服務名中間用英文逗號 ,
隔開。
服務注冊成功後,client 端將會與 server 端維持長連接,通過心跳來檢查服務的可用性,當超過一定時間內 server 端沒有收到 client 端的心跳時,會將服務標記成不可用,這樣其他 client 在查詢時就能發現此服務當前處於不可用的狀態。
如果短時間內,大量 client 與 server 心跳失敗,則會出發降級保護機製,這時候,這些服務會暫時不被標記成不可用的狀態。
客戶端結合
與客戶端結合的方式, EDAS 服務發現組件與 Eureka 是完全一致的。
對於 RestTemplate 和 AsyncRestTemplate 來說,添加上 @LoadBalanced 注解,即可直接接入服務發現以及負載均衡。
添加了此注解後,他們將分別會被添加 LoadBalancerInterceptor 和 AsyncLoadBalancerInterceptor 這兩個攔截器。
執行的過程中,所有請求都會分別被這兩個 Interceptor 所攔截,通過其所持有的 LoadBalancerClient 對象去執行這個請求。
而這個 LoadBalancerClient 對象,其實就是 RibbonLoadBalancerClient 的一個實例,在其源碼中,execute 方法的執行邏輯如下
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
首先我們會拿到一個 ILoadBalancer 對象,然後再通過這個 loadBalancer 對象去拿到我們真正需要調用的服務的地址。
ILoadBalancer有這麼幾個實現類,BaseLoadBalancer、DynamicServerListLoadBalancer、ZoneAwareLoadBalancer等。
看看這個類RibbonClientConfiguration,默認注入的是這個,ZoneAwareLoadBalancer
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
關於ServerList的來源,我們看這個類 EurekaRibbonClientConfiguration
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
可以看到,是通過 EurekeClient 來維護實例的地址列表的。
負載均衡選擇策略?ZoneAware,字麵上可以看出是和 zone 有關的,不過 zone 都是默認的情況下,退化成 RoundRobin。
目前的植入做的很簡單,單純地重新搶先注入了上文中提到的兩個 Bean。
@Bean
@ConditionalOnMissingBean
public ServerList<Server> ribbonServerList(IClientConfig config) {
return new VipserverList(config.getClientName());
}
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping) {
return new DynamicServerListLoadBalancer<Server>(config, rule, ping, serverList, serverListFilter,
new PollingServerListUpdater(1000L, 1000L));
}
負載均衡使用的也是默認的RoundRobin。後續我們會把權重等支持添加進來。
高可用實現
服務端高可用
- eureka
Eureka的多個server 是對等的實體,在 CAP 中選擇了 AP。
節點間的數據使用的是最終一致性,eureka 會將注冊的信息同步到 peer 節點,但是 peer 節點不會二次傳播。
peer節點需要顯示地在配置中設置。如果 peer 節點配置的不全,那麼集群的概念也不存在了,節點之間的關係是通過 peer 節點的顯示配置來維護的。 - EDAS 服務注冊發現組件
EDAS 服務注冊中心的多個 server,存在主從,各節點之間使用 raft 協議保證一致性。
server 之間的互相感知是通過訪問 https://jmenv.tbsite.net/vipserver/serverlist 來獲取其他 peer 節點地址來實現的。
然後通過自定義的端口和協議來進行選舉和數據同步等操作。CAP 中選擇的是 CP。
客戶端高可用
- eureka
通過本地緩存來實現,當 server 連接不上時,直接使用本地緩存。每 30s 異步更新一次緩存,避免了每次請求都強依賴於服務注冊中心。 - EDAS 服務注冊發現組件
通過本地緩存來實現,當 server 連接不上時,直接使用本地緩存。異步更新緩存,避免了每次請求都強依賴於服務注冊中心。同時,還提供了通過 UDP 主動 push 的方式在新服務節點加入時及時通知。
安全的實現
EDAS 服務注冊發現組件,結合 EDAS 已有的安全功能,在每次注冊、心跳和查詢請求中都添加了驗簽鑒權的操作,保護了服務的安全性。
最後更新:2017-11-17 18:04:42