京東京麥:微服務架構下的高可用網關與容錯實踐
各自的系統在這一指導思想下收獲了優雅的可維護性,但一方面也給接口調用提出了新的要求,比如眾多的 API 調用急需一個統一的入口來支持客戶端的調用。
在這種情況下 API Gateway 誕生,我們將接入、路由、限流等功能統一由網關負責,各自的服務提供方專注于業務邏輯的實現,從而給客戶端調用提供了一個穩健的服務調用環境。
之后,我們在網關大調用量的情況下,還要保證網關的可降級、可限流、可隔離等等一系列容錯能力。
今天跟大家分享微服務下京麥開放平臺的網關實現,以及我們如何抗量、如何在大訪問量的情況下做容錯處理,重點介紹容錯的方法及每種容錯方法的使用場景與經驗。
網關
這里說的網關是指 API 網關,意思是將所有 API 調用統一接入到 API 網關層,由網關層統一接入和輸出。
一個網關的基本功能有如下幾種能力:
- 統一接入
- 安全防護
- 協議適配
- 流量管控
- 長短鏈接支持
- 容錯
有了網關之后,各個 API 服務提供團隊可以專注于自己的的業務邏輯處理,而 API 網關更專注于安全、流量、路由等問題。
單體應用
當業務簡單、團隊組織很小時,我們常常把功能都集中于一個應用中,統一部署、統一測試,玩得不亦樂乎。
但隨著業務迅速發展,組織成員日益增多,我們再將所有的功能集中到一個 Tomcat 中去,每當更新一個功能模塊,勢必要更新所有的程序,搞不好還會牽一發動全身,導致實在難以維護的情況。
微服務
單體應用滿足不了我們逐漸增長的擴展需求之后,微服務就出現了,它是將原來應用集中于一體的架構。
比如商品功能、訂單功能、用戶功能拆分出去,各自有各自的自成體系的發布、運維等,這樣就解決了在單體應用下的弊端。
API 網關
進行微服務后,原先客戶端調用服務端的地方就要有 N 多個 URL 地址,包括商品的、訂單的、用戶等。
這時就必須要有個統一的入口和出口,這種情況下,我們的 API Gateway 就出現了,它很好地幫助我們解決了微服務下客戶端調用的問題。
泛化調用
對于普通的 RPC 調用,我要拿到服務端提供的 class 或者 jar 包,這樣過于繁重,更不好維護。
不過成熟的 RPC 框架都支持泛化調用,我們的網關就是基于這種泛化調用來實現的。
服務端開放出來他們的 API 文檔,我們拿到接口、參數、參數類型通過泛化調用到服務端程序。
public Object $invoke(String method, String[] parameterTypes, Object[] args);
容錯
容錯,這個詞的理解,書面意思就是可以容下錯誤,不讓錯誤再次擴張,讓這個錯誤產生的影響在一個固定的邊界之內。
“千里之堤,毀于蟻穴”,我們采用容錯的方式就是不讓這種蟻穴繼續變大。在工作中,降級、限流、熔斷器、超時重試等都是常見的容錯方法。
抗量
所謂的抗量,就是增大我們系統的吞吐量,所以容錯的第一步就是系統要能抗量,沒有量的情況下幾乎用不到容錯。
我們的容器使用的是 Tomcat,在傳統的 BIO 模型下,一請求一線程,在機器線程資源有限的情況下是沒有辦法來實現我們的目標的。
NIO 給我們提供了這個機會,基于 NIO 的機制,利用較少的線程來處理更多的連接。
連接多不可怕,通過調整機器的參數一臺 8c8g 的機器,超過 10w 是不成問題的。
Tomcat 的 Conector 修改成 NIO 后,我們再從代碼層面引入了 Servlet3,它是從 Tomcat7 以后支持的,NIO 是 Tomcat6 以后就支持的。
利用 Servlet3 的特性,所有的 request 和 response 都由 Tomcat 的工作線程來處理,我們將業務邏輯異步到別的業務線程中去。
在異步環境下,可以提高單位時間內的吞吐量,所有的 Servlet 請求都是由 Tomcat 的 Executor 線程池的線程處理的,也就是 Tomcat 的工作線程。
這些線程處理的時間越短越好,越短越能迅速地將線程歸還給 Executor 線程池,現在 Servlet 支持異步后就能將耗時的操作,比如有 RPC 請求的交給業務線程池來處理,使得 Tomcat 工作線程可立即歸還給 Tomcat 工作線程池。
另外,將業務異步處理之后,我們可以對業務線程池進行線程池隔離,這樣就避免了因一個業務性能問題而影響了其他的業務。
總結一下異步的優勢:
- 可以用來做消息推送,通過 Nginx 做代理,設置連接超時時間,客戶端通過心跳探測。
- 提高吞吐量。
- 請求線程和業務線程分開,從而可以通過業務線程池對業務線程做隔離。
脫離 DB
脫離 DB,這里不是說 DB 的性能不行,分庫分表、DB 集群化之后,在一定量的情況下是沒有問題的。
但是,如果從抗量的角度說的話,為何不使用 Redis 呢?如果軟件架構里面有一種銀彈的話,那么 Redis 就是這種銀彈。
另外一個脫離 DB 的原因是:每當大促備戰前夕我們一項重點的工作就是優化慢 SQL,但它就像小強一樣生命力是那樣的頑強,殺不絕。
如果有那么一個慢 SQL,平時是沒有問題的,比如一個查詢大字段的 SQL,平時量小不會暴露問題,但量一上來了,就是個災難。
再就是我們的網關,包括接入、分發、限流等這些功能都應該是很輕的,所以我們就通過數據異構的方式把數據重新轉載到 Redis 中,而且是將數據持久化到 Redis 里面去。
當然,使用 Redis 的過程中也需要注意大 key,大訪問量下也能讓集群趴下。
還有一個很重要的原因,我們使用的 DB 是 MySQL,鑒于 MySQL 的 failover 機制生效時間總是要長于 Redis 集群,最后就是因為 DB 切換的時候,常常伴隨 Web 應用服務器要重啟,將原來的連接釋放掉,才能方便使用新的數據庫連接。
多級緩存
最簡單的緩存就是查一次數據庫然后將數據寫入緩存。比如在 Redis 中設置過期時間,因為有過期失效,因此我們要關注下緩存的穿透率。
這個穿透率的計算公式,比如查詢方法 queryOrder(調用次數 1000/1s)里面嵌套查詢 DB 方法 query Product From DB(調用次數 300/s),那么 Redis 的穿透率就是 300/1000。
在這種使用緩存的方式下,是要重視穿透率的,穿透率大了說明緩存的效果不好。
還有一種使用緩存的方式就是將緩存持久化,也就是不設置過期時間,這個會面臨一個數據更新的問題。
一般有兩種辦法:
- 利用時間戳,查詢默認以 Redis 為主,每次設置數據的時候放入一個時間戳,每次讀取數據的時候用系統當前時間和上次設置的這個時間戳做對比。
比如超過 5 分鐘,那么就再查一次數據庫,這樣可以保證 Redis 里面永遠有數據,一般是對 DB 的一種容錯方法。
- 讓 Redis 真正作為 DB 來使用,就是如圖里畫的通過訂閱數據庫的 binlog,通過數據異構系統將數據推送給緩存,同時將緩存設置為多級。
可以通過使用 jvm cache 作為應用內的一級緩存,一般是體積小,訪問頻率大的更適合這種 jvm cache 方式,將一套 Redis 作為二級 remote 緩存,另外的最外層三級 Redis 作為持久化緩存。
超時與重試
超時與重試機制也是容錯的一種方法,凡是發生 RPC 調用的地方,比如讀取 Redis、DB、MQ 等。
因為網絡故障或者是所依賴的服務故障了,長時間不能返回結果,就會導致線程增加,加大 CPU 負載,甚至導致雪崩。所以對每一個 RPC 調用都要設置超時時間。
對于強依賴 RPC 調用資源的情況,還要有重試機制,但重試的次數建議 1-2 次。
另外如果有重試,超時時間還要相應都調小,比如重試 1 次,那么一共是發生 2 次調用。
如果超時時間配置的是 2s,那么客戶端就要等待 4s 才能返回,因此重試+超時的方式,超時時間要調小。
這里也再談一下 1 次 PRC 調用的時間都消耗在哪些環節。
1 次正常的調用統計的耗時主要包括:①調用端RPC框架執行時間 + ②網絡發送時間 + ③服務端RPC框架執行時間 + ④服務端業務代碼時間。
調用方和服務方都有各自的性能監控,比如調用方 tp99 是 500ms,服務方 tp99 是 100ms,找了網絡組的同事確認網絡沒有問題的。
那么時間都花在什么地方了呢?兩種原因:客戶端調用方,還有一個原因是網絡發生 TCP 重傳,所以要注意這兩點。
熔斷
熔斷技術可以說是一種“智能化的容錯”,當調用滿足失敗次數,失敗比例就會觸發熔斷器打開,有程序自動切斷當前的 RPC 調用,來防止錯誤進一步擴大。
實現一個熔斷器主要是考慮三種模式:
- 關閉
- 打開
- 半開
各個狀態的轉換如下圖:
在了解了熔斷器的狀態機制后,我們可以自己來實現一個熔斷器。當然也可以使用開源的解決方案,比如 Hystrix 中的 breaker。
下圖是一個熔斷器打開關閉的示意圖:
這里要談的是熔斷器的使用注意項:
我們在處理異常時,要根據具體的業務情況來決定處理方式。比如我們調用商品接口,對方只是臨時做了降級處理,那么作為網關調用就要切到可替換的服務上來執行或者獲取托底數據,給用戶友好提示。
還有要區分異常的類型,比如依賴的服務崩潰了,這個可能需要花費比較久的時間來解決,也可能是由于服務器負載臨時過高導致超時。
作為熔斷器應該能夠甄別這種異常類型,從而根據具體的錯誤類型調整熔斷策略。
增加手動設置,在失敗的服務恢復時間不確定的情況下,管理員可以手動強制切換熔斷狀態。最后,熔斷器的使用場景是調用可能失敗的遠程服務程序或者共享資源。
如果是本地緩存本地私有資源,使用熔斷器則會增加系統的額外開銷。還要注意,熔斷器不能作為應用程序中業務邏輯的異常處理替代品。
線程池隔離
在抗量這個環節,Servlet3 異步時,有提到過線程隔離。線程隔離的直接優勢就是防止級聯故障,甚至是雪崩。
當網關調用 N 多個接口服務的時候,我們要對每個接口進行線程隔離,比如我們有調用訂單、商品、用戶。
那么訂單的業務不能夠影響到商品和用戶的請求處理。如果不做線程隔離,當訪問訂單服務出現網絡故障導致延時,線程積壓最終導致整個服務 CPU 負載滿。
就是我們說的服務全部不可用了,有多少機器都會被此刻的請求塞滿。那么,有了線程隔離就會使得我們的網關能保證局部問題不會影響全局。
降級、限流
關于降級限流的方法業界都已經有很成熟的方法了,比如 Failback 機制,限流方法令牌桶、漏桶、信號量等,這里談一下我們的一些經驗。
降級一般都是由統一配置中心的降級開關來實現的,那么當有很多個接口來自同一個提供方,這個提供方的系統或這機器所在機房網絡出現了問題,我們就要有一個統一的降級開關。
不然就要一個接口一個接口地來降級,也就是要對業務類型有一個大閘刀。
還有就是降級切記暴力降級,什么是暴力降級?比如把論壇功能降調,結果用戶顯示一個大白板,我們要實現緩存住一些數據,也就是有托底數據。
限流一般分為分布式限流和單機限流,如果實現分布式限流的話就要一個公共的后端存儲服務,比如 Redis,在大 Nginx 節點上利用 Lua 讀取 Redis 配置信息。
我們現在的限流都是單機限流,并沒有實施分布式限流。
網關監控與統計
API 網關是一個串行的調用,每一步發生的異常都要記錄下來,統一存儲到一個地方,比如 Elasticsearch 中,便于后續對調用異常的分析。
鑒于公司 Docker 申請都是統一分配,而且分配之前 Docker 上已經存在 3 個 Agent 了,不再允許增加。
我們自己實現了一個 Agent 程序,來負責采集服務器上面的日志輸出,然后發送到 Kafka 集群,再通過 Web 查詢消費到 Elasticsearch 中。現在做的追蹤功能還比較簡單,這塊還需要繼續豐富。
總結
網關基本功能有統一接入、安全防護、協議適配等。這篇文章里我們并沒有講如何來實現這些基本的功能,因為現在有很多成熟的解決方案可以直接拿過來使用。
比如 Spring Cloud 這種全家桶里面的很多組件,Mashape 的 API 層 Kong 等。
責任編輯:售電衡衡
-
碳中和戰略|趙英民副部長致辭全文
2020-10-19碳中和,碳排放,趙英民 -
兩部門:推廣不停電作業技術 減少停電時間和停電次數
2020-09-28獲得電力,供電可靠性,供電企業 -
國家發改委、國家能源局:推廣不停電作業技術 減少停電時間和停電次數
2020-09-28獲得電力,供電可靠性,供電企業
-
碳中和戰略|趙英民副部長致辭全文
2020-10-19碳中和,碳排放,趙英民 -
深度報告 | 基于分類監管與當量協同的碳市場框架設計方案
2020-07-21碳市場,碳排放,碳交易 -
碳市場讓重慶能源轉型與經濟發展并進
2020-07-21碳市場,碳排放,重慶
-
兩部門:推廣不停電作業技術 減少停電時間和停電次數
2020-09-28獲得電力,供電可靠性,供電企業 -
國家發改委、國家能源局:推廣不停電作業技術 減少停電時間和停電次數
2020-09-28獲得電力,供電可靠性,供電企業 -
2020年二季度福建省統調燃煤電廠節能減排信息披露
2020-07-21火電環保,燃煤電廠,超低排放
-
四川“專線供電”身陷違法困境
2019-12-16專線供電 -
我國能源替代規范法律問題研究(上)
2019-10-31能源替代規范法律 -
區域鏈結構對于數據中心有什么影響?這個影響是好是壞呢!