하고 싶었던 일은 아주 단순했습니다.
새 글을 쓰면 최대한 빨리 올린다.
그게 전부였습니다.
그런데 어느새 글을 올리기 전에 회의가 열려 있었습니다.
이번엔 누가 맡지? Workers야, Actions야? 예산은 남았나? 지난번 잠금은 아직 살아 있나? 표시는 누구 이름으로 되어 있지? 몇 번이나 다시 시도했지?
글은 기다리고 있습니다.
시스템도 기다리고 있습니다.
모두가 "내 일이 아니다"라고 생각합니다.
회사라면 회의실로 불러 모을 수 있지만, 분산 시스템은 말없이 멈춥니다.
이것이 Live Schedule 지옥입니다.
1. 바로 올리고 싶었을 뿐인데, 어느새 담당자 선거를 하고 있었다
게시 과정을 튼튼하게 만들려고 하면 자연스럽게 안전장치를 더하게 됩니다.
Workers가 멈추면 Actions로 넘긴다. Actions 예산이 빠듯하면 Workers로 넘긴다. 같은 일을 두 번 하지 않도록 잠금을 둔다. 두 곳이 같은 일을 건드리지 않도록 표시를 둔다. 실패하면 다시 시도한 횟수를 센다. 감시하는 역할도 붙인다.
하나씩 보면 모두 맞는 생각입니다.
문제는 이것들을 한 줄로 이어 놓는 순간 생깁니다.
"이 글을 올릴 수 있나?"보다 먼저 "누가 올릴 자격이 있나?"가 거대한 상태 관리가 됩니다.
가장 위험한 것은 담당자 표시만 살아남는 경우입니다.
Workers 쪽 작업은 멈췄습니다. 그런데 기록에는 "Workers 담당"이라고 남아 있습니다. 다음에 Actions가 깨어나도 "Workers 담당이니 나는 건너뛴다"고 판단합니다.
고장 난 곳은 없습니다. 일하는 곳도 없습니다.
분산 시스템판 "담당자는 자리를 비웠습니다"입니다.
2. 진짜 문제는 실행 능력이 아니라 조율이었다
어떤 실행에서는 Actions에 처리할 힘도 예산도 남아 있었습니다.
그런데도 본 작업은 돌지 않았습니다.
이유는 간단합니다. "그 일은 Workers 담당"이라는 판정이 나왔기 때문입니다.
여기가 중요합니다.
CPU가 모자란 것이 아닙니다. API가 전부 죽은 것도 아닙니다. Actions를 시작할 수 없는 것도 아닙니다.
문제는 실행이 아니라 조율(coordination)입니다.
일할 힘이 아니라 담당을 정하는 일이 막고 있습니다.
이렇게 되면 버그를 하나씩 고쳐도 끝나지 않습니다.
데이터를 가져오는 양을 고친다. main에 반영하지 못하는 실패를 고친다. 다시 시도하는 부분을 고친다. 담당을 바꾸는 부분을 고친다. 그러면 이번엔 오래된 표시가 남는다. 다음엔 잠금이 남는다. 감시는 문제를 알아채지만, 감시 역할에는 글을 올릴 권한이 없다.
두더지 잡기 같지만 두더지가 나오는 구멍은 하나입니다.
관리하는 부분이 너무 똑똑합니다.
3. schedule은 시계일 뿐, 작업 목록이 아니다
GitHub Actions의 schedule은 편리합니다. 하지만 공식 문서에도 부하가 높을 때 늦어질 수 있고, 부하가 아주 높으면 대기 중인 작업 일부가 사라질 수 있다고 적혀 있습니다.
그러니 schedule은 "대충 이 시간쯤 깨워 줘"에는 잘 맞습니다.
하지만 "이 원고는 반드시 누군가 가져가서, 성공할 때까지 붙들고 있어 줘"까지 맡기면 역할이 맞지 않습니다.
시계는 울립니다.
시계는 원고를 안아 주지 않습니다.
반면 메시지 큐는 작업 자체를 보관합니다. 다시 시도, 지연, 그리고 계속 실패한 작업을 따로 모아 두는 "실패 보관함(dead-letter queue)" 같은 실패 처리도 할 수 있습니다.
여기서 중요한 것은 "반드시 새 서비스를 넣으라"는 말이 아닙니다.
역할을 이렇게 나누자는 것입니다.
- schedule = 일정한 간격으로 깨우는 장치
- 대기 목록 = 아직 끝나지 않은 일의 기준 기록
- 실행자 = 실제로 일하는 쪽
- 짧은 잠금 = 동시에 두 곳이 일하지 못하게 하는 열쇠
- 다시 시도와 실패 보관함 = 실패한 일이 가는 곳
시계에게 인사팀, 창고, 분실물 센터까지 맡기면 대개 시계가 웁니다.
4. "누가 하나?"보다 "무엇이 아직 안 끝났나?"를 기준으로 삼는다
복잡한 담당 배정에서는 기록의 중심이 "담당자"가 되기 쉽습니다.
하지만 글 올리기에서 정말 알고 싶은 것은 이것입니다.
이 글은 올라갔나, 안 올라갔나.
거의 이것뿐입니다.
그래서 기준은 대기 목록이면 충분합니다.
새 원고가 들어오면 대기 목록에 넣는다. 올리기 시작할 때 짧은 잠금을 건다. 성공하면 "올림"으로 바꾼다. 실패하면 대기 목록으로 되돌린다. 정해 둔 횟수만큼 실패하면 실패 보관함으로 옮기고 경고한다.
"Workers 담당", "Actions 담당"은 참고 정보일 뿐, 영원한 거부권이 되어서는 안 됩니다.
"Workers 담당"이라고 적혀 있어도 일정 시간 진전이 없으면 다른 실행자가 가져갈 수 있습니다.
그러면 죽은 담당자 표시가 일을 인질로 잡지 못합니다.
5. 본선은 사건이 생기면 바로 시작, schedule은 놓친 것을 줍는 용도
바로 올리고 싶은데 다음 정해진 시각까지 기다리는 것은 본말이 뒤바뀐 일입니다.
새 원고가 도착한 순간 실행자를 깨웁니다.
이것을 본선으로 삼습니다.
그리고 schedule은 몇 분에서 몇십 분이 지나도 남아 있는 오래된 대기 건을 주워 줍니다.
즉,
사건이 일어나면 바로 시작하는 것이 본선, schedule은 보험입니다.
이렇게 하면 평소에는 빠릅니다.
알림을 놓쳐도 schedule이 나중에 주워 줍니다.
반대로 schedule을 본선으로 삼으면 늦어지는 순간 올리는 일 자체가 늦어집니다. 거기에 담당 판정까지 붙으면 "일어났는데 아무것도 안 한다"가 벌어집니다.
시간이 되어 깨어났고,
검사도 통과했는데,
"오늘은 제 담당이 아닙니다" 하고 퇴근합니다.
대체 뭐 하러 온 겁니까.
6. 대체 수단은 "담당 변경"이 아니라 "오래된 미완료 작업 회수"로 만든다
대체 수단을 설계할 때 흔한 함정은 A에서 B로 공식적으로 담당을 넘기는 것입니다.
담당을 넘기려면 기록을 고쳐야 합니다.
그 고침이 실패하면 이번엔 넘기는 일 자체가 고장입니다.
더 단순해도 됩니다.
평소에는 Workers가 처리합니다. 성공하면 끝입니다. 실패하면 대기 목록에 남습니다.
Actions 같은 대체 실행자는 정기적으로 확인하고,
- 대기 건이 있고
- 정해진 시간보다 오래됐고
- 살아 있는 잠금이 없고
- 자신이 실행할 수 있으면
그 일을 가져갑니다.
"Workers에게서 넘겨받았다는 증명서"는 필요 없습니다.
봐야 할 것은 일이 끝나지 않았다는 사실입니다.
이 방식에서는 담당표가 깔끔한지보다 결과물이 있는지가 더 중요합니다.
글 게시 시스템이라면 그쪽이 자연스럽습니다.
7. Live Schedule을 버릴 필요는 없다. 다만 생명유지장치로 만들지는 말자
Live Schedule 자체가 나쁜 것은 아닙니다.
사람이 보고 있을 때 우선순위를 바꿉니다. 이상한 점을 조사합니다. 개선안을 만듭니다. 수리를 빠르게 합니다. 새 기획을 넣습니다.
이런 용도로는 매우 강합니다.
문제는 그것이 멈추는 순간 글 만드는 공장까지 멈추는 것입니다.
Live는 속도를 높이는 장치(accelerator)일 뿐, 하나만 고장 나도 전체가 멈추는 약한 고리(single point of failure)가 되면 안 됩니다.
Live가 없어도
새 원고 → 대기 목록 → 실행자 → 검사 → 게시
가 스스로 돌아야 합니다.
Live가 오면 빨라지고,
없어도 죽지 않습니다.
사람의 심장을 "담당자의 Slack 답장"에 연결하면 안 되는 것과 같습니다.
8. 완료 기준을 "똑똑한 시스템 완성"에서 "한 개 넣으면 한 개 나온다"로 되돌린다
이런 시스템은 마음먹으면 끝없이 고칠 수 있습니다.
공평한 담당 배정. 무료 사용량 최적화. 중복 방지. 예산 예측. 우선순위. 감시. 자동 복구. 분석.
다 재미있습니다.
그리고 다 하다 보면 글이 안 나갑니다.
그래서 처음의 완료 기준은 더 거칠어도 됩니다.
새 원고를 한 개 넣으면, 특별한 조작 없이 한 개가 올라간다.
실패해도 대기 목록에 남습니다. 다른 실행자가 가져갑니다. 여러 번 실패한 것만 사람에게 올립니다.
이 단순한 한 줄이 안정되면 그다음에 최적화를 되살리면 됩니다.
"새 글을 빨리 올리고 싶었을 뿐"인데 어느새 분산 합의 비슷한 것을 직접 만들고 있었습니다.
그런 날도 있습니다.
하지만 끝내는 방법은 있습니다.
복잡함을 잘라 냅니다.
게시 시스템의 가치는 담당자 선거가 얼마나 아름다운가가 아닙니다.
글이 세상에 나가는 것입니다.
결론
Live Schedule 지옥의 본질은 schedule 자체가 아닙니다.
"일이 끝나지 않았다는 상태"보다 "담당자의 상태"를 너무 강하게 만든 것입니다.
설계를 되돌린다면 이 정도면 됩니다.
- 대기 목록을 아직 끝나지 않은 일의 기준 기록으로 삼는다
- 사건이 생기면 바로 시작한다
- 잠금은 짧게 둔다
- 대체 실행자는 오래된 대기 건을 가져간다
- schedule은 보험으로 쓴다
- 정해 둔 횟수만큼 실패한 건은 실패 보관함으로 따로 둔다
- Live는 있으면 좋은 가속 장치로 둔다
멋진 공장보다 멈추지 않는 공장이 낫습니다.
먼저 거기서부터 시작합니다.
자료
- GitHub Docs: Events that trigger workflows — schedule https://docs.github.com/actions/reference/workflows-and-actions/events-that-trigger-workflows
- Cloudflare Workers Docs: Cron Triggers https://developers.cloudflare.com/workers/configuration/cron-triggers/
- Cloudflare Queues Docs: Overview https://developers.cloudflare.com/queues/
- Cloudflare Queues Docs: Dead Letter Queues https://developers.cloudflare.com/queues/configuration/dead-letter-queues/
