Armeria와 Spring Boot Actuator 연동 중 겪은 버그 해결하기

이 글은 Spring과 Armeria 통합 시 사용할 수 있는 인터널 서비스 중 하나인 Actuator 연동 중 겪은 버그에 대해 조사하고 수정하려고 노력…까지 해본 개발기입니다. 만약 연동 방법에 관심이 있으시다면 이전 글인 Armeria Spring Boot 연동 글을 참고하시면 도움이 될 것 같습니다.

배경

저는 Armeria와 Spring Boot 연동 과정을 정리한 문서를 만들기 위해 Armeria에서 지원하는 인터널 서비스들을 하나씩 직접 사용하고 어떻게 동작하는지 정리하는 과정을 거쳤습니다. 연동 과정에서 Armeria가 지원하는 인터널 서비스는 총 4가지로 문서, 메트릭, 헬스체크, Actuator 연동입니다. 그 중 4번째인 Actuator 연동 인터널 서비스를 테스트하는 도중 버그를 겪었습니다.

버그를 설명하기 앞서 Actuator 연동 서비스가 정확히 무엇을 하는지 설명드리겠습니다. Armeria는 앞서 말씀드린 인터널 서비스들을 armeria.internal-services.port에 바운드합니다. 그리고 Spring Actuator 서비스를 사용해보셨다면 아시겠지만 Actuator 서비스는 management.server.port에서 실행됩니다. 여기서 Armeria의 Actuator 연동 서비스는 모든 인터널 서비스를 management.server.port에 서빙하고 또한 모든 Actuator 서비스를 armeria.internal-service.port에 서빙합니다. 요약하면, 다음과 같습니다.

  • armeria.internal-services.port에서 인터널 서비스, Actuator 서비스 이용 가능
  • management.server.port에서 인터널 서비스, Actuator 서비스 이용 가능

버그 발견

해당 서비스를 사용하기 위해서는 armeria-spring-boot3-actuator-starter 아티팩트를 추가하고, application.ymlactuator를 추가해야 합니다.

그런데 build.gradle에 의존성을 추가한 순간 java.net.BindException, Address already in use 예외가 발생하였습니다. 해당 에러는 같은 포트에 둘 이상의 프로세스를 실행시키려 할 때 발생하는 예외입니다.

원인 찾기

application.yml에서 설정하여 제가 사용하는 포트는 총 3개였습니다.

각 포트별로 서비스들이 정상적으로 동작하는지 확인하기 위해 제공하는 서비스는 크게 4가지입니다.

  1. legacy tomcat service
  2. armeria annotated service
  3. internal service
  4. management service (Acutator service)

각 포트별로 실행되어야 하는 서비스들이 정상 작동하는지 수동으로 테스트했습니다. 참고로 테스트 가능했던 이유는 BindException이 실행 후 3초 뒤에 발생했기 때문에 재시작을 반복하면서 작동하는 서비스를 확인했습니다. 수동으로 (노가다) 테스트한 결과 9090 포트에서 인터널 서비스와 Actuator 서비스가 작동해야 하는데 인터널 서비스가 제대로 작동하지 않는 것을 확인했습니다. 위에서 언급했듯이 Armeria의 actuator 연동 인터널 서비스는 다른 모든 인터널 서비스들을 management.server.port로 서빙합니다. 이 과정에서 모종의 프로세스가 먼저 management.server.port를 점거하여 충돌이 발생했다고 추측했습니다.

이 시점에서 문제 원인에 대해 거의 확신했지만 수동으로 재시작하여 노가다로 찾은 원인이기 때문에 부정확할 가능성이 있어 Spring Boot와 Armeria의 자동 빈 설정 과정과 NIO의 ServerSocketChannelImpl#bind()에 브레이크 포인트를 걸었습니다. 그 결과 Armeria에서 인터널 서비스 빈 설정 전에 9090 포트의 bind가 먼저 일어나는 것을 확인했습니다. 동시에 o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9090 (http) with context path '' 로그를 확인할 수 있었습니다. 결정적으로 해당 로그를 통해 management.server.port가 설정되어있는 경우 Spring의 Tomcat이 해당 포트를 점유하여 Actuator 서비스를 먼저 실행하기 때문에 Armeria와 충돌이 발생한다는 것을 알 수 있었습니다.

그럼 테스트 코드에서는 왜 문제가 없었을까요? Armeria에서 해당 코드를 테스트하는 테스트 코드에서는 Spring을 의존성 주입에 사용하고 Tomcat은 전혀 사용하지 않았습니다. 따라서 아무런 서비스도 management.server.port에서 실행되지 않습니다. 반면, 제 상황은 Tomcat을 사용하는 legacy Spring boot와 Armeria를 통합했기 때문에 충돌이 발생했던 것입니다.

해결해보기

문제를 해결하기 위해 어떤 방식으로 인터널 서비스와 Actuator 서비스를 실행시키는지 코드를 확인했습니다. 문제가 발생한 인터널 서비스 관련 소스 코드를 보면 다음과 같이 actuatorServerConfigurator 이름의 빈이 존재할 때만 managementServerPort를 추가하는 것을 확인할 수 있습니다. 해당 빈은 armeria-spring-boot3-actuator-starter 모듈에 속하기 때문에 build.gradle에 의존성을 추가했을 때만 인터널 서비스가 management.server.port에서 실행하는 것을 알 수 있습니다.

일단 application.yml의 설정을 변경하여 문제를 해결해보려 시도했지만 실패했습니다. 예를 들어 management.endpoints.enabled-by-default: false 설정 등을 적용했지만 여전히 BindException이 발생했습니다.

다음으로는 설정이 아닌 Spring 빈 설정을 통한 문제 해결을 탐색하기 위해 Actuator 서비스는 어떻게 armeria.internal-services.port에서 실행되는지도 확인했습니다. Armeria의 actuatorServerConfigurator 빈은 org.springframework.boot.actuate.endpoint.web.EndpointsSupplier 빈을 주입받는데 해당 빈 안에 모든 Actuator 엔드포인트들이 들어있습니다. 코드를 더 따라가면 이를 Armeria의 WebOperationService로 변환해서 사용합니다. 따라서 management.server.port에서 Tomacat이 실행하는 Actuator 서비스만 중단할 수 있다면 위와 동일한 방법으로 Armeria가 Actuator 서비스를 포함한 모든 서비스를 관장할 수 있겠다는 생각이 들었습니다.

그러나 아쉽게도 Tomcat의 Actuator 서비스 실행을 막는 빈 설정은 찾지 못했습니다. 그래서 결론적으로 문제를 해결하지는 못했지만 아이디어는 떠올렸는데요. Spring Boot의 Tomcat 서비스를 Armeria와 통합할 때 server.port를 -1로 설정해야 합니다. 현재 이슈와 매우 유사한데 armeria.ports를 통해서 Tomcat 서비스로 접속하고 Tomcat에 직접적인 접근을 막기 위함입니다. 그래서 같은 원리로 management.server.port를 -1로 설정하고 새롭게 armeria.internal-services.management-port 설정을 추가하여 해당 포트를 통해 Actuator 서비스에 접속하도록 소스 코드를 수정하면 해결될 것으로 예상됩니다.

느낀 점

결국 문제를 완벽히 해결하지는 못했지만 해결 과정에서 Armeria가 만든 다양한 빈 설정들이 어떻게 동작하는지 더 자세히 알 수 있었습니다. 게다가 Spring Boot에 대한 관심도 늘었습니다. 워낙 편리하게 설정이 되다보니 그다지 의문을 갖지 않았는데 이번 기회를 통해 Spring Boot의 소스 코드도 조금씩 보면서 재미를 느꼈습니다. 스스로 개선할 점도 찾았는데요. 처음 문제가 발생하는 포트와 서비스를 찾기 위해 매우 많이 수동으로 재시작을 하면서 원인을 찾았습니다. 해당 과정을 Spring이 제공하는 편리한 테스트 기능으로 진행했으면 정리도 잘 되고 재현도 쉬웠을 것입니다. 테스트 작성법을 잘 몰라 찾아보고 생각하기 귀찮아 노가다로 진행했는데 다음에 비슷한 상황에 처한다면 꼭 테스트를 만들기로 결심했습니다.

여기까지 Armeria의 Actuator 연동 인터널 서비스 테스트 중 버그 발견과 해결 도전까지 개발기였습니다. 사실 문제 해결까지 완료해서 완결성 있는 글을 쓰고 싶었는데 그러지 못하고 다소 장황한 글이 된 것 같아 아쉽습니다. 실패한 도전기 같아서 글로 쓰지 않을까 하다가 정리하는 겸 삽질을 공유하기 위해 글로 써봤습니다. 이 글을 읽는 분께서도 Armeria를 사용하면서 겪었던 일들을 공유해주시면 커뮤니티에도 저에게도 큰 도움이 될 것 같습니다. 😄

comments powered by Disqus

Related