今天主要向大家介紹一下pika
pika 是360 DBA和基礎(chǔ)架構(gòu)組聯(lián)合開發(fā)的類redis 存儲系統(tǒng), 完全支持Redis協(xié)議,用戶不需要修改任何代碼, 就可以將服務(wù)遷移至pika. 有維護redis 經(jīng)驗的DBA 維護pika 不需要學(xué)習(xí)成本
pika 主要解決的是用戶使用redis的內(nèi)存大小超過50G, 80G 等等這樣的情況, 會遇到比如啟動恢復(fù)時間長, 一主多從代價大, 硬件成本貴, 緩沖區(qū)容易寫滿等等問題. pika 就下針對這些場景的一個解決方案
pika 目前已經(jīng)開源, github地址:
https://github.com/Qihoo360/pika
重點:
本次分享分成4個部分
redis 提供了豐富的多數(shù)據(jù)結(jié)構(gòu)的接口, 在redis 之前, 比如memcache 都認(rèn)為后端只需要存儲kv的結(jié)構(gòu)就可以, 不需要感知這個value 里面的內(nèi)容. 用戶需要使用的話通過json_encode, json_decode 等形式進行數(shù)據(jù)的讀取就行. 但是其實redis 類似做了一個微創(chuàng)新, redis 提供了多數(shù)據(jù)結(jié)構(gòu)的支持, 讓前端寫代碼起來更加的方便了
因此redis 在公司的使用率也是越來越廣泛, 用戶不知不覺把越來越多的數(shù)據(jù)存儲在redis中, 隨著用戶的使用, DBA 發(fā)現(xiàn)有些redis 實例的大小也是越來越大. 在redis 實例內(nèi)存使用比較大的情況下, 遇到的問題也會越來越多, 因此DBA和我們一起實現(xiàn)了大容量redis 的解決方案
最近半年公司每天redis 的訪問情況
redis 架構(gòu)方案
我們線上的redis 一般同時開啟rdb 和 aof. 我們知道aof的作用是實時的記錄用戶的寫入操作, rdb 是redis 某一時刻數(shù)據(jù)的完整快照. 那么恢復(fù)的時候一般是通過 rdb + aof 的方式進行恢復(fù), 根據(jù)我們線上的情況 50G redis 恢復(fù)時間需要差不多70分鐘
redis 在主庫掛掉以后, 從庫升級為新的主庫. 那么切換主庫以后, 所有的從庫都需要跟新主做一次全同步, 全量同步一次大容量的redis, 代價非常大.
為了防止同步緩沖區(qū)被復(fù)寫,dba給redis設(shè)置了2G的巨大同步緩沖區(qū),這對于內(nèi)存資源來講代價很大. 當(dāng)由于機房之間網(wǎng)絡(luò)有故障, 主從同步出現(xiàn)延遲了大于2G以后, 就會觸發(fā)全同步的過程. 如果多個從庫同時觸發(fā)全同步的過程, 那么很容易就將主庫給拖死
我們一般線上使用的redis 機器是 64G, 96G. 我們只會使用80% 的空間.
如果一個redis 的實例是50G, 那么基本一臺機器只能運行一個redis 實例. 因此特別的浪費資源
總結(jié): 可以看到在redis 比較小的情況下, 這些問題都不是問題, 但是當(dāng)redis 容量上去以后. 很多操作需要的時間也就越來越長了
主要組成:
pika 基于pink 對線程進行封裝. 使用多個工作線程來進行讀寫操作,由底層nemo引擎來保證線程安全,線程分為11種:
PikaServer:主線程
DispatchThread:監(jiān)聽端口1個端口,接收用戶連接請求
ClientWorker:存在多個(用戶配置),每個線程里有若干個用戶客戶端的連接,負(fù)責(zé)接收處理用戶命令并返回結(jié)果,每個線程執(zhí)行寫命令后,追加到binlog中
Trysync:嘗試與master建立首次連接,并在以后出現(xiàn)故障后發(fā)起重連
ReplicaSender:存在多個(動態(tài)創(chuàng)建銷毀,本master節(jié)點掛多少個slave節(jié)點就有多少個),每個線程根據(jù)slave節(jié)點發(fā)來的同步偏移量,從binlog指定的偏移開始實時同步命令給slave節(jié)點
ReplicaReceiver:存在1個(動態(tài)創(chuàng)建銷毀,一個slave節(jié)點同時只能有一個master),將用戶指定或當(dāng)前的偏移量發(fā)送給master節(jié)點并開始接收執(zhí)行master實時發(fā)來的同步命令,在本地使用和master完全一致的偏移量來追加binlog
SlavePing:slave用來向master發(fā)送心跳進行存活檢測
HeartBeat:master用來接收所有slave發(fā)送來的心跳并恢復(fù)進行存活檢測
bgsave:后臺dump線程
scan:后臺掃描keyspace線程
purge:后臺刪除binlog線程
我們知道redis 是需要支持多數(shù)據(jù)結(jié)構(gòu)的, 而rocksdb 只是一個kv的接口, 那么我們?nèi)绾螌崿F(xiàn)的呢?
比如對于Hash 數(shù)據(jù)結(jié)構(gòu):
對于每一個Hash存儲,它包括hash鍵(key),hash鍵下的域名(field)和存儲的值 (value).
nemo的存儲方式是將key和field組合成為一個新的key,將這個新生成的key與所要存儲的value組成最終落盤的kv鍵值對。同時,對于每一個hash鍵,nemo還為它添加了一個存儲元信息的落盤kv,它保存的是對應(yīng)hash鍵下的所有域值對的個數(shù)。
每個hash鍵、field、value到落盤kv的映射轉(zhuǎn)換
每個hash鍵的元信息的落盤kv的存儲格式
比如對于List 數(shù)據(jù)結(jié)構(gòu):
顧名思義,每個List結(jié)構(gòu)的底層存儲也是采用鏈表結(jié)構(gòu)來完成的。對于每個List鍵,它的每個元素都落盤為一個kv鍵值對,作為一個鏈表的一個節(jié)點,稱為元素節(jié)點。和hash一樣,每個List鍵也擁有自己的元信息。
每個元素節(jié)點對應(yīng)的落盤kv存儲格式
每個元信息的落盤kv的存儲格式
其他的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的方式也類似, 通過將hash_filed 拼成一個key, 存儲到支持kv的rocksdb 里面去. 從而實現(xiàn)多數(shù)據(jù)結(jié)構(gòu)的結(jié)構(gòu)
Pika的主從同步是使用Binlog來完成的. binlog 本質(zhì)是順序?qū)懳募? 通過Index + offset 進行同步點檢查.
解決了同步緩沖區(qū)太小的問題
支持全同步 + 增量同步
master 執(zhí)行完一條寫命令就將命令追加到Binlog中,ReplicaSender將這條命令從Binlog中讀出來發(fā)送給slave,slave的ReplicaReceiver收到該命令,執(zhí)行,并追加到自己的Binlog中.
當(dāng)發(fā)生主從切換以后, slave僅需要將自己當(dāng)前的Binlog Index + offset 發(fā)送給master,master找到后從該偏移量開始同步后續(xù)命令
為了防止讀文件中寫錯一個字節(jié)則導(dǎo)致整個文件不可用,所以pika采用了類似leveldb log的格式來進行存儲,具體如下:
pika 線上架構(gòu)
旁白: 為了減少用戶的學(xué)習(xí)成本, 目前pika 的主從同步功能是和redis完全一樣, 只需要slaveof 就可以實現(xiàn)主從關(guān)系的建立, 使用起來非常方便
背景
pika支持master/slave的復(fù)制方式,通過slave端的slaveof命令激發(fā) salve端處理slaveof命令,將當(dāng)前狀態(tài)變?yōu)閟lave,改變連接狀態(tài) slave的trysync線程向master發(fā)起trysync,同時將要同步點傳給master master處理trysync命令,發(fā)起對slave的同步過程,從同步點開始順序發(fā)送binlog或進行全同步
pika同步依賴binlog binlog文件會自動或手動刪除 當(dāng)同步點對應(yīng)的binlog文件不存在時,需要通過全同步進行數(shù)據(jù)同步
全同步
上圖1是一個主從同步的一個過程(即根據(jù)主節(jié)點數(shù)據(jù)庫的操作日志,將主節(jié)點數(shù)據(jù)庫的改變過程順序的映射到從節(jié)點的數(shù)據(jù)庫上),從圖1中可以看出,每一個從節(jié)點在主節(jié)點下都有一個唯一對應(yīng)的BinlogSenderThread。
(為了說明方便,我們定一個“同步命令”的概念,即會改變數(shù)據(jù)庫的命令,如set,hset,lpush等,而get,hget,lindex則不是)
主要模塊的功能:
WorkerThread:接受和處理用戶的命令;
BinlogSenderThread:負(fù)責(zé)順序地向?qū)?yīng)的從節(jié)點發(fā)送在需要同步的命令;
BinlogReceiverModule: 負(fù)責(zé)接受主節(jié)點發(fā)送過來的同步命令
Binglog:用于順序的記錄需要同步的命令 ?主要的工作過程: 1.當(dāng)WorkerThread接收到客戶端的命令,按照執(zhí)行順序,添加到Binlog里;
2.BinglogSenderThread判斷它所負(fù)責(zé)的從節(jié)點在主節(jié)點的Binlog里是否有需要同步的命令,若有則發(fā)送給從節(jié)點;
3.BinglogReceiverModule模塊則做以下三件事情: a. 接收主節(jié)點的BinlogSenderThread發(fā)送過來的同步命令; b. 把接收到的命令應(yīng)用到本地的數(shù)據(jù)上; c. 把接收到的命令添加到本地Binlog里
至此,一條命令從主節(jié)點到從節(jié)點的同步過程完成
BinLogReceiverModule的工作過程:
上圖2是BinLogReceiverModule(在源代碼中沒有這個對象,這里是為了說明方便,抽象出來的)的組成,從圖2中可以看出BinlogReceiverModule由一個BinlogReceiverThread和多個BinlogBGWorker組成。
BinlogReceiverThread: 負(fù)責(zé)接受由主節(jié)點傳送過來的命令,并分發(fā)給各個BinlogBGWorker,若當(dāng)前的節(jié)點是只讀狀態(tài)(不能接受客戶端的同步命令),則在這個階段寫B(tài)inlog
BinlogBGWorker:負(fù)責(zé)執(zhí)行同步命令;若該節(jié)點不是只讀狀態(tài)(還能接受客戶端的同步命令),則在這個階段寫B(tài)inlog(在命令執(zhí)行之前寫)
BinlogReceiverThread接收到一個同步命令后,它會給這個命令賦予一個唯一的序列號(這個序列號是遞增的),并把它分發(fā)給一個BinlogBGWorker;而各個BinlogBGWorker則會根據(jù)各個命令的所對應(yīng)的序列號的順序來執(zhí)行各個命令,這樣也就保證了命令執(zhí)行的順序和主節(jié)點執(zhí)行的順序一致了
之所以這么設(shè)計主要原因是: a. 配備多個BinlogBGWorker是可以提高主從同步的效率,減少主從同步的滯后延遲; b. 讓BinlogBGWorker在執(zhí)行執(zhí)行之前寫B(tài)inlog可以提高命令執(zhí)行的并行度; c. 在當(dāng)前節(jié)點是非只讀狀態(tài),讓BinglogReceiverThread來寫B(tài)inlog,是為了讓Binglog里保存的命令順序和命令的執(zhí)行順序保持一致;
不同于Redis,Pika的數(shù)據(jù)主要存儲在磁盤中,這就使得其在做數(shù)據(jù)備份時有天然的優(yōu)勢,可以直接通過文件拷貝實現(xiàn)
流程: 打快照:阻寫,并在這個過程中或的快照內(nèi)容 異步線程拷貝文件:通過修改Rocksdb提供的BackupEngine拷貝快照中文件,這個過程中會阻止文件的刪除
快照內(nèi)容
當(dāng)前db的所有文件名 manifest文件大小 sequence_number 同步點: binlog index + offset
在我們大量的使用場景中. 對于Hash, zset, set, list這幾種多數(shù)據(jù)機構(gòu),當(dāng)member或者field很多的時候,用戶有批量刪除某一個key的需求, 那么這個時候?qū)嶋H刪除的就是rocksdb 底下大量的kv結(jié)構(gòu), 如果只是單純暴力的進行刪key操作, 那時間肯定非常的慢, 難以接受. 那我們?nèi)绾慰焖賱h除key?
剛才的nemo 的實現(xiàn)里面我們可以看到, 我們在value 里面增加了version, ttl 字段, 這兩個字段就是做這個事情.
Solution 0:暴力刪除每一個member,時間復(fù)雜度O(m) , m是member的個數(shù);
優(yōu)點:易實現(xiàn);
缺點:同步處理,會阻礙請求;
Solution 1: 啟動后臺線程,維護刪除隊列,執(zhí)行刪除,時間復(fù)雜度O(m)
優(yōu)點:不會明顯阻住server;
缺點:仍然要O(m)去刪除members,可以優(yōu)化刪除的速度;
Redis 是怎么做的?
舊版本的Del接口,在實際free大量內(nèi)存的時候仍然會阻塞server;
新版增加了lazy free,根據(jù)當(dāng)前server的負(fù)載,多次動態(tài)free;
Solution 2: 不刪除, 只做標(biāo)記, 時間復(fù)雜度O(1)
優(yōu)點:效率就夠了;
缺點:需要改動下層rocksdb,一定程度破壞了rocksdb的封裝,各個模塊之間耦合起來;
方案:
Key的元信息增加版本,表示當(dāng)前key的有效版本;
操作:
Put:查詢key的最新版本,后綴到val;
Get:查詢key的最新版本,過濾最新的數(shù)據(jù);
Iterator: 迭代時,查詢key的版本,過濾舊版本數(shù)據(jù);
Compact:數(shù)據(jù)的實際刪除是在Compact過程中,根據(jù)版本信息過濾;
目前nemo 采用的就是第二種, 通過對rocksdb 的修改, 可以實現(xiàn)秒刪的功能, 后續(xù)通過修改rocksdb compact的實現(xiàn), 在compact 的過程中, 將歷史的數(shù)據(jù)淘汰掉
rocksdb 的compact 策略是在寫放大, 讀放大, 空間放大的權(quán)衡.
那么我們DBA經(jīng)常會存在需求盡可能減少空間的使用, 因此DBA希望能夠隨時觸發(fā)手動compact, 而又盡可能的不影響線上的使用, 而rocksdb 默認(rèn)的手動compact 策略是最高優(yōu)先級的, 會阻塞線上的正常流程的合并.
rocksdb 默認(rèn)的 manual compact 的限制
a) 當(dāng)manual compact執(zhí)行時,會等待所有的自動compact任務(wù)結(jié)束, 然后才會執(zhí)行本次manual compact; b) manual執(zhí)行期間,自動compact無法執(zhí)行
為了避免這種情況,我們對compact的策略進行調(diào)整,使得自動compact一直優(yōu)先執(zhí)行,避免停寫;
總結(jié):
pika相對于redis,最大的不同就是pika是持久化存儲,數(shù)據(jù)存在磁盤上,而redis是內(nèi)存存儲,由此不同也給pika帶來了相對于redis的優(yōu)勢和劣勢
優(yōu)勢:
劣勢:
由于Pika是基于內(nèi)存和文件來存放數(shù)據(jù), 所以性能肯定比Redis低一些, 但是我們一般使用SSD盤來存放數(shù)據(jù), 盡可能跟上Redis的性能。
總結(jié):
從以上的對比可以看出, 如果你的業(yè)務(wù)場景的數(shù)據(jù)比較大,Redis 很難支撐, 比如大于50G,或者你的數(shù)據(jù)很重要,不允許斷電丟失,那么使用Pika 就可以解決你的問題。
而在實際使用中,大多數(shù)場景下pika的性能大約是Redis的50%~80%,在某些特定場景下,例如range 500,pika的性能只有redis的20%,針對這些場景我們?nèi)匀辉诟倪M
在360內(nèi)部使用情況:
粗略的統(tǒng)計如下:
當(dāng)前每天承載的總請求量超過100億, 實例數(shù)超過100個
當(dāng)前承載的數(shù)據(jù)總量約3 TB
Server Info: CPU: 24 Cores, Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz MEM: 165157944 kB OS: CentOS release 6.2 (Final) NETWORK CARD: Intel Corporation I350 Gigabit Network Connection
測試過程, 在pika 中先寫入150G 大小的數(shù)據(jù). 寫入Hash key 50個, field 1千萬級別. redis 寫入5G 大小的數(shù)據(jù) ?Pika: 18個線程
redis: 單線程
結(jié)論: pika 的單線程的性能肯定不如redis, pika是多線程的結(jié)構(gòu), 因此在線程數(shù)比較多的情況下, 某些數(shù)據(jù)結(jié)構(gòu)的性能可以優(yōu)于redis
github 地址:
https://github.com/Qihoo360/pika
github wiki:
https://github.com/Qihoo360/pika/wiki/pika介紹
開發(fā)需要做的:
開發(fā)不需要做任何事,不用改代碼、不用替換driver(pika使用原生redis的driver),什么都不用動,看dba干活就好
dba需要做的: 1.dba遷移redis數(shù)據(jù)到pika 2.dba將redis的數(shù)據(jù)實時同步到pika,確保redis與pika的數(shù)據(jù)始終一致 3.dba切換lvs后端ip,由pika替換redis
遷移過程中需要停業(yè)務(wù)/業(yè)務(wù)會受到影響嗎: 然而并不會
遷移是無縫且溫和的嗎: 那當(dāng)然 ?
我們之前在bada 上面支持過redis 的多數(shù)據(jù)結(jié)構(gòu), 并且兼容redis協(xié)議, 但是遇到了問題.
在分布式系統(tǒng)里面, 對key 的hash 場景的通常是兩種方案:
我們bada 目前支持的是取模的hash 方案, 在實現(xiàn)redis 的多數(shù)據(jù)結(jié)構(gòu)的時候, 比如hash 我們采用key取模找到對應(yīng)的分片. 那么這樣帶來的問題是由于多數(shù)據(jù)結(jié)構(gòu)里面key 不多, field 比較多的場景還是大部分的情況, 因此極容易照成分片的不均勻, 性能退化很明顯.
更多建議: