redis的哨兵模式,不做过多解释,可以网上自行搜索,仅仅说明一些和本缺陷有关的内容吧:哨兵是负责放哨的,它本身不负责操作数据,并且redis本身的节点都是也是主从,每一个节点都会一个哨兵,它们的端口号不同。客户端在连接的时候,配置的是哨兵的地址,连接有两个过程:
这次就只强调这两个过程吧,因为vertx的缺陷和这个有关系。目前还没有调研最新的版本有没有解决这个问题,就当记录一个疑难问题的分析和解决过程了。
从配置上支持了哨兵,vertx那边也提供了一些API吧,在redisOptions里面,如下:
RedisOptions options = new RedisOptions();
String masterNames = info.getMasterNames();
if (masterNames != null && masterNames.trim().length() != 0) {
options.setMasterName(masterNames);
}
String nodesStr = info.getNodes();
String[] nodesArray = nodesStr.split(",");
List<String> nodes = Arrays.asList(nodesArray);
if (nodes != null && nodes.size() > 0) {
options.setSentinels(nodes);
}
重点就是设置了一下主节点名称以及所有的哨兵节点。比较简单。
但是在实际用的过程当中,直接就报错了,错误如下:
Sentinel unreachable. ERR unknown command `SELECT`, with args beginning with: `6`,
其实刚开始看这个错误的话,可能是比较懵的,现在回头看的话,还是能看懂它的意思,但是可能也不知道咋解决。
意思就是:
访问哨兵失败,未知命令:使用参数6的Select命令
前面是其它同事先发现的,然后跟了一部分代码,做了很多铺垫。后面我才介入跟代码的,主要是跟的下述类的下述方法:
io.vertx.redis.impl.RedisConnection#connect()
private void connect() {
if (state.compareAndSet(State.DISCONNECTED, State.CONNECTING)) {
runOnContext(v -> {
if (useSentinel()) {
RedisMasterResolver resolver = new RedisMasterResolver(context.owner(), config);
resolver.getMasterAddressByName(jsonObjectAsyncResult -> {
if (jsonObjectAsyncResult.succeeded()) {
JsonObject masterAddress = jsonObjectAsyncResult.result();
connect(SocketAddress.inetSocketAddress(masterAddress.getInteger("port"), masterAddress.getString("host")), true);
} else {
// clean up any waiting command
clearQueue(waiting, jsonObjectAsyncResult.cause());
// clean up any pending command
clearQueue(pending, jsonObjectAsyncResult.cause());
state.set(State.DISCONNECTED);
}
resolver.close();
});
} else {
// if the domain socket option is enabled, use the domain socket address to connect to redis server
SocketAddress socketAddress;
if (config.isDomainSocket()) {
socketAddress = SocketAddress.domainSocketAddress(config.getDomainSocketAddress());
} else {
socketAddress = SocketAddress.inetSocketAddress(config.getPort(), config.getHost());
}
connect(socketAddress, false);
}
});
}
}
我简述一下上面的过程吧:
先判断是不是开启了redis的哨兵模式。
解析出来配置的所有的哨兵节点,为每一个哨兵创建新的RedisOptions(画重点,因为这里有问题),重新去创建一个redis客户端的实例(这个客户端和正常的略有区别,它的接口是RedisSentinel)。
然后遍历哨兵去咨询主节点地址,不过是怎么控制并发以及以哪个为准的,这个没看;并不是我关注的点。关注的点在于,是怎么和每一个哨兵进行连接并咨询主节点的。
vertx的源码的过程是:先建立tcp连接,然后验证密码,然后选择一个数据库。这里千万要注意一点是,先创建了一个RedisConnection实例去连接,但是在连接内部判断是哨兵所以又去创建RedisConnection去连接了,所以跟代码的时候,要注意这个问题,会反复的进来,而且后续进来的时候,走的不是哨兵的那条分支,要迅速反应过来,不然就懵逼了。
虽然说上一步失败了,但是从代码里面来看,确实也发现了去咨询主节点的命令,也很关键,后面会用到:
SENTINEL get-master-addr-by-name mastername
过程是这样的,问题是啥呢?
验证完密码之后,在选择数据库的时候,报错了。
使用命令行连接过redis的同学肯定知道,选择数据库的过程,非常简单:
select 0
这么简单的命令,这么就不行了呢?
其实到这边的时候,已经定位到问题了,就是在连接哨兵去咨询主节点的时候,执行select命令失败了。如果非常懂哨兵模式的人的话,再往后面看已经没有意思了,因为哨兵节点,是不支持select命令的。
但是我们并不是特别懂,也不知道这个事情。
那么接下来是怎么做的呢?
当时的第一反应是,怎么能不支持select命令呢?难道是这redis有问题吗?是的,一定有问题。赶紧找个客户端界面,连接一下那个哨兵(注意就用的是哨兵的端口号)。
我去,居然连上了,而且也能操作数据。加了一条数据,没啥问题,又连接上其余节点,数据也直接看见了,证明了它们是集群,而且通过info命令,也能够看到节点相关的信息,服务本身的端口号和哨兵的是不一样的。
接下来打开命令行,随便敲了几个命令,auth,select全都成功了,但是获取主节点地址的命令,报错了,告诉命令不存在。
我们发现的现象其实是调用select失败了,但是用redis客户端来调用,发现是成功的,而且redis本身也是没有问题的。
那接下来怎么办呢?很关键的一个转折点了,不然可能就卡在这边了。
哨兵本身的作用只是来负责告诉客户端谁现在是主节点以及具体的地址,并且后续的交互,都是客户端和主节点直接交互的,那说明了一个问题,使用可视化客户端连接的过程,敲的那些命令都是和主节点交互的,并不是哨兵。但前面发现的问题是,连接哨兵的时候,select失败了。所以上述测试并不可靠,因此得找一个更加可靠手段。
怎么弄呢?灵机一动,立马想到telnet工具。
telnet不就是用来测试一下端口通不通吗?看看防火墙之类的吗?大多数情况确实是,但是它本质上是和服务器的某个端口建立了一个TCP连接,所以我们大多数情况下只是使用了它的连接功能,既然可以连接,那就可以发送指令,并且得到响应(我一直知道这个事情,但是也确实没有用过,之前也仅仅只是测试端口号而已,这次就直接用了)。
很快连上其中一个哨兵,auth敲一下,提示成功。select敲一下,提示命令不支持。然后又敲了一个获取主节点的命令,哇塞,居然成功了,真的从哨兵那边获取到了主节点地址。
通过3个事情可以证明一个问题:连接哨兵的时候,只要验证一下密码就行了,不需要select,也不能select,所以vertx代码有问题。
事实:
咋解决呢?改vertx的代码。怎么改:
上面两步既支持了哨兵,也同时兼容了单节点的情况。
改了3个类,重写,编译打包,测试,验证通过。
具体改的地方,我就不贴了,如果真的遇到了这个问题并且仔细阅读了,应该会知道怎么改的。
虽然写了很长,但是实际上整个过程花得时间并不多。两个很关键的问题:
两个点,都非常重要,也容易阻塞,也许有时候就靠着那一点点灵感,能够瞬间想明白比较复杂的问题。