什么是API網關
網關這個詞其實是一個硬件概念。因為按照定義,網絡網關出現在網絡的邊緣,所以防火牆和代理服務器等相關功能 往往與之集成在一起。在家庭網絡 和小型企業中,寬帶路由器通常充當網絡網關。它將你家中或企業的設備與 Internet 連接。網關是路由器的最重要功能,路由器是最常見的網關類型。
今天我們要講的網關並非是路由器(開個玩笑),既然做應用開發自然討論的是調用各個服務的入口-API,所有服務的入口,簡稱API網關。
在大多數微服務實現中,內部微服務端點不會暴露在外部。它們被保留為私人服務。一組公共服務將使用API網關向客戶端公開。這樣做有很多原因:
- 客戶端僅需要一組選定的微服務;
- 很難在服務端點上實現特定於客戶端的轉換;
- 如果需要數據聚合,尤其是為了避免在帶寬受限的環境中進行多個客戶端調用,則中間需要網關;
- 服務實例數量及其位置(主機+端口)動態變化;
- 如果要應用特定於客戶的策略,則很容易將它們應用於單個位置,而不是多個位置。這種情況的一個示例是跨域訪問策略。
使用網關的好處:
- 客戶端與網關后面的微服務架構分區是隔離的;
- 客戶不必擔心特定服務的位置;
- 如果要應用特定於客戶的策略,則很容易將它們應用於單個位置,而不是多個位置。這種情況的一個示例是跨域訪問策略;
- 為每個客戶端提供最佳的API;
- 減少請求/往返次數;
- 通過將聚合邏輯移至API網關來簡化客戶端。
缺點:
- 復雜性增加 API 網關是微服務體系結構中要管理的又一動態部分;
- 與通過直接調用相比,響應時間增加了,因為通過API網關進行了額外的網絡跳躍;
- 在聚合層中實施過多邏輯的危險。
另外,在微服務體系下,被拆分的各個子服務對外是單一功能的服務,是整體構架布局下的單一功能,那么對外提供的各個接口提供的地址必須是相同的,比如一個用戶中心的服務,所有的接口都應該在api.userCenter.com
下面:
https://api.userCenter.com/user/getUser
https://api.userCenter.com/dept/get
而不能一會在api.userCenter.com
,一會又跑到別的地址下面,如果地址不一樣,那就應該是兩個服務,而不是一個。
所以網關的核心作用是:統一接口路由。
網關存在的意義是為了提供服務,那么身為一個網關,它所應該具有的能力有哪些呢?
1.接收請求:網關最終的能力就是接收請求,然后將請求轉發出去;那么首先它就要有MVC的能力,則它需要實現servlet;
2.發出請求:網關需要將請求轉發到其他服務,那么它就要有發送請求的能力,則它需要實現Http相關方法;
3.過濾請求:網關提供對請求的權限、日志等操作,那么他就要有過濾請求的能力,則它需要實現filter;
4.獲取服務列表:網關提供路由功能,那么它就需要獲取到路由地址,從微服務的架構設置來看,即它需要從注冊中心拿到服務列表;
5.路由配置:網關實現路由操作,那么就需要設置請求路徑與服務的對應關系;
Zuul
Spring Cloud Zuul 主要的功能是提供負載均衡、反向代理、權限認證、動態路由、監控、彈性、安全等的邊緣服務。其主要作用是為微服務架構提供了前門保護的作用,同時將權限控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務集群主體能夠具備更高的可復用性和可測試性。
沒有Netflix Zuul的微服務呼叫:
與Netflix Zuul進行微服務通話:
使用Netflix Zuul + Netflix Eureka進行微服務呼叫:
搭建Zuul網關示例
基於前面我們已經搭建的 Eureka 和 Feign調用工程示例:https://github.com/rickiyang/SpringCloud-learn,我們繼續搭建 Zuul 網關中心。
在pom文件中新增 Zuul 配置:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
整體配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.rickiyang.learn</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.rickiyang.learn</groupId>
<artifactId>zuul-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuul-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<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>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
啟動類加上開啟 Zuul 的注解:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
配置文件添加Eureka的配置屬性:
server:
port: 8767
spring:
application:
name: zuul-server
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone : http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
prefer-ip-address: true # 訪問的路徑變為IP地址
這樣一個api網關就簡單的搭建好了。食用方式:先啟動Eureka-server,接着啟動Eureka-client,最后啟動zuul-server。
首先驗證一下 Eureka-client 接口是否可用:
http://localhost:8766/hello/xiaoming
可用的情況下再來使用Zuul調用,網關 zuul 默認轉發地址是:
http://網關IP:網關端口/被轉發的服務application.name/要訪問的接口
,
本示例中就是:
http://localhost:8767/eureka-client/hello/xiaoming
如果我們覺得服務名:eureka-client 太長了,或者是不想暴露服務名,想用簡潔的字段來替代,可以用如下配置:
zuul:
routes:
eureka-client:
path: /client1/**
serviceId: eureka-client
那么訪問服務的url就變為:
http://localhost:8767/client1/hello/xiaoming
這樣替換之后,如果該路徑中有一些url已經被業務方調用,無法替換,那么需要把這些url排除:
zuul:
#所有服務路徑前統一加上前綴
prefix: /api
# 排除某些路由, 支持正則表達式
ignored-patterns:
- /**/modify/pwd
# 排除服務
ignored-services: user-center
routes:
eureka-client:
path: /client1/**
serviceId: eureka-client
Zuul默認使用 Apache 的 HttpClient 作為HTTP客戶端發送請求,超時參數和連接池參數配置如下:
zuul:
host:
maxTotalConnections: 200 #連接池最大連接數,僅用於Apache的HttpClient,對於okhttp和restclient無效
maxPerRouteConnections: 20 #每個路由最大連接數,僅用於Apache的HttpClient,對於okhttp和restclient無效
Zuul完全沒有開啟重試,如果需要開啟重試,添加配置:zuul.retryable=true
,並且pom.xml添加如下依賴:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
PS:如果配置了zuul.retryable = true,但沒有添加spring-retry到項目中,重試不會開啟,反之亦然,必須要兩個條件都滿足才會開啟重試。
PS:重試操作需要服務提供者保證冪等性,相同操作的多次請求需保證結果一致。
Zuul在開啟了重試的情況下,重試參數配置如下(替換值):
ribbon:
MaxAutoRetries: 0 #當前服務器最大重試次數,不包含第1次請求
MaxAutoRetriesNextServer: 1 #切換服務器最大次數,不包含第1台服務器
OkToRetryOnAllOperations: false #是否所有操作都要重試,false:只有GET請求才會重試,true:GET、POST、PUT等所有請求都會重試
自定義 Filter
網關最核心的功能當然是集中式路由轉發,那么在轉發過程中對請求做一些鑒別和限制就是網關提供的高級功能也是必要的功能。
我們假設有這樣一個場景,因為服務網關應對的是外部的所有請求,為了避免產生安全隱患,我們需要對請求做一定的限制,比如請求中含有 token 便讓請求繼續往下走,如果請求不帶 token 就直接返回並給出提示。
首先自定義一個 Filter,繼承 ZuulFilter 抽象類,在 run () 方法中驗證參數是否含有 token ,具體如下:
package com.rickiyang.learn.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.HttpServletRequest;
/**
* @author rickiyang
* @date 2019-12-13
* @Desc TODO
*/
public class TokenFilter extends ZuulFilter {
/**
* 過濾器的類型,它決定過濾器在請求的哪個生命周期中執行。
* 這里定義為pre,代表會在請求被路由之前執行。
*
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* filter執行順序,通過數字指定。
* 數字越大,優先級越低。
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 判斷該過濾器是否需要被執行。這里我們直接返回了true,因此該過濾器對所有請求都會生效。
* 實際運用中我們可以利用該函數來指定過濾器的有效范圍。
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 過濾器的具體邏輯
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null || token.isEmpty()) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("token is empty");
}
return null;
}
}
在上面實現的過濾器代碼中,我們通過繼承 ZuulFilter
抽象類並重寫了下面的四個方法來實現自定義的過濾器。這四個方法分別定義了:
filterType()
:過濾器的類型,它決定過濾器在請求的哪個生命周期中執行。這里定義為pre
,代表會在請求被路由之前執行。filterOrder()
:過濾器的執行順序。當請求在一個階段中存在多個過濾器時,需要根據該方法返回的值來依次執行。通過數字指定,數字越大,優先級越低。shouldFilter()
:判斷該過濾器是否需要被執行。這里我們直接返回了true
,因此該過濾器對所有請求都會生效。實際運用中我們可以利用該函數來指定過濾器的有效范圍。run()
:過濾器的具體邏輯。這里我們通過ctx.setSendZuulResponse(false)
來讓 Zuul 過濾該請求,不對其進行路由,然后通過ctx.setResponseStatusCode(401)
設置了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過ctx.setResponseBody(body)
對返回 body 內容進行編輯等。
在實現了自定義過濾器之后,它並不會直接生效,我們還需要為其創建具體的 Bean 才能啟動該過濾器,比如,在應用主類中增加如下內容:
import com.rickiyang.learn.filter.TokenFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}
}
Filter是Zuul的核心,用來實現對外服務的控制。Filter的生命周期有4個,分別是:
-
pre:在請求被路由之前調用。
-
routing:將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
-
post:在路由到微服務以后執行。這種過濾器可用來為響應添加標准的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
-
error:其他階段發生錯誤時執行該過濾器。
整個生命周期可以用下圖來表示:
在 Zuul 中提供了一些默認的 Filter:
類型 | 順序 | 過濾器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 標記處理Servlet的類型 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
route | 1 | DebugFilter | 標記調試標志 |
route | 5 | PreDecorationFilter | 處理請求上下文供后續使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發 |
route | 100 | SimpleHostRoutingFilter | url請求轉發 |
route | 500 | SendForwardFilter | forward請求轉發 |
post | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
使用 Zuul 進行限流
添加依賴:
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.3.2.RELEASE</version>
</dependency>
spring-cloud-zuul-ratelimit是和zuul整合提供分布式限流策略的擴展,只需在yaml中配置幾行配置,就可使應用支持限流:
ratelimit:
enabled: true
repository: REDIS #使用redis存儲,一定要大寫!
policies:
eureka-client: #針對上面那個服務的限流
limit: 100 #每秒多少個請求
quota: 20 #quota 單位時間內允許訪問的總時間(統計每次請求的時間綜合)
refreshInterval: 60 #刷新時間窗口的時間,默認值 (秒)
type:
- ORIGIN #這里一定要大寫,類型說明:URL通過請求路徑區分,ORIGIN通過客戶端IP地址區分,USER是通過登錄用戶名進行區分,也包括匿名用戶
ratelimit 支持的限流粒度:
- 服務粒度 (默認配置,當前服務模塊的限流控制)
- 用戶粒度 (詳細說明,見文末總結)
- ORIGIN粒度 (用戶請求的origin作為粒度控制)
- 接口粒度 (請求接口的地址作為粒度控制)
- 以上粒度自由組合,又可以支持多種情況
- 如果還不夠,自定義RateLimitKeyGenerator實現。
支持的存儲方式:
- InMemoryRateLimiter - 使用 ConcurrentHashMap作為數據存儲
- ConsulRateLimiter - 使用 Consul 作為數據存儲
- RedisRateLimiter - 使用 Redis 作為數據存儲
- SpringDataRateLimiter - 使用 數據庫 作為數據存儲
關於Zuul網關的基本使用本節先講到這里,下一節繼續講 Zuul 的高階使用。