ZeroMQ 기본적인 것 정리

이번에 하고 있는 프로젝트에서 분산시스템을 구현하는 데 zeromq를 사용하게 되었다.
그래서 zeromq의 기본적이지만 헷갈릴 수 있는 내용들을 한 번 정리해보았다.
공식 guide document는 http://zguide.zeromq.org/ 이다.
여기에 쓴 내용들은 공식 guide에 설명된 내용을 포함해서 내가 추가적으로 여러가지 실험을 통해 알아낸 것들을 포함하고 있다.
그리고 내 개인적인 생각들도 포함되어 있다.

Valid Combination

PUB and SUB
REQ and REP
REQ and ROUTER (take care, REQ inserts an extra null frame)
DEALER and REP (take care, REP assumes a null frame)
DEALER and ROUTER
DEALER and DEALER
ROUTER and ROUTER
PUSH and PULL
PAIR and PAIR
출처) zquide.zeromq.org

Asynchronous Request & Response

REQ, REP, PAIR같은 애들은 비동기 request-response가 불가능하다.
그냥 패킷 하나 주고 하나 받고 하나 주고 하나 받고 이렇게 동기적으로 해야한다.
그래서 비동기 req-res를 하려면 ROUTER나 DEALER를 사용해야한다.
DEALER나 ROUTER의 특징은 아래쪽에 써놧으니 읽어보면 된다.
만약 비동기적인 PAIR to PAIR 를 구현하려면 DEALER to DEALER 를 사용하면 된다.
만약 비동기적인 REQ to REP 를 구현하려면 DEALER to ROUTER 를 사용하면 된다.

근데 DEALER나 ROUTER는 이름에서 알 수 있듯이 각각 연상되는 활용처가 있는 법인데,
단순히 비동기 req/res를 하기 위해서 이걸 사용해야한다는 점이 네이밍 측면에서 좀 안좋게 느껴진다.
ASYNC_REQ 뭐 이런 네이밍의 socket type을 추가로 만들어줬어도 참 괜찮았을 거 같은데 말이다..ㅠㅜ
하지만 이렇게 하면 유저가 공부하고 머릿속에 넣어야할 것들이 증가하고 DEALER/ROUTER로도 ASYNC_* 을 대체할 수 있으므로 오히려 더 혼란만 가중할 것이라고 예측해볼 수 있다. 그런 측면에서 난 zeromq의 선택이 옳았다고 생각한다.


Message Envelope & Socket Type

message을 보내거나 받을 때 socket type과 frame 구조를 꼭 숙지해야한다.

1) REQ / REP
REQ/REP 동기 구조이기 때문에 send/recv가 번갈아가며 차례차례 일어나야만 한다.
REQ/REP의 경우 message를 보낼 때, 맨 앞에 empty delimiter frame이 자동으로 들어가고 받을 때, 자동으로 없어진다. 만약 받을 때 edf가 없으면 message를 그냥 버린다.
근데 사실 좀 더 엄밀한 룰이 있다.
message를 받을 때는 frame들중 최초 등장하는 edf 를 기준으로 뒷쪽부분만 수신한다. 그리고 그 앞쪽부분은 따로 저장해둔다. 여기서 만약 뒷쪽부분이 3개의 frame으로 되어 있다면 recv를 총 3번 호출해야한다. 혹은 recv_multipart를 1번 호출하면 된다.
그리고 message를 보낼 때는 앞서 따로 저장해둔 앞쪽부분에 이어서 그 뒤에 edf를 넣고 그 뒤에 data를 넣어서 message를 보내게 된다.
즉, 이런 규칙때문에 REQs - ROUTER | DEALER - REPs 와 같은 구조에서 DEALER가 REPs로 부터 받은 reply들을 그냥 ROUTER를 통해 보내면 알아서 알맞는 REQ로 가게되는 것이다. 근데 내 생각에는 실제로 이렇게 REP로 부터 받은 message를 그대로 믿는 것은 보안상 취약할 것 같다. 왜냐하면 만약 REPs 쪽에서 message를 조작해서 맨 앞 frame의 identity를 invalid한 값으로 조작하면 결국 broker에서 router로 message를 보내려 할때, zeromq exception error가 뜰 것이다. 물론 zeromq api단에서는 조작이 불가능하므로 memory를 직접 접근하거나 후킹같은 기술을 사용해야 성공할 수 있을 것이다. 어쨌든 그래서 이런 구조로 가는 것보단 REQs - ROUTER | ROUTER - REPs 같은 형태로 패턴을 구성해서 중간에 frontend와 backend를 연결해주는 것을 직접 구현하는 것이 보안상 안전할 것 같다.
그냥 REQ - REP 쌍으로 쓸 경우는 이런 룰들이 별로 상관이 없을 수 있지만 ROUTER나 DEALER와 함께 쓰게 되면 이런 룰들을 고려해야만 한다.

2) PUB / SUB
PUB/SUB의 경우는 SUB가 msg가 어떤 PUB에서 왔는지 구분해야함으로 address frame이 '2번째'에 들어간다.
1번째에는 key, 2번째에는 address, 3번째에는 data frame이 오게 된다. 물론 이 때, key filtering을 사용하면 key frame이 붙지 않을 것이다. PUB의 경우에는 address frame이 없다.

3) DEALER
DEALER같은 경우는 zeromq가 내부적으로 frame을 건들지 않는다. 즉 socket.send(b'data') 를 하면 그냥 frame 1개 짜리 message가 날라간다. socket.recv()를 할 때는 받은 message의 frame개수 만큼 recv를 호출해야 한다.
DEALER는 진짜 말 그대로 dealer 역할을 하는 셈이다. 직접 frame을 자유분방하게 조작하여 송수신을 하기에 참 좋다. 그리고 snipping 용으로 쓰기에도 좋다. 단, DEALER는 ROUTER처럼 identity를 check하는 기능도 없고 비동기방식임으로 SERVER로서 사용은 불가능할 것이다. 그리고 1:1 연결의 경우에도 REQ-DEALER 조합은 작동이 안되는 것에 주의해야 한다.

4) ROUTER
ROUTER같은 경우는 받은 message의 맨 앞에 identity frame을 추가시킨다. 그리고 send할 때는 반드시 send_multipart를 이용해 맨 앞에 identity frame을 넣어줘야 한다.  identity frame은 이 message가 누구한테 왔고 누구한테 가야하는 지를 의미한다. 말 그대로 router의 역할을 한다. 주의할 점은 ROUTER는 send할 때 무조건 send_multipart를 이용해 identity를 지정해줘야한다는 것이다. 즉, 최소 한 번 recv를 했던 connection에 한해서만 send가 가능하다는 것이다.


zmq.asyncio

zmq.asyncio.ZMQEventLoop를 먼저 asyncio에 등록한 다음에 zmq.asyncio.Context를 생성해야한다.
순서가 반대로 될 시 문제가 발생할 수 있다.
(구체적으로 왜 문제가 발생하는 진 모르겠지만 multiprocessing 모듈을 이용한 상황에서 문제가 발생했었다.)



정리는 여기까지다.
이건 잡설인데.. zeromq guide document가 여러가지 request-reply pattern들을 공부하는 데 굉장히 도움이 될 것 같다. 요즘은 바빠서 안되겠지만 나중에 한 번 시간이 된다면 차례대로 읽으면서 정리를 해두고 싶다.

댓글

댓글 쓰기