어떤 작업이 자원을 계속 얻지 못해 진행하지 못하는 상태(starvation). 데드락과 달리 시스템 전체는 잘 돌아가는데 특정 작업만 소외된다.
데드락과의 차이
| 데드락 | 기아 | |
|---|---|---|
| 전체 진행 | 완전 정지 | 다른 작업은 잘 됨 |
| 원인 | 순환 대기 | 스케줄링 불공정 |
| 해결 시 | 사이클을 끊어야 | 기다리면 풀릴 수도 있음 |
| 발견 | 명확 | 어렵다 — 평균 지표는 정상, p99만 나쁨 |
어떻게 생기나
① 우선순위 기반 스케줄링
- 높은 우선순위 작업이 계속 들어옴
- 낮은 우선순위 작업은 영원히 차례가 오지 않는다
② 불공정 락
new ReentrantLock(); // 기본 = 비공정
비공정 락은 막 도착한 스레드가 대기열을 건너뛰고 획득할 수 있다. 운 나쁜 스레드는 계속 밀린다.
왜 굳이 비공정이 기본인가 — 공정 락은 대기열 순서를 지키려고 매번 컨텍스트 스위칭을 하므로 처리량이 크게 떨어진다. 막 깨어난 스레드에게 바로 주면 스위칭 없이 진행되어 훨씬 빠르다. 처리량과 공정성의 트레이드오프다.
③ 읽기-쓰기 락 읽기가 끊임없이 들어오면 쓰기가 영원히 못 들어간다(writer starvation). 그래서 대부분의 구현이 쓰기 대기 중에는 새 읽기를 막는다.
해결 — 에이징(aging)
기다린 시간에 비례해 우선순위를 올린다.
- 대기 시간이 길어질수록 우선순위 +1
- 아무리 낮은 작업도 언젠가는 가장 높아진다 → 반드시 처리된다
리눅스 CFS(Completely Fair Scheduler)는 다른 방식으로 같은 목표를 이룬다 — 가상 실행 시간(vruntime)이 가장 적은 프로세스를 다음에 실행한다. 못 뛴 프로세스는 vruntime이 안 늘어 결국 차례가 온다.
실무에서 만나는 형태
① 커넥션 풀 고갈
- 느린 쿼리가 커넥션을 오래 쥠 → 빠른 요청들도 커넥션을 못 받아 대기
- 특정 API만 타임아웃 폭증
② 스레드 풀에서 한 종류의 작업이 독점
- 느린 외부 API 호출이 스레드 풀을 다 차지
- 빠른 내부 처리 요청까지 굶는다
- 대응: 작업 종류별로 풀을 분리 (bulkhead 패턴)
③ 메시지 큐의 특정 파티션 키가 편중되면 그 파티션의 컨슈머만 밀려 해당 사용자만 지연된다.
라이브락 — 헷갈리는 개념
기아와 비슷하지만 다르다. 둘 다 계속 움직이는데 진전이 없는 상태다.
복도에서 마주친 두 사람이 서로 비켜 주려다 계속 같은 방향으로 움직이는 상황
CAS 재시도가 서로를 계속 실패시키거나, 데드락 회피 로직이 "둘 다 놓고 재시도"를 동시에 반복할 때 생긴다. 무작위 지연(jitter) 을 넣어 대칭을 깨는 것이 해법이다.