전화는 새벽 2시에 울렸다. Riverside에 있는 이커머스 DC의 시프트 슈퍼바이저가 거의 소리를 지르다시피 말했다. "Lao Mi, 소터가 맛이 갔어요! 같은 카톤이 세 번이나 스캔됐고, 슈트는 넘쳐나고, 컨베이어가 곧 막힐 것 같습니다!"

나는 그에게 장비에 손대지 말고 대신 로그 파일 두 개를 뽑으라고 했다. 30분 뒤 그는 머쓱한 목소리로 다시 전화했다. "맞는 말씀이셨습니다……WES가 같은 웨이브를 두 번 릴리스했더라고요."

이런 영화는 너무 여러 번 봤다. 현장은 언제나 "시스템이 글리치를 일으키고 있다"고 말한다. 내 경험으로는 열에 아홉, 글리치처럼 보이는 것은 WES와 WCS가 서로 싸우고 있는 상태이고, 근본 원인은 예외 없이 거의 그 사이의 인터페이스에 있다. 다음은 내가 실제로 사용하는, 증상에서 근본 원인까지의 트러블슈팅 순서다.

라인에 손대지 마라. 먼저 두 로그를 나란히 놓고 이야기하게 하라.

명령 충돌은 대개 두 가지 중 하나로 일어난다. 작업 중복 디스패치, 또는 상태 콜백 로스트다. WES가 릴리스 명령을 보내고, 네트워크가 끊기고, 수신 확인이 돌아오지 않자 WES는 설정된 타임아웃 뒤에 같은 명령을 다시 쏜다. WCS는 두 명령을 모두 기쁘게 받아들인다. 인터페이스에 작업 번호 중복 제거를 넣어 둔 사람이 아무도 없었기 때문이다. 하나의 작업이 분류 큐에 두 번 들어가고, 같은 파셀이 슈트에서 두 번, 세 번씩 요청된다.

스텝 1은 항상 같다. WES 디스패치 로그와 WCS 명령 수신 로그를 나란히 놓고, 작업 번호를 하나씩 맞추어 본다. 그런데 열 개의 창고 중 여덟이 걸리는 함정이 있다. 두 서버의 시계가 동기화되지 않은 것이다. WES 로그에는 명령이 02:14:03에 나갔다고 되어 있고, WCS 로그에는 02:13:47에 받았다고 되어 있다. 타임스탬프가 일치하지 않으면 인과관계는 결코 확정할 수 없다. 어떤 로그에 손대기 전에 양쪽이 NTP에 올라 있는지, 그리고 타임존을 확인하라. 한쪽은 UTC, 다른 쪽은 현지 시간으로 로그를 찍던 현장을 나는 한 곳이 아니라 여러 곳에서 봤다.

나 자신도 2021년에 이것에 걸려 넘어졌다. Ontario의 DC에서, 100,000 평방피트가 넘는 곳, 크로스벨트 소터를 돌리고 있었다. 피크 시즌 한밤중, 같은 파셀이 시스템에 세 번 나타났다. 두 로그를 나란히 놓으니 명백했다. WES가 웨이브를 두 번 릴리스했고, 간격은 정확히 타임아웃 한 번 분이었고, WCS 실행 로그에는 동일한 작업 번호가 두 개, 둘 다 실행되어 있었다. 한 문장으로 근본 원인: 멱등성 없는 인터페이스에 타임아웃 재전송이 부딪혔다는 것이다.

이 타임아웃 값에는 진짜 트레이드오프가 숨어 있다. 너무 짧게 잡으면 네트워크의 작은 흔들림마다 오판 재전송이 날아가고, 너무 길게 잡으면 웨이브 변경과 긴급 주문이 기다리다 늙는다. 이 숫자는 감으로 정할 수 없다. 실제 네트워크에서 측정한 레이턴시에서 나와야 하고, 재시도 횟수와 백오프 정책은 인터페이스 사양서에 문서로 남겨야 한다. 말로만 합의하고 넘어가는 인테그레이터가 너무 많다. 그리고 그 합의는 엔지니어와 함께 문밖으로 걸어나간다.

실행 플로어의 분류 라인

긴급 주문이 실행 중인 작업을 밀어낼 때, 옛 작업은 어디로 가는가

긴급 주문 선점(preemption)은 가장 까다로운 충돌 지대이며, 로직은 단순 중복보다 훨씬 까다롭다. WES가 긴급 주문을 받으면, 현재 웨이브 작업의 일부를 취소로 표시하고 새 작업을 끼워 넣는다. 그런데 WCS는 이미 그 명령들을 PLC로 내려보냈다. 이미 움직이고 있는 다이버트를 멈추기에는 취소가 너무 늦게 도착한다. 이제 같은 파셀이 옛 작업에서는 슈트 A로, 새 작업에서는 슈트 B로 불리고 있다. 장비는 어느 쪽 말을 들을까.

트러블슈팅이란 두 실타래를 추적하는 일이다. WES 작업 상태 기계(취소가 정말 나갔는지, 정확히 언제인지)와 WCS 명령 큐(취소가 도착했을 때 그 명령이 실행의 어느 지점에 있었는지). 결정적 증거는 역시 타임스탬프 차이, 즉 취소가 발행된 시점과 명령의 실행이 실제로 끝난 시점이다. 계산해 보면 취소가 따라잡을 가망이 있었는지 알 수 있다.

그 아래에 더 교활한 함정이 있다. 애초에 양쪽은 같은 작업 번호로 말하고 있지 않다. WES는 자체 작업 ID를 쓰고, WCS는 그것을 설비 명령 번호로 변환한다. 중간에 매핑 테이블이 있다. 그걸 잃어버리면 두 시스템은 서로 다른 이야기를 하게 된다. 2023년에 Chicago 외곽의 창고에서 봤다. 매핑 테이블이 메모리에 살고 있었는데, 밤에 WCS 서비스가 재시작하면서 사라졌다. 야간 근무 엔지니어는 두 로그를 들여다봐도 일치하는 작업을 하나도 찾지 못했다. 로그가 없었기 때문이 아니라 번호가 맞지 않았기 때문이다. 화려하지 않지만 확실한 해결책이 있다. 매핑 테이블을 디스크에 영속화하고, WCS 로그에 원래 WES 작업 번호를 끝까지 싣는 것이다.

긴급 주문에 대한 내 결론은 이렇다. 물량이 요구하지 않는 한 하드 선점은 하지 마라. 긴급 주문에는 전용 슈트나 전용 웨이브를 줘라. 이미 실행 중인 작업에는 손대지 마라. 선점으로 아끼는 몇 분으로는 라인 정체 한 번의 비용을 감당할 수 없다. 선점해야 한다면 취소는 반드시 2단계로 가야 한다. WCS에서 미실행 명령을 잠그는 프리 취소(pre-cancel), 그 다음 정식 취소. 이미 움직이고 있는 것은 완료 후 보상 플로우로 간다. 하나의 취소 명령이 물리 법칙을 이기길 기대하는 건 광속에 대한 예의를 잊은 것이다.

왜냐고 묻지 마라. 멱등하게 만들어라.

위에서 본 두 가지 고장 모드는 근본 원인이 하나다. 인터페이스가 멱등하지 않다는 것이다. WES 측에 필요한 것은 간단하다. 모든 작업이 전역에서 유일한 작업 번호를 가지고, 재전송 시에도 같은 번호를 유지하는 것. WCS 측은 수신 시 "이 작업 번호를 본 적 있는가"를 확인하는 것. 본 적이 있다면 성공을 반환하고 다시 실행하지 않는다. 상태 콜백도 같다. 단조 증가하는 시퀀스 번호를 붙여서, WES가 순서가 뒤바뀌거나 중복된 콜백을 스스로 정리할 수 있게 한다.

인수 단계에서 나는 인테그레이터에게 세 문장을 건넨다. 그리고 계약서 부록으로 들어간다. 첫째, 네트워크를 몇 초 끊었다가 다시 연결한다. 어떤 작업도 두 번 실행되어서는 안 된다. 둘째, WCS를 재시작한다. 과거 작업 매핑은 살아남아야 한다. 셋째, 실행 중인 작업에 대한 긴급 주문 취소는 정의된 보상 액션을 발동시켜야지, "취소는 보냈다"로 끝나서는 안 된다. 이 셋을 통과하면 실행 계층 괴담의 90%는 일어나지 않는다.

내 결론은 그대로다. "시스템 글리치"의 열에 아홉은 멱등성 없는 인터페이스다. 나머지 열의 하나 중 절반은 동기화되지 않은 시계이고, 절반은 새벽 2시 버튼 위 누군가의 떨리는 손이다.

다음 고라이브(go-live) 전에, 네트워크 케이블을 몇 초 뽑았다가 직접 다시 꽂아 봐라. 그 몇 초를 살아남는 시스템만이 피크 시즌을 맡을 자격이 있다.

영어판 번역본입니다. 내용이 다를 경우 영어판을 우선합니다.