crash-only software - high availability server 만들기

지난번 글 에서 high availability 서버를 만들기 어려운 이유를 설명했었다. 그럼에도 high availability 서버를 만드는 것은 중요하기 때문에 availability를 높이기 위한 여러 가지 방법들이 존재한다. 이번에 설명할 crash-only software 도 그중 하나다. Crash-only software의 기본 철학은 해결할 수 없는 문제가 발생했을 때, 다른 시도를 하지 않고 바로 종료시키는 것이 오히려 availability를 올린다는 것이다. 이렇게 하면 서버가 떠 있는지만 검사하면 되기 때문에 서버에 문제가 생겼는지 바로 알 수 있고, 문제가 생기면 바로 crash로 끝내기 때문에 빠르게 종료할 수 있다. 다만 crash-only software는 언제든지 죽을 수 있기 때문에 persistence layer가 프로그램 외부로 빠져야 한다. 즉, 로직 레이어와 데이터 레이어가 구분되는 multitier architecture 가 된다. 자연스럽게 로직 레이어는 스테이트를 가지지 않게 되기 때문에 컴포넌트별로 독립시키기 쉽기 때문에 자연스럽게 마이크로 서비스 로 만들게 되고, 서비스 하나의 크기가 줄어들기 때문에 재시작에 걸리는 시간도 줄어들고, 자연스럽게 availability가 증가하게 된다. 또한, crash-only-software에서 사용하는 공유 자원은 사용권을 소유한다는 개념이 아니라 사용권을 빌린다는 개념으로 접근해야 한다. 사용권을 빌렸기 때문에 명시적으로 사용권을 돌려주지 않았더라도 일정 시간이 지나면 사용권을 잃고 다른 컴포넌트에서 사용할 수 있어야 한다.

high availability 서버를 만들기 어려운 이유

서비스에서 availability를 보장해주는 것은 매우 중요하다. three-nine(99.9%)의 availability를 보장하려면 일 년에 아홉 시간 이하의 다운타임만 있어야 하고, four-nine(99.99%)을 보장하려면 약 한 시간 이하, five-nine(99.999%)을 보장하려면 일 년에 다운 타임이 오 분 이하여야 한다. 보통 서버 장애는 서버가 가장 바쁠 때 발생하기 때문에 가능하면 다운 타임을 줄이는 것이 중요하지만, 현실적으로 이는 쉽지 않다. 상용 서비스 중에서도 three-nine 이상 보장하는 서비스를 찾기 힘들고, 어느 정도 이름 있는 서비스들은 돼야 four-nine, 정말 안정화가 잘 돼 있는 서비스들만이 five-nine 이상의 availability를 보장한다. 이는 high availability 서버를 만드는 것이 근본적으로 어려운 일이기 때문이다. 다운 타임이 적은 서버를 만들기 위해서는, 일단 버그가 없는 것은 기본이어야 한다. 하지만 서버 다운의 이유가 버그만 있는 게 아니다. 디스크나 네트워크 등 하드웨어 문제로 예상치 못하게 서버를 사용할 수 없게 되는 경우도 있다. 따라서 high availability 서버를 만들기 위해서는 문제가 생겼을 때 빠르게 재시작하는 것이 가장 중요한데 이를 위해서는 서버에 문제가 발생했을 때 이를 감시하고 있던 모니터가 빠르게 감지하여 서버에 문제가 발생하면 서버를 안전하게 죽이고, 새 서버를 띄워야 한다. 보통 서버에서 일정 시간 간격으로 메시지를 보내고 메시지가 오지 않으면 죽은 것으로 판단하는 heartbeat 방식 을 많이 사용한다. 이때 heartbeat 자체는 보통 몇 초에 한 번 보내지만, 실제 서버가 죽지는 않았지만, 네트워크나 다른 문제로 메시지가 오지 않았을 것을 대비하여 몇 번의 메시지가 도착하지 않았을 때 서버가 죽었다고 판단한다. 즉, 실제로 서버에 문제가 발생해도 그를 감지하는 데만 적게는 십수초 많게는 몇십초의 시간이 걸린다. 그다음 ...

[C++] array to pointer decay

이미지
C++은 그 근본이 C에서 나왔기 때문에 C에서 많은 문제점을 물려받았다. 대표적으로 implicit conversion 이 그런데, 그중에서 array가 pointer로 implicit conversion 되는 것을 decay. 혹은 다른 decay들과 구분하기 위해 array to pointer decay라고 부른다. 이는 말 그대로 T[N] 타입으로 선언된 변수의 값이 T* 타입의 변수에 바인딩 될 때 implicit casting이 되는 것을 말한다. 이는 컴파일 타임 정보인 배열의 크기를 잃는다는 문제도 있지만, 그보다 더 큰 문제는 포인터에 대해서 upcasting 또한 implicit conversion이 가능하다는 데서 생긴다. 위와 같은 코드가 있다고 해보자. A 를 상속받은 B 클래스가 있고, B instance의 크기는 A instance의 크기보다 크다(정확히 따지자면 사용하는 alignment의 크기가 B 클래스보다 작은지 따져봐야 하지만, 여기서는 설명을 간단히 하기 위해 int 크기로 align 된다고 가정하고 진행하도록 하겠다). 이때 f 함수가 하는 일은 다음과 같다. B(3, 4) 를 생성한다. 1에서 생성한 객체의 A 부분만 slice 한 A(3) 을 인자로 받은 a 위치에 넣는다. B(5, 6) 를 생성한다. 3에서 생성한 객체의 A 부분만 slice 한 A(5) 를 인자로 받은 a 에 1 더한 주소에 저장한다. 앞에서 말했듯이 위의 코드는 일단 포인터는 실제 배열의 크기를 알지 못하기 때문에 f 함수가 받은 a 의 길이가 2 이상인지 알 수 없다는 문제가 있다. 하지만 이보다 더 큰 문제가 있다. 만약 f 함수를 아래와 같이 사용했다고 해보자. B[4] 타입인 a 는 B* 타입으로 decay 되고, B* 는 부모 타입인 A*로 upcasting 되어 f 함수에 넘어간다. f 함수를 실행한 뒤 사용자가 원했던 모습은 아마 위와 같은 모습일 것이다. 혹은 ob...

[C++] Variadic template

전통적인 C나 C++에서 잘못 쓰면 위험한 것 중 하나로 stdarg 가 있었다. 이를 이용하면 임의의 개수의 인자를 받는 variadic function 을 만들 수 있지만, 함수의 시그니쳐를 통한 타입 체크를 완전히 무시하기 때문에, 함수의 호출자가 규약을 지키지 않고 함수를 사용하더라도 컴파일 타임에 문제를 잡을 수 없기 때문에 실행 시에 여러 가지 문제가 발생했다. 이런 문제를 피하고자 stdarg 를 사용하지 않고, 예상되는 인자의 최대 개수를 예상해서 받는 인자의 개수가 다른 함수를 오버로딩하여 사용하는 경우가 더 많았다. C++11에서는 variadic function을 타입 시스템과 붙여 컴파일 시간에 타입 체크 할 수 있게 하려고 variadic template이라는 문법을 추가하였다. Variadic template을 사용하면 사용된 인자의 타입 별로 instantiation 되기 때문에 바이너리의 크기는 커지지만, 컴파일 타임에 타입 체크를 할 수 있으며 실행 시에 생기는 비용을 줄여준다. Variadic template이 될 템플릿 인자는 ellipsis( ... )를 붙여 다음과 같이 사용한다. Variadic template을 사용하는 패턴은 크게 2가지이다. 첫 번째는 다른 함수로 인자를 전달하는 것이다. 대표적으로 make_unique 가 이런 경우에 속한다. Possible implementation 에서 볼 수 있듯이 make_unique 는 std::forward<Args>(args)... 를 이용해 T 타입의 생성자에게 모든 인자를 전달한다. 여기서 std::forward 함수 호출 뒤에 ellipsis가 붙어있다는 것이 중요하다. 이를 pack expansion이라고 하는데 std::forward 함수에 인자를 넘기는 패턴을 variadic arguments인 args 에 전부 적용하는 것이다. 따라서 args 가 N 개의 인자였으면 번의 std::forward 함수가 불리게 된다. 이때 단순히...

c++에서 static 한정자가 하는 일

C++을 처음 배울 때 사람들이 많이 헷갈리는 것 중 하나가 static 이다. 사실 이건 배우는 사람의 문제도 아니고 가르치는 방법의 문제도 아니다. 그저 C++에서 static 을 다양한 용도로 사용하고 있기 때문이다. static storage static 키워드를 사용하는 한 가지 목적은 지역 변수에 할당할 객체를 static storage 로 만들기 위해서다. C++에서 block scope에 선언되는 변수가 가지는 객체는 기본적으로 automatic storage에 선언되어 block이 끝나면 자동으로 소멸한다. 하지만 간혹 특정한 범위 내에서만 접근할 수 있으면서 프로그램이 끝날 때까지 살아있는 객체, 다시 말해 지칭하는 변수는 block scope에 선언됐지만, static storage duration을 가지는 객체가 필요할 때가 있다. 이럴 때 static 한정자를 사용하면 된다. block scope에 선언되는 지역 변수에 static 한정자를 붙이면 이 변수가 가리키는 객체는 static storage duration을 가지고, block이 종료돼도 소멸하지 않는다. internal linkage static 한정자는 namespace scope에 선언되는 변수에도 사용될 수 있다. 하지만 namespace scope에 선언된 변수가 가지는 객체는 기본적으로 static storage에 할당된다. 따라서 namespace scope에 선언된 변수에 붙은 static 한정자는 객체가 할당될 storage에 영향을 주지 않는다. 여기서 static 은 다른 의미를 가진다. static 한정자는 namespace scope에서는 변수뿐 아니라 함수 선언에도 붙일 수 있다. 이 경우 선언된 변수나 함수의 linkage를 internal linkage 로 바꿔서 translation unit 밖으로 나가지 않도록 해준다. static member C++에서 static 이 가지는 마지막 의미는 클래스의 멤버 변수나 함...

[C++] internal linkage와 external linkage의 차이

보통 프로그램은 하나의 소스가 아닌 여러 개의 소스로 구성된다. 이 소스를 C++ 표준에서는 translation unit이라고 하고, 소스를 컴파일하는 과정을 translation이라고 한다. translation의 마지막 단계는 link인데, 이 과정을 제외하고는 모든 translation은 독립적으로 진행된다. 이때 translation unit 내에서 이름이 어느 범위까지 사용될 수 있는가를 정하는 것이 지난번 글 에서 설명한 스코프이다. 이번에 설명할 linkage는 같은 이름이 같은 entity를 지칭하는 범위를 정하는 것이다. 각 이름은 어떤 linkage를 가지는가에 따라서 translation의 마지막 단계에서 어떻게 link 될지 달라진다. linkage에는 internal linkage와 external linkage 두 가지 종류의 linkage가 있다. 하지만 모든 이름이 linkage를 가지는 것은 아니다. 우선은 linkage를 가지지 않는 이름에 대해 알아보도록 하겠다. no linkage 영어로는 has no linkage 라고 하여 has internal/external linkage 와 자연스럽게 대구가 된다. 하지만 한국어로 말하면 no linkage를 가진다고 하는 것은 어색하므로 그냥 linkage를 가지지 않는다고 하는 것이 적당해 보인다. 어찌 됐든 모든 이름이 linkage를 가지는 것은 아니다. block scope에 선언된 이름은 함수와 extern으로 선언된 변수를 제외하면 전부 linkage를 가지지 않는다. 따라서 함수 안에서 선언한 로컬 클래스, enumeration, enumerator, 타입 등은 전부 linkage를 가지지 않는다. linkage를 가지지 않는 이름은 선언된 스코프에서밖에 접근할 수 없고, 스코프 밖에서 그 이름을 다시 사용해도 그건 다른 entity를 지칭하는 것이 된다. static 한정자를 사용하여 static storage에 저장한 경우도 마찬가지다. 이는 block...

[C++] 이름과 스코프

C++에서는 많은 것들이 이름을 가질 수 있다. 변수는 모두 이름을 가지고, 객체는 변수를 통해서 이름을 가질 수 있다. 타입도 이름이 있고, 함수도 이름이 있다. 람다 함수는 익명 함수라는 번역 그대로 이름이 없지만, 변수에 할당하여 이름을 통해 접근할 수도 있다. 이 이름을 어디서부터 어디까지 사용할 수 있는가를 그 이름의 스코프라고 하는데, C++은 상황에 따라 다른 스코프를 사용한다. 이번 글에서는 C++의 이름이 가지는 스코프에 관해서 설명하도록 하겠다. block scope block scope는 선언된 문장부터 선언된 block이 끝날 때까지 사용할 수 있다. 블록 밖에서 선언된 같은 이름이 있으면 이름이 선언되기 전까지는 블록 밖에서 선언된 이름을 그대로 가지고, 선언된 문장부터 새로 선언한 의미를 가진다. 예를 들어 위와 같은 코드가 있을 때 4번째 줄까지 a 는 2번째 줄에서 선언된 a 이지만, 4번째 줄부터 block이 끝나는 10번째 줄까지는 4번째 줄에서 선언된 a 이다. 또한, block scope를 가지는 이름은 같은 block 안에서 다른 의미를 가지기 위해 다시 사용될 수 없다. 즉, 위에서 5번째 줄에서 10번째 줄까지 사이에서는 a 를 다른 의미로 사용할 수 없다. 스코프를 가지는 이름의 대표적인 예제로는 함수 안에서 선언된 변수인 지역 변수(local variable)가 있다. 이때 지역 변수와 automatic storage 에 저장되는 지역 객체를 혼동하면 안 된다. 스코프는 어디까지나 컴파일 시 그 이름을 어디까지 사용할 수 있는가 하는 것이지 객체의 라이프타임과는 상관없다. 지역 변수 중에서도 static 이나 thread_local 같은 storage를 지정해주는 specifier가 붙은 경우 이 객체들은 automatic storage에 저장되지 않는다. 지역 변수 외에도 typedef 나 using 으로 선언되는 type alias, 함수 안에서 선언되는 class 와 enum 같은 경우도 함...

이 블로그의 인기 게시물

USB 2.0 케이블의 내부 구조

[C++] enum class - 안전하고 쓰기 쉬운 enum

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

[Web] SpeechSynthesis - TTS API

[Python] cache 데코레이터로 최적화하기