ØMQ - The Guide


Chapter 1 - Basics

topprevnext

Fixing the World

topprevnext

어떻게 ZeroMQ를 설명해야 할까? 우리 중의 몇몇은 ZeroMQ가 하는 모든 놀라운 것들을 말함으로써 이를 시작한다. ZeroMQ의 소켓은 스테로이드를 복용한 소켓이다. 라우팅을 하는 우편함과 같으며, 빠르다! 또 다른 이들은 ZeroMQ에 대해 직관적으로 번뜩하고는 기존과 다른 패러다임 전환을 깨우쳐서 모든 것이 명료해졌던 순간을 공유하려고 한다. 상황은 매우 간단해지고, 복잡함은 어느새 멀리 사라진다. 그리고 마음을 열게 된다. 또 어떤 이들은 비교를 통해 설명을 하려고 한다. 매우 작고 단순해 보이지만 친숙하게 보여진다. 지금와서도 나는 개인적으로 우리가 왜 ZeroMQ를 만들었는지에 대한 이유가 당신과 같이 이 글을 읽는 독자들로부터 나온 것들이라고 생각하기 때문에 가능한 그 것들을 모두 기억하고 싶다.

프로그래밍은 예술을 빙자한 과학이다. 우리 대부분이 소프트웨어 물리학에 대해 학습한 적이 있다고 하더라도 그 이해가 부족하기 때문이다. 소프트웨어 물리학은 알고리즘이나 자료 구조, 언어와 관념이 아니다. 이는 단지 우리가 만들고, 사용하고, 버리는 도구들일 뿐이다. 진정한 소프트웨어 물리학은 사람 간의 물리학이다.—특히, 복잡성에서 오는 제약 사항들을 마주했을 때 우리는 큰 문제들을 작은 부분으로 나누어 함께 해결하길 원한다. 사람들이 쉽게 이해하고 사용할 수 있는 빌딩 블록을 만들고 사람들은 거대한 문제들을 해결하기 위해 함께 일하는 것. 이것이 프로그래밍의 과학이다.

우리는 연결된 세상에 살고 있고, 현대 소프트웨어는 이 세상을 항해해야 한다. 그래서 오늘날의 매우 큰 솔루션들을 위한 빌딩 블록들은 연결되어 있고 대규모로 병렬화되어 있다. 이는 더 이상 "강하고 조용한" 코드라기에는 충분하지 않다. 코드는 코드로 통해야 한다. 코드는 수다스럽고, 사교적이고, 잘 연결되어야 한다. 코드는 사람의 두뇌와 같이 중앙 제어가 없는 병렬 네트워크에서 아무런 단일 결함 지점이 없으면서도, 크고 어려운 문제를 해결할 수 있는, 서로가 서로에게 메시지를 전달하는 수조 개의 개별적인 뉴런들과 갈이 움직여야 한다. 그리고 어떤 관점에서는 모든 네트워크의 종착점은 사람의 두뇌이기 때문에 미래의 코드가 이와 갈이 보여지는 것은 전혀 이상한 일이 아니다.

스레드나 프로토콜 혹은 네트워크 작업을 해본 적이 있다면, 이러한 것들이 아름다울 수 없다는 것을 깨닫게 될 것이다. 그건 그냥 꿈이다. 심지어 현실에서 몇몇 프로그램들을 소켓을 통해 연결해서 제어하는 일을 시작할 때조차도 이는 매우 괴로운 일이다. 수조? 그 비용은 상상조차 할 수도 없다. 컴퓨터들을 연결하는 것은 소프트웨어와 서비스들을 연결하는 것이고, 이는 수십억불의 사업이기 때문에 매우 어려운 일이다.

우리는 통신망이 우리가 이를 사용하는 능력보다 몇년은 앞서가 있는 세계에서 살고 있다. 우리에겐 Fred Brooks와 같이 "생산성, 신뢰성, 혹은 단순성 면에서의 한 승수만큼의 향상을 약속하는" "은탄환"은 없다고 생각했던 소프트웨어 엔지니어들이 이끄는 1980년대에 소프트웨어 위기가 있었다.

Brooks는 우리의 지식을 효율적으로 공유함으로써 이러한 위기를 풀어내었던 자유/오픈 소스 소프트웨어를 간과했다. 오늘날 우리는 또 다른 소프트웨어 위기에 직면했지만, 우리는 이에 대해 더 이상 이야기 하지 않는다. 그저 부유한 대기업들이나 이러한 연결된 애플리케이션들을 만들 여유가 있다. 클라우드가 있지만 이는 독점적이다. 우리의 자료와 지식은 우리의 개인 컴퓨터로부터 나타나지 않고 접근할 수도, 우리끼리 경쟁할 수도 없는 클라우드로 들어간다. 누가 우리가 사용하는 사회 연결망을 가지고 있을까? 시대 역설적이게도 그건 바로 메인프레임 컴퓨터다.

우리는 정치적 철학이 담긴 또 다른 책을 남길 수 있다. 핵심은 인터넷이 수많은 연결된 코드의 잠재력을 제공한다는 것이고, 현실은 그 대부분이 우리 손에 닿지 않으며, 이로 인해 코드를 연결할 수 있는 방법이 없고, 그렇게 매우 큰 문제들 (건강, 교육, 환경, 물류 그리고 기타 등등) 을 해결하기 위해 함께 일할 수 있는 두뇌들이 연결될 수 있는 방법이 없기 때문에 이러한 문제들이 해결되지 않는 채 남아있다는 점이다.

연결된 코드들의 도전을 해결하기 위한 많은 시도들이 있어왔다. 그 퍼즐의 각 부분들을 해결하는 수천의 IETF 명세들이 있었다. HTTP는 애플리케이션 개발자에게 있어 이를 위한 충분히 단순한 해결책일 수도 있다. 허나 단언컨데, 이러한 명세는 개발자들과 아키텍트들이 대형 서버와 가볍고 멍청한 클라이언트의 관점에서 생각하도록 장려하기 때문에 문제를 더 악화시킬 뿐이다.

오늘날 사람들은 여전히 raw UDP와 TCP, 독점 프로토콜, HTTP, 그리고 웹소켓을 사용하여 애플리케이션들을 연결하고 있다. 이는 고통스럽고, 느리고, 유연하기 어렵기에 본질적으로 중앙 집중화가 이루어진다. 분산 P2P 아키텍처는 대부분 놀이를 위한 것이지 일하기 위한 것이 아니다. 얼마나 많은 애플리케이션들이 데이터를 교환하기 위해 스카이프나 비트토렌트를 사용하는가?

우리를 프로그래밍의 과학으로 돌아가게 이끄는 것. 세계를 구원하기 위해서, 우리는 2가지가 필요하다. 첫째, "어떻게 모든 코드를 모든 코드에, 모든 곳에서 연결할 것인가"의 일반적인 문제를 해결하기 위한 것. 둘째, 가능한 한 가장 간단하게 사람들이 이해할 수 있고, 사용하기 쉬운 빌딩 블록들을 만드는 것이다.

터무니 없이 간단하게 들리겠지만 아마도 이 말이 핵심일 것이다.

Starting Assumptions

topprevnext

우리는 당신이 ØMQ의 최신 안정된 버전을 사용하고 있으며, 리눅스 또는 유사한 무언가를 사용한다고 가정합니다. 우리는 당신이 예제의 기본 언어인 C 코드를 알고 있다고 가정합니다. 우리가 PUSH, SUBSCRIBE와 같은 상수를 사용할 때 실제적으로는 ZMQ_PUSH , ZMQ_SUBSCRIBE가 사용 될 것이라고 이해 할 수 있다고 가정합니다.

Getting the Examples

topprevnext

이 예제들은 공개 GitHub 저장소 에 있다. 모든 예제를 가져오는 가장 간단한 방법은 이 저장소를 복제하는 것이다.

git clone git://github.com/imatix/zguide.git

다음으로, 이 예제들의 하위 디렉터리를 살펴보자. 당신은 언어별 예제들을 찾을 수 있을 것이다. 당신이 사용하는 언어로 된 예제들이 없다면, 변환된 예제를 제출하는 것을 권한다. 이것이 어떻게 많은 사람들의 작업 덕분에 이 텍스트가 유용하게 되었는가의 이유다. 모든 예제들은 MIT/X11 라이센스 하에 허가된다.

Ask and Ye Shall Receive

topprevnext

이제 몇 가지 코드와 함께 시작해보자. 우리는 Hello World 예제로 코스를 시작한다. 우리는 하나의 클라이언트와 하나의 서버를 만들 것이다. 이 클라이언트는 "Hello"를 서버로 보내고, "World"[figure]를 응답을 받는다. 서버는 C로 되었고, 5555번 포트에 대해 ZeroMQ 소켓을 열여서 요청을 읽은 후에 각 요청에 대해 "World"로 응답한다.


C++ | C# | Clojure | CL | Delphi | Erlang | F# | Felix | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Q | Racket | Ruby | Scala | Tcl | Ada | Basic | ooc

그림 2 - 요청-응답

fig2.png

REQ-REP 소켓 쌍은 서로가 발을 맞추어 가는 방식이다. 클라이언트는 한 반복문 내에서 zmq_send()을 호출한 뒤에 zmq_recv()를 호출한다 (혹은 필요하다면 한번만). 다른 시퀀스를 수행하는 것 (예를 들어, 한번에 두 개의 메시지를 보내는 것) 은 send 혹은 recv 호출로부터 -1의 반환 코드를 얻게될 것이다. 이와 유사하게, 서비스는 필요할 때마다 순서대로 zmq_recv(), zmq_send()를 호출한다.

ZeroMQ는 그 레퍼런스 언어로서 C를 사용하고 우리가 예제들에서 사용할 주 언어다. 당신이 온라인으로 이걸 읽고 있다면, 아래 예제의 링크는 다른 언어로 변환된 예제를 제공한다. 같은 서버 코드를 C++과 비교해보자.

//
// Hello World server in C++
// Binds REP socket to tcp://*:5555
// Expects "Hello" from client, replies with "World"
//

#include <zmq.hpp>
#include <string>
#include <iostream>
#include <unistd.h>

int main () {
// Prepare our context and socket
zmq::context_t context (1);
zmq::socket_t socket (context, ZMQ_REP);
socket.bind ("tcp://*:5555");

while (true) {
zmq::message_t request;

// Wait for next request from client
socket.recv (&request);
std::cout << "Received Hello" << std::endl;

// Do some 'work'
sleep (1);

// Send reply back to client
zmq::message_t reply (5);
memcpy ((void *) reply.data (), "World", 5);
socket.send (reply);
}
return 0;
}

hwserver.cpp: Hello World server

당신은 C와 C++에서 ZeroMQ API가 유사하다는 것을 볼 수 있다. PHP나 Java와 같은 언어에서, 우리는 코드를 더 읽기 쉽게 만들기 위해서 더 많은 것들을 은닉화할 수 있다.

<?php
/*
* Hello World server
* Binds REP socket to tcp://*:5555
* Expects "Hello" from client, replies with "World"
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/

$context = new ZMQContext(1);

// Socket to talk to clients
$responder = new ZMQSocket($context, ZMQ::SOCKET_REP);
$responder->bind("tcp://*:5555");

while(true) {
// Wait for next request from client
$request = $responder->recv();
printf ("Received request: [%s]\n", $request);

// Do some 'work'
sleep (1);

// Send reply back to client
$responder->send("World");
}

hwserver.php: Hello World server

//
// Hello World server in Java
// Binds REP socket to tcp://*:5555
// Expects "Hello" from client, replies with "World"
//

import org.zeromq.ZMQ;

public class hwserver {

public static void main(String[] args) throws Exception {
ZMQ.Context context = ZMQ.context(1);

// Socket to talk to clients
ZMQ.Socket responder = context.socket(ZMQ.REP);
responder.bind("tcp://*:5555");

while (!Thread.currentThread().isInterrupted()) {
// Wait for next request from the client
byte[] request = responder.recv(0);
System.out.println("Received Hello");

// Do some 'work'
Thread.sleep(1000);

// Send reply back to client
String reply = "World";
responder.send(reply.getBytes(), 0);
}
responder.close();
context.term();
}
}

hwserver.java: Hello World server

다른 언어들에서의 서버 코드:


C++ | C# | Clojure | CL | Delphi | Erlang | F# | Felix | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Q | Racket | Ruby | Scala | Tcl | Ada | Basic | ooc

클라이언트 코드:


C++ | C# | Clojure | CL | Delphi | Erlang | F# | Felix | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Q | Racket | Ruby | Scala | Tcl | Ada | Basic | ooc

실질적으로 매우 쉽게 보이지만, ZeroMQ 소켓들은 앞서 배운 것과 같이 강력한 힘을 가진다. 수천의 클라이언트들이 이 서버에 한꺼번에 요청해도 빠르고 안정적으로 동작할 것이다. 재미삼아 클라이언트를 먼저 시작하고, 그 후에 서버를 시작해보자. 이제 모두 여전히 동작하는지 지켜본 후에 이게 의미하는 바를 잠깐 생각해보자.

이 두 프로그램이 실제로 어떻게 동작하는지를 간단히 설명해보고자 한다. 이들은 동작을 위한 ZeroMQ 컨텍스트와 소켓을 하나 만든다. 이 말이 무엇을 의미하는지는 곧 알게될 테니 신경쓰지 말자. 서버는 자신의 REP(응답) 소켓을 5555번 포트와 묶는다. 서버는 루프 내에서 요청을 대기하고, 요청이 올 때마다 응답한다. 클라이언트는 요청을 보내고 서버로부터 돌아오는 응답을 읽는다.

서버를 죽이고 (Ctrl-C) 재시작 한다면, 클라이언트는 정상적으로 복구되지 않을 것이다. 깨진 프로세스로부터 복구를 하는 것은 그리 쉬운 일이 아니다. 신뢰성 있는 요청-응답 흐름을 만드는 것은 우리가 [#reliable-request-reply]를 배우기 까지는 다룰 수 없을 정도로 복잡하다.

무대 뒤에서 많은 일들이 일어나지만 프로그래머들이 걱정할 것은 어떻게 짧고 친절한 코드를 만드는지, 그리고 고부하 상태에서 얼마동안 문제가 발생하지 않는지이다. 이것은 요청-응답 패턴이고, 아마 ZeroMQ를 사용하는 가장 쉬운 방법일 것이다. ZeroMQ는 RPC나 고전적인 클라이언트/서버 모델에 해당된다.

A Minor Note on Strings

top prev next

ØMQ은 전송하려는 데이터의 바이트 크기를 제외하고 관여하지 않습니다. 이것은 어플리케이션이 그것을 다시 읽을 수 있도록 안전하게 포맷팅 해야 할 책임은 여러분에게 있다는 것을 의미합니다. 객체와 복잡한 데이터 유형을 위한 것은 프로토콜 버퍼와 같은 전문 라이브러리 작업에 해당됩니다. 그래서 문자열에 대해서 신경을 써야 합니다.

C와 다른 언어에서 문자열은 NULL byte로 종료됩니다. 우리는 "HELLO"와 추가 NULL byte를 같이 문자열로 보낼 수 있습니다.:

zmq_msg_init_data (&request, "Hello", 6, NULL, NULL);

당신이 다른 언어에서 문자열을 보낼 경우, 아마도 그 NULL 바이트를 포함하지 않을 것입니다. 예를 들어, 우리는 Python에서 동일한 문자열을 보낼 때, 아래와 같이 합니다. :

socket.send ("Hello")

이것은 아래와 같이 표현이 됩니다. :

fig3.png

그리고 이것을 C 프로그램에서 읽으면, 문자열 같이 보이는 무엇인가를 얻게 됩니다. 이것은 적절한 문자열이 아니면 문제가 발생될 수 있습니다. (만약 5bytes다음에 NULL이 따라온 다면 다행입니다.). 이것은 클라이언트와 서버가 문자열 형식이 일치하지 않으면 이상한 결과를 얻을 수 있다는 것을 의미 합니다.

ØMQ에서 문자열 데이터를 수신할 때, C에서는 문자열이 안전하게 종료되었는지 신임할 수 없습니다. 문자열은 읽을 때마다 매번 여분의 byte를 위한 충분한 새로운 버퍼를 할당하고, 문자열을 복사하고, 적당하게 종료문자 NULL을 넣어야 합니다.

그러면, ØMQ 문자열을 길이와 종료문자 NULL없이 보내봅시다. 가장 간단한 경우에는 (예제에서 이것을 해 볼 것입니다.) ØMQ 문자열은 위의 그림에서 보이는 것처럼 길이와 문자열로 된 ØMQ 메시지 프레임으로 되어 있습니다.

C언어에서는 ØMQ 문자열을 받고, 어플리케이션이 가용한 C 문자열을 받기 위해서는 아래와 같이 할 필요가 있습니다.:

// Receive 0MQ string from socket and convert into C string
static char *
s_recv (void *socket) {
zmq_msg_t message;
zmq_msg_init (&message);
zmq_recv (socket, &message, 0);
int size = zmq_msg_size (&message);
char *string = malloc (size + 1);
memcpy (string, zmq_msg_data (&message), size);
zmq_msg_close (&message);
string [size] = 0;
return (string);
}

이것으로 함수를 만들면 좋습니다. 올바른 ØMQ 포맷 문자열을 보내는 ‘s_send’와 유사한 이름으로 함수를 만들고 재사용할 수 있도록 헤더파일을 만들므로 재사용 할 수 있습니다.

C언어로 ØMQ 어플리케이션을 만드는 것이 좀더 쉽고 짧게 되는 것이 zhelpers.h 때문입니다.
이것은 상당히 긴 소스라서 개발자들은 여유를 가지고 재미있게 읽기를 바랍니다.

Version Reporting

top prev next

ØMQ는 꽤 자주 여러 번의 버전을 거쳐왔으며, 만약 문제가 발생되면 이후 버전에서 해결되었습니다. 아래 ØMQ의 버전을 알 수 있는 짧은 프로그램이 있습니다.:


C++ | C# | CL | Erlang | F# | Java | Lua | Objective-C | PHP | Python | Ruby | Ada | Basic | Clojure | Go | Haskell | Haxe | Node.js | ooc | Perl | Scala

Getting the Message Out

top prev next

두 번째 고전적인 패턴은 서버가 클라이언트들에게 정보를 PUSH하는 단방향 데이터 분산 입니다. 우편 번호, 온도 및 상대 습도의 날씨 정보를 업데이트하는 것이 아래 예제입니다. 실제 날씨처럼random값을 생성할 것입니다.

이것이 서버이며, 포트 5556를 사용합니다.:


C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | ooc

업데이트 정보의 시작과 끝이 없습니다. 이것은 끝이 없는 broadcast 와 같습니다.

fig4.png

여기에서 클라이언트 프로그램은 원하는 zipcode(어떤 모험을 시작하기에 좋은 장소이기 때문에 기본 값은 New York City로 하자)에 대한 정보를 가져옵니다.:


C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | ooc

SUB 소켓을 사용하는 경우, 반드시 zmq_setsockopt(3)를 사용하여 subscription을 설정해야 합니다. 만약 subscription을 설정하지 않으면 어떤 메시지도 받을 수 없습니다. 이것은 초보자들이 많이 실수하는 것입니다. Subscriber는 많은 subscription을 설정할 수 있습니다. 즉, 어떤 subscription에 매칭이 되면 subscriber는 메시지를 수신합니다. Subcriber는 특정 subscription을 수신하지 않을 수 있습니다. Subscription은 length-specified blobs입니다. 상세한 내용은 zmq_setsockopt(3)을 참조하십시오.

PUB?SUB 소켓 한 쌍은 비동기입니다. 클라이언트는 루프 (또는 한번)에서, zmq_recv(3)를 사용합니다. SUB 소켓에서 메시지를 보내려고 하면 오류가 발생합니다. 마찬가지로 서버는 필요한 만큼 zmq_send(3)을 사용하며 PUB 소켓에서는 zmq_recv(3)을 사용하면 안 됩니다.

ØMQ에서는 이론상으로, 이것을 어느곳에 연결(connect)하든, 어느곳에 바인드(bind)하든 문제가 되지 않습니다. 그러나 만약 당신이 PUB-SUB소켓에서 SUB소켓에 바인드하고 PUB소켓에 연결을 한다면, SUB소켓은 오래된 메시지를 받을 수 있습니다. 즉, SUB이 시작되기 전에 메시지를 보낸 것입니다. 이것은 바인딩하고 연결하는 한개의 아티팩트 입니다. 그러나 가능하면 PUB은 바인드(bind)를하고 SUB은 연결(connect)를하는 것이 가장 좋습니다.

PUB-SUB 소켓에서 알아야 될 중요한 한 가지가 있습니다 :
Subscriber는 메시지를 가져오기 시작하는 시간을 정확히 모릅니다. 심지어 subscriber가 시작되어 기다리고 있고, publisher가 작동하고 있어도 그렇습니다. Subscriber는 publisher가 보낸 첫 번째 메시지를 항상 잃을 수도 있습니다. 왜냐하면 subscriber는 publisher(접속시간은 짧지만, 없지는 않다.)에 접속을 해야 하며, 그 동안 publisher가 이미 메시지를 보냈을 수도 있기 때문입니다.

"slow joiner"현상은 상세히 이것을 설명하기에 충분합니다. ØMQ는 백그라운드로 비동기 처리한다는 것을 기억하시기 바랍니다. 이 순서로 이렇게 처리하는 두 노드를 가집니다.:

  • Subscriber는 endpoint에 연결 후 수신하고 메시지를 셉니다.
  • Publisher는 endpoint에 바인딩한 후 즉시 1000개의 메시지를 보냅니다.

그러면 subscriber는 대부분 아무것도 받을 수 없습니다. 당신은 왜 그런지 모른체 올바른 필터를 설정했는지 확인하고 다시 시도해도 아무것도 받을 수 없습니다.

TCP에 연결하기 위해서는 네트워크나 Peers사이의 hop수에 따라 몇 milliseconds가 걸리는 핸드쉐이킹을 합니다. 그 때 ØMQ는 매우 많은 메시지를 보낼 수 있습니다. 예를들면, 연결하기 위해 5msecs가 소요되고, 그 연결로 초당 1M 메시지를 처리할 수 있다고 가정해 봅시다. Subscriber가 publisher에 연결하는 5 msecs 동안, publisher는 1K 메시지를 보내는데 단 1msec가 걸립니다.

2장에서는 subscriber가 연결하고 준비되기까지 데이터를 발송하지 않도록 publisher와 subscriber를 동기화 하는 방법에 대해서 설명할 것입니다. Publisher를 sleep하여 대기하는 것은 간단하지만 옳지 못한 방법이기에 실제 응용 프로그램은 이렇게 작업을 수행하지 않을 것입니다. 그것은 너무 세련되지 못하고 느립니다. 무슨 일이 발생할지 sleep를 사용해 볼 수 있지만, 어떤 방법이 있는지 2장에서 보도록 합시다.

동기화 대안은 단순히 게시된 데이터 스트림이 무한이며 시작도없고 끝도 없다고 가정하는 것입니다. 이것은 우리가 기상 클라이언트 예제를 만드는 방법에서 다룰 것입니다.

클라이언트는 선택 되어진 우편코드만 subscribe하고 천 개의 최신 우편번호를 수집합니다. 이것은 우편번호를 무작위로 배포할 수 있는 서버로부터 약 10만개 최신정보를 받았다는 것을 의미합니다. 클라이언트가 시작되고 그 다음 서버가 시작된다면 클라이언트는 기다릴 것입니다. 서버는 때로는 여러 번 재 구동 하지만 클라이언트는 기다릴 것입니다. 클라이언트는 천 개의 최신정보를 수집했을 때 평균을 계산해 출력하고 종료됩니다.

Publish-subscribe패턴의 몇 가지 특징 :

  • Subscriber는 사실 ‘connect’를 매번 호출을 하는 방식으로 한 개 이상의 publisher에 연결할 수 있다. 메시지는 번갈아 가면서 각 publisher로부터 도착될 것입니다.
  • Subscriber가 없다면 모든 Publisher의 메시지는 유실됩니다.
  • TCP를 사용하고, subscriber가 느리다면 메시지는 publisher의 큐에 쌓일 것이다. 나중에 "high-water mark"를 사용하면서 publisher를 보호하는 방법에 대해 살펴보겠습니다.
  • ØMQ의 현재 버전에서 필터링은 subscriber쪽에서 하며 publisher쪽에서는 하지 않습니다. 이것은 TCP상에서 publisher는 모든 메시지를 모든subscriber에게 보내지만, subscriber는 원하는 메시지만 받습니다.

이것은 인텔 4 코어 Q8300에서 10MB 메시지를 필터하여 받는데 얼마나 걸리는지 보여주고 있으며, 빠르지만 특별한 것은 없습니다. :

ph@ws200901:~/work/git/ØMQGuide/examples/c$ time wuclient
Collecting updates from weather server...
Average temperature for zipcode '10001 ' was 18F

real    0m5.939s
user    0m1.590s
sys     0m2.290s

Divide and Conquer

top prev next

마지막 예제에서는 (여러분이 확실히 juicy code의 피곤과 비교 추상 표준에 대한 언어학적 논의로 다시 탐구하기를 원합니다) 작은 슈퍼컴퓨팅을 해봅시다. 그런 다음 커피한잔 합시다. 우리의 슈퍼 컴퓨팅 응용 프로그램은 상당히 전형적인 병렬처리 모델입니다 :

  • 우리는 생산공정을 병렬로 처리할 수 있는 ventilator 를 가지고 있습니다.
  • 우리는 프로세스 공정 수행 Workers를 가지고 있습니다.
  • 우리는 worker 프로세스에서 다시 결과를 수집하는 Sink가 있습니다.

사실, Workers는 어려운 수학을 연산하는GPU(그래픽 처리 장치)를 사용하는 superfast 상자를 실행합니다. 여기 ventilator가 있습니다. 이것은 100타스크를 생성하며 각각은 수 milliseconds동안 sleep을 하는 worker에 전달하는 메시지 입니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | ooc
fig5.png

여기 worker 응용 프로그램입니다. 그것은 메시지를 받고 몇 초 동안 sleep을 한 다음 완료 신호를 받습니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | ooc

여기 sink어플리케이션이 있습니다. 이것은 100개의 작업을 수집한 후 전체 처리시간이 얼마인지 계산을 합니다. 그래서 worker가 여러 개 라면 실제 병렬로 처리되는 것을 확인할 수 있습니다 :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | ooc

배치의 평균 소요시간은 5 초입니다. 우리는1, 2, 4 작업자가 시작할 때 sink로부터 아래와 같은 결과를 얻을 수 있습니다. :

#   1 worker
Total elapsed time: 5034 msec
#   2 workers
Total elapsed time: 2421 msec
#   4 workers
Total elapsed time: 1018 msec

좀더 자세하게 코드의 몇 가지 측면을 살펴보겠습니다. :

  • Worker는 ventilator에 위로 연결되어 있고, sink와는 아래로 연결되어 있습니다. 이것은 worker를 임의로 추가 할 수 있다는 것을 의미 합니다. worker가 그것들의 종점에 바인딩되어 있다면 worker가 추가할 때마다 매번 ventilator와 sink가 변경하기 위해서 더 많은 종점이 필요하게 됩니다. 이 구조에서 ventilator와 sink는 stable part이며 작업자는 dynamic part라고 부릅니다.
  • 모든 worker는 시작을 동기화하여 실행되어야 합니다. 이것은 ØMQ에서는 일반적인 것이지만 쉬운 솔루션은 아닙니다. 연결하는 데는 특정한 시간이 걸립니다. 그래서 worker들이 ventilator에 접속할 때 처음 연결에 성공한 작업자는 다른 작업자가 연결하는 짧은 시간 동안 전체메시지를 받게 됩니다. 어떻게든 시작을 동기화하지 않으면 시스템은 병렬로 실행되지 않습니다. 기다림을 제거하는 것을 해 봅시다.
  • ventilator의 PUSH 소켓은 균등하게 근로자 (시작하기 전에 작업자가 모두 연결되어 있다고 가정한다.)에 작업을 분배합니다. 이것은 load-balancing 이라고 하며, 이것이 무엇인지 자세히 다시 보게 될 것입니다.
  • Sink의 PULL소켓은 균등하게 노동자로부터 결과를 수집합니다. 이것은 fair-queuing이라고 합니다 :
fig6.png

pipeline패턴은 또한 PUSH소켓이 적당하게 load-balancing되지 않는 ‘slow joiner’가 발생됩니다. 당신이 PUSH와 PULL을 사용한다면 작업자중에 한명은 다른 작업자보다 더 많은 메시지를 얻게 될 것입니다. 이것은 그 PULL소켓이 다른 것보다 빨리 연결되어 다른 것들이 연결을 하는 동안 더 많은 메시지를 받기 때문입니다.

Programming with ØMQ

top prev next

몇 가지 예제를 보겠습니다. 당신은 몇몇 어플리케이션에서 ØMQ를 사용하길 원할 것입니다. 그전에 심호흡과 진정을 하고 스트레스와 혼돈을 피하기 위해 몇 가지 기본적인 조언을 하겠습니다.

  • 단계별로 ØMQ를 배워가십시요. 이것은 간단한 API지만 많은 가능성들이 숨겨져 있습니다. 천천히 가능한 것을 배워가면서 각각 하나씩 마스터 하시길 바랍니다.
  • 좋은 코드를 작성하세요. 보기 좋지 않은 코드는 문제를 숨기고 다른 사람의 도움도 어렵게 만듭니다. 의미 없는 변수는 사용해도 되지만 사람들이 당신의 코드를 읽는 것을 어렵게 합니다. 이 변수가 실제 무슨 역할을 하는지 너무 조심성 있게 정하기 보다는 무슨 의미인지 실제 단어를 사용하여 명명 하세요. 일관된 들여쓰기와 깨끗한 레이아웃을 사용하세요. 좋은 코드를 작성하는 것은 당신을 좀더 편안하게 할 것입니다.
  • 당신이 만든 코드를 테스트하세요. 프로그램이 동작하지 않을 때 5라인이 문제인 것을 알 것입니다. 이것은 당신이 코딩한 처음은 동작하지 않는다는 ØMQ 마법이 사실이라는 것을 증명합니다.
  • 기대한 대로 동작하지 않을 때 코딩을 멈추고 문제가 있는 부분을 테스트 하세요. ØMQ는 본질적으로 모듈화 코드를 만들 수 있도록 되어 있어서 자신에게 유리하게 사용할 수 있습니다.
  • 적절하게 클래스, 메소드 등을 만드세요. 만약 너무 많은 코드를 복사/붙여 넣기 하면 복사/붙여 넣기에서 에러가 발생할 수 있습니다.

예를 들어 이것은 누군가 나에게 교정을 요청했던 코드의 일부입니다. :

//  NOTE: do NOT reuse this example code!
static char *topic_str = "msg.x|";

void* pub_worker(void* arg){
    void *ctx = arg;
    assert(ctx);

    void *qskt = zmq_socket(ctx, ZMQ_REP);
    assert(qskt);

    int rc = zmq_connect(qskt, "inproc://querys");
    assert(rc == 0);

    void *pubskt = zmq_socket(ctx, ZMQ_PUB);
    assert(pubskt);

    rc = zmq_bind(pubskt, "inproc://publish");
    assert(rc == 0);

    uint8_t cmd;
    uint32_t nb;
    zmq_msg_t topic_msg, cmd_msg, nb_msg, resp_msg;

    zmq_msg_init_data(&topic_msg, topic_str, strlen(topic_str) , NULL, NULL);

    fprintf(stdout,"WORKER: ready to receive messages\n");
    //  NOTE: do NOT reuse this example code, It's broken.
    //  e.g. topic_msg will be invalid the second time through
    while (1){
    zmq_send(pubskt, &topic_msg, ZMQ_SNDMORE);

    zmq_msg_init(&cmd_msg);
    zmq_recv(qskt, &cmd_msg, 0);
    memcpy(&cmd, zmq_msg_data(&cmd_msg), sizeof(uint8_t));
    zmq_send(pubskt, &cmd_msg, ZMQ_SNDMORE);
    zmq_msg_close(&cmd_msg);

    fprintf(stdout, "received cmd %u\n", cmd);

    zmq_msg_init(&nb_msg);
    zmq_recv(qskt, &nb_msg, 0);
    memcpy(&nb, zmq_msg_data(&nb_msg), sizeof(uint32_t));
    zmq_send(pubskt, &nb_msg, 0);
    zmq_msg_close(&nb_msg);

    fprintf(stdout, "received nb %u\n", nb);

    zmq_msg_init_size(&resp_msg, sizeof(uint8_t));
    memset(zmq_msg_data(&resp_msg), 0, sizeof(uint8_t));
    zmq_send(qskt, &resp_msg, 0);
    zmq_msg_close(&resp_msg);

    }
    return NULL;
}

이것은 제가 버그를 찾은 부분으로 재 작성 한 것입니다. :

static void *
worker_thread (void *arg) {
void *context = arg;
void *worker = zmq_socket (context, ZMQ_REP);
assert (worker);
int rc;
rc = zmq_connect (worker, "ipc://worker");
assert (rc == 0);

void *broadcast = zmq_socket (context, ZMQ_PUB);
assert (broadcast);
rc = zmq_bind (broadcast, "ipc://publish");
assert (rc == 0);

while (1) {
char *part1 = s_recv (worker);
char *part2 = s_recv (worker);
printf ("Worker got [%s][%s]\n", part1, part2);
s_sendmore (broadcast, "msg");
s_sendmore (broadcast, part1);
s_send (broadcast, part2);
free (part1);
free (part2);

s_send (worker, "OK");
}
return NULL;
}

결국, 문제는 어플리케이션이 기묘하게 충돌하는 스레드 사이에 소켓을 사용 했기에 비롯되었습니다. 이것은 ØMQ/2.1에서 수정되었지만, 위험하기에 다시 그렇게 하지 않도록 충고하는 것입니다.

ØMQ/2.1

top prev next

MQ/2.0시작은 low-latency분산 메시징의 어려움을 겪다가, buzzworlds 및 기업용어의 무거운 코트를 벗어 마구 흔들고, 마치 아무런 제한이 없다는 듯이 최고조에 도달했을 때입니다. 우리는 2010년 8월의 뜨거운 여름날 ØMQ/2.0.8을 양산한 이후에 안정적인 버전으로 사용하게 되었습니다

그러나 시대가 변화되어 2010년에 멋진 것들이 2011년에는 더 이상 유행하지 않았습니다. ØMQ 개발자 및 커뮤니티는 세련된 메시징으로 바꾸기 위해서 미친듯이 작업해서, 새로운 안정적인 버전 2.1이 되었습니다.

아래 가이드는 2.1.x버전과 기존 2.0의 차이를 나열한 것입니다. :

  • 2.0에서는 zmq_close (3)zmq_term (3)에서 전송중인 메시지가 삭제되었습니다.. 그래서 소켓을 close하고, 메시지를 전송한 후 곧바로 종료시키는 것은 안정적이지 않았습니다. 2.1에서는 이 API는 safe합니다:zmq_term은 보내려고 기다리고 있는 것을 flush합니다. 2.0에서는 이문제로 해결하려고 sleep(1)을 추가했지만, 2.1에서는 이것이 필요하지 않습니다.
  • 반대로, 2.0에서 만약 소켓이 오픈되어 있다면 zmq_term(3) 을 호출하는 것이 safe합니다. 2.1에서 이것은 safe하지 않으며 이것은 zmq_term이 블락킹 되는 원인이 될 수 있습니다. 그래서 2.1에서는 종료하기 전에 모든 소켓을 닫아야 합니다. 보낼 메시지가 있거나 소켓이 연결을 대기하고 있다면 기본적으로 2.1에서는 이것을 전달하기 위해 영원히 노력하고 기다릴 것입니다. 당신은 zmq_term을 호출하기 전에 아직 활동중인 모든 소켓에 ‘LINGER’ 소켓옵션을 설정해야 합니다. :

int zero = 0;
zmq_setsockopt (mysocket, ZMQ_LINGER, &zero, sizeof (zero));

  • 2.0에서 zmq_poll(3)은 불 특정하게 반환이 될 것입니다. 그래서 당신은 타이머로써 이것을 사용할 수 없습니다. 2.1에서 zmq_poll은 이벤트가 있을 때까지 기다립니다.

*2.0에서, ØMQ는 중단(interrupted) 시스템 호출을 무시 합니다. 동작 중에 이런 신호를 받으면 EINTR을 반환 하지 않습니다. 이것은 특히 런타임에 SIGINT (Ctrl - C를 처리)와 같은 신호를 손실하는 문제가 발생됩니다. 2.1에서는, 중단신호가 발생되면 zmq_recv(3)와 같은 차단 호출 EINTR 반환합니다.

Getting the Context Right

top prev next

ØMQ 응용 프로그램은 항상 context를 만드는 것에서 시작하고, 다음은 소켓을 생성하기 위해서 사용합니다. C에서 이것을 위해 zmq_init(3)을 호출합니다. 당신은 당신의 프로세스에서 정확하게 한 개의 context를 생성하고 사용해야 합니다. 기술적으로, ,context는 단일 프로세스에서 모든 소켓을 위한 container이며, inproc 소켓(한 프로세스에서 스레드들을 연결하기 위한 가장 빠른 방법)을 통해 전송됩니다. 만약 런타임에 한 프로세스가 2개의 context를 가지고 있다면 이것은 구분된 ØMQ instances일 것입니다. 이것이 당신이 원하는 것이라면 상관없지만, 여하튼 이것만은 기억하세요. :

당신의 메인 코드의 시작에는zmq_init(3) , 그리고 마지막에는 zmq_term(3)을 사용하세요.

만약 fork() 시스템 콜을 사용한다면, 각각의 프로세스는 자신의 context를 필요로 합니다. 만약 fork()를 호출하기 전에 메인 프로세스에서zmq_init(3)을 사용한다면, 자식 프로세스는 그들 자신의 context를 얻을 것입니다. 일반적으로 자식 프로세스는 중요한 일을 수행하고 부모 프로세스는 이것을 관리 합니다.

Making a Clean Exit

top prev next

품위 있는 프로그래머는 고상한 암살단과 같습니다 - 작업이 끝 났을 때 항상 깨끗이 정리합니다. 당신이 Python와 같은 언어로 ØMQ를 사용할 때 자동으로 객체를 free해 줍니다.. 그러나 C를 사용할 때는 작업이 끝났을 때 주의 깊게 객체를 free해야 합니다.

아니면 메모리 누수(leak)나 불안정한 어플리케이션, 나쁜 Karma에 빠질 수 있습니다. 메모리 누수(leak)은 한가지 이지만, ØMQ는 당신이 어플리케이션을 어떻게 종료하는지에 꼼꼼하게 신경을 씁니다. 그 이유는 만약 어떤 소켓을 열고 있다면 zmq_term(3) 함수는 영원히 끊기지 않으며, 심지어 모든 소켓을 닫았다고 해도, zmq_term(3) 은 연결되어 있거나 보내는 것이 있다면 영원히 기다릴 것입니다. 이들 소켓을 종료하기 전에 LINGER을 zero로 설정하지 않았어도 말입니다.

우리가 걱정해야 하는 ØMQ 개체는 메시지, 소켓, 그리고 context입니다. 다행히 그것은 적어도 간단한 프로그램에서는 매우 간단 합니다. :

  • 항상 zmq_msg_close(3)을 사용하여, 메시지를 사용한 후 즉시 닫습니다.
  • 많은 소켓을 열고 닫고 한다면 당신의 어플리케이션을 재설계 해야 합니다.
  • 프로그램을 종료할 때 소켓을 닫고 zmq_term(3) 을 호출하면 context는 사라집니다.

당신이 다중 스레드 작업을 하고 있다면 이것보다 좀더 복잡할 것입니다. 다음 장은 다중 스레딩에 대해서 다룰 것입니다. 경고에도 불구하고 다수의 사람들은 안정적으로 걷기 전에 달려가려고 할 것입니다. 아래 다중 스레드 ØMQ 어플리케이션에서 깔끔하게 종료하기 위한 빠르고 조잡한 가이드가 있기 때문입니다.

첫째, 멀티스레드에서 같은 소켓을 사용하지 말기 바랍니다. 매우 재미있을 것이라는 여러분의 생각을 얘기하지 마세요. 단지 그렇게 하지 말기 바랍니다. 둘째, relingerfy와 모든 소켓을 닫고 메인 스레드의 context를 종료하면 결국, 이것은 에러를 리턴하기 위한 스레드(즉, 같은 context를 공유하는 것)에서 receive/poll/send blocking의 원인이 됩니다. 이렇게 되면 relingerize 그리고, 그 스레드에서 소켓이 닫고 종료됩니다. 두번째 같이 context를 종료하지 마세요. 모든 소켓을 안전하게 닫혀질 때까지 메인 스레드에서 zmq_term은 기다릴 것입니다.

짜잔!, 이것은 복잡하고 고통스러운 일입니다. 그래서 언어를 개발한 개발자가 자동으로 이렇게 동작하도록 하고 즉시 소켓을 닫게 만들 것입니다.

Why We Needed ØMQ

top prev next

이제 ØMQ을 보았으니까, 왜 필요한지로 돌아갑니다.

요즘 대부분의 어플리케이션은 여러 종류의 네트워크, LAN이나 Internet을 통하는 컴포넌트로 구성합니다. 그래서 많은 어플리케이션 개발자들은 메시징을 처리하게 됩니다. TCP나 UDP를 사용할 시간이 없는 몇몇 개발자들은 메시지 큐잉 제품을 사용합니다. 이들 프로토콜은 사용하기 쉽습니다. 그러나 A에서 B로 문자열을 전송하는 것과 신뢰 할 수 있는 방식으로 메시징하는 것에는 큰 차이가 있습니다.

우리가 raw TCP를 사용하여 connection할 때 직면하게 되는 전형적인 문제를 살펴 보겠습니다. 재사용 가능한 메시징은 모두 또는 대부분 이를 해결해야 합니다. :

  • 우리는 I/O를 어떻게 처리합니까? 우리의 어플리케이션을 blocking합니까? 아니면 백그라운드에서 I/O를 처리합니까? 이것은 디자인 결정의 핵심입니다. I/O Blocking하는 것은 스케일이 좋지 않는 아키텍처가 됩니다. 그러나 I/O를 백그라운드로 처리하는 것은 잘 동작하게 하기 위해 매우 어려울 수 있습니다.
  • 일시적으로 사용하는 다이나믹 컴포넌트는 어떻게 다룹니까? 보통 ‘클라이언트’와 ‘서버’ 컴포넌트로 구분하며, 서버는 항상 실행되어 있습니다. 그 다음에 우리는 서버에서 서버를 연결하려면 어떻게 해야 합니까? 몇 초마다 연결하려고 합니까?
  • 어떻게 메시지를 표현 합니까? buffer overflow로 부터 안전하고 작은 메시지에 효과적이고 파티모자를 쓰고 춤을 추는 고양이가 나오는 큰 동영상에도 적합하게 쉽게 쓰고 읽을 수 있기 위해서 어떻게 데이터를 프레임 합니까?
  • 즉시 제공할 수 없는 메시지를 어떻게 처리합니까? 특히, 우리가 온라인으로 돌아올 컴포넌트를 위해 기다리고 있다면? 우리는 메시지를 버리고 그것들을 데이터베이스나 메모리 큐에 넣겠습니까?
  • 어디에 메시지 큐를 저장합니까? 큐로부터 읽고 있는 컴포넌트가 너무 느리고, 큐를 만드는데 발생되는 것이 무엇입니까? 그 다음 우리의 전략은 무엇입니까?
  • 어떻게 메시지 유실을 처리합니까? 다음 메시지를 기다립니까? 재전송을 요청합니까? 아니면 메시지를 잃어 버리지 않도록 보장하는 신뢰 가능한 레이어 같은 것을 만들어야 합니까? 만약 그 레이어 자체가 깨지면 어찌 됩니까?
  • 서로 다른 네트워크간의 전송이 필요할 때 어떻게 합니까? 이를테면 TCP unicast대신에 multicast나, IPv6경우 어플리케이션을 다시 만들어야 됩니까? 아니면 일부 레이어의 추상화된 전송부분을 수정해야 합니까?
  • 어떻게 메시지 경로를 줍니까? 같은 메시지를 여러 peers에 보낼 수 있습니까? 요청자에게 reply를 보낼 수 있습니까?
  • 우리가 다른 언어에 대한 API를 작성하려면 어떻게 해야 합니까? 우리는 다시 wire-level 프로토콜을 다시 구현하거나, 라이브러리를 다시 패키지 해야 합니까? 전자의 경우 어떻게 효율적이고 안정적인 stacks을 보장합니까? 후자의 경우 우리가 어떻게 상호 운용성을 보장할 수 있습니까?
  • 다른 아키텍쳐 사이에 메시지를 읽을 수 있도록 하기 위해 어떻게 데이터를 표시합니까? 우리는 데이터 유형에 대한 특정 인코딩을 시행합니까? 높은 레이어 작업보다 오히려 메시징 시스템의 작업이 얼마나 걸립니까?
  • 우리가 어떻게 네트워크 오류를 처리합니까? 우리가 기다리고 재시도하고, 자동으로 무시하거나, 중지합니까?

Hadoop Zookeeper와 같은 전형적인 오픈소스 프로젝트에서 얻은 C API 코드 src/c/src/zookeeper.c를 읽어 보면, client-server 네트워크 통신 프로토콜로 문서화되지 않은 신비로운 3200라인이 있습니다. 개인적으로 select()대신에 poll()을 사용하기 때문에 그것이 효과적이라고 생각하지만 사실, Zookeeper는 일반적인 메시징 레이어와 명시적으로 문서화된 wire레벨 프로토콜을 사용해야 합니다. 이것은 반복되는 특정 모듈를 만드는 팀에게는 매우 소모적인 작업입니다.

fig7.png

그러나 재사용 가능한 메시징 레이어는 어떻게 만들까요?. 그렇게 많은 프로젝트들이 이 기술을 필요로 할 때 여전히 그들의 코드에서 TCP 소켓을 사용하고, 반복적으로 오래된 목록에서 그 문제를 해결하는 어려운 방법으로 진행합니까?

이것은 재사용 가능한 메시징 시스템을 구축하는 것은 몇몇 FOSS프로젝트에서 시도를 해 봤던 적이 있지만 정말 어렵고, 왜 상용 메시징 제품은 복잡하고 비싸고, 유연성이 떨어지고, 불안전한지를 증명하는 것입니다. 2006년, iMatrix는 FOSS개발자들이 메시징 시스템에 아마도 처음으로 재사용성을 제공하도록 AMQP를 설계했습니다. AMQP는 많은 다른 디자인보다 잘 동작하지만 http://www.imatix.com/articles:whats-wrong-with-amqp 상대적으로 비싸고, 복잡하고, 불안전 합니다.] 이것은 사용법을 배우는 데는 수 주일이 걸리며, 여러 상황에서도 안정된 아키텍쳐로 만드는 데는 수개월이 걸립니다.

재사용 가능한 방법으로 문제되는 긴 목록을 해결하려고 AMQP와 같은 대부분의 메시징 프로젝트는 addressing, routing, queuing하는 “broker”라는 새로운 개념을 발명해 왔습니다. 이렇게, 어플리케이션이 broker와 통신할 수 있는 client-server 프로토콜 이나 몇몇 문서화되지 않은 프로토콜 API집합이 생겨났습니다. Broker는 대형 네트워크의 복잡성을 줄일 수 있는 훌륭한 일을 하고 있습니다. 그러나 Zookeeper와 같은 제품에 broker기반 메시징을 추가하는 것은 더 나빠지는 것이며, 그럴만한 장점도 가치도 없습니다. 이것은 단지 커지는 것이며, 한 결함이 더해질 수 있습니다. Broker는 빠르게 병목과 관리의 새로운 위험이 됩니다. 소프트웨어가 이것을 지원한다면 2,3,4 broker를 추가하고 몇 가지 fail-over 구조를 만들어야 될 수 있습니다. 이렇게 하게 되면 변경이 많아지고, 더 복잡하고, 위험요소들이 많이 생깁니다.

그리고 broker주심의 환경은 자체 운영팀이 필요합니다. 당신은 말 그대로 broker를 밤낮으로 감시해야 하고, 그것이 오동작할 때 조치를 해야 합니다. 그런 시스템이 필요하고, 백업 시스템이 필요합니다. 그리고 이들 시스템을 관리하는 사람이 필요합니다. 이것은 단지 몇 년 동안 여러 팀에 의해 만들어지는 거대한 어플리케이션을 위한 가치일 뿐입니다.

그래서 몇몇 중간 어플리케이션 개발자들은 네트워크 프로그램을 피하고 규모가 작은 단일 어플리케이션을 만듭니다. 아니면 그들은 네트워크 프로그램을 건너 뛰고, 불안전하고 복잡한 관리하기 어려운 어플리케이션을 만듭니다. 아니면 메시징 제품을 구입하여 비싸고 불안정한 기술로 뒤 덮인 확장 어플리케이션으로 끝나게 됩니다. 메시징은 지난 세기에 크게 정착하거나 마음을 움직일 만한 좋은 선택이 못 되어 왔습니다. 단지, 사용자에게는 부정적이고, 지원과 라이선스를 판매하는 사람들에게는 기쁨일 뿐이었습니다.

fig8.png

우리가 필요한 것은 제로 비용에 가깝게 어떤 어플리케이션에서도 동작할 수 있는 간단하고 큰 노력 없이 메시징 처리를 할 수 있는 것이다. 이것은 어떤 다른 종속성 없이 단지 참조하는 LIB이어야 됩니다. 추가 변동되는 부분이 없기 때문에 추가적인 위험도 없고, 이것은 어떤 프로그램 언어에서 동작하고 모든 OS에서 실행되어야 합니다.

바로 이것이 ØMQ입니다 : 많은 비용 없이, 네트워크 연결에 유연성이 필요한 어플리케이션의 대부분 문제를 풀 수 있는 효율적인 임베디드 라이브러리입니다.

특징 :

  • 이것은 백그라운드에서 비동기 I/O를 처리 합니다. 이것은 lock-free 데이터 구조를 사용하는 어플리케이션 스레드로 통신합니다. 그래서 ØMQ 응용 프로그램은 락(lock), 세마포(semaphores), 다른 대기상태가 필요하지 않습니다.
  • 컴포넌트들은 동적으로 오고,갈수도 있으며, ØMQ는 자동으로 재 연결이 됩니다. 이것은 어떤 순서로 구성 요소를 시작할 수있다는 것을 의미합니다. 이 서비스는 언제든지 네트워크에 참여하고 떠날 수있는 "서비스 지향 아키텍처를"(SOAs)를 만들 수 있습니다.
  • 이것은 필요 시 자동으로 메시지를 대기 시킵니다. 메시지가 대기하기 전에 지능적으로 수신자에 가능한 가깝게 메시지를 밀어 넣는 작업을 수행합니다.
  • 이것은 over-full 큐를 처리하는 방법을 제공합니다.(‘high water mark라고 함). 큐가 가득 차면, ØMQ는 당신이 하려고 하는 메시징의 성격에 따라(패턴[pattern]이라 함) 자동적으로 발신자를 차단하거나, 메시지를 버립니다.
  • 이것은 어플리케이션이 임의의 전송 레이어와 서로 대화하도록 허락합니다 : TCP, multicast, in-process, inter-process. 당신은 다른 전송 레이어를 사용하도록 코드를 변경할 필요가 없습니다.
  • 이것은 메시징 패턴에 따라 서로 다른 전략을 사용하여 안전하게 수신을 늦게 혹은 차단하도록 처리 합니다.
  • 이것은 request-reply, publish-subscribe와 같은 다양한 패턴을 사용하여 메시지 라우팅을 제공합니다. 이러한 패턴은 어떻게 당신이 토폴로지, 네트워크의 구조를 구성하느냐에 달려 있습니다.
  • 이것은 네트워크에서 많은 부분이 상호 연결하는 복잡도를 줄이기 위해 확장패턴을 위한 “devices”(small brokers)를 둘 수 있습니다.
  • 이것은 간단한 프레임을 사용하여 정확하게 전체 메시지를 전달합니다. 10k 메시지를 보내며 10k 메시지를 받을 것입니다.
  • 이것은 특정 메시지 형식이 없습니다. 이것은 0에서 gigabytes의 큰 blob입니다. 데이터를 표현하고 싶을 때 구글의 protocol buffer, XDR, 기타 다른 것과 같은 제품을 선택하면 됩니다.
  • 이것은 지능적으로 네트워크 오류를 처리합니다. 때로 재시도하고, 때로 실패로 처리 합니다.
  • 이것은 carbon footprint를 줄여 줍니다. 적은 CPU로 많은 일을 수행하면 적은 전력을 사용한다는 의미입니다. 그리고 더 오랫동안 노후 시스템을 유지할 수 있습니다. Al Gore는 ØMQ을 사랑합니다.

사실 ØMQ의 장점은 더 많이 있습니다. 이것은 당신이 네크워크 프로그램을 개발하는데 혁신적인 효과를 제공합니다. 표면적으로 이것은 단지 당신이 zmq_recv (3)zmq_send (3) API를 사용하는 것이지만, 메시지 처리는 빠르게 처리되어, 당신의 어플리케이션은 곧 모든 메시지를 처리하고 종료 될 것입니다. 이것은 우아하고 자연스러운 것입니다. 그리고 이들 각각의 작업은 node로 연결합니다. 이 node는 임의의 전송 레이어를 통해 서로 연결 됩니다.(코드 변경없이 node는 한 프로세스에 있는 두 스레드 일 수 있으며, 한 시스템에 있는 두 프로세스 일 수 있으며, 네트워크상에 있는 두 시스템 일 수 있습니다.)

Socket Scalability

top prev next

다음은 ØMQ의 확장성에 대한 얘기입니다. 여기 날씨 서버가 시작된 후, 병렬로 클라이언트가 분기되는 쉘 스크립트가 있습니다. :

wuserver &
wuclient 12345 &
wuclient 23456 &
wuclient 34567 &
wuclient 45678 &
wuclient 56789 &

클라이언트가 실행 된 후 ‘top’을 사용해서 실행중인 프로세스를 보세요. :

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 7136 ph        20   0 1040m 959m 1156 R  157 12.0  16:25.47 wuserver
 7966 ph        20   0 98608 1804 1372 S   33  0.0   0:03.94 wuclient
 7963 ph        20   0 33116 1748 1372 S   14  0.0   0:00.76 wuclient
 7965 ph        20   0 33116 1784 1372 S    6  0.0   0:00.47 wuclient
 7964 ph        20   0 33116 1788 1372 S    5  0.0   0:00.25 wuclient
 7967 ph        20   0 33072 1740 1372 S    5  0.0   0:00.35 wuclient

여기 무슨 일이 일어나고 있는지에 대해 잠시 생각해 봅시다. 날씨 서버는 하나의 소켓을 가지고 있으며, 병렬로 5개의 클라이언트에게 데이터를 보내려고 하고 있습니다. 우리는 수천의 동시 클라이언트가 있을 수 있습니다. 서버 어플리케이션은 클라이언트를 보거나 직접 얘기 할 수 없습니다.

Missing Message Problem Solver

top prev next

당신이 ØMQ로 프로그램을 시작할 때 한번 이상 이 한 문제에 직면할 것입니다. : 당신이 받을 것으로 기대한 메시지를 잃게 되는 것입니다. 이것은 가장 일반적인 원인을 통해 해결하는 기초적인 문제해결 방법입니다.. 전문용어 중 일부가 아직 익숙하지 않더라도 걱정하지 마세요, 그것은 다음 장에서 명확하게 알게 될 것입니다.

fig9.png

만약 오류 비용이 큰 환경에서 ØMQ를 사용하는 경우, 당신은 적당한 테스트 계획을 세우길 원할 것입니다. 첫째, 당신이 디자인한 서로 다른 측면을 테스트할 Prototype을 구축하고, 당신이 설계한 것이 얼마나 견고하고 정확한지 검증을 위해 죽을 때까지 부하를 줍니다. 둘째, 테스트에 투자합니다. 이것은 충분한 컴퓨터 자원으로 운영환경과 같은 테스트 환경을 만드는 것을 의미 하며, 시간을 점점 늘리거나 심각할 정도로 실제적인 테스트가 되도록 도움을 줍니다. 이상적으로 한 팀은 코드를 만들고, 두번째 팀은 에러가 나도록 노력합니다. 마지막으로 정말 제대로 작동할 수 있도록 도울 수 있는 방법을 논의하기 위해 iMatrix에 연락하는 것이며, 당신은 신속하게 문제를 해결 할 수 있을 것입니다.

즉, 실 환경에서 동작하는지 테스트하고 증명되지 않았다면, 최악의 가능한 순간에 문제가 발생 할 것입니다.

Warning - Unstable Paradigms!

top prev next

전통적인 네트워크 프로그래밍은 한 소켓이 한 connection으로 한 대상과 통신한다고 가정합니다. 좀 다르지만 멀티캐스트 프로토콜 이란 것도 있기는 합니다. 우리는 "한 소켓 = 하나의 연결"이라 가정했을 때 다른 방식으로 우리의 아키텍처를 확장할 수 있습니다. 여러 스레드가 한 소켓으로 처리하도록 하는 로직을 만들 수 있습니다. 우리는 이들의 스레드에 정보와 상태를 설정합니다.

ØMQ에서 소켓은 자동으로 연결 전체 집합을 관리하는 영리한 멀티스레드 어플리케이션입니다. 당신은 이들 연결이 작동되고, 열고, 닫고, 추가적인 상태를 알수 없습니다. 당신이 send/receive/poll을 차단하고 당신이 제어 할 수 있는 것이 소켓일지라도, 당신이 관리하는 것은 connection이 아닙니다. Connection은 개인적이고 보이지 않는 것이며, ØMQ 확장성의 핵심입니다.

당신은 코드 변경 없이 네트워크 프로토콜이 무엇이든지 간에 connection수를 제어할 수 있습니다. ØMQ의 메시지 패턴은 당신의 어플리케이션 메시지 패턴보다 더 싸게 스케일 할 수 있습니다.

그래서 일반적인 가정으로는 더 이상 적용되지 않습니다. 예제코드를 참고로 당신의 뇌는 알고 있는 것을 구상하여 그리려고 합니다. 당신은 “socket”을 “ah, that represents a connection to another node”로 생각합니다. 이것은 잘못된 것입니다. 당신이 “thread”를 봤을 때 당신의 뇌는 "ah, a thread represents a connection to another node"라고 다시 생각합니다. 이것 또한 잘못된 것입니다.

당신이 처음 이 가이드를 읽는다면, 특히 간단한 ØMQ 어플리케이션을 구상하고 하루 이틀(그리고 아마 3,4일)에 ØMQ코드를 작성 할 때까지는 혼란을 느낄 수도 있을 것이라는 것을 깨닫게 될 것입니다. 그리고, 당신이 ØMQ에 일반적인 가정을 둔다면 그것은 작동하지 않을 것입니다. 이 모든 것이 확실하게 될 때 zap-pow-kaboom satori paradigm-shift와 같이 당신은 순간 깨달음과 신뢰를 경험하게 될 것입니다.

Chapter Two - Intermediate Stuff

top prev next

1장에서는 ØMQ의 주요 패턴(request-reply, publish-subscribe, and pipeline)에 대한 기본 예제와 소개를 했습니다. 이번 장에서는 손을 좀 놀려 볼 것이며 실제 프로그램에서 이들 도구를 사용하는 방법을 배우기 시작할 것입니다.

본 장에서 다룰 내용 :

  • ØMQ를 생성하고 작동하는 방법.
  • 소켓에 메시지를 송/수신하는 방법.
  • ØMQ 비동기 I/O모듈을 만드는 방법.
  • 한 thread에서 멀티 소켓을 조작하는 방법.
  • 치명적이거나 비 치명적인 에러를 적절하게 처리하는 방법.
  • ‘Ctrl-C’와 같은 interrupt signals를 처리하는 방법
  • MQ 어플리케이션을 정상적으로 종료하는 방법
  • MQ 어플리케이션의 메모리 누수를 확인하는 방법
  • 다중(multipart) 메시지를 송/수신하는 방법
  • 네트워크를 통해 메시지를 전달하는 방법
  • 간단한 메시지 대기열 브로거(broker)를 만드는(build) 방법
  • ØMQ와 함께 멀티 스레드 응용 프로그램을 작성하는 방법.
  • 스레드 사이에 신호처리를 위해 ØMQ를 사용하는 방법.
  • 네트워크 노드를 조율하기 위해 ØMQ를 사용하는 방법.
  • 소켓 ID를 사용하여 내구성(durable) 있는 소켓을 만드는 방법.
  • Pub-Sub 메시지를 생성하고 사용하는 방법
  • 충돌에서 복구할 수 있는 내구성 있는 subscribers를 만드는 방법.
  • 메모리 overflow를 방지하기 위해 high-water mark(HWM)를 사용하는 것

The Zen of Zero

top prev next

ØMQ의 Ø는 모든 tradeoffs에 관한 것입니다. 한편으로 이 이상한 이름은 구글과 트위터에 ØMQ의 게재 빈도를 감소하고 다른 한편으로는, 일부 덴마크 민족이나 사용할 만한 "ØMG røtfl"같은 사용을 귀찮아 합니다. Ø는 쉽게 찾을 수 있는 zero가 아니며, "Rødgrød med Fløde!" 이 “당신 이웃이 Grendel의 직접적인 후예 일 수 있다”라는 의미를 뜻한다는 것은 분명한 모욕입니다. 공정 거래 처럼 보입니다

원래 ØMQ의 zero는 “zero broker”와 “zero latency” 를 의미 하였습니다. 한편으로는, 다른 목표를 충당해 왔습니다.: zero administration, zero cost, zero waste. 더 일반적으로 “zero”는 프로젝트에서 퍼진 최소주의 문화를 말합니다. 우리는 새로운 기능을 발표하기 보다는 복잡성을 제거하는데 힘을 집중합니다.

The Socket API

top prev next

솔직히 말해서, ØMQ는 미끼상품(switch-and-bait)과 같은 종류입니다. 이것은 여러분을 위한 것이고, 여러분 보다는 우리에게 더 피해가 가는 것이기에 사과하진 않겠습니다. 익숙한 BSD 소켓 API가 있지만, 이것은 분산 소프트웨어를 설계하고 작성하는 방법에 대한 여러분의 안목을 형성하는데 느리게 할 것이며 메시지 처리 기계 부분을 숨깁니다.

소켓은 사실상 네트워크 프로그래밍을 위한 표준 API입니다. 특히 개발자의 입맛에 맞게 만든 ØMQ는 표준 소켓 API를 사용합니다. 이것은 “메시지 지향 미들웨어(Message Oriented Middleware)”가 피자(pizza)에 대한 이상한 갈망에서 멀어지게 하는 “Extra Spicy Sockets”으로 변화하고 더 알기를 갈망하도록 했습니다.

좋은 페퍼로니 피자 처럼 ØMQ소켓은 소화하기 쉽습니다. 소켓은 BSD소켓처럼 네 부분으로 구분 됩니다. :

  • 메시지를 쓰고 읽는 것으로 데이터를 운반하기 위해 소켓을 사용(zmq_send(3), zmq_recv(3)).

C코드로는 아래와 같습니다. :

void *mousetrap;

// Create socket for catching mice
mousetrap = zmq_socket (context, ZMQ_PULL);

// Configure the socket
int64_t jawsize = 10000;
zmq_setsockopt (mousetrap, ZMQ_HWM, &jawsize, sizeof jawsize);

// Plug socket into mouse hole
zmq_connect (mousetrap, "tcp://192.168.55.221:5001");

// Wait for juicy mouse to arrive
zmq_msg_t mouse;
zmq_msg_init (&mouse);
zmq_recv (mousetrap, &mouse, 0);
// Destroy the mouse
zmq_msg_close (&mouse);

// Destroy the socket
zmq_close (mousetrap);

소켓은 항상 void pointers이고 구조화된 메시지입니다. 그래서 C에서 당신은 그렇게 소켓을 전달하지만, zmq_send(3) , zmq_recv(3) 와 같이 메시지를 처리하는 모든 함수에서는 메시지의 주소를 전달합니다. 알고 있는 대로 “ØMQ에서 모든 소켓은 우리에게 속해 있다”로 인식하지만, 메시지는 코드에서 실제로 당신이 소유합니다.

소켓을 생성, 소멸, 설정하는 것은 어떤 개체를 위해 필요한 작업입니다. 그러나 ØMQ는 비동기, 탄성 섬유라는 점을 기억해야 합니다. 이것은 우리가 네트워크 토폴로지에 소켓을 어떻게 끼워 넣고, 그 후에 어떻게 소켓을 사용하는지에 따라 좀 다릅니다.

Plugging Sockets Into the Topology

top prev next

두 노드간의 연결을 위해서는 한 노드에서 zmq_bind(3)를 사용하고 다른 노드에는 zmq_connect(3)을 사용합니다. 일반적으로, zmq_bind(3)을 수행하는 노드는 고정 네크워크 주소를 가지고 있는 서버이고 zmq_connect(3) 을 수행하는 노드는 잘 알려지지 않은 혹은 임시 네트워크 주소를 가지는 클라이언트 입니다. 그래서 우리는 "bind a socket to an endpoint"와 "connect a socket to an endpoint"이라 말합니다. Endpoint는 잘 알려진 네트워크 주소입니다.

ØMQ연결은 전통적인 TCP연결과 다소 다릅니다. 주요 주목할만한 차이점은 다음과 같습니다. :

  • 이것은 서버가 이미 endpoint에 zmq_bind(3)를 했든 안 했든 클라이언트가 endpoint에 zmq_connect(3) 했을 때 존재 합니다.
  • 이것은 비동기 이며, 필요할 시점과 장소에 마술과 같이 존재하는 대기열(queues)을 가지고 있습니다.
  • 이것은 각각의 끝점에 사용되는 소켓의 종류에 따라, 특정 "Messaging Pattern"을 표현 할 수 있습니다.
  • 하나의 소켓이 많은 incoming/outgoing 연결을 가질 수 있습니다.
  • zmq_accept() 메소드는 없습니다. 소켓이 endpoint에 바인딩하는 경우 자동으로 연결을 받아 들입니다.
  • 당신의 어플리케이션 코드는 이것을 직접 연결하면 작동하지 않을 수 있으며, 이들은 소켓으로 캡슐화되어 있습니다.

많은 아키텍쳐들은 client-server 모델의 몇가지 유형을 따릅니다 (서버는 가장 안정적인 컴포넌트이고, 클라이언트는 가장 다이나믹한 컴포넌트 입니다.). 종종 주소에 이슈가 있습니다:서버는 클라이언트를 볼 수 있지만 클라이언트는 그렇지 않습니다. 그래서 대개 서버는 zmq_bind(3)해야 되고 클라이언트는 zmq_connect(3) 를 해야 합니다. 이것은 또한 비정상적인 네트워크 아키텍쳐에 대한 몇 가지 예외로, 당신이 사용중인 소켓의 종류에 따라 달라집니다. 우리는 나중에 소켓 유형에 대해서 알아 볼 것입니다.

지금, 서버를 시작하기 전에 클라이언트를 시작하는 것을 생각해 봅시다. 전통적인 네트워크에서 우리는 큰 에러가 발생합니다. 그러나, ØMQ는 시작하고 임의로 중지하는 것을 허용합니다. 클라이언트가 zmq_connect(3)을 하자마자 연결은 존재하며 노드는 소켓에서 메시지를 쓰기위해 시작할 수 있습니다. 몇가지 단계를 거쳐, 서버는 살아나고, zmq_bind(3)하고 ØMQ는 메시지를 전달하기 시작합니다.

서버 노드는 많은 endpoint에 바인딩할 수 있으며 하나의 소켓을 사용하여 이 작업을 수행할 수 있습니다. 이것은 다른 전송매체를 통해 연결을 수락한다는 의미입니다. :

zmq_bind (socket, "tcp://*:5555");
zmq_bind (socket, "tcp://*:9999");
zmq_bind (socket, "ipc://myserver.ipc");

당신은 두번씩 같은 endpoint에 바인딩 할 수 없습니다. 이것은 에러의 원인이 됩니다.

매번 클라이언트 노드는 endpoints중 하나에 zmq_connect(3)을 하며 서버노드의 소켓의 다른 연결을 얻습니다. 소켓의 연결을 하는 수는 제한이 없습니다. 클라이언트 노드는 단일 소켓을 사용하여 많은 endpoint에 연결할 수 있습니다.

대부분의 경우, 어떤 노드가 클라이언트인지 혹은 서버인지는 메시지 흐름 보다는 네크워크 토폴로지에 따르게 됩니다. 그러나, 동일한 소켓 유형은 서버든 클라이언트든 다르게 처리되는 경우(연결이 끊긴 후 재전송할때)가 있다.

이것이 의미하는 것은 거의 고정된 endpoint 주소를 가진 토폴로지의 안정적인 부분으로 항상 “server”관점에서, 그리고 왔다 갔다하는 유동적인 부분은 ‘clients’ 관점에서 생각해야 된다는 것입니다. 그 다음 어플리케이션은 이 모델을 통해 디자인 합니다. 이처럼 해야 될 것들은 더 많이 있습니다.

소켓은 유형이 있습니다. 소켓유형은 소켓의 의미를 정의합니다. 이것은 안쪽과 바깥쪽 라우팅 메시지, 큐 등에 대한 정책입니다. 당신은 소켓의 어떤 유형(예를 들어 publisher 소켓과 subscriber 소켓)과도 연결할 수 있습니다. 소켓은 메시지 패턴과 함께 동작합니다. 더 자세한 것은 나중에 살펴 보겠습니다.

ØMQ은 기본으로 제공하는 메시지 큐잉 시스템을 이용하여 다른 방법으로 소켓을 연결할 수 있습니다. 우리가 나중에 얘기할 디바이스와 주제 라우팅와 같은 상위 단계가 있습니다. 그러나 본질적으로, ØMQ는 아이의 건축 장난감처럼 함께 조각들을 연결 하며 네트워크 아키텍처를 정의합니다.

Using Sockets to Carry Data

top prev next

당신이 zmq_send(3)zmq_recv(3) 메소드를 사용하여 메시지를 보내고 받을 수 있습니다. 메소드 이름은 일반적이지만, ØMQ의 I/O 모델은 당신이 이해하기에 시간이 필요하며, TCP모델과는 다릅니다.

fig10.png

데이터를 처리하는 경우 TCP 소켓과 ØMQ 소켓 사이의 주요 차이점에 대해 살펴보겠습니다. :

  • ØMQ 소켓은 바이트 (TCP에서와 같이) 또는 프레임(UDP와 같이)이라기 보다는 메시지를 처리합니다. 메시지는 이진 데이터로 길이가 지정된 BLOB입니다. 우리는 곧 설계가 성능에 최적화되어 있고 그래서 다소 이해하기 어려운 메시지를 볼 것입니다.
  • ØMQ 소켓은 백그라운드 스레드에서 I/O를 처리합니다. 이것은 메시지가 어플리케이션이 바쁘든 상관없이 로컬 입력 대기열에 도착하고, 로컬 출력 대기열로부터 전송한다는 것을 의미 합니다. 이러한 방법에 의해, 메모리 대기열을 구성할 수 있습니다.
  • ØMQ 소켓은 소켓의 종류에 따라 다르지만, 많은 다른 소켓을 연결하고 연결 될 수 있습니다. TCP는 일대일 전화통화를 에뮬레이트 하지만, ØMQ는 1:N (라디오 방송 등),N:N (우체국 등), N:1 (메일 박스 등), 그리고 1:1을 구현할 수 있습니다..
  • ØMQ 소켓은 여러 endpoint (fan-out 모델)로 보내거나, 여러 끝점 (fan-in모델)에서 받을 수 있습니다.
fig11.png

따라서 소켓에 메시지를 작성하는 것은 하나 또는 한 번에 여러 다른 곳으로 메시지를 보낼 수 있으며, 반대로 하나의 소켓은 메시지를 보내는 모든 연결에서 메시지를 수집합니다. 각 보낸 사람도 기회를 얻을 수 있도록 zmq_recv(3) 메소드는 공정한 큐잉 알고리즘을 사용합니다.

zmq_send(3)메소드는 실제로 소켓 연결로 메시지를 전송하지 않습니다. 그것은 I/O 스레드가 비동기적으로 그것을 보낼 수 있도록 메시지를 큐잉 합니다. 그것은 몇 가지 예외의 경우를 제외하고 차단하지 않습니다. 그래서 메시지는 어플리케이션이 zmq_send(3)의 리턴을 받아도 반드시 전송되지는 않습니다. 만약 당신이 zmq_msg_init_data(3)을 사용하여 메시지를 생성했다면 데이터를 재사용하거나 free할 수 없습니다. 그렇지 않다면 I/O스레드는 빠르게 그자체를 덮어 쓰거나 할당되지 않은 garbage가 될 것입니다. 이것은 초보자에게는 일반적인 실수입니다. 우리는 적당하게 메시지 처리하는 방법을 나중에 볼 것입니다.

Unicast Transports

top prev next

ØMQ는 유니캐스트(unicast) 전송(inproc, IPC, 그리고 TCP)과 멀티캐스트(multicast) 전송(epgm, PGM)을 제공합니다. 멀티캐스트는 나중에 다루게 될 고급 기술입니다. fan-out 비율이 1-to-N 유니캐스트를 불가능하게 한다는 것을 알지 못한다면 이것을 사용하지 마십시오

대부분의 경우 tcp를 사용합니다. 이것은 유연하고 간편하고 대개 충분히 빠릅니다. ØMQ의 TCP 전송은 연결하기 전에 endpoint가 존재하는지 요구하지 않기 때문에 ‘disconnected’라고 부릅니다. 클라이언트와 서버는 연결할 수 있고 언제든지 바인딩할 수 있으며, 갔다 되돌아 올 수 있습니다. 그리고 이것은 어플리케이션에 투명하게 남아 있습니다.

Inter-process전송, IPC는 LAN에서 추상화되어 있다는 것을 제외하고는 TCP와 같습니다. 이것은 IP주소나 도메인 명이 필요하지 않다는 것입니다. 이것은 몇몇 목적을 위해서 효과적이며 이 책에서 매우 자주 사용합니다. ØMQ의 IPCTCP처럼 연결이 끊어져 있습니다. 이것은 한가지 제한이 있습니다:Window 환경에서는 작동하지 않습니다. 이것은 ØMQ의 향후 버전에서는 가능하게 될지 모릅니다. 관습적으로 우리는 다른 파일명과 잠재적인 충돌을 피하기 위해 ‘.ipc’ 확장자로 endpoint이름을 사용합니다. UNIX에서 당신이 ipc endpoint을 사용한다면 다른 사용자로 실행중인 프로세스가 공유하지 못하도록 이것에 적당한 권한이 필요할 것입니다. 당신은 또한 모든 프로세스가 이 파일을 참조할 수 있도록 해야 합니다.

Inter-thread 전송, inproc는 연결된 다음에 신호 전송을 합니다. 이것은 tcpipc보다 휠씬 빠릅니다. 이것은 ipctcp에 비해 특별한 제한이 있습니다.:당신이 연결하기 전에 바인딩 해야 합니다. 이것은 ØMQ의 향후 버전에서 수정 될 수 있지만, 현재는 inproc소켓을 사용하여 정의합니다. 우리는 한 소켓을 만들고 바인딩하고, 다른 소켓을 생성하고 연결한 자식 스레드를 시작합니다.

ØMQ is Not a Neutral Carrier

top prev next

초심자가 ØMQ에 대해 일반적인 질문일 수 있지만, (이것은 나 자신에게 한 질문의 하나입니다.) "어떻게 ØMQ를 사용하여 XYZ 서버에 메시지를 쓸 수 있습니까?" 예를 들어, "내가 어떻게 ØMQ에서 HTTP 서버와 통신할 수 있습니까?"

이 의미는 만약 우리가 HTTP 요청과 응답을 처리하기 위해 일반적인 소켓을 사용한다면, ØMQ 는 좀더 빠르고 더 잘 할 수 있어야 한다는 것입니다.

아쉽게도 대답은 “이것은 작동되지 않습니다” 입니다. ØMQ는 중간 연결매체가 아니라, 이것을 사용하는 전송 프로토콜에서의 프레임입니다. 이 프레임은 그들 자신 프레임을 사용하는 기존 프로토콜과 호환되지 않습니다. 예를 들어, 여기 TCP/IP 구간에 HTTP요청과 ØMQ요청이 있습니다. :

fig12.png

HTTP 요청은 간단한 프레임 구분자로 CR - LF 사용하고, ØMQ는 길이가 지정된 프레임을 사용합니다. :

fig13.png

그래서 당신은 ØMQ를 사용해서(예를 들면 request-reply소켓 패턴) HTTP와 같은 프로토콜 처럼 사용 할 수 있습니다. 하지만 HTTP 일 수는 없습니다.

질문에 좋은 답변은 “나의 새로운 XYZ서버를 만들 때 어떻게 ØMQ를 사용하여 효과적으로 만들 수 있습니까?” 입니다. 당신은 어떤 경우에서든 연결을 원하는 어떤 프로토콜로든 구현하기를 원할 것입니다. 하지만 당신은 실제 작업을 수행하는 ØMQ 백앤드에 그 프로토콜 서버를 연결할 수 있습니다. 여기서 아름다운 부분은 당신이 원하는 대로 로컬 혹은 원격으로 실행하는 모든 언어 코드를 사용하여 백앤드로 확장할 수 있다는 것입니다. Mongrel2 웹서버가 그러한 구조의 좋은 예입니다

I/O Threads

top prev next

우리는 ØMQ가 백앤드 스레드에서 I/O를 수행한다고 알고 있습니다. 하나의 I / O 스레드(모든 소켓을 위한)는 모든 극단적인 어플리케이션을 제외한 모든 어플리케이션에서 충분합니다. 이것은 context를 만들 때 사용하는 마법’1’이며, “한 I/O스레드를 사용하라”라는 의미 입니다. :

void *context = zmq_init (1);

ØMQ 응용 프로그램과 연결 당 한 소켓을 생성하지 않는 전통적인 네트워크 응용 프로그램 사이의 주요 차이점이 있습니다. 한 소켓이 작업의 특정 지점을 위해 모든 송/수신 연결을 처리합니다. 예를 들어, 당신은 수천 subscriber에 publish할 때, 하나의 소켓을 통해서 합니다. 당신은 20개 서비스 간의 분산 작업을 할 때, 하나의 소켓을 통해서 합니다. 당신은 수천 웹 응용 프로그램에서 데이터를 수집할 때 , 하나의 소켓을 통해서 합니다.

이것은 응용 프로그램을 작성하는 방법에 근본적인 영향을 미치고 있습니다. 전통적인 네트워크 응용 프로그램이 원격 연결 당 한 프로세스 또는 한 스레드를 가지고 있으며, 그 프로세스 또는 스레드가 한 소켓을 핸들링 합니다. ØMQ은 단일 스레드로 이 전체 구조를 깨거나(collapse), 확장의 필요에 의해 그것을 깰(break up) 수 있습니다

Core Messaging Patterns

top prev next

ØMQ 소켓 API의 갈색 표지 안에 메시징 패턴의 세계가 자리잡고 있습니다. 당신은 엔터프라이즈 메시징에 대한 배경 지식이 있다면, 약간 친숙할 것입니다. 대부분 ØMQ 초보자들은 놀라겠지만, 우리는 이렇게 소켓이 다른 노드를 나타내는 TCP 패러다임으로 사용하고 있습니다.

이제 간단히 ØMQ가 당신을 위해 무엇을 하는지 정리해 보겠습니다. 그것은 신속하고 효율적으로 노드에 데이터 (메시지)를 제공합니다. 노드는 스레드, 프로세스, 또는 시스템에 매핑할 수 있습니다. 이것은 당신의 어플리케이션에 사용할 수있는 단일 소켓 API를 제공합니다. 이것은 실제적인 전송형식((like in-process, inter-process, TCP, or multicast)이 무엇이든지 상관없습니다. 이것은 오고 가고 함으로써 대상에 자동으로 재접속 합니다. 필요에 따라 송/수신 양쪽에 메시지를 큐잉할 수 있습니다. 그 때 해당 디스크에 넘치지 않고, 프로세스의 메모리가 부족하지 않도록 신중하게 대기열을 관리합니다. 그것은 소켓 오류로 처리합니다. 그것은 백그라운드 스레드의 I/O 모두에 해당됩니다. 그것은 노드 사이의 통신에 대해 잠금이 없는 기술(lock-free)을 사용하므로 잠금 장치(locks), 대기(waits), 세마포(semaphores), 또는 교착상태(deadlocks)가 절대로 필요 없습니다.

그러나 ‘패턴’이라 불리는 정확한 방법에 따라 메시지를 라우트 하고 큐잉합니다. 이것은 ØMQ가 제공하는 패턴입니다. 이것은 데이터와 분산 작업을 하기 위한 최고의 경험이 녹아 있습니다. ØMQ의 패턴이 고정 되어 있지만, 향후 버전에는 사용자 정의 패턴을 제공할 수도 있습니다.

ØMQ 패턴은 타입이 일치하는 소켓 쌍에 의해 구현됩니다. 즉, 당신은 소켓 유형을 이해하고 그것이 함께 동작하는 방법을 이해해야 ØMQ 패턴을 이해하는 것입니다. 대부분이 배우는데 조금 시간이 걸립니다.

기본으로 제공하는 핵심적인 ØMQ 패턴은 다음과 같습니다. :

  • Request-reply : 클라이언트와 서비스의 집합을 연결하는 패턴. 이것은 원격 프로시저 호출 및 작업 분산 패턴입니다.
  • Publish-subscribe : publisher와 subscriber 집합을 연결하는 패턴, 이것은 데이터 분산 패턴입니다.
  • Pipeline : 여러 단계와 루프를 가질 수 있는 fan-out /fan-in패턴으로 된 노드를 연결합니다. 이것은 병렬 작업 분산 및 수집 패턴입니다.

우리는 1장에서 이들 각각을 살펴 봤습니다. 사람들은 아직도 전통적인 TCP 소켓의 관점에서 ØMQ를 생각하여 사용 하려고 하는 경향이 있는 한 패턴이 더 있습니다. :

  • Exclusive pair : 독점 쌍의 두 소켓을 연결하는 패턴. 이것은 특정 고급 사용 - 경우에 낮은 수준의 패턴입니다. 우리는 이 장의 마지막에서 예를 다룰 것입니다..

zmq_socket(3) 설명(man) 페이지는 이 패턴에 대해 명백하게 설명합니다. 이것은 이해 할 때까지 여러 번 읽을 가치가 있습니다. 우리는 그것에 담겨 있는 각각의 패턴과 사례을 볼 것입니다.

연결 결합 쌍 (양쪽이 바인딩 할 수 있음)으로 유효한 소켓조합 입니다.

  • PUB and SUB
  • REQ and REP
  • REQ and ROUTER
  • DEALER and REP
  • DEALER and ROUTER
  • DEALER and DEALER
  • ROUTER and ROUTER
  • PUSH and PULL
  • PAIR and PAIR

기타 다른 조합은 문서화되지 않았으며, 신뢰할 수 없는 결과를 초래 할 것이고, 당신이 이것을 시도할 경우 ØMQ 향후버전에서는 에러를 리턴 할 것입니다. 당신은 코드를 통해 한 소켓에서 읽고 다른 소켓에 쓰는 것과 같이 다른 소켓유형간에 통할 수 있게 할 수 있습니다.

High-level Messaging Patterns

top prev next

이들 4가지 패턴은 ØMQ에 정의되어 있습니다. 이것들은 핵심 C++ 라이브러리로 구현된 ØMQ의 일부이며, 모든 소매점에서 사용할 수 있도록 보장합니다.

상위에, 우리는 높은 수준의 패턴을 추가합니다. 우리는 ØMQ 위에 이러한 높은 수준의 패턴을 구축하고 우리의 응용 프로그램에서 사용하는 어떤 언어로 그들을 구현합니다. 이것은 핵심 라이브러리의 일부가 아니며, ØMQ패키지도 아니며, ØMQ 커뮤니티에 존재합니다.

이 가이드를 통한 목표 중 하나는 당신이 작고(정상적으로 메시지를 처리하는 법), 큰(신뢰할 수 있는 PUB-SUB 구조를 만드는 방법) high-level 패턴을 만드는데 도움이 되는 것입니다.

Working with Messages

top prev next

ØMQ 메시지는 메모리에 맞는 제로보다 큰 특정 크기의 BOLB입니다. 당신은 Google 프로토콜 버퍼, XDR, JSON, 또는 당신이 원하는 것을 사용해서 직렬화 할 것입니다. 이것은 이식성이 좋고 빠르게 데이터를 표현하는 것을 선택하는 것이 현명하지만, trade-offs에 따라 결정할 수도 있습니다.

메모리에서 ØMQ 메시지는 zmq_msg_t 구조 (언어에 따라 다름)로 표현합니다. 여기에서 C로 ØMQ 메시지를 사용하기 위한 기본 규칙은 다음과 같습니다. :

  • zmq_msg_t 개체를 만들고 사용합니다.
  • 메시지를 읽기위해 당신은 zmq_msg_init(3)을 사용하여 메시지를 초기화 한 후 zmq_recv(3).으로 메시지를 수신합니다.
  • 새로운 데이타로 메시지를 작성하려면 메시지를 만들고 필요한 크기의 데이터 블록를 할당하기 위해 zmq_msg_init_size(3) 사용합니다. 그런 다음 memcpy를 사용하여 데이터를 채우고, zmq_send(3)로 메시지를 전송합니다.
  • 메시지를 릴리즈하기 위해 zmq_msg_close(3)를 호출합니다. 이것은 참조를 끊으며, 결국 ØMQ는 메시지를 초기화 합니다.
  • 메시지 내용에 액세스하려면 zmq_msg_data(3). 를 사용합니다. 메시지에 포함된 데이터의 크기를 알려면 zmq_msg_init_size(3)를 사용합니다.
  • 설명 페이지를 읽지 않고, 여러분이 용도를 정확하게 알지 못한다면, zmq_msg_move(3), zmq_msg_copy(3), 또는 zmq_msg_init_size(3)을 사용하지 말기 바랍니다.

다음은 주의를 기울여 알고 있어야 하는 메시지 처리의 전형적인 코드 입니다. 이것은 우리가 모든 예제에서 사용하는 zhelpers.h 파일에 있습니다. :

// Receive 0MQ string from socket and convert into C string
static char *
s_recv (void *socket) {
zmq_msg_t message;
zmq_msg_init (&message);
zmq_recv (socket, &message, 0);
int size = zmq_msg_size (&message);
char *string = malloc (size + 1);
memcpy (string, zmq_msg_data (&message), size);
zmq_msg_close (&message);
string [size] = 0;
return (string);
}

// Convert C string to 0MQ string and send to socket
static int
s_send (void *socket, char *string) {
int rc;
zmq_msg_t message;
zmq_msg_init_size (&message, strlen (string));
memcpy (zmq_msg_data (&message), string, strlen (string));
rc = zmq_send (socket, &message, 0);
assert (!rc);
zmq_msg_close (&message);
return (rc);
}

당신은 쉽게 임의 길이의 메시지를 보내고 받을 때 이 코드를 확장 할 수 있습니다.

zmq_send (3)으로 메시지를 전송할 때 ØMQ는 메시지를 clear(즉 사이즈를 zero로 설정)한다는 것에 주의하세요. 그래서 두번 같은 메시지를 보낼 수 없고, 메시지를 보낸 후 메시지 데이터를 액세스 할 수도 없습니다.

만약 한번 이상 같은 메시지를 보내려면, 두번째 메시지를 생성하고 zmq_msg_init(3)를 사용하여 초기화 하고 첫번째 메시지를 복사하기 위해 zmq_msg_copy(3) 을 사용하십시오. 이것은 데이터를 복사하지 않고 reference합니다. 그 다음 당신은 두번 메시지를 보낼 수 있습니다.(만약 더 많이 복사하면 더 할 수 있습니다.) 그리고 메시지는 마지막 복사본이 보내지고 종료될 때 결국은 초기화 됩니다.

또한, ØMQ는 단일메시지를 여러 개 모은 리스트로 조작하는 다중(multipart) 메시지를 지원합니다. 이것은 널리 실제 어플리케이션에서 사용되며, 우리는 본 장 후반부와 3장에서 보겠습니다.

일반적인 알고 있는 메시지와 차이점 :

  • ØMQ는 자동으로 메시지를 보내고 받습니다. 즉 전체 메시지를 가져오거나, 아니면 전혀 가져오지 않습니다.
  • ØMQ 어떤 불특정한 시간 이후에 바로 메시지를 보낼 수 있지만 그렇게 하지 않습니다.
  • 당신이 제로 길이 메시지를 보낼 수 있습니다, 예를 들면 한 스레드 에서 다른 쪽으로 신호를 보낼 때 사용합니다.
  • 메시지는 메모리용량 내에서 사용해야 합니다. 당신은 임의 크기의 파일을 보내려면, 당신은 조각으로 그것을 분할하고, 별도의 메시지로 각 조각들을 보내야 합니다.
  • 당신은 프로그램이 종료할 때 자동으로 객체를 파괴하지(destroy) 않는 언어일 경우 메시지처리를 완료 했을 때 zmq_msg_close(3) 를 호출해야 합니다.

그리고 반드시 반복적으로 zmq_msg_init_data (3)을 사용하지 마십시오. 이것은 초기화 하는 메소드이며 문제가 될 수 있습니다. 마이크로초 절약을 걱정하기 전에 ØMQ에 대해서 배울 중요한 것들이 많이 있습니다.

Handling Multiple Sockets

top prev next

지금까지 예제들 중에서 대부분 예제의 메인 루프에서 처리하는 것은 아래와 같습니다. :

  1. 소켓에서 메시지 대기
  2. 메시지 처리
  3. 반복

우리가 동시에 여러 소켓에서 읽고 싶다면? 가장 간단한 방법은 여러 endpoint를 한 소켓에 연결하고 ØMQ fan-in을 하는 것입니다. 원격 endpoint가 같은 패턴에 있다면 이것은 정상적이지만, PULL소켓을 PUB endpoint에 연결하는 것은 비정상적입니다. 당신이 혼합 패턴을 시작하면 확장성이 깨집니다.

올바른 방법은 zmq_poll(3) 사용하는 것입니다. 더 좋은 방법은 좋은 이벤트 중심의 reactor에 그것을 전환하는 프레임워크로 zmq_poll(3)을 감쌀 수도 있지만, 우리가 여기서 다루려고 하려는 것 보다 훨씬 더 많은 노력이 필요합니다.

이것은 non-blocking 읽기를 사용하는 것으로 두 소켓에서 읽기를 하는 간단한 예제입니다. 이것은 날씨를 변경하는 subscriber로써 2가지를 일을 하는 혼재된 프로그램 입니다. worker는 병렬처리 합니다. :


C++ | C# | Clojure | CL | Erlang | F# | Java | Lua | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Haxe | Node.js | ooc

이 방법의 문제는 첫 번째 메시지 (처리를 위해 메시지를 기다리지 않을 때, 루프의 끝에 sleep)에 대한 몇 가지 추가 지연이 있습니다. 이것은 하위 밀리초 지연이 필수적 입니다 어플리케이션에서 문제가 될 것입니다. 또한, nanosleep()나 루프를 돌리지 않고 할 수 있는 함수를 위해 매뉴얼을 확인할 필요가 있습니다.

당신은 오히려 우리가 이 예제에서 했던 것처럼 소켓들을 우선순위보다는 오히려 하나에서 첫번째, 두번째 읽는 것처럼 공정하게 소켓을 다룰 수 있습니다. 한 소켓이 많은 소스로부터 메시지를 받을 때 ØMQ가 자동적으로 처리하는 것을 “fair-queuing”라고 부릅니다.

zmq_poll(3)을 사용하는 잘 동작하는 작은 어플리케이션을 봅시다.


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haxe | ooc

Handling Errors and ETERM

top prev next

ØMQ의 오류처리 철학은 fail-fast와 탄력성(resilience)을 혼합했습니다. 우리가 생각하는 프로세스는 내부오류에 가능한 약하고 외부 공격이나 에러에는 가능한 한 강해야 합니다. 비유하면, 내부오류를 감지하면 살아 있는 세포는 자동 소멸됩니다. 그렇지만, 모든 가능한 방법에 의한 외부로부터의 공격은 저항합니다. ØMQ의 주장은 강력한 코드가 뒷받침되어야 한다는 것입니다. 그것은 단지 세포 벽 오른쪽에 있어야 하며, 그러한 벽이 있어야 합니다. 만약 내부 혹은 외부 결함인지 분명하지 않다면, 설계에 문제가 있을 수 있습니다.

C에서는 에러가 발생하면 즉시 어플리케이션이 중지합니다. 다른 언어에서는 예외를 얻어오거나, 중단 할 수 있습니다.

ØMQ가 외부 결함을 감지하면 그것을 호출 코드에 오류를 반환합니다. 오류에서 복구에 대한 분명한 전략이 없는 드문 경우 그것은 자동으로 메시지를 폐기 합니다. 몇 가지 관점에서 ØMQ는 여전히 외부 결함에 대한 것이라 주장하지만, 고려되어야 할 버그들이 있습니다.

우리가 지금까지 지켜본 C 예제의 대부분에는 오류 처리가 없습니다. 실제 코드는 모든 단일 ØMQ 호출에서 오류 처리를 해야 합니다. 당신은 C 이외의 언어 바인딩을 사용하는 경우 바인딩은 당신을 위해 오류를 처리할 수 있습니다. C에서는 직접해야 합니다. POSIX 규칙으로 시작하는 몇 가지 간단한 규칙이 있습니다. :

  • 어플리케이션이 실패한 경우, 오브젝트가 NULL을 반환하게 하는 방법
  • 다른 방법은 성공한 경우 0 리턴하고, 특별한 조건 (일반적으로 실패)에 다른 값 (주로 -1) 을 반환합니다.
  • 오류 코드는 errno 또는 zmq_errno(3)로 제공됩니다.
  • 로깅을 위해 설명하는 오류 텍스트는 zmq_strerror(3)에 의해 제공됩니다.

치명적인 오류로 처리 하지 말아야 하는 두가지 주요 예외 상태가 있습니다. :

  • 스레드가 NOBLOCK 옵션으로 zmq_recv(3)을 호출하고 대기 데이터가없는 경우. ØMQ는 errno에 EAGAIN을 설정하고 -1 반환합니다.
  • 스레드가 zmq_term(3)를 호출하고, 다른 스레드는 작업을 기다리고 있을때, zmq_term(3)호출은 컨텍스트를 종료하고 모든 대기중인 호출은 errno를 ETERM으로 설정하고 -1로 종료합니다.

C에서 대부분의 경우 ØMQ호출에 대한 에러처리는 아래와 같습니다. :

void *context = zmq_init (1);
assert (context);
void *socket = zmq_socket (context, ZMQ_REP);
assert (socket);
int rc;
rc = zmq_bind (socket, "tcp://*:5555");
assert (rc == 0);

이 코드의 첫 번째 버전에서는 assert()를 넣었습니다. 최적화 빌드가 assert()를 null로 바꾸기 때문에 좋은 생각이 아니었습니다.

정상적으로 프로세스를 종료하는 방법을 보겠습니다. 우리는 이전 섹션에서 병렬 파이프라인 예제를 가져 올 것입니다. 우리가 백그라운드에서 전체 worker가 시작하고, 배치가 완료되면 작업들을 종료하고 싶을 것입니다. Worker에 종료메시지를 보내 봅시다. 이것을 하기에 가장 좋은 위치는 sink입니다. 이것은 배치가 끝난 시점을 알고 있기 때문입니다.

어떻게 sink에 worker를 연결합니까? PUSH/PULL소켓은 단지 단방향(one-way)입니다. 표준 ØMQ의 답변 : 당신이 해결해야 할 문제의 각 유형을 위해 새로운 소켓흐름을 만들어라. 우리는 worker에게 종료 메시지를 보내기 위해 publish-subscribe모델을 사용할 것입니다. :

  • sink는 새 endpoint에 PUB소켓을 만듭니다.
  • worker는 이 endpoint에 그들의 입력소켓을 바인딩 합니다.
  • sink가 배치의 끝 부분을 감지하면 만든 PUB소켓에 종료메시지를 보냅니다.
  • Worker가 이 종료메시지를 감지하면 종료합니다.

sink에 많은 새로운 코드가 필요하지는 않습니다.:

void *control = zmq_socket (context, ZMQ_PUB);
zmq_bind (control, "tcp://*:5559");

// Send kill signal to workers
zmq_msg_init_data (&message, "KILL", 5);
zmq_send (control, &message, 0);
zmq_msg_close (&message);

fig14.png

여기에는 우리가 이전에 본 zmq_poll(3) 기술을 사용하여 두 소켓을 (PULL소켓이 작업을 얻어오고 SUB소켓은 제어명령어를 가져옵니다.) 관리하는 작업자 프로세스가 있습니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Objective-C | Perl | PHP | Python | Ruby | Ada | Basic | Go | Haskell | Node.js | ooc | Scala

여기 수정된 sink어플리케이션이 있습니다. 그 결과수집이 완료되면 그것은 모든 노동자에게 KILL 메시지를 전송합니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Lua | Objective-C | Perl | PHP | Python | Ruby | Ada | Basic | Go | Haskell | Java | Node.js | ooc | Scala

Handling Interrupt Signals

top prev next

Ctrl-C또는 SIGNTERM와 같은 제어신호가 발생했을 때 어플리케이션을 정상적으로 종료해야 합니다. 기본적으로, 이들은 단순히 프로세스를 죽이기만 하고, 메시지를 flush하지 않으며, 파일을 정상적으로 종료하지도 않습니다.

아래는 우리가 다양한 언어에서 신호를 처리하는 방법입니다. :


Erlang | Haxe | Lua | Python | Ruby | Ada | Basic | C++ | C# | Clojure | CL | F# | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl | PHP | Scala

이 프로그램은 Ctrl-C (SIGINT)와 SIGTERM을 감지하기 위해 s_catch_signals ()을 제공합니다. 이러한 신호가 도착하면 s_catch_signals ()는 전역 변수 s_interrupted을 설정합니다. 응용 프로그램이 자동으로 죽지는 않을 것이므로, 당신은 지금 명시적으로 인터럽트를 확인하고 적절하게 처리해야 합니다. 방법은 다음과 같습니다. :

  • 당신의 메인코드의 시작에 s_catch_signals ()(interrupt.c에서 복사) 를 호출합니다. 이것은 신호 조작을 설정합니다.
  • 당신의 코드가 zmq_recv(3), zmq_poll(3) 또는 zmq_send(3)에서 차단하는 경우 신호가 도착하면, 호출은 EINTR로 리턴합니다.
  • 만약 그것이 인터럽트 된다면 NULL을 리턴하는 s_recv()와 같이 처리합니다.
  • 그래서, 당신의 어플리케이션은 EINTR반환코드, NULL리턴, 또는 s_interrupted를 체크합니다.

다음은 전형적인 코드의 일부입니다. :

s_catch_signals ();
client = zmq_socket (...);
while (!s_interrupted) {
    char *message = s_recv (client);
    if (!message)
        break;          //  Ctrl-C used
}
zmq_close (client);

당신이 s_catch_signals ()를 호출하고 인터럽트에 대한 테스트를 하지 않으면, 당신의 어플리케이션은 Ctrl - C와 SIGTERM를 제어하지 않을 것입니다. 이것은 유용할 수 있지만, 일반적이지 않습니다.

Detecting Memory Leaks

top prev next

모든 장기(long-running) 실행 응용 프로그램이 정상적으로 메모리를 관리하거나, 혹은, 결국 그것이 가능한 모든 메모리를 사용하고 에러가 발생될 것입니다. 자동으로 이것을 처리하는 언어를 사용하는 경우에는 행복하겠지만, 만약 C나 C++이나 메모리 관리의 책임이 있는 다른 언어로 개발을 한다면, 당신의 프로그램이 갖고 있는 누수(leak)를 찾아 주는 valgrind를 사용하는 짧은 tutorial을 참고 하시기 바랍니다.

  • Ubuntu 이나 Debian에서 valgrind 설치하기 위해: sudo apt-get install valgrind.
  • 기본적으로 ØMQ는 valgrind을 사용하게 되면 많은 경고가 발생합니다. 이러한 경고를 제거하려면, ZMQ_MAKE_VALGRIND_HAPPY를 매크로에 추가해서 ØMQ를 다시 빌드해야 합니다. 즉
$ cd zeromq2
$ export CPPFLAGS=-DZMQ_MAKE_VALGRIND_HAPPY
$ ./configure
$ make clean; make
$ sudo make install
  • Ctrl-C후 어플리케이션이 정상적으로 종료되게 수정해야 합니다. 저절로 종료 처리하는 어플리케이션에서는 필요하지 않지만, 장기 어플리케이션(like devices)은 이것이 필수적입니다. 그렇지 않다면 valgrind는 모든 현재 할당된 메모리에 대한 문제를 보여 줄 것입니다.
  • 만약 기본 설정이 아니라면, ‘-DDEBUG’로 응용 프로그램을 빌드합니다. 그러면 valgrids는 메모리 누수가 있는 지점을 정확하게 알려 줄 수 있습니다.
  • 마지막으로, valgrind을 실행합니다. :
valgrind --tool=memcheck --leak-check=full someprog

그리고 그것이 보고한 오류를 수정한 다음, 당신은 정상적인 메시지를 얻을 수 있습니다. :

==30536== ERROR SUMMARY: 0 errors from 0 contexts...

Multipart Messages

top prev next

ØMQ은 '다중 메시지(multipart messages)'를 제공하며 여러 프레임 메시지를 작성할 수 있습니다. 보통 어플리케이션은 특히 "봉투(envelop)"를 만들기 위해 어렵게 다중 메시지를 사용합니다. 지금 우리가 배울 것은 우리가 작성한 devices는 다중메시지를 사용하는 어플리케이션이 아니지만, 어떻게 안전하게 다중 메시지를 쓰고 읽느냐는 것입니다.

당신은 여러 부분 메시지로 작업할 때, 각 부분은 zmq_msg 으로 이루어 집니다. 예를 들어, 당신은 다섯 부분으로 메시지를 보내는 경우, 당신은 다섯 zmq_msg 항목을 만들고, 전송하고 파괴 해야 합니다. 당신은 사전에 이 작업을 수행할 수 있으며(zmq_msg 항목을 배열이나 구조체에 저장합니다), 또는 당신은 하나씩 보낼 수 있습니다.

이것은 우리가 다중 메시지에서 (우리는 각 프레임에 한 메시지 개체를 받습니다.) 프레임을 전송하는 방법입니다. :

zmq_send (socket, &message, ZMQ_SNDMORE);

zmq_send (socket, &message, ZMQ_SNDMORE);

zmq_send (socket, &message, 0);

이것은 single part나 multipart로 되어 있는 메시지를 어떻게 받고 처리하는지에 대한 것입니다. :

while (1) {
zmq_msg_t message;
zmq_msg_init (&message);
zmq_recv (socket, &message, 0);
// Process the message part
zmq_msg_close (&message);
int64_t more;
size_t more_size = sizeof (more);
zmq_getsockopt (socket, ZMQ_RCVMORE, &more, &more_size);
if (!more)
break; // Last message part
}

다중 메시지에 대해 알아야 할 몇가지 내용 :

  • 당신이 다중 메시지를 보낼 때, 한번에 보냅니다.(첫 번째부터 마지막 부분까지)
  • 당신이 zmq_poll(3)를 사용하는 경우 메시지의 첫 부분을 받을 때, 나머지도 도착합니다.
  • 당신은 메시지의 전체부분을 받거나 아무것도 받지 못할 것입니다.
  • 메시지의 각 부분은 구분된 zmq_msg 항목입니다.
  • 당신은 RCVMORE 옵션을 선택 여부에 상관없이 메시지의 모든 부분을 받게 됩니다.
  • 메시지를 보내자 마자 ØMQ는 마지막 메시지를 받을 때까지 메시지를 대기열에 넣고 한번에 모두 보낸다.
  • 소켓을 닫는 것을 제외하고 부분적으로 보낸 메시지를 취소할 수 있는 방법은 없습니다.

Intermediates and Devices

top prev next

어떤 연결된 장치는 장치 회원 증가에 따라 복잡한 곡선을 그립니다. 회원이 적을 때는 서로에 대해 알 수 있지만 장치가 커짐으로써, 모든 다른 흥미로운 회원을 알려고 하는 각각 회원의 비용은 선형적으로 증가하고, 연결 회원의 전체 비용은 factorially하게 커집니다. 솔루션은 더 작은 것들로 집합을 만들고, 집합을 연결하는 중계자를 만듭니다.

이 패턴은 현실 세계에서 매우 일반적이며, 우리 사회와 경제가 큰 네트워크의 복잡성과 크기 조정 비용을 절감하기 보다 다른 실제 기능이 없는 중개인으로 가득 차있는 이유입니다. 중개인은 일반적으로 도매업자, 유통, 관리자 등으로 불립니다.

이와 같이 ØMQ 네트워크는 필요한 중개인 없이 특정 크기 이상 성장할 수 없습니다. ØMQ에서, 우리는 이것을 "devices"라고 부릅니다. 우리가 ØMQ을 사용 할때, 우리는 일반적으로 중개인 없이 서로 얘기할 수 있는 노드로 된 네트워크, 노드의 집합으로 우리의 어플리케이션을 구축하기 시작합니다. :

fig15.png

그리고 우리는 특정 장소에 장치를 배치하고 노드의 수를 최대 확장하여, 더 넓은 네트워크를 통해 응용 프로그램을 확장할 수 있습니다. :

fig16.png

ØMQ 장치는 엄격한 설계 규칙은 없습니다, 하지만 일반적으로 '백엔드'소켓 세트에 '프론트 엔드'소켓 세트를 연결합니다. 이것은 이상적으로 상태 없이(no state) 동작합니다. 그래서 필요로 하는 많은 중계로 어플리케이션을 확장하는 것이 가능하게 됩니다. 당신은 프로세스 내에서 스레드, 또는 독립 실행형 프로세스로써 이것을 실행할 수 있습니다. ØMQ는 몇몇 매우 기본적인 devices를 제공하지만 당신은 실제로 자신을 개발하는 것입니다.

ØMQ devices는 주소, 서비스, 큐 혹은 메시지와 소켓 레이어 상에서 정의할 수 있는 어떤 다른 추상적인 것의 중계를 할 수 있습니다. 다른 메시징 패턴은 다른 복잡한 문제를 가지며, 여러 종류의 중개가 더 필요합니다. 예를 들면, request-reply는 대기열과 서비스 추상화와 잘 작동하고, publish-subscribe는 stream이나 topics과 잘 작동합니다.

어떤 전통적인 중앙 중개인에 비해 ØMQ에 대한 흥미로운 건, 당신이 필요로 하는 곳에 정확하게 device를 게재할 수 있다는 것입니다, 이것은 최적의 중개를 할 수 있습니다.

A Publish-Subscribe Proxy Server

top prev next

이것은 하나 이상의 네트워크 세그먼트 또는 전송을 통해 publish-subscribe아키텍처를 확장하기 위한 일반적인 요구 사항입니다. 아마, 원격시스템에 설정한 subscribers가 있다면, 우리는 멀티캐스트를 통해 지역 subscribers, 혹은 TCP를 통해 원격 subscribers에게 전파하기를 원할 것입니다.

우리는 두 네트워크를 연결하는, publisher와 subscribers 집합 사이에 설정된 간단한 프록시 서버를 쓰는 것입니다. 이것은 아마도 유용한 장치의 간단한 경우입니다. 이 장치는 두 소켓을 가집니다. 날씨서버가 있는 내부 네트워크의 frontend와 외부 네트워크에 subscriber가 있는 backend입니다. 이것은 frontend소켓에서 날씨서비스를 subscribe하고 backend소켓에 그 데이터를 republish합니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Ada | Basic | Go | Node.js | Objective-C | ooc | Scala

이것은 publisher에게 subscriber역할을 하고 subscriber에게 publisher역할을 하기 때문에 우리는 이것을 proxy라고 부릅니다. 이것은 당신이 그것에 영향을 주지 않는 기존 네트워크에 장치를 끼워 넣을 수 있다는 것을 의미 합니다.(물론 새로운 subscribers는 proxy와 통신하기 위해 알 필요가 있습니다.)

fig17.png

이 어플리케이션은 다중안정(multipart safe) 합니다. 이것은 정확하게 다중 메시지를 감지하고 그것을 읽고 그것을 보냅니다. 우리가 보내는 다중 데이터에 SNDMORE 옵션을 설정하지 않은 경우 최종 수신자가 손상된 메시지를 받을 수 있습니다. 그것이 스위치한 데이터가 손상될 수 있는 리스크를 없애기 위해 당신은 항상 당신의 장치를 다중안전하게 만들어야 합니다.

A Request-Reply Broker

top prev next

ØMQ에 작은 메시지 대기열 브로커를 작성하여 scale의 문제를 해결하는 방법을 알아 봅시다. 우리는 이 경우를 위해 request-reply패턴을 보겠습니다.

Hello World client-server어플리케이션에서 우리는 하나의 서비스와 통신하는 하나의 클라이언트가 있습니다. 그러나 실제의 경우 우리는 일반적으로 여러 서비스뿐만 아니라 여러 클라이언트를 허용해야 합니다. 이것은 우리가 서비스의 능력을 크게 할 필요가 (단지 하나보다는 여러 스레드, 프로세스, 서버) 있습니다. 유일한 제약 조건은 서비스는 무상태(stateless)여야 하며, 모든 상태(state)는 요청이나 데이터베이스 같은 공유 저장소에 존재합니다.

여러 서버에 여러 클라이언트를 연결하는 방법은 두 가지가 있습니다. 이 Brute-force방법은 여러 서비스 끝점에 각 클라이언트 소켓을 연결하는 것입니다. 하나의 클라이언트 소켓은 여러 서비스 소켓에 연결할 수 있고, 요청이 서비스간에 로드밸런스 됩니다. 자, 당신이 세 서비스 끝점에 대한 클라이언트 소켓연결 A, B, C가 있고, 클라이언트는 요청 R1, R2, R3, R4가 있습니다. R1과 R4는 서비스A, R2는 B로 이동하고 R3은 서비스 C로 이동합니다.

fig18.png

이 디자인은 적은 비용으로 더 많은 고객을 추가할 수 있습니다. 당신은 또한 더 많은 서비스를 추가할 수 있습니다. 각 클라이언트는 서비스 요청을 로드밸런스 합니다. 그러나 각각의 클라이언트는 서비스 토폴로지를 알고 있습니다. 당신이 100개 클라이언트를 가지고 있고, 3개 서비스를 추가하려는 경우, 당신은 재구성이 필요하고 3개 새로운 서비스에 대해 알고 있으며, 클라이언트을 위해 100개 클라이언트를 다시 시작합니다.

그것은 분명히 우리의 슈퍼 컴퓨팅 클러스터 리소스가 부족하면 오전 3시에서 일을 하려던 일을 못 합니다. 그래서 우리는 필사적으로 새로운 서비스 노드 수백개를 추가 해야 할 필요가 있습니다. 너무 많은 조작은 액상 콘크리트와 같습니다.:지식은 분산되어 있고 당신이 가지고 있는 많은 안정적인 조작과 노력은 토폴로지를 변경하는 것입니다. 우리가 원하는 것은 토폴로지의 모든 지식을 집중하여 클라이언트와 서버 사이에 두는 것입니다. 이상적으로, 우리는 토폴로지의 다른 부분을 건드리지 않고도 언제든지 서비스 또는 클라이언트를 추가하고 제거할 수 있습니다.

그래서 우리는 이 유연성을 제공하는 작은 메시지 대기열 브로커를 작성합니다. 브로커는 두 종점, 클라이언트을 위한 프런트 엔드 및 서비스에 대한 백엔드에 바인딩합니다. 그런 다음 활성화를 위해 두 소켓을 모니터링 하는 zmq_poll(3)사용하고 두 소켓 사이에 메시지가 돌아 다닙니다. 사실은 명시적으로 모든 대기열을 관리하지 않습니다 ? ØMQ는 각 소켓에서 자동으로 처리합니다.

당신이 REP와 대화하기 위해 REQ를 사용할 때 당신은 엄격한 synchronous request-reply 결과를 얻습니다. 클라이언트는 요청을 보내고 서비스는 요청을 읽고 응답을 보냅니다. 그 다음 클라이언트는 응답을 읽습니다. 만약 클라이언트나 서비스가 뭔가를 (예를 들어 응답을 기다리지 않고 연속 두 요청을 보내는)하려고 하면 에러가 발생합니다.

그러나 브러커는 non-blocking을 가집니다. 분명히 우리는 두 소켓의 활동을 기다리는 zmq_poll(3)를 사용할 수 있지만, REP와 REQ는 사용할 수 없습니다.

다행히 non-blocking request-response은 허락하는 DEALER과 ROUTER라고 하는 두 소켓이 있습니다. 이러한 소켓은 XREQ 및 XREP를 호출하는 데 사용되고, 예전 코드에서 이러한 이름을 볼 수 있습니다. 옛 이름은 XREQ은 " extended REQ"이고 XREP은 " extended REP"이라고 하지만 그건 정확하지 않습니다. 3장에서는 어떻게 DEALER과 ROUTER소켓이 비동기 request-reply 흐름의 모든 종류를 처리하는지 볼 것입니다.

지금, 우리는 단지 어떻게 DEALER와 ROUTER가 장치(즉 우리의 작은 브로커)를 통해 extend REQ-REP를 처리하는가를 볼 것입니다.

이것은 REQ가 ROUTER와 통신하고 DEALER가 REP와 통신하는 간단히 확장된 request-reply패턴입니다. 이 DEALER와 ROUTER사이에서 우리는 한 소켓에서 메시지를 가져오고 다른 쪽에서 그것을 밀어주는 코드(브로커 같은)를 가져야 합니다. :

fig19.png

Request-reply 브로커는 두개의 종점을 바인딩하며, 하나는 클라이언트를 위해 frontend socket에 연결하고 다른 한 개는 서비스를 위해 backend socket에 연결합니다. 이 브로커를 테스트 하기 위해 우리는 그것이 backend소켓에 연결할 수 있도록 서비스를 변경하려고 합니다. 이것의 의미가 무엇인지 보여주는 client와 service입니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Node.js | Objective-C | ooc

여기 서비스는 다음과 같습니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Node.js | Objective-C | ooc

그리고 여기 브로커가 있습니다. 당신은 다중안전(multipart safe)을 확인 할 수 있을 것 입니다:


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc

Request-reply브로커를 사용하면 client는 service를 직접 볼 수 없고, service또한 client를 볼 수 없기 때문에 client-server 구조를 쉽게 만들 수 있습니다. 단지 안정한 노드는 중간에 있는 장치(device)입니다. :

fig20.png

Built-in Devices

top prev next

대부분의 고급 사용자가 자신의 장치를 작성하지만 ØMQ는 몇 가지 기본 장치를 제공합니다. Built-in장치는 다음과 같습니다

  • QUEUE. request-reply 브로커와 같습니다.
  • FORWARDER. pub-sub 프록시 서버와 같습니다.
  • STREAMER. pipeline 흐름을 제외하고 FORWARDER와 같습니다.

장치를 시작하려면, 당신은 zmq_device(3)을 호출하며, 이것은 두 소켓, 프론트엔드를 위해 하나, 백엔드를 위해 하나를 통과(pass)합니다. :

zmq_device (ZMQ_QUEUE, frontend, backend);

QUEUE를 시작하면 장치가 그 시점에 당신의 코드에 request-reply 브로커의 본체에 연결하는 것과 같습니다. 당신은 zmq_device(3).을 호출하기 전에 소켓을 만들어야 하고 그것에 바인드나 연결을 한 후 가능한 설정을 해야 합니다. 이것은 간단한 작업입니다. 이것은 QUEUE를 호출하기 위해 재 작성한 request-reply입니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Ada | Basic | Go | Node.js | Objective-C | ooc | Scala

위의 예제에는 보이지 않지만, built-in devices에는 적절한 에러처리가 되어 있습니다. Devices를 시작하기 전에 당신이 필요로 하는 소켓구성을 할 수 있기 때문에 built-in devices를 사용할 가치가 있습니다.

만약 당신이 대부분의 ØMQ 사용자와 같다면, 이 상황에서 당신의 마음은 “만약 devices에 임의의 소켓 유형을 꽂을 수 있다면 어떤 사악한 물건 같은 것을 만들 수 있을 텐데!”를 생각하기 시작할 것입니다. 짧게 말해서 : 그렇게 하지 말아라, 당신이 혼합된 소켓유형을 만들 수 있지만, 결과는 기괴하게 될 것입니다. 그래서 queue장치를 위해서는 ROUTER/DEALER, forwarder를 위해서는 SUB/PUB 그리고 streamer를 위해서는 PULL/PUSH를 사용하도록 되어 있습니다.

당신이 다른 조합이 필요하기 시작하면 자신의 장치를 작성할 때가 된 것입니다.

Multithreading with ØMQ

top prev next

ØMQ는 아마도 멀티 스레드(MT) 어플리케이션을 작성하는 가장 좋은 방법입니다. 당신이 전통적인 소켓을 사용하는 경우 ØMQ 소켓은 몇몇 재조정이 필요한 반면 ØMQ 멀티스레딩은 당신이 MT 응용 프로그램에 대해 아는대로 다 됩니다.

완전히 완벽한 MT 프로그램을 (그리고 그 말 그대로)하기 위해서 우리는 mutexes, locks, 또는 ØMQ 소켓을 통해 보내는 메시지를 제외하고는 스레드 간 통신의 다른 형태는 필요하지 않습니다.

"완벽한" MT 프로그램이라는 것은 작성과 이해가 쉽고, 어떤 언어/OS에서 한 기술로 동작이 되고, 제로 대기상태(zero wait states) 및 결과의 체감 없이 CPU의 수로 규모산정이 되는 것을 의미 합니다.

만약 당신이 locks, semaphores와 중요한 섹션을 혼자서 빠르게 당신의 MT 코드를 만드는데 몇 년이 소요되는데, 아무 것도 없이 그것이 실현된 다면 당신은 화가 날 것입니다. 만약 우리가 동시성 프로그램(단지, 상태를 공유하지 않는다.)에 30년 이상을 배워야 하는 강의코스가 있다면, 이것은 맥주를 공유하려고 하는 두 술주정꾼과 같습니다. 그들이 좋은 친구인지는 그리 중요하지 않습니다. 조만간 그들은 싸움을 하게 될 겁니다. 그리고 거리에 술수정꾼이 많아질수록 맥주를 통한 싸움은 더 많아 질 것입니다. MT 응용 프로그램의 비극 대부분은 술집 싸움과 같습니다.

이상한 문제리스트를 당신은 직접적인 스트레스나 리스크로 이해하지 않지만 전통적인 shared-state MT코드를 작성함으로써, 고통속에서 갑자기 에러를 발생시킬 것 같은 코드와 싸워야 합니다. 여기에 버그 코드(forgotten synchronization, incorrect granularity, read and write tearing, lock-free reordering, lock convoys, two-step dance, and priority inversion)에 있어서 세계적인 경험을 가지고 있는 큰 회사가 제시한 “당신의 스레드 코드에 11가지 잠재적 문제점”의 목록이 있습니다.

우리는 11개가 아닌 7개가 있습니다. 이것이 요점은 아닙니다. 요점은 바쁜 수요일 오후 3시에 두 단계 잠금 convoys를 시작하려는 전력망이나 주식시장을 운영할 코드를 실제 원하는가? 입니다. 누가 실제 의미하는 용어가 무엇인지 관여 하겠습니까. 이것은 더 복잡한 부작용과 더 복잡한 해킹과 싸우는 프로그램으로 바꾸는 것은 아닙니다.

수십억 달러 산업의 기초임에도 불구하고 몇가지 널리 사용되는 은유는 기본적으로 고장(broken)이며, 공유 상태 동시성(shared state concurrency)은 그중 하나 입니다. 제한 없이 사용하고 싶은 코드는 오류 프로그램 일부만 보이는 것 외에 공유하지 않고 메시지만 보내는 인터넷과 같습니다.

당신은 ØMQ로 만족할 만한 멀티 스레드 코드를 작성하기 위해 몇 가지 규칙을 따라야 합니다 :

  • 당신은 여러 스레드에서 같은 데이터를 액세스할 수 없습니다. mutexes 같은 고전 MT 기법을 사용하는 것은 ØMQ 응용 프로그램에서 anti-pattern입니다. 이것에 유일한 예외는 threadsafe한 ØMQ 컨텍스트 객체입니다.
  • 당신은 당신의 프로세스에서 ØMQ 컨텍스트를 생성하고 inproc 소켓을 통해 연결하려는 모든 스레드에게 전달해야 합니다.
  • 당신은 자신의 컨텍스트와 함께 별도의 작업으로 스레드를 취급해도 되지만, 이 스레드는 inproc을 통해 통신을 할 수 없습니다. 그러나 그것은 나중에 독립(standalone) 프로세스에 침투하기 쉽습니다.
  • 당신은 쓰레드간에 ØMQ 소켓을 공유하지 않아야 합니다. ØMQ 소켓은 threadsafe하지 않습니다. 기술적으로 그렇게 하는 것이 가능하지만 그것은 세마포, 잠금, 또는 mutexes을 요구합니다. 이것은 응용 프로그램이 느리고 약하게 합니다. 이 스레드간에 소켓을 공유하는 유일한 방법은 소켓에 대한 가비지 수집과 같은 기능을 제공하는 언어 바인딩에 있습니다.

당신은 응용 프로그램에서 하나 이상의 장치를 시작할 필요가 있다면 예를 들어, 당신은 자신의 스레드에 각각 실행하는 것이 좋습니다. 그것은 하나의 스레드에 있는 장치 소켓을 만들면 오류를 확인하기 쉽습니다. 그리고 다른 스레드에 있는 장치에 소켓을 통과 합니다. 이것은 작동하는 것처럼 보일 수 있지만 무작위로 실패합니다. 주의 사항 : 이것을 만든 스레드를 제외하고는 소켓을 사용하거나 닫지 마십시오.

당신이 이 규칙을 따른다면 당신이 필요로 할 때, 당신은 아주 쉽게, 별도의 프로세스로 스레드를 분리하실 수 있습니다. 어플리케이션 로직은 규모에 상관없이 스레드, 프로세스, 서버에 있습니다.

ØMQ는 가상의 ‘green’ 스레드보다는 기본 OS 스레드를 사용합니다. 이것의 장점은 당신이 새로운 스레딩 API를 배울 필요가 없다는 것입니다, 그리고 ØMQ 스레드는 운영 체제에 완전하게 연결됩니다. 당신의 어플리케이션이 무엇을 하는지 보기 위해 인텔의 ThreadChecker와 같은 표준 툴을 사용할 수 있습니다. 단점은 코드가, 예를 들어 그것이 새로운 스레드를 시작할 때, 이식성이 좋지 않다는 것입니다. 그리고 당신이 많은 스레드를 실행시키면 일부 운영 체제는 부하를 받을 것이다.

실제로 어떻게 처리되는지 봅시다. 우리는 기존 Hello World 서버에 몇가지 기능을 추가할 것입니다. 기존 서버는 단일 스레드 입니다. 만약 요청마다 작업이 느려도 괜찮습니다.: 단일 ØMQ 스레드는 많은 작업을 수행하는데 wait없이 단일 CPU에서 최대 속도로 실행할 수 있습니다. 그러나 현실적으로 서버는 요청에 따라 중요한 작업을 해야 합니다. 10,000개의 클라이언트가 모두 한번에 서버에 접속할 때 한개의 코어로는 충분하지 않을 수 있습니다. 그래서 현실적인 서버는 여러 개의 작업자 스레드를 시작합니다. 그런 다음 그것을 가능한 한 빨리 요청을 수용하고, 작업자 스레드에 이들을 배포합니다. 작업자 스레드는 작업을 통해 분쇄하고, 결국 다시 그들의 답장을 보냅니다.

당신은 물론 대기열 장치 및 외부 작업자 프로세스를 사용하여 이 모든 것을 할 수 있지만, 보통 한 코어에 16개 프로세스보다 16개 코어에 한 프로세스로 시작하는 것이 더 쉽습니다. 또한, 스레드로 worker를 실행하면 네트워크 홉 (Hop), 지연 시간 및 네트워크 트래픽이 없습니다.

기본적으로 Hello World 서비스의 MT버전은 단일 프로세스의 queue 장치와 worker로 구성됩니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Java | Node.js | Objective-C | ooc

어떻게 작동하는지 모든 코드를 읽을 수 있어야 합니다. :

  • 서버는 작업자 스레드들을 시작합니다. 각 작업자 스레드는 REP 소켓을 생성하고 이 소켓에 대한 요청을 처리합니다. 작업자 스레드는 단일 스레드 서버와 같습니다. 유일한 차이는 전송매체 (TCP대신 inproc)와 bind-connect direction입니다.
  • 서버가 클라이언트에 연결하기 위해 ROUTER소켓을 생성하고 외부 인터페이스 (TCP상) 자체에 이것을 바인딩합니다.
  • 서버는 worker와 연결하기 위해 DEALER를 생성하고, 내부 인터페이스 (inproc 상) 자체에 이것을 바인딩합니다.
  • 서버는 두 개의 소켓에 연결된 queue 장치를 시작합니다. 대기열 장치는 들어오는 요청에 대해 하나의 queue을 유지하고, workers에게 분배합니다. 그것은 또한 다시 그것의 회신을 라우팅을 합니다.

생성한 스레드는 대부분의 프로그래밍 언어로 이식되지 않습니다. POSIX 라이브러리는 pthreads이지만, 윈도우에서 당신은 다른 API를 사용해야 합니다. Portable API에서 이것을 포장(wrap)하는 방법은 3장에서 볼 것입니다.

여기 ‘work’는 단지 1초 정지됩니다. 우리는 다른 노드에 연결하는 것을 포함하여, worker에 어떤 것을 할 수 있습니다. 이것은 MT 서버가 ØMQ 소켓과 노드의 관점에서 비슷하게 보인다는 것입니다. 어떻게 Request-reply 연결이 REQ-ROUTER-queue-DEALER-REP로 되는지 주의해서 보시기 바랍니다. :

fig21.png

Signaling between Threads

top prev next

당신이 ØMQ로 멀티 스레드 응용 프로그램을 만들기 시작하면, 당신은 스레드를 조정하는 방법에 대한 질문을 던질 것입니다. 당신이 'sheep'문장을 삽입하려 하고, 또는 세마포 또는 mutexes와 같은 멀티 스레딩 기술을 사용하려고 시도 할 수 있지만, 당신이 사용해야 하는 유일한 메커니즘은 ØMQ 메시지입니다. Drunkards의 이야기와 Beer Bottle을 기억하십시오.

아래는 준비가 되었을 때 각각 다른 신호를 보내는 3개 스레드를 보여주는 간단한 예입니다.

fig22.png

이 예제에서 우리는 inproc 전송매체를 통해 PAIR소켓을 사용합니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haskell | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Ada | Basic | Go | Node.js | Objective-C | ooc | Scala

이것은 ØMQ에서 멀티 스레딩에 대한 고전적인 패턴입니다. :

  1. 두 스레드는 공유 컨텍스트를 사용하여 inproc를 통해 통신합니다.

#부모 스레드가 inproc:// endpoint//에 바인딩한 하나의 소켓을 생성하고 그것에 컨텍스트를 전달하는 자식 스레드를 시작합니다.

  1. 자식 스레드가 inproc:// endpoint//에 연결하는 두 번째 소켓을 만들고 준비된 부모 스레드에 신호를 보냅니다.

이 패턴을 사용하는 멀티 스레딩 코드는 http://zguide.zeromq.org//프로세스로 확장되지 않는 것//**에 유의하시기 바랍니다. 당신이 inproc 및 소켓 쌍을 사용한다면, 당신은 밀접하게 바인딩된 응용 프로그램을 구축하고 있는 것입니다. 낮은 지연 시간이 정말 중요할 때 이 작업을 수행합니다. 모든 정상적인 어플리케이션을 위해 스레드 마다 하나의 컨텍스트를 사용하고 ipctcp를 사용합니다. 그러면 당신은 쉽게 필요에 따라, 별도의 프로세스, 또는 서버로 떼어내어 당신의 스레드를 분리할 수 있습니다.

여기서 우리가 PAIR소켓을 사용하는 예제를 보는 것은 처음입니다. 왜 PAIR을 사용합니까? 다른 소켓 조합도 작동하는 것으로 보일지 모르지만, 이것은 신호를 연계하는데 부작용을 가지고 있습니다. :

  • 당신은 sender를 위해 PUSH를 사용하고 receiver를 위해 PULL을 사용 할 수 있습니다. 이것은 간단하게 작동되지만, PUSH는 가능한 모든 receiver에 메시지를 로드밸런스 한다는 것을 기억하기 바랍니다. 만약 당신이 우연히 두개의 receivers를(예, 당신은 이미 한 개가 실행되어 있고, 두번째를 실행합니다.) 실행한다면 신호의 절반을 잃게 됩니다. PAIR는 하나 이상의 연결을 거부하는 장점이 있으며, 두 개가 독점을 합니다.

? 당신은 sender로 PUB, receiver로 SUB를 사용할 수 있습니다. PUB은 PUSH나 DEALER처럼 로드밸런스를 하지 않고 보내려는 메시지를 정확하게 보낼 것입니다. 그러나 당신은 빈 subscription으로 subscriber를 구성해야 합니다. 단점은, PUB-SUB 연결의 신뢰성은 시간에 달려 있고 PUB소켓이 메시지를 보내는 동안 SUB소켓이 연결중에 있다면 메시지는 유실 됩니다.

위와 같은 이유로, PAIR소켓은 한 쌍의 스레드 사이의 조화을 위한 최고의 선택입니다.

Node Coordination

top prev next

당신이 노드를 조정하고자 할 때, PAIR소켓은 더 이상 제대로 작동하지 않습니다. 이것은 스레드와 노드에 대한 전략이 다르기 때문에 발생되는 원인 중 하나입니다. 주로 노드는 스레드가 stable한 반면 오고 갑니다. 원격 노드가 사라지거나 다시 온다면 PAIR 소켓은 자동으로 다시 연결하지 않습니다.

스레드와 노드 사이의 두 번째 큰 차이점은 일반적으로 스레드는 고정 된 수를 가지고 있고, 노드는 다양한 수를 가집니다. 이전 시나리오 중 하나를 보면 (날씨 서버와 클라이언트) subscriber가 시작할 때 데이터 유실을 막기 위해 노드를 조정했습니다.

아래는 어플리케이션이 어떻게 작동하는지 보여 줍니다. :

  • Publisher는 subscriber가 얼마나 많은지 미리 알고 있습니다. 이것은 어딘가에서 얻은 마법의 숫자입니다.
  • Publisher는 시작된 후 모든 subscriber 연결되기를 기다립니다. 이것은 노드 조정 부분입니다. 각 subscriber는 subscribe하고 다음 소켓을 통해 연결된 publisher와 통신합니다.
  • Publisher는 모든 subscriber가 연결된 경우, 데이터를 게시(publish)하기 시작합니다.

이 경우에 우리는 subscriber와 publisher를 동기화 하기 위해 REQ-REP소켓을 사용합니다. 여기 publisher는 다음과 같습니다:


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Node.js | Perl | PHP | Python | Ruby | Ada | Basic | Go | Haskell | Objective-C | ooc | Scala
fig23.png

이것은 subscriber입니다:


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Node.js | Perl | PHP | Python | Ruby | Ada | Basic | Go | Haskell | Objective-C | ooc | Scala

이 리눅스 쉘 스크립트는 10개의 subscriber를 시작하고 그 다음 publisher를 시작합니다. :

echo "Starting subscribers..."
for a in 1 2 3 4 5 6 7 8 9 10; do
    syncsub &
done
echo "Starting publisher..."
syncpub

만족스러운 결과물 입니다. :

Starting subscribers...
Starting publisher...
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates
Received 1000000 updates

우리는 REQ/REP 출력화면이 완료되는 시점에 SUB연결이 끝날 거라고 추측 할 수 없습니다. 당신은 inproc 제외한 모든 전송매체를 사용하는 경우 아웃 바운드가 어떠한 순서로 완료된다고 보장할 수 없습니다. 그래서, 예제는 subscribing사이에 1초를 강제로 sleep하고 REQ/REP동기화를 보냅니다.

보다 강력한 모델이 되기 위해서 :

  • Publisher는 PUB소켓을 열고, “Hello”메시지(데이터는 없음)를 보내기 시작합니다.
  • Subscriber는 SUB소켓을 열고 “Hello”메시지를 받았을 때 REQ/REP 한 쌍의 소켓을 통해 publisher와 통신 합니다.
  • Publisher가 필요한 확인을 거친 후 실제 데이터를 전송하기 시작합니다.

Zero Copy

top prev next

Zero-copy를 모르는 ØMQ 초보였을 때를 지나, 여기까지 왔다면 당신은 zero-copy를 사용할 준비가 된 것입니다. 그러나 잘못 된 곳으로 가는 많은 길이 있고, 조기 최적화는 즐거운 일도 아니고 수익성도 없습니다. 당신의 아키텍쳐가 완벽하지 않는 상태에서 zero-copy를 하려고 하는 것은 아마도 시간 낭비이고 작업을 더 악화시키는 것이기에 좋을 것이 없습니다.

ØMQ의 메시지 API는 데이터를 복사하지 않고 데이터를 응용 프로그램 버퍼로부터 직접 메시지를 보내고 받을 수 있습니다. ØMQ가 백그라운드에서 메시지를 보낸다면 zero-copy는 일부 소스 수정이 필요합니다.

zero-copy를 사용하기 위해 우리는 malloc()으로 힙에 할당된 데이터 블록을 참조하는 메시지를 생성하기 위해 zmq_msg_init_data(3)을 사용하고, 다음 zmq_send(3)으로 전송합니다. 당신은 생성한 메시지의 송신이 완료되었을 때 데이터 블록을 풀기(free)위해 호출하는 함수를 사용합니다. 이것은 'buffer'가 힙에 할당된 1000 바이트의 블록을 설정하는 간단한 예제입니다. :

void my_free (void *data, void *hint) {
free (data);
}
// Send message from buffer, which we allocate and 0MQ will free for us
zmq_msg_t message;
zmq_msg_init_data (&message, buffer, 1000, my_free, NULL);
zmq_send (socket, &message, 0);

수신에서는 zero-copy하는 방법은 없습니다 : ØMQ는 당신이 원하는대로 저장한 버퍼를 전달할 수 있지만, 응용 프로그램 버퍼에 직접 데이터를 작성하지는 않을 것입니다.

ØMQ의 다중 메시지는 zero-copy와 함께 쓰기에 잘 작동합니다. 전통적인 메시징방식에서 당신은 보낼 수 있는 한 버퍼와 함께 다른 버퍼를 마샬링할 필요가 있습니다. 이것은 복사 데이터를 의미합니다. ØMQ를 사용하면 개별 메시지 부품 등 다양한 소스에서 오는 여러 버퍼를 보낼 수 있습니다. 우리는 length-delimited 프레임으로 각 필드를 보냅니다. 응용 프로그램은 전송과 수신 호출의 반복 입니다. 그러나 내부적으로 여러 부분이 네트워크에 쓰고, 단일 시스템 호출로 다시 읽습니다. 그래서 이것은 매우 효율적입니다.

Transient vs. Durable Sockets

top prev next

고전적인 네트워킹에서, 소켓은 API 객체입니다, 이것들의 수명은 그들을 사용하는 코드보다 절대 길지 않습니다. 그러나 소켓을 보면 자원(네트워크 버퍼)을 수집하는 것을 볼 수 있습니다. ØMQ 사용자가 물었습니다, "내 프로그램이 깨지는 경우 원복 말고, 어떤 방법이 있습니까? "

이것은 매우 유용한 것으로 밝혀 졌습니다. 이것은 간단하지는 않습니다. 특히, ØMQ에서는 pub-sub경우에 유용합니다. 잠시 보겠습니다.

여기 두 소켓이 날씨에 대해서 행복하게 채팅 하는 일반적인 모델이 있습니다.

fig24.png

만약 소켓의 receiver(SUB, PULL, REQ)쪽에서 identity를 설정한다면, 그다음 송신(PUB,PUSH,PULL)쪽은 HWM까지 연결되지 않는 경우 메시지가 버퍼에 쌓일 것입니다. 송신쪽은 처리하기 위해 identity를 설정할 필요가 없습니다.

ØMQ의 전송과 수신 버퍼는 보이지 않으며 자동 동작 합니다.(TCP 버퍼 처럼).

모든 소켓에서 우리는 일시적으로 과도하게 사용해 왔습니다. transient 소켓을 durable한 소켓으로 변경하기 위해 명시적으로 identity를 설정해야 합니다. 모든 ØMQ 소켓이 ID를 가지고 있지만 기본적으로 ØMQ는 누구와 얘기하는지 기억하기 위해 UUID(unique universal identifiers)를 생성합니다.

한 소켓과 다른 소켓이 연결할 때 우리는 모르는 사이 두 소켓은 identities를 교환합니다. 일반적으로 소켓은 대상의 ID를 알려고 하지 않기 때문에 서로간에 임의의 ID를 생성합니다.

fig25.png

하지만 소켓은 그것의 ID를 교환한 후 다음번에 만나서 이럴겁니다. “내가 들은 것은 당신이 사무실 가는 법을 알고 있는 어떤 방법과 다르다고 말했습니다. 그들은 수다쟁이 입니다. 나는 어떤 누구에도 어떤 것도 얘기를 하지 않았으며 사실이 아닙니다.”

fig26.png

여기 내구성 소켓을 만들기 위한 소켓 ID을 설정하는 방법은 다음과 같습니다. :

zmq_setsockopt (socket, ZMQ_IDENTITY, "Lucy", 4);

소켓 ID를 설정하는 몇 가지 설명 :

  • 당신은 소켓을 연결하거나 바인딩 전에 반드시 ID를 설정을 해야 합니다.
  • Receiver가 ID를 설정합니다. : 그것은 client/sender가 사용할 쿠키를 생성하는 경우는 제외하고, HTTP 웹 응용 프로그램에서 세션 쿠키 같은 것입니다.
  • ID는 이진 문자열 입니다 : zero바이트로 시작하는 ID는 ØMQ 사용을 위해 예약되어 있습니다.
  • 하나 이상의 소켓에 동일한 ID를 사용하지 마십시요. 이미 다른 소켓에서 만들어진 ID를 사용하여 연결하려고 하면 연결이 안 됩니다.
  • 많은 소켓을 사용하는 어플리케이션에서 임의의 ID를 사용하지 마십시요. 이렇게 하는 것은 충돌하는 durable 소켓이 많아지는 원인이 되고, 결국 노드가 깨집니다.
  • 당신이 메시지를 받은 peer의 ID를 인지할 필요가 있는 경우 ROUTER소켓이 자동으로 이 작업을 수행합니다. 다른 소켓 유형을 위해 명시적으로 주소를 메시지 일부로써 보내야 합니다.
  • Durable소켓을 사용하는 것은 종종 나쁜 생각이라고 말을 합니다. 이것은 sender가 아키텍쳐를 약하게 만드는 엔트로피를 쌓이게 합니다. ØMQ에서는 명시적인 정체성을 구현하지 않는 게 좋습니다.

ZMQ_IDENTITY 소켓 옵션의 요약을 위해 zmq_setsockopt(3)을 봅시다. zmq_getsockopt(3) 메소드는 작업중인 소켓의ID를 제공합니다.

Pub-sub Message Envelopes

top prev next

우리는 multipart 메시지를 간단히 봤습니다. 지금은 메시지 envelopes에 대해서 보겠습니다. Envelop은 데이터를 건드리지 않고 주소와 함께 안정하게 데이터를 포장하는 한 방법입니다.

pub-sub패턴에서, 적어도 envelop은 필터링을 위한 subscription key를 가지고 있지만, envelop에 발신자ID를 추가할 수 있습니다.

당신이 pub-sub envelop을 사용하기 원한다면, 당신이 직접 envelop을 만들 수 있습니다. 이것은 선택이며, 이전 pub-sub예제에서는 이렇게 하지 않았습니다. Pub-sub envelop을 사용하는 것은 간단한 경우에는 조금 더 많은 작업이 들어가지만, 키와 데이터가 자연스럽게 분리되는 실제 경우에는 더 간단합니다. 당신이 어플리케이션 버퍼에 직접 데이터를 쓴다면 더 속도가 빨라집니다.

envelop된 publish-subscribe 메시지는 아래와 같이 보여집니다. :

fig27.png

Pub-sub은 접두사가 일치하는 메시지를 가져옵니다. 구분된 프레임에 키를 넣는 것은 매우 분명한 일치성을 제공합니다. 그래서 우연히 데이터의 일부만 일치하는 경우를 없애 줍니다.

여기 pub-sub envelop이 코드상으로 어떻게 보이는지 간단한 예제가 있습니다. Publisher는 A,B 두개 유형의 메시지를 보냅니다. Envelop은 메시지 유형을 가집니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc

Subscriber는 B타입의 메시지만 원합니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc

이 두 프로그램을 실행하면, subscriber 결과는 아래와 같이 출력합니다. :

[B] We would like to see this
[B] We would like to see this
[B] We would like to see this
[B] We would like to see this
...

이 예제는 subscription 필터를 반환하거나, 전체 multipart메시지(key와 data포함)를 가져오는 것을 보여 줍니다. 결코, multipart메시지의 일부만은 얻을 수 없습니다.

당신은 다중의 publisher에 가입하고 당신이 그들에게 또 다른 소켓을 통해 데이터를 (그리고 이것은 매우 일반적인 사용 케이스 입니다) 보낼 수 있도록 자신의 ID을 알고 싶다면, 당신은 세 부분으로 메시지를 작성하시면 됩니다. :

fig28.png

(Semi-)Durable Subscribers and High-Water Marks

top prev next

ID(Identities)는 모든 종류의 소켓에서 사용합니다.만약 당신이 PUB과 SUB 소켓을 가지고 있고, subscriber가 publisher에게 자체 ID를 준다면 publisher는 subscriber에게서 데이터를 넘길 때까지 잡고 있습니다.

이것은 동시에 놀랍고 끔찍한 것입니다, 이것이 놀라운 이유는 당신이 연결하고 이것을 수집할 때까지 업데이트가 publisher의 전송버퍼에서 기다릴 수 있다는 것입니다. 끔찍한 것은, 기본적으로 이것은 빠르게 publisher를 죽이고 당신의 시스템을 잠글 수 있다는 것입니다.

당신이 durable subscriber 소켓을 사용한다면(예, 당신이 SUB소켓에 ID를 설정하는 경우), 당신은 반드시 publisher 소켓에 HWM(high-water-mark)를 사용하여 대기열이 넘치는 것을 방지해야 합니다. Publisher의 HWM은 모든 subscriber에 독립적으로 영향을 미칩니다.

만약 당신이 이것을 증명하려면, 1장에서 wuclinet및 wuserver를 가져와서, 그것을 연결하기 전에 wuclient 라인을 추가해야 합니다.

zmq_setsockopt (subscriber, ZMQ_IDENTITY, "Hello", 5);

두 프로그램을 빌드하고 실행해 봅시다. 모두 정상으로 보일 것 입니다. 그러나 publisher가 사용하는 메모리를 주시해 보면, subscriber가 종료되면 Publisher의 메모리가 증가하고 있는 것을 확인할 수 있습니다. 당신이 subscriber를 다시 시작하면 publisher 대기열의 증가가 멈춥니다. 즉시 subscriber가 나가게 되면, 이것은 다시 커지고. 그것은 빠른 속도로 시스템을 압도합니다.

우리는 이것이 어떻게 작동 하는지, 그 다음 적당히 수행하는 방법을 볼 것입니다. 여기 2장에는 동기화 하기 위해 ‘node coordination’ 기술을 사용하는 publisher와 subscriber가 있습니다. Publisher는 매번 1초 기다리면서 10개 메시지를 보냅니다. 당신은 Ctrl-C를 사용하여 subscriber를 죽이기 위해 몇 초 동안 기다리고 재 시작 합니다.

여기 publisher는 다음과 같습니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc

그리고 여기 subscriber가 있습니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc

이것을 실행하려면, 자신의 윈도우에서 publisher를 시작하고, 그 다음 subscriber를 실행시킵니다. subscriber는 하나 또는 두 개의 메시지를 수집하도록 허용한 후 Ctrl-C를 누릅니다. 셋을 세고, 그리고 그것을 다시 시작합니다. 아래처럼 볼 수 있습니다 :

$ durasub
Update 0
Update 1
Update 2
^C
$ durasub
Update 3
Update 4
Update 5
Update 6
Update 7
^C
$ durasub
Update 8
Update 9
END

단지 그 차이를 보기 위해 소켓 ID를 설정한 subscriber의 라인을 막고, 다시 시도합니다. 당신은 메시지를 잃는 것을 볼 수 있습니다. ID를 설정하면 일시적인 subscriber를 지속적인 subscriber로 바뀝니다. 당신은 실제적으로 구성 파일에서 ID를 가져오거나, UUIDs를 생성하고 어딘가에 그들을 저장하는 것을 신중하게 선택할 것입니다.

우리가 PUB소켓에 high-water-mark를 설정하면, publisher는 많은 메시지를 저장하지만, 무한한 것은 아닙니다. 우리가 소켓의 publish 시작 전에, publisher에 HWM = 2로 설정하고 테스트해 봅시다.:

uint64_t hwm = 2;
zmq_setsockopt (publisher, ZMQ_HWM, &hwm, sizeof (hwm));

지금 테스트를 실행하고, 죽이고, 몇 초 후 subscriber를 재 시작하면 아래와 같은 결과를 볼 것입니다.:

$ durasub
Update 0
Update 1
^C
$ durasub
Update 2
Update 3
Update 7
Update 8
Update 9
END

자세히 보세요 : 우리는 기다리는 두 메시지가 있고, 여러 메시지들의 차이, 그리고 다시 새로운 Update들이 있습니다. HWM은 ØMQ가 대기열을 담을 수 없는 메시지를 버리게 하는 원인이 됩니다. ØMQ 매뉴얼 ‘exceptional condition’을 참고하세요.

간단히, subscriber ID를 사용한다면 publisher소켓에 high-water-mark를 설정해야 합니다. 그렇지 않으면 메모리가 부족하고 깨져서 서버가 위험해지게 됩니다. 그러나 다른 방법은 있습니다. ØMQ는 ‘swap’이라 불리는 것을 제공합니다. 이것은 대기열에 저장할 수 없는 메시지를 담는 디스크 파일 입니다. 이것은 적용하기에 매우 간단합니다.

// Specify swap space in bytes
uint64_t swap = 25000000;
zmq_setsockopt (publisher, ZMQ_SWAP, &swap, sizeof (swap));

우리는 속도가 저하되고 차단되고, subscriber가 없어지는 좋지 않은 publisher를 만들기 위해 이것을 넣을 수는 있으며, 여전히 이것이 필요한 곳에 영원한 가입(durable descriptions)을 제공하는 것은 가능합니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc

실제로, HWM=1로 설정하여 디스크에 모든 것은 저장하는 것은 pub-sub시스템을 매우 느리게 할 것입니다. 알 수 없는 subscriber를 감당해야만 하는 publisher를 위해 좀더 합리적인 best practice가 있습니다. :

  • PUB소켓에 항상 HWM을 설정해라. 예상되는 최대 subscriber수, 큐에 할당할 수 있는 메모리 양, 메시지의 평균 사이즈에 기초해서 HWM을 설정 할 수 있습니다. 예를 들어, 5000 subscriber를 예상하고, 가용한 메모리가 1GB이고, 메시지가 ~200bytes라면, 그때 적당한 HWM은 (1000000000 / 200 / 5,000) = 1,000 입니다.
  • 만약 subscriber의 속도가 느리거나 데이터를 잃어 버리는 것을 원하지 않으면, 당신이 커버하려는 최대 메시지 속도, 메시지의 평균 크기와 시간, subscriber의 숫자에 따라 최고점(peek)를 처리하기에 충분한 대형의 SWAP을 설정해야 합니다. 예를 들어, subscriber는 5,000이고, 초당 100,000건씩 ~200 바이트의 메시지가 들어오면, 초당 디스크 공간 100MB까지 해야 합니다. 최대 1 분 정도의 정전을 커버하려면, 따라서 디스크 공간 6GB이 필요할 것이며 다른 이야기 이지만, 그것도 빨리 해야만 할 것 입니다.

Durable subscriber의 주의할 사항 :

  • subscriber가 어떻게 죽는지, updates의 빈도, 네트워크 버퍼의 크기, 사용하는 전송 프로토콜에 따라 데이터가 유실될 수 있습니다. Durable subscriber는 transient한 것 보다 훨씬 더 신뢰성을 가지고 있지만 완벽하지는 않습니다.
  • publisher가 죽고 다시 시작하는 경우 SWAP파일은 복구 할 수 없습니다. 이것은 임시버퍼와 네트워크 I/O 버퍼에 있는 데이터로 잃게 됩니다.

HWM 옵션 사용에 주의할 사항

  • 이것은 단일 소켓의 전송과 수신 모두에 영향을 미칩니다. 일부 소켓(PUB,SUB)는 전송버퍼에만 가집니다. 일부(SUB, PULL, REQ, REP)는 수신버퍼에만 가집니다. 일부(DEALER,ROUTER,PAIR)은 전송/수신버퍼 모두를 가집니다.
  • 당신의 소켓이 high-water-mark에 도달할 경우, 소켓 유형에 따라 대기하거나 데이터를 버릴 것입니다. PUB소켓은 high-water-mark에 도달하면 메시지를 버리고, 다른 소켓은 대기할 것입니다.
  • inproc 경우에는 송신자와 수신자가 같은 버퍼를 공유합니다. 그래서 실제 HWM은 양쪽에 설정한 HWM의 합계입니다. 이것은 한쪽에 HWM을 설정하지 않으면 버퍼크기의 제한이 없다는 것을 의미 합니다.

A Bare Necessity

top prev next

ØMQ는 당신의 상상과 진지함으로 한 조각 한 조각 끼워 넣는 조각상자와 같습니다.

당신이 얻을 확장 가능한 아키텍쳐에 눈을 뜨세요. 당신은 커피 한, 두 잔이 필요할 수 있습니다. 한번 만드는 실수를 한 다음 Entkoffeiniert 라벨이 붙은 이국적인 독일 커피를 구입하지 마세요. 이것이 맛있다는 것은 아닙니다. 확장 가능한 아키텍쳐는 새로운 아이디어가 아닙니다.- flow-based programmingErlang같은 언어는 이미 이와 같이 작동합니다. 그러나 ØMQ는 전에 어떤 것보다도 더 사용하기 쉽게 만들었습니다.

Gonzo Diethelm said 말에 따라, ‘나의 직감은 이 문장으로 요약이 됩니다:” 만약 ØMQ가 존재하지 않는다면 발명이 필요합니다.” 나는 몇 년 동안 구상한 후 ØMQ에 뛰어 들었고, 만들게 되었다는 것을 의미 합니다. ØMQ는 요즘 나에게 생활필수품인 것 같습니다.'

Chapter Three - Advanced Request-Reply Patterns

top prev next

2장에서 우리는 매번 ØMQ새로운 측면을 탐구하면서 작은 응용프로그램들을 개발하여 기본적인 ØMQ를 사용해 봤습니다. 우리는 이 장에서 ØMQ's core request-reply pattern의 최고로 진보된 pattern을 탐구함으로써 이 접근을 계속할 것입니다.

본 장의 내용:

  • request-reply에 대한 메시지 envelop을 생성하고 사용하는 방법
  • REQ, REP, DEALER, ROUTER 소켓들을 사용하는 방법
  • ID를 사용하여 수동으로 reply 주소를 설정하는 방법
  • 사용자 임의의 분산형 라우팅을 수행하는 방법
  • 가장 최근에 사용된 라우팅을 수행하는 방법
  • 상위 수준의 메시지 class를 구축하는 방법
  • 기초적인 request-reply broker를 구축하는 방법
  • 소켓들을 위한 좋은 이름을 선택하는 방법
  • Clients와 workers의 클러스터를 테스트하는 방법
  • request-reply 클러스터의 확장 가능한 클라우드를 구축하는 방법
  • 모니터링 스레드를 위한 pipeline 소켓들을 사용하는 방법

Request-Reply Envelopes

top prev next

request-reply 패턴에서, envelope은 응답에서 응답주소를 가지고 있습니다. 이것은 상태가 없는 ØMQ network가 왕복 요청 응답 envelop을 어떻게 생성하는지 설명합니다.

여러분은 사실 request-reply envelops가 일반적인 경우에 어떻게 사용하여 작업하는지 이해할 필요가 없습니다. 여러분이 REQ,REP를 사용할 때, 소켓은 자동으로 envelop을 만들고 사용합니다. 여러분이 디바이스를 쓸 때(우리가 마지막 장에 이것을 적용을 할 것입니다), 여러분은 메시지의 모든 부분을 읽고 쓰기 해야 합니다. ØMQ는 다중 데이터를 사용하여 envelop을 구현하므로, 여러분이 안전하게 다중 데이터를 복사한다면, 그렇게 암시적으로 envelop을 복사하면 됩니다.

그러나 후드를 얻고 request-reply envelop를 활용하는 것은 진보된 request-reply 작업을 위하여 필요합니다. envelop관점에서 ROUTER가 어떻게 작업하는지 설명하는 시간입니다. :

  • ROUTER socket으로부터 메시지를 받을 때, 이것은 "This came from Lucy." 라는 지울 수 없는 메시지와 낙서 주변의 갈색 종이 봉투로 쓰여져 있습니다. 그리고 여러분에게 전달됩니다. 즉, ROUTER socket은 그것에 대한 응답 주소를 가진 envelop에 싸여 여러분에게 제공합니다.
  • ROUTER socket에게 메시지를 보낼 때, 그것이 갈색 종이 봉투에서 떼어내어, 자체 필적 감정으로 읽으려고 시도하거나, “Lucy”가 누구인지 안다면 Lucy에게 다시 내용을 보냅니다. 이는 메시지 수신의 반대 과정입니다.
  • 만일 여러분 이 갈색봉투를 홀로 보내면, 그 다음 다른 ROUTER socket에게 그 메시지를 전달하고(예, ROUTER에 연결된 DEALER에게 보냄), 두 번째 ROUTER socket은 차례 차례 그 위에 다른 갈색 봉투를 붙일 것이고, 그 위에 DEALER 이름을 적습니다.

요약하면 각 ROUTER는 올바른 목적지로 응답을 보내는 방법을 알고 있습니다. 여러분의 프로그램에서 필요한 것은 envelop을 기다리는 것 입니다. 이제 REP socket은 이해할 수 있을 것입니다. 이것은 조심스럽게 envelop을 열고 하나하나 안전하게 envelop을 옆에 두고 실 메시지는 여러분에게 제공합니다. 응답을 보낼 때 envelop에 응답을 재포장하고 ROUTER socket에게 전달합니다.

request-reply 패턴에 ROUTER-DEALER 디바이스를 넣고자 한다면 다음과 같습니다. :

[REQ] <--> [REP]
[REQ] <--> [ROUTER--DEALER] <--> [REP]
[REQ] <--> [ROUTER--DEALER] <--> [ROUTER--DEALER] <--> [REP]
...etc.

여러분이 REQ socket에서 ROUTER socket으로 접속하고 요청 메시지 하나를 전송하면, 이것이 ROUTER socket으로부터 응답 받은 것입니다. :

fig29.png

잠시 설명하면:

  • 프레임 3의 data는 전송 프로그램이 REQ socket에 송신한 것입니다.
  • 프레임 2의 빈 메시지 부분은 ROUTER socket에 메시지를 보낼 때 REQ socket에의해 prepended 된 것 입니다.
  • 프레임 1의 응답 주소는 수신 프로그램에서 메시지가 지나가기 전에 ROUTER에 의해 prepended 된 것 입니다.

이제 우리가 디바이스를 연결하여 확장한다면 우리는 stack의 처음에 있는 가장 최근의 envelope을 얻게 됩니다. :

fig30.png

이제 여기 request-reply 패턴에서 사용하는 4가지 socket 유형에 대하여 좀 더 자세한 설명을 하겠습니다. :

  • DEALER는 모든 접속된 peers에 메시지를 load balancin하고, fair-queues는 수신한 메시지를 분배합니다. 이것은 PUSH,PULL socket 조합으로 구성 합니다.
  • REQ는 여러분이 보내는 모든 메시지에 빈 메시지 부분을 추가하고 여러분이 받는 각각의 메시지로부터 빈 메시지 부분을 제거합니다. 그것은 엄격하게 송신과 수신을 반복하는 것을 제외하고 DEALER처럼 동작합니다.
  • ROUTER는 어플리케이션에 보내기 전에 수신한 각 메시지의 응답 주소로 envelope을 추가합니다. 이것은 또한 전송한 각 메시지로부터 메시지 처음부분의 envelope을 잘라내고, 메시지가 어디로 가야 되는지 결정할 수 있도록 응답 주소로 이것을 사용합니다.
  • 메시지를 받을 때 REP는 첫 번째 빈 메시지 부분에 모든 메시지 부분을 저장하고, 당신의 응용프로그램에 나머지(데이터)를 전달합니다. 당신은 응답을 보낼 때 REP는 메시지에 저장된 envelop을 추가하고, ROUTER 처럼 동일한 의미로 사용하여 그것을 다시 보내지만 (사실 REP는 ROUTR 위에 구축합니다), REQ와 매치되어야 하며, 엄격한 receive / send 주기로 처리합니다.

REP는 빈 메시지 부분을 가진 envelop 끝이 있어야 합니다. 만일 당신이 연결된 상대의 REQ를 사용하지 않는 경우 당신이 빈 메시지 부분을 직접 추가해야 합니다.

따라서 ROUTER에 대한 명백한 질문은, 그것이 어디로부터 응답 주소를 얻습니까 입니다. 그리고 확실한 대답은, 그것은 소켓의 ID를 사용한다 입니다. 우리는 이미 배운 대로, 소켓은 다른 소켓이 그 소켓과 연관을 맺을 수 있는 임의 ID를 생성하는 경우 는 일시적 일 수 있습니다. 또는, 소켓은 다른 소켓 자체 ID를 명시적으로 보내면 영속적일 수 있으며, ROUTER는 임시 레이블을 생성하는 것 보다는 이것을 사용 할 수 있습니다.

이것은 transient 소켓 입니다. :

fig31.png

이것은 durable 소켓 입니다. :

fig32.png

실제로 두 경우를 관찰합시다. 이 프로그램은 ROUTER 소켓이 하나는 ID를 사용하지 않고, 하나는 “Hello” ID를 사용하는 두 개의 REP 소켓으로부터 받는 메시지 부분의 내용을 보여줍니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Lua | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl

이것은 dump function의 화면입니다. :

----------------------------------------
[017] 00314F043F46C441E28DD0AC54BE8DA727
[000]
[026] ROUTER uses a generated UUID
----------------------------------------
[005] Hello
[000]
[038] ROUTER socket uses REQ's socket identity

Custom Request-Reply Routing

top prev next

우리는 어떤 클라이언트가 응답을 주기 위한 루트를 결정하기 위하여 envelop(message envelope)을 사용하는걸 이미 보았습니다. 이제 다른 방안을 살펴 보겠습니다. : 만약 제대로 작성된 envelop을 통해 바른 라우팅 주소를 제공한다면 ROUTER는 그것에 연결된 모든 peer에 비동기적으로 메시지를 라우팅 할 것 입니다.

그래서 ROUTER는 실제로 완전히 제어 가능한 라우터 입니다. 우리는 자세하게 이 마술을 파악할 것 입니다.

그러나 먼저 우리가 거칠고 포장되지 않은 off-road로 갈 것이기 때문에, REQ와 REP를 좀더 자세히 보도록 하겠습니다. 일부 사람은 알겠지만, 메시징의 유치원 접근에도 불구하고, REQ 와 REP는 실제적으로 화려한 특성이 있습니다. :

  • REQ는 mama SOCKET으로써 응답을 제외하고는 listen하지 않는다.mama는 엄격한 SYNC이며, 요청처리를 합니다.
  • REP는 papa SOCKET으로써 항상 응답을 하지만 절대 대화를 시작하지 않습니다. papa는 엄격한 SYNC이며, 응답처리를 합니다.

우리가 일반적으로 to-and-fro 패턴으로 request-reply을 생각하는 동안에 사실, 이것은 우리가 어떤 mama나 papa가 항상 Sync이고, 중간이 아닌 체인의 끝에 있다는 것을 이해하는 동안 완전한 ASync입니다. 우리가 알아야 할 모든 것은 통신하려는 peer의 주소이며, 그리고 우리는 다음 라우터를 통해 ASync로 메시지를 보낼 수 있습니다. 라우터는 하나이고 오직 ØMQ socket type은 “X에게 이 메시지를 보내”라고 말할 수 있는 능력이 있습니다.

메시지를 보낼 주소를 알 수 있는 방법이 있으며, 대부분의 custom request-reply 라우팅 예제에서 사용되어지는 것을 볼 것입니다:

  • 만약 transient socket(즉 아무 ID도 설정하지 않은)이라면, 라우터는 UUID를 생성할 것이며, 들어오는 요청 envelop을 전달할 때 접속을 참조하기 위하여 사용합니다.
  • 만약 durable socket이라면 라우터는 들어오는 요청 envelop을 전달할 때 peer의 ID를 줄 것입니다.
  • 명시적인 ID를 가진 peer는 다른 소켓을 통하여 다른 메커니즘으로 이것을 송신할 수 있습니다.
  • Peer는 환경설정 파일이나 다른 방법을 통해 각각의 ID에 대한 사전정보를 가질 수 있습니다.

우리는 쉽게 라우터에 접속할 수 있는 각 소켓 형태 하나 마다 적어도 3개의 라우팅 패턴이 있습니다. :

  • Router-to-dealer.
  • Router-to-mama (REQ).
  • Router-to-papa (REP).

이런 각각의 경우 우리가 메시지를 어떻게 라우팅 할지에 대하여 전체적인 제어를 가지고 있지만 다른 패턴에 대하여 다른 사용 사례와 메시지 흐름을 통하여 보완하여야 합니다. 다른 알고리즘의 예제인 다음 섹션을 통해 그것을 분석해 봅시다.

사용자 라우팅에 대한 최초의 몇 가지 경고들 :

  • 이것은 ØMQ룰에 어긋납니다: 소켓에 peer 주소지정을 위임한다. ØMQ가 다양하고 광범위한 라우팅 알고리즘 면에서는 부족하기 때문에 우리가 할 유일한 이유 입니다.
  • ØMQ 향후 버전에는 아마도 우리가 여기서 만들려고 하는 라우팅을 제공할 것입니다. 우리가 지금 설계하는 코드는 향후에 중복되거나 없어질 수 있습니다.
  • 이미 작성된 라우팅은 디바이스에 친화적인 것과 같은 확정성의 보장을 가지고 있으나 사용자 지정 라우팅은 그렇지 못합니다. 여러분은 당신의 디바이스에 맞게 만들어야 합니다.

그래서 사용자 지정 라우팅은 이것을 ØMQ에 위임하는 것 보다 비싸며 깨지기 쉽습니다. 필요할 때만 사용하기 바랍니다. 언급한대로, 물에 뛰어들어야만이 물의 위대함을 알 수 있습니다.

Router-to-Dealer Routing

top prev next

router-to-dealer 패턴은 가장 간단합니다. 여러분은 한 라우터 에서 여러 딜러에 접속하고, 여러분이 선호하는 어떤 알고리즘을 사용하여 딜러들에게 메시지를 분산합니다. 딜러는 sink(응답 없이 메시지 처리)일수 있고 프락시(다른 노드에 메시지 보내기) 혹은 서비스(응답 전송)가 될 수 있습니다.

만약 딜러의 응답을 기대한다면 한 라우터만 그것에게 요청해야 합니다. 딜러는 특정한 peer에 어떻게 응답하는지 모르며, 그래서 그들이 복수의 peers를 가졌다면 예언한 것처럼 그들은 그들간에 load-balance을 할 것입니다. 딜러가 sink이면, 모든 라우터는 그것에 통신할 수 있습니다.

당신은 router-to-dealer 패턴에 어떤 종류의 라우팅을 할 수 있습니까? 만약 딜러가 라우터에게 응답을 했다면, 즉 타스크가 끝났을 때 라우터에게 알려주는 것, 여러분은 딜러가 얼마나 빠른지에 따라 라우팅을 위한 정보로 사용할 수 있습니다. 라우터와 딜러가 모두 ASync이기 때문에 조금 까다로울 수 있습니다. 여러분은 결국 zmq_poll(3)을 사용할 것입니다.

우리는 딜러가 응답하지 않는 예제를 만들 것입니다. 그들은 순수한 sink입니다. 우리의 라우팅 알고리즘은 weighted random scatter가 될 것입니다:우리는 두 개의 딜러를 가지고 다른 쪽에 보내 것보다 한쪽에 2배의 메시지를 보냅니다.

fig33.png

여기 이것이 어떻게 동작하는지 보여주는 코드를 봅시다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc | Perl

이 코드에 대한 몇가지 설명:

  • 라우터는 딜러가 준비 되었는지를 알지 못합니다, 그리고 그렇게 하기 위한 신호를 추가하는 것은 예제에 혼란을 줄 것 입니다. 그래서 라우터는 딜러 스레드가 시작한 후 단지 “sleep(1)”을 합니다. 이 sleep이 없다면 라우터는 라우팅 할 수 없는 메시지를 송신할 것이고 ØMQ는 그것을 버릴 것입니다.
  • 이 문제는 라우터 소켓에만 해당됨을 주의하세요. PUB 소켓은 subscriber가 없다면 메시지를 버릴 것입니다. 그러나 그 외 소켓유형은 메시지를 수신할 peer가 존재할 때까지 메시지를 queue에 송신 합니다.

딜러에 라우팅하기 위해서, 우리는 이처럼 envelope을 생성합니다. :

fig34.png

라우터 소켓은 첫 번째 프레임을 제거하고 딜러가 현재 얻는 두 번째 프레임을 송신합니다. 딜러가 라우터에게 메시지를 송신할 때 하나의 프레임을 송신합니다. 라우터는 딜러의 주소를 매달고 두 개의 부분으로 비슷한 envelop을 보냅니다.

주의할 것들 : 여러분이 잘못된 주소를 사용한다면, 라우터는 조용하게 메시지를 버립니다. 많지 않지만 그것은 유용하게 할 수 있습니다. 정상적인 경우에 이것은 peer를 사라지게 하거나, 프로그램 에러가 어딘가에 있고 잘못된 주소를 사용하고 있다는 의미입니다. 어떤 경우에 당신은 목적지 노드에서 어떤 종류의 응답이든 받을 때까지는 성공적으로 라우트된 메시지를 얻을 수 없다고 가정할 수 없습니다. 우리는 나중에 신뢰할 수 있는 패턴을 만들어 볼 것입니다.

딜러는 사실 정확하게 PUSH 와 PULL 조합으로 동작합니다. 그러나 request-reply소켓에 PULL나 PUSH로 접속하는 것은 무의미합니다.

Least-Recently Used Routing (LRU Pattern)

top prev next

우리가 얘기한 바와 같이 MAMAS(REQ 소켓)는 당신 말을 듣지 안으며 당신이 돌려 말하려고 하면 무시할 것입니다. 여러분은 어떤 것을 얘기하기 위해 기다려야만 하고, 그 다음 당신은 비꼬는 대답을 줄 수 있습니다. 그것은 우리가 답변을 기다리는 MAMAS들을 유지할 수 있다는 의미이기 때문에 라우팅에 매우 유용합니다. 효과적으로, 그들이 준비 되었을 때 MAMAS는 우리에게 알려줍니다.

당신은 여러 MAMAS에 하나의 라우터를 접속할 수 있고 딜러에게 메시지를 분산할 수 있습니다. MAMAS는 보편적으로 응답하기를 원할 것이지만, 그들은 당신에게 한번에 마지막 단어 하나만을 갖게 할 것입니다. :

  • Mama speaks to router
  • Router replies to mama
  • Mama speaks to router
  • Router replies to mama
  • etc.

딜러와 같이, MAMAS는 한 라우터와 통신할 수 있으며, MAMAS는 항상 라우터와 통신하는 것으로 시작합니다, 당신은 multi-pathway redundant routing과 같이 기발하지 하지 않다면 여러 개의 라우터에 하나 이상의 MAMA 접속을 결코 하지 않아야 합니다. 나는 지금 그것을 설명하려 하지 않으며, 다행히 JARGON은 당신이 필요할 때까지의 노력을 기울이는 것을 그만두게 할 만큼 충분히 복잡합니다.

fig35.png

여러분이 router-to-mama패턴으로 할 수 있는 라우팅의 종류는 무엇입니까? 아마도 가장 명백한 것은 우리가 항상 가장 오랬동안 기다려 왔던 mama에게 라우팅하는 “least-recently-used”(LRU)입니다. 여기 한 세트의 mama에게 LRU 라우팅을 하는 예제가 있습니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Lua | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl

이 예제에서, 우리는 어떤 것으로 workers와 동기화할 필요가 없기 때문에 LRU는 ØMQ가 우리에게 무엇을 주는 것 이상으로 어떤 정형화된 데이터 구조가 필요하지 않습니다. 좀 더 현실적인 LRU 알고리즘은 큐에 준비된 만큼 worker가 수집해야만 하고, 이것은 클라이언트 요청을 라우팅 할 때 사용합니다. 우리는 나중 예제에서 이것을 할 것입니다.

LRU가 예상한 것과 같이 동작하는지 증명하기 위해, mama는 그들이 하는 전체 테스크를 출력합니다. mama가 임의의 동작 수행하는 이래로, 우리는 load balancin을 하지 않습니다. 우리는 각각의 mama가 임의의 변수를 가졌지만 같은 합계에 근접할 것으로 기대합니다. 이것이 결과 입니다. :

Processed: 8 tasks
Processed: 8 tasks
Processed: 11 tasks
Processed: 7 tasks
Processed: 9 tasks
Processed: 11 tasks
Processed: 14 tasks
Processed: 11 tasks
Processed: 11 tasks
Processed: 10 tasks

위 코드에대한 설명

  • 모두 준비되었을 때 MAMAS가 router에게 명시적으로 통신하는 이래로, 어떤 정해진 시간이 필요하지 않습니다.
  • 우리는 zhelpers.h의 s_set_id를 사용하여 출력 가능한 문자열로 우리의 ID를 생성합니다. 그것은 우리의 인생을 좀더 간단하게 만듭니다. 실제 application에서 MAMAS는 완전히 익명이 될 것이고 여러분은 오직 문자열로만 제어할 수 있는 zhelpers의 s_recv()와 s_send() function 대신에 직접적으로 zmq_recv(3)zmq_send(3)를 호출합니다.
  • 더 나쁜것은, 임의의 ID를 사용하는 것입니다. 실제 코드에서 이와 같이 하지 말기 바랍니다. 무작위로 영속적인 소켓은 실제 상황에서는 좋지 않습니다. 그들은 소모되어 지고 결국 node는 죽일 것입니다.
  • 이해하지 못하고 예제 코드를 복사, 붙이기 한다면 무슨 가치가 있겠는가!. 그것은 스파이더맨이 지붕 위를 점프하는 것을 보는 것과 같을 당신이 그것을 시도하는 것과 같습니다.

mama에게 라우팅하기 위하여 우리는 이처럼 mama-friendly한 envelop을 만들어야 한다. :

fig36.png

Address-based Routing

top prev next

Papas는 우리가 그들에게 관심이 있다면, 거기에 대한 대답만 할뿐 입니다. 그리고 mama는 정비소에 차를 몰고 가서 고치고 청구서를 지불하며, 비가 올 때 개와 산책합니다. 그러나 이것과는 대조적으로 Papas는 단지 질문에 대한 대답만 합니다.

전통적인 request-reply패턴에서 라우터는 전혀 papa 소켓과 통신하지 않지만, 오히려 그것을 위한 일을 할 딜러를 얻습니다. 그래서 딜러가 하는 것이 이것입니다:임의의 papa에게 질문을 전달하고 그들의 답변을 돌려 받는다. 라우터는 전체적으로 mama에게 통신하기에 좀더 안정적입니다. 됐습니다. 여러분, 정신분석을 멈추십시오. 인생 이야기는 아니지만 유사함이 있습니다.

이것은 고전적 패턴으로 가장 잘 동작하는 ØMQ로 기억할 만한 가치가 있습니다, 우리가 절벽에서 떨어지고 좀비에게 먹히는 위험을 감수하고 비포장 도로로 가듯 하나씩 밟아가는 이유가 있습니다. 말하자면, 라우터에 papa를 적용하고, 어떻게 되는지 보겠습니다.

농담은 그만하고, papas에 대한 특별한 것은 실제로 두 가지가 있습니다. :

  • 첫째, 그들은 엄격하게 융통성 없는 request-reply 입니다.
  • 둘째, 그들은 어떤 크기의 envelope을 받고, 원본을 그대로 리턴 할 것입니다.

정상 request-reply패턴에서 papa는 익명이고 교체 가능합니다(와우! 이런 유추는 겁납니다)그러나 우리는 사용자 지정 라우팅에 대하여 배우고 있습니다. 그래서 이 경우에 papa B보다 papa A에 요청을 보낼 이유를 가지고 있습니다. 큰 네트워크의 한쪽 끝에서 여러분간에 어떤 종류의 대화를 유지하기 원한다면 이것을 필수입니다. 그리고 papa는 멀리 어딘가에 자리 잡고 있습니다.

ØMQ의 핵심 철학은 끝은 똑똑하고 많으며 중간은 광대하고 광활하다 입니다. 이것은 끝은 각자 서로 주소를 가질 수 있음을 뜻하고 그리고 이것은 또한 우리가 주어진 papa에 도달하는 방법을 알기 원한다는 것을 뜻합니다. 여러 hop을 통과하여 라우팅 하는 것은 우리가 나중에 살펴볼 것입니다. 그러나 우리는 지금 마지막 단계에서 볼 것은 라우터가 특정papa와 통신하는 것입니다. :

fig37.png

이 예제는 매우 특별한 이벤트 고리를 보여줍니다. :

  • 클라이언트는 어떤 노드에 라우팅 할 메시지를 가지고 있습니다.그 메시지는 empty part과 body의 두 주소를 가집니다.
  • 클라이언트는 그것을 라우터에게 전달하지만 먼저 papa 주소를 명시한다.
  • 라우터는 papa가 메시지 보내기를 결정하는데 사용되는 papa 주소를 삭제합니다.
  • Papa는 addresses, empty part, body를 수신합니다.
  • 그것은 주소를 삭제하고 그것은 저장하고 worker에게 body를 전달합니다.
  • Worker는 papa에게 응답을 송신합니다.
  • Papa는 envelop 스택을 재생성하고 그것을 worker의 응답과 함께 라우터에게 보냅니다.
  • 라우터는 papa의 주소를 추가하고 주소 스택의 나머지,empty part,그리고 body과 함께 클라이언트에 보냅니다.

그것은 복잡하지만 당신이 이해할 만한 가치가 있습니다. 단지 papa는 GIGO(garbage in, garbage out) 라는 것을 기억하세요.


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Lua | PHP | Python | Ruby | Scala | Ada | Basic | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl

프로그램을 실행결과 입니다. :

----------------------------------------
[020] This is the workload
----------------------------------------
[001] A
[009] address 3
[009] address 2
[009] address 1
[000]
[017] This is the reply

이 코드에 대한 몇 가지 설명 입니다. :

  • 실제로 우리는 구분된 노드에 papa와 라우터를 가집니다. 이 예제는 이벤트의 순차를 만들기 때문에 하나의 스레드로 되어 있습니다.
  • zmq_connect(3)는 즉시 일어나지 않습니다. Papa 소켓이 라우터에 접속할 때 얼마의 시간이 걸리고 이것은 background로 처리됩니다. 실제 application에서 라우터는 통신이 이루어져야 papa의 존재를 알게 됩니다. 예제에서 우리는 접속을 정확히 하기 위하여 sleep(1)을 사용할 것입니다. Sleep을 제거한다면 papa 소켓은 메시지를 얻지 못할 것 입니다. (한번 해보세요)
  • 우리는 papa의 ID를 사용하여 라우팅 합니다. 이것이 실제로 그런지 검증하기 위해 “B”와 같이 잘못된 주소를 전송해 보세요. Papa는 메시지를 얻지 못할 것입니다.
  • S_dump와 다른 utility function은 zhelpers.h 헤더 파일에 있습니다. 그리고 ØMQ API외에 개발 할 수 있는 흥미로운 레이어가 있습니다. 우리는 나중에 이런 장난감 예제보다 실제 application을 만들 때 다룰 것입니다.

papa에게 라우팅 하기 위해, 우리는 이와 같은 papa-friendly envelop을 만들어야 합니다. :

fig38.png

A Request-Reply Message Broker

top prev next

우리는 지금까지 ØMQ 메시지 envolopes와 함께 다루었던 내용의 개요를 언급하고, message broker라고 불릴 수 있는 일반적인 custom routing queue의 핵심을 만들 것입니다. 전문용어를 사용해서 미안합니다. 우리가 만들 것은 clients와 workers를 연결하는 queue device입니다. 그리고 여러분이 원하는 라우팅 알고리즘을 사용해 봅시다. 우리가 할 것은 least-recently used입니다.

먼저, 고전적인 request-reply패턴을 다시 봅시다. 그리고 크고 큰 service-oriented network를 통해 확장하는 방법을 살펴보도록 하겠습니다. :

fig39.png

이것은 다중 papas로 뻗어 있지만, 만약 다중 mamas를 처리하기를 원한다면 우리는 중간에 device가 필요합니다. 이것은 가능한 빠르게 두 소켓 사이에 메시지를 복사하는 전통적인 ZMQ_QUEUE device에 의해 연결되는 라우터와 뒤에 잇따른 딜러로 구성됩니다. :

fig40.png

여기서 핵심은 라우터가 요청 envelope에 있는 원래 mama주소를 저장합니다, 딜러와 papa는 그것을 건드리지 않습니다. 그래서 라우터는 응답을 보낼 mama를 압니다. Papas는 anonymous이며, 이 패턴에서 주소를 사용하지 않습니다, 모든 ,papas는 동일한 서비스를 제공한다고 가정합니다.

fig41.png

우리 브로커(a router-to-router LRU queue)는 메시지 일부를 맹목적으로 복사할 수 없습니다. 아래에 코드가 있으며, 여기 핵심로직은 매우 복잡하지만 핵심 논리 LRU 라우팅을 수행하기를 원하는 어떤 request-reply broker에 재사용 할 수 있습니다.

이 프로그램의 어려운 부분은 (A)각 소켓이 읽고 쓰는 envelopes와 LRU알고리즘 입니다. 우리는 순서대로 이것들을 다룰 것입니다. 메시지 envelope 포맷을 시작하겠습니다.

첫째, mama REQ 소켓은 항상 보낼 때 empty part(the envelope delimiter)를 넣고 받을 때 empty part를 제거한다는 것을 기억하세요. 이에 대한 이유는 중요하지 않습니다, 정상적은 request-reply패턴의 일부입니다.
여기서 주의할 것은 mama가 원하는 것을 할 수 있도록 하는 것입니다. 둘째, 라우터는 메시지가 온 주소로 envelope을 추가합니다.

우리는 지금 클라이언트에서 노동자까지의 완전한 request-reply 연결을 통하게 할 수 있습니다. 코드에서 우리가 원한다면 쉽게 메시지 프레임을 인쇄할 수 있도록 클라이언트와 노동자 소켓의 ID을 설정합니다. 가자는 클라이언트의 ID가 "CLIENT"이고 노동자의 ID은 "WORKER"라고 가정합니다. 클라이언트는 하나의 프레임을 보냅니다. :


C++ | C# | Clojure | CL | Erlang | F# | Haxe | Java | Lua | PHP | Python | Scala | Ada | Basic | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby
fig42.png

라우터 프론트엔드 소켓에서 읽을 때 큐에서 가져온 것 입니다. :

fig43.png

Broker는 LRU queue에서 가져와 worker의 주소와 empty part를 앞에 붙이고 끝의 mama는 유지하여 worker에게 보냅니다. :

fig44.png

이 복잡 한 큐 스택은 첫 번째 프레임을 제거 하는 백 엔드 라우터 소켓에 의해 처리됩니다. 다음 작업자에 있는 mama 소켓은 빈 부분을 제거 하고 작업자에게 나머지를 제공 합니다. :

fig45.png

이것은 큐가 frontend router socket 자체에서 받은 것과 정확히 동일한 것입니다. Worker는 envelope을 저장하고(empty part를 포함한 전체) 데이터 부분을 가지고 필요한 작업을 합니다.

반환될 때 메시지는 그들이 받은 것과 동일합니다, 즉 backend소켓은 5개 부분으로 된 메시지를 큐에 주고, 큐는 3부분으로 된 메시지를 frontend 소켓에 보내고, client는 한 부분으로 된 메시지를 받습니다.

이제 LRU알고리즘을 살펴 봅시다. Clients와 workers는 mama소켓을 사용해야 하고, workers는 정확하게 그들이 수신한 메시지에 envelops을 저장하고 재 처리해야 합니다. 이 알고리즘은 다음과 같습니다.

  • 항상 backend와 가용한 한 개 이상의 worker가 있는 frontend를 폴링하는 pollset을 만듭니다.
  • 무제한 타임아웃으로 처리하도록 폴링 합니다.
  • backend 처리에서는 “ready”메시지나 client를 위한 응답을 가집니다. 이 두 경우에 우리는 LRU queue에 worker주소(처음부분)를 저장하며, 만약 마지막 부분에 client응답이 있다면 우리는 frontend로 그것을 되돌려 보냅니다.
  • frontend처리에서 우리는 client 요청을 가져오고 가장 최근에 사용된 다음 worker를 팝업하고, backend에 요청을 보냅니다. 이것은 worker주소, empty part 그리고 client요청의 3 부분을 보낸다는 의미입니다.

여러분은 worker가 초기 ‘ready’메시지에서 제공하는 정보를 기반으로 하는 변화로 LRU알고리즘을 재사용하고 확장할 수 있게 되었습니다. 예를 들어, worker는 시작하고 자체 테스트를 수행한 다음, 그들이 얼마나 빠른지 broker에게 알려줍니다. Broker는 LRU나 round-robin보다는 가용한 가장 빠른 worker를 선택할 수 있습니다.

A High-Level API for ØMQ

top prev next

기본 ØMQ API를 사용하여 다중 메시지를 읽고 쓰는 것은 이쑤시개를 사용하여, 프라이드 치킨과 여분의 야채와, 뜨거운 국수 국물 한 그릇을 먹는 것과 같습니다. :

while (1) {
// Read and save all frames until we get an empty frame
// In this example there is only 1 but it could be more
char *address = s_recv (worker);
char *empty = s_recv (worker);
assert (*empty == 0);
free (empty);

// Get request, send reply
char *request = s_recv (worker);
printf ("Worker: %s\n", request);
free (request);

s_sendmore (worker, address);
s_sendmore (worker, "");
s_send (worker, "OK");
free (address);
}

이 코드는 단지 한 envelope을 사용할 수 있기 때문에, 재사용할 수 없습니다. 이 코드는 이미 ØMQ API로 랩핑(wrapping) 했습니다. 만약 우리가 직접적으로 libzmq API를 사용했다면 아래와 같이 될 것입니다. :

while (1) {
// Read and save all frames until we get an empty frame
// In this example there is only 1 but it could be more
zmq_msg_t address;
zmq_msg_init (&address);
zmq_recv (worker, &address, 0);

zmq_msg_t empty;
zmq_msg_init (&empty);
zmq_recv (worker, &empty, 0);

// Get request, send reply
zmq_msg_t payload;
zmq_msg_init (&payload);
zmq_recv (worker, &payload, 0);

int char_nbr;
printf ("Worker: ");
for (char_nbr = 0; char_nbr < zmq_msg_size (&payload); char_nbr++)
printf ("%c", *(char *) (zmq_msg_data (&payload) + char_nbr));
printf ("\n");

zmq_msg_init_size (&payload, 2);
memcpy (zmq_msg_data (&payload), "OK", 2);

zmq_send (worker, &address, ZMQ_SNDMORE);
zmq_close (&address);
zmq_send (worker, &empty, ZMQ_SNDMORE);
zmq_close (&empty);
zmq_send (worker, &payload, 0);
zmq_close (&payload);
}

우리가 원하는 것은 우리 모든 envelopes을 포함 하여 한 번에 전체 메시지를 보내고 받을 수 있는 API이며, 코드라인을 최소화 하고 싶어합니다. ØMQ 핵심 API들은 이것을 목표로 하지 않지만, 그 위에 layer를 만드는 것을 막지 않으며, 지능적인 ØMQ를 사용하는 학습부분은 정확하게 도움이 될 것입니다.

좋은 메시지 API를 만드는 것은 상당히 까다로우며, 만약 너무 많이 데이터를 복사하는 것을 피하려고 한다면 특히 더 그렇습니다. 우리는 기술적인 문제를 가지고 있습니다 : ØMQ는 다중 메시지와 메시지의 개별적인 부분을 모두 설명하기 위해 ‘message’를 사용합니다. 우리는 의미론적인 문제가 있습니다 : 가끔은 2진 blob으로 때로는 출력 가능한 문자열 데이터로써 메시지 내용을 보는 것은 자연스러운 것입니다.

그래서 하나의 솔루션은 세가지 개념을 사용하려 합니다 : string (이미 s_send 및 s_recv를 위한 기반), frame (메시지 부분) 및 message (하나 이상의 frame 목록). 이러한 개념을 사용하여 API를 다시 작성한 코드는 다음과 같습니다. :

while (1) {
zmsg_t *zmsg = zmsg_recv (worker);
zframe_print (zmsg_last (zmsg), "Worker: ");
zframe_reset (zmsg_last (zmsg), "OK", 2);
zmsg_send (&zmsg, worker);
}

22라인을 교체하면 결과가 이해하기 쉽고 읽기 쉽기 때문에 좋습니다. 우리는 ØMQ를 가지고 다른 측면에서의 접근도 이 프로세스로 계속할 수 있습니다. 우리가 만들고 싶어 하는 높은 수준의 API 를 만들어 봅시다.

  • Automatic handling of sockets. 소켓을 수동을 닫으려면 정말 성가신 일이며, 모든 경우는 아니지만 일부 시간이 걸리는 타임아웃을 설정해야 합니다. Context를 닫을 때 자동으로 소켓이 닫게 하는 방법을 가지는 것이 가장 좋습니다.
  • Portable thread management. 모든 잘 만들어진 ØMQ어플리케이션은 스레드를 사용지만, POSIX 스레드는 portable하지 않습니다. 그래서 높은 수준의 API는 portable layer에 이것을 숨기려 합니다.
  • Portable clocks. 심지어 millisecond단위로 시간처리를 하거나, millisecond단위로 sleeping을 하는 것은 portable하지 않습니다. 현실적인 ØMQ 응용 프로그램은 portable clocks이 필요하며, 그래서 우리 API는 이것을 제공합니다.
  • A reactor to replace zmq_poll(3). Poll 루프는 좀 어색하지만 간단합니다. 이것을 많이 사용하면, 우리는 반복해서 결국 동일한 작업을 하게 됩니다 : 시간을 계산하고, 소켓이 준비되었을 때 코드를 호출합니다. Reactor와 timer는 많은 반복된 작업을 줄여 줍니다.
  • Proper handling of Ctrl-C. 우리는 이미 인터럽트를 얻는 방법을 보았습니다. 모든 응용프로그램에서 이것이 발생한다면 유용할 것입니다.

위 리스트는 ØMQ에서 높은 수준의 C API인 czmq로 제공합니다. 사실이 높은 수준의 바인딩은 이전 버전의 가이드에서 개발했습니다.

czmq를 사용하여 재작성한 LRU queue broker 입니다. :


C# | Haxe | Lua | PHP | Python | Scala | Ada | Basic | C++ | Clojure | CL | Erlang | F# | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl | Ruby

czmq에서 제공하는 한가지는 완벽한 인터럽트를 처리한다는 것입니다. 이것은 Ctrl-C가 리턴코드 -1과 에러번호를 EINTR으로 설정하여 종료하도록 어떤 blocking ØMQ를 발생시키게 한다는 의미 입니다. czmq의 recv메소드는 이러한 경우에 NULL을 리턴할 것이며, 여러분은 아래처럼 루프를 깔끔하게 빠져나올 수 있습니다. :

while (1) {
zstr_send (client, "HELLO");
char *reply = zstr_recv (client);
if (!reply)
break; // Interrupted
printf ("Client: %s\n", reply);
free (reply);
sleep (1);
}

혹은, zmq_poll을 사용할 거라면, 반환코드를 확인해 보세요. :

int rc = zmq_poll (items, zlist_size (workers)? 2: 1, -1);
if (rc == -1)
break; // Interrupted

이전 예제는 여전히 zmq_poll(3)를 사용합니다. Reactors는 어떻습니까? czmq zloop reactor는 간단하지만 실용적입니다.

  • 소켓에서 수신자를 설정, 즉 소켓에 수신메시지가 있을 때마다 호출되는 코드입니다.
  • 소켓에 수신자를 제거 합니다.
  • 한번 또는 특정 간격으로 여러 번 처리하는 타이머를 설정합니다.

물론 zloop 내부적으로 zmq_poll(3)를 사용합니다. 이것은 수신자를 추가하거나 제거할 때마다 poll을 설정하여 재빌드하며, 이것은 다음 타이머와 일치시키기 위해 poll 타임아웃을 계산합니다. 다음은 주의가 필요한 각 소켓과 타이머를 위하여 수신자와 타이머 핸들러를 호출합니다.

우리가 reactor패턴을 사용할 때, 우리의 코드는 내부에 드러납니다. 주요 로직은 다음과 같습니다. :

zloop_t *reactor = zloop_new ();
zloop_reader (reactor, self->backend, s_handle_backend, self);
zloop_start (reactor);
zloop_destroy (&reactor);

메시지의 실재처리는 전용 함수나 메소드 내부에 있습니다. 당신은 이렇게 하지 않아도 되며, 취향 문제 입니다. 좀 도움이 될 만한 것은 타이머와 소켓의 처리를 합치는 것입니다. 본 글의 마지막에 우리는 간단한 경우의 zmq_poll(3)와 좀더 복잡한 예제에 zloop을 사용할 것입니다.

이것은 LRU queue broker를 다시 한번 재 작성한 것이며, 이번에는 zloop를 사용합니다. :


Haxe | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

여러분이 Ctrl-C를 눌렀을 경우 적당하게 shut-down하는 어플리케이션을 만들어 봅시다. 만약 여러분이 zctx클래스를 사용한다면 자동적으로 신호처리를 설정하겠지만, 여러분의 코드는 좀 수정이 필요합니다. 여러분은 zmq_poll이 -1을 리턴 하거나 어떤 recv메소드((zstr_recv, zframe_recv, zmsg_recv)가 NULL을 리턴 한다면 루프를 빠져 나와야 합니다. 만약 여러분이 중첩 루프를 가지고 있다면 !zctx_interrupted으로 빠져나올 수 있는 상태변수를 만드는 것이 유용할 수 있습니다.

Asynchronous Client-Server

top prev next

router-to-dealer 예제에서 우리는 한 클라이언트가 비동기적으로 여러 클라이언트와 통신하는1:N을 사용하는 케이스를 보았습니다. 우리는 여러 클라이언트가 하나의 서버와 통신하는 매우 유용한 N:1 구조를 가지기 위해 위와 아래를 바꿀 수 있으며, 비동기적으로 수행할 수 있습니다. :

fig46.png

이것은 아래와 같이 작동합니다. :

  • 클라이언트가 서버에 연결 하고 요청합니다.
  • 각 요청에 대해 서버는 N개 회신에 0을 보냅니다.
  • 클라이언트는 응답을 기다리지 않고 여러 요청을 보낼 수 있습니다.
  • 서버는 새 요청을 기다리지 않고 여러 개의 회신을 보낼 수 있습니다.

어떻게 이 일을 하는지 보여주는 코드가 있습니다:


C# | Clojure | Erlang | F# | Haxe | Lua | PHP | Python | Scala | Ada | Basic | C++ | CL | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl | Ruby

그냥 그 자체로 그 예를 실행 합니다. 다른 멀티 작업 예제와 마찬가지로, 하나의 프로세스에서 실행하지만, 각 작업 자체 컨텍스트와 개념적으로 별도의 프로세스 역할을 하고 있습니다. 당신은 서버로부터 회신하여 출력한 세개의 클라이언트( 각 임의의 ID )를 볼 수 있을 것입니다. 주의해서 보면, 당신은 요청에 대한 0 또는 더많은 회신을 가져오는 다른 클라이언트의 작업을 보게 될 것입니다.

이 코드에 대 한 몇 가지 의견입니다. :

  • 클라이언트는 초당 한번 요청을 보내고, ‘0’ 이나 많은 응답을 얻습니다. zmq_poll(3)을 사용하여 이 작업을 확인하기 위해, 우리는 단순히 1초 시간제한으로 poll할 수 없거나, 우리가 마지막 응답을 받은 단 1초 후 새로운 요청을 보내는 것을 끝내는 것이 좋습니다. 그래서 우리는 어느정도 정확한 빠른 주기로 polling(100 times at 1/100th of a second per poll) 합니다. 이것은 서버가 heartbeat과 같은 형태의 요청, 즉 클라이언트가 현재 연결이나 연결이 끊어진 것을 감지할 때 사용할 수 있음을 의미합니다.
  • 서버는 하나의 요청을 동기적으로 각각 처리(processing) 하는 작업자 스레드 poll을 사용 합니다. 그것은 내부 큐를 사용하여 frontend socket에 이것을 연결합니다. 이것을 디버그하기 위해 코드는 자체 큐 장치에 로직을 구현합니다. C 코드에서는 디버깅을 하기 위해 zmsg_dump()의 주석처리를 해제 할 수 있습니다.

서버의 소켓 로직은 상당히 훌륭합니다. 이것은 서버의 세부 아키텍처입니다 :

fig47.png

우리는 클라이언트와 서버 사이에 dealer-to-router 가 있지만, 내부적으로 서버의 메인 스레드와 worker는 dealer-to-dealer 입니다. 만약 worker가 sync처리를 원한다면, REP를 사용해도 됩니다. 그러나 다중 응답을 보내기를 원한다면, 우리는 aync소켓을 사용해야 합니다.

라우팅 envelope에 대해서 생각해 보겠습니다. 클라이언트는 간단한 메시지를 보냅니다. 서버 스레드는 두 부분으로된 메시지(client ID + 실제 메시지)를 받습니다. 우리는 server-to-worker연결을 위한 두 가지 가능한 설계를 가집니다.

  • Worker가 주소가 없는 메시지를 받으면, 우리는 확실하게 서버 스레드에서 worker스레드까지 router소켓을 사용하여 연결을 관리합니다. 이것은 worker에게 요청을 라우팅할 수 있는 존재하는 서버과 통신하는 것으로 worker가 시작되는 것이 필요합니다. 이것은 이미 LRU패턴에 포함되어 있습니다.
  • Worker가 주소가 있는 메시지를 받으면, 이것은 주소가 있는 응답을 되돌려 줍니다. 이것은 어떤 다른 메커니즘이 필요한 것은 아니지만 worker는 envelope을 편집하는 것이 필요합니다.

두 번째 디자인은 훨씬 간단하며, 다음과 같습니다. :

     client          server       frontend       worker
   [ DEALER ]<---->[ ROUTER <----> DEALER <----> DEALER ]
             1 part         2 parts       2 parts

클라이언트와 안정적인 통신을 유지하는 서버를 만들 때, 우리는 한 전통적인 문제에 부딪칩니다. 만약 서버가 클라이언트마다 몇 가지 상태를 유지하고 클라이언트도 주고받는 것을 유지한다면, 결국 자원을 다 써버리게 될 것입니다. 심지어 동일한 클라이언트가 연결을 유지하고, 여러분이 transient 소켓(명시적인 ID가 없는)을 사용한다면, 각각의 연결은 새로운 것처럼 보일 것입니다.

우리는 매우 짧은 시간( 요청을 처리하기 위해 worker가 걸리는 시간) 동안 단지 상태를 유지하고 그 다음에 상태를 버리는 예제를 위에서 보았지만, 여러 경우에 사용할 만큼 실용적 이진 않습니다.

안정적인 ASync서버에서 적정수준의 client상태를 유지하기 위해서 필요 사항 :

  • server에서 client로 heartbeating합니다. 이 예제에서 우리는 살아있는지 확인 할 수 있도록 1초마다 한번씩 요청을 보냅니다.
  • Key로써 client ID를 사용하여 상태를 저장합니다. 이것은 duralbe과 transient 소켓 둘 다 작동합니다.
  • 클라이언트의 서버가 죽었는지 감지합니다. 만약에 신간에 2초간 응답이 없으면, 서버는 감지를 할 수 있고, 가지고있던 클라이언트의 상태를 없앨 수 있습니다.

Worked Example: Inter-Broker Routing

top prev next

우리가 지금까지 본 것들을 이해하고 확장 합니다. 우리의 클라이언트는 대단위 클라우드 컴퓨팅 환경의 설계를 가져야 합니다.클라이언트는 클라이언트의 클러스터와 worker들이 전체가 하나로 동작하는 최신 데이터 센터의 클라우드 비전을 가지고 있습니다.

우리는 연습이 최선의 방법임을 알기에 ØMQ를 사용하여 동작 시뮬레이션 만들기를 제안합니다. 우리 고객은 사장의 생각을 바꾸기 전에 예산을 삭감하기를 원하고 Twitter에서 ØMQ에 대한 중요한 내용들을 보고 있습니다.

Establishing the Details

top prev next

우리는 코드 제작을 시작하려 하지만, 놀라운 솔루션을 만들기 전에 더 자세하게 알고자 우리에게 말하는 작은 소리가 들립니다. " Cloud는 어떤 종류의 일을 합니까?" 클라이언트는 설명을 해줍니다. :

  • worker는 다양한 종류의 하드웨어에서 동작하며, 어떤 처리도 할 수 있어야 합니다. 클러스터당 수백 개의 worker가 있고 무려 총 12 클러스터가 존재합니다.
  • 클라이언트는 worker를 위한 임무를 생성한다. 각각의 임무는 독립적인 일의 단위이고 모든 클라이언트는 가용한 worker를 찾기 원하고 가능한 빨리 임무를 전달합니다. 많은 클라이언트가 있고 그들은 자유롭게 왕래할 것입니다.
  • 실질적인 어려움은 언제나 클러스터를 추가하고 삭제할 수 있게 만드는 것입니다. 클러스터는 모든 worker와 클라이언트를 포함하여 cloud에 즉시 추가되거나 제외될 수 있습니다.
  • 그들의 클러스터에 worker가 없다면, 클라이언트가 만든 임무는 cloud의 다른 가용한 worker에게 전달될 것입니다.
  • 클라이언트는 한번에 하나의 임무를 송신하고 응답을 대기 한다. 정해진 시간 안에 응답을 받지 못한다면 또다시 임무를 전송할 것입니다.이것은 우리의 관심사가 아니며 클라이언트 API가 이미 구현하고 있습니다.
  • worker는 한번에 하나의 임무를 처리합니다. 오류가 발생하면 스크립트에 의하여 재 시작 됩니다.

이상의 내용을 정확하게 이해하기 위하여 다시 점검을 합니다. :

  • "클러스터 사이에 몇 가지 종류의 최고 network가 연결되어 있을 것입니다. 맞나요?", 그럼 클라이언트는 대답합니다. "예,물론 입니다.우리는 바보가 아닙니다."
  • -”어는 정도의 용량입니까?” 질문을 하면 클라이언트는 대답합니다.” 클러스터 당 천 개 이상의 클라이언트가 최대로 일을 하고 있습니다.초당 10개의 요청입니다.요청과 응답은 작은 사이즈 이며 1kbyte를 넘지 않습니다.
  • 그래서 우리는 간단한 계산으로 TCP상에서 훌륭히 동작할 것이라고 확인할 수 있습니다. 2,500 clients x 10/second x 1,000 bytes x 2 directions = 50MB/sec or 400Mb/sec, 1Gb network에서 문제 없습니다.

그것은 최신 기계나 프로토콜은 필요 없고, 단지 현명한 라우팅 알고리즘과 주의 깊은 디자인을 요구하는 간단한 문제입니다. 우리는 하나의 클러스터를 설계하는 것에서 시작하여 여러 클러스터를 함께 연결하는 방법을 파악할 것입니다.

Architecture of a Single Cluster

top prev next

worker와 클라이언트는 동기적입니다.우리는 worker에 임무를 배정하기 위하여 LRU 패턴을 사용하기 원합니다.worker는 모두 동일합니다. worker는 익명이며 클라이언트는 그들을 직접적으로 지정할 수 없습니다. 우리는 여기에서 전달보장, 재시도, 등등을 제공하지 않습니다.

우리가 이미 보았던 것과 같이 클라이언트와 worker는 직접 서로간의 통신을 하지 않습니다. 그것은 노드를 동적으로 추가하거나 삭제가 불가능하게 만듭니다. 그래서 우리의 기본 모델은 우리가 이미 보았던 reqest-reply message broker로 구성되어 있습니다. :

fig48.png

Scaling to Multiple Clusters

top prev next

이제 우리는 하나 이상의 클러스터에 대해 봅시다. 각 클러스터는 클라이언트와 worker로 구성되어 있고 이들은 borker로 연결되어 있습니다. :

fig49.png

질문 : 우리는 각각 클러스터의 클라이언트가 다른 클러스터의 worker에게 어떻게 얘기하는지 알 수 있습니까? 찬반 양론 각각에 작은 가능성이 있습니다. :

  • 클라이언트는 양쪽 브로커에 직접 접속할 수 있습니다. 우리는 브로커와 worker를 수정할 필요가 없다는 이점이 있습니다. 그러나 클라이언트는 더 복잡해 지고 전체적인 포톨로지로 인식됩니다. 세 번째 또는 네 번째 클러스터를 추가하기를 원한다면 모든 클라이언트는 영향을 받습니다. 사실상 우리는 클라이언트의 라우팅 로직, failover 로직을 옮겨야 하고 이는 옳은 방법이 아닙니다.
  • worker는 양쪽 브로커에 직접 접속할 수 있습니다. 그러나 부모가 되는 worker는 이렇게 할 수 없고 그들은 단지 하나의 브로커에 응답을 할 수 있습니다.우리는 papa를 사용할 수 있으나 papa는 기존 만들어진 load balancing을 제외하고 LRU와 같은 broker-to-worker 라우팅을 수정하지 못합니다. 만약 여유 있는 worker에게 일을 분산하기 원한다면 이것은 실패할 것입니다. 우리는 정확한 LRU를 필요로 합니다. 해결책으로 하나의 worker node에 라우터 socket을 사용할 수 있습니다. 이것을 “Idea #1”라고 합시다.
  • 브로커는 서로 접속할 수 있습니다.이것은 몇몇 새로운 접속을 만들기 때문에 가장 깔끔해 보입니다. 우리는 즉시 클러스터를 추가할 수 있지만 그것은 아마도 범위 밖일 것 입니다. 이제 클라이언트와 worker는 실제 네트웍 topology의 무지에 남게 되고 브로커는 그들이 여유 있을 때 서로에게 통신합니다. 이것을 “Idea #2”라고 합시다.

Idea #1을 보도록 합시다. worker는 양쪽 브로커에 접속하고 양쪽에서 일을 받아 들입니다. :

fig50.png

그것은 가능해 보입니다. 그러나 그것은 우리가 원하는 것을 제공하지 않으며, 클라이언트는 가능하면 기다려서 리모트 worker을 얻는 것보다 로컬 worker를 얻는 게 낫습니다. 또한 worker는 “ready” 신호를 양 브로커에 보내고 다른 worker가 유휴 상태로 남아 있어도 한번에 두 가지 일을 얻을 수 있습니다. 이 설계는 가장자리에 라우팅 로직을 넣기 때문에 실패한 것처럼 보입니다.

이제 idea #2 입니다.우리는 브로커를 상호 연결하고 우리가 사용하는 것과 같은 mamas 인 클라이언트와 worker는 변경하지 않습니다. :

fig51.png

이 설계는 문제가 한 지점에서 해결되기 때문에 호소력이 있습니다. 기본적으로 브로커는 서로에게 비밀 채널을 열고 통신하며 마치 낙타 무역상과 같이 “이봐, 나는 좀 여유가 있으니 네가 많은 클라이언트를 가졌다면 흥정을 하자”.

그것은 사실상 더 정교한 라우팅 알고리즘입니다: 브로커는 서로를 위한 하청인이 됩니다. 우리가 실제 코드로 실행해 보기 전에 이 설계에 대해서 봅시다. :

  • 그것은 기본적으로 일반적인 케이스(같은 클러스터의 클라이언트와 worker)로 다루고 예외 경우(클러스터 사이의 불규칙한 일들)를 위해 추가적인 일을 합니다.
  • 그것은 작업의 다른 타입에 대하여 다른 메시지 플로우를 사용하게 한다. 그것은 우리가 다르게 그들을 제어 할 수 있다는 것을 의미 합니다, 예 를 들면 다른 타입의 네트워크 접속을 사용하는 것과 같습니다.
  • 그것은 자연스럽게 확장하는 것 같이 느껴 집니다. 세 개 혹은 더 이상의 브로커 내부 연결은 더 복잡해 지지 않습니다. 만약 이것이 문제가 된다면 슈퍼 브로커를 추가 함으로써 쉽게 해결할 수 있습니다.

우리는 이제 예제를 만들 수 있습니다. 우리는 전체 클러스터를 하나의 프로세스로 묶을 것입니다. 그것은 분명 현실적인 것은 아니지만 그것은 시뮬레이션 하기에 간단하게 하고 시뮬레이션은 실제 프로세스를 분명하게 확장할 수 있습니다. 이것은 ØMQ의 장점이고 여러분은 아주 세세한 부분까지 설계할 수 있고 매크로 수준까지 확장할 수 있습니다. 스레드는 프로세스화 정규화, 패턴화, 로직화 됩니다. 각각의 클러스터 프로세스는 클라이언트 스레드, worker 스레드 그리고 브로커 스레드를 가질 수 있습니다.

우리는 지금까지 기본 모델에 대해 봤습니다. :

  • mama 클라이언트(REQ) 쓰레드는 부하를 발생하고 브로커(ROUTER)에 전달 한다.
  • mama worker(REQ) 쓰레드는 업무를 처리하고 브로커(ROUTER)에게 결과를 리턴 한다.
  • 브로커 큐와 LRU 라우팅 모델을 사용하여 부하를 분산한다.

Federation vs. Peering

top prev next

브로커를 서로 연결하는 여러가지 방법이 있습니다. 우리가 원하는 것은 다른 브로커에게 "우리는 능력이 있습니다" 라고 말해 줄 수 있는 것입니다, 그리고 여러 작업을 받을 수 있습니다. 우리는 또한 다른 브로커에게 "멈춰라, 우리는 가득 찾습니다”라고 말할 필요가 있습니다. 그것은 완벽하지 않아도 : 때때로 우리는 우리가 즉시 처리할 수 없는 작업을 수락 할 수 있습니다 그리고 가능한 한 바로 그 작업들을 처리할 것입니다.

가장 간단한 상호연락은 브로커들이 서로에 대해 클라이언트와 작업자를 시뮬레이션 하는 연맹입니다. 우리는 다른 브로커의 backend 소켓에 우리의 frontend를 연결하여 이 작업을 수행합니다. 참고로 그것은 endpoint에 소켓을 bind하거나 다른 끝점에 그것을 연결하는 것은 당연한 것입니다.

fig52.png

이것은 우리에게 브로커와 비교적 만족할 만한 매커니즘 모두 간단한 로직을 제공합니다: 클라이언트가 없을 때, 다른 브로커 '준비'를 얘기할 때, 그것으로부터 한가지 일을 수락할 때. 문제는 이 문제에 대해 너무 간단하다는 것입니다. 제휴 브로커는 한 번에 하나의 작업을 처리할 수 있을 것입니다. 브로커가 lock-step 클라이언트와 작업자를 에뮬레이트 하는 경우, 그것은 정의된 대로 잠금 단계가 될 것입니다. 만일 그것이 이용할 수 있는 작업자가 많이 있다면 그들은 사용되지 않습니다. 우리의 브로커는 완전히 비 동기 방식으로 연결해야 합니다.

제휴 모델은 다른 종류의 라우팅, 특히 서비스 지향 아키텍처 또는 SOAs (LRU or load-balancing or random scatter보다 서비스 이름 과 proximity 에 의한 경로)에 완벽합니다. 따라서 쓸모 없는 것으로 생각하지 않으며, 그것은 least-recently used and cluster load-balancing을 위해 옳지 않습니다.

그래서 연방 대신에, 브로커들이 서로 명시적으로 파악하고 있으며 권한을 가진 채널을 통해 이야기하는 peering 방법을 살펴보겠습니다. 우리는 N개의 브로커를 서로연결하기를 원하는 가정을 파괴하겠습니다. 모든 브로커는 (N-1)개의 peer를 가지고 있으며, 모든 브로커는 정확히 동일한 코드와 로직을 사용하고 있습니다. 브로커들 사이에는 정보의 두 가지 별개의 흐름이 있습니다.:

  • 각 브로커는 얼마나 많은 작업자들이 언제든지 그것을 사용할 수 있는지 peers 에게 말할 필요가 있습니다. 이것은 정기적으로 업데이트된 단지 수량적인 상당히 단순한 정보일 수 있습니다. 이것에 대한 분명한(정확환) 소켓 패턴은 publish-subscribe 입니다. 그래서 모든 브로커가 PUB 소켓을 오픈하고 거기에 상태 정보를 publishe 합니다, 그리고 모든 브로커는 또한 SUB 소켓을 오픈하고 다른 모든 브로커의 PUB 소켓에게 연결하며, 그것의 peers로부터 상태 정보를 얻습니다.
  • 각 브로커가 peer 에게 작업을 위임하거나 비동기적으로 다시 응답을 받을 수 있는 방법이 필요합니다. 우리는 ROUTER/ROUTER 소켓을 사용하여 이 작업을 수행하며, 다른 조합은 없습니다. 각 브로커는 그것이 받는 작업을 위해 하나, 그것을 위임하는 작업을 위해 하나, 두 개의 소켓을 가지고 있습니다. 우리가 두 소켓을 사용하지 않은 경우 우리는 매번 요청이나 응답을 읽을 것인지 알기 위해서 더 많은 작업이 있을 것입니다. 그것은 메시지 envelope에 더 많은 정보의 추가를 의미할 것입니다.

그리고 브로커와 브로커의 로컬 클라이언트과 작업자 사이의 정보의 흐름이 있습니다.

The Naming Ceremony

top prev next

(3개 Flow) X (각 Flow를 위한 2개 소켓) = (우리가 브로커에서 관리해야 하는 6 개 소켓) 입니다. 좋은 이름을 선택하는 것은 우리의 마음에 합리적으로 일관된 멀티 소켓 저글링의 행동을 유지하기 위해 매우 중요합니다. 소켓들이 무엇인가를 하고 그들은 무엇인가 자신의 이름을 위해 기초를 형성해야 합니다. 그것은 추운 월요일 아침 커피 마시기 전 몇 주 후에도 코드를 쉽게 읽을 수 있을 것입니다.

소켓에 대한 샤머니즘적인 명명 의식을 합시다. 세 흐름은 다음과 같습니다. :

  • A local request-reply flow between the broker and its clients and workers.
  • A cloud request-reply flow between the broker and its peer brokers.
  • A state flow between the broker and its peer brokers.

모두 똑같은 길이의 의미 있는 이름을 찾는 것은 우리의 코드가 아름답게 정렬된 것을 의미합니다. 그것은 관계가 없는 것 같지만, 그런 세부 이러한 관심은 더 예술과 같은 뭔가가 일반적인 코드를 설정합니다.

각 흐름에 대한 브로커는 우리가 the "frontend" and "backend"로 부를 수 있는 두 개의 소켓이 있습니다. 우리는 매우 자주 이러한 이름을 사용했습니다. frontend 는 정보 또는 작업을 받습니다. backend 다른 peers에게 정보 또는 작업을 지시합니다. 개념적흐름은 앞에서 뒤로 흐르고 응답은 반대로 뒤에서 앞으로 갑니다.

따라서 우리가 지침서를 위해 작성한 모든 코드에서 이러한 소켓 이름을 사용하는 것입니다. :

  • localfe and localbe for the local flow.
  • cloudfe and cloudbe for the cloud flow.
  • statefe and statebe for the state flow.

우리는 전송을 위해 모든 것에 IPC를 사용합니다. 이것은 (즉 inproc와는 같지 않으며, 연결이 끊어진 전송입니다.) 연결의 측면에서 TCP와 같은 작업의 장점을 가지며, 아직 우리는 여기에서 고민이 될 어떤, IP 주소 또는 DNS 이름을 필요하지 않습니다. 대신, 우리는 어딘가에 무엇인가 우리의 시뮬레이션 클러스터의 이름의 something-local, something-cloud, and something-state 라고 부르는 ipc endpoints를 사용 합니다.

당신은 몇몇 이름을 위해 많은 일이 있다는 것을 생각하고 있을 수 있습니다. 왜 그들을 S1, S2, S3, S4 등으로 부르지 않을까? 대답은 당신의 머리가 완벽한 기계가 아니라면, 코드를 읽는 데 많은 도움이 필요하고, 이러한 이름은 도움이 되는 것을 확인할 수 있습니다. 그것은 "six different sockets" 보다 "three flows, two directions" 이 기억하기 훨씬 쉽습니다.

아래는 브로커 소켓 배치입니다. :

fig53.png

참고로 우리는 다른 모든 브로커내 ‘cloudfe’에 각 브로커내 ‘cloudbe’를 연결합니다. 그리고 마찬가지로 우리는 다른 모든 브로커내 ‘statefe’에 각 브로커내 ‘statebe’를 연결합니다.

Prototyping the State Flow

top prev next

각 소켓 흐름은 부주의한 자체의 작은 함정을 가지고 있기 때문에, 우리는 한번에 전체 코드를 하는 것 보다는 실제로 코드에 차례차례 그들을 테스트하는 것이 좋습니다. 우리가 각각의 흐름에 만족할 때, 우리는 전체 프로그램에 그들을 함께 넣을 수 있습니다. 우리는 상태 흐름과 함께 시작할 것입니다. :

fig54.png

코드에서 어떻게 작동하는지는 다음과 같습니다. :


Clojure | F# | Haxe | Lua | PHP | Python | Scala | Ada | Basic | C++ | C# | CL | Erlang | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl | Ruby

이 코드의 참고사항 입니다. :

  • 각 브로커는 우리가 IPC 끝점 이름을 구성하는 데 사용하는 ID를 가집니다. 실제 브로커는 TCP와 보다 정교한 구성 체계와 함께 작동하도록 해야 합니다. 우리는 이 책에서 나중에 이러한 방식으로 보이지만 지금은, 생성된 IPC 이름을 사용하는 것은 우리의 TCP / IP 주소 또는 이름을 어디서 얻어야 하는지에 대한 문제를 무시 할 수 있습니다.
  • 우리는 프로그램의 핵심으로 zmq_poll(3) 루프를 사용합니다. 이것은 들어오는 메시지를 처리하고 상태 메시지를 보냅니다. 만일 우리가 들어오는 메시지를 받지 못하고 우리가 잠시 기다렸다면 우리는 상태 메시지를 보냅니다. 만일 우리가 매시간 상태 메시지를 보낸다면 그것을 받아 들이게 되고, 결국 너무 많은 메시지를 받게 될 것입니다
  • 우리는 보낸 사람 주소와 데이터로 구성된 두 부분 pubsub 메시지를 사용합니다. 참고로, 우리는 작업을 전송하기 위해 publisher 의 주소를 알아야 됩니다, 그리고 유일한 방법은 메시지의 일부로 명확하게 이것을 전송하는 것입니다.
  • 실행중인 브로커에 연결할 때 최신의 상태정보를 받았기 때문에 subscribers 의 ID를 설정하지 않습니다.
  • subscribers 가 일시적이기 때문에 우리는 publisher 의 HWM을 설정하지 않습니다. 하나의 HWM을 설정할 수 있지만 여기서는 의미 없는 추가 작업입니다.

우리는 이 작은 프로그램을 빌드하고 세 클러스터를 시뮬레이션하기 위해 세 번 실행할 수 있습니다. 자, 그들 DC1, DC2, DC3 (이름은 임의로 지은 것입니다.)를 호출하십시오. 우리는 이 세 가지 명령을 별도의 창에서 각각 실행합니다. :

peering1 DC1 DC2 DC3  #  Start DC1 and connect to DC2 and DC3
peering1 DC2 DC1 DC3  #  Start DC2 and connect to DC1 and DC3
peering1 DC3 DC1 DC2  #  Start DC3 and connect to DC1 and DC2

사실, 우리는 정기적으로 상태 메시지를 보낼 수 있지만 오히려 상태가 변경될 때 (worker가 가용하거나 가용하지 않을 때) 보냈습니다. 그것은 트래픽이 많은 것처럼 보일 수 있습니다. 그러나 상태 메시지가 소규모이고, 우리는 inter-cluster 연결을 super-fast 하게 구성했습니다.

우리가 정확한 간격으로 상태 메시지를 보내기를 원한다면 우리는 자식 스레드를 생성하고 스레드내에 statebe 소켓을 오픈해야 합니다. 그러면 우리의 메인 스레드에서 해당 자식 스레드에게 불규칙한 상태 업데이트를 전송하고, 자식 스레드가 정기적으로 보내는 메시지로 그들을 융합 할 수 있습니다. 이것은 우리가 여기에서 필요한 것 그 이상의 작업입니다.

Prototyping the Local and Cloud Flows

top prev next

이제 로컬과 클라우드 소켓을 통해 작업의 흐름에 대해서 prototype을 합니다. 이 코드는 클라이언트로부터 요청을 가져와서 임의의 기준으로 로컬 작업자와 클라우드 peers에게 배포합니다. :

fig55.png

우리가 좀 복잡함을 갖고 있는 코드로 가기 전에, 그 핵심 라우팅 로직을 스케치하고 간단하지만 강력한 설계로 그것을 설명하겠습니다.

우리는 로컬 클라이언트의 요청에 대해 하나 클라우드 클라이언트의 요청에 대해 하나 두 개의 queues 가 필요합니다. 한 옵션은 로컬 및 클라우드 frontends 에서 메시지를 뽑아 , 이러한 각각의 queues 에 넣는 것입니다. 그러나 ØMQ 소켓이 이미 queues 이기 때문에 이것은 무의미한 것입니다. 그래서 queues로 ØMQ 소켓 버퍼를 사용합니다.

이것은 우리가 LRU queue 브로커에서 사용되는 기술이며, 멋지게 작동했습니다. 요청을 보내 것이 어딘가에 있을 때 우리는 단지 두 개의 frontends에서 읽습니다. 그들이 우리에게 돌아갈 경로를 응답해 주기 때문에 우리는 항상 backends에서 읽을 수 있습니다. backends가 우리에게 말하지 않는 오랫동안, 심지어 frontends를 보고 있는 순간에도 아무 문제가 없습니다.

그래서 메인 루프가 되어 사용됩니다. :

  • backends의 활동을 위해 poll을 합니다. 우리가 메시지를 얻을 때, 그것은 작업자로부터 "READY" 해도 되고, 응답해도 됩니다. 만일 응답을 한다면, 로컬이나 클라우드 frontend를 통해 돌아갈 경로를 라우트팅 합니다.
  • 만일 작업자가 응답했다면, 그것은 가능한 상태가 됩니다, 그래서 우리는 그것을 queue에 넣고, 수를 셉니다.
  • 가용한 근로자가 있는 동안, 로컬 작업자, 또는 임의의 클라우드 peer에게 frontend 및 경로 중 하나에서, 어떤 경우의 요청을 받습니다.

작업자가 클러스터에 걸쳐 작업 배포를 시뮬레이션하는 것 보다 임의로 peer 브로커에게 작업을 전송하는 것이 좋습니다.

우리는 브로커 사이의 메시지 경로로 브로커 ID를 사용합니다. 각 브로커는 우리가 이 간단한 prototype에 있는 커맨드 라인에서 제공하는 이름을 가지고 있습니다. 이 이름은 클라이언트 노드에 사용되는 ØMQ 생성된 UUIDs와 중복되지 않는 한, 우리는 클라이언트 또는 브로커로 복구 경로 응답 여부를 알아낼 수 있습니다.

이 코드가 어떻게 작동하는지 보세요. 흥미로운 부분은 코멘트 " Interesting part "주위에 있습니다.


F# | Haxe | Lua | PHP | Python | Scala | Ada | Basic | C++ | C# | Clojure | CL | Erlang | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl | Ruby

예를 들어, 두 개의 Windows에서 브로커의 두 인스턴스를 시작하여 작업을 실행합니다. :

peering2 me you
peering2 you me

이 코드의 일부를 설명 합니다. :

  • zmsg 클래스를 사용하면 훨씬 쉽고 코드가 간편해 집니다. 이것은 ØMQ 프로그래머로서 당신의 도구 상자의 일부를 구성해야 하는 작업을 분명하게 추상화 합니다.
  • 우리는 peers 로부터 어떤 상태 정보도 얻지 못하기 때문에, 우리는 단지 그들이 실행하고 있는 것으로 가정합니다. 이 코드는 모든 브로커를 시작했을 때를 확인하기 위해 유도(prompt)합니다. 사실, 우리는 브로커가 존재하는지 모르기 때문에 아무것도 보내지 않습니다.

당신은 코드가 영원히 실행되는 것을 보면서 만족할 것입니다. 어떤 잘못된 경로로 보낸 메시지가 있다면, 클라이언트는 결국 차단되고 브로커는 추적정보 출력을 중단합니다. 당신은 브로커 중 하나를 죽이고 그것을 증명할 수 있습니다. 다른 브로커는 클라우드에 요청을 보내고, 하나씩 클라이언트가 답변을 기다리려고 blocking할 것입니다.

Putting it All Together

top prev next

이것을 단일 패키지 안에 넣어 봅시다. 이전처럼, 우리는 한 개의 프로세스로 전체 클러스터를 실행할 수 있습니다. 우리는 이전의 두 예제를 가지고 당신은 클러스터의 모든 수를 시뮬레이션 할 수 있게 설계되어 한 개로 적절하게 작동하도록 그들을 병합할 겁니다.

이 코드는 270 LoC에서 이전 prototype 정도의 크기입니다. 그것은 클라이언트와 작업자와 클라우드 작업량 분포를 포함하는 클러스터의 시뮬레이션을 위하여 매우 좋습니다. 여기 코드는 다음과 같습니다. :


F# | Haxe | Lua | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | Go | Haskell | Java | Node.js | Objective-C | ooc | Perl | Ruby | Scala

그것은 평범한 프로그램이 아니며, 작업하는데 하루 정도 걸렸습니다.. 이것의 특징입니다. :

  • 클라이언트 스레드는 실패한 요청을 감지하고 보고합니다. 이것은 응답을 폴링하여 작업을 수행하고, 잠시(10초) 후에 아무런 응답이 도착하지 않는 경우, 오류 메시지를 출력합니다.
  • 클라이언트 스레드는 직접 출력하지 않고, 대신에 메인 루프가 수집(PULL)하고 인쇄하는 모니터 소켓 (PUSH)에 메시지를 보냅니다. 이것은 우리가 모니터링과 로깅을 위하여 ØMQ 소켓 사용을 본 첫 사례이며 이것은 우리에게 나중에 돌아올 큰 유스케이스 입니다.
  • 클라이언트는 임의의 순간에 클러스터 100 %를 얻기 위해서 부하를 다양하게 시뮬레이션하며, 작업은 클라우드로 전환하고 있습니다. 클라이언트, 작업자, 그리고 클라이언트와 작업자 스레드에서 지연의 숫자는 이것을 제어합니다. 만일 당신이 보다 현실적인 시뮬레이션을 만들 수 있다면, 그들과 함께 볼 수 있도 록 마음대로 실행해 보십시오.
  • 메인 루프는 두 pollsets를 사용합니다. 그것은 실제로 information, backends, and frontends. 세가지를 사용할 수 있습니다.. 초기 prototype과 마찬가지로, 만일 backend용량이 없으면 frontend 메시지를 가질 수 있는 지점이 없습니다

이들은 이 프로그램의 개발 시 부딪치는 문제의 일부입니다. :

  • 클라이언트가 요청 또는 답장이 어딘가에 잃어버린 것 때문에 멈출 것입니다. ØMQ 라우터 / 라우터 소켓은 라우팅 할 수 없는 메시지는 버린다는 것을 기억하세요. 여기에 첫 번째 전술은 이러한 문제를 감지하고 보고하는 클라이언트 스레드를 수정하는 것입니다. 다음으로, 무엇이 문제가 있었는지 분명해질 때까지, 메인 루프에서 send()하기 전과 recv()후에 zmsg_dump ( )을 넣습니다.
  • 메인 루프는 잘못하여 하나 이상의 준비된 소켓에서 읽었습니다. 이것은 첫 번째 메시지가 손실되는 원인이 됩니다. 그 첫 번째 준비된 소켓에서 읽도록 해야 합니다.
  • zmsg 클래스가 제대로 C 문자열로 UUIDs 인코딩 되지 않았습니다. 이것은 손상되고 0 바이트를 포함한 UUIDs의 원인이 됩니다. 인쇄 가능한 16 진수 문자열로 UUIDs를 인코딩 하는 zmsg를 수정하여 고정해야 합니다.

이 시뮬레이션은 cloud peer의 소멸을 감지하지 않습니다. 만일 당신이 여러 peer를 시작하고 하나를 중지하고, 그것이 다른 peer에게 능력을 전달했다면, 그들은 비록 그것이 사라졌을 경우에도 일을 보낼 것입니다. 당신이 이것을 시도 할 수 있고, 당신은 잃어버린 요청에 불평하는 client를 얻을 것입니다. 이 솔루션은 두 가지입니다. : 첫째, 만일 peer 가 사라지면, 그 용량이 빠르게 '제로'로 설정되도록 짧은 시간 동안 용량 정보를 유지합니다. 둘째, request-reply chain 에 안정성을 추가합니다. 우리는 다음 장에서는 신뢰성 보겠습니다.

Chapter Four - Reliable Request-Reply

top prev next

3장에서 우리는 개발된 샘플을 통하여 ØMQ의 request-reply
패턴의 장점을 보았습니다. 이장에서는 신뢰성에 대한 일반적인 질문을 보고 ØMQ의 request-reply패턴의 핵심인 신뢰메시징(reliability messaging) 패턴들을 만들어 보겠습니다.

이장에서 여러분이 ØMQ아키텍쳐를 디자인하는데 도움을 주는 재사용 모델인 user-space 패턴에 초점을 두고 있습니다. :

  • The Lazy Pirate pattern: reliable request reply from the client side.
  • The Simple Pirate pattern: reliable request-reply using a LRU queue.
  • The Paranoid Pirate pattern: reliable request-reply with heartbeating.
  • The Majordomo pattern: service-oriented reliable queuing.
  • The Titanic pattern: disk-based / disconnected reliable queuing.
  • The Binary Star pattern: primary-backup server failover.
  • The Freelance pattern: brokerless reliable request-reply.

What is "Reliability"?

top prev next

신뢰성(reliability)이 무엇인지를 이해하기 위해서 우리는 반대 즉 오류(failure)를 살펴봐야 합니다. 만약 어떤 오류경우들을 처리할 수 있다면, 우리는 이런 오류들에 대해서 신뢰할 수 있습니다. 더도 말고, 덜도 말고, 분산 ØMQ 어플리케이션에서 일반적으로 확률이 높은 것부터 가능한 오류의 원인을 살펴 봅시다. :

  • 어플리케이션 코드의 최악의 문제점들은 충돌(crash), 종료(exit), 멈춤(freeze), 입력에 대한 무응답, 입력에 대한 지연, 그리고 메모리 낭비입니다.
  • 시스템코드 ? ØMQ를 사용하여 작성한 브로커 같은 것 ? 죽을 수 있습니다. 시스템코드는 어플리케이션 코드보다 더 안정적이어야 하지만, 충돌, 디스크 레코딩, 특히, 느린 클라이언트를 위한 보상처리에 따른 메모리 부족경우가 발생 할 때는 그렇지 못합니다.
  • 메시지 대기열은 느린 클라이언트에 대해서 많은 처리를 요청하는 전형적인 시스템코드에서 오버플로우가 발생할 수 있습니다. 큐에 오버플로우가 발생했을 때, 메시지는 버려지기 시작합니다.
  • 네크워크가 간헐적으로 메시지 손실을 초래하는 일시적인 오류 가능성이 있습니다. 이러한 오류는 네트워크가 강제로 연결이 끊어 졌을 때 자동적으로 재연결하는 ØMQ어플리케이션는 해당되지 않습니다.
  • 하드웨어는 시스템에서 실행중인 모든 프로세스를 실패하고 유지할 수 있게 할 수 있습니다.
  • 네트워크는 특이한 방법으로 오류처리 할 수 있습니다. 예를 들어 스위치의 일부 포트를 죽일 수 있으며 그러면 네트워크의 일부에 접근 할 수 없게 됩니다.
  • 전체 데이터 센터는 낙뢰, 지진, 화재, 일반적인 전력이나 냉각 실패에 의해 문제가 발생할 수 있습니다.

이 가능한 모든 실패에 대해 소프트웨어 시스템이 완벽하게 신뢰할 수 있도록 만드는 것은 매우 어렵고, 비용이 많이 들며, 본 가이드의 범위를 넘습니다.

처음 5가지 경우가 대기업 이외의 실제 요구 사항의 99.9%입니다. 당신이 마지막 2가지 경우에 소비할 돈이 있는 큰 회사라면, 나에게 연락을 해주시기 바랍니다.

Designing Reliability

top prev next

가장 간단한 상황을 만들어 보면, 신뢰성은 죽는 상황을 최소화 하기 위해 코드가 정지하거나 충돌이 발생 할 때 적당히 처리되도록 유지하는 것 입니다. 그러나 우리가 적당히 처리하도록 유지하려고 하는 것은 메시지보다 더 복잡합니다. 우리는 각 core ØMQ messaging패턴에서 코드가 죽었을 때 어떻게 동작하도록 만드는지를 볼 필요가 있습니다.

이제 하나씩 보겠습니다. :

  • Request-reply : 만약 요청 처리중 서버가 죽어 응답을 받지 못하기 때문에 클라이언트는 이것을 알 수 있습니다. 그래서 클라이언트는 일을 중지하거나, 기다리거나, 후에 다시 시도하거나, 다른 서버를 찾는 등을 할 수 있습니다. 이렇게 클라이언트가 죽도록 하면 우리는 여러 문제들을 무시 할 수 있습니다.
  • Publish-subscribe : 클라이언트가 죽으면(일부 데이터를 받아오는 중), 서버는 그것을 모릅니다. Pubsub은 클라이언트에서 서버로 어떤 정보도 전송하지 않습니다. 그러나 클라이언트는 대역외(out-of-band, 예 request-reply)으로 서버에 접속할 수 있으며,“내가 놓친 모든 것을 재전송 요청할 수 있습니다. 서버가 죽는 경우는 여기에서는 범위 밖입니다. Subscriber는 너무 느리게라도 작동하지 않는 것을 자가 체크 할 수 있으며, 경고를 보낸다는지 죽는등의 처리를 할 수 있습니다.
  • Pipeline : 만약 worker가 작업중 죽으면, ventilator는 알지 못합니다. Pubsub처럼 시간의 연속기어와 같이 Pipeline은 단지 단방향으로 동작합니다. 그러나 아래방향 colloctor는 특정 task가 작동하지 않는 것을 알 수 있습니다.

이장에서 우리는 request-reply에 초점을 둘 것이며, 다음장에서는 신뢰 가능한 pub-sub과 pipeline을 다룰 것입니다.

기초적인 request-reply패턴(REQ클라이언트 소켓이 REP서버 소켓에 송수신 하는 것)은 실패의 가장 일반적인 유형을 처리해야 하는 경우의 수가 적습니다. 만약 서버가 요청을 처리하는 중 오류가 발생하면 클라이언트는 영원히 멈춰(hang) 있고, 네트워크가 요청이나 응답을 유실하면 클라이언트는 영원히 멈춰 있게 됩니다.

이것은 메시지 로드밸런싱이나 재연결등을 하는 ØMQ의 기능으로 TCP보다 월등합니다. 하지만 실제 작업을 위해서는 아직 충분하지는 않습니다. 기초적인 request-reply패턴을 신뢰할 수 있는 유일한 경우는 네트워크 또는 별도의 서버 프로세스가 아닌 같은 프로세스의 두 스레드 간입니다.

그러나, 약간의 추가 작업을 가지는 이 패턴은 분산 네트워크를 통해 실제 작업을 위한 좋은 기초가 되며, 내가 “Pirate”패턴이라 부르기를 좋아하는 reliable request-reply패턴 집합 입니다.

클라이언트가 서버에 연결할 때 신뢰성 확보를 위한 3가지 접근방식이 있습니다. :

  • 여러 클라이언트가 단일서버에 직접 연결하기. Use case : 클라이언트들이 연결해야 하는 하나의 잘 알려진 서버. 처리대상 장애유형 : 서버 충돌과 재시작, 네트워크 연결 끊김.
  • 여러 클라이언트가 여러 서버에 작업을 분산하는 single queue device와 연결하기. Use case : 워크로드 분산 작업자. 처리대상 장애유형 : 작업자 충돌과 재시작, 작업자 바쁜 루핑, 작업자 과부하, 대기열 충돌과 재시작, 네트워크 연결 끊김.
  • 여러 클라이언트가 중간 장치 없이 여러서버와 연결하기. Use case : name resolution과 같은 분산 서비스. 처리대상 장애유형 : 서비스 충돌과 재시작, 서비스 바쁜 루핑, 서비스 과부하, 네크워크 연결 끊김.

Client-side Reliability (Lazy Pirate Pattern)

top prev next

우리는 클라이언트의 일부 변경으로 매우 간단한 reliable request-reply를 구현할 수 있습니다. :

  • REQ소켓은 응답이 도착하는 경우에만 그것에서 받을 수 있습니다.
  • 요청을 여러 번 보내면 타임아웃 시간내에 도착된 응답이 없습니다.
  • 여러 요청후 응답이 없는 경우 transaction을 버립니다.
fig56.png

send-recv 보다 REQ소켓을 사용할 경우 오류빈도가 높습니다.(기술적으로, REQ소켓은 send-recv ping-pong를 위한 작은 finite-state machine을 구현합니다. 그리고 에러코드는 ‘EFSM’이라고 부릅니다.) 우리가 응답을 얻기 전에 여러 요청을 보내야 하기 때문에, 이 패턴은 REQ를 사용할 때 좀더 번거로운 일입니다. 꽤 좋은 brute-force 솔루션은 오류 후 REQ소켓을 닫고 다시 오픈합니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ruby | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Scala

일치하는 서버와 함께 실행 하세요. :


C++ | Haxe | Java | Lua | PHP | Python | Ruby | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Scala

이 테스트케이스를 실행하려면 두 콘솔 창에 클라이언트와 서버를 시작합니다. 서버는 무작위로 몇 가지 메시지를 보낼 것입니다. 당신은 클라이언트의 응답을 확인할 수 있습니다. 서버의 전형적인 출력은 다음과 같습니다. :

I: normal request (1)
I: normal request (2)
I: normal request (3)
I: simulating CPU overload
I: normal request (4)
I: simulating a crash

여기는 클라이언트의 응답 부분입니다. :

I: connecting to server...
I: server replied OK (1)
I: server replied OK (2)
I: server replied OK (3)
W: no response from server, retrying...
I: connecting to server...
W: no response from server, retrying...
I: connecting to server...
E: server seems to be offline, abandoning

클라이언트가 각 메시지를 순서대로 보내고 순서대로 정확하게 응답이 오는지 확인 해봅시다. : 요청이나 응답의 유실이 없는지, 응답이 한번이상 돌아오지 않는지, 순서가 맞지 않은지. 당신은 이 메커니즘이 실제로 작동하는지 확신이 들 때까지 여러 번 테스트 해 봅시다. 실제로는 순서번호(sequence numbers)가 필요 없지만, 디자인을 신뢰하는데 도움이 됩니다.

클라이언트는 REQ소켓을 사용하고, REQ소켓이 엄격한 보내기/받기 주기를 고수하기 때문에 brute-force는 닫기(close)/재열기(reopen)를 합니다. 당신은 대신에 DEALER를 사용하려고 할 수 있지만, 이것은 좋은 결정이 아닙니다.

클라이언트에서 오류를 처리하는 것은 단일 서버에 연결하는 클라이언트가 여러 개 일 때 수행합니다. 이것은 서버 충돌을 처리할 수 있지만, 동일서버를 재시작하는 정도 입니다. 만약 영구적인 오류 ? 서버 파워가 나간 경우 ? 는 가능하지 않습니다. 서버의 어플리케이션 코드는 보통 어떤 구조에서는 가장 큰 실패의 원인이 되기 때문에 단일서버에 의존하는 것은 좋은 생각이 아닙니다.

그래서, 장단점을 보면 아래와 같습니다. :

  • 장점 : 이해와 구현이 쉽습니다.
  • 장점 : 기존 클라이언트 및 서버 응용 프로그램 코드를 쉽게 사용할 수 있습니다.
  • 장점 : ØMQ는 자동적으로 동작할 때 까지 실제적인 재연결을 재시도합니다.
  • 단점 : 백업/대체 서버로 장애 조치 안 됩니다.

Basic Reliable Queuing (Simple Pirate Pattern)

top prev next

두번째 접근은 ‘worker’를 더 정확하게 호출할 수 있으며, 투명하게 여러 서버와 연결할 수 있도록하는 queue device를 확장한 request-reply패턴을 보겠습니다. 우리는 최소한의 작업 모델을 시작하는 단계에서 이것을 개발할 것입니다.

모든 request-reply패턴에서 작업자(worker)는 stateless 이거나, 일부 공유 상태를 가집니다. Queue device를 가진다는 것은, 작업자는 클라이언트가 무엇을 하는지 알 필요 없이 오고 가고 할 수 있다는 것을 의미 합니다. 이것은 단지 한개의 약점과 관리의 문제가 될 수 있는 자체 중앙 큐, 그리고 실패의 단일 지점을 가지는 좋고 간단한 구조입니다.

queue device의 기초는 3장에서 다룬 LRU(least-recently-used) routing queue 입니다. 우리는 죽었거나 차단된 작업자를 처리하기 위해 최소한 무슨 작업이 필요할까요? 조금 힌트를 주면, 우리는 이미 클라이언트의 재시도 매커니즘을 가지고 있습니다. 그래서 표준 LRU queue를 사용하면 아주 잘 작동합니다. 이것은 중간에 장치를 끼워넣은 request-reply과 같이 peer-to-peer 패턴을 확장 할 수 있는 ØMQ의 사상에 맞는 것입니다. :

fig57.png

우리는 특별한 클라이언트가 필요하지 않으며, 아직 request-reply패턴을 사용합니다. 이것은 그 이상도 이하도 아닌 정확한 LRU queue입니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이것은 request-reply서버에 LRU패턴(REQ ‘ready’신호를 사용함)을 적용한 worker 입니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이것을 테스트 하기 위해서 소수의 작업자, 클라이언트, 그리고 큐 순서로 시작합니다. 당신은 작업자들이 결국 모두 죽고 레코딩을 하며 클라이언트는 재시도 한 후 죽는 것을 볼 수 있습니다. 대기열은 절대 멈추지 않으며, 당신은 작업자와 클라이언트를 지겹도록 다시 시작할 수 있습니다. 이 모델은 클라이언트들과 작업자들 간에 작동합니다.

Robust Reliable Queuing (Paranoid Pirate Pattern)

top prev next

Simple Pirate Queue패턴은 기존 두개 패턴의 조합으로 잘 동작하지만 몇가지 약점을 가지고 있습니다. :

  • 이것은 큐의 충돌과 재시작에 대응할 방법이 없습니다. 클라이언트는 복구되지만, 작업자는 그렇지 않습니다. ØMQ가 자동으로 작업자의 소켓을 재연결하는 동안 새롭게 시작된 큐가 관여 할 때까지, 작업자는 ‘READY’신호를 가지지 못하고 그래서 존재하지 않게 되는 것입니다. 이것을 해결하기 위해서 우리는 큐가 작업자에게 heartbeating해야 합니다. 그렇게 해야 큐가 사라졌을 때 작업자가 이것을 알 수 있습니다.
  • queue는 worker의 실패를 알지 못합니다. 그래서 만약 worker가 idle하다가 죽으면 queue는 그것에 처음 요청을 보낸 작업자 큐에서 해당 worker를 제거 할 수 있습니다. Client는 그냥 기다리고 재시도 합니다. 이것은 심각한 오류는 아니지만, 좋지만도 않습니다. 이일을 위해서 우리는 worker에서 queue로 heartbeating을 합니다. 그래야 queue는 어떤 단계에서 worker를 잃었는지 알 수 있습니다.

우리는 적절하게 Paranoid Pirate패턴에서 이러한 문제를 해결합니다.

우리는 이전에 worker를 위한 REQ소켓을 사용했습니다. Worker를 위해서 우리는 DEALER소켓으로 전환합니다. 이것은 REQ가 보내고 받는 고정된 단계를 거치는 것보다 언제든지 메시지를 보내고 받을 수 있다는 이점이 있습니다. DEALER의 단점은 우리가 직접 envelope관리를 해야 한다는 것입니다. 만약 이것이 의미하는 것을 모른다면 다시 3장을 보시기 바랍니다.

fig58.png

우리는 여전히 Lazy Pirate 클라이언트를 사용하고 있습니다. 여기 Paranoid Pirate큐장치(queue device)가 있습니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

Queue는 workers의 heartbeating으로 LRU패턴 기능을 추가합니다. 작동은 간단하지만, 발명하기에는 꽤 어렵습니다. 잠시후에 heartbeating에 대해서 자세히 설명하겠습니다.

여기 Paranoid Pirate worker가 있습니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이 예제에 대한 몇 가지 설명 입니다. :

  • 코드는 이전 실패 시뮬레이션을 포함하고 있습니다. 이것은 디버그 하기에 매우 어렵게 하고, 재활용하기에도 어렵습니다. 당신이 이것을 디버그 하기 원한다면 실패 시뮬레이션을 해제 하기 바랍니다.
  • Paranoid Pirate queue를 위한 heartbeating를 바로 얻기에는 꽤 까다롭습니다. 이것에 대한 논의는 아래에서 다룰 것입니다.
  • worker는 Lazy Pirate client를 위해 디자인된 것과 유사한 재연결 방법을 사용합니다. 두가지 주요 차이점 : (a)무수하게 back-off하며, (b) 결코 버리지 않음.

Cient, queue, workers를 테스트 하기 위해서 아래 스크립트를 사용하세요. :

ppqueue &
for i in 1 2 3 4; do
    ppworker &
    sleep 1
done
lpclient &

당신은 workers가 하나씩 죽고, 결국 client가 죽는 것을 볼 것입니다. 당신이 queue를 정지하고 재기동 하면, client와 workers는 다시 연결하고 일을 수행합니다. Queue와 worker에게 무슨 짓을 하든, client는 응답을 받을 것입니다.

Heartbeating

top prev next

Paranoid Pirate 예제를 작성할 때, queue-to-worker heartbeating이 올바르게 작동하기 위해 약 5시간이 걸렸습니다. Request-reply chain의 나머지는 약 10분 걸렸습니다. Heartbeating은 더 많은 문제를 일으키는 신뢰성 레이어중 하나입니다. 이것은 ‘잘못된 오류(false failures)’가 많이 발생합니다. 즉 peers는 heartbeats가 적당하게 전송되지 못할 때 연결을 끊을지 결정합니다.

heartbeating을 이해하고 구현할 때 고려해야 할 몇 가지 주의점 입니다. :

  • heartbeating은 request-reply가 아닙니다. 양쪽 방향으로 비동기 흐름을 가집니다. Peer는 다른 것이 죽은 것을 결정하고 연결을 중지 할 수 있습니다.
  • Peer중 하나가 durable socket을 사용한다면 이것은 재연결할 때 queue에서 heartbeats를 가져올 수 있다는 것을 의미 합니다. 이러한 이유로, workers는 durable sockets을 재사용해서는 안 됩니다. 예제 코드는 디버그 목적으로 durable socket을 사용하지만, 이것들은 기존 소켓을 재사용 못 하도록 랜덤(randomized)하게 합니다.
  • 처음, heartbeating 작업을 하며, 단지 메시지 흐름의 마지막에 추가 합니다. 당신은 임의의 순서로 peers를 구동하고, 정지하고, 재시작하고, 정지 시뮬레이션을 통하여 heartbeating 작동을 증명할 수 있어야 합니다.
  • 메인 루프가 zmq_poll(3)를 기반으로하는 경우, heartbeats를 trigger하기 위해서 보조 타이머를 사용합니다. 너무 많은 heartbeats를 보내거나(overloading the network), 너무 적게(causing peers to disconnect) 보낼 수 있기 때문에 poll루프를 사용하면 안 됩니다. zhelpers패키지는 현재 시스템 클럭을 밀리초단위로 반환하는 s_clock()메소드를 제공합니다. 이것은 C에서 heartbeats를 보낼 때 계산하기 위해 사용하기 쉽습니다. :

// Send out heartbeats at regular intervals
uint64_t heartbeat_at = s_clock () + HEARTBEAT_INTERVAL;
while (1) {

zmq_poll (items, 1, HEARTBEAT_INTERVAL * 1000);

// Do this unconditionally, whatever zmq_poll did
if (s_clock () > heartbeat_at) {
… Send heartbeats to all peers that expect them
// Set timer for next heartbeat
heartbeat_at = s_clock () + HEARTBEAT_INTERVAL;
}
}

  • 주 poll루프는 시간초과로써 heartbeats 간격을 사용해야 합니다. 물론, 무한대를 사용하지 마십시오. 그렇다고 너무 작으면 불필요한 루프를 돌게 됩니다.
  • 작업에 대한 추적(trace)는 간단하게 사용하세요. 예:콘솔출력. 당신을 돕기 위한 몇가지 팁은 peers사이에 메시지 흐름(제공하는 zmsg로 덤프방법, GAP을 줄이기 위해 숫자를 증가시키면서 메시지를 출력)을 추적하는 것입니다.
  • 실제 어플리케이션에서, heartbeating는 peer와 통신하기 위해 구성하고 조정을 해야 합니다. 어떤 peers는 10msec보다 낮은 heartbeating을 원할 것이고, 다른 peers는 길게 30초 보다 높은 heartbeating를 원할 것입니다.
  • 만약 다른 peers를 위해 다른 heartbeat간격을 가진다면, 당신의 poll타임아웃은 이것들보다 더 낮아야 합니다.
  • 당신은 heartbeats를 위한 별도의 소켓 대화상자를 열려고 할 수 있습니다. 이것은 당신이 다른 대화상자(예, 비동기 heartbeating에서 동기 request-reply)로 분리할 수 있기 때문에 피상적으로는 좋지만, 여러면에서 좋은 아이디어는 아닙니다. 첫째, 만약 당신이 데이터를 보낼려면 heartbeats를 보낼 필요가 없습니다. 둘째, 네트워크의 예상밖의 변화가 발생할 때 소켓은 먹통이 됩니다. 당신의 주 데이터 소켓이 조용할 때 바쁘지 않아서라기 보다는 죽었는지 알아야 하므로, 당신은 그 소켓에 대하여 heartbeats가 필요합니다. 마지막으로 두소켓은 한 개 보다 더 복잡합니다.

Contracts and Protocols

top prev next

유심히 보면, Paranoid Pirate는 heartbeats때문에 Simple Pirate와 호환되지 않는다는 것을 알게 될 것입니다.

사실, 여기서는 프로토콜에 대해서 다룰 것입니다. 이것은 스펙없이 테스트하기에는 재미 있지만, 실제 어플리케이션을 위해서는 중요한 기초는 아닙니다. 다른 언어로 worker를 만들면 어떻게 되겠습니까? 어떻게 작동하는지 보기 위해서 코드를 읽어야 합니까? 우리가 어떤 이유로 프로토콜을 변경하려는 경우 어떻게 해야 합니까? 포로토콜은 간단할 수 있지만, 분명히 그렇지 않습니다, 그리고 그것이 성공한다고 해도 복잡하게 될 것입니다.

계약을 하지 않으면 일회성 어플리케이션을 만들게 될 수 있습니다.. 그래서 이 프로토콜을 위한 계약을 해야 합니다. 어떻게 이것을 할 수 있을까요?

  • rfc.zeromq.org라는 wiki사이트가 있습니다. 우리는 특히 공공 ØMQ 계약을 위해 만들었습니다.
  • 새로운 규격을 만들기 위해 등록하고 지침을 따르세요. 기술적인 내용은 모든 사람들을 위한 것은 아니지만, 간단하고 쉽게 해주세요.

새로운 Pirate Pattern Protocol 초안을 작성했습니다. 이것은 큰 스펙은 아니지만 충분히 상세하게 되어 있습니다.(당신의 queues는 PPP 호환되지 않습니다. 이것을 고쳐주세요)

실제 프로토콜로 PPP를 설정하면 더 많은 작업이 소요됩니다. :

  • 안정하게 새로운버전의 PPP를 만들 수 있도록 READY명령에 프로토콜 버전 번호가 있어야 합니다.
  • 지금 당장, READY와 HEARTBEAT는 요청과 응답에서 완전히 분리되지 않습니다. 이것을 분명하게 하기 위해, 우리는 "message type"을 포함하는 메시지 구조를 원하는 것입니다.

Service-Oriented Reliable Queuing (Majordomo Pattern)

top prev next

보통 변호사와 위원회가 관여하지 않으면 오히려 일이 빨리 진행되는 좋은 점이 있습니다. 몇 문장전에 우리는 전체를 고칠 좋은 프로토콜에 대한 아이디어 있었습니다. 바로 이것입니다. :

이 한페이지 스펙은 PPP이며 이것을 좀더 충실하게 변경합니다. 이것은 복잡한 아키텍쳐를 설계하는 방법입니다. : 계약서를 쓰고 그 다음 그것을 구현하기 위한 소프트웨어를 만드세요.

Majordomo Protocal(MDP)는 위의 두 요점으로부터 향상된 방법으로 PPP를 확장하고 향상시킵니다. 이것은 클라이언트가 보내는 요청에 “service name”을 추가하고 특정서비스에 등록하기 위해 worker에 요청합니다. MDP에 대해 좋은 점은 그것이 작동가능한 코드, 간단한 프로토콜, 개선의 정확한 집합에서 온 것입니다. 이것은 초안을 작성하기에 쉽게 합니다.

Service name를 추가하는 것은 작은 일이지만, Paranoid Pirate queue을 service-oriented broker로 바꾸는 것은 중요한 변화입니다. :

fig59.png

Majordomo를 구현하기 위해서 우리는 clients와 workers를 위한 프레임워크를 작성해야 합니다. 이것을 모든 개발자가 읽고, 만들도록 요청하는 것은 시간낭비 입니다.

그래서, 첫 번째 계약(MDP자체)은 분산 아키텍쳐의 조각에 대해서 서로 논의하는 방법을 정의합니다. 두번째 계약은 사용자 어프리케이션이 설계하는 기술 프레임워크와 논의하는 방법을 정의합니다.

Majordomo는 Client측과 worker측, 두 측면이 있습니다. 입니다. 우리가 client와 worker어플리케이션을 작성하려고 할때, 두가지 API가 필요합니다. 이것은 간단한 object-oriented 접근법을 사용한 client API에 대한 것입니다. 우리는 ZFL library 스타일을 사용하여 C로 작성합니다. :

mdcli_t *mdcli_new (char *broker);
void mdcli_destroy (mdcli_t **self_p);
zmsg_t *mdcli_send (mdcli_t *self, char *service, zmsg_t **request_p);

우리는 브로커에 대한 한 세션을 열고, 요청 메시지를 보내고, 응답 메시지를 받습니다, 그리고 마지막에 연결을 닫습니다. 여기 worker API가 있습니다. :

mdwrk_t *mdwrk_new (char *broker,char *service);
void mdwrk_destroy (mdwrk_t **self_p);
zmsg_t *mdwrk_recv (mdwrk_t *self, zmsg_t *reply);

이것은 좀 대칭적이지만, worker대화상자와 약간 다릅니다. 처음에 worker는 recv()를 하고, null응답을 보냅니다, 이후 이것은 현재의 응답을 보내고, 새 요청을 얻을 수 있습니다.

이것은 우리가 이미 개발한 Paranoid Pirate 코드를 기반으로 하기 때문에, client와 worker API는 구축이 상당히 간단했습니다. 여기 client API는 다음과 같습니다. :


Haxe | Java | Lua | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

100K request-reply사이클을 수행하는 예제 테스트 프로그램 :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

여기는 worker API입니다. :


Haxe | Java | Lua | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

'echo' 서비스를 구현하는 예제 테스트 프로그램 :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이 코드에 대한 주의사항 :

  • API는 단일 스레드 입니다. 예를들어 이것은 worker가 백그라운드로 heartbeats를 보낼수 없습니다. 다행히, 이것이 정확하게 원하는 것입니다. : 만약 worker어플리케이션이 아무일도 못하게 되면 heartbeats는 정지하고 broker는 worker에게 요청 전송을 중지할 것입니다.
  • worker API는 급격한 back-off를 하지 않습니다, 이것에 대해 추가로 복잡하게 할 가치가 없습니다.
  • API는 모든 오류보고를 하지 않습니다. 뭔가 예상대로 되지 않을 경우, assertion(or language에 따른 예외)이 발생합니다. 이것은 참조구현에 이상적이며, 그래서 어떠한 프로토콜 오류가 즉시 표시됩니다. 실제 어플리케이션에서 API는 오류 메시지에 대해서 강력하게 대응해야 합니다.

ØMQ는 peer가 사라지고 다시 나타나면 자동적으로 소켓을 재연결하는데, 그 때 worker API가 수동으로 소켓을 닫고 새로운 소켓을 오픈하는지 궁금 할 것입니다. Paranoid Pirate worke를 이해하기 위해서 간단한 Pirate worker를 되돌아 봅시다. ØMQ가 자동으로 workers를 재열결 하는 동안, 만약 broker가 죽었다 다시 살아나면, 이것은 broker에 workers를 다시 등록하기에 충분하지 않습니다. 제가 아는 적어도 2가지 해답이 있습니다. 가장 단순한것은, woker가 heartbeats를 이용하여 연결을 모니터링하다가 broker가 죽으면 소켓을 닫고 새로운 소켓으로 새로 시작합니다. 이 대안은 알수없는 worker를 확인하는 broker를 위한 것입니다.-worker로부터 heartbeats를 얻었을 때 ? 다시 등록하도록 요청합니다. 이것은 프로토콜적인 지원이 필요합니다.

Majordomo broker를 디자인해 봅시다. 핵심구조는 서비스마다 한 개를 가지는 queue집합입니다. 우리는 worker가 나타날 때 이들 queue를 만들 것입니다.(우리는 worker가 사라질 때 이것들을 삭제합니다. 이것은 복잡합니다.) 추가로 우리는 서비스마다 worker의 queue를 유지합니다.

C예제를 쓰고 읽는데 더 쉽게 만들기 위해 ZFL project에서 hash와 list container 클래스를 사용했으며, zmsg를 사용하는데 zlistzhash 로 이름을 변경했습니다. 물론 언어에 따라 내장된 containers를 사용할 수 있습니다.

그리고 이것이 broker입니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이것은 지금까지 우리가 본 것 중 가장 복잡한 예제입니다. 코드가 거의 500 줄입니다. 이것을 충분하게 잘 작동하게 하기 위해서 이틀이 걸렸습니다. 그러나 이것은 아직 완전한 service-oriented broker에 대한 코드의 작은 일부일 뿐입니다.

이 코드에대한 주의사항 :

  • Majordomo프로토콜은 단일 소켓으로 clients와 workers 모두를 처리할 수 있습니다. 이것은 broker를 전개하고 관리하기에 유용합니다. : 이것은 대부분 devices가 필요로하는 ØMQ endpoint가 두개라기 보다는 1개라는 것입니다.
  • broker는 이것이 오류 명령, heartbeats를 전송하거나, 아무일도 않고 있을때 연결이 단절되는 것을 포함하여 제대로 MDP/0.1(내가 아는 한)을 모두 구현합니다.
  • 이것은 각각 한소켓 혹은, client과 worker로 된 한쌍을 관리하는 다중 스레드를 실행하는 것으로 확장할 수 있습니다.
  • broker는 기본적으로 서비스 이외에 상태를 가지고 있지 않기 때문에 primary-failover 나 live-live broker 모델 구성은 간단합니다. 이것은 첫번째 선택한 broker가 실행되지 않은 경우 다른 broker를 선택하는 것은 clients와 workers에게 달려 있습니다.
  • 예제는 trace할 때 출력의 양을 줄이기 위해 5초 heartbeats를 사용합니다. 현실적인 값은 대부분의 LAN어플리케이션을 위해서는 더 낮아야 합니다. 그러나 모든 시도가 재시작하는 서비스에 대한 충분한 시간을 가져야 하는데, 적어도 10초라고 말합니다.

Asynchronous Majordomo Pattern

top prev next

위의 Majordomo를 구현하는 방법은 간단합니다. Client는 단지 Simple Pirate입니다. 테스트 상자에서 client, broker, worker를 fire up 시킬때, 이것은 약 14초동안 100,000요청을 처리할 수 있습니다. 이것은 부분적으로 메시지 프레임을 복사하는 코드 때문입니다. 하지만 진짜 문제는 우리가 round-tripping하는 것입니다. ØMQ는 Nagle's algorithm을 사용하지 않을 수 있지만, round-tripping은 여전히 느립니다.

이론적으로 크지만, 실전에서는 좋습니다. 간단한 테스트 프로그램으로 round-tripping의 비용을 측정해 봅시다. 여러번 메시지를 보냅니다, 처음은 각 메시지마다 응답을 기다리고, 두번째는 배치로 요청을 하고 모든 응답을 받습니다. 이 두가지는 같은 일을 하지만, 매우 다른 결과를 줍니다. 우리는 client, broker, worker를 mockup합니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

실행 결과 입니다. :

Setting up test...
Synchronous round-trip test...
 9057 calls/second
Asynchronous round-trip test...
 173010 calls/second

client스레드가 시작하기 전에 잠시 멈추는 것에 주의하세요. 이것은 router소켓의 기능중 하나를 해결할 것입니다.:당신이 아직 연결되지 않은 peer의 주소와 메시지를 보낼 경우 메시지가 삭제됩니다. 이 예제에서 worker스레드가 연결하는데 너무 느리다면 sleep없이 LRU메케니즘을 사용하지 마세요, 이것은 메시지를 잃게되고, 테스트를 망치게 됩니다.

보다시피, 가장 간단한 경우에서 round-tripping은 "shove it down the pipe as fast as it'll go" 비동기 접근보다 20배가 더 느립니다. 이것을 Majordomo에 적용할 수 있는지 봅시다.

첫째, send/recv 메소드로 구분된 client API를 변경해 봅시다. :

mdcli_t *mdcli_new (char *broker);
void mdcli_destroy (mdcli_t **self_p);
int mdcli_send (mdcli_t *self, char *service, zmsg_t **request_p);
zmsg_t *mdcli_recv (mdcli_t *self);

이것은 말 그대로 동기 client API를 비동기로 바꾸는 작업입니다. :


Haxe | Java | Lua | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이것은 대응하는 client 테스트 프로그램 입니다. :


C++ | Haxe | Java | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

프로토콜을 변경하지 않기 때문에 broker와 worker는 변경하지 않습니다. 성능이 즉시 향상되는 것을 볼 수 있습니다. 이것은 100K request-reply 사이클을 처리하는 동기 client입니다. :

$ time mdclient
100000 requests/replies processed

real    0m14.088s
user    0m1.310s
sys     0m2.670s

그리고 이것은 단일 worker와 비동기 client입니다. :

$ time mdclient2
100000 replies received

real    0m8.730s
user    0m0.920s
sys     0m1.550s

2배 빠릅니다. 나쁘진 않지만, 10 workers가 fire up 할 때 이것을 어떻게 처리하는지 봅시다. :

$ time mdclient2
100000 replies received

real    0m3.863s
user    0m0.730s
sys     0m0.470s

LRU방식에서 workers는 메시지를 수신하는데 완전한 비동기는 아닙니다. 그러나, 이것은 더 많은 workers를 위해서는 더 확장성이 좋습니다. 여기 테스트에서는 여덟개의 workers이후에는 더 빨라지지 않습니다. Broker는 아직 최적화가 안되어 있습니다. 이것은 zero copy하는 대신에 message frames를 복사하는데 대부분의 시간을 보냅니다. 그러나 우리는 꽤 낮은 노력으로 신뢰할 수 있는 25K request/reply를 가집니다.

그러나 비동기 Majordomo패턴은 모든 것이 좋은 것은 아닙니다. 이것은 근본적인 문제가 있습니다, 즉 broker가 많은 작업없이 충돌을 피할수 없다는 것입니다. 만약 당신이 mdcliapi2코드를 보면 그것이 실패 후 다시 연결을 시도하지 않습니다. 적당한 재연결이 필요합니다.

  • 모든 요청은 번호가 붙어 있고, 모든 응답은 그 요청에 일치하는 번호를 가집니다.
  • Client API는 track하며, 미 수신 응답등을 위해서 요청을 잡고 있습니다.
  • 장애 경우, client API는 브로커에 대한 모든 요청을 재전송 합니다.

이것은 거래가 중지되는 것은 아니지만, 종종 성능은 복잡성에 관련이 있다는 것을 보여 줍니다. Majordomo을 위해 가치있는 일입니까? 이것은 여러분의 관심사에 따라 달라집니다. name lookup service를 위하여 당신은 매 세션마다 호출을 하느냐, 그것은 아닙니다. 수천의 cients가 web front-end 서비스를 하느냐, 아마도 그럴 것 입니다.

Service Discovery

top prev next

우리는 훌륭한service-oriented broker가 있지만, 특정 서비스가 가용한지 그렇지 않은지 알 수 있는 방법이 없습니다. 요청이 실패한 것은 알 수 있지만, 원인은 모릅니다. Echo service가 동작중인지를 broker에 확인하는 것은 가능합니다. 가장 좋은 방법은 어떤 service X가 동작중인지 broker에 확인하는 명령어를 추가하기 위해 MDP/Client 프로토콜을 변경하는 것입니다. MDP/Client는 간단하게 구현되는 강력한 장점이 있습니다. 이것에 서비스 검색을 추가하면 MDP/Worker는 프로토콜만큼 복잡하게 만들어 질 것입니다.

다른 옵션은 배달할 수 없는 요청을 반환하는 메일과 같이 처리하는 것입니다. 이것은 비동기적으로 잘 동작할 수 있지만 이것또한 복잡성은 있습니다. 우리는 반환된 요청과 응답을 구분하고, 적절하게 이것을 처리할 수 있는 방법이 필요합니다.

이것을 변경하는 대신에 이미 MDP로 만든 것을 사용해 봅시다. 서비스 검색(Service discovery)은 서비스 그 자체입니다. 이것은 참으로 “disable service X”, “provide statistics”등 과 같이 여러 관리 서비스중 하나가 될 수 있습니다. 우리가 원하는 건 프로토콜이나 기존 어플리케이션에 영향을 안 주는 일반적이고 확장가능한 솔루션입니다.

그래서 작은 RFC가 여기 있습니다 ? MMI 또는 Majordomo 관리 인터페이스 ? MDP의 상위 layer: http://rfc.zeromq.org/spec:8 우리는 이미 broker에 이것을 구현 했습니다, 여러분이 아마 이것을 잊고 전체를 읽지 않았어도 말입니다. 이것은 어플리케이션에서 서비스 검색을 사용하는 방법입니다. :


Haxe | Java | Lua | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Node.js | Objective-C | ooc | Perl | Ruby | Scala

Broker는 서비스 이름을 확인하고 worker에 요청을 전달하는 것 보다 오히려 “mmi.”로 시작하는 서비스를 처리합니다. 이것을 돌려보면 woker 실행없이, 당신은 “200” 이나 “400”뒤에 따르는 작은 프로그램 리포트를 볼 수 있습니다. 예제 broker에서 MMI구현은 꽤 완성도가 떨어 집니다. 예를 들어 한 worker가 사라지면, 서비스는 현재 상태를 유지합니다. 사실, broker는 일부 설정시간 이후에 worker없이 서비스를 제거해야 합니다.

Idempotent Services

top prev next

Idempotency은 알약을 복용하는 무엇이 아닙니다. 이것은 작동을 반복하기에 안전하다는 것입니다. 시간을 체크하는 것은 idempotent입니다. 많은 client-to-server 경우에는 항상 idempotent하지 않습니다. Idempotent 사용 사례는 다음과 같습니다.

  • Stateless task distribution, 즉 서버가 요청 상태에 대해 단순한 응답을 하는 stateless workers가 되는 pipeline 입니다. 이러한 경우, 그 같은 요청은 여러 번 실행하는 것이 안전(비효율적일지라도)합니다.

.

  • 바인딩하거나 연결하기 위해 endpoint에 논리 주소를 변환하는 것은 이름서비스(name service) 입니다. 이러한 경우, 이것은 동일한 조회 요청을 여러 번 만들기에 안전합니다.

그리고 다음은non-idempotent 사용사례 입니다. :

  • 로깅 서비스입니다. 한 번 이상 같은 로그 정보를 원하지 않습니다.
  • 어떤 서비스는 다른 노드에 정보를 보내는 downstream노드에 영향이 있습니다. 만약 서비스가 두번이상 동일한 요청을 하게되면 downstream노드는 중복된 정보를 얻을 수 있습니다.
  • 어떤 서비스는 몇몇 non-idempotent방식으로 공유데이터를 수정합니다. 예, 은행계좌를 인출하는 서비스는 확실히 idempotent하지 않습니다.

서버 어플리케이션이 idempotent되지 않을 때, 우리는 그것이 다운되는 것에 대해서 더 신중하게 생각해야 합니다. 이것이 유휴상태나 요청처리 중 죽었다면 괜찮습니다. 대개 출금과 신용처리가 함께 수행되었는지 확인하기 위해 데이터베이스 트랜젝션을 사용할 수 있습니다. 만약 서버가 응답을 보내는 동안 죽으면, 작업이 완료되었는지 알수 없기 때문에 문제가 됩니다.

네트워크가 클라이언트에 되돌려주는 응답처리를 하다 죽으면 같은 문제가 발생합니다. 클라이언트는 서버가 죽었음을 알고 재요청할 것이며, 서버는 같은 작업을 두번할 것입니다. 우리가 원하는 것은 이것이 아닙니다.

우리는 중복요청을 감지하고 거부하기 위한 공정한 표준 솔루션을 사용합니다. 이것은 다음을 의미 합니다. :

  • 클라이언트는 고유한 client ID와 독특한 message number로 모든 요청을 채번해야합니다.
  • 서버가 응답을 다시 보내기 전에, 키로써 ‘client ID + message number’를 사용하여 저장해야 합니다.
  • 서버는 client로부터 요청을 처음 받을 때, client ID + message number로 응답을 했었던 적이 있는지 확인해야합니다. 그래서 요청은 처리하지만 바로 답장을 resends하지 않습니다.

Disconnected Reliability (Titanic Pattern)

top prev next

여러분이 몇 회전 rust1을 추가할수도 있지만, Majordomo는 신뢰가능한 메시지 broker라고 알고 있습니다. 결국, 이것은 모든 엔터프라이즈 메시징 시스템에서 작동합니다. 아키텍쳐 입장에서 rust-based brokers가 불합리한 몇가지가 있습니다.

  • 여러분이 본것과 같이 Lazy Pirate client는 놀라울 정도로 잘 동작합니다. 이것은 직접적인 client-to-server에서 distributed queue devices이르기 까지 전 아키텍쳐 범위에 걸쳐 사용할 수 있습니다. 이것은 worker가 stateless와 idempotent 한 것으로 가정하는 경향이 있습니다. 그러나 우리는 rust하기 위해 재정렬없이 제한사항을 해결 할 수 있습니다.
  • Rust는 성능저하부터 관리하고 복구해야하는 문제점들을 유발하며, 시스템이 멈추는 panics을 만듭니다. 일반적으로 Pirate 패턴의 장점은 간단함 입니다. 이것은 멈추지 않을 것입니다. 그래도 하드웨어 장애을 고려한다면, broker가 없는 peer-to-peer 패턴으로 할 수 있습니다. 이것은 이장 후반부에 설명할 것입니다.

그러나, 비동기 deconnected 네트워크인 rust-based 신뢰성에는 하나의 use case가 있습니다. 그것은 Pirate의 주요 문제점, 즉 클라이언트가 실시간으로 응답을 기다리는 것을 해결합니다. 만약 client와 worker가 간헐적으로 (이메일과 같이) 연결되어있다면, 우리는 client와 worker 사이에 stateless network를 사용할 수 없습니다. 우리는 중간에 상태를 제공해야 합니다.

그래서, client와 workers가 산발적으로 연결하는 것에 상관없이 결코 잊지 않기 위해서 디스크에 메시지를 저장하는 Titanic 패턴이 이것입니다. 우리는 서비스를 발견했던 것처럼, extend MDP보다 Majordomo의 Titanic를 사용할 것입니다. broker보다는 특화된 worker에서 fire-and-forget 신뢰를 구현할 수 있다는 의미 입니다. 이것은 몇가지 이유에서 우수합니다. :

  • 이것은 훨씬 쉽습니다.
  • 이것은 다른 언어로된 worker와 broker를 섞어 사용할 수 있습니다.
  • 독립적으로 fire-and-forget 기술을 발전시켰습니다.

유일한 단점은 브로커와 하드 디스크 사이에 별도의 네트워크 홉이 있다는 것입니다.

Persistent request-reply 구조를 만드는 방법은 여러가지가 있습니다. 몇시간을 들어 할 수 있는 가장 간단한 방법은 “proxy service”로써 Titanic입니다. 즉, 이것은 전혀 worker에 영향을 주지 않습니다. 만약 즉시 client가 응답을 원한다면 직접 가용한 서비스를 요청하면 됩니다. 만약 client가 잠시 기다리겠다면, Titanic과 통신합니다.

fig60.png

Titanic은 worker와 client 둘다 입니다. Cient와 client사이의 대화는 다음과 같습니다. :

  • Client: 나를 위해 이 요청을 수락해 주세요. Titanic: 좋아, 다 됐습니다.
  • Client: 당신이 내게 응답을 주었습니까? Titanic: 예, 여기입니다. 아니면, 아니, 아직은.
  • Client: 좋아, 당신은 지금 요청을 지울수 있나요?. Titanic: 물론, 다 됐습니다.

반면, Titanic 과 broker와 worker 사이의 대화 상자는 다음과 같습니다.

  • Titanic: 이봐, broker, 거기 echo 서비스 있습니까? Broker : 음 그래요.
  • Titanic: 안녕하세요, Echo, 날 위해 이것을 처리해 주세요. Echo : 네, 여기 있습니다.
  • Titanic: sweeeeet!

이처럼 작동되면 가능한 장애 시나리오가 있습니다. 만약 worker가 요청을 받아 처리하다 죽으면 Titanic은 재요청 합니다. 만약 응답이 어디선가 분실된 것을 알게 되면 Titanic은 재시도 할 것입니다. Client가 응답을 받지 않았는데 요청이 완료되면, 클라이언트는 다시 요청할 것입니다. 만약 Titanic이 요청이나 응답을 처리하는 동안 죽으면, client는 다시 시도 할 것입니다. 요청이 저장소에 안정하게 commit되기 까지 작업은 손실되지 않습니다.

Handshaking은 pedantic 하지만 pipelined 일 수 있습니다, 즉 client는 많은 작업을 수행하고 나중에 응답을 처리하기 위해 비동기 Majordomo패턴을 사용할 수 있습니다.

답장을 요청하는 client를 위한 여러 방법이 필요합니다. 우리는 같은 서비스를 호출하는 많은 client를 가질 수 있으며, client는 다른 ID로 사라졌다 나타납니다. 그래서 여기 간단하고 합리적인 보안 솔루션이 있습니다.

? 모든 요청은 요청을 할 때 Titanic이 client에게 반환할 universally unique ID(UUID)를 만듭니다.
? 클라이언트가 응답을 요구하면, 그것은 원래 요청에 대한 UUID를 지정해야합니다.

이것은 안전하게 해당 요청 UUIDs를 저장하기 위해 클라이언트에서 몇 가지 무거운 짐을두고 있지만, 인증을 위한 어떤 필요성은 없습니다. 어떤 대안이 있는가? 우리는 내구성 소켓, 즉 명시적인 client ID를 사용할 수 있습니다. 그래서 많은 고객이 있을 때 관리 문제가 발생하고, 같은 ID를 사용하는 두 클라이언트로 인한 피할 수 없는 오류가 생깁니다.

우리가 다른 공식적인 규격를 작성하기 전에 client가 Titanic에게 말하는 방법을 생각해 봅시다. 한 가지 방법은 단일 서비스를 사용하고, 세 가지 다른 요청 유형을 전송합니다. 간단하게 보이는 또 다른 방법은, 아래 3개 서비스를 사용하는 것입니다.

? titanic.request - 요청 메시지를 저장하고, 요청에 대한 UUID를 반환합니다.
? titanic.reply ? 제공된 요청 UUID에 대한 답변을 가져옵니다.
? titanic.close - 응답이 저장되고 처리되었는지 확인합니다.

우리는 ØMQ를 가지고 멀티스레딩 환경과 같은 다중스레드 worker를 만들 것입니다. 그러나 코드를 보기 전에 Titanic이 ØMQ메시지와 프레임 관점에서 보여지는 것을 구상해 해 봅시다. http://rfc.zeromq.org/spec:9. 이것은 “Titanic Service Protocal” 혹은 TSP입니다.

TSP를 사용하면 MDP를 통해 직접 서비스에 액세스하는 것보다 client어플리케이션 작업이 더 많이 분명해 집니다. 다음은 짧고 강력한 'echo' client 예제입니다. :


Haxe | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | Ruby | Scala

물론, 이것은 실제적으로 몇가지 프레인워크로 가능합니다. 실제 어플리케이션 개발자들은 가까이에서 메시지를 보지 않습니다, 이것은 프레임 워크와 API를 구축하기 위해 많은 기술을 가진 전문가를위한 도구입니다. 우리가 이것을 관찰하기 위해 무한한 시간이 있다면, TSP API예제를 만들 것이고, client어플리케이션 뒷부분에 코드 몇 줄을 넣을 것입니다. 그러나 MDP에서 본것과 같은 원리로 반복은 필요 없습니다.

여기 Titanic을 구현한 것이 있습니다. 이서버는 제안된 대로 3개 스레드를 사용하여 3개 서비스를 처리합니다. 이것은 대부분 brute-force approach possible(메시지마다 한 파일)를 사용하여 디스크에 full persistence 합니다. 반복적으로 폴더를 읽는 것을 피하기 위해 모든 요청 큐에 저장하는 복잡한 부분이 있습니다. :


Haxe | PHP | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이것을 테스트하려면 mdworkertitanic을 시작한 후, titanic를 시작합니다. 지금 임의로 mdworker를 시작하면 응답을 받고 종료하는 client를 볼 수 있습니다.

이 코드에 대한 몇 가지 참고사항 입니다. :

  • 우리는 동작중인 서비스에 요청을 보내기 위해 MMI를 사용합니다. 이것은 broker에있는 MMI 구현으로 작동합니다.
  • 우리는 주요 dispatcher를 통해 titanic.request 서비스에서 새로운 요청 데이터를 전송하는 inproc 연결을 사용합니다. 이것은 디스크 디렉토리 스캔, 모든 요청 파일을 로드, 날짜/시간 기준으로 결과를 정렬하는 데에서 dispatcher를 절약할 수 있습니다.

이 예제에 대해 중요한 것은 성능이 아니라, 얼마나 잘 구현됐냐는 것입니다. 이것을 시도하려면, mdbroker 및 타이타닉 프로그램을 시작합니다. 다음 ticlient를 시작한 다음 mdworker의 에코 서비스를 시작합니다. 당신은 활동의 자세한 추적을 할 수있는 '- V'옵션을 사용하여 이러한 네 개 모두를 실행할 수 있습니다. 당신은 client를 제외하고 중지 및 다시 시작해도 아무것도 잃지 않을 것입니다.

실제 경우에 Titanic을 사용하기 원하는 경우, 가장 처음 질문은 “어떻게 빨리 만들 것인가?” 입니다. 무엇을 해야 하는지 구현예를 설명합니다.

  • 여러 파일보다는 모든 데이터를 위해 하나의 디스크 파일을 사용합니다. 운영체제는 대개 많은 작은 파일보다는 큰 몇 개 파일을 핸들링하는 것이 더 효율적입니다.
  • 원형 버퍼로 디스크파일을 사용합니다. 단일 스레드로 사용하는 것이 가장 빠르게 작업할 수 있습니다.
  • 메모리에 인덱스를 유지하고 디스크 버퍼에서 시작 시간에 인덱스를 다시 작성하십시오. 이것은 디스크에 있는 인덱스가 완전히 안전하게 유지하는 데 필요한 추가 디스크 작동을 줄여 줍니다. 당신이 모든 시스템 오류의 경우에 마지막 M 메시지를 잃을 준비가되어있다면, 모든 메시지 후에 fsync, 혹은 모든 N의 milliseconds를 원하는 것입니다.
  • 하드디스크를 사용하는것 보다는 드라이브를 사용하세요.
  • 전체 파일을 미리 할당하거나, 필요에 따라 원형 버퍼가 커지고, 축소 할수 있도록 큰 덩어리에 할당하세요. 이것은 단편화를 방지하고 대부분 읽고 쓰기가 인접하도록 보장합니다.

그외, 특별한 database 이거나, 성능을 걱정하지 않는 다면 몰라도, 심지어 빠르게 key/value를 저장한다고 해도 database에 메시지를 저장하지 않기를 추천합니다.

만약 당신이 더욱 신뢰할 수 있는 Titanic을 만들기를 원한다면, 물리적으로 먼 곳에 두번째 서버를 두고 요청을 복제하는 것으로써 이것을 할 수 있습니다

만약 당신이 더 빠르고 덜 신뢰할 수 있는 Titanic을 만들기를 원한다면, 당신은 메모리에 요청/응답을 저장할 수 있습니다. 이것은 비접속 네트워크의 기능을 제공할 것이지만, Titanic서버 자체는 crash에 빈약합니다.

High-availability Pair (Binary Star Pattern)

top prev next

Overview

top prev next

Binary Star패턴은 primary-backup high-availability pair에 두 서버를 놓습니다. 주어진 시간에, 이 중 하나는 클라이언트 응용 프로그램 (이것은 "master"입니다)에서 접속을 허용하고, 하나는 (그것이 "slave"입니다) 허용하지 않습니다. 각 서버는 다른서버를 모니터링합니다. 마스터가 네트워크에서 사라지면, 일정 시간이 지나면 슬레이브가 마스터로 takeover합니다.

Binary Star 패턴은 iMatix OpenAMQ 서버를 위하여 Pieter Hintjens과 Martin Sustrik에 의해 개발되었습니다. 설계한 것은 이와 같습니다.

  • straight-forward high-availability 솔루션을 제공합니다.
  • 실제로 이해하고 사용하기에 간단합니다.
  • 필요한 경우에 failover를 합니다.
fig61.png

Binary Star pair가 동작하는 것을 확인하기 위해, 여기 failover에서 발생되는 현상을 보여주는 다른 시나리오가 있습니다.

  1. primary 서버로 동작하는 하드웨어는 치명적인 문제 (전원 공급 장치 폭발, 기계 화재, 또는 누군가가 단순히 실수로 플러그를 뽑았을때)를 가지고 있습니다. 응용 프로그램이 이것을 모니터링하고 있으며, 백업 서버에 다시 연결합니다.
  2. primary server의 네크워크가 죽고 어플리케이션은 백업서버로 재 접속하기 시작합니다.
  3. primary서버는 다운 되거나, 운영자에 의해 정지 됐으며, 자동으로 다시 시작하지 않습니다.

장애 조치 (Failover)에서 복구는 다음과 같이 작동합니다. :

  1. 운영자는 primary서버를 재기동하고 네트워크에서 사라진 원인이 무엇인지 해결합니다.
  2. 운영자는 어플리케이션에 최소한의 혼란을 주도록 즉시, 백업서버를 중지합니다.
  3. 어플리케이션이 primary서버에 재연결되었을 때, 운영자는 백업서버를 재기동합니다.

복구는 (master를 primary서버로 사용하기 위해) 수동 조작합니다. 자동복구는 적절하지 않다고 고통스러웠던 경험이 말해줍니다. 거기에는 몇 가지 이유가 있습니다. :

  • Faileover는 10-30초 어플리케이션에 서비스의 중단을 만듭니다. 실제 비상 사태가있다면, 이것은 전체 정전보다 훨씬 좋은 것입니다. 복구가 10~30초 보다 더 중단시간이 필요하다면, 사용자가 네트워크를 사용하지 않고, peak타임이 아닐 때 하는 것이 좋습니다.
  • 비상 사태가되면, 그것들을 고치기 위해 계획을 만드는 것도 좋은 생각입니다. 자동 복구는 더 이상 중복 확인없이 담당하는 서버를 기동하기에 시스템 관리자에게 불확실함을 줍니다.
  • 마지막으로, 당신은 네트워크가 failover되고 복구되면 운영자는 무슨 일이 일어 났는지 분석하기 어려

운 위치에 처해질 수 있습니다. 이 서비스 중단이 발생했지만, 원인은 명확하지 않습니다.

Binary Star 패턴은 primary서버가 다시 살고 백업서버가 죽으면 fail back할 것입니다. 사실 이것은 우리가 복구를 유발하는 방법입니다.

fig62.png

Binary Star pair에 대한 종료 과정 중 하나 입니다. :

  1. 수동 서버를 정지 후 최종 active 서버를 중지하거나 또는,
  2. 몇초 이내에 임의의 순서로 두서버를 정지합니다.

failover타임아웃보다 좀더 긴 시간이 필요하는 수동서버는 어플리케이션에 연결을 끊고, 재 연결하고 사용자에 방해가 된다고 해도 다시 연결을 끊는 원인이 발생 할 것입니다.

Detailed Requirements

top prev next

Binary Star는 정확하게 작동하고 간단합니다. 사실 현재의 디자인은 세 번째 재설계된 디자인 입니다. 이전 버전에서는 너무 복잡하고 너무 많은 일을 하려고 노력했습니다. 그리고 우리는 이해와 사용 가치가 충분한 설계가 될 때까지 기능을 파악했습니다.

여기 high-availability 아키텍쳐를 위한 우리의 요구사항입니다.

  • failover는 하드웨어 고장, 화재, 사고등 재해 시스템 장애에 대한 보장을 제공하기 위한 것입니다.
  • failover 시간은 60초 이하여야 하고 10 초 이하면 더 좋을 것입니다.
  • failover는 자동으로 일어나는 반면, 복구는 수동으로 해야 합니다. 우리는 자동으로 백업서버로 옮겨지기를 원하지만, 운영자가 문제들을 해결했을 때를 제외하고는 primary서버로 되돌려지는 것을 원하지 않으며, 다시 어플리케이션을 잠시 정지할 수 있는 좋은 시간을 결정합니다.
  • client어플리케이션에 대한 의미는 간단하고 개발자들이 이해하기 쉽게해야합니다. 이상적으로 이것은 client API에 내재하고 있어야합니다.
  • Binary Star pair에서 두 서버가 마스터 서버라고 생각하는 디자인을 방지하는 방법에 대한 네트워크 설계자에 대한 명확한 지침이 있어야합니다.
  • 두 서버가 시작되는 순서에 아무런 의존성이 없어야합니다.
  • 그것은 (그들이 강제로 재연결 하더라도) 클라이언트 응용프로그램을 정지없이 어느한쪽 서버의 계획을 정지하고 재시작을 할 수 있어야합니다.
  • 운영자는 항상 두 서버를 모니터 할 수 있어야 합니다.
  • 그것은 고속 전용 네트워크 연결을 사용하여 두 서버를 연결할 수 있어야합니다. 즉, failover동기화는 특정 IP 경로를 사용할 수 있어야합니다.

우리는 이런 부분을 가정 합니다. :

  • 단일 백업서버가 충분한 보장을 제공하며, 우리는 다중 백업서버가 필요하지 않습니다.
  • primary 및 백업 서버가 응용 프로그램 부하를 수용할 수 있는 동일한 사양 이어야 합니다. 우리는 서버에서 로드밸런스를 하지 않습니다.
  • 거의 모든 시간을 아무것도 안 하는 완벽하게 사용하지 않는 백업 서버를 사용하기에 충분한 예산이 있습니다.

가정 하지 않은 부분 입니다. :

  • active 백업서버 또는 로드밸런싱의 사용입니다. Binary Star pair에서, 백업 서버는 비활성 상태이고 primary 서버가 off-line이 될때까지 유용한 작업을 수행하지 않습니다.
  • 어떤 식으로든 영구적인 메시지 또는 트랜잭션을 처리합니다. 우리는 신뢰할 수 없는 (그리고 아마도 신뢰할 수) 서버 또는 Binary Star pairs의 네트워크라고 가정합니다.
  • 네트워크의 어떤 automatic exploration. Binary Star pair는 수동적이고, 명확하게 네트워크에 정의되고, 어플리케이션(적어도 구성데이터에서라도)에 알려져 있습니다.
  • 서버사이에서 상태나 메시지 복제. 모든 서버 상태는 failover할 때 어플리케이션에 의해 재생성되어 집니다.

Binary Star에서 사용하는 주요 용어는 다음과 같습니다. :

  • Primary ? 기본서버이며, 일반적으로 ‘master’라는 한 개의 서버 입니다.
  • Backup - 백업서버는 일반적으로 'slave'하는 한 개의 서버 입니다. 이것은 primary서버가 문제가 있을 때, client어플리케이션이 백업서버에 연결하도록 요청할때 master가 됩니다.
  • Master ? 마스터서버는 client 연결을 받는 Binary Star pair의 하나 입니다. 정확히 하나의 마스터 서버는 항상있습니다.
  • Slave - 슬레이브 서버는 마스터가 사라지면 takeover되는 하나의 시스템입니다. Binary Star pair가 정상적으로 동작할때를 보면, primary서버는 master이고 bakup은 slave입니다. Failover가 발생했을 때 역할이 전환 됩니다.

Binary Star pair를 구성하기 위해 필요사항 입니다. :

  1. backup서버가 어디에 있는지 primary서버에게 알립니다.
  2. primary서버가 어디에 있는지 backup서버에게 알립니다.
  3. 옵션으로, 두 서버를 동일하게 해서 failover응답시간을 조절합니다.

주요 튜닝은 얼마나 자주 서버의 상태를 체크하고, 얼마나 빠르게 failover하기를 원하느냐에 달려 있습니다. 예제에서, failover timeout이 2000msec로 기본설정되어 있습니다. 만약 이것을 줄일려면, backup서버가 빠르게 master서버로 takeover해야 하지만, primary서버가 복구되는 경우에는 그 이상이 걸릴 수 있습니다. 예를 들어, 재시작하는데 crash가 발생하는 경우 쉘 스크립트에 기본서버로 설정 할 수 있습니다. 이 경우에 제한 시간은 primary 서버를 다시 시작하는 데 필요한 시간보다 더 많이 듭니다.

Binary Star pair에서 동작하는 client어플리케이션을 위해서는,

  1. 두 서버 주소를 알아야 합니다
  2. 장애가 발생했다면 primary서버에서 backup서버로 접속 시도를 합니다.
  3. 장애 접속을 감지하기 위해 일반적으로 heartbeating을 사용합니다.
  4. 적어도 서버 failover 타임아웃보다 높은 재시도 간격으로 primary, 그다음 backup서버로 재연결을 시도합니다.
  5. 서버에 필요한 모든 상태를 재 생성 합니다.
  6. 메시지가 reliable하다면 failover하는 동안 발생된 메시지를 재 송신 합니다.

이것은 사소한 작업이 아니며, 실제 최종 사용자 어플리케이션에서는 이것들이 API로 제공됩니다.

이것은 Binary Star 패턴의 주요 제한사항 입니다. :

  • 서버 프로세스가 하나 이상의 Binary Star pair의 일부가 될 수 없습니다.
  • primary 서버는 단일 백업 서버를 가집니다.
  • backup 서버가 slave모드에서 유용한 일을 할 수 없습니다.
  • backup서버는 전체 어플리케이션 부하를 처리할 수 있어야합니다.
  • failover 구성은 런타임에 수정할 수 없습니다.
  • client어플리케이션은 failover를 위해 몇 가지 작업을 해야합니다.

Preventing Split-Brain Syndrome

top prev next

"Split-brain syndrome"는 클러스터의 다른 부분들이 동시에 ‘master’라고 생각할 때입니다. 어플리케이션이 서로 바라보는 것을 멈추게 하는 원인이 됩니다. Binary Star는 3방향 결정 메커니즘에(서버가 어플리케이션 연결 요청을 얻거나 peer서버를 볼 수 없을 때까지 마스터가 되는 것을 결정하지 않을 것입니다.) 기초하여 split brain을 감지하고 제거를 위한 알고리즘을 가지고 있습니다.

그러나 이것은 알고리즘을 속일 수 있는 네트워크를 (MIS)디자인하는 것은 여전히 가능합니다. 전형적인 시나리오는 어플리케이션 군을 가지고 있는 각 2개의 서버사이에 분산된 Binary Star pair이며, 두 서버사이에 단일 네트워크 링크가 있습니다. 연결이 단절되면 Binary Star pair의 절반 각각이 클라이언트 응용프로그램의 두 세트를 생성하고, 각 failover서버가 활성화 될 것입니다.

Split-brain상황을 방지하기 위해, 우리는 같은 스위치로 두개를 간단하게 연결하거나 더 좋게, 두 서버사이에 직접 cross-over 케이블을 사용하는 것과 같이 전용 네트워크 링크를 사용하여 Binary Star pairs를 연결해야 합니다.

우리는 어플리케이션 군으로 각각 두 지점의 Binary Star 아키텍쳐를 구분하지 말아야 합니다. 이것은 네트워크 구조의 일반적인 유형인 반면, 우리는 이러한 경우에 high-availability failover가 아닌 federation을 사용합니다.

적당한 paranoid 네트워크 구성은 단일 보다는 두개의 개인 클러스터 상호연결을 사용합니다. 더욱이, 클러스터를 위해 사용된 네트워크 카드는 메시지 in/out을 위해 사용된 것들과 다를수 있으며, 아마도 서버 하드웨어에 다른 PCI 경로에 대한 것입니다. 클러스터내의 가능한 실패로부터 네트워크내의 가능한 실패를 분류하는 것이 목표입니다. 네트워크 포트는 상대적으로 높은 실패율을 가집니다.

Binary Star Implementation

top prev next

이것은 Binary Star 서버를 구현한 것입니다. :


Haxe | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이것은 클라이언트입니다. :


Haxe | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

Binary Star를 테스트 하려면, 서버, 그리고 클라이언트를 시작합니다. :

bstarsrv -p     # Start primary
bstarsrv -b     # Start backup
bstarcli

그런 다음 primary서버를 죽이는 것에 의해 failover를 발생시킬수 있으며, backup서버를 죽이고, primary서버를 재시작하는 것으로 복구할 수 있습니다. client vote가 failover와 recovery를 어떻게 유발하는지에 유의 하세요.

아래 그림은 fine state machine을 보여 줍니다. 녹색 states는 cient 요청을 받고 분홍색 states는 그것을 거부합니다. 이벤트는 peer state이므로, “Peer Active”는 그것의 active를 알려주는 다른 서버를 의미 합니다. “Client request”는 우리가 client요청을 받았다는 것을 의미 합니다. ‘Client Vote”는 client요청을 받았고 peer는 두개의 heartbeats에 의해 비활성 상태라는 것을 의미 합니다.

fig63.png

서버는 상태교환을 위해 PUB-SUB을 사용합니다. 다른 소켓의 조합은 여기서 사용하지 않습니다. 메시지를 받을 준비가 된 peer가 없으면 PUSH와 DEALER는 기다립니다. PAIR는 peer가 사라지고 다시 돌아와도 재연결하지 않습니다. ROUTER는 메시지를 보내기 전에 peer의 주소가 필요합니다.

이것은 이진 스타 패턴의 주요 제한사항입니다.

  • 서버 프로세스가 하나 이상의 Binary Star pair의 일부가 될 수 없습니다.
  • Primary서버는 단일 백업 서버를 가집니다. (많은 백업서버가 아닌)
  • Backup 서버가 slave모드에서 유용한 일을 할 수 없습니다.
  • Backup 서버는 전체 응용 프로그램 부하를 처리할 수 있어야합니다.
  • Failover 구성은 런타임에 수정할 수 없습니다.
  • 클라이언트 응용 프로그램은 failover를 위해 몇 가지 작업을해야합니다.

Binary Star Reactor

top prev next

Binary Star는 유용하고, 재사용 reactor class로써 일반적으로 패키지화 합니다. 다른언어에서는 달리 사용될 수 있지만, C에서 우리는 czmq의 zloop클래스를 랩핑(wrapping) 합니다. C에서 bstar인터페이스는 다음과 같습니다. :

// Create a new Binary Star instance, using local (bind) and
// remote (connect) endpoints to set-up the server peering.

bstar_t *bstar_new (int primary, char *local, char *remote);

// Destroy a Binary Star instance
void bstar_destroy (bstar_t **self_p);

// Return underlying zloop reactor, for timer and reader
// registration and cancelation.

zloop_t *bstar_zloop (bstar_t *self);

// Register voting reader
int bstar_voter (bstar_t *self, char *endpoint, int type,
zloop_fn handler, void *arg);

// Register main state change handlers
void bstar_new_master (bstar_t *self, zloop_fn handler, void *arg);
void bstar_new_slave (bstar_t *self, zloop_fn handler, void *arg);

// Start the reactor, ends if a callback function returns -1, or the
// process received SIGINT or SIGTERM.

int bstar_start (bstar_t *self);


Haxe | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

다음은 서버를 위한 짧은 주 프로그램 입니다. :


Haxe | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

Brokerless Reliability (Freelance Pattern)

top prev next

우리는 “brokerless messaging”으로써 ØMQ를 설명할 때 broker-based신뢰성에 대해서 너무 많은 초점을 두었습니다. 그러나 실상황의 메시지 입장에서 broker는 부담과 이익 모두를 가지고 있습니다. 실제로, 대부분의 메시징 구조는 분산되고 브로커로 된 메시징의 혼합에서 이익을 누릴 수 있습니다. 당신이 중점을 두는 tradeoff에 의해 자유롭게 결정했을 때 최고의 결과를 얻을 수 있습니다. 그리고 이것은 최적의 메시지 기반 아키텍쳐에 필수 입니다.

Brokers를 만드는 도구가 있어도 broker중심의 구조를 도입하지 않는 이유는, 일명 “devices”, 그리고 단지 실상황을 위해서 우리는 지금까지 수십번 다른 것들을 만들어 왔습니다.

그래서 우리가 지금까지 만든 broker-based를 사용하지 않는 것으로 이 장을 끝낼 것이며, 우리가 “Freelance 패턴”라고 부르는 분산 peer-to-peer 구조로 변경할 것입니다. 우리의 사용사례는 resolution service 로 명명 될 것 입니다. 이것은 ØMQ아키텍쳐에서 일반적인 문제입니다. : 어떻게 연결할 수 있는 endpoint을 알 것인가? 코드에서 Hard-coding TCP/IP는 굉장히 취약합니다. 구성파일을 만들어 관리를 해야 합니다. 당신이 "google.com"는 "74.125.230.82"이라고 인식하기 위해, 당신이 사용하는 모든 PC 나 휴대 전화, 웹 브라우저를 직접 구성해야 한다고 상상해보십시오.

ØMQ name service (그리고 우리는 간단히 구현해 볼 것입니다.)가 해야만 하는 것 입니다. :

  • 논리적 이름을 bind endpoint나 connect endpoint로 해석합니다. 실제 name service는 여러 bind endpoint와 가능한 여러 connect endpoint를 제공합니다.
  • 여러 병렬 환경(예, 코드변경없이 “test” 대 “production”) 관리 제공
  • 만약 이것이 불가능하기 때문에 안정적이며, 어플리케이션이 네트워크에 연결할 수 없게 됩니다.

Service-oriented Majordome broker 뒤에 name service를 넣는 것은 몇 몇 관점에서는 현명 합니다. 그러나 이것은 간단한 일이며, client가 직접 연결할 수 있는 서버로 name service를 노출시키는 것은 그렇게 놀라운 것이 아닙니다. 만약 우리가 이렇게 한다면, name service는 우리의 코드나 구성파일에 직접 입력이 되는 유일한 것이 global network endpoint가 됩니다.

우리가 처리해야 하는 실패의 유형은 server crashes, server busy looping, server overload 그리고 network issues입니다. 안정성을 획득하기 위해서, 우리는 한 서버가 충돌하고 사라지더라도 client가 다른 서버에 접속할 수 있도록 name servers의 pool을 만드는 것입니다. 실제로 두 개면 충분하지만, 예제에서 우리는 pool을 임의 크기로 할 것입니다. :

fig64.png

이 아키텍쳐에서 client의 큰 집합은 직접 작은 서버 집합에 연결합니다. 서버는 각각의 주소로 바인딩합니다. 이것은 workers가 broker에 접속하는 Majordomo와 같은 broker-based 접근방식과 근본적으로 다릅니다. Client를 위한 몇가지 옵션이 있습니다.

  • lients는 REQ 소켓과 Lazy Pirate 패턴을 사용 할 수 있습니다. 간단하지만 계속해서 죽은 서버에 재접속하지 않기 위해 몇가지 추가적인 정보가 필요합니다.
  • Clients는 DEALER소켓을 사용할 수 있으며, 응답을 얻을 때까지 모두 연결된 서버에 로드밸런스되는 외부 요청을 받습니다.
  • Clients는 특정 서버를 주소화하기 위해 ROUTER소켓을 사용할 수 있습니다. 그러나 client는 어떻게 서버소켓의 ID를 알수 있나요? 서버가 처음에 client에 ping을 하던가, 각서버는 client에 해당하는 고정ID를 직접 입력해야 합니다.

Model One - Simple Retry and Failover

top prev next

지금 볼 것은 simple, brutal, complex, nasty 입니다. 먼저 ‘simple’로 시작합시다. 우리는 Lazy Pirate을 가지고 다중서버 endpoints로 재작성했습니다. 먼저 서버를 시작하고 인자로써 bind endpoint를 명시합니다. 하나 혹은 여러 서버를 시작합니다. :


Lua | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

다음 인자로 하나 이상의 연결 endpoints을 지정하여 클라이언트를 시작합니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

예를 들면 다음과 같습니다. :

flserver1 tcp://*:5555 &
flserver1 tcp://*:5556 &
flclient1 tcp://localhost:5555 tcp://localhost:5556
  • 단일서버경우, 클라이언트는 Lazy Pirate에 대해 정확하게, 여러 번 다시 시도합니다.
  • 다중서버경우, 클라이언트는 답장을 받을 때까지 아니면, 모든 서버를 거의 한번씩 시도 합니다.

이것은 Lazy Pirate의 주요 약점을(즉 백업/대체 서버로 failover할 수 없음)해결합니다,

그러나 이 디자인은 실제 응용 프로그램에서 잘 작동하지 않습니다. 우리는 많은 소켓을 연결하고 있으며, 우리 주 이름 서버가 다운되면, 우리는 이 고통스러운 타임아웃를 매번 처리해야 할 겁니다.

Model Two - Brutal Shotgun Massacre

top prev next

이제 DEALER 소켓을 사용하기 위해 클라이언트를 바꿔봅시다. 여기서 우리의 목표는 가능한 최단 시간 내에 primary서버의 다운 여부에 상관없이 답변을 얻도록 하는 것입니다. Client는 이 접근 방식을 취합니다.

  • 우리는 모든 서버에 접속하도록 설정합니다.
  • 요청이 있을 때, 우리가 갖고 있는 서버에 매번 전달합니다.
  • 우리는 첫번째 답변을 기다리고, 그것을 처리합니다.
  • 우리는 다른 답변은 무시합니다.

모든 서버가 동작중일 때 발생되는 것은, ØMQ는 각서버가 한 요청을 얻고 한 응답을 보내도록 요청을 분배할 것입니다. 어떤 서버가 offline이고 disconected일 때, ØMQ는 나머지 서버에 요청을 보낼 것입니다. 그래서 어떤 경우에는 한 서버가 한 개 이상의 같은 요청을 받을 수 있습니다.

client입장에서 번거로운 것은 많은 응답을 받을 수 있지만, 얻게 될 정확한 응답개수를 보장받지 못한 다는 것입니다. 요청과 응답은 유실될 수 있습니다. (예, 요청을 처리하는 동안 서버가 죽는 경우)

그래서 우리는 요청에 숫자를 매겨야 하며 요청 숫자에 일치하지 않는 응답은 무시해야 합니다. 한 서버에서 작동하는 echo 서버로 이것을 이해하기에는 적당하지 않습니다. 그래서 우리는 내용으로 “OK”을 가지고 정확한 숫자를 매긴 응답을 리턴하는 두 서버를 만들 것입니다. 우리는 두 부분(순번 + 전문[body])으로 구성된 메시지를 사용할 것입니다.

매번 bind endpoint를 지정한 한 개 이상의 서버를 시작합니다. :


Lua | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

클라이언트를시작하고, 인수(argument)로 endpoint 연결지정을 합니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이 노트에 일부 코드들 입니다. :

  • Client는 ØMQ 컨텍스트와 소켓을 생성하는 복잡한 작업을 간단한 API로 만들어 서버와 통신합니다.
  • Client는 몇 초내에 어떤 응답가능한 서버를 찾지 못한다면 추적(chase)을 포기합니다.
  • Client는 메시지 앞에 빈 메시지 부분을 추가하는 가용한 REP envelope을 만들어야 합니다.

Client는 10,000 이름 확인 요청(서버가 없는 것은 모두 가짜임)을 수행하고 평균 비용을 측정합니다. 우리의 테스트에서 한서버에 통신하는 것은 약 60usec가 걸립니다. 3서버에 통신하는 것은 약 80usec가 걸립니다.

shotgun 방식의 장단점 :

  • 장점 : 간단합니다. 만들고 이해하기에 쉽습니다.
  • 장점 : Failover 작업을 수행하며, 적어도 한서버가 동작중이라면 빠르게 동작합니다.
  • 단점 : 불필요한 네트워크 트래픽을 생성합니다.
  • 단점 : 서버의 임의 우선순위를 지정 할 수 없습니다. 즉, Primary 다음 secondary 입니다.
  • 단점 : 서버는 한 번에 한 요청을 처리할 수 있습니다.

Model Three - Complex and Nasty

top prev next

Shotgun 방식은 사실 너무 좋은 것 같습니다. 과학적으로 모든 대안을 가지고 해 봅시다. 결국 어렵게 밖에 파악할 수 없는 경우에는 복잡하고 잡다한 경우와 옵션들을 일일이 해보는 것입니다.

우리는 ROUTER소켓으로 변환하여 client의 주요 문제들을 해결할 수 있습니다. 우리는 죽은 서버는 피하고 일반적으로 만들 수 있는 한 현명하게 특정서버에 요청을 보낼 수 있습니다. 우리는 또한 ROUTER소켓으로 전환하여 서버(단일 threadedness)의 주요 문제점을 해결 할 수 있습니다.

그러나 두 transient소켓 사이의 ROUTER-to-ROUTER를 하는 것은 불가능합니다. 양쪽은 첫 메시지를 받을 때만 ID를 생성하고, 따라서 첫 메시지를 받을 때까지 다른 것과 통신할 수 없습니다. 이것을 해결하는 유일한 방법은 속임수를 쓰는 것입니다. 즉, 한 방향으로 ID를 하드코딩해서 사용하는 것입니다. 클라이언트와 서버 경우에는 클라이언트가 서버ID를 알고 있는 것입니다. 그 반대로 하는 것은 복잡하고 미친짓 입니다.

다른 개념을 발명하기 보다, 우리는 ID로 연결 endpoint를 사용할 것입니다. 이것은 이미 가지고 있는 shotgun모델보다 더 사전 지식없이 양쪽이 동의할 수 있는 고유 문자열 입니다. 그것은 두 ROUTER소켓을 연결하는 엉큼하고 효과적인 방법입니다.

ØMQ ID 처리 방식을 기억하세요. 이것은 소켓에 바인딩하기 전에 서버 ROUTER소켓이 ID를 설정합니다. Client가 연결할 때 양쪽이 실제 메시지를 보내기 전에 ID를 교환하기 위해 handshake를 합니다. ID를 가지고 있지 않은 client ROUTER소켓은 서버에 ID를 null로 보냅니다. 서버는 client를 위해 임의의 UUID를 생성합니다. 서버는 client에 그 ID를 보냅니다.

이것은 client가 연결이 되자마자 서버에 메시지를 보낼 수 있다는 것을 의미 합니다. 이것은 zmq_connect한후 즉시는 아니지만 어떤 임의의 시간 이후 입니다. 여기서 한가지 문제점 : 우리는 언제 서버가 실제적으로 가용하고 연결 handshake를 완료 했는지를 모릅니다. 만일 서버가 실제로 온라인 상태라면 이것은 몇 milliseconds 후 일 수 있습니다. 만일 서버가 다운되고 관리자가 점심 먹으로 갔다면 1시간이 될 수도 있습니다.

그래서 우리는 서버가 연결되어 있고 작동이 가능한지 알아야 할 필요가 있습니다. Freelance패턴에서 broker-based 패턴과는 달리 우리가 이전 장에서 보았듯이, 서버는 연결하기 전까지 가만히 기다리고 있습니다. 그래서 우리가 이것에 요청할 때까지 아무것도 할 수 없고, 이것이 온라인이 되어 우리에게 연결할 때까지 우리는 서버와 통신할 수 없습니다.

내 솔루션은 model 2에서 shotgun방식의 조금을 혼합한 것입니다. 우리는 실제 요청 보다는 ping-pong heartbeat의 종류를 fire하지 않을 것입니다.

이것을 다시 프로토콜 영역에서 보면, 여기 Freelance client와 서버가 PING-PONG 명령, request-reply명령을 교환하는 방법을 정의하는 간단한 사양(spec)이 있습니다. :

이것은 간단히 서버로 구현한 것입니다. 여기 Model Tree, echo서버가 있습니다.

서버의 Model Three는 약간 다릅니다. :


Lua | Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

그러나, Freelance client는 좀 큽니다. 명확하게, 이것은 예제 어플리케이션과 작업을 수행하는 클래스로 분리했습니다. 이것이 상위 어플리케이션입니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

그리고 여기 Majordomo broker처럼 복잡하고 큰 클라이언트 API 클래스가 있습니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이 API 구현은 매우 정교하고 우리가 전에 보지 못했던 기술을 사용합니다. :

Asynchronous agent class

Client API는 두 부분, 어플리케이션 thread에서 동작하는 동기 ‘flcliapi’와 백그라운드에서 동작하는 비동기 ‘agent’클래스로 구성됩니다. Flcliapi와 agent 클래스는 inproc소켓을 통해 서로 통신합니다. 모든것은 ØMQ API에(예 : 컨텍스트를 생성하고 종료 등) 숨겨져 있습니다. Mini-broker와 같은 효과적인 동작을 위해 agent는 우리가 요청 할 때, 가용한 서버에 도달하기 위해 최선의 노력을 할 수 있도록 백그라운드에서 서버와 통신합니다.

Patient connections

ROUTER소켓은 라우팅 할 수 없는 메시지를 조용히 없애는 기능이 있습니다. 이것은 당신이 client를 서버(ROUTER-to-ROUTER)에 연결하고 즉시 메시지를 보내려고 한 다면 작동하지 않는다는 것을 의미 합니다. flcliapi클래스는 어플리케이션이 서버에 초기 연결을 할 때 잠시 아무 동작도 하지 않습니다. 이후 durable 소켓 때문에, ØMQ는 서버가 사라진다고 해도 서버의 메시지를 없애지 않습니다.

Tickless poll timer

이전에서 루프는 항상 고정된 값을 사용했습니다. 예를 들어 1초, 이것은 항상 살아 있어야 되는 CPU 파워 비용이 드는 노트북과 모바일 폰과 같은 전원에 민감한 client에서는 훌륭하진 않지만 적당합니다. 재미로, 지구를 구하기 위해 agent가 기대하는 다음 timeout에 근간해서 지연을 계산하는 ‘tickless timer’를 사용합니다. 적절한 구현은 정해진 timeouts 목록을 유지하는 것입니다. 우리는 그냥 시간 초과를 확인하고 다음 때까지 지연을 계산합니다.

Conclusion

top prev next

이 장에서 우리는 각기 어떤 비용과 이익을 가지고 있는 신뢰할 수있는 request-reply 메커니즘의 다양한 면을 봤습니다. 이것이 최적은 아니지만 예제 코드는 실제 사용 가능하도록 대부분 준비가 된 것입니다. 모든 패턴중 broker-based 신뢰성을 위한 Majordomo패턴과 brokerless 신뢰성을 위한 Freelance패턴 두가지는 뛰어 납니다.

Chapter Five - Advanced Publish-Subscribe

top prev next

제 3,4장에서 우리는 ØMQ의 request-reply 패턴을 사용해 봤습니다. 모든 것을 이해 했다면 축하합니다. 본 장에서는 publish-subscrib에 대해서 다룰 것이며, 성능, 신뢰성, 상태 배포, 보안을 위한 높은수준의 유형들(higher-level patterns)을 포함해서 ØMQ의 핵심인 pub-sub 유형을 살펴 볼 것입니다.

본장에서 다룰 내용:

  • too-slow subscribers 를 처리하는 방법 (the Suicidal Snail pattern).

*high-speed subscribers를 설계하는 방법 (the Black Box pattern).
*shared key-value cache를 구축하는 방법 (the Clone pattern).

Slow Subscriber Detection (Suicidal Snail Pattern)

top prev next

실생활에서 pub-sub 패턴을 사용할 때 이슈가 될만한 문제는 slow subscriber 입니다. 일반적으로, publishers는 subscribers에게 최고속도로 데이터를 보냅니다. 현실적으로 subscriber applications은 Interprited언어로 작성 되거나, 많은 작업을 수행하며, 또는 Publisher의 부하를 유지하지 못할 정도가 될 수 있습니다.

어떻게 slow subscriber를 처리할 수 있을까요? 더 빠른 subscriber를 만들기 위해서는 시간과 노력이 많이 필요합니다. slow subscriber를 해결하기 위한 전형적인 전략 몇 가지 있습니다. :

  • Queue messages on the publisher. 몇 시간 동안 메일을 읽지 않았을 때 하는 GMAIL이 하는 행동 입니다. 그러나 high-volume messaging(대용량 메세징)에서, queue에 publishing할 수 있지만, publisher는 메모리 부족과 오작동의 결과가 초래될 수 있습니다. 특히, subscriber가 많다면 성능차원에서 디스크 flush가 원활하지 않을 수 있습니다.
  • Queue messages on the subscriber. 이 방식은 좀 더 좋은 방법이며, ØMQ가 기본적으로 제공하는 기능입니다. 큐를 사용해서 메모리 부족과 오작동이 발생하면 publisher보다는 subscriber쪽이 낫습니다. 이것은 subscriber가 수용할 수 없는 최고부하 시점에는 큐잉을 하고 여유로운 시점에는 나머지를 처리할 수 있는 완벽한 방법입니다. 그러나 subscriber가 너무 느리다면 문제가 발생됩니다.
  • Stop queuing new messages after a while. 이것은 메일 함 용량이 7.555GB가 넘을 때 Gmail이 하는 것입니다. 신규 메시지가 바로 거부되거나 삭제 됩니다. 이것은 publisher의 관점에서 훌륭한 전략이며, publisher가 최고 수위 점 or HWM 을 설정하였을 때 ØMQ가 하는 것이다. 그러나 그것은 여전히 우리가 slow subscriber를 해결하는데 도움이 되지 않습니다. 지금, 우리는 message stream에서 해결할 것입니다.
  • Punish slow subscribers with disconnect. 15번째 Hotmail 계정에 2주 동안 Login 하지 않을 때 Hotmail이 하는 것입니다 이것은 subscriber에서 주의를 요하게 하며, 이상적 일수도 있지만, ØMQ는 이렇게 처리하지 않습니다.

이것은 나의 15번째 Hotmail 계정에, 내가 2주 동안 Login 하지 않을 때 Hotmail이 하는 것입니다. 이것은 subscriber에서 주의를 요하게 하며, 이상적 일수도 있지만, ØMQ는 이렇게 처리하지 않습니다.
이러한 전형적인 전략에 적합한 것은 아무것도 없습니다. 그래서 창의성이 필요합니다. Publisher가 연결을 끊는 것 보다는 subscriber 자체가 죽도록 하는 하는 것이 좋을 것 같습니다. 이것이 the Suicidal Snail pattern 입니다. Subscriber는 너무 느리게 동작한다고 판단될 때 죽는 것입니다.
Subscriber는 어떻게 이것을 감지할 수 있습니까? 한가지 방법은 메시지에 순서대로 번호를 (순서대로 숫자를 매기는 것) 부여하고, publisher에 HVM 를 적용하는 것 입니다. Subscriber가 GAP(예: 번호가 연속되지 않습니다.)를 발견한다면, 그것은 무언가 잘못된 것을 알게 되는 것입니다. 우리는 적당하게 죽는 수준을 정하기 위해 HVM을 조정합니다.

이 솔루션에는 2가지 문제가 있습니다. 첫째, 많은 publisher가 있을 때, 우리는 어떻게 messages에 시퀀스를 매깁니까? 이 솔루션은 각 publisher에게 유일한 ID를 주고, 시퀀스를 추가해야 합니다. 둘째, subscribers가 ZMQ_SUBSCRIBE 필터를 사용하면, 정의된 것에 의해서 GAP이 생깁니다. 그러면 시퀀스를 부여하는 것은 무의미 해집니다.

일부 use-case에서 필터를 사용하지 않으면 시퀀스는 잘 처리 될 것입니다. 그러나 보다 일반적인 솔루션은 publisher가 각각의 메시지에 timestamps 사용하는 것입니다. Subscriber가 메시지를 받았을 때 시간을 확인하고, 그 차이가 1초 이상이라면, 그것은 죽는 것입니다.
The Suicide Snail pattern은 subscribers들의 자신의 clients 와 service-level agreements을 가지고, 특정 최대 latencies를 보장할 때 동작합니다. Subscriber를 중지하기 위해 최대 latency보장하는 좋은 방법처럼 보이지 않을 수도 있지만 그것은 assertion model입니다. 오늘이 지나면, 그 문제는 해결될 것입니다. 늦게 데이터를 처리하도록 허락하면, 그 문제가 더 광범위하게 영향을 주는 원인이 되고, 원인 분석하는데 시간이 오래 걸리 수 있습니다.

이것은 Suicidal Snail에 대한 예제 입니다. :


C++ | Lua | PHP | Python | Ada | Basic | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Node.js | Objective-C | ooc | Perl | Ruby | Scala

이 예제에 대한 참고 사항:

  • 이 메시지는 여기에 milliseconds 번호로 현재 시스템 시간으로 단순하게 구성되어 있습니다. 현실적인 application에서는 적어도 timestamp를 포함한 메시지 header와 데이터 메시지 body를 가져아 합니다.
  • 예를 들어 한 프로세스에 두개의 스레드로 subscriber 와 publisher가 있습니다. 실제로 그들은 별개의 프로세스 입니다. 스레드를 사용하면 데모가 좀더 편리 합니다.

High-speed Subscribers (Black Box Pattern)

top prev next

pub-sub에 대한 일반적인 use-case는 다량의 데이터 스트림을 배포하는 것입니다. 예를 들어 증권거래소의 ‘시장 데이터’ 입니다. 전형적으로 publisher는 주식을 거래하기 위해 연결하고, 값을 책정한 다음 subscriber에게 그것을 보냅니다. Subscriber가 소수라면, 우리는 TCP를 사용할 수 있으며, Subscriber가 다수라면, 우리는 아마 신뢰할 수 있는 멀티 캐스트, 즉 PGM을 사용할 것입니다..
100bytes메시지를 초당 평균 100,000을 처리한다고 생각해 봅시다. 이것은 전형적인 속도입니다. 초당 100K 메시지는 ØMQ application에서 쉽게 처리할 수 있으며, 우리는 훨씬 더 빠른 처리를 원합니다.
그래서 우리는 publisher 하나, 각 subscriber당 하나씩 서버로 구성합니다. 8개 core, 12개 publisher로 잘 정의된 서버들이 있습니다. (2015년에 이것을 읽는다면, 그때 가이드는 완료될 예정입니다. 이 숫자에 0을 추가 하십시오.)

그리고 이것은 subscriber에게 데이터를 넣을 때, 두 가지 주의사항 입니다. :

  1. 우리가 메시지 처리를 아주 조금 처리 했어도, 다시 publisher의 처리를 따라 잡을 수 없는 지점에서 subscriber 속도가 느려집니다.
  2. 우리는 주의 깊게 최적화와 TCP를 튜닝 한 후, 약 초당 6M 메시지를 처리하도록 Publisher와 subscriber 모두에게 부하를 줄 것입니다.

우리가 해야 하는 첫 번째 일은 멀티스레드로 설계에 subscriber를 끼워 넣는 것입니다. 그래서 다른 스레드에서 메시지를 읽는 동안 스레드 중 한 세트에서 메시지를 처리 할 수 있습니다. 일반적으로 우리는 똑같은 방법으로 모든 메시지를 처리하기를 원하지 않습니다. 오히려 subscriber는 아마 prefix key로 대부분의 메시지를 필터링 합니다. 메시지가 어떤 기준과 일치하면, subscriber는 메시지를 처리하는 작업자를 호출합니다. ØMQ에서 이것은 작업자 스레드에게 메시지를 보내는 것을 의미합니다.

그래서 subscriber는 queue device 같은 것으로 보입니다. 우리는 subscriber와 작업자들을 연결하는 다양한 소켓을 사용할 수 있습니다. 만약 단 방향 트래픽 이고 모두 동일한 작업을 처리한다면 우리는 PUSH와 PULL 사용할 수 있으며, ØMQ에 모든 라우팅 작업을 위임할 수 있습니다. 이것은 가장 간단하고 빠른 접근방법입니다. :

fig65.png

Subscriber는 TCP 또는 PGM을 통해 Publisher와 통신합니다. Subscriber는 inproc를 통해 모두 동일한 프로세스로 Workers와 이야기 합니다.
이제 한계를 벗어날 때 입니다. subscribe 스레드가 CPU 100 %에 도달한 일이 발생한 것은 하나의 스레드이기 때문이며, 그것은 하나 이상의 코어를 사용할 수 없습니다. 단일 스레드는 항상 초당 2M, 6M, 또는 그 이상 메시지로 한계에 도달할 것입니다. 우리는 다수의 스레드에 걸쳐 작업을 분할하기를 원하며, 병렬로 실행할 수 있습니다.

많은 고성능 제품에서 사용하는 접근방법은 분할입니다. 병렬 및 독립적인 스트림으로 작업은 분할한다는 것을 의미 합니다. 예로 topic key의 절반은 한 스트림에서, 절반은 다른 스트림으로 실행 합니다. 우리는 많은 스트림을 사용할 수 있지만, 여유 cpu가 없다면 성능이 향상되지 않습니다.

그래서 두 스트림으로 조작하는 방법을 보겠습니다. :

fig66.png

두 스트림에서 최대 속도로 처리하기 위해, 우리는 다음과 같이 ØMQ를 구성하는 것입니다. :

  • 하나보다는 두 개의 I / O 스레드.
  • 두 개의 네트워크 인터페이스 (NIC), 각 subscriber마다 하나씩.
  • 각 I / O 스레드는 특정 NIC에 바인딩
  • 특정 코어에 바인딩된 두 subscriber스레드.
  • 두 개의 SUB 소켓, 각 subscriber스레드 마다 하나씩.
  • 나머지 코어들은 작업자 스레드들에게 할당.
  • 작업자 스레드는 양쪽 subscriber PUSH 소켓에 연결

우리의 아키텍쳐는 이상으로 스레드당 한 코어를 가집니다. 일단 우리가 코아보다 더 많은 스레드를 생성하면, 스레간의 경합이 발생하고, 반환이 점점 떨어질 겁니다. 예를들어 더 많은 I/O 스레드를 생성하는 것은 어떤 이득도 없을 것입니다.

A Shared Key-Value Cache (Clone Pattern)

top prev next

pub-sub는 라디오 방송과 유사합니다. 당신이 가입하기 전엔 아무것도 없으며, 당신이 얻고자 하는 수많은 정보는 당신이 수용하려는 양에 의존합니다. 놀랍게도 “완벽”을 목포로 하는 엔지니어들에겐 이 모델이 정보의 실제 배포와 완벽하게 매치되기 때문에 유용하고 넓게 확산되어 있습니다. 페이스 북과 트위터, BBC 월드 서비스 및 스포츠 결과를 생각해 보세요.

그러나 가능하다면, 보다 신뢰가능한 pub-sub이 가치가 있는 많은 경우가 있습니다. 우리가 request-reply 위해 했던 것처럼 오류 동작하는 시각에서 신뢰성을 정의 해야 합니다. pub-subd의 고전적인 문제는 다음과 같습니다. :

  1. Subscriber들의 늦은 가입, 그래서 서버에 이미 전달되어 메시지를 놓친 경우
  2. Subscriber의 연결 속도가 느려, 그 시간 동안 메시지를 잃는 경우.
  3. Subscriber들이 떠나가서, 떠나있는 동안 메시지를 잃는 경우.

드물지만, 이와 같은 문제도 있습니다. :

  1. Subscriber들은 충돌, 재시작 할 수 있고, 이미 받은 데이터를 잃을 수 있습니다.
  2. Subscriber들은 메시지들을 너무 느리게 처리합니다. 그래서 queue에 쌓고, 다음 overflow 될 수 있습니다.
  3. Networks에서 과부하가 발생하고, 데이터를 놓칠 수 있습니다. (specifically, for PGM).
  4. Networks에서 속도가 너무 느려져서, publisher-side queues overflow되고, publishers crash.

더 많이 잘못될 수 있지만 이것은 현재의 시스템에서 볼 수 있는 전형적인 오류입니다.

우리는 “the Suicidal Snail pattern”으로 느린 subscriber처럼 이것들의 몇 가지를 해결 하였습니다. 그러나 나머지는 신뢰성 있는 pub-sub을 위하여 포괄적이고, 재사용할 수 있는 프레워크를 가져가는 것이 좋을 것입니다.

어려운점은 우리의 대상 Application이 실제로 그들의 데이터를 가지고 하고자 하는 것이 무엇인가 라는 아이디어가 없다는 것입니다. 그들은 그것을 필터링하고, 메시지의 하위집합만 처리 합니까? 그들은 나중에 재사용을 위하여 데이터를 어딘가에 기록합니까? 그들은 작업자에게 그 이상으로 데이터를 배포합니까? 그럴듯한 시나리오는 수십가지 있으며, 각각 어떤 신뢰성수단, 노력과 성능측면에서 그것의 가치가 얼마인지에 따라 자신의 아이디어를 가질 수 있습니다.

그래서 우리는 구현할 수 있는 abstraction을 구축할 것입니다. 그리고 많은 applications에서 재사용 합니다. 이 abstraction은 고유 키에 의해 색인된 blobs의 집합으로 저장하는 shared value-key cached 입니다.

분산 네트워크에서 피어(peers) 연결의 광범위한 문제를 해결하는 분산 hash tables나 non ?SQL 데이터베이스와 같은 역할을 하는 분산key-value tables와 혼동하지 마십시오. 구축할 모든 것은 서버에서 클라이언트 세트에 안정적인 일부 메모리상태를 복제하는 시스템입니다. 우리가 원하는 아래와 같습니다. :

  • 클라이언트를 언제든지 네트워크에 연계시키고, 안정적으로 현재 서버 상태를 유지합니다.
  • 모든 클라이언트에 key-value cache를 업데이트 (새로운 key-value pairs를 삽입, or 기존의 key-value로 업데이트, or key-value 등을 삭제)
  • 안정적으로 모든 클라이언트에 변경사항을 전파하고, 이것은 최소한의 잠재 오버헤드 가집니다.
  • 수천, 수만의 클라이언트를 처리합니다.

클론 패터의 핵심은 클라이언트가 서버로 응답을 보낼 수 있다는 것입니다. 이것은 일반적인 pub-sub보다 훨씬 더 발전한 것입니다. 이런 이유로 이 글에서는 publisher, subscriber라는 용어대신 server, client라는 용어를 사용하고 있습니다. 클론은 내부적으로 pub-sub을 사용하기는 하지만 그 이상의 기능을 제공합니다.

Distributing Key-Value Updates

top prev next

이번 단계에서는 한번에 한가지 문제를 해결하도록 하는 Clone을 개발합니다. 첫번째로, 서버에있는 클라이언트들에게 key-value 업데이트를 배포하는 방법을 살펴 보겠습니다. 우리는1장에서 weather server를 다루었으며, 한 쌍의 key-value로 메시지를 보내기 위해 이것을 refactor했습니다. 우리는 해시 테이블에 이들을 저장하기 위해 우리의 클라이언트를 수정합니다. :

fig67.png

이것은 서버입니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

그리고 이것은 클라이언트입니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이 코드에 대한 몇 가지 참고사항 입니다. :

  • 모든 어려운 작업은 kvmsg 클래스에서 이루어집니다. 이 클래스는 key-value 메시지 객체와 함께 작동하며,이 클래스는 3개의 프레임으로 구성된 다중 ØMQ 메시지 입니다. : a key (a ØMQ string), a sequence number (64-bit value, in network byte order), and a binary body (holds everything else).
  • 서버는 randomized 4-digit key로 메시지를 생성합니다. 크지만 엄청나지 않은 hash table(10K entries)을 시물레이션 할 수 있습니다.
  • 서버는 소켓을 바인딩 이후 200 millisecond 동안 정지합니다. 이것은 substriber가 서버의 소켓에 연결할 때 메시지를 잃는 "slow joiner syndrome"을 방지합니다. 우리는 이 "slow joiner syndrome"를 이후 버전에서 해결할 것입니다
  • 우리는 소켓을 참조하는 코드에서 'publisher' 와 'subscriber' 을 사용합니다. 이것은 우리가 다중 소켓들을 가지고 여러가지 일들을 할 때 나중에 도움이 될 것입니다.

현재 동작하는 것 중 가장 간단한 형태의 kvmsg 클래스가 있습니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

우리는 나중에 좀 더 정교한 kvmsg class를 applications에서 사용하기 위해 만들 것입니다.

서버와 클라이언트 모두 hash tables을 관리하지만, 첫번쩨 모델에서 만일 서버보다 모든 클라이언트가 먼저 시작되고, 클라이언트들의 충돌이 전혀 없었다면 제대로 작동합니다. 안정적이진 않습니다.

Getting a Snapshot

top prev next

클라이언트가 지연(또는 복구)현상을 서버를 통해 발견 할 수 있도록 하기 위해서는 서버 상태 snapshot 을 얻어야 합니다. 우리는 "a sequenced key-value pair" 의미를 "message"로 줄인 것 처럼 "a hash table" 의미를 "state"로 줄일 수 있습니다. 서버 상태를 얻으려면, client는 REQ 소켓을 열고 명확하게 요청해야 합니다. :

fig68.png

이 작업을 하기위해서, 우리는 타이밍 문제를 해결해야 합니다. 상태 snapshot을 얻는 것은 snapshot이 큰 경우 아마도 상당히 오랜 특정시간이 소요됩니다. 우리는 snapshot에 정확히 업데이트를 적용해야 합니다. 그러나 서버는 우리에게 업데이트를 보낼 때를 알 수 없습니다. 한가지 방법은 subscribing을 시작하면서 첫 업데이트를 얻고, 다음 “업데이트 N에 대한 상태”를 요청하는 것입니다. 이는 실용적진 않지만 각 업데이트에 대한 하나의 snapshot을 저장하는 것을 서버에 요구하게 됩니다.

그래서 우리는 다음과 같이 클라이언트에서 동기화를 할 것 입니다. :

  • 클라이언트는 먼저 업데이트를 subscribes(승낙or신청or예약)하고 상태 요청을 합니다. 이것은 상태가 가장 오래된 업데이트 보다 최신이란 것을 보장합니다.
  • 클라이언트는 상태와 응답을 서버로부터 받기 위해 대기하며, 그동안 queue들은 모두 업데이트 됩니다. 그것은 단순히 그들을 읽는 것이 아니라 작업을 수행합니다: ØMQ는 우리가 HWM을 설정하지 않기 때문에 소켓 queue에서 그들queue를 유지합니다.
  • 클라이언트가 상태 업데이트를 할 때, 업데이트를 다시 읽기 시작합니다. 그러나 그것은 상태 업데이트 보다 더 오래된 모든 업데이트는 버립니다. 그래서 상태 업데이트가 200까지 업데이트를 포함한다면, 클라이언트는 201까지 업데이트를 버릴 것 입니다.
  • 클라이언트는 자체 상태 snapshot으로 업데이트를 적용합니다

이것은ØMQ 자신의 내부 queues를 이용하는 간단한 모델입니다. 여기 서버는 다음과 같습니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

클라이언트 입니다.:


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이 코드들에 대한 몇 가지 요약입니다. :

  • 서버는 단순한 설계를 위해 두 개의 스레드를 사용합니다. 한 스레드는 임의 업데이트를 수행하고, 두 번째 스레드는 상태를 처리합니다. 두 스레드는 PAIR 소켓을 통하여 통신합니다. 당신은 SUB 소켓을 사용할 수 있지만. 연결하는 동안 subscriber가 무작위로 몇 가지 메시지를 놓치는 “"slow joiner" 문제에 부딪칠 것입니다 PAIR 소켓은 두 개의 스레드를 명확히 동기화 시킵니다.
  • hash table 삽입이 상대적으로 느리기 때문에 우리는 업데이트한 한 쌍의 소켓에 HWM을 설정합니다. 이것이 없으면, 서버의 out of memory가 발생합니다. inproc 연결에서 실제 HWM은 두 소켓의 HWM의 합계이며, 그래서 우리는 각 소켓에 HWM을 설정합니다.
  • 클라이언트는 정말 간단합니다. C에서, 코드는 60 라인 이하입니다. 많은 어려운 부분은 kvmsg 클래스에서 수행하지만, 여전히 기본적인 Clone pattern 은 처음에 보였던 것보다 쉽게 구현됩니다.
  • 우리는 직렬화 상태에 대한 멋진 어떤것도 사용하지 않습니다. hash table 은 kvmsg 개체의 집합을 보유하고, 서버가 클라이언트 요청 상태 메시지의 일괄 처리로 이들을 보냅니다. 다수의 클라이언트가 동시에 상태를 요청하는 경우, 각각 다른 snapshot을 얻을 것입니다
  • 우리는 클라이언트가 할 얘기가 정확히 하나의 서버가 실행이 되고 있다고 가정합니다. : 우리는 서버가 충돌할 때 무엇이 발생하는지에 대한 질문을 답하지는 않을 것입니다..

지금, 이 두 프로그램은 진짜 아무것도 하지 않지만, 그들은 정확하게 상태를 동기화할 수 있습니다. 이것은 다른 패턴을 결합하는 방법의 적절한 사례입니다. PAIR-over-inproc, PUB-SUB, and ROUTER-DEALER.

Republishing Updates

top prev next

두 번째 모델에서, 서버 자체로부터 key-value cache를 변경합니다. 이것은 우리가 각 노드에서 로컬 캐싱과 함께 배포하려는 중앙 설정 파일이 있다면 예제로서 유용한 중앙집중 모델(centralized model )입니다. 더 흥미로운 모델은 서버가 아닌 클라이언트에서 업데이트를 합니다. 서버는 이렇게 상태가 없는 브로커가 됩니다. 이것은 우리에게 몇가지 장점을 제공합니다. :

  • 우리는 서버의 안정성에 대해 좀 덜 걱정하게 합니다. 충돌이 발생하는 경우, 우리는 새 인스턴스를 시작하고 새로운 값을 줄 수 있습니다.
  • 우리는 동적인 peers간의 지식을 공유하는 key-value cache를 사용할 수 있습니다.

클라이언트로부터의 업데이트는 클라이언트에서 서버로 PUSH-PULL 소켓 흐름을 통해 이동합니다. :

fig69.png

왜 클라이언트가 다른 클라이언트에게 직접 업데이트를 게시(publish)하는 것을 허용하지 않을까요? 이것이 지연시간을 줄일 것이지만, 그것이 메시지에게 고유한 시퀀스 번호를 오름차순으로 할당하는 것은 불가능하게 만듭니다. 서버가 이 작업을 수행할 수 있습니다. 더 미묘한 두 번째 이유가 있습니다. 많은 응용 프로그램에서 많은 클라이언트에 걸쳐 업데이트를 단일 명령으로 하는 것은 중요합니다. 서버를 통해 모든 업데이트를 강행하는 것은 결국 클라이언트에 도달했을 동일한 명령이 수행된 것을 보장하는 것입니다.

고유의 시퀀스를 통해 클라이언트는 애먹이는 오류를 감지할 수 있습니다 - 네트워크 혼잡 및 큐 오버플로 입니다. 만일 클라이언트가 수신 메시지 스트림의 결함을 발견하면, 그것은 조치를 취할 수 있습니다. 이것은 클라이언트가 서버에 접속하여 누락된 메시지를 요청하듯 현명한 것 같지만, 실제로 그렇게 유용하지 않습니다. 만일 결함(hole)이 있다면, 그들은 네트워크 스트레스에 의해 발생하고 있으며, 네트워크에 더 많은 스트레스를 추가하면 상황은 악화 되기만 합니다. 모든 클라이언트는 정말 "계속 할 수 없습니다", 중지하고, 누군가가 수동으로 문제의 원인을 확인하기 전까지는 다시 시작되지 않습니다 라고 사용자에게 경고 할 수 있습니다.

우리는 이제 클라이언트에서 상태 업데이트를 생성하실 수 있습니다. 서버는 다음과 같습니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이것은 클라이언트입니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이 코드에 대한 몇 가지 참고 사항 :

  • 서버는 클라이언트에서 업데이트를 수집하고 그들을 재 배포하는 하나의 스레드로 나뉘게 됩니다. 그것은 수신되는 업데이트에 대한 PULL 소켓, 상태 요청에 대한 ROUTER 소켓, 발신 업데이트에 대한 PUB 소켓을 관리합니다.
  • 클라이언트는 일초에 한번 서버에 임의의 업데이트를 전송하는 단순하고 소리없는 타이머를 사용합니다. 실제는 업데이트가 응용 프로그램 코드에 의해 구동됩니다

Clone Subtrees

top prev next

실질적인 key-value cache는 다수(다량)의 것을 얻을 것이며, 클라이언트는 일반적으로 cache의 일부에 관심을 가질 것입니다. 하위트리(subtree)로 작업하는 것은 상당히 간단합니다. 클라이언트는 상태 요청을 보낼 때 서버 subtree에게 전달하며, 그것이 업데이트를 동의하면 동일한 subtree를 지정합니다.

trees에 대한 두 가지 공통 구문이 있습니다. 하나는 "경로 계층 구조(path hierarchy) "이며, 다른 하나는 "항목 트리(topic tree) "입니다. 이것은 이래와 같습니다. :

  • Path hierarchy: "/some/list/of/paths"
  • Topic tree: "some.list.of.topics"

우리는 path hierarchy를 사용할 것이고, 클라이언트는 단일 subtree와 함께 작업할 수 있도록 클라이언트와 서버를 확장합니다. 다수의 subtree로 작업하는 것은 어렵지 않으며, 여기서 보여 주지는 않지만 간단합니다.

여기 모델3을 조금 변경한 서버 입니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

클라이언트입니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

Ephemeral Values

top prev next

동적으로 만료되는 임시값은 하나입니다.. 당신이 DNS와 같은 서비스를 위해 복제를 생각한다면, 임시 값은 동적 DNS를 만들도록 할 것입니다. 노드는 네트워크를 연결하고, 그것의 주소를 할당하고며, 정기적으로 확인합니다. 만일 노드가 죽으면, 그 주소는 결국 제거됩니다.

임시 값에 대한 일반적인 개념는 "세션"에 연결하고, 세션이 종료될 때 삭제하는 것입니다. Clone에서는 세션이 클라이언트에 의해 정의 될 것이며, 클라이언트가 세션을 끊으면 종료될 것입니다.

세션을 사용하는 단순 대안은 값이 종료될 때 서버에게 알려주는 "time to live"와 함께 모든 임시 값을 정의하는 것입니다. 클라이언트는 값을 재생하고, 만일 그들이 하지 않으면 값이 종료됩니다.

우리가 아직은 좀 더 복잡한 모델을 만들만한 가치가 있는지 모르기 때문에 간단한 모델을 구현하려고 합니다. 차이점은 성능일 뿐입니다. 만일 클라이언트가 소수의 임시 값을 갖고 있다면, 그것은 각각 하나 TTL을 설정하는 것이 좋습니다. 만일 클라이언트가 임시 값을 다수 사용한다면, 한번에 그들을 세션에 연결하고, 종료 시키는데 더 효율적입니다.

우선, key-value 메시지에 TTL을 인코딩하는 방법이 필요합니다. 우리는 프레임을 추가할 수 있습니다. 속성에 대한 프레임을 이용시의 문제점은 우리가 새로운 속성을 추가할 때마다, 우리는 kvmsg 클래스의 구조를 변경해야 한다는 것입니다. 그것은 호환성을 깨트립니다. 그래서 메시지에 'properties' 프레임을 추가하고, 속성 값을 얻고 넣을 수 있게 코드합니다.

다음으로, "delete this value"라고 하는 방법이 필요합니다. 지금까지 서버와 클라이언트는 항상 그들의 hash table에 맹목적으로 새로운 값을 삽입하거나 업데이트 했습니다. 만일 값이 비어있다면 그것은 "delete this key”를 의미하는 것입니다.

여기에 'properties'프레임 (우리가 나중에 필요한 UUID 프레임을, 추가)를 구현한 kvmsg 클래스의 보다 완전한 버전 있습니다. 필요한 경우, 해시에서 키를 삭제하여 빈 값을 처리합니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

Model5 클라이언트는 Model4와 거의 동일하지만, kvsimple대신에 전체 kvmsg 클래스를 사용하고, 각 메시지에 무작위로 'TTL'속성 (초 단위로 측정)을 설정합니다. :

kvmsg_set_prop (kvmsg, "ttl", "%d", randof (30));

Model5서버는 완전히 변화되었습니다. poll loop 대신에, 우리는 지금 reactor를 사용하고 있습니다. 이것은 타이머와 소켓 이벤트를 혼합해서 사용하는 것을 간단하게 만듭니다. C에서 reactor style은 더 많은 verbose입니다. 마일리지는 다른 언어로 바꿀 수 있습니다. 그러나 reactor는 더 복잡한 ØMQ 애플리케이션을 구축하는 더 좋은 방법 같습니다. 여기 서버는 다음과 같습니다. :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

Clone Server Reliability

top prev next

클론 모델 1.5는 상대적으로 간단합니다. 우리는 지금 불행히도 복잡한 영역에 들어갈 것입니다. 복잡한 영역으로 들어가기 전에 "우리는 실제로 이것이 필요합니까?"라고 당신은 항상 물어볼 필요가 있을만큼 안정적인 메시지를 만드는 것은 복잡합니다. 당신이 신뢰성을 해결할 수 있다면, 당신은 비용과 복잡성 측면에서 큰 승리를 얻을 수 있습니다. 물론, 당신은 때때로 일부 데이터가 손실될 수 있습니다. 그것은 좋은 교환조건 입니다.

백업 서버는 클라이언트 역할을 할수 있고, 모든 클라이언트가 하는것 처럼 업데이트를 수신하여 동기의 상태를 유지할 수 있습니다. 또한 클라이언트의 새로운 업데이트를 합니다. 아직은 hash table에 이들을 저장할 수 없습니다, 그러나 잠시 동안 그것을 붙잡아 둘수 있습니다.

우리가 처리해야 할 실패 목록을 봅시다. :

  • 복제 서버 프로세스가 충돌하면 자동 또는 수동으로 다시 시작됩니다. 이 프로세스는 가지고 있던 상태를 잃고 어떠한 한 지점으로 되돌려야 합니다.
  • 복제 서버 시스템이 죽고, 상당한 시간 동안 off-line됩니다. Clients는 어딘가 다른 서버로 전환해야합니다.
  • 복제 서버 프로세스 또는 시스템이 네트워크에서 연결이 끊깁니다. 예: 스위치는 죽는다. 네트워크 언젠가 복구되겠지만, 그 동안에 Clients는 대체 서버가 필요합니다.

우리의 첫 번째 단계는 두 번째 서버를 추가하는 것입니다. 우리는 기본 및 백업을 정리했던 4장에서 이진 스타 패턴(the Binary Star pattern)을 사용할 수 있습니다. Binary Star 는 reactor 이기 때문에 이미 마지막 서버 모델을 reactor style로 리펙토링(refactoring)했던 것이 유용하게 쓰일 것입니다.

우리는 주 서버가 충돌하는 경우 업데이트가 손실되지 않도록 보장할 필요가 있습니다. 가장 간단한 기술은 두 서버에 업데이트들을 전송하는 것입니다.

백업 서버는 클라이언트 역할을 할수 있고, 모든 클라이언트가 하는것 처럼 업데이트를 수신하여 동기의 상태를 유지할 수 있습니다. 또한 클라이언트의 새로운 업데이트도 합니다. 아직은 hash table에 이들을 저장할 수 없지만, 잠시 동안 붙잡아 둘수 있습니다.

그래서, Model6는 Model5를 통해 이러한 변경 사항을 소개합니다. :

  • 우리는 클라이언트 업데이트(서버)를 위해서 push-pull flow 대신에 pub-sub flow을 사용합니다. 그 이유는 더이상의 수신자가 없다면 push소켓이 차단되고 round-robin이 됩니다. 그래서 그들 둘을 open할 필요가 있습니다. 우리는 서버의 SUB소켓들과 연결하고, 그들에게 클라이언트의 PUB 소켓을 연결합니다. 이것은 하나의 클라이언트에서 두 서버로 fan-out하는 것을 유념하십시오.
  • 우리는 주 서버가 죽었 때 클라이언트가 검색할 수 있도록 서버 업데이트(클라이언트)에 heartbeats를 추가합니다. 그런 다음 백업 서버로 전환할 수 있습니다.
  • 우리는 the Binary Star bstar reactor 클래스를 사용하여 두 서버를 연결합니다. Binary Star는 "master"라고 생각하는 서버에 요청을 하는 방법으로 투표하기 위해 클라이언트에게 의존합니다. 우리는 이것을 위해서 스냅샷 요청을 사용합니다.
  • 우리는 UUID 필드를 추가하여 모든 메시지를 유일한 ID로 업데이트 합니다. 클라이언트는 이것을 생성하고, 서버가 re-published 업데이트시에 그것을 다시 전파합니다.
  • 슬레이브 서버는 아직 마스터 서버가 아닌, 클라이언트에서 받은 업데이트의 "pending list""를 유지합니다. 또는, 아직 클라이언트가 아닌, 마스터로부터 받은 업데이트입니다. 이 리스트는 가장 오래된것부터 최신까지의 명령이기 때문에 처음부터 업데이트를 제거하는 것은 간단한 일입니다.

유한 상태 머신으로 클라이언트 로직을 설계하는 것은 유용합니다. 클라이언트 주기 :

  • 클라이언트가 오픈하고 그 소켓을 연결하고, 다음 첫 번째 서버에서 스냅샷을 요청합니다. 요청 쇄도를 피하기 위해, 두 번만 다른 서버에 요청합니다. 한 번의 요청으로 원하는 결과를 얻지 못했다면, 운이 나쁜 케이스일 뿐입니다. 하지만 두 번씩 요청했음에도 불구하고 결과를 얻지 못한 것은, 부주의 때문입니다.
  • 클라이언트가 스냅샷을 받게 되었을때, 이것은 프로세스 업데이트를 기다립니다. 다시 말하지만, 그것이 일부 초과 시간 안에 서버에서 아무런 응답하지 않은 경우, 그것은 다음 서버로 fail-over 됩니다.
  • 클라이언트가 스냅샷을 받게 되었을때, 이것은 프로세스 업데이트를 기다립니다. 다시 말하지만, 그것이 일부 초과 시간 안에 서버에서 아무런 응답하지 않은 경우, 그것은 다음 서버로 fail-over 됩니다.

클라이언트 루프는 영원합니다. 그것은 startup과 일부 클라이언트가 백업 서버와 연결하려고 시도하는 동안, 일부 클라이언트가 주 서버에 접속하려고 할 때 발생하는 fail-over시에 연관이 있습니다. Binary Star pattern은 희망차고, 정확하게, 이것을 처리합니다. (이 같은 디자인을 만드는 즐거움은 우리가 그들이 옳다는 증명할 수 없지만, 우리는 그들이 틀렸다는 것은 증명할 수 있다는 것입니다.)

우리는 클라이언트 유한 상태 시스템(finite state machine)을 설계 할 수 있습니다. :

fig70.png

Fail-over는 다음과 같이 발생합니다. :

  • 클라이언트는 주서버가 더 이상 heartbeats 전송하지 않는 것, 죽은 것을 감지합니다. 클라이언트는 백업 서버에 연결하여 새 상태 스냅샷을 요청합니다.
  • 백업 서버는 클라이언트로부터 스냅샷 요청을 수신하기 시작하며, 주 서버가 죽은 것을 감지하고, 주서버로 전환합니다.
  • 백업 서버는 자체 해시 테이블에 대기중인 목록을 적용하고, 다음 상태 스냅샷 요청을 처리하기 시작합니다

주 서버가 다시 온라인 상태가 되면, 아래 작업이 이루어 질 것입니다. :

  • 슬레이브 서버로서 시작하고, 클론 클라이언트로 백업 서버에 연결합니다.
  • 클라이언트에서 SUB 소켓을 통해 업데이트를 받기 시작합니다.

우리는 몇 가지 가정을 합니다 :

  • 하나 이상의 서버가 계속 실행됩니다 두 서버가 충돌하는 경우, 우리는 모든 서버 상태를 잃게되고 그것을 복구할 수있는 방법은 없습니다.
  • 여러 클라이언트가 동시에 동일한 해시 키를 업데이트하지 않습니다. 클라이언트 업데이트는 다른 순서로 두 서버에 도달할 것입니다. 그래서, 백업 서버는 주 서버보다 다른 순서로 보류 목록에서 업데이트를 적용할 수 있습니다. 하나의 클라이언트에서 업데이트는 항상 두 서버에서 동일한 순서로 도달되며, 안전 합니다.

이것은 Binary Star pattern을 사용한 우리의 고가용성 서버입니다. :

fig71.png

이것은 구축 첫 단계로써, 우리는 재사용 가능한 클래스로써 클라이언트를 refactoring할 것입니다. 이것은 즐거움(ØMQ로 비동기 클래스를 작성하는 것는 아름다운 실습과 같습니다)의 일부이지만, 주로 우리는 복제가 임의의 응용 프로그램에 플러그인을 정말 쉽게 되기를 원하기 때문입니다. 탄력성이 올바르게 동작하는 클라이언트에 의존한 이래로, 재사용 가능한 클라이언트 API가 있을 때 이것을 보장하는 것은 훨씬 쉬워졌습니다. 우리는 클라이언트내에서 fail-over를 처리하기 시작하면, 그것은 (클론 클라이언트와 Freelance 클라이언트를 혼합된 것을 상상해보라) 조금 복잡해 집니다.

내 평소 디자인 방식은 정확하다고 판단되는 API를 첫번째로 설계하는 것이고, 다음 그것을 구현하는 것입니다. 그래서, 우리는 복제 클라이언트로 시작하고, 클론이라고 불리는 일부 추정 클래스 API 위에 위치시키기 위해 그것을 다시 작성합니다. 임의로 작성한 코드가 API로 된다는 것은 응용 프로그램을 합리적이고 안정적으로 추상적인 정의를 한다는 것을 의미합니다. 예를 들어, 모델5에서, 클라이언트는 소스에 하드 코딩된 끝점을 사용하여 서버에 별도의 세 가지 소켓을 열었습니다. 우리는 이 같은 세 가지 방법으로 API를 만들 수 있습니다. :

// Specify endpoints for each socket we need
clone_subscribe (clone, "tcp://localhost:5556");
clone_snapshot (clone, "tcp://localhost:5557");
clone_updates (clone, "tcp://localhost:5558");

// Times two, since we have two servers
clone_subscribe (clone, "tcp://localhost:5566");
clone_snapshot (clone, "tcp://localhost:5567");
clone_updates (clone, "tcp://localhost:5568");

그러나 이것은 말이 많고 오래가지 못합니다. 디자인의 내부 응용프로그램으로 내놓는 것은 좋은 생각이 아닙니다. 오늘, 우리는 3개의 소켓을 사용합니다. 내일은, 2개 또는 4개가 될 것입니다. 정말 복제 클래스를 사용하는 모든 응용프로그램을 변경하기를 원합니까?
소시지 공장 세부를 숨기기 위해, 우리는 이와 같이 작게 추상화 합니다. :

// Specify primary and backup servers
clone_connect (clone, "tcp://localhost:5551");
clone_connect (clone, "tcp://localhost:5561");

어떤 것은 단순화(하나의 서버가 한 끝점에 위치)의 이점을 가지고 있지만 내부 디자인에 영향을 미치고 있습니다. 우리는 지금 어떻게든 세 끝점에서 단일 끝점으로 변경해야 합니다. 한 가지 방법은 우리의 client-server 프로토콜의 " 클라이언트와 서버는 연속적으로 3개 포트를 통해 통신." 에 대한 지식을 적용하는 것입니다. 또 다른 방법은 서버에서 두 누락된 endpoints를 얻는 것입니다. 우리는 가장 간단한 방법으로 합니다. :

  • 서버 상태 라우터 (ROUTER)는 포트 P 입니다.
  • 서버 업데이트 publisher (PUB)는 포트 P + 1입니다.
  • 서버 업데이트 subscriber (SUB)는 포트 P + 2입니다.

클론 클래스는 4 장에서 flcliapi 클래스와 같은 구조를 가지고 있습니다. 그것은 두 부분으로 구성되어 있습니다 :

  • background 스레드에서 실행되는 비동기 복제 에이전트. 에이전트는 모든 네트워크 I / O를 처리하고, 실시간으로 서버와 통신합니다. 응용 프로그램이 무엇을 하던지 문제는 없습니다.
  • 호출자의 스레드에서 실행되는 동기 '클론' 클래스. 당신이 복제 오브젝트를 생성할 때, 자동으로 에이전트 스레드를 시작하고, 당신이 복제 오브젝트를 파괴했을 때, 그것은 에이전트 스레드를 죽입니다.

frontend 클래스는 inproc 'pipe' 소켓 의 agent 클래스와 통신합니다.. C에서, czmq 스레드 layer는 "attached thread"를 시작할 때 자동으로 이 파이프를 생성합니다. 이것은 ØMQ에서 멀티스레딩을 위한 자연스러은 패턴입니다.

ØMQ 없이는, 이런 비동기 클래스 디자인 종류는 정말 몇주간 열심히 작업해야 합니다. ØMQ로는, 하루 또는 이틀정도 작업하면 됩니다. 결과는 실제로 작동하는 복제 프로토콜의 단순화를 한 복합체입니다. 이렇게 만든 이유가 몇 가지 있습니다. 우리는 이것을 reactor로 만들 수도 있지만, 응용 프로그램에서 그것을 사용하는 것이 더 어려울 것이기 때문입니다. 그래서 API는 일부 서버에서 말하는 key-value 테이블과 비슷합니다. :

clone_t *clone_new (void);
void clone_destroy (clone_t **self_p);
void clone_connect (clone_t *self, char *address, char *service);
void clone_set (clone_t *self, char *key, char *value);
char *clone_get (clone_t *self, char *key);

클론 클라이언트의 Model6는 다음과 같습니다 :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

그리고 여기는 실제 복제 클래스 구현입니다 :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

마지막으로, 여기에 복제 서버의 여섯 번째이자 마지막 모델입니다 :


Python | Ada | Basic | C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Haxe | Java | Lua | Node.js | Objective-C | ooc | Perl | PHP | Ruby | Scala

이 메인 프로그램은 코드가 단지 몇 백 라인이지만, 작동하는데 시간이 좀 걸립니다. 정확하게 , Model6를 구축하는 것은 어려운 일 이었고, 작업에 한주 전채가 "사랑하는 하나님, 이 가이드는 너무 복잡합니다." 걸렸습니다. 우리는 이 작은 응용프로그램으로 거의 모든 것을 만들었습니다. failover, ephemeral values, subtrees 등등. 초기 디자인이 매우 정확하게 되었다는 것이 나를 깜작 놀라게 하였습니다. 그러나 너무 많은 소켓 흐름을 상세하게 작성하고 디버깅하는 것은 특별한 일입니다. 내가 이 작품을 만든 방법은 다음과 같습니다.

  • 코드에서 많은 지루한 작업을 제거하고, 나머지는 더 단순하고 확실하게 남기도록 reactors (bstar, on top of zloop) 사용했습니다. 전체 서버는 하나의 스레드로 작동되므로, 스레드 사이의 이상한 작동하는 것은 없습니다. 단지 행복하게 자신의 일을 할 수있는 모든 핸들러 주위에 구조 포인터('self') 를 전달합니다. reactors 를 사용하는 한 좋은 부작용은 덜 밀접하게 poll loop에 통합된 코드이며, 재사용이 훨씬 용이합니다. Model6의 큰 덩어리는 Model5에서 가져옵니다.
  • 그것을 조금씩 빌드함으로써, 각 조각들을 얻을 수 있었고 이는 다음 작업 전에 제대로 작동하는지 알 수 있게 해주었습니다. 4 or 5개의 소켓 플로우가 있는 것은, 꽤 많은 디버깅 및 테스트를 했다는 것을 의미 합니다. 나는 콘솔의 프린트 자료로 디버그 합니다. (예 : dumping messages). 실제로 이런 작업을 위해 디버거를 여는 것은 아무 의미가 없습니다.
  • 항상 Valgrind에서 시험하기때문에, memory leaks이 없을 것입니다. C에서는 이것이 주요 관심사지만, 당신은 어떤 garbage collector를 할당할 수 없습니다. kvmsg 및 czmq 같은 적절하고 일관된 추상적 개념을 사용하면 매우 도움이 됩니다.

코드에 여전히 결함이 있어, 어떤 독자들은 이것을 위해 디버깅과 수정하는데 주말을 보낼 것이라고 확신합니다. 실제 응용프로그램을 위하여 기초로 사용하는 이 모델만으로도 충분합니다.

여섯 번째 모델을 테스트하기위해서 임의의 순서로 기본 서버와 백업 서버와 클라이언트를 시작합니다. 그리고 임의로 서버중 하나를 죽이고 재 시작하고, 이일을 게속하도록 유지합니다. 디자인과 코드가 정확한 경우, 클라이언트는 master서버가 어떤 것이든 간에 업데이트의 동일한 스트림을 얻는 것을 유지해 갈 것입니다.

Clone Protocol Specification

top prev next

신뢰할 수있는 pub-sub을 구축하기 위해 이런 많은 작업 후, 우리는 개발하기 위해서 안전하게 응용프로그램을 구축할 수있게 몇 가지 보장을 원합니다. 좋은 시작은 프로토콜을 작성하는 것입니다. 이것은 우리가 다른 언어로 구현하게 하고, 우리는 코드에 깊이 손을 대는 것보다 종이에 디자인을 향상시켜야 합니다.

그 다음, 여기에 Clustered Hashmap Protocol이 있습니다, “cluster-wide key-value hashmap을 정의하고 클라이언트 집합에 걸쳐 이것을 공유하기 위한 메커니즘 입니다. CHP는 클라인언트가 hashmap의 subtrees와 함께 작동하고, 값을 업데이트하고, 임시값을 정의하도록 허용합니다.”

(More coming soon…)