一個簡單可參考的API網關架構設計

網關一詞較早出如今網絡設備裏面,好比兩個相互獨立的局域網段之間經過路由器或者橋接設備進行通訊, 這中間的路由或者橋接設備咱們稱之爲網關。html

相應的 API 網關將各系統對外暴露的服務聚合起來,全部要調用這些服務的系統都須要經過 API 網關進行訪問,基於這種方式網關能夠對 API 進行統一管控,例如:認證、鑑權、流量控制、協議轉換、監控等等。前端

API 網關的流行得益於近幾年微服務架構的興起,本來一個龐大的業務系統被拆分紅許多粒度更小的系統進行獨立部署和維護,這種模式勢必會帶來更多的跨系統交互,企業 API 的規模也會成倍增長,API 網關(或者微服務網關)就逐漸成爲了微服務架構的標配組件。java

以下是咱們整理的 API 網關的幾種典型應用場景:數據庫

一、面向 Web 或者移動 App編程

這類場景,在物理形態上相似先後端分離,前端應用經過 API 調用後端服務,須要網關具備認證、鑑權、緩存、服務編排、監控告警等功能。後端

二、面向合做夥伴開放 APIapi

這類場景,主要爲了知足業務形態對外開放,與企業外部合做夥伴創建生態圈,此時的 API 網關注重安全認證、權限分級、流量管控、緩存等功能的建設。緩存

三、企業內部系統互聯互通安全

對於中大型的企業內部每每有幾10、甚至上百個系統,尤爲是微服務架構的興起系統數量更是急劇增長。系統之間相互依賴,逐漸造成網狀調用關係不便於管理和維護,須要 API 網關進行統一的認證、鑑權、流量管控、超時熔斷、監控告警管理,從而提升系統的穩定性、下降重複建設、運維管理等成本。微信

設計目標

  1. 純 Java 實現;
  2. 支持插件化,方便開發人員自定義組件;
  3. 支持橫向擴展,高性能;
  4. 避免單點故障,穩定性要高,不能由於某個 API 故障致使整個網關中止服務;
  5. 管理控制檯配置更新可自動生效,不須要重啓網關;

應用架構設計

整個平臺拆分紅 3 個子系統,Gateway-Core(核心子系統)、Gateway-Admin(管理中心)、Gateway-Monitor(監控中心)。

  • Gateway-Core 負責接收客戶端請求,調度、加載和執行組件,將請求路由到上游服務端,處理上游服務端返回的結果等;
  • Gateway-Admin 提供統一的管理界面,用戶可在此進行 API、組件、系統基礎信息的設置和維護;
  • Gateway-Monitor 負責收集監控日誌、生成各類運維管理報表、自動告警等;

系統架構設計

說明:

  1. 網關核心子系統經過 HAProxy 或者 Nginx 進行負載均衡,爲避免正好路由的 LB 節點服務不可用,能夠考慮在此基礎上增長 Keepalived 來實現 LB 的失效備援,當 LB Node1 中止服務,Keepalived 會將虛擬 IP 自動飄移到 LB Node2,從而避免由於負載均衡器致使單點故障。DNS 能夠直接指向 Keepalived 的虛擬 IP。
  2. 網關除了對性能要求很高外,對穩定性也有很高的要求,引入 Zookeeper 及時將 Admin 對 API 的配置更改同步刷新到各網關節點。
  3. 管理中心和監控中心能夠採用相似網關子系統的高可用策略,若是嫌麻煩管理中心能夠省去 Keepalived,相對來講管理中心沒有這麼高的可用性要求。
  4. 理論上監控中心須要承載很大的數據量,好比有 1000 個 API,平均每一個 API 一天調用 10 萬次,對於不少互聯網公司單個 API 的量遠遠大於 10 萬,若是將每次調用的信息都存儲起來太浪費,也沒有太大的必要。能夠考慮將 API 每分鐘的調用狀況彙總後進行存儲,好比 1 分鐘的平均響應時間、調用次數、流量、正確率等等。
  5. 數據庫選型能夠靈活考慮,原則上網關在運行時要儘量減小對 DB 的依賴,不然 IO 延時會嚴重影響網關性能。能夠考慮首次訪問後將 API 配置信息緩存,Admin 對 API 配置更改後經過 Zookeeper 通知網關刷新,這樣一來 DB 的訪問量能夠忽略不計,團隊可根據自身偏好靈活選型。

非阻塞式 HTTP 服務

管理和監控中心能夠根據團隊的狀況採用本身熟悉的 Servlet 容器部署,網關核心子系統對性能的要求很是高,考慮採用 NIO 的網絡模型,實現純 HTTP 服務便可,不須要實現 Servlet 容器,推薦 Netty 框架(設計優雅,大名鼎鼎的 Spring Webflux 默認都是使用的 Netty,更多的優點就不在此詳述了),內部測試在相同的機器上分別經過 Tomcat 和 Netty 生成 UUID,Netty 的性能大約有 20% 的提高,若是後端服務響應耗時較高的話吞吐量還有更大的提高。(補充:Netty4.x 的版本便可,不要採用 5 以上的版本,有嚴重的缺陷沒有解決)

採用 Netty 做爲 Http 容器首先須要解決的是 Http 協議的解析和封裝,好在 Netty 自己提供了這樣的 Handler,具體參考以下代碼:

一、構建一個單例的 HttpServer,在 SpringBoot 啓動的時候同時加載並啓動 Netty 服務

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int  sobacklog = Integer.parseInt(AppConfigUtil.getValue( "netty.sobacklog" ));
 
ServerBootstrap b =  new  ServerBootstrap();
 
b.group(bossGroup, workerGroup)
 
.channel(NioServerSocketChannel. class )
 
.localAddress( new  InetSocketAddress( this .portHTTP))
 
.option(ChannelOption.SO_BACKLOG, sobacklog)
 
.childHandler( new  ChannelHandlerInitializer( null ));
 
// 綁定端口
 
ChannelFuture f = b.bind( this .portHTTP).sync();
 
logger.info( "HttpServer name is "  + HttpServer. class .getName() +  " started and listen on "  + f.channel().localAddress());

  

二、初始化 Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
 
protected  void  initChannel(SocketChannel ch)  throws  Exception {
 
ChannelPipeline p = ch.pipeline();
 
p.addLast( new  HttpRequestDecoder());
 
p.addLast( new  HttpResponseEncoder());
 
int  maxContentLength =  2000 ;
 
try  {
 
maxContentLength = Integer.parseInt(AppConfigUtil.getValue( "netty.maxContentLength" ));
 
catch  (Exception e) {
 
logger.warn( "netty.maxContentLength 配置異常,系統默認爲:2000KB" );
 
}
 
p.addLast( new  HttpObjectAggregator(maxContentLength *  1024 )); // HTTP 消息的合併處理
 
p.addLast( new  HttpServerInboundHandler());
 
}

  

HttpRequestDecoder 和 HttpResponseEncoder 分別實現 Http 協議的解析和封裝,Http Post 內容超過一個數據包大小會自動分組,經過 HttpObjectAggregator 能夠自動將這些數據粘合在一塊兒,對於上層收到是一個完整的 Http 請求。

三、經過 HttpServerInboundHandler 將網絡請求轉發給網關執行器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
 
public  void  channelRead0(ChannelHandlerContext ctx, Object msg)
 
throws  Exception {
 
try  {
 
if  (msg  instanceof  HttpRequest && msg  instanceof  HttpContent) {
 
CmptRequest cmptRequest = CmptRequestUtil.convert(ctx, msg);
 
CmptResult cmptResult =  this .gatewayExecutor.execute(cmptRequest);
 
FullHttpResponse response = encapsulateResponse(cmptResult);
 
ctx.write(response);
 
ctx.flush();
 
}
 
catch  (Exception e) {
 
logger.error( "網關入口異常,"  \+ e.getMessage());
 
e.printStackTrace();
 
}
 
}

  

設計上建議將 Netty 接入層代碼跟網關核心邏輯代碼分離,不要將 Netty 收到 HttpRequest 和 HttpContent 直接給到網關執行器,能夠考慮作一層轉換封裝成本身的 Request 給到執行器,方便後續能夠很容易的將 Netty 替換成其它 Http 容器。(如上代碼所示,CmptRequest 即爲自定義的 Http 請求封裝類,CmptResult 爲網關執行結果類)

組件化及自定義組件支持

組件是網關的核心,大部分功能特性均可以基於組件的形式提供,組件化能夠有效提升網關的擴展性。

先來看一個簡單的微信認證組件的例子:

以下實現的功能是對 API 請求傳入的 Token 進行校驗,其結果分別是認證經過、Token 過時和無效 Token,認證經過後再將微信 OpenID 攜帶給上游服務系統。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 
* 微信 token 認證,token 格式:
 
* {appID: '' ,openID: '' ,timestamp: 132525144172 ,sessionKey:  '' }
 
*
 
public  class  WeixinAuthTokenCmpt  extends  AbstractCmpt {
 
private  static  Logger logger = LoggerFactory.getLogger(WeixinAuthTokenCmpt. class );
 
private  final  CmptResult SUCCESS_RESULT;
 
public  WeixinAuthTokenCmpt() {
 
SUCCESS_RESULT = buildSuccessResult();
 
}
 
@Override
 
public  CmptResult execute(CmptRequest request, Map<String, FieldDTO> config) {
 
if  (logger.isDebugEnabled()) {
 
logger.debug( "WeixinTokenCmpt ......" );
 
}
 
CmptResult cmptResult =  null ;
 
//Token 認證超時間 (傳入單位: 分)
 
long  authTokenExpireTime = getAuthTokenExpireTime(config);
 
WeixinTokenDTO authTokenDTO =  this .getAuthTokenDTO(request);
 
logger.debug( "Token="  + authTokenDTO);
 
AuthTokenState authTokenState = validateToken(authTokenDTO, authTokenExpireTime);
 
switch  (authTokenState) {
 
case  ACCESS: {
 
cmptResult = SUCCESS_RESULT;
 
Map<String, String> header =  new  HashMap<>();
 
header.put(HeaderKeyConstants.HEADER\_APP\_ID_KEY, authTokenDTO.getAppID());
 
header.put(CmptHeaderKeyConstants.HEADER\_WEIXIN\_OPENID_KEY, authTokenDTO.getOpenID());
 
header.put(CmptHeaderKeyConstants.HEADER\_WEIXIN\_SESSION_KEY, authTokenDTO.getSessionKey());
 
cmptResult.setHeader(header);
 
break ;
 
}
 
case  EXPIRED: {
 
cmptResult = buildCmptResult(RespErrCode.AUTH\_TOKEN\_EXPIRED,  "token 過時, 請從新獲取 Token!" );
 
break ;
 
}
 
case  INVALID: {
 
cmptResult = buildCmptResult(RespErrCode.AUTH\_INVALID\_TOKEN,  "Token 無效!" );
 
break ;
 
}
 
}
 
return  cmptResult;
 
}
 
...
 
}

  

上面例子看不懂不要緊,接下來會詳細闡述組件的設計思路。

一、組件接口定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public  interface  ICmpt {
     /**       
      * 組件執行入口
      *
      * @param request
      * @param config,組件實例的參數配置
      * @return
      */   
     CmptResult execute(CmptRequest request, Map<String, FieldDTO> config);
 
     /**
      * 銷燬組件持有的特殊資源,好比線程。
      */
     void  destroy();
}

  

execute 是組件執行的入口方法,request 前面提到過是 http 請求的封裝,config 是組件的特殊配置,好比上面例子提到的微信認證組件就有一個自定義配置 -Token 的有效期,不一樣的 API 使用該組件能夠設置不一樣的有效期。

FieldDTO 定義以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public  class  FieldDTO {
 
private  String title;
 
private  String name;
 
private  FieldType fieldType = FieldType.STRING;
 
private  String defaultValue;
 
private  boolean  required;
 
private  String regExp;
 
private  String description;
 
}

  

CmptResult 爲組件執行後的返回結果,其定義以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public  class  CmptResult {
 
RespErrMsg respErrMsg; // 組件返回錯誤信息
 
private  boolean  passed; // 組件過濾是否經過
 
private  byte \[\] data; // 組件返回數據
 
private  Map<String, String> header =  new  HashMap<String, String>(); // 透傳後端服務響應頭信息
 
private  MediaType mediaType; // 返回響應數據類型
 
private  Integer statusCode =  200 ; // 默認返回狀態碼爲 200
 
}

  

二、組件類型定義

執行器須要根據組件類型和組件執行結果判斷是要直接返回客戶端仍是繼續往下面執行,好比認證類型的組件,若是認證失敗是不能繼續往下執行的,但緩存類型的組件沒有命中才繼續往下執行。固然這樣設計存在一些缺陷,好比新增組件類型須要執行器配合調整處理邏輯。(Kong 也提供了大量的功能組件,沒有研究過其網關框架是如何跟組件配合的,是否支持用戶自定義組件類型,知道的朋友詳細交流下。)

初步定義以下組件類型:

認證、鑑權、流量管控、緩存、路由、日誌等。

其中路由類型的組件涵蓋了協議轉換的功能,其負責調用上游系統提供的服務,能夠根據上游系統提供 API 的協議定製不一樣的路由組件,好比:Restful、WebService、Dubbo、EJB 等等。

三、組件執行位置和優先級設定

執行位置:Pre、Routing、After,分別表明後端服務調用前、後端服務調用中和後端服務調用完成後,相同位置的組件根據優先級決定執行的前後順序。

四、組件發佈形式

組件打包成標準的 Jar 包,經過 Admin 管理界面上傳發布。

附 - 組件可視化選擇 UI 設計

組件熱插拔設計和實現

JVM 中 Class 是經過類加載器 + 全限定名來惟一標識的,上面章節談到組件是以 Jar 包的形式發佈的,但相同組件的多個版本的入口類名須要保持不變,所以要實現組件的熱插拔和多版本並存就須要自定義類加載器來實現。

大體思路以下:

網關接收到 API 調用請求後根據請求參數從緩存裏拿到 API 配置的組件列表,而後再逐一參數從緩存裏獲取組件對應的類實例,若是找不到則嘗試經過自定義類加載器載入 Jar 包,並初始化組件實例及緩存。

附 - 參考示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  static  ICmpt newInstance( final  CmptDef cmptDef) {
     ICmpt cmpt =  null ;
     try  {
         final  String jarPath = getJarPath(cmptDef);
         if  (logger.isDebugEnabled()) {
             logger.debug( "嘗試載入 jar 包,jar 包路徑: "  + jarPath);
         }
         // 加載依賴 jar
         CmptClassLoader cmptClassLoader = CmptClassLoaderManager.loadJar(jarPath,  true );
         // 建立實例
         if  ( null  != cmptClassLoader) {
             cmpt = LoadClassUtil.newObject(cmptDef.getFullQualifiedName(), ICmpt. class , cmptClassLoader);
         else  {
             logger.error( "加載組件 jar 包失敗! jarPath: "  + jarPath);
         }
     catch  (Exception e) {
         logger.error( "組件類加載失敗,請檢查類名和版本是否正確。ClassName="  + cmptDef.getFullQualifiedName() +  ", Version="  + cmptDef.getVersion());
         e.printStackTrace();
     }
     return  cmpt;
}

  

補充說明:

自定義類加載器可直接須要繼承至 URLClassLoader,另外必須指定其父類加載器爲執行器的加載器,不然組件無法引用網關的其它類。

API 故障隔離及超時、熔斷處理

在詳細闡述設計前先講個實際的案例,大概 12 年的時候某公司自研了一款 ESB 的中間件(企業服務總線跟 API 網關很相似,當年 SOA 理念大行其道的時候都推崇的是 ESB,側重服務的編排和異構系統的整合。),剛開始用的還行,但隨着接入系統的增多,忽然某天運維發現大量 API 出現緩慢甚至超時,初步檢查發現 ESB 每一個節點的線程幾乎消耗殆盡,起初判斷是資源不夠,緊急擴容後仍是很快線程佔滿,最終致使上百個系統癱瘓。

最終找到問題的癥結是某個業務系統自身的緣由致使服務不可用,下游業務系統請求大量堆積到 ESB 中,從而致使大量線程堵塞。

以上案例說明了一個在企業應用架構設計裏面的經典原則 - 故障隔離,因爲全部的 API 請求都要通過網關,必須隔離 API 之間的相互影響,尤爲是個別 API 故障致使整個網關集羣服務中斷。

接下來分別介紹故障隔離、超時管控、熔斷的實現思路。

一、故障隔離

有兩種方式能夠實現,一是爲每一個 API 建立一個線程池,每一個線程分配 10~20 個線程,這也是經常使用的隔離策略,但這種方式有幾個明顯的缺點:

  • 線程數會隨着 API 接入數量遞增,1000 個 API 就須要 2 萬個線程,光線程切換對 CPU 就是不小的開銷,而其線程還須要佔用必定的內存資源;
  • 平均分配線程池大小致使個別訪問量較大且響應時間相對較長的 API 吞吐量上不去;
  • Netty 自己就有工做線程池了,再增長 API 的線程池,致使某些須要 ThreadLocal 特性的編程變得困難。

二是用信號量隔離,直接複用 Netty 的工做線程,上面線程池隔離提到的 3 個缺點均可以基本避免, 建議設置單個 API 的信號量個數小於等於 Netty 工做線程池數量的 1/3,這樣既兼顧了單個 API 的性能又不至於單個 API 的問題致使整個網關堵塞。

具體實現能夠考慮直接引用成熟的開源框架,推薦 Hystrix,能夠同時解決超時控制和熔斷。

參考配置以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
         .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey ))
         .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                 // 艙壁隔離策略 - 信號量
                 .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                 // 設置每組 command 能夠申請的信號量最大數
                 .withExecutionIsolationSemaphoreMaxConcurrentRequests(CmptInvoker.maxSemaphore)
                 /* 開啓超時設置 */
                 .withExecutionIsolationThreadInterruptOnTimeout( true )
                 /* 超時時間設置 */
                 .withExecutionIsolationThreadTimeoutInMilliseconds(timeout)
                 .withCircuitBreakerEnabled( true ) // 開啓熔斷
                 .withCircuitBreakerSleepWindowInMilliseconds(Constants.DEFAULT_CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS) // 5 秒後會嘗試閉合迴路

  

二、超時管控

API 的超時控制是必需要作的,不然上游服務即使是間歇性響應緩慢也會堵塞大量線程(雖然經過信號量隔離後不會致使整個網關線程堵塞)。

其次,每一個 API 最好能夠單獨配置超時時間,但不建議可讓用戶隨意設置,仍是要有個最大閾值。(API 網關不適合須要長時間傳輸數據的場景,好比大文件上傳或者下載、DB 數據同步等)

實現上能夠直接複用開源組件的功能,好比:HttpClient 能夠直接設置獲取鏈接和 Socket 響應的超時時間,Hystrix 能夠對整個調用進行超時控制等。

三、熔斷

熔斷相似電路中的保險絲,當超過負荷或者電阻被擊穿的時候自動斷開對設備起到保護做用。在 API 網關中設置熔斷的目的是快速響應請求,避免沒必要要的等待,好比某個 API 後端服務正常狀況下 1s 之內響應,但如今由於各類緣由出現堵塞大部分請求 20s 才能響應,雖然設置了 10s 的超時控制,但讓請求線程等待 10s 超時不只沒有意義,反而會增長服務提供方的負擔。

爲此咱們能夠設置單位時間內超過多少比例的請求超時或者異常,則直接熔斷鏈路,等待一段時間後再次嘗試恢復鏈路。

實現層面能夠直接複用 Hystrix。

運行時配置更新機制

前面章節提到過出於性能考慮網關在運行時要儘量減少對 DB 的訪問,設計上能夠將 API、組件等關鍵內容進行緩存,這樣一來性能是提高了,但也帶來了新的問題,好比 Admin 對 API 或者組件進行配置調整後如何及時更新到集羣的各個網關節點。

解決方案不少,好比引入消息中間件,當 Admin 調整配置後就往消息中心發佈一條消息,各網關節點訂閱消息,收到消息後刷新緩存數據。

咱們在具體實現過程當中採用的是 Zookeeper 集羣數據同步機制,其實現原理跟消息中間件很相似,只不過網關在啓動的時候就會向 ZK 節點進行註冊,也是被動更新機制。

性能考慮

性能是網關一項很是重要的衡量指標,尤爲是響應時間,客戶端原本能夠直連服務端的,如今增長了一個網關層,對於一個自己耗時幾百毫秒的服務接入網關後增長几毫秒,影響卻是能夠忽略不計;但若是服務自己只須要幾毫秒,由於接入網關再增長一倍的延時,用戶感覺就會比較明顯。

建議在設計上須要遵循以下原則:

  1. 核心網關子系統必須是無狀態的,便於橫向擴展。
  2. 運行時不依賴本地存儲,儘可能在內存裏面完成服務的處理和中轉。
  3. 減少對線程的依賴,採用非阻塞式 IO 和異步事件響應機制。
  4. 後端服務若是是 HTTP 協議,儘可能採用鏈接池或者 Http2,測試鏈接複用和不復用性能有幾倍的差距。(TCP 創建鏈接成本很高)

附 -HttpClient 鏈接池設置:

1
2
3
4
5
6
7
PoolingHttpClientConnectionManager cmOfHttp =  new  PoolingHttpClientConnectionManager();
cmOfHttp.setMaxTotal(maxConn);
cmOfHttp.setDefaultMaxPerRoute(maxPerRoute);
httpClient = HttpClients.custom()
         .setConnectionManager(cmOfHttp)
         .setConnectionManagerShared( true )
         .build();

  

說明:

httpClient 對象能夠做爲類的成員變量長期駐留內存,這個是鏈接池複用的前提。

結語

API 網關做爲企業 API 服務的匯聚中心,其良好的性能、穩定性和可擴展性是基礎,只有這個基礎打紮實了,咱們才能在上面擴展更多的特性。

這篇文章主要介紹網關的整體架構設計, 後面的篇幅在詳細探討下各類組件的具體設計和實現。

 

 

 

 

 

轉自:https://www.cnblogs.com/kaleidoscope/p/9648004.html

相關文章
相關標籤/搜索