1. 背景
zhe800公司內大量使用redis,且用途多樣:緩存、隊列、數據庫,都有功能。同時公司內對redis的使用比較隨意,每個應用按自己的需求要求運維部署redis-server,導致對redis的使用很混亂,運維部門都無法精確得知redis-server的部署情況。經過長時間的調查、搜集資料才整理出公司內redis的使用情況,目前部署數百多個redis-server實例、版本分為:2.8.9、2.8.11、2.8.17、2.8.19、2.8.23 多種。
2. 需求的演變
鑒于公司redis使用、管理混亂的情況,公司要求改善對redis的使用和管理,由成都“基礎架構部”負責此項改進。此時產生了第一階段需求:
1、通過一個統一管理系統管理公司內所有的redis進程
2、可以通過這個管理系統監控redis的運行狀態
3、redis資源通過管理系統來分配
針對第一階段的需求,我們基礎架構部和用戶、運維部、技術委員會進行了多次討論,認為目前redis使用的問題:
沒有實現跨機房高可用,這點很重要,公司內所有應用要求必須跨機房高可用。
沒有考慮橫向擴展問題。
一部分隊列、持久化等功能不適合使用redis,應該使用更“專業”的隊列系統和數據庫解決。
此時對第一階段需求進行了修改,產生了第二階段需求:
1、redis僅作為緩存使用。
2、實現橫向擴展。
3、實現跨機房高可用,雙機房至少要實現“溫備”。
4、由一個后臺系統管理、分配、監控redis資源。
其中第3個需求:實現跨機房高可用最為困難,因為緩存的典型場景:緩沖數據庫查詢,如果主機房失效后,切換到備機房的緩存服務,此時備機房緩存沒有數據(是”冷“的),會導致對數據庫(MySQL)的查詢量爆增;因此要求備機房的緩存必須至少有主機房的部分數據,在主機房失效的情況下才能防止數據庫查詢量爆增,而且在緩存的持續使用下,備機房緩存會很快達到主機房的狀態。
因此跨機房數據同步是必須的。
3. 選型
在最終需求的指導下,已經明確我們基礎架構部需要打造一套緩存服務系統,包含redis-server和管理、監控系統。
此時我們有兩種方案可選擇:
1. 基于代理的橫向擴展方案
以codis為代表的基于代理的集群方案,通過在redis-client和redis-server之間增加一個代理層,通過代理層sharding鏈接到正確的redis-server進行操作。
2. 基于redis官方集群的方案
redis從3.0版開始提供了很受期待的集群功能,按照CAP理論,redis cluster屬于保證AP,放棄C,僅能提供最終一致性。按照redis cluster的協議,client需要在本地sharding,選擇正確的redis結點,然后進行操作,否則會返回MOVE錯誤。
調研結論是:采用redis官方集群方案
有以下原因促使我們選擇redis cluster方案:
1、基于代理的方案會導致性能下降,對于一個緩存服務來說性能很重要。
2、redis cluster方案較簡單,不需要部署額外的進程,其本身就能實現高可用了,如果是基于代理的方案,代理本身也需要高可用,這增加了復雜度。
3、redis cluster是官方支持的自帶功能,比起第三方開發的代理進程,可能更穩定可靠。
4、redis cluster能實現讀擴展、讀寫分離功能;如果使用代理,那么讀寫擴展時必須同時考慮redis-server和代理程序。
redis cluster仍然不能實現跨機房容災,跨機房高可用的功能最后決定由基礎架構部自行實現
4. redis cluster簡介
其基本思想是將數據放置于槽(slots)中,slot有0x3fff(16384)個。為了實現數據分片,這些槽分布在多個redis master結點中,為了實現高可用,每個master結點擁有零到多個slave結點,這些slave同步保存master的數據,一旦master crash以后,會選舉他的slave結點成為新的master。
僅master結點可寫入,然后數據會異步同步到slave,slave可讀不可寫。
因此理想情況下:結點永不crash、只讀寫master結點,redis cluster可保證強一致。 但因為現實使用中,結點可能crash,且可讀slave結點,因此redis cluster僅能保證最終一致性。
redis cluster通過謠言傳播同步結點的狀態,每一個結點都保存了所有結點的狀態信息,其中最重要的是:結點的ip+端口、每個節點包含的slots。
client 集群模式流程:
1、從任意一個集群的結點獲取所有結點的狀態(使用CLUSTER NODES命令),同時獲知了每一個slot所在的結點。結點有兩個角色:master/slave。
2、在get/set/hget/hset等命令時,對key進行計算:crc16(key) & 0x3fff,所得的結果就是slot編號。
3、通過slot編號獲取結點的ip+port,然后發送此結點。
4、此時如果返回-ASK ip:port錯誤,表明slot臨時發生了變化,此時應往ASK指定的結點先發送ASKING命令,再發送實際的命令。
5、此時如果返回-MOVED slot-number ip:port錯誤,表明集群發生了變化(擴容、縮減、槽遷移過),則先將命令發送到MOVED錯誤指明的結點后,重新獲取集群結點狀態,以同步最新的結點狀態。
5. 管理系統的設計與實現
zhe800基于redis cluster的緩存服務被命令為z(he800-r)edis,包含一個管理系統對所有緩存集群進行管理,以及雙機房數據同步機制,保證主備兩個機房之間部分數據同步,實現雙機房容災。
對于zedis,我們有幾個指導原則:
1、簡單:復雜的方案容易出錯,且成本高,因此在簡單和復雜之中選擇簡單的方案。
2、用戶透明:不要要求用戶了解大量的細節才能使用。
3、可用:可正常使用,可從錯誤中恢復。
5.1 管理系統的功能設計
緩存集群的操作:
容量擴容/讀寫擴容:增加1個到多個分片,同時擴展了讀寫能力。
高可用/讀擴容:僅增加slave結點數,增強高可用性,同時擴展了讀能力。
1、分配:按應用的需求分配一個全新的redis cluster,如果配置了跨機房容災選項,還會按需求在備機房啟動一個災備集群。
2、下線:將一個運行中的redis cluster刪除,并進行清理。
3、擴容:
4、縮減:擴容的逆操作,支持讀寫縮減和讀縮減。
監控:
1、物理機資源監控:監控物理機當前的資源,為緩存集群的操作提供依據。
2、集群監控:對每個節點到集群本身進行監控,提供給運維查看。
3、報警:對接公司報警系統,遇到結點crash、集群不可用等情況及時發出報警
緩存集群的操作需要以物理機資源數據為依據,這部分數據通過zabbix獲取。同時集群的每個節點需要使用cgroup進行資源隔離,這些操作都由管理后臺自動進行。
對物理機的操作:啟動redis-server進程、創建執行文件夾、生成redis.conf文件等操作通過salt進行,因為公司內每臺服務器都安裝了salt-minion,因此可直接利用。
5.2 事務支持
對redis cluster的各種操作都是通過redis client向redis-server發送命令實現,但是這些操作必須是原子操作,長時間的操作必須可中途放棄并會滾到初始狀態,否則對redis cluster的操作風險會比較高,運維人員可能不敢使用管理系統。
事務系統采取模仿數據庫的一種實現方式:記錄日志。數據庫日志分為:undo、redo、undo/redo三種形式。
我們采取undo日志的形式,對于管理系統四大操作:
分配
下線
擴容
縮減
都通過事務系統執行。
模仿數據庫,事務管理器提供三個原語:
1、begin: 開始一個事務,并返回一個事務id(tid)
2、abort-rollback: 中途放棄,會滾到begin時候的狀態
3、commit: 提交事務,標記事務已經完成。
四大操作每一個步驟都會在操作成功后,記錄undo日志到文件系統中,因此還需要確保每個“步驟”是原子操作,如果遇到abort或者錯誤時,逆向執行undo日志中命令就能恢復到初始狀態了。如果rollback仍然失敗,則說明遇到了管理系統無法處理的故障(如機器斷電、網絡中斷等)此時需要暫停會滾,通知運維人員恢復故障,再進行回滾。
6. 高可用的實現
6.1 redis cluster的高可用
redis很早就可以通過master-slave機制實現雙機熱備,到了cluster時代,仍然保留了master-slave機制。
在cluster模式中,每個master結點保存了一部分slots,同時每個master結點可設置0~多個slave結點,master的數據會異步同步到slave結點(redis的特點:所有IO操作皆異步),這樣一個master-slave的組合可稱之為一個partition;一個partition是高可用的:
slave結點crash,對集群本身無影響,對partition無影響;slave結點重新連接上master后會檢查自己與master的數據差別,如果差距太大,會先進行一次全量同步,然后開始增量同步。
master結點crash,對集群本身無影響,對partition有影響;集群會選舉一個slave作為新的master,因為slave結點幾乎有master結點的所有數據,因此數據僅有小概率丟失。
partition整體crash,集群會整體不可用,因為集群認為slots不連續了,保存在這個partition中的slots無法訪問。如果沒開啟持久化,會導致這部分數據永久丟失。
可見,redis cluster做到了一定的高可用,我們使用jedis測試結果來看,在一個1主1備的redis cluster中,隨機kill一個結點不會導致任何錯誤,但kill一個master結點和它所有的slave結點就會導致集群不可用的錯誤,此時無法get/set數據。
6.2 對于高可用的思考
對于redis cluster高可用的研究和實驗發現,其應對單機房高可用是可以勝任的,已有的客戶端:jedis新版本也能很好的處理高可用問題。
但對于跨機房高可用/容災,仍然沒有發現現成的方案可供使用,對此有以下方案被提出:
1、客戶端雙寫:客戶端需要連接到主、備兩個集群,同步寫主、異步寫備;平時讀主集群的數據,主集群不可用后馬上切換為同步寫備、讀備。
2、集群間數據同步:開啟持久化,然后同步redis的數據庫文件。
3、服務端雙寫:redis-server將set/hset等寫命令再寫一份到備集群。
最終我們選擇了第三個方案:服務端雙寫。原因為:
按照用戶透明原則,如果要求客戶端雙寫,客戶端需要做的事太多,而服務端雙寫方案客戶端僅需處理主備集群切換就可以了。
集群間數據同步方案,要求數據先寫到文件中,在同步到災備集群,這其實是不必要的,因為如果做到雙機房容災,持久化都是可以關閉的,因為partition整體crash的集群大幅度下降了(按平方下降,如果單個partition crash的幾率為p,主備parttion同時crash的幾率僅有p^2,且p隨著slave結點的數量指數級的變小),退一步說:因為本身為緩存服務,可接受少量數據丟失。
服務端雙寫足夠簡單,僅需要將客戶單對redis-server的寫入命令復制一份發送到災備集群即可。
服務端雙寫并不需要完全雙寫,僅保證災備集群擁有主集群的部分數據即可,因為對于緩存服務來說,只要發生災備時,災備集群不“冷”就可以接受了;同時這也節省了機房間光纖帶寬。
6.3 跨機房高可用
跨機房高可用的基本思想為:在主備兩個兩個機房各啟動一個redis cluster,主機房集群在寫入數據的時候,同時寫入一分到備機房。
要達到以上設計目的,必須對官方redis進行修改,我們選擇了版本3.2.0為基礎進行修改。 redis的一個重要設計思想是所有IO操作皆異步非阻塞進行,為此redis封裝了ae一個事件處理庫封裝了各個操作系統提供的事件處理模型:
OS X/Darwin/BSD:kqueue
Linux:epoll
Sun OS:evport
Others:select
在3大操作系統中并不支持Windows的IO Complete Port,原因是IOCP是真異步模型,在收到事件通知時,數據已經接收到/發送完畢,而不像epoll,kqueue等模型,在收到事件通知時,還需要再調用read/write,通知僅僅是告訴用戶態可讀/可寫了,而遠不同于IOCP,通知是通知用戶態讀/寫已經完成了。由此可見IOCP現在比較難融入redis現有的IO層中。
我們先開始研究redis的代碼,可得知它的IO層設計:
redis執行一個命令的時序:
最終我們決定在processInputBuffer中,解析redis協議時,將寫操作按配置的百分比過濾后,復制一份發送到備機房;此時有新的考慮:
備機房的接收者也是集群,如果需要發送給集群,那么勢必會增加對redis-server代碼的修改程度,因為還必須處理目標集群的高可用、sharding問題,風險增加。我們希望對redis-server的修改盡可能的小。
如果機房間光纖中斷,會丟失一部分數據;雖然作為緩存集群,數據可以丟失一小部分,但能保證數據不丟失盡量不丟。
只能寫入另一個redis cluster中,不靈活。
因此我們增加了一個名為zedis-gateway的代理程序,redis-server中復制出來的寫操作先發送到zedis-gateway,zedis-gateway負責對這些寫操作進行處理,發送到備份集群。增加zedis-gateway的好處:
zedis-gateway可靈活處理接收到的寫操作,比如:寫入備份集群、寫入MySQL數據庫中、等等。
zedis-gateway負責處理較復雜的,寫入備份集群的failover、sharding問題,簡化了對redis-server的修改。
如果機房間光纖中斷,zedis-gateway可將寫操作暫時寫入文件;等待通信恢復后在從文件中發送暫時保存的寫操作;數據可以不丟失。
首先,我們為redis增加了兩個配置項:
同時zedis-gateway也需要高可用,我們簡單實現:
每個redis-server可配置多個zedis-gateway地址,從中隨機選擇一個連接發送寫命令。
如果某個zedis-gateway地址連接不上,或連接錯誤,則再隨機選一個,直到能連上一個正確的或者一個也不能連上。
如果沒有一個zedis-gateway地址連接成功,則寫入錯誤日志;但不影響其他操作。
最后將官方版redis-server修改為:
雙機房容災zedis集群結構為:
實現結果:
我們實現了主備機房間集群數據0~100%同步,但為了節省機房間光纖帶寬,一般不允許開啟100%同步。
性能損失:在100%雙寫的配置下,性能損失<10%。
動態調整:所有關于雙寫的配置都可以在運行中隨時調整。
可維護:zedis-gateway可以隨時更新版本,而不會影響雙寫。
7. 問題與展望
目前的zedis還存在一些問題,需要我們持續已經,以達到完善。
1、redis client中,只有jedis對集群支持最好,其他語言版本,如ruby、node.js、golang版本的客戶端還需要進行一定改造才能支持redis cluster。
2、zedis-gateway目前還是單進程、單線程模型,如果未來它成為瓶頸,我們還需要增加進程數;后續根據需求也許需要將它改造為類似nginx的多進程模型。
3、修改版的redis還需要優化。
4、目前只能從主到備雙寫數據,如果從主切換到備,再從備切換到主,會丟失一部分數據;因為目前定位為緩存集群,因此可接受這個損失,如果今后需要升級作為內存數據庫使用,我們就還需要處理這個問題。
5、只有部分命令支持雙寫,剩余的還在添加中。