如何基于Redis Replication设计并实现Redis-replicator?
http://www.infoq.com/cn/articles/Redis-Replication-Redis-replicator
文章主要內(nèi)容圍繞 Redis-replicator 的設(shè)計(jì)與實(shí)現(xiàn),提綱如下:
首先,有兩個(gè)材料可能需要大家提前預(yù)習(xí)一下,以便更輕松地了解此次分享的內(nèi)容。
- https://redis.io/topics/protocol
- https://github.com/leonchen83/redis-replicator/wiki/RDB-dump-data-format
1. Redis-replicator 的設(shè)計(jì)動機(jī)
在之前的開發(fā)中,經(jīng)常有如下需求:
- Redis 數(shù)據(jù)的跨機(jī)房同步
- 異構(gòu)數(shù)據(jù)的遷移,比如 Redis 到 MySQL、MQ
1.1 Redis 數(shù)據(jù)的跨機(jī)房同步
Redis 跨機(jī)房同步,傳統(tǒng)的方式通常采取雙寫的方式,這樣會生產(chǎn)一種非常難以維護(hù)的用戶代碼。稍微好一點(diǎn)的做法是提煉出一個(gè)中間層。但也難以保證同時(shí)雙寫成功,因此又需要做復(fù)雜的異常處理,這同時(shí)也增加了程序的響應(yīng)時(shí)間。
除了雙寫的方式,還有一種方式是利用 Redis 自身的 Replication 協(xié)議,讓一臺機(jī)器成為另一臺機(jī)器的 slave,用這種方式來同步數(shù)據(jù)。
這種方式的問題是,雙機(jī)房中必須有一個(gè)是 master,一個(gè)是 slave。在切換的過程中,需要作 slave 提升等處理,變相增加了運(yùn)維難度。而且一般在集群環(huán)境中,用戶常常期望兩個(gè)機(jī)房各一個(gè)獨(dú)立集群,而不是兩個(gè)機(jī)房組成一個(gè)混合集群(這樣出問題切換方便些),并且保持兩個(gè)獨(dú)立集群之間數(shù)據(jù)是同步的。
如下圖所示:
1.2 異構(gòu)數(shù)據(jù)的遷移
上面是屬于同構(gòu)數(shù)據(jù)遷移,再來說說異構(gòu)數(shù)據(jù)遷移。現(xiàn)實(shí)需求中,有可能會有異構(gòu)遷移的情況,比如 Redis 每日數(shù)據(jù)量很大,需要把一些數(shù)據(jù)以文件或者數(shù)據(jù)庫存儲的方式落盤(MySQL、MQ、SSDB 等),每日異地備份等等,如果還是采用雙寫等方式處理的話,又會有代碼擴(kuò)張、維護(hù)困難等上述提過的問題。
如下圖所示:
1.3 如何用 Redis-replicator 來實(shí)現(xiàn)需求
在以上的需求中,催生了我開發(fā) Redis-replicator 的動機(jī)。 這個(gè)工具完整實(shí)現(xiàn)了 Redis Replication 協(xié)議,并把 RDB 以及 AOF 解析成一個(gè)一個(gè)的事件供用戶消費(fèi),并且支持 Redis4.0 的新特性以及新命令。
如果用 Redis-replicator 來實(shí)現(xiàn)上述需求的話,可以不干擾用戶態(tài)的代碼,單獨(dú)用這個(gè)工具實(shí)現(xiàn)中間件來進(jìn)行異構(gòu),同構(gòu)數(shù)據(jù)同步備份等任務(wù)。
如下圖所示:
2. Redis-replicator 的設(shè)計(jì)與實(shí)現(xiàn)
2.1 Redis-replicator 的架構(gòu)圖
那么講完了動機(jī),我們可以探尋一下 Redis-replicator 的實(shí)現(xiàn)。Redis-replicator 的架構(gòu)如下所示 ?:
2.2 Redis-replicator 的樣例代碼
通用的代碼如下:
Replicator replicator = new RedisReplicator("redis://127.0.0.1:6379"); replicator.addRdbListener(new RdbListener.Adaptor() { // 解析 RDB 事件 @Override public void handle(Replicator replicator, KeyValuePair<?> kv) { System.out.println(kv); } }); replicator.addCommandListener(new CommandListener() { // 解析 AOF 實(shí)時(shí)命令 @Override public void handle(Replicator replicator, Command command) { System.out.println(command); } }); replicator.open();這里稍微對代碼做一下解釋,首先是 Redis 的 URI 表示redis://127.0.0.1:6379,這種表示通過 socket 進(jìn)行在線的實(shí)時(shí)數(shù)據(jù)同步,不但支持在線實(shí)時(shí)同步,而且 Redis-replicator 也可以進(jìn)行離線的 RDB 以及 AOF 文件的解析,相應(yīng)的 URI 修改為redis:///path/to/dump.rdb或redis:///path/to/appendonly.aof,其余的代碼保持不變。
RdbListener 表示監(jiān)聽 RDB 事件,CommandListener 表示監(jiān)聽 AOF 事件。所以我們可以僅僅更改 URI 來做到遠(yuǎn)程同步和文件解析之間的自由切換。
2.3 Redis-replicator 的源碼目錄結(jié)構(gòu)及源碼導(dǎo)讀
在對架構(gòu)和樣例代碼有一定了解之后,我們來了解一下源碼的目錄結(jié)構(gòu)和一些關(guān)鍵的 class。
源碼結(jié)構(gòu)如下圖所示:
上圖中 cmd 包和 AOF 事件相關(guān),比如在同步完 RDB 數(shù)據(jù)之后 master 寫入了一條這樣的命令set foo bar,就會產(chǎn)生一條 Command 并觸發(fā) CommandListener。(重點(diǎn)類有 Command、CommandParser、CommandListener、ReplyParser)
event 包包含了 RDB 事件與 AOF 事件的基類 Event,以及包含兩個(gè)自定義事件 PreFullSyncEvent 和 PostFullSyncEvent,這兩個(gè)自定義事件標(biāo)記了全量數(shù)據(jù)同步的開始和結(jié)束(增量同步不觸發(fā)這兩個(gè)標(biāo)記事件)。
io、net、util 包與 Redis-replicator 的網(wǎng)絡(luò)傳輸以及內(nèi)部用數(shù)據(jù)結(jié)構(gòu)相關(guān),不多做介紹。
rdb 包和 RDB 事件相關(guān),會把 RDB 的數(shù)據(jù)流解析成一個(gè)一個(gè) KeyValuePair 并觸發(fā) RdbListener。同時(shí)這個(gè)包也包含了 Module 解析和自定義 RDB 解析器相關(guān)的類。(重點(diǎn)的類有 KeyValuePair、Module、ModuleParser、RdbVisitor、RdbParser)
還有根目錄下的一些重點(diǎn)類:ReplicatorListener 包含用戶所有可以注冊的監(jiān)聽器,Configuration 包含一切可配置的參數(shù),Replicator 是實(shí)現(xiàn) Replication 協(xié)議的重要接口。
3. Redis Replication 的協(xié)議簡析
講到這里,就再仔細(xì)說一下 Redis Replication 協(xié)議,很多同學(xué)以為這個(gè)協(xié)議很復(fù)雜,實(shí)現(xiàn)起來很困難。但實(shí)際上如果仔細(xì)了解這個(gè)協(xié)議的話,即使用 Java 這種略臃腫的語言,在 3000 行內(nèi)也可以實(shí)現(xiàn)一個(gè)完整的同步協(xié)議(Redis-replicator 第一版 5000 行代碼)。我鼓勵(lì)大家也去用不同語言來實(shí)現(xiàn) Redis 的同步協(xié)議,以豐富 Redis 的工具鏈。
具體的協(xié)議格式是一個(gè)非嚴(yán)格(這里的非嚴(yán)格是指 AOF 的格式有可能不是標(biāo)準(zhǔn)格式,因?yàn)橛锌赡茉趦蓚€(gè) AOF 命令之間插入\n)的 AOF 格式,第一個(gè) AOF 是同步命令的回復(fù),第二個(gè) AOF 命令很特殊,是一個(gè) RESP Bulk String,其內(nèi)包含了 RDB 格式。
其余的 AOF 就是 master 的實(shí)時(shí)命令。了解 AOF 格式的話請參照 https://redis.io/topics/protocol,關(guān)于增量同步還是全量同步返回的格式也有不同,如下圖所示:
3.1 第一個(gè) AOF
第一個(gè) AOF 是同步命令的回復(fù),在同步之前我們要發(fā)送同步命令,比如 2.8 版本之前我們要發(fā)送SYNC, 2.8 之后我們要發(fā)送PSYNC repl-id repl-offset開啟 PSYNC 同步,repl-id 占 40 字節(jié),不知道 repl-id 的情況下發(fā)送?, repl-offset 表示同步的 offset,不知道 offset 的情況下發(fā)送-1,回復(fù)的話有可能是如下形式:+FULLRESYNC repl-id offset\r\n或者+CONTINUE\r\n或者 Redis-4.0 引入的 PSYNC2 回復(fù)+CONTINUE repl-id\r\n
3.2 第二個(gè) AOF
上面我們說第二個(gè) AOF 是一個(gè) RESP Bulk String,那么其符合$payload\r\nRDB(注意結(jié)尾沒有\(zhòng)r\n) 這樣的形式,payload 表示要傳輸?shù)?rdb 大小,內(nèi)容的話就是一個(gè)完整的 RDB 文件。
關(guān)于 RDB 文件的格式,我做了一個(gè) RDB data format wiki 供大家詳細(xì)了解,在此不做贅述。
https://github.com/leonchen83/redis-replicator/wiki/RDB-dump-data-format
稍微需要注意的是,如果 redis-server 開啟了repl-diskless-sync = yes那么這個(gè)格式會稍有變化。
在?https://redis.io/topics/protocol?文檔中 RESP Bulk String 還有一種沒有提到的格式用在同步協(xié)議中,?$EOF:<40 bytes delimiter>\r\nRDB<40 bytes delimiter>,此時(shí)的 payload 變成EOF:<40 bytes delimiter>所以在實(shí)現(xiàn)同步協(xié)議的時(shí)候需要注意。
第二點(diǎn)需要注意的是如果 master 產(chǎn)生的 RDB 特別巨大的時(shí)候,在同步 RDB 之前會發(fā)送連續(xù)的\n以此來維持與 slave 的連接。所以同步的數(shù)據(jù)流有可能是這樣的:
+FULLRESYNC8de1787ba490483314a4d30f1c628bc5025eb761 2443808505\r\n\n\n\n\n\n\n$payload\r\nRDB<其他 AOF 命令>
3.3 其他的 AOF
參照?https://redis.io/topics/protocol?進(jìn)行解析。
3.4 心跳
4. 設(shè)計(jì)可插拔式 API 以及開發(fā)中的取舍
4.1 設(shè)計(jì)可插拔式 API
我們從第二節(jié)的代碼中可以用很簡單的方式與 Redis master 實(shí)現(xiàn)同步,這小節(jié)我們主要講 Redis-replicator 的擴(kuò)展性,從以下幾個(gè)方面來詳細(xì)說明:
先討論第一點(diǎn),當(dāng)升級 Redis-server 有新的命令而 Redis-replicator 不支持時(shí),可以使用命令擴(kuò)展。
寫一個(gè)命令解析器并注冊進(jìn) Redis-replicator 中即可 handle 新的命令。一個(gè)詳細(xì)的例子在 CommandExtensionExample,
https://github.com/leonchen83/redis-replicator/blob/master/examples/com/moilioncircle/examples/extension/CommandExtensionExample.java
再討論第二點(diǎn),由于 Redis-replicator 默認(rèn)是把 KV 完全讀到內(nèi)存再交由用戶處理的,當(dāng)處理比如超過本機(jī)內(nèi)存的大 KV 時(shí),會引發(fā) OOM。一個(gè)比較好的方法是以迭代的方式來處理大 KV。
在 Redis-replicator 中,可以注冊自己的 RDB 解析器來應(yīng)對這種情況,一個(gè)好消息是此工具已經(jīng)內(nèi)置了處理大 KV 的 RDB 解析器 ValueIterableRdbVisitor ,
https://github.com/leonchen83/redis-replicator/blob/master/src/main/java/com/moilioncircle/redis/replicator/rdb/iterable/ValueIterableRdbVisitor.java
與此相關(guān)的例子在 HugeKVSocketExample ,
https://github.com/leonchen83/redis-replicator/blob/master/examples/com/moilioncircle/examples/huge/HugeKVSocketExample.java
再討論第三點(diǎn),加載自定義 Module 時(shí),可以實(shí)現(xiàn)自定義的 Module parser 并注冊到 Redis-replicator 中,實(shí)現(xiàn) Module 擴(kuò)展,一個(gè)相關(guān)的例子在 ModuleExtensionExample,
https://github.com/leonchen83/redis-replicator/blob/master/examples/com/moilioncircle/examples/extension/ModuleExtensionExample.java
總結(jié)設(shè)計(jì)可插拔式 API 的重點(diǎn)是要求平等對待內(nèi)建 (built-in)API 和外部 API。Redis-replicator 只提供了一個(gè)同步協(xié)議的大框架,其內(nèi)的命令解析、RDB 解析、Module 解析都是可插拔的,這樣可以提供最大的靈活性給用戶。
4.2 開發(fā)中的取舍
4.2.1 無緒
最近我讀完一本書很有啟發(fā),書名叫《軟件框架設(shè)計(jì)的藝術(shù)》,書中提到了一個(gè)叫無緒的概念,大意是當(dāng)你依賴一個(gè)庫,可以不用深入了解這個(gè)庫的內(nèi)部實(shí)現(xiàn),就可直接根據(jù) API 上手使用,并做出相對可靠的應(yīng)用程序。
對這個(gè)概念我深以為然,但是這本書是我寫完 Redis-replicator 之后才讀到的,有一些不一致為了兼容性已經(jīng)不可更改(有興趣的朋友可以找一找代碼存在的問題),但總體上根據(jù) Redis-replicator 提供的文檔以及 example 和對 issue 的快速回應(yīng)以及修改可以讓依賴此庫風(fēng)險(xiǎn)可控。
4.2.2 兼容
同樣還是《軟件框架設(shè)計(jì)的藝術(shù)》這本書,提到了一個(gè)兼容性問題。書中有一句話:API 就如同恒星,一旦出現(xiàn),便與我們永恒共存。大意就是一個(gè) API 在被用戶發(fā)現(xiàn)并使用了之后,就盡量不要做不兼容的修改,做出不兼容修改用戶升級時(shí)會產(chǎn)生運(yùn)行時(shí)錯(cuò)誤等等問題,降低用戶對一個(gè)庫的好感度。我舉一個(gè)在 Redis-replicator 中存在的例子。
用戶實(shí)現(xiàn)自己的 RDB 解析器時(shí)需要繼承 RdbVisitor 這個(gè)類,
https://github.com/leonchen83/redis-replicator/blob/master/src/main/java/com/moilioncircle/redis/replicator/rdb/RdbVisitor.java
這個(gè)類如果被設(shè)計(jì)成接口, Redis 每增加一個(gè)存儲結(jié)構(gòu),這個(gè)接口就要增加一個(gè)方法,即使用戶沒用到這么高版本的 Redis 也要對實(shí)現(xiàn)類進(jìn)行修改。設(shè)計(jì)成抽象類的話,每次升級 Redis-replicator,不會對用戶代碼造成影響,僅僅在同時(shí)升級了 Redis-server 的時(shí)候才會出現(xiàn)異常。
4.2.3 依賴
開發(fā)基礎(chǔ)庫上選擇依賴一定要更加謹(jǐn)慎。因?yàn)?Java 的 jar hell 等原因,在一個(gè)稍微復(fù)雜的系統(tǒng)中,出現(xiàn)循環(huán)依賴,以及依賴同一個(gè)包的不同版本這種情況會經(jīng)常發(fā)生。比如在一個(gè)工程中經(jīng)常有多個(gè)版本的 slf4j-api、netty。在不實(shí)際運(yùn)行的話很難發(fā)現(xiàn)問題。
第二點(diǎn)就是在設(shè)計(jì)公共庫涉及寫日志時(shí),最好不要依賴具體的 log 實(shí)現(xiàn),要盡量依賴 log 的 API(commons-logging、slf4j-api 等)。一個(gè)不好的例子是:
<dependency><groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> </dependency>這個(gè)包經(jīng)常用在 ZooKeeper 客戶端中比如 curator-client,然而這個(gè)包依賴了一個(gè)很低版本的 log4j 實(shí)現(xiàn)庫,導(dǎo)致實(shí)際應(yīng)該依賴 log 的 API 變成依賴于 log 的實(shí)現(xiàn)庫,如果用戶選擇的是 logback 這種實(shí)現(xiàn)庫來寫日志的話,會有一些沖突,需要各種橋接來做 work around。在 Redis-replicator 中,唯一依賴的 jar 包是 commons-logging,盡最大程度保證用戶與自己的工程依賴的兼容性。
5. 總結(jié)
限于篇幅和重點(diǎn),并沒有展現(xiàn) Redis-replicator 的全部功能,比如此工具還可以做 RDB 及 AOF 文件的拆分與合并,RDB 格式轉(zhuǎn) Redis 的 dump 格式 (和 dump 命令得到的格式一致),以及 RDB 與 AOF 文件的備份和 Redis-4.0 混合格式的支持等。歡迎關(guān)注并 star Redis-replicator。
6. Q&A
Q: 這個(gè)后續(xù)的開發(fā)計(jì)劃是什么?
A: 后續(xù)計(jì)劃是支持redis-4.2,以及增加failover能力和jdk9支持。
Q: Redis-replicator性能怎么樣?
A: 性能比唯品匯C語言實(shí)現(xiàn)的Redis遷移工具略慢,純解析大概能達(dá)到80%左右的性能。但是因?yàn)檫@個(gè)工具的速度也取決于消費(fèi)事件的速度,如果消費(fèi)慢的話,會阻塞Redis-server或者Redis-server給主動斷開連接。
Q: 我提問的問題可能跨過此中間件的本身了,我更關(guān)注于場景。對于跨機(jī)房的場景,平均數(shù)據(jù)延遲有多大,在多大數(shù)據(jù)量的情況下如何保證延遲降到最低?在出現(xiàn)網(wǎng)絡(luò)抖動的情況如何避免數(shù)據(jù)的丟失?
A: 跨機(jī)房平均延遲的話有很多相關(guān)性,不僅僅和Redis-replicaor相關(guān),還和網(wǎng)絡(luò)速度等等因素相關(guān)。這個(gè)工具能盡量做到解析不會影響跨機(jī)房同步。再來說出現(xiàn)抖動的情況, 在Redis-server端參數(shù)配置合理的情況下,如果出現(xiàn)網(wǎng)絡(luò)抖動,那么Redis-replicator會盡量采取部分同步來進(jìn)行重試,如果在Redis-server的backlog之外的話,會全量同步重連。
Q: 可以基于AOF文件合并生成RDB文件嗎?對內(nèi)存的占用是否會造成OOM問題?
A: AOF文件轉(zhuǎn)RDB這個(gè)工具還做不到,但反過來自己擴(kuò)展一些代碼可以做到RDB轉(zhuǎn)AOF。在分享中特意有這個(gè)OOM的實(shí)踐,因?yàn)橛杏脩舫晒τ眠@個(gè)工具同步8GB單KV,單實(shí)例30GB的Redis,就是因?yàn)榭梢宰远xRDB解析器,把KV轉(zhuǎn)成迭代的方式減小占用內(nèi)存。
Q: 消費(fèi)慢,有統(tǒng)計(jì)過過達(dá)到多大的并發(fā)量?以及當(dāng)消費(fèi)慢對redis性能影響有數(shù)據(jù)統(tǒng)計(jì)嗎?
A: 消費(fèi)慢的行為和Redis slave的行為一致,比如某臺Redis slave消費(fèi)慢,有可能產(chǎn)生無限重連的情況,這里redis-replicator和slave的行為是保持一致的,可能需要調(diào)整一些參數(shù)比如repl-backlog-size、repl-backlog-ttl、repl-ping-slave-periods。
Q: 有因?yàn)楦鞣N原因中斷后進(jìn)行retry的功能嗎?
A: 有斷線重聯(lián),而且是盡量以避免全量同步的方式重聯(lián)。并且有標(biāo)記event來監(jiān)測到是否是全量同步。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/articles/9254205.html
總結(jié)
以上是生活随笔為你收集整理的如何基于Redis Replication设计并实现Redis-replicator?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 同程旅游缓存系统(凤凰)打造Redis时
- 下一篇: cachecloud:Redis云管理平