Think Deeper, Design Better.
多年開發之後,程式設計師可能會逐漸失去當初程式設計的新鮮感,又不知道如何進一步提升自己。其實,設計方案,作為思考的輸出,是一個非常重要的環節。多年之後,你可能忘了 SpringMVC 具體流程是怎樣的,忘了 Dubbo 十層架構是怎樣的,但技術方案一旦積累下來,會成為伴隨程式設計師一生的財富。此外,AI 來襲,程式設計變得越來越“不值錢”了,未來的程式設計師更需要具有一種高層軟體設計的能力。在“一個靠譜的技術方案文件是怎樣的”一文中,定義了一份好的技術文件是什麼樣子的。那麼,如何達到這個目標呢?
在程式設計實現中,實際上融合了設計方案的思考結果。無論需求大小,在設計上考慮充分一些,實現質量上就能更容易理解和維護。 那麼,在設計方案時需要考慮哪些因素呢? 如何做出一個比較適合的設計方案 ?
通常有如下步驟: 明確痛點 -> 有序思考 -> 確定技術重難點並給出解決方案 -> 根據質量要求尋找合適的設計準則 -> 尋找現有合適的方案 -> 設計溝通 -> 對現有方案進行組合或創新,制定可選方案 -> 權衡取捨,得到最終方案。
好的設計方案
好的設計方案是怎樣的?
- 實現功能,滿足需求,良好的容錯處理
- 表達清晰,有豐富的細節,容易理解和完善
- 盡力保持簡單
- 易用性,可行性
- 效能可接受,對系統穩定性的影響很小
- 一致性
- 用合適的工具做合適的事情
- 適度考慮可複用、可擴充套件
- 儘量不引入額外元件
- 必要的話,考慮應對大資料物件、大流量的穩定性
設計方法
明確痛點
每一個需求/最佳化/重構,總能追溯到某個痛點。痛點主要有如下:
- 功能訴求: 競爭對手有拼團功能,賺了好多錢好多粉絲,我也要有!【新功能】
- 穩定性最佳化: 時不時出現xx報錯,真是令人煩躁!同時一波大流量來襲,系統波動有點大啊!【穩定性】
- 效能提升:怎麼這麼慢啊 ! 這麼多訂單,得處理到什麼時候?【響應速度與吞吐量】
- 維護成本: 這方案得佔雙倍的儲存資源,還有兩個同步,理解起來多費勁!這個報錯,沒法看出問題在哪裡,還得再打個日誌看看。商家在等著修復問題,真急人!【資源/時間】
- 彈性: 明年訂單量要增加3倍,現在這個方案貌似扛不住啊!【容量擴充套件】
- 資料: 這待發貨訂單數顯示為2,怎麼點進去沒訂單?【資料不一致性】
- 體驗: 要做完一個批次操作,要好多步驟,還容易出錯,真耗費時間啊!【步驟繁瑣,易錯】
- 及時: 更新一個內容,要馬上生效,而不需要重新修改程式碼部署系統。【即時更新】
- 安全: 啊啊,不小心把DB/重要檔案目錄資料刪除了!【安全性提醒】
- 擴充套件:實現一個需求,要改這麼多程式碼?【改動大,易出錯】
- 重構: 這麼多新的業務需求,真沒法改了!非得動大手術了!【無法承接新需求】
痛點是否足夠痛? 避免為了解決/最佳化問題而解決/最佳化問題,避免為了嘗試新技術而引入新技術。一定是為了解決痛點。因為事情是做不完的,顧此則失彼,要對做的事情進行仔細規劃。
明確痛點,才能真正對症下藥,藥到病除。
有序思考
拿到一個需求/最佳化/重構,如何有序地思考設計方案呢 ?
步驟一: 弄清楚問題/需求的背景及來龍去脈。
步驟二: 明確功能或服務目標,確定硬性質量要求(通常是效能),或軟性質量要求(通常是健壯性、可擴充套件性、可維護性);
步驟三:確定重點關注者,資料的儲存和分佈;
步驟四:梳理現狀,確定要達到的質量目標;
步驟五:對現有方案進行組合和創新,確定可選方案(核心是儲存與演算法);
步驟六:考慮部署及升級問題
步驟七: 考慮必要的效能、穩定性、高可用、可擴充套件等質量目標。
步驟八:設計溝通,尋求更有經驗的幫助和評審;
步驟九: 結合現有資源限制、質量目標和設計準則,權衡取捨,確定最終方案。
技術重難點
在實現技術方案中,技術重難點是最重要的一環。做技術方案時,先明確此次專案或系統的技術重難點和對應解決方案。不明確,不解決,不要急於動工。
- 結構擴充套件變更,改動較大、影響範圍較大、涉及大量資料遷移,容易產生故障。
- 累積大資料量(效能、儲存成本)。
- 瞬時大流量(穩定性)。
- 衝突的要求(權衡取捨)。
- 既要又要還要(優先順序)。
- 涉及範圍大(細緻梳理)。
- 可擴充套件性和可伸縮性。
- 高可用與一致性。
設計準則及案例
自然清晰
- 有清晰、直觀、容易理解的心智模型/領域模型;
- 沒有拐彎抹角的地方。
- 域的劃分清晰。
- 分層清晰。
- 語義一致。
- 所見即所得與形式的一致性。
案例:
- 訂單同步,使用 Input-Filters-Output 過濾器-管道模式,輔以基礎元件的配置和組合, 清晰地表達了各種具體任務的實現流程。
- 訂單詳情,使用 Providers-Plugins 兩層結構,清晰地表達瞭如何從資料儲存獲取源資料並透過外掛格式化成最終資料的過程。
- 訂單搜尋,ES 查詢提供了索引與 JSON 查詢語句的簡潔抽象。 每個搜尋欄位相互獨立,且聯合搜尋 DSL 直觀易懂。
- 訂單匯出,使用多個詳情收集器的順序組合來獲取所需要的訂單詳情,每個收集器相互獨立變更;使用策略模式分離標準報表和自定義報表。
反例:
- API 入參中的繼承導致 API 使用很迷暈,容易使用錯誤; 不要為了追求一點點複用效果在 API 使用繼承。
- 要匯出零售總店的所有訂單,需要傳 head_shop_id, 並將 shop_id 置為空。Workaround 方案。
- 將大量邏輯放在一個類裡。程式碼分層不清晰。
- 獲取核銷人資訊,需要核銷人所在的店鋪ID,但交易只能拿到訂單所在的店鋪ID,這兩者的語義在連鎖形態下不一定一致,導致有時拿不到核銷人資訊。
容錯處理
- 減少了錯誤發生的可能性。
- 錯誤發生時,更安全友好地處理。
- 不會因為次要區域性影響整體。
- 減少了故障可能性,或降低故障影響。
- 故障發生時,能夠更快更安全地處理和恢復正常。
案例:
- 對每個訂單的處理進行異常捕獲打日誌,且對每個訂單的每個欄位的處理進行異常捕獲打日誌。避免某個訂單的某個欄位錯誤影響該訂單的其他欄位的匯出,或者避免某個訂單的資料錯誤,影響了其他訂單的匯出。【隔離】
- 捕獲 API 或 IO 訪問異常,並進行轉譯處理。 避免因區域性影響整體,或提供更友好的上層提示。【容錯】
- 重試機制。適合於“在極端情況下暫時不可用,而在正常情況下自動恢復”的防禦機制。【容錯】
- 針對可能導致故障的點,進行重點防護。【防禦】
- IO 訪問設定超時。【隔離】
反例:
- 對於 API 返回結果不做任何校驗而直接使用,導致 NPE 。
- 由於單個介面呼叫失敗導致整體資訊載入失敗。
效能與穩定
- 快速的響應時間和吞吐量;
- 大幅減少任務執行時長。
- 系統在大流量情形下的穩定執行。
- 對外部依賴進行降級或熔斷。
案例:
- 採用多個執行緒,批次、併發地拉取訂單詳情。
- 備份的過程,使用非同步來完成,提升響應速度。
- 減少不必要的IO訪問;僅在真正需要的時候去訪問 IO 或 API 。
- 限流處理。 在連續多個大流量匯出的情形下,進行限流,只允許指定數目的大流量匯出。
- 熔斷降級。 HBase 主叢集訪問失敗時,自動切換到備叢集訪問。
擴充套件與可定製
- 底層模型統一。
- 核心簡潔而穩定,外圍可擴充套件。
- 適當地分離關注點,組合和組織關注點。
- 元件化、配置化,透過增減外掛來支援需求。
案例:
- 不是按照前端頁面所需功能,而是按照所需要提供的能力模型,來設計訂單搜尋服務。底層具有強大而通用的能力,上層進行適配,提供受限的能力。
- 梳理整個流程,將整體流程中可變的子流程進行抽象,允許配置不同的子流程實現,來實現多變的具體流程。
- 將匯出構建成“查詢-詳情-過濾-排序-格式化-生成報表”的外掛化流程。只需要新增或編排外掛,就能實現各種形態的匯出。
彈性擴充套件
- 當業務量增長時,可以自然應對而無需額外改動。
- 可以即時加機器,解決臨時高併發吞吐量問題。
- 可以即時減機器,去掉不必要的資源空閒。
案例:
- 為熱狀態訂單搜尋建立熱索引。無論訂單總量及增長量如何,熱狀態訂單量始終維持在漲幅不大的程度。
- 應用對等,無狀態設計,可以隨時增減應用伺服器而無影響。
維護成本
- 減少了儲存資源佔用。
- 減少了多處同步。
- 能更快速地定位問題,大幅減少了排查和解決問題的時間(秒/分鐘/小時/天)。
- 分離出了變化的部分,更容易識別變化和擴充套件。
案例:
- 去掉了對老訂單同步的依賴,訂單匯出的整體理解和維護更加簡單。
- 更明顯的錯誤原因指明和建議措施,利於快速定位問題和解決。
可複用
- 以小見大,從一個需求點看到一類需求。
- 建立可重複使用的方法、機制和流程,更容易地解決相似問題。
案例:
- 建立一個可複用的 HBase 詳情獲取外掛,來解決匯出商品編碼的問題;同時又能為其他欄位匯出需求所使用。
- 使用模板方法模式,將匯出的“入參校驗-儲存匯出任務記錄-上傳匯出結果-更新匯出任務記錄”基本流程實現為可複用的模板流程。具體匯出只要關心如何生成報表即可。
配置化
- 解決一個需求時,建立相應的配置,當後續可能發生細節變更時,只需要修改配置即可即時生效。
案例:
- 根據支付方式搜尋訂單,新增一個前端入參與後端搜尋值的對映配置; 當新增支付方式時,只需要更改配置,就能支援新增支付方式的搜尋,無需改動程式碼和釋出系統。
一勞常逸
- 建立良好的約定,解決一次,出問題只追溯源頭。
案例:
- 零售訂單的導購員姓名取下單表的擴充套件欄位XXX 。建立這個約定後,推進和完善各個場景下這個欄位的落庫。
依賴弱化
- 減少了不必要的依賴(API,apollo,NSQ, KV 等),或者至少不引入新的依賴。
案例:
- 訂單詳情介面去除對某個外部介面的依賴。
- 訂單匯出任務完成後,直接更新DB裡的任務記錄,不再依賴訊息中介軟體。
反例:
- 訂單詳情(高頻應用)依賴外部某介面,外部介面掛了,導致詳情大量報錯,進而影響列表大量報錯。【雪崩效應】
最小複雜
- 總是首先尋找簡單、改動最小、比較徹底的方案。
- 複雜度衡量: 少量順序程式碼 < 一些條件分支程式碼 < 增加少量apollo配置 < 增加DB < 增加DB和快取 < 增加一個模組。
舉一反三
- 發現一處,解決多處類似的問題,而不是發現一個解決一個。
案例:
- 訂單詳情介面的商品圖片URL欄位未輸出,可以藉此梳理下還有哪些欄位需要輸出。因為每改一次的測試和釋出成本較大。
整合能力
- 發現多個需求點的關聯,綜合考慮和合並最佳化,避免來一個解決一個,導致解決方案比較鬆散。
技術的積累
常用技術手段積累
積累常用技術手段,即是積累技術方案工具箱,為設計方案打下良好基礎。
-
資料結構與演算法設計:使用程式設計解決問題的基本內功。【基礎】
-
併發:程序、執行緒、協程;執行緒池、協程池。【效能】
-
批次:設計適合批次處理的結構、批次演算法設計、批次插入或更新資料庫。【效能】
-
非同步:非同步更新、非同步通知機制。【及時性、解耦主流程與次要流程】
-
輪詢:輪詢直到期望狀態發生;等待一個時間點不確定的條件發生。
-
回撥:指定事件發生時的邏輯處理。【事件處理的及時性】
-
切面:不同操作的公共邏輯。【可複用】
-
模板:相似資料內容的生成;相似操作的執行。【可複用】
-
模式:資料、操作、物件互動的常用模式。【互動與可擴充套件】
-
懶載入: 直到需要的時候才載入和執行。【效能與資源節省】
-
重試:重複執行一個操作。【可靠性】
-
重續:從操作的上一次執行的儲存點開始重續往後執行。【效能】
-
索引:大量資料的高效查詢結構。資料的濃縮精華。【效能】
-
快取:提升熱點資料獲取效能。本地快取:只讀快取,有資料來源支援,從資料來源讀取;高可用快取:多節點共享讀寫資料。【效能】
-
事務:保證多個操作的原子性。【原子性、隔離性、一致性、持久化】
-
分表:同一業務的資料分拆到多張表。【效能、可擴充套件、解耦】
-
分庫: 同一業務或不同業務的資料分拆到多個資料庫例項。【效能、可擴充套件、解耦】
-
分割槽:資料副本與冗餘,提升可用性;高可用資料分割槽,避免資料傾斜【容錯、高可用、可擴充套件】
-
分片:資料可擴充套件性,支援資料容量的增長。【資料容量與可擴充套件性】
-
冗餘:透過基礎設施和資料的冗餘,保證容錯下的高可用性。【容錯與可用性】
-
限流:在瞬時高請求量的情形下保證請求量在可接受的範圍內,保證服務穩定執行。【穩定性】
-
熔斷:主流程發生錯誤時切換到備份流程,返回備份資料。Plan B 思想。【可用性】
-
冪等:保證多次操作與一次操作的等價。【尤其是資產類業務需注意】
-
負載均衡:使用多節點來保證請求的均衡處理。【穩定性和可伸縮性】
-
鎖、阻塞等待與通知:無法獲取共享資源,等待直到資源釋放。【併發安全】
-
IO 多路複用: 處理大量 IO 請求,儘可能避免阻塞。【多IO請求的效能與可擴充套件】
-
物件池:建立開銷大的物件複用。比如連線池。【物件複用】
-
延遲佇列:累積一段時間的資料量延遲處理。【延遲處理、效能與穩定性】
-
優先順序佇列:優先處理優先順序高的資料。【及時性】
-
滑動視窗:有序處理資料。【有序】
-
正規表示式:識別和處理文字。
-
全域性ID生成:雪花演算法。【唯一性、效能】
-
文件說明: 當設計變更涉及較大變動時,建立文件說明改動點及緣由。
-
API :繼承不可超過兩層,避免巢狀;避免將不相關的東西混雜在 API 引數中;避免將底層實現細節暴漏在API 引數與傳參中。
-
分層: 提煉出一系列關注點,分離到不同的語義層次,分離到多個類的單一職責中。
-
元件化: 將工程裡的程式碼與功能實現抽象為元件介面與實現。
-
策略模式: 使用策略模式分離同一個介面的不同實現,並根據場景選擇適宜的實現。
-
外掛流程: 如果流程是可變的,那麼將單個流程節點變成可配置的外掛,並進行編排。
-
啟動檢查: 當應用啟動時,載入所有必要元件,任一不滿足時及時報錯退出,避免錯上加錯。
-
使用切面: 當多個功能要複用同一個前置或後置邏輯時,使用切面來實現這些前置或後置邏輯。
-
受控執行緒池: 切忌在應用裡動態建立單個執行緒或執行緒池;使用全域性受控的執行緒池來執行任務。
-
過載函式: 使用過載函式建立適合的工具類。
-
無狀態: 除非必要,不要在例項間共享狀態;不要讓請求的處理結果依賴於某個狀態。
-
快速失敗: 當前置要件不滿足時,快速失敗勝於設計失當的智慧容錯處理。
-
事務: 多個關聯操作的原子性和一致性保障。
-
冪等: 處理多個完全相同的請求時,與處理一次的效果相同。
-
正規化: 在關係型資料設計中,要遵循基本的規範正規化。
-
日誌: 在開發時,列印合適級別的必要的日誌(關鍵路徑和關鍵狀態),方便快速排查錯誤。
-
來源監控: 如果有多個來源或型別,建立監控瞭解每個來源或型別的業務量及佔比。
常見技術問題積累
積累常見技術問題的解決方案,也是非常有必要的。積累越多,遇到問題就越容易解決,通常不需要重新思量,提升效率。
- 分頁排序
- 同步轉非同步
- 併發批次處理
- 同一目標的多策略處理
- 配置化表達
- 一份資料多份消費
- 時區問題
- 系統通訊互動
- 多節點業務處理的一致性
- 大資料量查詢和統計
- 瞬時大流量的介面處理
- 訊息堆積處理和應急方案
設計溝通與取捨
設計溝通
設計溝通也是非常重要的。一個人難免因為經驗不足,想不到某些關鍵點,需要別人提醒。 尤其是業務越來越複雜,業務關聯錯綜複雜的情況下,個人因為主要負責部分模組的開發,缺乏對系統整體的理解,常常就會欠缺考慮。此外,別人可能有更好的方案可以參考借鑑。一切為我所用。不要太介意主意是你的還是他的還是我的。
如何能夠讓別人更好地參與進來,幫助一起完善設計方案呢(同時也可以幫助感興趣的小夥伴增長知識和經驗面) ?通常,問題的發起者應該做到如下三點:
- 建立文件說明場景、痛點及來龍去脈;
- 多種可選方案,各自的優點與弊端,利弊權衡。
- 提出自己的疑問。
這樣,別人才能更好地提出好的建議。
權衡取捨
- 沒有完美的方案,只有合適的權衡取捨。
- 針對不同的場景,衡量收益和代價。
- 要綜合思考,避免線性思考;避免為了解決一個次要的問題引入更大的問題。
優先順序:穩定性 > 清晰性 > 靈活性。
通常可以認為:
- 功能需求實現、容錯處理,是最基本的要求。
- 其次是效能和穩定性的考慮。
- 為擴充套件留下實現空間,但可以暫時不實現。
- 如果能夠達到建立新功能、避免故障、彈性擴充套件、大幅降低維護成本、可複用, 其收益將是非常高的,此時,增加少許依賴、複雜度,其實是可以接受的。
- 一勞常逸/舉一反三,相比只是解決當前問題,更有價值;多往前走一步。
- 自然清晰,是非常不容易達到的;但很值得為之一步步接近。
設計方案要素
- 背景、問題、需求
- 遍歷已有方案
- 主要技術難題及解決方案(調研)
- 系統元件互動(訊息、API)
- 技術元件選型(中介軟體與應用框架)
- 資料通訊格式(訊息欄位及型別、訊息結構)
- 模組設計
- 儲存設計(儲存元件與表設計)
- 流程設計(流程閉環,可靠性;管道過濾器、外掛、批次併發)
- API 介面(易用性、靈活性、安全)
- 系統初始化與變更處理
- 容錯考量
- 效能、穩定性(大資料量,大流量)
- 可用性、水平擴容(多節點)
- 部署升級考慮
- 技術微決策
- 方案的優點與缺點、取捨權衡
- 安全考量
- 複用性、擴充套件性考量
思考力
程式設計師最核心的競爭力是什麼?高質量的思考力。
工作與生活,實質就是如何思考這個世界並與之相處。對世界、對人性、對組織、對制度、對管理、對溝通、對各方面的理解,以及你的思考模式和行為模式、決定了你會如何應對這個世界。
常常深思察己,建立元情緒監控能力,你將有更強大的自控能力。
沒有完美的方案。所有方案都有利弊,在於適用場景以及權衡取捨。