Docker container의 로깅 시스템 정리

|

Docker 의 로깅 시스템

컨테이너 운영환경에서 Application을 운영하다보면 기본적인 로깅 시스템의 이해가 필요하다. 도커는 log stream(도커의 기본 로그는 stderr, stdout만 스트림)을 컨테이너 엔진에 의해 처리하고 로깅 드라이버로 리디렉션 한다. 이 로그들이 어느 위치에 어떠한 형태로 저장되는지 등의 도커 컨테이너의 로깅 시스템에 대한 기본적인 정리를 하고자 한다.

Logging drivers

로깅 드라이버는 컨테이너들의 log stream를 모으는 역할을 한다. 기본 driver는 json-file__이며 __syslog, journald(systemd journal용 드라이버), fluentd, logagent 등이 있다. logging driver는 도커를 실행할 때 daemon.json 파일에 설정할 수 있고 도커 실행 시 옵션으로 줄 수도 있다.
참고로 fluentd를 로깅 드라이버로 쓰려면 컨테이너와 동일한 host에 fluentd daemon이 있어야한다. daemon.json을 아래와 같이 작성하거나 도커 실행 시 flunetd를 옵션으로 주면 로그 스트림은 fluentd로 리디렉션된다.

fluentd docker logging 레퍼런스

# daemon.json
 {
   "log-driver": "fluentd",
   "log-opts": {
     "fluentd-address": "fluentdhost:24224"
   }
 }
# docker command
$ docker run --log-driver=fluentd --log-opt fluentd-address=fluentdhost:24224

Logs location

JSON log 파일은 linux 환경에서는 /var/lib/docker/containers/ 하위에 존재하고 mac os에서는 ~/Library/Containers/com.docker.docker/Data/ 하위에 존재한다. 이 때 하나 당 하나의 로그 파일을 가지며 컨테이너의 id로 파일을 찾으면 된다. 아래의 명령을 통해 확인할 수 있는 로그가 언급한 location에서 가져오는 것이다.

# docker command
$ docker logs <container_id>

한계점

기본 설정값만으로도 도커는 로그를 적절히 처리해주는거 같지만 실서비스로 운영하기에는 여러 문제를 가진다.
컨테이너는 stateless라 컨테이너가 종료되면 로그도 사라진다.
log stream을 기록하다보면 disk space가 가득찰 것이다. 이는 다수의 컨테이너를 가지는 클러스의 경우 더 빨리 도래할 것이다. 이 문제는 log rotate(기본 제공기능이 아님)를 이용해 해결할 수 있다. 하지만 오랜기간 보관해야할 로그의 경우는 적절하지 않다.
실서비스는 클러스터 레벨에서 수 많은 컨테이너가 내뿜는 로그를 지속적으로 모니터링하는것이 필수적이다. 따라서 여러 컨테이너의 로그를 대량의 데이터를 다루기 용이한 파일시스템을가진 스토리지에 모아 모니터링해야한다. 위 문제를 해결하기위해 EFK, ELK 등의 스택을 쓰기도하며 kafka를 이용해 log stream을 spark, hdfs에 적절히 streaming하기도 한다.

여러개의 container를 가지는 Pod

|

Pod란

Pod는 kubernetes에 의해 배포될 수 있는 가장 작은 유닛이다. 이는 하나의 container를 kubernetes에 실행하기 위해 반드시 하나의 Pod가 필요하다는 뜻이다. 동시에 Pod는 여러개의 container를 가질 수 있다. 하나의 Pod안의 container들은 어떤 목적을 위해 강하게 결합된 것들이다. 이러한 특징을 단순히 사용하기보다 깊이 이해하고 사용한다면 kubernetes 운영환경에서 서비스를 설계하는데 큰 도움이 될 것이다.

Kubernetes를 만든이들은 Pod가 왜 여러개의 container를 가질 수 있게 하였을까

Kubernetest는 container를 관리하기위해 restart policy, live probe와 같은 정보를 가지고 있다. 이러한 정보들을 각 container마다 설정하기보다 통합된 entity로 관리한다면 이득을 얻게 될 것이다. Pod의 container들은 같은 logical host이다. 같은 network namespace를 사용하고 볼륨을 공유 할 수 있다. 이런 특징을 가진 container들은 효율적인 통신을 할 것이고 상호간의 data locality를 보장한다. 그렇다면 Pod와 마찬가지로 하나의 container가 여러 기능을 담으면 될거 같지만 그렇지 않다. 이유는 많은 기능들을 하나의 container에 담는 것은 하나의 container는 하나의 Process를 가지는 전략에 반한다. 이것은 SOLID단일책임의 원칙(SRP)과 결이 비슷하다고 생각한다. 왜냐면 서비스를 뒷받침하는 여러 소프트웨어들의 디펜던시의 결합성을 낮출 수 있고(decoupling), 또한 협업 시 세분화된 container는 재상용성이 좋다. 물론 하나의 Pod가 여러 container를 가지는 것, 하나의 container는 하나의 프로세스를 가지는 것에도 trade-off는 존재할 것이다. 적어도 이런 특징을 깊이 고민한 후 설계한다면 더 나은 소프트웨어가 만들어지지 않을까.

여러 container를 가지는 Pod의 예

Pod가 여러 container를 가지는 주요 목적은 주요 기능을 위해 특정 container가 다른 container를 헬퍼 프로세스로써 동작하기 위함이다.

  • Sidecar container는 main container를 도와준다. 예컨데 main conatiner(웹 서버)가 뱉어내는 로그를 모니터링(이 방식은 잘 쓰이진 않는다. cluster-lever logging)하는 conatiner, 생성된 파일이나 데이터를 주요 container에 로드하는 container가 있다(shared volume). 이런 일종의 helper container를 빌드해둔다면 이런 역할이 필요한 conatiner에 사용할 수 있으므로 재사용성이 높다.
  • Proxy container는 main container에 연결된다. 예컨데 nginx container는 static file들을 client에 서빙하고 main container(웹 서버)의 reverse proxy 역할을 하기도 한다.

Pod의 container간 커뮤니케이션 방식

  • Shared volumes Pod의 container들은 같은 호스트에 존재하므로 볼륨을 공유할 수 있다. 하지만 Pod는 stateless라 Pod가 재시작되거나 종료되면 이 볼륨의 상태 또한 사라진다. 이 점을 기억해야 할 것이다. 데이터를 영구적으로 보존하기 위해선 Persistent Volume을 사용해야한다.
  • Inter-process communications (IPC) Pod의 container들은 같은 IPC namespace를 가진다. container들은 표준 inter-process 통신을 할 수 있다. 가령 System V semaphore나 POSIX shared memory. 표준 Linux message queue를 이용해 producer container와 consumer container를 가지는 Pod를 설계할 수 도 있다.
apiVersion: v1
kind: Pod
metadata:
  name: std_linux_message_queue
spec:
  containers:
  - name: 1st
    image: allingeek/ch6_ipc
    command: ["./ipc", "-producer"]
  - name: 2nd
    image: allingeek/ch6_ipc
    command: ["./ipc", "-consumer"]

덧붙임

  • 하나의 Pod속 container들은 어떻게 expose될까? 위에서 말한대로 같은 Pod의 container들은 같은 IP, port space를 가지는데, 각각이 다른 포트를 가짐으로써 expose될 수 있다.
  • 하나의 Pod의 container들이 실행될 때 이들은 병렬적으로 실행된다. 일반적으로 실행 우선순위를 줄 수 없다. 위 linux message 예제의 Pod는 두번째 container가 먼저 실행된다면 메시지가 큐에 없는 상태로 생성되므로 정상 실행이 되지 않을것이다. 이를 회피하기 위해선 Init Container를 참고해야 manifest를 작성해야한다.

kubernetes deployment

|

Deployment

ReplicaSet의 목적은 파드의 집합을 실행 및 업데이트하고 안정적으로 유지하기 위함이다.
Deployment는 ReplicaSet의 기능과 더불어 파드를 정의하여 선언적으로 어플리케이션을 관리하고 상태를 변경할 수 있다.
kubectl로 실행중인 어플리케이션을 변경했던 ReplicaSet이나 ReplicationController와 달리
Deployment manifest로 선언하고 추상화시켜 Deployment 오브젝트만으로 무중단으로 어플리케이션의 상태를 변경하고 관리할 수 있다.

Deployment 선언

Deployment는 크게 레이블 셀렉터, 레플리카 수, 파드 템플릿, 업데이트의 수행방법이 선언되어있다.
아래의 예제를 보면 간단히 선언하고 생성할 수 있음을 알 수 있다.

Deployment example: simple app

Deployment 업데이트

kubectl rolling-update를 이용해 업데이트를 수행하고 파드가 업데이트가 될때까지 기다렸던
ReplicationController, ReplicaSet과 달리 Deployment는 미리 선언한 정의대로 손쉽게 업데이트 할 수 있다.
Deployment의 업데이트 전략은 기본적으로 RollingUpdate이다. 이는 새로운 파드는 생성하고 기존 파드를 삭제하며 점진적으로 새로운 파드로 트래픽을 유입시키는 방식이다.
업데이트 전략중 Recreate도 있는데 이는 짧은 다운타임이 발생하므로 상황에 맞을 때 사용해야 한다.
이 전략은 가령 새로 수정된 API가 기존 프론트엔드와는 호환되지 않을 때 즉 병렬로 기존 파드와 새로운 파드를 사용할 수 없을 때 선택할 수 있는 전략이다.
업데이트는 빠르게 진행된다. 하지만 minReadySeconds를 사용하면 롤아웃 속도를 늦출 수 있어 해당 과정을 잘 모니터링할 수 있다. 이는 위 Deployment example: simple app에서 확인할 수 있다.
업데이트를 실행하게되면 기본 전략인 RollingUpdate방식대로 기존 파드는 스케일 다운하고 새로운 파드는 스케일 업하며 점차적으로 새로운 파드를 교체하는 것을 확인 할 수있다.

[kubectl describe deployment simpleapp 명령어를 통해 확인한 업데이트 이벤트]

Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  16m    deployment-controller  Scaled up replica set simpleapp-79b55dfbc4 to 3
  Normal  ScalingReplicaSet  7m18s  deployment-controller  Scaled up replica set simpleapp-699dcbcb87 to 1
  Normal  ScalingReplicaSet  7m3s   deployment-controller  Scaled down replica set simpleapp-79b55dfbc4 to 2
  Normal  ScalingReplicaSet  7m3s   deployment-controller  Scaled up replica set simpleapp-699dcbcb87 to 2
  Normal  ScalingReplicaSet  6m48s  deployment-controller  Scaled down replica set simpleapp-79b55dfbc4 to 1
  Normal  ScalingReplicaSet  6m48s  deployment-controller  Scaled up replica set simpleapp-699dcbcb87 to 3
  Normal  ScalingReplicaSet  6m37s  deployment-controller  Scaled down replica set simpleapp-79b55dfbc4 to 0

Deployment 롤백

kubectl rollout history deployment simpleapp 명령어를 통해 확인해보면 revision history 확인할 수 있다.
업데이트된 버전에 문제가 생겼을 경우 revision 버전을 명시해 롤백을 하거나 이전 버전으로 되돌릴 수 있다. 이 점이 Deployment의 강력한 장점중 하나이다.

$ kubectl rollout undo deployment simpleapp # 이전 버전으로 롤백
$ kubectl rollout undo deployment simpleapp --to-revision=1 # 특정 버전으로 롤백

Deployment 전략

문제가 있는 어플리케이션을 배포하게된 경우 Deployment 전략으로 어떻게 문제를 회피할 수 있을지 설명하려 한다.
minReadySeconds, maxSurge, maxUnavailable, readinessProbe 개념과 함께 설명하겠다.
우선 Probe는 해당글을 참고하면된다.
앞서 롤아웃을 느리게 만드는 minReadySeconds는 사실 업데이트 실행 시 새로운 파드가 교체될 때 해당 파드가 정상적으로 동작하는지 확인하기 위한 최소 보장 시간이다. 즉 minReadySeconds가 20으로 설정되어 있으면 새로운 파드가 생성되고 20초간 기다린 후 롤아웃이 재개되는 것이다. 하지만 기다리기만 하면 되는 것이 아니다. 파드가 정상적으로 실행되는지 확인을 해야한다. 이를위해 probe를 사용하며, 이용하지 않는다면 문제있는 파드를 계속 업데이트 해나갈 것이다. 이러한 상황을 방지하기 위해 파드 health check를 하고 정상 실행이 가능한 상태라면 앤드포인트가 노출되는 것이다.
정상적으로 잘 업데이트가 된다면 문제가 없겠지만 문제가 있는 어플리케이션 배포 시 업데이트를 중단시켜야 하는데 이때 사용할 수 있는 설정값이 maxUnavailable과 maxSurge이다. maxUnavilable은 의도하는 replica 수에 사용하지 못하는 파드의 최대 수 또는 비율(비율계산 후 내림)이고, maxSurge는 의도하는 replica 수에 허용되는 최대 파드 수 또는 비율(비율계산 후 반올림)이다. 예를들어 replicas가 3이고 maxSurge가 1이면 롤아웃 시 허용되는 최대 파드 수는 4이며 maxUnavailable이 1이면 파드의 수는 항상 2이상으로 유지해야한다.(단, maxSurge 1로 인해 4개 이하) 비율과 절대값을 쓸 수 있는데 절대값으로 예를들어 설명하겠다. 아래의 예제는 Deployment에 replica를 3으로 설정하고 maxSurge는 1, maxUnavailable은 0, minReadySeconds가 20인 Deployment가 있다. 새로운 이미지를 교체하는 Deployment를 업데이트(v1->v2)한다면 v1의 replica는 3으로 되어있을 것이고 maxSurge 1에 의해 새로운 이미지를 가진 파드가 하나 생성된다. 이때 minReadySeconds에 의해 20초간 기다릴것이고 ReadinessProbe에 의해 health check를 한다. 이때 probe에 의해 파드가 문제가 있음을 발견하고 ready상태로 남아 앤드포인트를 노출하지 않는다. 이때 동작하지 않는 파드가 이미 하나 있고 replica 수가 4이며 maxUnavailable 0이므로 롤아웃이 더 이상 진행되지 않을 것이다.(의도한 replica 수 3이라 3보다 더 작아지면 안되므로 기존 파드를 삭제하지 않음)
따라서 문제가 있는 어플리케이션 배포를 위 전략으로 막을 수 있는 것이다. 기본적으로 롤아웃은 10분동안 진행되지 않으면 실패한 것으로 간주하는데 progressDeadlineSeconds로 시간을 설정할 수 도 있다.

Deployment strategy example

kubernetes health probe

|

쿠버네티스 health probe

Health probe는 쿠버네티스가 실행중인 어플리케이션이 정상적인 서비스를 제공할 수 있는지의 여부를 확인하는 것이다. 쿠버네티스는 컨테이너 프로세스 상태를 주기적으로 확인하고 문제가 감지되면 컨테이너를 재시작한다. 하지만 컨테이너 프로세스의 확인만으론 어플리케이션이 정상적으로 서비스할 수 있는지 장담할 수 없다.(예를들어 무한 루프에 빠져있거나 데드락에 빠진 상태는 서비스장애를 겪지만 프로세스는 계속 실행중이다) 따라서 쿠버네티스는 health probe 기법중 liveness probe와 readiness probe 이용해 어플리케이션이 서비스 가능한 상태인지 주기적으로 체크하여 어플리케이션의 문제를 자동으로 관리할 수 있다.

Liveness probe

Liveness probe는 어플리케이션의 생존 상태를 점검하는 것이다. 어플리케이션의 상태를 점검하는 방법은 아래와 같다.

  • HTTP probe: HTTP 요청을 통한 응답코드(200 ~ 399)를 확인
  • TCP probe: TCP 소켓 확인
  • Command probe: Exec 컨테이너 커널 네임스페이스에서 작성된 명령을 실행하고 성공적인 종료코드 0을 확인

개발자는 어플리케이션의 특성에 따라 적합한 방법을 선택해야한다. liveness probe는 상태 점검을 통과하지 못하면 컨테이너를 재시작한다. 재시작을해도 특정 상황에서 병목에 빠진다면 근본적인 문제해결이 아니다. 로깅을 이용해 시스템 에러를 로그로 남기고 알림을 트리거하고 분석해야할 것이다.

http liveness probe pod 예제

Readiness probe

점검에 실패하면 컨테이너를 재시작하는 liveness probe와는 다르게 readiness probe는 점검에 실패한 컨테이너에게 트래픽을 차단함으로서 컨테이너에게 과부하 걸린 일을 처리할 시간을 벌어준다. 점검 방법은 liveness probe와 똑같고 주기적으로 수행된다. 하지만 장애 시 컨테이너를 재시작하지 않고 새로운 트래픽을 차단한다는 점이 다르다.

Startup probe

컨테이너 내의 애플리케이션이 정상적으로 시작되었는지 점검한다. 만약 startup probe가 주어진 경우, 성공할 때 까지 다른 나머지 점검은 활성화 되지 않는다. 만약 startup probe가 실패하면, 컨테이너는 삭제되고, 컨테이너는 재시작 정책에 따라 처리된다. 컨테이너에 startup probe가 없는 경우, default 상태는 Success이다.

kubernetes volume_2

|

쿠버네티스 volume_2

퍼시스턴트 스토리지 사용

볼륨 사용 시 파드의 라이프 사이클과 별개로 데이터를 영구적으로 저장할 수 있는 방법은 퍼시스턴트 스토리지를 사용하는 것이다. 이를 사용하는 순서는 아래와 같으며 파드를 삭제하더라도 데이터는 디스크에 남게된다.

  • 쿠버네티스 클러스터가 있는 동일한 영역에 퍼시스턴트 디스크를 생성한다.
  • 파드 메니페스트의 볼륨 정의에 퍼시스턴트 디스크를 지정하고 이를 마운트한다.

디스크가 삭제되지 않는 한 데이터는 영구적으로 저장되지만 파드의 볼륨이 실제 기반 인프라를 참조하는 것은 쿠버네티스가 추구하는 바가 아니다. 이유는 파드 정의가 특정 클러스터에 밀접하게 연결되면 동일한 파드 정의를 다른 클러스터에서는 사용할 수 없기 때문이다.

PersistentVolume(PV)와 PersistentVolumeClaim(PVC)

기반 스토리지 기술과 파드를 분리하면 개발자는 애플리케이션에 필요한 퍼시트턴트 스토리지를 요청할 수 있고 기반 스토리지 기술에 대해서는 신경을 쓸 필요가 없다. 이를 위한 쿠버네티스 리소스가 PersistentVolume(이하 PV)와 PersistentVolumeClaim(이하 PVC)이다. 개발자는 파드에 스토리지 세부사항을 기재한 볼륨을 추가하는 대신 PVC를 통해 클러스터 관리자가 만든 기반 스토리지를 참조하는 PV를 바인딩하여 이용하면 된다.

퍼시스턴트볼륨 프로비저닝 순서

  • 퍼시스턴트 스토리지 유형을 설정하고 생성한다. (ops)
  • PV 디스크립터를 설정해 PV를 생성한다. (ops)
  • PVC를 생성한다. 이때 PVC에 정의된 필요한 볼륨을 찾아 자동으로 클레임에 바인딩한다. 여기는 어떤 볼륨(용량, 읽기쓰기모드 등)이 필요한지 작성되어 있다. (dev)
  • PVC를 참조하는 볼륨을 가진 파드를 정의하여 스토리지를 사용한다. (dev)

비록 PV, PVC를 생성하는 추가 절차가 필요하지만 개발자는 기저에 사용된 실제 스토리지 기술을 알 필요가 없다. 또한 동일한 파드와 PVC 매니페스트는 인프라스트럭처와 관련된 어떤 것도 참조하지 않으므로 다른 쿠버네티스 클러스터에서도 사용할 수 있다. 즉, PVC 매니페스트에 필요한 스토리지를 추상화하여 정의한 후 사용할 수 있는 것이다.

AWS EBS를 프로비저닝한 후 PV, PVC를 사용한 예제

PV의 동적프로비저닝

PVC가 PV를 바인딩하기 위해선 클러스터 관리자가 미리 스토리지를 프로비저닝 해줘야한다. 그렇다면 PVC가 PV를 바인딩할 때 마다 스토리지를 프로비저닝하고 PV를 생성해야하거나 또는 미리 스토리지를 여러개 프로비저닝 해둬야하는데 이는 비효율적이다. 이를 해결하기위해 쿠버네티스는 PV 프로비저너를 배포하고 개발자 선택 가능한 PV의 타입을 하나 이상의 StorageClass 오브젝트로 정의할 수 있다. PVC에서 StorageClass를 참조하면 프로비저너가 퍼시스턴트 스토리지를 생성하고 PV를 생성해 PVC에 바인딩한다. 쿠버네티스는 대부분의 클라우드 공급자의 프로비저너를 포함하므로 항상 프로비저너를 배포하지 않아도 된다. 단, 온프렘 환경에 배포된 쿠버네티스는 사용자 정의 프로비저너가 배포돼야 한다.

퍼시스턴트볼륨 동적프로비저닝 순서

  • 퍼시스턴트볼륨 프로비저너를 설정한다. (ops)
  • 하나 이상의 StorageClass를 생성한다. 이중 하나는 디폴트인데 이미 있을 수 있다. (ops)
  • StorageClass 중 하나를 참조하는 PVC를 생성한다. 만약 참조안하면 default StorageClass를 참조한다. 참조 안한다는 말은 빈 문자열(““)이라는 뜻이 아니다. 빈 문자열의 경우 PVC가 동적프로비저닝된 PV를 바인딩하지 않고 미리 프로비저닝된 PV를 바인딩한다. (dev)
  • 프로비저너는 실제 퍼시스턴트 스토리지를 프로비저닝하고 PV를 생성한 후 PVC에 바인딩한다.
  • PVC를 참조하는 볼륨과 파드를 생성한다. (dev)

AWS EKS의 경우 StorageClass 리소스를 생성하고 PVC에서 해당 StorageClass를 참조하게되면 자동으로 퍼시스턴트 스토리지와 PV가 생성된다.

AWS EBS볼륨(gp2 type)을 사용하여 PV를 동적프로비저닝하는 예제
AWS EBS 볼륨 유형