1、什麼是API閘道器
API閘道器是所有請求的入口,承載了所有的流量,API Gateway是一個門戶一樣,也可以說是進入系統的唯一節點。這跟物件導向設計模式中的Facet模式很像。API Gateway封裝內部系統的架構,並且提供API給各個客戶端。它還可能有其他功能,如授權、監控、負載均衡、快取、請求分片和管理、靜態響應處理等
API Gateway負責請求轉發、合成和協議轉換。所有來自客戶端的請求都要先經過API Gateway,然後路由這些請求到對應的微服務。API Gateway將經常通過呼叫多個微服務來處理一個請求以及聚合多個服務的結果。它可以在web協議與內部使用的非Web友好型協議間進行轉換,如 HTTP協議、WebSocket協議。
畫圖表示,沒有閘道器的情況,客戶端的請求會直接落到後端的各個服務中,無法集中統一管理。
畫圖表示,有閘道器的情況,所有的請求都先經過閘道器,然後進行分發到對應服務
2、API閘道器的重要性
API閘道器在微服務專案中是很重要的,閘道器提供一個統一的管理,服務間的排程變得有序
引用nginx官方的一篇優質部落格,https://www.nginx.com/blog/building-microservices-using-an-api-gateway/,例子介紹了一個龐雜的電商系統,按照微服務理論進行設計,有如下各種服務:
- 購物車服務:購物車中的物品數量
- 訂單服務:訂單歷史記錄
- 目錄服務:基本產品資訊,例如其名稱,影像和價格
- 稽核服務:客戶稽核
- 庫存服務:庫存不足警告
- 運送服務:運送選項,期限和費用與運送提供商的API分開提取
- 推薦服務:建議專案
在不使用閘道器的情況,客戶端直接呼叫各服務:
理想情況,各服務呼叫是可以正常使用的,但是隨著業務擴充,服務之間的呼叫越來越複雜,到時候系統就會變成如圖:
如果沒有一個統一的管理,肯定是不合理的,所以可以引入閘道器,作為一個統一的門戶,如圖:
3、API Gateway的作用
ok,簡單介紹閘道器之後,要說說閘道器的作用,在Spring cloud官網也有過歸納:
當然,我們可以自己挑幾個重要的介紹
- 動態路由
閘道器可以做路由轉發,假如服務資訊變了,只要改閘道器配置既可,所以說閘道器有動態路由(Dynamic Routing)的作用,如圖:
- 請求監控
請求監控可以對整個系統的請求進行監控,詳細地記錄請求響應日誌,如圖,可以將日誌丟到訊息佇列,如果沒有使用閘道器的話,記錄請求資訊需要在各個服務中去做
- 認證鑑權
認證鑑權可以對每一個訪問請求做認證,拒絕非法請求,保護後端的服務,不需要每個服務都做鑑權,在專案中經常有加上OAuth2.0、JWT,Spring Security進行許可權校驗
- 壓力測試
有閘道器的系統,如果要要對某個服務進行壓力測試,可以如圖所示,改下閘道器配置既可,測試請求路由到測試服務,測試服務會有單獨的測試資料庫,這樣測試的請求就不會影響到正式的服務和資料庫
4、什麼是Netflix Zuul?
Netflix Zuul是Netflix公司的產品,是一款API閘道器中介軟體。Zuul是一個基於 JVM 路由和服務端的負載均衡器。提供了路由、監控、彈性、安全等服務。Zuul 能夠與 Eureka、Ribbon、Hystrix 等元件配合使用,提供統一的API閘道器處理
5、Netflix Zuul工作原理
參考Zuul官網wiki,Zuul的核心如圖其實就是過濾器,zuul基於Servlet實現。當一個請求進來時,會先進入 pre 過濾器,在 pre 過濾器執行完後,接著就到了 routing 過濾器中,開始路由到具體的服務中,錯誤的情況會被錯誤過濾器攔截
- 過濾器型別:
- 前置過濾器(PRE FILTER):在路由過濾器之前執行。功能可以包括請求身份驗證,選擇原始伺服器以及記錄除錯資訊。
- 路由過濾器(ROUTE FILTER):處理將請求路由到源的過程。這是使用Apache HttpClient或Netflix Ribbon構建和傳送原始HTTP請求的地方。
- 後置過濾器(POST FILTER):在將請求路由過濾器之後執行。功能可以包括向響應中新增標準HTTP標頭,收集統計資訊和指標以及將響應從源流傳輸到客戶端。
- 錯誤過濾器(ERR FILTER):在其他階段之一發生錯誤時就會呼叫到錯誤過濾器
6、Zuul實驗環境準備
環境準備:
- JDK 1.8
- SpringBoot2.2.3
- SpringCloud(Hoxton.SR6)
- Maven 3.2+
- 開發工具
- IntelliJ IDEA
- smartGit
建立一個SpringBoot Initialize專案,詳情可以參考我之前部落格:SpringBoot系列之快速建立專案教程
maven配置:
<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>
本部落格的是基於spring-cloud-starter-netflix-eureka-client
進行試驗,試驗前要執行eureka服務端,eureka服務提供者,程式碼請參考上一章部落格
專案建立成功後,先在啟動類加上@EnableZuulProxy
:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class SpringcloudZuulApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudZuulApplication.class, args);
}
}
7、eureka、zuul配置
eureka客戶端配置:
server:
port: 8082
# 指定application name,這個是微服務註冊的serviceId
spring:
application:
name: zuul-api-gateway
eureka:
client:
# 服務註冊中心url
service-url:
defaultZone: http://localhost:8761/eureka/
# 閘道器服務註冊、發現都開放,所以 register-with-eureka、fetch-registry都是true
register-with-eureka: true
fetch-registry: true
instance:
status-page-url-path: http://localhost:8761/actuator/info
health-check-url-path: http://localhost:8761/actuator/health
prefer-ip-address: true
instance-id: zuul-api-gateway8082
Zuul 配置路由規則:
zuul:
routes:
provider: # 路由標識,可以自己定義
service-id: eureka-service-provider # 服務id(必須配置)
path: /provider/** # 對映的路徑,一般和routes.provider一致
url: http://localhost:8083 # 路由到的url,可以不配置
Zuul配置訪問字首:訪問時候需要加上字首,eg:http://localhost:8082/api-gateway/provider/api/users/mojombo
zuul:
# 配置字首
prefix: /api-gateway
Zuul配置Header過濾:
zuul:
# 配置過濾敏感的請求頭資訊,設定為空就不會過濾
sensitive-headers: Cookie,Set-Cookie,Authorization
Zuul配置重定向新增Host:
zuul:
# 重定向會新增host請求頭
add-proxy-headers: true
Zuul超時設定:
zuul:
host:
# 配置連線超時時間
connect-timeout-millis: 15000
# socker傳送超時時間
socket-timeout-millis: 60000
zuul所有配置參考,詳情參考官網:
zuul:
# 配置字首
prefix: /api-gateway
routes:
provider: # 路由標識,可以自己定義
service-id: eureka-service-provider # 服務id
path: /provider/** # 對映的路徑,一般和routes.provider一致
url: http://localhost:8083 # 路由到的url
host:
# 配置連線超時時間
connect-timeout-millis: 15000
# socker傳送超時時間
socket-timeout-millis: 60000
# 請求url編碼
decode-url: true
# 查詢字串編碼
force-original-query-string-encoding: false
# 配置過濾敏感的請求頭資訊,設定為空就不會過濾
sensitive-headers: Cookie,Set-Cookie,Authorization
# 重定向會新增host請求頭
add-proxy-headers: true
訪問:http://localhost:8082/api-gateway/provider/api/users/mojombo,要加上字首,配置的path
可能遇到的錯誤,504錯誤:
經過排查,需要加上超時設定,因為呼叫服務超時,導致504錯誤
zuul:
host:
connect-timeout-millis: 15000
socket-timeout-millis: 60000
加上配置,呼叫服務成功
8、Zuul自定義過濾器
在前面的介紹中,已經介紹了幾種過濾器,現在自定義實現這四種過濾器
ps:spring cloud官網也提供了zuul過濾器的例子,詳情可以去github檢視:https://github.com/spring-cloud-samples/sample-zuul-filters
專案結構:
過濾器型別參考org.springframework.cloud.netflix.zuul.filters.supportFilterConstants.java:
實現一個前置過濾器:攔截請求,必須帶token過來,不然丟擲提示資訊等等
package com.example.springcloud.zuul.web.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.FORWARD_TO_KEY;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY;
/**
* <pre>
* API閘道器預過濾器
* </pre>
*
* <pre>
* @author mazq
* 修改記錄
* 修改後版本: 修改人: 修改日期: 2020/08/05 18:08 修改內容:
* </pre>
*/
@Slf4j
//@Component
public class ZuulApiGatewayPreFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String accessToken = request.getParameter("token");
if (StringUtils.isEmpty(accessToken)) {
// zuul過濾該請求,不進行路由
ctx.setSendZuulResponse(false);
// 設定返回的錯誤碼
ctx.setResponseStatusCode(403);
ctx.setResponseBody("AccessToken is Invalid ");
return null;
}
log.info("accessToken: {}",accessToken);
// 否則業務繼續執行
return null;
}
}
後置過濾器,經常被用於列印日誌等等操作,程式碼參考:https://www.baeldung.com/zuul-filter-modifying-response-body,實現效果時,路由過濾器執行之後,執行後置過濾器列印日誌:
package com.example.springcloud.zuul.web.filter;
import com.google.common.io.CharStreams;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.protocol.RequestContent;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
/**
* <pre>
* API Gateway後置過濾器
* </pre>
*
* <pre>
* @author mazq
* 修改記錄
* 修改後版本: 修改人: 修改日期: 2020/08/06 10:05 修改內容:
* </pre>
*/
@Slf4j
//@Component
public class ZuulApiGatewayPostFilter extends ZuulFilter {
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
try (final InputStream responseDataStream = context.getResponseDataStream()) {
if(responseDataStream == null) {
log.warn("RESPONSE BODY: {}", "");
return null;
}
String responseData = CharStreams.toString(new InputStreamReader(responseDataStream, "UTF-8"));
log.info("RESPONSE BODY: {}", responseData);
context.setResponseBody(responseData);
}
catch (Exception e) {
throw new ZuulException(e, INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
return null;
}
}
註冊過濾器,將過濾器載入到Spring容器,也可以在過濾器類加上@Component
package com.example.springcloud.zuul;
import com.example.springcloud.zuul.web.filter.ZuulApiGatewayErrFilter;
import com.example.springcloud.zuul.web.filter.ZuulApiGatewayPostFilter;
import com.example.springcloud.zuul.web.filter.ZuulApiGatewayPreFilter;
import com.example.springcloud.zuul.web.filter.ZuulApiGatewayRouteFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
public class SpringcloudZuulApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudZuulApplication.class, args);
}
@Bean
public ZuulApiGatewayPreFilter zuulApiGatewayPreFilter(){
return new ZuulApiGatewayPreFilter();
}
@Bean
public ZuulApiGatewayPostFilter zuulApiGatewayPostFilter(){
return new ZuulApiGatewayPostFilter();
}
@Bean
public ZuulApiGatewayRouteFilter zuulApiGatewayRouteFilter(){
return new ZuulApiGatewayRouteFilter();
}
@Bean
public ZuulApiGatewayErrFilter zuulApiGatewayErrFilter(){
return new ZuulApiGatewayErrFilter();
}
}
訪問閘道器:http://localhost:8082/api-gateway/provider/api/users/mojombo,不帶token的情況
http://localhost:8082/api-gateway/provider/api/users/mojombo?token=?,帶上token呼叫成功
9、檢視Zuul路由資訊
加上spring-boot-starter-actuator,進行路由資訊監控:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
spring-boot-starter-actuator配置:
management:
endpoints:
web:
exposure:
# 預設只支援info,health,開啟對routes的監控
include: info,health,routes
# 開啟健康檢查詳細資訊
endpoint:
health:
show-details: always
檢視路由詳細,訪問http://localhost:8082/actuator/routes,SpringBoot2.2.3版本要加上actuator字首,呼叫成功,返回json資料:
{
"/api-gateway/provider/**":"eureka-service-provider",
"/api-gateway/eureka-service-provider/**":"eureka-service-provider"
}
檢視路由詳細資訊,訪問連結:http://localhost:8082/actuator/routes/details
{
"/api-gateway/provider/**":{
"id":"provider",
"fullPath":"/api-gateway/provider/**",
"location":"eureka-service-provider",
"path":"/**",
"prefix":"/api-gateway/provider",
"retryable":false,
"customSensitiveHeaders":false,
"prefixStripped":true
},
"/api-gateway/eureka-service-provider/**":{
"id":"eureka-service-provider",
"fullPath":"/api-gateway/eureka-service-provider/**",
"location":"eureka-service-provider",
"path":"/**",
"prefix":"/api-gateway/eureka-service-provider",
"retryable":false,
"customSensitiveHeaders":false,
"prefixStripped":true
}
}
本部落格程式碼例子下載:code download
Zuul官網手冊:
spring Cloud官網zuul資料:https://docs.spring.io/spring-cloud-netflix/docs/2.2.x-SNAPSHOT/reference/html/#router-and-filter-zuul
zuul github wiki:https://github.com/Netflix/zuul/wiki/How-it-Works
github zuul過濾器例子:https://github.com/spring-cloud-samples/sample-zuul-filters
優質學習資料參考:
-
Nginx官網對微服務閘道器的介紹:https://www.nginx.com/blog/building-microservices-using-an-api-gateway/
-
SpringCloud元件之閘道器Zuul(Hoxton版本):https://juejin.im/post/6847902220214763527
-
方誌鵬大佬系列Spring Cloud部落格:https://www.fangzhipeng.com/spring-cloud.html
-
使用Spring Cloud與Docker實戰微服務:https://eacdy.gitbooks.io/spring-cloud-book/content/
-
程式設計師DD大佬系列Spring Cloud部落格:http://blog.didispace.com/spring-cloud-learning/