1. 程式人生 > >Spring Cloud系列(二十三) API閘道器服務Spring Cloud Zuul(Finchley.RC2版本)

Spring Cloud系列(二十三) API閘道器服務Spring Cloud Zuul(Finchley.RC2版本)

為什麼使用Spring Cloud Zuul?

通過前幾章的介紹,我們對於Spring Cloud Netflix 下的核心元件已經瞭解了大半,利用這些元件我們已經可以構建一個簡單的微服務架構系統,比如通過使用Spring Cloud Eureka實現高可用的服務註冊中心以及實現微服務的註冊與發現;通過Spring Cloud Ribbon或Feign實現服務間負載均衡的介面呼叫;同時,為了使分散式系統更健壯,對依賴的服務呼叫使用Spring Cloud Hystrix來進行包裝,實現執行緒隔離並進入熔斷機制,以免在微服務架構中因個別服務出現異常而引起級聯故障蔓延。通過上述思路,我們可以設計出類似下圖的基礎系統架構。

在該架構在,我們的服務叢集包含內部服務ServiceA和ServiceB,它們都會向Eureka Server叢集進行註冊與訂閱服務,而Open Service是一個對外的RESTFul API服務,它通過F5、Nginx等網路裝置或工具軟體實現對各個微服務的路由與負載均衡,並公開給外部的客戶端呼叫。

在接下來的幾篇部落格內,我們把視線聚焦在對外服務這塊內容,通常也稱為邊緣服務。首先需要肯定的是上面的架構實現系統功能是完全沒有問題的,但是這樣的架構也有不足之處會使運維人員或開發人員感到痛苦。

首先來說運維人員,他們需要保證客戶端發起的請求通過F5或Nginx等設施的路由和負載均衡分配後,把請求分發到各個服務例項,每當有例項增減或IP地址變動時,都需要手動維護這些資訊以保持例項資訊與中介軟體配置內容的一致性。如果系統太大,維護起來會變得困難。

然後再從開發人員的角度看,大多數情況下,為了保證對外服務的安全性,我們在服務端實現的微服務介面往往會有一定的許可權校驗機制,比如對使用者登入狀態的校驗等;同時為了防止客戶端在發起請求時被篡改等安全方面的考慮,還會有一些簽名校驗的機制存在。這時候,由於使用了微服務架構的理念,我們將原本處於一個應用中的多個模組拆成了多個應用,這些應用提供的介面都需要這些校驗邏輯,我們不得不在每個應用實現這樣一套校驗邏輯,隨著微服務規模的擴大,這些校驗邏輯變得越來越冗餘,如果有一天需要改的話,心態肯定爆炸了。

為了解決上面這些問題,API閘道器的概念應運而生。API閘道器是一個更為智慧的應用伺服器,它的定義類似於面向物件設計模式中的Facade模式,它的存在就像是整個微服務架構系統的門面一樣,所有的外部客戶端訪問都需要經過它來進行排程和過濾。它除了要實現請求路由、負載均衡、校驗過濾等功能外,還需要更多的能力,比如與服務治理框架的結合、請求轉發時的熔斷機制、服務的聚合等一系列高階功能。

Spring Cloud Zuul就是基於Netflix Zuul實現的API閘道器元件。那麼它是如何解決上面兩個問題的呢?

首先,對於路由規則與服務例項的維護問題。Spring Cloud Zuul和Spring Cloud Eureka進行整合,將自身註冊為Eureka服務治理下的應用,同時從Eureka中獲得了所有微服務的例項資訊。這就使得維護例項的工作交給服務治理框架來完成,不再需要人工介入。對於路由規則的維護,Zuul預設會將通過以服務名作為ContextPath的方式來建立路由對映,大部分情況下這麼的預設設定已經可以實現我們大部分的路由需求,除了一些特殊情況(比如相容老的URL)還需要做一些特別的配置,但是已經大大減少了運維的工作量。

其次,對於類似簽名校驗、登入校驗在微服務架構中的冗餘問題。理論上來說這些邏輯在本質上與微服務應用自身的業務沒有多大的關係,所以完全可以獨立成一個單獨的服務存在,只是它們被獨立出來後,並不是給各個微服務呼叫,而是在API閘道器服務上進行統一呼叫來對微服務介面做前置過濾,以實現對微服務介面的攔截和校驗。Spring Cloud Zuul提供了一套過濾器機制,它可以很好的支援這樣的任務。開發者可以通過使用Zuul來建立各種校驗過濾器,然後指定哪些規則的請求需要執行哪些校驗邏輯,只有通過校驗的才會被路由到具體的服務介面,不然就返回錯誤提示。通過這樣的改造,各個業務層的微服務應用就不再需要非業務性質的校驗邏輯了,這使得我們的微服務更加專注於業務邏輯的開發。

所以在一個微服務系統架構中,API閘道器服務的使用幾乎成為了必然的選擇。

構建閘道器

首先。在實現各種API閘道器服務的高階功能之前,我們需要做一些準備各種,必然,構建最基本的API閘道器服務,並且搭建幾個用於路由和過濾使用的微服務應用等。對於微服務應用,我們可以直接使用之前實現的hello-service服務和feign-consumer服務,不清楚的可以看一下我的前幾篇部落格。雖然之前一直把feign-consumer服務當作消費者,其實在Eureka的服務註冊與發現體系中,每個服務既是服務提供者也是服務消費者,所以這裡也可以把feign-consumer服務當作服務提供者,它提供的介面就可以當作它提供的服務。現在我來構建一個API閘道器服務。

第一步,建立一個基本的Spring Boot工程,命名為api-gateway-vFinchley.RC2。

第二步,修改pom.xml,新增對Zuul的依賴

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.2.RELEASE</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>1.8</java.version>
	<spring-cloud.version>Finchley.RC2</spring-cloud.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-tomcat</artifactId>
		<scope>provided</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

檢視Zuul的依賴發現它除了包括Netflix Zuul的核心依賴zuul-core以外,還包含這些閘道器功能需要的重要依賴:

  1. spring-cloud-starter-netflix-hystrix:該依賴用來在閘道器服務中實現對微服務轉發時候的保護機制,通過執行緒隔離和斷路器,放在微服務的故障引起API閘道器資源無法釋放,從而影響其他應用的對外服務。
  2. spring-cloud-starter-netflix-ribbon:該依賴用於實現在閘道器服務進行路由轉發時候的客戶端負載均衡以及請求重試。

第三步,修改應用主類,新增@EnableZuulProxy註解,開啟Zuul的API閘道器服務功能。

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

第四步,修改application.yml檔案

spring:
  application:
    name: api-gateway #為服務命名
server:
  port: 5111
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:1111/eureka/ #指定服務註冊中心位置
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: hello-service
    api-b:
      path: /api-b/**
      serviceId: feign-consumer

首先指定服務註冊中心的地址為http://localhost:1111/eureka/,服務的埠為5111,服務名為api-gateway;以/api-a/ 開頭的請求都轉發給hello-service服務;以/api-b/開頭的請求都轉發給feign-consumer服務。

測試

啟動服務註冊中心,即eureka-server-vFinchley.Rc2工程

啟動服務提供者hello-service,即eureka-client-vFinchley.Rc2 工程

啟動服務消費者feign-consumer,即feign-consumer-vFinchley.RC2工程

啟動API閘道器服務api-gateway,即api-gateway-vFinchley.RC2工程。

這就簡單的構建了一個API閘道器服務,而且實現了請求路由的功能。

注意:如果你請求後報錯com.netflix.zuul.exception.ZuulException: Forwarding error

檢視你的依賴裡是否有spring-cloud-starter-netflix-eureka-client,沒有的話會報這個錯誤,至於為什麼報這個錯,在說請求路由時會解釋。

請求路由

傳統路由方式

使用Spring Cloud Zuul實現路由功能非常簡單,只需對api-gateway服務增加一些關於路由規則的配置,就能實現傳統的路由轉發功能。比如:

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

該配置定義了發往API閘道器的請求中,所有符合/api-a/**規則的訪問都將被路由轉發到http://localhost:2222/地址上,也就是說當我們請求http://localhost:5111/api-b/hello時,API閘道器服務會將該請求轉發到http://localhost:2222/hello提供的微服務介面上。其中配置屬性zuul.routes.api-a.path中的api-a部分為路由的名字,可以任意定義,但是一組path和url對映的路由名必須相同。

修改上面示例中的配置,如下:

zuul:
  routes:
    api-a:
      path: /api-a/**
      url: http://localhost:2222/
    api-b:
      path: /api-b/**
      url: http://localhost:5001/ 

面向服務的路由

很顯然,傳統路由的配置方式對我們來說並不友好,它同樣需要運維人員花費大量時間來維護path和url的關係。為了解決這個問題,Spring Cloud Zuul實現了與Spring Cloud Eureka的無縫整合,我們可以讓路由的path不是對映具體的url,而是對映到某個具體的服務,而具體的url交給Spring Cloud Eureka的服務發現機制去自動維護,這就是面向服務的路由。在上面構建閘道器的示例中使用的就是面向服務的路由。使用這種方式必須注意幾點:

  1. 引入了Spring Cloud Eureka依賴,上面示例中說到報錯com.netflix.zuul.exception.ZuulException: Forwarding error就是因為沒有引入依賴,導致無法註冊到服務註冊中心。
  2. 指定了服務註冊中心

配置方式我就不寫了,上面有。

通過面向服務的方式,我們不再需要再為各個路由維護微服務應用的具體例項的位置,而是簡單的通過path和serviceId的對映,使維護工作變得簡單,建議使用這種方式。