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)
在本地调试过程种发现上层是由于使用了ES的Sniffer,导致我们的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_port、http.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文章来源:https://www.toymoban.com/news/detail-791541.html
5.使用iptables组件
在最后补一个最新的解决方案
参考自:
https://cloud.tencent.com/developer/article/2348224文章来源地址https://www.toymoban.com/news/detail-791541.html
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
到了这里,关于Redis、Es内网网络映射问题排查及解决的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!