2014-04-27

Fluentd - Pluggable log collector

 지난번에 소개했던 에서 여러 가지 log aggregator들을 소개했었다. 이번에는 그중에서도 특별히 마음에 들었던 fluentd를 더 자세히 소개해 보도록 하겠다.

Semi-structured log

http://blog.treasure-data.com/post/13047440992/fluentd-the-missing-log-collector-software
 우선 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파일을 바꾸는 것만으로도 여러 머신들 간의 설정을 쉽게 바꿀 수 있다.

http://blog.treasure-data.com/post/16034997056/enabling-facebooks-log-infrastructure-with-fluentd
 위의 그림은 가장 기본적인 형태로 frontend에 붙어 있는 fluentd에서 보내는 이벤트를 중개 서버(?)에 해당하는 fluentd에서 한번 수집하여 최종 저장소에 보내는 형태이다.
 위의 그림은 특별히 fluentd의 성능을 고려하여 하나의 중개 서버가 너무 무리하는 일 없도록 여러 개의 중개 서버에 나누어서 보내는 방식이다.

http://docs.fluentd.org/articles/high-availability
 위의 그림은 backup server를 두는 방식이다. fluentd는 내부적으로 버퍼를 가지고 있어 일정 시간 서버에 문제가 생기는 것에 대응할 수 있게 되어 있지만, 기본적으로 로그를 저장하기 위해서 쓰이고, 버퍼가 버틸 수 있는 것 이상으로 서버의 문제가 복구되지 않는다면 로그를 버리도록 설계되어 있다.
 그럴 때를 대비하여 backup server를 둘 수 있다. backup server는 보통 때에는 사용하지 않지만, main server에 로그를 남길 수 없을 때 기록을 남긴다.

http://stackoverflow.com/questions/10525725/which-nosql-database-should-i-use-for-logging
 backup server를 만들 수도 있지만, out_copy plugin을 이용하여 위의 그림처럼 한 개의 소스에서 다른 fluentd 서버로 보낼 수도 있다.

 위의 기능들을 다양하게 조합하면 아래와 같은 복잡한 구조도 가능해진다.
http://d.hatena.ne.jp/tagomoris/20121029/1351491111

Architecture

http://blog.treasure-data.com/post/13047440992/fluentd-the-missing-log-collector-software
 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이 결정해야 하기 때문이다.

 buffer plugin을 사용하지 않는 output plugin을 non-buffered output plugin이라고 부른다.
 대표적인 예가 out_nullout_stdout plugin이다.
 out_null의 경우 들어오는 입력을 전부 버리는 plugin이고, out_stdout은 들어오는 입력을 커맨드창에 띄워주는 plugin이다.
 또 다른 경우는 out_copy다. 이 plugin은 하나의 fluentd로 들어온 event를 2개 이상의 output으로 보낼 때 쓰인다. 따라서 뒤에 다른 output plugin이 있고, 이 output plugin이 적절한 buffer를 사용하기 때문에 자체적으로 buffer를 이용할 이유가 없다.

 평범하게 buffer plugin을 사용하는 plugin들은 buffered output plugin이라고 부르는데 이 중 일부는 time sliced output plugin이라고 불린다.
 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에 대해서 회사에서 발표하며 사용했던 자료를 첨부한다.

2014-04-17

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

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를 사용하지 않는다는 것이다. Facebook은 이미 Java로 작성한 Calligraphus를 사용하기 시작했다. scribe는 open source이고 개발이 완전히 멈춘 것은 아니다. 하지만 Facebook이 중심적으로 만들던 시절에 비하면 거의 발전이 없는 상태라고 해도 될 정도이다.
 앞에서 말했듯이 Scribe는 message를 보내는 부분을 완전히 새롭게 작성해야 한다. 또한, 바이너리 배포를 안하기 때문에 사용하려면 Scribe 자체도 빌드하여야 하는데 Scribe 자체가 여러 라이브러리에 의존성이 걸려있기 때문에 빌드하는 것도 쉽지 않다.

 남은 장점은 성능밖에 없는 관계로 일단 다른 솔루션을 사용해보고, 그것으로 감당이 되지 않을 정도로 많은 부하가 걸리는 게 아니라면 굳이 Scribe를 사용할 이유는 없어 보인다.

Flume

 다음으로 소개할 Flume은 과거 Cloudera에서 제작하여 지금은 apache의 top level project가 되었다.
 Flume의 가작 큰 특징(?)은 중간에 설계가 크게 바뀌었다는 것이다.
Flume OG: http://archive.cloudera.com/cdh/3/flume/UserGuide/
Flume NG: http://flume.apache.org/
 원래의 Flume OG는 master가 존재하여 agent나 collector를 master를 통해서 제어해야 했다. 하지만 Flume NG에서는 각각의 Agent가 독립적으로 어디로 data를 보낼지 결정할 수 있다.
 Flume은 deb package로 배포하고 있기 때문에 사용하기 쉽다는 장점이 있지만, plugin을 만들기 쉬운 구조가 아니므로 주어진 조건에 맞게 사용하는 것 외에는 힘들다는 단점이 있다. 아직은 Flume을 검색하면 Flume OG 시절의 문서와 사용 방법이 나오는 경우가 있다는 것도 단점이다.

Fluentd

 Fluentd는 ruby와 c로 짜여진 log aggregator 시스템이다.
fluentd: http://blog.treasure-data.com/post/13047440992/fluentd-the-missing-log-collector-software
 기본적인 구조는 Flume NG와 비슷한 구조로 되어 있다. Flume의 Source, Channel, Sink가 각각 Input, Buffer, Output이 되었다고 보면 된다.
 Fluentd의 가장 큰 특징이자 장점은 각 파트별로 plugin을 만들기 쉽다는 것이다. 직접 plugin을 만들지 않더라도, ruby로 짜여 있으며 plugin을 gem으로 배포하기 때문에 plugin을 쉽게 붙일 수 있다는 것도 큰 장점이다.
 그러면서도 성능이 필요한 부분은 c로 작성하여 ruby로 wrapping을 하였기 때문에 성능이 크게 떨어지지도 않는다.

 물론 Fluentd도 단점이 있다.
 중요한 부분은 c로 짜여 있다고 해도 대부분의 부분이 scribe보다는 느리다.
 대부분이 ruby로 짜여 만큼 ruby의 고질적인 문제인 memory fragmentation을 피할 수 없다.
 로그에 시간을 반드시 남기게 되어 있기 때문에 agent들 사이에 시간이 맞지 않으면 log가 이상하게 쌓이기도 한다.

 하지만 이런 문제들은 전부 어렵지 않게 해결할 방법이 있다.
 scribe보다 느리다고 했지만, 대부분 시스템에서 사용하기에는 충분하다. 그래도 느리다면 멀티코어를 이용하는 플러그인을 사용하면 된다.
 memory fragmentation은 jemalloc을 사용하면 된다.
 시간이 맞지 않는 문제는 클라이언트들에서 ntpd를 돌리면 해결된다.

 사용하기 쉽고, 단점들을 해결하기도 쉽기 때문에 아마 Fluentd를 사용하게 될 것 같다.

logstash

 logstash는 다른 것들과는 약간 다른 관점에서 조사하다가 나온 것이다.
 로그를 수집하였다고 하더라도 수집된 로그를 분석하여 보기좋게 표현할 방법이 없으면 그건 그저 information이 되지 못한 단순한 data일 뿐이다.
 logstash는 elsaticsearch family의 하나가 되면서 쌓인 로그를 웹으로 보여주는데 좋은 툴인 kibana와 함께 쓸 수 있어서 손쉽게 로그를 보고 분석할 수 있는 기능을 제공해준다.

 하지만 fluentd도 kibana를 붙일 수 있고, logstash자체의 기능이 fleuntd보다 못하기 때문에 굳이 logstash를 쓸 일은 없어 보인다.

2014-04-12

Actor model and akka

다음 프로젝트로 scalable 한 게임 서버프레임워크 구현을 진행 중이다.
아직 구상 중이라 결정된 것은 없지만, scalability와 functional 한 특성을 동시에 살릴 수 있는 scalaakka프레임워크가 후보로 들어왔고 이에 대해 간단하게 정리하여 발표할 기회가 있었다.
일단 발표자료는 간단하게 키워드들만 적었기에 이에 대해 보충 설명을 해보고자 한다.

akka는 무엇인가

 akka는 scala로 구현된 concurrency 제어를 위해 actor model을 도입한 프레임워크로 java와 scala API를 제공한다.
우선 akka는 actor model을 기본으로 하고 있기 때문에 akka의 특성을 이해하려면 actor model을 이해해야 한다.

Actor의 특징

 actor model은 간단히 설명하면 behavior, state, mailbox로 구성된 actor를 기본 단위로 하는 message processing을 이용하여 behavior를 비동기적으로 실행하는 model이다.
 이때 기본단위가 되는 actor는 몇 가지 특징이 있다. 우선 각 actor는 서로 간에 공유하는 자원이 없고 서로간의 state를 건드릴 수 없고, 오로지 message를 이용해서만 간섭할 수 있다.
 message는 mailbox에 쌓였다가 들어온 순서대로 처리된다.
 실행되는 behavior는 message에 의해 결정되고, 할 수 있는 일은 자신의 state를 바꾸거나, child actor를 만들거나, child actor를 죽이거나 다른 actor에 message를 보낼 수 있다.

 actor model의 actor는 사실 OOP에서 말하는 object와 매우 비슷하다.
 object는 member variable(state)을 가지고 있고, 어떤 방식으로 동작할지 method(behavior)를 가지고 있다.
 method는 다른 object를 만들거나, 자기가 관리하는 object를 부수거나 다른 object의 method를 호출하는 일을 한다.
 현대의 OOP 언어들(Java, C#, c++ 등)만을 사용한 사람들은 message를 이용해 method를 호출한다는 개념이 익숙하지 않을 수 있다.
 하지만 과거의 pure한 OOP 언어들(Simula, Smalltalk)에서는 다른 object에 message를 보내면 받은 message에 해당하는 method를 부른다는 개념이 있고, 이것이 간략화되어 object의 method를 호출한다는 개념이 된 것이다.
 그렇다면 OOP의 object와 actor model의 actor는 어떤 차이가 있을까?

 그 차이는 단 한 가지이다. Object의 method는 message를 보낸 context에서 바로 실행되어 method가 끝날 때까지 기다리지만, actor model의 actor는 message를 보낸 context와 독립적인 context에서 비동기적으로 실행된다는 것이다.

Actor model을 사용하는 이유

사실 Actor model은 그리 새로운 개념이 아니다. 처음 그 개념이 나온 것은 1973년의 일로 프로그래밍 모델중에서는 나름 오래된 편에 속하는 개념이다. 이런 오래된 개념이 요새와서 다시 각광을 받는 이유는 multi processing에 적합한 개념이기 때문이다.
더 이상 moore's law가 적용되지 않기 때문에 CPU vendor들은 하나의 CPU에 여러개의 프로세스를 장착하여 연산속도를 증가시키기 시작했다. multi-core환경을 효율적으로 사용하려면 여러개의 thread를 이용하여 구현하는 것이 중요하다.
 하지만 shared resource를 가지는 멀티쓰레드 환경에서는 여러가지 문제들(race condition, deadlock, blocking call 등)이 발생하기 쉽기 때문에 이를 회피하기 위한 패턴 혹은 모델들이 여러가지 나오게 되었고, 그 과정에서 actor model이 다시 각광받기 시작하였다.

 Actor model의 장점과 단점

 Actor model의 가장 큰 장점은 이해하기 쉽다는 것이다.
 message를 받으면 그에 맞는 behavior를 실행한다는 매우 간단한 동작원리와 다른 것에 영향을 받지 않는다는 특징 때문에 실행 순서를 이해하고 결과를 예측하기 매우 쉽다.
 또한, 모든 간섭을 message를 통해서 한다는 것도 큰 장점이다.
 우선 shared resource가 존재하지 않기 때문에 shared resource들로 말미암아서 생기던 문제들(race condition, deadlock 등)이 발생하지 않는다.
 그리고 message가 serializable하기만 한다면 같은 서버에서 실행하던 Actor를 다른 서버에서 실행하여 message를 주고받는 것도 가능하므로 손쉽게 서버를 scale-out할 수 있다.

 물론 actor model이 장점만 가지는 것은 아니다.
 shared state를 가지지 않고 모든 통신을 message로 하는 것은 control flow를 제어하고 correctness를 보장하는 것에는 큰 장점이었지만 속도 면에서는 큰 단점이 된다.

Concurrency in akka

 그렇다면 scala의 구현체인 akka는 어떻게 actor를 사용할지 코드를 통해서 알아보도록 하자.
다음의 코드를 보자.
 Actor 를 상속받아 receive method만 구현하면 Actor로 사용할 수 있다.
 receive method Any타입을 받을 수 있기 때문에 보통 패턴매칭을 이용하여 구현한다.
 Actor에 메세지를 보내는 방법은 크게 3가지가 있다.

 첫번째 방법은  ActorRef의 tell method를 이용해서 메세지를 보내는 것이다.

 하지만 이런 방식은 Actor에서 응답을 보내주는 것을 처리할 방법이 없다.
 두번째 방법은 메세지의 응답을 synchronous하게 기다리는 방법이다.
 Inbox를 이용하여 message를 보내면 recevie method를 통해서 Actor가 message를 처리하고 응답을 보내기를 기다린다. timeout시간을 주기는 하지만 이렇게 하면 결국 서로 다른 2개의 Actor에서 서로를 기다리면서 deadlock이 생길 수 있다.
 그렇기 때문에 akka에서는 Actor안에서는 Inbox를 이용하여 synchronous하게 메세지 보내는 것을 추천하지 않는다.
 그래서 대부분의 통신은 3번째 방법을 사용한다.

 위의 방법은 ask 함수를 이용하여 Future객체를 만들고 callback을 등록하여 asynchronous하게 처리할 수 있게 해준다.


 이 외에도 akka의 중요한 특징으로 concurrency외에 scalability와 fault-tolerance를 들 수 있다.
 akka는 serializable 메시지들만을 이용하였다면, 물리적으로 다른 서버에 있는 actor와 메시지를 주고받을 수 있게 해준다. 이를 이용해서 scale-out을 쉽게 구현할 수 있게 해준다.
 또한, child actor를 만들어서 메시지를 주고 모니터링할 수 있는 시스템을 제공해주기 때문에 손쉽게 오류에서 복구할 수 있도록 해준다.
 쓰다 보니 의욕이 떨어지고 있는 관계로 이 두 가지에 관해서는 다음 기회에 더 자세히 글을 쓰도록 하겠다.

p.s. 위의 코드는 activator에 들어 있는 hello-akka tutorial을 기반으로 작성되었다.