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