3년간 Docker 운영 후 Podman 전환기: 메모리 40% 절감과 아키텍처 개선 실무 후기

|Platform Decision|18분 읽기

새벽 2시, 60초 만에 무너진 클러스터

60초. 그 짧은 시간 안에 노드 세 개가 다운됐습니다. 6개 클러스터에 걸쳐 42개의 마이크로서비스가 한꺼번에 멈췄고, 런타임들이 고장난 엘리베이터처럼 재시작을 반복하고 있었어요. 어둠 속에서 노트북을 켜고 프로세스 모니터를 열어보니, 믿기 어려운 화면이 펼쳐졌습니다.

dockerd 프로세스 하나가, 그것도 단순한 Go 서비스들만 돌리는 호스트에서 5.8기가바이트의 상주 메모리를 점유하고 있었거든요. 그런데 정작 끔찍했던 건 메모리 수치가 아니라 그 바로 위에 찍힌 로그였습니다.

"dockerd 메모리 부족 현상 감지됨"

전에도 여러 번 봤던 경고입니다. 그때마다 '괜찮겠지' 하고 스스로를 다독였던 메시지. 엔지니어는 차에서 나는 이상한 소리를 못 들은 척하는 운전자처럼, 종종 스스로에게 거짓말을 합니다. 저도 다르지 않았고요.

계속 외면했던 경고들

이 이야기에서 가장 부끄러운 부분을 고백하자면, 저는 2021년에 이미 Podman 글을 읽은 적이 있었다는 점입니다. 브라우저 탭을 열어놓고 첫 단락만 훑은 뒤 그대로 닫아버렸죠. 새 도구로 갈아타는 게 위험하게 느껴졌고, Docker가 손에 익었으니까요.

돌이켜보면 **인프라 엔지니어링에서 가장 비싼 항목은 '익숙함'**이었습니다. 이 비용은 어떤 청구서에도 적히지 않습니다. 그래서 무섭죠. 적히지 않을 뿐 사라지는 게 아니라, 어딘가에 쌓였다가 가장 곤란한 새벽에 한꺼번에 청구됩니다. 금융권에서 인터넷뱅킹을 운영할 때도 비슷했어요. 미뤄둔 결정은 사라지지 않고, 트래픽이 몰리는 날 이자까지 붙어서 돌아오더라고요.

사건 다음 날 아침, 공동 팀장인 프리야에게 어젯밤 스크린샷을 보냈습니다. 아무 설명도 달지 않고요. 프리야는 한참 화면을 보더니 이렇게 말하더군요.

"우리는 우리 점심을 갉아먹는 악마한테 돈까지 주면서 계속 먹이를 주고 있었네요."

틀린 말이 아니었습니다. 프리야는 거의 틀리는 법이 없는데, 그게 가끔 불편하긴 해도 이번엔 정확히 옳았어요. 그 한 마디가 마이그레이션을 시작하게 만든 실질적인 방아쇠였습니다.

Podman이 근본적으로 다른 이유

Docker와 Podman의 차이를 이해하려면 명령어가 아니라 아키텍처부터 봐야 합니다. Docker는 모든 것을 중앙 데몬을 거쳐 실행합니다. 하나의 큰 백그라운드 프로세스가 컨테이너 세계 전체를 관장하는 구조죠. 편리합니다. 동시에, 이 프로세스가 흔들리면 그 위에 올라간 모든 게 같이 흔들립니다. dockerd를 재시작하면? 컨테이너도 전부 같이 재시작되죠.

이건 단일 장애점이라는 익숙한 개념을 운영 레벨에서 그대로 재현한 구조입니다. 금융권에서 인터넷뱅킹을 운영할 때 가장 경계했던 게 바로 "모든 트래픽이 한 컴포넌트를 통과하는" 그림이었는데, dockerd가 딱 그 자리에 앉아 있었던 셈이에요. 아키텍처 다이어그램에서는 작은 네모 하나지만, 그 네모가 죽으면 위에 그려진 선이 전부 끊깁니다. 도면에선 잘 안 보이고, 새벽에야 보입니다.

Podman은 전제 자체가 다릅니다. 각 컨테이너는 그것을 실행한 주체의 자식 프로세스일 뿐입니다. 중간에 무거운 데몬이 끼어들지 않아요. 컨테이너 하나가 죽어도 호스트의 다른 프로세스는 아무 영향을 받지 않습니다.

이런 차이가 처음엔 사소해 보일 수 있습니다. 그런데 최악의 순간에 6기가바이트짜리 상주 메모리를 마주하고 나면, 사소하다는 생각은 사라집니다.

두 번째로 저를 놀라게 한 건 루트리스 모드였습니다. Podman은 처음부터 루트리스를 전제로 설계됐거든요. 루트 권한 없이 컨테이너를 돌린다는 건 저희에게 선택이 아니라 필수였는데, 이 한 가지 덕분에 보안 검토 기간이 2주에서 3일로 줄었습니다. 데몬이 루트로 떠 있느냐, 아니냐. 보안팀 입장에서 이 차이는 생각보다 큽니다. 권한 경계가 하나 줄어들면 검토할 위협 모델도 그만큼 줄어드니까요.

마이그레이션은 생각보다 단순했다

진입 장벽은 거의 없었습니다. Dockerfile 구문도 같고, OCI 이미지도 같고, 레지스트리도 같았어요. 명령어조차 거의 그대로입니다.

podman build -t api:v2 .
podman run -d -p 8080:8080 api:v2
podman ps
alias docker=podman

예외 케이스를 잡아내려고 2주 동안 스테이징에서 빡빡하게 돌려봤습니다. 의외로 대부분의 스크립트가 손댈 곳 없이 그대로 돌아갔어요. 호환성을 의식해 만든 도구라는 게 이런 데서 드러나더라고요. 새 도구가 기존 사용자의 근육 기억을 깨지 않으려고 얼마나 신경 썼는지가 명령어 한 줄 한 줄에서 보였습니다.

단계별로 진행한 실제 전환 과정

저희는 스위치를 한 번에 올리지 않았습니다. 운영 PM으로 일하면서 배운 게 하나 있다면, "전부 한 번에" 같은 전환은 결국 누군가 새벽에 롤백하게 되어 있다는 거예요. 가장 안전한 순서를 택했습니다.

먼저 가장 지루한 서비스부터 옮겼습니다. 몇 시간 죽어도 아무도 눈치채지 못할 내부 메트릭 전송 서비스를 골라 10일 동안 Podman에서 돌려봤어요. 그다음 읽기 전용 API, 그다음 워커 풀 순으로 붙였습니다. 영향 범위가 작은 것부터, 트래픽이 가벼운 것부터. 전환은 기술 작업이기 전에 리스크 배분 작업이라고 보거든요. 가장 안 아픈 곳에서 먼저 깨져봐야, 진짜 아픈 곳을 옮길 때 무엇을 조심해야 하는지 알게 됩니다.

진짜 걸림돌은 Docker Compose였습니다. Podman에도 자체 Compose 구현이 있지만, 저희가 의존하던 일부 네트워킹 플래그는 아직 따라오지 못한 상태였거든요.

결국 해당 파일들을 컨테이너 전용 systemd 유닛 파일인 **Podman 쿼드릿(Quadlet)**으로 다시 작성했습니다.

[Unit]
Description=API 서비스

[Container]
Image=registry.local/api:v2
PublishPort=8080:8080
Environment=ENV=prod

[Install]
WantedBy=default.target

이러면 systemctl start api로 서비스가 뜹니다. 재시작은 systemd가 처리하고, 로그는 journald로 흘러가죠. 이 과정을 감시하는 별도 데몬은 없습니다. 컨테이너 관리가 OS의 표준 프로세스 관리 체계 안으로 들어왔다는 점, 그게 핵심이었어요. 별도 계층을 하나 만들어 관리하던 것을, 이미 OS가 수십 년간 다듬어온 체계에 위임한 셈입니다. 깔끔하더라고요.

숫자로 확인한 결과

8주에 걸쳐 한 클러스터 전체를 옮긴 뒤 지표를 들여다봤습니다. 솔직히 15% 정도 개선되면 본전이라고 생각했는데, 실제 수치는 그 예상을 한참 넘었어요.

항목 Docker Podman
RAM (평균) 4.9GB 2.9GB
RAM (최대) 7.2GB 3.4GB
시작 시간 1.8초 1.1초
실패한 풀 0.7% 0.2%

다음 날 아침 이 표를 프리야에게 보여줬습니다. 그녀는 잠깐 보더니 "인스턴스가 네 개네요."라고만 했어요. 그게 전부였습니다. EC2 인스턴스 네 개를 끌 수 있었던 거죠. 누구도 의도적으로 선택한 적 없는 중간 업체 하나를 치웠을 뿐인데 말입니다.

흥미로운 건 이 절감이 신규 투자로 얻은 게 아니라는 점입니다. 새 하드웨어를 산 것도, 더 비싼 인스턴스로 옮긴 것도 아니에요. 그냥 데몬이 먹던 오버헤드를 걷어냈더니 그 자리가 비용으로 돌아왔습니다. 컨설팅을 다니다 보면 비용 절감 얘기는 늘 "무엇을 새로 사야 하느냐"로 흐르는데, 정작 큰 덩어리는 "이미 사놓고 새고 있던 것"에 숨어 있는 경우가 많습니다. 이번이 딱 그랬어요. 그렇게 매년 절감된 비용 덕분에, 그동안 조용히 새어나가던 엔지니어링 여력을 도로 끌어올 수 있었습니다.

아키텍처 변화를 그림으로 이해하기

팀원들을 위해 화이트보드에 그린 그림으로 옮기면 이렇습니다.

이전 (Docker)

엔지니어 -> Docker CLI -> dockerd (배고픈 데몬)
                              |
            +-----------------+-----------------+
            |                 |                 |
        컨테이너          컨테이너          컨테이너

이후 (Podman + systemd)

엔지니어 -> podman -> 컨테이너 (systemd 소유)
엔지니어 -> podman -> 컨테이너 (systemd 소유)
엔지니어 -> podman -> 컨테이너 (systemd 소유)

중간 유통업체가 사라졌습니다. 각 컨테이너는 완전히 독립적으로 돌아가고, 장애의 전파 경로도 그만큼 짧아졌죠. 그림에서 가운데로 모이던 선들이 사라지면, 사실상 장애가 번질 통로 자체가 없어집니다.

솔직하게 말하는 한계점들

Podman이 완벽한 도구는 아닙니다. 마이그레이션을 검토 중이라면 이런 제약을 미리 알고 들어가는 게 좋아요.

첫째, 호환성 문제. 일부 타사 시스템은 여전히 Docker 소켓이 그 자리에 있다고 가정합니다. CI 실행기에서 오류가 나지 않게 하려면 Podman 소켓을 호환 모드로 노출해야 했어요. 결국 Docker를 완전히 지운 게 아니라, Docker라고 믿게 만드는 얇은 호환 계층을 하나 끼워둔 셈입니다. 레거시를 걷어낼 때 흔히 마주치는 그림이죠. 떼어냈다고 생각한 의존성이 사실은 얇은 어댑터 뒤에 살아 있는.

둘째, 네트워킹 성능. slirp4netns 기반 루트리스 네트워킹은 브리지 네트워킹보다 눈에 띄게 느립니다. 그래서 최고 처리량이 필요한 워커 서버에서는 여전히 루트 권한으로 Podman을 돌리고 있어요. 보안과 처리량 사이에서 타협한 지점이죠. 모든 호스트에 같은 정답을 강요하지 않은 겁니다.

셋째, Docker Swarm 환경이라면 Podman은 맞는 선택이 아닙니다. 이건 에둘러 말할 것 없이 명확한 한계예요.

전부 실제로 감수해야 하는 트레이드오프입니다. 전환을 권하면서 이런 부분을 가리는 건 정직하지 않다고 봐요.

3년 운영 경험에서 얻은 교훈

지난 3년간 Docker를 운영하고, 다시 Podman으로 옮기면서 가장 크게 남은 건 도구의 기본 철학이 운영 단계에서 그대로 청구된다는 사실이었습니다. Docker의 중앙집중식 데몬은 초기엔 편했지만, 규모가 커질수록 단일 장애점으로 변했거든요. 처음의 편리함과 나중의 위험은 늘 분리해서 봐야 하는데, 도입 시점엔 그게 잘 안 보입니다. 편리함은 첫날 체감되고, 위험은 몇 년 뒤 새벽에 청구되니까요.

반면 Podman의 데몬리스 구조는 낯설었지만, 컨테이너가 각자 독립적으로 돌면서 전체의 예측 가능성이 올라갔습니다. 루트리스 모드가 보안 쪽에서 가져다준 이점은 솔직히 기대 이상이었고요.

메모리 40% 절감이라는 수치도 의미 있지만, 더 중요한 건 시스템이 예측 가능해졌다는 점입니다. 더 이상 dockerd 때문에 새벽에 깨는 일이 없어졌으니까요. 운영자에게 잘 자는 밤은 어떤 벤치마크 수치보다 정직한 지표입니다.

물론 모든 환경에서 Podman이 정답은 아닙니다. 다만 Docker의 리소스 사용량이나 안정성이 계속 거슬린다면, 한 번쯤 진지하게 들여다볼 만한 대안이라고 생각해요.

기술 선택의 핵심은 결국 지금 상황에 맞는 도구를 고르는 일입니다. 그런데 그 앞 단계, 익숙함이라는 함정에서 한 발 빼는 데에도 생각보다 큰 용기가 필요하더군요. 저는 그 용기를 새벽 2시에야 냈고요.

#Docker#Podman#컨테이너#마이그레이션#성능최적화