Redis、Es内网网络映射问题排查及解决

Redis、Es内网网络映射问题排查及解决

背景

我们在客户环境上安装了SpringCloud应用,需要使用中间件Redis集群、ES集群,集群不可以自建、只能使用客户提供的集群(下面IP地址均为假IP)

Redis外网集群信息210.0.0.1:6379、210.0.0.1:6380、210.0.0.1:6381、210.0.0.1:6382、210.0.0.1:6383、210.0.0.1:6384

Redis内网集群信息:190.0.0.1:6379、190.0.0.1:6380、190.0.0.1:6381、190.0.0.1:6382、190.0.0.1:6383、190.0.0.1:6384

ES集群信息:210.0.1.1:9201

Nginx信息:210.0.1.2:9200

网络环境示意

请添加图片描述

ES网络问题:

错误信息:

[DataAsset] - 16:01:22.281 [es_rest_client_sniffer[T#1]] ERROR o.e.c.sniff.Sniffer - [run,141] - error while sniffing nodes 
java.net.ConnectException: Connection refused 
	at org.elasticsearch.client.RestClient.extractAndWrapCause(RestClient.java:918) 
	at org.elasticsearch.client.RestClient.performRequest(RestClient.java:299) 
	at org.elasticsearch.client.RestClient.performRequest(RestClient.java:287) 
	at org.elasticsearch.client.sniff.ElasticsearchNodesSniffer.sniff(ElasticsearchNodesSniffer.java:105) 
	at org.elasticsearch.client.sniff.Sniffer.sniff(Sniffer.java:209) 
	at org.elasticsearch.client.sniff.Sniffer$Task.run(Sniffer.java:139) 
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) 
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) 
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 
	at java.lang.Thread.run(Thread.java:748) 
Caused by: java.net.ConnectException: Connection refused 
	***************

排查过程:

现在服务器上调用了ES接口

# 获取索引的名称、文档数、存储大小等详细信息
http://210.0.1.1:9200/_cat/indices?v 
# 集群的名称、状态、节点数量、分片数量、未分配分片数量等信息
http://210.0.1.1:9200/_cat/health?v 
# 这个接口用来获取 Elasticsearch 中所有节点的详细信息
http://210.0.1.1:9200/_cat/nodes?v

这些接口都可以正常返回集群及相关信息。但是返回的内容和预期稍微有点不同。返回的集群节点相关信息都是127.0.0.1,和预期相差有点区别

浏览器可以和http://210.0.1.1:9200这个地址产生正确的交互,但是后台确一直报错,这让我们十分的不解。我们对源码进行了详细的排查。

发现异常栈下面这段代码

at org.elasticsearch.client.RestClient.performRequest(RestClient.java:299) 

发现它对异常进行了try catch并且对异常进行了打印。

RequestLogger.logFailedRequest(logger, request.httpRequest, context.node, e);

随后我们开启了logback的debug级调试。

logging.level.root=debug

随后我们获取到了全新日志。

[DataAsset] - 17:26:42.666 [es_rest_client_sniffer[T#1]] DEBUG o.a.h.i.n.c.PoolingNHttpClientConnectionManager - [requestConnection,279] - Connection request: [route: {}->http://127.0.0.1:9201][total kept alive: 1; route allocated: 0 of 10; total allocated: 1 of 30] 
[DataAsset] - 17:26:42.669 [pool-4-thread-1] DEBUG o.a.h.i.n.c.PoolingNHttpClientConnectionManager - [failed,316] - Connection request failed 
java.net.ConnectException: Connection refused 
	at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 
	at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:716) 
	at org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor.processEvent(DefaultConnectingIOReactor.java:174) 
	at org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor.processEvents(DefaultConnectingIOReactor.java:148) 
	at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor.execute(AbstractMultiworkerIOReactor.java:351) 
	at org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager.execute(PoolingNHttpClientConnectionManager.java:221) 
	at org.apache.http.impl.nio.client.CloseableHttpAsyncClientBase$1.run(CloseableHttpAsyncClientBase.java:64) 
	at java.lang.Thread.run(Thread.java:748)

在本地调试过程种发现上层是由于使用了ESSniffer,导致我们的ES失效了。

Sniffer sniffer = Sniffer.builder(restClient).setSniffIntervalMillis(30000).build();

为什么日志中报错连接地址为 http://127.0.0.1:9201 ,这个地址哪里来的,为什么会连接9201端口?我们只是显示的配置了210.0.1.2:9200,完全不应该出现这样的情况才对,然后根据堆栈信息我们向上排查,在ElasticsearchNodesSniffer这个类中发现了关键信息。

this.request = new Request("GET", "/_nodes/http");
request.addParameter("timeout", sniffRequestTimeoutMillis + "ms");

发现在源码中使用了下面这个接口

#  Elasticsearch 集群中所有节点的 HTTP(S) 端口信息。它提供了每个节点的IP地址、HTTP(S)监听的端口号、协议、主机名等信息
http://210.0.1.1:9200/_nodes/http

在响应体中确实发现了返回127.0.0.1:9201这个内网IP。思路从找到127.0.0.1:9201转向了如何修改这个地址。

在搜索es sniffer过程中,查到了相关资料在

https://www.elastic.co/cn/blog/elasticsearch-sniffing-best-practices-what-when-why-how

在官网的以下节点可以得出答案!

什么场景适合使用监听功能?

  • 如果您的 Elasticsearch 集群位于负载均衡器后面会怎样?

此处为对http节点的配置的相关解释

https://www.elastic.co/guide/en/elasticsearch/reference/7.8/modules-http.html#_http_settings

下方为ES网络配置的相关说明

[]: https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-network.html “ES官网”

解决方案:

1.解决方案http.publish_porthttp.publish_host,将前方两个配置修改为210.0.1.2:9200(实际的端口地址)后,问题解决!

2.在官网:https://discuss.elastic.co/t/error-while-sniffing-nodes/291186/2

中有类似的人提问,这位同学禁用了ElasticsearchRestClientAutoConfiguration这个类达到了相似的效果,由于我们没有使用这个自动装配,所以暂时无法测试。

Redis网络问题:

错误信息:

日志不便展示。

我们的日志信息是这样的,我们的Redis集群有6个节点、标准的3主3从结构。210.0.0.1:6379、210.0.0.1:6380、210.0.0.1:6381、210.0.0.1:6382、210.0.0.1:6383、210.0.0.1:6384

我们在配置文件中配置为:

spring:
  redis:
    cluster:
      max-redirects: 5
      nodes: 210.0.0.1:6379、210.0.0.1:6380、210.0.0.1:6381、210.0.0.1:6382、210.0.0.1:6383、210.0.0.1:6384
    password: 123456

很奇怪的是我们在业务中连接redis的过程中,前台报错190.0.0.1:6384,有的时候连的上,有的时候连不上,这个节点redis连不上,奇怪的是,这个IP是我们第六个redis,而且我们没有任何地方配置了它们的内网地址,迷惑????????????

在redis-cli上使用cluster info的时候反馈了很多地址,都是190.0.0.1:6379、190.0.0.1:6380、190.0.0.1:6381、190.0.0.1:6382、190.0.0.1:6383、190.0.0.1:6384

排查过程:

我们底层使用了LettuceConnectionFactorySpring默认的Redis集群工具初始化的Redis连接,它的底层是由 Lettuce 通过 接口ClusterTopologyRefresh,具体的实现类是:DefaultClusterTopologyRefresh定期检查集群的拓扑,确保在集群节点发生变化时更新拓扑信息。它会周期性地向任意一个节点发送 CLUSTER NODES 命令并解析响应,将新的拓扑信息存储在 io.lettuce.core.cluster.models.partitions.Partitions 对象中。
下方代码个人增加了部分中文注释(如果有错误,欢迎指出)

        if (!isEventLoopActive()) {
            return CompletableFuture.completedFuture(Collections.emptyMap());
        }

        long commandTimeoutNs = getCommandTimeoutNs(seed);
        ConnectionTracker tracker = new ConnectionTracker();
        long connectionTimeout = commandTimeoutNs + connectTimeout.toNanos();
        openConnections(tracker, seed, connectionTimeout, TimeUnit.NANOSECONDS);
		
        CompletableFuture<NodeTopologyViews> composition = tracker.whenComplete(map -> {
            return new Connections(clientResources, map);
        }).thenCompose(connections -> {
            // 创建异步请求,向Redis Server发出请求,或者Cluster info以及Cluster nodes
            Requests requestedTopology = connections.requestTopology(commandTimeoutNs, TimeUnit.NANOSECONDS);
            Requests requestedInfo = connections.requestInfo(commandTimeoutNs, TimeUnit.NANOSECONDS);
            // Requests对象封装了Map<RedisURI, TimedAsyncCommand<String, String, String>>的一个成员变量,
            // 下面这个位置是说等待这两个request对象请求完成后,再进行调用
            return CompletableFuture.allOf(requestedTopology.allCompleted(), requestedInfo.allCompleted())
           	// 在getNodeSpecificViews 方法中从异步请求中获取了【集群信息】和【集群节点的信息】
                    .thenApplyAsync(ignore -> getNodeSpecificViews(requestedTopology, requestedInfo),
                            clientResources.eventExecutorGroup())
                    .thenCompose(views -> {
                        if (discovery && isEventLoopActive()) {

                            Set<RedisURI> allKnownUris = views.getClusterNodes();
                            Set<RedisURI> discoveredNodes = difference(allKnownUris, toSet(seed));

                            if (discoveredNodes.isEmpty()) {
                                return CompletableFuture.completedFuture(views);
                            }

                            openConnections(tracker, discoveredNodes, connectionTimeout, TimeUnit.NANOSECONDS);

                            return tracker.whenComplete(map -> {
                                return new Connections(clientResources, map).retainAll(discoveredNodes);
                            }).thenCompose(newConnections -> {

                                Requests additionalTopology = newConnections
                                        .requestTopology(commandTimeoutNs, TimeUnit.NANOSECONDS).mergeWith(requestedTopology);
                                Requests additionalInfo = newConnections.requestInfo(commandTimeoutNs, TimeUnit.NANOSECONDS)
                                        .mergeWith(requestedInfo);
                                return CompletableFuture
                                        .allOf(additionalTopology.allCompleted(), additionalInfo.allCompleted())
                                        .thenApplyAsync(ignore2 -> getNodeSpecificViews(additionalTopology, additionalInfo),
                                                clientResources.eventExecutorGroup());
                            });
                        }

                        return CompletableFuture.completedFuture(views);
                    }).whenComplete((ignore, throwable) -> {

                        if (throwable != null) {
                            try {
                                tracker.close();
                            } catch (Exception e) {
                                logger.debug("Cannot close ClusterTopologyRefresh connections", e);
                            }
                        }
                    }).thenCompose((it) -> tracker.close().thenApply(ignore -> it)).thenCompose(it -> {

                        if (it.isEmpty()) {
                            Exception exception = tryFail(requestedTopology, tracker, seed);
                            return Futures.failed(exception);
                        }

                        return CompletableFuture.completedFuture(it);
                    });
        });

        return composition.thenApply(NodeTopologyViews::toMap);

RedisURI对象中的Value属性中发现了TimedAsyncCommand对象中的result属性中包含了集群信息,集群信息中可以发现都是内网的IP

解决方案:

找了很多大佬,提供了大概4种解决方案。

1.在使用Redis地址的时候,修改源码,将Redis使用的地址映射成新的外网地址

2.使用MappingSocketAddressResolver 这个类,用于网络映射的解析

https://github.com/lettuce-io/lettuce-core/discussions/1872

3.也有大佬说使用下面这个方法可以改善这个问题

 public JedisConnectionFactory jedisConnectionFactory() {
        RedisClusterConfiguration config = new RedisClusterConfiguration(Arrays.asList(redisNodes));
        config.setMaxRedirects(maxRedirects);
        return new JedisConnectionFactory(config);
    }

4.解决方案Redis-server的配置,在其配置文件种加入下面这个配置,可以让它反馈的节点地址变成下面这个

cluster-announce-ip 210.0.1.2:9200
cluster-announce-port 9200

5.使用iptables组件
在最后补一个最新的解决方案
参考自:
https://cloud.tencent.com/developer/article/2348224

sudo iptables -t nat -A OUTPUT -d 172.17.0.2 -p tcp --dport 8001 -j DNAT --to-destination 10.8.46.40:8001
sudo iptables -t nat -A OUTPUT -d 172.17.0.3 -p tcp --dport 8002 -j DNAT --to-destination 10.8.46.40:8002
sudo iptables -t nat -A OUTPUT -d 172.17.0.4 -p tcp --dport 8003 -j DNAT --to-destination 10.8.46.40:8003
$ sudo iptables -t nat -nvL --line-number
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1       95  6219 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1        0     0 DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL
2        0     0 DNAT       tcp  --  *      *       0.0.0.0/0            172.17.0.2           tcp dpt:8001 to:10.8.46.40:8001
3        0     0 DNAT       tcp  --  *      *       0.0.0.0/0            172.17.0.3           tcp dpt:8002 to:10.8.46.40:8002
4        0     0 DNAT       tcp  --  *      *       0.0.0.0/0            172.17.0.4           tcp dpt:8003 to:10.8.46.40:8003

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1        6   360 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0           

Chain DOCKER (2 references)
num   pkts bytes target     prot opt in     out     source               destination         
1        0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0 
  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值