Dubbo 프레임워크의 우아한 재시작에 대해 이야기하기

1. 배경

최근 Dubbo 서비스가 프로덕션 환경에 도입되었습니다.서비스가 온라인으로 다시 시작될 때마다 시간 초과 알람이 발생합니다.이상한 점은 클라이언트와 서버의 재시작이 영향을 미치고 알람이 볼륨이 클 때 더 분명합니다.

일반적인 알람 정보는 다음과 같습니다.

cause: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2021-09-09 11:59:56.822, end time: 2021-09-09 11:59:58.828, client elapsed: 0 ms, server elapsed: 2006 ms, timeout: 2000 ms, request: Request [id=307463, version=2.0.2, twoway=true, event=false, broken=false, data=null], channel: /XXXXXX:52149 -> /XXXXXX:20880] with root cause]

그 이유는 무엇입니까?

  1. 정상적인 종료가 없습니까?

  2. 다시 시작하는 순간 요청량이 너무 많고 워밍업이 없습니까?

  3. Dubbo가 성공적으로 시작된 후 SpringBoot가 성공적으로 시작되지 않았으며 지연 노출이 없습니까?

  4. 무리한 파라미터 구성은 없는가?

위의 모든 것이 가능합니다 거의 반달 동안 Dubbo 프레임워크의 소스 코드를 읽고 검증한 후 마침내 모든 답을 찾았고 이에 내 마음을 담아 채굴장 기록을 정리했습니다.

2. 설명

  1. 버전

구성 요소 버전
더보 2.7.7
네티 4.0.36.최종
사육사 3.4.9

  1. 기본적인 상황

멱등성인 읽기 요청의 경우 기본적으로 재시도하지만 쓰기 요청의 경우 기본적으로 재시도하지 않습니다.

기본 제한 시간은 2000ms입니다.

서비스는 모두 도커 컨테이너이며 Dubbo 클라이언트의 수는 서비스 공급자보다 훨씬 많으며 비율은 약 10:1입니다.

  1. 팁 이 글은 서비스 재시작과 관련된 기술적인 포인트와 원칙을 설명하는 데 중점을 두고 있으며 Dubbo 프레임워크 기반, Netty 기반 및 이전 버전 간의 차이점에 대해서는 설명하지 않습니다.

3. 핵심 기술 포인트를 정상적으로 다시 시작하십시오.

위의 문제에 대해 Dubbo 프레임워크도 솔루션을 제공하므로 차례로 살펴보겠습니다.

  1. Dubbo 우아한 다운타임 메커니즘

Dubbo는 JDK의 ShutdownHook을 사용하여 정상적인 종료를 완료합니다. Dubbo에서 구현된 우아한 종료 메커니즘은 주로 6단계를 포함합니다. 

(1) kill PID 프로세스 종료 신호를 수신한 후 Spring 컨테이너는 컨테이너 파괴 이벤트를 트리거합니다.

(2) 공급자 측에서 서비스 메타데이터 정보를 로그아웃합니다(ZK 노드 삭제).

(3) 소비자는 최신 서비스 제공자 목록을 가져옵니다.

(4) 공급자는 서비스를 사용할 수 없음을 소비자에게 알리기 위해 읽기 전용 이벤트 메시지를 보냅니다.

(5) 서버는 이미 실행된 작업이 종료될 때까지 대기하고 새로운 작업의 실행을 거부합니다.

우아하게 종료

핵심 코드:

  @Override
    public void close(final int timeout) {
        startClose();
        if (timeout > 0) {
            final long max = (long) timeout;
            final long start = System.currentTimeMillis();
            if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
                //发送 readonly 事件报文通知 consumer 服务不可用
                sendChannelReadOnlyEvent();
            }
            while (HeaderExchangeServer.this.isRunning()
                    && System.currentTimeMillis() - start < max) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        doClose();
        server.close(timeout);
    }

관련 구성

dubbo:
  application:
        shutwait: 10000 # 优雅退出等待时间,单位毫秒 默认等待 10s
  1. Dubbo 예열 메커니즘

Dubbo 서비스의 기본 가중치는 100입니다. Dubbo는 실제로 의사 워밍업 메커니즘을 제공합니다. 이 메커니즘은 서비스 제공자의 실행 시간에 따라 가중치를 계산한 다음 로드 밸런싱 전략을 사용하여 트래픽을 작은 것에서 큰 것으로 실현합니다. Dubbo 소스 코드에서 시작하여 서비스 예열의 특정 구현을 살펴보겠습니다. 특정 소스 코드는 다음 위치에 있습니다.AbstractLoadBalance#getWeight

 /**
     * Get the weight of the invoker's invocation which takes warmup time into account
     * if the uptime is within the warmup time, the weight will be reduce proportionally
     *
     * @param invoker    the invoker
     * @param invocation the invocation of this invoker
     * @return weight
     */
    int getWeight(Invoker<?> invoker, Invocation invocation) {
        int weight;
        URL url = invoker.getUrl();
        // Multiple registry scenario, load balance among multiple registries.
        if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
            weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
        } else {
            weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
            if (weight > 0) {
                //获取服务启动时间 timestamp
                long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
                if (timestamp > 0L) {
                    //使用当前时间减去服务提供者启动时间,计算服务提供者已运行时间 `uptime`
                    long uptime = System.currentTimeMillis() - timestamp;
                    if (uptime < 0) {
                        return 1;
                    }
                    //获取服务预热时间基数,默认是10分钟
                    int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
                    //如果服务启动时间 小于 warmup 则重新计算权重
                    if (uptime > 0 && uptime < warmup) {
                        //根据已运行时间动态计算服务预热过程的权重
                        weight = calculateWarmupWeight((int)uptime, warmup, weight);
                    }
                }
            }
        }
        return Math.max(weight, 0);
    }

가중치 계산 알고리즘을 살펴보자

 /**
     * Calculate the weight according to the uptime proportion of warmup time
     * the new weight will be within 1(inclusive) to weight(inclusive)
     *
     * @param uptime the uptime in milliseconds
     * @param warmup the warmup time in milliseconds
     * @param weight the weight of an invoker
     * @return weight which takes warmup into account
     */
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        int ww = (int) ( uptime / ((float) warmup / weight));
        return ww < 1 ? 1 : (Math.min(ww, weight));
    }

여기서의 계산 방법은 실제로 매우 간단합니다. 간단히 말해서 서비스가 오래 실행될수록 가중치가 높아집니다. 가동 시간 = 워밍업일 때 정상 가중치가 복원됩니다.

기본적으로 (Dubbo 서비스의 기본 가중치는 100이며 준비 시간은 10분입니다.)

서비스 공급자가 1분 동안 실행된 경우 가중치는 10이 됩니다.

서비스 공급자가 5분 동안 실행된 경우 가중치는 50이 됩니다.

서비스 공급자가 11분 동안 실행 중이고 기본 워밍업 시간 임계값인 10분을 초과하면 추가 계산이 수행되지 않고 기본 가중치 가중치가 직접 반환됩니다.

알림: 로드 밸런싱 전략 consistenthash(일관성 해시)는 서비스 예열을 지원하지 않습니다.

관련 구성

dubbo:
    provider:
         warmup: 600000 # 单位毫秒 默认10分钟
  1. 지연된 노출

일부 외부 컨테이너(예: tomcat)는 완전히 시작되기 전에 dubbo 서비스에 대한 호출을 차단하여 소비자 측에서 시간 초과를 초래합니다. 이러한 상황은 릴리스 중에 특정 확률로 발생할 수 있습니다. 이 문제를 피하기 위해 일정한 지연 시간(Tomcat 시작 후 보장)을 설정하여 원활한 릴리스를 달성하십시오.

소스 코드에서 dubbo의 지연된 노출은 주로 ServiceBean클래스와 해당 상위 클래스 ServiceConfig#export에 반영됩니다.

 public synchronized void export() {
        //是否已经暴露
        if (!shouldExport()) {
            return;
        }

        if (bootstrap == null) {
            bootstrap = DubboBootstrap.getInstance();
            bootstrap.init();
        }

        checkAndUpdateSubConfigs();

        //init serviceMetadata
        serviceMetadata.setVersion(version);
        serviceMetadata.setGroup(group);
        serviceMetadata.setDefaultGroup(group);
        serviceMetadata.setServiceType(getInterfaceClass());
        serviceMetadata.setServiceInterfaceName(getInterface());
        serviceMetadata.setTarget(getRef());

        if (shouldDelay()) { //是否需要延迟暴露
            DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {
            //真正执行服务暴露的方法
            doExport();
        }

        exported();
    }

위의 코드에서 Dubbo가 스케줄 지연 태스크를 사용하여 doExport의 실행을 지연시키는 것을 볼 수 있습니다.

지연 노출 시퀀스 다이어그램은 다음과 같습니다.

관련 구성

dubbo:
    provider:
         delay: 5000 # 默认null不延迟, 单位毫秒
  1. 다른

이를 해결한 후에도 서비스를 다시 시작하면 여전히 많은 시간 초과가 발생하며 이는 클라이언트 로그를 확인하여 발견됩니다.

/XXX:57330 -> /XXXX:20880 is established., dubbo version: 2.7.7, current host: XXXX
2021-09-07 15:01:07.748 [NettyClientWorker-1-16] INFO  o.a.d.r.t.netty4.NettyClientHandler   -  [DUBBO] The connection of /XXXX:57332 -> /XXXX:20880 is established., dubbo version: 2.7.7, current host: XXXX

# 简单统计一下发现 客户端启动时建立了3600个长连接
$ less /u01/logs/order-service-api_XXX/dubbo.log  | grep NettyClientWorker- |grep  '2021-09-07 15' | wc -l
3600

이 질문을 염두에 두고 소스 코드를 확인하여 알아보세요.

DubboProtocol#getClients

private ExchangeClient[] getClients(URL url) {
        boolean useShareConnect = false;

        //获取配置连接数, 如果没有配置默认0
        int connections = url.getParameter(CONNECTIONS_KEY, 0);
        List<ReferenceCountExchangeClient> shareClients = null;
        // if not configured, connection is shared, otherwise, one connection for one service
        if (connections == 0) {
            //注意: 如果Provider 配置了connections, 就不会使用共享连接,Consumer就算配置了shareConnections也不会生效
            useShareConnect = true;

            /*
             * The xml configuration should have a higher priority than properties.
             */
            String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
            connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
                    DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);
            shareClients = getSharedClient(url, connections);
        }

        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            if (useShareConnect) {
                clients[i] = shareClients.get(i);

            } else {
                //初始化创连接
                clients[i] = initClient(url);
            }
        }

        return clients;
    }

문제는 우리 서버 구성이

dubbo:
  provider:
    connections: 200

위의 코드를 설명하세요 연결이 설정되어 있지 않으면 공유 연결이 사용됩니다 공유 연결의 수는 Consumer가 설정한 공유 연결의 수에 따라 결정됩니다 기본값은 1입니다 반대로 연결이 설정되어 있으면 각 서비스에 대해 연결 수가 설정됩니다.

initClient 프로세스를 살펴보겠습니다.

initClient(URL url) {

        // client type setting.
        String str = url.getParameter(CLIENT_KEY, url.getParameter(SERVER_KEY, DEFAULT_REMOTING_CLIENT));

        url = url.addParameter(CODEC_KEY, DubboCodec.NAME);
        // enable heartbeat by default
        url = url.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT));

        // BIO is not allowed since it has severe performance issue.
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("Unsupported client type: " + str + "," +
                    " supported client type is " + StringUtils.join(ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(), " "));
        }

        ExchangeClient client;
        try {
            // 是否配置了懒加载
            if (url.getParameter(LAZY_CONNECT_KEY, false)) {
                client = new LazyConnectExchangeClient(url, requestHandler);

            } else {
                //没有配置懒加载会初始化长连接
                client = Exchangers.connect(url, requestHandler);
            }

        } catch (RemotingException e) {
            throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
        }

        return client;
    }

위의 코드에서 알 수 있듯이 Lazy Loading을 설정하지 않으면 긴 연결이 바로 초기화됩니다. 즉, 소비자가 다시 시작할 때마다 서비스 수 * 200 * 서버에서 도커 서비스의 여러 긴 연결이 설정됩니다. 우리 서비스의 수는 3이고 도커 서비스의 수는 6이며 정확히 3600개의 긴 연결입니다.

그런 다음 서버가 다시 시작되면 ZK는 서버가 다시 시작될 때 소비자(약 60개의 도커 서비스)에 알리고 새로 시작된 도커 서비스와 연결을 설정합니다. 소비자는 200 * 3, 총 36,000개의 긴 연결을 설정합니다. 설립됩니다. .

서비스를 다시 시작할 때마다 많은 수의 긴 연결을 설정해야 하므로 시간이 특히 오래 걸립니다(대략 계산하면 약 10초).

최적화: 연결 풀 수 줄이기 스트레스 테스트 후 구성 2로 충분합니다.

dubbo:
  provider:
    connections: 2

물론 서버도 기본적으로 구성할 수 없으며 긴 연결 수는 소비자가 결정할 수 있습니다. 지연 로딩은 많은 긴 연결이 필요할 때 사용할 수 있습니다. 서버를 다시 시작한 직후에 설정된 영구 연결의 총 수는 500개를 초과하지 않는 것이 좋습니다. 위의 문제를 해결한 후 재시작 시간 초과 문제가 마침내 해결되었습니다.

요약하다

Dubbo의 우아한 재시작 문제는 큰 구덩이이며 매개 변수 구성의 이유를 아는 것의 중요성도 보여줍니다. 그렇지 않으면 예측할 수 없는 문제가 발생할 수 있습니다.

또한 다음 글에서 소개할 쓰레드 풀 피트도 밟았습니다.

팔로우, 길 잃지 마시고, 좋아요와 수집을 환영합니다.

추천

출처blog.csdn.net/weixin_38130500/article/details/120279023