← 문제로

12. 확장성의 한계: 암달의 법칙·구스타프슨 법칙과 경합

난이도 중
내 답안
모범답안

해설 — 확장성의 한계: 암달·구스타프슨과 경합

난이도: 중상

1. 암달의 법칙

직렬 비율 s, 병렬 비율 1 - s, 코어 N 개일 때 속도향상:

Speedup(N) = 1 / ( s + (1 - s)/N )

병렬 부분은 N 으로 나뉘지만 직렬 부분 s 는 그대로 남는다. N → ∞(1-s)/N → 0 이므로:

Speedup_max = 1 / s
  • s = 5% (0.05) → 상한 1 / 0.05 = 20배. 코어가 1000개여도 20배를 못 넘는다. 참고로 N=20 에서 이미 1/(0.05+0.95/20)=10.3배, N=100 에서 1/(0.05+0.0095)=16.8배 — 수확 체감이 가파르다.
  • 핵심 통찰: 직렬 비율이 천장을 정한다. 병렬화 노력보다 직렬 구간을 줄이는 게 결정적이다.

2. 구스타프슨의 법칙

암달은 문제 크기를 고정하고 "같은 일을 더 빨리"를 본다. 구스타프슨은 시간을 고정하고 "코어가 늘면 같은 시간에 더 큰 일을"을 본다(문제 크기를 코어에 비례해 키움). 스케일드 스피드업:

Speedup(N) = N - s·(N - 1)      (s = 직렬 비율)

이 식은 N 에 대해 (직렬 비율 페널티를 뺀) 거의 선형으로 증가한다. 현실적 의미: 대규모 서버에서 코어/장비를 늘리는 목적은 보통 "같은 응답시간으로 더 많은 동접/엔티티/존을 처리"다 — 즉 문제 크기가 자원과 함께 커진다. 이때는 암달의 비관적 천장보다 구스타프슨의 관점이 운영 현실에 가깝다. 단, 직렬 병목(공유 자원) 이 크면 문제를 키워도 그 부분이 경합으로 막혀 구스타프슨의 낙관도 깨진다. 둘은 모순이 아니라 "고정된 일 vs 늘어나는 일" 두 시나리오를 보는 것.

3. 직렬 구간의 정체 (s 를 키우는 것들)

게임서버 틱 루프에서 흔한 직렬 병목:

  • 전역 락 하나로 보호되는 월드 상태: 모든 워커가 그 락을 직렬로 통과 → 사실상 단일 코어. s 가 급등.
  • 단일 작업 큐/단일 송신 스레드: 모든 코어가 한 큐에 push/pop 경쟁(큐 락·캐시 핑퐁). 입출력 경로의 직렬화.
  • 순차 커밋/순서 보장 지점: 틱 끝의 일괄 저장, 순서 있는 브로드캐스트 등 "모두 모여 한 줄로" 처리되는 배리어.
  • false sharing: 논리적으로 독립인 per-thread 카운터가 같은 캐시라인에 있어 코어 간 무효화 트래픽 → 동기화 안 했는데도 직렬처럼 느려짐. 이들은 코드상 "병렬"이어도 실행이 한 자원에서 줄 서므로 s 에 합산된다.

4. 경합과 음의 확장성

코어를 늘렸는데 처리량이 떨어지는 경우:

  • 락 경합: 대기·깨우기·스핀 비용이 코어 수에 따라 초선형으로 증가. 임계영역이 짧아도 진입 빈도가 높으면 폭증.
  • 캐시 코히어런시 트래픽: 공유 쓰기 라인의 MESI 무효화·전송이 코어 수에 비례해 늘어 메모리 대역을 잡아먹음.
  • 컨텍스트 스위칭/오버서브스크립션: 스레드 > 코어면 스위칭·캐시 오염 비용.
  • NUMA 원격 접근: 다른 소켓의 메모리 접근 지연으로 추가 코어가 손해.

측정/진단: 코어 수 1,2,4,8,... 로 처리량/지연을 찍어 확장성 곡선을 그린다. 선형선과 갈라지는 지점이 병목 시작점. perf/lock contention 프로파일러로 어느 락·라인에서 시간이 새는지, perf c2c 로 false sharing/원격 캐시 접근을 본다. "스레드를 늘려도 CPU 사용률은 오르는데 처리량은 정체"면 경합 신호.

5. 설계 함의 — 직렬 비율 줄이기

  • 샤딩/파티셔닝: 월드를 존/그리드/엔티티 버킷으로 쪼개 락을 분할(각 샤드 독립 처리). 전역 락 → 다수의 무경합 락.
  • 락프리/불변 스냅샷: 읽기 핫 데이터는 불변 스냅샷 + 원자 포인터 교체로 reader 경합 제거(읽기-쓰기 분리).
  • per-core 자료구조: 스레드 로컬 큐/카운터로 합산은 나중에(작업 훔치기, 샤딩된 카운터). false sharing 은 alignas(64) 패딩으로 분리.
  • 배치·입도 조절: 너무 잘게 나누면 동기화 오버헤드가, 너무 굵으면 부하 불균형이 커진다 — 작업 입도를 튜닝.
  • 사고 과정: ① 직렬 병목(공유 자원)을 찾는다 → ② 그 자원을 분할/복제/불변화해 경합을 없앤다 → ③ 남은 동기화 지점을 배치·비동기로 줄인다 → ④ 확장성 곡선으로 재측정. "코어를 더 주면 빨라지나?"가 아니라 "무엇이 직렬인가?"를 먼저 묻는다.

면접 포인트

  1. 암달: Speedup = 1/(s + (1-s)/N), 상한 1/s. s=5% → 최대 20배. 직렬 비율이 천장을 결정한다.
  2. 구스타프슨은 문제 크기를 자원과 함께 키우는 관점 — 대규모 서버의 "동접 확장"에 더 부합하지만, 직렬 병목이 크면 그 낙관도 깨진다.
  3. 확장의 적은 공유 자원의 경합(락·캐시 코히어런시·NUMA)이다. 샤딩·락프리·per-core 화s 를 줄이는 게 코어를 더 꽂는 것보다 중요하다.