프로덕션에서 CompletableFuture를 사용할 때 주의해야 할 문제

프로덕션에서 CompletableFuture를 사용할 때 주의해야 할 문제

1 배경

작업을 받은 후 주문 인터페이스를 최적화해야 하는데 비즈니스 로직을 확인한 후 병렬 또는 비동기로 쿼리할 수 있는 곳이 있다는 것을 알았으므로 CompletableFuture를 사용하여 비동기 최적화를 수행하여 인터페이스 응답 속도를 향상시켰습니다. 의사 코드는 다음과 같습니다

//查询用户信息
            CompletableFuture<JSONObject> userInfoFuture = CompletableFuture
                .supplyAsync(() -> proMemberService.queryUserById(ordOrder.getId()));
            //查询积分商品信息
            CompletableFuture<JSONObject> integralProInfoFuture = CompletableFuture
                .supplyAsync(() -> proInfoService
                    .getProById(ordOrderIntegral.getProId()));

            //查询会员积分信息
            CompletableFuture<Integer> integerFuture = CompletableFuture
                .supplyAsync(() -> proMemberService
                    .getTotalIntegralById(ordOrder.getOpenId()));

2번의 테스트

최적화 후 테스트 실행 속도가 2000ms에서 600ms로 떨어졌고 로컬 및 테스트 환경 테스트 후 프로덕션 로그에 스레드
번호가 출력되었지만 CompletableFuture의 기본 스레드 풀에서 가져오지 않았습니다.

여기에 이미지 설명 삽입

로컬 및 테스트 환경 인쇄 로그.
여기에 이미지 설명 삽입
프로덕션 환경이 각 스레드에 대해 새 스레드를 생성하는 로그에서 발견됨.동시성이 너무 높으면 스레드 리소스가 고갈되어 서버가 충돌할 가능성이 있습니다. .

3가지 이유

CompletableFuture의 소스 코드를 읽은 후 마침내 이유를 찾았습니다. 기본 ForkJoinPool 스레드 풀을 사용할지 여부는 시스템 구성과 관련이 있습니다 .

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    
    
        return asyncSupplyStage(ASYNC_POOL, supplier);
    }

클릭하여 asynPool 입력

//是否使用默认线程池的判断依据
private static final Executor ASYNC_POOL = USE_COMMON_POOL ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

//useCommonPool的来源
private static final boolean USE_COMMON_POOL =
        (ForkJoinPool.getCommonPoolParallelism() > 1);

CompletableFuture가 기본 스레드 풀을 사용하는지 여부는 useCommonPool 값에 따라 판단되며 값은 true입니다.

public static int getCommonPoolParallelism() {
    
    
        return COMMON_PARALLELISM;
    }

public ForkJoinPool() {
    
    
        this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
             defaultForkJoinWorkerThreadFactory, null, false,
             0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS);
    }

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode,
                        int corePoolSize,
                        int maximumPoolSize,
                        int minimumRunnable,
                        Predicate<? super ForkJoinPool> saturate,
                        long keepAliveTime,
                        TimeUnit unit) {
    
    
        checkPermission();
        int p = parallelism;
        if (p <= 0 || p > MAX_CAP || p > maximumPoolSize || keepAliveTime <= 0L)
            throw new IllegalArgumentException();
        if (factory == null || unit == null)
            throw new NullPointerException();
        this.factory = factory;
        this.ueh = handler;
        this.saturate = saturate;
        this.keepAlive = Math.max(unit.toMillis(keepAliveTime), TIMEOUT_SLOP);
        int size = 1 << (33 - Integer.numberOfLeadingZeros(p - 1));
        int corep = Math.min(Math.max(corePoolSize, p), MAX_CAP);
        int maxSpares = Math.min(maximumPoolSize, MAX_CAP) - p;
        int minAvail = Math.min(Math.max(minimumRunnable, 0), MAX_CAP);
        this.bounds = ((minAvail - p) & SMASK) | (maxSpares << SWIDTH);
        this.mode = p | (asyncMode ? FIFO : 0);
        this.ctl = ((((long)(-corep) << TC_SHIFT) & TC_MASK) |
                    (((long)(-p)     << RC_SHIFT) & RC_MASK));
        this.registrationLock = new ReentrantLock();
        this.queues = new WorkQueue[size];
        String pid = Integer.toString(getAndAddPoolIds(1) + 1);
        this.workerNamePrefix = "ForkJoinPool-" + pid + "-worker-";
    }

4. 요약

  • CompletableFuture를 사용하려면 스레드 풀을 사용자 지정해야 합니다.
  • CompletableFuture가 기본 스레드 풀을 사용하는지 여부는 머신 코어 수와 관련이 있습니다. 기본 스레드 풀은 코어 수에서 1을 뺀 값이 1보다 큰 경우에만 사용되며, 그렇지 않으면 각 작업에 대해 스레드가 생성됩니다.
  • 서버 코어가 2보다 크고 기본 스레드 풀을 사용하더라도 스레드 풀에 너무 적은 수의 스레드가 있을 수 있으므로 많은 수의 스레드가 대기하고 처리량이 줄어들며 서버를 끌어내릴 수도 있습니다.
  • ForkJoinPool은 CPU를 많이 사용하는 작업(계산)에 사용됩니다.

프로세서 사용률에 대한 스레드 풀 크기의 비율은 다음 공식을 사용하여 추정할 수 있습니다.
N 스레드 = N CPU * U CPU * (1 + W/C)
여기에서:


  • NCPU는 Runtime.getRuntime().availableProcessors()를 통해 얻을 수 있는 프로세서의 코어 수입니다.

  • U CPU는 원하는 CPU 사용률입니다(값은 0과 1 사이여야 함).

  • W/C는 계산 시간에 대한 대기 시간의 비율입니다.

스레드 풀의 크기를 설정하는 일반적인 규칙은 다음과 같습니다.

  • 서비스가 CPU를 많이 사용하는 경우 컴퓨터의 코어 수로 설정합니다.

  • 서비스가 io 집약적인 경우 컴퓨터의 코어 수*2로 설정합니다.

추천

출처blog.csdn.net/weixin_42202992/article/details/131990901