1. 程式人生 > >03. Spring Cloud--服務路由

03. Spring Cloud--服務路由

目錄

3.1 簡介

在向微服務框架這樣的分散式架構中,需要確保跨多個服務呼叫的關鍵行文的正常執行,如安全、日誌記錄和使用者跟蹤。要實現此功能,開發人員需要在所有服務中始終如一地強制這些特性,而不需要每個開發團隊都構建自己的解決方案。雖然可以使用公共庫或擴家來幫助在單個服務中直接構建這些功能,但這樣做會造成3個影響。

  1. 在構建的每個服務中很難始終實現這些功能。開發人員專注於交付功能,在每日的快速開發工作中,他們很容易忘記實現服務日誌記錄或跟蹤。
  2. 很難正確地實現這些功能。對每個正在開發的服務進行諸如微服務安全的建立與配置可能是很痛苦的。將實現橫切關注點(cross-cutting concern,如安全問題)的責任推給各個開發團隊,大大增加了開發人員沒有正確實現或忘記實現這些功能的可能性。
  3. 這會在所有服務中建立多個強依賴。開發人員將本該寫在通用程式碼中的功能,分別寫在各個服務中的次數越多,更新迭代的時候就越困難。

為了解決這個問題,需要將這些橫切關注點抽象成一個獨立且作為應用程式中所有微服務呼叫的過濾器和路由器的服務。這種橫切關注點被稱為服務閘道器

(service gateway)。服務客戶端不再直接呼叫服務。取而代之的是,服務閘道器作為單個策略執行點,所有呼叫都通過服務閘道器進行路由,然後被路由到最終目的地。

使用服務閘道器時的注意事項:
在實現服務閘道器功能時,我們通常將服務閘道器置於所有服務例項的前面,但是這樣做有一定的危險性,因為它會是服務閘道器成為瓶頸。
為了儘量避免瓶頸的出現,我們要保持為服務閘道器編寫的程式碼是無狀態的。不要在記憶體中為服務閘道器儲存任何資訊。如果不小心,就有可能限制閘道器的伸縮性,導致不得不確保資料在所有服務閘道器例項中被複制。

3.2 Netflix Zuul

Spring Cloud集成了Netflix開源專案Zuul。Zuul是一個服務閘道器,它非常容易通過Spring Cloud註解進行建立和使用。Zuul提供了許多功能,具體包括以下幾個:

  • 將應用程式中的所有服務的路由對映到一個URL – 在Zuul中,開發人員可以定義多個路由條目,使路由對映非常細粒度(每個服務端點都有自己的路由對映)。而Zuul最常見的用力是構建一個單一的入口點,所有服務客戶端呼叫都將經過這個入口點。
  • 構建可以對通過閘道器的請求進行檢查和操作的過濾器 – 這些過濾器允許開發人員在程式碼中諸如策略執行點,以一致的方式對所有服務呼叫執行大量操作。

3.2.1 構建Zull伺服器

在pom.xml中匯入依賴:

<!--實現Zuul伺服器的主要依賴-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--將zuul伺服器註冊到Eureka Server上-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--通過Spring Config Server實現動態路由-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--通過Spring Config Server實現動態路由-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--用於獲取Zuul的路由對映表-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在bootstrap.properties中加入配置:

spring.application.name=zuul-server
server.port=9002
eureka.client.service-url.defaultZone=http://localhost:9000/eureka/

spring.rabbitmq.host=192.168.3.12
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.service-id=config-server
spring.cloud.config.profile=dev

# 開放一個API介面,用於獲取路由表
management.endpoints.web.exposure.include=routes

至此,我們就搭建好了Zuul伺服器。

怎樣獲取路由表:
使用GET方法,訪問以下URL:http://<zuul伺服器IP地址>:<zuul伺服器埠號>/actuator/routes,可以獲取到路由表。

3.2.2 通過服務發現自動對映路由

Zuul的所有路由對映都是通過在properties或者yml檔案中定義路由來完成的。但是,Zuul可以根據其服務ID自動路由請求,而不需要配置,比如有一個服務的ID的order-eureka,那麼路由對映中就會自動多出一行"/order-eureka/**": "order-eureka"

使用自動對映路由有一個好處,當我們想Eureka伺服器中新增新服務的時候,Zuul可以將新服務新增到路由對映表中,而無需修改Zuul。

3.2.3 使用服務發現手動對映路由

Zuul允許開發人員更細粒度地明月定義路由對映,而不是單純依賴服務的Eureka服務ID建立的自動路由。假設開發人員希望通過縮短服務名稱來簡化路由,而不是通過預設路由在Zuul中訪問服務,做法如下:

# 將order-eureka服務的路由對映為/order/**
zuul.routes.order.service-id=order-rureka
zuul.routes.order.path=/order/**

如果想要排除Eureka服務ID路由的自動對映,只提供自定義的服務路由,可以這樣來做:

# 排除order-rureka的自動路由對映
zuul.ignored-services=order-rureka

如果要排除所有基於Eureka的路由,可以將ignored-services屬性設定為*

3.2.4 使用靜態URL手動對映路由

Zuul可以用來路由那些不受Eureka管理的服務。在這種情況下,可以建立Zuul直接路由到一個靜態定義的URL。做法如下:

zuul.routes.angular.path=/angular/**
zuul.routes.angular.url=http://<serverIp>:<port>

3.3 過濾器

雖然通過Zuul閘道器代理所有請求確實可以簡化服務呼叫,但是在想要編寫應用於所有流經閘道器的服務呼叫的自定義邏輯時,Zuul的真正威力才發揮出來,在大多數情況下,這種自定義邏輯用語強制執行一組一致的應用程式策略,如安全性、日誌記錄和對所有服務的追蹤。

這些應用程式策略被認為是橫切關注點,因為開發人員希望將它們應用於應用程式中的所有服務,而無須修改每個服務來實現它們。通過這種方式,Zuul 過濾器可以按照與J2EE servlet 過濾器或Spring Aspect 類似的方式來使用。這種方式可以攔截大量行為,並且在原始編碼人員意識不到變化的情況下,對呼叫的行為進行裝飾或更改。servlet 過濾器或Spring Aspect 被本地化為特定的服務,而使用Zuul 和Zuul 過濾器允許開發人員為通過Zuul 路由的所有服務實現橫切關注點。

Zuul允許開發人員使用Zuul閘道器內的過濾器構建自定義邏輯。過濾器可用於實現每個服務請求在執行時都會經過的業務邏輯。Zuul支援以下3中型別的過濾器。

  1. 前置過濾器 – 前置過濾器在Zuul將實際請求傳送到目的地之前被呼叫。前置過濾器通常執行確保服務具有一致的訊息格式(如,關鍵的HTTP Header是否設定妥當)的任務,或者充當看門人,確保呼叫該服務的使用者已經通過驗證和授權。
  2. 後置過濾器 – 後置過濾器在目標服務被呼叫並將響應傳送給客戶端後被呼叫。通常後置過濾器會用來記錄從目標服務返回的響應、處理錯誤或稽核對敏感資訊的響應。
  3. 路由過濾器 – 路由過濾器用於在呼叫目標服務之前攔截呼叫。通常使用路由過濾器來確定是否需要進行某些級別的動態路由。例如,本章的後面講使用路由級別的過濾器,該過濾器將在同一服務的兩個不同版本之間進行路由,以便將一小部分服務呼叫路由到服務的新版本,而不是路由到現有的服務。這樣就能夠在不讓每個人都使用新服務的情況下,讓少量的使用者體驗新功能。

3.3.1 過濾器呼叫流程

流程:

  1. 在請求進入Zuul閘道器時,Zuul呼叫所有在Zuul閘道器中定義的前置過濾器。前置過濾器可以在HTTP請求到達實際服務之前對HTTP請求進行檢查和修改。前置過濾器不能講使用者重定向到不同的端點或服務。
  2. 在針對Zuul的傳入請求執行前置過濾器之後,Zuul將執行已定義的路由過濾器。路由過濾器可以更改服務所指向的目的地。
  3. 路由過濾器可以將服務呼叫重定向到Zuul伺服器被配置的傳送路由意外的位置。但Zuul路由過濾器不會執行HTTP重定向,而是會終止傳入的HTTP請求,然後代表原始呼叫者呼叫路由。這意味著路由過濾器必須完全負責動態路由的呼叫,並且不能執行HTTP重定向。
  4. 如果路由過濾器沒有動態地將呼叫者重定向到新路由,Zuul伺服器將傳送到最初的目標路由。
  5. 目標路由被呼叫之後,Zuul後置過濾器將被呼叫。後置過濾器可以檢查和修改來自被呼叫服務的響應。

3.3.2 前置過濾器例項

我們要做一個前置過濾器,用來檢測URL引數裡面有沒有token,當存在token時不做任何處理,當不存在token時,返回一個401異常。
程式碼如下:

@Component
public class TokenFilter extends ZuulFilter {
    /**
     * filterType()方法用於告訴Zuul,該過濾器是前置過濾器、後置過濾器還是路由過濾器
     * @return
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * filterOrder()方法返回一個整數值,只是不同型別的過濾器的執行順序
     * 數值越小,越優先執行
     * @return
     */
    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    /**
     * shouldFilter()方法返回一個布林值來指示該過濾器是否要執行
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
         HttpServletRequest request = requestContext.getRequest();
         String token = request.getParameter("token");
         if (StringUtils.isBlank(token)) {
             requestContext.setSendZuulResponse(false);
         	 requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
         }
        return null;
    }
}

3.3.3 後置過濾器例項

我們要做一個後置過濾器,當請求結果返回給客戶端的時候,往HTTP Header中新增資料。
程式碼如下:

@Component
public class AddHeaderFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setHeader("token", "this is token");
        return null;
    }
}