Docker and OOM(Out Of Memory) Killer

이야기는 이렇게 시작된다.

  1. Docker를 쓰다가
  2. Container위에서 돌던 Java 프로세스가 OOM(Out Of Memory) Error를 내고 죽은 로그를 발견!
  3. Docker Container의 OOM 문제에 대해서 리서치를 시작!
  4. 언제나 그렇듯 나는 삼천포로 빠져들어…
  5. Linux Kernel의 OOM Killer와의 예정되지 않았던 만남!
  6. 더듬더듬 흔적을 따라가다 보니
  7. Host의 OOM Killer가 Docker Container내의 프로세스를 죽이는 또 하나의 원인이 될 수 있다는 점을 발견!
  8. 가급적 중요한 역할을 수행하는 컨테이너를 Host의 OOM Killer로 부터 보호할 수 있는 방법을 조사하게 되었고
  9. 이거 정리해 놓지않으면 무조건 까먹겠구나! 생각이 들어 블로그에 글을 남기게 되었다.

작년 말부터 Docker를 쓰기 시작해서 아직 엉금엉금 배우는 단계라, 다소 두서없는 글이 되겠지만, Docker와 OOM Killer에 대해서 조사했던 기록을 블로그에 정리해본다. 혹시 틀린 부분이 있다면, 주저없이 댓글을 달아주시길 바랍니다! (그러려고 쓰는 블로그가 되겠다.)

처음엔 Docker에 할당한 메모리의 문제인 줄 알았다.

OOM Error를 처음 만났을 때, Docker에 할당한 메모리를 초과할 경우 발생하는 문제점으로 추측하고, Docker의 memory관련 파라미터를 먼저 체크했다.

Docker Container를 run할때 사용하는 resource관련 옵션은 아래 URL에서 확인할 수 있다.
Docker run reference
Memory – Limit a container’s resources

간단히 살펴보면 다음과 같다.

옵션 설명
-m,--memory="" 사용 가능한 메모리 b,k,m,g 로 구분. 최소값은 4m
--memory-swap="" total memory limit (memory + swap) 예를 들어 --memory-swap이 600m 이고 --memory가 300m이면 container가 사용하는 swap은 300m이 된다. default는 -1(무제한) 이다.
--memory-reservation="" Memory soft limit --memory옵션보다 작은 값으로 설정되어야 하며, 메모리 경합이 있거나 호스트의 메모리가 부족하다고 판단될 때 최소 메모리로 우선적으로 할당하게 된다.
--kernel-memory="" Kernel memory limit. Kernel 메모리는 swap되지 않는 물리 메모리다. 이 값을 설정할 경우 호스트의 리소스를 block할 수 있고, 호스트 및 다른 컨테이너에 영향을 미칠 수 있다. 자세한 설명은 여기를 참고. 최소값은 4m
--memory-swappiness="" Tune container memory swappiness. default는 -1 (호스트의 vm.swappiness설정을 따름)이다. 0 부터 100까지 container내에서의 swappiness를 설정할 수 있다.
혹시 swappiness를 처음 만났다면
  • vm.swappiness = 0 : 스왑 사용안함 (실제로 swap이 disable되지는 않는다. 참고)
  • vm.swappiness = 1 : 스왑 사용 최적화
  • vm.swappiness = 60 : 기본값
  • vm.swappiness = 100 : 적극적으로 스왑 사용

규칙으로 설정되며, 현재 서버의 swappiness 값은 다음 명령으로 확인할 수 있다.

$ sudo sysctl -n vm.swappiness
60

그리고 운명적으로 다음 파라미터를 발견하게 된다.

옵션 설명
–oom-kill-disable default: false, Disable OOM Killer 참고
–oom-score-adj default: 0, Tune hosts’ OOM preferences (-1000 to 1000)
oom-kill-disable??? oom-score-adj??? OOM Killer????

여기서부터 나는 OOM Killer의 마성(이라 쓰고 삼천포라 읽는다) 에 빠지고 마는데…

여기서 숨을 크게 들이쉬고 시작해보자!

OOM Killer에 대해 알아보기 전에

먼저 Linux Kernel의 Memory Overcommit에 대한 이해가 필요하다.
malloc() 등의 시스템 콜을 통해서 Linux Kernel에 메모리 할당을 요청하게 되면, Kernel이 바로 물리 메모리 상의 실제영역을 할당(바인딩)하는것이 아니라, 요청한 메모리 영역의 주소값만 반환 하게 되는데 이를 memory commit이라고 한다. [중요] 아직 실제 물리 메모리에는 할당이 되지 않은 상태

나는 malloc을 배우고, 와! 개발자가 되어야겠다! 하고 결심을 했었더랬다.
dynamic memory allocation이라니 얼마나 멋진말인가!

이 memory commit이 이루어 질때, Linux 가상 메모리 시스템은 현재 사용가능한 메모리영역을 초과한 영역의 반환을 허락해주는데 이를 Memory Overcommit이라고 한다.

메모리를 요청만하고 실제로 사용하지 않을 수도 있고, 요청 후 바로 사용하지 않고 한참 있다가 사용할 수도 있고, 요청할 땐 부족했던 가용메모리가 메모리 반납을 통해서, 사용을 시작할 땐 충분히 확보될수도 있기 때문에 메모리 할당요청 시점과 실제사용 시점 사이의 비어있는 시간에 다른 프로세스가 이 메모리 영역을 사용할 수 있도록, 주소값만 리턴하는 방법으로 메모리 사용을 극대화하는 것이다. (숨이차다.)

좀 더 궁금한 분은 vm.overcommit에 대한 짧은 이야기 – 강진우님의 블로그를 읽어보길 바란다. 엄청 해매고 있던 내용이었는데 강진우님 블로그 글을 읽고 머리가 상쾌해지는 기분! 을 느낄 수 있었다.

OOM Killer(Out of Memory Killer)

이렇게 memory commit에서는 먼저 주소값만 리턴하고, 해당 주소값에 쓰기 요청이 들어왔을 때 비로서 해당 주소값을 실제 물리 메모리에 바인딩하게 되는데, 이 때 실제 메모리와 가상 메모리공간(Swap memory)을 전부 활용해도 더 이상 메모리 확보가 불가능한 경우, 쓸모없다고 판단되는 프로세스를 강제 종료시켜 여유 메모리를 확보하는 Linux Kernel의 메커니즘을 OOM(Out Of Memory) Killer라고 한다.

메모리를 최대한 넓게 쓰라고 아량을 배풀었던 over commit이 OOM Killer에 의해서, 되려 독이 되어 다가올 때가 있는 것이다.

OOM Killer가 프로세스를 죽이는 알고리즘이 Heuristic(체험적, 발견적)에 기반하기 때문에 때에따라 중요한 Server Process가 죽는 경우가 발생할 수 있다.
자료를 조사하는 과정에서는, mysqld가 OOM Killer에 의해 강제종료되서 당혹스러워 하는 사람들을 종종 만날 수 있었다.

물리 메모리가 주소값에 바인딩 되는 순간

여유 물리 메모리가 부족하고, 사용률이 낮은 물리 메모리를 swap할 디스크 공간도, shrink(수축)할 cache공간도 없다고 판단 했을 때 실행 중인 모든 task에 대해 badness()를 계산하고, 이 중 가장 나쁜(badness()가 가장큰) task를 kill하게 되는데, 이 가장 나쁜녀석으로 우리가 중요하게 생각하는 프로세스가 선정되기도 하는 것이다.

프로세스 마다 커널에 의해 -1000 에서 1000(구버전 커널에서는 -16 ~ 15)사이의 score가 매겨지게 되는데
Kernel이 판단하기에, -1000에 가까울수록 착한 프로세스로 1000에 가까울수록 나쁜 프로세스로 점수를 매기고, 이 score가 높은 프로세스부터 OOM Killer의 대상으로 선정된다.
(-1000은 OOM Killer에 의해 종료되지 않도록 함을 의미한다.)

score 값은 아래와 같이 /proc/{process_id}/oom_score로 확인할 수 있다.

$ cat /proc/29285/oom_score
0

다시 긴숨을 들이쉬기 전으로 돌아가서

Docker에서는 다음의 파라미터로 oom-killer에 의한 프로세스 종료를 예방할 수 있다.

옵션 설명
--oom-kill-disable default: false, Disable OOM Killer 참고
--oom-score-adj default: 0, Tune hosts’ OOM preferences (-1000 to 1000)

--oom-kill-disable의 값을 true로 하면, host의 OOM Killer를 disable할 수 있고
--oom-score-adj의 값을 설정하면 /proc/{process_id}/oom_score_adj에 값이 할당되어(default는 0) Kernel이 설정한 badness() score(oom_score)의 값보다, 사용자가 설정한 oom_score_adj값을 우선시 하도록 설정 가능해지는 것이다.

참고로 Container가 Host의 OOM Killer에 의해 중지된 경우 STATUS를 Exited (137) xxx ago와 같이 표시한다.

Error 137
from https://bobcares.com/blog/error-137-docker/

OOM Error가 발생했던 Container를 docker stats 명령으로 확인해보니 이 프로세스가 해당 서버내에서 가장 많은 메모리를 사용하고 있었고, Kernel은 이 프로세스를 쓸모없는 프로세스 로 판단했던 듯 하다.

이제 OOM Killer에 의한 Docker Container 내부 프로세스의 종료를 방지해보자.

--oom-kill-disable=true 파라미터는 왜 때문인지 운영하는 서버(Ubuntu 16.04기반)에서 정상적으로 동작하지 않았다.
--oom-score-adj 파라미터는 정상적으로 작동했다. --oom-score-adj 값을 -1000을 주었을 때 --oom-kill-disable=true와 같은 효과를 주기 때문에, 큰 문제는 아닌 것 같다.

문제가 됐던 Process를 Host의 Process id 기반으로 추적해보니 /proc/{process_id}/oom_score 값이 600 ~ 760 사이를 왔다갔다 했다.

$ cat /proc/29285/oom_score
766
$ cat /proc/29285/oom_score_adj
0

$ echo -500 > /proc/29285/oom_score_adj 명령 등으로 소중한 프로세스의 oom_score_adj 값을 조정해주면, OOM Killer에 의해서 이 프로세스가 가장 나쁜 프로세스로 선정되는 것은 방지할 수 있을 것 으로 생각된다.

혹은 docker run 할 때 --oom_score_adj=-500 과 같이 파라미터로 전달할 수도 있다.

이 프로세스 말고 시스템의 모든 프로세스의 oom_score 값을 조사해 보니, 대부분의 프로세스는 oom_score를 0으로 가지고 있었고 음수 값으로 설정된 프로세스도 다수 조회됐다. 아마도 시스템에서 중요한 역할을 하는 프로세스에는 음수값으로 자동 세팅이 되는 듯하다.

Docker 컨테이너 자체의 프로세스는 서버 마다 다른 oom_score로 측정되었는데 0으로 되어있는 서버도 있고 -500으로 되어 있는 서버도 있었다.

docker-compose.yml 파일에 oom-score-adj 파라미터를 적용하기 위해서는

다음과 같이 version: ‘2’ 를 명시 해줘야만 사용이 가능했다.
version: '2'가 아닌 경우 oom_score_adj는 지원하지 않는다는 에러가 발생한다.

version: '2'
services:
   db:
     image: mysql:5.7
     restart: always
     ...
     oom_score_adj: -1000

vm.overcommit_memory

앞서, malloc() 에서 가용영역을 넘어선 메모리영역을 리턴하는 것이 overcommit이라 설명했는데,
vm.overcommit_memory Kernel Parameter를 통해서 다음과 같이 overcommit rule을 설정할 수 있다.

vm.overcommit 설명
0 (디폴트) heuristic에 따라 overcommit여부를 판단. 메모리 확보가 불가능한 경우, 실행중인 프로세스를 강제 종료해서 메모리를 확보
1 무조건 overcommit 허용. 메모리 확보가 불가능한 경우 OOM Killer 동작(0과 같다)
2 overcommit 불가. 메모리가 부족할 경우 에러 발생 [Swap size] + ([RAM size] * vm.overcommit_ratio/100)

대부분의 서버는 다음과 같이 기본 값으로 설정되어 있다.

$ sudo sysctl -n vm.overcommit_memory
0
$ sudo sysctl -n vm.overcommit_ratio
50 

의도하지 않게 프로세스가 OOM Killer에 의해 지속적으로 종료된다면, 메모리 관리를 Kernel heuristic에 맡기기보다 vm.overcommit을 2로 설정하고, overcommit_ratio를 적당한 수치로 조정해볼 필요가 있다.

앞으로 여러 번의 시도와 경험축적이 필요하겠지만

docker run의 oom-score-adj optionvm.overcommit Kernel 파라미터의 적절한 조합으로 조금 더 안전하게 프로세스를 유지할 수 있지 않을까? 하는 생각을 한다.

oom-xxx 의 옵션외에도

Save Yourself From The OOM Killer 글에서는 vm.min_free_kbytes의 값을 설정함으로서, 시스템이 지속적으로 유지할 수 있는 최소 free memory를 물리 메모리의 5~10% 값으로 상향조정하는 방법을 권유하고 있다.

$ sysctl -n vm.min_free_kbytes
10978

# 영구적으로 이 값을 수정하려면 /etc/sysctl.conf 파일을 수정해야함
$ sysctl -w vm.min_free_kbytes=20978

vm.min_free_kbytes와 Linux Kernel의 메모리 할당에 대해서 강진우님의 블로그 메모리 재할당과 커널 파라미터를 읽어보면 큰 도움이 된다. 감사합니다. 강진우님!

덧붙여 Docker내에서 Java프로세스를 실행한다면,

Analyzing java memory usage in a Docker container 이 블로그를 참고하면 Docker내에서의 Java 프로세스 메모리 사용을 좀 더 정확하게 추적할 수 있을 것 같다.
Xmx로 설정한 memory limit외에도 시스템이 사용하는 메모리 RSS(Heap size + MetaSpace + OffHeap size) 등에 대해서 사용량을 예측, 메모리를 할당, 관리할 필요가 있다는 것을 알게 되었다.

이렇게 Docker와 OOM-Killer에 대해 조사한 내용을 정리해보았다.

처음에 글을 쓸때는, 사내위키에 정리했던 글이라 블로그에 옮기기 무척 수월할 것으로 생각했는데, 막상 글을 적기 시작하니, 마무리를 지을때까지 짧지 않은 시간이 소요됐다. 부디 이 글이 다 까먹을 미래의 나를 비롯해서 Docker를 공부하는 누군가에게 도움이 되었으면 좋겠다.

아직 블로그 두번째 글이라 훈련하는 것이라 생각하고 더 많이 열심히 글을 써나갈 수 있으면 좋겠다. 이제 막 도커 컨테이너를 배워가는 입장에서 쓰는 글이라, 틀린 내용이 수두룩할것 같은데, 편하게 댓글로 의견을 적어주시면 공부하는데 더더 도움이 되고 다음 글을 쓰는데 응원도 될 것 같다.

참고자료 (감사합니다!)

vm.overcommit에 대한 짧은 이야기
메모리 재할당과 커널 파라미터
OOM Killer & Overcommit
Overcommit and OOM, The Linux Kernel
메모리 overcommit
How to Prevent Docker containers from crashing with error 137
Limit a container’s resources
OOM Killer
Save Yourself From the OOM Killer
Analyzing java memory usage in a Docker container

Kubernetes

logo.png

Kubernetes(쿠버네티스)Docker, rkt container를 위한 Open Source Orchestration System 입니다.
약 10여년 전, 구글이 Container Management를 위해 개발한 Borg를 시작으로 Omega를 거쳐 Kubernetes가 탄생하게 됩니다. Kubernetes가 사용하는 Pod, Service, Label, IP-per-pod model 개념등이 Borg에서 시작되었습니다.

Kubernetes는 그리스어로 배의 조타수를 의미 (영문 표기로는 Helmsman)
K와s 사이의 8글자를 생략해서 K(ubernete)s = K8s라고 표기하기도 함
2014년 Google에서 개발시작, 2015년 7월 첫 릴리즈
2016년 Cloud Native Computing Foundation(Linux Foundation)에 기부
100% Open Source, Go 언어로 개발
2017년 3월 현재 v1.4.9 (v1.6.0-beta.1)

아래 1분 30초 분량의 동영상을 통해서 Container Orchestration이 왜 필요한지, Kubernetes의 역할이 무엇인지 이해해 봅시다!

특징

  • Automatic binpacking
    가용성을 희생하지 않는 범위안에서 리소스를 충분히 활용해서 Container를 배치합니다.

  • Self-healing
    Container의 실행 실패, node가 죽거나 반응이 없는 경우, health check에 실패한 경우에 해당 Container를 자동으로 복구합니다.

  • Horizontal scaling
    pod의 CPU사용이나 app이 제공하는 metric을 기반으로 ReplicaSet을 scaling할 수 있습니다.

  • Service discovery and load balancing
    익숙하지 않는 Service Discovery매커니즘을 위해 Application을 수정할 필요가 없습니다. Kubernetes는 Container에 고유 IP, 단일 DNS를 제공하고 이를 이용해 load balacing합니다.

  • Automated rollouts and rollbacks
    application, configuration의 변경이 있을 경우, 전체 인스턴스의 중단이 아닌, 점진적으로 Container에 적용(rolling update) 가능합니다. 변경내용이 문제 있을 경우 자동으로 Rollback을 진행할 수 있습니다.

  • Secret and configuration management
    Secret key, configuation을 이미지의 변경없이 업데이트할 수 있고, 외부로 노출(expose)하지 않고 관리/사용할 수 있습니다.

  • Storage orchestration
    local storage를 비롯해서 public cloud(GCP, AWS), network storage등을 구미에 맞게 자동 mount할 수 있습니다.

  • Batch Execution
    batch, CI 작업의 수행을 관리할 수 있습니다. 원한다면 실패 시 container를 replace하는 것도 가능합니다.

아키텍쳐

architecture.png
from https://en.wikipedia.org/wiki/Kubernetes

Kubernetes에서는 Kubernetes Master와 1개 이상의 (worker) node로 이뤄진 가상/물리 머신의 Set를 통해 Cluster가 구성됩니다.
Kubernetes v1.5(alpha)부터 Master의 replication(Google Compute Engine only)을 지원하기 시작했습니다.

주요 키워드

  • Kubernetes Master
    Kubernetes Cluster의 main component입니다.

    • API server (kube-apiserver)
      Kubernetes Components(kubectl, scheduler, replication controller, etcd datastore, kubelet, kube-proxy…)들의 hub로서 HTTP, HTTPS 기반의 RESTFul API를 제공합니다.

    • Scheduler (kube-scheduler)
      어떤 Container가 어떤 Node에서 실행될지 결정합니다.
      CPU, Memory, 얼마나 많은 Container가 해당 Node에서 작동 중인가에 기반한 간단한 알고리즘으로 Container를 배치합니다.

    • Controller manager (kube-controller-manager)
      Kubernetes node를 관리합니다.
      Kubernetes internal information을 생성하고 갱신합니다.
      Container의 상태를 기대하는 상태로 변경합니다.

    • etcd storage
      shared configuration, namespace, replication information등
      pod/service의 세부사항, 상태 저장 및
      service discovery를 위해 skydns를 기반으로한 DNS 데이터 저장에 사용됩니다.

  • Kubernetes Node – minion
    Kubernetes cluster의 slave(work) node입니다.

    • kubelet
      Kubernetes node의 main process.
      Kubernetes master와 통신하며 다음의 동작을 수행합니다.

      • 주기적으로 API Controller에 check&report access합니다.
      • container operation을 수행합니다.
      • API제공을 위한 simple http 서버를 구동합니다.
    • Proxy (kube-proxy)
      • 각 container간의 network proxy와 load balancer를 핸들링합니다.
      • container간의 TCP/UDP 패킷 송수신을 위해 Linux iptables rules(nat table)의 변경을 수행합니다.
    • cAdvisor
      • cAdviser(Container Advisor)는 동작중인 container의 resource 사용량과 performance에 대해 분석/제공합니다.
  • Container
    우리가 아는 그 Docker/rkt Container 입니다.

  • Pod
    deployable unit의 가장작은 단위
    container의 묶음
    pod내부의 container들은 network와 data volume을 공유합니다.
    Pod내부의 container들은 localhost로 서로에 접근가능합니다.
    라우팅 가능한 IP를 부여받습니다.

  • Service
    Persistent Endpoint for Pods
    Pod의 lifetime은 짧고 mortal(언젠간 죽어요)합니다.
    Pod이 몇개가 존재하던, Service가 Pod묶음의 proxy로서 존재합니다.
    Service는 Pod에 Virtual IP, DNS name을 제공하고 label selector를 통해 Pod을 구분합니다.
    Service는 요청을 받아 Pod에 load balancing하게 됩니다.

  • Replication Controller
    pod 복제에 사용하는 pod template 를 제공합니다.
    pod의 scaling logic을 제공합니다.
    Replication Controller를 통해 rolling deploy가 가능하게 됩니다.

여기서 아키텍쳐를 한번 더 복습!

kubernetes-architecture.png
from http://blog.arungupta.me/key-concepts-kubernetes/

  • ReplicaSets
    pod의 run * copies(replicas)
    health check를 통해 pod의 상태를 체크합니다.
    pod의 상태가 불량할 경우, 재 시작합니다.

  • deployment
    Drive current state towards desired state
    rolling update, rollback을 지원하는 pod, replica set

  • Label
    Kubernetes는 Label을 Kubernetes가 사용하는 각각Object를 분류하는 nametag로 사용합니다.
    내부적으로 Selector가 이 Label을 사용해서 query합니다..

  • Selector
    Selector
    Selector2
    Selector3
    from WSO2Con US 2015 Kubernetes: a platform for automating deployment, scaling, and operations

  • Volume
    pod은 volume을 filesystems처럼 마운트 할 수 있습니다.
    empty directory, host directory, Google Persistent Disk, Amazon Blob Store, NFS, glusterfs, rdb, cephs, git repository 등을 Volume으로 사용할 수 있습니다.

  • Namespace
    pod, replicasets, volume등을 그룹핑합니다.
    각각의 pod, replication controller, volume, secret을 구분하는데 사용합니다.

더 알고싶은 키워드

  • PetSets
    Pet is a stateful pod
    Pet is bound to a dynamically created data volume that data volume will nerver be deleted automatically.
    the Pet is bound to the same volume on a restart

  • Jobs
    run short living tasks
    retry on failure
    ScheduledJobs can be started at specific times(like cron)

  • DaemonSets
    DaemonSets run pods on all (or a selected set of) nodes in the cluster
    userfule for running containers for logging and monitoring

  • Secrets and ConfigMaps
    separate your application code(=images) and configuration
    both Secrets and ConfigMaps are key-value-pairs
    use Secrets for binary values
    use ConfigMaps for string values
    both can be read by the container via environment variables or mapped into a data volume like property files.

API

Service Discovery

Kuberentes는 Environment VariablesDNS(skydns based)의 2가지 방법을 통해서 Service Discovery할 수 있습니다. Enviroment Variables를 사용하는 경우, Pod이 생성되기 전에 미리 선언을 해야하는 등 순서에 따른 문제가 발생할 수 있어, DNS 사용을 통한 Service Discovery방식을 추천하고 있습니다.

Scaling

  • Horizontal Pod Autoscaling
    pod의 CPU사용이나 app이 제공하는 metric을 기반으로 ReplicaSet을 scaling
# Scale a replicaset named 'foo' to 3.
$ kubectl scale --replicas=3 rs/foo

# Scale a resource identified by type and name specified in "foo.yaml" to 3.
$ kubectl scale --replicas=3 -f foo.yaml

# If the deployment named mysql's current size is 2, scale mysql to 3.
$ kubectl scale --current-replicas=2 --replicas=3 deployment/mysql

# Scale multiple replication controllers.
$ kubectl scale --replicas=5 rc/foo rc/bar rc/baz

# Scale job named 'cron' to 3.
$ kubectl scale --replicas=3 job/cron

# create an autoscaler for replication controller foo, with target CPU utilization set to 80% and the number of replicas between 2 and 5.
$ kubectl autoscale rc foo --min=2 --max=5 --cpu-percent=80
  • Cluster Autoscaling
    CPU, memory 사용량을 기반으로 cluster의 node개수를 scaling
    cloud provider에 의존합니다.

Network

https://kubernetes.io/docs/admin/networking/

Mornitoring & Logging

https://kubernetes.io/docs/concepts/clusters/logging/

그 밖의 이야기

참고자료