Redisson-3.8 查找DNS异常的解决办法_org.redisson.connection.dnsmonitor-程序员宅基地

技术标签: At least two sentinels should be de  DNS  redisson  3.8  redis  

背景

  1. Redisson 连接 redis 时出现 “At least two sentinels should be defined in Redis configuration!”
  2. 可能是因为配置的 sentinel 数量少于2个
  3. 可能是从其中一个 sentinel 上查找不到其他 sentinel 的信息
  4. 可能是尝试连接各个 sentinel 时,发现能连接上的数量少于2个
  5. 也可能是它使用的 netty 版本有 bug,在进行 DNS 解析时出现问题
  6. 如果本篇不能解决遇到的问题,可以参考另外一篇《 解决Redisson无法连接Sentinel, Netty查找DNS失败

本文主要针对第5点进行分析和解决。

环境 (ubuntu)

  1. redis sentinel的链接url为
sentinel://redis:26379,redis:26380?masterNames=mymaster&poolSize=100&poolName=xxx
  1. /etc/resolv.conf的内容如下:
nameserver 127.0.0.1


search aaa.bbb ostechnix.lan

(在这里,aaa.bbb是解析不了的)

  1. ping 和 nslookup redis:
$ ping redis
PING redis.ostechnix.lan (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.026 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.057 ms


$ nslookup redis
Server:		127.0.0.1
Address:	127.0.0.1#53

Name:	redis.ostechnix.lan
Address: 127.0.0.1
  1. 使用redis-cli可以连接redis的sentinel:
$ src/redis-cli -h redis -p 26379
redis:26379> sentinel sentinels mymaster
1)  1) "name"
    2) "127.0.0.1:26380"
    3) "ip"
    4) "127.0.0.1"
    5) "port"
    6) "26380"
    ...

$ src/redis-cli -h redis -p 26380
1)  1) "name"
    2) "127.0.0.1:26379"
    3) "ip"
    4) "127.0.0.1"
    5) "port"
    6) "26379"

现象

java程序使用redisson-3.8.2尝试连接redis,出现了错误

Exception in thread "main" org.redisson.client.RedisConnectionException: At least two sentinels should be defined in Redis configuration!
	at org.redisson.connection.SentinelConnectionManager.<init>(SentinelConnectionManager.java:159)
	at org.redisson.config.ConfigSupport.createConnectionManager(ConfigSupport.java:195)
	at org.redisson.Redisson.<init>(Redisson.java:122)
	at org.redisson.Redisson.create(Redisson.java:161)
	...

打开redisson的debug日志:

2018-11-21 18:11:53 [main] WARN  o.r.c.SentinelConnectionManager - Can't connect to sentinel server. Unable to connect to: redis://redis:26379
...
2018-11-21 18:12:03 [main] WARN  o.r.c.SentinelConnectionManager - Can't connect to sentinel server. Unable to connect to: redis://redis:26380

Well, 为何 redis-cli 能连接得上sentinel,而java程序会出错?java程序在使用以前的版本redisson-2.5.1的时候是一切正常的。

重新看一次debug日志,发现了一个奇怪的地方:

2018-11-21 18:11:48 [main] DEBUG i.netty.resolver.dns.DnsQueryContext - [id: 0xda4ab0e7] WRITE: [49889: /127.0.0.1:53], DefaultDnsQuestion(redis.aaa.bbb ostechnix.lan. IN A)
2018-11-21 18:11:48 [main] DEBUG i.netty.resolver.dns.DnsQueryContext - [id: 0xda4ab0e7] WRITE: [34575: /127.0.0.1:53], DefaultDnsQuestion(redis.aaa.bbb ostechnix.lan. IN AAAA)

解释一下:
IN A:代表主机名到 IPv4 地址的映射
IN AAAA:代表主机名到 IPv6 地址的映射
ostechnix.lan.” 最后的点,代表根,“lan.” 表示lan为根下的第一级域

再来看这个域名 “redis.aaa.bbb ostechnix.lan.”,有点奇怪,域名中间为何会出现空格?


回头看/etc/resolv.conf文件,发现

search aaa.bbb ostechnix.lan

解释一下:
search:各项间以空格或者tab分隔,当域名没有以点结尾时,需要从这里追加各项,作为完全限定域名再发送DNS请求。

很明显,在解析search项的时候,没有用空格分隔开各项,导致DNS请求的域名存在错误。


查找出错的地方

查看redisson的源码,发现RedisClient的resolvAddr方法会对地址进行解析,如果/etc/resolv.conf里面存在多个DNS server的配置,会给每个配置都建立一个DnsNameResolver(这部分是属于netty-4.1.30.Final的源码)。

查看DnsNameResolver的源码:

    static {
        String[] searchDomains;
        try {
            List<String> list = PlatformDependent.isWindows()
                    ? getSearchDomainsHack()
                    : UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains();
            searchDomains = list.toArray(new String[0]);
        } catch (Exception ignore) {
            // Failed to get the system name search domain list.
            searchDomains = EmptyArrays.EMPTY_STRINGS;
        }
        DEFAULT_SEARCH_DOMAINS = searchDomains;
...
    }

searchDomains 是通过UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains() 来解析的

查看该方法源码:

    static List<String> parseEtcResolverSearchDomains(File etcResolvConf) throws IOException {
        String localDomain = null;
        List<String> searchDomains = new ArrayList<String>();

        FileReader fr = new FileReader(etcResolvConf);
        BufferedReader br = null;
        try {
            br = new BufferedReader(fr);
            String line;
            while ((line = br.readLine()) != null) {
                if (localDomain == null && line.startsWith(DOMAIN_ROW_LABEL)) {
                    int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
                    if (i >= 0) {
                        localDomain = line.substring(i);
                    }
                } else if (line.startsWith(SEARCH_ROW_LABEL)) {
                    int i = indexOfNonWhiteSpace(line, SEARCH_ROW_LABEL.length());
                    if (i >= 0) {
                        searchDomains.add(line.substring(i));
                    }
                }
            }
        } finally {
            if (br == null) {
                fr.close();
            } else {
                br.close();
            }
        }

Well,看来是netty对于search的解析有了新的想法,认为search是每项一行,所以木有再对每行进行空格或者tab的切割

searchDomains.add(line.substring(i));

解决办法

方法1:

把 UnixResolverDnsServerAddressStreamProvider 的源码 copy 到应用中,按照相同的package路径放置,然后修改 parseEtcResolverSearchDomains 方法,对每行进行split

方法2:

修改/etc/resolv.conf文件,把search改成每行一项,譬如:

nameserver 127.0.0.1


search aaa.bbb 
search ostechnix.lan

参考

  1. Introduction to the Domain Name System (DNS)
  2. Build your own DNS server on Linux (适用于redhat,centos)
  3. Install and configure DNS server in Ubuntu 16.04 LTS (适用于ubuntu)

PS:

  1. 在ubuntu上,/etc/resolv.conf 文件不能直接修改(会被定时重写),需要通过修改目录 /etc/resolvconf/resolv.conf.d/ 下的base, head, tail文件来完成,具体可以通过"man resolvconf"来查看相关说明,修改后使用命令“sudo resolvconf -u”可以马上复写/etc/resolv.conf 文件
  2. 在某些版本的ubuntu上,修改base文件是不会生效的,需要修改head文件来实现(虽然head文件说不要edit它,但是真的没办法)

Something Important !!!

如果你自己搭建了DNS server来模拟上述的实验,有可能还是出错,说连接不了sentinel。

以实验为例,每个DnsNameResolver在上述改动后,都会拿到2个domain(aaa.bbb 和 ostechnix.lan)。

你可以尝试改变这2个domain的顺序,譬如:

search ostechnix.lan aaa.bbb

或者

search ostechnix.lan
search aaa.bbb

再一次实验,就会发现这次竟然通过了。

Well,是不是很神奇?


调试一下 DnsResolveContext的 resolve方法

            searchDomainPromise.addListener(new FutureListener<List<T>>() {
                private int searchDomainIdx = initialSearchDomainIdx;
                @Override
                public void operationComplete(Future<List<T>> future) {
                    Throwable cause = future.cause();
                    if (cause == null) {
                        promise.trySuccess(future.getNow());
                    } else {
                        if (DnsNameResolver.isTransportOrTimeoutError(cause)) {
                            promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));
                        } else if (searchDomainIdx < searchDomains.length) {
                            Promise<List<T>> newPromise = parent.executor().newPromise();
                            newPromise.addListener(this);
                            doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], newPromise);
                        } else if (!startWithoutSearchDomain) {
                            internalResolve(hostname, promise);
                        } else {
                            promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));
                        }
                    }
                }
            });

顺带说一下,这个方法会一个一个searchDomain的去尝试。
着重调试:

promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));

你会发现,当发送DNS请求(redis.aaa.bbb)到自己的DNS时,会出现5000ms的超时错误。

可以在命令行进行尝试:

$ nslookup redis.aaa.bbb 127.0.0.1
Server:		127.0.0.1
Address:	127.0.0.1#53

** server can't find redis.aaa.bbb: SERVFAIL

忽略这个错误,注意测量一下耗时,是不是早就过了5s?

结合上面的代码,当出现timeout错误时,下一个domain是不会继续去连接的,所以当顺序为"[aaa.bbb, ostechnix.lan]"时,程序一样报错。

那这个5000ms的限制是哪里加入的咧?

再次调试源码,发现DnsNameResolver是由DnsNameResolverBuilder来构造的

public final class DnsNameResolverBuilder {
...
    private long queryTimeoutMillis = 5000;
...
解决办法

copy源码到应用目录,改一下这个数字,完事

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/helowken2/article/details/84327753

智能推荐

关于Android SQLite数据库版本升级的补充。。_android sqlite 版本直接是2导致没有新增字段-程序员宅基地

文章浏览阅读702次。犹豫最近比较忙,导致博客没有更新。今天上午终于_android sqlite 版本直接是2导致没有新增字段

SSH的两种登录方式-程序员宅基地

文章浏览阅读1.1k次。ssh客户端使用的是Xshell,windows环境。第一种方式,用户名密码方式原理如下:客户端发起ssh请求之后,服务器把自己的公钥传给客户端客户端输入服务器密码通过公钥加密之后传给服务器服务器根据自己的私钥解密登录密码,如果正确那么就让客户端登录Xshell操作如下:1.2.3.第二种方式基于秘钥的登录方式首先在客户端生..._在进行ssh配置时,可以允许用户使用以下何种方式进行登入

子空间的交与(直)和_空间的和和交公式-程序员宅基地

文章浏览阅读6k次,点赞6次,收藏16次。文章目录子空间的交也是子空间子空间的和子空间的和也是子空间子空间的交与和的结论维数公式证明推论子空间的交也是子空间V1,V2V_1,V_2V1​,V2​是子空间VVV的子空间那么,V1∩V2V_1\cap V_2V1​∩V2​也是VVV的子空间子空间的和V1,V2V_1,V_2V1​,V2​是线性空间VVV的子空间V1+V2V_1+V_2V1​+V2​是指:所有能表示成α1+α2..._空间的和和交公式

UPDATE SET a.id = (select) 关联多张表更新多条记录_update set a=(select)-程序员宅基地

文章浏览阅读4.3k次。UPDATE wallet_account_trade_record A SET A.shop_id =( SELECT c.shop_id FROM mob_checkout_counter.payment_data_info B,mob_checkout_counter.checkout_record c WHERE A.clie..._update set a=(select)

wince6.0 开发流程_wince6.0编程-程序员宅基地

文章浏览阅读401次。wince6.0 开发流程Windows CE概述 从6.0版本开始,Windows CE的名字改为Windows Embedded CE,当然这也是为了结合Windows Embedded品牌作出的改变。CE经过了十年的风风雨雨之后,终于在CE 6.0这个版本上再次浴火重生了。CE 6.0经历了CE历史上第二次内核重写,使CE操作系统更加符合当今嵌入式开发的方向。 CE 6.0在_wince6.0编程

前端开发使用vue脚手架快速构建前端项目_? target directory exists. continue? (abc1)-程序员宅基地

文章浏览阅读864次。前端开发使用vue脚手架快速构建前端项目一、使用步骤Vue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。1.配置node环境前往node官网下载并安装,地址https://nodejs.org/zh-cn/;& node -v 检测是否安装成功2.下载安装vue官方脚手架npm install -g @vue/cli& vue --version 检测是否安装成功3.通过cd命令进入你想要创建项目的文件夹&_? target directory exists. continue? (abc1)

随便推点

Lighttpd1.4.20源码分析之状态机(1)---状态机总览-程序员宅基地

文章浏览阅读523次。http://www.cnblogs.com/kernel_hcy/archive/2010/03/24/1694203.html前面讲了lighttpd的fdevent系统,从这一篇开始,我们将进入lighttpd的状态机。状态机可以说是lighttpd最核心的部分。lighttpd将一个连接在不同的时刻分成不同的状态,状态机则根据连接当前的状态,决定要对连接进行的处理以及下一步要进_lighttpd1.4.20源码分析

黑马《linux基础编程》学习笔记(从6到10)_linux编程基础黑马程序员课后习题-程序员宅基地

文章浏览阅读2.8k次。六. 绝对路径和相对路径 七. ls和常用参数八. cd和pwd命令九. rm命令十. cp命令实验操作//建立空的文件夹test[root@VM_0_15_centos home]# mkdir test[root@VM_0_15_centos home]# cd test[root@VM_0_15_centos test]# l..._linux编程基础黑马程序员课后习题

面试中的概率问题 - 数学期望(2) - 武器升级需要的宝石数-程序员宅基地

文章浏览阅读9.9k次,点赞4次,收藏14次。问题描述:这个笔试题来自今年的知名游戏公司,因为签了保密协定,为了避免麻烦,自行改编一下。 在一款游戏中,武器等级可以分为0-7级,武器每次升级需要一块宝石,每次升级可能出现三种情况:升一级、保持不变、降一级。 已知i->i+1升一级概率为Ai,保持不变概率为Bi,降一级概率为Ci。A0~A7,B0~B7,C0~C7均已知,其中从等级0到等级1必定成功,即A0=1,B0=0

基于51单片机的温度报警器设计_单片机温度传感器创新点分析-程序员宅基地

文章浏览阅读260次。关键词STC89C51单片机,数字控制,温度计,DS18B20 前言 随着人们生活水平的不断提高,单片机控制无疑是人们追求的目标之一,它所给人带来的方便也 是不可否定的,其中数字温度计就是一个典型的例子,但人们对它的要求越来越高,要为现代人工作、科研、生活、提供更好的更方便的设施就需要从数单片机技术入手,一切向着数字化控制,智能化控制方向发展。为了满足对对象的控制要求,单片机的指令系统均有极丰富的条件:分支转移能力,I/O口的逻辑操作及位处理能力,非常适用于专门的控制功能。的核心部分,品种繁多。_单片机温度传感器创新点分析

better-scroll插件什么是 better-scroll_betterscroll 是啥-程序员宅基地

文章浏览阅读197次。better-scroll 滚动插件什么是 better-scrollbetter-scroll 是一个移动端滚动的解决方案,它是基于 iscroll 的重写,它和 iscroll 的主要区别在这里。better-scroll 也很强大,不仅可以做普通的滚动列表,还可以做轮播图、picker 等等。better-scroll的滚动原理浏览器的滚动条大家都会遇到,当页面内容的高度超过视口高..._betterscroll 是啥

MLU370-M8部署stable-diffusion部署手册-程序员宅基地

文章浏览阅读462次,点赞10次,收藏11次。MLU370 跑sd_mlu370-m8部署stable-diffusion部署手册