2014-12-19

Facade pattern - 간단한 인터페이스 만들기

 퍼사드 패턴이란 복잡한 서브 시스템에 하나의 레이어를 씌워서 복잡한 시스템을 사용하기 쉽게 만드는 방식을 말한다. 퍼사드 패턴은 레이어를 하나 추가하지만, 이 레이어는 추상화가 목적이 아니다. 어디까지나 사용하기 쉽게 만드는 것이 목적이다. 그래서 보통 퍼사드의 인터페이스는 매우 간단한 모습을 가진다.
http://en.wikipedia.org/wiki/File:Example_of_Facade_design_pattern_in_UML.png

 퍼사드 패턴의 대표적인 사용처는 로그 API다. 로그를 그대로 STDOUT에 출력할 수 도 있고 어딘가에 저장할 수도 있다. 저장하는 것도 로컬에 있는 파일에 기록을 남길 수도 있고, 데이터 베이스에 파일을 저장할 수도 있고, 그 이외의 방식으로 로그를 저장할 수도 있다. 즉, 만약 파일에 저장한다면 적절하게 파일 포인터를 관리해야 하고, 데이터 베이스에 저장한다면 그 connection을 적절하게 관리해야한다. 게다가 로그를 저장하는 것도 IO로 인한 병목을 피하기 위해 일정 시간 혹은 일정 갯수의 로그를 모았다가 저장할 수 도 있다. 만약에 모았다가 저장한다면, 저장할 조건을 만족시키지 않았더라도 프로세스가 종료되기전에 저장을 완료해야 한다.
 이런 조건들을 사용자가 일일히 챙겨가며 로그를 저장하는 것은 귀찮은 일이다. 그래서 Logback같은 API에서는 back-end가 어떻게되든 상관 없이 필요한 back-end를 사용하도록 초기화해주면, 그 뒤로는 같은 인터페이스로 기록을 남길 수 있게 해준다.

 전에 소개한 적 있던 Pluggable한 log aggregator인 fluentd의 경우도 퍼사드 패턴을 사용한다. Input plug-in, Buffer plug-in, Output plug-in은 모두 간단한 인터페이스만을 요구한다. 덕분에 fluentd의 engine은 간단한 코드를 유지할 수 있고, 간단하게 원하는 back-end를 선택해서 동작을 바꿀 수도 있다.


본 글은 CC BY-SA 3.0 라이센스를 따릅니다.

2014-12-18

file URI와 same-origin policy

 modern web browser에는 보안을 위한 여러 가지 기능들이 들어있다. 그중 가장 대표적인 기능이 same-origin policy다. same-origin policy 덕분에 (개발자 입장에서는 약간 짜증 나기는 하지만) 특별히 신경을 쓰지 않아도 보안에 관해 상당히 많은 부분을 커버할 수 있다. same-origin policy의 원칙은 매우 간단하다. 내 사이트가 다른 사이트에서 호스팅 되는 리소스에 의존하는 것을 금지해서, 내 사이트가 오염되거나 다른 사이트에 의해 공격당하는 것을 막는 것이다.

 same-origin인지 결정하는 것은 매우 간단한데 프로토콜, 호스트, 포트가 같은 URI를 same-origin이라고 판단한다. 요새는 대부분 개인 서버와 개인 도메인을 사용하기 때문에, 프로토콜과 포트까지 같은지 판단하는 것은 너무 빡빡한 기준이라고 생각할 수도 있지만, 워크스테이션이나 공용 서버에서 작업하는 일도 많다는 것을 생각하면 포트와 프로토콜까지 고려하는 것은 역시 상식적인 판단이라고 할 수 있다.

 브라우저에서 많이 쓰이는 http나 https에서는 이 규칙이 상식적이라고 할 수 있다. 문제는 file URI에서의 동작이다. file URI에 대해서는 어떤 URI를 same-origin이라고 할 것인지 정해진 것이 없고, 브라우저마다 알아서 자신이 옳다고 생각하는 방식으로 구현했다.

 우선 오페라는 file URI도 다른 URI와 같은 정책으로 처리한다. 따라서 file URI로 접근한 페이지에서는 읽기 권한이 있는 모든 파일을 읽어서 리소스로 활용할 수 있다. 어차피 파일은 OS가 access list로 보호하고 있으니, 로컬 파일에 대한 보안을 OS에 맡겨 버린 것이라고 할 수 있겠다. 약간 무책임한 것 같지만, 오페라의 구현이 가장 웹서버 없이 웹 페이지를 테스트하기 편한 구현이다.

 반면 크롬은 modern browser 중에 가장 빡빡한 규칙을 적용한다. 크롬에서는 file URI로 들어오는 요청에 대해 무조건 다른 origin인 것으로 처리한다. 심지어 host name을 명시적으로 입력해서 겉으로 보기에는 protocol, host name, port가 같아서 같은 origin으로 보이는 경우도 다른 origin인 것처럼 처리한다. 따라서 크롬에서는 웹 서버 없이 웹 페이지를 테스트하는 것이 불가능한 경우도 생긴다.
 이렇게 빡빡한 규칙을 적용한 이유는 찾을 수 없었다. 아마도 크롬이 Chrome OS와 코드를 공유하기 때문으로 추측된다. Chrome OS의 파일 권한이 앱별로 있는 것이 아니라서 다른 앱에서 로컬에 있는 파일을 읽는 것을 막기 위해서 file URI를 모두 다른 origin으로 취급해야 했던 게 아닐까 싶다.

 파이어폭스는 크롬과 오페라의 중간 정도 되는 정책을 취한다. 파이어폭스에서 file URI를 쓰면 현재 directory를 기준으로 자신보다 아래 경로에 있는 파일은 읽을 수 있도록 허용한다.1) 덕분에 파이어폭스도 웹 서버 없이 테스트하지 못하는 웹 페이지가 생기기도 한다. 하지만 크롬보다는 상황이 조금 나은 게 최소한 자신의 것이 확실한 (자기 디렉토리 아래에 있는)리소스는 마음껏 사용할 수 있다.

 크롬 브라우저에는 file URI를 이용하면서도 다른 파일을 같은 origin인 것처럼 사용하는 방법도 있다. 크롬을 실행할 때 --allow-file-access-from-files switch를 이용하면 file URI에서 모든 파일에 접근할 수 있도록 해준다. 주의해야 할 것은 기존에 실행시켰던 크롬이 백그라운드로 떠 있으면, 새로 크롬을 실행시켜도 백그라운드 프로세스로 떠 있던 크롬이 포크 되며 실행되기 때문에 switch가 반영되지 않는다. 따라서 switch를 주면서 크롬을 실행시킬 때는 기존의 프로세스가 전부 꺼졌는지 확인하고 실행해야 한다.
 파이어폭스도 file URI에 대한 동작을 변경할 수 있다. security.fileuri.strict_origin_policy 설정을 이용하면 오페라와 같이 모든 로컬파일을 리소스로 쓸 수 있게 된다.

 이런 식으로 크롬이나 파이어폭스를 이용해도 switch나 preference를 통해서 file URI로 로컬에 있는 페이지에 접근해서 테스트할 수도 있다. 하지만 테스트를 위해서가 잠시 값을 바꿔서 쓰는 게 아니라 기본값을 바꿔서 계속 사용하는 것은 좋은 방식이 아니라고 생각한다. same-origin policy라는 것의 목적은 보안을 위한 것이다. 보안을 위한 기능을 편의를 위해 끄고 쓰는 것은 열쇠 가지고 다니기 귀찮다고 문 열어놓고 다니는 것과 마찬가지의 행동이다.
 게다가 file URI를 통해 간단한 테스트는 가능할 수도 있지만, file URI는 http나 https와 동작이 달라서 실제 웹앱과 완벽하게 같은 테스트는 안된다. 따라서 실제로 웹 서버를 띄우는 것이 가장 확실한 방법이다.

 하지만 구태여 웹 페이지를 file URI로 테스트하려고 했던 것은, 현재 머신에 웹서버를 설치하기 싫거나, 설치할 수 없거나, 설정하기 귀찮은 경우일 것이다. 그런 상태에서 웹 서버를 설치하고 테스트하라고 말해봐야 보안에 구멍이 생기더라도 브라우저 설정을 바꾸는 것을 택할 것이다. 그런 사람들을 위해 간단하게 웹서버 돌리는 방법을 소개해 주겠다.

 어디든지 실행시키고 싶은 웹 앱의 최상단 경로에서 python3 -m http.server {port}를 입력하면 python으로 웹 서버가 실행된다. 아무 기능도 없지만, 최소한 file URI로 테스트하려던 수준의 웹 페이지라면 충분히 테스트할 수 있다.

1) https://developer.mozilla.org/en-US/docs/Same-origin_policy_for_file:_URIs

2014-12-14

WebGL 공부 시작

 나는 그래픽스 관련해서는 모든 것을 야매로 배웠었기 때문에, 전반적으로 기초가 없다. 이게 나름대로 꽤 스트레스였다. 하지만 천성적으로 게을러서, 계속 언젠가는 제대로 공부해야 한다고 생각만 하고 미루기만 했다. 그러다가 문득 이렇게 미루기만 하면 언젠가 후회할 날이 올 것 같아서 바로 공부를 시작하기로 했다.

 우선 공부할 그래픽스 라이브러리는 OpenGL로 정했다. 모바일 시장이 있어도 역시 많이 쓰이는 것은 Direct3D고, 게다가 apple에서는 metal이라는 새로운 그래픽스 라이브러리를 발표해서 OpenGL의 입지가 더 줄어들 것이라고 한다. 하지만 나는 이번 목표가 당장 어딘가에 써먹기 위한 것을 배우기 위한 것이 아니라 기본적인 것을 배우기 위한 거라서 뭘 하더라도 상관없었다.
 사실 지금 내 윈도우 pc는 망가지고, iMac은 이제 개발용으로 쓰기에는 성능이 너무 나빠서 별 다른 선택의 여지가 없었다.

 OpenGL API 구현체도 platform 별로 여러 가지 구현체가 있다. 나는 그중에서 WebGL을 공부하기로 했다. 특별히 web platform에서 작업할 일이 있었던 것은 아니지만, 리눅스 환경에서 OpenGL로 코딩하려면 리눅스 드라이버를 수동으로 재 설치해야 하는 경우가 많아서 브라우저만 있으면 바로 작업 가능하다는 특징은 WebGL의 큰 장점이라고 생각한다.

 그리고 무엇보다도 WebGL Inspector라는 좋은 디버깅 툴이 있다는 것은 WebGL
 사실 WebGL의 가장 큰 장점은라고 생각한다. 리눅스에서 OpenGL 개발 환경을 구성할때는 개발 환경 자체를 준비하는 것도 귀찮지만, 디버깅 환경을 구성하는 것이 진짜 귀찮다. 그래픽 드라이버 버전에 따라서는 아예 디버깅할 수 없어서 무조건 실행시켜보는 수밖에 없는 경우도 있다.
 하지만 브라우저의 플러그인 형식으로 설치 가능한 WebGL Inspector를 사용하면, 프레임을 멈추거나 속도를 조정할 수도 있고, 어떤 콜이 불렸는지, array buffer나 element array buffer에 어떤 값이 들어있는지, 어떤 프로그램을 사용했고, 현재 state가 어떤지, texture의 값은 어떤지까지 알 수 있어 다른 환경보다 상대적으로 쉽게 디버깅할 수 있다.

 물론 WebGL이 무조건 좋은 것은 아니다. 기본적으로 OpenGL API에서는 멀티 스레딩을 위한 lock이나 동기화 방식을 제공하지 않는다. 그래서 고급 테크닉들을 보면 대부분 어떻게 멀티 스레딩 환경에서 효율적으로 작업을 나누어 그림을 그리는가가 대부분이다. 하지만 WebGL은 main thread에서밖에 호출할 수 없어서 이런 테크닉을 쓸 수 없다.
 하지만 이는 어디까지나 고급 테크닉들이고 기초와는 상관없기 때문에 WebGL로 시작하는 게 딱히 문제 될 것 같지는 않았다.

2014-12-11

도메인 이전했습니다.

며칠 전에 말했던 대로 http://blog.seulgi.kim으로 이전했습니다.

기존 도메인인 blog.seulgik.im으로 접속해도 리다이렉트 되겠지만, 해당 도메인은 앞으로 사용할 예정이 없어서 2016년 6월(!) 이후로는 접속이 안 될 겁니다.

2014-12-03

조만간 도메인 이전합니다.

기다리고 기다리던 seulgi.kim을 드디어 샀네요.
조만간에 blog.seulgi.kim으로 이전합니다.

바로 이전하고 싶은데 blogger가 동시에 여러 개의 cname을 지원하지 않네요.
요새 너무 바빠서 시간 나면 리다이렉트 페이지 만들고 그때가서 이동할게요.

2014-11-28

컴파일러의 구조 - front-end와 back-end

 지난번 글에서 보여준 컴파일러의 구조를 잘 보면, Code Generator를 전후로 machine independent한 작업과 machine dependent한 작업들로 나누어지는 것을 알 수 있다.
 여기서 machine independent한 작업들(Lexical Analyzer, Syntax Analyzer, Semantic Analyzer, Intermediate Code Generator, Machine-Independent Optimizer)을 컴파일러의 front-end라고 부르고, machine dependent한 작업들(Code Generator, Machine-Dndependent Optimizer)를 컴파일러의 back-end라고 부른다.

 현대의 컴파일러들은 대부분 front-end와 back-end가 확실하게 나누어진다. Front-end와 Back-end 사이를 연결해 주는 것을 Intermediate Representation(a.k.a. IR)이라고 한다. IR은 컴퓨터가 실행할 프로그램을 표현해주는데 여러 가지 형태로 표현될 수 있다.1) JVM이나 .Net 같은 가상머신에서는 쓰이는 bytecode도 일종의 stack-based IR이다.

1) http://cs.lmu.edu/~ray/notes/ir/

2014-11-27

컴파일러의 구조

 사람이 읽기 쉽게 쓰여 있는 소스코드를 기계가 실행할 수 있는 byte 코드로 변환하여 주는 프로그램을 컴파일러라고 한다. 소스를 그에 대응하는 기계어의 집합으로 바꿔주는 일은 언뜻 보기에는 간단해 보이지만, 최적화나 platform dependent한 문제들이 엮이면 쉽지 않은 작업이다. 그래서 복잡한 작업을 최대한 간단하게 만들기 위해 현대의 컴파일러는 다음과 같은 복잡한 구조를 가진다.

  Source Code
Lexical Analyzer
  Token Stream
Syntax Analyzer
  Syntax Tree
Semantic Analyzer
  Syntax Tree, Symbol table
Intermediate Code Generator
  Intermediate Representation, Symbol Table
Machine-Independent Optimizer
  Optimized Intermediate Representation, Symbol Table
Code Generator
  Machine Code
Machine-Dependent Optimizer
  Optimized Machine Code

 위의 구조는 컴파일러 교재로 유명한 dragon book1)이 설명하고 있는 모델이다. 이런 복잡한 구조를 가지는 이유는 dragon book의 표지 그림이 상징적으로 설명해준다.
 앞에서 말했듯이 컴파일러가 해야 하는 일은 매우 복잡하해서 이 complexity of compiler design을 상대하기 위해서 고안된 모델이다.
 하지만 이는 어디까지나 복잡한 일을 쉽게 하기 위해서다. 어디까지나 일을 쉽게 하기 위한 것인 만큼 이 구조에 얽매일 필요는 없다.

 하지만 이보다 단순한 구조로 컴파일러를 만들기는 쉽지 않을 것이다. 그래서 경우에 따라 컴파일러를 새로 만드는 것이 아니라, 기존의 언어에서 제공하는 기능을 이용하여 언어를 확장하여 DSL을 만들거나(Lisp의 macro나, Scala의 implicit 등이 여기에 해당한다.), 기존의 언어로 convert하여 기존의 언어 컴파일러를 이용하는 경우(JVM 위에서 돌아가는 Rhino engine이 대표적이다.)도 있다.


1) LAM, Monica, et al. Compilers: Principles, Techniques, and Tools. 2006. p.5

2014-10-31

[Scala] 함수 선언과 호출에서의 괄호 생략


 위에서 보듯이 Scala에서는 인자가 0개인 함수를 선언하거나 호출할 때 괄호를 생략할 수 있다.1) 컴파일 타임에 괄호 생략에 대해 아무런 제약도 하지 않고, 컴파일된 byte code에도 차이가 없다. 그렇다고 아무 괄호나 생략해도 되는 것은 아니다. 언어적으로는 괄호 생략에 관해 아무 제약을 하지 않지만, side-effect가 없을 때만 괄호를 생략하여야 한다.2)

 side-effect가 없는 함수에 대해서만 괄호를 생략하는 것은 함수가 선언된 모양만으로 그 함수가 하는 일을 추측할 수 있기 때문에 코드의 가독성을 높이는 데 도움이 된다. 그렇다면 어째서 컴파일 타임에 side-effect가 없는 함수에 대해서만 괄호를 생략할 수 있도록 강제하지 않을까?
 이유는 간단하다. 컴파일러가 함수가 side-effect가 있는지 없는지 구분할 수 없기 때문이다. Haskelmonad같은 개념을 도입하여 side-effect를 분리해 낼 수 있을지도 모르지만, Scala는 그런 개념을 도입하지 않고, 사용자에게 책임을 넘겼다.

 side-effect는 functional 프로그래밍에서 매우 중요한 부분이기 때문에 이것에 대한 책임을 프로그래머에게 넘겼다는 점에서, Scala의 안 좋은 부분이라고 평하는 사람도 있다. 하지만 나는 이 정도 자유는 프로그래머에게 넘기는 것이 적당하다고 생각한다.

 예를 들어, 무언가 side-effect가 없는 일을 하는 함수가 있을 때 그 함수가 호출되면 logging을 하도록 수정하였다고 생각해보자. 이 함수는 side-effect가 있는 함수라고 봐야 할까? side-effect가 없는 함수라고 봐야 할까? 엄밀한 의미에서 따져본다면 side-effect가 있는 함수라고 분류해야겠지만, 실제 돌아가는 logic을 생각해보면 이 함수를 side-effect가 있다고 구분하는 것은 뭔가 억울(?)하다.

1) http://docs.scala-lang.org/style/method-invocation.html#arity-0
2) http://docs.scala-lang.org/style/naming-conventions.html#parentheses

2014-10-24

Big endian과 little endian

 Endianness는 것은 메모리상에서 byte를 배치하는 방법을 말한다. 크게는 big-endian과 little-endian으로 구분된다.

Big endian

출처 : 위키피디아1)
 우선 big-endian은 가장 큰 byte(most significant byte, a.k.a. MSB)가 가장 앞에 나오는 방식이다. 일반적으로 사람이 사용하는 방식이라고 생각하면 된다.
 Big-endian은 사람이 흔하게 사용하는 방식이기 때문에 big-endian으로 기록되어 있는 값은 사람이 읽기 쉽고, bit order와 byte order, word order까지 일관성 있다.

Little endian

출처 : 위키피디아2)
 Little-endian은 가장 작은 byte(least significant byte, a.k.a. LSB)가 가장 앞에 나오는 방식을 의미한다. 다시 말해 위의 그림처럼 0x0A0B0C0D를 메모리에 올리고, 메모리를 순서대로 읽으면 0x0D0C0B0A가 나온다는 것이다.
 처음 little-endian을 보면 이게 도대체 뭐하는 짓인가 싶은데 현대의 컴퓨터들은 대부분 little-endian을 이용한다. 왜냐하면 인텔이 머신을 이렇게 만들었으니까. 그렇다면 인텔은 왜 little-endian을 사용할까?

 Little-endian이 컴퓨터에서 컴퓨터의 연산을 아주 미묘하게 빠르게 만들어준다. little-endian을 사용하면 아무런 연산 없이 사이즈가 다른 변수로 casting 할 수 있다. 또한, 덧셈할 때도 little-endian을 사용하는 게 chip을 설계하기 쉽다. 언제나 LSB의 offset이 0으로 같아서 몇 바이트 변수를 더하더라도 언제나 offset이 0인 byte부터 시작하여 더하여 올릴 수 있기 때문에 쉽게 가산기를 구현할 수 있다.

 근데 이게 사람이 메모리를 읽기 어렵게 만들면서까지 성능을 올릴 필요가 있는지는 모르겠다. 개인적인 생각으로는 이렇게까지 해서 성능을 올리고 칩을 간단하게 만들 필요가 있을지 잘 모르겠다. 하지만 인텔이 little-endian을 사용하고 있으니 앞으로도 계속 사용될 듯하다.

1) http://en.wikipedia.org/wiki/Endianness
2) http://en.wikipedia.org/wiki/Endianness

 혹시 endianness에 대해 더 궁금한 것이 있다면 Danny Cohen이 쓴 ON HOLY WARS AND A PLEA FOR PEACE를 읽어보면 도움이 될 것이다.

2014-10-08

[C#] extension method - 존재하는 class 확장하기.

 지난 글에서 이미 존재하는 class에 새로운 함수를 추가해주는 Scala의 implicit class라는 기능을 소개했다. Scala의 implicit class는 라이브러리에서 제공해주어 내가 수정할 수 없는 class를 확장할 때 유용하게 사용되며, 코드의 일관성을 유지하기 위해 원래 class에는 최소한의 기능만을 구현하고, 추가적인 구현은 종류별로 여러 implicit class를 구현하는 방식으로 사용되기도 한다. class의 선언과 구현 detail을 분리할 수 있게 해주는 여러모로 재밌는 기능이다.

 C#은 이런 기능을 extension method라는 방식으로 구현하였다.
 Extension method가 없던 시절(사실 없던 시절은 꽤 오래전 이야기다. Extension method가 유명하지 않아서 그렇지 vs2008부터 들어갔던 기능이다.)에는 확장하고 싶은 class를 상속받아 새로운 class를 만들거나, wrapper class를 만들거나, static method를 만들어 객체를 넘기는 방식으로 구현하여야 했다.
 하지만 이런 방식들은 전부 문제가 있다. 우선 상속으로 새로운 class를 구현하는 방법은 확장할 클래스가 sealed class일 경우 사용하지 못한다. wrapper class를 만드는 방법은 새로 만드는 함수 이외에 다른 함수들을 wrapper class로 연결해 주어야 해서 코드가 매우 boilerplate 해진다. 무엇보다 이 두 방법은 모두 하고자 하는 일에 비해 너무 많은 코드를 작성해야 한다. 마지막 static method를 만드는 방법은 함수를 호출하는 모양이 달라서 코드의 일관성이 없어진다.
 하지만 extension method를 사용하면 이런 문제를 쉽게 해결할 수 있다.

 Extension method를 정의하는 것은 간단하다. 확장하고자 하는 method를 static class의 static method로 정의하면서 첫 번째 인자에 this modifier를 붙이면 된다.
 전체적으로 작업할 코드의 양은 static method를 만들어서 Random 객체를 인자로 넘기는 함수를 만드는 것과 크게 다르지 않다. 단순히 'this'라는 4글자의 modifier가 추가된 정도이다. 이렇게 extension method를 정의하면 첫 번째 parameter가 instance로 쓰이고 나머지 인자들은 함수의 인자로 사용된다.

 C# library 중 extension method를 사용하는 대표적인 라이브러리는 Linq다. Linq는 IEnumerable을 extension method로 확장하여 C#의 IEnumerable을 상속받는 collection들을 전부 일관된 형식으로 사용할 수 있게 해줬다.

 앞에서 말했듯이 extension method라는 기능이 c#에 들어간 지는 매우 오래됐다. 하지만 존재 자체를 모르는 사람들도 많다. 왜냐하면, extension method를 사용하는 것이 장려되지 않기 때문이다. MSDN의 guide 문서에서도 가능하면 상속을 이용하고, 최대한 extension method 사용을 피하라고 말하고 있다. Extension method를 너무 남용하면, 오히려 가독성이 떨어지게 되기 때문이다.
 그래서 Linq처럼 하나의 Domain-specific language(a.k.a. DSL)을 새로 만들어야 하는 경우가 아니면 잘 쓰이지 않는다.

2014-09-30

[Scala] implicit keyword (4) - 마치며

 지난 3번의 posting으로 Scala에서 implicit keyword의 3가지 사용법인 implicit converter, implicit classimplicit parameter에 대해서 알아봤다. 이 3가지 모두 약간씩 다르지만, 명시적으로 적어야 하는 코드를 줄여 코드를 간편하게 만들어 주는 역할을 해준다.
 그 때문에 적절하게 사용하면 verbose 코드를 줄여주어 가독성을 높여줄 뿐 아니라, Scala를 이용하여 domain-specific language(a.k.a. DSL)를 만들기 쉽게 만들어 준다. 실제로 많이 쓰이는 Play framework의 Configuration file이나 route file 또는 Scala와 Java 양쪽에서 많이 쓰이는 SBT는 Scala로 구현한 일종의 DSL로 Scala를 이용하여 static하게 compile 가능하게 되어있다.

 하지만 이런 power는 잘못 사용하면 해가 된다. 뭐 어떤 기능이 안 그러겠는가만은 특히 DSL을 만들기 쉽게 해준다는 것은 결국 기존의 언어가 아닌 새로운 언어를 정의하기 쉽게 된다는 것이고, 너무 많이 사용된 라이브러리는 Scala가 아닌 새로운 언어를 다시 배워야 한다는 문제가 생기기도 한다.
 실제로 SBT가 Scala로 컴파일되는 DSL이지만 필자는 아직도 SBT의 문법을 완벽히 이해하지 못했을 정도로 완전히 새로운 언어가 되었다. 또한, 많은 사람이 Scala를 처음 사용하면서 어려워하는 부분이 implicit converter와 implicit class로 인해 현재 class에 정의되지 않은 함수가 호출되는 것이다.1)

 Implicit keyword는 Scala의 난이도를 높여주는 장애물인 것은 분명하지만, 제대로 이해하고 있으면 새로운 세상을 보여주는 받침대가 된다.

1) 이전 글에서도 말했지만, 이러한 특성 때문에 Scala를 처음 사용할 때는 다른 언어보다 더욱더 IDE가 필요하다. 다시 한 번 IntelliJ IDEA를 추천한다.

Scala implicit keyword

[Scala] implicit keyword (3) - Implicit parameter

 implicit keyword의 3번째 사용법은 implicit parameter와 implicit value를 선언하는 것이다. implicit parameter는 함수를 호출할 때 인자를 생략할 수 있도록 해준다. 정확히는 함수를 호출할 때 인자를 생략하면 자동으로 그 인자를 채워서 실행시켜준다.
 이때 무엇으로 채워주는지를 정하는 규칙은 2가지가 있다. 1)

 첫 번째 규칙은 함수가 호출된 scope에서 prefix 없이 접근할 수 있는 implicit parameter와 같은 타입의 변수 중에 implicit label이 붙은 변수를 사용하는 것이다. 이 규칙에는 2가지 타입의 변수가 해당한다. 하나는 implicit parameter이고, 다른 하나는 해당 스코프에 선언된 implicit modifier가 붙은 local 변수이다. 이때 함수가 호출된 스코프에서 해당하는 규칙에 적용되는 변수가 2개 이상 있으면 "ambiguous implicit values"라는 메시지와 함께 컴파일 에러를 발생시킨다. 여기서 주의해야 할 점은 반드시 implicit parameter와 같은 타입의 변수여야 한다는 것이다. 설령 implicit converter로 변환 가능한 변수가 있어도 이는 implicit parameter로 넘어가지 않는다.

 두 번째 규칙은 companion object에 정의된 변수 중 implicit parameter와 같은 타입으로 선언된 implicit label이 붙은 변수를 사용하는 것이다. 이는 첫 번째 규칙에 해당하는 변수가 없을 때 사용되고, 첫 번째 규칙과 마찬가지로 두 번째 규칙에 해당하는 변수가 2개 이상 있으면 "ambiguous implicit values"라는 메시지와 함께 컴파일 에러를 발생시킨다. implicit converter를 적용하지 않는 것도 첫 번째 규칙과 같다.

 Scala library에서 implicit parameter를 잘 활용하는 대표적인 예는 Future class다. Future class의 method들은 언제나 자신이 실행될 ExecutionContext를 필요로 한다. 원래대로라면 함수를 호출할 때마다 ExecutionContext를 넘겨야 한다. 하지만 이를 implicit parameter로 선언하여 함수를 호출할 때 명시적으로 ExecutionContext를 넘기지 않아도 되도록 하였다.
 이는 boilerplate 코드를 간단하게 만든다는 장점도 있다. 하지만 다른 무엇보다도 서로 다른 symbol을 가지는 Option이나 List의 list comprehension method들2)과 같은 look and feel을 가지게 해준다는 장점이 크다. 같은 look and feel을 가지는 것이 단순히 감성적인 문제 때문에 좋은 게 아니다. Scala는 list comprehension method들에 대해서 for comprehension3)을 사용할 수 있게 해주기 때문에 좋은 것이다.

1) http://www.scala-lang.org/old/node/114
2) map, flatMap, filter 등...
3) http://www.scala-lang.org/old/node/111 for comprehension도 Scala의 중요 feature 중 하나이기 때문에 후에 따로 설명하도록 하겠다.

Scala implicit keyword

2014-09-07

[Scala] implicit keyword (2) - Implicit class

 Scala에서 implicit keyword를 사용하는 또 다른 예제는 Implicit class이다.

 Implicit class는 이미 존재하는 class에 새로운 함수를 추가하여 확장하기 위해서 사용된다.
 정확히 말하면 원래 있던 클래스 자체를 바꾸지는 않고, class를 implicit conversion해서 호출할 수 있는 함수를 추가할 수 있게 해준다.
 Standard library에 있는 implicit class의 대표적인 예는 Duration class들이다.

 위의 예제에서 보듯이 DurationIntDurationDouble이라는 implicit class가 각각  Int와 Double을 확장시켜서 seconds/milliseconds/nanoseconds 같은 method를 추가해서 Duration 객체를 만들 수 있게 해준다.

 이 설명을 보면 지난 글에서 설명해줬던 implicit converter가 해줄 수 있는 일과 크게 다르지 않아 보인다.
 사실 Implicit class는 implicit converter의 syntactic sugar에 불과하다. 내부적으로 implicit class로 정의된 class는 class의 이름과 같은 implicit converter를 같은 scope에 추가한다.
 즉, 아래와 같이 정의된 implicit class는

 아래와 같이 변환된다.


 내부적으로 implicit converter에 해당하는 함수를 정의하는 것이기 때문에 몇 가지 제약사항이 있다.
 첫 번째 제약사항으로 implicit class는 trait이나 classobject 안에 정의되어야 한다.
 이는 함수의 정의가 trait이나 class나 object 안에 정의되어야 하기 때문이다. 그래서 보통 implicit class들은 package object 안에 정의된다.
 두 번째 제약사항은 non-implicit인 parameter가 반드시 1개인 constructor를 가지고 있어야 하는 것이다.
 implicit parameter1)는 여러 개 가질 수 있으니 그 외에 넘기고 싶은 인자가 있으면 implicit parameter로 넘겨야 한다.
 세 번째 제약사항은 implicit class가 선언되는 scope 안에 implicit class와 같은 이름의 무언가가 있을 수 없다는 것이다.
 이것도 내부적으로 implicit converter에 해당하는 함수가 만들어지기 때문이다. 이 때문에 내부적으로 class와 같은 이름의 object를 만드는 case class는 implicit class가 될 수 없다.

1) implicit parameter에 대해서는 다음 글에서 설명하도록 하겠다.

Scala implicit keyword

2014-09-06

[Scala] implicit keyword (1) - implicit converter

 Implicit converter가 하는 일은 이름 그대로 value를 적절한 type으로 implicit하게 convert하는 것이다.
 Scala는 기본적으로 strong typed language이기 때문에 implicit conversion을 지원하지 않는다. 함수를 호출할 때 함수의 symbol에 맞지 않으면 바로 컴파일에러가 발생한다.
 implicit conversion을 하기 위해서는 해당 scope 안에 implicit converter를 구현해두어야 한다.

 Implicit converter의 선언은 다음과 같다.

 Implicit converter는 unary function에 implicit keyword를 붙여주는 것으로 정의된다.
 이렇게 Implicit converter를 구현해두면, sizeToRectangle 함수가 정의되어 있는 scope에서는 Size 객체가 Rectangle 객체로 implicit conversion이 가능해진다.

 예를 들어 Scala에서 자주 쓰이는 Option이라는 class를 보자.
 Option은 c#의 nullable과 유사한 type으로 원소가 없을 수 있는 객체이다. 이는 다른 관점에서 보면 최대 원소가 1개인 collection이라고 볼 수 있고, Option을 사용할 때 collection처럼 list comprehensive method1)를 사용하는 것을 권장한다.
 하지만 Option코드를 보면 Option은 Iterable이나 Traversable을 상속받지 않았고, 일부 함수를 제외하고 collection처럼 이용는데 필요한 모든 함수를 가지고 있지도 않다.
 하지만 Option을 Iterable로 변환해주는 option2Iterable이라는 implicit converter가 있기 때문에 iterable이 필요한 경우 자동으로 convert해서 넘겨준다.

 Implicit converter에 대응되는 개념이 다른 언어에 없는 것은 아니다.
 C++에서는 unary constructor를 구현하거나 assignment operator나 type case operator를 구현하여 implicit conversion을 구현한다. C#은 conversion operator를 구현하여 implicit conversion을 구현할 수 있다.
 하지만 implicit converter는 이들보다 더 표현력 있고 확장성 있는 개념이다.

 implicit converter가 다른 언어의 implicit conversion보다 표현력 있고 확장성 있다는 것을 보여주는 좋은 예제가 RichInt다.
 위의 예제는 Scala가 기본으로 제공해주는 Int타입의 확장함수 들이다. 하지만 Int 구현체에는 위의 함수들에 대한 구현이 없다. 왜냐하면 Scala는 정책상으로 Int class내에는 primitive operator2)에 대해서만 구현하도록 하고 있기 때문이다.
 그렇다면 위의 함수들은 어디에 정의되어 있을까?
 primitive operator가 아닌 함수들은 RichInt에 구현되어 있다. Int에서 RichInt로 implicit convert해주는 함수가 정의되어 있기 때문에 Int로 선언된 변수들에 대해서도 RichInt에 정의된 함수를 쓸 수 있는 것이다.

 여기서 Scala의 implicit converter가 C++에서 제공하는 implicit conversion보다 가지는 장점을 알 수 있다. C++에서 type case operator나 unary constructor를 구현하거나 C#에서 conversion operator를 구현하더라도 implicit type casting을 해서 함수의 symbol을 찾지 않는다.
 다시 말해서  C++이었다면 RichInt와 implicit conversion을 위한 operator들이 구현되어 있다고 해도 symbol을 찾기 위해서는 아래와 같은 추가 작업이 필요하다.
 하지만 Scala는 instance의 현재 class에 해당하는 symbol의 함수가 없어도 현재 scope에 있는 implicit converter를 1번 적용해서 나오는 class에서 해당하는 symbol의 함수를 찾을 수 있으면, 해당하는 함수를 부른다.

 또 기존의 implicit conversion에는 큰 문제가 있는데, implicit conversion의 정의가 class 내에 정의되어야 한다는 것이다. 즉, A에서 B로 가는 implicit conversion의 구현을 하고 싶으면 A와 B 둘 중 최소한 하나는 내가 수정할 수 있는 class여야 한다는 것이다.
 library에서 제공해주는 class 간의 implicit conversion을 구현할 방법이 없고 이 때문에 쓸데없이 Wrapper class를 만들어야 하는 일이 종종 발생한다.
 게다가 class에 implicit conversion이 정의되기 때문에 특정한 함수에서만 implicit conversion을 허용하거나, 반대로 특정 함수에서는 implicit conversion을 허용하지 않는 것을 할 수 없다.

 물론 이런 강대한 표현력은 가끔 과다해서 단점이 되기도 한다. 어떤 객체의 함수가 그 객체의 class 정의 밖에 존재하는 것이기 때문에 코드를 읽는데 어려움을 준다. 가끔은 다른 부분에서 자연스럽게 썼던 함수를 implicit converter를 import하지 않아서 사용하지 못하는 일도 있다.
 하지만 이 정도 문제는 좋은 IDE(Integrated Development Environment)가 있으면 쉽게 해결되는 문제다. 3)
 너무 과하지 않을 정도로 적당히4) 사용하면 한층 더 확장된 세계를 볼 수 있게 된다.

1) filter, map, flatMap ...
2) unary -, unary ~, +, * ...
3) 다시 한 번 IntelliJ IDEA를 추천한다.
4) 프로그래밍 언어는 결국 도구다. 어떤 도구를 쓰더라도 도구를 어떻게 사용할지는 그 도구를 사용하는 사람, 다시 말해 프로그래머가 잘 결정해야 한다.

Scala implicit keyword

2014-09-05

[Scala] implicit keyword (0)

 Scala의 가장 인상적인 keyword를 말하라고 하면 많은 사람이 implicit을 꼽을 것이다.
 implicit은 scala의 확장성에 무한한 힘을 주는 keyword임과 동시에 코드를 읽을 때 헬 게이트를 여는 주범이기도 하다. 1)

 implicit keywod는 Implicit converter, Implicit class, Implicit parameter의 3가지 목적으로 이용된다.
 앞으로 3번에 걸쳐서 각각에 대해 설명하도록 하겠다.

1) 개인적으로 코딩할 때 대부분 언어를 vim으로 작업해왔지만, scala를 배우면서 IntelliJ라는 IDE를 사용하기 시작했다.

Scala implicit keyword

2014-09-03

[MongoDB] ObjectId에 대해서

 지난번에 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에 해당하는 mongos2)config server의 data를 토대로 어떤 shard가 어느 범위의 값을 가졌는지를 저장하고 있다가 query를 요청할 때 자신이 필요로 하는 shard에게만 요청한다.
 따라서 shard key에 해당하는 data가 미완성인 상태로 서버에 저장하도록 요청할 수 없고, client가 ObjectId를 생성하여 값을 저장하도록 요청한다. 3)

ObjectId의 구성

 ObjectId는 크게 4부분으로 구성되어 있다.

 ObjectId의 처음 4 byte는 시간정보를 담고 있다.
 1970년 1월 1일 0시 0분 0초를 0으로 해서 4 byte의 정보를 담고 있으므로 2106년 2월 7일 6시 28분 15초까지 표현할 수 있다.

 다음 3 byte는 machine 별로 다른 고유한 id를 담고 있다.
 이것은 driver의 구현에 따라 다르지만 보통 mac address + ip address 혹은 hostname을 md5 hash한 값의 처음 3 byte를 따온다. 하지만 driver 별로 구현이 다르다. 같은 driver를 사용할 때 같은 머신이 같은 값을 가지는 방식 중에서 최대한 랜덤하게 분포하는 방식으로 구현된다.

 다음 2 byte는 process id 정보를 담는다.
 32-bit 머신에서 process id의 최댓값은 2^15이라서 2 byte로 문제없이 표현된다. 하지만 64-bit 머신에서는 최댓값이 2^22이 가능하므로 해당 범위에 들어가지 못한다. 이러면 어떻게 처리할지 문서에서 찾지 못했다. 하지만 실제로 process id가 필요한 것이 아니고 ObjectId의 유일함을 보장하기 위해 사용되는 bit들이므로 정확한 process id를 유지하고 있을 이유는 없다.
 때문에 mongodb가 가지고 있는 bson 구현체에서는 process id가 2 byte보다 크면 하위 2 byte만을 pid bits에 저장하고, 상위 1 byte는 machine number에 반영하지만 드라이버에 따라서는 하위 2 byte만을 process id로 저장하고 상위 1 byte는 버리는 드라이버도 있다.

 마지막 3 byte는 mongos가 생성한 값으로 inc라고 부른다.
 이것도 driver 별로 구현이 다르기는 하지만 보통 random 한 숫자에서 시작하여 auto increment되는 숫자다. 일부 구현체들은 0부터 시작하는 auto increment 되는 숫자를 쓰기도 하고, 단순히 random하게 고른 숫자를 쓰기도 하는데 이렇게 사용하면 원래의 방식보다 충돌할 확률이 늘어난다.

ObjectId의 unique성

 따라서 독립적으로 생성된 두 ObjectId가 같을 확률은 매우 낮다.
 일단 최소 조건은 두 ObjectId가 1초 내에 생성된 ObjectId이어야 한다. 4)
 그리고 같은 machine hash를 가지는 머신에서 같은 process id로 뜬 process id로 뜬 mongos에서 생성된 ObjectId이어야 한다.

 같은 머신에서 생성된 ObjectId는 기본적으로 이런 조건을 만족한다. 하지만 3 byte의 inc가 같은 머신에서 생성한 ObjectId가 중복되는 것을 막아준다. 하나의 mongos에서 1초에 4,096개 이상의 ObjectId를 생성하는 불가능한 시나리오가 아니면 중복된 ObjectId가 중복되지 않는다. 5)

 서로 다른 머신의 경우 같은 machine id와 process id를 가지는 확률은 매우 낮다.
 machine id로 3 byte를 사용하므로 machine id가 같을 확률은 1/4096이다. 거기에 같은 inc를 가질 확률 1/4096까지 더해져 실제로 machine id가 같은 mongos에서 같은 inc를 가질 확률은 천만분의 일이다. 여기에 서로 같은 process id로 process가 실행될 확률까지 더해져서 실제 확률은 더 낮아진다.

 하지만 이는 어디까지 확률적인 문제고 매우 낮은 확률로 혹은 의도적으로 같은 ObjectId가 생성되는 일도 있다. 이런 경우는 shard에서 E11000 duplicated key error를 발생시키고 insert가 실패한다.


1) 보통은 auto increment를 사용한다.
2) 혹은 query router
3) 사실 shard key 이외에 data가 미완성인 상태로 요청하여 server에서 완성하도록 하는 default 같은 방식을 지원하지 않는다. MongoDB는 자신이 요청받은 data를 그대로 저장한다.
4) 혹은 1970년 이전에 생성된 ObjectId거나 2106년 이후에 생성된 ObjectId이어야 하는데 MongoDB 개발자들은 이럴 경우는 없을 거라고 가정하고 있다.
5) 이것은 어디까지나 정상적인 드라이버에서의 얘기다. 만약 위에서 말한 대로 단순히 랜덤하게 생성하는 driver라면 4,096개를 생성하지 않더라도 1/4096의 확률로 충돌이 발생한다.

2014-08-11

[ZooKeeper] (2) zookeeper server는 어떠게 구성되는가?

ensemble

 테스트 환경이나 개발 환경에서는 stand-alone mode를 이용하여 한 대의 ZooKeeper 서버만을 실행하여 사용할 수 있지만, 이렇게 되면 ZooKeeper의 큰 장점인 availability를 크게 해치게 된다.
 그래서 실제 배포 환경에서는 보통 최소 3대의 ZooKeeper 서버를 cluster로 묶어서 배포하는 것이 일반적이다. 이때 ZooKeeper cluster를 ensemble이라고 부른다.

 같은 ZooKeeper ensemble에 포함된 서버는 모두 같은 data를 저장함으로써 특정 서버가 SPOF(Single Point Of Failure)가 되는 일을 막는다.
 그렇다면 분산 된 환경에서 모든 서버에 같은 data가 저장되는 것을 어떻게 보장해줄 수 있을까?

Leader

 ZooKeeper ensemble에는 외부에는 공개되지 않지만, 내부적으로 사용되는 leader가 있다.
 client는 ensemble에 포함된 어떤 서버에게도 query를 날릴 수 있다.
 서버는 query를 받으면 이 query를 ensemble의 leader에게 전달해 주고, leader가 모든 서버에 같은 data가 저장되는 것을 보장해 준다.

Two phase commit

 ZooKeeper는 모든 follower가 leader와 같은 data를 가지고 있는 것을 보장하기 위하여 간략화된 two phase commit을 사용한다.
 leader는 request에 대해서 follower에 해당하는 server들에게 propose message를 보낸다.

 propose message를 받은 follower는 해당 proposal에 대해서 local disk에 commit log를 저장하고 ack message를 보낸다.

 leader는 Follower로부터 받은 ack이 quorum(보통은 n/2 + 1이다.)을 넘으면 모든 Follower들에게 Commit을 날린다. Commit을 받으면 zookeeper는 commit log에 저장되어 있던 커밋을 실행하여 znode를 갱신한다.
 Follower들은 이후 자신에게 들어오는 read 요청에 대하여 request가 반영된 값을 보여준다. 이 순간 ZooKeeper 서버들은 각각 다른 값을 가지고 있을 수 있고, 이 때문에 ZooKeeper는 eventual consistency만을 보장한다고 한다.
http://zookeeper.apache.org/doc/r3.4.6/zookeeperInternals.html#sc_activeMessaging를 수정.
지금 document에 올라가 있는 그림은 틀렸다. 왼쪽 아래에 있는 commit의 방향이 반대로 되어 있다.

 일반적인 two phase coommit과는 다르게 propose에 대해서 agree/abort 하거나 commit/rollback을 하고 그 결과에 대해서 받는 과정이 없이 언제나 성공할 것으로 생각하고 ack을 기다린다.

 이때 leader는 보내는 모든 proposal에 대해 자신이 받은 순서대로 transaction id를 매겨서 처리하기 때문에 sequential consistency를 보장할 수 있다.

Leader election

 이렇게 leader를 통해서 consistency와 동기화가 보장된다면 leader가 또 다른 SPOF가 되는 것은 아닐까 하는 의문이 든다.
 ZooKeeper는 leader가 또 다른 SPOF가 되는 것을 막기 위하여, leader에 문제가 생겼을 때, 내부적으로 leader를 선출하는 mechanism을 가지고 있다.
 ZooKeeper의 leader election mechanism에 대해서는 다음에 설명하도록 하겠다.

2014-08-09

[Graphics] Sprite는 무엇인가

 Sprite는 렌더링 파이프라인을 타지 않고 target(screen이든 FBO든)에 직접 그림을 그릴 수 있게 해주는 2D bitmap을 의미한다. Sprite에 그린 그림은 rendering pipeline을 타지 않기 때문에 transform 이나 다른 효과들과 독립적으로 화면에 보이게 된다.
 이러한 특성 때문에 3D게임에서 UI를 그릴 때 이용된다.

2014-08-08

Service Provider Interface란?

 Service Provider Interface(a.k.a. SPI)는 extensible한 코드를 작성하기 위해서 Java 진영에서 주로 쓰이는 방법이다.

 보통의 API들은 구현체의 Interface를 외부로 공개하여 구현체를 사용하는 주체가 자신의 환경에 맞게 사용한다.
 반면에 SPI는 사용자가 구현해야 할 Interface를 정의한다.
 SPI 사용자(보통은 driver vendor)가 자신의 환경에 맞는 구현체를 직접 정의하여 제공하면 SPI를 제공해준 service에서는 제공 받은 구현체를 불러다 사용하는 형태로 동작한다.

 대표적인 예로 Java Cryptography Extension가 있다.

2014-08-07

[MongoDB] Sharding (3) - shard key

 MongoDB는 auto sharding을 해주기 때문에 사용자가 어떤 shard에 저장할지 신경 쓰지 않아도 된다.
 그렇다면 어떤 document를 어떤 shard에 저장할지 어떻게 결정할까?

Shard Key

 MongoDB는 shard key를 이용하여 구분한다.
 별도로 지정하지 않았다면 shard key는 object ID(_id)이다. 하지만 해당 collection에 모든 document에 존재하는 field index 혹은 compound field index라면 shard key로 지정할 수 있다.
 하지만 compound index는 shard key로 지정할 수 없다.

Shard key의 제약 조건

 shard key에는 몇 가지 제약이 있다. 우선 shard key는 512 byte를 넘을 수 없다.
 하지만 이는 시스템적 제약조건이지 실제로 512 byte를 넘는 field를 shard key로 만들 일은 거의 생기지 않는다. (사실 512 byte가 넘는 index를 지정하는 일도 거의 생기지 않는다.)

 또한 한번 sharding한 collection에 shard key는 변경할 수 없다.
 만약 변경하고 싶다면 새 collection을 만들어 shard key를 설정하고 collection 전체를 복사해서 새로운 collection을 만들어야 한다.

 그다음 제약은 꽤 까다로운데 shard key로 지정된 field의 value는 변경할 수 없다. Update 때 document를 다른 shard로 옮겨야 할 일이 없도록 하기 위해서다. 변경할 일 없는 field들만을 shard key로 지정해야 한다.

 특별히 튜닝해야 할 일이 없다면 기본값인 object id를 shard key로 사용하는 것을 추천한다.

MongoDB Sharding

2014-08-06

[MongoDB] Sharding (2) - Primary shard

 MongoDB는 collection 별로 sharding을 할지 안 할지를 결정할 수 있다.
 이때 sharding되지 않은 collection들이 저장되는 shard를 "Primary Shard"라고 부른다.

 sharding되지 않은 data들이 들어 있기 때문에 이는 Single point of failure이다.

 혹시 primary shard인 머신을 down시켜야 한다면 movePrimary command로 다른 shard를 primary로 만들고 down시켜야 한다. movePrimary는 sharding되지 않은 collection들을 모두 copy해가기 때문에 무거운 작업이다. 될 수 있으면 movePrimary를 호출할 상황이 오지 않도록 노력해야 한다.

 별도로 primary shard를 정하지 않았다면, 가장 먼저 cluster에 붙은 shard가 primary shard가 된다.


 p.s. 솔직히 말하면 난 아직 primary shard를 사용해야 하는 경우를 보지 못했다.
 아마 내 MongoDB 튜닝 경험이 적어서 그럴 것이다.
 혹시 primary shard를 이용해야 했던 경험이 있다면 공유해주기 바란다.

MongoDB Sharding

2014-08-05

[MongoDB] Sharding (1) - Sharded cluster의 구성

Scale up과 Scale out

 한 머신이 처리하지 못할 정도로 부하가 들어왔을 때의 해결책을 크게 scale up과 scale out 두가지로 분류한다.
 Scale up은 머신 자체의 성능을 올리는 것으로 vertical scaling이라고 불린다.
 효과는 확실하지만, scale out보다 비용이 많이 든다.
 Scale out은 기존의 머신을 그대로 두고 새로운 머신을 추가하는 방식이다. 다른 말로는 horizontal scaling이라고 부른다.

 Sharding은 scale out의 일종으로 data를 여러 서버에 나눠서 저장하는 방식을 말한다.
 이때 data가 저장된 서버들을 shard라고 부르고 shard들을 포함한 몽고디비 환경을 sharded cluster라고 부른다.

Sharded cluster

 MongoDB의 sharded cluster는 크게 3가지 군으로 나뉜다.
  1. Shards
  2. Config Servers
  3. Query Routers

Shard

 Shard는 실제 data가 저장되는 곳이다. MongoDB는 보통 하나의 data를 세 군데의 shard에 저장하여 특정 shard가 Single point of failure(a.k.a. SPOF)가 되는 것을 막는다. Scaling을 위해 수를 줄이고 늘리고 하는 것이 이 shard이다.

Config Server

 Config server에는 shard된 data들의 metadata. 즉, 어떤 data가 어떤 shard들에 저장되어 있는지에 관한 정보가 저장되어 있다.
 하나의 Config server만을 이용할 수도 있지만 보통 3개의 Config server를 사용하여 특정 Config server가 SPOF가 되는 것을 막는다.

 Config server에 저장되는 정보는 two-phase commit으로 저장하여 consistency를 보장해준다.

 Config server는 shard와 달리 scaling의 대상이 아니다. 보통 3개의 Config Server를 실행하는 것을 기본으로 하고, 만약 하나의 config server라도 name 혹은 address가 변하면 모든 mongod(shard + config server)와 mongos(query router)를 재 시작하여야 한다. 혹시 address가 변경될 것을 대비하여 CNAME을 사용하는 것을 권장한다.

Query Router

 Query router는 Query router를 실행시키는 binary의 이름을 따서 mongos라고 부르기도 한다.
 Query router는 일종의 client interface이다. client는 data가 들어 있는 shard에게 직접 command를 날릴 수 없고, router를 통해서만 요청할 수 있다. Router는 client가 요청한 command를 어떤 shard에게 명령해야 하는지를 보고 필요한 shard에게 명령을 날리고 수행된 결과를 취합한다.
 Router가 어떤 shard에 어떤 data가 들어 있는지 알기 위해 항상 Config server에 물어보는 것은 아니다. Router는 Config server에 저장된 metadata를 cache 해둔다.

MongoDB Sharding

2014-07-28

[Scala] sealed modifier

 Scala에는 sealed라는 독특한 modifier가 있다.

 같은 방식으로 사용하는데, sealed라는 modifier를 붙인 class는 선언된 파일 안에서만 상속받을 수 있다. 선언된 파일이 아닌 다른 파일에서는 사용할 수는 있지만 상속받으려고 한다면 컴파일 에러가 발생한다.
 다만, sealed의 자식은 sealed가 아니어서 주로 final modifier와 함께 쓰인다.

 Scala library 중 sealed modifier를 사용하는 대표적인 예제는 OptionTry다.
 Option은 Some과 None 2개의 자식이 있고, Try는 Success와 Failure 2개의 자식을 가지고 있다.
 Option과 Try는 sealed로 선언되어 같은 파일에서 선언한 Some, None과 Success, Failure 이외에는 자식을 가지지 못하게 하고, Some, Success, Failure는 final class로 None은 상속할 수 없는 object로 선언하여 사용자가 추가로 상속받을 방법을 막아놓았다.

2014-07-05

morse code 입력기

 자고 일어나보니 친구들이 http://morsecode.me/라는 사이트에서 놀았던 로그가 있었다.
 뭐하는 사이트인가 보니 키보드 혹은 마우스로 morse code를 입력하는 사이트였다.
 하.... 이 마니악한 인간들이라는 생각이 먼저 들었는데 조금 놀다 보니까 이게 은근히 꿀잼이다. 옛날에 들었던 논리설계실험이라는 수업에서 프로젝트 과제로 모스 기계 만들던 생각도 나고....

 모스기계의 원리는 간단하다. 어떤 임의의 unit time이 있고, one unit만큼의 signal은 short mark(기호상으로는 ., 발음은 dot 혹은 dit), 그 시간의 3배의 시간 동안 signal은 long mark(기호로는 -, 발음은 dash 혹은 dah)라고 한다. unit time만큼 입력을 쉬고 있는 것을 short gap, 3 unit times만큼 쉬는 것을 long gap이라고 하여, short gap으로 이어진 mark는 같은 character로 인식하고, character가 달라지면 medium gap이라고 부른다. word가 달라지는 건 long gap(unit time의 7배)으로 구분한다.
 문제는 원리는 간단하지만, 모스부호를 외우는 게 장난이 아니고, 외웠다고 하더라도 실수 없이 치는 게 쉬운 일이 아니다.
 그래서 translator를 만들면 조금 더 쉽게 작업할 수 있지 않을까 하여 F12를 눌러 콘솔 창을 열었다.
 처음에는 사이트가 jQuery로 짜여 있는 것을 보고 jQuery의 event trigger를 이용하면 되지 않을까 해서 시도해봤는데 trigger가 안된다. 왜지? 라고 생각하면서 코드를 봤는데, 모스부호를 입력하는데 가장 중요한 object인 Morser의 instance인 me가 그대로 노출되어 있었다.

 그래서 만들어진 스크립트는 다음과 같다.


 근데 아무리 고생스러워도 이런 건 직접 입력하면서 노는 게 더 재밌다. 스크립트 이용해서 하니까 갑자기 재미없어진다.


p.s. 코드에
* The license is closed source, proprietary, and
* prohibitive. For now. When the code, software,
* community and support for morsecode.me reaches
* maturity, I am likely to open source this project. 
라는 주석이 있는 걸 보면, 언제 바뀔지 모른다.

2014-07-03

WeeChat에서 SSL 사용하기

 지금까지 irc 클라이언트로 irssi를 쓰고 있었다.
 irssi의 기능이나 여기서 제공해주는 스크립트들에 불만이 있는 건 아니지만, perl 스크립트만 support 한다는 것이 결국 문제가 되어(원래 계획은 이 기회에 perl을 배운다는 것이었는데 굳이 irc하나 때문에 perl을 새로 배우는건 손해처럼 보였다.) WeeChat으로 갈아타기로 했다.

 WeeChat은 irssi와 매우 비슷한 look and feel의 UI를 가졌지만, extensible 하다는 것을 장점으로 내세우며 Perl은 물론이고 Lua, Ruby, Python같은 유명한 스크립트 언어들이나 TCL, scheme 같은 교육용으로 자주 쓰이는 언어까지 지원해준다.
 게다가 irssi에서 많이 쓰이는 스크립트(trackbar, nickcolor 등)은 builtin기능으로 제공해 주고, 선택할 수 있는 configuration들도 irssi보다 많다.

 다양한 스크립터언어의 지원이 마음에 들어서 WeeChat으로 옮기기로 했지만, 너무 많은 configuration이 있는 것 때문에 설정하는 게 irssi보다 복잡하다.

 이하 SSL을 사용하기 위해 삽질한 내용들이다.
 WeeChat의 기본 설정 값은 ssl을 사용하지 않아서 따로 사용하도록 설정해 줘야 한다.

 문제는
/set server.ssl = on
을 해줘도 ssl을 사용해서 freenode채널에 접속할 수 없다.
 이에 대해서는 WeeChat FAQ에 해결책이 나온다.
 WeeChat에서는 irssi와는 다르게 Diffie-Hellman key의 길이를 설정해 줄 수 있는데, WeeChat의 기본값은 2048이고 freenode에서 사용하는 key size는 1024다.
/set server.ssl_dhkey_size = 1024
 도 해줘야 한다.

 freenode는 워낙 유명한 채널이라서 FAQ에도 나와있고 금방 해결되었다.
 문제는 국내에서 많이 쓰이는 urric라는 서버에서도 SSL 인증서의 문제로 hand shake가 실패했다고 나오는데, uriirc사용자 중에서 weechat 사용자가 얼마 없는지 이에 대해 해결하는 방법은 나와있지 않았다.
 uriirc 서버의 SSL인증서가 가지는 문제는 크게 3가지이다.

  1. diffie-Hellman key size가 1024이다.
  2. 인증서가 기한 만료되었다.
  3. 인증서의 hostname이 irc.uriirc.org가 아니다.
 key size는 위와 같은 방법으로 해결하면 되는데, 공식적으로 관리되는 서버가 아니라서 그런지 인증서에 이래저래 문제가 많다.
 WeeChat 매뉴얼을 읽어보니 WeeChat은 기본적으로 SSL verify기능이 켜져있고, configuration을 통해서 꺼야한다.
 따라서
/set server.ssl_verify = off
를 해야 한다.

 irssi에서도 SSL verify 기능이 있지만, 이쪽은 default가 off라서 irssi로 접속할 때는 SSL 인증서에 문제가 있는지 모르고 써왔던 것이다.

 초기 세팅을 위해 검색하느라 시간을 조금 쓰기는 했지만, 이것저것 설정해줘야 해서 조금 더 안전하게 사용할 수 있는 것 같은 착각(?)이 든다.

2014-06-19

Rhino - JavaScript framework

 Rhino는 mozilla에서 개발한 Java로 구현된 JavaScript engine이다.

 과거 Netscape에서 Java로 구현된 navigator를 구현하려는 시도를 한 적이 있는데, 이때 사용했던 JavaScript engine이 Rhino engine의 전신이 된다.
 Javagator라고 불리던 이 프로젝트는 JavaScript를 Java byte code로 컴파일하여 실행하기 때문에 당시에 있던 다른 브라우저보다 빠른 성능을 낼 수 있을것을 기대했지만, JVM 자체의 성능 이슈와 다른 여러가지 상황때문에 중간에 중단되었지만, 일부 회사들의 지원으로 JavaScript framework은 분리되어 Rhino가 되었다.

 Rhino의 가장 큰 특징은 내부적으로 Reflection을 이용하여 JavaScript 코드에서 Java class를 그대로 가져다 쓸 수 있다는 것이다.
 또한 Java구현체를 그대로 사용할 수 있기 때문에, JavaScript engine 중에서는 특이하게 multi thread support가 된다는 특징을 가진다.

 JVM이 꾸준히 성장하여 많은 성능 개선을 이루었지만, WebKit이 사용하는 JSC(JavaScript Core)나 Google이 개발한 v8 engine도 내부적으로 JavaScript를 compile하기 때문에 Rhino가 가지는 성능상의 이점은 없다. 사실상 v8이나 jsc보다 느리다.

 성능상에 이점은 없지만, 반드시 Java를 사용해야 하거나 multi-core support가 필요한 일부 환경에서는 Rhino engine을 사용하는 경우가 있다. 하지만 이 중에 이름만 들어서 알만한 유명한 프로젝트는 없다.
 Rhino를 사용하는 가장 유명한 구현체는 RingoJS로 보인다.

2014-06-12

[java] shutdown hook 사용하기

 프로그램을 작성하다 보면, 프로세스가 종료될 때 반드시 실행해야 하는 코드가 나온다.
 exit 포인트가 하나뿐인 프로그램이라면, exit 하기 전에 실행하면 되지만, 보통 코드를 그렇게 작성하지 않기 때문에 Java에서는 Runtime의 shutdown hook을 이용한다.

 사용하는 방법은 간단하다. Runtime의 addShutdownHook을 이용해 필요한 hook을 추가하면 된다.
 process가 종료되기 시작하면 프로세스 내의 non-daemon thread들이 종료되기 시작한다. 모든 non-daemon thread들이 종료되면, 등록된 hook들이 실행된다.

 shutdown hook은 C나 C++의 atexit과 비슷한 역할을 하지만, 함수를 등록하는 것이 아닌 Thread를 등록한다는 것이 다르다.
 또한, atexit은 등록된 함수가 stack에 쌓여서 LIFO로 동작한다. 하지만 Java의 addShutdownHook은 등록된 Thread가 순서 없이 실행되기 시작하여 병렬적으로 돌아간다.
 hook들이 병렬적으로 돌아가기 때문에 hook의 순서를 보장하고 싶다면 같은 thread에서 실행되도록 hook을 작성해야 한다.
 물론, 일반적인 병렬 프로그래밍처럼 lock을 이용하여 순서를 보장할 수도 있다. 하지만, shutdown hook은 프로세스가 종료될 때 반드시 실행되고, hook이 종료될 때까지 정상적으로는 프로세스가 종료되지 않기 때문에, shutdown hook에서 dead lock이 발생하면 프로세스가 종료되지 않는다. 이러한 이유로 shutdown hook에서는 가능하면 lock을 사용하지 않고 로직을 작성하는 것을 권장한다.

 removeShutdownHook이라는 API도 존재한다. 이 API를 이용하여 더는 필요 없어진 hook을 제거할 수 있다. atexit을 사용할 때는 필요 없어진 hook을 제거하기 위해서는 전역 flag를 두어 flag를 설정하거나, custom한 stack을 구현해야 하는데 이럴 필요가 없어진 것이다.

 shutdown hook은 Thread이기 때문에 hook을 달아놓으면 종료될 때까지 idle한 Thread가 하나 생기는 것이므로 이것이 overhead가 되지 않을까 걱정하는 사람도 있다. 기본적으로 이것이 틀린 생각은 아니다. 하지만 대부분의 구현체에서 Thread는 실제로 시작하기 전까지는 thread stack을 할당하지 않기 때문에 걱정할 필요 없다. 물론 아주 약간의 overhead도 감당할 수 없는 embedded 환경에서는 이런 부분도 고려해야 하지만, desktop application이나 server 같은 환경에서 고려해야 할 문제는 아니다.

 shutdown hook을 사용할 때 조심해야 할 것이 2가지 있다.

 우선 shutdown hook은 최대한 짧게 작성되어야 한다.
 만약 프로세스의 종료가 머신의 종료에 의한 것이라면 JVM은 프로세스가 완전히 종료되는 것을 기다리지 않고, 일정 시간이 지난 후 종료한다. 이 때문에 shutdown hook이 실행되기 전에 종료될 수 도 있고, shutdown hook이 실행되는 도중에 종료될 수도 있다.

 두 번째로 shutdown hook을 사용하는데 조심해야 하는 것은 shutdown hook은 반드시 실행되는 것이 아니라는 것이다.
 shutdown hook은 정상적인 종료에서만 호출된다는 것이다.
 따라서 비정상적인 종료에서도 반드시 수행돼야 하는 일은 하나의 Java application만으로는 처리할 수 없다. 이런 것은 보통 Java application을 실행하는 스크립트를 작성하거나, Java application을 실행하는 application을 작성하여 처리한다.

 여기서 비정상인 종료라는 것은 에러코드를 반환하는 것이 아니다.
 System.exit으로 호출되는 경우 status 코드를 무엇으로 반환하든지 정상 종료이다.
 SIGTERM을 받아서 종료되는 경우에도 정상종료이다.
 SIGTERM을 받은 프로세스는 정상적으로 shutdown hook을 호출하고 Thread를 정리하고 죽는다.
 handle되지 않은 Exception이 발생하여 process가 종료되는 경우도 Java는 정상종료로 취급한다.

 위의 3가지 경우에는 문제없이 shutdown hook이 호출된다.

 그렇다면 비정상종료는 무엇일까?
 우선 대표적인 비정상 종료는 halt다.
 halt는 프로세스를 강제종료하는 것으로 실행되는 즉시 종료되므로 shutdown hook이 불리지 않는다.
 프로세스가 SIGKILL을 받는 경우도 비정상 종료이다. SIGTERM과는 달리 SIGKILL을 받으면 리소스를 해제하거나 shutdown hook이 불리지 않고 바로 종료된다.
 마지막으로 JVM에 문제가 발생해서 죽는 일도 있다.
 JVM에 문제가 발생하는 것은 매우 드문 일이다. 하지만 JVM 코드에 버그가 있어서 죽거나, JNI를 사용하다가 문제가 생기는 경우, shutdown hook이 호출되지 않는다.

2014-06-10

[Design Pattern] Loan pattern - resource를 안전하게 사용하기

 언젠가 썼던 글에서도 설명했듯이 C++에서는 RAII를 이용하여 Resource의 안전한 해제를 보장하는 것을 넘어 control flow를 제어하는 역할까지 해준다.
 하지만 Garbage Collection을 사용하는 C#이나 Java 같은 언어에서는 언제 메모리가 해제될지 모르기 때문에 RAII pattern을 사용할 수 없다. 그래서 코드의 실행을 보장하기 위하여 finally 구문이 생기게 된 것이다.

 try finally를 사용하는 일반적인 방법은 아래와 같다.

 exception이 발생할 수 있으면 try 구문으로 감싸고 반드시 실행시켜야 하는 코드를 finally에 두는 것이다.

 하지만 위의 코드는 딱 보기에도 재사용성이 떨어진다.
 다른 동작을 하기 위해서는 언제나 try / catch를 써야 해서 boilerplate한 코드가 반복되기도 한다.
 이를 해결하는 방법은 없을까?

 Scala에서는 이를 해결하기 위하여 resource를 빌려주는 방식을 자주 이용한다.
 resource의 management를 하는 함수(lender)가 있고, resource를 사용하는 함수(lendee)에게 빌려주어 잠시 사용하게 해주는 것이다.
 이를 이용하여 API의 encapsulation과 reusability를 올릴 수 있다.
 우선은 다음 예제를 보자.

 위의 예시에서는 executeSql이라는 함수가 connection string과 Statement를 인자로 받는 Function을 인자로 받는다(말은 복잡한데 실제로 복잡한건 아닌데....... 말로 설명하려니 복잡해졌다.).
 첫 번째 인자로 받은 connection string을 이용하여 Statement라는 resource를 만들어 관리하게 된다. 즉, executeSql이 lender가 되는 것이다.
 그리고 두 번째 인자인 Statement를 인자로 받는 Function을 landee로 삼아 자신이 만든 Statement를 빌려주어 원하는 작업을 수행하게 한다.

 이런 pattern을 resource를 관리하는 lender와 빌려서 사용하는 lendee로 나뉘기 때문에 lender-lendee pattern으로 부르는 사람도 있지만, 보통은 loan pattern이라고 부른다.

 loan pattern은 resource의 관리/사용을 구분하였기 때문에 resource를 안전하게 사용하는 것을 보장해준다.
 또한, 변하는 부분인 사용에 해당하는 부분만을 재정의하면 되기 때문에 손쉽게 코드를 재사용 가능하게 만들어주고, 함수를 library로 제공하게 되면 user들에게 resource의 할당과 해제를 숨길 수 있어서 encapsulation이라는 측면에서도 매우 좋다.
 실제로 scala library의 많은 부분은 loan pattern을 사용하고 있다.

2014-05-23

[ZooKeeper] (1) ZNode - ZooKeeper가 data를 저장하는 방법.

 지난번 글에서 ZooKeeper는 일종의 파일시스템을 제공해주어 이를 이용하여 semaphore나 mutex를 만들어 사용할 수 있다고 말했다.
 이때 ZooKeeper가 제공해주는 파일시스템에 저장되는 파일 하나하나를 znode라고 부른다.
 이번에는 znode에 대해서 자세히 설명해보도록 하겠다.

ZNode의 특징

hierarchy

 znode는 unix-like 시스템에서 쓰이는 file system처럼 node 간에 hierarchy namespace를 가지고, 이를 /(slash)를 이용하여 구분한다.
http://zookeeper.apache.org/doc/r3.3.2/zookeeperOver.html

 일반적인 file system과 다른 부분이 있다. ZooKeeper는 file과 directory의 구분이 없이 znode라는 것 하나만을 제공한다. 즉, directory에도 내용을 적을 수 있는, directory와 file 간의 구분이 없는 file system이라는 것이다.
 이는 znode의 큰 특징 중 하나이다. namespace hierarchy를 가지기 때문에 관련 있는 일들을 눈에 보이는 하나의 묶음으로 관리할 수 있으면서, directory가 내용을 가질 수 있게 함으로써(혹은 file 간에 hierarchy를 가진다고 하기도 한다. 원하는 표현으로 말하면 된다.) redundunt한 file을 생성해야 하는 것을 막을 수 있다.

size 제한

 ZooKeeper는 모든 data를 메모리에 저장한다.
 data를 메모리에 저장하기 때문에 jvm의 heap memory를 모든 znode를 올릴 수 있는 충분한 크기로 만들어야 한다.

 심지어 The disk is death to ZooKeeper.라고 말하면서, JVM이 heap memory를 swap 하여 하드에 저장하는 것을 피하도록 설정하는 것을 강요(?)하고 있다.

 data를 저장하는 보통의 파일 시스템이나 DBMS같은 경우 모든 data가 메모리에 올라갈 수 있는 크기로 제한된다는 제약조건은 말도 안 되는 조건이라고 할 것이다.
 하지만 znode의 목적은 data를 저장하는 것이 아니라, distributed 된 시스템 간의 조정을 하기 위함이다. 따라서 znode에는 조정에 필요한 meta data만을 저장하는 것이 기본적은 사용법이고, znode 자체도 크기가 작은 data를 저장할 것이라고 가정하고 구현되어 있기 때문에 각 znode의 크기는 1MB로 제한된다.


Recovery

 ZooKeeper설정파일을 봤으면 모든 data를 메모리에 올린다는 설명이 이상하게 느껴질 것이다. ZooKeeper의 설정파일에는 dataDir을 설정할 수 있게 되어 있다.
 그렇다면 dataDir은 무엇을 위한 것일까?

 dataDir은 zookeeper의 recovery를 위해 사용된다.
 ZooKeeper는 모든 data를 메모리에 들고 있기 때문에 서버가 종료되었다가 재 시작했을 때 자료를 보존할 수 없다. 이때 원래의 자료를 복구할 수 있는 것을 보장하기 위하여 ZooKeeper는 모든 transaction log를 dataDir에 저장한다.
 zookeeper를 재 시작하면 dataDir에서 transaction log를 읽어와서 모든 트랜잭션을 다시 실행하여 data를 복구한다.

 하지만 언제나 transaction log만을 이용하여 자료를 복구한다면, 자료를 복구하는데 시간이 걸리기도 하고, 무엇보다도 로그가 쌓일수록 복구할 자료의 양에 비해서 로그의 크기가 커지는 문제가 생긴다.
 이를 해결하기 위하여 ZooKeeper는 transaction log가 일정 이상이 되면, 지금까지 쌓인 transaction log로 만들 수 있는 data를 하드에 저장하고 transaction log를 지운다. 이 data를 snapshot이라고 하는데, 다음에 복구할 일이 생기면 이 snapshot에서부터 자료를 읽어온 뒤, 추가로 쌓인 transaction log만을 실행시켜 자료를 복구한다.

ZNode의 종류

 znode를 생성할 때 2종류의 옵션을 줄 수 있다.
 하나는 life cycle에 관한 option으로 persistent인지 ephemeral인지를 설정하는 것이고, 다른 하나는 znode의 uniqueness에 관한 option으로 sequential node인지 아닌지를 설정하는 것이다.

Persistent mode와 Ephemeral mode

 Persistent mode와 Ephemeral mode는 znode의 life cycle에 관한 설정으로, 모든 znode는 persistent 하거나 ephemeral 하지만 동시에 둘 다일 수는 없다.

 Persistent mode로 생성된 znode는 명시적으로 삭제될 때까지 지워지지 않는, 우리가 일반적으로 생각하는 file과 같다.
 그렇다면 Ephemeral mode는 어떻게 동작할까?
 Ephemeral mode로 생성된 znode는 ZooKeeper서버와 znode를 생성하도록 요청한 클라이언트 사이의 connection이 종료되면 자동으로 지워진다. ephemeral node의 이런 특성을 이용하여 lock이나 leader election을 구현하기도 한다.

Sequence mode

 Sequence mode는 znode의 uniqueness를 보장하기 위한 것이다.
 sequence mode로 만들어진 znode는 주어진 이름 뒤에 int 범위의 10개의 숫자가 postfix로 붙는다.
 이 숫자는 atomic 하게 증가하여 같은 이름으로 만든 node라고 해도 서로 다른 이름의 znode로 만들어준다.1)

 ephemeral mode와 persistent mode 둘 다 sequence node로 만들 수 있다.
 하지만 ephemeral mode와 sequence mode를 동시에 사용하여 node를 생성하는 것은 문제가 생길 수 있다. ZooKeeper서버와 ZooKeeper클라이언트는 비동기적으로 동작할 뿐 아니라 둘 사이의 connection은 빈번하게 끊길 수 있다.
 따라서 ephemeral + sequence node를 생성하라고 요청하였을 때 성공인지 실패인지 응답이 와야 하지만 실제로는 응답이 오지 않고 timeout이 발생할 수 있다.
 이때 성공인지 실패인지 알기 위해서는 node가 생성되었는지 확인해야 하는데 생성된 znode의 이름을 서버에서 결정하기 때문에 클라이언트는 znode의 생성 여부를 알 방법이 없다.
 ZooKeeper를 사용하기 쉽게 해주는 curator라는 라이브러리에서는 이를 위해 protect mode라는 것을 도입하였다. 이에 대해서는 다음 기회에 curator를 설명할 기회가 있으면 더 자세히 다루도록 하겠다.

ACL (Access Control List)

 znode는 ACL(Access control list)을 이용하여 각각의 znode에 접근권한을 설정할 수 있다.

 하지만 unix-like 파일시스템과 다르게 znode에는 user/group/others라는 개념이 존재하지 않는다. 대신
 하지만 차이가 있는
 ACL은 permission과 scheme
 이때 조심해야 하는 것이 있다. 각 znode의 ACL은 자기 자신의 ACL이고, 자식들에게 recursive 하게 적용되지 않는다는 것이다.
 즉, /some-node에 권한을 설정하였다고 해도 /some-node/child에는 권한이 설정되지 않는다는 것이다.

ACL Permissions

 ZooKeeper에서의 권한은 unix-like 파일시스템의 권한과 크게 다를 것은 없다.
 특정 권한들에 대해 allow flag가 있어서 이것이 어떻게 설정되는가에 따라서 해당 권한을 실행시킬 수 있는지가 결정된다.

 ZooKeeper에서 설정할 수 있는 ACL의 종류는 아래의 5가지이다.
  1. CREATE : 해당 znode의 자식 node를 만들 수 있는 권한.
  2. READ : 해당 znode에서 data를 읽고 와 그 자식들의 목록을 읽을 수 있는 권한.
  3. WRITE : 해당 znode에 값을 쓸 수 있는 권한.
  4. DELETE : 해당 znode의 자식들을 지울 수 있는 권한.
  5. ADMIN : 해당 znode에 권한을 설정할 수 있는 권한.
 unix-like file system과 다른 부분이 2가지 있다.
 첫 번째는 보통의 file system에는 없는 CREATE와 DELETE라는 권한이 존재하여 자식 node를 생성하고 삭제할 수 있는 권한이 있다는 것이다.
 unix-like file system에서 directory는 실제로는 자기 자식의 list를 가지고 있는 file이다.
 그래서 자식을 만들고 지우는 것은 부모 directory에 내용을 변경하는 것이고, 부모 directory에 쓰기 권한이 있는지가 자식을 만들고 지우는 권한이 된다.
 하지만 ZooKeeper에서는 모든 znode가 directory이기도 하고, file이기도 해서 자기 자신에 대한 쓰기 권한과 자식 node에 대한 생성/삭제 권한을 같이 쓸 수 없다.

Schemes

 ZooKeeper는 unix-like 시스템과 다르게 각 znode에 user/group/others라는 개념이 존재하지 않는다.
 대신 scheme이라는 것을 이용하여 권한을 구분하게 되어 있다.
 built in으로 제공되는 설정할 수 있는 scheme은 아래와 같이 4가지가 있다.

  1. WORLD
  2. AUTH
  3. DIGEST
  4. IP
 WORLD는 모든 요청에 대해 허락하는 것이고, AUTH는 authenticated된 session에서 들어오는 요청에 대해서만 허락하는 것이다.
 DIGEST는 username과 password를 보내서 이를 이용하여 만든 MD5 hash값이 같은 요청에 대해서만 처리하는 것이고, IP는 해당 IP에서의 요청만을 처리하도록 하는 것이다.

Stat

 ZNode는 node와 node의 data에 관한 여러 정보를 들고 있고, 이것을 stat이라고 부른다. stat이 가지는 정보는 다음과 같다.
  • czxid : znode를 생성한 트랜잭션의 id
  • mzxid : znode를 마지막으로 수정 트랜잭션의 id
  • ctime : znode가 생성됐을 때의 시스템 시간
  • mtime : znode가 마지막으로 변경되었을 때의 시스템 시간
  • version : znode가 변경된 횟수
  • cversion : znode의 자식 node를 수정한 횟수
  • aversion : ACL 정책을 수정한 횟수
  • ephemeralOwner : 임시 노드인지에 대한 flag
  • dataLength : data의 길이
  • numChildren : 자식 node의 수

1) 이 숫자의 범위는 int 범위로 최대 2147483647까지 올라가고 그보다 커지면 overflow가 발생한다. 문서에는 overflow가 발생했을 때 -2147483647가 된다고 적혀 있지만, 실제로는 -2147483648이다. 이는 단순한 실수로 보인다.

2014-05-13

[ZooKeeper] (0) zookeepr는 무엇인가?

 보통 분산 시스템을 구현할 때, 모든 시스템이 완전히 독립적으로 돌아가는 시스템이 아니라면, 시스템 간의 락, 설정 공유, 리더 선출, atomic 한 연산 등을 구현하는 것이 필요하지만, 분산환경에서 이를 구현하는 것은 매우 어려운 일이다.
 위의 기능들을 구현하기 어렵기 때문에 보통은 apache에서 제작한 zookeeper라는 시스템을 이용하여 분산 시스템 간의 동기화된 작업을 구현한다.
 zookeeper는 위의 기능들을 직접적으로 제공하지는 않지만 이런 일들을 하기 쉽게 해주는 환경을 제공한다.

 zookeepr가 제공해주는 환경이라는 것은 일종의 공유 가능한 file system을 제공해준다. 그러면 사용자가 file을 이용해서 semaphoremutex를 구현하듯이 zookeeper를 이용해서 semaphore나 mutex등을 구현하여 사용하면 된다.

 zookeeper는 분산환경에서의 다음과 같은 특징을 보장해준다.

ZooKeeper의 특징

Atomicity

 zookeeper에서 data의 저장은 원자성을 가진다.
 즉, node를 만들건 node에 data를 update하든 해당 request는 완벽하게 처리되거나 처리되지 않거나 하지 그 중간의 어중간한 상태는 존재하지 않는다.

Consistency

 분산환경에서, 특히나 data를 copy하여 여러 서버에 저장하면서 strong consistency를 보장하는 것은 매우 어려운 일다. 그래서 zookeeper에서는 아래의 2가지 consistency를 보장한다.


 첫 번째는 sequential consistency다.
 즉, 모든 요청은 들어온 순서대로 처리되고, 모든 서버가 요청을 같은 순서로 처리하는 것을 보장하는 것이다.


 두 번째는 eventual consistency다.
 strong consistency와 달리, 모든 요청에 대해 모든 서버가 완벽히 같은 순간에 같은 값을 갖지는 않지만 결국에는 같은 값을 가질 것을 보장하는 것이다.
 즉, 어떤 서버에서는 특정 request가 실행됐지만 다른 서버에서는 실행되지 않는 일은 없다는 것이다.

Durability

 zookeeper는 강한 durability를 보장한다.
 zookeeper에서 data 그 자체는 언제나 메모리에만 존재한다. 하지만 서버의 로컬디스크에 트랜잭션 로그와 스냅 샷을 저장하기 때문에 메모리가 날아가도 트랜잭션 로그와 스냅 샷을 이용하여 data를 복구할 수 있다.


 zookeeper가 어떻게 구현되어서 위의 특징들을 만족시키는지는 다음 기회에 설명하도록 하겠다

2014-05-06

config_script 재작성

 이 컴퓨터 저 컴퓨터 옮겨가면서 작업할 때, 컴퓨터별로 환경을 통일하는 작업은 귀찮은 일이다. 그래서 나는 이런 스크립트를 만들어서 써왔다.
 아직 사용하는데 큰 문제는 없었지만, 그때그때 필요한 기능들을 넣다 보니 스크립트가 난잡해지고, 내가 다른 프로젝트에서 작업할 때 자주 사용하지 않는 bash를 이용해서 만들다 보니 수정이 필요해서 코드를 다시 볼 때, 익숙하지 않아서 실수하는 문제가 생겼다.
 그래서 이번 연휴를 맞아서 스크립트를 완전히 새로 작성하였다.

 이번에 작업에서 초점을 맞춘 부분은 크게 2가지였다.
 첫 번째는 시간이 지나도 알아보기 쉽도록 익숙한 언어로 작업할 것, 두 번째는 수정이 필요하거나 새로운 일이 추가될 때 확장하기 쉽도록 하는 것이었다.

 첫 번째 요구사항을 맞추기 위하여 작업은 python을 이용하였다.
 어차피 하는 대부분 작업이 조건에 맞추어 파일을 옮기고 command를 실행시키는 일들뿐이고 복잡한 일은 없어서 언어는 무엇을 사용해도 상관없어 보였기에 그중에 가장 익숙한 python을 이용했다.

 두 번째 요구사항을 맞추기 위하여 기존의 스크립트를 분석해 보니 대부분의 일이
  1. 본 작업에 앞서 사전작업을 한다.
  2. 작업해야 할 파일 리스트에서 하나씩 꺼내서
    1. source가 있는지 파악하여 에러처리를 하고
    2. destination에 이미 파일이 존재하는지 확인하여 에러처리를 하고
    3. source를 destination으로 복사/링크 등을 한다.
  3. 본 작업이 끝난 뒤 마무리 작업을 한다.
의 순서로 진행되었다.
 그래서 Config라는 abstract class를 만들고, 각각의 단계에 맞춰서
  1. pre()

    1. source_exists()
    2. resolve_conflict()
    3. do()
  2. post()
의 5개의 abstract method를 만들어 원하는 작업마다 Config를 상속받는 class를 만들어 상황에 맞게 위의 method들을 override 하는 방식으로 작업을 진행하였다.
 또한, source_exists/resolve_conflict/do 함수 안에서 공통으로 쓰이는 함수들인 소스의 경로를 알아오는 source_dir/source_path, 목적지의 정보를 알아오는 destination_dir/destination_path, conflict 시 backup 할 경로를 알아오는 backup_path를 override 가능한 함수로 만들었다. 이렇게 해서 특정 경로에서 특정 경로로 symbolic link를 만드는 작업을 할 때는 경로만 재정의하여 사용할 수 있도록 하였다.
 그래서 나온 결과물의 class hierarchy는 위와 같다.

 다음 작업으로 고려하고 있는 것은 맥이나 윈도에서의 환경세팅과 rvm/npm/pip/virtualenv 등 작업에 필요한 일부 패키지들을 직접 컴파일해서 사용할 수 있도록 하는 것이다.
 하지만 당장은 하지 않을 것 같고 조만간에 시간이 나면 더 작업할 것 같다.

2014-05-04

왜 Triple buffering을 사용하는가

 지난번 글에서 double bufferingVSync에 관하여 설명하였다.
 이번 글에서는 VSync가 가지는 문제와 그것을 어떻게 triple buffering이 해결하는지를 적어보도록 하겠다.

 우선 다음 문제를 풀어보자.
vertical interval이 16.6ms마다 있는 display를 사용하고
double buffering을 사용하는 드라이버에서 VSync를 사용한다고 했을 때
back buffer에서 scene 하나를 완성하는 데 걸리는 시간이 16ms인 프로그램을 작성하면
이 프로그램은 몇 fps가 나올 것인가?
 매 frame마다 하나의 scene을 16ms 동안 그리고, 남은 0.6ms는 VSync를 기다리기 때문에 16.6ms마다 한 frame을 그려서 60fps가 나온다.
 VSync를 기다리는 0.6ms가 아쉽기는 하지만 어차피 60fps 이상은 사람 눈으로 잘 구분이 안 되니 큰 손해를 보는 것은 아니다.

 문제는 아래의 상황이다.
vertical interval이 16.6ms마다 있는 display를 사용하고
double buffering을 사용하는 드라이버에서 VSync를 사용한다고 했을 때
back buffer에서 scene 하나를 완성하는 데 걸리는 시간이
홀수 번째 scene은 15ms, 짝수 번째 scene은 17ms가 걸리도록 프로그램을 작성하면
이 프로그램은 몇 fps가 나올 것인가?
 매 프레임 back buffer에 그리는 데 걸리는 평균 시간은 첫 번째 경우와 같다.
 그렇다면 이 케이스에서도 60 fps가 나올까?
 아니다. 홀수 번째 frame에서는 15ms 동안 그림을 그리고 1.6ms 동안 기다리지만, 짝수 번째 frame에서는 back buffer에 그림을 그리는데 17ms가 걸렸기 때문에 첫 번째 vertical interval 때 swap buffer를 하지 못하고, 그 다음 vertical interval까지 기다려야 한다. 즉, 그림이 완성된 뒤 16.2ms만큼 더 기다려야 화면을 바꿀 수 있다는 것이다.
 이렇게 되면 frame 2개를 그리는데 3번의 vertical interval이 필요하므로, 약 40fps가 나오게 된다. 그림을 그리는 데 걸리는 평균 시간이 같은데도 성능은 2/3로 떨어지게 되는 것이다.

 아래의 상황도 한번 보자.
vertical interval이 16.6ms마다 있는 display를 사용하고
double buffering을 사용하는 드라이버에서 VSync를 사용한다고 했을 때
back buffer에서 scene 하나를 완성하는 데 걸리는 시간이 17ms인 프로그램을 작성하면
이 프로그램은 몇 fps가 나올 것인가?
 첫 번째 상황에 비해 scene 하나를 그리는 데 걸리는 시간이 고작 1ms 늘었을 뿐이다.
 하지만 실제로 성능은 하나의 scene을 그리는데 2번의 vertical interval이 필요로 하므로, 원래 성능의 절반 정도인 30fps가 나오게 된다.

 그러면 무조건 vertical interval 사이에 한 frame을 그리도록 프로그램을 작성해서 문제를 해결하면 안 될까?
 음.... 가능하면 그렇게 하는 것이 좋다.
 하지만 프로그램의 실행 시간이라는 것은 기본적으로 환경에 많은 영향을 받기 때문에 예측하기 어렵다. 가능하면 이 현상(이 현상에 이름이 있는지는 모르겠다.)의 근본적인 원인을 해결하는 것이 좋다. 그래서 해결책으로 나온 것이 Triple buffering이다.

Triple buffering은 기본적으로 Double buffering이랑 같지만 2개의 back buffer와 front buffer, 총 3개의 buffer를 사용한다.
 Triple buffering이 어떻게 문제를 해결하는지 이해하기 쉽게 해주는 다음 그림을 보자. 아래는 copy를 하는 경우만 나와 있지만, vsync를 사용한다면 copy든 flip이든 vertical interval 동안만 update가 가능하므로 flip과 큰 차이는 없다.
http://en.wikipedia.org/wiki/Multiple_buffering
 위 그림의 4번 시나리오는 Double buffering과 vsync를 사용하는 경우 중 한 scene을 그리는 시간이 한 frame rate보다 길면 어떻게 되는가를 보여주는 자료다.
 draw B가 끝나는 부분을 자세히 보자. 아쉽게도 draw B는 한 프레임을 약간 넘어가 vertical interval 동안 화면을 갱신하지 못하게 되었다. 이렇게 타이밍을 놓친 scene은 그다음 vertical interval이 되어야 화면에 보일 수 있다. 그전까지는 여전히 draw A에서 그렸던 장면이 화면에 남아 있게 된다. 이것이 위에서 말했던 성능 저하의 원인이다.

 다시 위 그림에서 5번 시나리오를 보자. 이건 4번과 똑같은 일을 triple buffering을 사용하여 처리한 것이다.
 double buffer에서는 draw Abuffer 1에서 video memory로 옮겨진 뒤 Vertical interval이 될 때까지 draw B가 시작하지 못했지만, triple buffering을 사용하는 경우 여분의 buffer가 하나 더 있기 때문에 draw B를 바로 시작할 수 있다.

 이렇게 식으로 triple buffering을 이용하면 VSync로 인한 성능 저하를 줄일 수 있다.
 하지만 surface만큼의 메모리를 더 사용해야 하는 단점이 있어서 아직 대부분의 그래픽 카드에서 기본으로 지원하는 기능은 아니다.
 하지만 새로 나오는 그래픽 카드들에는 그래픽 카드에서 지원하는 기능으로 들어가기 시작했고, 지원 하지 않는 그래픽 카드에서는 FBO를 이용하여 구현할 수 있다.

2014-05-03

copy와 flip - Double buffering의 2가지 기법

 Buffer는 그림을 그리기 위해 메모리에 잡아놓은 영역을 의미한다.
 그래픽 드라이버가 화면을 갱신해야 할 때 이 영역에서 그림을 읽어 화면을 그린다.

 과거의 graphic card(혹은 요새 나오는 저 사양의 embedded 용 graphic card)에서는 single buffering이라는 기법을 이용한다.
 single buffering이란, 말 그대로 한 개의 버퍼만을 사용하는 방식이다. 완성된 그림을 buffer에 그리고, 화면에 그림을 그려야 할 때마다 이 buffer에서 화면으로 그림을 가져가는 것이다.
 가장 기본적인 구현이지만, 이런 방식은 buffer에서 화면으로 가져가는 동안 buffer를 업데이트하지 못한다는 단점이 있다.
 이런 단점을 해결하기 위하여 double buffering이라는 기법을 사용한다.

Double buffering

 Double buffering에서는 front buffer와 back buffer라고 불리는 2개의 buffer를 사용한다. back buffer에 그림을 그리고 한 프레임이 완성되면 back buffer와 front buffer를 바꾼다. (이를 swap buffer라고 한다.) 화면에 그림이 필요할 때는 언제나 front buffer에서 화면을 가져간다.
 화면이 읽어들이는 buffer는 언제나 front buffer이므로 화면을 업데이트할 필요가 있으면 언제나 back buffer에 그리면 된다.
 이때 back buffer에서 front buffer로 옮기는 방식에는 2가지 방법이 있다.

Copy

 첫 번째 방법은 copy라고 부르는 방법이다. 이 방법은 front buffer는 그래픽 메모리에 만든다. back buffer는 그래픽 메모리나 시스템 메모리 어디에 만들어도 상관없지만, 보통 copy를 사용하는 경우는 그래픽 메모리를 아끼기 위한 경우가 많아서 back buffer는 시스템 메모리에 잡는다.
 프로그램이 그림을 그리라고 하면 이를 back buffer에 그리고, swap buffer를 하게 되면 back buffer에 있는 내용을 front buffer로 복사해 가게 된다.
http://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html
 전 frame에 그렸던 그림을 그대로 유지할 수 있어 화면 일부분만을 업데이트하는 경우에는 효율적이다. 하지만 대부분의 3d 엔진은 buffer를 지우고 처음부터 다시 그리기 때문에 이런 방식은 비효율적이다.
 게다가 copy를 사용하는 중에 화면을 업데이트하는 중에 copy가 발생하면 tearing 현상이 발생한다.

Tearing

 Buffer란 기본적으로 memory이기 때문에 차례대로 접근할 수밖에 없다. 따라서 back buffer에서 front buffer로 그림을 복사할 때 위에서부터 순서대로 그림을 그려야 한다. 이 때문에 화면의 일부는 이전 frame을 그리고 나머지는 현재의 frame을 그리는 현상이 생기게 된다.
 그렇게 되면 아래와 같이 찢어지는 화면이 보이게 되는데 그것을 tearing이라고 한다.
http://en.wikipedia.org/wiki/Screen_tearing

Page flipping

 front buffer에 화면을 업데이트하는 순간 전 frame과 새 frame이 겹치는 순간이 있는 것이 문제라면, 한순간에 front buffer를 완성할 수 는 없을까?
 그래서 나온 방법이 Page flipping이라는 방법이다.
http://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html
 Flip은 back buffer와 front buffer를 둘 다 그래픽 메모리에 만들어 놓고, display가 가리키는 buffer를 바꾸는 방식이다.
 이렇게 하면 front buffer에 old frame이 남아 있는 경우는 없다.
 게다가 대부분의 3D 엔진들이 동작하는 방법이 화면을 전부 지우고 새롭게 그림을 그리는 방식이기 때문에 대부분의 경우에 이전 그림을 유지하고 있을 필요도 없고(필요하면 front buffer에서 읽어서 back buffer에 복사하기는 한다), 복사하는 것보다는 포인터만 바꾸는 것이 시간이 조금 걸리기 때문에 더 빠르다.

 flip을 사용하면 front buffer에 이 전 frame과 현재의 frame이 동시에 존재하는 일은 없다. 하지만 flip을 사용해도 tearing은 발생한다. display의 구현 때문이다.
 display는 매 순간 front buffer에서 데이터를 읽어서 화면을 갱신시킨다. 문제는 순식간에 화면을 전부 갱신시키는 것이 아니라 위에서부터 순서대로 갱신시키기 때문이다. 그래서 화면을 갱신하는 중에 flip이 발생하면 old frame과 새 frame이 동시에 나오는 일이 생기게 된다.

 이런 문제를 해결하기 위해 Vertical synchronization이라는 방법을 사용한다.

Vertical Synchronization

 과거 빔을 사용하는 CRT 모니터를 사용하던 시절에는 오른쪽 아래까지 빔을 이용하여 그림을 그리고 나면, 빔을 왼쪽 위로 돌려보내는 시간이 필요로 했다. 이 시간을 Vertical Blanking Interval 혹은 vertical interval이라고 하는데 CRT 모니터를 사용하지 않는 지금도 프로토콜의 하위 호환성을 위하여 데이터를 보내는 VBI를 둔다. 이 interval 동안은 화면을 갱신하지 못하기 때문에 이 기간에 flip이나 copy를 하여 buffer를 갱신한다면 tearing 현상은 발생하지 않는다.
 물론 VSync에도 단점이 없는 것은 아니다. swap buffer를 언제나 vertical interval에만 하므로 frame의 최소 시간이 vertical interval 이하가 되지 못한다.
 하지만 사람의 눈은 보통 일정 이상의 fps를 인식하지 못하고, display의 interval은 보통 그 한계 근처에서 설정되어 있기 때문에 큰 문제는 안 된다.

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에 대해서 회사에서 발표하며 사용했던 자료를 첨부한다.