大量新老項目接入,服務限流如何排除差異快速落地?
一、背景
1、場景
某一天有一個項目服務突然出現異常,我們定位到的原因是有大量的突發流量進來,那么我們會先采取被動的臨時手段去處理當前故障,接著上線Nginx的限流功能進行快速止損,防止二次故障。但是Nginx的限流功能是比較粗糙的,所以我們有一個更好的長期措施,即項目接入限流功能,并實現按維度進行精細化的限流,化被動解決為主動防御、主動治理。對于這一個項目來說,做到這一步應該是比較好的效果。
那么我們能否更進一步?更進一步我們需要考慮經驗積累、通用性、整體效益這三個方面。
- 經驗積累:有什么經驗總結可以被其他項目借鑒?
- 通用性:解決方案其他項目可以直接使用嗎?
- 整體效益:可以降低團隊的整體成本嗎?
2、問題與目標
我們的方案設計需要考慮不同項目之間的差異性,因為我們需要接入的項目可能有十幾個甚至更多,不同項目的差異性會體現在開發語言、請求類型、生命周期、部署環境、鏈路節點等方面。
其中我們特別需要注意的是生命周期,有的項目可能是十幾年前就存在的老項目,有的可能是最近兩三年的較新的項目,還有的可能是以后會建立的全新項目,我們的方案需要能夠同時匹配這三種項目,以及需要抹平其他的幾種差異。
此外,我們的方案也有一些其他的期望目標,比如低成本、高效率、高質量,以及專業性、穩定性、可擴展性、高性能。
- 低成本:接入所需的成本是比較低的,包括開發、運維、硬件等成本。
- 高效率:能夠快速完成接入落地工作。
- 高質量:提供了整套限流的解決方案,接入方使用前后都能夠得到有效的技術支持和指引。
其他幾點則比較好理解,那么擺在我們眼前的問題就是項目的數量多,差異性大,期望目標和要求也多,那么我們應該如何設計這個方案?
3、協作方式
在介紹具體的技術方案之前,我簡單講述一下我們的協作方式。
在公司內部推廣一個技術落地,我們通常會采用單項目的方式進行,即單獨某個項目了解需求、設計、開發,在本項目落地之后再逐步推廣到其他項目,這種方式雖然沒有問題,但是存在幾個缺點:
- 需求的全面性:不能充分兼顧到其他項目的需求,方案設計上可能會有缺漏,導致后期推廣困難,因為只考慮到了本項目的需求和場景。
- 成本負擔:成本由單一項目承擔,可能會影響到項目組原本的工作,對項目本身也會有比較大的影響。
基于以上情況,我們設計了一個專業組機制,希望能夠讓專業的人做專業的事,工作流程如下:
首先從多個有需求的項目組里招募感興趣或者有經驗的成員加入專業組,比如圖示里從項目組1、項目組2和項目組3里各抽出一個人成立專業組,然后針對這幾個項目設計總體方案,以滿足他們的需求,在項目落地之后,再逐步推廣到其他項目。
與單項目推進相比,專業組機制的優點如下:
- 充分兼顧到了典型場景的需求。因為我們會從原先的三個項目組里去對接需求,那么這三個項目組的場景是比較明確的。
- 成本由多個項目分攤。每個項目只需要出1~2個人即可,對項目組原先的工作影響程度較低。
- 專業組的進出機制可以充分發揮成員的技術積累優勢和主觀能動性。能夠進專業組的成員一般是在專業問題上比較有技術積累的,或者是對該問題很感興趣,會主動研究問題。
在解決現有功能上,專業組在整個過程中發揮了很大的作用。
二、技術方案
下面我們介紹具體的技術方案。
1、限流實現層
首先的問題是我們的限流應該做在哪一層?一般來說我們可以在應用層限流,也就是在API服務節點再增加一個web中間件,在請求進入API服務時判斷請求是否被限流,這是比較常見的方式,對于單個項目而言成本也是最低的。
除了應用層實現限流之外,還有接入層實現限流的做法。在原先的LB到API節點之間,我們可以增加一個網關層,將限流功能做在網關層。
兩種方法相比較,應用層限流有以下缺陷:
- 異常流量依然落在API服務。如果有大量的突發流量進來,還是會把壓力落在API服務,那么效果相對而言是比較差的。
- 邏輯耦合,無法獨立變更限流功能。
- 職責不明確,增加服務復雜性。
- 多項目情況下難以復用,可行性低,無法達到我們的需求。比如我們前面提到的差異性,不同項目的開發語言、部署方式等不一樣,如果我們在應用層實現限流,就需要針對每一個項目單獨適配,那么成本是比較高的。
基于以上情況我們選擇了接入層實現限流。在接入層實現,我們就需要一個網關作為統一的記錄層。
2、Kong網關
按照官方的說法,Kong網關是一個輕量、快速、靈活的云原生API網關,Kong是基于OpenResty和Nginx實現的。我們在解決這一個問題的情況下,為什么要選擇Kong?也是基于我們常見的高性能、高可用、靈活、易擴展等方面。其中我們特別在意以下兩點:
- 一是輕量靈活,在最輕量部署的情況下,僅需一個主進程和一個yaml配置文件。
- 二是易擴展,Kong的插件機制可以實現在請求生命周期的各階段執行自定義邏輯,也就是我們可以做很多自己需要的工作。
3、Kong插件
Kong的插件機制可以支持多種語言,如Golang、Lua、Python、JS。我們選擇了Lua和Golang作為我們的開發插件。兩種之間的區別是Lua插件在Kong進程中是直接執行的,也就是它和Kong是同一個進程,而每一個Go插件是一個單獨的插件,和Kong主進程之間的通信需要通過IPC進行。
Lua和Golang相比較,在團隊技術棧匹配度、生態、工程化難度、開發維護成本上,Golang都占有比較明顯的優勢,而在性能方面Lua會更高。因為Golang在插件每次執行的時候都需要進行IPC通信,在IPC通信次數較高的情況下,性能會受到較大影響。但是我們在實踐過程中發現,在現有場景下,我們的IPC通信次數相對較少,對性能的影響也比較低。所以總體上我們是優先使用Golang開發插件,如果Golang實現不了再退而求其次用Lua進行開發。
4、插件模塊化
前面我們通過把Kong作為接入層解決了不同項目在環境上的差異性,接著我們可以使用插件機制解決不同項目在需求上的差異性。
我們可以將一個項目的限流需求分為業務需求模塊和限流功能模塊兩個部分。
- 業務需求模塊是比較頻繁迭代的,這一方面的需求包括解析用戶名等,根據路徑、IP、請求方法等不同,每個項目也會有不同的需求。
- 限流功能模塊相對而言比較穩定,比如我們需要進行短連接的頻率限流、并發數限流等。
在具體的限流算法的實現之上,還有一個策略的應用,即對于同一個限流方法,比如令牌桶,我們可能會設計不同的策略進行應用。
基于以上模塊劃分,我們在實際開發時分了三層,從下到上依次是限流算法層、限流插件層、業務插件層。
1)限流算法層
限流算法層主要是SDK的形式,因為我們的限流場景比較多,算法也不同,所以我們對于每一個不同的場景都會單獨開發SDK,比如短連接限制頻率的場景可能會有令牌桶或時間窗口等,短連接并發數、長連接也有具體的實現。
需要特別注意的是客戶端節流的場景,我們在實際應用時發現,該場景除了接入Kong服務端性能之外,客戶端請求時也需要進行限流。客戶端限流指的是客戶端主動發起請求調用第三方頻率時,我們也希望能夠控制頻率,保護第三方服務。
限流算法的SDK是以代碼嵌入的形式嵌入到限流插件層。
2)限流插件層
限流插件層會調用SDK,針對不同的場景需要設置不同的策略,比如我們的令牌桶限制短連接頻率,一個插件可能會實現令牌預知功能,而另一個插件可能會做限流額度、動態調控等,也就是已經往業務層走了一步。
3)業務插件層
業務插件層會根據項目組不同的需求制定單獨的業務場景,比如不同的項目組可能會有不同的限流維度進行解析,需要項目組自己適配等。
如配置監聽,以及業務組的其他業務定制功能,也可以做在這一層。
我們的分層主要分為三層,實際Kong執行會有兩個插件,分別是業務插件和限流插件,這里的插件指的是Kong本身自帶的執行插件。
接下來以長輪詢場景為例,講述需要限制接入的客戶端連接數場景。
首先請求進入Kong網關時,在業務插件我們會解析它的限流維度,比如user、path、method、ip,然后把這些維度生成一個字符串限流KEY,它可能是一個比較長的字符串。
接著它會匹配特定的限流策略,如果匹配不到則會進入兜底策略,把這些信息生成限流的協議數據。業務插件的配置會記錄請求的維度,比如path、method等,匹配到了限流策略之后,就會去生成它的限流協議,限流協議里會設置請求最大并發數、請求超時時間等。接著業務插件會把這些信息組裝成一個協議,協議的格式如圖右下角。
然后把這些數據通過Kong的Context機制傳遞到限流插件,限流插件則會解析該協議數據,然后執行限流邏輯判斷,來判斷這個請求應該被限流還是透傳到上游服務。
5、分工
項目組的需求,如果已有的插件可滿足,則直接使用;如果不滿足,可定制自己的業務插件。定制插件時項目組只需要關注項目需要的業務邏輯即可,相關的生態已由專業組提供,專業組負責所有限流插件、部分通用業務插件的開發,網關、插件公用功能開發,搭建安全性、穩定性保障,以及不同場景下的限流算法的設計實現等工作。插件開發完成后提交到插件庫,后續其他項目組若有相同的項目,也可以直接使用。
6、分發與使用
接下來我們簡單了解一下插件的分發與使用機制。
首先插件開發完成之后,我們會在GitLab上給它打一個tag,然后觸發一個CI流程。CI流程做的工作主要是編譯、寫入參數信息、壓縮打包,把插件打包成一個tag壓縮文件,將其上傳到文件服務,再發布到git release界面,那么項目組使用時就可以在release界面看到我們可以使用的插件及其功能。
而項目組在具體使用的時候,需要Fork部署倉庫,然后設置需要使用的多個插件及版本。接著再去觸發CI流程,我們會把插件重新下載回來,并且進行解析、解包等工作,然后把這些插件和它們的信息寫入到Kong里,將其構建、推送成為一個完整的鏡像,有了鏡像之后,我們就可以在部署平臺上進行部署,那么項目組就完成了Kong和插件的使用。
7、接入方式
接下來簡單介紹項目組實際的接入方式。前面我們提到項目組的差異比較多,因此我們分了5種情況:
1)如果是一個全新的項目,則直接接入。
2)現存項目,但是沒有網關,那么主要考慮業務場景,整體評估之后再進行接入。
3)現存項目,且已有Kong網關,這種情況下直接安裝和使用我們的限流插件即可。因為我們開發的插件也是Kong的標準插件,項目組使用限流插件時只需要對接其限流協議。
4)現存項目,且已有其他網關,那么就不能使用Kong網關了,直接對接限流算法的SDK即可。
5)客戶端限流的情況下不需要Kong網關,也是直接對接客戶端限流的SDK即可。
8、整體架構
該方案的整體結構是比較簡單的,便于適配不同的項目。
總體而言就是在LB和API節點之間加了一個Kong網關,網關方面首先我們會插入一個tracing插件,這個插件內嵌了Open-Tracing的實現,接著項目組根據自身需求選擇業務插件和限流插件。
業務插件有一個功能是動態監聽限流配置,需要外部的配置中心實現。動態監聽主要是考慮到當有異常流量進來時,我們可能需要動態調整額度,比如場景可能是原先給該項目定的額度已經不能滿足其需求了,該項目最近的用戶量猛增,那么在這種情況下,我們不能限制它的流量,因此需要將額度調大,避免對業務造成不好的影響。
限流插件根據不同限流算法的實現,我們也有不同的依賴,比如我們在大部分情況下都會做分布式限流,我們選擇通過Redis加一個Lua腳本實現分布式限流,那么這種情況下依賴還需要增加一個Redis。也有少部分情況下它可能是作為一個本地限流,需要根據項目組的具體實現。
三、實施方案
實施方案指的是一個項目組從0接觸到整個方案完成上線的過程,前期包括需求、評估和開發這幾個步驟。
1、需求
在需求階段,項目組根據我們提供的模板提issue,里面會包含以下內容:
- 限流場景、類型
- 限流維度、策略
- 項目組需要特化的業務需求
- 現有插件能否滿足項目組的需求
2、評估
接著進行整體的評估工作,評估的方面包括:
- 開發、維護、硬件等成本
- 部署架構
- 性能、鏈路的影響
3、開發(若需要)
如果我們現有的插件無法滿足項目組的需要,那么就需要進行開發工作。對于開發工作,我們有一個比較詳細的指南,因此上手開發的成本比較低。項目組需要了解以下幾個方面:
- 本地環境部署:我們提供了一個docker-compose環境可以一鍵拉起本地的開發環境。
- 相關知識快速了解:包括Kong的一些概念等。
- 完善的開發文檔:包括具體插件如何寫之類的,簡單來說就是我們提供了一個手把手教會項目組進行開發的文檔。
接下來是測試、部署與上線的環節。
4、測試
在測試環節我們的目的是需要保證接入Kong和插件之后的質量。
- 首先需要保證我們的限流功能是符合預期的。
- 原來的API服務的功能回歸之后需要與之前保持一致。
鏡像流量和故障演練對于我們而言是比較重要的。
- 鏡像流量指的是把生產環境的流量導入到測試環境,可以提前驗證限流之后我們的流量是否異常。也就是我們需要一個單獨的鏡像流量的環境,將生產環境的流量在該環境里提前進行驗證,因為我們增加了一個新的網關,變更的影響面是比較廣的,而且可能還會遇到奇怪的問題。如果我們提前把生產環境的流量導入過來,就可以提前發現這些問題。
- 故障演練也是為了提前發現可能存在的問題和風險。
關于這兩點,我們內部有兩個最佳實踐說明,分別是《如何做好鏡像流量》和《如何做好從場景出發的故障演練》。
5、部署
接下來是部署環節。
- 我們基于內部的一致性交付系統實現了模板部署,即不同的部署平臺,比如物理機或公有云、私有云,都可以用一致性交付的平臺實現統一的部署,那么項目組在部署時可以不用考慮太多的細節問題。
- 其次我們在部署的時候可以一鍵接入監控、日志、告警、鏈路等功能。
- 我們部署時有一個完善的部署指南會介紹方方面面的問題,比如我們接入Kong網關之后,對原先的服務拆分問題是如何處理的。
6、上線
最后的步驟是上線,我們也提供了詳細的上線步驟,以及checklist和最重要的灰度、回滾的技術方案。
總體而言我們這幾個環節也達到了手把手指引、實現的整個過程。
四、小結
前面我們提到的目標是低成本、高效率、高質量這幾個方面。
1、低成本
1)開發成本:項目組對接時基本上是0成本,或者只有少量的開發工作。
2)維護成本:后續的維護、升級工作都是由專業組負責的。
3)硬件成本:主要的是Kong的集群部署,根據項目組本身的流量大小,可以部署不同的副本數。如果選擇了分布式限流,那么還需要部署一個Redis,我們一般建議部署一個新的Redis,但是為了節約成本也可以和原先項目已有的Redis共用。
2、高效率
1)接入時間:我們在項目實踐時,最快的接入時間是三天,三天是在項目組沒有自己定制化的需求,不需要開發的情況下走完我們之前介紹的環節,即評估、測試、部署、上線。
2)硬件配置:我們有一個推薦配置,也就是根據我們不同的流量進行單獨測試,以及我們提供了比較完整的性能測試數據作為參考。
3)高效部署:網易內部的平臺相對完善,借助于我們的CI自動化、配置管理系統、容器云系統和一致性交付系統可以實現高效部署。
4)文檔建設:文檔建設我們是比較完善的,從剛開始接觸限流這套方案到上線的整個過程中各個環節,我們都有很詳細的文檔指引,遇到問題可以從文檔中尋找,也可以找專業組的同事了解。
3、高質量
1)技術支持:由專業組的同事提供技術支撐。
2)多樣場景:我們的插件機制適配了多樣化的限流場景和業務需求。
3)經驗復用:在各個項目對接和使用過程中的經驗或優化的問題,都可以總結復用,因為我們不同項目使用的是同一套技術體系,便于積累經驗。
4)自定擴展:我們雖然定制化了一些插件,以及一些Kong的部署過程,但是在實際開發時也考慮到了Kong原生態的兼容性,也就是項目組依然可以使用Kong原生態提供的豐富功能。
5)運維體系:接入了整套運維設施相關的功能體系,也就是前面我們提到的監控、報警、日志等。
6)充分驗證:我們的插件功能和性能得到了充分的測試,上線流程也比較完善,可以保證我們的可靠性。
通過以上環節可以保證服務限流方案的低成本、高效率和高質量。
Q&A
Q1:這個限流方案在哪些類型的項目上具有通用性?
A1:這個問題其實回歸到了我們項目的差異性上,總體來說,我們對于不管是普通的http短連接的項目,或者是一些長輪詢的場景以及長連接的場景,我們都有單獨做適配。因為本身Kong對于這幾種請求都可以支持,也就是我們只需要針對這幾種場景開發對應的插件即可。
Q2:Kong和插件對性能的影響大嗎?
A2:這一點我們在實際設計的時候也考慮過,經過實踐發現對性能的影響是比較小的。性能一方面是對我們的請求耗時的影響,經過我們的測試,耗時大概是在5毫秒左右,另外一方面是QPS,因為Kong和插件本身是支持橫向擴展的,以及Kong本身的性能也比較高,所以對QPS的影響是比較小的,我們在測試時最高達到過五六萬QPS,因此基本上不需要過于擔心這個問題。
Q3:如何實現長輪詢連接數限制?
A3:長輪詢本質是請求并發數的一個性質,只不過它是http請求,可能是一個掛起的狀態,需要掛起比如一分鐘。這個場景的實現過程大概如下:Kong本身對插件機制可以保證在請求前和請求后,我們可以插入鉤子函數。在請求前我們的鉤子函數會在Redis的Zset里設置一個請求的狀態,那么Zset的值就是該請求的唯一ID,比如它是一個雪花ID,然后分就是請求的過期時間,這個過期時間是當前時間加上請求配置里的TTL(請求的超時時間),兩者加起來就是該請求實際失效的時間。在請求進來時,我們會在Zset里記錄請求的數據,表示當前的請求并發數已經加一了。然后在執行Redis Lua腳本的時候,它會先剔除那些已經過期了的請求數,在請求結束之后,會從Zset里移除該請求的數據,也就是請求并發數減一,這是大致原理。
作者:肖晗網易互娛技術中心數據與平臺服務部高級開發工程師 網易互娛技術中心數據與平臺服務部高級開發工程師,具有多年服務端經驗,現專注于CMDB、微服務、圖數據庫等方向。