當技術重構遇上 DDD

  1. 困境:項目背景

愛番番溝通基於百度商橋快速完成了產品功能和技術架構的從無到有,但同時也繼承了百度商橋歷史功能繁雜、技術架構陳舊的缺點。爲了能更好地服務於愛番番溝通將來的產品演進,提高產研能效,需要從實際問題出發,聚焦主要矛盾,對產品架構和業務架構進行重構。

爲了更好的理解本文內容,以下是必要的名詞解釋:

0UbUF8

1.1 愛番番溝通是什麼?

愛番番溝通是連接訪客和商家的在線諮詢工具。一方面訪客可以隨時隨地諮詢,縮短訪客獲取服務的途徑,另一方面商家也可以快速響應並提供服務。同時在推廣場景中,商家還可以根據訪客的諮詢內容反哺回上游廣告通路,優化投放模型,提升營銷轉化效果。

1.2 爲什麼要重構?

百度商橋經歷了幾次不同的產品定位和多年版本迭代,產研團隊也換了幾波人。客戶問題較多,架構長期缺乏系統性治理。給產研團隊帶來多個層面的掣肘:

  1. 團隊內對產品的主要業務邏輯沒有知識儲備。經常需要研發去翻閱項目代碼東拼西湊出現有邏輯的大致模樣。

  2. 客戶反饋問題數量居高不下,典型問題如:

  1. 團隊士氣低落,生產力不高。疲於應對救火問題,難以承接較大功能需求開發。

  2. 現有架構陳舊,模塊繁雜,長期缺乏治理。模塊數量和人員規模失配,小需求可能涉及多個模塊改動。存在大量陳舊代碼,只能不停地進行『打補丁』方式修復問題。

  3. 反思:定義問題和挑戰

面對當前困境,整個產研團隊都意識到了需要儘快做出改變。透過現象找本質,上述現象背後的關鍵問題是什麼呢?又會面臨哪些挑戰呢?

2.1 定義問題

通過進一步分析問題的根本原因,可以把問題分爲以下幾類:

【產品層面】產品方向及定位不明確,功能層級及分類不清晰

舊版客戶端界面示例

【架構層面】客戶端架構多年未演進,功能迭代難以爲繼

【架構層面】服務端架構的基礎溝通層待演進

溝通協議層作爲溝通產品非常重要的一環,還存在架構方面的不足:

【架構層面】服務端架構的業務層待演進

業務層包含 20+ 服務模塊,主要的業務邏輯採用共享庫的方式維護,導致模塊邊界不清,數據鏈路混亂,功能重疊耦合嚴重,迫切需要演進爲主流微服務架構。

【架構層面】整體服務端架構自運維成本高,可維護性很低

歷史遺留系統中需要運維多種自運維中間件,導致團隊不能聚焦業務功能開發。既降低了研發生產力,也給系統穩定性帶來巨大挑戰。

【組織層面】產研團隊整體對業務的理解不夠且未拉齊

2.2 認清挑戰

歸因清楚問題後,重構的方向逐漸清晰起來。但執行落地階段也會面臨着業務演進壓力,原架構基礎薄弱,資源短缺等挑戰。

架構陳舊,代碼裏有不少隱蔽的『坑』

從以往經歷看,有時候一個很小的改動,看起來很有把握的一次上線也可能造成客戶問題。一方面代碼中缺乏設計的地方多,另一方面整體迴歸測試覆蓋不全。組內自嘲這種狀態爲『每一行代碼都剛剛好』,不能多也不能少。

重構和業務演進既要又要

這個挑戰是大部分團隊都會遇到的,業務不可能停止演進等待技術重構。如何能在不影響已有業務且保證部分高優業務需求正常迭代的情況下進行重構是必須要回答的問題。

不能僅僅是重構,客戶可感知的體驗要更好

涉及客戶端架構升級,必然會帶來一些新的用戶體驗,需要管理好存量用戶的預期。本次重構範圍大,產品質量不下降既是要求也是挑戰。

產研團隊較新,對原有業務功能缺乏足夠了解

業務研發團隊很依賴領域專家的業務知識指導,子領域間和模塊間的職責和邊界劃分,數據歸屬等理解需要建立在業務理解的基礎上。這些對現有團隊是個不小的挑戰。

因此,抓主要矛盾,分階段小步快跑是本次重構的基調。

  1. 紓困:解決問題

僅僅從技術層面做重構只能解決眼前的技術問題,隨着業務快速迭代,純技術重構的成果很容易消失殆盡。考慮到需要對業務和技術層面雙管齊下做出改變,在現有複雜業務基礎上仍能保持高效的產研交付效率,加上隔壁兄弟團隊之前在線索管家產品已經收穫了 DDD 改造的收益,因此本次技術重構決定結合 DDD 來做,從產品到技術來一次認知升級、架構升級。

3.1 定位:確定產品方向及核心痛點

產品定位及差異價值

產品定位:選擇『不做什麼』更加重要

產品使用角色:誰是我們的用戶?

差異化價值:客戶爲什麼會選擇我們?

3.2 分析:識別核心領域和模塊,拆解業務邏輯

3.2.1 事件風暴:剖析流程和對齊認知的好幫手

針對主要業務流程,產研團隊通過事件風暴的方式梳理了事件流,定義了每個事件相關的角色、動作、規則條件和事件結果。最重要的是對齊了團隊的業務認知,靠集體智慧剖析了整體業務細節。

3.2.2 邊界是合作的基礎:劃分領域和模塊,形成統一語言

根據產品定位及產品價值分析,結合梳理好的業務流程,需要劃分子領域,相應配比合適的資源投入。

【核心域】

【支撐域】

【通用域】

3.3 架構:搭建整體技術架構

架構目標及設計要點

  1. 根據流量南北向把各種服務按照職責類別分爲多個層次,用戶界面、接入網關、業務前後臺、溝通協議連接等 5 層由溝通團隊建設維護,底下基礎服務和存儲層主要藉助基礎技術能力。分層建設能夠定義服務不同等級、高效使用團隊研發資源、承接不同流量類型(實際用戶流量、後臺用戶流量、異步調用流量、定時任務流量等)、簡化請求涉及的數據鏈路、根據層次不同建設非功能性需求(技術棧選擇、熔斷限流、彈性伸縮等)。

  2. 技術架構匹配業務架構。服務模塊邊界符合業務邊界。核心服務內需設計領域模型,圍繞領域層和應用層構建業務邏輯,搭建 DDD 四層分層架構,做到領域模型和技術細節分離,不穩定實現依賴穩定實現。

  3. 符合典型微服務架構。服務職責內聚,服務和數據一體。數據歸服務私有,服務間不共享業務邏輯,服務間通過 API 或領域事件進行協作。

  4. 數據架構合理。儘可能採用數據最終一致性策略。每種數據非必要不多處存儲,多處存儲須有最終一致性方案保證。涉及 nosql 類存儲如 Redis、HBase、ES(Elastic Search) 時,防止大 key 造成分片不均,業務數據按需進行分庫分表存儲。

3.4 突破:架構設計的關鍵技術

3.4.1 落地真正的微服務架構

隨着子領域和模塊的劃分確定後,需要調整對應的模塊職責及模塊間協作關係進行改造,重點改造點包括:

合併老模塊

改造前服務端有 45 + 服務模塊,服務職責劃分不當,服務粒度不合適。具體表現爲:

  1. 有些功能粒度太細,徒增維護成本,可以合併。

  2. 某些類似功能散落在多個服務,比如 5 個模塊都有提供訪客相關信息查詢,可以合併。

  3. 有些服務隨着老客戶端的升級,功能改造後更合適合併到其他服務,原服務可以下線。

  4. 反向代理層職責劃分不合理導致服務集羣太多,絕大部分可以遷移至公司級的 BFE 進羣,少數包含很多 lua 邏輯 Nginx 集羣暫時保留,但可以合併。

經過合併下線改造後,服務數量減少了 15+。

拆分新模塊

有些功能很重要,需要形成獨立的模塊重點建設。比如:

  1. 訪客廣告信息解析服務。廣告信息對於客服刻畫訪客畫像,理解訪客非常重要。但之前的解析邏輯散落在多個模塊且實現不統一,解析準確率不高,沒有足夠的補償策略保證必要的解析成功率。

  2. 機器人智能回覆服務。這也是產品定位的一個差異化價值。爲了讓客服更高效接待訪客,引導訪客多留資,這塊的產品演進越來越多,複雜度也隨着加大。

  3. 線索服務。這裏的線索服務是愛番番溝通和線索管家產品的邊界,主要是針對會話內容或者留言內容提取聯繫方式,然後通過接口或事件的方式流轉到線索管家,同時也要形成諮詢到線索的閉環數據。

模塊間不共享業務邏輯

改造前的後端業務服務不是真正的微服務,雖然都是獨立部署,各自暴露接口,但服務實現層耦合嚴重:

  1. 通過公共庫( 即 java 的 jar 包 )共享業務邏輯。同一段業務代碼被多個業務服務依賴,既降低了代碼可維護性,也降低了服務的可測試性。

  2. 通過緩存( Redis )傳遞數據。一個 redis key 經常既有多個服務在寫入,也有多個服務在讀取。

  3. 通過 DB 共享數據,直接讀取屬於其他服務職責的數據表。

改造原則:不共享包括業務邏輯的公共庫,讓微服務垂直劃分,相關業務數據(包括緩存數據)歸服務私有,通過 API 接口提供能力,或者通過領域事件推動下游流程。

最終一致性前提下的高可用性

可用性的關鍵手段是數據複製。可以藉助不同的數據同步方法,結合不同特點的存儲類型完成多樣化業務場景的高可用性。常用的數據複製 / 同步手段有:

  1. 發佈 / 訂閱模式:上游服務利用消息隊列把相關數據以消息爲載體發,下游服務訂閱該消息並做相應的持久化。整個溝通服務端在大量使用這種方法,也是服務解耦的一大利器。

  2. CDC 模式( Change Data Capture ):簡單說就是通過監聽 MySQL 的 binlog 感知到上游服務的數據變化(包括新增、更新、刪除),解析日誌並做一些處理(比如關聯表查詢等)後發送到消息隊列,下游按需訂閱處理。

CDC 模式和發佈訂閱模式配合使用能滿足很多場景,分離讀寫服務和選取異構存儲介質。比如訪客進站記錄寫入 MySQL 和訪客歷史記錄查詢 ES,會話寫入 Table 和會話分析服務查詢 Doris 。即能有效滿足各自場景的數據存取需求也能提高場景的可用性。

當然,這種可用性往往會犧牲一定時效性內的數據一致性,需要根據實際業務場景做出權衡。根據經驗判斷在馬上得到答案和得到正確答案之間,大多數人更想要的其實是馬上得到答案。

3.4.2 數據鏈路治理

改造前主要場景包括進站、離站、自動回覆、會話內容校驗、線索識別、結束會話等的數據流的必經節點是實時計算服務,其核心實現是 storm,但因爲多種原因該集羣很不穩定,會引發出上述提到的大量客戶問題。深層分析現狀主要有以下弊端:

經過分析業務需求,只升級 storm 集羣版本不會解決實際問題,另外實時計算框架在現階段不是必須項,因此得出了以下改造思路:

業務程序的靈魂是數據,技術架構時要多花時間考慮數據存儲和讀取的方方面面。比如用什麼存儲系統( 存儲系統不可能讀也最快,寫也最快,需要權衡 )、什麼時候用緩存,整個業務流程的數據傳輸鏈路應該怎麼樣,溝通系統涉及到很多寫放大還是讀放大的權衡等等。本次重構也涉及到了這些方面的梳理和改造,在此不一一介紹。

3.4.3 溝通協議優化

爲什麼要做協議優化?

針對 1.2 章節中提到的客戶端上經常出現丟訪客,消息不上屏等問題,簡單的打補丁方式已經難以將問題徹底解決,因此必須從協議層進行徹底的改造優化。詳細痛點如下:

  1. 現有協議缺乏魯棒性,從協議層面埋藏着隱患。一個事件(如進站、建立溝通、離站)需要多個包來完成交互,如果一個訪客操作頻繁,訪客狀態也會頻繁做變更,很容易出錯。

  2. 富客戶端模式,端上維護了過多的狀態信息,過度依賴推送包的順序,而且缺乏容錯、自恢復恢復機制,容易出現訪客不展示,消息不上屏等問題。

如何優化?

  1. 通知模塊採用分佈式鎖控制併發,併爲報文增加 SeqId 來確認早晚順序,爲客戶端提供判斷依據。

  2. 優化狀態協議,簡化掉動作通知類報文,採用以訪客狀態爲主的報文,如下圖所示,將動作報文簡化掉,只保留狀態報文,報文數量減少約 60%,降低客戶端處理複雜度,減小出錯概率。

  3. 客戶端側,由 socket 長連接改爲爲 http + socket 推拉結合的方式,當斷網重連、或者報文丟失、錯亂時,則客戶端主動拉取最新狀態,徹底接解決訪客狀態不對,消息不上屏等問題。

猜你想問:

1、上面提到分佈式鎖控制併發,會因鎖競爭而增加請求處理時間嗎?

答:鎖粒度爲單個訪客粒度,粒度足夠小,而且同一個訪客在快速操作( 如頻繁快速打開頁面、發起溝通 )時,纔會出現鎖競爭的情況,對單訪客來說,常規的操作併發不大。

2、既然協議優化收益這麼搞,爲什麼不早點做協議優化呢?

答:之前受限於業務邊界劃分不清晰,訪客狀態變更散落在業務前臺、業務後臺、原 storm 集羣多個地方,無法做統一管控。只有在完成了前期建構優化、數據鏈路治理完成之後,站在原有的工作成果至上,才能做協議優化。

3、客戶端的推拉結合爲什麼不早點做呢?

答:如前文 2.1 中第 2 條所說,客戶端技術棧基於 C++,只能艱難維護,無力承接新功能需求。因此想改動客戶端的協議,可謂異常艱難,這也是下文 3.5 章節客戶端架構升級的一大原因。

小結

  1. 訪客、客服、會話管理模塊的 DDD 改造。

  2. 由貧血模型改爲富血模型,通過狀態機控制狀態變更。

  3. 客戶端請求以 http 爲主,同步得到返回值,降低出錯概率。socket 主要用於給端上的通知。

  4. 協議包簡化, 以訪客狀態維度進行交互,極大減少包的數量。

3.4.4 去除自運維中間件

如前面所述由於歷史技術棧原因愛番番溝通團隊內部運維了好幾種中間件,先不說引入這些中間件的正確與否,現狀是沒有足夠知識儲備,既給系統帶來了很多不穩定因素,也降低了團隊的研發效率。因此本次重構在這個方面的改造原則是優先考慮下線架構中不必要的中間件,必要的中間件也不另行維護,遷移到部門基礎技術團隊運維。

集羣改造下線

集羣遷移

此部分集羣雖然不能下線,但團隊內不另行維護,轉而遷移至部門集羣。包括 Kafka 和 Prometheus 集羣。

3.5 擴展:客戶端架構實踐

3.5.1 客戶端跨平臺架構

隨着原客戶端維護代價越來越大,結合客戶對 mac 端的訴求,因此選擇了跨平臺的 Electron 框架。

爲什麼選擇 Electron ?

  1. 開源的核心擴展比較容易。

  2. 界面定製性強,原則上只要是 Web 能做的它都能做。

  3. 是目前最廉價的跨平臺技術方案,HTML + JS 的技術儲備,而且有海量的現存 UI 庫。

  4. 相對其他跨平臺方案( 如 QT GTK+ 等 ),更穩定,bug 少, 只要瀏覽器跑起來了,問題不會太多 。

  5. 方便拓展,可以直接嵌入現有 web 頁面。

Electron 系統架構

愛番番前端團隊的技術棧是 Vue,所以我們選擇使用 Electron-Vue 來搭建項目。Electron 有兩個進程,分別爲主進程( main )和渲染進程( renderer )。主進程中包含了客戶端自動更新、插件核心、系統 API 等。渲染進程是 vue + webpack 的架構,兩個進程間通過 ipc 進行通信。

愛番番客戶端主要是 IM 業務,所以通信方面使用 websocket 來進行消息通知,由於客服發送消息包含樣式設置,所以傳輸內容包含富文本,這樣就很容易引起一些 xss 問題。我們使用 xss 白名單的方式來過濾 xss 攻擊,並且所有內容都會通過策略過濾,攔截黃反等不良文本。

愛番番溝通考慮到今後能更靈活地接入更多業務垂類並且支持第三方自主開發個性化功能。同時需要兼顧平臺代碼的穩定性和易用性,我們採用了插件化架構的方式來實現客戶端。

開發中遇到的問題

Electron 帶來很大便利的同時,其本身也有很多硬傷。如常被人吐槽的內存佔用高、和原生客戶端性能差異、API 系統兼容性問題等。這些問題在開發過程中需要提前考慮到。下面是開發過程中必然會遇到的幾個問題。

1、性能優化

性能優化是在開發完需求功能後經常需要考慮的。在 Electron 中,最好的分析工具就是 Chrome 開發者工具的 Performance ,通過火焰圖,JS 執行過程的任何問題都可以直觀的看到。

2、Window7 系統下白屏問題

因爲在測試過程中 QA 同學使用的一直都是 Win10 的系統,所以白屏問題一直沒有被發現。直到客戶端正式上線,白屏問題被集中反饋,至此我們開始重視白屏問題並積極解決。

由於我們使用的 electron 版本是 9.x 的版本,該版本下默認開啓 GPU 加速,但是 Win7 下啓用 GPU 加速需要管理員權限,如果沒有管理員權限去執行的話進程就會卡住,導致首頁白屏。所以解決此問題方法就可以從兩方面解決,第一是開啓管理員權限,第二是關閉 GPU 加速。考慮到客戶端使用的人羣大部分是客服,公司電腦配置較低且一般沒有管理員賬號權限,所以我們選擇通過關閉 GPU 加速( app.disableHardwareAcceleration() )來解決次問題。

3、其他問題

在 Electron 開發過程中還要注意一些常見問題。如讀寫文件的編碼問題,客戶端安全問題存在 rce,可被任意執行命令,內存使用率過高問題等。

3.5.2 微內核 / 插件化架構

什麼是插件化架構

插件化架構就是軟件本身只提供插件運行時的核心( pluginCore ),併爲插件運行時提供訪問的接口( pluginAPI ),通過插件平臺下載插件( plugin )後可以在軟件上完美運行。

最基本的例子就是 webpack,作爲主流的構建工具,webpack 只抽象了一個軟件運行時的環境,在不關心和改動這個系統已有的代碼前提下,卻能獨立開發新的插件來充實整個系統的能力。

pluginCore: 插件運行時核心;pluginAPI:爲插件運行提供訪問接口;plugin:實現具體功能的插件。

插件化架構優勢

插件化架構是開閉原則在跨系統級別的最佳實踐。在插件核心和接口不變情況下,系統可以持續接入新插件,來豐富系統的功能。在一個非插件化的系統中,隨着功能模塊的增多,代碼量激增,在引入新功能和修復 BUG 都會越來越困難和低效。但插件化架構不管已有系統功能多複雜,開發新的功能的複雜度始終一樣。而且隨着系統的平臺化,第三方接入差異化功能也不會影響系統的穩定性。

愛番番插件化現狀

爲了滿足其他第三方平臺的定製化需求,如電商平臺的商品及訂單模塊,CRM 平臺的客戶模塊,售後場景中的評價模塊,愛番番客戶端的插件化架構的設計要點:

  1. 插件化架構方案

  2. 提供兩種接入方式:JS-SDK 接入、Webview 方式嵌入。

  3. 第三方插件與愛番番客戶端存在兩種通信機制:事件廣播、實例注入。

  4. 番番客戶端插件分類:左側菜單插件、會話工具欄插件、會話側邊欄插件。

  5. 插件配置文件說明:

{
        "version":"0.0.1",   // 版本號
        "id":"demo-name",  // 綁定事件ID
        "name":"組件名稱",  // 插件名稱
        "viewUrl":"",  // 如果是菜單插件需要提供webview地址
        "target":"toolbar",  // menuList——菜單插件、toolbarList——溝通區插件、infoList——右側工具欄插件
        "dependent": {
            "method": [],
            "version":"1.0.6"  // 依賴客戶端版本
        }
}
  1. 歡喜:解決效果

4.1 產品架構升級

新客戶端設計原則

4.2 客戶體驗提升

遷移後,我們對新客戶端的使用客戶進行了回訪,除了需求的反饋,也收到了一些肯定:

32Zxpm

4.3 產研效能大幅提升

技術爲產品服務,產研共同創造業務價值。產研效能是技術重構的首要目標。可以通過兩方面衡量效果。

需求的整體交付速度

技術研發效率

4.4 產研效能大幅提升

4.4.1 系統穩定性

直接體現是前面交代的高頻技術穩定性問題如訪客進站識別不及時、自動回覆不觸發等已得到全面的治理,各系統模塊穩定性指標長期維持在 99.99%。

4.4.2 可維護性

代碼維護成本大大降低,架構在不同層面更具維護性:

4.4.3 可演進性

愛番番溝通系統的潛在可演進方向很多,有些方面已做好設計預留,比如:

  1. 成長:經驗總結

通過這次重構團隊經歷了從困境到反思的痛苦過程,相應地也獲得了組織、技術、人等層面的成長。

組織

技術

  1. 星辰:未來展望

目前的愛番番溝通由於進行了重新定位,方向更加聚焦,但同時也面臨着很多方向性的選擇。如:面對不同的上游場景以及不同的推廣平臺,後續的接入能力是否需要更加強大。智能機器人有些場景下的策略模型沒有保持持續迭代更新,是否需要往智能化方面更進一步。

技術架構的規劃首先應該圍繞業務訴求展開,除此之外會繼續向雲原生演進,增加容量評估、全鏈路壓測、流量治理等能力。比如近期計劃把底層基座從 K8s 式微服務治理升級成服務網格,對齊愛番番主集羣能力,便於以後能更好地複用基礎技術平臺的能力。同時進一步降低多開發語言下的統一服務治理成本( 接入層和協議連接層的服務是 golang,業務服務是 java )。

在未來,如何做到「既要好,又要不同」愛番番溝通產研團隊依然還有很長的路要走。

7. 作者介紹

本篇系愛番番溝通產研團隊多位同學共同編寫。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_ggIPOvB-ptBanbqqKULxQ