springcloud系列—Zuul—第5章-2: Spring Cloud Zuul 路由詳解
資料參考:《Spring Cloud 微服務實戰》
目錄
路由詳解
傳統路由配置
所謂的傳統路由配置方式就是在不依賴於服務發現機制的情況下,通過在配置檔案中具體指定每個路由表示式與服務例項的對映關係來實現API閘道器對外部請求的路由。
沒有Eureka和Consul的服務治理框架幫助的時候,我們需要根據服務例項的數量採用不同方式的配置來實現路由規則:
- 單例項配置:通過一組
zuul.routes.<route>.path
與zuul.routes.<route>.url
引數對的方式配置,比如:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/
該配置實現了對符合/user-service/**
規則的請求路徑轉發到http://localhost:8080/
地址的路由規則,比如,當有一個請求http://localhost:1101/user-service/hello
/user-service/hello
能夠被上述配置的path
規則匹配,所以API閘道器會轉發請求到http://localhost:8080/hello
地址。
- 多例項配置:通過一組
zuul.routes.<route>.path
與zuul.routes.<route>.serviceId
引數對的方式配置,比如:
zuul.routes.user-service.path=/user-service/** zuul.routes.user-service.serviceId=user-service ribbon.eureka.enabled=false user-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/
該配置實現了對符合/user-service/**
規則的請求路徑轉發到http://localhost:8080/
和http://localhost:8081/
兩個例項地址的路由規則。它的配置方式與服務路由的配置方式一樣,都採用了zuul.routes.<route>.path
與zuul.routes.<route>.serviceId
引數對的對映方式,只是這裡的serviceId
是由使用者手工命名的服務名稱,配合<serviceId>.ribbon.listOfServers
引數實現服務與例項的維護。由於存在多個例項,API閘道器在進行路由轉發時需要實現負載均衡策略,於是這裡還需要Spring Cloud Ribbon的配合。由於在Spring Cloud Zuul中自帶了對Ribbon的依賴,所以我們只需要做一些配置即可,比如上面示例中關於Ribbon的各個配置,它們的具體作用如下:
ribbon.eureka.enabled
:由於zuul.routes.<route>.serviceId
指定的是服務名稱,預設情況下Ribbon會根據服務發現機制來獲取配置服務名對應的例項清單。但是,該示例並沒有整合類似Eureka之類的服務治理框架,所以需要將該引數設定為false,不然配置的serviceId
是獲取不到對應例項清單的。user-service.ribbon.listOfServers
:該引數內容與zuul.routes.<route>.serviceId
的配置相對應,開頭的user-service
對應了serviceId
的值,這兩個引數的配置相當於在該應用內部手工維護了服務與例項的對應關係。
不論是單例項還是多例項的配置方式,我們都需要為每一對對映關係指定一個名稱,也就是上面配置中的<route>
,每一個<route>
就對應了一條路由規則。每條路由規則都需要通過path
屬性來定義一個用來匹配客戶端請求的路徑表示式,並通過url
或serviceId
屬性來指定請求表示式對映具體例項地址或服務名。
服務路由配置
服務路由我們在上一篇中也已經有過基礎的介紹和體驗,Spring Cloud Zuul通過與Spring Cloud Eureka的整合,實現了對服務例項的自動化維護,所以在使用服務路由配置的時候,我們不需要向傳統路由配置方式那樣為serviceId
去指定具體的服務例項地址,只需要通過一組zuul.routes.<route>.path
與zuul.routes.<route>.serviceId
引數對的方式配置即可。
比如下面的示例,它實現了對符合/user-service/**
規則的請求路徑轉發到名為user-service
的服務例項上去的路由規則。其中<route>
可以指定為任意的路由名稱。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
對於面向服務的路由配置,除了使用path
與serviceId
對映的配置方式之外,還有一種更簡潔的配置方式:zuul.routes.<serviceId>=<path>
,其中<serviceId>
用來指定路由的具體服務名,<path>
用來配置匹配的請求表示式。比如下面的例子,它的路由規則等價於上面通過path
與serviceId
組合使用的配置方式。
zuul.routes.user-service=/user-service/**
傳統路由的對映方式比較直觀且容易理解,API閘道器直接根據請求的URL路徑找到最匹配的path
表示式,直接轉發給該表示式對應的url
或對應serviceId
下配置的例項地址,以實現外部請求的路由。那麼當採用path
與serviceId
以服務路由方式實現時候,沒有配置任何例項地址的情況下,外部請求經過API閘道器的時候,它是如何被解析並轉發到服務具體例項的呢?
在Spring Cloud Netflix中,Zuul巧妙的整合了Eureka來實現面向服務的路由。實際上,我們可以直接將API閘道器也看做是Eureka服務治理下的一個普通微服務應用。它除了會將自己註冊到Eureka服務註冊中心上之外,也會從註冊中心獲取所有服務以及它們的例項清單。所以,在Eureka的幫助下,API閘道器服務本身就已經維護了系統中所有serviceId與例項地址的對映關係。當有外部請求到達API閘道器的時候,根據請求的URL路徑找到最佳匹配的path
規則,API閘道器就可以知道要將該請求路由到哪個具體的serviceId
上去。由於在API閘道器中已經知道serviceId
對應服務例項的地址清單,那麼只需要通過Ribbon的負載均衡策略,直接在這些清單中選擇一個具體的例項進行轉發就能完成路由工作了。
服務路由的預設規則
雖然通過Eureka
與zuul
的整合已經為我們省去了維護服務例項清單的大量配置工作,剩下來只需要再維護請求路徑的匹配表示式與服務名對映關係即可。
但是實際的運用過程中發現,大部分的路由規則機會都會採用服務名作為外部請求的字首,比如下面的列子,其中path路徑的字首使用了user-service
,而對應的服務名也是user-service
。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
其實zuul已經自動的幫我們實現以服務名作為字首的對映,我們不需要去配置它。
當我們為Zuul構建API閘道器服務引入Eureka的時候,它為Eureka中每個服務都自動建立一個預設路由規則,這些預設規則的path會使用serviceId配置的服務名作為字首。
但是,有一些服務我們不需要對外開發也被外部訪問到了。這個時候我們可以使用zuul.ignore-services
引數來設定一個服務名匹配表示式來定義不自動建立路由的規則。zuul
在自動建立服務路由的時候會根據該表示式來進行判斷,如果服務名匹配表示式,那麼zuul
將跳過該服務,不為其建立路由規則。比如,設定為zuul.ignored-services=*
的時候,zuul
將對所有的服務都不自動建立路由規則。在這種情況下,我們就要在配置檔案中為需要路由的服務新增路由規則(可以使用path
與serviceId
組合的配置方式,也可以使用更簡潔的zuul.routes.<serviceId>=<path>
配置方式),只有在配置檔案中出現的對映規則會被建立路由,而從Eureka
中獲取的其他服務,zuul
將不會為他們建立路由規則。
自定義路由對映關係
我們在構建微服服務系統的進行業務邏輯開發的時候,為了相容外部不同版本的客戶端程式(儘量不強迫使用者升級客戶端),一般都會採用開閉原則來進行設計與開發。這使得系統在迭代過程中,有時候需要我們為一組互相配合的微服務定義一個版本標記來方便管理它們的版本關係,根據這個標記我們可以很容易的知道這些服務需要一起啟動並配合使用。比如:userservice-v1
,userservice-v2
,orderservice-v1
,orderservice-v2
等等。預設情況下,zuul自動為服務建立的路由表示式會採用服務名作為字首,比如針對上面的userservice-v1
和userservice-v2
,它會產生/userservice-v1
和/userservice-v2
兩個路徑表示式來對映,這樣生成出來的表示式規則單一,不利於管理。通常的做法就是為這些不同的版本的微服務應用生成以版本號作為路由字首定義規則的路由規則,比如/v1/userservice/
。這時候,通過這樣具有版本號字首的url路徑,我們就可以很同意的通過路徑表示式來歸類和管理這些具有版本資訊的微服務了。
我們可以使用zuul中自定義服務與路由對映關係的功能,建立類似於/v1/userserivce/**
的路由匹配原則。
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}
PatternServiceRouteMapper
物件可以讓開發者通過正則表示式來自定義服務與路由對映的生成關係。建構函式第一個引數是用來匹配服務名稱是否符合該自定義規則的正則表示式,第二個引數是定義根據服務名中定義的內容轉換出的路徑表示式規則。當開發者在api閘道器中定義了PatternServiceRouteMapper
實現之後,只需符合第一個引數定義規則的服務名,都會優先使用該實現構建出的表示式,如果沒有匹配上的服務規則則還是會使用預設的路由對映規則,記採用完整服務名作為字首的路徑表示式。
路徑匹配
不論是使用傳統配置方式還是服務路由的配置方式,我們都需要為每個路由定義匹配表示式,也就是上面的oath引數,在zuul中,路由匹配的路徑表示式採用ant風格定義。
ant風格的路徑表示式使用起來非常簡單,
萬用字元 | 說明 |
---|---|
? | 匹配任意單個字元 |
* | 匹配任意數量的字元 |
** | 匹配任意數量的自負,支援多級目錄 |
url路徑 | 說明 |
---|---|
/user-service/? | 可以匹配/user-service/之後的一個人和字元的路徑,比如/user-service/a,/user-service/b,/user-service/c |
/user-service/* | 可以匹配/user-service/之後拼接的任意字元的路徑,比如說/user-service/a,/user-service/aaa,無法匹配/user-service/a/b |
/user-service/** | 可以匹配/user-service/*包含的內容之外,還可以匹配/user-service/a/b的多級目錄 |
但是隨著版本的迭代,對user-service
服務做了一些功能拆分,將原本屬於user-service
服務的某些功能拆分到user-service-ext
中去,而這些拆分的外部呼叫url路徑希望能夠複合/user-service/ext/**
。這個時候
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
zuul.routes.user-service-ext.path=/user-service/ext/**
zuul.routes.user-service-ext.serviceId=user-service-ext
此時,呼叫user-service-ext
服務的url路徑實際上會同時被/user-service/**
和/user-service/ext/**
兩個表示式所匹配。在邏輯上,api閘道器優先選擇/user-service/ext/**
路由,然後再去匹配/user-service/**
路由才能實現上述需求,但是如果使用上面的配置方式,實際上是無法保證這樣的路由優先順序的。
從下面的路由匹配演算法中,我們可以看到它在使用路由規則匹配的請求路徑的時候是通過線性便利的方法,在請求路徑獲取到第一個匹配的路由規則之後就返回並結束匹配過程。所以當存在多個匹配的路由規則時,匹配結果完全取決於路由規則的儲存順序。
由於properties的配置內容無法保證有序,所以當出現這樣的情況的時候,為了保證路由的優先順序,我們需要使用yml檔案來配置,以實現有序的路由規則
關於這邊的說法,官網給的介紹
If you need your routes to have their order preserved you need to use a YAML file as the ordering will be lost using a properties file.
忽略表示式
通過path引數定義的ant表示式已經能夠完成api閘道器上的路由規則配置功能,但是為了更細粒度和更為靈活地配置理由規則,zuul還提供了一個忽略表示式引數zuul.ignored-patterns
。該引數可以用來設定不希望被api閘道器進行路由的url表示式。
比如我們啟動user-service服務,訪問
http://192.168.1.57:6069/user/home
可以使用api閘道器路由
http://192.168.1.57:6069/user-service/user/home
在zuul-service中配置
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
zuul:
ignoredPatterns: /**/home/**
routes:
user-service:
path: /user-service/**
serviceId: user-service
order-service:
path: /pay-service/**
serviceId: pay-service
logging:
level:
com.netflix: debug
發現url中包括home的已經不能被正確路由了。zuul.ignoredPatterns=/**/home/** 的使用方法。
控制檯上輸出:
另外,該引數在使用時還需要注意它的範圍並不是針對某個路由,而是對所有路由。所以在設定的時候需要全面考慮url規則,防止忽略了不該被忽略的url路徑。
路由字首
為了方便地為路由規則增加字首資訊,zuul提供了zuul.prefix
引數來進行設定。比如,希望為閘道器上的路由規則增加/api字首,那麼我們可以在配置檔案中增加配置:zuul.prefix=/api
。另外,對於代理字首會預設從路徑中移除,我們可以通過設定zuul.strip-prefix=false
(預設為true,預設為true時字首生效,比如http://192.168.5.3:6069/zhihao/users/user/index
)來關閉該移除代理字首的動作。
demo
啟動user和order服務
http://192.168.1.57:8080/user/index
http://192.168.1.57:9090/order/index
啟動zuul服務,zuul中配置了字首
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
zuul:
routes:
user-service:
path: /user-service/**
serviceId: user-service
order-service:
path: /pay-service/**
serviceId: pay-service
prefix: /zhihao
http://192.168.1.57:6069/zhihao/order-service/order/index
http://192.168.1.57:6069/zhihao/user-service/user/index
stripPrefix的使用
修改zuul的配置:
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
zuul:
routes:
user-service:
path: /user-service/**
serviceId: user-service
order-service:
path: /pay-service/**
serviceId: pay-service
prefix: /zhihao
strip-prefix: false
logging:
level:
com.netflix: debug
增加了zuul.strip-prefix: false
和配置了zuul的日誌級別。
再去訪問之前的代理:
http://192.168.1.57:6069/zhihao/user-service/user/index
發現訪問不了,控制檯上打印出日誌,訪問到user服務的/zhihao/user/index了
2017-08-12 16:50:11.656 DEBUG 1407 --- [nio-6069-exec-8] c.n.loadbalancer.LoadBalancerContext
: user-service using LB returned Server: 192.168.1.57:8080 for request /zhihao/user/index
此時正確的姿勢是在user服務中增加server.context-path=/zhihao,再去訪問就正確了:
http://192.168.1.57:6069/zhihao/user-service/user/index
同時order服務也訪問不了,也要在order服務加server.context-path=/zhihao
,其實zuul.prefix=/zhihao
和zuul.strip-prefix=false
表示所有的服務都要跳過服務配置在真實請求求加上/zhihao。
也有針對當個服務的,zuul服務的配置檔案:
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
zuul:
routes:
user-service:
path: /user-service/**
stripPrefix: false
serviceId: user-service
order-service:
path: /pay-service/**
serviceId: pay-service
prefix: /zhihao
logging:
level:
com.netflix: debug
http://192.168.1.57:6069/zhihao/order-service/order/index
http://192.168.1.57:6069/zhihao/user-service/user/index
前者後者都沒有配置context-path
,前者能夠訪問,後者不能訪問,發現因為後者配置了zuul.routes.user-service.stripPrefix=false
,發現後者真正訪問到的的服務是/user-service/user/index
,此時要修改user-service增加server.context-path=/user-service
才能正確地訪問到服務。
坑:
user-service的配置改成下面這樣
spring:
application:
name: zuul-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
prefer-ip-address: true
server:
port: 6069
zuul:
routes:
user-service:
path: /zhihao-userservice/**
serviceId: user-service
order-service:
path: /pay-service/**
serviceId: pay-service
prefix: /zhihao
logging:
level:
com.netflix: debug
http://192.168.1.57:6069/zhihao/order-service/order/index
還是正確路由,http://192.168.1.57:6069/zhihao/zhihao-userservice/user/index
不能正確路由。
這時zuul的一個bug,當路由表示式字首是以zhihao開頭,與路由表示式一樣是/zhihao開頭的話就會產生錯誤的對映關係。
我使用的是Camden.SR7版本也存在這樣的問題。
本地跳轉
在zuul實現的api閘道器路由功能中,還支援forward形式的服務端跳轉配置。實現方式非常簡單,只需要通過使用path與url的配置方式就能完成,通過url中使用forward來指定需要跳轉的伺服器資源路徑。
在zuul-service服務中定義一個controller,
@RestController
public class HelloController {
@RequestMapping("/local/hello")
public String hello(){
return "hello world local";
}
}
配置檔案配置:
zuul:
routes:
user-service:
path: /zhihao-userservice/users/**
serviceId: user-service
pay-service:
path: /pays/**
serviceId: pay-service
zuul-service:
path: /api-b/**
serviceId: forward:/local
訪問http://192.168.5.3:6069/api-b/hello
就跳轉到了閘道器的/local/hello上了。
cookie與頭資訊
預設情況下,spring cloud zuul
在請求路由時,會過濾掉http請求頭資訊中一些敏感資訊,防止它們被傳遞到下游的外部伺服器。預設的敏感頭資訊通過zuul.sensitiveHeaders
引數定義,預設包括cookie,set-Cookie,authorization
三個屬性。所以,我們在開發web專案時常用的cookie在spring cloud zuul
閘道器中預設時不傳遞的,這就會引發一個常見的問題,如果我們要將使用了spring security
,shiro
等安全框架構建的web應用通過spring cloud zuul
構建的閘道器來進行路由時,由於cookie資訊無法傳遞,我們的web應用將無法實現登入和鑑權。為了解決這個問題,配置的方法有很多。
- 通過設定全域性引數為空來覆蓋預設值,具體如下:
zuul.sensitiveHeaders=
這種方法不推薦,雖然可以實現cookie的傳遞,但是破壞了預設設定的用意。在微服務架構的api閘道器之內,對於無狀態的restful api請求肯定時要遠多於這些web類應用請求的,甚至還有一些架構設計會將web類應用和app客戶端一樣歸為api閘道器之外的客戶端應用。
- 通過指定路由的引數來設定,方法有下面二種。
方法一:對指定路由開啟自定義敏感頭。
方法二:將指定路由的敏感頭設定為空。
將具體的服務的sensitiveHeaders(頭資訊設定為空)
比較推薦使用這二種方法,僅對指定的web應用開啟對敏感資訊的傳遞,影響範圍小,不至於引起其他服務的資訊洩露問題。
參考文章:Spring Cloud實戰小貼士:Zuul處理Cookie和重定向
重定向問題
在使用Spring Cloud Zuul對接Web網站的時候,處理完了會話控制問題之後。往往我們還會碰到如下圖所示的問題,我們在瀏覽器中通過Zuul發起了登入請求,該請求會被路由到某WebSite服務,該服務在完成了登入處理之後,會進行重定向到某個主頁或歡迎頁面。此時,仔細的開發者會發現,在登入完成之後,我們瀏覽器中URL的HOST部分發生的改變,該地址變成了具體WebSite服務的地址了。這就是在這一節,我們將分析和解決的重定向問題!
出現該問題的根源是Spring Cloud Zuul沒有正確的處理HTTP請求頭資訊中的Host導致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter
過濾器實現時完全沒有考慮這一問題,它更多的定位於REST API的閘道器。所以如果要在Brixton版本中增加這一特性就相對較為複雜,不過好在Camden版本之後,Spring Cloud Netflix 1.2.x版本的Zuul增強了該功能,我們只需要通過配置屬性zuul.add-host-header=true
就能讓原本有問題的重定向操作得到正確的處理。