放棄PHP轉投Go,10萬行代碼重構升級一步到位!

架構互聯高可用 2024-01-06 02:15:06
01業務場景介紹騰訊新聞底層頁是核心業務場景,底層頁服務請求 QPS 3.5萬+,單日請求量10億+。涉及到五大場景:騰訊新聞客戶端、騰訊新聞微信與 QQ 插件、騰訊網、騰訊新聞分享頁、騰訊新聞小程序。當用戶從列表入口列表點擊圖文或者問答文章,進入相應的底層頁。底層頁展示的信息包括:標題、摘要、作者信息、正文信息、點贊信息等,如下圖所示。02所面臨的問題 2.1 代碼曆史債務高底層頁接口代碼曆史債務高,代碼總量約 10W+,缺乏設計、業務邏輯混亂、層次關系不明確、代碼複雜度極高(核心函數長度超過2000行),甚至存在明顯的 bug 等。曆史遺留的代碼很多,存在大量實際功能已下線但是還保留的代碼,理解和維護成本極高,例如存在330版本之前的代碼(10年之前版本)。 2.2 研發效率低各場景同一個需求需要各自開發,例如底層頁增加點贊開關控制、增加點贊類型需要全場景統一生效的需求需要各場景開發,五個場景需要5人日開發,極大減緩業務叠代效率,統一場景後只需要1人日開發。底層頁各場景開發框架不統一,分別使用 butterfly、gin、sodoo(PHP) 框架,這些框架與公司基礎能力契合度不高而且維護成本高。 2.3 服務穩定性差原底層頁 PHP 服務穩定性極差,存在用戶體驗受損、服務可觀測性不高等問題服務穩定性長期在兩個九(黃色線):

PHP 的生態已經和公司發展趨勢或者說是行業趨勢基本脫軌,PHP 語言的性能、配套監控、RPC 調用基本不能適應開發需要。目前 PHP 性能是影響服務穩定性最重要的原因之一,升級前接口性能 P99.9 耗時在 3800ms+,穩定性長期維持在兩個9左右。03清理曆史債務-重構 10W 行 PHP 代碼端內 frontsystem 代碼服務情況複雜,已經存在近10年、曆史包袱及其重,PHP 有效代碼總行數超過 40W+。本次重構代碼按照路徑所涉及代碼按照類的維度統計,統計預計 10W 行代碼左右。 3.1 面臨的挑戰首先代碼的圈複雜度達到了 1500+,一般代碼超過 50+ 已經非常複雜。代碼複雜度超過 50+ 整個項目都是理解起來是非常困難的。這裏舉幾個例子:Apache HTTP Server:Apache HTTP Server 是一個廣泛使用的 Web 服務器軟件,其代碼中,圈複雜度的平均值大約在10到20之間。OpenSSL:OpenSSL 是一個用于加密和安全通信的開源庫,代碼總行數在 50W 行數,圈複雜度的平均值大約在10到20之間。我們要重構的底層頁代碼平均複雜度達到了66.25,最高的代碼複雜度達到了1234。

代碼複雜度分布圖使用靜態分析代碼整體的項目後的 PHP 錯誤數高達715處,警告數達到25000+,說明了曆史代碼的存在諸多問題。

代碼中錯誤數提示 其次接口依賴的類很多,一個類的行數可能超過3000行,方法調用的類圖很多調用層級深度超過20層、依賴120+個類文件,超長的執行路徑、很多廢棄的實現和類仍舊、重複調用,大量代碼性能很差、有很大的優化空間。然後是代碼的可測性,底層頁相關的代碼沒有代碼沒有單元測試覆蓋,這樣難以保證代碼線上的穩定,會導致代碼運行的穩定性。落地頁、底層頁調用鏈路圖 最後沒有完善的文檔,底層頁的具體邏輯對于開發人員是空白的,具體執行流程、依賴哪些服務、依賴哪些 Redis、需要將哪些數據上報、輸出的字段的含義是什麽?這些在剛接手時都是不清楚的。 3.2 解決思路梳理核心輸出字段:核心字段是保證底層頁能否正常展示的,這些字段我們一定要在重寫的過程中,例如:圖文正文、視頻 VID、標題、作者、廣告數據、核心控制字段(廣告開關等)等。根據輸出字段實現相應邏輯:代碼具體實現已無法具體了解,用接口最終輸出信息,依據相對有限的數據字段反查具體實現,客戶端依賴的展示字段可以明確的。驗證輸出字段正確性:通過離線 DIFF 方式,將新老服務輸出結果進行異步上報,將上報的 DIFF 信息進行驗證。DIFF 驗證通過後在,進行實驗驗證保證升級後符合預期。 3.3 工具助力,加速升級 3.3.1 使用 xhprof 生成調用流程圖爲了更好梳理代碼以及業務流程,我們先使用了 xhprof 能力分析代碼具體調用鏈路圖(見之前的調用鏈路)。調用鏈路圖可以作爲一個底層頁 PHP 服務一個整體的概覽。可以幫助我們提供執行流程、依賴的函數以及類、調用耗時三個基本信息。調用流程圖 3.3.2 使用 Xdebug 生成代碼執行路徑在之後的重構我們采用借助 Xdebug、PHP CodeCoverage 搭建,實現每一次服務請求到具體代碼執行路徑。Xdebug 和 PHP CodeCoverage 結合可以讓我們了解每一次代碼具體執行情況。請求代碼覆蓋目錄:代碼請求目錄:代碼具體執行:

(綠色爲請求覆蓋代碼,粉色爲非覆蓋代碼)

兩個方式極大加速了我們重構的進度,更好地分析之前服務代碼情況。尤其是第二個能力,我們經常會在代碼裏遇到分支條件的判斷如版本號、上遊下發的標識、文章類型、開關等各種條件判斷,使用第二種能力我們可以模擬不同的請求,查看具體代碼執行路徑。另一方面一些服務內部的交互不容易通過接口梳理。

另外結合 trpc-gateway 流量回放插件,進行流量的 copy,我們對新 copy 的流量到開啓代碼覆蓋檢測配置的新服務中,可以將采樣所有的請求聚合生成對應的覆蓋代碼文件,這樣我們可以基本得到接口各種參數情況下所執行的代碼路徑。

網關流量回放插件:

04提升研發效率-配置化設計隨著業務發展我們需要更合理的架構設計保證業務,升級後架構設計可以穩定支持多場景,采用了配置化方式進行各場景間的隔離。以保證在各端遷移時不相互影響,引發線上故障。新服務的設計對各端場景都是靈活的, 每個場景只需要簡單的配置就可以複用現有新服務的代碼,極大提升服務遷移的效率。現有的服務的每個配置化像就像是積木一樣,每個場景選擇自己所需要積木,搭建不同場景。

根據請求場景,文章類型加載配置,實現根據不同需求返回不同數據響應,實現差異化配置,分爲四層配置體系:

全場景統一生效的配置:一些全場景全文章類型核心的字段控制需要統一進行管理。

相同文章類型統一配置:爲了更好使相同文章類型的通用字段管理。

分場景的不同文章類型配置:文章類型配置,不同文章類型數據協議不一樣,返回的數據,這一層主要是針對各場景的差異化處理。

子場景的不同文章類型配置:與父級場景公用核心的配置,但是需要針對父場景作進一步差異化處理,例如落地頁場景的字段需要在不同層級字段下發。

請求配置加載示意圖 4.1 配置動態庫無 scheme 設計,借助底層頁強大配置能力,簡化開發、發布、上線流程。強大表達式開源庫:golang-expr,簡單高效的表達式引擎,支持 Golang 原生數據結構 map、struct、slice 等訪問,內置30+操作函數,以下官方示例,展示了 expr 庫對結構體、字符串、管道、遍曆函數的支持。user.Age in 18..45 and user.Name not in ["admin", "root"]foo matches "^[A-Z].*"tweets | filter(.Size < 280) | map(.Content) | join(" -- ")filter(posts, {now() - .CreatedAt >= 7 * duration("24h")}) 4.2 配置詳細實現底層頁基于 expr 庫實現定制化函數、其中包括比較類、轉換類、工具類、數據類、常量類:CASE 1:根據多個字段是否存在或者等于1,輸出相應的結果。news 函數接受傳入的路徑,返回當前底層頁展示的信息,pathA、 pathB、pathC 三個字段只要有一個符合那麽就會將輸出字段置爲1。 { "mapper": "ExprEngineMapper", "dst_path": "x", "source_path": "pathC", "desc": "描述信息", "ext": "news(src_path) == 1 || news('pathA') == 1 || news('pathB') == 1 ? 1 : nil ", "data_source": "ResourceInfo" }CASE 2:判空優先級選擇,分享文案需要根據文章信息的 pathA、pathB,以及默認值“騰訊新聞”字段。 { "mapper": "ExprEngineMapper", "dst_path": "x", "desc": "描述信息", "ext": "pre(news('pathA'), news('pathB'),'騰訊新聞')" }{ "mapper": "ExprEngineMapper", "dst_path": "x", "desc": "下載鏈接", "ext": "'http:\/\/xxxx'"}CASE 4:增加了 filter 限制的配置信息,例子當中提供的是版本號的限制,我們也可以是任何限制,例如可以針對文章中的數據進行限制。{ ...... "filter": "req.Apptype == 'android' && req.Appver >= 7260"}

最後請注意不是任何業務邏輯都能夠通過簡單配置動態庫實現,如果是很複雜的業務邏輯也是需要通過代碼實現,否則動態庫會變得冗余、複雜導致難以維護。需要建立一套標准什麽是可以動態配置實現什麽是可以通過配置實現,例如配置的代碼長度、前後依賴關系複雜程度等。

更多功能見:https://github.com/expr-lang/expr

4.3 配置如何管理

配置管理遇到問題開發時提交配置未經正確驗證測試,直接發布上線。其次是 配置發布管理的編寫的問題,不能達到自動化,需要人工操作。爲了解決上述兩個問題引入 Rainbow 七彩石配置管理能力放入對應的代碼。代碼層面接入了 rainbow as code,可以保證配置的可測,執行的正確性。

配置代碼目錄

代碼配置納入單測,保證配置可測性:配置的相關單測:CR 審批通過後自動觸發七彩石上線。05提升穩定性-性能優化在插件底層頁重構場景中我們面臨的問題比較棘手,原始服務由于本地緩存和依賴文章池耗時比較短會快于當前底層頁服務,考慮到短時訪問量比較大,峰值比較抖,必須引入本地緩存,降低訪問的響應耗時。由于插件的訪問流量存在兩個集中,第一是流量時間的集中,每天定點訪問的流量較大,短時流量集中,第二是文章的集中,尤其是中午批次的固定推送,個別文章的緩存命中率達到了95%以上。 5.1 針對客戶端請求請求緩存針對是移動客戶端整體的請求緩存,功能已經開發完成,主要考慮兩點影響鏈路上的各模塊的緩存,已經運營對時效性比較敏感,所以暫時沒有對線上開啓,等全部切換新的內容微服務後,再考慮是否開啓緩存。 5.2 針對上遊服務數據請求緩存整體服務緩存粒度比較粗,增加了對上遊服務的緩存,緩存粒度比較細,針對不同的上遊存入不同的 localcache,提升緩存命中率,提高服務加載速度,性能提升10%左右。06底層頁服務設計的思考 6.1 邏輯流表達與設計底層頁服務,底層頁面向各上遊數據集:讀取數據並聚合下發,文章關聯信息獲取的模型表達。當前服務聚合模型是先按照數據加載按照批次進行加載,按照配置順序進行映射輸出信息。這種設計方式目前基本滿足了底層頁服務設計需要,當有新的數據源接口接入時,開發相應的邏輯,然後配置到相應的加載批次當中。

未來可能演進方式:

數據加載沒有批次限制,每個節點都支持數據加載觸發。

更靈活的數據獲取和映射,數據獲取完全不被限制,調度全部配置化,面對複雜場景的聚合效率進一步提升。

6.2 落地頁場景差異化實現升級時大多複用之前實現 DataLoader、Mapper 等,但是有很多介質需要支持,例如事件、鏈接型文章、微博文章。配置上增加落地頁場景層級做爲客戶端場景子集,更好地複用上層已産生字段的能力。雖然落地頁場景展示和底層頁沒有差別,但是對于服務端而言是有一些差異的如:版本識別提示、PUSH 文章跳轉功能,在數據獲取後我們需要進行版本的升級提示。因此落地頁需要額外過濾功能,我們把這一功能抽象爲 Filter。

新增:Filter。

在每個階段增加 Hook 機制,監聽每個階段執行觸發相應的 Filter。

請求流程增加 Hook 機制:

07結語騰訊新聞服務端曆盡滄桑,終于迎來新的洗禮!這不是終點是新的起點,未來我們將進一步思考服務架構演進方向,在提升研發效率的同時保證架構合理性、穩定性,爲業務發展提供強有力支撐!

本文由高可用架構轉載。技術原創及架構實踐文章,歡迎通過公衆號菜單「聯系我們」進行投稿

0 阅读:0

架構互聯高可用

簡介:感謝大家的關注