ØMQ - The Guide

By Pieter Hintjens <moc.xitami|hp#moc.xitami|hp>, CEO iMatix Corporation.

With thanks to Bill Desmarais, Brian Dorsey, Daniel Lin, Eric Desgranges, Gonzalo Diethelm, Guido Goldstein, Hunter Ford, Kamil Shakirov, Martin Sustrik, Mike Castleman, Naveen Chawla, Nicola Peduzzi, Oliver Smith, Olivier Chamoux, Peter Alexander, Pierre Rouleau, Randy Dryburgh, John Unwin, Alex Thomas, Mihail Minkov, Jeremy Avnet, Michael Compton, Kamil Kisiel, Mark Kharitonov, Guillaume Aubert, Ian Barber, Mike Sheridan, Faruk Akgul, Oleg Sidorov, Lev Givon, Allister MacLeod, Alexander D'Archangel, Andreas Hoelzlwimmer, Han Holl, Robert G. Jakabosky, Felipe Cruz, Marcus McCurdy, Mikhail Kulemin, Dr. Gerg Erdi, Pavel Zhukov, Alexander Else, Giovanni Ruggiero, Rick "Technoweenie", Daniel Lundin, Dave Hoover, Simon Jefford, Benjamin Peterson, Justin Case, Devon Weller, Richard Smith, Alexander Morland, Wadim Grasza, Michael Jakl, Uwe Dauernheim, Sebastian Nowicki, Simone Deponti, Aaron Raddon, Dan Colish, Markus Schirp, Benoit Larroque, Jonathan Palardy, Isaiah Peng, Arkadiusz Orzechowski, Umut Aydin, Matthew Horsfall, Jeremy W. Sherman, Eric Pugh, Tyler Sellon, John E. Vincent, Pavel Mitin, Min RK, and Zed Shaw for their contributions, and to Stathis Sideris for Ditaa.

모든 설명 및 에러정보는 'issue tracker'참고하시길 바랍니다.
본 문서는 2011.12.6일 발표된 최신버전의 ØMQ를 기준으로 합니다.

가이드 소스는 주로 'C'로 되어 있으며 그 외 'PHP', ' Lua', ‘Haxe’를 사용 했습니다.

Chapter One - Basic Stuff

top prev next

Fixing the World

top prev next

ØMQ를 어떻게 설명 할 수 있을까요? 이런 미사어구를 사용할 수 있습니다. 소켓과는 차원이 다르다. 라우팅 가능한 메일박스다. 빠르다. 다른 사람들은 ØMQ에 익숙해져 가면 zap-pow-kaboom satori 패러다임과 같이 깨달아질 때의 순간을 공유하고 싶어 합니다. 일이 단순화 되고, 복잡함은 사라지며, 마음이 가벼워 집니다!. 다른 사람들은 ØMQ는 작고, 간단하지만, 친밀하게 느껴진다고 설명합니다. 개인적으로 왜 ØMQ 를 만들었는지 기억되었으면 좋겠습니다.

프로그래밍은 예술적인 과학입니다. 왜냐하면 우리 대부분이 소프트웨어 물리학을 이해하지 못하고, 그것을 가르치는 곳도 거의 없습니다. 소프트웨어 물리학은 algorithms, data structures, languages and abstractions은 아닙니다. 단지, 우리가 만들고 사용하고 버리는 도구입니다. 소프트웨어의 진정한 물리학은 사람입니다.

특히, 어려움이 닥쳤을 때 우리는 한계에 도달하게 되고, 큰 문제를 함께 풀기를 원합니다. 사람들이 쉽게 이해하고 사용할 수 있도록 빌딩블록을 만들고, 큰 문제를 해결하기 위해서 함께 일하는 것이 프로그래밍의 과학입니다.

우리는 서로 연관된 세계에 살고 있고 현대 소프트웨어는 이 세계를 탐색할 수 있습니다. 그래서 내일의 큰 솔루션을 위한 빌딩블록은 연결되어 있고 대규모 병렬화되어 있습니다. 코드는 더 이상 강한 침묵이 되어서는 안됩니다. 코드는 풀 수 있어야 되고, 서로 잘 연결되어야 합니다. 코드는 서로간의 메시지를 전달하는 수 백만개의 뉴런과 같이 중앙제어가 없어야 합니다. 또한 실패의 단일점만을 갖지 않는 거대한 병렬 네트워크로서 다른 문제도 풀 수 있는 인간의 두뇌처럼 처리해야 합니다. 몇몇 관점에서 보면 모든 네트워크의 종점은 인간 두뇌이기 때문에 코드의 미래는 인간의 두뇌에 달려 있습니다.

당신이 스레드, 프로토콜 또는 네트워크를 사용하는 모든 작업을 완료했다면, 꽤 많은 것이 불가능했다는 것을 깨닫게 될 것 입이다. 실제 상황에 적용할 몇몇 프로그램을 소켓으로 연결하는 작업도 쉽지 않고 비용 또한 상상 그 이상이기 때문입니다. 컴퓨터들을 연결하는 것은 수십억 달러 사업이기 때문에 쉬운 일이 아닙니다.

그래서 우리는 몇 년 앞서 그것을 사용할 수 있는 세계에 살고 있습니다. Fred Brook와 같이 'Silver Bullet은 없다'라고 믿었던 80년대에 소프트웨어 위기가 있었습니다. 무료 및 오픈소스 소프트웨어로 인해 효율적으로 지식을 공유할 수 있었기 때문에 그 위기를 해결했었습니다. 오늘 우리는 또 다른 소프트웨어의 위기에 직면하고 있지만 그것에 대해 많이 이야기 하지 않고 있습니다. 규모가 크고, 부유한 회사만이 연결된 응용 프로그램을 개발 할 수 있습니다. 클라우드가 있지만 독점되어 있는 실정입니다. 개인이 액세스 할 수 없고 경쟁 할 수 없는 클라우드 환경에서 데이터와 지식은 우리의 개인 컴퓨터에서 사라질 것입니다. 누가 사회적 네트워크를 소유하겠습니까? 이것은 mainframe-PC의 혁명과는 반대로 가는 길입니다.

우리는 또 다른 책에 정치 철학을 남길 수 있습니다. 요점은 “인터넷이 대규모로 연결된 코드의 가능성을 제공하는 반면, 현실은 우리 대부분이 도달하지 못하고 그래서 더 큰 흥미로운 문제(건강, 교육, 경제, 교통 등)가 발생되지만, 이런 문제를 해결하는 방법이 없고 이러한 문제를 해결하기 위해 함께 일할 수 있는 두뇌를 연결하는 방법이 없기 때문에 미해결로 남는다” 입니다.

연결 software는 많은 시도를 해 왔습니다. 수 천개의 IETF이 있고, 퍼즐의 각 해결 부분이 있습니다. 응용 프로그램 개발자를 위해 HTTP는 충분한 정보를 제공하는 한 솔루션입니다. 하지만 그것은 틀림없이 큰 서버와 얇은, 바보 같은 고객의 관점에서 생각하는 개발자 및 아키텍트를 장려함으로써 더 나쁜 문제를 만들 수 있습니다.

그래서 오늘 사람들은 여전히 원시 UDP 및 TCP, 사설 프로토콜, HTTP, WebSockets를 사용하여 응용 프로그램을 연결하고 있습니다. 그것은 어렵고, 느리고, 규모 산정이 어렵고, 본질적으로 중앙 집중되어 집니다. 분산 P2P 아키텍처는 거의 작업 없이 동작합니다. 얼마나 많은 응용 프로그램이 데이터 교환을 위해 Skype나 Bittorrent를 사용하나요?

프로그래밍 과학은 우리에게 제공해주는 것이 있습니다. 문제를 해결하기 위해, 우리는 두 가지를 할 필요가 있습니다. 첫 번째는, "어디서나, 어떤 코드에 코드를 연결하는 방법"의 일반적인 문제를 해결하는 것이며, 둘째로는 사람들이 이해하고 쉽게 사용할 수 있도록 간단하고 가능한 빌딩 블록으로 포장하는 것입니다.

이것은 말도 안되게 간단한 소리이지만, 이 글의 요점 입니다.

ØMQ in a Hundred Words

top prev next

ØMQ (ZeroMQ, 0MQ, zmq)는 임베디드 네트워킹 라이브러리 이지만, 동시성 프레임 워크와 같은 역할을 합니다.
이것은 in-process, inter-process, TCP, and multicast 처럼 다양한 방식으로 메시지를 전송하는 소켓을 제공합니다. 당신은 fanout, pub-sub, task distribution, and request-reply와 같은 패턴으로 N-to-N 소켓을 연결할 수 있습니다. 이것은 클러스터 구조에서 충분한 속도를 제공합니다. 비동기 I/O 모델은 비동기 메시지 처리를 제공하는 확장 멀티 코어 애플리케이션을 제공합니다. 이것은 language API를 제공하며 대부분의 운영 체제에서 실행됩니다. ØMQ는 iMatix에서 만들어 졌으며, LGPL 오픈소스 소프트웨어입니다.

Some Assumptions

top prev next

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

Getting the Examples

top prev next

가이드 예제는 가이드의 git repository에 있습니다. 모든 예제를 얻을 수 있는 가장 간단한 방법은 이 저장소에서 복사하는 것입니다 :

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

그리고 예제는 하위 디렉토리를 탐색하는 것입니다.
당신은 당신의 언어로 예제를 확인할 수 있습니다. 만일 당신이 사용하는 언어로 누락된 예제가 있다면, 당신은 번역물을 제출해 주시기 바랍니다.
이것은 가이드를 매우 유용하게 하는 방법이며, 많은 사람의 작품 덕분입니다.

소스 코드에 명시되어 있지 않더라도 모든 예제는 MIT/X11에 라이선스가 있습니다.

Ask and Ye Shall Receive

top prev next

몇 가지 코드로 시작하겠습니다. ‘Hello World’로 시작해 봅시다. 우리는 클라이언트와 서버를 만들 것이며, 클라이언트는 서버에 "Hello"를 보내면 ‘World’로 응답을 받습니다. 서버는 ‘C’로 되어 있고 포트 5555로 ØMQ 소켓을 열고 각 요청을 읽고 각각 요청에 대해 "World"로 응답합니다. :

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

#include <zmq.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main (void)
{
void *context = zmq_init (1);

// Socket to talk to clients
void *responder = zmq_socket (context, ZMQ_REP);
zmq_bind (responder, "tcp://*:5555");

while (1) {
// Wait for next request from client
zmq_msg_t request;
zmq_msg_init (&request);
zmq_recv (responder, &request, 0);
printf ("Received Hello\n");
zmq_msg_close (&request);

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

// Send reply back to client
zmq_msg_t reply;
zmq_msg_init_size (&reply, 5);
memcpy (zmq_msg_data (&reply), "World", 5);
zmq_send (responder, &reply, 0);
zmq_msg_close (&reply);
}
// We never get here but if we did, this would be how we end
zmq_close (responder);
zmq_term (context);
return 0;
}

hwserver.c: Hello World server

fig1.png

REQ-REP는 소켓 쌍으로 개발한다. 클라이언트는 zmq_send(3) 다음에 zmq_recv(3)을 수행해야지 순서를 바꾸면 에러가 납니다. 유사하게 서버는 zmq_recv(3) 다음에 zmq_send(3) 순서로 합니다.

ØMQ는 이 글에서 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

ØMQ API는 C와 C++이 유사함을 알 수 있습니다. PHP경우에는 좀더 간결하고 코드 읽기가 쉽습니다. :

<?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

여기에 클라이언트 코드가 있습니다. (좋아하는 프로그래밍 언어로 번역에 기여하기 위해 혹은 소스 코드를 보기 위해 아래 링크를 클릭하세요.) :


C++ | C# | Clojure | CL | Erlang | F# | Go | Haskell | Java | Lua | Node.js | Objective-C | Perl | PHP | Python | Ruby | Scala | Ada | Basic | Haxe | ooc
지금 이 현실이 너무 간단해 보이지만, ØMQ socket은 많은 기능을 담고 있습니다.
ØMQ 소켓은 네트워크시대의 세계를 구원할 슈퍼 영웅 입니다.
fig2.png

당신은 말 그대로 한번에, 한 서버에서 수천개의 클라이언트에게 메시지를 보낼 수도 있으며, 행복하고 신속하게 작업을 계속 할 수 있습니다. 재미로, 클라이언트를 시작하고 서버를 시작하면, 그 모든 것이 어떻게 작동되는지를 볼 수 있습니다. 이것이 무엇을 의미하는지 잠시 생각해 봅시다.

간략하게 이 두 프로그램이 실제로 어떻게 작동하는지 설명해 보겠습니다. 먼저 처리할 ØMQ context와 소켓을 만듭니다. 무슨 말인지 몰라도 걱정하지 마십시오. 곧 알게 될 것입니다. 서버는 5555번 응답 소켓을 열고, 루프 안에서 요청을 기다립니다. 그리고 매번 응답처리 합니다. 클라이언트는 요청을 보내고 서버로부터 보내온 회신을 읽습니다.

프로그래머에게 관심이 되는 코드가 얼마나 짧고 쉬운지, 얼마나 자주 충돌이 발생하는지, 때로는 무거운 짐이 될 수 있다는 것을 제외하고는 내부적으로 많은 일이 발생합니다. 이것은 request-reply 패턴이며 ØMQ를 사용하는 가장 간단한 방법입니다. 이것은 RFP나 고전적인 client-server모델에 해당됩니다.

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입니다. :