SpringCloud微服務基礎5:Zuul閘道器
我們使用Spring Cloud Netflix中的Eureka實現了服務註冊中心以及服務註冊與發現;而服務間通過Ribbon或Feign實現服務的消費以及均衡負載;通過Spring Cloud Config實現了應用多環境的外部化配置以及版本管理。為了使得服務叢集更為健壯,使用Hystrix的融斷機制來避免在微服務架構中個別服務出現異常時引起的故障蔓延。
在該架構中,我們的服務叢集包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務呼叫方。本文我們把焦點聚集在對外服務這塊,這樣的實現是否合理,或者是否有更好的實現方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
(1)首先,破壞了服務無狀態特點。為了保證對外服務的安全性,我們需要實現對服務訪問的許可權控制,而開放服務的許可權控制機制將會貫穿並汙染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀態的特點。從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外可續對介面訪問的控制處理。
(2)其次,無法直接複用既有介面。當我們需要對一個即有的叢集內訪問介面,實現外部服務訪問時,我們不得不通過在原有介面上增加校驗邏輯,或增加一個代理呼叫來實現許可權控制,無法直接複用原有的介面。
為了解決上面這些問題,我們需要將許可權控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器,它就是本文將來介紹的:服務閘道器。
服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
不管是來自於客戶端(PC或移動端)的請求,還是服務內部呼叫。一切對服務的請求都會經過Zuul這個閘道器,然後再由閘道器來實現 鑑權、動態路由等等操作。Zuul就是我們服務的統一入口。
【注意】 Zuul中預設就已經集成了Ribbon負載均衡和Hystix熔斷機制,但是所有策略都是預設,需要我們手動在application.yml中配置。
1.zuul工程搭建
1.1、pom檔案依賴
<dependencies> <!-- eureka客戶端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- zuul閘道器--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <!--是springboot提供的微服務檢測介面,預設對外提供幾個介面:/info--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
1.2、application.yml配置檔案
server:
port: 10010 #服務埠
spring:
application:
name: api-gateway #指定服務名
zuul: #zuul的對映規則
routes:
user-service: # 這裡是路由id,隨意寫
path: /user-service/** # 這裡是對映路徑
url: http://127.0.0.1:8081 # 對映路徑對應的實際url地址
上面配置檔案中url定義了ip地址和埠。且定義了將 /user-service/**
開頭的請求,代理到path
規則的一切請求,都代理到 url
引數指定的地址。
【注意】上面application.yml中zuul的配置就是單例項配置:單例項配置通過一組zuul.routes.<route>.path與zuul.routes.<route>.url引數對的方式配置。
1.3、啟動類@EnableZuulProxy
通過@EnableZuulProxy
註解開啟Zuul的功能
@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的閘道器功能
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
2.zuul服務路由配置
在配置zuul服務路由配置之前我們先說一下傳統的路由配置,也就是單例項路由配置和多例項路由配置。
2.1、zuul單例項路由配置和多例項路由配置
1、單例項配置:通過一組zuul.routes.<route>.path與zuul.routes.<route>.url引數對的方式配置。application配置程式碼如下:
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被髮送到API閘道器上,由於/user-service/hello能夠被上述配置的path規則匹配,所以API閘道器會轉發請求到http://localhost:8080/hello地址。
2、多例項配置:通過一組zuul.routes.<route>.path與zuul.routes.<route>.serviceId引數對的方式配置。application配置程式碼如下:
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的配合。
2.2、服務路由配置例項
1、Eureka客戶端pom依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、開啟Eureka客戶端發現功能
@SpringBootApplication
@EnableZuulProxy // 開啟Zuul的閘道器功能
@EnableDiscoveryClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
3、新增Eureka配置,獲取服務資訊
eureka:
client:
registry-fetch-interval-seconds: 5 # 獲取服務列表的週期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
4、修改對映配置,通過服務名稱serviceId獲取
zuul:
routes:
user-service: # 這裡是路由id,隨意寫
path: /user-service/** # 這裡是對映路徑
serviceId: user-service # 指定服務名稱
因為已經有了Eureka客戶端,我們可以從Eureka獲取服務的地址資訊,因此對映時無需指定IP地址,而是通過服務名稱來訪問,而且Zuul已經集成了Ribbon的負載均衡功能。
2.3、簡化的路由配置
在剛才的配置中,我們的規則是這樣的:
zuul.routes.<route>.path=/xxx/**: 來指定對映路徑。<route>是自定義的路由名
zuul.routes.<route>.serviceId=/user-service:來指定服務名。
而大多數情況下,我們的<route>路由名稱往往和服務名會寫成一樣的。因此Zuul就提供了一種簡化的配置語法:zuul.routes.<serviceId>=<path>。比方說上面我們關於user-service的配置可以簡化為一條:
zuul:
routes:
user-service: /user-service/** # 這裡是對映路徑
2.4、預設的路由規則
在使用Zuul的過程中,上面講述的規則已經大大的簡化了配置項。但是當服務較多時,配置也是比較繁瑣的。因此Zuul就指定了預設的路由規則:
預設情況下,一切服務的對映路徑就是服務名本身。 例如服務名為:user-service,則預設的對映路徑就是:/user-service/**。也就是說,剛才的對映規則我們完全不配置也是OK的,不信就試試看。
2.5、路由字首zuul.prefix
zuul:
prefix: /api # 新增路由字首
routes:
user-service: # 這裡是路由id,隨意寫
path: /user-service/** # 這裡是對映路徑
service-id: user-service # 指定服務名稱
我們通過zuul.prefix=/api來指定了路由的字首,這樣在發起請求時,路徑就要以/api開頭。路徑/api/user-service/user/1將會被代理到/user-service/user/1。
3.zuul過濾器
Zuul作為閘道器的其中一個重要功能,就是實現請求的鑑權。而這個動作我們往往是通過Zuul提供的過濾器來實現的。 Zuul允許開發者在API閘道器上通過定義過濾器來實現對請求的攔截與過濾,實現的方法非常簡單,我們只需要繼承ZuulFilter抽象類並實現它定義的四個抽象函式就可以完成對請求的攔截和過濾了。
Zuul場景常見應用場景:
(1)請求鑑權:一般放在pre型別,如果發現沒有訪問許可權,直接就攔截了;
(2)異常處理:一般會在error型別和post型別過濾器中結合來處理。
(3)服務呼叫時長統計:pre和post結合使用。
3.1、ZuulFilter介面
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 來自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
【ZuulFilter介面原始碼中方法說明】
(1)shouldFilter()方法:返回一個Boolean值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。
(2)run()方法:過濾器的具體業務邏輯。
(3)filterType()方法:返回字串,代表過濾器的型別。包含以下4種:
- pre:請求在被路由之前執行
- routing:在路由請求時呼叫
- post:在routing和errror過濾器之後呼叫
- error:處理請求時發生錯誤呼叫
(4)filterOrder()方法:通過返回的int值來定義過濾器的執行順序,數字越小優先順序越高。
3.2、自定義Zuul過濾器
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if(accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
}
在上面實現的過濾器程式碼中,我們通過繼承ZuulFilter抽象類並重寫了下面的四個方法來實現自定義的過濾器。這四個方法分別定義了:
(1)filterType:過濾器的型別,它決定過濾器在請求的哪個生命週期中執行。這裡定義為pre,代表會在請求被路由之前執行。
(2)filterOrder:過濾器的執行順序。當請求在一個階段中存在多個過濾器時,需要根據該方法返回的值來依次執行。
(3)shouldFilter:判斷該過濾器是否需要被執行。這裡我們直接返回了true,因此該過濾器對所有請求都會生效。實際運用中我們可以利用該函式來指定過濾器的有效範圍。
(4)run:過濾器的具體邏輯。這裡我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,然後通過ctx.setResponseStatusCode(401)設定了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過ctx.setResponseBody(body)對返回body內容進行編輯等。
在實現了自定義過濾器之後,它並不會直接生效,我們還需要為其建立具體的Bean才能啟動該過濾器,比如,在應用主類中增加如下內容:
@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();
}
}
在對api-gateway服務完成了上面的改造之後,我們可以重新啟動它,併發起下面的請求,對上面定義的過濾器做一個驗證:
http://localhost:1101/api-a/hello:返回401錯誤;
http://localhost:1101/api-a/hello&accessToken=token:正確路由到hello-service的/hello介面,並返回Hello World。
4、Zuul中的負載均衡和熔斷
Zuul中預設就已經集成了Ribbon負載均衡和Hystix熔斷機制。但是所有的超時策略都是走的預設值,比如熔斷超時時間只有1S,很容易就觸發了。因此建議我們手動進行配置:
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 連線超時時間(ms)
ReadTimeout: 2000 # 通訊超時時間(ms)
OkToRetryOnAllOperations: true # 是否對所有操作重試
MaxAutoRetriesNextServer: 2 # 同一服務不同例項的重試次數
MaxAutoRetries: 1 # 同一例項的重試次數
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔斷超時時長:6000ms