轉轉測試環境治理的高效能實踐
轉轉測試環境治理歷經3個版本的迭代,環境搭建耗時及資源占用大幅度下降,在此過程中積累了豐富的實踐經驗。本文將從測試環境的需求及背景出發,介紹轉轉測試環境治理各個版本的原理、技術、優缺點,毫無保留地將轉轉的實踐經驗分享給各位讀者。
1. 背景及需求
1.1 系統架構的發展
很久很久以前,在并發量較低時系統大多采用單體架構,由單個web服務直接連接數據庫,由nginx在多個web服務節點間做負載均衡。
單體架構
隨著系統并發量的提高,單體架構無法滿足性能需求,需要對單體服務進行拆分,于是來到了微服務架構。微服務架構與單體架構顯著的不同在于鏈路更長更復雜了。
微服務架構
1.2 測試環境的需求
測試環境與線上環境的典型不同在于線上環境各節點一般情況下代碼是一致的,各節點是對等的,請求到達任意節點業務邏輯都是一致的;而測試環境一般是多分支并行開發,每個節點的邏輯是不一致的,既然要測試本次所開發的功能,那就要求請求能精準地到達所部署的節點。
在單體架構下,該需求是很容易實現的,通過采用修改nginx配置,在upstream中僅包含目標測試節點可以很容易實現對目標節點的精準測試,或者不使用nginx直接通過IP+port的形式進行調用也同樣可以實現請求精準到達目標節點。如下圖所示的A'及A''屬于兩個不同的需求,分別通過固定nginx upstream及IP+port的形式實現了請求的精準控制。
單體架構測試
而在微服務架構下,該需求實現起來就沒那么簡單了。如下圖所示,鏈路上包括服務A、B、C,圖中A'、B'、C'屬于同一個需求,而A''、B''、C''屬于另一個需求。采用在單體架構下的解決方案只能控制請求精準地到達A'或者A'',無法實現請求精準地到達B'、C'及B''、C''。
微服務架構測試
2. 傳統的測試環境解決方案-物理隔離
為了實現請求精準地到達被測服務節點,傳統的做法是提供多套完全互相隔離的測試環境,每套測試環境包含全量的服務及注冊中心、MQ broker等。每個需求分配一套測試環境,只需將被測服務部署至該環境內即可實現請求精準地到達被測節點,這種做法也稱作物理隔離。
物理隔離
物理隔離在公司服務數量較少時(20個服務以下)稱得上是完美的解決方案,非常簡單可靠。但是隨著服務數量的增長,物理隔離的缺點逐漸顯露——資源浪費太嚴重。假設整個系統有100個服務,而每次需求僅修改其中的1-3個服務,資源浪費率高達97%-99%。而且服務數量的增長,往往也意味著公司業務的發展壯大,同時進行中的需求數量也在增長,需要提供更多的測試環境,資源消耗巨大。
3. 轉轉測試環境V1-改進的物理隔離
轉轉公司傳統的測試環境解決方案屬于改進型的物理隔離。
3.1 穩定環境
在轉轉有一套穩定環境包含全量的服務并且與線上代碼一致。在測試環境不使用注冊中心,而是為每一個服務分配唯一的域名并通過修改機器host映射的方式手動控制服務發現。默認情況下,如果服務A部署在穩定環境的??192.168.1.1?
??上,那么所有測試環境機器的host文件中都存在一項配置??192.168.1.1 A.zhuaninc.com?
?。
3.2 動態環境
每次需求所申請的環境稱為動態環境,為一臺kvm虛擬機。假設某動態環境的IP為192.168.2.1?,在該動態環境部署服務A時,同時將該機器的host文件中192.168.1.1 A.zhuaninc.com?(假設穩定環境服務A的IP為192.68.1.1?)映射,修改為127.0.0.1 A.zhuaninc.com。
如下圖所示的動態環境192.168.4.1?,其中A'及E'即為本次需求所修改的服務。
初始化過程如下:
- 申請動態環境192.168.4.1?,申請完成后該機器的host文件中包含全量的服務域名映射,默認映射到對應的穩定環境所在的IP,該例中僅展示了F服務的域名映射為192.168.5.1 F.zhuaninc.com ?(假設穩定環境服務F的IP為192.168.5.1)。
- 部署服務E',同時將127.0.0.1寫入host文件。
- 部署服務D,同時將127.0.0.1寫入host文件。
- 部署服務C,同時將127.0.0.1寫入host文件。
- 部署服務B,同時將127.0.0.1寫入host文件。
- 部署服務A',同時將127.0.0.1寫入host文件。
- 部署Entry。
- 部署nginx,同時將服務A的upstream修改為僅包含127.0.0.1。
如此以來,就像焊接管道一樣,從服務E至nginx倒序焊接出一條沒有分叉的管道。測試時只需要通過host映射將被測域名映射到該IP,即可實現請求精準地到達A'及E',E'以后的鏈路(從F開始的鏈路)則使用穩定環境。
對于MQ,則通過在動態環境topic添加IP前綴的方式實現與穩定環境的物理隔離,如下圖所示不同的topic在broker上存在著不同的隊列,穩定環境的消息和動態環境的消息不能互通。
MQ消息物理隔離
如在測試過程中發現需要修改更多的服務,例如需要修改服務F,則在將F服務部署在該動態環境的同時,需要重啟服務E',因為E'已經與穩定環境的F建了tcp連接,修改F服務的域名映射并不會導致E'與F之間重新建立tcp連接,所以需要重啟E'。如F服務的調用方不僅有E',則都需要重啟。
3.3 優缺點
該解決方案近似于物理隔離,但較物理隔離有所改進,改進的地方在于動態環境并沒有包含全量的服務,動態環境僅包含了從nginx開始至最后一個被測的服務,而使用穩定環境充當被測鏈路的尾巴。
3.3.1 優點
- 隔離性強,近似于物理隔離。
- 鏈路簡單,流量封閉在同一臺機器上。
3.3.2 缺點
- 需要部署從nginx開始至最后一個被測的服務,存在未修改服務部署在動態環境中的情況,造成資源浪費。
- 由于部署過程依賴服務的調用關系,導致部署效率低下,極端情況下需要數天調試測試環境。
- host管理復雜。
- topic添加IP前綴易出錯,代碼與環境相關。
- 單臺機器內存有限,鏈路過長無法滿足。
4. 轉轉測試環境V2-基于自動IP標簽的流量路由
?隨著轉轉公司業務的飛速發展,服務數量迅速增加,每個動態環境部署的服務數量增至30-60個,搭建測試環境的成本越來越高。為了解決該問題,轉轉架構部、運維部、工程效率部推出了流量路由解決方案。該解決方案在此前的公眾號文章??轉轉測試環境的服務治理實踐??中已有詳細講解,本文僅做簡要介紹。
該版本的流量路由技術稱為基于自動IP標簽的流量路由,之所以選擇IP為標簽是因為基于現有使用習慣發現所有被測服務部署在同一臺虛擬機上,IP地址一樣,并且IP易獲取,用戶無感知;“自動”則體現在無需用戶對服務及流量進行打標,打標是完全自動化進行的。
基于自動IP標簽的流量路由將測試環境的搭建時間從數小時-數天減少至30分鐘-1小時,每環境部署的服務數量從30-60個服務下降至個位數,并且完全兼容沒有流量路由時的使用習慣。但是仍然存在申請虛擬機耗時長,kvm內存無法擴容的問題。?
5. 轉轉測試環境V3-基于手動標簽的流量路由
基于自動IP標簽的流量路由解決了轉轉測試環境存在的大部分問題,使測試環境的搭建時間從數小時-數天下降至30分鐘-1小時,但是仍然存在申請環境耗時長,kvm內存無法擴容的問題,本著精益求精,用戶至上的原則,我們開發了基于手動標簽的流量路由。
5.1 docker化
為了解決申請環境耗時長,kvm內存無法擴容的問題,我們決定將服務部署在docker容器內,無需提前申請資源,理論上無內存限制。但是docker化以后,基于IP為標簽的流量路由顯然無法工作了,因為不同的服務部署在不同的docker pod內,IP也是不一樣的。
5.2 服務及流量打標
此時就需要手動為服務及流量指定標簽,我們稱為基于手動標簽的流量路由。為服務打標是自動化完成的,在環境平臺申請環境時指定標簽,在該環境內部署服務時,由環境平臺自動添加jvm參數-Dtag=xxx。而為流量打標則通過http header進行,在發起請求時添加header tag=xxx。
申請環境
自動添加jvm參數
請求添加http header
并非所有請求都是通過http發起的,有些由進程內部(如定時任務)直接發起的調用則自動攜帶當前節點的標簽。
5.3 目標形態
基于手動打標的流量路由目標形態如下圖所示,標簽為??yyy?
?的動態環境,本次需求修改了服務B及服務D,只需要在該動態環境內部署B和D,真正實現修改什么就部署什么。
標簽路由使用目標
5.4 RPC調用實現
5.4.1 服務注冊、發現及調用
以下圖服務A調用服務B為例,服務B有3個節點,穩定環境的B,動態環境的B'(標簽為??yyy?
?)及B''(標簽為??xxx?
?)。B'和B''在啟動時會將標簽參數注冊到注冊中心,服務A在啟動時會從注冊中心發現B、B'、B'',同時獲取它們的標簽參數。
RPC標簽路由
以紅色的鏈路為例,流量標簽為zzz,A在調用時發現并沒有動態環境的服務B擁有標簽zzz,于是調用穩定環境的B節點。
橙色鏈路的標簽為xxx,A在調用時發現B''的標簽為xxx,且為動態環境,則調用B''。
轉轉使用的RPC為自研RPC框架,在調用時除了傳遞請求方法及參數等信息之外,還可以通過attachement功能傳遞額外的參數,而路由標簽就是通過該功能在RPC調用時實現傳遞。
5.5 MQ消息實現
5.5.1 消費原理
若想實現消息可以在動態環境與穩定環境之間路由,通過不同的的topic前綴實現動態環境和穩定環境的物理隔離顯然已經行不通。此時的解決方案為動態環境和穩定環境擁有相同的topic,但是不同的消費group,不同的消費group就對應不同的消費offset。動態環境的group添加${tag}前綴,而穩定環境的group添加test_前綴,添加前綴的過程由MQ客戶端自動完成,對用戶透明。
如下圖所示服務B有穩定環境及動態環境(B')節點各一個,B'的標簽為xxx。圖中通過不同的顏色表示不同的標簽,其中綠色表示沒有標簽。
MQ標簽路由
B節點在消費時,首先判斷是否有和消息中標簽對應的消費組注冊到broker上,如果有則過濾掉,否則消費。其中消息1、3、5、7的標簽和B'匹配,消息2沒有標簽,而消息4、6的標簽所對應的消費者沒有注冊到broker上,所以B節點消費了消息2、4、6。
B'節點在消費時,只消費消息中標簽和自身標簽前綴一致的消息,即消息1、3、5。
5.5.2 存在的問題
假如此時B'下線了,我們發現消息7沒有被消費者消費,該消息丟了。這是因為穩定環境消費組和動態環境消費組offset不一致導致的,穩定環境消費組offset大于動態環境消費組offset,解決方案就是B'下線時將其與穩定環境offset之間的消息重新投遞。重新投遞后假如B'又上線了呢,就帶來了消息的重復消費,但是我們認為重復消費是沒有問題的,因為mq本身的消費語義就是至少一次,而不是僅僅一次,重復消費冪等性應該由業務邏輯來保證。
另一個問題是批量消費如何解決,mq客戶端有批量消費功能,一批消息所攜帶的標簽可能是不一樣的。對于這個問題,只需要將消息按標簽分組后再執行消費邏輯即可。
5.5.3 標簽的傳遞
轉轉使用的是RocketMQ,并進行了二次開發,RocketMQ提供了可擴展的header可以用來傳遞路由標簽。
5.6 進程內標簽的傳遞
跨進程的標簽傳遞相對來講比較容易解決,而進程內標簽的傳遞難度更高。
5.6.1 通過方法參數傳遞
通過為每一個方法調用添加tag參數可以實現標簽的的進程內傳遞,但是顯然這不現實,需要全公司所有的代碼配合改動。
5.6.2 通過ThreadLocal傳遞
jdk內置有ThreadLocal及InheritableThreadLocal可以實現標簽的隱式傳遞,但是ThreadLocal無法實現new Thread及跨線程池傳遞,而InheritableThreadLocal可以實現new Thread傳遞卻仍然無法實現跨線程池傳遞。雖然可以通過對Callable和Runnable進行包裝實現跨線程池傳遞,但這仍然要求修改現有的業務代碼,成本較高。
5.5.3 通過TransmittableThreadLocal傳遞
TransmittableThreadLocal[1]是阿里巴巴開源的,通過java agent技術實現的可以跨線程/跨線程池傳遞的??ThreadLocal?
?,對用戶透明,只需要在jvm啟動參數中加入對應的java agent參數即可,最終我們采用了該方案。
TransmittableThreadLocal Agent
5.6 上線收益
下圖為轉轉公司動態環境平均部署服務數量曲線,在2022年5月之前平均每環境約部署7-8個服務,在2022年5月之后標簽路由開始推廣,至2022年7月每環境約部署3-4個服務。雖然從原理上看基于手動標簽的流量路由每環境僅比基于自動IP標簽的流量路由僅少部署一個服務(Entry),但是由于kvm無法擴容,所以在自動IP路由時代環境初始化時用戶總是會預防性地多選擇一些服務,以達到申請更大內存虛擬機的目的,而docker化之后,此種擔憂不再存在,所以每環境部署的服務數量下降了約4個,動態環境總內存節省了約65%。
測試環境搭建時間從30分鐘-1小時下降至2分鐘-5分鐘,時間的節省主要來自無需提前申請kvm、docker資源隔離服務啟動快、無內存擴容擔憂初始化服務數量少。
每環境部署服數量曲線
5.7 輔助設施
docker化雖然帶來了效率的提升,資源占用的下降,但是也引入了一些問題。每次部署IP地址都會變化,導致遠程登錄及debug的成本增加。在Http Header中添加路由標簽增加了測試同學的工作量。為了解決這些問題,我們又開發了對應的輔助設施來提高工作效率。
5.7.1 泛域名解析
使用http傳遞標簽,仍然需要配置host映射將域名映射至測試環境的nginx,如192.168.1.1 app.zhuanzhuan.com,否則dns解析會將app.zhuanzhuan.com解析至線上nginx。
是否可以免去host配置呢,答案是可以的。通過直接使用域名傳遞標簽的形式來實現免去host配置,如app.zhuanzhuan.com?直接使用域名傳遞標簽寫作app-${tag}.test.zhuanzhuan.com,該域名會直接解析至測試環境nginx。在開發版app中內置了該功能,在app啟動時輸入環境標簽即可實現域名的切換,無需配置host即可開始測試。
泛域名解析
5.7.2 web shell
docker化以后每次部署都是一個新的docker pod,IP地址也隨之變化,如果需要登錄查看日志,通過xshell等工具則需要在每次部署后使用新的IP重新登錄。引入web shell功能只需要在環境平臺頁面中點擊按鈕即可通過web shell的方式直接登錄,并且登錄后的工作目錄默認為該服務的日志目錄。
web shell
5.7.3 debug插件
同樣因為docker化以后IP地址總是變化,測試環境的遠程debug也變得更加不方便,每次需要更換新的IP進行連接。為了解決此問題,我們開發了debug插件,該插件會自動獲取項目名作為服務名,需要debug時輸入環境標簽,插件會自動向環境平臺發起請求,而環境平臺則通過解析jvm參數的方式獲取debug端口并向插件返回IP和debug端口,插件在收到IP和debug端口后自動連接。
debug插件
5.8 優缺點
5.8.1 優點
- 更加節省資源,僅部署X(修改的服務),不需要部署nginx+Entry。
- 申請環境速度快,秒級完成。
- 搭建環境速度快,約2-5分鐘。
- 無內存限制。
5.8.2 缺點
- QA有感知,需要在HTTP Header中添加標簽。
6. 分布式調用跟蹤系統
以下圖為例,在D調用E'時出現問題,E'中未打出相應的業務日志,到底是D沒有調用呢,還是流量路由存在問題沒有路由到E'呢。此時就需要分布式調用跟蹤系統的輔助來排查問題。
6.1 原理
分布式調用跟蹤系統的原理就是在鏈路中每個模塊的入口和出口處進行埋點,并將埋點采集起來進行可視化展示。如下左圖為調用鏈路,右圖為采集到的埋點。
分布式調用跟蹤系統原理
每一條鏈路有唯一的Id稱為TraceId,而每一個埋點稱之為span,每個span有唯一的Id稱為SpanId。
6.2 架構
轉轉分布式調用跟蹤系統采用自研與開源結合的方式,如下圖所示。其中Radar為轉轉自研分布式調用跟蹤系統客戶端,可與MQ、SCF(轉轉RPC框架)、Servlet進行整合,異步批量地將埋點上傳到Collector服務,Collector服務再將埋點寫入kafka。開源部分則使用zipkin實現,zipkin具備自動從kafka消費埋點并存入DB的能力,而且自帶UI界面可供查詢。
分布式調用跟蹤系統架構圖
6.3 TraceId的獲取
轉轉使用統一日志門面slf4j,Radar客戶端自動將TraceId、SpanId存入MDCContext中,只需在日志配置文件中加入相應的占位符就可以將TraceId、SpanId打印至日志中,如下圖所示。
TraceId打印到日志中
在Entry層每次請求結束后將TraceId以Http Header的形式返回至前端,前端收到響應后可立即獲取TraceId進行查詢。
網關層將TraceId返回
6.4 在路由關鍵節點采集流量標簽和當前節點標簽
如下圖所示global.route.context.tag為流量標簽,而global.route.instance.tag為當前節點標簽,通過對比這兩個標簽是否匹配即可驗證流量路由是否正確,本節開頭所提到的問題也就迎刃而解。
流量標簽及節點標簽
7. 總結
轉轉測試環境治理共經歷3個版本,物理隔離、基于自動IP標簽的流量路由及基于手動標簽的流量路由。
- 物理隔離:隨著轉轉業務的發展,服務數量的增多,搭建測試環境極端情況下需要數天的時間,每環境平均部署服務數量高達30-60個。
- 基于自動IP標簽的流量路由:每環境平均部署服務數量下降至7-8個,而環境搭建時間也下降至30分鐘-1小時。
- 基于手動標簽的流量路由:每環境平均部署服務數量進一步下降至3-4個,搭建時間降至2分鐘-5分鐘。
流量路由帶來效率提升及資源占用下降的同時,也引入了一些問題,如鏈路復雜性高,ip地址變化等。為了更好地利用流量路由帶來的便利,消除負面影響,就需要各種配套設施的輔助,如分布式調用跟蹤、泛域名解析、web shell等。
回頭來看,流量路由減少了部署時間,降低了資源消耗,得到了業務線的一致好評。架構、運維與工程效率部門的同學排查問題的數量也大大減少。真真正正做到了降本增效,實實在在好項目。兩個版本的流量路由分別獲得轉轉公司優秀項目獎。
關于作者
王建新,轉轉架構部服務治理負責人,主要負責服務治理、RPC框架、分布式調用跟蹤、監控系統等。愛技術、愛學習,歡迎聯系交流。
參考資料
[1]TransmittableThreadLocal: ??https://github.com/alibaba/transmittable-thread-local??