[alibaba/nacos]nacos 2.x 节点间心跳转发优化的建议

2024-03-04 685 views
8

在 DistroFilter.java 中,有如下代码:

RestResult<String> result = HttpClient
        .request(HTTP_PREFIX + targetServer + req.getRequestURI(), headerList, paramsValue, body,
                PROXY_CONNECT_TIMEOUT, PROXY_READ_TIMEOUT, StandardCharsets.UTF_8.name(), req.getMethod());
String data = result.ok() ? result.getData() : result.getMessage();
try {
    WebUtils.response(resp, data, result.getCode());
} catch (Exception ignore) {
    Loggers.SRV_LOG.warn("[DISTRO-FILTER] request failed: " + distroMapper.mapSrv(distroTag) + urlString);
}

建议在 WebUtils.response 前添加一行:

resp.setHeader("Connection", "close");

如果没有这行,即使心跳发给了非责任节点,依旧会 keepalive,加了后,不再 keepalive,2.x 是通过 ip:port 来确定责任节点,这意味着 client 的责任节点是固定的,而不像 1.x 是不固定的。若发给非责任节点,则不 keepalive,默认 keepalive,那么,client 在几次心跳后,就会和其责任节点 keepalive,从而大幅减少 nacos 内部的心跳转发。(这里假设前置了 slb,在不 keepalive 的情况下,心跳是平均的发给 nacos 各个节点)

回答

2
  1. 1.x和2.x的责任节点都是固定的,IP:port和serviceName没有本质区别。
  2. keepalive的优化是否有必要?能提高多少?需要数据支撑,而且2.x推荐升级到对应的客户端版本,换成长连接,其实就更用不到keepalive了。
2

ip:port 和 serviceName 是有区别的,同一个 java 服务,可能提供了上百个 dubbo 接口,这些 dubbo 接口的责任节点在 2.x 时是固定的,在 1.x 时是不固定的,因为 服务名并不固定,而 ip:port 是固定的。

升级 sdk 是个方向,但是我们数千个微服务,这是一个浩大的工程,业务侧升级 sdk 搞性能优化,未必有时间排期,且担忧未知的风险。性能提升方面,理论上,3 节点的集群,client 发送 3 个心跳,每个节点收到 1 个,而责任节点只有一个,其余两个请求都将在 nacos 内部转发,即,本来集群可以处理 5 个心跳,现在只能处理 3 个,更多的数据支持,我准备近期在我们的测试环境做个实验。

8
  1. 所以你们的场景是ip 少, 但是每个ip的服务多?
  2. 3 节点的集群,client 发送 3 个心跳,每个节点收到 1 个,而责任节点只有一个,其余两个请求都将在 nacos 内部转发, 这个没任何区别啊, 发送请求是随机的, 同一个服务的也有2/3概率被转发, 同一个ip:port也是2/3被转发。
7
  1. 考虑这样一个场景,一个 pod 有 40 dubbo 服务,假设 pod ip 为 10.10.10.10,由于 dubbo 对外的端口是 20880,那么 ip:port 确定责任节点的情况下,这 40个 dubbo 服务的责任节点是同一个,如果是服务名确定责任节点,则是 40 个,然后分布在 nacos 各个节点上;
  2. 对于一个 pod 而言,责任节点只有一个时,可以考虑优化 keepalive,即若心跳发送给非责任节点,则 close,否则不用改动,保持 keepalive 即可,这样子内部的转发将大面积减少;
  3. 对于一个 pod 而言,如果责任节点有多个,这样优化后 time wait 的数量将飙升,因为 2/3的心跳请求给了非责任节点,而非责任节点会 close,A 服务的心跳假设发送给了对应的责任节点,则 keepalive,而由于 keepalive,B 服务的也会发给A的责任节点,而 A、B 的责任节点可能不是同一个,若优化,则会 close

我在我们测试环境做了验证,测试环境 7228 个 nacos 服务,13039 个 ip, 优化前,三台 nacos 的 cpu 占用分别为:33%、34%、33%,优化后 为:25%,25%,27% QPS 优化前为:3k,2.9k,3k 优化后:2.2k,2.3k,2.5k

ip:port 模式下,dubbo consumer 的心跳里,port 为 0,对此,修改了 DistroIpPortTagGenerator.java 修改后如下:

private static final Cache<String, String> IP_PORT_MAP = CacheBuilder
        .newBuilder()
        .maximumSize(204800)
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .build();

@Override
public String getResponsibleTag(ReuseHttpServletRequest request) {
    String ip = request.getParameter(PARAMETER_IP);
    String port = request.getParameter(PARAMETER_PORT);
    if (StringUtils.isBlank(ip)) {
        // some old version clients using beat parameter
        String beatStr = request.getParameter(PARAMETER_BEAT);
        if (StringUtils.isNotBlank(beatStr)) {
            try {
                RsInfo rsInfo = JacksonUtils.toObj(beatStr, RsInfo.class);
                ip = rsInfo.getIp();
                port = String.valueOf(rsInfo.getPort());
            } catch (NacosDeserializationException ignored) {
            }
        }
    }
    if (StringUtils.isNotBlank(ip)) {
        ip = ip.trim();
    }
    port = StringUtils.isBlank(port) ? DEFAULT_PORT : port.trim();
    if (DEFAULT_PORT.equals(port)) {
        String tmpPort = IP_PORT_MAP.getIfPresent(ip);
        if (StringUtils.hasText(tmpPort)) {
            port = tmpPort;
        }
    } else {
        IP_PORT_MAP.put(ip, port);
    }
    return ip + InternetAddressUtil.IP_PORT_SPLITER + port;
}

修改前,拉取了 1 小时的 access log,对于 nacos A 节点,B、C 节点的请求数分别为:1225395、1222911,优化后为:32904、32798,从 120万减少到 3万

不过,我这样的优化,似乎不具备通用性,如果方便,还是升级 nacos client 更直接,我们是由于全面转 go,这种情况下,存量的 java 服务,全面升级 nacos client 就没必要了。优化心跳并非心血来潮,去年 7-8月份,我们生产环境 nacos 集群曾经故障了 40 分钟,服务不停的注册、下线、注册,最后,我们排查出的结果是心跳转发,当时的 nacos server 版本为 1.3.2,使用的 httpclient 我记得超时设置了 -1,即永不超时,httpclient 的线程数,也是和核数有关,测试环境是自建的机房,cpu 60核,而生产是 aws 和 阿里云,4核,所以故障只出现在了生产,不过该问题,社区已经修复了。 @zrlw

之所以提出该问题,是因为当时并不知道 dubbo 的 consumer 心跳 port 为0,所以结论是,只需要在 DistroFilter.java 中加入一行 resp.setHeader("Connection", "close"); 便可以大幅减少 nacos server 内部的心跳转发(client 使用 http 时),这种优化是值得的,我最终的实现上,除了加了这行,还改了 DistroIpPortTagGenerator,虽然也达到了预期的目的,不过更像是定制的优化。

7

的确是定制化的优化, 是针对这个pod少,但是每个pod服务特别多的场景。

但是实际上大规模部署的时候, 应该是pod多,服务数有一定控制的情况下, 不然就不是微服务而是单机应用了。

因此在大规模部署的场景下, 其实ip:port的打散比服务打散更能解决转发热点问题。

resp.setHeader("Connection", "close"); 这个优化可能是可以加入的, 这个可能是通用的, 但是DistroIpPortTagGenerator应该是定制优化。

至于只有resp.setHeader("Connection", "close"); 的优化程度有多少, 大规模部署的时候会不会反而是负优化,需要重新考量和测试,

2

resp.setHeader("Connection", "close"); 这个优化可能是可以加入的

在不改造 DistroIpPortTagGenerator 的情况下,加入这个可能会让情况更糟,如果一个 pod 上有 100个 dubbo 服务,consumer 和 provider 各为 50个,由于 consumer 并不上报 port,其心跳请求的 port 值为 0,provider 会上报真实的 port,加入这行,client 将频繁的断开连接(发送 consumer 心跳时)最终导致 server 端 time wait 状态的请求数激增。

当然,如果不是作为 dubbo 的注册中心,而是 nacos discovery 的,那么加入这行还是非常好用的,确实能大幅减少心跳的转发,由于心跳转发减少,其他节点的请求数也相应减少,性能改进不少(类似 client 升级到 2.x 使用长连接一样)

另外有一个疑问,为什么使用 DistroIpPortTagGenerator 而不是 DistroIpTagGenerator 呢,即为什么不考虑直接使用 client ip 来确定责任节点呢?如果确定责任节点时,仅考虑 client ip,则这个优化就很有用,在考虑 port 的情况下,这个优化很鸡肋。

2

DistroIpPortTagGenerator 比 DistroIpTagGenerator 更细粒度同时不会减少转发量。

转发量的来源是客户端进程数, 同一个进程注册的服务基本上都是同一个ip port。 如果是按照ip区分,同一个机器启动的不同进程会分配到同一个实例转发,但是发起请求的时候2个进程是随机的, 无所谓ip port还是ip。

所以从服务端角度来看, 概率一致,但可以更细粒度区分责任节点,做打散。

2

多谢,如果是微服务,不存在一个机器启动多个进程使用多个端口的情况,那么 DistroIpPortTagGenerator 和 DistroIpTagGenerator 其实是没区别的了,使用 ip 和 port 来确定,是为了更细粒度,使流量更均衡,并没有其他隐藏的逻辑。

我们使用的 nacos server 其实是二开的,主要是容灾、mesh 这块,如果没隐藏的逻辑(比如类似 spring boot 的约定那样)这块我就改了。

6

社区这个地方应该不会打散逻辑,会一直用IP:port来判定,随意修改可能会导致兼容性问题。

如果您已经改成ip并且运行良好, 可以继续使用。