codis是一個分布式redis解決方案,與官方的純p2p模式不同,codis采用的是proxy-based的方案。今天我們介紹一下codis以及下一個大版本reborndb的設計,同時會介紹codis在實際應用場景中的一些tips。最後抛磚引玉,介紹一下我對分布式存儲的一些觀點和看法。
目錄
redis、rediscluster和codis
我們更愛一緻性
codis在生産環境中的使用的經驗和坑們
對于分布式資料庫和分布式架構的一些看法
答疑記錄
1redis,rediscluster和codis
1、redis:想必大家的架構中,redis已經是一個必不可少的部件,豐富的資料結構和超高的性能以及簡單的協定,讓redis能夠很好的作為資料庫的上遊緩存層。但是我們會比較擔心redis的單點問題,單點redis容量大小總受限于記憶體,在業務對性能要求比較高的情況下,理想情況下我們希望所有的資料都能在記憶體裡面,不要打到資料庫上,是以很自然的就會尋求其他方案。 比如,ssd将記憶體換成了磁盤,以換取更大的容量。
更自然的想法是将redis變成一個可以水準擴充的分布式緩存服務,在codis之前,業界隻有twemproxy,但是twemproxy本身是一個靜态的分布式redis方案,進行擴容/縮容時候對運維要求非常高,而且很難做到平滑的擴縮容。codis的目标其實就是盡量相容twemproxy的基礎上,加上資料遷移的功能以實作擴容和縮容,最終替換twemproxy。從豌豆莢最後上線的結果來看,最後完全替換了twem,大概2t左右的記憶體叢集。
2、redis cluster :與codis同期釋出正式版的官方cluster,我認為有優點也有缺點,作為架構師,我并不會在生産環境中使用,原因有兩個:
cluster的資料存儲子產品和分布式的邏輯子產品是耦合在一起的,這個帶來的好處是部署異常簡單,all-in-the-box,沒有像codis那麼多概念,元件和依賴。但是帶來的缺點是,你很難對業務進行無痛的更新。比如哪天redis cluster的分布式邏輯出現了比較嚴重的bug,你該如何更新?除了滾動重新開機整個叢集,沒什麼好辦法。這個比較傷運維。
對協定進行了較大的修改,對用戶端不太友好,目前很多用戶端已經成為事實标準,而且很多程式已經寫好了,讓業務方去更換redisclient,是不太現實的,而且目前很難說有哪個rediscluster用戶端經過了大規模生産環境的驗證,從hunantv開源的rediscluster proxy上可以看得出這個影響還是蠻大的,否則就會支援使用cluster的client了。
3、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和它的持久化引擎都是完全開源的。當然這樣的設計的壞處是,經過了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,主從(半)同步複制,這個之前提到過。
3codis在生産環境中的使用的經驗和坑們
來說一些 tips,作為開發工程師,一線的操作經驗肯定沒有運維的同學多,大家一會可以一起再深度讨論。
1、關于多産品線部署:很多朋友問我們如果有多個項目時,codis如何部署比較好,我們當時在豌豆莢的時候,一個産品線會部署一整套codis,但是zk共用一個,不同的codis叢集擁有不同的product name來區分,codis本身的設計沒有命名空間那麼一說,一個codis隻能對應一個product name。不同product name的codis叢集在同一個zk上不會互相幹擾。
2、關于zk:由于codis是一個強依賴的zk的項目,而且在proxy和zk的連接配接發生抖動造成sessionexpired的時候,proxy是不能對外提供服務的,是以盡量保證proxy和zk部署在同一個機房。生産環境中zk一定要是>=3台的奇數台機器,建議5台實體機。
3、關于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。
4、關于dashboard:dashboard在codis中是一個很重要的角色,所有的叢集資訊變更操作都是通過dashboard發起的(這個設計有點像docker),dashboard對外暴露了一系列restfulapi接口,不管是web管理工具,還是指令行工具都是通過通路這些httpapi來進行操作的,是以請保證dashboard和其他各個元件的網絡連通性。比如,經常發現有使用者的dashboard中叢集的ops為0,就是因為dashboard無法連接配接到proxy的機器的緣故。
5、關于go環境:在生産環境中盡量使用go1.3.x的版本,go的1.4的性能很差,更像是一個中間版本,還沒有達到production ready的狀态就釋出了。很多朋友對go的gc頗有微詞,這裡我們不讨論哲學問題,選擇go是多方面因素權衡後的結果,而且codis是一個中間件類型的産品,并不會有太多小對象常駐記憶體,是以對于gc來說基本毫無壓力,是以不用考慮gc的問題。
6、關于隊列的設計:其實簡單來說,就是不要把雞蛋放在一個籃子裡的道理,盡量不要把資料都往一個key裡放,因為codis是一個分布式的叢集,如果你永遠隻操作一個key,就相當于退化成單個redis執行個體了。很多朋友将redis用來做隊列,但是codis并沒有提供blpop/blpush的接口,這沒問題,可以将清單在邏輯上拆成多個list的key,在業務端通過定時輪詢來實作(除非你的隊列需要嚴格的時序要求),這樣就可以讓不同的redis來分擔這個同一個清單的通路壓力。而且單key過大可能會造成遷移時的阻塞,由于redis是一個單線程的程式,是以遷移的時候會阻塞正常的通路。
7、關于主從和bgsave:codis本身并不負責維護redis的主從關系,在codis裡面的master和slave隻是概念上的:proxy會将請求打到「master」上,master挂了codis-ha會将某一個「slave」提升成master。而真正的主從複制,需要在啟動底層的redis時手動的配置。在生産環境中,我建議master的機器不要開bgsave,也不要輕易的執行save指令,資料的備份盡量放在slave上操作。
8、關于跨機房/多活:想都别想。codis沒有多副本的概念,而且codis多用于緩存的業務場景,業務的壓力是直接打到緩存上的,在這層做跨機房架構的話,性能和一緻性是很難得到保證的。
9、關于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答疑記錄
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. 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的存儲引擎其實啟動後就是個redis-server,支援了psync協定,是以可以直接當成redis從來用。是一個節省從庫記憶體的好方法。
q10:redis執行個體記憶體占比超過50%,此時執行bgsave,開了虛拟記憶體支援的會阻塞,不開虛拟記憶體支援的會直接傳回err,對嗎?
a10:不一定,這個要看寫資料(開啟bgsave後修改的資料)的頻繁程度,在redis内部執行bgsave,其實是通過作業系統cow機制來實作複制,如果你這段時間的把幾乎所有的資料都修改了,這樣作業系統隻能全部完整的複制出來,這樣就爆了。
q11:剛讀完,贊一個。可否介紹下codis的autorebalance實作。
a11:算法比較簡單。代碼比較清楚,code talks:)。其實就是根據各個執行個體的記憶體比例,配置設定slot好的。
q12:主要想了解對降低資料遷移對線上服務的影響,有沒有什麼經驗介紹?
a12:其實作在codis資料遷移的方式已經很溫和了,是一個個key的原子遷移,如果怕抖動甚至可以加上每個key的延遲時間。這個好處就是對業務基本沒感覺,但是缺點就是慢。
<b></b>
<b>本文來自雲栖社群合作夥伴"dbaplus",原文釋出時間:2016-03-01</b>