Skip to content
hello tis
Go back

[26.09.15] Fluent Bit 설정과 로그 유실 대응 고민하기

어제는 Splunk 통계 수집을 일원화하는 PoC를 진행했습니다. API 직접 전송과 EFS·EC2·UF를 거치는 방식이 섞여 있었고, 인프라팀에서도 관리하기 어렵다는 피드백을 줬습니다.

오늘은 여러 서비스에 함께 적용할 Fluent Bit 설정을 정리했습니다. 기존 CloudWatch 수집을 유지하면서, 전송이 실패해도 로그를 보관할 방법이 필요했습니다.

Fluent Bit 설정 구조

설정은 .conf 파일에 작성합니다. 로그 처리 순서에 따라 INPUT, FILTER, OUTPUT으로 나누고, 전체 동작은 SERVICE에서 정합니다.

Fluent Bit의 INPUT, FILTER, OUTPUT과 CloudWatch·Splunk·S3 전송 경로

설정역할
[SERVICE]전송 주기, 종료 대기 시간, 파서 파일 설정
[INPUT]로그 입력
[FILTER]로그 파싱·변환·분기
[OUTPUT]목적지로 로그 전송

Name에는 플러그인을, Match에는 처리할 태그의 조건을 적습니다. logtype은 로그 본문의 필드라서, 필터가 이 값을 읽어 태그를 바꾸면 출력이 해당 태그를 선택해 전송합니다.

필터는 작성한 순서대로 적용됩니다. 파일을 나눌 때도 @INCLUDE에 읽을 파일을 순서대로 적습니다.

SERVICE — 전체 동작 설정

전송 주기와 종료 대기 시간을 정하고, JSON 파서 파일을 연결했습니다.

[SERVICE]
    Flush        1
    Grace        30
    Log_Level    info
    Parsers_File /fluent-bit/etc/altools-parsers.conffluent-bit.conf

Flush는 전송 주기라서, 실제 저장까지 걸리는 시간과는 다를 수 있습니다. 파서는 아래처럼 별도 파일에 정의하고 필터에서 altools_json이라는 이름으로 사용합니다.

[PARSER]
    Name   altools_json
    Format jsonaltools-parsers.conf

설정을 바꾼 뒤에는 Fluent Bit을 재시작해 새 파일을 읽게 하고, 시작 로그와 전송 결과를 확인합니다.

공통 설정과 서비스별 설정

모든 설정을 한 파일에 넣으면 서비스별 조건이 계속 늘어날 것 같았습니다. 수집·분기 규칙은 함께 관리하고, 목적지와 버퍼 한도는 서비스별로 나누려 합니다.

단계공통 설정서비스별 설정
input수집 방식버퍼 한도
filterJSON 파싱, logtype 분기, 기본 경로필드 변환
output목적지별 설정 형식목적지, 인증 정보, 재시도 횟수, 버퍼 한도

input — 로그 입력과 버퍼

forward 입력은 Unix 소켓으로 로그를 받을 수 있습니다. 태그는 로그를 전송하는 쪽에서 지정합니다.

[INPUT]
    Name      forward
    unix_path /var/run/fluent.sockfluent-bit.conf

기존 CloudWatch 수집 유지하기

기존 서비스의 로그 조회에 영향이 없도록 Splunk로 전송할 로그만 분기합니다. 나머지는 CloudWatch로 전송해, logtype이 없거나 분류하지 못한 로그도 남깁니다.

filter — logtype으로 분기

필터는 JSON 파싱 → logtype 분기 → 필드 정리 순서로 적용합니다. 아래는 파싱한 로그에서 logtype=splunk인 로그의 태그를 바꾸는 설정입니다.

[FILTER]
    Name         rewrite_tag
    Match        app.*
    Rule         $logtype ^splunk$ altools.splunk false
    Emitter_Name altools_splunk_emitterfluent-bit.conf

분기 예시 — 같은 입력 태그에서 목적지 나누기

Rule은 아래 순서로 작성합니다.

Rule  확인할필드  일치조건  새태그  기존로그유지여부

Splunk 분기 규칙에 적용하면 다음과 같습니다.

Rule $logtype ^splunk$ altools.splunk false

태그는 JSON 본문에 추가되는 필드가 아닙니다. rewrite_tag는 본문을 유지한 채 새 태그를 붙여 로그를 다시 입력하고, 출력은 Match로 이 태그를 선택합니다.

같은 app.demo 태그로 입력한 로그 중 logtype=splunk인 로그만 태그가 바뀝니다.

logtype이 splunk인 로그는 Splunk로, system이거나 logtype이 없는 로그는 CloudWatch로 전송

새 태그 altools.splunkMatch app.*에 걸리지 않아 같은 분기를 반복하지 않습니다. falsetrue로 바꾸면 기존 태그도 남아서, 아래 설정에서는 CloudWatch와 Splunk 양쪽으로 전송됩니다.

output — 목적지와 재시도

Splunk와 CloudWatch 출력에서 태그와 재시도 설정만 발췌했습니다. Retry_Limit 5는 재시도할 수 있는 전송 실패에 최대 5회 재시도하는 설정입니다.

[OUTPUT]
    Name        splunk
    Match       altools.splunk
    Retry_Limit 5

[OUTPUT]
    Name        cloudwatch_logs
    Match       app.*
    Retry_Limit 5fluent-bit.conf

경로를 나눠도 Splunk 장애가 CloudWatch 수집에 영향을 줄 수 있습니다. rewrite_tag의 emitter 버퍼가 가득 차면 파이프라인이 멈출 수 있기 때문입니다. Splunk 전송을 막은 상태에서 CloudWatch 수집이 계속되는지 확인할 예정입니다.

로그 유실에 대비하기

전송 실패 외에도 설정 오류나 강제 종료로 로그가 유실될 수 있어, 상황별로 확인할 내용을 나눴습니다.

상황확인할 내용
파싱·분기 오류기본 경로에 원본이 남는지, 태그에 맞는 출력이 있는지
목적지 장애·네트워크 단절재시도 횟수와 버퍼 용량으로 견딜 수 있는 시간
버퍼·디스크가 가득 참로그 삭제 시점과 알림 기준
정상 종료·강제 종료종료 대기 시간과 재시작 후 로그 복구 여부

재시도와 버퍼

Retry_Limit에 지정한 횟수만큼 재시도해도 실패하면 해당 로그는 기본적으로 삭제됩니다. 재시도를 무제한으로 설정해도 버퍼가 가득 찰 수 있으므로, 두 값을 함께 정해야 합니다.

파일 버퍼에 저장한 로그는 파일이 남아 있어야 재시작 후 복구할 수 있습니다. 일반 출력의 storage.total_limit_size에 도달하면 오래된 청크부터 삭제됩니다.

S3 출력

VoC 대응용 인입 로그를 보관하는 S3 출력은 자체 버퍼를 사용합니다. 버퍼 한도는 store_dir_limit_size로 설정하고, 저장 결과는 일반 출력의 성공 지표 대신 S3 객체로 확인해야 합니다.

ECS 자원과 버퍼 용량

Fluent Bit은 같은 태스크의 애플리케이션과 자원을 나눠 씁니다. 버퍼를 늘릴 때는 ECS 자원 설정과 태스크 전체 사용량도 봐야 합니다.

설정의미
memoryReservation메모리 예약량
memory메모리 사용량의 상한
cpuLinux 컨테이너 간 CPU 배분에 쓰는 값

Mem_Buf_Limit은 입력 버퍼의 메모리 한도입니다. emitter와 출력 처리에도 메모리가 필요하므로, 이 값을 컨테이너 메모리 상한과 같게 잡을 수는 없습니다.

버퍼 용량은 전송이 멈춰 있는 동안 쌓일 로그 양으로 계산한 뒤 실제 사용량에 맞춰 조정하려고 합니다.

필요한 버퍼 용량 ≈ 초당 로그 크기 × 장애 시간
버퍼 용량과 복구 시간 예시

초당 1 MB가 입력되고 10분간 전송이 멈추면 약 600 MB가 쌓입니다. 복구 후 초당 3 MB를 전송해도 1 MB가 계속 입력되므로, 쌓인 로그를 모두 전송하는 데 약 5분이 더 걸립니다.

그래서 장애 중 버퍼 사용량뿐 아니라 복구 후 전송 속도도 측정하려고 합니다.

로컬에서 버퍼 유실 재현하기

9월 16일에는 전송 실패와 강제 종료를 직접 재현했습니다. 수신 서버의 응답을 바꿀 수 있도록 로컬 HTTP 서버를 사용했습니다.

로그 생성기에서 Fluent Bit의 HTTP 입력, 버퍼, HTTP 출력을 거쳐 수신 서버로 전송하는 실험 구조

AWS for Fluent Bit 2.34.3에 포함된 Fluent Bit 1.9.10을 사용하고, Docker 컨테이너를 CPU 0.5개·메모리 약 134.2 MB로 제한했습니다. 로그마다 고유한 event_id약 4 KB 문자열을 넣어, 100건씩 0.3초 간격으로 총 1,000건을 입력했습니다.

측정 방법

정상 전송과 각 장애 조건을 따로 실행하고, 입력 ID와 수신 ID를 비교했습니다.

  1. 장애 실험에서는 수신 서버가 503을 반환하도록 설정
  2. 입력 요청의 201 응답과 Fluent Bit 입력 지표로 1,000건 입력 확인
  3. 조건에 따라 재시도 한도·버퍼 한도 도달을 확인하거나 Fluent Bit을 SIGKILL로 강제 종료
  4. 수신 서버를 200으로 복구하고 전송 대기 큐가 비워지는지 확인
  5. 확인용 로그를 새로 입력해 수신 여부를 확인한 뒤, 원래 1,000개 ID와 수신 ID 비교

확인용 로그는 집계에서 제외했습니다. 각 조건은 3회 반복했습니다.

측정 결과

세 번 모두 수신·유실 건수가 같았고, 중복은 0건이었습니다.

조건설정과 실행 방법수신유실
정상 전송계속 200 응답1,000건0건
재시도 한도 도달파일 버퍼 16M, Retry_Limit 2, 503 유지0건1,000건
버퍼 한도 초과파일 버퍼 1M, 무제한 재시도, 입력 계속200건800건
메모리에만 보관한 상태에서 강제 종료전송 실패 중 Fluent Bit을 SIGKILL로 종료한 뒤 재시작0건1,000건
버퍼 파일을 남겨 둔 상태에서 강제 종료Fluent Bit을 SIGKILL로 종료한 뒤 같은 버퍼 폴더를 연결해 재시작1,000건0건

버퍼 한도 초과 후 남은 로그

버퍼 한도를 넘자 오래된 800건이 삭제되고 마지막 200건만 수신됐습니다.

입력 1,000건 중 ID 00000799의 800건은 유실되고, ID 08000999의 200건만 수신된 결과

실행 로그에도 약 0.42 MB 크기의 청크를 삭제한 기록이 남았습니다.

remove chunk ... with size 415031 bytes ...
total_limit_size=1000000

dropped_records는 0이었지만 ID를 비교하니 800건이 유실됐습니다. 이번 실험에서는 이 지표만으로 유실을 알 수 없었습니다.

강제 종료와 재시도 실패 후 남은 로그

수신 서버가 503을 반환하게 해 전송하지 못한 로그 1,000건을 버퍼에 쌓았습니다. 이후 Fluent Bit을 강제 종료한 경우와 계속 실행한 채 재시도 한도에 도달한 경우를 비교했습니다.

재현된 상황별 대응 방법

AWS 공식 가이드를 참고해 ECS·FireLens에서 적용할 대응을 정리했습니다.

상황한계대응
재시도 한도 도달 → 1,000건 유실버퍼가 차면 로그가 유실됩니다.Retry_Limit False무제한 재시도를 검토하고, 재시도 간격도 조정합니다.
버퍼 한도 초과 → 오래된 800건 유실한도가 차면 오래된 청크부터 삭제됩니다.로그 발생량과 장애 시간으로 storage.total_limit_size를 정하고, 한도에 도달하기 전에 알림을 받습니다.
메모리 버퍼에서 강제 종료 → 1,000건 유실Fluent Bit에 도착하기 전 로그는 보호하지 못합니다.입력에 storage.type filesystem을 적용하고, 출력·필터의 메모리 사용량도 확인합니다.
파일 버퍼 유지 후 재시작 → 1,000건 복구파일 버퍼가 삭제되면 복구할 수 없습니다.파일 버퍼를 볼륨에 저장하고, 재시작 때 같은 경로를 읽습니다.

유실된 로그를 원본에서 다시 전송할 방법도 필요합니다.

애플리케이션보다 FireLens가 나중에 종료되는 순서를 유지하고, Grace보다 stopTimeout을 길게 두어 남은 로그를 전송해야 합니다. Fargate의 stopTimeout 상한은 120초입니다. 종료할 때도 목적지 장애가 계속되면 전송을 마치지 못할 수 있습니다.

FireLens 종료 신호를 기준으로 Grace 90초와 stopTimeout 120초를 표시한 시간 막대

애플리케이션이 마지막 로그를 기록하는 동안 FireLens는 수집을 계속합니다. 애플리케이션이 종료되면 FireLens가 종료 신호를 받고 남은 로그를 전송합니다.

Fargate 임시 볼륨은 태스크 종료 후 삭제됩니다.

ECS 태스크 종료 요청 후 애플리케이션이 먼저 종료되고 FireLens가 남은 로그를 전송하는 흐름

종료 단계별 설정 위치와 옵션을 정리했습니다.

단계설정 위치옵션과 역할
종료 신호 전달컨테이너 이미지STOPSIGNAL: 기본값은 SIGTERM입니다.
애플리케이션 종료 처리애플리케이션 코드·프레임워크SIGTERM 처리: 진행 중인 작업을 마치고 마지막 로그를 기록합니다.
애플리케이션 종료 대기태스크 정의의 애플리케이션 컨테이너stopTimeout: 종료 신호 후 강제 종료까지 기다리는 시간입니다.
애플리케이션 종료 → FireLens 종료 신호태스크 정의의 애플리케이션 컨테이너기본 의존성이 이 순서를 보장합니다. 직접 설정할 때는 dependsOn에 FireLens 컨테이너 이름과 condition: START를 지정합니다. 시작 순서의 역순으로 종료됩니다.
FireLens의 남은 로그 전송Fluent Bit 설정의 [SERVICE]Grace: 종료 전 대기 시간입니다. 그림에서는 90초입니다.
FireLens 종료 대기태스크 정의의 FireLens 컨테이너stopTimeout: Grace보다 길게 설정합니다. 그림에서는 120초이며, 시간이 지나도 실행 중이면 SIGKILL로 종료됩니다.

Share this post:

Previous Post
사내 버셀 구축 - 2단계 사내 적용 (진행중)
Next Post
[26.09.14] 30시간 soak test와 Splunk 통계 수집 일원화