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