Fluentd Buffer는 어떻게 동작하는가?

Fluentd에서는 match 태그 안에 buffer를 사용해서 output 목적지로 이동하기 전 일정 시간 저장할 수 있게끔 합니다.

이벤트 전송 중에 fluentd 에이전트가 이슈로 인해 종료되거나, output 목적지로 정상적으로 로그가 전송되지 못하는 경우 로그 수집에 문제가 생길 수 있는데, 이러한 문제를 해결하기 위한 메커니즘입니다.

 

해당 글에서는 이전 글에 이어서 Fluentd 공식 문서에서 설명하고있는 Buffer의 동작 방식을 적어보고자 합니다.

 

fluentd plugin api overview

Fluentd의 각 플러그인의 진행 도식을 나타내면 다음과 같습니다.

전체적인 순서는 Input 플러그인으로부터 들어온 이벤트들이 router를 통해 각 output 플러그인으로 전달되는 순서입니다.


Output plugin에서의 Buffer 모드

마지막 Output 플러그인 내부에서 Buffering을 수행하는데, Buffering mode에는 크게 세 가지가 존재합니다.

  1. Non-Buffered : Data를 버퍼링하지않고, 즉시 결과를 내보냅니다.
  2. Synchronous Buffered : "Staged"된 버퍼 청크(Chunk -> 이벤트들의 집합)와 청크들의 큐를 가지고, 이 동작은 <buffer> 섹션에서 조절할 수 있습니다.
  3. Asynchronous Buffered : "Stage"와 큐를 가지나, output 플러그인이 chunk의 커밋을 동기적으로 수행하지 않고, 나중에 수행합니다.

Output 플러그인은 해당 모드를 전부 지원할 수도 있고, 한 가지 모드만 지원할 수도 있습니다.

Fluentd는 설정파일에 <buffer> 섹션이 존재하지 않는다면, 자동으로 적절한 모드를 선택합니다. 만약 <buffer>를 지원하지 않는 output 플러그인에 <buffer> 섹션을 추가하는 경우, Fluentd에서 에러를 발생시킵니다.

 

Fluentd v1에서의 Output 플러그인은, 설정파일을 통해서 buffer chunking의 key를 동적으로 제어할 수 있습니다.

사용자는 Buffer chunk의 key를 시간으로 결정할 수도, tag와 레코드 내의 특정 키를 사용할 수도 있습니다.

Output plugin의 Buffer는 이벤트를 chunk로 분리하고, chunk 안에 존재하는 이벤트들은 chunk key에 대해서 같은 값들을 가집니다.

Output plugin의 Buffer는 Buffer plugin으로 사용할 수 있으며, 다른 Buffer plugin이 각 Output plugin에 대해 선택되어 사용될 수 있습니다.


Buffer가 어떻게 동작하는가?

Buffer는 근본적으로 Chunk 들의 집합입니다.

Chunk는 단일 blob으로 연결된 이벤트들의 집합입니다. 각 chunk는 파일 혹은 연속적인 메모리 블록 형태로 하나씩 관리됩니다.

Chunk의 라이프사이클

Buffer 플러그인은 Chunk를 경량의 컨테이너로 사용하고, 인풋 소스로부터 오는 이벤트들을 해당 chunk에 채우게 됩니다.

만약 Chunk가 가득 차게 되는 경우, 해당 chunk를 목적지로 이동시킵니다.

 

내부적으로, buffer 플러그인은 chunk를 저장하는 두 개의 분리된 장소를 가집니다.

  1. Stage : chunk가 event로 채워지는 공간
  2. Queue : chunk가 전송 전에 대기하는 공간

새롭게 생성된 모든 chunk들은 stage로부터 시작하고, queue로 전달됩니다. 그리고 이어서 목적지로 전송되게 됩니다.

Buffering/Retrying 속성값

Buffer plugin에 사용할 수 있는 속성 값들을 알아봅니다.

Flush 관련 속성값

  • flush_mode
    • default: default
    • default, lazy, interval, immediate 중 하나를 가질 수 있다.
    • interval의 경우 flush_interval 마다 flush를 수행함
    • immediate의 경우 이벤트가 도착한 즉시 flush를 수행함
  • flush_interval
    • buffer chunk가 flush 되는 간격
    • default: 60
  • flush_thread_count
    • buffer를 flush 하는 스레드의 수
    • default: 1
  • flush_thread_interval
    • flush 스레드에서 buffer를 flush check 사이의 휴면 시간(초)
    • default: 1
  • flush_thread_burst_interval
    • 많은 buffer chunk가 queue에 추가되었을 경우, buffer flush를 잠시 sleep 하는 시간(초)
    • default: 1
  • delayed_commit_timeout
    • 추후 플러그인이 커밋할 buffer chunk들에 대한 timeout(초)
    • default: 60
  • slow_flush_log_threshold
    • chunk flush 성능 체크에 대한 threshold
    • default: 20.0(초)
    • 속성 값 타입은 float 형태임을 주의한다.
    • chunk flush가 해당 threshold보다 더 오래걸리는 경우, 해당 메세지를 발생시킴
      • 2016-12-19 12:00:00 +0000 [warn]: buffer flush took longer time than slow_flush_log_threshold: elapsed_time=15.0031226690043695 slow_flush_log_threshold=10.0 plugin_id="foo"
  • overflow_action
    • queue가 가득차는 경우 buffer 동작을 제어
    • 지원 mode
      • default: throw_exception
        • BufferOverflowError 예외를 인풋 플러그인에 발생시킨다.
        • BufferOverflowError의 처리방식은 인풋 플러그인의 처리방식에 따른다
      • block
        • buffer full 이슈가 끝날때가지 input plugin의 thread를 정지시킨다.
        • in_tail 플러그인에서 메인으로 사용된다.
        • BufferOverflowError를 피하기 위해 block 옵션을 추천하지는 않는다.
        • BufferOverflowError를 해결하기 위해 목적지에서의 설정을 개선하거나, secondary 목적지를 설정하여서 overflow된 event가 다른 백업 목적지로 적재되도록 설정한다.
        • BufferOverflowError가 자주 발생한다면, 목적지의 용량이 트래픽을 제대로 처리하지못한다는 것을 의미한다.
      • drop_oldest_chunk
        • 오래된 chunk들을 drop 시킨다.
        • 모니터링 시스템에서 유용하다.

Retry 관련 속성값

  • 하단의 chunk가 쓰기 작업이 실패한다면, queue에 남아있다가 Fluentd는 몇 초 동안 기다린 이후에 재시도를 수행한다.( retry_wait )
  •  retry 제한이 꺼져있지 않고(retry_forever - false로 설정되어있을 경우), retry 횟수가 특정 횟수 이상을 초과한다면(retry_max_times), queue에 존재하는 모든 chunk들은 버려진다.
  • retry wait time은 각 시간마다 2배씩 증가하며(1.0s, 2.0s -> 4.0s..) retry_max_interval에 도달할 때까지 증가한다.
  • 만약 queue 길이가 특정 제한을 초과하게 되면(queue_limit_length), 새로운 이벤트들이 들어오지 못하게 된다.

 

  • retry_type
    • buffer flush를 위해 다음 재시도까지 기다리는 방식
    • default: exponential_backoff
    • periodic
  • retry_forever
    • true라면, plugin은 retry_timeout과 retry_max_times 옵션을 무시하고, 계속해서 재시도를 수행한다.
    • default: false
  • retry_timeout
    • Buffer chunk들을 버리기 전 실패하는 동안 flush를 재시도하는 최대 시간
    • default: 72h
  • retry_max_times
    • 실패하는 동안 flush를 다시 시도하는 최대 횟수.
    • retry_timeout이 기본값인 경우, exponential_backoff 와 함께 17로 설정된다.
    • default: nil
  • retry_secondary_threshold
    • 실패하는 동안 secondary를 사용하도록 전환하는 retry_timeout의 비율
    • default: 0.8
  • retry_wait
    • flush를 다시 시도하기 전 대기할 시간(초) 또는 exponentail_backoff의 상수값
    • default: 1
  • retry_exponential_backoff_base
    • 재시도에 대한 exponential_backoff에 사용되는 기본 숫자 값
    • default: 2
  • retry_max_interval
    • 실패하는 동안 재시도 사이의 exponential_backoff에 대한 최대 간격(초)
    • default: nil
  • retry_randomize
    • true라면, output plugin은 burst 재시도를 수행하지 않도록 랜덤 간격 이후에 재시도를 수행한다.
    • default: true
 

Buffer의 Retry 과정 자세히 살펴보기

Chunk의 목적지 전달이 실패하는 이유에는 여러 가지가 있을 수 있습니다. ( 네트워크가 중단되거나, 목적지 노드의 용량보다 트래픽이 초과하는 경우 등.. )

해당 공통 실패들을 잘 처리하기 위해서, buffer 플러그인은 내장된 retry 메커니즘을 가지고 있습니다.

Exponential Backoff 동작 방식

기본으로 Fluentd는 각 재시도 시의 간격을 지수 형태로 증가시킵니다.

초기의 interval이 1로 설정되어있고, exponentail 지수값이 2로 설정되어있다면, 각 재시도는 다음과 같은 시점에 수행됩니다.

1 2   4       8               16
x-x---x-------x---------------x-------------------------
│ │   │       │               └─  4th retry (wait = 8s)
│ │   │       └─────────────────  3th retry (wait = 4s)
│ │   └─────────────────────────  2th retry (wait = 2s)
│ └─────────────────────────────  1th retry (wait = 1s)
└───────────────────────────────  FAIL

1초 실패

2초에 재시도(1초 기다림)

4초에 재시도(2초 기다림)

8초에 재시도(4초 기다림)...

 

Fluentd는 위의 알고리즘을 몇 가지 속성 값으로 조정합니다.

  • 대기하는 interval은, 기본적으로 랜덤입니다. 즉 Fluentd는 대기하는 interval을 0.875와 1.125 사이에 있는 랜덤 값을 선택해서 곱하여 설정합니다. 이러한 설정은 retry_randomize를 false로 설정하여 끌 수 있습니다.
  • 대기하는 interval에 특정 제한을 걸어 유지시킬 수 있습니다. 예를 들어 retry_max_interval을 5로 설정한 경우, 대기시간은 5초를 넘기지 않습니다. 위의 4번째 재시도의 경우에서 8초가 아닌 5초로 설정됩니다.
  • 만약 exponential_backoff를 사용하고 싶지 않다면, retry_type을 periodic으로 설정합니다.

연속되는 실패를 처리하는 방법

Fluentd는 다음과 같은 상태에 따라서 실패한 chunk를 전송하려는 시도를 중단합니다.

  1. 재시도 횟수가 retry_max_times를 초과한 경우( default: none )
  2. 첫 번째 재시도 이후 retry_timeout 만큼의 시간보다 초과한 경우 ( default: 72h )

위의 경우에 queue에 존재하는 모든 이벤트들은 버려집니다.


복구 불능한 에러를 처리하는 방법

chunk에 들어오는 메시지 자체에 이상이 있는 경우가 존재할 수도 있습니다. 이러한 경우, 재시도를 하여 복구를 하는 의미가 없습니다.

 

v1.2.0의 Fluentd부터 이러한 복구 불능의 에러가 발생하는 경우 chunk 생성을 즉시 중단하고, secondary 혹은 backup 디렉터리로 이동시킵니다. backup 디렉터리의 위치는 <system>의 root_dir 속성 값에 의해 결정됩니다.

chunk를 백업할 필요가 없다면,  <buffer> 섹션에서 disable_chunk_backup(v1.2.6부터 사용 가능)을 활성화하면 됩니다.

아래와 같은 에러들을 복구 불능 상태라고 판단합니다.

Exception 해당 에러가 발생하는 일반적인 원인
Fluent::UnrecoverableError Output plugin에서 해당 예외를 발생시켜 복구불능의 에러에 대한 추가 재시도를 막을 수 있다.
TypeError 이벤트의 대상 필드에 예기치 않은 타입이 존재할 때
ArgumentError plugin이 library를 잘못사용하였을 때 발생
NoMethodError Event와 설정값이 서로 매치되지않을 때 발생

Fluentd의 공식문서를 통해 Output 플러그인에서 Buffer의 원리와 각종 설정값들에 대해 공부해보았습니다.

해석이 잘못되거나, 내용에 오류가 있는 경우 댓글 부탁드립니다.

 

참조