우선 fluentd의 가장 큰 특징은 log를 time/tag/record형식 의 semi-structured 형식으로 저장한다는 것이다.
시간은 event가 발생한 시간으로 event를 fluentd로 넘겨줄 때 시간을 같이 넘겨주지 않으면, fluentd에서 받은 시간을 기록하게 된다.
tag는 이벤트를 만들 때 넘기게 되어 있는데, fluentd에서 사용하는 값이다. 이에 대해서는 config를 어떻게 하는지 설명하면서 설명하도록 하겠다.
record는 사용자가 저장하려고 했던 값들로 json 형식의 key/value pair로 저장된다.
semi-structured라고 해도 record가 json 형식으로 저장되기 때문에 원하는 형식대로 저장할 수 있다.
Use case
fluentd는 config파일을 바꾸는 것만으로도 여러 머신들 간의 설정을 쉽게 바꿀 수 있다.
위의 그림은 backup server를 두는 방식이다. fluentd는 내부적으로 버퍼를 가지고 있어 일정 시간 서버에 문제가 생기는 것에 대응할 수 있게 되어 있지만, 기본적으로 로그를 저장하기 위해서 쓰이고, 버퍼가 버틸 수 있는 것 이상으로 서버의 문제가 복구되지 않는다면 로그를 버리도록 설계되어 있다.
그럴 때를 대비하여 backup server를 둘 수 있다. backup server는 보통 때에는 사용하지 않지만, main server에 로그를 남길 수 없을 때 기록을 남긴다.
fluentd는 크게 plugin을 붙일 수 있는 3부분과 plugin을 이용하는 engine으로 구성되어 있다. engine은 config를 읽어서 사용할 plugin을 결정하고, 설정하는 역할을 한다. 외부의 input을 받고 output으로 내보내는 역할은 전부 plugin에서 하도록 되어 있다. 그렇기 때문에 fluentd의 동작을 이해하려면 각 plugin들이 어떻게 동작하는지 아는 것이 중요하다.
Input plugin
Input plugin은 외부로부터 이벤트를 받아오거나 외부의 파일을 읽어서 이벤트를 만들어 주는 역할을 한다.
fluentd 이외의 다른 log aggregator들이 가장 취약한 부분이 이곳이다. 반대로 말하면 fluentd의 최고 장점이 되는 부분이기도 하다.
scribe같은 경우는 event를 만들어 보내주는 부분을 완전히 새로 작성해야 한다. flume은 이미 구현된 몇 개의 방법을 이용해서 통신하거나, 새로운 plugin을 작성해야 하는데, flume은 plugin을 만들기 쉽게 되어 있지 않다.
반면에 fluentd의 경우는 이미 많은 plugin들이 만들어져 있어서 필요한 대부분의 plugin을 찾을 수 있고, 찾지 못하더라도 쉽게 plugin을 만들 수 있다.
Buffer plugin
buffer가 해주는 중요한 기능이 2가지 있다.
그 중 하나는 output을 효율적으로 내보내는 것이다. log aggregator는 실시간으로 로그를 모아주지만, 모은 로그를 바로 바로 output으로 보낼 이유는 없다. 그래서 fluentd를 비롯한 대부분의 aggregator는 서버에서 일정량의 로그를 모았다가 처리하도록 해준다.
fluentd에서는 이 단위를 chunk라고 부른다. chunk는 log의 tag 별로 분류되어 저장된다.
output plugin은 우선 chunk를 queue에 집어넣지 않고 들어오는 log를 chunk에 적는다. 그러다가 chunk의 크기가 일정 이상 커지거나, chunk가 생긴지 일정 시간 이상 지나면 queue에 들어간다.
chunk는 tag를 key로 하므로 buffer에 들어가지 않고 있는 chunk가 한 개 이상일 수도 있다. queue의 크기를 일정 이상 키우지 않기 위해 queue에 chunk를 집어넣을 때, queue에서 chunk를 1개 빼서 output으로 내보낸다.
buffer가 해주는 또 다른 중요한 기능은 서버(중개 node이건 최종 저장소이건)에 문제가 생기더라도 log의 유실을 최소화하는 것이다. 하지만 buffer를 사용한다고 해도 메모리가 무한한 것이 아니므로 서버가 오랫동안 문제 있으면 버려지는 데이터가 생긴다.
fluentd에는 재시도를 하고 그래도 안 되면 버리는 것을 정책으로 삼는다. 정확히는 output으로 나가야 하는 data가 나가지 못했을 때 일정 시간이 지난 후 다시 시도한다. 그래도 실패한다면, 기다렸던 시간의 2배만큼 더 기다리고 다시 시도하기를 반복한다. 일정 횟수를 기다려도 보내는 것에 실패하면 이 데이터는 다음으로 보내지지 않고 버려진다. 이때 기다리는 시간을 retry_wait, 다시 시도하는 횟수를 retry_limit으로 설정할 수 있다.
이 경우 외에도 fluentd 자체가 문제가 생겨서 꺼지는 경우도 있다. fluentd는 이를 위해서도 buffer의 plugin으로 원하는 종류를 써서 해결할 수 있다. 기본적으로 fluentd가 buffer에 사용하는 것은 buf_memory라는 plugin으로 chunk를 memory에 기록하는 plugin이다. 하지만 서버가 죽었다 살아날 때도 보장하고 싶다면 buf_file plugin을 이용하면 된다. buf_file plugin을 사용하면 chunk의 내용을 file에 보관해 주기 때문에 서버가 다시 켜질 때 file을 읽어와 buffer를 복구해준다. file에 쓰는 만큼 속도가 느려지지만, 안정성이 증가하기도 하고, 사용할 수 있는 buffer의 크기도 커진다.
Output plugin
위에서 architecture를 설명한 그림에는 input -> buffer -> output 순으로 메시지가 전달되는 것처럼 그렸지만, 사실 정확한 구조는 다음과 같다.
입력은 input plugin을 통해서 들어와 engine을 거쳐서 buffer plugin을 거치지 않고 output plugin으로 나간다. buffer는 engine에서 사용되는 것이 아니라 output plugin 내부에서 사용된다. 왜냐하면, output의 종류에 따라서 buffer가 필요하지 않은 경우가 있어, buffer의 사용 여부를 output plugin이 결정해야 하기 때문이다.
out_null의 경우 들어오는 입력을 전부 버리는 plugin이고, out_stdout은 들어오는 입력을 커맨드창에 띄워주는 plugin이다.
또 다른 경우는 out_copy다. 이 plugin은 하나의 fluentd로 들어온 event를 2개 이상의 output으로 보낼 때 쓰인다. 따라서 뒤에 다른 output plugin이 있고, 이 output plugin이 적절한 buffer를 사용하기 때문에 자체적으로 buffer를 이용할 이유가 없다.
time sliced output plugin은 buffer를 사용하지만, chunk의 key로 tag가 아닌 시간을 사용한다는 것만이 다르다.
Configuration
마지막으로 fluentd를 실제로 어떻게 설정하는지에 대해 설명하면서 마무리하도록 하겠다.
fluentd의 config문법은 어렵지 않다. 일단 실제 config파일 예시를 한번 보자.
위에서 보았듯이 fluentd의 설정은 들과 들로 구성되어 있다.
source 하나는 하나의 input plugin을 의미하고, 하나의 fluentd에 1개 이상의 source가 있을 수 있다. 위의 예시는 forward plugin과 http plugin을 사용하는 경우다.
위와 같이 설정되어 있으면, forward를 통해서 받을 수 도 있고 http protocol을 이용해서 8888번 포트로 입력을 받을 수도 있다.
fluentd는 tag별로 다른 output을 사용할 수 있는데, 그 부분을 설정해 주는 것이 match이다.
쓰여진 순서대로 tag를 match시켜 그 중 첫 번째로 맞는 match에 맞는 output plugin을 이용한다.
output plugin 중에서 out_copy와 out_roundrobin는 라는 항목이 필요하다.
out_copy와 out_roundrobin 둘 다 하나의 log를 둘 이상으로 나눠주는 것이기 때문에 실제 사용할 output plugin을 설정해줘야 하는데 그 설정을 하는 부분이 다.
이 외의 plugin별로 설정해야 할 값들이 있는데, plugin별로 다르므로 하나하나 설명하기는 어렵고, 이에 관해서는 사용할 plugin들에 관해서 reference를 읽고 설정하는 것이 좋다.
p.s. 글에서 설명한 내용이라서 딱히 필요 없을 것 같지만, fluentd에 대해서 회사에서 발표하며 사용했던 자료를 첨부한다.
https://docs.google.com/presentation/d/12A5RlMCVDN6tA_zUnLrZ0TN5v08gz2GhlRdzxy3T3mU/edit?usp=sharing 회사에서 최근에 Log aggregator system으로 무엇을 사용해야 할지 조사해본 자료다. 우선 log aggregator가 무엇인지 한 문장으로 설명하면, 여러 머신에서 쌓인 로그들을 한 번에 분석할 수 있도록 수집하여 주는 시스템을 말한다. 요새는 특히나 클라우드 시스템이 유행하면서 같은 일을 하는 시스템임에도 다른 머신에서 돌아가는 일이 많아지면서 필요성이 크게 증가하였다. 이번 조사로 알게 된 것들을 적어보도록 하겠다. Scribe 우선 scribe 는 Facebook에서 제작하고 사용하던 log aggregator system이다. scribe: http://www.cnblogs.com/brucewoo/archive/2011/12/13/2285482.html 후에 다른 로그 수집 시스템들을 보면 알겠지만, Scribe는 다른 시스템보다 간단한 구조로 되어 있다. Scribe는 일종의 message queue와 message queue에 쌓인 message를 DB에 저장해 주거나, DB가 실패하였으면 local에 저장하였다가 DB가 복구되었을 때 다시 DB에 저장해 주는 것만을 책임진다. 다시 말하면, message queue에 실제로 메시지를 보내는 부분은 사용자가 직접 작성하여야 한다는 것이다. 흔히들 말하는 scribe의 장점은 c++로 만든 만큼 다른 시스템들의 3~5배 정도의 성능을 보여준다는 것이다. 하지만 실제 scribe 사용자들은 무엇보다도 Facebook이 실제로 사용하였던 솔루션인 만큼 성능과 안정성에서 신뢰도가 있다는 것을 장점으로 뽑는다. 하지만 나는 scribe를 사용하는 것을 추천하지는 않는다. 일단 가장 큰 문제는 더이상 Facebook이 Scribe를 사용하지 않는다는 것이다. Faceboo
RAII 는 C++에서 자주 쓰이는 idiom으로 자원의 안전한 사용을 위해 객체가 쓰이는 스코프를 벗어나면 자원을 해제해주는 기법이다. C++에서 heap에 할당된 자원은 명시적으로 해제하지 않으면 해제되지 않지만, stack에 할당된 자원은 자신의 scope가 끝나면 메모리가 해제되며 destructor가 불린다는 원리를 이용한 것이다. 원래는 exception 등으로 control flow가 예상치 못하게 변경될 때를 대비하기 위해서 쓰이던 기법이다. 아래의 예제를 보자. 첫 번째 코드는 위험하다. thisFunctionCanThrowException() 함수에서 exception을 발생시킨다면 resource를 해제하지 못한다. 두 번째 코드는 일단은 resource를 해제하고 있다. 하지만 보기에 좋은 코드는 아니다. 그리고 유지하기도 어려워진다. 세 번째 코드는 RAII를 위해 c++ 11의 스마트 포인터인 unique_ptr을 이용하는 방법이다. unique_ptr은 소멸할 때 자신이 들고 있는 메모리를 해제시켜주기 때문에 함수 밖으로 나가면 resource가 해제되는 것이 보장된다. 여기서 말하는 자원은 단순히 heap 메모리만을 말하는 것이 아니다. heap 메모리 이외에도 파일이나 db와 같은 것들도 전부 RAII를 이용해 안전하게 사용할 수 있다. 여기에 더 나아가서 특정 scope를 벗어나면 반드시 실행돼야 하는 코드들도 RAII를 이용해 처리할 수 있다. 즉, 다른 언어에서 finally에 해당하는 구문을 RAII를 이용해서 처리할 수 있다. 실제로 C++의 아버지이자 RAII라는 용어를 처음 만든 Bjarne Stroustrub는 c++에 finally를 집어넣지 않는 이유를 "RAII가 있는데 굳이 있을 필요가 없다." 라고 말하고 있다.
C++ 03 까지의 enum 은 여러 가지 문제를 가지고 있었다. 그래서 그 문제들을 해결하기 위해 C++ 11은 enum class 라는 것을 새로 만들었다. 이제부터 기존의 enum 에 어떤 문제가 있었고, 이것을 enum class 에서 어떻게 해결하였는지 살펴볼 것이다. 우선 기존의 enum 은 전방 선언할 수 없었다. 그 이유는 enumerator에 어떤 값이 들어있을지 알 수 없으면 그 크기를 정할 수 없기 때문 이다. 하지만 enum class 는 underlying type을 명시하지 않으면 int 타입과 같은 크기의 변수로 선언되고, int 값 안에 들어가지 못할 값을 지정하면 컴파일 에러를 발생시킨다. 만약 int 를 벗어난 범위의 값을 사용하고 싶다면, underlying type을 명시해주어야 한다. 기존 enum 의 또 다른 문제는 enumerator의 이름의 범위가 한정되지 않는다는 것이다. 예를 들어 아래와 같은 코드를 보자. IO 함수의 결과와 Parse 함수의 결과를 enum 으로 표현해 보았다. 하지만 이 코드는 컴파일되지 않는다. IOResult 의 Error , Ok 가 ParseResult 의 Error , Ok 와 겹치기 때문이다. 이를 해결하기 위해서는 다음과 같이 enumerator의 이름을 다르게 하거나 아래와 같이 namespace를 이용해야 했다. 하지만 enum class 는 enumerator의 이름이 enum class 안으로 한정되기 때문에 이런 복잡한 과정이 필요 없이 그저 enum class 를 선언하여 사용하면 된다. 무엇보다 기존 enum 의 가장 큰 문제는 정수형 변수로 암시적으로 변환되는 약 타입(weak type) 변수라는 것이다. 하지만 enum class 는 정수형 변수로 암시적 변환이 되지 않는다. enum class 를 정수형 변수처럼 사용하려고 하면 컴파일 에러를 발생시킨다. 만약 정수형 변수로 사용하고 싶으면 static_cas
댓글
댓글 쓰기