들어가기 앞서
현재 운영중인 서비스는 Logging 과 Monitoring 이 모두 구축되어 있다. 모니터링의 경우 Jennifer k8s, Jennifer apm, cloudwatch 를 통합하여 사용하고 있다. 로그의 경우 Fluent bit 이 수집하여 OpenSearch 와 Cloudwatch Logs 에 저장하여 관리하는 중이다.
fluent bit 은 운영중인 EKS 클러스터의 노드그룹에 Deamonset 으로 배포되어 있다. 문제는 특정 시점부터 fluent bit이 배포된 노드들의 메모리와 디스크 사용률이 우상향하는 현상이 발생하였다. 임시방편으로 Fluent bit 을 배포하는 데몬셋을 rollout 하여 메모리와 디스크를 다시 정상화시켰으나 동일 현상이 지속되었다. fluent bit 컨테이너의 에러 로그에는 "Failed to flush chunk" 메세지를 확인하였다.
Fluent bit 작동 원리
해당 문제를 해결하기 위해 Fluent bit 의 간단한 작동 원리를 파악하고자 한다.
Fluent Bit 이란?
고성능 로그 프로세서 및 로그 전달자로, CNCF에 의해 호스팅되는 오픈 소스 프로젝트이다. 경량화되어 있고 로그 데이터를 수집하고, 처리하고, 파이프라인을 통해 다양한 대상으로 전달하는 데 사용된다. Fluent Bit는 컨테이너화된 환경, 클라우드, 온-프레미스 시스템에서 로그 관리를 위해 널리 사용된다.
대략적인 아키텍처

Fluent Bit 파이프라인

fluent bit 은 쉽게 위와 같은 파이프라인을 가지고 log를 특정한 목적지로 보내주게 된다. Fluent Bit의 환경 설정은 ConfigMap 을 통해 관리되며, 각 섹션을 통해 동작 과정을 정의할 수 있다.
INPUT
로그 데이터의 입력 소스를 정의한다.
- Name: 입력 플러그인의 이름을 지정한다 (tail은 파일의 내용을 실시간으로 읽는다).
- Path: 로그 파일의 경로를 지정한다.
- Parser: 해당 로그 파일을 파싱하기 위해 사용할 PARSER 이름을 지정한다.
- Tag: 입력 데이터에 할당할 태그를 지정한다.
[INPUT]
Name tail
Path /var/log/nginx/access.log
Parser nginx_access_parser
Tag nginx_access
[INPUT]
Name tail
Path /var/log/nginx/error.log
Parser nginx_error_parser
Tag nginx_error
FILTER
입력 데이터를 처리하거나 변형하는 데 사용된다.
- Name: 필터 플러그인의 이름을 지정한다(grep, modify 등).
- grep : 정규 표현식 또는 특정 조건을 사용하여 로그 데이터를 필터링한다.
- Regex : 정규 표현식을 사용하여 로그 데이터 중 특정 패턴에 일치하는 데이터만을 선택한다.
- Exclude : 정규 표현식에 일치하는 데이터를 제외하고 처리한다.
- modify : 그 데이터에 필드를 추가, 삭제 또는 수정하는 작업을 수행한다.
- Add: 새로운 키-값 쌍을 로그 데이터에 추가한다.
- Remove: 지정한 키를 로그 데이터에서 삭제한다.
- Rename: 로그 데이터의 키 이름을 변경한다.
- grep : 정규 표현식 또는 특정 조건을 사용하여 로그 데이터를 필터링한다.
- Match: 이 필터가 적용될 태그 패턴을 지정한다.
- Regex: grep 필터를 사용할 경우, 로그 메시지를 필터링하기 위한 정규 표현식을 지정한다.
- Add: modify 필터를 사용할 경우, 로그 메시지에 추가할 필드를 지정한다.
[FILTER]
Name grep
Match nginx_access
Regex log .*somaz.*
[FILTER]
Name modify
Match nginx_access
Add log_type nginx_access
[FILTER]
Name modify
Match nginx_error
Add log_type nginx_error
OUTPUT
처리된 로그 데이터의 최종 목적지를 정의한다.
- Name: 출력 플러그인의 이름을 지정한다(stdout, loki 등).
- Match: 이 출력이 적용될 태그 패턴을 지정한다.
- Host, Port, URI: 로그 데이터를 전송할 대상 서버의 주소와 포트, URI를 지정한다.
- tls: TLS/SSL을 사용하여 데이터 전송을 암호화할지 여부를 지정한다.
- Labels: 로그 데이터와 함께 전송할 레이블을 지정한다.
[OUTPUT]
Name stdout
Match *
[OUTPUT]
Name loki
Match *
Host loki.somaz.link
Port 443
URI /loki/api/v1/push
tls On
Labels job=fluent-bit, log_type=$log_type
PARSER
로그 데이터의 포맷을 해석하는 규칙을 정의한다.
- Name: 파서의 이름을 지정한다.
- Format: 파싱할 로그 포맷의 유형을 지정한다(regex 등).
- Regex: 로그 메시지를 파싱하기 위한 정규 표현식을 지정한다.
- Time_Key, Time_Format: 로그 메시지에서 시간을 추출하기 위한 키와 시간 포맷을 지정한다.
[PARSER]
Name nginx_access_parser
Format regex
Regex ^(?<remote_addr>[^ ]*) - (?<remote_user>[^ ]*) \\[(?<time>[^\\]]*)\\] "(?<method>\\S+) (?<request>[^ ]*) (?<http_protocol>[^"]*)" (?<status>[^ ]*) (?<body_bytes_sent>[^ ]*) "(?<http_referer>[^"]*)" "(?<http_user_agent>[^"]*)" (?<request_length>[^ ]*) (?<request_time>[^ ]*) \\[(?<upstream_name>[^\\]]*)\\] \\[(?<upstream_addr>[^\\]]*)\\] (?<upstream_response_length>[^ ]*) (?<upstream_response_time>[^ ]*) (?<upstream_status>[^ ]*) (?<request_id>[^ ]*)$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name nginx_error_parser
Format regex
Regex ^(?<time>\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[(?<log_level>\\w+)\\] (?<process_info>\\d+#\\d+): (?<message>.*)$
Time_Key time
Time_Format %Y/%m/%d %H:%M:%S
Fluent bit 에러 로그 분석
fluent bit을 로그수집기로 사용하게 되면 /var/log/containers 아래의 로그파일을 읽어와서 지정된 output으로 http request를 보낸다. 별다른 이슈가 없다면 cpu와 memory 위주로 사용하게 되며 cpu memory가 부족할것 같다면 fluent bit 데몬셋의 cpu, memory limit을 조절해줄 수 있다. (노드의 리소스가 넉넉하다면)
그러나 cpu, memory limit을 넉넉하게 해놨는데도 아래와 같은 로그가 떨어지는 경우가 발생한다. (아래 로그는 fluent bit의 output이 kinesis firehose 로 지정되어 있는 상황이지만 에러의 원인과 조치방법만 알고있다면 output의 종류는 크게 문제가 되지 않는다.)
[2022/07/21 05:44:21] [error] [output:kinesis_firehose:kinesis_firehose.11] Failed to send log records to XX
[2022/07/21 05:44:21] [error] [output:kinesis_firehose:kinesis_firehose.11] Failed to send log records
[2022/07/21 05:44:21] [error] [output:kinesis_firehose:kinesis_firehose.11] Failed to send records
[2022/07/21 05:44:21] [ warn] [engine] failed to flush chunk '1-1658382261.194783957.flb', retry in 6 seconds: task_id=3, input=tail.11 > output=kinesis_firehose.11 (out_id=11)
[2022/07/21 05:44:27] [ info] [engine] flush chunk '1-1658382261.194783957.flb' succeeded at retry 1: task_id=3,
input=tail.11 > output=kinesis_firehose.11 (out_id=11)
fluent bit은 output으로 http request를 보냈을때 output 서버의 특정 문제(일반적으로 limit을 초과하는 경우, 필자의 경우 OpenSearch 의 스토리지 여유가 없었음)로 데이터 전송에 실패하게되면 먼저 전송에 실패했다고 에러로그를 찍는다. 실패했다고 해당 데이터가 유실된건 아니다. fluent bit은 실패한 케이스는 retry를 하도록 개발되어 있다. 실패하게 되면 chunk파일을 만들어 디스크에 저장한 후 새로운 스레드를 만들어 retry를 하게 된다.
limit이 초과하게 된 상황에서 계속해서 output이 발생하게 되면 retry 정보를 chunk 파일로 저장하게 되므로 디스크의 남은 용량이 점점 줄어들게 되며 retry를 위한 스레드도 계속해서 생성되므로 메모리도 폭발하고 retry request가 쌓이다보니 엄청난 네트워크 요청이 쌓이게 되고 최악의 경우 클러스터 전체에 무리를 주는 결과까지 초래할 수 있다. 기존 chunk파일을 무시하고자 하는 마음에 pod을 재시작하게 되더라도 fluent bit은 chunk파일이 있다면 retry를 위한 스레드를 생성하게 되므로 해결되지 않는다.
이럴경우 해당 노드에 접근해 강제로 chunk파일을 삭제해주면(필요시 백업) fluent bit은 정상적으로 돌아오며 output 서버의 limit을 증대시켜주면 fluent bit은 정상적으로 동작하게 된다.
Chunk 파일의 위치는 어디에?
기본적으로 로그는 /var/log/containers 에 저장된다. 또한, Fluent Bit 의 환경 설정 중 storage.type 의 기본 값은 memory 이다. 즉, 기본적으로 Fluent Bit은 로그를 디스크(/var/log/containers)에서 읽지만, 이를 메모리에 저장한 후 처리하는 방식이다.
- kubernetes 환경에서 컨테이너 로그는 cri 가 /var/log/containers 에 저장한다.
- 이 파일들은 보통 심볼릭 링크이고, 실제 로그는 /var/log/pods 또는 /var/log/docker 아래에 위치할 수 있다.
- Fluent Bit의 tail 플러그인이 이를 읽는다.
- storage.type 이 memory 일 경우 읽은 데이터를 메모리에 저장하고 처리
- storage.type 이 filesystem 의 경우 읽은 데이터를 디스크에 저장하여 처리
- 로그를 필터링하고 Output 으로 보낸다.
- storage.type 이 filesystem 이라면 Fluent Bit이 종료되더라도 재시작 시 이전에 처리하지 못한 로그를 다시 전송한다.
그렇다면 Chunk 파일의 위치는 어디에 있을까? 이는 storage type 설정에 따라 다르다. Storage.type 이 memory 일 경우 해당 파일은 메모리를 사용하여 저장한다. 메모리에 저장된 데이터는 Fluent Bit이 종료되면 사라진다. Storage.type 이 filesystem 인 경우 storage .path 설정 값에 따라 Chunk 파일이 저장된다. 디스크에 저장된 Chunk 는 Fluent Bit이 재시작되더라도 유지된다.
storage.path 의 경로는 기본적으로 컨테이너 내부 경로이다. 즉, Fluent Bit 파드가 재시작되면 해당 데이터는 유실된다. 이를 방지하기 위해 HostPath 또는 Persistent Volume을 사용하여 해당 경로를 마운트하면 데이터 유실을 방지할 수 있다.
Fluent bit 동작 과정 예시 (Failed to flush chunk, storage.type=filesystem)
로그 수집 단계
- Fluent Bit이 로그를 수집하는 경로는 주로 /var/log/containers/입니다. 여기서 컨테이너 로그 파일을 수집한다.
- Fluent Bit은 tail Input 플러그인을 사용하여 이 경로에 있는 로그 파일을 모니터링한다. tail 플러그인은 로그 파일의 끝부분을 지속적으로 읽어들이며, 새로운 로그 항목을 찾아서 처리한다.
버퍼링 및 저장 단계
Fluent Bit은 메모리에서 데이터를 버퍼링하고, 필요에 따라 디스크에 저장한다. 이 과정은 storage.type=filesystem일 때 다음과 같은 방식으로 진행된다.
1. 메모리 버퍼링 (초기 단계)
- Fluent Bit은 먼저 메모리에서 버퍼링을 시도한다. 이때 설정된 Mem_Buf_Limit 값에 따라 사용할 수 있는 메모리 크기가 제한된다. 예: Mem_Buf_Limit 50MB라면, 최대 50MB까지 메모리에서 데이터를 처리한다.
- Fluent Bit은 메모리 내에서 데이터를 잠시 보관하고 있으며, 이 데이터를 일정 주기마다 처리하여 출력으로 전송한다.
- 버퍼 크기: Buffer_Chunk_Size 및 Buffer_Max_Size와 같은 설정 값에 따라 한 번에 메모리로 버퍼링할 수 있는 로그 데이터의 양을 설정할 수 있다.
2. 디스크로의 저장 (메모리 버퍼 초과 시)
- Mem_Buf_Limit를 초과하거나, 메모리가 부족하면 Fluent Bit은 디스크에 저장하기 시작한다. 이때, storage.type=filesystem을 설정했기 때문에 디스크에 저장되는 데이터를 관리하게 된다.
- 디스크에 저장되는 파일 경로는 storage.path에 의해 결정된다. 이 경로는 Fluent Bit 컨테이너 내부 경로가 될 수 있다.
- Fluent Bit은 디스크에 파일로 데이터를 저장한다. 데이터가 디스크에 저장될 때는 chunk 단위로 나누어 저장된다. 각 chunk는 일정 크기로 구분되며, 시간이 지나면서 점차적으로 디스크에 저장된다.
출력 전송 (Output Plugin 처리)
이제, Fluent Bit은 수집한 로그 데이터를 출력 서버 (예: OpenSearch, Elasticsearch 등)로 전송해야 한다.
1. Output 플러그인 설정
Fluent Bit은 로그를 전송할 Output 플러그인(elasticsearch, opensearch, kafka 등)을 사용한다.
2. 버퍼 전송 과정
- Fluent Bit은 디스크에 저장된 로그를 메모리로 불러들인 후 Output 플러그인으로 전송한다. 이때 버퍼 크기나 전송 속도에 따라 데이터를 전송하는 속도 및 빈도가 결정된다.
- Failed to flush chunk와 같은 오류가 발생하면, Fluent Bit은 재시도 메커니즘을 사용하여 실패한 데이터를 다시 전송하려 시도한다. 재시도 횟수는 net.max_retries 설정을 통해 조정할 수 있다.
- 데이터를 성공적으로 전송한 후에는 해당 디스크에 저장된 chunk 파일을 삭제하거나 delete_after 주기마다 정리한다.
추가사항 - 또 다른 원인
메모리가 부족하여 Fluent Bit이 버퍼를 처리하지 못할 경우에도 Failed to flush chunk 오류가 발생할 수 있다. Mem_Buf_Limit 을 초과했을 경우가 그러하다. 즉, 메모리 부족으로 인해 Fluent Bit이 더 이상 데이터를 처리할 수 없다. 해당 값을 충분히 설정해야 한다.
다만, storage.type 이 filesystem 인 경우, Fluent Bit은 데이터를 메모리에 먼저 버퍼링한 후, 메모리 버퍼가 가득 차면 디스크로 옮겨서 저장한다. 따라서, Mem_Buf_Limit 은 메모리에서 처리할 수 있는 최대 데이터 크기를 제한한다. 디스크 공간 을 관리하기 위해 storage.backlog.mem_limit 또한 적절히 설정할 필요가 있다.
혹은, 출력 버퍼가 꽉 찬 경우이다. 출력 플러그인에서 설정한 버퍼 크기(net.buffer_size 및 Buffer_Size)가 너무 작거나, 데이터를 전송하는 속도가 너무 느린 경우에 발생할 수 있다. 버퍼 크기를 적절히 증가시켜 줄 필요가 있다. 혹은, flush_interval 및 net.max_retries 를 조정하여 전송 속도 제어를 조정할 필요가 있다.
'Logging & Monitoring > fluent bit 운영 특이사항' 카테고리의 다른 글
| [Fluent-bit] Exclude_Path 설정 (0) | 2025.10.25 |
|---|---|
| [Fluent-bit] OpenSearch 월별 인덱스 설정 (0) | 2025.04.15 |
| [Fluent-bit] Multi-line Parsing (멀티 라인 파싱) (0) | 2025.04.14 |
| [Fluent-bit] cannot increase buffer 에러 해결 (0) | 2025.02.11 |