分布式kv存儲
① 海量分布式存儲系統Doris原理概述
Doris( https://github.com/itisaid/Doris )是一個海量分布式 KV 存儲系統,其設計目 標是支持中等規模高可用可伸縮的 KV 存儲集群。
Doris可以實現海量存儲,線性伸縮、平滑擴容,自動容錯、故障轉移,高並發,且運維成本低。部署規模,建議部署4-100+台伺服器。
Doris採用兩層架構,Client 和 DataServer+Store。
有四個核心組件,Client、DataServer、Store、Administration。
應用程序通過Client SDK進行Doris的訪問,
每台伺服器上部署一個Data Sever做伺服器的管理,每台伺服器上有自己的存儲Store,整個集群的數據存儲,每台機器獨立部署。數據通過路由選擇寫入到不同的機器中。
Administration為管理中心,提供配置、管理和監控。
config指,應用程序啟動一個Data Server,在啟動時要配置管理中心的ip地址,通關管理中心。管理中心會修改配置項感知到集群中加了新機器,對新機器管理,擴容等。待機器處於可用狀態,將該機器的配置項通知給KV Client。從而KV Client進行新的路由選擇。
擴容、下線機器等的控制台界面通過Management管理。
Monitor監控機器是否正常。
client寫數據,綁定產品的namespace(邏輯隔離),構成新key,路由到具體機器上讀寫。
路由解析演算法是設計的一個關鍵點,決定集群的管理方式,也決定了集群擴容的復雜性和難度。
Doris的演算法類似redis,有桶的概念,key映射到1w個虛擬節點,虛擬節點在映射到物理節點。
由於Doris設計時,用於4-100+規模的集群。因此,Doris分了1w個虛擬節點,當伺服器超過100會導致負載不均衡,1000會更差,相當於每一個集群上有10個虛擬節點,虛擬節點會有10%的影響。
擴容時,需要調節虛擬節點指向新的位置。具體過程為,暴利輪詢新節點添加後,一個伺服器上應該承載的虛擬節點個數,將超出的虛擬節點遷移到新機器即可。如上圖左圖有2個物理節點,擴容後,有3個物理節點,變為右圖。
為了保證高可用。doris所有服務分成2個組,兩組伺服器對等。兩個group是可以有不同數量的伺服器。
寫操作時,client的路由演算法在兩個group分別選2個伺服器,分別(同時)寫入,兩個伺服器全部返回後,再繼續向下進行。讀操作時,從兩個伺服器隨機選一個讀。這樣,提高可用性,數據持久性,不會丟失。
集群管理的重要角色Config Server,有一個功能是負責發現故障伺服器。
發現故障的方式有2種:
節點失效分為:瞬間失效、臨時失效、永久失效
應用伺服器向伺服器寫,如果寫失敗,為 瞬間失效 。接著應用伺服器進行3次重試。3次都失敗,通知管理伺服器,進行服務的失效判斷。
管理伺服器再寫一次,如果寫成功,認為是客戶端自己通信通信問題。如果寫入失敗,判斷為 臨時失效 ,通知所有client,伺服器失效,不要寫,也不讀。
如果2小時恢復,則節點為臨時失效。如果2小時沒有恢復,認為是 永久失效 。
如圖,如果節點2失效,進入臨時失效階段。
如圖,節點2臨時失效2個小時還未恢復,判定為永久失效。進入永久失效的恢復。
設計中,有臨時日誌節點(備份節點),有空白節點。實際使用中沒有節點3空白節點。原因:1 自動遷移有風險,還是需要手動遷移。2 幾年宕機1台,一直有一個空白節點standby浪費。一般晚上報警失效也沒有事情,第二天,找機器擴容即可。認為24小時之內,同樣編號的2台機器連續down掉,概率很低。
物理節點分成2個group,寫的時候,向2個group同時寫。當其中一個group擴容機器時,該group上的所有節點進入臨時失效狀態。停止讀寫,將數據遷移到新的伺服器上。
由於是虛擬節點的映射在調整,所以遷移是按照虛擬節點調整。為了遷移方便,虛擬節點物理化,一個虛擬節點對應一個文件。遷移時其實就是拷貝文件。這時,如果group1有節點失效也會出現不一致,但是,通常擴容的過程很快,因為,是scp拷貝文件,瓶頸為網路帶寬,通常幾十T數據,幾分鍾遷移完成,十來分鍾進行數據恢復。
② 如何實現 Docker 與分布式資料庫結合
那麼Docker是什麼呢?
Docker 是一個開源的應用容器引擎,讓開發者可以打包他們的應用以及依賴包到一個可移植的容器中,然後發布到任何流行的 Linux 機器上,也可以實現虛擬化。容器是完全使用沙箱機制,相互之間不會有任何介面。幾乎沒有性能開銷,可以很容易地在機器和數據中心中運行。最重要的是,他們不依賴於任何語言、框架包括系統。
這是對Docker的一個官方解釋,簡單說,有兩個部分:
1) 對於應用程序,曾經我們需要為了不同的系統專門的調整應用程序的代碼或者是構造相應的依賴包驅動等等,大大增加了開發量以及開發的難度。現在,Docker向不同的應用程序,提供了一個統一的環境。
2) 對於伺服器,為了支持不同版本的應用,曾經可能需要在物理機上安裝多個版本或者不同的GuestOS或者說虛擬機。這就大大佔用了物理機的性能,影響了最終程序的表現,提高了資源的成本。
使用Docker容器的方式,對於應用程序,不需要開發多種多樣的版本或者是針對OS每個版本的升級再進行代碼方面的調整,實現了廣泛的兼容性和開發的最簡性。同時對於物理機,部署的環境「瘦身」也節約了更多的資源,將更多的資源用於提高應用程序本身的性能。
CoreOS是Docker的不二之選?
之前大概介紹了Docker,那麼伺服器上面還是需要最基本的應操作系統才能支撐Docker容器,那麼這么多中的Linux內核OS究竟哪一個好呢?筆者和很多Docker技術專家的的觀點就是Core OS。
CoreOS是一個基於Linux 內核的輕量級操作系統,為了計算機集群的基礎設施建設而生,專注於自動化,輕松部署,安全,可靠,規模化。作為一個操作系統,CoreOS 提供了在應用容器內部署應用所需要的基礎功能環境以及一系列用於服務發現和配置共享的內建工具。
簡單說,CoreOS去掉了大量的非必要的功能,只保留了Server端需要的最基本功能,真正意義做到了「輕量化」。
此外,CoreOS還做到了:整體系統升級/回滾方案;容器化所有非系統應用、無包管理器;集群化調度器Fleet;分布式高可靠的KV存儲系統ETCD
這些特性都讓它成為Docker生態的首選操作系統。不過最新的消息是,CoreOS不滿足於做Docker生態下的一環,它正在推出自己的容器AppC計劃,想對Docker來一招「釜底抽薪」。當然,現階段並沒有出現完全的兩者 「分手」,所以對於普通使用者,並沒有太大影響。
Docker+分布式資料庫
資料庫是每一個軟體項目必須的一個部分,作為這樣的一類底層基礎軟體,兼容性、通用性、易用度都是需要考慮的重點。非常遺憾的是,現在的操作系統以及資料庫都沒有完全的實現完全的通用。特別對於NoSQL資料庫這樣的分布式系統,需要部署在多台物理機時,對於通用性要求就更高了。
目前,像SequoiaDB已經實現了自動化的安裝,大大提升了部署的效率,但是考慮到部署之後的配置以及不同環境下的調試問題,仍然可能會耗費不小的人力物力。所以基於剛剛提到的Docker的優點,作為一個通用的基礎軟體,NoSQL資料庫的Docker化就成了必須。
一個簡單的例子,你可以用docker把資料庫的數據與資料庫程序本身分離開:用一個container A作為數據存儲,然後另一個container B運行資料庫。當你想升級資料庫時,用新的container C替換掉container B即可。
Docker+分布式資料庫的結合,帶來諸多的好處:
1) 部署簡單,使用鏡像部署非常簡單,特別是對集群環境,使用Docker鏡像的部署還可以再資料庫上提前集成Hadoop、Spark等架構,真正實現「一步到位」。
2) 方便應用的更新,應用的更新只需要考慮製作一個新的鏡像就可以與容器適配,無需重新再調整與底層的配置。數據和程序的分離,這樣升級替換等等都不會影響到數據。
3) 操作簡單方便,除了底層免除了復雜的與環境進行配置的工作,操作也更加方便,配置好的Docker鏡像在部署時候只需要一條指令就可以了。
4) 開發、應用環境一致,Docker讓資料庫能做到 開發---測試---實施應用 三個階段的環境是完全一致的。降低開發到應用過程中的工作量,開發出來就能保證實際應用環境上能同樣的運行。
5) 系統穩定,因為Docker的隔離作用,將應用與OS獨立開,這樣能更好保證整個系統的穩定性。
6) 節省系統資源,系統只需要運行一個統一的環境就可以,不需要佔用太多性能去支持運行環境本身,能將更多的系統資源投入到應用當中。
有了這些特性, Docker+資料庫,將成為一個資料庫發展的新方向,Docker這樣的通用性和簡單操作解決方案,大大提高了資料庫使用的效率,幫助使用者節約了大量成本。
Docker是如今技術圈的新潮流,開發人員是最樂見於Docker的這種應用部署模式,因為應用的生命周期起始於開發人員的開發系統,經過開發,測試,壓力測試,等過程,最終應用發布到生產系統,並可能在不同的生產系統中遷移。應用開發人員對此都會有切身的體會,任何微小的運行環境的錯誤都會導致應用出現問題,尤其在講究快速敏捷的今天,應用模塊,新的代碼,新的配置,被快速的加入應用的環境中,可能還沒等寫入到文檔,新特性就已經被推送到生產上了。作為一個新的技術,筆者也希望更多的產品能加強與Docker的結合,幫助產品更好的使用。
博文出處:http://segmentfault.com/a/1190000002930030
③ oss中kvengine模塊的作用
存儲數據的分布式管理。oss中kvengine模塊的作用是存儲數據的分布式管理。OSS也叫對象存儲服務(Object Storage Service),是阿里雲提供的一種存儲服務,隨著智能設備越來越普及,越來越多終端的內容需要存儲在雲端
④ Redis百億級Key存儲設計方案
該應用場景為DMP緩存存儲需求,DMP需要管理非常多的第三方id數據,其中包括各媒體cookie與自身cookie(以下統稱supperid)的mapping關系,還包括了supperid的人口標簽、移動端id(主要是idfa和imei)的人口標簽,以及一些黑名單id、ip等數據。
在hdfs的幫助下離線存儲千億記錄並不困難,然而DMP還需要提供毫秒級的實時查詢。由於cookie這種id本身具有不穩定性,所以很多的真實用戶的瀏覽行為會導致大量的新cookie生成,只有及時同步mapping的數據才能命中DMP的人口標簽,無法通過預熱來獲取較高的命中,這就跟緩存存儲帶來了極大的挑戰。
經過實際測試,對於上述數據,常規存儲超過五十億的kv記錄就需要1T多的內存,如果需要做高可用多副本那帶來的消耗是巨大的,另外kv的長短不齊也會帶來很多內存碎片,這就需要超大規模的存儲方案來解決上述問題。
人⼝標簽主要是cookie、imei、idfa以及其對應的gender(性別)、age(年齡段)、geo(地域)等;mapping關系主要是媒體cookie對supperid的映射。以下是數據存儲⽰示例:
媒體編號-媒體cookie=>supperid
supperid => { age=>年齡段編碼,gender=>性別編碼,geo=>地理位置編碼 }
imei or idfa => { age=>年齡段編碼,gender=>性別編碼,geo=>地理位置編碼 }
顯然PC數據需要存儲兩種key=>value還有key=>hashmap,⽽而Device數據需要存儲⼀一種
key=>hashmap即可。
存儲吃緊的一個重要原因在於每天會有很多新數據入庫,所以及時清理數據尤為重要。主要方法就是發現和保留熱數據淘汰冷數據。
網民的量級遠遠達不到幾十億的規模,id有一定的生命周期,會不斷的變化。所以很大程度上我們存儲的id實際上是無效的。而查詢其實前端的邏輯就是廣告曝光,跟人的行為有關,所以一個id在某個時間窗口的(可能是一個campaign,半個月、幾個月)訪問行為上會有一定的重復性。
數據初始化之前,我們先利用hbase將日誌的id聚合去重,劃定TTL的范圍,一般是35天,這樣可以砍掉近35天未出現的id。另外在Redis中設置過期時間是35天,當有訪問並命中時,對key進行續命,延長過期時間,未在35天出現的自然淘汰。這樣可以針對穩定cookie或id有效,實際證明,續命的方法對idfa和imei比較實用,長期積累可達到非常理想的命中。
Hash表空間大小和Key的個數決定了沖突率(或者用負載因子衡量),再合理的范圍內,key越多自然hash表空間越大,消耗的內存自然也會很大。再加上大量指針本身是長整型,所以內存存儲的膨脹十分可觀。先來談談如何把key的個數減少。
大家先來了解一種存儲結構。我們期望將key1=>value1存儲在redis中,那麼可以按照如下過程去存儲。先用固定長度的隨機散列md5(key)值作為redis的key,我們稱之為BucketId,而將key1=>value1存儲在hashmap結構中,這樣在查詢的時候就可以讓client按照上面的過程計算出散列,從而查詢到value1。
過程變化簡單描述為:get(key1) -> hget(md5(key1), key1) 從而得到value1。
如果我們通過預先計算,讓很多key可以在BucketId空間里碰撞,那麼可以認為一個BucketId下面掛了多個key。比如平均每個BucketId下面掛10個key,那麼理論上我們將會減少超過90%的redis key的個數。
具體實現起來有一些麻煩,而且用這個方法之前你要想好容量規模。我們通常使用的md5是32位的hexString(16進制字元),它的空間是128bit,這個量級太大了,我們需要存儲的是百億級,大約是33bit,所以我們需要有一種機制計算出合適位數的散列,而且為了節約內存,我們需要利用全部字元類型(ASCII碼在0~127之間)來填充,而不用HexString,這樣Key的長度可以縮短到一半。
下面是具體的實現方式
參數bit決定了最終BucketId空間的大小,空間大小集合是2的整數冪次的離散值。這里解釋一下為何一個位元組中只有7位可用,是因為redis存儲key時需要是ASCII(0~127),而不是byte array。如果規劃百億級存儲,計劃每個桶分擔10個kv,那麼我們只需2^30=1073741824的桶個數即可,也就是最終key的個數。
碎片主要原因在於內存無法對齊、過期刪除後,內存無法重新分配。通過上文描述的方式,我們可以將人口標簽和mapping數據按照上面的方式去存儲,這樣的好處就是redis key是等長的。另外對於hashmap中的key我們也做了相關優化,截取cookie或者deviceid的後六位作為key,這樣也可以保證內存對齊,理論上會有沖突的可能性,但在同一個桶內後綴相同的概率極低(試想id幾乎是隨機的字元串,隨意10個由較長字元組成的id後綴相同的概率*桶樣本數=發生沖突的期望值<<0.05,也就是說出現一個沖突樣本則是極小概率事件,而且這個概率可以通過調整後綴保留長度控制期望值)。而value只存儲age、gender、geo的編碼,用三個位元組去存儲。
另外提一下,減少碎片還有個很low但是有效的方法,將slave重啟,然後強制的failover切換主從,這樣相當於給master整理的內存的碎片。
推薦Google-tcmalloc, facebook-jemalloc內存分配,可以在value不大時減少內存碎片和內存消耗。有人測過大value情況下反而libc更節約。
1)kv存儲的量級必須事先規劃好,浮動的范圍大概在桶個數的十到十五倍,比如我就想存儲百億左右的kv,那麼最好選擇30bit 31bit作為桶的個數。也就是說業務增長在一個合理的范圍(10 15倍的增長)是沒問題的,如果業務太多倍數的增長,會導致hashset增長過快導致查詢時間增加,甚至觸發zip-list閾值,導致內存急劇上升。
2)適合短小value,如果value太大或欄位太多並不適合,因為這種方式必須要求把value一次性取出,比如人口標簽是非常小的編碼,甚至只需要3、4個bit(位)就能裝下。
3)典型的時間換空間的做法,由於我們的業務場景並不是要求在極高的qps之下,一般每天億到十億級別的量,所以合理利用CPU租值,也是十分經濟的。
4)由於使用了信息摘要降低了key的大小以及約定長度,所以無法從redis裡面random出key。如果需要導出,必須在冷數據中導出。
5)expire需要自己實現,目前的演算法很簡單,由於只有在寫操作時才會增加消耗,所以在寫操作時按照一定的比例抽樣,用HLEN命中判斷是否超過15個entry,超過才將過期的key刪除,TTL的時間戳存儲在value的前32bit中。
6)桶的消耗統計是需要做的。需要定期清理過期的key,保證redis的查詢不會變慢。
人口標簽和mapping的數據100億條記錄。
優化前用2.3T,碎片率在2左右;優化後500g,而單個桶的平均消耗在4左右。碎片率在1.02左右。查詢時這對於cpu的耗損微乎其微。
另外需要提一下的是,每個桶的消耗實際上並不是均勻的,而是符合多項式分布的。
上面的公式可以計算桶消耗的概率分布。公式是唬人用的,只是為了提醒大家不要想當然的認為桶消耗是完全均勻的,有可能有的桶會有上百個key。但事實並不沒有那麼誇張。試想一下投硬幣,結果只有兩種正反面。相當於只有兩個桶,如果你投上無限多次,每一次相當於一次伯努利實驗,那麼兩個桶必然會十分的均勻。概率分布就像上帝施的魔咒一樣,當你面對大量的桶進行很多的廣義的伯努利實驗。桶的消耗分布就會趨於一種穩定的值。接下來我們就了解一下桶消耗分布具體什麼情況:
通過采樣統計
31bit(20多億)的桶,平均4.18消耗
100億節約了1.8T內存。相當於節約了原先的78%內存,而且桶消耗指標遠沒有達到預計的底線值15。
對於未出現的桶也是存在一定量的,如果過多會導致規劃不準確,其實數量是符合二項分布的,對於2 30桶存儲2 32kv,不存在的桶大概有(百萬級別,影響不大):
Math.pow((1 - 1.0 / Math.pow(2, 30)), Math.pow(2, 32)) * Math.pow(2, 30);
對於桶消耗不均衡的問題不必太擔心,隨著時間的推移,寫入時會對HLEN超過15的桶進行削減,根據多項式分布的原理,當實驗次數多到一定程度時,桶的分布就會趨於均勻(硬幣投擲無數次,那麼正反面出現次數應該是一致的),只不過我們通過expire策略削減了桶消耗,實際上對於每個桶已經經歷了很多的實驗發生。
總結:信息摘要在這種場景下不僅能節約key存儲,對齊了內存,還能讓Key按照多項式分布均勻的散列在更少量的key下面從而減少膨脹,另外無需在給key設置expire,也很大程度上節約了空間。
這也印證了時間換空間的基本理論,合理利用CPU租值也是需要考慮的。
關注分布式存儲技術以及分布式計算方法
⑤ 什麼是kv資料庫
kv資料庫是指Key-value資料庫,是一種以鍵值對存儲數據的一種資料庫,類似java中的map。可以將整個資料庫理解為一個大的map,每個鍵都會對應一個唯一的值。
key-value分布式存儲系統查詢速度快、存放數據量大、支持高並發,非常適合通過主鍵進行查詢,但不能進行復雜的條件查詢。
如果輔以實時搜索引擎進行復雜條件檢索、全文檢索,就可以替代並發性能較低的MySQL等關系型資料庫,達到高並發、高性能,節省幾十倍伺服器數 量的目的。以MemcacheDB、Tokyo Tyrant為代表的key-value分布式存儲,在上萬並發連接下,輕松地完成高速查詢。
(5)分布式kv存儲擴展閱讀:
資料庫的安全直接關繫到整個資料庫系統的安全,其防護手段主要有以下八點:
1、使用正版資料庫管理系統並及時安裝相關補丁。
2、做好用戶賬戶管理,禁用默認超級管理員賬戶或者為超級管理員賬戶設置復雜密碼;為應用程序分別分配專用賬戶進行訪問;設置用戶登錄時間及登錄失敗次數限制, 防止暴力破解用戶密碼。
3、分配用戶訪問許可權時,堅持最小許可權分配原則,並限制用戶只能訪問特定資料庫,不能同時訪問其他資料庫。
4、修改資料庫默認訪問埠,使用防火牆屏蔽掉對 外開放的其他埠,禁止一切外部的埠探測行為。
5、對資料庫內存儲的重要數據、敏感數據進行加密存儲,防止資料庫備份或數據文件被盜而造成數據泄露。
6、設置好資料庫的備份策略,保證資料庫被破壞後能迅速恢復。
7、對資料庫內的系統存儲過程進行合理管理,禁用掉不必要的存儲過程,防止利用存儲過程進行資料庫探測與攻擊。
8、啟用資料庫審核功能,對資料庫進行全面的事件跟蹤和日誌記錄。
參考資料來源:
網路-Key-Value
網路-資料庫
⑥ 分布式容易發論文嗎
沒有那麼容易,比較難
分布式工程學是一門實踐性很強的工科學。所以會出現與其他工科一樣的現象就是實踐會先於理論。在1960年末被公認為是第一個分布式系統的ARPANET就誕生於美國[1]。在美國50年代到60年受曼哈頓計劃的影響,計算機理論迎來了大爆炸的時代。在那個年代發明了我們今天所用到的大部分計算機理論。作為一個新興學科,當年的科學家大都是剛剛畢業正是壯年。而今他們大多已經是高齡老人,有些科學家則已經離世。在這里向哪些為計算機理論作出貢獻的科學家們表示敬意。
⑦ B站分布式KV存儲實踐
在B站的業務場景中,存在很多種不同模型的數據,有些數據關系比較復雜像:賬號、稿件信息。有些數據關系比較簡單,只需要簡單的kv模型即可滿足。此外,又存在某些讀寫吞吐比較高的業務場景,該場景早期的解決方案是通過MySQL來進行數據的持久化存儲,同時通過redis來提升訪問的速度與吞吐。但是這種模式帶來了兩個問題,其一是存儲與緩存一致性的問題,該問題在B站通過canal非同步更新緩存的方式得以解決,其二則是開發的復雜度,對於這樣一套存儲系統,每個業務都需要額外維護一個任務腳本來消費canal數據進行緩存數據的更新。基於這種場景,業務需要的其實是一個介於Redis與MySQL之間的提供持久化高性能的kv存儲。此外對象存儲的元數據,對數據的一致性、可靠性與擴展性有著很高的要求。
基於此背景,我們對自研KV的定位從一開始就是構建一個高可靠、高可用、高性能、高拓展的系統。對於存儲系統,核心是保證數據的可靠性,當數據不可靠時提供再高的可用性也是沒用的。可靠性的一個核心因素就是數據的多副本容災,通過raft一致性協議保證多副本數據的一致性。
分布式系統,如何對數據進行分片放置,業界通常有兩種做法,一是基於hash進行分區,二是基於range進行分區,兩種方式各有優缺點。hash分區,可以有效防止熱點問題,但是由於key是hash以後放置的,無法保證key的全局有序。range分區,由於相鄰的數據都放在一起,因此可以保證數據的有序,但是同時也可能帶來寫入熱點的問題。基於B站的業務場景,我們同時支持了range分區和hash分區,業務接入的時候可以根據業務特性進行選擇。大部分場景,並不需要全局有序,所以默認推薦hash分區的接入方式,比如觀看記錄、用戶動態這些場景,只需要保證同一個用戶維度的數據有序即可,同一個用戶維度的數據可以通過hashtag的方式保證局部有序。
整個系統核心分為三個組件:
Metaserver用戶集群元信息的管理,包括對kv節點的健康監測、故障轉移以及負載均衡。
Node為kv數據存儲節點,用於實際存儲kv數據,每個Node上保存數據的一個副本,不同Node之間的分片副本通過raft保證數據的一致性,並選出主節點對外提供讀寫,業務也可以根據對數據一致性的需求指定是否允許讀從節點,在對數據一致性要求不高的場景時,通過設置允許讀從節點可以提高可用性以及降低長尾。
Client模塊為用戶訪問入口,對外提供了兩種接入方式,一種是通過proxy模式的方式進行接入,另一種是通過原生的SDK直接訪問,proxy本身也是封裝自c++的原生SDK。SDK從Metaserver獲取表的元數據分布信息,根據元數據信息決定將用戶請求具體發送到哪個對應的Node節點。同時為了保證高可用,SDK還實現了重試機制以及backoff請求。
集群的拓撲結構包含了幾個概念,分別是Pool、Zone、Node、Table、Shard 與Replica。
基於不同的業務場景,我們同時支持了range分區和hash分區。對於range場景,隨著用戶數據的增長,需要對分區數據進行分裂遷移。對於hash分區的場景,使用上通常會根據業務的數據量做幾倍的冗餘預估,然後創建合適的分片數。但是即便是幾倍的冗餘預估,由於業務發展速度的不可預測,也很容易出現實際使用遠超預估的場景,從而導致單個數據分片過大。
之所以不在一開始就創建足夠的分片數有兩個原因:其一,由於每一個replica都包含一個獨立的engine,過多的分片會導致數據文件過多,同時對於批量寫入場景存在一定的寫扇出放大。其二,每一個shard都是一組raftgroup,過多的raft心跳會對服務造成額外的開銷,這一點後續我們會考慮基於節點做心跳合並優化減少集群心跳數。
為了滿足業務的需求場景,我們同時支持了range和hash兩種模式下的分裂。兩種模式分裂流程類似,下面以hash為例進行說明。
hash模式下的分裂為直接根據當前分片數進行倍增。分裂的流程主要涉及三個模塊的交互。
metaserver
分裂時,metaserver會根據當前分片數計算出目標分片數,並且下發創建replica指令到對應的Node節點,同時更新shard分布信息,唯一不同的是,處於分裂中的shard狀態為splitting。該狀態用於client流量請求路由識別。當Node完成數據分裂以後上報metaserver,metaserver更新shard狀態為normal從而完成分裂。
Node
node收到分裂請求以後,會根據需要分裂的分片id在原地拉起創建一個新的分片。然後對舊分片的數據進行checkpoint,同時記錄舊分片checkpoint對應的logid。新分片創建完成後,會直接從舊分片的checkpoint進行open,然後在非同步復制logid之後的數據保證數據的一致性。新分片載入完checkpoint後,原來的舊分片會向raftgroup提交一條分裂完成日誌,該日誌處理流程與普通raft日誌一致。分裂完成後上報分裂狀態到metaserver,同時舊分片開始拒絕不再屬於自己分片的數據寫入,client收到分片錯誤以後會請求metaserver更新shard分布。
完成分裂以後的兩個分片擁有的兩倍冗餘數據,這些數據會在engine compaction的時候根據compaction_filter過濾進行刪除。
Client
用戶請求時,根據hash(key) % shard_cnt 獲取目標分片。表分裂期間,該shard_cnt表示分裂完成後的最終分片數。以上圖3分片的分裂為例:
hash(key) = 4, 分裂前shard_cnt為3,因此該請求會被發送到shard1. 分裂期間,由於shard_cnt變為6,因此目標分片應該是shard4, 但是由於shard4為splitting,因此client會重新計算分片從而將請求繼續發送給shard1. 等到最終分裂完成後,shard4狀態變更為Normal,請求才會被發送到shard4.
分裂期間,如果Node返回分片信息錯誤,那麼client會請求metaserver更新分片分布信息。
類似於MySQL的binlog,我們基於raftlog日誌實現了kv的binlog. 業務可以根據binlog進行實時的事件流訂閱,同時為了滿足事件流回溯的需求,我們還對binlog數據進行冷備。通過將binlog冷備到對象存儲,滿足了部分場景需要回溯較長事件記錄的需求。
直接復用raftlog作為用戶行為的binlog,可以減少binlog產生的額外寫放大,唯一需要處理的是過濾raft本身的配置變更信息。learner通過實時監聽不斷拉取分片產生的binlog到本地並解析。根據learner配置信息決定將數據同步到對應的下游。同時binlog數據還會被非同步備份到對象存儲,當業務需要回溯較長時間的事件流的時候,可以直接指定位置從S3拉取歷史binlog進行解析。
基於上述提到的binlog能力,我們還基於此實現了kv的多活。learner模塊會實時將用戶寫入的數據同步到跨數據中心的其他kv集群。對於跨數據中心部署的業務,業務可以選擇就近的kv集群進行讀取訪問,降低訪問延時。
kv的多活分為讀多活和寫多活。對於讀多活,機房A的寫入會被非同步復制到機房B,機房B的服務可以直接讀取本機房的數據,該模式下只有機房A的kv可以寫入。對於寫多活,kv在機房A B 都能同時提供寫入並且進行雙向同步,但是為了保證數據的一致性,需要業務上做數據的單元化寫入,保證兩個機房不會同時修改同一條記錄。通過將用戶劃分單元,提供了寫多活的能力。通過對binlog數據打標,解決了雙向同步時候的數據回環問題。
對於用戶畫像和特徵引擎等場景,需要將離線生成的大量數據快速導入KV存儲系統提供用戶讀取訪問。傳統的寫入方式是根據生成的數據記錄一條條寫入kv存儲,這樣帶來兩個問題。其一,大批量寫入會對kv造成額外的負載與寫入帶寬放大造成浪費。其次,由於寫入量巨大,每次導入需要花費較長的時間。為了減少寫入放大以及導入提速,我們支持了bulk load的能力。離線平台只需要根據kv的存儲格式離線生成對應的SST文件,然後上傳到對象存儲服務。kv直接從對象存儲拉取SST文件到本地,然後直接載入SST文件即可對外提供讀服務。bulk load的另外一個好處是可以直接在生成SST後離線進行compaction,將compaction的負載offload到離線的同時也降低了空間的放大。
由於LSM tree的寫入特性,數據需要被不斷的compaction到更底層的level。在compaction時,如果該key還有效,那麼會被寫入到更底層的level里,如果該key已經被刪除,那麼會判斷當前level是否是最底層的,一條被刪除的key,會被標記為刪除,直到被compaction到最底層level的時候才會被真正刪除。compaction的時候會帶來額外的寫放大,尤其當value比較大的時候,會造成巨大的帶寬浪費。為了降低寫放大,我們參考了Bitcask實現了kv分離的存儲引擎sparrowdb.
sparrowdb 介紹
用戶寫入的時候,value通過append only的方式寫入data文件,然後更新索引信息,索引的value包含實際數據所在的data文件id,value大小以及position信息,同時data文件也會包含索引信息。與原始的bitcask實現不一樣的是,我們將索引信息保存在 rocksdb。
更新寫入的時候,只需要更新對應的索引即可。compaction的時候,只需將索引寫入底層的level,而無需進行data的拷貝寫入。對於已經失效的data,通過後台線程進行檢查,當發現data文件里的索引與rocksdb保存的索引不一致的時候,說明該data已經被刪除或更新,數據可以被回收淘汰。
使用kv存儲分離降低了寫放大的問題,但是由於kv分離存儲,會導致讀的時候多了一次io,讀請求需要先根據key讀到索引信息,再根據索引信息去對應的文件讀取data數據。為了降低讀訪問的開銷,我們針對value比較小的數據進行了inline,只有當value超過一定閾值的時候才會被分離存儲到data文件。通過inline以及kv分離獲取讀性能與寫放大之間的平衡。
在分布式系統中,負載均衡是繞不過去的問題。一個好的負載均衡策略可以防止機器資源的空閑浪費。同時通過負載均衡,可以防止流量傾斜導致部分節點負載過高從而影響請求質量。對於存儲系統,負載均衡不僅涉及到磁碟的空間,也涉及到機器的內存、cpu、磁碟io等。同時由於使用raft進行主從選主,保證主節點盡可能的打散也是均衡需要考慮的問題。
副本均衡
由於設計上我們會盡量保證每個副本的大小盡量相等,因此對於空間的負載其實可以等價為每塊磁碟的副本數。創建副本時,會從可用的zone中尋找包含副本數最少的節點進行創建。同時考慮到不同業務類型的副本讀寫吞吐可能不一樣導致CPU負載不一致,在挑選副本的時候會進一步檢查當前節點的負載情況,如果當前節點負載超過閾值,則跳過該節點繼續選擇其他合適的節點。目前基於最少副本數以及負載校驗基本可以做到集群內部的節點負載均衡。
當出現負載傾斜時,則從負載較高的節點選擇副本進行遷出,從集群中尋找負載最低的節點作為待遷入節點。當出現節點故障下線以及新機器資源加入的時候,也是基於均值計算待遷出以及遷入節點進行均衡。
主從均衡
雖然通過最少副本數策略保證了節點副本數的均衡,但是由於raft選主的性質,可能出現主節點都集中在部分少數節點的情況。由於只有主節點對外提供寫入,主節點的傾斜也會導致負載的不均衡。為了保證主節點的均衡,Node節點會定期向metaserver上報當前節點上副本的主從信息。
主從均衡基於表維度進行操作。metaserver會根據表在Node的分布信息進行副本數的計算。主副本的數量基於最樸素簡單的數學期望進行計算: 主副本期望值 = 節點副本數 / 分片副本數。下面為一個簡單的例子:
假設表a包含10個shard,每個shard 3個replica。在節點A、B、C、D的分布為 10、5、6、9. 那麼A、B、C、D的主副本數期望值應該為 3、1、2、3. 如果節點數實際的主副本數少於期望值,那麼被放入待遷入區,如果大於期望值,那麼被放入待遷出區。同時通過添加誤差值來避免頻繁的遷入遷出。只要節點的實際主副本數處於 [x-δx,x+δx] 則表示主副本數處於穩定期間,x、δx 分別表示期望值和誤差值。
需要注意的是,當對raft進行主從切換的時候,從節點需要追上所有已提交的日誌以後才能成功選為主,如果有節點落後的時候進行主從切換,那麼可能導致由於追數據產生的一段時間無主的情況。因此在做主從切換的時候必須要檢查主從的日誌復制狀態,當存在慢節點的時候禁止進行切換。
3.7 故障檢測&修復
一個小概率的事件,隨著規模的變大,也會變成大概率的事件。分布式系統下,隨著集群規模的變大,機器的故障將變得愈發頻繁。因此如何對故障進行自動檢測容災修復也是分布式系統的核心問題。故障的容災主要通過多副本raft來保證,那麼如何進行故障的自動發現與修復呢。
健康監測
metaserver會定期向node節點發送心跳檢查node的健康狀態,如果node出現故障不可達,那麼metaserver會將node標記為故障狀態並剔除,同時將node上原來的replica遷移到其他健康的節點。
為了防止部分node和metaserver之間部分網路隔離的情況下node節點被誤剔除,我們添加了心跳轉發的功能。上圖中三個node節點對於客戶端都是正常的,但是node3由於網路隔離與metaserver不可達了,如果metaserver此時直接剔除node3會造成節點無必要的剔除操作。通過node2轉發心跳探測node3的狀態避免了誤剔除操作。
除了對節點的狀態進行檢測外,node節點本身還會檢查磁碟信息並進行上報,當出現磁碟異常時上報異常磁碟信息並進行踢盤。磁碟的異常主要通過dmesg日誌進行採集分析。
故障修復
當出現磁碟節點故障時,需要將原有故障設備的replica遷移到其他健康節點,metaserver根據負載均衡策略選擇合適的node並創建新replica, 新創建的replica會被加入原有shard的raft group並從leader復制快照數據,復制完快照以後成功加入raft group完成故障replica的修復。
故障的修復主要涉及快照的復制。每一個replica會定期創建快照刪除舊的raftlog,快照信息為完整的rocksdb checkpoint。通過快照進行修復時,只需要拷貝checkpoint下的所有文件即可。通過直接拷貝文件可以大幅減少快照修復的時間。需要注意的是快照拷貝也需要進行io限速,防止文件拷貝影響在線io.
過期數據淘汰
在很多業務場景中,業務的數據只需要存儲一段時間,過期後數據即可以自動刪除清理,為了支持這個功能,我們通過在value上添加額外的ttl信息,並在compaction的時候通過compaction_filter進行過期數據的淘汰。level之間的容量呈指數增長,因此rocksdb越底層能容納越多的數據,隨著時間的推移,很多數據都會被移動到底層,但是由於底層的容量比較大,很難觸發compaction,這就導致很多已經過期的數據沒法被及時淘汰從而導致了空間放大。與此同時,大量的過期數據也會對scan的性能造成影響。這個問題可以通過設置periodic_compaction_seconds 來解決,通過設置周期性的compaction來觸發過期數據的回收。
scan慢查詢
除了上面提到的存在大批過期數據的時候可能導致的scan慢查詢,如果業務存在大批量的刪除,也可能導致scan的時候出現慢查詢。因為delete對於rocksdb本質也是一條append操作,delete寫入會被添加刪除標記,只有等到該記錄被compaction移動到最底層後該標記才會被真正刪除。帶來的一個問題是如果用戶scan的數據區間剛好存在大量的delete標記,那麼iterator需要迭代過濾這些標記直到找到有效數據從而導致慢查詢。該問題可以通過添加 CompactOnDeletionCollector 來解決。當memtable flush或者sst compaction的時候,collector會統計當前key被刪除的比例,通過設置合理的 deletion_trigger ,當發現被delete的key數量超過閾值的時候主動觸發compaction。
delay compaction
通過設置 CompactOnDeletionCollector 解決了delete導致的慢查詢問題。但是對於某些業務場景,卻會到來嚴重的寫放大。當L0被compaction到L1時候,由於閾值超過deletion_trigger ,會導致L1被添加到compaction隊列,由於業務的數據特性,L1和L2存在大量重疊的數據區間,導致每次L1的compaction會同時帶上大量的L2文件造成巨大的寫放大。為了解決這個問題,我們對這種特性的業務數據禁用了CompactOnDeletionCollector 。通過設置表級別參數來控製表級別的compaction策略。後續會考慮優化delete trigger的時機,通過只在指定層級觸發來避免大量的io放大。
compaction限速
由於rocksdb的compaction會造成大量的io讀寫,如果不對compaction的io進行限速,那麼很可能影響到在線的寫入。但是限速具體配置多少比較合適其實很難確定,配置大了影響在線業務,配置小了又會導致低峰期帶寬浪費。基於此rocksdb 在5.9以後為 NewGenericRateLimiter 添加了 auto_tuned 參數,可以根據當前負載自適應調整限速。需要注意的是,該函數還有一個參數 RateLimiter::Mode 用來限制操作類型,默認值為 kWritesOnly,通常情況該模式不會有問題,但是如果業務存在大量被刪除的數據,只限制寫可能會導致compaction的時候造成大量的讀io。
關閉WAL
由於raft log本身已經可以保證數據的可靠性,因此寫入rocksdb的時候可以關閉wal減少磁碟io,節點重啟的時候根據rocksdb里保存的last_apply_id從raft log進行狀態機回放即可。
降副本容災
對於三副本的raft group,單副本故障並不會影響服務的可用性,即使是主節點故障了剩餘的兩個節點也會快速選出主並對外提供讀寫服務。但是考慮到極端情況,假設同時出現兩個副本故障呢? 這時只剩一個副本無法完成選主服務將完全不可用。根據墨菲定律,可能發生的一定會發生。服務的可用性一方面是穩定提供服務的能力,另一方面是故障時快速恢復的能力。那麼假設出現這種故障的時候我們應該如何快速恢復服務的可用呢。
如果通過創建新的副本進行修復,新副本需要等到完成快照拷貝以後才能加入raft group進行選舉,期間服務還是不可用的。那麼我們可以通過強制將分片降為單副本模式,此時剩餘的單個健康副本可以獨自完成選主,後續再通過變更副本數的方式進行修復。
RaftLog 聚合提交
對於寫入吞吐非常高的場景,可以通過犧牲一定的延時來提升寫入吞吐,通過log聚合來減少請求放大。對於SSD盤,每一次寫入都是4k刷盤,value比較小的時候會造成磁碟帶寬的浪費。我們設置了每5ms或者每聚合4k進行批量提交。該參數可以根據業務場景進行動態配置修改。
非同步刷盤
有些對於數據一致性要求不是非常高的場景,服務故障的時候允許部分數據丟失。對於該場景,可以關閉fsync通過操作系統進行非同步刷盤。但是如果寫入吞吐非常高導致page cache的大小超過了 vm.diry_ratio ,那麼即便不是fsync也會導致io等待,該場景往往會導致io抖動。為了避免內核pdflush大量刷盤造成的io抖動,我們支持對raftlog進行非同步刷盤。
透明多級存儲,和緩存結合,自動冷熱分離,通過將冷數據自動搬遷到kv降低內存使用成本。
新硬體場景接入,使用SPDK 進行IO提速,使用PMEM進行訪問加速。
參考文獻
[1] Bitcask A Log-Structured Hash Table for Fast Key/Value Data
[2] Lethe: A Tunable Delete-Aware LSM Engine
⑧ 為什麼分布式資料庫這么喜歡用kv store
大部分資料庫都有KV存儲這個抽象,但仍然存在很大的設計空間,例如單機的KV是否需要支持事務,是否需要感知schema,是否需要暴露多版本的介面。因此,不能籠統地說分布式資料庫都喜歡用KV store。
分布式資料庫系統通常使用較小的計算機系統,每台計算機可單獨放在一個地方,每台計算機中都可能有DBMS的一份完整拷貝副本,或者部分拷貝副本,並具有自己局部的資料庫,位於不同地點的許多計算機通過網路互相連接,共同組成一個完整的、全局的邏輯上集中、物理上分布的大型資料庫。
結構模式
根據我國制定的《分布式資料庫系統標准》,分布式資料庫系統抽象為4層的結構模式。這種結構模式得到了國內外的支持和認同。
4層模式劃分為全局外層、全局概念層、局部概念層和局部內層,在各層間還有相應的層間映射。這種4層模式適用於同構型分布式資料庫系統,也適用於異構型分布式資料庫系統。
⑨ WTable:RocksDB使用技巧之分布式存儲擴容演進
RocksDB是由Facebook公司開源的一款高性能Key Value存儲引擎,目前被廣泛應用於業界各大公司的存儲產品中,其中就包括58存儲團隊自研的分布式KV存儲產品WTable。
RocksDB基於LSM Tree存儲數據,它的寫入都是採用即時寫WAL + Memtable,後台Compaction的方式進行。當寫入量大時,Compaction所佔用的IO資源以及對其讀寫的影響不容忽視。而對於一個分布式存儲系統來說,擴展性尤為重要,但是在擴展的過程中,又不可避免地會涉及到大量的數據遷移、寫入。
本篇文章將會著重介紹WTable是如何利用RocksDB的特性對擴容流程進行設計以及迭代的。
WTable為了實現集群的可擴展性,將數據劃分成了多個Slot,一個Slot既是數據遷移的最小單位。當集群的硬碟空間不足或寫性能需要擴展時,運維人員就可以添加一些機器資源,並將部分Slot遷移到新機器上。這就實現了數據分片,也就是擴容。
具體哪些數據被分到哪個Slot上,這是通過對Key做Hash演算法得到的,偽演算法如下:
SlotId = Hash(Key)/N 其中的N就是Slot的總量,這個是一個預設的固定值。
另外,為了讓同一個Slot中所有Key的數據在物理上能夠存儲在一起,底層實際存儲的Key都會在用戶Key的前面加上一個前綴:該Key對應的SlotId。具體方式是將SlotId以大端法轉換成2個位元組,填充到Key位元組數組的前面。
在RocksDB中,除了level 0外,其餘level中的sst文件,以及sst文件內部都是有序存儲的。這樣一來,WTable也就實現了單個Slot內數據的連續存儲,以及所有Slot整體的有序性。
WTable初始的擴容方式如下:
如上圖所示,遷移一個Slot可以分成3個階段:全量遷移、增量遷移、路由信息切換。
其中全量遷移會在該Slot所在的老節點上創建一個RocksDB的Iterator,它相當於創建了一份數據快照,同時Iterator提供了seek、next等方法,可以通過遍歷Iterator有序地獲取一定范圍內的數據。對應到這里,就是一個Slot在某一時刻的全量快照數據。老節點通過遍歷Slot數據,將每個Key,Value傳輸到新節點,新節點寫入到自己的RocksDB中。
增量遷移則會利用老WTable節點記錄的Binlog,將全量遷移過程中新的寫入或更新,傳輸到新的節點,新節點將其應用到RocksDB。
最後,當發現新老節點上待遷移Slot的數據已經追平之後,則在ETCD上修改該Slot對應的節點信息,也就是路由信息切換。從此以後,該Slot中數據的線上服務就由新節點提供了。
然而,上述的擴容方式在線上運行過程中存在一個問題:當數據遷移速度較高(如30MB/s)時,會影響到新節點上的線上服務。
深究其具體原因,則是由於一次擴容會串列遷移多個Slot,率先遷移完成的Slot在新節點上已經提供線上服務,而遷移後續的Slot還是會進行全量數據、增量數據的遷移。
通過上個章節的描述,我們可以得知,全量數據是用RocksDB Iterator遍歷產生,對於一個Slot來說,是一份有序的數據。而增量數據則是線上實時寫入的數據,肯定是無序的數據。所以當新節點同時寫入這兩種數據時,從整體上就變成了無序的數據寫入。
在RocksDB中,如果某一個level N中的文件總大小超過一定閾值,則會進行Compaction,Compaction所做的就是:將level N中的多個sst文件與這些文件在level N+1中Key范圍有重疊的文件進行合並,最終生成多個新文件放入level N+1中。合並的方式可以簡單表述為:如果多個文件中的Key確實有交集,則按照規則進行歸並排序,去重,按大小生成多個新sst文件;如果Key沒有交集(說明這次合並,就沒有level N+1中的文件參與),則直接將level N中的文件move到levelN+1。
這樣我們就可以看出,當大量的整體無序的數據寫入遷移新節點時,各level之間的sst文件Key的范圍難免會重疊,而其上的RocksDB則會頻繁進行大量的,需要歸並排序、去重的Compaction(而不是簡單的move)。這勢必會佔用大量的IO,進而影響讀、寫性能。
另外,Compaction過多、過重造成level 0層的文件無法快速沉澱到level 1,而同時數據遷移使得新節點RocksDB的寫入速度又很快,這就導致level 0中的文件個數很容易就超過閾值level0_stop_writes_trigger,這時則會發生write stall,也就是停寫,這時就會嚴重影響寫服務。
根據前面的問題描述,我們深入分析了RocksDB Compaction的特點,提出了兩點改進思路:
根據以上分析,我們最終將擴容分為了3個大的階段:
整體流程如下圖所示:
經過上述擴容方式的改進,目前線上WTable集群已經可以進行較高速的擴容,比如50~100MB/s,並且在整個流程中不會對線上服務有任何影響。
在制定方案之初,我們也調研過其他的方案,比如老節點傳輸sst文件給新節點,後者通過IngestExternalFile的方式將sst文件導入RocksDB。
但是WTable的主備同步是通過Binlog進行的,而當主機通過IngestExternalFile的方式導入數據時,並不會有Binlog產生,也就無法通過正常流程同步數據給備機。因此如果以此方式實現數據遷移,需要增加新的主備同步方式,這對原有流程是一個破壞,另外也需要比較大的工作量,效率方面也無法保證。
因此我們最終利用RocksDB Compaction的特點設計了適合WTable的快速擴容方案,解決了這個痛點。
⑩ java集合存儲統計部門的職位
java集合存儲統計部門的職位是後端Java開發工程師。根據查詢相關公開資料信息顯示,後端Java開發工程師參與公司存儲平台、分布式資料庫平台建設,新技術研究以及實施。主要技術方向包括分布式對象存儲、分布式KV存儲、共享文件系統、分布式關系型資料庫。負責分布式系統的需求分析,技術調研,方案設計,代碼編寫以及優化等工作。歸於java集合存儲統計部門。