背景
对 Redis 场景降本增效,涉及到将部分 Redis 实例迁移到类似社区 pika 这种支持 Redis 协议的基于 SSD 磁盘存储的项目(阿里云 Tair),降低存储成本。迁移过程需要进行性能验证,除了基本的选型压测之外,还必须对每个业务场景做全指令的性能覆盖,才能确保业务迁移的性能以及指令兼容稳定性。常规的做法是需要业务开发配合在工程里进行流量双发,或者小范围流量灰度。
以上这个问题,不管哪种方式都需要投入更多的人力和时间,对降本增效本身这件事情来说,大大降低了 roi 。如果能够做到直接将原 Redis 的所有读流量重放到目标 Redis SSD 的实例,则迁移整件事件 SRE 可以完成 99% ,而且将大大缩短迁移实例的时间,所以 Redis 流量镜像这个需求就应运而生了。
tips:我们的数据迁移方案采用阿里云的 DTS ,DTS 是基于 Redis 主从复制的原理实现的,所以写流量性能在数据同步过程就可以直接验证了
调研
Google 上、Github 上逛了一圈,没有十分契合的东西,所以最后决定自研。查到的一些相关信息如下:
- 阿里云的 SLS Redis 审计日志
阿里云的 Redis 实例支持将 Redis 的执行日志丢到 SLS(一个日志记录查询的产品) 记录,但是只有写流量的记录,达不到 Redis 读流量回放的需求。
- pika redis-copy 工具:https://github.com/OpenAtomFoundation/pika/issues/2044
这个工具已经被 pika 项目丢弃了。目前只有文档,仓库里已经没有相关的代码了。不过本文实现也是基于和 pika 的实现原理一样的
- istio 实现 Redis 流量镜像:https://github.com/cloudnativeto/cloudnativeto.github.io/issues/76
基于 istio 做 Redis 的镜像流量,必须接入 istio 才行,局限性太大了,而且引入一个新的 istio 组件需要做很多的稳定性测试,所以这个路线就直接否了
实现
直接接入正题,采用 Redis 的 monitor 指令来实现这个需求。
/**
* @author kl (http://kailing.pub)
* @since 2023/9/27
*/
public class RedisMonitorTest {
static final JedisPool targetRedisPool = new JedisPool("127.0.0.1", 6398);
static final JedisPool sourceRedisPool = new JedisPool("127.0.0.1", 6379);
static final Set<String> redisReadCommands = new HashSet<>(Arrays.asList(
"get", "strlen", "exists", "getbit", "getrange", "substr", "mget", "llen", "lindex",
"lrange", "sismember", "scard", "srandmember", "sinter", "sunion", "sdiff", "smembers",
"sscan", "zrange", "zrangebyscore", "zrevrangebyscore", "zrangebylex", "zrevrangebylex",
"zcount", "zlexcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", "zscan", "hget",
"hmget", "hlen", "hstrlen", "hkeys", "hvals", "hgetall", "hexists", "hscan", "randomkey",
"keys", "scan", "dbsize", "type", "sync", "psync", "ttl", "touch", "pttl", "dump", "object",
"memory", "bitcount", "bitpos", "georadius_ro", "georadiusbymember_ro", "geohash",
"geopos", "geodist", "pfcount", "xrange", "xrevrange", "xlen", "xread", "xpending",
"xinfo", "lolwut"
));
public static void main(String[] args) {
try (Jedis jedis = sourceRedisPool.getResource()) {
jedis.monitor(new JedisMonitor() {
@Override
public void onCommand(String command) {
sendCommand(command);
}
});
}
}
public static void sendCommand(String commandStr) {
String[] parts = commandStr.split(""");
if (parts.length < 2) {
return;
}
String cmd = parts[1];
List<String> args = new ArrayList<>();
for (int i = 3; i < parts.length; i += 2) {
args.add(parts[i]);
}
if (redisReadCommands.contains(cmd.toLowerCase())) {
ProtocolCommand command = () -> cmd.getBytes(StandardCharsets.UTF_8);
try (Jedis jedis = targetRedisPool.getResource()) {
try {
long startTime = System.currentTimeMillis();
jedis.sendCommand(command, args.toArray(new String[0]));
System.out.println(cmd + ":" + (System.currentTimeMillis() - startTime));
} catch (Exception e) {
System.err.println(cmd + e.getMessage());
}
}
}
}
}
以上是一段可以直接执行的伪代码,将 sourceRedis 的所有读流量转发到 targetRedis 执行
实现解析
上面采用的 Java 的 Redis 客户端 jedis 来开发,首先调用了 monitor 这个指令,这个指令是一个阻塞指令,会一直订阅 Redis 服务端的指令执行记录,记录的格式如下:
1695869359.747056 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869359805247076" "LIMIT" "0" "1"
1695869359.748040 [0 127.0.0.1:64257] "EXISTS" "asynq:{sys}:paused"
1695869359.748259 [0 127.0.0.1:64257] "EXISTS" "asynq:{sync}:paused"
1695869359.748578 [0 127.0.0.1:64257] "EXISTS" "asynq:{default}:paused"
1695869359.748916 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869359869190783" "LIMIT" "0" "1"
1695869359.749154 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869359877625076" "LIMIT" "0" "1"
1695869359.749348 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869359878760313" "LIMIT" "0" "1"
1695869359.749530 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869359882064571" "LIMIT" "0" "1"
1695869359.779048 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869360024586886" "LIMIT" "0" "1"
1695869359.785898 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869360031603858" "LIMIT" "0" "1"
1695869359.786092 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869360031656719" "LIMIT" "0" "1"
1695869359.786923 [0 127.0.0.1:64257] "ZRANGEBYSCORE" "delayed_tasks" "0" "1695869360031666910" "LIMIT" "0" "1"
所以我们只要解析出指令,然后发送到目标实例就好了
这里还涉及到一个问题,怎么过滤出只有读指令的记录?
我尝试过问 chatGPT ,但是他一点都不靠谱,不是少了读的指令,就是用其他写指令凑数。所以不可信。好在 Redis 服务端对每个指令都进行了打标,区分了读指令还是写指令。
所以只需要把所有的只读指令打印到控制台复制出来就解决这个问题了。
注意事项
Redis monitor 指令是一个对 Redis 性能有损的指令,官方测试会对单实例 Redis 降低 50% 左右的性能,我实际测试在 Redis 实际负载不高的情况下,这个影响可以忽略不计(特别高 QPS 的实例谨慎使用)。因为 Redis 单机 QPS 能支撑 10W 。比如线上实时 QPS 1W ,使用 monitor 的时候,QPS 和 RT 几乎没有变化。
另外需要注意,monitor 长时间运行会增加 Redis 的内存消耗,所以如果做性能验证,最好控制下时间,不要一直跑。
- monitor 文档:https://redis.io/commands/monitor/
结语
在本篇博客中,我们探讨了 Redis 流量镜像的实现方法和其在降本增效方面的重要性。我们了解到,传统的验证方式在迁移 Redis 实例时需要大量的人力和时间投入,降低了降本增效的 ROI。为了解决这一问题,引入了 Redis 流量镜像的需求。
通过将原 Redis 的所有读流量直接重放到目标 Redis SSD 实例,我们可以高效地完成迁移实例的过程,减少了 SRE 的工作量,并显著缩短了迁移时间。这种方法不仅提高了迁移过程的效率,还降低了成本和风险,使得降本增效的目标更加可行和实现。通过采用这种方法,我们可以更高效地迁移 Redis 实例,并在降低成本、提高效率的同时保持业务的性能和稳定性。
谢谢您的阅读!如果您有任何问题或想法,请随时参与评论留言。