우선 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에 대해서 회사에서 발표하며 사용했던 자료를 첨부한다.
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
지난번에 shard key를 설명한 글 을 썼을 때 댓글 로 ObjectId 에 관한 얘기가 나왔었다. 그래서 sharding과 큰 연관은 없지만, 이번 기회에 ObjectId에 관해서 먼저 설명하고 가는 게 좋을 것 같아서 sharding에 관한 것은 뒤로 미루고 이번에는 ObjectId에 대해 먼저 설명하고 가도록 하겠다. ObjectId란 무엇인가 ObjectId는 같은 document 내에서 유일함이 보장되는 12 byte binary data다. 전통적인 centralized 되어 있는 시스템이라면 한 collection 내에서 유일함을 보장하는 것을 쉽게 할 수 있다. 하지만 sharding을 하는 MongoDB에서 유일함을 보장하는 것은 기존과는 다른 솔루션이 필요하다. 그리고 이 방법을 설명하는 게 사실상 ObjectId의 모든 것을 설명하는 것이다. 왜 ObjectId를 사용하는가 전통적인 RDBMS 에서 Primary key 를 만들 때는 DB 서버로 data를 보내서 중복되지 않는 key를 골라서 1) 그 값을 key로 저장하는 방식을 이용한다. 하지만 MongoDB와 같은 분산 database에서는 key를 서버에서 만들지 않고 클라이언트에서 만든다. 그 이유는 MongoDB가 query를 날릴 shard를 결정하는 방식 을 보면 알 수 있다. MongoDB는 자신이 필요한 shard에게만 query를 요청한다. 다시 말해서 client에 해당하는 mongos 2) 가 config server 의 data를 토대로 어떤 shard가 어느 범위의 값을 가졌는지를 저장하고 있다가 query를 요청할 때 자신이 필요로 하는 shard에게만 요청한다. 따라서 shard key 에 해당하는 data가 미완성인 상태로 서버에 저장하도록 요청할 수 없고, client가 ObjectId를 생성하여 값을 저장하도록 요청한다. 3) ObjectId의 구성 ObjectId는 크게 4부분으로 구성되어 있다. Ob
Cyclomatic Complexity Cyclomatic complexity (a.k.a. CC)는 코드의 복잡성을 나타내는 지표 중 하나다. CC를 계산하는 방법은 매우 간단하다. 단순히 코드의 컨트롤 플로우가 분기하는 부분의 개수를 세면 된다. CC를 처음 제안했던 Thomas J. McCabe는 함수 하나의 CC가 10을 넘기지 말도록 했지만 이건 76년에 나온 기준이고, 지금의 소프트웨어는 40년 전과 비교가 되지 않게 복잡해진 만큼 15나 20까지는 괜찮다고 주장하는 사람도 있다. 어찌됐든 간에 중요한 것은 CC가 커지면 커질수록 소프트웨어의 에러가 발생할 확률 1) 이 증가한다는 것이다. 최댓값을 얼마로 잡을지는 프로젝트의 성격과 팀의 성향에 따라서 다르게 잡지만, 지난 40년간 코드의 복잡도를 정적으로 측정할 수 있는 몇 안 되는 지표로써 널리 쓰이고 있다. Extended Cyclomatic Complexity 하지만 CC가 단순히 분기점만을 세기 때문에 불만을 가지는 사람들이 있었다. 그들이 불만을 가지는 이유는 크게 2가지다. 우선 CC는 단순히 분기점의 수를 세기 때문에, 실제로 같은 코드를 어떻게 표현하느냐에 따라서 값이 달라진다. 그래서 단순히 분기점을 세는 것이 아니라 조건문에 들어가는 Boolean operator(&&, ||)의 수를 더하는 지표가 나왔다. 이를 Extend Cyclomatic Complexity(a.k.a ECC)라고 부른다. Modified Cyclomatic Complexity CC에 다른 이유로 불만을 가지는 사람들도 있다. 원래의 CC는 switch에 사용되는 case 문의 수만큼 증가한다. 하지만 대부분의 경우 switch 문에 들어가는 구문은 매우 간단하다. CC의 원래 목적이 코드의 복잡성을 측정하기 위함이라는 것을 생각하면 실제 코드를 복잡하게 하지 않는 switch 때문에 매우 증가하는 CC는 불공평하다. 그래서 나온 것이 Modified Cyclomatic
댓글
댓글 쓰기