先看一段對話。html
從上面對話能夠看出拆分的理由:mysql
1) 應用間耦合嚴重。系統內各個應用之間不通,一樣一個功能在各個應用中都有實現,後果就是改一處功能,須要同時改系統中的全部應用。這種狀況多存在於歷史較長的系統,因各類緣由,系統內的各個應用都造成了本身的業務小閉環;git
2) 業務擴展性差。數據模型從設計之初就只支持某一類的業務,來了新類型的業務後又得從新寫代碼實現,結果就是項目延期,大大影響業務的接入速度;github
3) 代碼老舊,難以維護。各類隨意的if else、寫死邏輯散落在應用的各個角落,到處是坑,開發維護起來戰戰兢兢;redis
4) 系統擴展性差。系統支撐現有業務已經是顫顫巍巍,不管是應用仍是DB都已經沒法承受業務快速發展帶來的壓力;sql
5) 新坑越挖越多,惡性循環。不改變的話,最終的結果就是把系統作死了。數據庫
一個老生常談的問題,系統與業務的關係?編程
咱們最指望的理想狀況是第一種關係(車輛與人),業務以爲不合適,能夠立刻換一輛新的。但現實的狀況是更像心臟起搏器與人之間的關係,不是說換就能換。一個系統接的業務越多,耦合越緊密。若是在沒有真正把握住業務複雜度以前貿然行動,最終的結局就是把心臟帶飛。緩存
如何把握住業務複雜度?須要多維度的思考、實踐。網絡
一個是技術層面,經過與pd以及開發的討論,熟悉現有各個應用的領域模型,以及優缺點,這種討論只能讓人有個大概,更多的細節如代碼、架構等須要經過作需求、改造、優化這些實踐來掌握。
各個應用熟悉以後,須要從系統層面來構思,咱們想打造平臺型的產品,那麼最重要也是最難的一點就是功能集中管控,打破各個應用的業務小閉環,統一收攏,這個決心更多的是開發、產品、業務方、各個團隊之間達成的共識,能夠參考《微服務(Microservice)那點事》一文,「按照業務或者客戶需求組織資源」。
此外也要與業務方保持功能溝通、計劃溝通,確保應用拆分出來後符合使用需求、擴展需求,獲取他們的支持。
業務複雜度把握後,須要開始定義各個應用的服務邊界。怎麼纔算是好的邊界?像葫蘆娃兄弟同樣的應用就是好的!
舉個例子,葫蘆娃兄弟(應用)間的技能是相互獨立的,遵循單一職責原則,好比水娃只能噴水,火娃只會噴火,隱形娃不會噴水噴火但能隱身。更爲關鍵的是,葫蘆娃兄弟最終能夠合體爲金剛葫蘆娃,即這些應用雖然功能彼此獨立,但又相互打通,最後合體在一塊兒就成了咱們的平臺。
這裏不少人會有疑惑,拆分粒度怎麼控制?很難有一個明確的結論,只能說是結合業務場景、目標、進度的一個折中。但整體的原則是先從一個大的服務邊界開始,不要太細,由於隨着架構、業務的演進,應用天然而然會再次拆分,讓正確的事情天然發生才最合理。
一旦系統的宏觀應用拆分圖出來後,就要落實到某一具體的應用拆分上了。
首先要肯定的就是某一應用拆分後的目標。拆分優化是沒有底的,可能越作越深,越作越沒結果,繼而又影響本身和團隊的士氣。好比說能夠定這期的目標就是將db、應用分拆出去,數據模型的從新設計能夠在第二期。
動手前的思考成本遠遠低於動手後遇到問題的解決成本。應用拆分最怕的是中途說「他*的,這塊不能動,原來當時這樣設計是有緣由的,得想別的路子!」這時的壓力可想而知,整個節奏不符合預期後,極可能會連續不斷遇到一樣的問題,這時不只同事們士氣降低,本身也會喪失信心,繼而可能致使拆分失敗。
錦囊就四個字「有備無患」,能夠貼在桌面或者手機上。在之後具體實施過程當中,多思考下「方案是否有多種能夠選擇?複雜問題可否拆解?實際操做時是否有預案?」,應用拆分在具體實踐過程當中比拼得就是細緻二字,多一份方案,多一份預案,不只能提高成功機率,更給本身信心。
收拾下心情,開幹!
DB拆分在整個應用拆分環節裏最複雜,分爲垂直拆分和水平拆分兩種場景,咱們都遇到了。垂直拆分是將庫裏的各個表拆分到合適的數據庫中。好比一個庫中既有消息表,又有人員組織結構表,那麼將這兩個表拆分到獨立的數據庫中更合適。
水平拆分:以消息表爲例好了,單表突破了千萬行記錄,查詢效率較低,這時候就要將其分庫分表。
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添加#{},致使繼續使用自增,從而形成衝突;
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語句插入一條記錄,若是主鍵衝突,插入不進去。
如今主鍵已經接入全局惟一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。
以上步驟準備完成後,就開始進入真正的切庫環節,這裏提供兩種方案,咱們在不一樣的場景下都有使用。
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增長
無論什麼切庫方案,開關少不了,這裏開關的初始值必定要設置爲null!
若是隨便設置一個默認值,好比」讀老表A「,假設咱們已經進行到讀新表B的環節了。這時重啓了應用,在應用啓動的一瞬間,最新的「讀新表B」的開關推送等可能沒有推送過來,這個時候就可能使用默認值,繼而形成髒數據!
之前不少表都在一個數據庫內,使用事務很是方便,如今拆分出去了,如何保證一致性?
1)分佈式事務
性能較差,幾乎不考慮。
2)消息機制補償(如何用消息系統避免分佈式事務?)
3)定時任務補償
用得較多,實現最終一致,分爲加數據補償,刪數據補償兩種。
一句話:懷疑第三方,防備使用方,作好本身!
1)懷疑第三方
a)防護式編程,制定好各類降級策略;
b)遵循快速失敗原則,必定要設置超時時間,並異常捕獲;
c)強依賴轉弱依賴,旁支邏輯異步化
d)適當保護第三方,慎重選擇重試機制
2)防備使用方
a)設計一個好的接口,避免誤用
b)容量限制
3)作好本身
a)單一職責
b)及時清理歷史坑
c) 運維SOP化
d)資源使用可預測
舉個例子: 某一個接口相似於秒殺功能,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訪問。
1)作好準備面對壓力!
2)複雜問題要拆解爲多步驟,每一步可測試可回滾!
這是應用拆分過程當中的最有價值的實踐經驗!
3)墨菲定律:你所擔憂的事情必定會發生,並且會很快發生,因此準備好你的SOP(標準化解決方案)!
某個週五和組裏同事吃飯時討論到某一個功能存在風險,約定在下週解決,結果週一剛上班該功能就出現故障了。之前講小几率不可能發生,可是機率再小也是有值的,好比p=0.00001%,互聯網環境下,請求量足夠大,小几率事件就真發生了。
4)借假修真
這個詞看上去有點玄乎,顧名思義,就是在借者一些事情,來提高另一種能力,前者稱爲假,後者稱爲真。在任何一個單位,對核心系統進行大規模拆分改造的機會不多,所以一旦你承擔起責任,就絕不猶豫地盡心盡力!不要被過程的曲折所嚇倒,心智的磨礪,纔是本真。