[alibaba/nacos]2.0.3 升级问题,upgrade check 总是 false,以及平滑升级问题,nacos 采用了 hash 算法,而不是 hash 一致算法,导致扩容时波动很大。

2024-07-18 470 views
3

我们公司大范围使用 nacos,大约有 300 多个微服务,而 dubbo service 个数大约有 2000,我最近在测试环境升级到 2.0.3 后,upgrade check result 一直显示是 false,这和 https://github.com/alibaba/nacos/issues/5638 这个 issues 里的情况基本一样,但是这个 issue 没有写明解决方案就已经关闭了。

我发现有一个类 MockSelfUpgradeChecker.java ,如果将 upgrading.checker.type 设置为 mock 那么 upgrade check result 将直接返回 true,而不关心是否有双写任务等。那么,如果将这个参数设置为 mock,之后直接给升级了,会有什么影响呢?会不会造成双写数据丢失,或者集群崩溃等问题呢?

回答

4

同样重启就会出现,需要不断重启集群才能恢复,生产环境影响有点多

3

我目前的做法是:写了一个接口,这个接口调用后,若无同步任务,upgrade check result 直接返回 true,目前在测试环境并没有因此引起别的 bug。

但是 nacos 本身并不是无状态的,而是通过服务名字的 hash 值做路由,这导致无法扩容或者缩容,因为一旦有新节点加入或者离开,整个集群的流量都将重新分配,这个过程中,我这边 dubbo 报 no provider 高达 1000 多次,要从 1.3.2 无缝升级到 2.0.3 (至少不影响 dubbo 调用 )其实很复杂也很艰难,除非我们确定升级过程中流量重新分配,dubbo no provider 不影响业务,否则只能通过运行时 aop 技术进行干预 nacos 的流量转发逻辑,nacos 本来是 普通的 hash 算法,得通过运行时 aop 技术改为 hash 一致算法,且验证通过后,才敢在正式环境升级。

后续如果我个人通过运行时 aop 技术顺利的,不影响 dubbo 调用的,升级了集群,我再补充。

3

DistroFilter 的hash 而不是 一致性hash, 1.x版本的老问题了,当客户端和服务端都升级到2.x版本之后,用长连接代替了DistroFilter的权威节点计算和转发,反而解决了扩容时整个集群流量重新分配的问题。不过带来了新的问题,就是新加入集群的节点不容易分配到流量,长连接没有异常的情况下不会主动断开重选节点主动做负载均衡。

至于1.x版本到2.x版本的升级,其实我没有试过,我们都是直接用2.x上线,并且在上线的源码里面直接启动时强制关闭双写逻辑了,因为升级这一块想要不出问题太难了。。。。

3

我说下我最终的方案:

首先,dubbo 获取 service provider hosts 信息时,调用了 InstanceController 里的 list 方法,而 nacos client sdk 在解析时,如果 hosts 为空,则会清空本地缓存数据,这个 问题虽然修复了,但需要 client 升级到 1.4.1,且需要 dubbo 配置 dubbo.registry.parameters.namingPushEmptyProtection=true,另外,nacos server 还会主动推送 空的 hosts 给 client,client 收到后,也会清空本地缓存,要解决 no provider 问题,就需要解决这两个问题,方案有两个:

1、修改 nacos 2.0.3 源码,使其不返回空 hosts,即 hosts 为空时不推送,若调用接口则返回 null,然后部署三个节点,数据库和旧版 nacos server 共享。 2、瞬间停掉 旧的 nacos 集群,此时 client 本地缓存还在,业务没影响; 3、修改 slb,由于 nacos 2.0.3 不会返回 空 hosts 也不会推送(因为我们改了源码),所以也没影响。 但该方案影响 心跳,会有一堆心跳失败,配置监听失败的报错。

还有一个方案: 1、同上,修改 nacos 2.0.3 源码,部署一套新集群,且数据库和旧版 nacos server 共享。 2、通过 jvm-sandbox 修改 旧版 nacos 的逻辑(运行时 aop 技术,不需要重启 nacos 集群)使其不推送,且 如果InstanceController.doSrvIpxt 返回 空 hosts,则返回 null 然后加载 jvm-sandbox 以及写的模块 3、修改 slb

这两个方案都会影响服务发布,如果不想影响服务发布,则需要 nacos 2.0.3 具有从 nacos 1.3.2 同步服务 hosts 的能力,也很简单,不过影响发布问题也不大。

我已通过方案二 升级了我们的集群,接近 3000个 dubbo 服务,接近 300 个微服务。沙箱模块代码可以如下:

@MetaInfServices(Module.class)
@Information(id = "nacosUpdater")
public class NacosUpdater implements Module {

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    @Command("cacheIp")
    public void cacheIps() {
        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.alibaba.nacos.naming.controllers.InstanceController")
                .onBehavior("doSrvIpxt")
                .onWatch(new AdviceListener() {

                    @Override
                    protected void afterReturning(Advice advice) throws Throwable {
                        Object object = advice.getReturnObj();
                        if (!(object instanceof ObjectNode)) {
                            System.out.println("this is a bug return not ObjectNode");
                            return;
                        }
                        ObjectNode objectNode = (ObjectNode) object;
                        JsonNode jsonNode = objectNode.get("hosts");
                        if (!(jsonNode instanceof ArrayNode)) {
                            System.out.println("this is a bug return not ArrayNode");
                            return;
                        }
                        ArrayNode arrayNode = (ArrayNode) jsonNode;
                        if (!arrayNode.isEmpty()) {
                            return;
                        }

                        ProcessController.returnImmediately(null);
                    }
                });

        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.alibaba.nacos.naming.push.PushService")
                .onBehavior("onApplicationEvent")
                .onWatch(new AdviceListener() {

                    @Override
                    protected void before(Advice advice) throws Throwable {
                        ProcessController.returnImmediately(null);
                    }
                });

        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.alibaba.nacos.naming.push.PushService")
                .onBehavior("udpPush")
                .onWatch(new AdviceListener() {

                    @Override
                    protected void before(Advice advice) throws Throwable {
                        ProcessController.returnImmediately(null);
                    }
                });
    }
}

最后补充一句,修改后的 nacos 2.0.3 即使三个节点都宕机也不影响已有业务(但影响服务发布)这个 issue 关闭。

1

@ijustyce 有个细节咨询下, 目前我们client和server都是1.4.1, 在尝试对server进行扩容时, 出现了dubbo No Provider的情况. 根据您提供的信息, 定位到可能是扩容后新节点数据未及时同步问题. 这块计划使用你方案里的 dubbo.registry.parameters.namingPushEmptyProtection=true 进行解决. 不过文章中有个点很疑惑, "nacos server 还会主动推送 空的 hosts 给 client,client 收到后,也会清空本地缓存" 这块我看下来1.4.x应该是通过PushService里的udp进行推送的, 推送后最终会交由Client的 com.alibaba.nacos.client.naming.core.HostReactor#processServiceJson 进行处理, 假如dubbo.registry.parameters.namingPushEmptyProtection设置为true里, Client里既有的instance信息应该不会被覆盖了哇.

是有其他场景么?

6

nacos server 会主动推送,nacos client 会每隔 10秒主动拉取,但最终都会走到 ServiceInfoHolder.java 中 processServiceInfo 方法

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
    String serviceKey = serviceInfo.getKey();
    if (serviceKey == null) {
        return null;
    }
    ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
    if (isEmptyOrErrorPush(serviceInfo)) {
        //empty or error push, just ignore
        return oldService;
    }
    serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
    boolean changed = isChangedServiceInfo(oldService, serviceInfo);
    if (StringUtils.isBlank(serviceInfo.getJsonFromServer())) {
        serviceInfo.setJsonFromServer(JacksonUtils.toJson(serviceInfo));
    }
    MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
    if (changed) {
        NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
                JacksonUtils.toJson(serviceInfo.getHosts()));
        NotifyCenter.publishEvent(new InstancesChangeEvent(notifierEventScope, serviceInfo.getName(), serviceInfo.getGroupName(),
                serviceInfo.getClusters(), serviceInfo.getHosts()));
        DiskCache.write(serviceInfo, cacheDir);
    }
    return serviceInfo;
}

如果设置了 dubbo.registry.parameters.namingPushEmptyProtection=true 那么 isEmptyOrErrorPush 将返回 true,此时,本地缓存不会被重置。

另外,需要注意的是 namingPushEmptyProtection 有版本要求,这个是 nacos client 自己的容灾,记不得是哪个版本了,应该是 1.4.x

7

dubbo.registry.parameters.namingPushEmptyProtection=true 不符合我的原因是,我的 nacos client 是 1.3.x,没法使用,升级 nacos client 基本需要半年乃至更久。

8

感谢答疑. 目前我们的容灾场景已测试有效.