본문 바로가기
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 한테 넘겨. 그 worker 도 같은 막힌 마운트에 손을 뻗어서 똑같이 hang 되고. 응답 없는 네트워크 경로 하나가 queue 전체를 인질로 잡은 거야. hang 된 프로세스 안에 아무리 영리한 재시도 로직을 넣어도 소용없어. 프로세스 자체가 얼어 있으니까.

중단 못 하면, 죽일 수 있는 데 격리해

답은 구조에 있고, 그게 이 레슨의 교훈이야. 위험한 작업은 별도의 자식 프로세스에서 돌려. 부모는 오디오 proxy 준비, 그러니까 네트워크 파일시스템을 건드리는 부분을 맡을 자식을 spawn 하고 지켜보기만 해. 부모는 계속 응답할 수 있는 상태로 남아. lease 도 계속 갱신하고. 자기가 통제하는 wall-clock timeout 을 강제하되 넉넉하게 잡아. 최소 30분쯤에서 시작해서 source 크기에 따라 늘리고 몇 시간에서 끊는 식으로. 느리긴 해도 제대로 되고 있는 전송이 억울하게 죽지 않게. 그리고 자식이 그 예산을 넘기면 부모가 죽여. 안에 있는 스레드는 중단 못 해도 프로세스는 언제든 죽일 수 있거든. syscall 에서 못 얻은 timeout 을 프로세스 경계가 대신 주는 거야.

이게 깔끔하게 맞아떨어지는 건 위치 덕분이야. 전부 유료 경계 앞쪽이거든. 여기서 난 timeout 은 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

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

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