跬步千里 —— 阿里雲Redis bitfield命令加速記

1. 問題

阿里雲某客戶發現本身使用讀寫分離實例,master的cpu特別高,而讀寫分離中承擔讀流量的slave節點卻相對空閒。用戶CPU打滿後,訪問到主節點的的線上服務受到了較大影響。git

1.1 讀寫分離原理

Redis讀寫分離實例的原理是:key統一寫入到master,而後經過主從複製同步到slave,用戶的請求經過proxy作判斷,若是是寫請求,轉發到master;若是是讀請求,分散轉發到slave,這種架構適合讀請求數量遠大於寫請求數量的業務,讀寫分離架構示意圖以下所示。
圖1. 阿里雲Redis讀寫分離版讀寫命令轉發示例
圖1. 阿里雲Redis讀寫分離版讀寫命令轉發示例github

1.2 bitfield命令

通過和客戶溝通查看後,客戶使用了大量的bitfield作讀取,首先介紹一下這個命令的用法和場景,bitfield 是針對bitmap數據類型操做的命令,bitmap一般被用來在極小空間消耗下經過位的運算(AND/OR/XOR/NOT)實現對狀態的判斷,常見的使用場景例如:redis

  • 經過bitmap來記錄用戶天天應用登陸狀態,即若是$ID用戶登陸,就SETBIT logins:20200404 $ID 1,表示用戶$ID在20200404這一天登陸了,經過BITCOUNT logins:20200404能夠獲得這一天全部登陸過的用戶數量;經過對兩天的記錄求AND,能夠判斷哪一個用戶連續兩天登陸了,即BITOP AND logins:20200404-05 logins:20200404 logins:20200404。
  • 判斷用戶是否閱讀了共同的文章,觀看了共同的視頻等。
  • 前一陣子,答題領獎活動很是火爆,「答對12道題的同窗有機會瓜分獎池」,這種若是使用bitmap來實現,就很是容易判斷出用戶是否所有答對。

image.png
圖2. 一個使用Redis BITMAP設計的答題遊戲系統
答題系統設計如:數據庫

  1. 每一個用戶每輪答題,設置一個key,好比user1在第一輪答題的key是round:1:user1
  2. 每答對一道題,設置相關的bit爲1,好比user1答對了第5題,那麼就設置第5個bit爲1就能夠了,如:SETBIT round:1:user1 5 1;若是用戶1在第一輪答對了第9題,那麼就把第9個bit設置爲1,SETBIT round:1:user1 9 1;值得注意的是,bitfield默認bit都是0,答錯能夠不設置
  3. 計算用戶總共答對了幾道題,就可使用BITCOUNT命令統計1的bit個數。如user1答對了3道題,user2在第一輪所有答對,那麼user2就有機會參與答題(第1輪)的後續玩法

可見,Redis的bitmap接口能夠用很是高的存儲效率和計算加速效果。回到bitfiled命令,它的語法以下所示:segmentfault

BITFIELD key 
[GET type offset] // 獲取指定位的值
[SET type offset value] // 設置指定位的值
[INCRBY type offset increment] // 增長指定位的值
[OVERFLOW WRAP|SAT|FAIL] // 控制INCR的界限

1.3 讀寫分離實例處理bitfield的問題

從上文可知,bitfield的子命令中,GET命令是讀屬性,SET/INCRBY命令爲寫屬性,所以Redis將其歸類爲寫屬性,從而只能被轉發到master實例,以下圖所示爲bitfield的路由狀況。
image.png
這就是爲何客戶使用了讀寫分離版,而只有master節點cpu使用高,其他slave節點卻沒有收到這個命令的打散的緣由。後端

2. 思路和處理

2.1 解決方案

• 方案一:改造Redis內核,將bitfield命令屬性標記爲讀屬性,可是當其包含SET/INCRBY等寫屬性的子命令時候,仍舊將其同步到slave等。此方案優勢是外部組件(proxy和客戶端)不須要作修改,缺點是須要對bitfiled命令作特殊處理,破壞引擎命令統一處理的一致性。架構

• 方案二:增長bitfield_ro命令,相似於georadius_ro命令,用來只支持get選項,從而做爲讀屬性,這樣就避免了slave沒法讀取的問題。此方案優勢是方案清晰可靠,缺點是須要proxy和客戶端作適配才能使用。性能

通過討論,最終採起了方案二,由於這個方案更優雅,也更標準化。阿里雲

2.2 添加bitfield_ro

{"bitfield_ro",bitfieldroCommand,-2,
"read-only fast @bitmap",
0,NULL,1,1,1,0,0,0},

完成以後,下圖是在slave上執行bitfield_ro命令,能夠看到被正確執行。url

tair-redis > SLAVEOF 127.0.0.1 6379
OK
tair-redis > set k v
(error) READONLY You can't write against a read only replica.
tair-redis > BITFIELD mykey GET u4 0
(error) READONLY You can't write against a read only replica.
tair-redis > BITFIELD_RO mykey GET u4 0
1) (integer) 0

2.3 Proxy轉發

爲了保持用戶不作代碼修改,咱們在proxy上對bitfiled命令作了兼容,即若是用戶的bitfield命令只有get選項,proxy會將此命令轉換爲bitfield_ro分散轉發到後端多個節點上,從而實現加速,用戶不用作任何改造便可完成加速,以下圖所示。
image.png
圖4. 添加BITFIELD_RO命令後處理BITFIELD邏輯流程
2.4 貢獻社區
咱們將本身的修改回饋給了社區,而且被Redis官方接受(https://github.com/antirez/redis/pull/6951
image.png
值得一提的是,阿里雲在國內是最大的Redis社區contributer,如在新發布的Redis-6.0rc中,阿里雲的貢獻排第三,僅次於做者和Redis vendor(Redis Labs)。阿里雲仍舊在不斷的回饋和貢獻社區。
image.png
圖5. Redis6.0 RC commit數目榜

3. 引伸和討論

3.1 總結

阿里雲Redis經過增長bitfield_ro命令,解決了官方bitfield get命令沒法在slave上加速執行的問題。

除過bitfield命令,阿里雲Redis也同時對georadius命令作了兼容轉換,即在讀寫分離實例上,若是georadius/georadiusbymember命令沒有store/storedist選項,將會被自動判斷爲讀命令轉發到slave加速執行。
3.2 思考
咱們思考讀寫分離版的場景,爲何用戶須要讀寫分離呢?爲何不是用集羣版呢?咱們作一下簡單對比,好比設置社區版的服務能力爲K,那麼表的對好比下(咱們只添加了加強版Tair的主備作對比,集羣版能夠直接乘以分片數):

方式

Redis社區版集羣

Redis社區版讀寫分離

Redis(Tair加強版)主備

寫(key均勻狀況)

K*分片數

K

K*3

讀(key均勻狀況)

K*分片數

K*只讀節點數

K*3

寫(單key或熱key)

K(最壞狀況)

K

K*3

讀(單key或熱key)

K(最壞狀況)

K*只讀節點數

K*3

表1. Redis社區版(集羣/讀寫分離)和加強版(主備)簡單場景對比

可見,其實讀寫分離版屬於對單個key和熱key的讀能力的擴展的一種方法,比較適合中小用戶有大key的狀況,它沒法解決用戶的突發寫的瓶頸,好比在這個場景下,若是用戶的bitfield命令是寫請求(子命令中帶有INCRBY和SET),就會遇到沒法解決的性能問題。

從表的對比看,這種狀況下,用戶若是能把key拆散,或者把大key拆成不少小key,就可使用集羣版得到良好的線性加速能力。大key帶來的問題包含但不只限於:

  • 大key會形成數據傾斜,使得Redis的容量和服務能力不能線性擴展
  • 大key意味着大機率這個key是熱點
  • 一旦不當心針對大key有range類的操做,會出現慢查詢,還容易打爆帶寬

這也是Tair加強版在阿里集團內各個應用建議的:「避免設計出大key和慢查,能避免90%以上的Redis問題」。

可是在實際使用中,用戶仍舊不可避免的遇到熱點問題,好比搶購,好比熱劇,好比超大型直播間等;尤爲是不少熱點具有「突發性」的特色,事先並不知曉,衝擊隨時可達。Redis加強版的性能加強實例具有單key在O(1)操做40~45w ops的服務能力和極強的抗衝擊能力,單機主備版就足夠應對一場中大型的秒殺活動!同時若是用戶沒有大key,加強性能集羣版可以近乎賦予用戶千萬甚至幾千萬OPS的服務能力,這也是Tair做爲阿里重器,支持每次平穩渡過雙11購物節秒殺的關鍵,歡迎你們試用!

最後,打一個小廣告~若是對KV存儲系統,圖數據庫有興趣的小夥伴,歡迎加入咱們團隊,簡歷發送至:zongdai at taobao dot com

相關文章
相關標籤/搜索