격벽 패턴: 세마포 대 threadPool

두 가지 구현이 다른 이유는 무엇입니까?



이 기사에서는 Spring과 Resilence4j를 언급할 것입니다.

이 기사를 읽고 있다면 벌크헤드 패턴, 해결하려는 문제 및 가장 일반적인 구현인 세마포어 기반 및 스레드 풀 기반에 대해 이미 알고 있을 것입니다.

적어도 나에게는 언제 세마포어 구현을 사용해야 하는지, 언제 threadPool 구현을 사용해야 하는지 깨닫는 것이 쉽지 않았습니다.

나는 Semaphore가 어떻게 작동하는지 알고 있으며 ThreadPool 패턴도 이해하고 있으므로 짧은 대답과 가장 분명한 것은 비동기 호출 수를 제한하기 위해 threadPool을 사용하고 동기 호출을 제한하기 위해 세마포어를 사용하는 것입니다.

그렇다면 어려운 부분은 왜였을까? 그 이유는 다음과 같은 질문이었습니다.
  • @Async를 벌크헤드 세마포 기반 구현과 결합하면 안 되는 이유는 무엇입니까?
  • 두 주석을 함께 사용할 수 있습니까? 그렇다면 왜 threadPool을 구현해야 합니까?

  • @Async와 @Bulkhead가 결합되었습니다.




    @Bulkhead(name = "Service3", fallbackMethod = "futureFallback")
        @Async
        public CompletableFuture<String> doSomeWork() {
            System.out.println("Excecuting service 3 - " + Thread.currentThread().getName());   
            Util.mockExternalServiceHttpCall(DELAY);
            return CompletableFuture.completedFuture("ok");
        }
    
    


    Complete Code .

    예, 두 주석을 함께 사용할 수 있습니다. 벌크헤드 세마포어 구성에 따라 제한된 수의 비동기 호출을 생성할 수 있습니다.

    그러나 알아두면 유용한 몇 가지 의미가 있습니다.

    SimpleAsyncTaskExecutor.



    @Async 주석으로 메서드에 주석을 추가하면 Spring은 TaskExecutor 인터페이스의 다른 구현을 사용할 수 있습니다.
    기본적으로 프레임워크는 SimpleAsyncTaslExecutor 을 사용합니다.

    이 구현은 주석이 달린 메서드가 호출될 때마다 새 스레드를 생성합니다. 이 스레드는 재사용되지 않습니다.

    이 접근 방식의 문제점은 세마포 카운터가 0(격벽이 가득 찼음)인 경우에도 새 스레드를 생성한다는 것입니다.



    다음 스택 추적에서 볼 수 있듯이 프레임워크는 먼저 스레드를 생성한 다음 실행을 계속할 수 있는 사용 가능한 권한이 있는지 확인하는 격벽 패턴을 호출합니다. 세마포어 카운터가 cero이면 벌크헤드는 메서드 실행을 거부합니다.

    Thread [_simpleAsyncTask1] (Suspended (breakpoint at line 18 in Service3))  
        Service3.doSomeWork() line: 18  
        Service3$$FastClassBySpringCGLIB$$6085f5a4.invoke(int, Object, Object[]) line: not available    
        MethodProxy.invoke(Object, Object[]) line: 218  
        CglibAopProxy$CglibMethodInvocation.invokeJoinpoint() line: 793 
        CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 163 
        CglibAopProxy$CglibMethodInvocation.proceed() line: 763 
        MethodInvocationProceedingJoinPoint.proceed() line: 89  
        BulkheadAspect.lambda$handleJoinPointCompletableFuture$0(ProceedingJoinPoint) line: 225 
        1457434357.get() line: not available    
        Bulkhead.lambda$decorateCompletionStage$1(Bulkhead, Supplier) line: 100 
        1251257755.get() line: not available    
        SemaphoreBulkhead(Bulkhead).executeCompletionStage(Supplier<CompletionStage<T>>) line: 557  
        BulkheadAspect.handleJoinPointCompletableFuture(ProceedingJoinPoint, Bulkhead) line: 223    
        BulkheadAspect.proceed(ProceedingJoinPoint, String, Bulkhead, Class<?>) line: 162   
        BulkheadAspect.lambda$bulkheadAroundAdvice$5eb13a26$1(ProceedingJoinPoint, String, Bulkhead, Class) line: 129   
        1746723773.apply() line: not available  
        1746723773(CheckedFunction0<R>).lambda$andThen$ca02ab3$1(CheckedFunction1) line: 265    
        1151489454.apply() line: not available  
        BulkheadAspect.executeFallBack(ProceedingJoinPoint, String, Method, CheckedFunction0<Object>) line: 139 
    
    ==> here
     BulkheadAspect.bulkheadAroundAdvice(ProceedingJoinPoint, Bulkhead) line: 128   
        NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]  
        NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62  
        DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43  
        Method.invoke(Object, Object...) line: 566  
        AspectJAroundAdvice(AbstractAspectJAdvice).invokeAdviceMethodWithGivenArgs(Object[]) line: 634  
        AspectJAroundAdvice(AbstractAspectJAdvice).invokeAdviceMethod(JoinPoint, JoinPointMatch, Object, Throwable) line: 624   
        AspectJAroundAdvice.invoke(MethodInvocation) line: 72   
        CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 175 
        CglibAopProxy$CglibMethodInvocation.proceed() line: 763 
        ExposeInvocationInterceptor.invoke(MethodInvocation) line: 97   
        CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 186 
        CglibAopProxy$CglibMethodInvocation.proceed() line: 763 
        AnnotationAsyncExecutionInterceptor(AsyncExecutionInterceptor).lambda$invoke$0(MethodInvocation, Method) line: 115  
        1466446116.call() line: not available   
        AsyncExecutionAspectSupport.lambda$doSubmit$3(Callable) line: 278   
        409592088.get() line: not available 
        CompletableFuture$AsyncSupply<T>.run() line: 1700   
    
        ==> here
    SimpleAsyncTaskExecutor$ConcurrencyThrottlingRunnable.run() line: 286   
        Thread.run() line: 829  
    
    


    @Aync 및 @Bulkhead 주석이 달린 메서드를 60초 동안 호출하는 부하 테스트를 실행한 후 프로파일링tool 그림에서 애플리케이션이 생성된 514개의 스레드 중 34개만 사용했음을 확인할 수 있습니다. 이것은 분명히 자원 낭비를 나타냅니다.



    ThreadPoolTaskExecutor



    또 다른 옵션은 TreadPoolTaskExecutor 구현을 사용하는 것입니다.
    이 구현을 사용하여 동일한 테스트를 실행한 후 생성된 스레드 수가 많이 감소했습니다(41).



    그러나 이 접근 방식의 문제점은 불필요한 중복성을 사용하고 있다는 것입니다. 제 생각에는 스레드 풀과 세마포어를 함께 사용하는 것은 실질적인 이점이 없습니다.

    결론.



    비동기 호출을 제한하려면 @Async와 Bulkhead 세마포어의 조합 대신 Bulkhead threadPool 구현을 사용하십시오.

    좋은 웹페이지 즐겨찾기