Codis做者黃東旭細說分佈式Redis架構設計和踩過的那些坑們

本次分享的內容主要包括五個大部分:

  • Redis、RedisCluster和Codis; node

  • 咱們更愛一致性; mysql

  • Codis在生產環境中的使用的經驗和坑們; git

  • 對於分佈式數據庫和分佈式架構的一些見解; github

  • Q & A環節。 web

  Codis是一個分佈式Redis解決方案,與官方的純P2P的模式不一樣,Codis採用的是Proxy-based的方案。今天咱們介紹一下Codis及下一個大版本RebornDB的設計,同時會介紹一些Codis在實際應用場景中的tips。最後拋磚引玉,會介紹一下我對分佈式存儲的一些觀點和見解,望各位首席們雅正。 算法

1、 Redis,RedisCluster和Codis


  Redis:想必你們的架構中,Redis已是一個必不可少的部件,豐富的數據結構和超高的性能以及簡單的協議,讓Redis可以很好的做爲數據庫的上游緩存層。可是咱們會比較擔憂Redis的單點問題,單點Redis容量大小總受限於內存,在業務對性能要求比較高的狀況下,理想狀況下咱們但願全部的數據都能在內存裏面,不要打到數據庫上,因此很天然的就會尋求其餘方案。 好比,SSD將內存換成了磁盤,以換取更大的容量。更天然的想法是將Redis變成一個能夠水平擴展的分佈式緩存服務,在Codis以前,業界只有Twemproxy,可是Twemproxy自己是一個靜態的分佈式Redis方案,進行擴容/縮容時候對運維要求很是高,並且很難作到平滑的擴縮容。Codis的目標其實就是儘可能兼容Twemproxy的基礎上,加上數據遷移的功能以實現擴容和縮容,最終替換Twemproxy。從豌豆莢最後上線的結果來看,最後徹底替換了Twem,大概2T左右的內存集羣。 sql

  Redis Cluster :與Codis同期發佈正式版的官方cluster,我認爲有優勢也有缺點,做爲架構師,我並不會在生產環境中使用,緣由有兩個: docker

  • cluster的數據存儲模塊和分佈式的邏輯模塊是耦合在一塊兒的,這個帶來的好處是部署異常簡單,all-in-the-box,沒有像Codis那麼多概念,組件和依賴。可是帶來的缺點是,你很難對業務進行無痛的升級。好比哪天Redis cluster的分佈式邏輯出現了比較嚴重的bug,你該如何升級?除了滾動重啓整個集羣,沒什麼好辦法。這個比較傷運維。 數據庫

  • 對協議進行了較大的修改,對客戶端不太友好,目前不少客戶端已經成爲事實標準,並且不少程序已經寫好了,讓業務方去更換Redisclient,是不太現實的,並且目前很難說有哪一個Rediscluster客戶端通過了大規模生產環境的驗證,從HunanTV開源的Rediscluster proxy上能夠看得出這個影響仍是蠻大的,不然就會支持使用cluster的client了。 api

  Codis:和Redis cluster不一樣的是,Codis採用一層無狀態的proxy層,將分佈式邏輯寫在proxy上,底層的存儲引擎仍是Redis自己(儘管基於Redis2.8.13上作了一些小patch),數據的分佈狀態存儲於zookeeper(etcd)中,底層的數據存儲變成了可插拔的部件。這個事情的好處其實不用多說,就是各個部件是能夠動態水平擴展的,尤爲無狀態的proxy對於動態的負載均衡,仍是意義很大的,並且還能夠作一些有意思的事情,好比發現一些slot的數據比較冷,能夠專門用一個支持持久化存儲的server group來負責這部分slot,以節省內存,當這部分數據變熱起來時,能夠再動態的遷移到內存的server group上,一切對業務透明。比較有意思的是,在Twitter內部棄用Twmeproxy後,t家本身開發了一個新的分佈式Redis解決方案,仍然走的是proxy-based路線。不過沒有開源出來。可插拔存儲引擎這個事情也是Codis的下一代產品RebornDB在作的一件事情。btw,RebornDB和它的持久化引擎都是徹底開源的,見https://github.com/reborndb/reborn和https://github.com/reborndb/qdb。固然這樣的設計的壞處是,通過了proxy,多了一次網絡交互,看上去性能降低了一些,可是記住,咱們的proxy是能夠動態擴展的,整個服務的QPS並不禁單個proxy的性能決定(因此生產環境中我建議使用LVS/HA Proxy或者Jodis),每一個proxy其實都是同樣的。

2、咱們更愛一致性


  不少朋友問我,爲何不支持讀寫分離,其實這個事情的緣由很簡單,由於咱們當時的業務場景不能容忍數據不一致,因爲Redis自己的replication模型是主從異步複製,在master上寫成功後,在slave上是否能讀到這個數據是沒有保證的,而讓業務方處理一致性的問題仍是蠻麻煩的。並且Redis單點的性能仍是蠻高的,不像mysql之類的真正的數據庫,沒有必要爲了提高一點點讀QPS而讓業務方困惑。這和數據庫的角色不太同樣。因此,你可能看出來了,其實Codis的HA,並不能保證數據徹底不丟失,由於是異步複製,因此master掛掉後,若是有沒有同步到slave上的數據,此時將slave提高成master後,剛剛寫入的還沒來得及同步的數據就會丟失。不過在RebornDB中咱們會嘗試對持久化存儲引擎(qdb)可能會支持同步複製(syncreplication),讓一些對數據一致性和安全性有更強要求的服務可使用。

  說到一致性,這也是Codis支持的MGET/MSET沒法保證本來單點時的原子語義的緣由。 由於MSET所參與的key可能分不在不一樣的機器上,若是須要保證原來的語義,也就是要麼一塊兒成功,要麼一塊兒失敗,這樣就是一個分佈式事務的問題,對於Redis來講,並無WAL或者回滾這麼一說,因此即便是一個最簡單的二階段提交的策略都很難實現,並且即便實現了,性能也沒有保證。因此在Codis中使用MSET/MGET其實和你本地開個多線程SET/GET效果同樣,只不過是由服務端打包返回罷了,咱們加上這個命令的支持只是爲了更好的支持之前用Twemproxy的業務。

  在實際場景中,不少朋友使用了lua腳本以擴展Redis的功能,其實Codis這邊是支持的,但記住,Codis在涉及這種場景的時候,僅僅是轉發而已,它並不保證你的腳本操做的數據是否在正確的節點上。好比,你的腳本里涉及操做多個key,Codis能作的就是將這個腳本分配到參數列表中的第一個key的機器上執行。因此這種場景下,你須要本身保證你的腳本所用到的key分佈在同一個機器上,這裏能夠採用hashtag的方式。

  好比你有一個腳本是操做某個用戶的多個信息,如uid1age,uid1sex,uid1name形如此類的key,若是你不用hashtag的話,這些key可能會分散在不一樣的機器上,若是使用了hashtag(用花括號擴住計算hash的區域):{uid1}age,{uid1}sex,{uid1}name,這樣就保證這些key分佈在同一個機器上。這個是twemproxy引入的一個語法,咱們這邊也支持了。

  在開源Codis後,咱們收到了不少社區的反饋,大多數的意見是集中在Zookeeper的依賴,Redis的修改,還有爲啥須要Proxy上面,咱們也在思考,這幾個東西是否是必須的。固然這幾個部件帶來的好處毋庸置疑,上面也闡述過了,可是有沒有辦法能作得更漂亮。因而,咱們在下一階段會再往前走一步,實現如下幾個設計:

  • 使用proxy內置的Raft來代替外部的Zookeeper,zk對於咱們來講,其實只是一個強一致性存儲而已,咱們其實可使用Raft來作到一樣的事情。將raft嵌入proxy,來同步路由信息。達到減小依賴的效果。

  • 抽象存儲引擎層,由proxy或者第三方的agent來負責啓動和管理存儲引擎的生命週期。具體來講,就是如今codis還須要手動的去部署底層的Redis或者qdb,本身配置主從關係什麼的,可是將來咱們會把這個事情交給一個自動化的agent或者甚至在proxy內部集成存儲引擎。這樣的好處是咱們能夠最大程度上的減少Proxy轉發的損耗(好比proxy會在本地啓動Redis instance)和人工誤操做,提高了整個系統的自動化程度。

  • 還有replication based migration。衆所周知,如今Codis的數據遷移方式是經過修改底層Redis,加入單key的原子遷移命令實現的。這樣的好處是實現簡單、遷移過程對業務無感知。可是壞處也是很明顯,首先就是速度比較慢,並且對Redis有侵入性,還有維護slot信息給Redis帶來額外的內存開銷。大概對於小key-value爲主業務和原生Redis是1:1.5的比例,因此仍是比較費內存的。

  在RebornDB中咱們會嘗試提供基於複製的遷移方式,也就是開始遷移時,記錄某slot的操做,而後在後臺開始同步到slave,當slave同步完後,開始將記錄的操做回放,回放差很少後,將master的寫入中止,追平後修改路由表,將須要遷移的slot切換成新的master,主從(半)同步複製,這個以前提到過。

3、Codis在生產環境中的使用的經驗和坑們


  來講一些 tips,做爲開發工程師,一線的操做經驗確定沒有運維的同窗多,你們一會能夠一塊兒再深度討論。

關於多產品線部署:不少朋友問咱們若是有多個項目時,codis如何部署比較好,咱們當時在豌豆莢的時候,一個產品線會部署一整套codis,可是zk共用一個,不一樣的codis集羣擁有不一樣的product name來區分,codis自己的設計沒有命名空間那麼一說,一個codis只能對應一個product name。不一樣product name的codis集羣在同一個zk上不會相互干擾。

關於zk:因爲Codis是一個強依賴的zk的項目,並且在proxy和zk的鏈接發生抖動形成sessionexpired的時候,proxy是不能對外提供服務的,因此儘可能保證proxy和zk部署在同一個機房。生產環境中zk必定要是>=3臺的奇數臺機器,建議5臺物理機。

關於HA:這裏的HA分紅兩部分,一個是proxy層的HA,還有底層Redis的HA。先說proxy層的HA。以前提到過proxy自己是無狀態的,因此proxy自己的HA是比較好作的,由於鏈接到任何一個活着的proxy上都是同樣的,在生產環境中,咱們使用的是jodis,這個是咱們開發的一個jedis鏈接池,很簡單,就是監聽zk上面的存活proxy列表,挨個返回jedis對象,達到負載均衡和HA的效果。也有朋友在生產環境中使用LVS和HA Proxy來作負載均衡,這也是能夠的。 Redis自己的HA,這裏的Redis指的是codis底層的各個server group的master,在一開始的時候codis原本就沒有將這部分的HA設計進去,由於Redis在掛掉後,若是直接將slave提高上來的話,可能會形成數據不一致的狀況,由於有新的修改可能在master中尚未同步到slave上,這種狀況下須要管理員手動的操做修復數據。後來咱們發現這個需求確實比較多的朋友反映,因而咱們開發了一個簡單的ha工具:codis-ha,用於監控各個server group的master的存活狀況,若是某個master掛掉了,會直接提高該group的一個slave成爲新的master。 項目的地址是:https://github.com/ngaut/codis-ha。

關於dashboard:dashboard在codis中是一個很重要的角色,全部的集羣信息變動操做都是經過dashboard發起的(這個設計有點像docker),dashboard對外暴露了一系列RESTfulAPI接口,無論是web管理工具,仍是命令行工具都是經過訪問這些httpapi來進行操做的,因此請保證dashboard和其餘各個組件的網絡連通性。好比,常常發現有用戶的dashboard中集羣的ops爲0,就是由於dashboard沒法鏈接到proxy的機器的緣故。

關於go環境:在生產環境中儘可能使用go1.3.x的版本,go的1.4的性能不好,更像是一箇中間版本,尚未達到production ready的狀態就發佈了。不少朋友對go的gc很有微詞,這裏咱們不討論哲學問題,選擇go是多方面因素權衡後的結果,並且codis是一箇中間件類型的產品,並不會有太多小對象常駐內存,因此對於gc來講基本毫無壓力,因此不用考慮gc的問題。

關於隊列的設計:其實簡單來講,就是「不要把雞蛋放在一個籃子」的道理,儘可能不要把數據都往一個key裏放,由於codis是一個分佈式的集羣,若是你永遠只操做一個key,就至關於退化成單個Redis實例了。不少朋友將Redis用來作隊列,可是Codis並無提供BLPOP/BLPUSH的接口,這沒問題,能夠將列表在邏輯上拆成多個LIST的key,在業務端經過定時輪詢來實現(除非你的隊列須要嚴格的時序要求),這樣就可讓不一樣的Redis來分擔這個同一個列表的訪問壓力。並且單key過大可能會形成遷移時的阻塞,因爲Redis是一個單線程的程序,因此遷移的時候會阻塞正常的訪問。

關於主從和bgsave:codis自己並不負責維護Redis的主從關係,在codis裏面的master和slave只是概念上的:proxy會將請求打到「master」上,master掛了codis-ha會將某一個「slave」提高成master。而真正的主從複製,須要在啓動底層的Redis時手動的配置。在生產環境中,我建議master的機器不要開bgsave,也不要輕易的執行save命令,數據的備份儘可能放在slave上操做。

關於跨機房/多活:想都別想。。。codis沒有多副本的概念,並且codis多用於緩存的業務場景,業務的壓力是直接打到緩存上的,在這層作跨機房架構的話,性能和一致性是很可貴到保證的

關於proxy的部署:其實能夠將proxy部署在client很近的地方,好比同一個物理機上,這樣有利於減小延遲,可是須要注意的是,目前jodis並不會根據proxy的位置來選擇位置最佳的實例,須要修改。

4、對於分佈式數據庫和分佈式架構的一些見解(one more Thing)


  Codis相關的內容告一段落。接下來我想聊聊我對於分佈式數據庫和分佈式架構的一些見解。 架構師們是如此貪心,有單點就必定要變成分佈式,同時還但願儘量的透明:P。就MySQL來看,從最先的單點到主從讀寫分離,再到後來阿里的相似Cobar和TDDL,分佈式和可擴展性是達到了,可是犧牲了事務支持,因而有了後來的OceanBase。Redis從單點到Twemproxy,再到Codis,再到Reborn。到最後的存儲早已和最初的面目全非,但協議和接口永存,好比SQL和Redis Protocol。

  NoSQL來了一茬又一茬,從HBase到Cassandra到MongoDB,解決的是數據的擴展性問題,經過裁剪業務的存儲和查詢的模型來在CAP上平衡。可是幾乎仍是都丟掉了跨行事務(插一句,小米上在HBase上加入了跨行事務,不錯的工做)。

  我認爲,拋開底層存儲的細節,對於業務來講,KV,SQL查詢(關係型數據庫支持)和事務,能夠說是構成業務系統的存儲原語。爲何memcached/Redis+mysql的組合如此的受歡迎,正是由於這個組合,幾個原語都能用上,對於業務來講,能夠很方便的實現各類業務的存儲需求,能輕易的寫出「正確」的程序。可是,如今的問題是數據大到必定程度上時,從單機向分佈式進化的過程當中,最難搞定的就是事務,SQL支持什麼的還能夠經過各類mysqlproxy搞定,KV就不用說了,天生對分佈式友好。

  因而這樣,咱們就默認進入了一個沒有(跨行)事務支持的世界裏,不少業務場景咱們只能犧牲業務的正確性來在實現的複雜度上平衡。好比一個很簡單的需求:微博關注數的變化,最直白,最正常的寫法應該是,將被關注者的被關注數的修改和關注者的關注數修改放到同一個事務裏,一塊兒提交,要麼一塊兒成功,要麼一塊兒失敗。可是如今爲了考慮性能,爲了考慮實現複雜度,通常來講的作法多是隊列輔助異步的修改,或者經過cache先暫存等等方式繞開事務。

  可是在一些須要強事務支持的場景就沒有那麼好繞過去了(目前咱們只討論開源的架構方案),好比支付/積分變動業務,常見的搞法是關鍵路徑根據用戶特徵sharding到單點MySQL,或者MySQLXA,可是性能降低得太厲害。

  後來Google在他們的廣告業務中遇到這個問題,既須要高性能,又須要分佈式事務,還必須保證一致性:),Google在此以前是經過一個大規模的MySQL集羣經過sharding苦苦支撐,這個架構的可運維/擴展性實在太差。這要是在通常公司,估計也就忍了,可是Google可不是通常公司,用原子鐘搞定Spanner,而後再Spanner上構建了SQL查詢層F1。我在第一次看到這個系統的時候,感受簡直驚豔,應該是第一個能夠真正稱爲NewSQL的公開設計的系統。因此,BigTable(KV)+F1(SQL)+Spanner(高性能分佈式事務支持),同時Spanner還有一個很是重要的特性是跨數據中心的複製和一致性保證(經過Paxos實現),多數據中心,恰好補全了整個Google的基礎設施的數據庫棧,使得Google對於幾乎任何類型的業務系統開發都很是方便。我想,這就是將來的方向吧,一個可擴展的KV數據庫(做爲緩存和簡單對象存儲),一個高性能支持分佈式事務和SQL查詢接口的分佈式關係型數據庫,提供表支持。

5、Q & A


Q1:我沒看過Codis,您說Codis沒有多副本概念,請問是什麼意思?

A1:Codis是一個分佈式Redis解決方案,是經過presharding把數據在概念上分紅1024個slot,而後經過proxy將不一樣的key的請求轉發到不一樣的機器上,數據的副本仍是經過Redis自己保證

Q2:Codis的信息在一個zk裏面存儲着,zk在Codis中還有別的做用嗎?主從切換爲什麼不用sentinel

A2:Codis的特色是動態的擴容縮容,對業務透明;zk除了存儲路由信息,同時還做爲一個事件同步的媒介服務,好比變動master或者數據遷移這樣的事情,須要全部的proxy經過監聽特定zk事件來實現 能夠說zk被咱們當作了一個可靠的rpc的信道來使用。由於只有集羣變動的admin時候會往zk上發事件,proxy監聽到之後,回覆在zk上,admin收到各個proxy的回覆後才繼續。自己集羣變動的事情不會常常發生,因此數據量不大。Redis的主從切換是經過codis-ha在zk上遍歷各個server group的master判斷存活狀況,來決定是否發起提高新master的命令。

Q3:數據分片,是用的一致性hash嗎?請具體介紹下,謝謝。

A3:不是,是經過presharding,hash算法是crc32(key)%1024

Q4:怎麼進行權限管理?

A4:Codis中沒有鑑權相關的命令,在reborndb中加入了auth指令。

Q5:怎麼禁止普通用戶連接Redis破壞數據?

A5:同上,目前Codis沒有auth,接下來的版本會加入。

Q6:Redis跨機房有什麼方案?

A6:目前沒有好的辦法,咱們的Codis定位是同一個機房內部的緩存服務,跨機房複製對於Redis這樣的服務來講,一是延遲較大,二是一致性難以保證,對於性能要求比較高的緩存服務,我以爲跨機房不是好的選擇。

Q7:集羣的主從怎麼作(好比集羣S是集羣M的從,S和M的節點數可能不同,S和M可能不在一個機房)?

A7:Codis只是一個proxy-based的中間件,並不負責數據副本相關的工做。也就是數據只有一份,在Redis內部。

Q8:根據你介紹了這麼多,我能夠下一個結論,大家沒有多租戶的概念,也沒有作到高可用。能夠這麼說吧?大家更多的是把Redis當作一個cache來設計。

A8:對,其實咱們內部多租戶是經過多Codis集羣解決的,Codis更多的是爲了替換twemproxy的一個項目。高可用是經過第三方工具實現。Redis是cache,Codis主要解決的是Redis單點、水平擴展的問題。把codis的介紹貼一下: Auto rebalance Extremely simple to use Support both Redis or rocksdb transparently. GUI dashboard & admin tools Supports most of Redis commands. Fully compatible with twemproxy(https://github.com/twitter/twemproxy). Native Redis clients are supported Safe and transparent data migration, Easily add or remove nodes on-demand.解決的問題是這些。業務不停的狀況下,怎麼動態的擴展緩存層,這個是codis關注的。

Q9:對於Redis冷備的數據庫的遷移,您有啥經驗沒有?對於Redis熱數據,能夠經過migrate命令實現兩個Redis進程間的數據轉移,固然若是對端有密碼,migrate就玩完了(這個我已經給Redis官方提交了patch)。

A9:冷數據咱們如今是實現了完整的Redissync協議,同時實現了一個基於rocksdb的磁盤存儲引擎,備機的冷數據,所有是存在磁盤上的,直接做爲一個從掛在master上的。實際使用時,3個group,keys數量一致,但其中一個的ops是另外兩個的兩倍,有多是什麼緣由形成的?key的數量一致並不表明實際請求是均勻分佈的,不如你可能某幾個key特別熱,它必定是會落在實際存儲這個key的機器上的。剛纔說的rocksdb的存儲引擎:https://github.com/reborndb/qdb,其實啓動後就是個Redis-server,支持了PSYNC協議,因此能夠直接當成Redis歷來用。是一個節省從庫內存的好方法。

Q10:Redis實例內存佔比超過50%,此時執行bgsave,開了虛擬內存支持的會阻塞,不開虛擬內存支持的會直接返回err,對嗎?

A10:不必定,這個要看寫數據(開啓bgsave後修改的數據)的頻繁程度,在Redis內部執行bgsave,實際上是經過操做系統COW機制來實現複製,若是你這段時間的把幾乎全部的數據都修改了,這樣操做系統只能所有完整的複製出來,這樣就爆了。

Q11:剛讀完,贊一個。能否介紹下codis的autorebalance實現。

A11:算法比較簡單,https://github.com/wandoulabs/codis/blob/master/cmd/cconfig/rebalancer.go#L104。代碼比較清楚,code talks:)。其實就是根據各個實例的內存比例,分配slot好的。

Q12:主要想了解對下降數據遷移對線上服務的影響,有沒有什麼經驗介紹?

A12:其實如今codis數據遷移的方式已經很溫和了,是一個個key的原子遷移,若是怕抖動甚至能夠加上每一個key的延遲時間。這個好處就是對業務基本沒感知,可是缺點就是慢。


感謝劉世傑的記錄與整理,臧秀濤、陳剛的校對,其餘多位編輯組志願者對本文亦有貢獻。更多關於架構方面的內容,請長按下面圖片,關注「高可用架構」公衆號,獲取通往架構師的寶貴經驗。

相關文章
相關標籤/搜索