字節(jié)跳動(dòng)合并編譯實(shí)踐
字節(jié)跳動(dòng)微服務(wù)過微的背景
截止 2023 年底,字節(jié)跳動(dòng)內(nèi)部微服務(wù)的數(shù)量超過了 30 萬,而且這個(gè)數(shù)字還在快速的增長當(dāng)中,每個(gè)季度仍然會(huì)新增上萬個(gè)微服務(wù)。伴隨著海量的微服務(wù),微服務(wù)過微帶來的編解碼、序列化、網(wǎng)絡(luò)和服務(wù)治理開銷過大問題也愈加凸顯,在一些性能敏感、QPS 大的的服務(wù)上急需優(yōu)化。于是極致的微服務(wù)合并方案合并編譯應(yīng)運(yùn)而生。目前公司內(nèi)采用合并編譯方式合并的服務(wù)超過 300w core,取得的 CPU Quota 收益超過 40w core,接口時(shí)延根據(jù)包大小有 2-15 ms 不等的優(yōu)化。
合并編譯如何解決微服務(wù)過微的問題
合并編譯是將兩個(gè)(或多個(gè))微服務(wù),在編譯期間合并為一個(gè)二進(jìn)制,以一個(gè)進(jìn)程的方式運(yùn)行。如果當(dāng)前存在 A -> B 這樣一個(gè)調(diào)用關(guān)系,A B 合并之后,將以一個(gè)二進(jìn)制的方式呈現(xiàn)。A 原來通過 RPC 方式調(diào)用 B 的邏輯,將轉(zhuǎn)變?yōu)?A 在進(jìn)程內(nèi)部通過函數(shù)調(diào)用 B 實(shí)際的處理函數(shù)。
流量比例 A : C : D = 8 : 1 : 1 示意圖
合并編譯優(yōu)勢相比 RPC 調(diào)用是非常明顯的。
- 在性能方面:RPC 調(diào)用時(shí)的「編解碼、服務(wù)治理、網(wǎng)絡(luò)」的開銷在合并后將完全的「減少到零」
- 在研發(fā)效率方面:合并編譯仍然能夠「保留微服務(wù)研發(fā)的優(yōu)勢」,在日常開發(fā)的時(shí)候還是 A 服務(wù)的團(tuán)隊(duì)去維護(hù) A, B 服務(wù)的團(tuán)隊(duì)去維護(hù) B,只有在上線時(shí)才會(huì)合并在一起,并不會(huì)對研發(fā)效率造成影響
- 在靈活性方面:合并編譯能夠做到「靈活地合并與拆分」,如示意圖所示,A 和 B 就合起來了, C 和 D 并不接入合并編譯,仍然以 RPC 的方式去調(diào)用真正部署的 B
- 在穩(wěn)定性方面:合并后的服務(wù)的「穩(wěn)定性也會(huì)相應(yīng)地提高」,合并編譯不再經(jīng)過網(wǎng)絡(luò)也意味著微服務(wù)帶來的超時(shí)、過載等問題將不復(fù)存在。
不過,合并編譯還是有一點(diǎn)劣勢的。首先,在運(yùn)行時(shí)隔離性上,微服務(wù)帶來的資源的隔離、故障隔離在合并后將不復(fù)存在;第二個(gè)點(diǎn)是版本管理,在合并之后,如果要更新 A 進(jìn)程中依賴的 B 的版本,需要將 A 重新編譯上線。我們也做了大量的工作去減少這些弊端對業(yè)務(wù)的影響。
合并編譯面臨的挑戰(zhàn)
合并編譯在研發(fā)過程中也面臨非常多的挑戰(zhàn),我們將這些挑戰(zhàn)分成了三大類:基礎(chǔ)挑戰(zhàn),可優(yōu)化的點(diǎn)以及理想形態(tài)?;A(chǔ)挑戰(zhàn)是合并編譯必須要解決的問題,否則在上線過程中可能會(huì)出現(xiàn)很多問題;而優(yōu)化項(xiàng)則是能夠讓用戶更友好的使用合并編譯,減少合并編譯對用戶的影響;最后是合并編譯的理想形態(tài),也是目前合并編譯還沒有解決的一些點(diǎn)。
合并編譯面臨的挑戰(zhàn)
在基礎(chǔ)挑戰(zhàn)方面,主要包括四大類:
1.在隔離性上:
a. 依賴隔離:兩個(gè)服務(wù)依賴了不兼容的依賴版本,該如何解決沖突?
b. 環(huán)境變量隔離:兩個(gè)服務(wù)依賴了相同的環(huán)境變量但是不同的值,該如何隔離環(huán)境變量?
c. 權(quán)限的隔離,合并后的服務(wù)如何仍然擁有原本的身份呢?
d. 身份的隔離:合并后的服務(wù)如何按照原有的身份進(jìn)行打點(diǎn)和上報(bào)呢?
2.調(diào)用轉(zhuǎn)換:如何自動(dòng)化地將 RPC 的方式去轉(zhuǎn)換為 Func call 的方式?
3.易用性上:合并編譯需要一個(gè)自動(dòng)化的工具去自動(dòng)的完成這一次的合并。
4.穩(wěn)定性上,合并編譯改造完成后,如何進(jìn)行第一次的上線,最好還能有一些灰度的邏輯呢?如何保障在后續(xù)迭代過程中的穩(wěn)定性呢?
在可優(yōu)化項(xiàng)當(dāng)中,主要包括兩大類:
1.穩(wěn)定性:合并后該如何測試才能保證合并后的穩(wěn)定性呢?以及如果線上出現(xiàn)了問題,該如何快速的定位到問題,讓相應(yīng)的同學(xué)快速止損和排查呢?
2.易用性上:
a. 版本管理:下游怎么知道上游用了哪些版本?上游又怎么知道自己該使用下游哪個(gè)版本呢?
b. 研發(fā)流程:上游有上游的研發(fā)流程,下游有下游的研發(fā)流程,那合并后的研發(fā)流程應(yīng)該是什么樣子的呢
c. 編譯問題排查:合并后的服務(wù),如果遇到編譯問題用戶就會(huì)找上來,如何讓用戶能夠擁有一定的排查能力呢?
d. 本地 Debug:用戶是沒有合并后代碼的,在本地用戶該如何進(jìn)行斷點(diǎn)調(diào)試呢?
以上的這些問題合并編譯都一一的解決了,不過針對最后一個(gè)大類理想層面,目前還沒有很好的解決方式。第一類是集中在運(yùn)行時(shí)進(jìn)程內(nèi)的隔離性,如何將資源、Panic 進(jìn)行隔離;第二類是如何讓用戶接受和理解合并編譯的形態(tài),就好像接受和理解微服務(wù)一樣。
合并編譯如何解決技術(shù)挑戰(zhàn)
依賴隔離
Go 采用 Go Module 的方式進(jìn)行依賴管理,不同的 import path 代表不同的依賴,比如
import (
"namespaceA/github.com/cloudweGo/kitex"
"namespaceB/github.com/cloudweGo/kitex"
)
代表兩個(gè)依賴。同時(shí) Go Module 支持 replace 的方式,將遠(yuǎn)端依賴替換到本地目錄當(dāng)中,并按照路徑進(jìn)行尋址。比如
replace github.com/cloudweGo/kitex => /tmp/kitex
那么,代碼中引用的 github.com/cloudweGo/kitex/client
會(huì)去 /tmp/kitex/client
路徑下尋找對應(yīng)的代碼。
于是合并編譯利用這兩個(gè)特性進(jìn)行了依賴隔離:首先將每個(gè)服務(wù)的依賴下載下來分別放到隔離后的目錄內(nèi),如下圖所示
之后對不同的服務(wù)內(nèi)的每個(gè) import path 添加相對應(yīng)的前綴,并使用 replace 將前綴指向?qū)?yīng)的本地目錄。
通過這種方式,合并編譯實(shí)現(xiàn)了完全的依賴隔離。有了依賴隔離作為基礎(chǔ),其他的環(huán)境變量的隔離、權(quán)限的隔離、身份的隔離等等都很容易能夠解決了。
調(diào)用轉(zhuǎn)換
調(diào)用轉(zhuǎn)換
左邊是一個(gè) RPC 方式的調(diào)用圖,Client 發(fā)起一次調(diào)用,需要經(jīng)過服務(wù)治理的中間件、傳輸?shù)脑畔⒑途幗獯a部分,再通過網(wǎng)絡(luò)傳輸?shù)綄Χ耍?Server 也需要進(jìn)行一次同樣的一些操作。合并編譯希望做到右邊的這種形式,Client 發(fā)起一次調(diào)用,它調(diào)用的是進(jìn)程內(nèi)的 Server 的對應(yīng)的方法。實(shí)現(xiàn)這樣的轉(zhuǎn)換需要兩步,第一步需要獲得 method 實(shí)現(xiàn);第二步將實(shí)現(xiàn)去注入到 Client 當(dāng)中去。
為了獲得 Server 暴露的接口,合并編譯做了下圖所示的處理。左邊這張圖是一個(gè)正常的 Kitex 服務(wù)的初始化和啟動(dòng),它會(huì)執(zhí)行一些初始化的邏輯,然后初始化并且啟動(dòng) Server。在合并編譯場景下,這部分的邏輯變成了右圖。合并編譯將 main 函數(shù)變成了一個(gè)可導(dǎo)出的內(nèi)函數(shù),可導(dǎo)出了才可以讓 Client 去調(diào)用。第二個(gè)合并編譯給這個(gè)函數(shù)增加了返回值,表示 Server 的元信息。
獲取接口的信息
得益于 Kitex 良好的擴(kuò)展性,Kitex 將 Client 抽象為了一個(gè)接口,只要實(shí)現(xiàn)這個(gè) Call 方法,就可以實(shí)現(xiàn)一個(gè) Kitex 的 Client,也是得益于這個(gè)抽象,使得合并編譯注入 Server 實(shí)現(xiàn)非常容易。
type Client interface {
Call(ctx context.Context, method string, request, response interface{}) error
}
一個(gè)普通的 RPC Client 的初始化只需要這一次 RPC 的信息就可以了。那針對合并編譯 ServiceInlineClient 的初始化,還需要增加Server 的元信息參數(shù)。這個(gè)信息就是通過上文對改造后的 main 函數(shù)調(diào)用獲得的。
serverInfo := server.Main()
kc, err := client.NewServiceInlineClient(serviceInfo(), serverInfo ,options…)
第二步合并編譯需要為 ServiceInlineClient 實(shí)現(xiàn) Call 方法,使得它在 Call 的時(shí)候不去走 RPC 的邏輯,而是去走本地調(diào)用,在 ServerInfo 里找對應(yīng)的方法。Kitex 針對合并編譯做了一些特殊的支持,以上的這部分代碼的實(shí)現(xiàn)在 CloudweGo Kitex 當(dāng)中以上代碼,感興趣的小伙伴可以參考 Kitex 中合并編譯部分。
版本管理
合并編譯和 SDK 版本管理的痛點(diǎn)有點(diǎn)相似,比如:
- 下游升級(jí)時(shí),上游感知不到,會(huì)造成版本的不一致
- 下游并不知道上游依賴的是自己的哪個(gè)版本,也就無法告知上游升級(jí)
- 版本選擇復(fù)雜,上游也不知道這次升級(jí)需要選擇下游的哪個(gè)版本
于是,合并編譯針對具體的業(yè)務(wù)場景做了梳理,并與研發(fā)流程與發(fā)布平臺(tái)做了聯(lián)動(dòng),平臺(tái)提供了基礎(chǔ)的能力,減少用戶對合并編譯的學(xué)習(xí)成本。
針對最終一致性,下游可以在鏡像平臺(tái)上配置好上游依賴的默認(rèn)版本,下次上游上線的時(shí)候可以默認(rèn)帶上去,也不用上游主動(dòng)去選擇該使用的版本。針對強(qiáng)一致性可以通過一條流水線,同時(shí)升級(jí)上下游;也可以擁有上游權(quán)限的團(tuán)隊(duì)直接去升級(jí)上游服務(wù)。除此之外,平臺(tái)上也會(huì)收集版本的元信息,用戶可以很直觀的看到自己依賴了哪些版本,以及自己的哪些版本被依賴了。
修改默認(rèn)發(fā)布的版本
上游選擇下游的版本
服務(wù)接入
合并編譯主要解決微服務(wù)過微帶來的性能問題,其收益公式如下
DownstreamQuota 指下游服務(wù)的資源申請量;MergeRatio 指合并的比例;Codec Ratio 指編解碼的開銷;ServiceGovernaceRatio 指服務(wù)治理的開銷。
從收益公式中可以看到,合并編譯應(yīng)該聚焦于「資源量大、調(diào)用關(guān)系密切、編解碼開銷大」的服務(wù),才能夠拿到較大的收益。為了能夠快速篩選出適合接入的服務(wù),合并編譯團(tuán)隊(duì)從 Trace 流量表、Quota 資源表出發(fā),對全公司內(nèi)的服務(wù)進(jìn)行篩選,篩選條件為:從 Server 視角看,來自單一最大上游的流量占總流量的比例超過 30% 或者從 Client 視角看,來自單一最大下游的流量占總流量的比例超過 30%。之后再和 Quota 表做關(guān)聯(lián),按照 Client + Server 總 Quota 降序排列,于是就得到了一張公司內(nèi)大致適合合并的鏈路表。該表是合并的必要條件,還要滿足:
- 非緩存、固定開銷類型的服務(wù):這類型的服務(wù)在合并后因?yàn)閷?shí)例數(shù)增加會(huì)導(dǎo)致開銷增加。
- 容器負(fù)載太高的服務(wù):容器負(fù)載高的服務(wù)本身就不是很穩(wěn)定,合并可能會(huì)加劇。除此之外,內(nèi)存很高的服務(wù)沒法合并,合并是內(nèi)存直接相加,但是容器規(guī)格是有上限的。
- 編解碼大于3%的服務(wù):編解碼大于 3% 合并后比較穩(wěn)妥的可以看到收益,如果低于 3% 的話服務(wù)是很重計(jì)算型的服務(wù),不一定適合合并。
案例分析
下面是從鏈路表中篩選出的一對比較適合合并的服務(wù)。從 Server Ratio 中 0.962 中可以看出,這個(gè)下游 96% 的流量都是來自這一個(gè)上游,流量的親和度非常高;同時(shí) Client Quota 和 Server 的 Quota 相差不多,那這一對就是潛在的適合合并的服務(wù)。
之后再結(jié)合火焰圖上尋找 Kitex 的編解碼開銷,一般來說編解碼開銷在 3% 以上合并是有收益的,開銷在 5% 以上的收益比較大。像下面的這個(gè)服務(wù)編解碼占到了近10%(包非常大),這樣的服務(wù)合并的收益是非常大的。
火焰圖編解碼開銷
結(jié)合流量關(guān)系表和火焰圖的篩選,這對服務(wù)取得了 4w+ 核的收益。
除此之外,除了拿到 CPU 收益,針對時(shí)延、SLA 等也拿到了不小的收益,甚至在很多非 CPU 收益的場景,合并編譯繼續(xù)發(fā)揮它的價(jià)值,比如:
- 大上游 + 小下游:防止突發(fā)流量導(dǎo)致下游過載,常態(tài)預(yù)留較多資源又會(huì)造成浪費(fèi)。合并后使得小下游可以使用整個(gè)服務(wù)的資源。
- 利用合并編譯做 RPC 權(quán)限收斂:下游每新增一個(gè)上游就要為這個(gè)上游添加訪問權(quán)限、配置限流等等,而利用合并編譯多身份的能力,用戶添加了一個(gè) proxy 層,并將多個(gè)上游與該 proxy 進(jìn)行合并編譯,大大減少了配置的成本。
合并規(guī)模
根據(jù)鏈路表中的數(shù)據(jù),粗篩公司內(nèi)部一共有 1.8w 條鏈路可以合并,鏈路總核數(shù)約 2.6 億核。抽樣 500 條鏈路,其中能夠合并的服務(wù)鏈路條數(shù)為 13 條。按照合并后 10% 的收益統(tǒng)計(jì),合并編譯可以帶來的 CPU 收益約為 67w core。
目前,合并編譯采用重點(diǎn)服務(wù)點(diǎn)對點(diǎn)跟進(jìn)的策略,公司內(nèi)部已經(jīng)完成合并編譯的 CPU 核數(shù)超過 300w core,取得了超過 40w core 的收益,接口時(shí)延也有 2-15ms 不等的收益。
總結(jié)與展望
合并編譯能夠在字節(jié)跳動(dòng)內(nèi)部大規(guī)模落地,證明了合并編譯這種形態(tài)在架構(gòu)上的可行性。目前,合并編譯推進(jìn)方式是點(diǎn)對點(diǎn)的,針對的是已有的服務(wù),在降本增效的背景下,如果合并后有性能和成本的收益,則會(huì)盡可能的推動(dòng)業(yè)務(wù)進(jìn)行合并。不過,這樣的推進(jìn)缺乏全局統(tǒng)一的視角,對業(yè)務(wù)架構(gòu)的演進(jìn)幫助不大,且效率相對比較低。未來,我們希望自頂向下的平臺(tái)化地推進(jìn)。
這與團(tuán)隊(duì)內(nèi)發(fā)起的業(yè)務(wù)域體系構(gòu)建項(xiàng)目不謀而合。業(yè)務(wù)域項(xiàng)目針對目前面臨的業(yè)務(wù)架構(gòu)混亂、鏈路復(fù)雜、架構(gòu)復(fù)雜度高等問題,推出一套完善的平臺(tái)和產(chǎn)品,幫助業(yè)務(wù)完成業(yè)務(wù)域的自動(dòng)劃分和分層。業(yè)務(wù)域項(xiàng)目會(huì)借助合并編譯和流量治理等工具和能力,從更高的視角去做架構(gòu)復(fù)雜度治理,包括「鏈路治理」和「過微服務(wù)治理」:
- 鏈路治理:對于鏈路過深的場景,可以借助合并編譯完成上下游的合并,降低鏈路深度;未來合并編譯也將探索循環(huán)依賴、相互依賴場景下多個(gè)微服務(wù)合并能力。
- 過微服務(wù)治理:對于微服務(wù)拆分過細(xì)、服務(wù)的資源 Quota 低的場景,合并編譯將支持多個(gè) Server 的合并,合并后以一個(gè)進(jìn)程的方式對外提供服務(wù),方便統(tǒng)一進(jìn)行治理和管控。
可以期待的是,結(jié)合合并編譯這一成熟且高效的工具,業(yè)務(wù)域的架構(gòu)師在「不修改代碼」的情況下,可以快速、自動(dòng)化完成不同場景下的微服務(wù)合并,「極大降低架構(gòu)優(yōu)化和業(yè)務(wù)改造的成本」,從而縮減低價(jià)值服務(wù),沉淀高價(jià)值服務(wù),最終形成清晰的業(yè)務(wù)架構(gòu)。微服務(wù)的合并并非是對微服務(wù)的全盤推翻,而是重新對業(yè)務(wù)架構(gòu)進(jìn)行審視和治理,結(jié)合當(dāng)前業(yè)務(wù)的規(guī)模和研發(fā)效率對其進(jìn)行優(yōu)化,朝著理想架構(gòu)演進(jìn)。
Reference
CloudWeGo:https:www.cloudwego.io
Kitex:https://github.com/cloudwego/kitex