目前的 gossip,大致還是維持著〈gossip 服務(三)發現〉的架構,Mail 服務、Account 服務與 Message 服務的客戶端,目前是基於 Web 應用的 gossip。
假設現在 gossip 微網誌廣受歡迎,除了提供 Web 介面的 gossip 之外,不斷地有人反映,可不可以有其他的介面,像是手機 App、桌面 GUI 等,於是你打算加入其他客戶端了:
各個客戶端分別向服務發現伺服器查找服務,然後個別地跟 Mail 服務、Account 服務與 Message 服務互動,顯然地,曝露了個別服務細節,如果現在想要知道,哪個客戶端的使用程度等資訊,或者是對某些客戶端加以控管、限制 API 開放與否等,也會有麻煩,要在 Mail 服務、Account 服務與 Message 服務等分別實現這類需求嗎?
可以試著加上閘道來進行服務路由,客戶端只面對閘道,閘道判斷要與哪個服務進行互動,例如:
現在客戶端只需要知道閘道服務在哪,不用知道實際上背後有哪些服務,如果想要知道,哪個客戶端的使用程度等資訊,或者是對某些客戶端加以控管,這類橫切的需求,也可以在閘道上實現。
Netflix 的閘道方案使用 Zuul,雖然最新版本是 Zuul 2,不過 Spring Cloud 整合的版本僅 Zuul 1,這中間的插曲是 Zuul 2 原本預計在 2016 年底左右發佈,然而卻拖到了 2018 年 4 月,在這段期間,Spring 就自己搞了個 Spring Cloud Gateway,不打算整合 Zuul 2 了。
這邊還是先介紹一下 Spring Cloud 與 Zuul 的整合,因為相對來說,資料還是比較多的(而且 Spring Cloud Gateway 必須基於 Spring 5、Spring Boot 2)。使用 Spring Tool Suite 的話,可以選擇 Cloud Routing 中的 Zuul 作為 Starter,其中 build.gradle 會包含:
implementation('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
Zuul 可以獨立使用,或者與服務發現伺服器結合使用,後者是比較有彈性的,因此可以在 build.gradle 中加入:
implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
接著,必須在啟動應用程式的主類別上,加註 @EnableZuulProxy
:
package cc.openhome;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
接著來設定一下應用程式名稱、服務發現伺務器的資訊等,這邊定義在 bootstrap.properties 中:
spring.application.name=zuulsvr
server.port=5555
eureka.instance.preferIpAddress=true
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
management.endpoints.web.exposure.include: routes
在這邊開啟了 routes
這個端點,可以透過它來得知路由資訊,接著可以啟動各個服務,然後啟動 Zuul 專案,在連線 http://localhost:5555/actuator/routes
時,可以看到以下的回應:
{
"/acctsvi/**": "acctsvi",
"/configsvr/**": "configsvr",
"/msgsvi/**": "msgsvi",
"/emailsvi/**": "emailsvi"
}
Zuul 預設會使用註冊服務時的名稱,作為路由時的依據,例如,"/acctsvi/**": "acctsvi"
表示,對閘道的請求若是 http://localhost:5555/acctsvi/accountByName?username=caterpillar
之類,Zuul 會路由至 acctsvi
服務的實體位址,因此會得到以下的回應:
{
"name": "caterpillar",
"email": "caterpillar@openhome.cc",
"password": "$2a$10$CEkPOmd.Uid2FpIOHA6Cme1G.mvhWfelv2hPu7cxZ/vq2drnXaVo.",
"_links": {
"self": {
"href": "http://Justin-2017:8084/accountByNameEmail?username=caterpillar"
}
}
}
Zuul 路由調用服務時,逾時的預設是 1 秒,若必要,可以藉由在 bootstrap.properties 中設置 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
來變更,如果需要指定特定服務的逾時,可以將 default
改為服務名稱。
除了預設使用服務名稱自動建立路由之外,Zuul 也可以手動設置路由,像是設定服務名稱與請求路徑的對應、關閉服務名稱自動對應路徑、設定路徑前置、靜態 URL 等,通常這會在 application.properties 中設定,詳情可參考〈Router and Filter: Zuul〉。
例如,底下設置會設定服務名稱與請求路徑的對應、關閉服務名稱自動對應路徑、設定路徑前置為 /api
:
zuul.routes.acctsvi: /acct/**
zuul.routes.msgsvi: /msg/**
zuul.routes.emailsvi: /email/**
zuul.ignored-services: *
zuul.prefix: /api
這時若請求 http://localhost:5555/actuator/routes
,會得到底下回應:
{
"/api/acct/**": "acctsvi",
"/api/msg/**": "msgsvi",
"/api/email/**": "emailsvi"
}
在客戶端的部份,只需要修改請求路徑就可以了,若是使用 Feign,@FeignClient
可以指定至服務的對應路徑,例如:
package cc.openhome.model;
...略
@FeignClient(value = "zuulsvr/api/msg", fallback = MessageServiceFallback.class)
public interface MessageService {
@GetMapping("messagesBy?username={username}")
Resources<Message> messages(@PathVariable("username") String username);
@PostMapping("addMessage?username={username}&blabla={blabla}")
void addMessage(@PathVariable("username") String username, @PathVariable("blabla") String blabla);
@DeleteMapping("deleteMessage?username={username}&millis={millis}")
void deleteMessage(@PathVariable("username")String username, @PathVariable("millis") String millis);
@GetMapping("newestMessages?n={n}")
Resources<Message> newestMessages(@PathVariable("n") int n);
}
你可以在 Routing/Zuul 中找到以上的範例專案。