關於數據庫的使用,在京東有幾個趨勢,早期在京東主要用SqlServer及Oracle也有少許採用MySQL,隨着業務發展技術積累及使用成本等因素,不少業務都開始使用MySQL,包括早期使用SqlServer及Oracle的不少核心業務也都漸漸的開始遷移到MySQL,單機的MySQL每每沒法支撐這類業務,須要考分佈式的解決方案,另外本來使用MySQL的業務隨着數據量及訪問量的增長也會遇到瓶頸最終也會考慮採用分佈式解決的方案,整個京東發展趨勢如圖1所示。前端
分佈式的數據庫解決方案有不少種,在各個互聯網公司使用得也是很是的廣泛,本質上就是將數據拆開存儲在多個節點上從而緩解單節點的壓力,業務層面也能夠根據業務特色自行進行拆分,如圖2所示,假設有一張user表,以ID爲拆分鍵,假設拆分紅兩份,最簡單的就是奇數ID的數據落到一個存儲節點上,偶數ID的數據落到另一個存儲節點上,實際部署示意圖如圖3所示。sql
除了業務層面作拆分,也能夠考慮採用較爲通用的一些解決方案,主要分爲兩類,一類是客戶端解決方案,這種方案是在業務應用中引入特定的客戶端包,經過該客戶端包完成數據的拆分查詢及結果彙總等操做,這種方案對業務有必定侵入性,隨着業務應用實例部署的數量比較大,數據庫端可能會面臨鏈接數壓力比較大的問題,另外版本升級也比較困難,優勢是鏈路較短,從應用實例直接到數據庫。數據庫
另外一類是中間件的解決方案,這種方案是提供兼容數據庫傳輸協議及語法規範的代理,業務在鏈接中間件的時候能夠直接使用傳統的JDBC等客戶端,從而大大減輕了業務開發層面的負擔,弊端是中間件的開發難度會比客戶端方案稍微高一點,另外網絡傳輸鏈路上多走了一段,理論上對性能略有影響,實際使用環境中這些系統都是在機房內網訪問,這種網絡上的影響徹底能夠忽略不計。後端
根據上述分析,爲了更好的支撐京東大量的大規模數據量的業務,咱們開發了一套兼容MySQL協議的分佈式數據庫的中間件解決方案,咱們稱之爲JProxy,這套方案通過了屢次的演變最終完成並支撐了京東全集團的去Oracle/Sqlserver任務。網絡
JProxy第一個版本如圖4所示,每一個JProxy都會有一個配置文件,咱們會在配置文件中配置相應業務的庫表拆分信息及路由信息, JProxy接收到SQL之後會對SQL進行解析再根據路由信息決定SQL是否須要重寫及該發往哪些節點,等各節點結果返回之後再將結果彙總按照MySQL傳輸協議返回給應用。架構
結合上文的例子,當用戶查詢user這張表時假設SQL語句是select * from user where id = 1 or id = 2,當收到這條SQL之後,JProxy會將SQL拆分爲select * from user where id=1 及select * from user where id = 2, 再分別把這兩條sql語句發日後端的節點上,最後將兩個節點上獲取到的兩條記錄一併返回給應用。併發
這種方案在業務庫表比較少的時候是可行的,隨着業務的發展庫表的數量可能會不斷增長,尤爲是針對去Oracle的業務在切換數據庫的時候多是一次切換幾張表,下一次再切換另外幾張表,這就要求常常修改配置文件。另外JProxy在部署的時候至少須要部署兩份甚至多份,如圖5所示,此時面臨一個問題是如何保證全部的配置文件在不斷修改的過程當中是徹底一致的。在早期運維過程當中,咱們靠人工修改完一份配置文件,再將相應的配置文件拷貝給其餘的JProxy,確保JProxy配置文件內容一致,這個過程心智負擔較重且容易出錯。運維
在以後的版本中咱們引入了JManager模塊,這個模塊負責的工做是管理配置文件中的路由元信息,如圖6所示。JProxy的路由元信息都是經過JManager來統一獲取,咱們只須要經過JManager往元數據庫裏添加修改路由元數據,操做完成之後通知各個JProxy動態加載路由信息就能夠保證每一個JProxy的路由信息是徹底一致的,從而解決維護路由元信息一致性的痛點。分佈式
在提到分佈式數據庫解決方案時必定會考慮的一個問題是擴容問題,擴容有兩種方式,一種咱們稱之爲re-sharding方案,簡單的說就是一片拆兩片,兩片拆爲四片,如圖7所示,本來只有一個MySQL實例一個shard,以後拆分紅shard1和shard2兩個分片,以後再添加新的MySQL實例,將shard1拆分紅shard11和shard12兩個分片,將shard2拆分紅shard21和shard22兩個分片放到另外新加的MySQL實例上,這種擴容方式是最理想的,但具體實現的時候會略微麻煩一點,咱們短時間以內選擇了另外一種偏保守一點在合理預估前提下足以支撐業務發展的擴容模式,咱們稱之爲pre-sharding方案,這種方案是預先拆分在必定時期內足夠用的分片數,在前期數據量較少時這些分片能夠放在一個或少許的幾個MySQL實例上,等後期數據量增大之後能夠往集羣中加新的MySQL實例,將本來的分片遷移到新添加的MySQL實例上,如圖8所示,咱們在一開始就拆分紅了shard一、shard二、shard三、shard4四個分片,這四個分片最初是在一個MySQL實例上,數據量增大之後咱們能夠添加新的MySQL實例,將shard3和shard4遷移新的MySQL實例上,整個集羣分片數沒有發生變化可是容量已經變成了原來的兩倍。性能
Pre-sharding方案至關於經過遷移完成達到擴容的目的,分片位置的變更涉及到數據的遷移驗證及路由元數據的變動等一系列變更,因此咱們引入了JTransfer系統,如圖9所示。JTransfer能夠作到在線無縫遷移,遷移擴容時只需提交一條遷移計劃,指定將某個分片從哪一個源實例遷移到哪一個目標實例,能夠指定在什麼時候開始遷移任務,等到了時間點系統會自動開始作遷移。整個遷移過程當中涉及到遷移基礎全量數據和遷移過程當中業務訪問產生的增量數據,一開始會將基礎全量數據從源實例中dump出來到目標實例恢復,確認數據正確之後開始追趕增量數據,當增量數據追趕到必定程度系統預估能夠快速追趕結束時,咱們會作一個短暫的鎖定操做,從而確保將最後的增量所有追趕完成,這個鎖定時間也是在提交遷移任務時能夠指定的一個參數,好比最多隻能鎖定20s,若是由於此時訪問量忽然增大等緣由最終剩餘的增量沒能在20s內追趕完成,整個遷移任務將會放棄,確保對線上訪問影響達到最小。遷移完成以後會將路由元信息進行修改,同時將路由元信息推送給全部的JProxy,最後再解除鎖定,訪問將根據路由打到分片所在的新位置。
系統在生產環境中使用的時候,除了考慮以上的介紹之外還須要考慮不少部署及運維的事情,首先要考慮的就是系統如何活下來,須要考慮系統的自我保護能力,要確保系統的穩定性,要作到性能可以知足業務需求。
在JProxy內部咱們採用了基於事件驅動的網絡IO模型同時考慮到多核等特色,將整個系統的性能發揮到極致,在壓測時JProxy表現出來的性能隨着MySQL實例的增長几乎是呈現線性增加的趨勢,並且整個過程當中JProxy所在機器毫無壓力。
保證性能還不夠,還須要考慮控制鏈接數、控制系統內存等,鏈接數主要是控制鏈接的數量這個比較好理解,控制內存主要是指控制系統在使用過程當中對內存的需求量,好比在作數據抽數時候,sql語句是相似select * from table這種的全量查詢,此時後端全部的MySQL數據會經過多條鏈接併發地往中間件發送數據,從中間件到應用只有一條鏈接,若是不對內存進行控制就會形成中間件OOM,在具體實現的時候咱們經過將數據壓在TCP棧中來控制中間件先後端鏈接的網絡流速從而很好的保證了整個系統的內存是在可控範圍內。
另外還須要考慮權限,哪些IP能夠訪問哪些IP不能訪問都須要能夠精確的控制,具體到某一張表還須要控制增刪改查的權限,咱們建議業務在寫SQL的時候儘可能都帶有拆分字段保證SQL均可以落在某個分片上從而保證整個訪問是足夠的簡單可控,咱們爲之提供了精細的權限控制,能夠作到表級別的增刪改查權限,包括是否要帶有拆分字段,最大程度作到對SQL的控制,保證業務在測試階段寫出不知足指望的SQL都能及時發現,大大下降後期線上運行時的風險。
除了基本的穩定性以外,在整個系統全局上還須要考慮到服務高可用方案。JProxy是無狀態的,一個業務在同一個機房內部署至少兩個JProxy且必須跨機架的,保證在同一個機房裏JProxy是高可用的。在另外的機房會部署再部署兩個JProxy,作到跨機房的高可用。除了中間件自身的高可用之外還須要保證數據庫層面的高可用,全鏈路的高可用纔是真正的高可用。數據庫層面在同一個機房裏會按照一主一從部署,在備用機房會再部署一個備,如圖10所示。JProxy訪問MySQL時經過域名訪問,若是MySQL的主出異常數據庫會進行相應的主從切換操做,JProxy能夠訪問到切換之後新的主,若是整個機房的數據庫異常能夠直接將數據的域名切換到備用機房,保證JProxy能夠訪問到備用機房的數據庫。業務訪問JProxy時也是經過域名訪問,若是一個機房的JProxy都出現了異常,和數據庫相似直接將JProxy前端的域名切換到備用機房,從而保證業務始終都能正常訪問JProxy。
數據高可靠也是很是關鍵的點,咱們會這對數據庫的數據進行按期備份,將備份數據存儲到相應的存儲系統中,從而保證數據庫中的數據即便被刪除依然是能夠恢復的。
系統在線上運行時候監控報警是極其重要的,監控能夠分多個層次,如圖11所示,從主機和操做系統的信息到應用系統的信息到特定系統內部特定的信息的監控等,針對操做系統及主機的監控京東有MJDOS系統能夠把系統的內存/cpu/磁/網卡/機器負載等各類信息都歸入監控系統,這些操做系統的基礎信息對系統異常的診斷很是關鍵,好比由於網絡丟包等引發的服務異常均可以在這個監控系統中及時找到根源。
京東還有統一的監控報警系統UMP,這個監控系統主要是給全部的應用系統服務,全部的應用系統按照必定的規則暴露接口,在UMP系統中註冊之後,UMP系統就能夠提供一整套監控報警服務,最基本的好比系統的存活監控以及是否有慢查詢等。
除了這兩個基本的監控系統之外,咱們還針對整套中間件系統開發了定製的監控系統JMonitor,之因此開發這套監控系統是由於咱們須要採集更多的定製的監控信息,在系統發生異常時可以第一時間定位問題,舉個例子當業務發現TP99降低時每每伴隨着有慢SQL,應用從發送SQL到收到結果這個過程當中通過了JProxy到MySQL又從MySQL通過JProxy再回到應用,這條鏈路上任何一個環節均可能慢,無論是哪一個階段耗時,咱們須要將這種慢SQL的記錄精細化,精細到各個階段都花了多少時間,作到出現慢SQL時能快速準確的找到問題根源快速解決問題。
另外在配合業務去Oracle/SqlServer時,咱們不建議使用跨庫的事務,可是會出現有一種狀況,同一個事務裏的SQL都是帶有拆分字段的,每條SQL都是單節點的,同一個事務裏有多條這種SQL,結果卻出現這個事務是跨庫的,這種事務咱們都會有詳細的記錄,業務方能夠直接經過JMonitor找到這種事務從而更好的進一步改進。除了這個之外,在測試環境時候業務系統一開始寫的SQL沒有考慮太多的優化可能會出現比較多的慢SQL,這些慢SQL咱們都會統一採集在JMonitor系統上進行分析處理,幫助業務方快速迭代調整SQL語句。
業務在使用這套系統的時候 要儘可能出現避免跨庫的SQL,有一個很重要的緣由是當出現跨庫SQL的時候會耗費MySQL較多的鏈接如圖12所示,一條不帶拆分字段的SQL將會發送到全部的分片上,若是在一個MySQL實例上有64個分片,那一條這樣的SQL就會耗費這個MySQL實例上的64個鏈接,這個資源消耗是很是可觀的,若是能夠控制SQL落在單個分片上能夠大大下降MySQL實例上的鏈接壓力。
跨庫的分佈式事務要要儘可能避免,一個是基於MySQL的分佈式數據庫中間件的方案沒法保證嚴格的分佈式事務語義,另外一個即便能夠作到嚴格的分佈式事務語義支持依然是要儘可能避免垮庫事務的,多個跨庫的分佈式事務在某個分片上發生死鎖將會形成其餘分片上的事務也沒法繼續致使直接引發大面積的死鎖,即便是單節點上的事務也要儘可能控制事務小一點,下降死鎖發生的機率。
具體的路由策略不一樣的業務能夠特殊對待,以京東分揀中心爲例,各個分揀中心的大小差別很大,北京上海等大城市的分揀中心數據量很大其餘城市的分揀中心相對會小一點, 針對這種特色咱們會給其定製路由策略,作到將大的分揀中心的數據落在特定的性能較好的MySQL實例上,其餘小的分揀中心的數據能夠按照普通的拆分方式處理。
在JProxy系統層面咱們能夠支持多租戶模式,但考慮到去Oracle/SqlServer的業務每每都是很是重要且數據量巨大的業務,因此咱們的系統都是不一樣的業務獨立部署一套,在部署層面避免各個業務之間的互相影響。考慮到獨立部署會形成一些資源浪費,咱們引入了容器系統,將操做系統資源經過容器的方式進行隔離,從而保證系統資源的充分利用。不少問題不必必定要在代碼層面解決,代碼層面解決起來比較麻煩或者不能作到百分之百把控的事情能夠經過架構層面來解決,架構層面很差解決的事情能夠經過部署的層面來解決,部署層面很差解決的事情能夠經過產品層面來解決,解決問題的方式各式各樣,須要從整個系統全局角度來綜合考量,引用鄧公的一句話「無論黑貓白貓,能抓老鼠的就是好貓」,一樣的道理能支撐住業務發展的系統就是好的系統。
另外再簡單討論一下爲何基於MySQL的分佈式數據庫中間件系統沒法保證嚴格的分佈式事務語義支持。所謂分佈式事務語義本質上就是事務的語義,包含了ACID屬性,分別是原子性、一致性、持久性、隔離性。
原子性是指一個事務要麼成功要麼失敗,不能存在中間狀態。持久性是指一個事務一旦提交成功那麼要作到系統崩潰之後再恢復依然是成功的。隔離性是指各個併發事務之間是隔離的,不可見的,在數據庫具體實現上可能會分不少個隔離級別。事務的一致性是指要保證系統要處於一個一致的狀態,好比從A帳戶轉了500元到B帳戶,那麼從總體系統來看系統的總金額是沒有發生變化的,不能出現A的帳戶已經減去500元可是B帳戶卻沒有增長500元的狀況。
事務在數據庫系統中執行的時候有一個可串行化調度的問題,假設有T一、T二、T3三個事務,那麼這三個事務的執行的效果應該和三個事務串行執行效果同樣,也就是最終效果效果應該是{T1/T2/T3, T1/T3/T2, T2/T1/T3, T2/T3/T1, T3/T1/T2, T3/T2/T1}集合中的一個,當涉及到分佈式事務時,每一個子事務之間的調度要和全局的分佈式事務的調度順序一致才能知足可串行化調度的要求,如圖13所示,T1/T2/T3的三個分佈式事務,在一個庫中的調度順序是T1/T2/T3和全局的調度順序一致,在另外一個庫中的調度順序變成了T3/T2/T1,此時站在全局的角度來看就打破了可串行化調度,可串行化調度保證了隔離性的實現,當可串行化調度被打破時天然隔離性也就隨之打破,在基於MySQL的分佈式中間件方案實現上,由於同一個分佈式事務的各個子事務的事務ID是在各個MySQL上生成的,並無提供全局的事務ID來保證各個子事務的調度順序和全局的分佈式事務一致,致使隔離性是沒法保證的,因此說當前基於MySQL的分佈式事務是沒法保證嚴格的分佈式事務語義支持的。固然隨着MySQL引入GR能夠作到CAP理論中的強一致,再增強中間件的相關功能及定製MySQL相關功能也是有可能作到支持嚴格的分佈式事務的。