C.W.K.
Stream
Lesson 03 of 05 · published

죽일 수 있는 자식

~12 min · killable-child, timeout, network-fs, process-isolation

Level 0Empty Shelf
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"커널에 박힌 스레드는 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 에 앉게 두지 마.

Code

부모는 지켜보고; 자식이 위험을 진다·python
import multiprocessing as mp

# 위험한 부분(네트워크-fs open 은 커널에서 영영 block 가능) 은
# 빌 수밖에 없는 스레드가 아니라 KILL 할 수 있는 자식에서 돈다.
child = mp.Process(target=prepare_audio_proxy, args=(video_path,))
child.start()

deadline = timeout_for(source_size)   # ~30분 바닥, 확장, 상한
while child.is_alive():
    control_plane.heartbeat(job.id, worker_id, token)  # 부모는 살아 있음
    if past(deadline):
        child.kill()                  # 프로세스는 항상 죽일 수 있다
        raise PreProviderTimeout      # credit 안 씀 -> 재시도 안전
    sleep_briefly()

# 핵심: 이 위험 전체가 유료 경계 전에 앉는다.
# 여기 timeout 은 공짜고, 경계 지어졌고, 안전하게 재시도 가능.

External links

Exercise

네 코드에서 지연을 통제 못 하는 걸 건드리는 작업을 찾아 — 네트워크 마운트, 외장 드라이브, 원격 파일시스템, C 라이브러리 호출. 물어: 그게 영영 block 되면, 실제로 뭐가 멈춰? 답이 스레드 timeout 이나 신호면, 커널에 박힌 호출에 진짜 먹힐지 검증해. 위험한 부분을 부모가 deadline 을 강제하는 자식 프로세스로 다시 설계하고, 그 덕에 뭐가 응답 상태로 남는지 적어봐.
Hint
테스트: 네 timeout 이 작업을 멈출 수 있어, 아니면 작업이 hang 된 채 *기다리기*만 멈춰? 그건 아주 달라 — 두 번째는 시도마다 얼어붙은 스레드를 흘려. 호출 자체를 중단 못 하면, 유일하게 믿을 deadline 은 죽일 수 있는 프로세스야. 위험한 단계가 되돌릴 수 없거나 유료인 경계 전에 앉는지도 확인해, timeout 이 아무 비용 안 들게.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.