阿裡雲某客戶發現自己使用讀寫分離執行個體,master的cpu特别高,而讀寫分離中承擔讀流量的slave節點卻相對空閑。使用者CPU打滿後,通路到主節點的的線上服務受到了較大影響。關于阿裡雲RDS請參考 雲資料庫RDS詳情連結
。
Redis讀寫分離執行個體的原理是:key統一寫入到master,然後通過主從複制同步到slave,使用者的請求通過proxy做判斷,如果是寫請求,轉發到master;如果是讀請求,分散轉發到slave,這種架構适合讀請求數量遠大于寫請求數量的業務,讀寫分離架構示意圖如下所示。

阿裡雲Redis讀寫分離版讀寫指令轉發示例
bitfield指令
經過和客戶溝通檢視後,客戶使用了大量的bitfield做讀取,首先介紹一下這個指令的用法和場景,bitfield 是針對bitmap資料類型操作的指令,bitmap通常被用來在極小空間消耗下通過位的運算(AND/OR/XOR/NOT)實作對狀态的判斷,常見的使用場景例如:
-
通過bitmap來記錄使用者每天應用登入狀态,即如果$ID使用者登入,就SETBIT logins:20200404 $ID 1,表示使用者$ID在20200404這一天登入了,通過BITCOUNT logins:20200404可以得到這一天所有登入過的使用者數量;通過對兩天的記錄求AND,可以判斷哪個使用者連續兩天登入了,即BITOP AND logins:20200404-05 logins:20200404 logins:20200404。
判斷使用者是否閱讀了共同的文章,觀看了共同的視訊等。
前一陣子,答題領獎活動非常火爆,“答對12道題的同學有機會瓜分獎池”,這種如果使用bitmap來實作,就非常容易判斷出使用者是否全部答對。
一個使用Redis BITMAP設計的答題遊戲系統
答題系統設計如:
-
每個使用者每輪答題,設定一個key,比如user1在第一輪答題的key是 round:1:user1
每答對一道題,設定相關的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,答錯可以不設定
計算使用者總共答對了幾道題,就可以使用 BITCOUNT 指令統計1的bit個數。如user1答對了3道題,user2在第一輪全部答對,那麼user2就有機會參與答題(第1輪)的後續玩法
可見,Redis的bitmap接口可以用非常高的存儲效率和計算加速效果。回到bitfiled指令,它的文法如下所示:
BITFIELD key
[GET type offset] // 擷取指定位的值
[SET type offset value] // 設定指定位的值
[INCRBY type offset increment] // 增加指定位的值
[OVERFLOW WRAP|SAT|FAIL] // 控制INCR的界限
讀寫分離執行個體處理bitfield的問題
從上文可知,bitfield的子指令中,GET指令是讀屬性,SET/INCRBY指令為寫屬性,是以Redis将其歸類為寫屬性,進而隻能被轉發到master執行個體,如下圖所示為bitfield的路由情況。
這就是為什麼客戶使用了讀寫分離版,而隻有master節點cpu使用高,其餘slave節點卻沒有收到這個指令的打散的原因。
解決方案
-
方案一:改造Redis核心,将bitfield指令屬性标記為讀屬性,但是當其包含SET/INCRBY等寫屬性的子指令時候,仍舊将其同步到slave等。此方案優點是外部元件(proxy和用戶端)不需要做修改,缺點是需要對bitfiled指令做特殊處理,破壞引擎指令統一處理的一緻性。
方案二:增加bitfield_ro指令,類似于georadius_ro指令,用來隻支援get選項,進而作為讀屬性,這樣就避免了slave無法讀取的問題。此方案優點是方案清晰可靠,缺點是需要proxy和用戶端做适配才能使用。
經過讨論,最終采取了方案二,因為這個方案更優雅,也更标準化。
添加bitfield_ro
{"bitfield_ro",bitfieldroCommand,-2,
"read-only fast @bitmap",
0,NULL,1,1,1,0,0,0},
完成之後,下圖是在slave上執行bitfield_ro指令,可以看到被正确執行。
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
Proxy轉發
為了保持使用者不做代碼修改,我們在proxy上對bitfiled指令做了相容,即如果使用者的bitfield指令隻有get選項,proxy會将此指令轉換為bitfield_ro分散轉發到後端多個節點上,進而實作加速,使用者不用做任何改造即可完成加速,如下圖所示。
添加BITFIELD_RO指令後處理BITFIELD邏輯流程
貢獻社群
我們将自己的修改回饋給了社群,并且被Redis官方接受
值得一提的是,阿裡雲在國内是最大的Redis社群contributer,如在新釋出的Redis-6.0rc中,阿裡雲的貢獻排第三,僅次于作者和Redis vendor(Redis Labs)。阿裡雲仍舊在不斷的回饋和貢獻社群。
阿裡雲Redis通過增加bitfield_ro指令,解決了官方bitfield get指令無法在slave上加速執行的問題。
除過bitfield指令,阿裡雲Redis也同時對georadius指令做了相容轉換,即在讀寫分離執行個體上,如果georadius/georadiusbymember指令沒有store/storedist選項,将會被自動判斷為讀指令轉發到slave加速執行。
我們思考讀寫分離版的場景,為什麼使用者需要讀寫分離呢?為什麼不是用叢集版呢?我們做一下簡單對比,比如設定社群版的服務能力為K,那麼表的對比如下(我們隻添加了增強版Tair的主備做對比,叢集版可以直接乘以分片數):
方式 Redis社群版叢集 Redis社群版讀寫分離 Redis(Tair增強版)主備
寫(key均勻情況) K分片數 K K3
讀(key均勻情況) K分片數 K隻讀節點數 K*3
寫(單key或熱key) K(最壞情況) K K*3
讀(單key或熱key) K(最壞情況) K隻讀節點數 K3
表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購物節秒殺的關鍵
原文連結:
https://www.9i0i.com/article-95490-1.html