트러블슈팅
문제가 발생했을 때 그 문제를 진단하고 해결하기 위해 하는 행동을 트러블슈팅(troubleshooting)이라고 한다. 트러블슈팅은 대략 다음과 같은 단계를 거친다.
- 문제 정의
- 문제가 무엇인지 확인.
- 예: CPU가 사용률 100% 상승하여 API 처리를 제대로 수행하지 못했다.
- 증거 수집
- 해당 문제가 어디에서 발생했는지, 언제 발생하는지, 또는 이를 발생시키는 특정 조건이 있는지 확인.
- 예: 특정 A API가 10초 내 100번 호출될 때 문제가 발생하고 있었다.
- 원인 규명
- 증거 수집 단계에서 얻은 정들을 기반으로 문제를 발생시키는 원인이 명확한지 확인.
- 예: 특정 A API를 10초 내 100번 이상 호출하여 CPU 사용률 100%를 달성하는지 확인한다.
- 추가 조사 또는 조치
- 원인이 규명되면 그 원인을 발생시키는 근본적인 원인을 추가로 확인. 이때는 2번과 3번 절차를 반복 수행.
- 더 규명할 근본적인 원인이 없다면 조치.
- 예: 특정 A API 내 b 메서드가 있는데 해당 메서드는 CPU 자원을 막대하게 사용하고 있었다. b메서드는 별도의 자원에서 수행될 수 있도록 분리하고 결과 또한 비동기적으로 전달할 수 있도록 재설계했다.
- 문제 해결 확인
- 조치된 사항이 정상적으로 해소되었는지 확인.
- 예: 특정 A API를 10초 내 100번 이상 호출하여 문제가 발생하는지 또는 다른 문제가 새롭게 발생하진 않았는지 확인한다.
이 단계에서 가장 중요한 단계가 2번 증거 수집이다. 증거 수집 단계에서 얼마나 많은 증거를 얻을 수 있는가에 따라 문제 해결을 완벽하게 종료할 수 있는지 아닌지를 결정하게 된다.
많은 증거를 얻기 위해서는 평상시에 많은 정보를 축적해 두고 있어야 한다. 애플리케이션이 구동되는 머신(machine)에 대한 자원 정보와 더불어 애플리케이션 내부 자원 상태와 동작할 때 생성하는 각종 로그가 그 대상 정보라고 볼 수 있다(비즈니스와 관련된 수치들은 그 대상이 아니냐고 궁금해할 수 있는데 해당 수치들은 애플리케이션 로그가 충분할 경우 트러블슈팅 시 큰 도움은 되지 않는다. 단, 서비스의 이상 탐지에는 로그와는 별개로 매우 좋은 효과를 보인다).
수집하면 좋은 정보를 좀 더 상세하게 살펴보자.
- CPU
- CPU 이용률(utilization, usage), CPU 부하(load)를 살펴보면 좋다.
- 리눅스 같은 서버에서
top
명령어를 통해 볼 수 있는 CPU 관련 다음 수치들도 살펴볼 수 있다면 더 좋다: idle, iowait, hardware interrupt, software interrupt, steal. - 관련 수치들이 지속해서 상승 추세에 있다면 어느 프로세스에서 문제를 일으키는지 확인해야 하며 유심히 살펴볼 필요가 있다. 예로 CPU 부하가 상승하였지만 직접 제작한 서버 애플리케이션이 아닌 외부 에이전트에 의해 상승했을 때도 있기 때문이다.
- 메모리
- 메모리 이용률(usage, utilization), swap, cache/buffer 이용률을 살펴보면 좋다.
- 메모리가 지속해서 상승하고 있다면 누출(leak)을 생각해 봐야 하고, swap이나 cache/buffer의 변화가 잦다면 메모리 부족을 생각해 봐야 한다.
- 디스크
- 디스크 사용량(usage), I/O 처리량(read/write amount or times), I/O 처리시간을 살펴보면 좋다.
- 디스크 사용량이 지속해서 상승하고 있다면 불필요한 파일을 제거하지 않는 것이 있나, 또는 일부 프로그램에 문제가 있지 않냐고 생각해봐야 한다. I/O 처리량을 보면서 갑자기 왜 상승했는지 생각해 볼 만하고, I/O 처리시간이 오래 걸린다면 랜덤 접근이 많아졌을 수 있는데 왜 그런지 생각해 볼 만하다.
- 네트워크
- 사실 위 다른 자원보다 더 깊이 있게 봐야 하는 항목이 아닐까 싶다. CPU, 메모리, 디스크는 물리적으로 인접한 공간에 있으므로 환경 변수가 적지만 네트워크는 그렇지 않다. 그리고 다수의 서버와 함께 통신하는 것이 기본이기 때문에 대부분 이 네트워크 때문에 다른 곳에 문제를 발생시킬 확률이 조금 더 높기도 하다.
- 대역폭(bandwidth), 처리량, 지연, 연결 상태와 에러 상태들을 살펴보면 좋다.
- 대역폭이 갑자기 상승한다면 대역폭을 많이 사용하게 된 원인을 찾아야 한다. 지정된 대역폭을 모두 사용해 버리면 서비스는 제대로 제공될 수 없다. 운영 도중 이런 경우가 갑자기 생긴다면 대부분 대역폭이 한정된 곳에서 매우 큰 리소스(애셋 파일들)들을 제공한다든지, 대용량 파일을 동시에 전송하도록 기능이 추가된 경우일 것이다.
- 연결 상태가 지속해서 상승하는 경우 연결의 끊고 맺음을 단기간 반복하고 있다고 볼 수 있으며 이 경우 fd(file descriptor)가 고갈되어 더 이상 연결을 맺을 수 없게 될 수 있다. 네트워크 에러가 지속해서 상승한다면 또는 평소와 추이가 다르다면 네트워크 구간에 어떤 문제가 생겼을 수도 있다.
- 서버 애플리케이션
- 위 머신의 자원에 대해 수집한 것처럼 애플리케이션에도 위와 같은 방식으로 수집하여 살펴보면 좋다.
- 서버 애플리케이션 로그
- 애플리케이션의 로그는 디스크 낭비라고 생각하지 말고 개인 정보 보안등 해당 부분에 어긋나지 않는 것이라면 남기고자 생각하는 모든 것을 남기도록 하는 것이 좋다.
- API 요청을 받거나 메시지를 컨슘할 때 그리고 이를 처리하고 응답을 전달해줬을 때의 시간과 소요 시간을 기록하면 좋다.
- 애플리케이션 내에서 외부 시스템과 통신하는 부분(다른 서버와 API를 이용한 통신, MQ로의 pub/sub, RDBMS와의 통신, Redis와 같은 캐시 시스템과의 통신)에 대해서 모두 기록하고 소요 시간은 어땠는지 남겨두면 좋다(당연히 이런 정보는 Prometheus 같은 모니터링 툴을 이용해 수집하고 있다면 더 좋다).
- 경우에 따라 메서드에 진입하는 구간들을 남겨도 좋다. 해당 부분이 필요하다고 생각되지만 과하게 느껴진다면 특정 서버 1~2개에 대해 APM 툴을 이용해도 되고, 특정 시간 동안만 해당 로그를 수집하도록 샘플링해도 된다.
- Java로 만든 서버 애플리케이션인 경우
- 힙 메모리 사용률, GC의 횟수와 속도, 스레드의 상태별 개수를 살펴보면 좋다.
- 문제가 발생할 당시 힙 덤프와 스레드 덤프를 추가로 획득하면 좋다. 만약 자동화를 하고 싶다면 해당 서버가 다중화로 구성된 다른 서버들과 자원 소모 경향이 다를 경우 자동으로 서비스로부터 배제하며 스레드 덤프와 힙 덤프를 취득하도록 만들어 볼 수 있다(여기서는 상세히 설명하지 않지만, 자동으로 배제할 때 단순히 이 조건으로만 배제하도록 할 때 더 큰 문제가 발생할 수 있다. 여기서는 단순한 예이다.)
- 스레드 덤프는 서비스에 투입되고 있을 때 2~3 초 간격으로 3번 이상 취득하는 게 많은 도움이 된다. 힙 덤프는 서비스에서 제외 후 취득한다. 이렇게 하는 이유는 스레드 덤프와 힙 덤프의 용도가 다르기 때문이다. 사건 현장에 비유하면 스레드 덤프는 CCTV와 같은 것이라면 힙 덤프는 사건이 벌어진 후 잔재들과 같기 때문이다.
- 클라이언트(앱) 애플리케이션
- 사용자 정보 보호와 위반되지 않은 것이라면 서버 애플리케이션과 같이 보낼 수 있는 것은 모두 보내서 살펴보는 것이 좋다.
- 화면과 화면 사이 이동할 때 시간을 수집해서 보내면 기술적인 문제 원인 분석뿐만 아니라 비즈니스적인 인사이트를 얻기에도 좋은 데이터가 된다.
- 앱이 크래시 될 때의 정보들(스택 트레이스, OS, 기기 모델, 브라우저나 WebView 버전, CPU, 메모리, 디스크, 네트워크, 통신사 등)도 같이 기록하여 로그 수집 서버로 전달해 주면 좋다.
위와 같은 정보가 있으면, 특히 애플리케이션 로그가 온전히 충분하다면 해결하지 못하는 문제는 거의 없다고 단언할 수 있다. 만약 한 부분의 정보만 있는 경우에는 자신이 추론한 원인이 맞는지 틀리는지를 검증하는데 시간을 많이 사용해야 하며 결국 시간만 소비하고 규명은 하지 못할 확률이 높다.
자, 그럼 위 정보들이 있을 때 이 정보들을 어떻게 활용하면서 트러블슈팅해야 할까? 어느 문제이든 간에 바로 활용할 수 있는 정보는 없다. 공통으로 사용할 수 있는 부분은 문제가 발생한 시간대를 잡고 그 시간대 주위에서 특이한 자원 이용 변화가 있는 것들을 모두 모아서 “왜 변화가 생겼을까?”라고 계속 되물어보는 것이다. “갑자기 CPU 이용률이 100% 되는 이상 현상”이라는 문제를 예로 든다면 다음과 같이 트러블슈팅해 볼 수 있을 것이다.
- CPU 이용률 이상 현상이 발생한 시간대를 정한다. 문제 영향을 받은 시간 기간에 따라 다르겠지만 영향의 시작과 끝 기준으로 조금씩 기간을 벌여나가면 된다. 예로 ±30분으로 넓혀서 보고 넉넉지 않으면 ±1시간으로 보고… 그런 식으로 확장한다.
- 해당 시간대에 유의미한 자원 이용 변화가 있는 부분을 모두 확인한다. 메모리 이용률, 디스크 처리량, 네트워크 처리량, 외부 시스템과의 네트워크 상황 등이다.
- CPU 이용률이 상승한 시간대에 디스크 I/O 처리량도 똑같이 많아졌다면 당연히 애플리케이션 내 파일과 관련된 작업이 문제가 있다고 가정할 수 있다. 해당 시간대 애플리케이션에서 발생시킨 로그들을 확인하고 문제 되는 지점을 보이면 관련 로직을 찾아 점검한다. 대부분 배치와 같은 스케줄러에 의한 작업이나, 1달에 1~2번씩 비즈니스 요건에 따라 호출되는 특정 기능일 확률이 높다.
- 또는 CPU 이용률이 상승한 시간대에 네트워크 처리량이 갑자기 급증했거나 외부 시스템과의 통신 시간이 평소와 다르게 급격하게 상승했다면 이 부분에 의해 병목이 발생했고 병목이 풀려서 내부 로직이 동시에 수행되어 CPU 이용률이 급격하게 상승했을 수도 있다. 이때는 병목이 발생할 수 있는 부분과 그 이후 수행되는 로직이 무엇인지 판별한다.
- 또는 CPU 이용률이 상승한 시간대에 Java 애플리케이션의 GC 횟수가 급격하게 많아졌거나 시간이 길어진 것을 확인했다면 메모리가 부족하거나 적절치 않은 JVM 옵션이 추가되었을 것이라고 가정해 볼 수 있다.
- 원인을 추정했다면 비슷한 환경을 구축하고 이를 똑같이 수행하여 문제가 발생하는지 확인한다. 문제가 발생하고 자원 이용 형태가 문제 발생 당시와 비슷하다면 원인이 맞음을 확신하고 문제를 수정한다.
- 문제 수정 후 똑같이 검증하여 문제가 발생하지 않는다면 이를 똑같이 반영한다.
결국 트러블슈팅이라는 것은 당시 얼마나 많은 정보가 있는가에 따라 문제 해결이 크게 좌우된다. 충분한 정보가 없는 경우라면 실력이 뛰어난 사람인 경우에만 문제를 잘 해결할 확률이 높다. 실력이 뛰어나다면 적은 정보만으로도 많은 추론을 통해 적절한 원인을 규명하여 결국엔 해결하기 때문이다. 또는 검색으로 우연히 현재 상황과 맞는 해결책을 찾았을 수도 있다!(하지만 운은 그리 오래 가지 않는다!) 아무튼 실력이 뛰어난 사람은 항상 주위에 있지는 않을 것이기 때문에 향후 문제 발생 시 원인을 즉각적으로 해결하고 싶다면 위에서 잠깐 나열했던 정보들은 최소한으로 수집하는 게 좋다.