Chapter5

Chapter Five - Advanced Publish-Subscribe

top prev next

제 3,4장에서 우리는 ØMQ의 request-reply 패턴을 사용해 봤습니다. 모든 것을 이해 했다면 축하합니다. 본 장에서는 publish-subscrib에 대해서 다룰 것이며, 성능, 신뢰성, 상태 배포, 보안을 위한 높은수준의 유형들(higher-level patterns)을 포함해서 ØMQ의 핵심인 pub-sub 유형을 살펴 볼 것입니다.

본장에서 다룰 내용:

  • too-slow subscribers 를 처리하는 방법 (the Suicidal Snail pattern).

*high-speed subscribers를 설계하는 방법 (the Black Box pattern).
*shared key-value cache를 구축하는 방법 (the Clone pattern).

Slow Subscriber Detection (Suicidal Snail Pattern)

top prev next

실생활에서 pub-sub 패턴을 사용할 때 이슈가 될만한 문제는 slow subscriber 입니다. 일반적으로, publishers는 subscribers에게 최고속도로 데이터를 보냅니다. 현실적으로 subscriber applications은 Interprited언어로 작성 되거나, 많은 작업을 수행하며, 또는 Publisher의 부하를 유지하지 못할 정도가 될 수 있습니다.

어떻게 slow subscriber를 처리할 수 있을까요? 더 빠른 subscriber를 만들기 위해서는 시간과 노력이 많이 필요합니다. slow subscriber를 해결하기 위한 전형적인 전략 몇 가지 있습니다. :

  • Queue messages on the publisher. 몇 시간 동안 메일을 읽지 않았을 때 하는 GMAIL이 하는 행동 입니다. 그러나 high-volume messaging(대용량 메세징)에서, queue에 publishing할 수 있지만, publisher는 메모리 부족과 오작동의 결과가 초래될 수 있습니다. 특히, subscriber가 많다면 성능차원에서 디스크 flush가 원활하지 않을 수 있습니다.
  • Queue messages on the subscriber. 이 방식은 좀 더 좋은 방법이며, ØMQ가 기본적으로 제공하는 기능입니다. 큐를 사용해서 메모리 부족과 오작동이 발생하면 publisher보다는 subscriber쪽이 낫습니다. 이것은 subscriber가 수용할 수 없는 최고부하 시점에는 큐잉을 하고 여유로운 시점에는 나머지를 처리할 수 있는 완벽한 방법입니다. 그러나 subscriber가 너무 느리다면 문제가 발생됩니다.
  • Stop queuing new messages after a while. 이것은 메일 함 용량이 7.555GB가 넘을 때 Gmail이 하는 것입니다. 신규 메시지가 바로 거부되거나 삭제 됩니다. 이것은 publisher의 관점에서 훌륭한 전략이며, publisher가 최고 수위 점 or HWM 을 설정하였을 때 ØMQ가 하는 것이다. 그러나 그것은 여전히 우리가 slow subscriber를 해결하는데 도움이 되지 않습니다. 지금, 우리는 message stream에서 해결할 것입니다.
  • Punish slow subscribers with disconnect. 15번째 Hotmail 계정에 2주 동안 Login 하지 않을 때 Hotmail이 하는 것입니다 이것은 subscriber에서 주의를 요하게 하며, 이상적 일수도 있지만, ØMQ는 이렇게 처리하지 않습니다.

이것은 나의 15번째 Hotmail 계정에, 내가 2주 동안 Login 하지 않을 때 Hotmail이 하는 것입니다. 이것은 subscriber에서 주의를 요하게 하며, 이상적 일수도 있지만, ØMQ는 이렇게 처리하지 않습니다.
이러한 전형적인 전략에 적합한 것은 아무것도 없습니다. 그래서 창의성이 필요합니다. Publisher가 연결을 끊는 것 보다는 subscriber 자체가 죽도록 하는 하는 것이 좋을 것 같습니다. 이것이 the Suicidal Snail pattern 입니다. Subscriber는 너무 느리게 동작한다고 판단될 때 죽는 것입니다.
Subscriber는 어떻게 이것을 감지할 수 있습니까? 한가지 방법은 메시지에 순서대로 번호를 (순서대로 숫자를 매기는 것) 부여하고, publisher에 HVM 를 적용하는 것 입니다. Subscriber가 GAP(예: 번호가 연속되지 않습니다.)를 발견한다면, 그것은 무언가 잘못된 것을 알게 되는 것입니다. 우리는 적당하게 죽는 수준을 정하기 위해 HVM을 조정합니다.

이 솔루션에는 2가지 문제가 있습니다. 첫째, 많은 publisher가 있을 때, 우리는 어떻게 messages에 시퀀스를 매깁니까? 이 솔루션은 각 publisher에게 유일한 ID를 주고, 시퀀스를 추가해야 합니다. 둘째, subscribers가 ZMQ_SUBSCRIBE 필터를 사용하면, 정의된 것에 의해서 GAP이 생깁니다. 그러면 시퀀스를 부여하는 것은 무의미 해집니다.

일부 use-case에서 필터를 사용하지 않으면 시퀀스는 잘 처리 될 것입니다. 그러나 보다 일반적인 솔루션은 publisher가 각각의 메시지에 timestamps 사용하는 것입니다. Subscriber가 메시지를 받았을 때 시간을 확인하고, 그 차이가 1초 이상이라면, 그것은 죽는 것입니다.
The Suicide Snail pattern은 subscribers들의 자신의 clients 와 service-level agreements을 가지고, 특정 최대 latencies를 보장할 때 동작합니다. Subscriber를 중지하기 위해 최대 latency보장하는 좋은 방법처럼 보이지 않을 수도 있지만 그것은 assertion model입니다. 오늘이 지나면, 그 문제는 해결될 것입니다. 늦게 데이터를 처리하도록 허락하면, 그 문제가 더 광범위하게 영향을 주는 원인이 되고, 원인 분석하는데 시간이 오래 걸리 수 있습니다.

이것은 Suicidal Snail에 대한 예제 입니다. :


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

이 예제에 대한 참고 사항:

  • 이 메시지는 여기에 milliseconds 번호로 현재 시스템 시간으로 단순하게 구성되어 있습니다. 현실적인 application에서는 적어도 timestamp를 포함한 메시지 header와 데이터 메시지 body를 가져아 합니다.
  • 예를 들어 한 프로세스에 두개의 스레드로 subscriber 와 publisher가 있습니다. 실제로 그들은 별개의 프로세스 입니다. 스레드를 사용하면 데모가 좀더 편리 합니다.

High-speed Subscribers (Black Box Pattern)

top prev next

pub-sub에 대한 일반적인 use-case는 다량의 데이터 스트림을 배포하는 것입니다. 예를 들어 증권거래소의 ‘시장 데이터’ 입니다. 전형적으로 publisher는 주식을 거래하기 위해 연결하고, 값을 책정한 다음 subscriber에게 그것을 보냅니다. Subscriber가 소수라면, 우리는 TCP를 사용할 수 있으며, Subscriber가 다수라면, 우리는 아마 신뢰할 수 있는 멀티 캐스트, 즉 PGM을 사용할 것입니다..
100bytes메시지를 초당 평균 100,000을 처리한다고 생각해 봅시다. 이것은 전형적인 속도입니다. 초당 100K 메시지는 ØMQ application에서 쉽게 처리할 수 있으며, 우리는 훨씬 더 빠른 처리를 원합니다.
그래서 우리는 publisher 하나, 각 subscriber당 하나씩 서버로 구성합니다. 8개 core, 12개 publisher로 잘 정의된 서버들이 있습니다. (2015년에 이것을 읽는다면, 그때 가이드는 완료될 예정입니다. 이 숫자에 0을 추가 하십시오.)

그리고 이것은 subscriber에게 데이터를 넣을 때, 두 가지 주의사항 입니다. :

  1. 우리가 메시지 처리를 아주 조금 처리 했어도, 다시 publisher의 처리를 따라 잡을 수 없는 지점에서 subscriber 속도가 느려집니다.
  2. 우리는 주의 깊게 최적화와 TCP를 튜닝 한 후, 약 초당 6M 메시지를 처리하도록 Publisher와 subscriber 모두에게 부하를 줄 것입니다.

우리가 해야 하는 첫 번째 일은 멀티스레드로 설계에 subscriber를 끼워 넣는 것입니다. 그래서 다른 스레드에서 메시지를 읽는 동안 스레드 중 한 세트에서 메시지를 처리 할 수 있습니다. 일반적으로 우리는 똑같은 방법으로 모든 메시지를 처리하기를 원하지 않습니다. 오히려 subscriber는 아마 prefix key로 대부분의 메시지를 필터링 합니다. 메시지가 어떤 기준과 일치하면, subscriber는 메시지를 처리하는 작업자를 호출합니다. ØMQ에서 이것은 작업자 스레드에게 메시지를 보내는 것을 의미합니다.

그래서 subscriber는 queue device 같은 것으로 보입니다. 우리는 subscriber와 작업자들을 연결하는 다양한 소켓을 사용할 수 있습니다. 만약 단 방향 트래픽 이고 모두 동일한 작업을 처리한다면 우리는 PUSH와 PULL 사용할 수 있으며, ØMQ에 모든 라우팅 작업을 위임할 수 있습니다. 이것은 가장 간단하고 빠른 접근방법입니다. :

fig65.png

Subscriber는 TCP 또는 PGM을 통해 Publisher와 통신합니다. Subscriber는 inproc를 통해 모두 동일한 프로세스로 Workers와 이야기 합니다.
이제 한계를 벗어날 때 입니다. subscribe 스레드가 CPU 100 %에 도달한 일이 발생한 것은 하나의 스레드이기 때문이며, 그것은 하나 이상의 코어를 사용할 수 없습니다. 단일 스레드는 항상 초당 2M, 6M, 또는 그 이상 메시지로 한계에 도달할 것입니다. 우리는 다수의 스레드에 걸쳐 작업을 분할하기를 원하며, 병렬로 실행할 수 있습니다.

많은 고성능 제품에서 사용하는 접근방법은 분할입니다. 병렬 및 독립적인 스트림으로 작업은 분할한다는 것을 의미 합니다. 예로 topic key의 절반은 한 스트림에서, 절반은 다른 스트림으로 실행 합니다. 우리는 많은 스트림을 사용할 수 있지만, 여유 cpu가 없다면 성능이 향상되지 않습니다.

그래서 두 스트림으로 조작하는 방법을 보겠습니다. :

fig66.png

두 스트림에서 최대 속도로 처리하기 위해, 우리는 다음과 같이 ØMQ를 구성하는 것입니다. :

  • 하나보다는 두 개의 I / O 스레드.
  • 두 개의 네트워크 인터페이스 (NIC), 각 subscriber마다 하나씩.
  • 각 I / O 스레드는 특정 NIC에 바인딩
  • 특정 코어에 바인딩된 두 subscriber스레드.
  • 두 개의 SUB 소켓, 각 subscriber스레드 마다 하나씩.
  • 나머지 코어들은 작업자 스레드들에게 할당.
  • 작업자 스레드는 양쪽 subscriber PUSH 소켓에 연결

우리의 아키텍쳐는 이상으로 스레드당 한 코어를 가집니다. 일단 우리가 코아보다 더 많은 스레드를 생성하면, 스레간의 경합이 발생하고, 반환이 점점 떨어질 겁니다. 예를들어 더 많은 I/O 스레드를 생성하는 것은 어떤 이득도 없을 것입니다.

A Shared Key-Value Cache (Clone Pattern)

top prev next

pub-sub는 라디오 방송과 유사합니다. 당신이 가입하기 전엔 아무것도 없으며, 당신이 얻고자 하는 수많은 정보는 당신이 수용하려는 양에 의존합니다. 놀랍게도 “완벽”을 목포로 하는 엔지니어들에겐 이 모델이 정보의 실제 배포와 완벽하게 매치되기 때문에 유용하고 넓게 확산되어 있습니다. 페이스 북과 트위터, BBC 월드 서비스 및 스포츠 결과를 생각해 보세요.

그러나 가능하다면, 보다 신뢰가능한 pub-sub이 가치가 있는 많은 경우가 있습니다. 우리가 request-reply 위해 했던 것처럼 오류 동작하는 시각에서 신뢰성을 정의 해야 합니다. pub-subd의 고전적인 문제는 다음과 같습니다. :

  1. Subscriber들의 늦은 가입, 그래서 서버에 이미 전달되어 메시지를 놓친 경우
  2. Subscriber의 연결 속도가 느려, 그 시간 동안 메시지를 잃는 경우.
  3. Subscriber들이 떠나가서, 떠나있는 동안 메시지를 잃는 경우.

드물지만, 이와 같은 문제도 있습니다. :

  1. Subscriber들은 충돌, 재시작 할 수 있고, 이미 받은 데이터를 잃을 수 있습니다.
  2. Subscriber들은 메시지들을 너무 느리게 처리합니다. 그래서 queue에 쌓고, 다음 overflow 될 수 있습니다.
  3. Networks에서 과부하가 발생하고, 데이터를 놓칠 수 있습니다. (specifically, for PGM).
  4. Networks에서 속도가 너무 느려져서, publisher-side queues overflow되고, publishers crash.

더 많이 잘못될 수 있지만 이것은 현재의 시스템에서 볼 수 있는 전형적인 오류입니다.

우리는 “the Suicidal Snail pattern”으로 느린 subscriber처럼 이것들의 몇 가지를 해결 하였습니다. 그러나 나머지는 신뢰성 있는 pub-sub을 위하여 포괄적이고, 재사용할 수 있는 프레워크를 가져가는 것이 좋을 것입니다.

어려운점은 우리의 대상 Application이 실제로 그들의 데이터를 가지고 하고자 하는 것이 무엇인가 라는 아이디어가 없다는 것입니다. 그들은 그것을 필터링하고, 메시지의 하위집합만 처리 합니까? 그들은 나중에 재사용을 위하여 데이터를 어딘가에 기록합니까? 그들은 작업자에게 그 이상으로 데이터를 배포합니까? 그럴듯한 시나리오는 수십가지 있으며, 각각 어떤 신뢰성수단, 노력과 성능측면에서 그것의 가치가 얼마인지에 따라 자신의 아이디어를 가질 수 있습니다.

그래서 우리는 구현할 수 있는 abstraction을 구축할 것입니다. 그리고 많은 applications에서 재사용 합니다. 이 abstraction은 고유 키에 의해 색인된 blobs의 집합으로 저장하는 shared value-key cached 입니다.

분산 네트워크에서 피어(peers) 연결의 광범위한 문제를 해결하는 분산 hash tables나 non ?SQL 데이터베이스와 같은 역할을 하는 분산key-value tables와 혼동하지 마십시오. 구축할 모든 것은 서버에서 클라이언트 세트에 안정적인 일부 메모리상태를 복제하는 시스템입니다. 우리가 원하는 아래와 같습니다. :

  • 클라이언트를 언제든지 네트워크에 연계시키고, 안정적으로 현재 서버 상태를 유지합니다.
  • 모든 클라이언트에 key-value cache를 업데이트 (새로운 key-value pairs를 삽입, or 기존의 key-value로 업데이트, or key-value 등을 삭제)
  • 안정적으로 모든 클라이언트에 변경사항을 전파하고, 이것은 최소한의 잠재 오버헤드 가집니다.
  • 수천, 수만의 클라이언트를 처리합니다.

클론 패터의 핵심은 클라이언트가 서버로 응답을 보낼 수 있다는 것입니다. 이것은 일반적인 pub-sub보다 훨씬 더 발전한 것입니다. 이런 이유로 이 글에서는 publisher, subscriber라는 용어대신 server, client라는 용어를 사용하고 있습니다. 클론은 내부적으로 pub-sub을 사용하기는 하지만 그 이상의 기능을 제공합니다.

Distributing Key-Value Updates

top prev next

이번 단계에서는 한번에 한가지 문제를 해결하도록 하는 Clone을 개발합니다. 첫번째로, 서버에있는 클라이언트들에게 key-value 업데이트를 배포하는 방법을 살펴 보겠습니다. 우리는1장에서 weather server를 다루었으며, 한 쌍의 key-value로 메시지를 보내기 위해 이것을 refactor했습니다. 우리는 해시 테이블에 이들을 저장하기 위해 우리의 클라이언트를 수정합니다. :

fig67.png

이것은 서버입니다. :


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

그리고 이것은 클라이언트입니다. :


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

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

  • 모든 어려운 작업은 kvmsg 클래스에서 이루어집니다. 이 클래스는 key-value 메시지 객체와 함께 작동하며,이 클래스는 3개의 프레임으로 구성된 다중 ØMQ 메시지 입니다. : a key (a ØMQ string), a sequence number (64-bit value, in network byte order), and a binary body (holds everything else).
  • 서버는 randomized 4-digit key로 메시지를 생성합니다. 크지만 엄청나지 않은 hash table(10K entries)을 시물레이션 할 수 있습니다.
  • 서버는 소켓을 바인딩 이후 200 millisecond 동안 정지합니다. 이것은 substriber가 서버의 소켓에 연결할 때 메시지를 잃는 "slow joiner syndrome"을 방지합니다. 우리는 이 "slow joiner syndrome"를 이후 버전에서 해결할 것입니다
  • 우리는 소켓을 참조하는 코드에서 'publisher' 와 'subscriber' 을 사용합니다. 이것은 우리가 다중 소켓들을 가지고 여러가지 일들을 할 때 나중에 도움이 될 것입니다.

현재 동작하는 것 중 가장 간단한 형태의 kvmsg 클래스가 있습니다. :


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

우리는 나중에 좀 더 정교한 kvmsg class를 applications에서 사용하기 위해 만들 것입니다.

서버와 클라이언트 모두 hash tables을 관리하지만, 첫번쩨 모델에서 만일 서버보다 모든 클라이언트가 먼저 시작되고, 클라이언트들의 충돌이 전혀 없었다면 제대로 작동합니다. 안정적이진 않습니다.

Getting a Snapshot

top prev next

클라이언트가 지연(또는 복구)현상을 서버를 통해 발견 할 수 있도록 하기 위해서는 서버 상태 snapshot 을 얻어야 합니다. 우리는 "a sequenced key-value pair" 의미를 "message"로 줄인 것 처럼 "a hash table" 의미를 "state"로 줄일 수 있습니다. 서버 상태를 얻으려면, client는 REQ 소켓을 열고 명확하게 요청해야 합니다. :

fig68.png

이 작업을 하기위해서, 우리는 타이밍 문제를 해결해야 합니다. 상태 snapshot을 얻는 것은 snapshot이 큰 경우 아마도 상당히 오랜 특정시간이 소요됩니다. 우리는 snapshot에 정확히 업데이트를 적용해야 합니다. 그러나 서버는 우리에게 업데이트를 보낼 때를 알 수 없습니다. 한가지 방법은 subscribing을 시작하면서 첫 업데이트를 얻고, 다음 “업데이트 N에 대한 상태”를 요청하는 것입니다. 이는 실용적진 않지만 각 업데이트에 대한 하나의 snapshot을 저장하는 것을 서버에 요구하게 됩니다.

그래서 우리는 다음과 같이 클라이언트에서 동기화를 할 것 입니다. :

  • 클라이언트는 먼저 업데이트를 subscribes(승낙or신청or예약)하고 상태 요청을 합니다. 이것은 상태가 가장 오래된 업데이트 보다 최신이란 것을 보장합니다.
  • 클라이언트는 상태와 응답을 서버로부터 받기 위해 대기하며, 그동안 queue들은 모두 업데이트 됩니다. 그것은 단순히 그들을 읽는 것이 아니라 작업을 수행합니다: ØMQ는 우리가 HWM을 설정하지 않기 때문에 소켓 queue에서 그들queue를 유지합니다.
  • 클라이언트가 상태 업데이트를 할 때, 업데이트를 다시 읽기 시작합니다. 그러나 그것은 상태 업데이트 보다 더 오래된 모든 업데이트는 버립니다. 그래서 상태 업데이트가 200까지 업데이트를 포함한다면, 클라이언트는 201까지 업데이트를 버릴 것 입니다.
  • 클라이언트는 자체 상태 snapshot으로 업데이트를 적용합니다

이것은ØMQ 자신의 내부 queues를 이용하는 간단한 모델입니다. 여기 서버는 다음과 같습니다. :


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

클라이언트 입니다.:


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

이 코드들에 대한 몇 가지 요약입니다. :

  • 서버는 단순한 설계를 위해 두 개의 스레드를 사용합니다. 한 스레드는 임의 업데이트를 수행하고, 두 번째 스레드는 상태를 처리합니다. 두 스레드는 PAIR 소켓을 통하여 통신합니다. 당신은 SUB 소켓을 사용할 수 있지만. 연결하는 동안 subscriber가 무작위로 몇 가지 메시지를 놓치는 “"slow joiner" 문제에 부딪칠 것입니다 PAIR 소켓은 두 개의 스레드를 명확히 동기화 시킵니다.
  • hash table 삽입이 상대적으로 느리기 때문에 우리는 업데이트한 한 쌍의 소켓에 HWM을 설정합니다. 이것이 없으면, 서버의 out of memory가 발생합니다. inproc 연결에서 실제 HWM은 두 소켓의 HWM의 합계이며, 그래서 우리는 각 소켓에 HWM을 설정합니다.
  • 클라이언트는 정말 간단합니다. C에서, 코드는 60 라인 이하입니다. 많은 어려운 부분은 kvmsg 클래스에서 수행하지만, 여전히 기본적인 Clone pattern 은 처음에 보였던 것보다 쉽게 구현됩니다.
  • 우리는 직렬화 상태에 대한 멋진 어떤것도 사용하지 않습니다. hash table 은 kvmsg 개체의 집합을 보유하고, 서버가 클라이언트 요청 상태 메시지의 일괄 처리로 이들을 보냅니다. 다수의 클라이언트가 동시에 상태를 요청하는 경우, 각각 다른 snapshot을 얻을 것입니다
  • 우리는 클라이언트가 할 얘기가 정확히 하나의 서버가 실행이 되고 있다고 가정합니다. : 우리는 서버가 충돌할 때 무엇이 발생하는지에 대한 질문을 답하지는 않을 것입니다..

지금, 이 두 프로그램은 진짜 아무것도 하지 않지만, 그들은 정확하게 상태를 동기화할 수 있습니다. 이것은 다른 패턴을 결합하는 방법의 적절한 사례입니다. PAIR-over-inproc, PUB-SUB, and ROUTER-DEALER.

Republishing Updates

top prev next

두 번째 모델에서, 서버 자체로부터 key-value cache를 변경합니다. 이것은 우리가 각 노드에서 로컬 캐싱과 함께 배포하려는 중앙 설정 파일이 있다면 예제로서 유용한 중앙집중 모델(centralized model )입니다. 더 흥미로운 모델은 서버가 아닌 클라이언트에서 업데이트를 합니다. 서버는 이렇게 상태가 없는 브로커가 됩니다. 이것은 우리에게 몇가지 장점을 제공합니다. :

  • 우리는 서버의 안정성에 대해 좀 덜 걱정하게 합니다. 충돌이 발생하는 경우, 우리는 새 인스턴스를 시작하고 새로운 값을 줄 수 있습니다.
  • 우리는 동적인 peers간의 지식을 공유하는 key-value cache를 사용할 수 있습니다.

클라이언트로부터의 업데이트는 클라이언트에서 서버로 PUSH-PULL 소켓 흐름을 통해 이동합니다. :

fig69.png

왜 클라이언트가 다른 클라이언트에게 직접 업데이트를 게시(publish)하는 것을 허용하지 않을까요? 이것이 지연시간을 줄일 것이지만, 그것이 메시지에게 고유한 시퀀스 번호를 오름차순으로 할당하는 것은 불가능하게 만듭니다. 서버가 이 작업을 수행할 수 있습니다. 더 미묘한 두 번째 이유가 있습니다. 많은 응용 프로그램에서 많은 클라이언트에 걸쳐 업데이트를 단일 명령으로 하는 것은 중요합니다. 서버를 통해 모든 업데이트를 강행하는 것은 결국 클라이언트에 도달했을 동일한 명령이 수행된 것을 보장하는 것입니다.

고유의 시퀀스를 통해 클라이언트는 애먹이는 오류를 감지할 수 있습니다 - 네트워크 혼잡 및 큐 오버플로 입니다. 만일 클라이언트가 수신 메시지 스트림의 결함을 발견하면, 그것은 조치를 취할 수 있습니다. 이것은 클라이언트가 서버에 접속하여 누락된 메시지를 요청하듯 현명한 것 같지만, 실제로 그렇게 유용하지 않습니다. 만일 결함(hole)이 있다면, 그들은 네트워크 스트레스에 의해 발생하고 있으며, 네트워크에 더 많은 스트레스를 추가하면 상황은 악화 되기만 합니다. 모든 클라이언트는 정말 "계속 할 수 없습니다", 중지하고, 누군가가 수동으로 문제의 원인을 확인하기 전까지는 다시 시작되지 않습니다 라고 사용자에게 경고 할 수 있습니다.

우리는 이제 클라이언트에서 상태 업데이트를 생성하실 수 있습니다. 서버는 다음과 같습니다. :


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

이것은 클라이언트입니다. :


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

이 코드에 대한 몇 가지 참고 사항 :

  • 서버는 클라이언트에서 업데이트를 수집하고 그들을 재 배포하는 하나의 스레드로 나뉘게 됩니다. 그것은 수신되는 업데이트에 대한 PULL 소켓, 상태 요청에 대한 ROUTER 소켓, 발신 업데이트에 대한 PUB 소켓을 관리합니다.
  • 클라이언트는 일초에 한번 서버에 임의의 업데이트를 전송하는 단순하고 소리없는 타이머를 사용합니다. 실제는 업데이트가 응용 프로그램 코드에 의해 구동됩니다

Clone Subtrees

top prev next

실질적인 key-value cache는 다수(다량)의 것을 얻을 것이며, 클라이언트는 일반적으로 cache의 일부에 관심을 가질 것입니다. 하위트리(subtree)로 작업하는 것은 상당히 간단합니다. 클라이언트는 상태 요청을 보낼 때 서버 subtree에게 전달하며, 그것이 업데이트를 동의하면 동일한 subtree를 지정합니다.

trees에 대한 두 가지 공통 구문이 있습니다. 하나는 "경로 계층 구조(path hierarchy) "이며, 다른 하나는 "항목 트리(topic tree) "입니다. 이것은 이래와 같습니다. :

  • Path hierarchy: "/some/list/of/paths"
  • Topic tree: "some.list.of.topics"

우리는 path hierarchy를 사용할 것이고, 클라이언트는 단일 subtree와 함께 작업할 수 있도록 클라이언트와 서버를 확장합니다. 다수의 subtree로 작업하는 것은 어렵지 않으며, 여기서 보여 주지는 않지만 간단합니다.

여기 모델3을 조금 변경한 서버 입니다. :


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

클라이언트입니다. :


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

Ephemeral Values

top prev next

동적으로 만료되는 임시값은 하나입니다.. 당신이 DNS와 같은 서비스를 위해 복제를 생각한다면, 임시 값은 동적 DNS를 만들도록 할 것입니다. 노드는 네트워크를 연결하고, 그것의 주소를 할당하고며, 정기적으로 확인합니다. 만일 노드가 죽으면, 그 주소는 결국 제거됩니다.

임시 값에 대한 일반적인 개념는 "세션"에 연결하고, 세션이 종료될 때 삭제하는 것입니다. Clone에서는 세션이 클라이언트에 의해 정의 될 것이며, 클라이언트가 세션을 끊으면 종료될 것입니다.

세션을 사용하는 단순 대안은 값이 종료될 때 서버에게 알려주는 "time to live"와 함께 모든 임시 값을 정의하는 것입니다. 클라이언트는 값을 재생하고, 만일 그들이 하지 않으면 값이 종료됩니다.

우리가 아직은 좀 더 복잡한 모델을 만들만한 가치가 있는지 모르기 때문에 간단한 모델을 구현하려고 합니다. 차이점은 성능일 뿐입니다. 만일 클라이언트가 소수의 임시 값을 갖고 있다면, 그것은 각각 하나 TTL을 설정하는 것이 좋습니다. 만일 클라이언트가 임시 값을 다수 사용한다면, 한번에 그들을 세션에 연결하고, 종료 시키는데 더 효율적입니다.

우선, key-value 메시지에 TTL을 인코딩하는 방법이 필요합니다. 우리는 프레임을 추가할 수 있습니다. 속성에 대한 프레임을 이용시의 문제점은 우리가 새로운 속성을 추가할 때마다, 우리는 kvmsg 클래스의 구조를 변경해야 한다는 것입니다. 그것은 호환성을 깨트립니다. 그래서 메시지에 'properties' 프레임을 추가하고, 속성 값을 얻고 넣을 수 있게 코드합니다.

다음으로, "delete this value"라고 하는 방법이 필요합니다. 지금까지 서버와 클라이언트는 항상 그들의 hash table에 맹목적으로 새로운 값을 삽입하거나 업데이트 했습니다. 만일 값이 비어있다면 그것은 "delete this key”를 의미하는 것입니다.

여기에 'properties'프레임 (우리가 나중에 필요한 UUID 프레임을, 추가)를 구현한 kvmsg 클래스의 보다 완전한 버전 있습니다. 필요한 경우, 해시에서 키를 삭제하여 빈 값을 처리합니다. :


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

Model5 클라이언트는 Model4와 거의 동일하지만, kvsimple대신에 전체 kvmsg 클래스를 사용하고, 각 메시지에 무작위로 'TTL'속성 (초 단위로 측정)을 설정합니다. :

kvmsg_set_prop (kvmsg, "ttl", "%d", randof (30));

Model5서버는 완전히 변화되었습니다. poll loop 대신에, 우리는 지금 reactor를 사용하고 있습니다. 이것은 타이머와 소켓 이벤트를 혼합해서 사용하는 것을 간단하게 만듭니다. C에서 reactor style은 더 많은 verbose입니다. 마일리지는 다른 언어로 바꿀 수 있습니다. 그러나 reactor는 더 복잡한 ØMQ 애플리케이션을 구축하는 더 좋은 방법 같습니다. 여기 서버는 다음과 같습니다. :


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

Clone Server Reliability

top prev next

클론 모델 1.5는 상대적으로 간단합니다. 우리는 지금 불행히도 복잡한 영역에 들어갈 것입니다. 복잡한 영역으로 들어가기 전에 "우리는 실제로 이것이 필요합니까?"라고 당신은 항상 물어볼 필요가 있을만큼 안정적인 메시지를 만드는 것은 복잡합니다. 당신이 신뢰성을 해결할 수 있다면, 당신은 비용과 복잡성 측면에서 큰 승리를 얻을 수 있습니다. 물론, 당신은 때때로 일부 데이터가 손실될 수 있습니다. 그것은 좋은 교환조건 입니다.

백업 서버는 클라이언트 역할을 할수 있고, 모든 클라이언트가 하는것 처럼 업데이트를 수신하여 동기의 상태를 유지할 수 있습니다. 또한 클라이언트의 새로운 업데이트를 합니다. 아직은 hash table에 이들을 저장할 수 없습니다, 그러나 잠시 동안 그것을 붙잡아 둘수 있습니다.

우리가 처리해야 할 실패 목록을 봅시다. :

  • 복제 서버 프로세스가 충돌하면 자동 또는 수동으로 다시 시작됩니다. 이 프로세스는 가지고 있던 상태를 잃고 어떠한 한 지점으로 되돌려야 합니다.
  • 복제 서버 시스템이 죽고, 상당한 시간 동안 off-line됩니다. Clients는 어딘가 다른 서버로 전환해야합니다.
  • 복제 서버 프로세스 또는 시스템이 네트워크에서 연결이 끊깁니다. 예: 스위치는 죽는다. 네트워크 언젠가 복구되겠지만, 그 동안에 Clients는 대체 서버가 필요합니다.

우리의 첫 번째 단계는 두 번째 서버를 추가하는 것입니다. 우리는 기본 및 백업을 정리했던 4장에서 이진 스타 패턴(the Binary Star pattern)을 사용할 수 있습니다. Binary Star 는 reactor 이기 때문에 이미 마지막 서버 모델을 reactor style로 리펙토링(refactoring)했던 것이 유용하게 쓰일 것입니다.

우리는 주 서버가 충돌하는 경우 업데이트가 손실되지 않도록 보장할 필요가 있습니다. 가장 간단한 기술은 두 서버에 업데이트들을 전송하는 것입니다.

백업 서버는 클라이언트 역할을 할수 있고, 모든 클라이언트가 하는것 처럼 업데이트를 수신하여 동기의 상태를 유지할 수 있습니다. 또한 클라이언트의 새로운 업데이트도 합니다. 아직은 hash table에 이들을 저장할 수 없지만, 잠시 동안 붙잡아 둘수 있습니다.

그래서, Model6는 Model5를 통해 이러한 변경 사항을 소개합니다. :

  • 우리는 클라이언트 업데이트(서버)를 위해서 push-pull flow 대신에 pub-sub flow을 사용합니다. 그 이유는 더이상의 수신자가 없다면 push소켓이 차단되고 round-robin이 됩니다. 그래서 그들 둘을 open할 필요가 있습니다. 우리는 서버의 SUB소켓들과 연결하고, 그들에게 클라이언트의 PUB 소켓을 연결합니다. 이것은 하나의 클라이언트에서 두 서버로 fan-out하는 것을 유념하십시오.
  • 우리는 주 서버가 죽었 때 클라이언트가 검색할 수 있도록 서버 업데이트(클라이언트)에 heartbeats를 추가합니다. 그런 다음 백업 서버로 전환할 수 있습니다.
  • 우리는 the Binary Star bstar reactor 클래스를 사용하여 두 서버를 연결합니다. Binary Star는 "master"라고 생각하는 서버에 요청을 하는 방법으로 투표하기 위해 클라이언트에게 의존합니다. 우리는 이것을 위해서 스냅샷 요청을 사용합니다.
  • 우리는 UUID 필드를 추가하여 모든 메시지를 유일한 ID로 업데이트 합니다. 클라이언트는 이것을 생성하고, 서버가 re-published 업데이트시에 그것을 다시 전파합니다.
  • 슬레이브 서버는 아직 마스터 서버가 아닌, 클라이언트에서 받은 업데이트의 "pending list""를 유지합니다. 또는, 아직 클라이언트가 아닌, 마스터로부터 받은 업데이트입니다. 이 리스트는 가장 오래된것부터 최신까지의 명령이기 때문에 처음부터 업데이트를 제거하는 것은 간단한 일입니다.

유한 상태 머신으로 클라이언트 로직을 설계하는 것은 유용합니다. 클라이언트 주기 :

  • 클라이언트가 오픈하고 그 소켓을 연결하고, 다음 첫 번째 서버에서 스냅샷을 요청합니다. 요청 쇄도를 피하기 위해, 두 번만 다른 서버에 요청합니다. 한 번의 요청으로 원하는 결과를 얻지 못했다면, 운이 나쁜 케이스일 뿐입니다. 하지만 두 번씩 요청했음에도 불구하고 결과를 얻지 못한 것은, 부주의 때문입니다.
  • 클라이언트가 스냅샷을 받게 되었을때, 이것은 프로세스 업데이트를 기다립니다. 다시 말하지만, 그것이 일부 초과 시간 안에 서버에서 아무런 응답하지 않은 경우, 그것은 다음 서버로 fail-over 됩니다.
  • 클라이언트가 스냅샷을 받게 되었을때, 이것은 프로세스 업데이트를 기다립니다. 다시 말하지만, 그것이 일부 초과 시간 안에 서버에서 아무런 응답하지 않은 경우, 그것은 다음 서버로 fail-over 됩니다.

클라이언트 루프는 영원합니다. 그것은 startup과 일부 클라이언트가 백업 서버와 연결하려고 시도하는 동안, 일부 클라이언트가 주 서버에 접속하려고 할 때 발생하는 fail-over시에 연관이 있습니다. Binary Star pattern은 희망차고, 정확하게, 이것을 처리합니다. (이 같은 디자인을 만드는 즐거움은 우리가 그들이 옳다는 증명할 수 없지만, 우리는 그들이 틀렸다는 것은 증명할 수 있다는 것입니다.)

우리는 클라이언트 유한 상태 시스템(finite state machine)을 설계 할 수 있습니다. :

fig70.png

Fail-over는 다음과 같이 발생합니다. :

  • 클라이언트는 주서버가 더 이상 heartbeats 전송하지 않는 것, 죽은 것을 감지합니다. 클라이언트는 백업 서버에 연결하여 새 상태 스냅샷을 요청합니다.
  • 백업 서버는 클라이언트로부터 스냅샷 요청을 수신하기 시작하며, 주 서버가 죽은 것을 감지하고, 주서버로 전환합니다.
  • 백업 서버는 자체 해시 테이블에 대기중인 목록을 적용하고, 다음 상태 스냅샷 요청을 처리하기 시작합니다

주 서버가 다시 온라인 상태가 되면, 아래 작업이 이루어 질 것입니다. :

  • 슬레이브 서버로서 시작하고, 클론 클라이언트로 백업 서버에 연결합니다.
  • 클라이언트에서 SUB 소켓을 통해 업데이트를 받기 시작합니다.

우리는 몇 가지 가정을 합니다 :

  • 하나 이상의 서버가 계속 실행됩니다 두 서버가 충돌하는 경우, 우리는 모든 서버 상태를 잃게되고 그것을 복구할 수있는 방법은 없습니다.
  • 여러 클라이언트가 동시에 동일한 해시 키를 업데이트하지 않습니다. 클라이언트 업데이트는 다른 순서로 두 서버에 도달할 것입니다. 그래서, 백업 서버는 주 서버보다 다른 순서로 보류 목록에서 업데이트를 적용할 수 있습니다. 하나의 클라이언트에서 업데이트는 항상 두 서버에서 동일한 순서로 도달되며, 안전 합니다.

이것은 Binary Star pattern을 사용한 우리의 고가용성 서버입니다. :

fig71.png

이것은 구축 첫 단계로써, 우리는 재사용 가능한 클래스로써 클라이언트를 refactoring할 것입니다. 이것은 즐거움(ØMQ로 비동기 클래스를 작성하는 것는 아름다운 실습과 같습니다)의 일부이지만, 주로 우리는 복제가 임의의 응용 프로그램에 플러그인을 정말 쉽게 되기를 원하기 때문입니다. 탄력성이 올바르게 동작하는 클라이언트에 의존한 이래로, 재사용 가능한 클라이언트 API가 있을 때 이것을 보장하는 것은 훨씬 쉬워졌습니다. 우리는 클라이언트내에서 fail-over를 처리하기 시작하면, 그것은 (클론 클라이언트와 Freelance 클라이언트를 혼합된 것을 상상해보라) 조금 복잡해 집니다.

내 평소 디자인 방식은 정확하다고 판단되는 API를 첫번째로 설계하는 것이고, 다음 그것을 구현하는 것입니다. 그래서, 우리는 복제 클라이언트로 시작하고, 클론이라고 불리는 일부 추정 클래스 API 위에 위치시키기 위해 그것을 다시 작성합니다. 임의로 작성한 코드가 API로 된다는 것은 응용 프로그램을 합리적이고 안정적으로 추상적인 정의를 한다는 것을 의미합니다. 예를 들어, 모델5에서, 클라이언트는 소스에 하드 코딩된 끝점을 사용하여 서버에 별도의 세 가지 소켓을 열었습니다. 우리는 이 같은 세 가지 방법으로 API를 만들 수 있습니다. :

// Specify endpoints for each socket we need
clone_subscribe (clone, "tcp://localhost:5556");
clone_snapshot (clone, "tcp://localhost:5557");
clone_updates (clone, "tcp://localhost:5558");

// Times two, since we have two servers
clone_subscribe (clone, "tcp://localhost:5566");
clone_snapshot (clone, "tcp://localhost:5567");
clone_updates (clone, "tcp://localhost:5568");

그러나 이것은 말이 많고 오래가지 못합니다. 디자인의 내부 응용프로그램으로 내놓는 것은 좋은 생각이 아닙니다. 오늘, 우리는 3개의 소켓을 사용합니다. 내일은, 2개 또는 4개가 될 것입니다. 정말 복제 클래스를 사용하는 모든 응용프로그램을 변경하기를 원합니까?
소시지 공장 세부를 숨기기 위해, 우리는 이와 같이 작게 추상화 합니다. :

// Specify primary and backup servers
clone_connect (clone, "tcp://localhost:5551");
clone_connect (clone, "tcp://localhost:5561");

어떤 것은 단순화(하나의 서버가 한 끝점에 위치)의 이점을 가지고 있지만 내부 디자인에 영향을 미치고 있습니다. 우리는 지금 어떻게든 세 끝점에서 단일 끝점으로 변경해야 합니다. 한 가지 방법은 우리의 client-server 프로토콜의 " 클라이언트와 서버는 연속적으로 3개 포트를 통해 통신." 에 대한 지식을 적용하는 것입니다. 또 다른 방법은 서버에서 두 누락된 endpoints를 얻는 것입니다. 우리는 가장 간단한 방법으로 합니다. :

  • 서버 상태 라우터 (ROUTER)는 포트 P 입니다.
  • 서버 업데이트 publisher (PUB)는 포트 P + 1입니다.
  • 서버 업데이트 subscriber (SUB)는 포트 P + 2입니다.

클론 클래스는 4 장에서 flcliapi 클래스와 같은 구조를 가지고 있습니다. 그것은 두 부분으로 구성되어 있습니다 :

  • background 스레드에서 실행되는 비동기 복제 에이전트. 에이전트는 모든 네트워크 I / O를 처리하고, 실시간으로 서버와 통신합니다. 응용 프로그램이 무엇을 하던지 문제는 없습니다.
  • 호출자의 스레드에서 실행되는 동기 '클론' 클래스. 당신이 복제 오브젝트를 생성할 때, 자동으로 에이전트 스레드를 시작하고, 당신이 복제 오브젝트를 파괴했을 때, 그것은 에이전트 스레드를 죽입니다.

frontend 클래스는 inproc 'pipe' 소켓 의 agent 클래스와 통신합니다.. C에서, czmq 스레드 layer는 "attached thread"를 시작할 때 자동으로 이 파이프를 생성합니다. 이것은 ØMQ에서 멀티스레딩을 위한 자연스러은 패턴입니다.

ØMQ 없이는, 이런 비동기 클래스 디자인 종류는 정말 몇주간 열심히 작업해야 합니다. ØMQ로는, 하루 또는 이틀정도 작업하면 됩니다. 결과는 실제로 작동하는 복제 프로토콜의 단순화를 한 복합체입니다. 이렇게 만든 이유가 몇 가지 있습니다. 우리는 이것을 reactor로 만들 수도 있지만, 응용 프로그램에서 그것을 사용하는 것이 더 어려울 것이기 때문입니다. 그래서 API는 일부 서버에서 말하는 key-value 테이블과 비슷합니다. :

clone_t *clone_new (void);
void clone_destroy (clone_t **self_p);
void clone_connect (clone_t *self, char *address, char *service);
void clone_set (clone_t *self, char *key, char *value);
char *clone_get (clone_t *self, char *key);

클론 클라이언트의 Model6는 다음과 같습니다 :


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

그리고 여기는 실제 복제 클래스 구현입니다 :


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

마지막으로, 여기에 복제 서버의 여섯 번째이자 마지막 모델입니다 :


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

이 메인 프로그램은 코드가 단지 몇 백 라인이지만, 작동하는데 시간이 좀 걸립니다. 정확하게 , Model6를 구축하는 것은 어려운 일 이었고, 작업에 한주 전채가 "사랑하는 하나님, 이 가이드는 너무 복잡합니다." 걸렸습니다. 우리는 이 작은 응용프로그램으로 거의 모든 것을 만들었습니다. failover, ephemeral values, subtrees 등등. 초기 디자인이 매우 정확하게 되었다는 것이 나를 깜작 놀라게 하였습니다. 그러나 너무 많은 소켓 흐름을 상세하게 작성하고 디버깅하는 것은 특별한 일입니다. 내가 이 작품을 만든 방법은 다음과 같습니다.

  • 코드에서 많은 지루한 작업을 제거하고, 나머지는 더 단순하고 확실하게 남기도록 reactors (bstar, on top of zloop) 사용했습니다. 전체 서버는 하나의 스레드로 작동되므로, 스레드 사이의 이상한 작동하는 것은 없습니다. 단지 행복하게 자신의 일을 할 수있는 모든 핸들러 주위에 구조 포인터('self') 를 전달합니다. reactors 를 사용하는 한 좋은 부작용은 덜 밀접하게 poll loop에 통합된 코드이며, 재사용이 훨씬 용이합니다. Model6의 큰 덩어리는 Model5에서 가져옵니다.
  • 그것을 조금씩 빌드함으로써, 각 조각들을 얻을 수 있었고 이는 다음 작업 전에 제대로 작동하는지 알 수 있게 해주었습니다. 4 or 5개의 소켓 플로우가 있는 것은, 꽤 많은 디버깅 및 테스트를 했다는 것을 의미 합니다. 나는 콘솔의 프린트 자료로 디버그 합니다. (예 : dumping messages). 실제로 이런 작업을 위해 디버거를 여는 것은 아무 의미가 없습니다.
  • 항상 Valgrind에서 시험하기때문에, memory leaks이 없을 것입니다. C에서는 이것이 주요 관심사지만, 당신은 어떤 garbage collector를 할당할 수 없습니다. kvmsg 및 czmq 같은 적절하고 일관된 추상적 개념을 사용하면 매우 도움이 됩니다.

코드에 여전히 결함이 있어, 어떤 독자들은 이것을 위해 디버깅과 수정하는데 주말을 보낼 것이라고 확신합니다. 실제 응용프로그램을 위하여 기초로 사용하는 이 모델만으로도 충분합니다.

여섯 번째 모델을 테스트하기위해서 임의의 순서로 기본 서버와 백업 서버와 클라이언트를 시작합니다. 그리고 임의로 서버중 하나를 죽이고 재 시작하고, 이일을 게속하도록 유지합니다. 디자인과 코드가 정확한 경우, 클라이언트는 master서버가 어떤 것이든 간에 업데이트의 동일한 스트림을 얻는 것을 유지해 갈 것입니다.

Clone Protocol Specification

top prev next

신뢰할 수있는 pub-sub을 구축하기 위해 이런 많은 작업 후, 우리는 개발하기 위해서 안전하게 응용프로그램을 구축할 수있게 몇 가지 보장을 원합니다. 좋은 시작은 프로토콜을 작성하는 것입니다. 이것은 우리가 다른 언어로 구현하게 하고, 우리는 코드에 깊이 손을 대는 것보다 종이에 디자인을 향상시켜야 합니다.

그 다음, 여기에 Clustered Hashmap Protocol이 있습니다, “cluster-wide key-value hashmap을 정의하고 클라이언트 집합에 걸쳐 이것을 공유하기 위한 메커니즘 입니다. CHP는 클라인언트가 hashmap의 subtrees와 함께 작동하고, 값을 업데이트하고, 임시값을 정의하도록 허용합니다.”

(More coming soon…)