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