HTTP 200 OK

Memento mori & Carpe diem

Spring

BlockHound - Blocking 코드 존재여부 확인

sjoongh 2023. 12. 24. 20:12

사용이유

Spring Webflux나 Coroutine을 사용할 때는 모든 코드가 reactive 하게 동작하길 원합니다. 비동기 처리를 하면서 느낀내가 만든 코드가 점은 비동기로 데이터가 처리되는것인지 동기형식으로 처리되는 것인지 확인해볼 수가 없다는 것이였습니다. 그저 비동기 문법에 맞게 코드를 만들었을뿐.. 즉 비동기로작성한 코드에 blocking 코드가 존재하는지 확인해주는 도구가 바로 blockhound입니다.

 

BlockHound란?

BlockHound는 webflux 에서 사용하는 reactor 팀에서 개발한 도구로 애플리케이션에서 blocking 코드가 작성되었는지 여부를 검출해주는 도구입니다. 직접 작성한 코드 뿐만 아니라, 서드 파티 라이브러리에서 사용한 블로킹 코드도 전부 검출합니다. 또한 JUnit과 함께 사용하면서 테스트코드를 작성할때 미리 blocking코드를 검출해내는것도 가능합니다.

 

작동원리

java.net.Socket 내부에서 Blocking Java 메소드를 감지하기 위해 BlockHound에서 해당 메소드의 바이트 코드를 변경하고 메소드 본문 시작에 Blocking 차단 코드를 넣어서 차단 메소드를 탐지합니다.

 

JVM은 java.lang.Thread 를 통해 JVM의 기본 메서드 계측기술을 활용해 Blocking 호출 감지 코드를 추가해 감지합니다.

 

모든 Blocking을 차단하는 것은 아니며 class loading 같은 작업은 오류가 발생하지 않도록 구성해야 하기때문에 BlockHound에서는 현재 상태를 확인하며 화이트/블랙 리스트를 지원한다고 합니다. 물론 사용자 커스텀을 통한 리스트도 추가할 수 있습니다. 더 자세한 작동원리는 공식문서를 보면 좋을 것 같습니다. (제 멋대로 이해한 내용이라 오류가 있을수도 있습니다ㅠ)

 

적용 방법

1.  implementation 'io.projectreactor.tools:blockhound:1.0.6.RELEASE' 해당 구문을 gradle에 적용

2. 흔히 main 스레드에 적용

// Application Main 함수에서 BlockHound 를 활성화 한다.
fun main(args: Array<String>) {
    BlockHound.install()
    runApplication<MyApplication>(*args)
}

 

3. 이후 실행시키면 blocking 코드를 검출해주며 blocking을 검출하고 싶은 로직의 API를 호출할 때 아래와 같은 Error를 반환합니다.

error 코드

 

BlockHound.install() 을 작성하면 스프링 부트 애플리케이션이 시작되는 시점부터 모든 바이트 코드를 분석해서 블로킹 코드가 존재하는지 검사합니다.

 

커스터 마이징

 

1. BlockHound를 install에 허용할 대상을 지정해준다.

  • 라이브러리들에서 blocking 코드로 검출되어 에러를 유발시키는 경우가 있는데 이런 경우에는 의도적으로 허용시켜 에러를 없앨 수 있습니다.
  • 대표적으로 Jackson의 ObjectMapper가 (jackson-module-kotlin 의존성) blocking 코드를 사용한다고 BlockHound 에러가 발생하였는데, 이는 외부 IO를 사용하는 것이 아니기 때문에 허용가능한 수준으로 본다는 github이슈도 있었습니다.
// BlockHound 를 그냥 install 하지 않고 다음과 같이 허용할 대상을 지정해줄 수 있다.
BlockHound.install(
    BlockHoundIntegration { builder: BlockHound.Builder ->
        builder
        .allowBlockingCallsInside(ObjectMapper::class.qualifiedName, "readValue")
        .allowBlockingCallsInside(ObjectMapper::class.qualifiedName, "canSerialize")
    }
)

 

2. BlockHound.install(BlockHoundIntegration ... integrations)

가장 간단한 방법은 BlockHound.install(); 코드의 인자로 BlockHoundIntegration 객체를 전달해주는 것입니다.

public interface BlockHoundIntegration extends Comparable<BlockHoundIntegration> {
    void applyTo(BlockHound.Builder var1);

    default int compareTo(BlockHoundIntegration o) {
        return 0;
    }
}

 

3. gradle에서 blocking을 허용할 클래스와 메소드를 지정할 수도 있습니다.

builder.allowBlockingCallsInside(
    "ch.qos.logback.classic.Logger",
    "callAppenders"
);

 

4. 테스트 코드에 BlockHound 추가

@Test
public void blockHoundWorks() throws TimeoutException, InterruptedException {
    try {
        FutureTask<?> task = new FutureTask<>(() -> {
            Thread.sleep(0);
            return "";
        });
        Schedulers.parallel().schedule(task);

        task.get(10, TimeUnit.SECONDS);
        Assert.fail("should fail");
    } catch (ExecutionException e) {
        Assert.assertTrue("detected", e.getCause() instanceof BlockingOperationError);
    }
}

 

만일 BlockHound를 추가했을때 테스트가 중단되는 상황이 발생하면 이벤트 루프가 오류를 제대로 처리하지 못하는 상황일수도 있기 때문에 어느 부분이 틀렸는지 찾기가 힘들다면 콜백을 재정의해 어느 부분에서 오류가 발생한건지 정확히 판별해 트러블슈팅을 할 수 있습니다. 

BlockHound.install(builder -> {
    builder.blockingMethodCallback(it -> {
        new Exception(it.toString()).printStackTrace();
    });
});

 

  • 또한 BlockHound에서는 화이트 리스트에 추가하는 API를 제공하므로 원하는 메소드를 넣어 놓는다면 BlockHound가 이를 감지하지 못하도록 구성할 수도 있습니다.
  • 이 외의 커스텀 예제가 궁금하신 분들은 공식문서를 보시면 좋을 것 같습니다.

 

출처

https://github.com/reactor/BlockHound/blob/master/docs/README.md

https://github.com/reactor/BlockHound

https://blog.frankel.ch/blockhound-how-it-works/