Redisson-3.8 查找DNS异常的解决办法

背景

  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源码到应用目录,改一下这个数字,完事

### 回答1: Spine-Unity-3.8-2021是一个Spine动画的Unity插件,可以让开发人员在游戏开发过程中轻松实现Spine动画。这个插件包含了最新的Spine Runtime库,可以让开发人员在使用Spine动画时获得更好的性能和可靠性。此版本还提供了对C# 9的支持,并修复了在以前的Spine Unity版本中存在的一些问题和异常。此外,此版本也提供了一些改进和优化,能够让开发人员更快速、更容易地实现动画效果并提高游戏的运行效率。总的来说,Spine-Unity-3.8-2021是一个值得开发人员关注和使用的插件,它可以帮助游戏开发人员更好地实现动画效果,提升游戏的质量和用户体验。 ### 回答2: spine-unity-3.8-2021是一款在Unity中使用的Spine动画软件包。Spine是一种2D骨骼动画软件,它可以帮助制作2D游戏中的角色动画。与传统的逐帧动画相比,Spine骨骼动画具有更高的效率和更好的表现效果。 spine-unity-3.8-2021是Spine运行在Unity中的版本。它提供了一个简便的方式来将Spine制作的动画集成到Unity游戏中。spine-unity-3.8-2021拥有许多实用的功能,例如动画的播放、循环、暂停和停止等。 此外,spine-unity-3.8-2021还支持动画的混合、遮罩、缩放等高级特性。这些功能可以大大提升2D游戏的动画表现效果。 总之,spine-unity-3.8-2021是一款强大的Spine骨骼动画软件包,它可以帮助Unity开发者更方便、更高效地制作2D游戏中的动画效果。 ### 回答3: Spine-Unity-3.8-2021是Spine动画引擎的一个版本,其主要特点是可以与Unity引擎无缝集成,提供了高效、灵活、可定制的动画解决方案。此版本相比之前版本,主要增加了一些新功能和改进,如支持GPU动画混合、2D环境的自适应、高效的顶点色边框渲染、Spine Atlas纹理集加载、支持Mecanim(动画过渡和状态机和蒙太奇)和 IK姿势、环境光遮蔽、大量优化和 bug 修复等。除此之外,它还易于使用和实现,并具有快速迭代的能力,可以让开发者轻松创建精美的动画效果,提升游戏的用户体验。该版本是Spine引擎的主要升级版本之一,同时也体现出Spine开发团队对于产品需求和用户反馈的重视和努力,不仅提高了动画制作领域的生产力和创造力,也为游戏行业推陈出新提供了有力支持。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值