배포는 성공이라는데 사이트는 502
기능을 만드는 시간보다 "왜 안 뜨는가"를 붙들고 있는 시간이 길었던 시기가 있습니다. 코드에는 아무 문제가 없는데 밖에서 보면 사이트가 죽어 있는, 그런 종류의 문제들입니다.
기록해 두지 않으면 반년 뒤에 똑같이 헤맬 것 같아서, 겪은 순서대로 정리합니다.
입구가 하나뿐인 집
제 서버는 집에 있는 미니PC 한 대입니다. 공인 주소가 하나이고 공유기에서 열어 줄 수 있는 입구도 사실상 하나입니다. 그런데 여기서 웹 서비스를 여럿 돌립니다.
기존 서비스들은 앞단에 웹서버를 하나 두고 들어온 요청의 주소를 보고 각 컨테이너로 넘기는 방식을 씁니다. 잘 도는 구조지만 단점이 분명합니다. 그 웹서버 설정을 잘못 건드리면 전부 같이 내려갑니다. 새 서비스를 붙일 때마다 이미 돌고 있는 서비스들의 입구를 열어 고쳐야 한다는 뜻이기도 합니다.
그래서 이번 서비스는 반대로 갔습니다. 컨테이너가 밖으로 먼저 연결을 걸어 통로를 만드는 방식입니다. 이러면 서버의 입구를 하나도 차지하지 않습니다. 공유기 설정도, 기존 웹서버 설정 파일도 건드릴 일이 없습니다. 기존 서비스 입장에서는 새 서비스가 생긴 줄도 모릅니다. 혼자 여러 서비스를 돌릴 때 제일 중요한 게 하나를 건드릴 때 다른 게 안 죽는 것이라는 걸 이 무렵에 배웠습니다.
주소가 엉뚱한 데로 가던 날
새 서비스 주소를 만들었는데 열어 보니 다른 서비스 화면이 떴습니다.
원인은 도메인 설정이었습니다. 하위 주소를 매번 등록하기 귀찮아서 전부 받아 주는 규칙을 걸어 뒀는데, 그래서 새 주소도 집으로 들어와 버린 겁니다. 그리고 집에서 입구를 쥐고 있는 건 기존 웹서버입니다. 웹서버는 자기가 아는 주소가 하나도 일치하지 않으면 맨 처음 정의된 설정으로 보냅니다. 그렇게 전혀 다른 서비스가 응답하고 있었습니다.
통로 방식은 전용 주소 기록을 따로 만들고 개별 기록이 전부 받아 주는 규칙보다 우선합니다. 그걸 만들어 주니 정리됐습니다. 그리고 문서에 "전부 받아 주는 규칙이 있으니 알아서 연결된다"고 적어 둔 문장을 지웠습니다. 잘못된 설명을 남겨 두면 다음에 같은 자리에서 또 걸립니다.
컨테이너 안에서 말하는 "여기"
통로 설정을 하면서 연결 대상 주소를 적어야 했는데, 습관대로 "이 서버 자신"을 뜻하는 주소를 적었습니다. 그러자 계속 502가 났습니다.
통로를 만드는 프로그램도 컨테이너 안에서 돕니다. 그 안에서 "자기 자신"은 호스트가 아니라 그 컨테이너입니다. 결국 자기를 가리키게 해 둔 셈이었습니다. 연결할 컨테이너의 이름을 적어야 했습니다.
비슷한 착각을 배포 쪽에서도 했습니다. 빌드 서버가 "배포 경로가 없다"며 실패했는데, 서버에 들어가 보면 그 폴더가 멀쩡히 있었습니다. 한참 뒤에야 빌드 서버 자체가 컨테이너 안에서 돌고 있다는 걸 알았습니다. 호스트에 있는 폴더가 그 안에서는 보이지 않는 게 당연했습니다.
여기서 한 번 더 걸렸습니다. 폴더를 컨테이너에 연결해 주면서 컨테이너 안 경로를 편한 이름으로 바꿔 줬는데, 그 안에서 실행한 컨테이너 명령이 또 실패했습니다. 컨테이너 안에서 실행하더라도 폴더 연결을 실제로 해석하는 건 호스트 쪽 도커이기 때문입니다. 호스트에 없는 경로를 찾으니 될 리가 없었습니다. 안과 밖의 경로 이름을 똑같이 맞춰 주고 나서 풀렸습니다.
컨테이너를 쓰면서 겪은 문제의 상당수가 "지금 이 코드는 어디에서 돌고 있는가"를 잘못 잡은 데서 나왔습니다.
죽었는데 살아 있는 프로세스
어느 날 밖에서 접속하면 502가 뜨는데, 서버에 들어가 확인해 보면 컨테이너는 전부 정상이었습니다. 컨테이너 안에서 불러 보면 응답도 정상입니다. 밖에서만 죽어 있었습니다. 지금까지 겪은 장애 중에 이게 가장 오래 걸렸습니다.
단서는 접속 기록이었습니다. 서비스 컨테이너의 접속 기록에 자체 상태 확인 요청만 찍혀 있고 바깥에서 들어온 흔적이 하나도 없었습니다. 요청이 아예 도달하지 못하고 있다는 뜻입니다.
범인은 며칠 전 도커를 재시작할 때 남은 유령 프로세스였습니다. 컨테이너는 사라졌는데 그 프로세스가 죽지 않고 남아 여전히 통로에 붙어 있었습니다. 도커는 그 컨테이너를 잊었기 때문에 목록 어디에도 나오지 않습니다. 바깥 서비스 입장에서는 통로에 붙은 연결이 둘이니 요청을 나눠 보냈습니다. 유령 쪽으로 간 요청은 이제 없는 컨테이너를 찾다가 502를 돌려주고 있었습니다.
프로세스를 직접 찾아 종료하고 스택을 다시 올리니 정상으로 돌아왔습니다. 이 일 이후로 장애 점검 순서 맨 앞에 "도커가 모르는 프로세스가 입구를 잡고 있는지"를 넣었습니다.
초록불이 배포를 보장하지 않는다
같은 새벽 배포에서 또 하나를 놓쳤습니다. 빌드는 성공으로 끝났는데, 실제로는 이미지만 만들어지고 컨테이너는 하나도 뜨지 않은 채 방치돼 있었습니다.
빌드 도구가 찍어 주는 성공 표시는 "스크립트가 끝까지 돌았다"는 뜻이지 "서비스가 살아 있다"는 뜻이 아닙니다. 그래서 배포 후 확인을 두 가지로 고정했습니다. 컨테이너 목록을 보고 밖에서 실제 주소를 한 번 불러 봅니다. 둘 다 통과해야 배포가 끝난 것으로 칩니다.
비슷한 함정이 하나 더 있었습니다. 데이터베이스 구조 변경을 손으로 반영하고 기록용 표식을 찍는 걸 빼먹었는데, 그날은 아무 일도 없었습니다. 문제는 그다음 빌드에서 터졌습니다. 검사가 빌드보다 앞에서 도는 탓에 직전에 만들어진 이미지 기준으로 통과해 버리고 한 박자 뒤에 실패가 나타나는 구조였습니다. 데이터베이스와 전혀 상관없는 커밋에서 배포가 멈추니 원인을 짐작하기가 더 어려웠습니다. 결국 반영 스크립트가 표식까지 같이 찍도록 고쳐서 사람이 빠뜨릴 여지를 없앴습니다.
백업이 11일 동안 한 건도 없었다
백업 스크립트를 걸어 뒀고 예약 설정도 정확히 등록돼 있었습니다. 그런데 기록 파일을 열어 보니 "권한이 없습니다"만 열한 줄 쌓여 있었습니다. 11일 동안 백업이 한 건도 만들어지지 않았습니다. 지금까지 겪은 일 중에 제일 아찔했습니다.
원인은 배포 방식이었습니다. 배포할 때 파일을 복사하면서 실행 권한이 떨어져 나갔습니다. 형상관리에 실행 속성을 지정해 두지 않았던 탓입니다. 마침 다른 스크립트 하나는 살아 있어서 겉으로는 아무 문제가 없어 보였습니다.
이 함정은 이미 한 번 겪고 문서에도 적어 둔 것이었습니다. "실행 속성도 같이 지정해야 한다, 전에 이미 겪었다"는 문장이 그대로 있었는데 같은 자리에서 또 걸렸습니다. 글로 적어 두는 것만으로는 안 막힌다는 증거였습니다. 그래서 권한을 복구하는 것으로 끝내지 않고 예약 작업이 실행 권한과 무관하게 스크립트를 부르도록 방식을 바꿨습니다. 사람이 기억해야 하는 절차는 결국 언젠가 빠집니다.
서버 안과 밖
여기 적은 장애 중에 코드가 원인이었던 건 거의 없습니다. 대부분 설정 문제이거나, 환경을 잘못 이해한 탓이거나, 자동화가 조용히 실패한 경우였습니다.
그래서 요즘은 배포가 끝나면 항상 밖에서 주소를 한 번 불러 봅니다. 서버 안에서 잘 도는 것과 밖에서 접속되는 것은 다른 이야기였습니다.