1. 程式人生 > >夢想起飛的地方.........

夢想起飛的地方.........

微服務場景下,每一個微服務對外暴露了一組細粒度的服務。客戶端的請求可能會涉及到一串的服務呼叫,如果將這些微服務都暴露給客戶端,那麼客戶端需要多次請求不同的微服務才能完成一次業務處理,增加客戶端的程式碼複雜度。另外,對於微服務我們可能還需要服務呼叫進行統一的認證和校驗等等。微服務架構雖然可以將我們的開發單元拆分的更細,降低了開發難度,但是如果不能夠有效的處理上面提到的問題,可能會造成微服務架構實施的失敗。

Zuul參考GOF設計模式中的Facade模式,將細粒度的服務組合起來提供一個粗粒度的服務,所有請求都匯入一個統一的入口,那麼整個服務只需要暴露一個api,對外遮蔽了服務端的實現細節,也減少了客戶端與伺服器的網路呼叫次數。這就是API服務閘道器(API Gateway)服務。我們可以把API服務閘道器理解為介於客戶端和伺服器端的中間層,所有的外部請求都會先經過API服務閘道器。因此,API服務閘道器幾乎成為實施微服務架構時必須選擇的一環。

Spring Cloud Netflix的Zuul元件可以做反向代理的功能,通過路由定址將請求轉發到後端的粗粒度服務上,並做一些通用的邏輯處理。

通過Zuul我們可以完成以下功能:

  • 動態路由
  • 監控與審查
  • 身份認證與安全
  • 壓力測試: 逐漸增加某一個服務叢集的流量,以瞭解服務效能;
  • 金絲雀測試
  • 服務遷移
  • 負載剪裁: 為每一個負載型別分配對應的容量,對超過限定值的請求棄用;
  • 靜態應答處理

1. 構建閘道器

1.1 構建Zuul-Server

編寫pom.xml檔案

Zuul-Server是一個標準的Spring Boot應用,所以還是繼承自我們之前的parent:

<?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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>twostepsfromjava.cloud</groupId>
        <artifactId>twostepsfromjava-cloud-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../parent</relativePath>
    </parent>

    <artifactId>zuul-server</artifactId>
    <name>Spring Cloud Sample Projects: Zuul Proxy Server</name>

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

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

這裡我們增加了spring-cloud-starter-zuul的依賴。

編寫啟動類

/**
 * TwoStepsFromJava Cloud -- Zuul Proxy 伺服器
 *
 * @author CD826([email protected])
 * @since 1.0.0
 */
@EnableZuulProxy
@SpringBootApplication
public class Application {

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

}

這裡我們增加了對主應用類增加了@EnableZuulProxy,用以啟動Zuul的路由服務。

編寫配置檔案application.properties

server.port=8280

spring.application.name=ZUUL-PROXY

eureka.client.service-url.defaultZone=http://localhost:8260/eureka

這裡定義服務名稱為: ZUUL-PROXY,埠設為: 8280

1.2 構建User-Service

為了後面的則是我們再增加一個微服務: 使用者服務。

編寫pom.xml檔案

同樣繼承自我們之前的parent:

<?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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>twostepsfromjava.cloud</groupId>
        <artifactId>twostepsfromjava-cloud-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../parent</relativePath>
    </parent>

    <artifactId>user-service</artifactId>
    <name>Spring Cloud Sample Projects: User Service Server</name>

    <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>${project.groupId}</groupId>
            <artifactId>service-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

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

編寫啟動類

啟動類和之前的Product-Service一樣,所以這裡不再列出來。

編寫服務介面

示例的服務介面非常簡單,就是根據給定的登入名稱查詢一個使用者資訊。如下:

/**
 * User API服務
 *
 * @author CD826([email protected])
 * @since 1.0.0
 */
@RestController
@RequestMapping("/users")
public class UserEndpoint {
    protected Logger logger = LoggerFactory.getLogger(UserEndpoint.class);

    @Value("${server.port:2200}")
    private int serverPort = 2200;

    @RequestMapping(value = "/{loginName}", method = RequestMethod.GET)
    public User detail(@PathVariable String loginName) {
        String memos = "I come form " + this.serverPort;
        return new User(loginName, loginName, "/avatar/default.png", memos);
    }
}

其中User類定義在之前的service-api專案中,程式碼如下:

/**
 * 使用者資訊DTO物件
 *
 * @author CD826([email protected])
 * @since 1.0.0
 */
public class User {
    private static final long serialVersionUID = 1L;

    // ========================================================================
    // fields =================================================================
    private String loginName;                                   // 使用者登陸名稱
    private String name;                                        // 使用者姓名
    private String avatar;                                      // 使用者頭像
    private String memos;                                       // 資訊備註

    // ========================================================================
    // constructor ============================================================
    public User() {
    }

    public User(String loginName, String name, String avatar, String memos) {
        this.loginName = loginName;
        this.name = name;
        this.avatar = avatar;
        this.memos = memos;
    }

    // ==================================================================
    // setter/getter ====================================================
    // ... 省略,請自行補充 ...
}

編寫配置檔案application.properties

server.port=2200

spring.application.name=USER-SERVICE

eureka.client.service-url.defaultZone=http://localhost:8260/eureka

這裡定義服務名稱為: USER-SERVICE,預設埠設為: 2200

程式碼修改,就是這麼多,下面讓我們啟動進行測試。

1.3 啟動測試

啟動各服務

請按照下面的順序啟動各伺服器:

  1. Service-discovery
  2. Product-Service
  3. User-Service(2200)
  4. User-Service(2300): java -jar user-service-1.0.0-SNAPSHOT.jar --server.port=2300
  5. Zuul-Server

Ok, 服務啟動後我們可以在Eureka伺服器看到如下介面:

Zuul-proxy-010

Zuul-proxy-010

這裡我們啟動兩個User-Service主要是為了後面進行負載均衡測試使用。

測試路由服務

Zuul-proxy-020

Zuul-proxy-020

Zuul-proxy-030

Zuul-proxy-030

可見,Zuul-Server已經幫我們路由到相應的微服務。

負載均衡測試

接下來我們測試一下負載均衡是否可以正常工作。前面我們已經啟動了兩個User-Service微服務,埠分別為:2200和2300。我們多次在瀏覽器中輸入以下地址: http://localhost:8280/user-service/users/admin進行請求,我們將會看到以下資訊會在螢幕中交替輸出:

{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2200"}
{"loginName":"admin","name":"admin","avatar":"/avatar/default.png","memos":"I come form 2300"}

可見,負載均衡也是正常工作的。

Hystrix容錯與監控測試

之前我們是在Mall-Web專案中整合Hystrix的監控,那麼我們啟動該服務。然後在Hystrix Dashboard中輸入: http://localhost:8280/hystrix.stream,然後進行監控,那麼我們將看到如下介面:

Zuul-proxy-050

Zuul-proxy-050

這說明,Zuul已經整合了Hystrix。

spring-cloud-starter-zuul本身已經集成了hystrix和ribbon,所以Zuul天生就擁有執行緒隔離和斷路器的自我保護能力,以及對服務呼叫的客戶端負載均衡功能。但是,我們需要注意,當使用path與url的對映關係來配置路由規則時,對於路由轉發的請求則不會採用HystrixCommand來包裝,所以這類路由請求就沒有執行緒隔離和斷路器保護功能,並且也不會有負載均衡的能力。因此,我們在使用Zuul的時候儘量使用path和serviceId的組合進行配置,這樣不僅可以保證API閘道器的健壯和穩定,也能用到Ribbon的客戶端負載均衡功能。

2. Zuul配置

2.1 路由配置詳解

1) 服務路由預設規則

當我們構建API服務閘道器時引入Eureka時,那麼Zuul會自動為每個服務都建立一個預設路由規則: 訪問路徑的字首為serviceId配置的服務名稱,也就是之前為什麼我們能夠所使用:

http://localhost:8280/product-service/products

來訪問Product-Service中所提供的products服務端點的原因。

2) 自定義微服務訪問路徑

配置格式為: zuul.routes.微服務Id = 指定路徑,如:

zuul.routes.user-service = /user/**

所要配置的路徑可以指定一個正則表示式來匹配路徑,因此,/user/*只能匹配一級路徑,但是通過/user/**可以匹配所有以/user/開頭的路徑。

3) 忽略指定微服務

配置格式為: zuul.ignored-services=微服務Id1,微服務Id2...,多個微服務之間使用逗號分隔。如:

zuul.ignored-services=user-service,product-service

4) 同時指定微服務Id和對應路徑

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B

5) 同時指定微服務Url和對應路徑

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8080/api-a

如之前所述,通過url配置的路由不會由HystrixCommand來執行,自然,也就得不到Ribbon的負載均衡、降級、斷路器等功能。所以在實施儘量使用serviceId進行配置,也可以採用下面的配置方式。

6) 指定多個服務例項及負載均衡

如果需要配置多個服務例項,則配置如下:

zuul.routes.user.path: /user/**
zuul.routes.user.serviceId: user

ribbon.eureka.enabled=false
user.ribbon.listOfServers: http://192.168.1.10:8081, http://192.168.1.11:8081

7) forward跳轉到本地url

zuul.routes.user.path=/user/**
zuul.routes.user.url=forward:/user

8) 路由字首

可以通過zuul.prefix可為所有的對映增加統一的字首。如: /api。預設情況下,代理會在轉發前自動剝離這個字首。如果需要轉發時帶上字首,可以配置: zuul.stripPrefix=false來關閉這個預設行為。例如:

zuul.routes.users.path=/myusers/**
zuul.routes.users.stripPrefix=false

注意: zuul.stripPrefix只會對zuul.prefix的字首起作用。對於path指定的字首不會起作用。

9) 路由配置順序

如果想按照配置的順序進行路由規則控制,則需要使用YAML,如果是使用propeties檔案,則會丟失順序。例如:

zuul:
  routes:
    users:
      path: /myusers/**
    legacy:
      path: /**

上例如果是使用properties檔案進行配置,則legacy就可能會先生效,這樣users就沒效果了。

10) 自定義轉換

我們也可以一個轉換器,讓serviceId和路由之間使用正則表示式來自動匹配。例如:

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
    return new PatternServiceRouteMapper(
        "(?<name>^.+)-(?<version>v.+$)",
        "${version}/${name}");
}

這樣,serviceId為“users-v1”的服務,就會被對映到路由為“/v1/users/”的路徑上。任何正則表示式都可以,但是所有的命名組必須包括servicePattern和routePattern兩部分。如果servicePattern沒有匹配一個serviceId,那就會使用預設的。在上例中,一個serviceId為“users”的服務,將會被對映到路由“/users/”中(不帶版本資訊)。這個特性預設是關閉的,而且只適用於已經發現的服務。

2.2 Zuul的Header設定

敏感Header設定

同一個系統中各個服務之間通過Headers來共享資訊是沒啥問題的,但是如果不想Headers中的一些敏感資訊隨著HTTP轉發洩露出去話,需要在路由配置中指定一個忽略Header的清單。

預設情況下,Zuul在請求路由時,會過濾HTTP請求頭資訊中的一些敏感資訊,預設的敏感頭資訊通過zuul.sensitiveHeaders定義,包括CookieSet-CookieAuthorization。配置的sensitiveHeaders可以用逗號分割。

對指定路由的可以用下面進行配置:

# 對指定路由開啟自定義敏感頭
zuul.routes.[route].customSensitiveHeaders=true 
zuul.routes.[route].sensitiveHeaders=[這裡設定要過濾的敏感頭]

設定全域性:

zuul.sensitiveHeaders=[這裡設定要過濾的敏感頭]

忽略Header設定

如果每一個路由都需要配置一些額外的敏感Header時,那你可以通過zuul.ignoredHeaders來統一設定需要忽略的Header。如:

zuul.ignoredHeaders=[這裡設定要忽略的Header]

在預設情況下是沒有這個配置的,如果專案中引入了Spring Security,那麼Spring Security會自動加上這個配置,預設值為: Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries

此時,如果還需要使用下游微服務的Spring Security的Header時,可以增加下面的設定:

zuul.ignoreSecurityHeaders=false

2.3 Zuul Http Client

Zuul的Http客戶端支援Apache Http、Ribbon的RestClient和OkHttpClient,預設使用Apache HTTP客戶端。可以通過下面的方式啟用相應的客戶端:

# 啟用Ribbon的RestClient
ribbon.restclient.enabled=true

# 啟用OkHttpClient
ribbon.okhttp.enabled=true

如果需要使用OkHttpClient需要注意在你的專案中已經包含com.squareup.okhttp3相關包。

3. Zuul容錯與回退

我們再來仔細看一下之前Hystrix的監控介面:

Zuul-proxy-080

Zuul-proxy-080

請注意,Zuul的Hystrix監控的粒度是微服務,而不是某個API,也就是所有經過Zuul的請求都會被Hystrix保護起來。假如,我們現在把Product-Service服務關閉,再來訪問會出現什麼結果呢?結果可能不是我們所想那樣,如下:

Zuul-proxy-060

Zuul-proxy-060

呃,比較鬱悶是麼!那麼如何為Zuul實現容錯與回退呢?

Zuul提供了一個ZuulFallbackProvider介面,通過實現該介面就可以為Zuul實現回退功能。那麼讓我們改造之前的Zuul-Server

3.1 實現回退方法

程式碼如下:

/**
 * Product Service服務失敗回退處理
 *
 * @author CD826([email protected])
 * @since 1.0.0
 */
@Component
public class ProductServiceFallbackProvider implements ZuulFallbackProvider {
    protected Logger logger = LoggerFactory.getLogger(ProductServiceFallbackProvider.class);

    @Override
    public String getRoute() {
        // 注意: 這裡是route的名稱,不是服務的名稱,
        // 如果這裡寫成大寫PRODUCT-SERVICE將無法起到回退作用
        return "product-service";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("商品服務暫不可用,請稍後重試!".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                return headers;
            }
        };
    }
}

需要說明的是:

  • getRoute方法返回了我們要為那個微服務提供回退。這裡需要注意的返回的值是route的名稱,不是服務的名稱,不能夠寫為: PRODUCT-SERVICE,否則該回退將不起作用;
  • fallbackResponse方法返回ClientHttpResponse物件,作為我們的回退響應。這裡實現非常簡單僅僅是返回:商品服務暫不可用,請稍後重試! 的提示。

3.2 重啟測試

重啟Zuul-Server,再重複上面的實驗,將會看到以下介面:

Zuul-proxy-070

Zuul-proxy-070

說明,回退方法已經起作用了。如果你的沒有起作用,那麼仔細檢查一下getRoute的返回是否正確。