"커널에 박힌 스레드는 timeout 못 걸어. 근데 프로세스는 죽일 수 있어."
중단할 수 없는 hang
Recall 의 executor 는 네트워크 마운트된 아카이브에서 영상을 읽어. 그리고 얌전한 로컬 디스크만 다뤄본 사람을 놀라게 하는 사실이 여기 있어: 네트워크 파일시스템의 파일 open 은 무한정 block 될 수 있어. 느리게가 아니라 — 무한정. 마운트가 응답을 멈추고, open() 을 부른 스레드는 네 Python 코드가 닿을 수 없는 상태로 커널 안에 주차돼. 널 구할 timeout 파라미터가 없어. 중단 못 해. KeyboardInterrupt 도 안 돼. 파일시스템이 답하기로 결정할 때까지 스레드는 그냥 사라진 거고, 그건 영영일 수도 있어.
이제 그게 worker 에 뭘 하는지 추적해. 메인 프로세스가 그 open 에서 hang 돼. heartbeat 를 멈춰. lease 가 만료돼. control plane 은, 올바르게, worker 가 죽었다고 판단하고 job 을 다른 worker 에 넘겨 — 그게 같은 막힌 마운트에 손 뻗어 똑같이 hang 돼. 응답 없는 네트워크 경로 하나가 방금 queue 전체를 인질로 잡았고, hang 된 프로세스 안의 어떤 영리한 재시도 로직도 못 도와, 프로세스 자체가 얼었으니까.
중단 못 하면, 죽일 수 있는 데 격리해
고침은 구조적이고, 그게 이 레슨의 교훈이야: 위험한 작업을 별도의 자식 프로세스에서 돌려. 부모가 오디오 proxy 준비 — 네트워크 파일시스템을 건드리는 부분 — 를 할 자식을 spawn 하고, 그냥 지켜봐. 부모는 완전히 응답 상태로 남아. lease 를 계속 갱신해. 자기가 통제하는 wall-clock timeout 을 강제해, 넉넉하게 잡아서(바닥 30분쯤, source 크기로 확장, 몇 시간 상한) 진짜 느리지만-되는 전송이 살해당하지 않게. 그리고 자식이 그 예산을 넘으면, 부모가 그걸 죽여. 프로세스는 안의 스레드를 중단 못 해도 항상 죽일 수 있어. 프로세스 경계가 syscall 에서 못 얻은 그 timeout 이야.
이게 깔끔하게 착지하는 건 그 위치 때문이야: 전적으로 유료 경계 전. 여기 timeout 은 pre-provider 실패로 기록돼 — 전사 credit 을 하나도 안 썼으니, 증명 가능하게 재시도 안전한 실패 범주(트랙 4 의 safe-only 재시도) 에 있어. 그다음 daemon 은 멈추는 대신 다음 durable job 으로 넘어가. 무한 hang 이 경계 지어지고, 공짜고, 복구 가능한 실패가 돼.
그리고 느린 게 준비된 작업을 막게 두지 마
같은 본능을 따르는 동반 규칙이 있어. executor 는 새 영상을 찾으려 아카이브를 주기적으로 스캔하기도 해 — 그리고 그 스캔이 같은 잠재적으로-느린 파일시스템을 건드려. 그래서 daemon 은 기존 durable job queue 를 먼저 비우고 그다음에야 인벤토리를 시도해. 저하된 네트워크 마운트는 발견을 느리게 만들 수 있지만, 이미 등록되고 돌 준비된 작업을 절대 인질로 잡으면 안 돼. 다른 옷을 입은 같은 원리야: 네가 지연을 통제 못 하는 작업이, 진행할 수 있었을 작업의 critical path 에 앉게 두지 마.