프로그램의 복잡도가 올라갈수록 소프트웨어 개발자에게는 좋은 디버깅 도구가 필요합니다. 가장 이상적인 디버깅 과정은 마음껏 실험해볼 수 있는 개발환경에서 문제를 안정적으로 재현하는 방법을 알아내고 이를 자동화된 테스트로 만드는 것이죠. 하지만 재현 시나리오 구성 자체가 너무 복잡하거나 프로덕션 환경에서만 가끔씩 랜덤하게 발생하는 종류의 버그들은 차선책으로 로그를 상세히 남겨서 사후에라도 어떤 문제가 있었는지 파악할 수 있도록 해야 합니다. 이번 글에서는 복잡한 asyncio 프로그램의 디버깅을 쉽게 하기 위해 개발한 aiomonitor-ng 도구를 소개합니다.
asyncio 애플리케이션 디버깅은 고유한 어려움들이 있습니다. 파이썬에서 디버깅할 때 가장 자주 활용하는 것이 바로 프로그램이 어느 부분을 실행하다가 예외가 발생했는지 보여주는 stack trace입니다. 그런데 asyncio 애플리케이션은 여러 개의 코루틴 작업들이 각자의 스택을 가지고 동시에 엮여서 실행되기 때문에 특정 예외가 발생한 코루틴 작업의 스택뿐만 아니라 '관련된' 코루틴 작업들의 스택도 함께 관찰해야 다른 코루틴 작업으로부터 야기된 오류인지 아닌지를 정확하게 파악할 수 있습니다. 특히, 내 코드에서 사용한 외부 라이브러리가 암묵적으로 코루틴 작업을 생성하고 그 코루틴 작업이 다시 내 코드를 호출하는 상황이라면 더욱 중요한 문제가 됩니다. 게다가 개발환경에서는 잘 발생하지 않고 프로덕션 환경에서만 발생하는 코루틴 작업 폭주 문제나 지속적으로 실행되어야 하는 코루틴 작업이 조용하게 종료되어버린다거나 하는 종류의 버그들은 굉장히 잡기 어렵습니다. 이런 종류의 버그들은 명시적인 예외가 발생하는 것이 아니기 때문에 사후 로그를 통해 문제점을 간접적으로 유추하는 수밖에 없기 때문입니다.
aiomonitor는 asyncio 코어 개발자들이 개발한 프로덕션용 라이브 디버깅 도구입니다. asyncio 기반 코드를 monitor 객체로 감싸두면, 해당 코드가 실행 중일 때 프로세스 외부에서 미리 설정된 TCP 포트로 텔넷 세션을 열어 간단한 명령어들을 통해 이벤트루프가 실행하고 있는 코루틴 작업들의 목록과 개별 스택 현황을 조회할 수 있게 해줍니다. Backend.AI에는 이미 이 aiomonitor가 적용되어 개별 서비스 프로세스마다 고유의 디버깅용 텔넷 포트가 할당되어 있습니다. (물론 보안 상 이유로 localhost로부터의 접속만 허용합니다.) 이를 통해 프로덕션에서만 발생하는 문제들을 디버깅하는 데 큰 도움을 받을 수 있었죠. 하지만 여전히 Backend.AI 자체의 코드가 아닌 외부 라이브러리에 의해서 발생하는 코루틴 작업 폭주 문제나 조용하게 종료되어버리는 코루틴 작업이 왜 죽는지 디버깅하는 것은 정확히 그 문제가 발생하는 시점을 특정하여 그 순간에 aiomonitor를 들여다보는 방식으로는 디버깅에 한계가 있었습니다.
그래서 aiomonitor-ng라는 확장 버전을 개발하게 되었습니다. ng는 next-generation의 약자입니다. 크게 다음과 같은 기능들이 추가 및 개선되었습니다.:
- Task creation tracker: 모든 실행 중인 코루틴 작업에 대해, 각 코루틴 작업을 생성(
asyncio.create_task()
)한 작업들에 대해 그 순간의 stack trace를 모두 보존하여 연속된 작업 생성 체인을 모두 알 수 있도록 하였습니다. (ps
,where
명령) - Task termination tracker: 최근 종료된 코루틴 작업들을 최대 N개까지 로그를 보존하고 조회할 수 있게 해줍니다. 특히 어떤 한 작업이 다른 작업을 취소(
Task.cancel()
)한 경우, 취소를 트리거한 순간의 stack trace를 함께 보존하여 연속된 취소 체인을 모두 알 수 있도록 하였습니다. (ps-terminated
,where-termianted
명령) - Persistent task marker: 기본값으로는 메모리 누수를 방지하기 위해 종료된 작업을 최근 N개까지만 추적하지만, 애플리케이션 수명 주기 동안 계속 실행되어야 하는 특정 작업들을 데코레이터로 표시해두면 해당 작업들은 이력 개수 제한과 관계 없이 항상 종료 로그를 보존해주고 종료 로그 조회 명령에서 별도 옵션으로 필터링하는 기능을 제공합니다. (
aiomonitor.task.preserve_termination_log
데코레이터) - 세련된 terminal UI: 기존에 손으로 짜여진 명령어 파싱을 바탕으로 했던 단순 REPL (read-evaluate-print loop) 구성이었던 명령줄 처리를 개선하였습니다. Click과 prompt_toolkit을 활용하도록 aiomonitor 서버측 구현을 재작성하고, 클라이언트도 asyncio로 native하게 동작하는 텔넷 클라이언트를 자체 구현하여 명령어 및 task ID 등의 인자 자동완성을 제공합니다.
실제 사용 화면은 다음과 같습니다.:
aiomonitor-ng 도구를 활용하여 grpcio 라이브러리에서 콜백으로 생성하는 코루틴 작업이 과다 생성되어 발생하는 리소스 누수 및 성능 저하 문제, docker 데몬이 발생시키는 이벤트를 모니터링하는 작업이 특정한 메시지 입력 패턴에 의해 조용하게 종료되어 버리는 바람에 컨테이너 생성이나 삭제 작업의 결과가 리턴되지 않아 시스템이 멈추는 문제 등을 성공적으로 디버깅할 수 있었습니다.
앞으로 aiomonitor-ng를 통해 래블업뿐만 아니라 다양한 Python asyncio 애플리케이션을 개발하는 독자분들께서도 디버깅에 큰 도움을 받기를 바라며 글을 마칩니다.
aiomonitor-ng는 PyPI를 통해 pip install aiomonitor-ng
명령으로 설치하실 수 있으며, 제 깃헙 계정에 오픈소스로 공개되어 있으므로 누구나 사용 및 기여가 가능합니다.