LabHub
배우기 러닝패스 코스

FDE 캡스톤: 창고가 같은 주문을 세 번 받았다 · 초록불 다음의 한 건 · 이론

헬스체크는 초록인데 주문이 안 들어간다

LabHub 에서 이어서 보기

한 줄 요약

배포는 "새 판이 떠 있다" 가 아니라 "새 판으로 업무 한 건이 끝까지 된다" 를 확인할 때 끝난다. 판을 디렉터리로 나누고 심링크 하나로 한 번에 바꾸면, 확인에 실패했을 때 같은 방법으로 한 번에 되돌릴 수 있다.

왜 이게 필요했나

고객사의 주문 접수 API 를 1.5.0 으로 올린 날, 배포 담당은 헬스체크가 초록인 것을 보고 완료를 보고했다. 그날 밤 화면은 주문마다 "접수됨" 을 띄웠는데 창고에는 한 건도 들어가지 않았다. 새 판에서 저장 경로 설정이 빠져 있었던 것이다. 헬스체크는 거짓말을 하지 않았다. 프로세스는 살아 있었고 /health 는 200 을 돌려줬다. 다만 그 질문은 "살아 있나" 였고, 고객이 궁금한 것은 "주문이 들어가나" 였다.

FDE 는 고객 호스트에서 배포 자동화가 없거나 빈약한 상태로 일하는 경우가 많다. 쿠버네티스의 롤링 업데이트나 블루그린 전환을 쓸 수 없는 VM 한 대에서도, 같은 원리를 파일시스템과 셸 스크립트로 구현할 수 있어야 한다.

어떻게 동작하나

구조는 단순하다.

/root/site/├── releases/│   ├── 1.4.0/        (VERSION, app.py)│   └── 1.5.0/├── current -> releases/1.4.0├── shared/           orders.jsonl · app.pid · app.log  (판이 바뀌어도 남는 것)└── deploy.log        배포 한 번에 JSON 한 줄

앱은 항상 current/app.py 로 뜬다. 판을 바꾸는 일은 파일을 덮어쓰는 것이 아니라 current 가 가리키는 곳을 바꾸는 것이고, 되돌리기도 똑같은 동작이다. 옛 판 디렉터리가 그대로 있으니 되돌리는 데 다시 받거나 풀 필요가 없다.

핵심은 "바꾸는 순간" 이다. [rename(2)](https://man7.org/linux/man-pages/man2/rename.2.html)은 newpath 가 이미 있으면 원자적으로 교체해서, 다른 프로세스가 그 이름이 없는 순간을 보지 않는다고 보장한다. newpath 가 심링크면 링크 자체가 덮어써진다. 그래서 같은 디렉터리에 임시 링크를 만들고 current 위로 rename 하면 된다. 같은 디렉터리여야 하는 이유도 같은 문서에 있다. 서로 다른 마운트 사이의 rename 은 EXDEV 로 실패한다. 파이썬에서는 [os.replace](https://docs.python.org/3/library/os.html)가 같은 일을 하고, 성공하면 원자적이라고(POSIX 요구사항) 문서에 적혀 있다.

그렇다면 흔히 쓰는 ln -sfn 은? [GNU ln 매뉴얼](https://www.gnu.org/software/coreutils/manual/html_node/ln-invocation.html)은 -f 를 "기존 대상을 지운다" 로 설명하면서도, --backup 을 쓰지 않으면 대상이 없는 짧은 순간이 없다고 적는다. 이것은 오래된 동작이 아니다. [coreutils NEWS](https://raw.githubusercontent.com/coreutils/coreutils/master/NEWS)의 8.27(2017-03-08) 항목이 ln -f A B 가 더 이상 B 를 먼저 지우지 않는다고 알린다. 실습 이미지(coreutils 9.4)에서 실측하니, 한 프로세스가 ln -sfn 으로 3,000번, 임시 링크+mv -T 로 또 3,000번 current 를 번갈아 바꾸는 동안 다른 프로세스가 약 1천만 번 readlink 해서 두 방식 모두 "없음" 을 한 번도 보지 않았다. 그러니 차이는 도구의 판에 기대느냐, 시스템 호출의 보장에 기대느냐다. 고객 호스트의 ln 이 몇 판인지 모른다면 rename 쪽이 설명하기 쉽다.

더 자주 사고를 내는 것은 -n 이다. 매뉴얼은 -n 을 "마지막 인자가 디렉터리를 가리키는 심링크일 때 특별히 취급하지 않는다" 로 설명한다. 실측에서 current 가 releases/a 를 가리킬 때 ln -sf releases/b current 를 실행하면 current 는 그대로이고, releases/a 안에 b 라는 링크가 새로 생겼다. 명령은 성공으로 끝나므로 아무도 모른다.

헬스체크와 스모크 시험은 역할이 다르다.

| 확인 | 묻는 것 | 이 실습의 방법 |
| --- | --- | --- |
| 헬스체크 | 새 프로세스가 떠서 응답하나 | /health 200, 그리고 version 이 새 판인가 |
| 스모크 | 업무 경로 한 건이 끝까지 되나 | 주문 한 건 POST 후 GET 으로 되읽기 |

헬스체크에서 version 을 확인하는 이유가 있다. 옛 프로세스를 내리지 못한 채 새 프로세스가 포트 충돌로 죽으면, /health 에 답하는 것은 옛 판이다. 초록불이 새 판의 것인지부터 확인해야 한다. 또 새 판이 캐시를 적재하느라 2초 넘게 걸릴 수 있으니 상한까지 기다리되, 프로세스가 이미 죽었으면 기다리지 않는다.

현장에서 만나는 모습

되돌리기에서 가장 흔한 실수는 기록 없이 되돌리는 것이다. 다음 날 아침 고객은 "어젯밤 1.5.0 이 올라갔다고 들었는데 왜 1.4.0 이냐" 고 묻는다. 무엇을, 왜, 어느 판으로 되돌렸는지 한 줄이 있어야 대화가 시작된다. 실패한 판의 디렉터리도 지우지 않는다. 조사할 대상이기 때문이다.

두 번째는 보존 정리다. 디스크를 아끼려고 "최신 3개만 남긴다" 를 크론에 걸어 두면, 되돌림 뒤에는 current 가 최신이 아닐 수 있다. 1.6.0 을 되돌려 1.5.2 로 돌아온 상태에서 1.7.0, 1.7.1 이 연달아 실패하면 최신순 정리가 지금 돌고 있는 판을 지운다. current 와, current 가 문제일 때 돌아갈 직전 판은 날짜와 무관하게 지킨다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

빌드를 손으로 풀어 current 를 만든 뒤, 원자 전환 스크립트, 헬스체크와 스모크가 들어간 배포 스크립트, 되돌림, 보존 정리를 차례로 만든다. 채점기는 매번 다른 판 번호로 정상·스모크에서만 깨지는·뜨자마자 죽는·늦게 뜨는 빌드를 만들어 여러분의 스크립트를 실행하고, 실제로 응답하는 판과 심링크를 다시 잰다. 마지막에 고객의 1.5.0 을 직접 배포해 되돌리고 사건 기록을 남긴다.