
前面的文章我們介紹了,Eureka用于服務的注冊于發現,Feign支援服務的調用以及均衡負載,Hystrix處理服務的熔斷防止故障擴散,Spring Cloud Config服務叢集配置中心,似乎一個微服務架構已經完成了。
我們還是少考慮了一個問題,外部的應用如何來通路内部各種各樣的微服務呢?在微服務架構中,後端服務往往不直接開放給調用端,而是通過一個API網關根據請求的url,路由到相應的服務。當添加API網關後,在第三方調用端和服務提供方之間就建立了一面牆,這面牆直接與調用方通信進行權限控制,後将請求均衡分發給背景服務端。
為什麼需要API Gateway
- 簡化用戶端調用複雜度
在微服務架構模式下後端服務的執行個體數一般是動态的,對于用戶端而言很難發現動态改變的服務執行個體的通路位址資訊。是以在基于微服務的項目中為了簡化前端的調用邏輯,通常會引入API Gateway作為輕量級網關,同時API Gateway中也會實作相關的認證邏輯進而簡化内部服務之間互相調用的複雜度。
- 資料裁剪以及聚合
通常而言不同的用戶端對于顯示時對于資料的需求是不一緻的,比如手機端或者Web端又或者在低延遲的網絡環境或者高延遲的網絡環境。
是以為了優化用戶端的使用體驗,API Gateway可以對通用性的響應資料進行裁剪以适應不同用戶端的使用需求。同時還可以将多個API調用邏輯進行聚合,進而減少用戶端的請求數,優化用戶端使用者體驗。
- 多管道支援
當然我們還可以針對不同的管道和用戶端提供不同的API Gateway,對于該模式的使用由另外一個大家熟知的方式叫Backend for front-end, 在Backend for front-end模式當中,我們可以針對不同的用戶端分别建立其BFF,進一步了解BFF可以參考這篇文章:Pattern: Backends For Frontends
- 遺留系統的微服務化改造
對于系統而言進行微服務改造通常是由于原有的系統存在或多或少的問題,比如技術債務,代碼品質,可維護性,可擴充性等等。API Gateway的模式同樣适用于這一類遺留系統的改造,通過微服務化的改造逐漸實作對原有系統中的問題的修複,進而提升對于原有業務響應力的提升。通過引入抽象層,逐漸使用新的實作替換舊的實作。
Zuul介紹
介紹
服務網關是微服務架構中一個不可或缺的部分。通過服務網關統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了權限控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時将權限控制這些較重的非業務邏輯内容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
Zuul是提供動态路由,監控,彈性,安全等的邊緣服務。Zuul 相當于是裝置和Netflix流應用的Web網站後端所有請求的前門。Zuul可以适當的對多個Amazon Auto Scaling Groups進行路由請求。
zuul執行流程
通過圖檔可以清晰看出執行過程,在微服務中後端各種引用,利用zuul進行合理調用還是很有必要的,例如 負載、限流、監控、安全等等功能。
使用執行個體
準備工作
在使用Zuul之前,我們先建構一個服務注冊中心、以及兩個簡單的服務,比如:我建構了一個service-A,一個service-B。然後啟動eureka-server和這兩個服務。通過通路eureka-server,我們可以看到service-A和service-B已經注冊到了服務中心。
開始使用Zuul
引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通過指定serviceId的方式,eureka依賴不需要,但是為了對服務叢集細節的透明性,還是用serviceId來避免直接引用url的方式吧。
應用主類使用@EnableZuulProxy注解開啟Zuul
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
這裡用了@SpringCloudApplication注解,之前沒有提過,通過源碼我們看到,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的還是簡化配置。這幾個注解的具體作用這裡就不做詳細介紹了,之前的文章已經都介紹過。
application.properties中配置Zuul應用的基礎資訊,如:應用名、服務端口等。
spring.application.name=api-gateway
server.port=5555
Zuul配置
完成上面的工作後,Zuul已經可以運作了,但是如何讓它為我們的微服務叢集服務,還需要我們另行配置,下面詳細的介紹一些常用配置内容。
服務路由
通過服務路由的功能,我們在對外提供服務的時候,隻需要通過暴露Zuul中配置的調用位址就可以讓調用方統一的來通路我們的服務,而不需要了解具體提供服務的主機資訊了。
在Zuul中提供了兩種映射方式:
# routes to url
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/
該配置定義了,所有到Zuul的中規則為:/api-a-url/**的通路都映射到http://localhost:2222/上,也就是說當我們通路http://localhost:5555/api-a-url/add?a=1&b=2的時候,Zuul會将該請求路由到:http://localhost:2222/add?a=1&b=2上。
其中,配置屬性zuul.routes.api-a-url.path中的api-a-url部分為路由的名字,可以任意定義,但是一組映射關系的path和url要相同,下面講serviceId時候也是如此。
通過url映射的方式對于Zuul來說,并不是特别友好,Zuul需要知道我們所有為服務的位址,才能完成所有的映射配置。而實際上,我們在實作微服務架構時,服務名與服務執行個體位址的關系在eureka server中已經存在了,是以隻需要将Zuul注冊到eureka server上去發現其他服務,我們就可以實作對serviceId的映射。例如,我們可以如下配置
# routes to serviceId
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
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
針對我們在準備工作中實作的兩個微服務service-A和service-B,定義了兩個路由api-a和api-b來分别映射。另外為了讓Zuul能發現service-A和service-B,也加入了eureka的配置。
接下來,我們将eureka-server、service-A、service-B以及這裡用Zuul實作的服務網關啟動起來,在eureka-server的控制頁面中,我們可以看到分别注冊了service-A、service-B以及api-gateway
嘗試通過服務網關來通路service-A和service-B,根據配置的映射關系,分别通路下面的url
- http://localhost:5555/api-a/add?a=1&b=2:通過serviceId映射通路service-A中的add服務
- http://localhost:5555/api-b/add?a=1&b=2:通過serviceId映射通路service-B中的add服務
- http://localhost:5555/api-a-url/add?a=1&b=2:通過url映射通路service-A中的add服務
推薦使用serviceId的映射方式,除了對Zuul維護上更加友好之外,serviceId映射方式還支援了斷路器,對于服務故障的情況下,可以有效的防止故障蔓延到服務網關上而影響整個系統的對外服務
- 服務過濾
在完成了服務路由之後,我們對外開放服務還需要一些安全措施來保護用戶端隻能通路它應該通路到的資源。是以我們需要利用Zuul的過濾器來實作我們對外服務的安全控制。 在服務網關中定義過濾器隻需要繼承ZuulFilter抽象類實作其定義的四個抽象函數就可對請求進行攔截與過濾。 比如下面的例子,定義了一個Zuul過濾器,實作了在請求被路由之前檢查請求中是否有accessToken參數,若有就進行路由,若沒有就拒絕通路,傳回401 Unauthorized錯誤。 自定義過濾器的實作,需要繼承ZuulFilter,需要重寫實作下面四個方法: filterType:傳回一個字元串代表過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型,具體如下: i. pre:可以在請求被路由之前調用 ii. routing:在路由請求時候被調用 iii. post:在routing和error過濾器之後被調用 iv. error:處理請求時發生錯誤時被調用 filterOrder:通過int值來定義過濾器的執行順序 shouldFilter:傳回一個boolean類型來判斷該過濾器是否要執行,是以通過此函數可實作過濾器的開關。在上例中,我們直接傳回true,是以該過濾器總是生效。 run:過濾器的具體邏輯。需要注意,這裡我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,然後通過ctx.setResponseStatusCode(401)設定了其傳回的錯誤碼,當然我們也可以進一步優化我們的傳回,比如,通過ctx.setResponseBody(body)對傳回body内容進行編輯等。 在實作了自定義過濾器之後,還需要執行個體化該過濾器才能生效,我們隻需要在應用主類中增加如下内容:
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
啟動該服務網關後,通路: http://localhost:5555/api-a/add?a=1&b=2:傳回401錯誤 http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正确路由到server-A,并傳回計算内容
總結
不僅僅實作了路由功能來屏蔽諸多服務細節,更實作了服務級别、均衡負載的路由。 實作了接口權限校驗與微服務業務邏輯的解耦。通過服務網關中的過濾器,在各生命周期中去校驗請求的内容,将原本在對外服務層做的校驗前移,保證了微服務的無狀态性,同時降低了微服務的測試難度,讓服務本身更集中關注業務邏輯的處理。 實作了斷路器,不會因為具體微服務的故障而導緻服務網關的阻塞,依然可以對外服務。