微服務(wù)回歸單體,代碼行數(shù)減少75%,性能提升1300%(微服務(wù)hsf)
內(nèi)容架構(gòu)是 QQ 瀏覽器搜索的內(nèi)容接入和計算層,主要負(fù)責(zé)騰訊域內(nèi)的內(nèi)容接入和處理,當(dāng)前接入了多個合作方的上千類內(nèi)容。正如前面《如何避免舊代碼成包袱?5步教你接手別人的系統(tǒng)》中提到,這是一套包含 93 個小服務(wù)的微服務(wù)架構(gòu)。經(jīng)過 23 年 Q1 的大力治理,讓我們穩(wěn)住陣腳,進(jìn)一步對老系統(tǒng)做深入的評估:
?? 研發(fā)效率較低:新增一類數(shù)據(jù)需要在 3~4 個服務(wù)上做開發(fā),代碼量不多,但很繁瑣。
?? 系統(tǒng)性能較差:數(shù)據(jù)流經(jīng)多個小服務(wù),且服務(wù)內(nèi)部的實現(xiàn)普遍較差。譬如:核心服務(wù)的 CPU 最高只能用到 40%、一條消息從進(jìn)入到流出需要經(jīng)過 20 多次的反復(fù) JSON 解析、多處存在多余的字符串拷貝和查找…
從架構(gòu)和代碼層面,我們看到系統(tǒng)存在較多的缺陷,同時我們也多次收到業(yè)務(wù)同學(xué)、上層領(lǐng)導(dǎo)對吞吐性能的投訴反饋,譬如:傳輸 6 億的文檔需要 12 天,太慢了;內(nèi)容接入周期太長,成了某項目的瓶頸等等。
作為偏后方的基礎(chǔ)架構(gòu)系統(tǒng),可靠高效是基本要求, 我們決定對系統(tǒng)做徹底的改造,設(shè)計簡單的系統(tǒng)、寫清晰的代碼,提升系統(tǒng)性能和研發(fā)效率,為搜索業(yè)務(wù)提供穩(wěn)定高效服務(wù)。
內(nèi)容架構(gòu)主要做內(nèi)容的接入和計算,支持的內(nèi)容類型非常多,由于舊系統(tǒng)過度微服務(wù)化,且缺乏插件復(fù)用設(shè)計,使得需求開發(fā)人力較高,同時也存在性能缺陷、容災(zāi)不足等嚴(yán)重架構(gòu)缺陷。新系統(tǒng)基于“零基思考”,重新規(guī)劃設(shè)計,架構(gòu)層面聚焦下面 5 個點:
?? 微服務(wù)和單體服務(wù):舊系統(tǒng)由多個細(xì)碎的小服務(wù)組成,RPC 交互消耗很大,結(jié)合“處理量大、計算量小、失敗容忍度低”的業(yè)務(wù)場景,新系統(tǒng)采用單體服務(wù)設(shè)計,數(shù)據(jù)在內(nèi)存間流動,減少消耗。
?? 插件系統(tǒng):面對繁雜多樣的處理流程,舊系統(tǒng)沒有插件化設(shè)計,代碼里全是“if-else”邏輯;新系統(tǒng)我們使用插件化的設(shè)計,靈活支持業(yè)務(wù)需求。
?? 兼顧增量和批量(刷庫):老系統(tǒng)應(yīng)對批量數(shù)據(jù)處理(刷庫)流程非常乏力,沒有做流程拆分,使得刷庫性能較差;新系統(tǒng)可以為刷庫場景做定制化配置,大幅度提升刷庫性能。
?? 故障容災(zāi):舊架構(gòu)幾乎沒有考慮容器遷移時的數(shù)據(jù)保障,新架構(gòu)結(jié)合消息中間件實現(xiàn)流量削峰和消息緩存,實現(xiàn)故障時數(shù)據(jù)不丟。
?? 水平擴(kuò)容:老系統(tǒng)的消費和計算沒有分離,使得 CPU 最高只能用到 40%,且無法水平擴(kuò)容;新系統(tǒng)將消費線程與處理線程分離,大幅提升單機處理性能,也能水平擴(kuò)容。
舊系統(tǒng)設(shè)計:
新系統(tǒng)設(shè)計:
從微服務(wù)到單體服務(wù)
十多年來微服務(wù)在后臺系統(tǒng)大行其道,我們接手的老系統(tǒng)也是微服務(wù)設(shè)計,那么我們要繼續(xù)微服務(wù)嗎?
首先來看我們業(yè)務(wù)的特點:
?? 處理量大:每天有幾十億次內(nèi)容新增/更新。
?? 計算量?。簝?nèi)容架構(gòu)主要做接入和計算調(diào)度,計算量主要在下游的算子服務(wù)或者工廠。
?? 失敗容忍度低:內(nèi)容丟失便無法被搜索到,不能容忍內(nèi)容丟失。
?? 內(nèi)容類別多:已有上千種類型,還在持續(xù)增加。
?? 需求小且類別單一:所有的需求都是新內(nèi)容源接入,需求類型較固定。
再來看老系統(tǒng)的設(shè)計,以接入系統(tǒng)為例,內(nèi)網(wǎng)推送、公網(wǎng)推送、HTTP/Kafka 拉取這四類接入的實現(xiàn),分散在四個服務(wù)上,再經(jīng)過統(tǒng)一接入代理服務(wù)、數(shù)據(jù)處理服務(wù)、分發(fā)服務(wù)處理,一個條內(nèi)容數(shù)據(jù)需 6 次 RPC 交互。在實踐中帶來這些問題:
?? 需要更復(fù)雜的容錯處理:首先微服務(wù)群需要考慮超時時間合理分配;然后每一個微服務(wù)都需要考慮失敗重試、重試雪崩等容錯處理,復(fù)雜度隨微服務(wù)個數(shù)成倍數(shù)增長。幾十億文檔處理疊加上多個微服務(wù),稍有不慎就會導(dǎo)致海量告警轟炸,甚至出現(xiàn)數(shù)據(jù)丟失。
?? 需求迭代慢:一個需求一般由一個人承接,需要改動多個微服務(wù),整體代碼量不多,但分散在多個服務(wù)中。
?? 計算浪費:內(nèi)容數(shù)據(jù)在多個服務(wù)中流動,需要反復(fù)地做序列化和反序列化,而服務(wù)本身有價值的處理主要是字段轉(zhuǎn)換、簡單字符串處理等輕量計算,框架帶來的計算消耗比本職計算還高。
最后,我們的新架構(gòu)采用單體服務(wù)設(shè)計,在容錯處理、迭代效率、計算量等方面都取得不錯的效果(見文末數(shù)據(jù)指標(biāo))。
(內(nèi)容接入系統(tǒng)新老架構(gòu)對比圖)
接入處理流程插件化
內(nèi)容接入系統(tǒng)需要處理上千類內(nèi)容,不同的內(nèi)容通常來自不同的團(tuán)隊,各個團(tuán)隊都有一套對外輸出內(nèi)容的標(biāo)準(zhǔn)協(xié)議,因此內(nèi)容接入系統(tǒng)需要編寫大量的對接適配代碼,如何更輕便地實現(xiàn)新內(nèi)容接入,是我們重點關(guān)注的。
如設(shè)計圖所示,我們的業(yè)務(wù)功能整體分為三層:接入層,處理層,分發(fā)層。
在接入層,我們需要處理多種途徑接入的多種數(shù)據(jù)格式。途徑包括:DB 定時拉取、Kafka 流式拉取、HTTP/COS 拉取、RPC 拉取等;數(shù)據(jù)格式也多種多樣,每個數(shù)據(jù)方提供的數(shù)據(jù)格式各不相同。以 Kafka 拉取類接入為例,小說業(yè)務(wù)推送的是 JSON 格式數(shù)據(jù),而小程序業(yè)務(wù)推送的是 PB 序列化的二進(jìn)制字節(jié)流。
在處理層,不同的業(yè)務(wù)我們要執(zhí)行不同的格式校驗;有的業(yè)務(wù)收到數(shù)據(jù)后,需要再請求其他服務(wù)以補全特定屬性;有的業(yè)務(wù)需要我們執(zhí)行一些字段格式轉(zhuǎn)換;有的業(yè)務(wù)需要我們對數(shù)據(jù)中的值進(jìn)行定制化修改。
在分發(fā)層,每個業(yè)務(wù)要分發(fā)的目的地也不同:有的業(yè)務(wù)只需發(fā)往 Kafka,有的業(yè)務(wù)需要存入 DB、 Redis、DCache 等,有的業(yè)務(wù)需發(fā)送 HTTP / RPC 請求至特定服務(wù)通知更新。其中,Kafka 的 Topic、 DB 的存儲表、目標(biāo)服務(wù)的地址、協(xié)議也各有不同。
面對這樣復(fù)雜的業(yè)務(wù)功能,老系統(tǒng)建設(shè)了一套數(shù)據(jù)處理流程,然后在主流程中通過 if-else 判斷來走不同的處理流程,可以明顯看到“堆代碼”的痕跡,其源碼組織的清晰度、功能的可插拔性都較差。
在新的接入系統(tǒng)中,我們將接入、處理、分發(fā)中的各個關(guān)鍵功能點實現(xiàn)為插件架構(gòu),每一個子功能都是一個插件,同時按照業(yè)務(wù)粒度的處理流配置組合使用插件。
例:批式接入任務(wù)執(zhí)行流程
例:文檔處理流程
當(dāng)有新增的定制化業(yè)務(wù)需求時,我們只需要在相關(guān)環(huán)節(jié)增加插件,開發(fā)插件時,只需實現(xiàn)關(guān)鍵函數(shù),如拉取任務(wù)插件只需實現(xiàn)拉取和拉取任務(wù)是否結(jié)束這兩個接口。分發(fā)插件只需要實現(xiàn)分發(fā)邏輯;其余部分在框架層實現(xiàn)并統(tǒng)一調(diào)度,開發(fā)者無需了解。如果新業(yè)務(wù)只用到現(xiàn)有的功能,我們則只需要在 DB 中配置插件組合序列,無需代碼開發(fā)。
通過此插件化設(shè)計,讓業(yè)務(wù)接入更輕便,大幅降低業(yè)務(wù)需求的 LeadTime(見文末數(shù)據(jù)指標(biāo))。另外,老系統(tǒng)在各服務(wù)代碼中各種硬編碼 if 業(yè)務(wù) ID == 指定 ID,則執(zhí)行/不執(zhí)行指定邏輯,排查業(yè)務(wù)問題時需要跨多個服務(wù)看代碼,效率極低。而新系統(tǒng)只看配置便可清楚了解一個業(yè)務(wù)的接入處理全流程執(zhí)行過程,極大地提升了運維排查效率。
兼顧增量更新和批量刷庫
接入系統(tǒng)經(jīng)常收到“刷庫”類的需求:將指定業(yè)務(wù)的全部數(shù)據(jù)經(jīng)過某個處理后發(fā)給某個指定下游。因老系統(tǒng)沒有插件化設(shè)計,在組件組合使用上缺乏彈性,使得刷庫需求不得不通過增量更新流程滿足,因而做了大量無效計算。
新系統(tǒng)兼顧增量更新和批量刷庫。我們結(jié)合接入系統(tǒng)的輸入特點,將數(shù)據(jù)流配置分為了四種:數(shù)據(jù)源更新處理流、特征更新處理流、數(shù)據(jù)源刷庫處理流和特征刷庫處理流。
在數(shù)據(jù)源/特征更新的處理流中,我們需要配置業(yè)務(wù)線上數(shù)據(jù)處理的各類算子及分發(fā)算子。而在刷庫處理流中,數(shù)據(jù)來源于我們的底表 HBase ,實際未發(fā)生變更,不需要重新計算。并且,在常見的刷庫場景中,一個業(yè)務(wù)數(shù)據(jù)正常更新時需要分發(fā)給多個下游,刷庫時只有部分下游需要重刷,此時我們只需要配置目標(biāo)地的分發(fā)算子即可。
通過區(qū)分四類處理場景的數(shù)據(jù)處理配置,同一個業(yè)務(wù)在正常處理時和刷庫時,新接入系統(tǒng)可執(zhí)行不同的數(shù)據(jù)處理流,進(jìn)而移除了刷庫場景下的不必要計算和分發(fā)邏輯,單核刷庫 QPS 提升了 16 倍。
數(shù)據(jù)接入服務(wù)故障容災(zāi)
數(shù)據(jù)不丟是內(nèi)容架構(gòu)的核心指標(biāo),無論數(shù)據(jù)是怎么來的,只要進(jìn)入了我們系統(tǒng),就應(yīng)該保證不丟失。
接入系統(tǒng)的各類接入方式可歸為三類:接口推送類、Kafka 通道類和定時任務(wù)批式拉取類。這三類接入方式中,Kafka 通道類自帶數(shù)據(jù)備份,數(shù)據(jù)未處理完時不執(zhí)行 Offset Commit,即可保證該數(shù)據(jù)不會丟失;批式定時拉取類的任務(wù)是可重入的,若拉取任務(wù)運行過程中進(jìn)程退出,新節(jié)點重啟任務(wù)即可恢復(fù),數(shù)據(jù)不會丟失;只有接口推送類的數(shù)據(jù)可能在進(jìn)程退出時未處理完,導(dǎo)致丟數(shù)據(jù)。
老系統(tǒng)對接口推送類數(shù)據(jù)沒有做任何的保護(hù),也就意味著進(jìn)程異常退出、容器故障遷移等接入服務(wù)故障場景沒有有效處理,數(shù)據(jù)可能丟失。
我們在新架構(gòu)上增加了消息中間件 Kafka 實現(xiàn)數(shù)據(jù)容災(zāi)。對于 HTTP / trpc 接口推送進(jìn)來的更新數(shù)據(jù),接口層直接將其發(fā)進(jìn) Kafka,并返回給業(yè)務(wù)成功。此中間 Kafka 由指定的分區(qū) (set) 進(jìn)行異步消費處理,消息處理完成后才會執(zhí)行 Offset Commit。如在消費處理過程中,部分節(jié)點進(jìn)程崩潰/退出,其他健康節(jié)點會通過接手消費處理對應(yīng)分區(qū)的文檔消息,最大限度保證數(shù)據(jù)不會丟失,同時消息中間件也帶來削峰的效果。
消費與處理線程分離
老接入系統(tǒng)處理性能較差的重要原因在于:未將 Kafka 消費和文檔處理線程分離。某業(yè)務(wù)配置 N 個線程處理,則這些線程先從 Kafka 拉取文檔,再按照配置執(zhí)行各環(huán)節(jié)的處理,處理完一批消息再去 Kafka 拉取,消費線程同時是處理線程,重計算的業(yè)務(wù)無法充分利用 CPU。同時,一個 Kafka 分區(qū)最多只能被一個線程消費,集群最大處理并發(fā)數(shù)受限于 Kafka 總分區(qū)數(shù),無法實現(xiàn)水平擴(kuò)容。
新系統(tǒng)設(shè)計了一個基于無鎖隊列的文檔計算工作線程池,每個 Kafka 分區(qū)可以被一個線程消費,并被多個計算線程處理。通過消費和計算線程分離,充分利用 CPU,大幅提高了 CPU 利用率和處理性能。同時,計算線程數(shù)量不再局限于 Kafka 總分區(qū)數(shù)量,可以水平擴(kuò)容。
整個系統(tǒng)有 15 種分發(fā)出口,這些出口分散在老系統(tǒng)的多個服務(wù)。如果基于機器本地日志去比較 diff,顯然零散且費力。因此,我們搭建了一個 diff 校驗服務(wù)。同時,在多個服務(wù)的分發(fā)出口進(jìn)行埋點,并上報分發(fā)內(nèi)容至 diff 校驗服務(wù),從而對分散的 diff 日志進(jìn)行統(tǒng)一收集并分析。整個數(shù)據(jù)流如下所示:
比較 diff 的過程中,我們發(fā)現(xiàn)分發(fā)數(shù)據(jù)格式復(fù)雜,存在多種類型。例如,分發(fā)數(shù)據(jù) Json Member Value 為一個 JSON 字符串,而 JSON 字符串 Member 的順序是不固定的。為解決該問題,我們實現(xiàn)了一個遞歸的 JSON 對比工具,來校驗多種類型數(shù)據(jù)的 diff。
更少的代碼
表驅(qū)動編程。如下圖所示,重構(gòu)后使用數(shù)據(jù)遍歷替代冗長的 if 判斷。
針對數(shù)據(jù)動態(tài)加載,使用 C 20 的std::atomic<std::shared_ptr<T>>替代原來雙 buffer 設(shè)計,如下圖所示。
更高的性能
用迭代器代替查找和括號取值。RapidJSON 的查找和中括號取值都需要遍歷 member list,對于先查找后中括號取值的場景,可以先保存查找 member 獲得的迭代器,然后通過迭代器來獲取 member value,減少一次 member list 的遍歷。
減少 JSON 反序列化。老代碼的函數(shù)參數(shù)是 JSON 序列化后的 string, JSON 對象需要反復(fù)的反序列化和序列化,存在性能浪費。我們重構(gòu)后,將需要多輪處理的 JSON 數(shù)據(jù)定義成 rapidjson::Document 對象并置于上下文中,消除了反復(fù)的序列化和反序列化。這不僅能提升數(shù)據(jù)處理的性能,還能減少重復(fù)的解析 JSON 代碼片段。
更好的基礎(chǔ)庫
修復(fù) rapidjson::Document 引發(fā)的內(nèi)存泄漏假象,降低內(nèi)存使用。為了減少重復(fù)解析,我們在 DB 拉取模塊拉取到字符串后,就將其解析為 rapidjson::Document,然后存起來。
然而,執(zhí)行上述優(yōu)化后,我們發(fā)現(xiàn) DB 每加載一輪,容器的內(nèi)存就會顯著上漲一截,加載 5-6 輪后,進(jìn)程內(nèi)存用滿,發(fā)生 OOM。經(jīng)過 Valgrind 工具分析和本地多種測試,我們確定實際內(nèi)存未泄露,內(nèi)存不斷上漲是因為:使用 RapidJSON 基于內(nèi)存池 MemoryPoolAllocator 分配器構(gòu)造 Document 對象,在對象釋放后,空閑內(nèi)存不會立刻歸還給操作系統(tǒng)。
系統(tǒng)分析后發(fā)現(xiàn)這和 RapidJSON 沒有關(guān)系,是操作系統(tǒng)的內(nèi)存策略設(shè)計如此。對此類內(nèi)存釋放不及時的問題,我們調(diào)研發(fā)現(xiàn)有兩種解決方案:
?? 在服務(wù)啟動時用 mallocopt(M_TRIM_THRESHOLD) 調(diào)低內(nèi)存釋放閾值,并在對象釋放后,調(diào)用 malloc_trim(0) 強制其釋放內(nèi)存;
?? 通過過引入 jemalloc 等內(nèi)存分配器。本項目采用鏈接 jemalloc 庫解決。
此外,我們還引入開源的 Sonic-JSON 庫?;谖覀儍?nèi)容數(shù)據(jù)的評測,Sonic-JSON 比 RapidJSON 快 40%,因此我們引入了 Sonic-JSON 代替 RapidJSON ,在新接入系統(tǒng)的壓測中顯示,Sonic-JSON 可以提升 15% 的吞吐,或者降低 17% 的 CPU 開銷。
更好的可讀性
函數(shù)遵循單一職責(zé)原則。如下圖所示,針對不同的訂閱類型,老代碼中職責(zé)不清晰,在函數(shù)中通過 if 判斷來使得不同的訂閱類型走不同的特殊處理邏輯。重構(gòu)后,我們使用多態(tài)設(shè)計,不同的訂閱類型派生類繼承基礎(chǔ)類,并針對自己的特殊邏輯進(jìn)行泛化,從而使得每一個類只處理一種訂閱類型。
將 switch-case 轉(zhuǎn)換為工廠。如下圖所示,應(yīng)用插件設(shè)計和查表法,提高代碼的可維護(hù)性和擴(kuò)展性。
插件化和配置化。功能組件可以自由組合,從而避免頻繁出現(xiàn) trick 代碼。如下圖所示,在老代碼中,通過硬編碼實現(xiàn)對指定資源類型做指定的處理。重構(gòu)后,不同資源可配置不同的處理流程,實現(xiàn)功能熱插拔和組件復(fù)用。
整體流程
研發(fā)流程上,我們沿用開發(fā)搜索中臺技術(shù)產(chǎn)品時積累的 CICD 建設(shè)經(jīng)驗,包括以下措施:
?? 需求確認(rèn)和啟動,約定 TAPD 必填字段、TAPD 扭轉(zhuǎn)流程。
?? 開發(fā)者資質(zhì),只有獲得開發(fā)者資質(zhì)認(rèn)證,才能輸出生產(chǎn)線代碼。
?? 編碼和注釋規(guī)范,統(tǒng)一采用騰訊編碼規(guī)范和 doxygen 注釋。
?? 代碼評審,制定可按步驟執(zhí)行的流程,并提供學(xué)習(xí)案例。
?? 基礎(chǔ)庫規(guī)則,統(tǒng)一第三方庫、工具庫使用規(guī)范,消除項目依賴混亂。
?? 流水線,統(tǒng)一 MR 模板,嚴(yán)格約束靜態(tài)代碼質(zhì)量檢查紅線、單測覆蓋紅線等。
?? 版本規(guī)范,統(tǒng)一版本命名和使用規(guī)范:MAJOR.MINOR.PATCH。
?? 發(fā)布流程,騰訊域采用 XAC 發(fā)布。
需求管理
在需求規(guī)劃時,我們按大模塊(或功能)劃分大需求(EPIC),并把大需求分發(fā)給不同的開發(fā)人員。開發(fā)人員在梳理出模塊的詳細(xì)實現(xiàn)后,再自行劃分出不同的小需求(feature),并調(diào)整對應(yīng)的開發(fā)耗時。開發(fā)過程中使用甘特圖,可以方便確定項目開發(fā)進(jìn)度。
多人協(xié)作,難免會出現(xiàn)工作量分布不均勻或者需要延長工期,所以我們在每周三早上有一個十幾分鐘的晨會:確定需求進(jìn)展,可能風(fēng)險則及時調(diào)整開發(fā)人力,保障團(tuán)隊目標(biāo)達(dá)成。
代碼評審
代碼質(zhì)量對項目的長期發(fā)展有至關(guān)重要。我們團(tuán)隊要求每位開發(fā)者都必須通過代碼安全考試和規(guī)范考試,生產(chǎn)線的每一行代碼都需要經(jīng)過 CR,同時鼓勵全員提升代碼品味,寫出一手好代碼。這里推薦一篇騰訊技術(shù) Leader 總結(jié)的 Code Review 指南,非常有參考性:《騰訊 13 年,我所總結(jié)的 Code Review 終極大法》
文檔協(xié)同
文檔可以跨越時間限制,是一種高效的異步溝通工具。在接手內(nèi)容架構(gòu)系統(tǒng)后,我們補充了大量文檔,包括資源接入現(xiàn)狀、系統(tǒng)鏈路、日常運維和各種排查文檔,為穩(wěn)定性維護(hù)提供了重要保障。
在系統(tǒng)重構(gòu)過程中,我們也積累了各類文檔,存放在小組各個方向目錄中。同時在代碼倉庫里,一些復(fù)雜的業(yè)務(wù)邏輯或者復(fù)雜的模塊,目錄下維護(hù)著 README.md,說明模塊功能、設(shè)計、實現(xiàn)和使用方法。
流水線加速
藍(lán)盾流水線是實現(xiàn) CICD (持續(xù)集成持續(xù)部署) 的核心工具,我們在代碼發(fā)起 MR 后設(shè)置了MR流水線,代碼合入主干后設(shè)置了主干構(gòu)建流水線。
MR 流水線是代碼開始 CR 前必須通過的紅線,所以 MR 流水線的執(zhí)行耗時會影響到整個 MR 耗時和需求開發(fā)耗時。針對重構(gòu)期間多人協(xié)作出現(xiàn)大量并發(fā)檢查任務(wù),以及對流水線關(guān)鍵路徑的耗時分析,我們做了如下優(yōu)化。
- 減小流水線鎖粒度
MR 流水線包含了代碼安全掃描、代碼規(guī)范掃描、單元測試、接口測試等多個步驟。接口測試需要共享特性環(huán)境作為部署和測試環(huán)境,存在資源競爭。之前限制整個流水線只能有一個構(gòu)建在執(zhí)行,其他都要等待。通過配置藍(lán)盾流水線模板的互斥組,可以實現(xiàn) stage 級別的鎖,多個構(gòu)建可以并行執(zhí)行,僅接口測試 stage 互斥,使得流水線構(gòu)建可以加快 25% 以上 。
- 使用 gitHub 鏡像提速
我們有一個公共倉庫專門存放各類外部依賴,通過 genrule 生成可被 bazel 直接導(dǎo)入的規(guī)則,外部依賴需要通過 tar 或者 git 獲取源碼數(shù)據(jù)。在實際執(zhí)行過程中,發(fā)現(xiàn)部分外部依賴?yán)‘惓>徛ㄔ?analyzing 步驟,甚至造成編譯失敗。在分析 log 后發(fā)現(xiàn)部分含有二進(jìn)制依賴的第三方庫,直接從 GitHub 拉取會 QPS 出現(xiàn)卡頓,因此我們修改了 bazel genrule 的生成規(guī)則,全部使用鏡像代理。實測中,發(fā)現(xiàn)部分任務(wù)卡頓會超過 3 分鐘,優(yōu)化后不再卡頓。
性能收益
- 性能提升指標(biāo)概覽
內(nèi)容接入系統(tǒng):
指標(biāo) | 改造前 | 改造后 | 對比 |
平均單核處理 QPS | 13 | 172 | 提升 13 倍 |
平均單核刷庫 QPS | 13 | 230 | 提升 17 倍 |
集群刷庫 QPS | 500~1000 | 10000(受限于外部存儲) | 提升 10 倍 |
平均處理延遲 | 2.7 秒 | 0.8 秒 | 降低 71% |
p99處理延遲 | 17 秒 | 1.9 秒 | 降低 88% |
p999處理延遲 | 19 秒 | 3.7 秒 | 降低 80% |
CPU 利用率 | 不超過40% | 可達(dá)到100% | 提升 2.5 倍 |
內(nèi)容計算系統(tǒng):
指標(biāo) | 改造前 | 改造后 | 對比 |
單核處理 QPS | 243 | 398 | 提升 64% |
平均處理延遲(含重試延遲) | 19 秒 | 2 秒 | 降低 89% |
p99處理延遲(含重試延遲) | 91 秒 | 6 秒 | 降低 93% |
p999處理延遲(含重試延遲) | 1166 秒 | 7.1 秒 | 降低 99% |
CPU 利用率 | 最高 90% | 可達(dá)到 100% | 提升 10% |
- 處理性能 – 提升13倍
新系統(tǒng)單核性能從 13 QPS 提升到 172 QPS,處理性能提升了 13 倍。
以視頻業(yè)務(wù)為例,舊接入系統(tǒng)處理峰值為 33465/min,總核數(shù)為 40 核,平均單核處理 QPS 為 13。
遷移到新接入系統(tǒng)后,處理峰值為 32119/min,總核數(shù) 6 核,平均單核處理 QPS 為 90。下圖可以看到調(diào)大并發(fā)處理的線程數(shù)后,處理性能會等比例提升。當(dāng) CPU 壓到 100% 時處理 QPS 峰值可達(dá) 162。
- 刷庫性能 – 提升10倍
通過拆分增量數(shù)據(jù)更新、批量刷庫的處理流,我們?yōu)樗靾鼍白龆ㄖ苹渲茫蠓忍嵘煨阅?,集群刷庫性能?1000QPS 提升到 10000QPS(受限于外部存儲性能),提升 10 倍。性能對比如下圖所示:
- 處理延遲 – 降低70%
平均處理延時從 2.7 秒降低到 0.8 秒。以視頻業(yè)務(wù)為例,舊接入系統(tǒng)處理一條消息需要經(jīng)過 5 個系統(tǒng)。每個子系統(tǒng)的性能又較差,p999 處理延遲達(dá)到十幾秒。
新接入系統(tǒng)處理一條消息僅需經(jīng)過 3 個,且系統(tǒng)性能較高,p999 處理延遲為秒級。
研發(fā)效率收益
- 研發(fā)效率提升指標(biāo)概覽
指標(biāo) | 改造前 | 改造后 | 對比 |
業(yè)務(wù)需求P80 leadtime | 5.72 天 | <= 1 天 | 降低 82% |
代碼質(zhì)量 – codecc 問題數(shù) | 568 | 0 | 降低 100% |
代碼質(zhì)量 – 單測覆蓋率 | 0 | 0.77 | 提升 77% |
代碼質(zhì)量 – 平均圈復(fù)雜度 | 24 | 2.31 | 降低 90% |
代碼總行數(shù) | 11.3 萬行 | 2.8 萬行 | 降低 75% |
關(guān)鍵鏈路服務(wù)數(shù)量 | 15 | 3 | 減少 80% |
- 業(yè)務(wù)需求 P80 leadtime – 下降82%
得益于代碼質(zhì)量提升、單測覆蓋率提升、微服務(wù)合并為單體服務(wù)、插件化的設(shè)計,在新接入系統(tǒng)下開發(fā)新功能或者業(yè)務(wù)定制化功能,開發(fā)難度和開發(fā)成本大幅下降,從 5.72 天降低到 1 天。
- 代碼總行數(shù) – 減少75%
重構(gòu)后,業(yè)務(wù)代碼量從 11.3 萬行降低到 2.8 萬行,下降 75%。主要由下面幾點帶來:
?? 微服務(wù)合并為單體服務(wù)。多個微服務(wù)小倉合并成大倉后,消除重復(fù)的功能代碼。例如舊系統(tǒng)不同業(yè)務(wù) Kafka 接入時,都拷貝了相同的一套實現(xiàn)。
?? 優(yōu)雅的系統(tǒng)設(shè)計。譬如:插件化設(shè)計,消除大量的 if-else;序列化對象傳參代替字符串傳參,消除大量的 JSON 解析。
?? 現(xiàn)代 C 語法的大規(guī)模使用,讓代碼更精簡,譬如:必要的 auto、for-range、emplace 等。
作者:李浩津
來源:微信公眾號:騰訊云開發(fā)者
出處:https://mp.weixin.qq.com/s/OvGi7eEbq4tQwVFML6–7g