로깅 아키텍처

애플리케이션 로그는 애플리케이션 내부에서 발생하는 상황을 이해하는 데 도움이 된다. 로그는 문제를 디버깅하고 클러스터 활동을 모니터링하는 데 특히 유용하다. 대부분의 최신 애플리케이션에는 일종의 로깅 메커니즘이 있다. 따라서, 대부분의 컨테이너 엔진은 일종의 로깅을 지원하도록 설계되었다. 컨테이너화된 애플리케이션에 가장 쉽고 가장 널리 사용되는 로깅 방법은 표준 출력과 표준 에러 스트림에 작성하는 것이다.

그러나, 일반적으로 컨테이너 엔진이나 런타임에서 제공하는 기본 기능은 완전한 로깅 솔루션으로 충분하지 않다. 예를 들어, 컨테이너가 크래시되거나, 파드가 축출되거나, 노드가 종료된 경우에도 여전히 애플리케이션의 로그에 접근하려고 한다. 따라서, 로그는 노드, 파드 또는 컨테이너와는 독립적으로 별도의 스토리지와 라이프사이클을 가져야 한다. 이 개념을 클러스터-레벨-로깅 이라고 한다. 클러스터-레벨 로깅은 로그를 저장하고, 분석하고, 쿼리하기 위해 별도의 백엔드가 필요하다. 쿠버네티스는 로그 데이터를 위한 네이티브 스토리지 솔루션을 제공하지 않지만, 기존의 많은 로깅 솔루션을 쿠버네티스 클러스터에 통합할 수 있다.

클러스터-레벨 로깅 아키텍처는 로깅 백엔드가 클러스터 내부 또는 외부에 존재한다고 가정하여 설명한다. 클러스터-레벨 로깅에 관심이 없는 경우에도, 노드에서 로그를 저장하고 처리하는 방법에 대한 설명이 여전히 유용할 수 있다.

쿠버네티스의 기본 로깅

이 섹션에서는, 쿠버네티스에서 표준 출력 스트림으로 데이터를 출력하는 기본 로깅의 예시를 볼 수 있다. 이 데모에서는 일부 텍스트를 초당 한 번씩 표준 출력에 쓰는 컨테이너와 함께 파드 명세를 사용한다.

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']

이 파드를 실행하려면, 다음의 명령을 사용한다.

kubectl apply -f https://k8s.io/examples/debug/counter-pod.yaml

출력은 다음과 같다.

pod/counter created

로그를 가져오려면, 다음과 같이 kubectl logs 명령을 사용한다.

kubectl logs counter

출력은 다음과 같다.

0: Mon Jan  1 00:00:00 UTC 2001
1: Mon Jan  1 00:00:01 UTC 2001
2: Mon Jan  1 00:00:02 UTC 2001
...

컨테이너가 크래시된 경우, kubectl logs--previous 플래그를 사용해서 컨테이너의 이전 인스턴스에 대한 로그를 검색할 수 있다. 파드에 여러 컨테이너가 있는 경우, 명령에 컨테이너 이름을 추가하여 접근하려는 컨테이너 로그를 지정해야 한다. 자세한 내용은 kubectl logs 문서를 참조한다.

노드 레벨에서의 로깅

노드 레벨 로깅

컨테이너화된 애플리케이션이 stdout(표준 출력)stderr(표준 에러) 에 쓰는 모든 것은 컨테이너 엔진에 의해 어딘가에서 처리와 리디렉션 된다. 예를 들어, 도커 컨테이너 엔진은 이 두 스트림을 로깅 드라이버로 리디렉션 한다. 이 드라이버는 쿠버네티스에서 json 형식의 파일에 작성하도록 구성된다.

참고: 도커 json 로깅 드라이버는 각 라인을 별도의 메시지로 취급한다. 도커 로깅 드라이버를 사용하는 경우, 멀티-라인 메시지를 직접 지원하지 않는다. 로깅 에이전트 레벨 이상에서 멀티-라인 메시지를 처리해야 한다.

기본적으로, 컨테이너가 다시 시작되면, kubelet은 종료된 컨테이너 하나를 로그와 함께 유지한다. 파드가 노드에서 축출되면, 해당하는 모든 컨테이너도 로그와 함께 축출된다.

노드-레벨 로깅에서 중요한 고려 사항은 로그 로테이션을 구현하여, 로그가 노드에서 사용 가능한 모든 스토리지를 사용하지 않도록 하는 것이다. 쿠버네티스는 현재 로그 로테이션에 대한 의무는 없지만, 디플로이먼트 도구로 이를 해결하기 위한 솔루션을 설정해야 한다. 예를 들어, kube-up.sh 스크립트에 의해 배포된 쿠버네티스 클러스터에는, 매시간 실행되도록 구성된 logrotate 도구가 있다. 예를 들어, 도커의 log-opt 를 사용하여 애플리케이션의 로그를 자동으로 로테이션을 하도록 컨테이너 런타임을 설정할 수도 있다. kube-up.sh 스크립트에서, 후자의 접근 방식은 GCP의 COS 이미지에 사용되며, 전자의 접근 방식은 다른 환경에서 사용된다. 두 경우 모두, 기본적으로 로그 파일이 10MB를 초과하면 로테이션이 되도록 구성된다.

예를 들어, kube-up.sh 가 해당 스크립트에서 GCP의 COS 이미지 로깅을 설정하는 방법에 대한 자세한 정보를 찾을 수 있다.

기본 로깅 예제에서와 같이 kubectl logs를 실행하면, 노드의 kubelet이 요청을 처리하고 로그 파일에서 직접 읽은 다음, 응답의 내용을 반환한다.

참고: 현재, 일부 외부 시스템에서 로테이션을 수행한 경우, kubectl logs 를 통해 최신 로그 파일의 내용만 사용할 수 있다. 예를 들어, 10MB 파일이 있으면, logrotate 가 로테이션을 수행하고 두 개의 파일이 생긴다(크기가 10MB인 파일 하나와 비어있는 파일). 그 후 kubectl logs 는 빈 응답을 반환한다.

시스템 컴포넌트 로그

시스템 컴포넌트에는 컨테이너에서 실행되는 것과 컨테이너에서 실행되지 않는 두 가지 유형이 있다. 예를 들면 다음과 같다.

  • 쿠버네티스 스케줄러와 kube-proxy는 컨테이너에서 실행된다.
  • Kubelet과 컨테이너 런타임(예: 도커)은 컨테이너에서 실행되지 않는다.

systemd를 사용하는 시스템에서, kubelet과 컨테이너 런타임은 journald에 작성한다. systemd를 사용하지 않으면, /var/log 디렉터리의 .log 파일에 작성한다. 컨테이너 내부의 시스템 컴포넌트는 기본 로깅 메커니즘을 무시하고, 항상 /var/log 디렉터리에 기록한다. 그것은 klog 로깅 라이브러리를 사용한다. 로깅에 대한 개발 문서에서 해당 컴포넌트의 로깅 심각도(severity)에 대한 규칙을 찾을 수 있다.

컨테이너 로그와 마찬가지로, /var/log 디렉터리의 시스템 컴포넌트 로그를 로테이트해야 한다. kube-up.sh 스크립트로 구축한 쿠버네티스 클러스터에서 로그는 매일 또는 크기가 100MB를 초과하면 logrotate 도구에 의해 로테이트가 되도록 구성된다.

클러스터 레벨 로깅 아키텍처

쿠버네티스는 클러스터-레벨 로깅을 위한 네이티브 솔루션을 제공하지 않지만, 고려해야 할 몇 가지 일반적인 접근 방법을 고려할 수 있다. 여기 몇 가지 옵션이 있다.

  • 모든 노드에서 실행되는 노드-레벨 로깅 에이전트를 사용한다.
  • 애플리케이션 파드에 로깅을 위한 전용 사이드카 컨테이너를 포함한다.
  • 애플리케이션 내에서 로그를 백엔드로 직접 푸시한다.

노드 로깅 에이전트 사용

노드 레벨 로깅 에이전트 사용

각 노드에 노드-레벨 로깅 에이전트 를 포함시켜 클러스터-레벨 로깅을 구현할 수 있다. 로깅 에이전트는 로그를 노출하거나 로그를 백엔드로 푸시하는 전용 도구이다. 일반적으로, 로깅 에이전트는 해당 노드의 모든 애플리케이션 컨테이너에서 로그 파일이 있는 디렉터리에 접근할 수 있는 컨테이너이다.

로깅 에이전트는 모든 노드에서 실행해야 하므로, 이를 데몬셋 레플리카, 매니페스트 파드 또는 노드의 전용 네이티브 프로세스로 구현하는 것이 일반적이다. 그러나 후자의 두 가지 접근법은 더 이상 사용되지 않으며 절대 권장하지 않는다.

쿠버네티스 클러스터는 노드-레벨 로깅 에이전트를 사용하는 것이 가장 일반적이며 권장되는 방법으로, 이는 노드별 하나의 에이전트만 생성하며, 노드에서 실행되는 애플리케이션을 변경할 필요가 없기 때문이다. 그러나, 노드-레벨 로깅은 애플리케이션의 표준 출력과 표준 에러에 대해서만 작동한다 .

쿠버네티스는 로깅 에이전트를 지정하지 않지만, 쿠버네티스 릴리스에는 두 가지 선택적인 로깅 에이전트(Google 클라우드 플랫폼과 함께 사용하기 위한 스택드라이버(Stackdriver) 로깅엘라스틱서치(Elasticsearch))가 패키지로 함께 제공된다. 전용 문서에서 자세한 정보와 지침을 찾을 수 있다. 두 가지 다 사용자 정의 구성이 된 fluentd를 에이전트로써 노드에서 사용한다.

로깅 에이전트와 함께 사이드카 컨테이너 사용

다음 중 한 가지 방법으로 사이드카 컨테이너를 사용할 수 있다.

  • 사이드카 컨테이너는 애플리케이션 로그를 자체 stdout 으로 스트리밍한다.
  • 사이드카 컨테이너는 로깅 에이전트를 실행하며, 애플리케이션 컨테이너에서 로그를 가져오도록 구성된다.

사이드카 컨테이너 스트리밍

스트리밍 컨테이너가 있는 사이드카 컨테이너

사이드카 컨테이너를 자체 stdoutstderr 스트림으로 스트리밍하면, 각 노드에서 이미 실행 중인 kubelet과 로깅 에이전트를 활용할 수 있다. 사이드카 컨테이너는 파일, 소켓 또는 journald에서 로그를 읽는다. 각 개별 사이드카 컨테이너는 자체 stdout 또는 stderr 스트림에 로그를 출력한다.

이 방법을 사용하면 애플리케이션의 다른 부분에서 여러 로그 스트림을 분리할 수 ​​있고, 이 중 일부는 stdout 또는 stderr 에 작성하기 위한 지원이 부족할 수 있다. 로그를 리디렉션하는 로직은 미미하기 때문에, 큰 오버헤드가 거의 없다. 또한, stdoutstderr 가 kubelet에서 처리되므로, kubectl logs 와 같은 빌트인 도구를 사용할 수 있다.

다음의 예를 고려해보자. 파드는 단일 컨테이너를 실행하고, 컨테이너는 서로 다른 두 가지 형식을 사용하여, 서로 다른 두 개의 로그 파일에 기록한다. 파드에 대한 구성 파일은 다음과 같다.

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

두 컴포넌트를 컨테이너의 stdout 스트림으로 리디렉션한 경우에도, 동일한 로그 스트림에 서로 다른 형식의 로그 항목을 갖는 것은 알아보기 힘들다. 대신, 두 개의 사이드카 컨테이너를 도입할 수 있다. 각 사이드카 컨테이너는 공유 볼륨에서 특정 로그 파일을 테일(tail)한 다음 로그를 자체 stdout 스트림으로 리디렉션할 수 있다.

다음은 사이드카 컨테이너가 두 개인 파드에 대한 구성 파일이다.

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-1
    image: busybox
    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-2
    image: busybox
    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/2.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

이제 이 파드를 실행하면, 다음의 명령을 실행하여 각 로그 스트림에 개별적으로 접근할 수 있다.

kubectl logs counter count-log-1
0: Mon Jan  1 00:00:00 UTC 2001
1: Mon Jan  1 00:00:01 UTC 2001
2: Mon Jan  1 00:00:02 UTC 2001
...
kubectl logs counter count-log-2
Mon Jan  1 00:00:00 UTC 2001 INFO 0
Mon Jan  1 00:00:01 UTC 2001 INFO 1
Mon Jan  1 00:00:02 UTC 2001 INFO 2
...

클러스터에 설치된 노드-레벨 에이전트는 추가 구성없이 자동으로 해당 로그 스트림을 선택한다. 원한다면, 소스 컨테이너에 따라 로그 라인을 파싱(parse)하도록 에이전트를 구성할 수 있다.

참고로, CPU 및 메모리 사용량이 낮음에도 불구하고(cpu에 대한 몇 밀리코어의 요구와 메모리에 대한 몇 메가바이트의 요구), 로그를 파일에 기록한 다음 stdout 으로 스트리밍하면 디스크 사용량은 두 배가 될 수 있다. 단일 파일에 쓰는 애플리케이션이 있는 경우, 일반적으로 스트리밍 사이드카 컨테이너 방식을 구현하는 대신 /dev/stdout 을 대상으로 설정하는 것이 더 낫다.

사이드카 컨테이너를 사용하여 애플리케이션 자체에서 로테이션할 수 없는 로그 파일을 로테이션할 수도 있다. 이 방법의 예로는 정기적으로 logrotate를 실행하는 작은 컨테이너를 두는 것이다. 그러나, stdoutstderr 을 직접 사용하고 로테이션과 유지 정책을 kubelet에 두는 것이 권장된다.

로깅 에이전트가 있는 사이드카 컨테이너

로깅 에이전트가 있는 사이드카 컨테이너

노드-레벨 로깅 에이전트가 상황에 맞게 충분히 유연하지 않은 경우, 애플리케이션과 함께 실행하도록 특별히 구성된 별도의 로깅 에이전트를 사용하여 사이드카 컨테이너를 생성할 수 있다.

참고: 사이드카 컨테이너에서 로깅 에이전트를 사용하면 상당한 리소스 소비로 이어질 수 있다. 게다가, kubelet에 의해 제어되지 않기 때문에, kubectl logs 명령을 사용하여 해당 로그에 접근할 수 없다.

예를 들어, 로깅 에이전트로 fluentd를 사용하는 스택드라이버를 사용할 수 있다. 여기에 이 방법을 구현하는 데 사용할 수 있는 두 가지 구성 파일이 있다. 첫 번째 파일에는 fluentd를 구성하기 위한 컨피그맵이 포함되어 있다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  fluentd.conf: |
    <source>
      type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag count.format1
    </source>

    <source>
      type tail
      format none
      path /var/log/2.log
      pos_file /var/log/2.log.pos
      tag count.format2
    </source>

    <match **>
      type google_cloud
    </match>
참고: fluentd의 구성은 이 문서의 범위를 벗어난다. fluentd를 구성하는 것에 대한 자세한 내용은, 공식 fluentd 문서를 참고한다.

두 번째 파일은 fluentd가 실행되는 사이드카 컨테이너가 있는 파드를 설명한다. 파드는 fluentd가 구성 데이터를 가져올 수 있는 볼륨을 마운트한다.

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-agent
    image: k8s.gcr.io/fluentd-gcp:1.30
    env:
    - name: FLUENTD_ARGS
      value: -c /etc/fluentd-config/fluentd.conf
    volumeMounts:
    - name: varlog
      mountPath: /var/log
    - name: config-volume
      mountPath: /etc/fluentd-config
  volumes:
  - name: varlog
    emptyDir: {}
  - name: config-volume
    configMap:
      name: fluentd-config

얼마 후 스택드라이버 인터페이스에서 로그 메시지를 찾을 수 있다.

이것은 단지 예시일 뿐이며 실제로 애플리케이션 컨테이너 내의 모든 소스에서 읽은 fluentd를 로깅 에이전트로 대체할 수 있다는 것을 기억한다.

애플리케이션에서 직접 로그 노출

애플리케이션에서 직접 로그 노출

모든 애플리케이션에서 직접 로그를 노출하거나 푸시하여 클러스터-레벨 로깅을 구현할 수 있다. 그러나, 이러한 로깅 메커니즘의 구현은 쿠버네티스의 범위를 벗어난다.

최종 수정 September 11, 2020 at 11:21 PM PST: First Korean l10n work for release-1.19 (986be3462)