使用 Zuul


目前的 gossip,大致還是維持著〈gossip 服務(三)發現〉的架構,Mail 服務、Account 服務與 Message 服務的客戶端,目前是基於 Web 應用的 gossip。

假設現在 gossip 微網誌廣受歡迎,除了提供 Web 介面的 gossip 之外,不斷地有人反映,可不可以有其他的介面,像是手機 App、桌面 GUI 等,於是你打算加入其他客戶端了:

使用 Zuul

各個客戶端分別向服務發現伺服器查找服務,然後個別地跟 Mail 服務、Account 服務與 Message 服務互動,顯然地,曝露了個別服務細節,如果現在想要知道,哪個客戶端的使用程度等資訊,或者是對某些客戶端加以控管、限制 API 開放與否等,也會有麻煩,要在 Mail 服務、Account 服務與 Message 服務等分別實現這類需求嗎?

可以試著加上閘道來進行服務路由,客戶端只面對閘道,閘道判斷要與哪個服務進行互動,例如:

使用 Zuul

現在客戶端只需要知道閘道服務在哪,不用知道實際上背後有哪些服務,如果想要知道,哪個客戶端的使用程度等資訊,或者是對某些客戶端加以控管,這類橫切的需求,也可以在閘道上實現。

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 中找到以上的範例專案。