陳宗志:奇虎360基礎架構組 高級存儲研發工程師,目前負責360分布式存儲系統Bada的設計和實現,同時負責360虛擬化相關技術的研究。
本次分享主題
主要向大家介紹一下360自主研發的分布式存儲系統Nosql-Bada,作為設計者我一直覺得設計過程就是在做一些折衷,所以大部分的內容是我們開發實現Bada過程中的一些經驗和坑, 也有很多的權衡, 希望和大家一起分享, 有不對的地方歡迎指出。
雖然項目目前還未開源, 但是我們的一些組件, 用于異步同步數據的Mario庫等, 均已經開源,后續Bada也會開源。這是360官方的Github賬號https://github.com/Qihoo360
主要應用場景
我們的定位是海量數據的持久化存儲, 為線上的熱門應用服務。不過我們目前沒有接入跟錢相關的業務, 因為我們的系統畢竟是最終一致性的系統。
我們傾向使用Bada的用戶數據value的大小在10k以內, 那么我們的延遲能夠做到1ms左右。我們為了讀取性能有一定的優勢, 一般要求機器都掛載SSD盤。如果用于存儲冷數據, 我們會建議用戶存數據到公司的其他存儲產品, 比如hbase,cassandra等等。
目前公司內部云盤, 移動搜索, LBS, Onebox, 導航影視, 白名單等多個業務均在使用。
云盤的場景是:通過Bada查詢文件所在的存儲位置。這個業務數據量千億級別, 每天的訪問量近百億。
LBS這個業務是將所有的POI的信息存儲在Bada中, 業務需要在5個機房進行數據同步。每天的請求量十億級別。
整體架構
Bada SDK 是我們提供給用戶SDK, 360 QConf 配置管理服務 大家之前也了解過, 我們是QConf的重度用戶。用戶通過SDK從QConf中獲得存活的Bada節點, 然后進行訪問。
Data Server是我們的服務節點,其設計是學習自Amazon Dynamo(不過好像Dynamo 本身也被很多人噴), 每一個節點都是對等結構, 每一個節點存儲了所有的元信息。為什么這么做?
目前主流的設計一般是兩種:
BigTable 為代表的,有MetaServer, DataServer的設計, MetaServer存儲元數據信息, DataServer存儲實際的數據。包括 BigTable, HBase, 百度的Mola等等。
Dynamo 為代表的,對等結構設計. 每一個節點都是一樣的結構, 每一個節點都保存了數據的元信息以及數據. 包括 Cassandra, Riak 等等。
[page]Bada 的選擇
其實我覺得兩個結構都是合適的。為了部署, 擴展等方便,我們不希望部署的時候需要分開部署Meta節點, Data節點。計算機行業, 加一層可以解決大部分問題, 因此我們覺得對等網絡的設計更有挑戰性。個人觀點, 在數據量更大的情況下, Meta 節點極有可能成為瓶頸。當然Dynamo的結構肯定也有自身的缺點, 比如如何保證元數據的一致性等問題。
Data Server主要模塊
Network Proxy: 用于接收客戶端的請求, 我們的協議是定制的protobuf 協議, Network Proxy模塊負責解析協議, 然后請求轉發到對應的節點
Meta Info: 用于存儲公共的元信息, 元信息包括每一個分片存儲在哪個節點
DB Engine: 我們底下的引擎是基于LevelDB的定制化開發, 包括支持cas, 過期時間, 多數據結構等等
數據分布策略
可以看到我們目前使用的是有主從的副本策略, 圖中的Primary 是主節點, Secondary 是從節點。為什么這么做?
首先為什么不使用ec編碼(erasure code 糾刪碼), 因為ec編碼主要用于保存偏冷數據, ec編碼遇到的問題是如果某一個副本掛掉以后, 想要恢復副本的過程必須與其他多個節點進行通信來恢復數據, 會照成大量的網絡開銷. 因此這里3副本更合適。
常見的分布式系統的多副本策略主要分成兩類:
以Cassandra, Dynamo 為主的, 沒有主從結構的設計, 讀寫的時候滿足quorum W + R >N, 因此寫入的時候寫入2個副本成功才能返回。讀的時候需要讀副本然后返回最新的。這里的最新可以是時間戳或者邏輯時間。
以MongoDB, Bada為主的,有主從結構的設計, 那么讀寫的時候, 客戶端訪問的都是主副本, 通過binlog/oplog 來將數據同步給從副本。
兩種設計都只能滿足最終一致性。那么我們再從CAP理論上看, 那么都是在哪些維度做了權衡?
從性能上來看,有主從的設計很明顯性能會由于無主從的, 因為有主從的設計只需要訪問一個副本就可以返回, 而無主從的至少兩個副本返回才可以。
從一致性來看,有主從的設計如果掛掉一個節點, 如果這個節點是主, 那么就會造成由于數據同步的不及時, 這段時間寫入的數據丟。如果掛掉的是從節點, 那么則對數據沒有任何的影響。只要這個節點在接下來的時間內能夠起來即可。無主從的設計如果掛掉一個節點, 理論上對結果是無影響的, 因為返回的時候會比較最新的結果。有主從的結構由于寫入都在一個節點, 因此不存在沖突。而無主從的結構由于寫入的是任意的兩個副本, 會存在對同一個key的修改在不同的副本, 導致客戶端讀取的時候是兩個不一致的版本, 這個時候就需要去解決沖突, 常見的方案就涉及到vector clock, 時間戳等等。不過, 總體來看無主從的設計一致性應該優于有主從的設計。
從分區容錯來看,兩邊都必須有一半以上的節點存活才能夠對外提供服務, 因為有主從的設計中必須獲得超過一半節點的投票才能成為主節點。而無主從的結構, 常見在W = 2, R = 2的情況下, 必須2個副本以上才能對外提供服務。
從可靠性來看,有主從的設計因為只訪問一個副本, 性能優于無主從的設計。而且無主從的設計中, 因為對單條數據必須有兩次讀取, 因此對系統的訪問壓力也會比無主從的來的多。當然有主從的設計容易造成主落在同一個機器上, 造成負載不均的情況, 但是這里只要將主平均到所有的機器, 就可以解決這個問題。但是有主從的設計在切換主從的時候, 必然有一段時間無法對外提供服務, 而無主從的設計則不存在這樣的問題。總體來說, 筆者認為從可靠性的角度來說, 有主從的設計應該比無主從來的可靠。
我們使用的是有主從結構的設計, 原因:
Bada主要的應用場景對性能的要求比較高, 大部分的請求需要在1ms左右的時間返回, 因此有主從的設計, 性能更滿足需求
線上服務的可靠性是我們另外一個考慮的因素
具體的分析過程可以看 http://baotiao.github.io/2015/03/Bada-design-replicaset/
數據分片策略,我們叫兩次映射.
key -> PartitionId(hash)
PartitionId -> Node(MetaData)
比如上面這張圖中我們可以看出, 我們將所有數據分成10個Partition, 然后每一個機器存有主節點和從節點. 我們會盡可能的保證每一個機器上面的主節點是一樣多的, 這樣能夠做到每一個節點的負載都是均衡的。
[page]本文作者主要向大家介紹了360自主研發的分布式存儲系統Nosql-Bada,以以作者本人在設計過程所做的一些折衷。其中,大部分的內容是在開發實現Bada過程中的一些經驗和坑, 也有很多的權衡, 希望和大家一起分享。
請求流程
當請求的數據Primary正好是當前這個節點
當請求的數據Primary 不是當前節點
多機房架構
360的機房是比較多的, 而且某些機房之間的網絡較差。業務部署一個服務的時候, 后端的DB也需要部署在多個機房上, 因此這個常常是業務的痛點。因此我們設計之初就考慮多機房的架構。
我們的多機房架構能保證
用戶不用管理多個機房, 任意一個機房數據寫入, 其他機房能夠讀取
在機房存在問題的時候, 我們可以立刻切換機房的流量
提供每一個機房之間數據的統計和Check
整體實現
這個是目前LBS業務的場景
可以看出我們這里有一個專門的隊列用于同步機房之間的數據。這個QBus 是我們團隊內部基于kafka開發的消息隊列服務。
目前主流的機房同步方法也是兩種:
節點負責機房數據的同步, 比如Cassandra, CouchBase, Riak
由外部的隊列來同步機房之間的數據, 比如 Yahoo pnuts
[page]Cassandra 做法
在寫入的時候, 每一個機房的協調者。比如這個圖里面10這個節點。會把寫入發送給其它機房的某一個節點, 這個時候Client這邊收到的只是根據配置的一致性級別就可以返回, 比如這里配置的只要1個返回即可, 那么Client寫入成功10這個節點以后,即可返回。至于與其他機房同步是10這個節點的事情, 這樣子客戶端的寫入就可以在本地寫入, 不用管多機房的latency。
這里我們可以看到是Eventual Consistency. 那么Cassandra是如何做到沖突修復的呢. 這里Cassandra 讀的時候有一個Read Repair 機制, 就是讀取的時候讀取本地多個副本. 如果副本不一致, 那么就選時間戳最新的重新寫入. 讓數據重新同步, 這里Cassandra只是說修復本地多副本數據不一致的方法, 同樣的方法我們也可以用在多個IDC里面, 可以同時跑多個任務check不同機房的數據, 然后修復他們。
CouchBase 做法
Continuous Replication提供配置的不同Server之間同步的Stream的個數,也就是不同的機房之間連接的數目是可配置的。解決沖突辦法.CouchBase提供的是最終一致性的方法,不同的版本之間首先根據修改的次數, 然后是修改時間等信息。
我們最后考慮的是使用團隊內部的QBus作為我們通信的隊列, 主要考慮
省去了自己實現隊列的麻煩
穩定運行于線上, 有專門的同事維護. 減少的很多問題
Bada 目前線上3種多機房的使用場景
單機房寫入, 任意機房讀取
跨機房寫入, 任意機房讀取
任意機房寫入, 任意機房讀取
我們的實現方案也是通過QConf來實現??蛻舳嗽L問的時候, 從QConf中讀取目前需要訪問的機房, 默認是訪問本機房, 如果需要跨機房訪問, 將QConf中的配置制定成需要訪問的機房就可以了。
多機房寫入的沖突解決方案
時間戳最新
任意機房寫入數據, 根據時間戳來進行沖突解決。
Yahoo Pnuts Primary Key
這里我們對每一個Key 有一個Primary IDC, 也就是這個Key的修改刪除等操作都只會在當前這個IDC完成, 然后讀取可以有多個IDC去讀取. 那么因為對于同一個Key的修改, 我們都在同一個IDC上. 我們通過給每一個Key加上一個Version信息, 類似Memcached的cas操作, 那么我們就可以保證做到支持單條數據的事務。如果這條數據的Primary IDC是在本機房, 那么插入操作很快。
如果這條數據的Primary IDC不是本機房, 那么就有一個Cross IDC的修改操作, 延遲將會比較高。不過我們考慮一下我們大部分的應用場景,比如微博, 90%的數據的修改應該會在同一個機房。比如一個用戶有一個profile信息, 那么和修改這個信息的基本都是這個用戶本人, 90%的情況下應該就是在同一個地點改, 當然寫入也會在同一個機房. 所以大部分的修改應該是同一個機房的修改。但是訪問可能來自各個地方,當然為了做優化, 有些數據可能在一個地方修改過了以后, 多次在其他地方修改, 那么我們就可以修改這個Key的Primary IDC到另外這個機房。
Vector Lock
Vector Lock的核心思想就是Client對這個數據的了解是遠遠超過服務端的, 因為對于服務端而言, 這個Key 對應的Value 對于Server 端只是一個字符串。而Client端能夠具體了解這個Value所代表的含義, 對這個Value進行解析。那么對于這個例子,當這兩個不一樣的Value寫入到兩個副本中的時候, Client進行一次讀取操作讀取了多個副本。
Client發現讀到的兩個副本的結果是有沖突的, 這里我們假設原始的Key的Vector Lock信息是[X:1], 那么第一次修改就是[X:1,Y:1], 另一個客戶端是基于[X:1]的Vector Lock修改的, 所以它的Vector Lock信息就應該是[X:1,Z:1]。這個時候我們只要檢查這個Vector Lock信息就可以可以發現他們沖突, 這個就是就交給客戶端去處理這個沖突.并把結果重新Update即可。
我們線上目前支持的是時間戳最新, 以及Primary Key的方案. 大部分使用的是時間戳最新來進行沖突解決。
[page]多數據結構支持
我們開發了一套基于leveldb的多數據結構的引擎。目前支持 Hash, List, Set, Zset等結構。
主要是由于用戶習慣了Redis提供的多數據結構, 能夠滿足用于快速開發業務的過程, 因此我們也提供了多數據結構的支持。
為什么不使用ZooKeeper
ZooKeeper 和 mnesia對比, ZooKeeper 是一個服務, 而 mnesia是一個庫, 因此如果使用ZooKeeper的話, 我們需要額外的維護一套服務。而 mnesia可以直接集成在代碼里面,使用更方便。
mnesia和 Erlang 集成的更好,mnesia本身就是用Erlang 來開發。
Bada 和 MongoDB對比
360的MongoDB 之前也是我們團隊在維護, 在使用MongoDB的過程中, 我們也遇到一些問題, 比如MongoDB 的擴容非常不方便, 擴容需要很長的時間, 因為MongoDB 擴容的過程是將一條一條的數據寫入的. 我們開發的時候考慮到這些問題, 因此Bada 使用的是leveldb, 當需要擴容的時候, 只要將某一個分片下面的數據文件拷貝過去即可. 前提是初始化的時候分片設置的足夠大, 我們現實默認的分片是1000以上。
MongoDB 的數據膨脹度比較大, 因為MongoDB 畢竟是文檔型數據庫, 肯定會保持一些冗余信息. 我們底下使用leveldb, leveldb 本身的壓縮功能基于snappy 壓縮. 還是做的比較好. 線上實際的磁盤空間大小相對于MongoDB 4:1
Bada 和 Cassandra 對比
Cassandra的定位和Bada是不一樣的, 我們面向的是線上頻繁訪問的熱數據, 因此我們偏向于存儲小value數據, 熱數據, 對latency 的要求會苛刻。
比如在云盤的場景, 我們存儲的就是文件的索引信息, 而Cassandra存儲的是具體的Cassandra的數據, 也因此我們線上部署Bada的機器是掛載SSD盤的。
Bada 和 Redis 對比
Bada 的性能比Redis 低, 但是目前redis cluster 還沒發展完善. 我們公司的DBA也在跟進Redis cluster之中. 所以當數據量比較大的時候, Redis可能就不適用于這么大量的數據存儲。
Bada 的多數據結構支持不如Redis來得完善. 因此我們也在逐步的支持Bada的多數據結構。
Redis 畢竟是內存型的服務. 因此假如用戶是偏向于存儲持久化數據, 可能Redis不太合適。
一些非技術的經驗
技術是為業務服務, 包括我們Bada在公司內部推廣的過程中也發現, 我們很多業務很頭疼的問題在于360的機房較多, 每一個小業務都需要維護在多個機房, 因此為了降低用戶的開發試錯成本, 我們將能標準化的事情都做了。包括我們組的定位也是專注底層技術, 加速產品團隊開發效率, 盡可能降低業務對服務端集群架構的關注。
[page]Q&A:
Q1:客戶端訪問Bada時,怎么確保數據的均衡?從qconf拿到的是一個ip列表吧?
是的。從QConf 中獲得是隨機的一個節點的ip,所以對每一個節點的訪問基本的均衡的。服務端這邊, 因為我們是有主從結構的。但是我們的主從是分片級別的主從,這點和redis cluster 不一樣。比如 Redis cluster 有Master 節點, slave節點,一般情況slave 節點不接受任何的線上訪問,但是從下面的圖中可以看到 Bada 每一個節點都有主, 從分片。 因為每一個節點的訪問基本是均衡的。
Q2:我有一個問題,對于kv存儲,選擇leveldb的動機是什么?其他leveldb分支是否考慮過?
對于存儲的考慮, 我們之前對 Rocksdb 和 leveldb 做過對比.在數據量小的情況下, leveldb 的性能和 Rocksdb 性能差不多. 數據量大的時候 Rocksdb 會有性能優勢. 因為我們之前對leveldb 做了修改. 所以后續我們會遷移過去。這里我們的讀寫都走的是 Master 節點. 只有當主節點掛掉以后, 才會訪問從節點。
這個截圖是之前對 leveldb 和 rocksdb 在數據量比較小的情況下的對比
Q3:能否說一下擴容,新增節點,以及摘除失效節點的處理?
從上面兩張圖中可以看出, 我們會將新增的節點中, 均衡的將新的主節點遷移的新節點上。目前擴容的過程是這樣 我們先把當前這個節點加入到集群。然后通過 rebalance 來進行平衡。我們一般預先分配1024 個分配。這個應該也是業內場景的做法, 之前對騰訊的CKV 也是這么做,Riak 也是這么做。
Q4:遷移是直接對leveldb復制,延時會有多少,在遷移過程中的訪問如何處理呢?
遷移是直接對 leveldb 的文件進行復制, 這個時候性能是取決于網絡的開銷。這也是我們比mongo擴容快的地方, mongo 在擴容的時候需要將數據一條一條寫。遷移之前, 我們會將當前這個節點進行切主操作, 就是將所有的主切走。那么這個時候是不會影響線上訪問,帶來的最多的影響就是這個節點的網絡有額外的開銷,但是這個節點不是面向用戶的請求的,所以影響不大。
Q5 :主切走也需要有一個時間吧?這個時間段內,如果要訪問原來主上的數據,怎么處理?
這里是這樣的一個過程, 遷移的時候比如A 節點。 那么A節點上有主分片, 那么在遷移之前,我們會先將A節點上的主讓給其他節點。這里就涉及到追Binlog 的問題,如果這個時候用戶有大量的數據寫入, 會導致Binlog 一直追不齊。確實會導致無法遷移。
Q6:關于leveldb的遷移,能否詳細介紹一下?
leveldb 的遷移很簡單,就是直接通過scp 就可以了。這個是leveldb 本身的功能,就是通過scp leveldb 對應的數據文件就可以。其實我們在binlog 這塊也做了挺多的事情, 不過太細了有機會下次講。使用binlog 來同步的副本策略之中, 常見的問題比如,分布式系統中由于主從切換導致的數據丟失,然后我們也開發了binlog merge 來減少這種問題帶來的影響。
Q7:leveldb的部分數據在內存中,這個遷移的時候怎么解決的?
這個沒有影響。因為leveldb 的memtable 的數據在磁盤上有對應的.log 文件。leveldb 啟動的時候會默認讀取.log文件, 將里面的內容加載到內存中。
Q8 : 我還是沒太明白,擴容的時候,A節點切到其他節點,是把A的meta信息做切換,然后再復制數據,最后再映射meta?
擴容的時候是這樣一個過程。先將新增的節點加入到現有的集群,不過這個節點不負責任何的分片, 因此沒有任何數據在這個節點上;然后我們遷移的過程是節點上的一個個的分片進行遷移。比如A 這個節點有 10~20 這幾個分片, 并且這個時候 10~20 這個分片是主, 那么依次我們先將A這個節點的10~20變成從, 這個時候需要修改meta信息。然后接下來是復制對應的數據文件到新節點, 復制結束以后, 修改10~20 這幾個分片到新的主上.最后修改meta 信息 ,和大部分系統比最大的不同在于 Bada 的主從是分片級別的主從, 不是節點級別的主從.這樣任何操作造成的影響都是非常小. 并且可以做到每個節點的負載盡可能的均衡。
Q9:mnesia用來存儲meta信息嗎?
mnesia 對于我們的定位就類似于ZooKeeper。有兩個用途, 一個是選主的過程提供一個全局的鎖, 一個是保存元信息。
為什么不使用ZooKeeper
ZooKeeper 和 mnesia 對比, ZooKeeper 是一個服務, 而mnesia是一個庫, 因此如果使用ZooKeeper的話, 我們需要額外的維護一套服務. 而mnesia可以直接集成在代碼里面. 使用更方便
mnesia 和 erlang 集成的更好. mnesia本身就是用Erlang 來開發
Q10:meta信息是存儲在單獨的機器上,而不是分布在存儲節點上嗎?
不是, 存儲在每一個節點上. 每一個節點都部有mnesia
Q11:既然用mnesia,那你前端機器連在一個集群?規模多大?
前端是按照業務劃分的,最大的有30幾個節點.
本文策劃陳剛, 內容由王杰編輯,國忠和四正校對與發布,其他多位志愿者對本文亦有貢獻。
本文轉載自“高可用架構(ArchNotes)”微信公眾號,特此感謝。