... visits
서버로 들어오는 요청이나 DB에서 실행되는 SQL, 각종 배치 스크립트가 실행되면서 남기는 로그들을 기본적인 파일 형태로만 남기고, 주기적으로 로테이팅 되기만 하고 버려지고 있었다.
가끔 서비스에 문제가 생겼거나 디버깅 목적으로 로그를 찾아볼 때는 모든 서버를 돌아다니면서 find & grep 해야 하는 번거로움이 있었다. (또 그렇게 찾아서 나온 결과가 엄청 많을 때는 터미널을 가득 채웠기 때문에 거북목을 하고서 눈이 빠지라고 모니터를 쳐다봐야 하는 헬게이트가 열리기도 했다)
이렇게 불편하고 활용도가 떨어지는 부분을 보완하기 위해 ELK 구성을 도입했고, 각 노드에서는 Logstash 가 로그 파일을 Tail 해서 적절한 가공 후에 Elasticsearch로 적재하게 했다. 하지만 어느 날 서비스 이용자가 몰리면서 서버가 갑자기 바빠지더니 덩달아 Logstash도 바빠졌다. 시스템 자원이 부족한 상황에서 Logstash가 잡아먹는 자원이 매우 아까운 상황이었다.
힘들었던 고비를 넘기고 나니까 전체적인 로그 수집 프로세스를 재정비해야겠다는 생각이 들었다. 현재 준비된 ELK 구성에서 가장 신속하게 개선할 수 있는 방향으로는 일단 로그 수집을 중앙화해서 수집 현황을 모니터링하고, 이슈 감지와 대응에 대한 비용을 줄인다. 그리고 각 서버에 배치된 Logstash를 보다 가벼운 대체품으로 교체하는 것으로 판단했다.
Fluentd를 조사하면서 위와 같은 구성이 가능할 것이라 확신했고, 현재는 안정적인 구성을 마친 상태이다. 이 글을 시작으로 앞으로 이어지는 글은 이 프로젝트를 진행하면서 조사했던 내용과 삽질 그리고 노하우에 대한 소개가 될 것이다.
먼저 Fluentd에 대해 알아보자!
‘이런 개념이구나.. 이런 기능이 있구나..‘하면서 가볍게 살펴보는 것을 추천한다.
Fluentd
는 로그(데이터) 수집기(collector)다. 보통 로그를 수집하는 데 사용하지만, 다양한 데이터 소스(HTTP, TCP 등)로부터 데이터를 받아올 수 있다.
Fluentd로 전달된 데이터는 tag, time, record(JSON) 로 구성된 이벤트로 처리되며, 원하는 형태로 가공되어 다양한 목적지(Elasticsearch, S3, HDFS 등)로 전달될 수 있다.
Fluentd는 C와 Ruby로 개발되었다. 더 적은 메모리를 사용해야 하는 환경에서는 Fluentd forwarder의 경량화 버전인 Fluentd-Bit 와 함께 사용할 수 있다. (최초 로그 수집 구조를 설계 할 때는 각 서버에 Fluent-Bit를 배치하려고 했었으나 HA 구성이 안 되는 이유로 모두 Fluentd로 구성했다. Fluent-Bit의 Load Balancing/Failover 기능에 대한 이슈는 여기 에서 확인할 수 있다)
데이터 유실을 막기 위해 메모리와 파일 기반의 버퍼(Buffer) 시스템을 갖고 있으며, Failover 를 위한 HA(High Availability) 구성도 가능하다.
이 글은 Fluentd v1.0을 기준으로 작성되었다.
Fluentd가 읽어들인 데이터는 tag, time, record 로 구성된 이벤트(Event) 로 처리된다.
Fluentd의 특징 중에 가장 핵심은 태그(Tag) 이다. 태그는 이벤트가 흘러가면서 적절한 Filter, Parser 그리고 Output 플러그인으로 이동할 수 있는 기준이 된다.
아래 예시의 경우 input_tail 플러그인으로 전달된 이벤트에는 dev.sample라는 태그가 붙게 된다.
# tag 사용 예시
<source>
@type tail
tag dev.sample
path /var/log/sample.log
</source>
<match dev.sample>
@type stdout
<match>
먼저 Fluentd를 어떻게 쓸 수 있는지 알아보는 것이 이해에 도움이 될 것 같다.
* 참고: 공식 문서 - Use Cases
Fluentd는 원하는 기능들을 플러그인 방식으로 설정 파일에 추가함으로써 사용할 수 있다.
전체적인 동작 흐름은 Input -> Filter -> Buffer -> Output 단계로 동작하며, 세부적으로 7개의 플러그인(Input, Parser, Filter, Fomatter, Storage, Buffer, Output)을 목적대로 자유롭게 활용할 수 있다.
Fluentd를 설치하고, 작성한 설정 파일을 환경변수 FLUENT_CONF에 명시하거나 -c 실행 파라미터 에 전달하면 된다.
# export FLUENT_CONF="/etc/fluent/fluent.conf"
fluentd -c /etc/fluent/fluent.conf
(이 글에서는 fleuntd 설치 방법과 실행에 대한 자세한 내용은 다루지 않는다. 하지만 이어지는 다음 글에서는 docker로 구성한 fluentd 실행 환경과 플러그인 설치, 그리고 실행과 배포에 대한 내용을 다룰 예정이다.)
설정 파일을 작성하기 위한 기본적인 문법은 공식 문서에서 가볍게 읽어보는 것을 추천한다. 자세히 보기
fluentd로 할 수 있는 것들에 대해 알아보자.
이 글에서는 모든 플러그인을 다루진 않고, 필자가 로그 수집 프로세스를 구성하면서 사용했던 플러그인 위주로 설명한다.
다양한 데이터 소스로부터 로그 데이터를 받거나 가져온다.
in_tail
대표적인 in_tail 플러그인은 파일을
tail
해서 데이터를 읽어 들인다.
단 파일의 시작부터 읽지 않으며, 로테이팅 되어 새로운 파일이 생성된 경우에만 처음부터 읽게 된다.
그리고 해당 파일의 inode를 추적하기 때문에 pos_file 파라미터를 사용할 경우 fluentd가 재실행 되었을 때 파일의 마지막에 읽은 부분부터 다시 처리하게 된다.
* 참고: 공식 문서 - in tail
<source>
@type tail
path /var/log/nginx/access.log
pos_file /var/log/fluent/nginx-access.log.pos
tag nginx.access
<parse>
@type nginx
</parse>
</source>
in_forward
forward라는 프로토콜을 사용해 TCP로 데이터를 수신할 수 있다. 보통 다른 Fluentd 노드로부터 데이터를 전달받기 위해 사용한다.
forward로 전달되는 데이터는 JSON이나 Messagepack 형식으로 되어 있다.
fluentd 인스턴스를
멀티 프로세스
로 실행 했을때는 각각의 프로세스가 동일한 forward 포트를 공유하게 된다.
* 참고: 공식 문서 - in_forward
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
전달 받은 데이터를 파싱하기 위해 <parse> 섹션을 정의해서 사용한다.
<parse> 섹션은 Input 플러그인(<source>), Output 플러그인(<match>), Filter 플러그인(<filter>) 안에서 정의하며, @type 파라미터로 사용할 Parser 플러그인 이름을 지정한다.
기본적으로 내장된 Parser 플러그인은 regexp, apache2, nginx, syslog, csv, tsv, json, none 등이 있다.
* 참고: 공식 문서 - Config: Parse Section
parser_regexp
정규표현식으로 데이터를 파싱할 수 있는 Parser이다.
정규표현식 패턴은 expression 파라미터에 명시하며, 반드시 최소 1개 이상의
캡쳐 그룹
과 time 캡쳐 그룹이 필요하다.
time 캡쳐 그룹의 키 이름은 time_key 파라미터로 변경할 수 있다.
시간과 관련된 추가 파라미터로는 시간 포맷을 지정할 수 있는 time_format과 타임존을 설정하는 timezone 파리미터가 있다.
<parse>
@type regexp
expression /^(?<remote_addr>[^ ]+) "(?<http_x_forwarded_for>([^ ]+(, )?)+)" (?<http_x_client>[^ ]+) \[(?<timestamp>(0?[1-9]|[12][0-9]|3[01])/[a-zA-Z]+/\d\d\d\d:(00|0[0-9]|1[0-9]|2[0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9]) \+[0-9]+)\] "(?<request_method>\S+) (?<request_uri>[^"]+) (?<server_protocol>[^"]+)" (?<status_code>\d{3}) (?<body_byte_sent>\d+) "(?<http_referer>[^"]*)" "(?<http_user_agent>.+)" (?<request_time>[^ ]+)$/
time_key timestamp
time_format %d/%b/%Y:%H:%M:%S %z
timezone +09:00
</parse>
parser_none
데이터를 행마다 새로운 필드 1개로 다시 담을 때 사용한다. 데이터를 필터/가공하지 않고, 다음 플러그인이나 다른 Fluentd 노드로 전달할 때 사용될 수 있다.
<parse>
@type none
message_key log # JSON 형식의 "log" Key로 데이터가 담긴다
</parse>
filter_grep
명시된 필드값에 정규표현식과 매칭되는 값만 필터링한다.
<filter dev.postgresql.*>
@type grep
<regexp>
key log
pattern ^statement: .+$
</regexp>
</filter>
filter_parser
이벤트 레코드를 파싱해서 파싱된 결과를 다시 이벤트에 적용한다. filter_parser 플러그인은 데이터를 파싱하기 위해 Parser 플러그인을 함께 사용한다.
<filter dev.django-rest-api.*>
@type parser
key_name log
reserve_data true
<parse>
@type regexp
expression /^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<timestamp>\d\d\d\d-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01]) (00|0[0-9]|1[0-9]|2[0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])(,[0-9]{3}))\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)$/
time_key timestamp
time_format %Y-%m-%d%H:%M:%S,%L
timezone +09:00
</parse>
</filter>
filter_record_transformer
이벤트 레코드에 새로운 컬럼을 추가하거나 수정, 삭제할때 사용하는 플러그인이다.
<filter dev.django-rest-api.*>
@type record_transformer
<record>
worker_name fluentd_multi
tag ${tag}
remove_keys sample
</record>
</filter>
Output 플러그인은 <match> 섹션에 정의하며, v1.0부터 Buffering과 Flushing에 대한 설정을 <match> 섹션안에 <buffer> 서브 섹션으로 정의한다.
Buffering과 Flushing에 대해서는 3가지 모드를 제공한다.
Output 플러그인은 buffer chunk에 key를 지정할 수 있으며, key와 동일한 이름을 갖는 이벤트를 분리해서 chunk에 담도록 설정할 수 있다. Buffer 설정에 대한 내용은 아래에서 자세히 다룬다.
* 참고: 공식 문서 - Output Plugins
output_stdout
이벤트를 표준출력(stdout)으로 내보낸다. Fluentd 설정을 만들기 초반에 디버깅용으로 자주 사용한다.
<match **>
@type stdout
</match>
output_forward
다른 Fluentd 노드로 이벤트를 전달할때 사용하며, 반드시 1개 이상의 <server> 섹션을 포함해야 한다.
이 플러그인은 Load-Balancing, Fail-Over, Replication 기능을 설정하기 위한 파라미터들을 포함하고 있다.
<match **>
@type forward
<server>
name another.fluentd1
host 127.0.0.1
port 24224
weight 60
</server>
<server>
name another.fluentd2
host 127.0.0.1
port 24225
weight 40
</server>
</match>
send_timeout
과 동일heartbeat_interval
보다 반드시 커야 한다.output_elasticsearch
Elascticsearch로 이벤트 레코드를 전송한다. 레코드 전송은 Bulk 단위로 이뤄지기 때문에 최초 전달받은 이벤트가 즉시 ES로 전송되지 않는다.
output_elasticsearch
플러그인은 fluentd에 기본으로 포함되어 있지 않기 때문에 추가 설치가 필요하다.
<match **>
@type elasticsearch
hosts 127.0.0.1:9200,127.0.0.1:9201
index_name django-rest-api
type_name django-rest-api
include_timestamp true
time_key timestamp
include_tag_key true
tag_key fluentd_tag
</match>
※ ES, Index 관련 파라미터 설정
logstash_format
파라미터를 true 로 설정하면 index_namm
파라미터는 무시되며, logstash 에서 사용하는 형태로 logstash-2018.04.04 형식으로 자동 부여된다.logstash_prefix
, logstash_prefix_separator
, logstash_dateformat
옵션을 지정하면 logstash 스타일 Index 이름 형식을 변경할 수 있다. (ex. #{logstash_prefix}-#{formated_date})
자세히 보기logstash_format
파라미터를 사용 했을때 추가되는 @timestamp 필드만 별도로 추가※ Index, Type 이름을 동적으로 생성하기
fluentd 태그명에 있는 문자열들을 조합해서 Index, Type 이름이 동적으로 생성되도록 할 수 있다.
이 기능을 사용하기 위해서는 플러그인 이름을 기존 @type elasticsearch 에서 @type elasticsearch_dynamic 으로 변경해야 한다.
<match dev.django-rest-api.*>
@type elasticsearch_dynamic
hosts 127.0.0.1:9200,127.0.0.1:9201
index_name ${tag_parts[0]}-${tag_parts[1]}-${Time.at(time).getutc.strftime(@logstash_dateformat)} # eg. dev-django-rest-api-2018.03.08
type_name ${tag_parts[1]} # eg. django-rest-api
</match>
buffer 플러그인은 Output 플러그인에서 사용된다.
buffer에는 chunk 들의 집합을 담고 있으며, 각 chunk 에는 이벤트들의 묶음이 저장된 하나의
Blob
파일이다.
이 chunk 가 가득차게 되었을때 다음 목적지로 전달된다.
buffer는 내부적으로는 이벤트가 담긴 chunk를 저장하는 “stage” 영역과 전달되기 전 대기하는 chunk 를 보관하는 “queue” 로 나뉜다.
※ chunk 전달 실패에 대한 재시도 파라미터
chunk 를 목적지로 전달할 수 없는 상황이 발생했을때 fluentd는 기본적으로 재시도 횟수를 배수로 증가 시킨다.
exponential_backoff
, periodic
으로 변경하면 주기적으로 재시도 하도록 할 수 있다.retry_timeout
이 초과하게 되면 queue
에 있는 모든 chunk들은 제거된다.위 파라미터들은 모두 기본값이 설정되어 있기 때문에 모두 설정할 필요는 없다. 자세히 보기
지금까지 살펴본 Fluentd의 개념과 기능들을 활용해 다음 글에서는 간단한 샘플 구성을 작성해보고, 직접 실행하는 가이드를 정리할 예정이다.