상황

사내 서비스를 기존 Elastic Beanstalk(이하 EB)에서 Ncloud Kubernetes Service(이하 NKS)로 옮기는 작업을 하던 중, 운영 환경에 배포한 Node.js API Pod 중 하나가 계속 CrashLoopBackOff 상태로 재시작되고 있었다.

파드를 확인해보니 종료 사유는 OOMKilled, exit code는 137이었다. 컨테이너 리소스는 cpu: 200m, memory: 512Mi로 겉보기에는 충분해 보였다.

결론부터 말하자면, 서버 환경변수에는 Node.js 클러스터링 모드를 제어하는 옵션이 있었고, 이 값을 껐더니 Pod가 정상으로 떴다.

일단 문제는 해결됐지만, 아래와 같은 두가지 질문이 생겼다.

  1. 클러스터링 모드를 끄니까 해결이 된 이유는 뭘까?
  2. 클러스터링 모드가 켜져 있을 때 OOM이 발생한 이유는 무엇일까?

위 질문들에 대한 답을 위해서는 Linux, Docker, Nodejs 의 지식이 필요했고, 그 지식을 이 글에서 공유하고자 한다.


1. worker 프로세스가 4개였던 이유

당시 로그는 다음과 같았다.

Master server started on 7
Cluster server started on 27
Cluster server started on 15
Cluster server started on 21
Cluster server started on 28

Master server started가 한 번, Cluster server started가 네 번. primary 프로세스 1개에 worker 프로세스 4개가 생성된 상태였다.

여기서 참고로 짚고 넘어갈 게 하나 있다. Node.js cluster는 멀티스레드가 아니다. 스레드 풀을 만드는 게 아니라 별개의 프로세스를 fork하는 구조다. 공식 문서를 보면, cluster의 worker는 child_process.fork()로 생성된다고 나와 있다. 즉 worker 하나하나가 독립된 프로세스다.

이게 중요한 이유는 프로세스와 스레드는 필요로 하는 리소스 양이 다르기 때문이다.

구분스레드 (thread)프로세스 (process)
메모리 공간부모와 heap을 공유각자 독립된 메모리 공간
생성 비용가볍다무겁다
Node.js에서worker_threadscluster (child_process.fork)
앱 초기화공유 가능프로세스마다 다시

스레드는 같은 프로세스 안에서 메모리를 나눠 쓰기 때문에 가볍다. 반면 프로세스는 각자 완전히 독립된 메모리 공간을 갖는다. 그래서 cluster로 worker를 4개 띄운다는 건, 곧 애플리케이션을 4번 새로 부팅하는 것과 다를 바가 없다.

Primary 프로세스
├─ worker 프로세스 1
├─ worker 프로세스 2
├─ worker 프로세스 3
└─ worker 프로세스 4

worker가 늘어나면, 프로세스마다 갖는 자원이 worker 수만큼 필요하다.

예를 들면 아래와 같은 자원이 각 프로세스마다 필요할 수 있다.

  • V8 heap
  • NestJS module graph
  • dependency injection container
  • DB connection pool
  • Redis connection
  • secret/config 로딩 결과
  • logger buffer
  • native module memory
  • OpenTelemetry 또는 SDK 초기화 비용

정리하면, worker 4개는 “하나의 앱을 4개 스레드로 나눠 실행하는 것”이 아니라, 운영 관점에서는 “같은 컨테이너 안에 NestJS 앱 프로세스 4개를 각각 띄운 것”에 가깝다. worker 수만큼 앱 초기화 비용이 중복되고, primary 프로세스의 오버헤드까지 더해지므로 단일 프로세스 실행보다 메모리 사용량이 크게 늘 수 있다.

그럼 일단 클러스터링이 프로세스 4개를 띄운다는 건 이해했는데, 왜 하필 4개였을까? 2개도 3개도 아니고 왜 4개일까?

4라는 숫자가 코드 어딘가에서 사용되고 있다는 뜻이고, 로그가 찍혔다는 건 서버 코드에 해당 로그를 남기는 부분이 있다는 것으로 해석할 수 있다.

코드로 가보자.


1-1. os.cpus().length의 함정

코드를 보니 cluster 로직은 다음과 같았다.

const clusterize = (callback: Function): void => {
  const numCPUs = os.cpus().length;
  if (config.clustering === true && cluster.isPrimary) {
    console.log(`Master server started on ${process.pid}`);
    for (let i = 0; i < numCPUs; i++) {
      cluster.fork();
    }
    cluster.on('exit', (worker, code, signal) => {
      console.log(`Worker ${worker.process.pid} died. Restarting`);
      cluster.fork();
    });
  } else {
    console.log(`Cluster server started on ${process.pid}`);
    callback();
  }
};
 
clusterize(bootstrap);

한 줄로 요약하면, config.clustering === true일 때 os.cpus().length만큼 worker를 fork한다.

로그에 Cluster server started...가 4번 찍혔으니 for (let i = 0; i < numCPUs; i++)를 4번 돌았다는 뜻이고, 그러려면 numCPUs는 4여야 한다. 즉 os.cpus().length가 4를 반환했다는 것이다.

그런데 좀 이상하다. 내가 Deployment에서 정의한 CPU limit이 200m인데, os.cpus().length도 그에 맞게 나와야 하는 거 아닌가?

이게 바로 이번 이슈의 근본 원인이었다.

os는 Node.js의 기본 내장 모듈이고, os.cpus()는 운영체제가 노출하는 CPU 정보 배열을 반환한다. 이번 환경에서 os.cpus().length는 Pod의 CPU limit을 읽지 않고, OS 레벨에서 보이는 logical CPU 개수를 셌다.

이번 Pod가 올라간 Node VM은 4 vCPU였다.

Node: app-prod-node-1
Capacity:
  cpu:    4
  memory: 16377148Ki

Node VM이 4 vCPU라면 CPU topology는 이렇게 보인다.

logical CPU 0
logical CPU 1
logical CPU 2
logical CPU 3

즉 이번 환경에서는 컨테이너 안 Node.js 프로세스가 OS 레벨에서 CPU 4개를 “볼 수 있는” 상태였다. 그래서 os.cpus().length는 4를 반환했고, worker도 4개가 만들어졌다.

그럼 내가 설정한 CPU limit과 실제 노드의 CPU 개수는 별개라는 말인데, 왜 이런 일이 생길까?

이 이유를 이해하기 위해 Kubernetes와 컨테이너 기술을 살펴보자.


1-2. 컨테이너는 작은 VM이 아니다

컨테이너를 흔히 작은 VM처럼 생각하곤 한다. CPU limit을 200m으로 걸었으니 컨테이너 안에서도 CPU가 0.2개처럼 보여야 하는 것 아닌가 싶은 생각도 컨테이너를 잘 이해하지 못해 발생한 일이다.

하지만 컨테이너의 등장 배경을 생각해보면, 컨테이너는 VM처럼 별도의 OS 커널을 띄우는 방식이 아니다. 만약 컨테이너마다 OS 커널을 따로 띄웠다면 도커는 지금보다 훨씬 무거웠을 것이다.

컨테이너는 기본적으로 호스트의 Linux kernel을 그대로 공유하고, 그 위에서 namespace와 Control Groups(이하 cgroup)으로 프로세스를 논리적으로 격리한다.

Host VM
├─ Linux kernel
├─ container namespace
│  ├─ process id 격리
│  ├─ network 격리
│  └─ filesystem view 격리
└─ container cgroup
   ├─ CPU 사용 시간 제한
   └─ memory 사용량 제한

위 구조를 간단한 그림으로 살펴보자.

flowchart TB
    subgraph Host["Host VM / Kubernetes Node"]
        CPU["CPU topology<br/>logical CPU 0,1,2,3"]
        Kernel["Linux kernel"]

        subgraph Container["Container"]
            App["Node.js process"]
            NS["namespace<br/>무엇이 보이는가 격리"]
            CG["cgroup<br/>얼마나 쓸 수 있는가 제한"]
        end
    end

    App -->|"os.cpus() 조회"| CPU
    Kernel --> NS
    Kernel --> CG
    NS -->|"PID / network / filesystem view 격리"| App
    CG -->|"cpu quota = 200m"| Throttle["CPU 사용 시간 제한"]
    CG -->|"memory limit = 512Mi"| OOM["초과 시 OOMKilled"]

    CPU -.->|"CPU 목록은 4개처럼 보일 수 있음"| App
    Throttle -.->|"CPU 개수를 줄이는 것이 아님"| App

컨테이너를 이해하는 핵심 키워드는 namespacecgroup이다.

namespace는 컨테이너의 시야를 격리한다. 컨테이너 안에서는 자기 프로세스만 보이고, 자기 파일시스템처럼 보이고, 자기 네트워크 인터페이스처럼 보인다. 즉 프로세스가 바라보는 세계를 제한하는 장치다.

반면 cgroup은 자원을 “얼마나 쓸 수 있는가”를 제한한다. CPU를 얼마나 오래 쓸 수 있는지, 메모리를 얼마나 쓸 수 있는지 같은 자원 사용량을 제한한다. CPU나 메모리 같은 자원은 한정적이기 때문에, 각 프로세스 그룹에 사용 가능한 한도를 정해두는 방식이다.

여기서 앞서 설명했던 Kubernetes의 CPU limit은 cgroup을 통해 “CPU를 사용할 수 있는 시간”을 제한하는 설정이다(뒤에서 추가로 설명하겠다). 하지만 logical CPU 개수는 기본적으로 Pod의 CPU limit만큼 줄어들어 보이는 값이 아니다. cpuset 같은 추가 설정을 쓰면 달라질 수 있지만, 이번 환경에서는 Node.js의 os.cpus()가 노드의 logical CPU 목록을 그대로 볼 수 있었다.

그래서 이번 상황을 아래와 같이 정리할 수 있다.

Node CPU topology: 4
os.cpus().length: 4
cluster worker: 4개
Container CPU limit: 200m
Container memory limit: 512Mi

컨테이너 안에서 CPU 4개가 보였기 때문에 worker 4개가 생성됐다. 하지만 실제로 쓸 수 있는 CPU 시간은 200m으로 제한되어 있었고, 메모리는 worker 전체가 512Mi를 같이 쓰는 구조였다.

여기서 두 가지 의문이 생긴다.

첫째, 앞서 얘기한 CPU limit이 시간을 제한한다는 게 정확히 무슨 뜻일까? 둘째, 그럼 OOMKilled는 왜 발생했을까?

이걸 이해하려면 CFS Quota가 무엇인지, CPU와 메모리가 각각 어떻게 다르게 제한되는지를 알아야 한다.


2. CPU limit은 CPU 사용 시간을 제한한다

그럼 대체 CPU limit은 어떤 설정일까?

Kubernetes에서 CPU limit을 이렇게 설정했다고 하자.

resources:
  limits:
    cpu: 200m

200m은 CPU 1개의 0.2배 분량이다.

CPU 1개 전체 = 1000m
200m = CPU 0.2개 분량

여기서 중요한 점은 CPU limit이 “CPU 개수를 물리적으로 잘라서 주는 설정”이 아니라는 것이다. Linux scheduler는 프로세스에 CPU 실행 시간을 배분하고, cgroup은 그 시간 예산을 제한한다.

그래서 “CPU 0.2개를 준다”는 말은 CPU의 20% 영역만 내가 쓰고 나머지 80%를 다른 프로세스가 동시에 쓴다는 뜻이 아니다. 기본 100ms period 기준으로 보면, cgroup에 속한 프로세스들이 총 최대 20ms 만큼의 CPU 실행 시간을 사용할 수 있다는 뜻이다.

이것이 Linux cgroup의 CFS Quota로 반영된다. CFS는 Completely Fair Scheduler, Linux의 기본 CPU 스케줄러다. CFS Quota는 특정 프로세스 그룹이 일정 시간 동안 CPU를 얼마나 쓸 수 있는지 제한하는 기능이다.

cgroup v2에서는 이런 식으로 표현된다.

cpu.max = 20000 100000
 
=> 100ms 중 총 20ms만 CPU 사용 가능

핵심은, CFS Quota는 CPU 개수를 줄이는 게 아니다. CPU가 4개처럼 보여도, cgroup 전체가 실제로 쓸 수 있는 CPU 시간 예산이 200m으로 제한될 뿐이다.

100ms 주기
├─ 20ms: 총 실행 가능
└─ 80ms: quota를 다 쓰면 대기 또는 throttling

정리하면 다음과 같다.

CPU Topology
= CPU가 몇 개처럼 보이는가
 
CFS Quota
= 그 CPU들을 시간 기준으로 얼마나 쓸 수 있는가

CPU limit을 초과하면 느려진다. 흔히 아는 CPU Throttling이다. 쉽게 말하면 할 일은 산더미인데 그 일을 할 시간이 충분히 주어지지 않는 것이다. 그러면 해당 프로세스가 느려지긴 하지만, 그렇다고 죽지는 않는다.

그러면 이번 Pod는 왜 죽은 걸까. 답은 메모리에 있었다.


3. 메모리는 프로세스마다 나눠주지 않는다

컨테이너는 내부에서 프로세스를 여러 개 만들 수 있다. 하지만 cgroup limit은 컨테이너 단위로 적용된다. 즉 프로세스가 몇 개든 상관없이 같은 container cgroup 아래에 들어가고, 그 안의 모든 프로세스가 합쳐서 최대 limit만큼만 쓸 수 있다.

Container limit: cpu 200m, memory 512Mi
├─ primary process
├─ worker process 1
├─ worker process 2
├─ worker process 3
└─ worker process 4
 
=> 다 합쳐서 cpu 200m / memory 512Mi 공유

worker 4개가 각자 512Mi를 받는 게 아니다. primary를 포함한 프로세스 5개가 512Mi를 같이 쓰게 된다.

이때 각 worker 프로세스는 NestJS 앱을 통째로 다시 부팅하게 된다. 프로세스 사이에 공유되는 메모리는 제한적이기 때문에, worker 수가 늘수록 전체 메모리 사용량도 크게 늘어난다. 이 총합이 512Mi를 넘어서는 순간 문제가 발생한다.

여기서 CPU와 메모리의 결정적인 차이가 나온다.

항목초과 시 결과의미
CPU limitthrottling실행 시간이 제한되어 느려질 뿐, 죽지는 않음
메모리 limitOOMKilledhard limit 초과 시 커널이 프로세스를 강제 종료

반면 메모리는 hard limit이라, 넘는 순간 커널의 OOM killer가 프로세스를 종료한다. Kubernetes는 이를 OOMKilled로 기록한다.

이번 이슈의 인과관계를 정리하면 이렇다.

flowchart TB
    A["컨테이너 resource limit<br/>cpu 200m / memory 512Mi"]
    B["Pod가 올라간 Node<br/>4 vCPU"]
    C["os.cpus().length = 4"]
    D["cluster worker 4개 생성"]
    E["primary + worker 4개가<br/>하나의 container cgroup 공유"]
    F["NestJS 앱과 connection pool 등<br/>worker 수만큼 중복 초기화"]
    G["memory 512Mi 초과"]
    H["OOMKilled / exitCode 137"]
    I["CrashLoopBackOff 반복"]

    B --> C
    C --> D
    A --> E
    D --> E
    E --> F
    F --> G
    G --> H
    H --> I

이제 clustering: false로 두는 것이 왜 OOMKilled를 해결했는지도 명확하게 설명할 수 있다. Pod 안에서 primary + worker 4개 구조를 만들지 않으니, 하나의 NestJS 프로세스만 512Mi를 쓰게 되기 때문이다.


4. EB에서는 왜 됐을까?

여기서 한 가지 의문이 더 생겼다. 기존 EB 환경에서도 같은 코드에 같은 클러스터링 설정이 켜져 있었는데, 왜 거기서는 문제가 없었을까?

EC2 한 대에서 Node.js 앱을 직접 실행하는 EB 환경을 생각해보자.

EC2 instance: 4 vCPU / 8GiB
├─ primary
├─ worker 1
├─ worker 2
├─ worker 3
└─ worker 4

당시 EB 구성에서는 앱 프로세스에 Kubernetes Pod처럼 cpu: 200m, memory: 512Mi 같은 작은 컨테이너 limit이 걸려 있지 않았다. worker 4개와 primary 1개가 동시에 떠도 전체 메모리 예산이 8GiB였기 때문에, 512Mi짜리 Pod 안에서 5개 프로세스가 경쟁하는 상황보다 훨씬 여유가 있었다. 그래서 os.cpus().length가 4를 반환하고 worker를 4개 띄워도 문제가 드러나지 않았다.

반면 Kubernetes는 하나의 노드 안에 여러 파드를 띄우는 것을 지원한다. 이때 여러 파드가 노드의 자원을 잘 나눠서 써야 하기 때문에, 노드가 가진 자원을 Pod 단위로 잘라 배분하는 모델을 갖고 있다.

Deployment replicas: 4
 
Pod 1: Node.js 프로세스 1개, 메모리 512Mi
Pod 2: Node.js 프로세스 1개, 메모리 512Mi
Pod 3: Node.js 프로세스 1개, 메모리 512Mi
Pod 4: Node.js 프로세스 1개, 메모리 512Mi

이렇게 하면 Pod마다 메모리 limit이 독립적으로 적용된다.

반대로 한 Pod 안에 worker 4개를 넣으면 Kubernetes 입장에서는 여전히 Pod 1개다.

Pod 1: memory 512Mi
├─ primary
├─ worker 1
├─ worker 2
├─ worker 3
└─ worker 4

worker 4개가 내부에서 어떻게 자원을 나눠 쓰는지, Kubernetes는 알지 못한다. 장애 격리도 흐려진다. worker 하나에서 메모리 누수가 생기면, 같은 컨테이너 안의 다른 worker까지 같이 영향을 받는다.

항목큰 Pod 1개 + cluster작은 Pod N개
CPU 활용가능가능
메모리 격리약함. 한 container limit 공유좋음. Pod별 limit 분리
장애 격리worker 문제로 Pod 전체 영향 가능Pod 하나만 영향
rolling update큰 단위로 교체점진적 교체 쉬움
HPAPod 단위라 내부 worker를 모름replica 단위 확장 쉬움
DB connection 수Pod 내부 worker 수 × replica 수로 증가해 추적이 어려움replica 수 기준으로 증가해 예측이 쉬움
운영 단순성낮음높음

Kubernetes에서는 아래 구조가 더 예측 가능하다.

Pod 1개 = Node.js process 1개
scale-out = Pod replica 증가

정리하면, EB에서 문제가 없던 이유는 clustering이 항상 안전해서가 아니라, 당시 EB 환경의 자원 경계가 그 방식에 더 잘 맞았기 때문이다. 같은 코드가 Kubernetes로 오면서 자원 경계가 Pod limit으로 바뀌었고, 클러스터링을 사용하던 전제가 깨진 것이다.


5. 클러스터링은 왜 하는 걸까?

그럼 EB에서 클러스터링을 쓸 수 있다는 건 이해를 했는데, 애초에 왜 이 클러스터링이라는 걸 쓰는 걸까?

이유는 Node.js가 싱글스레드이기 때문이다.

정확히는, Node.js에서 우리가 작성하는 JavaScript 코드는 하나의 스레드(이벤트 루프) 위에서 실행된다. 그래서 서버에 CPU 코어가 4개 있어도, Node.js 프로세스 하나의 JavaScript 실행은 한 시점에 주로 하나의 코어에서 처리된다. 나머지 코어는 상대적으로 활용되지 못할 수 있다.

4 vCPU 서버 + Node.js 프로세스 1개
 
CPU 0: ████ 이벤트 루프 실행 (바쁨)
CPU 1: ░░░░ 놀고 있음
CPU 2: ░░░░ 놀고 있음
CPU 3: ░░░░ 놀고 있음

이 낭비를 줄이기 위해 나온 게 cluster다. CPU 코어 수만큼 프로세스를 띄우면, 여러 프로세스가 여러 코어에 분산되어 병렬로 요청을 처리할 수 있다.

4 vCPU 서버 + cluster worker 4개
 
CPU 0: ████ worker 1
CPU 1: ████ worker 2
CPU 2: ████ worker 3
CPU 3: ████ worker 4

os.cpus().length만큼 worker를 fork하던 코드도 바로 이 발상에서 나온 것이다. “코어 수만큼 프로세스를 띄워 코어를 놀리지 말자”는 의도다.

문제는 이 발상이 “서버 한 대의 CPU 개수 = 내가 쓸 수 있는 CPU 개수”라는 전제 위에 서 있다는 점이다. EB의 EC2 환경에서는 이 전제가 대체로 맞았지만, 컨테이너와 Kubernetes 환경에서는 성립하지 않을 수 있다. 앞에서 본 것처럼 컨테이너가 보는 CPU 개수와 실제로 쓸 수 있는 CPU 시간 예산이 다를 수 있기 때문이다.


6. 그럼 진짜 문제는 뭘까?

그렇다면 Kubernetes에서 Node.js의 cluster mode는 무조건 문제를 일으킬까?

단순하게 그렇게 볼 수도 있지만, 꼭 그런 건 아니다.

정확히는 cluster mode 자체가 잘못된 게 아니라, os.cpus().length로 정한 worker 수와 Pod의 실제 resource limit이 안 맞는 게 문제였다.

이번 사례의 조합을 보자.

Node CPU topology: 4
os.cpus().length: 4
cluster worker: 4개
Container CPU limit: 200m
Container memory limit: 512Mi

만약 Node가 4 vCPU이고 Pod CPU limit도 4에 가깝다면 CPU 관점의 불일치는 줄어든다.

Node CPU topology: 4
os.cpus().length: 4
Container CPU limit: 4
cluster worker: 4개

하지만 CPU가 맞아도 메모리는 별개다. cluster worker는 스레드가 아니라 별도 프로세스이므로 메모리도 충분해야 한다.

즉 cluster mode를 Kubernetes에서 쓰려면 최소한 이 조건을 맞춰야 한다.

1. worker 수를 명시적으로 제한한다
2. CPU limit을 worker 수에 맞춘다
3. 메모리 limit이 primary + worker N개의 실제 사용량을 감당해야 한다
4. DB/Redis connection pool 총합이 backend 한도를 넘지 않아야 한다
5. worker 수 증가로 부팅 시간이 길어진다면 startupProbe timeout도 함께 조정해야 한다
6. HPA/Deployment replica 전략과 중복 확장되지 않도록 설계한다

특히 os.cpus().length에 worker 수를 그대로 맡기는 건 컨테이너 환경에서 위험할 수 있다. 더 안전한 방식은 worker 수를 환경 변수로 직접 지정하고, 그 값에 맞춰 resource를 함께 조정하는 것이다.

const workerCount = Number(process.env.WEB_CONCURRENCY || 1);
 
for (let i = 0; i < workerCount; i++) {
  cluster.fork();
}

이렇게 하려면 Kubernetes manifest에서는 worker 수와 resource를 함께 맞춰줘야 한다.

env:
  - name: WEB_CONCURRENCY
    value: "2"
resources:
  requests:
    cpu: "2"
    memory: 2Gi
  limits:
    cpu: "2"
    memory: 2Gi

혹은, 아예 request와 limit을 각 노드의 크기에 맞게 잡을 수도 있다.

resources:
  requests:
    cpu: "4"
    memory: 4Gi
  limits:
    cpu: "4"
    memory: 4Gi

이 경우 Pod는 거의 4 CPU짜리 큰 Pod가 된다. Node가 4 vCPU라면 사실상 Node 하나의 대부분을 이 Pod가 차지한다.

하지만, 조금 더 깊게 들어가면 실제 Node의 allocatablecapacity보다 작기 때문에(Allocatable CPU: 3920m), requests.cpu: "4"는 스케줄 자체가 안 된다.

종합하면, Kubernetes에서 클러스터링 모드를 어떻게든 쓸 수는 있다. 하지만 worker 수, CPU, 메모리, connection pool, probe, HPA 전략을 함께 맞춰야 하므로 운영 복잡도가 커진다. 그래서 cluster mode로 컨테이너 안에서 worker를 늘리는 것보다, Pod replica를 늘리는 게 훨씬 Kubernetes다운 접근 방식이다.


마치며

이번 문제를 정리해보면,

  1. 이번 환경에서 os.cpus().length는 Pod의 CPU limit이 아니라 노드에서 보이는 logical CPU 개수 4를 반환했다. 이 값을 그대로 worker 수로 쓰면서 Pod resource limit과 worker 수가 어긋났다.
  2. 컨테이너 안에서 만들어진 모든 프로세스는 하나의 container cgroup limit을 공유한다.

위 두 가지가 겹치면서, 4 vCPU Node 위에서 cpu: 200m, memory: 512Mi짜리 컨테이너가 worker 4개를 만들었고, 5개의 프로세스가 512Mi를 나눠 쓰다가 OOM으로 종료된 것이다.

적어도 이번처럼 cpu: 200m, memory: 512Mi인 Pod에서 os.cpus().length 기준으로 worker를 늘리는 구조라면, clustering: false가 Kubernetes의 자원 모델에 더 맞는 선택이었다.

이번 문제를 파고들며 알게 된 사실을 간단히 정리하고 마무리한다.

  • 컨테이너는 작은 VM이 아니다. namespace는 시야를, cgroup은 사용량을 격리한다. 이 둘은 다르게 동작하고, 그래서 컨테이너 안에서 호스트의 CPU 개수가 그대로 보일 수 있다.
  • Kubernetes의 CPU limit은 CPU 개수를 줄이는 게 아니라 CPU 사용 시간을 제한한다. CPU는 시간으로 분배되는 공통 자원이기 때문이다.
  • CPU와 메모리는 초과했을 때 결과가 다르다. CPU는 throttling으로 느려지고, 메모리는 OOMKilled로 죽는다.
  • Kubernetes에서 scale-out을 하려면 프로세스가 아니라 Pod를 늘리는 편이 더 자연스럽다. Pod가 이미 Kubernetes의 배포·격리·확장의 단위이기 때문이다.