一個複雜系統的拆分改造實踐
一個複雜系統的拆分改造實踐
這是篇轉載,寫的很好,看不懂,但是慢慢看1 為什麼要拆分?
先看一段對話。
從上面對話可以看出拆分的理由:
1)應用間耦合嚴重。系統內各個應用之間不通,同樣一個功能在各個應用中都有實現,後果就是改一處功能,需要同時改系統中的所有應用。這種情況多存在於歷史較長的系統,因各種原因,系統內的各個應用都形成了自己的業務小閉環;
2)業務擴充套件性差。資料模型從設計之初就只支援某一類的業務,來了新型別的業務後又得重新寫程式碼實現,結果就是專案延期,大大影響業務的接入速度;
3)程式碼老舊,難以維護。各種隨意的if else、寫死邏輯散落在應用的各個角落,處處是坑,開發維護起來戰戰兢兢;
4)系統擴充套件性差。系統支撐現有業務已是顫顫巍巍,不論是應用還是DB都已經無法承受業務快速發展帶來的壓力;
5)新坑越挖越多,惡性迴圈。不改變的話,最終的結果就是把系統做死了。
2 拆前準備什麼?
2.1 多維度把握業務複雜度
一個老生常談的問題,系統與業務的關係?
我們最期望的理想情況是第一種關係(車輛與人),業務覺得不合適,可以馬上換一輛新的。但現實的情況是更像心臟起搏器與人之間的關係,不是說換就能換。一個系統接的業務越多,耦合越緊密。如果在沒有真正把握住業務複雜度之前貿然行動,最終的結局就是把心臟帶飛。
如何把握住業務複雜度?需要多維度的思考、實踐。
一個是技術層面,通過與pd以及開發的討論,熟悉現有各個應用的領域模型,以及優缺點,這種討論只能讓人有個大概,更多的細節如程式碼、架構等需要通過做需求、改造、優化這些實踐來掌握。
各個應用熟悉之後,需要從系統層面來構思,我們想打造平臺型的產品,那麼最重要也是最難的一點就是功能集中管控,打破各個應用的業務小閉環,統一收攏,這個決心更多的是開發、產品、業務方、各個團隊之間達成的共識,可以參考《微服務(Microservice)那點事》一文,“按照業務或者客戶需求組織資源”。
此外也要與業務方保持功能溝通、計劃溝通,確保應用拆分出來後符合使用需求、擴充套件需求,獲取他們的支援。
2.2 定義邊界,原則:高內聚,低耦合,單一職責!
業務複雜度把握後,需要開始定義各個應用的服務邊界。怎麼才算是好的邊界?像葫蘆娃兄弟一樣的應用就是好的!
舉個例子,葫蘆娃兄弟(應用)間的技能是相互獨立的,遵循單一職責原則,比如水娃只能噴水,火娃只會噴火,隱形娃不會噴水噴火但能隱身。更為關鍵的是,葫蘆娃兄弟最終可以合體為金剛葫蘆娃,即這些應用雖然功能彼此獨立,但又相互打通,最後合體在一起就成了我們的平臺。
這裡很多人會有疑惑,拆分粒度怎麼控制?很難有一個明確的結論,只能說是結合業務場景、目標、進度的一個折中。但總體的原則是先從一個大的服務邊界開始,不要太細,因為隨著架構、業務的演進,應用自然而然會再次拆分,讓正確的事情自然發生才最合理。
2.3 確定拆分後的應用目標
一旦系統的巨集觀應用拆分圖出來後,就要落實到某一具體的應用拆分上了。
首先要確定的就是某一應用拆分後的目標。拆分優化是沒有底的,可能越做越深,越做越沒結果,繼而又影響自己和團隊的士氣。比如說可以定這期的目標就是將db、應用分拆出去,資料模型的重新設計可以在第二期。
2.4 確定當前要拆分應用的架構狀態、程式碼情況、依賴狀況,並推演可能的各種異常。
動手前的思考成本遠遠低於動手後遇到問題的解決成本。應用拆分最怕的是中途說“他*的,這塊不能動,原來當時這樣設計是有原因的,得想別的路子!”這時的壓力可想而知,整個節奏不符合預期後,很可能會接二連三遇到同樣的問題,這時不僅同事們士氣下降,自己也會喪失信心,繼而可能導致拆分失敗。
2.5 給自己留個錦囊,“有備無患”。
錦囊就四個字“有備無患”,可以貼在桌面或者手機上。在以後具體實施過程中,多思考下“方案是否有多種可以選擇?複雜問題能否拆解?實際操作時是否有預案?”,應用拆分在具體實踐過程中比拼得就是細緻二字,多一份方案,多一份預案,不僅能提升成功概率,更給自己信心。
2.6 放鬆心情,緩解壓力
收拾下心情,開幹!
3 實踐
3.1 db拆分實踐
DB拆分在整個應用拆分環節裡最複雜,分為垂直拆分和水平拆分兩種場景,我們都遇到了。垂直拆分是將庫裡的各個表拆分到合適的資料庫中。比如一個庫中既有訊息表,又有人員組織結構表,那麼將這兩個表拆分到獨立的資料庫中更合適。
水平拆分:以訊息表為例好了,單表突破了千萬行記錄,查詢效率較低,這時候就要將其分庫分表。
3.1.1 主鍵id接入全域性id發生器
DB拆分的第一件事情就是使用全域性id發生器來生成各個表的主鍵id。為什麼?
舉個例子,假如我們有一張表,兩個欄位id和token,id是自增主鍵生成,要以token維度來分庫分表,這時繼續使用自增主鍵會出現問題。
正向遷移擴容中,通過自增的主鍵,到了新的分庫分表裡一定是唯一的,但是,我們要考慮遷移失敗的場景,如下圖所示,新的表裡假設已經插入了一條新的記錄,主鍵id也是2,這個時候假設開始回滾,需要將兩張表的資料合併成一張表(逆向迴流),就會產生主鍵衝突!
因此在遷移之前,先要用全域性唯一id發生器生成的id來替代主鍵自增id。這裡有幾種全域性唯一id生成方法可以選擇。
1)snowflake:https://github.com/twitter/snowflake;(非全域性遞增)
2) mysql新建一張表用來專門生成全域性唯一id(利用auto_increment功能)(全域性遞增);
3)有人說只有一張表怎麼保證高可用?那兩張表好了(在兩個不同db),一張表產生奇數,一張表產生偶數。或者是n張表,每張表的負責的步長區間不同(非全域性遞增)
4)……
我們使用的是阿里巴巴內部的tddl-sequence(mysql+記憶體),保證全域性唯一但非遞增,在使用上遇到一些坑:
1)對按主鍵id排序的sql要提前改造。因為id已經不保證遞增,可能會出現亂序場景,這時候可以改造為按gmt_create排序;
2)報主鍵衝突問題。這裡往往是程式碼改造不徹底或者改錯造成的,比如忘記給某一insert sql的id新增#{},導致繼續使用自增,從而造成衝突;
3.1.2 建新表&遷移資料&binlog同步
1) 新表字符集建議是utf8mb4,支援表情符。新表建好後索引不要漏掉,否則可能會導致慢sql!從經驗來看索引被漏掉時有發生,建議事先列計劃的時候將這些要點記下,後面逐條檢查;
2) 使用全量同步工具或者自己寫job來進行全量遷移;全量資料遷移務必要在業務低峰期時操作,並根據系統情況調整併發數;
3) 增量同步。全量遷移完成後可使用binlog增量同步工具來追資料,比如阿里內部使用精衛,其它企業可能有自己的增量系統,或者使用阿里開源的cannal/otter:https://github.com/alibaba/canal?spm=5176.100239.blogcont11356.10.5eNr98
https://github.com/alibaba/otter/wiki/QuickStart?spm=5176.100239.blogcont11356.21.UYMQ17
增量同步起始獲取的binlog位點必須在全量遷移之前,否則會丟資料,比如我中午12點整開始全量同步,13點整全量遷移完畢,那麼增量同步的binlog的位點一定要選在12點之前。
位點在前會不會導致重複記錄?不會!線上的MySQL binlog是row 模式,如一個delete語句刪除了100條記錄,binlog記錄的不是一條delete的邏輯sql,而是會有100條binlog記錄。insert語句插入一條記錄,如果主鍵衝突,插入不進去。
3.1.3 聯表查詢sql改造
現在主鍵已經接入全域性唯一id,新的庫表、索引已經建立,且資料也在實時追平,現在可以開始切庫了嗎?no!
考慮以下非常簡單的聯表查詢sql,如果將B表拆分到另一個庫裡的話,這個sql怎麼辦?畢竟跨庫聯表查詢是不支援的!
因此,在切庫之前,需要將系統中上百個聯表查詢的sql改造完畢。
如何改造呢?
1)業務避免
業務上鬆耦合後技術才能鬆耦合,繼而避免聯表sql。但短期內不現實,需要時間沉澱;
2)全域性表
每個應用的庫裡都冗餘一份表,缺點:等於沒有拆分,而且很多場景不現實,表結構變更麻煩;
3)冗餘欄位
就像訂單表一樣,冗餘商品id欄位,但是我們需要冗餘的欄位太多,而且要考慮欄位變更後資料更新問題;
4)記憶體拼接
4.1)通過RPC呼叫來獲取另一張表的資料,然後再記憶體拼接。1)適合job類的sql,或改造後RPC查詢量較少的sql;2)不適合大資料量的實時查詢sql。假設10000個ID,分頁RPC查詢,每次查100個,需要5ms,共需要500ms,rt太高。
4.2)本地快取另一張表的資料
適合資料變化不大、資料量查詢大、介面效能穩定性要求高的sql。
3.1.4切庫方案設計與實現(兩種方案)
以上步驟準備完成後,就開始進入真正的切庫環節,這裡提供兩種方案,我們在不同的場景下都有使用。
a)DB停寫方案
優點:快,成本低;
缺點:
1)如果要回滾得聯絡DBA執行線上停寫操作,風險高,因為有可能在業務高峰期回滾;
2)只有一處地方校驗,出問題的概率高,回滾的概率高
舉個例子,如果面對的是比較複雜的業務遷移,那麼很可能發生如下情況導致回滾:
sql聯表查詢改造不完全;
sql聯表查詢改錯&效能問題;
索引漏加導致效能問題;
字符集問題
此外,binlog逆向迴流很可能發生字符集問題(utf8mb4到gbk),導致迴流失敗。這些binlog同步工具為了保證強最終一致性,一旦某條記錄迴流失敗,就卡住不同步,繼而導致新老表的資料不同步,繼而無法回滾!
b)雙寫方案
第2步“開啟雙寫開關,先寫老表A再寫新表B”,這時候確保寫B表時try catch住,異常要用很明確的標識打出來,方便排查問題。第2步雙寫持續短暫時間後(比如半分鐘後),可以關閉binlog同步任務。
優點:
1)將複雜任務分解為一系列可測小任務,步步為贏;
2)線上不停服,回滾容易;
3)字符集問題影響小
缺點:
1)流程步驟多,週期長;
2)雙寫造成RT增加
3.1.5 開關要寫好
不管什麼切庫方案,開關少不了,這裡開關的初始值一定要設定為null!
如果隨便設定一個預設值,比如”讀老表A“,假設我們已經進行到讀新表B的環節了。這時重啟了應用,在應用啟動的一瞬間,最新的“讀新表B”的開關推送等可能沒有推送過來,這個時候就可能使用預設值,繼而造成髒資料!
3.2 拆分後一致性怎麼保證?
以前很多表都在一個數據庫內,使用事務非常方便,現在拆分出去了,如何保證一致性?
1)分散式事務
效能較差,幾乎不考慮。
2)訊息機制補償(如何用訊息系統避免分散式事務?)
3)定時任務補償
用得較多,實現最終一致,分為加資料補償,刪資料補償兩種。
3.3 應用拆分後穩定性怎麼保證?
一句話:懷疑第三方,防備使用方,做好自己!
1)懷疑第三方
a)防禦式程式設計,制定好各種降級策略;
- 比如快取主備、推拉結合、本地快取……
b)遵循快速失敗原則,一定要設定超時時間,並異常捕獲;
c)強依賴轉弱依賴,旁支邏輯非同步化
- 我們對某一個核心應用的旁支邏輯非同步化後,響應時間幾乎縮短了1/3,且後面中介軟體、其它應用等都出現過抖動情況,而核心鏈路一切正常;
d)適當保護第三方,慎重選擇重試機制
2)防備使用方
a)設計一個好的介面,避免誤用
- 遵循介面最少暴露原則;很多同學搭建完新應用後會隨手暴露很多介面,而這些介面由於沒人使用而缺乏維護,很容易給以後挖坑。聽到過不只一次對話,”你怎麼用我這個介面啊,當時隨便寫的,效能很差的“;
- 不要讓使用方做介面可以做的事情;比如你只暴露一個getMsgById介面,別人如果想批量呼叫的話,可能就直接for迴圈rpc呼叫,如果提供getMsgListByIdList介面就不會出現這種情況了。
- 避免長時間執行的介面;特別是一些老系統,一個介面背後對應的可能是for迴圈select DB的場景。
- …
b)容量限制
- 按應用優先順序進行流控;不僅有總流量限流,還要區分應用,比如核心應用的配額肯定比非核心應用配額高;
- 業務容量控制。有些時候不僅僅是系統層面的限制,業務層面也需要限制。舉個例子,對saas化的一些系統來說,”你這個租戶最多1w人使用“。
3)做好自己
a)單一職責
b)及時清理歷史坑
- 例:例如我們改造時候發現一年前留下的坑,去掉後整個叢集cpu使用率下降1/3
c)運維SOP化
- 說實話,線上出現問題,如果沒有預案,再怎麼處理都會超時。曾經遇到過一次DB故障導致髒資料問題,最終只能硬著頭皮寫程式碼來清理髒資料,但是時間很長,只能眼睜睜看著故障不斷升級。經歷過這個事情後,我們馬上設想出現髒資料的各種場景,然後上線了三個清理髒資料的job,以防其它不可預知的產生髒資料的故障場景,以後只要遇到出現髒資料的故障,直接觸發這三個清理job,先恢復再排查。
d)資源使用可預測
- 應用的cpu、記憶體、網路、磁碟心中有數
- 正則匹配耗cpu
- 耗效能的job優化、降級、下線(迴圈呼叫rpc或sql)
- 慢sql優化、降級、限流
- tair/redis、db呼叫量要可預測
- 例:tair、db
舉個例子: 某一個介面類似於秒殺功能,qps非常高(如下圖所示),請求先到tair,如果找不到會回源到DB,當請求突增時候,甚至會觸發tair/redis這層快取的限流,此外由於快取在一開始是沒資料的,請求會穿透到db,從而擊垮db。
這裡的核心問題就是tair/redis這層資源的使用不可預測,因為依賴於介面的qps,怎麼讓請求變得可預測呢?
如果我們再增加一層本地快取(guava,比如超時時間設定為1秒),保證單機對一個key只有一個請求回源,那樣對tair/redis這層資源的使用就可以預知了。假設有500臺client,對一個key來說,一瞬間最多500個請求穿透到Tair/redis,以此類推到db。
再舉個例子:
比如client有500臺,對某key一瞬間最多有500個請求穿透到db,如果key有10個,那麼請求最多可能有5000個到db,恰好這些sql的RT有些高,怎麼保護DB的資源?
可以通過一個定時程式不斷將資料從db刷到快取。這裡就將不可控的5000個qps的db訪問變為可控的個位數qps的db訪問。
4 總結
1)做好準備面對壓力!
2)複雜問題要拆解為多步驟,每一步可測試可回滾!
這是應用拆分過程中的最有價值的實踐經驗!
3)墨菲定律:你所擔心的事情一定會發生,而且會很快發生,所以準備好你的SOP(標準化解決方案)!
某個週五和組裡同事吃飯時討論到某一個功能存在風險,約定在下週解決,結果週一剛上班該功能就出現故障了。以前講小概率不可能發生,但是概率再小也是有值的,比如p=0.00001%,網際網路環境下,請求量足夠大,小概率事件就真發生了。
4)借假修真
這個詞看上去有點玄乎,顧名思義,就是在借者一些事情,來提升另外一種能力,前者稱為假,後者稱為真。在任何一個單位,對核心系統進行大規模拆分改造的機會很少,因此一旦你承擔起責任,就毫不猶豫地全力以赴吧!不要被過程的曲折所嚇倒,心智的磨礪,才是本真。
轉載請標明源地址:http://www.cnblogs.com/LBSer