옵저버빌리티란 무엇인가
원래는 제어이론 용어로, 시스템 밖으로 나오는 출력만 보고 내부 상태를 얼마나 알아낼 수 있는가를 뜻합니다. 소프트웨어로 옮기면 이렇게 됩니다. 서버에 직접 들어가지 않고, 코드를 고쳐 로그를 더 심지 않고, 지금 나오고 있는 데이터만으로 문제를 설명할 수 있는가.
핵심은 미리 정해둔 질문에만 답하는 게 아니라는 점입니다. 응답 시간이 3초를 넘으면 알려달라는 건 질문을 미리 정해둔 겁니다. 반면 "어제 오후에 특정 기종에서만 업로드가 실패한 이유"는 미리 정해둘 수 없었던 질문입니다. 이런 걸 나중에 던질 수 있으면 관측 가능한 시스템이고, 던질 수 없으면 코드를 고쳐 배포한 뒤 문제가 다시 나기를 기다려야 합니다.
말로만 보면 당연한 얘기 같은데, 실제로 어떤 상황에서 갈리는지는 서비스를 놓고 봐야 와닿습니다.
반장노트에서는 왜 필요할까
반장노트는 건설 현장의 반장과 팀장을 잇는 앱입니다. 지금까지 톡과 문자로 오가던 구인 공고, 지원, 서류 제출, 근태 기록을 앱으로 옮깁니다. 팀장이 공고를 올리면 반장이 지원하고, 제출한 서류를 팀장이 검토해 부족한 건 보완을 요청하고, 선정된 사람을 팀에 배치한 뒤 출퇴근과 일당을 기록합니다.
구조에서 중요한 건 이겁니다. 사용자 식별과 인증, 서류 파일 보관, 알림, 결제 등 사내 플랫폼의 다른 마이크로서비스를 사용하고 있습니다. 반장노트 서버는 자체의 비즈니스 로직도 실행하지만, 각 마이크로서비스의 흐름을 엮어 더 복잡한 기능을 수행합니다.
이제 상황 하나를 놓고 보겠습니다. "어제부터 서류 제출이 가끔 안 된다"는 제보를 받았습니다.
가끔이라는 게 문제입니다. 열 번 눌러보면 아홉 번은 됩니다. 서버는 살아 있고 CPU도 정상이고 에러 알림도 안 왔습니다. 우리가 지켜보던 지표 중에 이상한 건 하나도 없습니다. 로컬에서 재현하려고 하면 잘만 됩니다.
로그를 뒤진다 해도 어떤 키워드로 찾을지, 어느 시간대를 볼지부터 막힙니다. 게다가 이 요청은 우리 서버에서만 처리되는 게 아닙니다. 인증을 거치고 파일 보관 서비스를 부르니, 우리 로그에 아무 이상이 없다고 해서 문제가 없는 것도 아닙니다. 실패한 요청 하나를 골라 그게 어디까지 갔다가 어디서 멈췄는지 따라갈 수 있어야 하는데, 그럴 수단이 없으면 할 수 있는 게 추측밖에 없습니다.
여기서 모니터링이 왜 반쪽인지도 드러납니다. 모니터링은 알고 있는 문제를 감시하는 일입니다. 디스크가 차면 알려줘, 에러율이 1%를 넘으면 알려줘. 무엇이 위험한지 이미 알고 있어야 임계값을 정할 수 있습니다. 그런데 실제 장애의 상당수는 우리가 예상하지 못한 조합에서 나옵니다. 특정 버전 앱에서, 특정 시간대에만 나는 문제는 미리 알람을 걸어둘 수가 없습니다. 무슨 조합인지 몰랐으니까요.
정리하면 모니터링은 알람이 울리게 하는 일이고, 옵저버빌리티는 알람이 울린 뒤에, 혹은 울리지도 않은 문제에 대해 이유를 찾을 수 있게 하는 일입니다. 알람만 있고 파고들 데이터가 없으면 장애 상황에서 추측만 하게 되고, 데이터만 쌓고 알람이 없으면 문제가 났다는 사실을 사용자가 먼저 알게 됩니다. 둘 다 필요합니다.
옵저버빌리티에서의 세 가지
메트릭은 숫자를 시간축에 쌓은 것입니다. 요청 수, 응답 시간 분위수, 에러율 같은 것들입니다. 가볍고 오래 보관할 수 있는 대신 개별 사건은 사라집니다. 에러율이 2%라는 건 알아도 누가 왜 실패했는지는 없습니다. 라벨 조합마다 시계열이 생기므로 사용자 ID처럼 값의 종류가 많은 건 넣으면 안 됩니다.
로그는 그 순간 무슨 일이 있었는지 적은 기록입니다. 가장 자세하지만 양이 많고 비쌉니다. 사람이 읽기 좋은 문장으로만 쌓아두면 나중에 조건을 걸어 찾기 어려우니, 키와 값이 구분된 형태로 남기는 편이 낫습니다.
트레이스는 요청 하나가 시스템을 지나간 경로입니다. 셋 중 유일하게 요청 단위로 보는 데이터라, 개별 사건을 잃는 메트릭과 흐름을 잃는 로그 사이를 메웁니다.
셋을 따로 놓으면 각각 반쪽입니다. 실제로는 순서를 갖고 이어집니다. 메트릭이 실패율이 0.1%에서 4%로 올랐다고 알립니다. 트레이스가 실패한 요청들의 경로를 열어 전부 파일 서비스 구간에서 멈춰 있다는 걸 보여줍니다. 로그가 그 구간에서 어떤 파라미터로 호출해 무슨 응답을 받았는지 말해줍니다. 무엇이 이상한지, 어디가 문제인지, 왜 그런지가 차례로 좁혀집니다.
이 셋을 잇는 접착제가 상관관계 ID입니다. 요청이 들어올 때 추적 ID를 하나 만들고 거쳐가는 모든 서비스와 모든 로그 줄에 같은 ID를 답니다. 그러면 트레이스에서 본 느린 구간을 눌러 그 구간의 로그로 바로 넘어갈 수 있습니다. 이게 없으면 셋은 서로 다른 데이터 더미일 뿐이고, 그래프가 튀는 걸 보고 로그 검색창에서 시간대를 눈대중으로 맞춰 뒤지게 됩니다. 그건 관측이라기보다 수색에 가깝습니다.
서비스가 쪼개지면 트레이싱이 필요해진다
옵저버빌리티가 선택이 아니라 필수가 된 계기는 시스템이 여러 서비스로 나뉘면서부터입니다.
모놀리식 서비스는 예외가 나면 스택 트레이스에 원인이 다 들어 있었습니다. 하지만 서비스를 나누면 이게 사라집니다. 스택 트레이스는 서버 간의 경계를 넘지 못하고, 우리 로그에는 "호출 실패"까지만 남습니다. 다른 마이크로서비스의 로그를 열어볼 수는 있습니다만, 문제는 그 수많은 줄 중에 어느 것이 우리 요청이었는지 짚을 방법이 없다는 겁니다. 볼 권한이 있느냐가 아니라 이어붙일 수단이 있느냐의 문제입니다.
여기에 몇 가지가 겹칩니다. 화면 하나를 그리려고 서비스 다섯 개를 부르는데 하나만 실패하면 응답은 200으로 나가서 에러율에 잡히지도 않습니다. 배포 주기도 서비스마다 달라서, 어제까지 되던 게 안 되는데 우리는 배포한 적이 없을 수 있습니다. 실패가 우리 코드 때문인지 상대 서비스 때문인지 구분이 안 되면, 장애 대응이 원인 추적이 아니라 서로 확인해달라는 요청을 주고받는 일이 됩니다.
또한, 이 상황에서 메트릭과 로그만으로는 버티기 어렵습니다. 메트릭은 서비스마다 각자 정상이라고 말합니다. 다섯 개가 200밀리초씩 쓰는데 그게 순차로 쌓여 사용자는 2초를 기다리고 있어도, 개별 지표만 봐서는 합쳐진 모습이 안 나옵니다. 로그는 한곳에 모아둔다 해도 시간순 나열과 요청 하나의 흐름이 다릅니다. 호출이 병렬로 나가거나 비동기로 넘어가면 시간순으로는 인과 관계를 알 수 없습니다.
트레이싱은 이 둘이 못 하는 일을 합니다. 요청 하나를 단위로, 어떤 호출이 어떤 호출을 낳았는지를 부모 자식 관계로 남기고 각 구간의 시간을 함께 기록합니다. 흩어진 사실을 시간순이 아니라 인과순으로 복원하는 겁니다. 그래서 이런 질문에 답할 수 있게 됩니다. 이 요청이 실제로 어디를 거쳤나. 전체 2초 중 어느 구간이 시간을 썼나. 연쇄 실패에서 가장 시끄러운 곳 말고 실제 발화점은 어디였나. 한 요청에서 같은 서비스를 여덟 번 부르고 있지는 않나.
반장노트에 대입하면 더 분명해집니다. 서류 제출 한 번에 요청 하나가 인증을 거쳐 사용자를 식별하고, 파일 보관 서비스에 파일을 넘기고, 결과를 저장한 뒤 상대에게 알림을 보내기까지 갑니다. 우리가 짠 코드는 그중 일부일 뿐이고, 그래서 실패가 나는 자리도 '반장노트 서버 안'이 아니라 서비스 사이입니다. 서류와 알림처럼 각 마이크로서비스의 흐름에 가까운 기능일수록 실제 처리는 우리 프로세스 밖에서 일어납니다.
트레이스가 없으면 우리 쪽에는 "서류 제출 실패" 한 줄만 남습니다. 트레이스가 있으면 인증까지는 20밀리초에 끝났고 파일 보관 호출에서 멈췄다는 게 바로 보이고, 그 지점을 눌러 로그를 열면 어떤 요청을 보내 무슨 응답을 받았는지가 나옵니다. 원인을 찾는 일이 어느 팀 책임인지 따지는 대화가 아니라, 어느 구간에서 무슨 응답이 왔는지 확인하는 일이 됩니다.
프로세스가 깁니다. 공고에 지원하고, 팀장이 검토하고, 선정되고, 대기하다가 팀에 배치되고, 출근이 확정되기까지 여러 단계를 거칩니다. 중간 한 단계가 조용히 실패하면 사용자 화면에는 에러조차 안 뜹니다. 그냥 아무 일도 일어나지 않습니다. "지원했는데 며칠째 아무 연락이 없다"는 문의를 받으면 어느 단계까지 갔다가 어디서 멈췄는지 되짚는 게 대응의 거의 전부입니다. 요청 단위로 경로가 남아 있느냐가 여기서 갈립니다.
사용자에게 되물을 수 없습니다. 반장노트를 쓰는 분들은 현장에서 일하는 근로자와 관리자입니다. 개발자 도구를 열어달라거나, 다시 해보고 어떻게 되는지 알려달라고 부탁하는 것은 불가능에 가깝습니다. 고객 문의(Customer Service)로 시작해 원인까지 파악하려면 그 시각에 무슨 일이 있었는지가 시스템에 남아 있어야 합니다.
마무리
옵저버빌리티를 한 문장으로 줄이면, 미처 예상하지 못했던 질문을 나중에 던질 수 있게 시스템을 만들어두는 일입니다.
주의할 건 도구를 붙이면 관측된다고 생각하는 것입니다. 라이브러리를 넣고 수집기를 띄우는 건 시작일 뿐입니다. 신호가 실제로 저장소까지 도달하는지, 도달한 걸 누가 보는지, 이상할 때 누구에게 알려지는지까지가 한 세트입니다. 셋 중 하나만 빠져도 관측되지 않는 것과 같습니다.
우리는 코드를 짤 때 잘 도는 경우를 먼저 생각합니다. 하지만 운영에서 시간을 잡아먹는 건 어쩌다 한 번 이상해지는 경우이고, 서비스가 나뉘어 있으면 그게 어디서 났는지조차 알기 어렵습니다. 기능을 하나 만들 때마다 한 번쯤 생각해 볼 만합니다. 이게 새벽 세 시에 잘못되면, 나는 무엇을 보고 원인을 찾아야 할까.
Ted