ZeroMQ 기본적인 것 정리
이번에 하고 있는 프로젝트에서 분산시스템을 구현하는 데 zeromq를 사용하게 되었다.
그래서 zeromq의 기본적이지만 헷갈릴 수 있는 내용들을 한 번 정리해보았다.
공식 guide document는 http://zguide.zeromq.org/ 이다.
여기에 쓴 내용들은 공식 guide에 설명된 내용을 포함해서 내가 추가적으로 여러가지 실험을 통해 알아낸 것들을 포함하고 있다.
그리고 내 개인적인 생각들도 포함되어 있다.
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
그냥 패킷 하나 주고 하나 받고 하나 주고 하나 받고 이렇게 동기적으로 해야한다.
그래서 비동기 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의 선택이 옳았다고 생각한다.
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가 가능하다는 것이다.
순서가 반대로 될 시 문제가 발생할 수 있다.
(구체적으로 왜 문제가 발생하는 진 모르겠지만 multiprocessing 모듈을 이용한 상황에서 문제가 발생했었다.)
정리는 여기까지다.
이건 잡설인데.. zeromq guide document가 여러가지 request-reply pattern들을 공부하는 데 굉장히 도움이 될 것 같다. 요즘은 바빠서 안되겠지만 나중에 한 번 시간이 된다면 차례대로 읽으면서 정리를 해두고 싶다.
그래서 zeromq의 기본적이지만 헷갈릴 수 있는 내용들을 한 번 정리해보았다.
공식 guide document는 http://zguide.zeromq.org/ 이다.
여기에 쓴 내용들은 공식 guide에 설명된 내용을 포함해서 내가 추가적으로 여러가지 실험을 통해 알아낸 것들을 포함하고 있다.
그리고 내 개인적인 생각들도 포함되어 있다.
Valid Combination
PUB and SUBREQ 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들을 공부하는 데 굉장히 도움이 될 것 같다. 요즘은 바빠서 안되겠지만 나중에 한 번 시간이 된다면 차례대로 읽으면서 정리를 해두고 싶다.
reduWfrusnPaterson Khaled Marte https://wakelet.com/wake/BSZY7s8QUYMrZLstopyjJ
답글삭제kennbeadsmorab
Nglutulig_tsu1986 Nikki Marie click
답글삭제link
click here
https://colab.research.google.com/drive/1iv1QWhsl0RkSp4rxClT8cJ84o6bc__ia
clinlatabmarg