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关机及重启_FLY_THINK2012的博客-程序员秘密

步骤一:编写Java代码java部分代码: case R.id.broadcast_reboot: //重启 Log.v("Reboot", "R...

文字溢出换行(单行或多行)_TinaAsura的博客-程序员秘密

单行文本换行css需设置属性:overflow: hidden;text-overflow: ellipsis;white-space: nowrap;多行文本换行css需设置属性:(-webkit内核才有效)overflow : hidden;display: -webkit-box;-webkit-line-clamp:3;-webkit-box-orient: vertical;-...

黑马程序员---SQL基础之用T-SQL代码实现数据库、表等的创建_weixin_33898876的博客-程序员秘密

---------------------- Windows Phone 7手机开发、.Net培训、期待与您交流! ----------------------近几天学习了SQL的一些基础,现在总结一下通过T-SQL代码来实现数据库、表的创建。创建&amp;删除数据库创建数据库create database stuDBon(  name='stuDB_data',...

大数据啊大数据!_浪尖聊大数据-浪尖的博客-程序员秘密

首先,祝大家双十一快乐。开始本文之前,希望大家参与一下下面的投票。做这个投票的主要原因是最近经常有找浪尖咨询大数据,自学,培训及找工作的事情,问题归类如下:大数据要不要培...

nyoj85——炮兵阵地——————【状态压缩、动态规划】_aGoshawk的博客-程序员秘密

/**   结题思路:用dp[r][i][j]表示第r行r行状态为i,r-1行状态为j时的最大可部署炮兵的个数。通过求解合法状态,缩小需要遍历的状态范围。同时求出各个合法状态的二进制中1的个数,然后特殊处理首行,然后dp求解每行每种可行状态对应的可部兵的最大个数。状态转移方程dp[r][i][j]=max(dp[r][i][j],dp[r-1][j][k]+num_1[i])。*/

华为终于放出方舟编译器源代码!开源平台同步亮相,网友:硬核项目_QbitAl的博客-程序员秘密

乾明 边策 发自 凹非寺量子位 报道 | 公众号 QbitAI华为方舟编译器终于正式开源,源代码放出,兑现了在8月开源的承诺。代码不在GitHub,而是在自家开源平台上...

随便推点

20个代码生成框架_dizhong1566的博客-程序员秘密

1.1 CodeSmith一款人气很旺国外的基于模板的dotnet代码生成器官方网站:http://www.codesmithtools.com官方论坛:http://forum.codesmithtools.com/default.aspx版权形式:30天试用开源:否需要先注册确认后才能下载1.2 MyGeneratorMyGenerator是又一个国外...

ElasticSearch利用updateByQuery同时修改多个字段_渊云的博客-程序员秘密

环境; 阿里云ElasticSearch6.7.0需求: 利用updateByQuery同时修改一条记录的多个字段POST /ads_lading_trade_brief_es/_update_by_query?conflicts=proceed&amp;wait_for_completion=false{ "script": { "source": "ctx._source.exporter_province='浙江省';ctx._source.exporter_city='绍兴市'",

一个人生活,如何摆脱孤独提升幸福感?_谭正强的博客-程序员秘密

算起来,我已经一个人生活了很久。一个人生活,难免连周边的空气里都有种颓靡感。一个人逛街,在姹紫嫣红里盯着广告牌发呆,扑面而来的人群几欲将你的寂寞打翻;一个人看电影,无人讨论便默默回味着剧情,影子被路灯拉的笔长;一个人吃饭,都不好意思进入那些嘈杂的饭馆去,坐在圆桌边,慢悠悠的品上几菜几汤。……一个人的寂寞,更多是来自于快乐时无人分享,难过时无人倾诉。一个人常常要去担心很多问题,比如钥匙有没有带

有关路径优化遗传算法原理(结合matlab代码)_基于遗传算法的路径优化_张叔zhangshu的博客-程序员秘密

1、遗传算法介绍遗传算法是类借鉴生物界的进化规律(适者生存,优胜劣汰遗传机制)演化而来的随机化搜索方法,其主要特点是直接对结构对象进行操作,不存在求导和函数连续性的限定,具有内在的隐并行性和更好的全局寻优能力,采用概率化的寻优方法,能自动获取和指导优化的搜索空间,自适应地调整搜索方向,不需要确定的规则。以这篇博文代码篇为例(点击打开)本文采用代码与原理相结合的办法给大家讲解遗传算法,有助于大家更好的理解遗传算法。在遗传算法中包括几个重要的步骤,分别为 个体、种群、适应度、选择、交叉、变异,下边为大家

使用 EasyExcel 动态添加自增序号列_easyexcel 序号_庄周de蝴蝶的博客-程序员秘密

前言本文将介绍如何通过使用EasyExcel自定义拦截器实现在最终的Excel文件中新增一列自增的序号列,最终的效果如下:此外,本文所使用的完整代码示例已上传到GitHub。实现本文主要是通过自定义一个继承AbstractRowWriteHandler的拦截器来实现在最终导出的结果中新增序号列,通过修改源码中保存头部标题的Map内容来给自己添加的序号列留出位置,先展示最终的代码:/** * 自定义 excel 行处理器, 增加序号列 * * @author butterfly * @da