tag:blogger.com,1999:blog-185083562026-02-13T11:24:21.556-05:00Just a little PythonBlog about all things Python that intersect my work and hobbiesRick Copelandhttp://www.blogger.com/profile/11612114223288841087noreply@blogger.comBlogger9015tag:blogger.com,1999:blog-18508356.post-2442065746150484532013-04-05T16:58:00.000-04:002013-04-05T16:58:47.367-04:00MongoDB Pub/Sub with Capped Collections<p>If you've been following this blog for any length of time, you know that my NoSQL database of choice is MongoDB. One thing that <a href="http://mongodb.org">MongoDB</a> <em>isn't</em> known for, however, is building a publish / subscribe system. <a href="http://redis.io/topics/pubsub">Redis</a>, on the other hand, <em>is</em> known for having a high-bandwith, low-latency pub/sub protocol. One thing I've always wondered is whether I can build a similar system atop MongoDB's <a href="http://docs.mongodb.org/manual/core/capped-collections/">capped collections</a>, and if so, what the performance would be. Read on to find out how it turned out... <a name='more'></a></p> <h2>Capped Collections</h2> <p>If you've not heard of capped collections before, they're a nice little feature of MongoDB that lets you have a high-performance circular queue. Capped collections have the following nice features:</p> <ul> <li>They "remember" the insertion order of their documents</li> <li>They store inserted documents in the insertion order on disk</li> <li>They remove the oldest documents in the collection automatically as new documents are inserted</li> </ul> <p>However, you give up some things with capped collections:</p> <ul> <li>They have a fixed maximum size</li> <li>You cannot shard a capped collection</li> <li>Any updates to documents in a capped collection <em>must not</em> cause a document to grow. (i.e. not all <code>$set</code> operations will work, and no <code>$push</code> or <code>$pushAll</code> will)</li> <li>You may not explicitly <code>.remove()</code> documents from a capped collection</li> </ul> <p>To create a capped collection, just issue the following command (all the examples below are in Python, but you can use any driver you want including the Javascript <code>mongo</code> shell):</p> <div class="codehilite"><pre><span class="n">db</span><span class="o">.</span><span class="n">create_collection</span><span class="p">(</span> <span class="s">&#39;capped_collection&#39;</span><span class="p">,</span> <span class="n">capped</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="n">size_in_bytes</span><span class="p">,</span> <span class="c"># required</span> <span class="nb">max</span><span class="o">=</span><span class="n">max_number_of_docs</span><span class="p">,</span> <span class="c"># optional</span> <span class="n">autoIndexId</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="c"># optional</span> </pre></div> <p>In the example above, I've created a collection that takes up <code>size_in_bytes</code> bytes on disk, will contain no more than <code>max_number_of_docs</code>, and which does <em>not</em> create an index on the <code>_id</code> field as would normally happen. Above, I mentioned that the capped collection remembers the insertion order of its documents. If you issue a <code>find()</code> with no sort specified, or with a sort of <code>('$natural', 1)</code>, then MongoDB will sort your result in insertion order. (<code>($natural, -1)</code> will likewise sort the result in reverse insertion order.) Since insertion order is the same as the on-disk ordering, these queries are extremely fast. To see this, let's create two collections, one capped and one uncapped, and fill both with small documents:</p> <div class="codehilite"><pre><span class="n">size</span> <span class="o">=</span> <span class="mi">100000</span> <span class="c"># Create the collection</span> <span class="n">db</span><span class="o">.</span><span class="n">create_collection</span><span class="p">(</span> <span class="s">&#39;capped_collection&#39;</span><span class="p">,</span> <span class="n">capped</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="mi">2</span><span class="o">**</span><span class="mi">20</span><span class="p">,</span> <span class="n">autoIndexId</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">create_collection</span><span class="p">(</span> <span class="s">&#39;uncapped_collection&#39;</span><span class="p">,</span> <span class="n">autoIndexId</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="c"># Insert small documents into both</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">insert</span><span class="p">({</span><span class="s">&#39;x&#39;</span><span class="p">:</span><span class="n">x</span><span class="p">},</span> <span class="n">manipulate</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="n">db</span><span class="o">.</span><span class="n">uncapped_collection</span><span class="o">.</span><span class="n">insert</span><span class="p">({</span><span class="s">&#39;x&#39;</span><span class="p">:</span><span class="n">x</span><span class="p">},</span> <span class="n">manipulate</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="c"># Go ahead and index the &#39;x&#39; field in the uncapped collection</span> <span class="n">db</span><span class="o">.</span><span class="n">uncapped_collection</span><span class="o">.</span><span class="n">ensure_index</span><span class="p">(</span><span class="s">&#39;x&#39;</span><span class="p">)</span> </pre></div> <p>Now we can see the performance gains by executing <code>find()</code> on each. For this, I'll use the <a href="http://ipython.org">IPython</a>, <a href="https://github.com/rick446/ipymongo">IPyMongo</a>, and the magic <code>%timeit</code> function: </p> <div class="codehilite"><pre><span class="n">In</span> <span class="p">[</span><span class="mi">72</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="o">%</span><span class="n">timeit</span> <span class="n">x</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">find</span><span class="p">())</span> <span class="mi">1000</span> <span class="n">loops</span><span class="p">,</span> <span class="n">best</span> <span class="n">of</span> <span class="mi">3</span><span class="p">:</span> <span class="mi">708</span> <span class="n">us</span> <span class="n">per</span> <span class="n">loop</span> <span class="n">In</span> <span class="p">[</span><span class="mi">73</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="o">%</span><span class="n">timeit</span> <span class="n">x</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">uncapped_collection</span><span class="o">.</span><span class="n">find</span><span class="p">()</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="s">&#39;x&#39;</span><span class="p">))</span> <span class="mi">1000</span> <span class="n">loops</span><span class="p">,</span> <span class="n">best</span> <span class="n">of</span> <span class="mi">3</span><span class="p">:</span> <span class="mi">912</span> <span class="n">us</span> <span class="n">per</span> <span class="n">loop</span> </pre></div> <p>So we get a moderate speedup, which is nice, but not spectacular. What becomes really interesting with capped collections is that they support <em>tailable cursors</em>.</p> <h2>Tailable cursors</h2> <p>If you're querying a capped collection in insertion order, you can pass a special flag to <code>find()</code> that says that it should "follow the tail" of the collection if new documents are inserted rather than returning the result of the query on the collection at the time the query was initiated. This behavior is similar to the behavior of the Unix <code>tail -f</code> command, hence its name. To see this behavior, let's query our capped collection with a 'regular' cursor as well as a 'tailable' cursor. First, the 'regular' cursor:</p> <div class="codehilite"><pre><span class="n">In</span> <span class="p">[</span><span class="mi">76</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">find</span><span class="p">()</span> <span class="n">In</span> <span class="p">[</span><span class="mi">77</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">cur</span><span class="o">.</span><span class="n">next</span><span class="p">()</span> <span class="n">Out</span><span class="p">[</span><span class="mi">77</span><span class="p">]:</span> <span class="p">{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">}</span> <span class="n">In</span> <span class="p">[</span><span class="mi">78</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">cur</span><span class="o">.</span><span class="n">next</span><span class="p">()</span> <span class="n">Out</span><span class="p">[</span><span class="mi">78</span><span class="p">]:</span> <span class="p">{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span> <span class="n">In</span> <span class="p">[</span><span class="mi">79</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">insert</span><span class="p">({</span><span class="s">&#39;y&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span> <span class="n">Out</span><span class="p">[</span><span class="mi">79</span><span class="p">]:</span> <span class="n">ObjectId</span><span class="p">(</span><span class="s">&#39;515f205cfb72f0385c3c2414&#39;</span><span class="p">)</span> <span class="n">In</span> <span class="p">[</span><span class="mi">80</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="nb">list</span><span class="p">(</span><span class="n">cur</span><span class="p">)</span> <span class="n">Out</span><span class="p">[</span><span class="mi">80</span><span class="p">]:</span> <span class="p">[{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">2</span><span class="p">},</span> <span class="o">...</span> <span class="p">{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">99</span><span class="p">}]</span> </pre></div> <p>Notice above that the document we inserted <code>{'y': 1}</code> is not included in the result since it was inserted <em>after we started iterating</em>. Now, let's try a tailable cursor:</p> <div class="codehilite"><pre><span class="n">In</span> <span class="p">[</span><span class="mi">81</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">tailable</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">In</span> <span class="p">[</span><span class="mi">82</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">cur</span><span class="o">.</span><span class="n">next</span><span class="p">()</span> <span class="n">Out</span><span class="p">[</span><span class="mi">82</span><span class="p">]:</span> <span class="p">{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span> <span class="n">In</span> <span class="p">[</span><span class="mi">83</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">cur</span><span class="o">.</span><span class="n">next</span><span class="p">()</span> <span class="n">Out</span><span class="p">[</span><span class="mi">83</span><span class="p">]:</span> <span class="p">{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">2</span><span class="p">}</span> <span class="n">In</span> <span class="p">[</span><span class="mi">84</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">insert</span><span class="p">({</span><span class="s">&#39;y&#39;</span><span class="p">:</span> <span class="mi">2</span><span class="p">})</span> <span class="n">Out</span><span class="p">[</span><span class="mi">84</span><span class="p">]:</span> <span class="n">ObjectId</span><span class="p">(</span><span class="s">&#39;515f20ddfb72f0385c3c2415&#39;</span><span class="p">)</span> <span class="n">In</span> <span class="p">[</span><span class="mi">85</span><span class="p">]</span> <span class="p">(</span><span class="n">test</span><span class="p">):</span> <span class="nb">list</span><span class="p">(</span><span class="n">cur</span><span class="p">)</span> <span class="n">Out</span><span class="p">[</span><span class="mi">85</span><span class="p">]:</span> <span class="p">[{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">3</span><span class="p">},</span> <span class="o">...</span> <span class="p">{</span><span class="s">u&#39;x&#39;</span><span class="p">:</span> <span class="mi">99</span><span class="p">},</span> <span class="p">{</span><span class="s">u&#39;_id&#39;</span><span class="p">:</span> <span class="n">ObjectId</span><span class="p">(</span><span class="s">&#39;515f205cfb72f0385c3c2414&#39;</span><span class="p">),</span> <span class="s">u&#39;y&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">},</span> <span class="p">{</span><span class="s">u&#39;_id&#39;</span><span class="p">:</span> <span class="n">ObjectId</span><span class="p">(</span><span class="s">&#39;515f20ddfb72f0385c3c2415&#39;</span><span class="p">),</span> <span class="s">u&#39;y&#39;</span><span class="p">:</span> <span class="mi">2</span><span class="p">}]</span> </pre></div> <p>Now we see that both the "y" document we created before as well as the one created during <em>this</em> cursor's iteration included in the result.</p> <h3>Waiting on data</h3> <p>While tailable cursors are nice for picking up the inserts that happened while we were iterating over the cursor, one thing that a true pub/sub system needs is low latency. Polling the collection to see if messages have been inserted is a non-starter from a latency standpoint because you have to do one of two things:</p> <ul> <li>Poll continuously, using prodigious server resources</li> <li>Poll intermittently, increasing latency</li> </ul> <p>Tailable cursors have another option you can use to "fix" the above problems: the <code>await_data</code> flag. This flag tells MongoDB to actually wait a second or two on an exhausted tailable cursor to see if more data is going to be inserted. In PyMongo, the way to set this flag is quite simple:</p> <div class="codehilite"><pre><span class="n">cur</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="o">.</span><span class="n">find</span><span class="p">(</span> <span class="n">tailable</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">await_data</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> </pre></div> <h2>Building a pub/sub system</h2> <p>OK, now that we have a capped collection, with tailable cursors awaiting data, how can we make this into a pub/sub system? The basic approach is:</p> <ul> <li>We use a single capped collection of moderate size (let's say 32kB) for all messages</li> <li>Publishing a message consists of inserting a document into this collection with the following format: <code>{ 'k': topic, 'data': data }</code></li> <li>Subscribing to the collection is a tailable query on the collection, using a regular expression to only get the messages we're interested in.</li> </ul> <p>The actual query we use is similar to the following:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">get_cursor</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="n">topic_re</span><span class="p">,</span> <span class="n">await_data</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&#39;tailable&#39;</span><span class="p">:</span> <span class="bp">True</span> <span class="p">}</span> <span class="k">if</span> <span class="n">await_data</span><span class="p">:</span> <span class="n">options</span><span class="p">[</span><span class="s">&#39;await_data&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">collection</span><span class="o">.</span><span class="n">find</span><span class="p">(</span> <span class="p">{</span> <span class="s">&#39;k&#39;</span><span class="p">:</span> <span class="n">topic_re</span> <span class="p">},</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="o">.</span><span class="n">hint</span><span class="p">([(</span><span class="s">&#39;$natural&#39;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)])</span> <span class="c"># ensure we don&#39;t use any indexes</span> <span class="k">return</span> <span class="n">cur</span> </pre></div> <p>Once we have the <code>get_cursor</code> function, we can do something like the following to execute the query:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">re</span><span class="o">,</span> <span class="nn">time</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">get_cursor</span><span class="p">(</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="s">&#39;^foo&#39;</span><span class="p">),</span> <span class="n">await_data</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">cur</span><span class="p">:</span> <span class="n">do_something</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span> </pre></div> <p>Of course, the system above has a couple of problems:</p> <ul> <li>We have to receive every message in the collection before we get to the 'end'</li> <li>We have to go back to the beginning if we ever exhaust the cursor (and its <code>await_data</code> delay)</li> </ul> <p>The way we can avoid these problems is by adding a <em>sequence number</em> to each message.</p> <h2>Sequences</h2> <p>"But wait," I imagine you to say, "MongoDB doesn't have an autoincrement field like MySQL! How can we generate sequences?" The answer lies in the <code>find_and_modify()</code> command, coupled with the <code>$inc</code> operator in MongoDB. To construct our sequence generator, we can use a dedicated "sequence" collection that contains nothing but counters. Each time we need a new sequence number, we perform a <code>find_and_modify()</code> with <code>$inc</code> and get the new number. The code for this turns out to be very short:</p> <div class="codehilite"><pre><span class="k">class</span> <span class="nc">Sequence</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">&#39;mongotools.sequence&#39;</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span> <span class="bp">self</span><span class="o">.</span><span class="n">_name</span> <span class="o">=</span> <span class="n">name</span> <span class="k">def</span> <span class="nf">cur</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span> <span class="n">doc</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_name</span><span class="p">]</span><span class="o">.</span><span class="n">find_one</span><span class="p">({</span><span class="s">&#39;_id&#39;</span><span class="p">:</span> <span class="n">name</span><span class="p">})</span> <span class="k">if</span> <span class="n">doc</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">return</span> <span class="mi">0</span> <span class="k">return</span> <span class="n">doc</span><span class="p">[</span><span class="s">&#39;value&#39;</span><span class="p">]</span> <span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sname</span><span class="p">,</span> <span class="n">inc</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span> <span class="n">doc</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_name</span><span class="p">]</span><span class="o">.</span><span class="n">find_and_modify</span><span class="p">(</span> <span class="n">query</span><span class="o">=</span><span class="p">{</span><span class="s">&#39;_id&#39;</span><span class="p">:</span> <span class="n">sname</span><span class="p">},</span> <span class="n">update</span><span class="o">=</span><span class="p">{</span><span class="s">&#39;$inc&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;value&#39;</span><span class="p">:</span> <span class="n">inc</span> <span class="p">}</span> <span class="p">},</span> <span class="n">upsert</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">new</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">return</span> <span class="n">doc</span><span class="p">[</span><span class="s">&#39;value&#39;</span><span class="p">]</span> </pre></div> <p>Once we have the ability to generate sequences, we can now add a sequence number to our messages on publication:</p> <div class="codehilite"><pre><span class="n">def</span> <span class="n">pub</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="n">sequence</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">data</span><span class="p">=</span><span class="n">None</span><span class="p">):</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">dict</span><span class="p">(</span> <span class="n">ts</span><span class="p">=</span><span class="n">sequence</span><span class="p">.</span><span class="n">next</span><span class="p">(</span><span class="n">collection</span><span class="p">.</span><span class="n">name</span><span class="p">),</span> <span class="n">k</span><span class="p">=</span><span class="n">key</span><span class="p">,</span> <span class="n">data</span><span class="p">=</span><span class="n">data</span><span class="p">)</span> <span class="n">collection</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">manipulate</span><span class="p">=</span><span class="n">False</span><span class="p">)</span> </pre></div> <p>Our subscribing query, unfortunately, needs to get a bit more complicated:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">get_cursor</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="n">topic_re</span><span class="p">,</span> <span class="n">last_id</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">await_data</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&#39;tailable&#39;</span><span class="p">:</span> <span class="bp">True</span> <span class="p">}</span> <span class="n">spec</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&#39;ts&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;$gt&#39;</span><span class="p">:</span> <span class="n">last_id</span> <span class="p">},</span> <span class="c"># only new messages</span> <span class="s">&#39;k&#39;</span><span class="p">:</span> <span class="n">topic_re</span> <span class="p">}</span> <span class="k">if</span> <span class="n">await_data</span><span class="p">:</span> <span class="n">options</span><span class="p">[</span><span class="s">&#39;await_data&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">collection</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">spec</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="o">.</span><span class="n">hint</span><span class="p">([(</span><span class="s">&#39;$natural&#39;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)])</span> <span class="c"># ensure we don&#39;t use any indexes</span> <span class="k">return</span> <span class="n">cur</span> </pre></div> <p>And our dispatch loop likewise must keep track of the sequence number:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">re</span><span class="o">,</span> <span class="nn">time</span> <span class="n">last_id</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">get_cursor</span><span class="p">(</span> <span class="n">db</span><span class="o">.</span><span class="n">capped_collection</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="s">&#39;^foo&#39;</span><span class="p">),</span> <span class="n">await_data</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">cur</span><span class="p">:</span> <span class="n">last_id</span> <span class="o">=</span> <span class="n">msg</span><span class="p">[</span><span class="s">&#39;ts&#39;</span><span class="p">]</span> <span class="n">do_something</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span> </pre></div> <p>We an actually improve upon this a tiny bit by finding the <code>ts</code> field of the last value in the collection and using it to initialize our <code>last_id</code> value:</p> <div class="codehilite"><pre><span class="n">last_id</span> <span class="p">=</span> <span class="o">-</span>1 <span class="n">cur</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">capped_collection</span><span class="p">.</span><span class="nb">find</span><span class="p">().</span><span class="n">sort</span><span class="p">([(</span><span class="s">&#39;$natural&#39;</span><span class="p">,</span> <span class="o">-</span>1<span class="p">)])</span> <span class="k">for</span> <span class="n">msg</span> <span class="n">in</span> <span class="n">cur</span><span class="p">:</span> <span class="n">last_id</span> <span class="p">=</span> <span class="n">msg</span><span class="p">[</span><span class="s">&#39;ts&#39;</span><span class="p">]</span> <span class="k">break</span> <span class="p">...</span> </pre></div> <p>So we've fixed the problem of processing messages multiple times, but we still have a slow scan of the whole capped collection on startup. Can we fix this? It turns out we can, but not without questionable "magic."</p> <h2>Now, for some questionable magic...</h2> <p>You may be wondering why I would use a strange name like <code>ts</code> to hold a sequence number. It turns out that there is poorly documented option for cursors that we can abuse to substantially speed up the initial scan of the capped collection: the <code>oplog_replay</code> option. As is apparent from the name of the option, it is mainly used to replay the "oplog", that magic capped collection that makes MongoDB's replication internals work so well. The oplog uses a <code>ts</code> field to indicate the timestamp of a particular operation, and the <code>oplog_replay</code> option requires the use of a <code>ts</code> field in the query.</p> <p>Now since <code>oplog_replay</code> isn't really intended to be (ab)used by us mere mortals, it's not directly exposed in the PyMongo driver. However, we <em>can</em> manage to get to it via some trickery:</p> <div class="codehilite"><pre><span class="kn">from</span> <span class="nn">pymongo.cursor</span> <span class="kn">import</span> <span class="n">_QUERY_OPTIONS</span> <span class="k">def</span> <span class="nf">get_cursor</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="n">topic_re</span><span class="p">,</span> <span class="n">last_id</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">await_data</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&#39;tailable&#39;</span><span class="p">:</span> <span class="bp">True</span> <span class="p">}</span> <span class="n">spec</span> <span class="o">=</span> <span class="p">{</span> <span class="s">&#39;ts&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;$gt&#39;</span><span class="p">:</span> <span class="n">last_id</span> <span class="p">},</span> <span class="c"># only new messages</span> <span class="s">&#39;k&#39;</span><span class="p">:</span> <span class="n">topic_re</span> <span class="p">}</span> <span class="k">if</span> <span class="n">await_data</span><span class="p">:</span> <span class="n">options</span><span class="p">[</span><span class="s">&#39;await_data&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">collection</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">spec</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="o">.</span><span class="n">hint</span><span class="p">([(</span><span class="s">&#39;$natural&#39;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)])</span> <span class="c"># ensure we don&#39;t use any indexes</span> <span class="k">if</span> <span class="n">await</span><span class="p">:</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">cur</span><span class="o">.</span><span class="n">add_option</span><span class="p">(</span><span class="n">_QUERY_OPTIONS</span><span class="p">[</span><span class="s">&#39;oplog_replay&#39;</span><span class="p">])</span> <span class="k">return</span> <span class="n">cur</span> </pre></div> <p>(Yeah, I know it's bad to import an underscore-prefixed name from another module. But it's marginally better than simply saying <code>oplog_replay_option=8</code>, which is the other way to make this whole thing work....)</p> <h2>Performance</h2> <p>So now we have the skeleton of a pubsub system using capped collections. If you'd like to use it yourself, all the code is available on Github in the <a href="https://github.com/rick446/mongotools">MongoTools</a> project. So how does it perform? Well obviously the performance depends on the particular type of message passing you're doing. In the MongoTools project, there are a couple of Python example programs <code>latency_test_pub.py</code> and <code>latency_test_sub.py</code> in the <code>mongotools/examples/pubsub</code> directory that allow you to do your own benchmarking. In my personal benchmarking, running everything locally with small messages, I'm able to get about 1100 messages per second with a latency of 2.5ms (with publishing options <code>-n 1 -c 1 -s 0</code>), or about 33,000 messages per second with a latency of 8ms (this is with <code>-n 100 -c 1 -s 0</code>). For pure publishing bandwidth (the subscriber can't consume this many messages per second), I seem to max out at around 75,000 messages (inserts) per second.</p> <p>So what do you think? With MongoTools <code>pubsub</code> module is MongoDB a viable competitor to Redis as a low-latency, high-bandwidth pub/sub channel? Let me know in the comments below!</p><div class="blogger-post-footer"><!-- // MAILCHIMP SUBSCRIBE CODE \\ --> <a href="http://eepurl.com/ifqEc">Subscribe via email</a> <!-- \\ MAILCHIMP SUBSCRIBE CODE // --></div>Rick Copelandhttp://www.blogger.com/profile/11612114223288841087noreply@blogger.com22tag:blogger.com,1999:blog-18508356.post-3781731314398734382012-09-25T12:04:00.000-04:002012-09-25T12:04:31.329-04:00MongoDB Schema Design at Scale<p>I had the recent opportunity to present a talk at MongoDB Seattle on <a href="http://www.slideshare.net/rick446/schema-design-at-scale">Schema Design at Scale</a>. It's basically a short case study on what steps the <a href="http://www.10gen.com/mongodb-monitoring-service">MongoDB Monitoring Service</a> (MMS) folks did to evolve their schema, along with some quantitative performance comparisons between different schemas. Given that one of my most widely read blog posts is still <a href="http://blog.pythonisito.com/2011/12/mongodbs-write-lock.html">MongoDB's Write Lock</a>, I decided that my blog readers would also be interested in the quantitative comparison as well.</p> <a name='more'></a> <h2>MongoDB Monitoring Service</h2> <p>First off, I should mention that I am not now, nor have I ever been, an employee of <a href="http://10gen.com">10gen</a>, the company behind <a href="http://www.mongodb.org">MongoDB</a>. I am, however, a longtime MongoDB user, and have seen a <em>lot</em> of presentations and use cases on the database. My knowledge of MMS's internal design comes from watching publically-available talks. I don't have any inside knowledge or precise performance numbers, so I decided to do some experiments on my own to see the impact of different schema designs they <em>might</em> have used to build MMS.</p> <p>So what is MMS, anyway? The MongoDB Monitoring Service is a free service offered by 10gen to all MongoDB users to monitor several key performance indicators on their MongoDB installations. The way it works is this:</p> <ul> <li>You download a small script that you run on your own servers that will periodically upload performance statistics to MMS.</li> <li>You access reports through the MMS website. You can graph per-minute performance of any of the metrics as well as see historical trends.</li> </ul> <h2>Eating your own dogfood</h2> <p>When 10gen designed MMS, they decided that it would not only be a useful service for those who have deployed MongoDB, but that it would also be a showcase of MongoDB's performance, keeping the performance graphs updated in real time across all customers and servers. To that end, they store all the performance metrics in MongoDB documents and get by on a modest (I don't know exactly <em>how</em> modest) cluster of MongoDB servers. </p> <p>To that end, it was extremely important in the case of MMS to use the hardware they had allocated <em>efficiently</em>. Since this service is available for real-time reporting 24 hours per day, they had to make design the system to be responsive even under "worst-case" conditions, avoiding anything in the design that would cause an uneven performance during the day. </p> <h2>Building an MMS-like system</h2> <p>Since I don't have access to the <em>actual</em> MMS software, I decided to build a system that's similar to MMS. Basically, what I wanted was a MongoDB schema that would allow me to keep per-minute counters on a collection of different metrics (we could imagine something like a web page analytics system using such a schema, for example). </p> <p>In order to keep everything compact, I decided to keep a day's statistics inside a single MongoDB document. The basic schema is the following:</p> <div class="codehilite"><pre><span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&quot;20101010/metric-1&quot;</span><span class="p">,</span> <span class="nx">metadata</span><span class="o">:</span> <span class="p">{</span> <span class="nx">date</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s2">&quot;2000-10-10T00:00:00Z&quot;</span><span class="p">),</span> <span class="nx">metric</span><span class="o">:</span> <span class="s2">&quot;metric-1&quot;</span> <span class="p">},</span> <span class="nx">daily</span><span class="o">:</span> <span class="mi">5468426</span><span class="p">,</span> <span class="nx">hourly</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;00&quot;</span><span class="o">:</span> <span class="mi">227850</span><span class="p">,</span> <span class="s2">&quot;01&quot;</span><span class="o">:</span> <span class="mi">210231</span><span class="p">,</span> <span class="p">...</span> <span class="s2">&quot;23&quot;</span><span class="o">:</span> <span class="mi">20457</span> <span class="p">},</span> <span class="nx">minute</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;0000&quot;</span><span class="o">:</span> <span class="mi">3612</span><span class="p">,</span> <span class="s2">&quot;0001&quot;</span><span class="o">:</span> <span class="mi">3241</span><span class="p">,</span> <span class="p">...</span> <span class="s2">&quot;1439&quot;</span><span class="o">:</span> <span class="mi">2819</span> <span class="p">}</span> <span class="p">}</span> </pre></div> <p>Here, we keep the date and metric we're storing in a "metadata" property so we can easily query it later. Note that the date and metric name are also embedded in the <code>_id</code> field as well (that will be important later). Actual metric data is stored in the <code>daily</code>, <code>hourly</code>, and <code>minute</code> properties. </p> <p>Now if we want to update this document (say, to record a hit to a web page), we can use MongoDB's in-place update operators to increment the appropriate daily, hourly, and per-minute counters. To further simplify things, we'll use MongoDB's "upsert" feature to create a document if it doesn't already exist (this prevents us from having to allocate the documents ahead-of-time). The first version of our update method, then, looks like this:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">record_hit</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span><span class="p">,</span> <span class="n">measure</span><span class="p">):</span> <span class="n">sdate</span> <span class="o">=</span> <span class="n">dt</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">&#39;%Y%m</span><span class="si">%d</span><span class="s">&#39;</span><span class="p">)</span> <span class="n">metadata</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span> <span class="n">date</span><span class="o">=</span><span class="n">datetime</span><span class="o">.</span><span class="n">combine</span><span class="p">(</span> <span class="n">dt</span><span class="o">.</span><span class="n">date</span><span class="p">(),</span> <span class="n">time</span><span class="o">.</span><span class="n">min</span><span class="p">),</span> <span class="n">measure</span><span class="o">=</span><span class="n">measure</span><span class="p">)</span> <span class="nb">id</span><span class="o">=</span><span class="s">&#39;</span><span class="si">%s</span><span class="s">/</span><span class="si">%s</span><span class="s">&#39;</span> <span class="o">%</span> <span class="p">(</span><span class="n">sdate</span><span class="p">,</span> <span class="n">measure</span><span class="p">)</span> <span class="n">minute</span> <span class="o">=</span> <span class="n">dt</span><span class="o">.</span><span class="n">hour</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">+</span> <span class="n">dt</span><span class="o">.</span><span class="n">minute</span> <span class="n">coll</span><span class="o">.</span><span class="n">update</span><span class="p">(</span> <span class="p">{</span> <span class="s">&#39;_id&#39;</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span> <span class="s">&#39;metadata&#39;</span><span class="p">:</span> <span class="n">metadata</span> <span class="p">},</span> <span class="p">{</span> <span class="s">&#39;$inc&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;daily&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">&#39;hourly.</span><span class="si">%.2d</span><span class="s">&#39;</span> <span class="o">%</span> <span class="n">dt</span><span class="o">.</span><span class="n">hour</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">&#39;minute.</span><span class="si">%.4d</span><span class="s">&#39;</span> <span class="o">%</span> <span class="n">minute</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">},</span> <span class="n">upsert</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> </pre></div> <p>To use this to record a "hit" to our website, then, we would simply call it with our collection, the current date, and the measure being updated:</p> <div class="codehilite"><pre><span class="gp">&gt;&gt;&gt; </span><span class="n">record_hit</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">daily_hits</span><span class="p">,</span> <span class="n">datetime</span><span class="o">.</span><span class="n">utcnow</span><span class="p">(),</span> <span class="s">&#39;/path/to/my/page.html&#39;</span><span class="p">)</span> </pre></div> <h2>Measuring performance</h2> <p>To measure the performance of this approach, I created a 2-server cluster on Amazon EC2: one server to run MongoDB and one to run my benchmark code to do a bunch of <code>record_hit()</code> calls, simulating different times of day to see the performance over multiple 24-hour periods. This is what I found:</p> <p><img alt="Initial Schema Performance" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuDFVd29uqr33DugVtGE2Q4Lv60yPBbIi-fTx-5LsHQ4gJhkaD2IpD4gLqMIjihol8c_OAo6klGGfua6kLJ9sFZdPD7eEYIhit9wM4KqfVnqPbE0AIeW91vI5ikYG5XMznOyk/s320/Initial.png" title="Initial Design" /></p> <p>Ouch! For some reason, we see the performance of our system steadily decrease from 3000-5000 writes per second to 200-300 writes per second as the day goes on. This, it turns out, happens because our "in-place" update was not, in fact, in-place. </p> <h2>Growing documents</h2> <p>MongoDB allows you great flexibility when updating your documents, even allowing you to add new fields and cause the documents to grow in size over time. And as long as your documents don't grow <em>too much</em>, everything just kind of works. MongoDB will allocate some "padding" to your documents, assuming some growth, and as long as you don't outgrow your padding, there's really very little performance impact.</p> <p>Once you <em>do</em> outgrow your padding, however, MongoDB has to <em>move</em> your document to another location. As your documeng gets bigger, this takes longer (more bytes to copy and all that). So documents that grow and grow and grow are a real performance-killer with MongoDB. And that's exactly what we have here. Consider the first time we call <code>record_hit</code> during a day. After this, the document looks like the following:</p> <div class="codehilite"><pre><span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="p">...,</span> <span class="nx">metadata</span><span class="o">:</span> <span class="p">{...},</span> <span class="nx">daily</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">hourly</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;00&quot;</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="nx">minute</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;&quot;</span><span class="mi">0000</span><span class="err">&quot;</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}</span> </pre></div> <p>Then we record a hit during the second minute of a day and our document grows:</p> <div class="codehilite"><pre><span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="p">...,</span> <span class="nx">metadata</span><span class="o">:</span> <span class="p">{...},</span> <span class="nx">daily</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nx">hourly</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;00&quot;</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="nx">minute</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;&quot;</span><span class="mi">0000</span><span class="s2">&quot;: 1, &quot;</span><span class="mi">0001</span><span class="err">&quot;</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}</span> </pre></div> <p>Now, even if we're only recording a single hit per minute, our document had to grow 1439 times, and by the end of the day it takes up <em>substantially</em> more space than it did when we recorded our first hit just after midnight.</p> <h2>Fixing with preallocation</h2> <p>The solution to the problem of growing documents is preallocation. However, we'd prefer not to preallocate all the documents at once (this would cause a large load on the server), and we'd prefer not to manually schedule documents for preallocation throughout the day (that's just a pain). The solution that 10gen decided upon, then, was to randomly (with a small probability) preallocate <em>tomorrow's</em> document each time we record a hit <em>today</em>. </p> <p>In the system I designed, this preallocation is performed at the beginning of <code>record_hit</code>:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">record_hit</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span><span class="p">,</span> <span class="n">measure</span><span class="p">):</span> <span class="k">if</span> <span class="n">PREALLOC</span> <span class="ow">and</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="p">(</span><span class="mf">1.0</span><span class="o">/</span><span class="mf">2000.0</span><span class="p">):</span> <span class="n">preallocate</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span> <span class="n">measure</span><span class="p">)</span> <span class="c"># ... </span> </pre></div> <p>Our preallocate function isn't that interesting, so I'll just show the general idea here:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">preallocate</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span><span class="p">,</span> <span class="n">measure</span><span class="p">):</span> <span class="n">metadata</span><span class="p">,</span> <span class="nb">id</span> <span class="o">=</span> <span class="c"># compute metadata and ID</span> <span class="n">coll</span><span class="o">.</span><span class="n">update</span><span class="p">(</span> <span class="p">{</span> <span class="s">&#39;_id&#39;</span><span class="p">:</span> <span class="nb">id</span> <span class="p">},</span> <span class="p">{</span> <span class="s">&#39;$set&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;metadata&#39;</span><span class="p">:</span> <span class="n">metadata</span> <span class="p">},</span> <span class="s">&#39;$inc&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;daily&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#39;hourly.00&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="c"># ...</span> <span class="s">&#39;hourly.23&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#39;minute.0000&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="c"># ...</span> <span class="s">&#39;minute.1439: 0 } },</span> <span class="n">upsert</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> </pre></div> <p>There are two important things to note here:</p> <ul> <li>Our <code>preallocate</code> function is <em>safe</em>. If by some chance we call preallocate on a date/metric that already has a document, nothing changes.</li> <li>Even if <code>preallocate</code> is never called, <code>record_hit</code> is still functionally correct, so we don't have to worry about the small probability that we get through a whole day without preallocating a document.</li> </ul> <p>Now with these changes in place, we see much better performance:</p> <p><img alt="Performance with Preallocation" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZWPiQOf_Tf5qh0_Dvxmry06Y73q0yzkFwts3zDkcljqDVpiN2hvQW7NfUB1RcdrSPYmvNEbnLIs5Z32KJsOCssXsrDPPnZh6iWSzGDNrDNBU1KRGeaRMDCLWLBkiLwNuEDfo/s1600/Prealloc.png" title="With Preallocated Documents" /></p> <p>We've actually improvide performance in two ways using this approach:</p> <ul> <li>Preallocation means that our documents never grow, so they never get moved</li> <li>By preallocating throughout the day, we don't have a "midnight problem" where our upserts all end up inserting a new document and increasing load on the server.</li> </ul> <p>We do, however, have a curious downward trend in performance throughout the day (though much less drastic than before). Where did that come from?</p> <h2>MongoDB's storage format</h2> <p>To figure out the downward performance through the day, we need to take a brief detour into the actual format that MongoDB uses to store data on disk (and memory), <a href="http://bsonspec.org">BSON</a>. Normally, we don't need to worry about it, since the <a href="http://api.mongodb.org/python/current/">pymongo</a> driver converts everything so nicely into native Python types, but in this case BSON presents us a performance problem.</p> <p>Although MongoDB documents, such as our <code>minute</code> embedded document, are represented in Python as a <code>dict</code> (which is a constant-speed lookup hash table), BSON actually <em>stores</em> documents as an <a href="http://en.wikipedia.org/wiki/Association_list">association list</a>. So rather than having a nice hash table for <code>minute</code>, we actually have something that looks more like the following:</p> <div class="codehilite"><pre><span class="n">minute</span> <span class="o">=</span> <span class="p">[</span> <span class="p">[</span> <span class="s">&quot;0000&quot;</span><span class="p">,</span> <span class="mi">3612</span> <span class="p">],</span> <span class="p">[</span> <span class="s">&quot;0001&quot;</span><span class="p">,</span> <span class="mi">3241</span> <span class="p">],</span> <span class="c"># ...</span> <span class="p">[</span> <span class="s">&quot;1439&quot;</span><span class="p">,</span> <span class="mi">2819</span> <span class="p">]</span> <span class="p">]</span> </pre></div> <p>Now to actually update a particular minute, the MongoDB server performs the something like the following operations (psuedocode, with lots of special cases ignored):</p> <div class="codehilite"><pre><span class="n">inc_value</span><span class="p">(</span><span class="n">minute</span><span class="p">,</span> <span class="s">&quot;1439&quot;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="k">def</span> <span class="nf">inc_value</span><span class="p">(</span><span class="n">document</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">document</span><span class="p">:</span> <span class="k">if</span> <span class="n">entry</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="n">key</span><span class="p">:</span> <span class="n">entry</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">value</span> <span class="k">break</span> </pre></div> <p>The performance of <em>this</em> algorithm, far from our nice O(1) hash table, is actually O(N) in the number of entries in the document. In the case of the <code>minute</code> document, MongoDB has to actually perform <strong>1439</strong> comparisons before it finds the appropriate slot to update.</p> <h2>Fixing the downward trend with hierarchy</h2> <p>To fix the problem, then, we need to reduce the number of comparisons MongoDB needs to do to find the right minute to increment. The way we can do this is by splitting up the minutes into hours. Our daily stats document now looks like the following:</p> <div class="codehilite"><pre><span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&quot;20101010/metric-1&quot;</span><span class="p">,</span> <span class="nx">metadata</span><span class="o">:</span> <span class="p">{</span> <span class="nx">date</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s2">&quot;2000-10-10T00:00:00Z&quot;</span><span class="p">),</span> <span class="nx">metric</span><span class="o">:</span> <span class="s2">&quot;metric-1&quot;</span> <span class="p">},</span> <span class="nx">daily</span><span class="o">:</span> <span class="mi">5468426</span><span class="p">,</span> <span class="nx">hourly</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;0&quot;</span><span class="o">:</span> <span class="mi">227850</span><span class="p">,</span> <span class="s2">&quot;1&quot;</span><span class="o">:</span> <span class="mi">210231</span><span class="p">,</span> <span class="p">...</span> <span class="s2">&quot;23&quot;</span><span class="o">:</span> <span class="mi">20457</span> <span class="p">},</span> <span class="nx">minute</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;00&quot;</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&quot;0000&quot;</span><span class="o">:</span> <span class="mi">3612</span><span class="p">,</span> <span class="s2">&quot;0100&quot;</span><span class="o">:</span> <span class="mi">3241</span><span class="p">,</span> <span class="p">...</span> <span class="p">},</span> <span class="p">...,</span> <span class="s2">&quot;23&quot;</span><span class="o">:</span> <span class="p">{</span> <span class="p">...,</span> <span class="s2">&quot;1439&quot;</span><span class="o">:</span> <span class="mi">2819</span> <span class="p">}</span> <span class="p">}</span> </pre></div> <p>Our <code>record_hit</code> and <code>preallocate</code> routines have to change a bit as well:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">record_hit_hier</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span><span class="p">,</span> <span class="n">measure</span><span class="p">):</span> <span class="k">if</span> <span class="n">PREALLOC</span> <span class="ow">and</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="p">(</span><span class="mf">1.0</span><span class="o">/</span><span class="mf">1500.0</span><span class="p">):</span> <span class="n">preallocate_hier</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span> <span class="n">measure</span><span class="p">)</span> <span class="n">sdate</span> <span class="o">=</span> <span class="n">dt</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">&#39;%Y%m</span><span class="si">%d</span><span class="s">&#39;</span><span class="p">)</span> <span class="n">metadata</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span> <span class="n">date</span><span class="o">=</span><span class="n">datetime</span><span class="o">.</span><span class="n">combine</span><span class="p">(</span> <span class="n">dt</span><span class="o">.</span><span class="n">date</span><span class="p">(),</span> <span class="n">time</span><span class="o">.</span><span class="n">min</span><span class="p">),</span> <span class="n">measure</span><span class="o">=</span><span class="n">measure</span><span class="p">)</span> <span class="nb">id</span><span class="o">=</span><span class="s">&#39;</span><span class="si">%s</span><span class="s">/</span><span class="si">%s</span><span class="s">&#39;</span> <span class="o">%</span> <span class="p">(</span><span class="n">sdate</span><span class="p">,</span> <span class="n">measure</span><span class="p">)</span> <span class="n">coll</span><span class="o">.</span><span class="n">update</span><span class="p">(</span> <span class="p">{</span> <span class="s">&#39;_id&#39;</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span> <span class="s">&#39;metadata&#39;</span><span class="p">:</span> <span class="n">metadata</span> <span class="p">},</span> <span class="p">{</span> <span class="s">&#39;$inc&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;daily&#39;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">&#39;hourly.</span><span class="si">%.2d</span><span class="s">&#39;</span> <span class="o">%</span> <span class="n">dt</span><span class="o">.</span><span class="n">hour</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="p">(</span><span class="s">&#39;minute.</span><span class="si">%.2d</span><span class="s">.</span><span class="si">%.2d</span><span class="s">&#39;</span> <span class="o">%</span> <span class="p">(</span><span class="n">dt</span><span class="o">.</span><span class="n">hour</span><span class="p">,</span> <span class="n">dt</span><span class="o">.</span><span class="n">minute</span><span class="p">)):</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">},</span> <span class="n">upsert</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">def</span> <span class="nf">preallocate</span><span class="p">(</span><span class="n">coll</span><span class="p">,</span> <span class="n">dt</span><span class="p">,</span> <span class="n">measure</span><span class="p">):</span> <span class="sd">&#39;&#39;&#39;Once again, simplified for explanatory purposes&#39;&#39;&#39;</span> <span class="n">metadata</span><span class="p">,</span> <span class="nb">id</span> <span class="o">=</span> <span class="c"># compute metadata and ID</span> <span class="n">coll</span><span class="o">.</span><span class="n">update</span><span class="p">(</span> <span class="p">{</span> <span class="s">&#39;_id&#39;</span><span class="p">:</span> <span class="nb">id</span> <span class="p">},</span> <span class="p">{</span> <span class="s">&#39;$set&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;metadata&#39;</span><span class="p">:</span> <span class="n">metadata</span> <span class="p">},</span> <span class="s">&#39;$inc&#39;</span><span class="p">:</span> <span class="p">{</span> <span class="s">&#39;daily&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#39;hourly.00&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="c"># ...</span> <span class="s">&#39;hourly.23&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#39;minute.00.00&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="c"># ...</span> <span class="s">&#39;minute.00.59&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#39;minute.01.00&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="c"># ...</span> <span class="s">&#39;minute.23.59&#39;</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}</span> <span class="p">},</span> <span class="n">upsert</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> </pre></div> <p>Once we've added the hierarchy and re-run our experiment, we get the nice, level performance we'd like to see:</p> <p><img alt="Performance with Hierarchical Minutes" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrE68TT3rtiO9guikM0w1Toc9phHubpv8SMK70hPwihG_feueNj4flCNuPlxWYxaU8Z5sx8EzbNh3NAjCwTthhfbLusdOKOXpNZQDLhUsGrng2OFwnf6UrZ-o0L96-93BJPmQ/s320/Hier.png" title="With Hierarchical Minutes" /></p> <h2>Conclusion</h2> <p>It's always nice to see "tips and tricks" borne out through actual, quantitative results, so this was probably the most enjoyable talk I've ever put together. The things I got out of it were the following:</p> <ul> <li>Growing documents is a <strong>very bad thing</strong> for performance. Avoid it if at all possible.</li> <li>Awareness of the BSON specification and data representation can actually be quite useful when diagnosing performance problems.</li> <li>To get the best performance out of your system, you need to actually run it (or a highly representative stand-in). Actually seeing the results of performance tweaking in graphical form is incredibly helpful in targeting your efforts.</li> </ul> <p>The source code for all these updates is available in my <a href="https://github.com/rick446/mongodb-sdas">mongodb-sdas Github repo</a>, and I welcome any feedback either there, or here in the comments. In particular, I'd love to hear of any performance problems you've run into and how you got around them. And of course, if you've got a really perplexing problem, I'm always available for consulting by emailing me at <a href="mailto:info@arborian.com">Arborian.com</a>.</p><div class="blogger-post-footer"><!-- // MAILCHIMP SUBSCRIBE CODE \\ --> <a href="http://eepurl.com/ifqEc">Subscribe via email</a> <!-- \\ MAILCHIMP SUBSCRIBE CODE // --></div>Rick Copelandhttp://www.blogger.com/profile/11612114223288841087noreply@blogger.com23tag:blogger.com,1999:blog-18508356.post-67130621740146980992012-09-13T16:27:00.001-04:002012-09-13T16:27:40.962-04:00Python and MongoDB Travel Plans<p>It's been a little longer than normal between articles, so I wanted to let everyone know what I've been up to lately, and hope that I'll be able to see some of you at some upcoming events. So here's a summary of the things I'll be involved with over the next couple of months:</p> <h2>MongoDB Seattle</h2> <p>I have the privilege of speaking at <a href="http://www.10gen.com/events/mongodb-seattle">MongoDB Seattle</a> tomorrow, September 14, 2012. My talk is on "Schema Design at Scale", and wll have some insights based on experience 10gen gained when developing the MongoDB Monitoring Service. I'll even have some cool graphs based on experiments I did to illustrate the schema design tips I'll be giving. </p> <h2>MongoDB for Developers Online Training</h2> <p>I'm offering an 4-week <a href="http://www.arborian.com/mongodb-developers-online-training">online training class</a> starting this Monday at 8pm EDT. I'll be delivering the training in 2 parts each week: a "lecture" portion on Mondays and an unstructured "office hours" on Tuesdays where I'll be available for questions. If you're interested, you can sign up by clicking the following link: <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=UUGMVF39P8CM6">Register for MongoDB for Developers</a>. Right now the class is quite small, so I'll be able to spend a lot of time answering questions and interacting with students. </p> <h2>Python Training in Beijing</h2> <p>I'll be in Beijing, China to deliver an onsite "Fast Track to Python" class from October 5-10. The actual training is only on the 8-10, so if anyone's interested in getting together before that, I'd love to hang out in the People's Republic.</p> <h2>Big Dive and MongoTorino</h2> <p>I'm also happy to announce that I'm one of the featured instructors for the <a href="http://www.bigdive.eu/">Big Dive</a> training event in Turin, Italy this October. I'll be teaching a 2-day class on MongoDB and a 1-day class on Gevent, so if you're in northern Italy, I'd love to see you there! I'll also be at <a href="www.mongotorino.org/">MongoTorino</a> on October 19th, but the webpage for that doesn't seem to be updated yet. I'll be in Turin from October 13-20 if you want to come to either event, or just hang out over a bottle of wine.</p> <h2>PyCarolinas</h2> <p>To cap off my world tour I'm happy to be able to speak on Gevent and Socket.io at <a href="http://blog.pycarolinas.org/">PyCarolinas</a> on October 21st, so I'd love to see any of you there. Sadly I'll have to miss the 1st day of PyCarolinas on my way back from Italy.</p> <p>And that's about it (and that's enough!). I'd love to see any of you who are in any of the cities mentioned above when I'm passing through, so feel free to contact me on twitter (@rick446), email (rick at arborian.com), or in the comments below! </p><div class="blogger-post-footer"><!-- // MAILCHIMP SUBSCRIBE CODE \\ --> <a href="http://eepurl.com/ifqEc">Subscribe via email</a> <!-- \\ MAILCHIMP SUBSCRIBE CODE // --></div>Rick Copelandhttp://www.blogger.com/profile/11612114223288841087noreply@blogger.com0tag:blogger.com,1999:blog-18508356.post-71399633774211787782012-08-28T13:53:00.000-04:002012-09-04T12:13:09.615-04:00Using ZeroMQ devices to support complex network topologies<p>Continuing in my <a href="http://www.zeromq.org">ZeroMQ</a> series, today I'd like to look at ZeroMQ "devices" and how you can integrate ZeroMQ with <a href="http://gevent.org">Gevent</a> so you can combine the easy networking topologies of ZeroMQ with the cooperative multitasking of ZeroMQ. If you're just getting started with ZeroMQ, you might want to check out the following articles:</p> <ul> <li><a href="http://blog.pythonisito.com/2012/08/distributed-systems-with-zeromq.html">Introduction to ZeroMQ</a></li> <li><a href="http://blog.pythonisito.com/2012/08/zeromq-flow-control-and-other-options.html">ZeroMQ flow control and other options</a></li> </ul> <p>And if you want some background on Gevent, you might want to check out <em>that</em> series at the following links:</p> <ul> <li><a href="http://blog.pythonisito.com/2012/07/introduction-to-gevent.html">Introduction to Gevent</a></li> <li><a href="http://blog.pythonisito.com/2012/07/gevent-threads-and-benchmarks.html">Gevent, Threads, and Benchmarks</a></li> <li><a href="http://blog.pythonisito.com/2012/07/gevent-and-greenlets.html">Gevent and Greenlets</a></li> <li><a href="http://blog.pythonisito.com/2012/08/gevent-monkey-patch.html">Greening the Python Standard Library with Gevent</a></li> <li><a href="http://blog.pythonisito.com/2012/08/building-tcp-servers-with-gevent.html">Building TCP Servers with Gevent</a></li> <li><a href="http://blog.pythonisito.com/2012/08/building-web-applications-with-gevents.html">Building Web Applications with Gevent's WSGI Server</a></li> </ul> <p>Once you're caught up, let's get started... <a name='more'></a></p> <h2>ZeroMQ "devices"</h2> <p>One of the nice aspects of ZeroMQ is that it decouples the communication pattern with the connection pattern of your endpoints. Without ZeroMQ, you'd commonly have a "server" socket which binds to a port and receives requests, while "clients" connect to that socket and send requests. <em>With</em> ZeroMQ, it's perfectly acceptable to have the "server" connect to the "client" to receive requests (and in fact you can have multiple "servers" connected to the same "client" socket).</p> <p>While this is handy, in some cases you may want to have both client and server relatively dynamic, with <em>both</em> connecting and <em>neither</em> binding to a particular port. For this use case, ZeroMQ provides so-called "devices" that bind a couple of sockets and perform forwarding operations between them to support common communication patterns. The "client" and "server" then both connect to the device. In this section, I'll cover the devices provided by the ZeroMQ library.</p> <h3>Queue device</h3> <p>The Queue device is responsible for mediating the <code>REQ/REP</code> communication pattern. Suppose we have the following request code:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">REQ</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">print</span> <span class="s">&#39;REQ is&#39;</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="k">print</span> <span class="s">&#39;REP is&#39;</span><span class="p">,</span> <span class="n">sock</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> </pre></div> <p>and the following response code:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">REP</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">x</span> <span class="o">=</span> <span class="n">sock</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> <span class="k">print</span> <span class="s">&#39;REQ is&#39;</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">reply</span> <span class="o">=</span> <span class="s">&#39;x-</span><span class="si">%s</span><span class="s">&#39;</span> <span class="o">%</span> <span class="n">x</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">reply</span><span class="p">)</span> <span class="k">print</span> <span class="s">&#39;REP is&#39;</span><span class="p">,</span> <span class="n">reply</span> </pre></div> <p>To set up a broker that forwards between the two, you need a tiny script:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="n">s1</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">ROUTER</span><span class="p">)</span> <span class="n">s2</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">DEALER</span><span class="p">)</span> <span class="n">s1</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">s2</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="n">zmq</span><span class="o">.</span><span class="n">device</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">QUEUE</span><span class="p">,</span> <span class="n">s1</span><span class="p">,</span> <span class="n">s2</span><span class="p">)</span> </pre></div> <p>Note that we've used a couple of new socket types above: <code>zmq.ROUTER</code> and <code>zmq.DEALER</code>. These are similar to <code>zmq.REP</code> and <code>zmq.REQ</code>, respectively, but they allow us to break the strict "request-response" lockstep of <code>REQ/REP</code>. The way they work is the following:</p> <ol> <li>The <code>ROUTER</code> socket receives a message on a particular connection. It adds a message ID to the beginning of the message that identifies the sending <code>REQ</code> socket. </li> <li>The <code>FORWARDER</code> device sends the message received on the <code>ROUTER</code> to the <code>DEALER</code> socket.</li> <li>The <code>DEALER</code> picks a connection to send the message to, stripping the prefix but noting the message ID and linking it to the <code>REP</code> socket handling the message.</li> <li>The <code>DEALER</code> receives the response message, pairs it up with the message ID it's responding to, and adds the message ID to the beginning of the mssage.</li> <li>The <code>FORWARDER</code> device sends the message received on the <code>DEALER</code> to the <code>ROUTER</code> socket.</li> <li>The <code>ROUTER</code> socket strips the message ID and sends the message to the <code>REQ</code> socket that sent the initial request.</li> </ol> <p>By using message IDs as above, we can have multiple messages 'in flight', being handled by various servers. If you'd rather not use the built-in ZeroMQ device, you can build your own fairly simply. The code below shows a device that that uses a pair of threads to relay messages from one socket to another:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="kn">import</span> <span class="nn">time</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="n">s1</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">ROUTER</span><span class="p">)</span> <span class="n">s2</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">DEALER</span><span class="p">)</span> <span class="n">s1</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">s2</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="k">def</span> <span class="nf">zeromq_relay</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span> <span class="sd">&#39;&#39;&#39;Copy data from zeromq socket a to zeromq socket b&#39;&#39;&#39;</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> <span class="n">more</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="n">getsockopt</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">RCVMORE</span><span class="p">)</span> <span class="k">if</span> <span class="n">more</span><span class="p">:</span> <span class="n">b</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">zmq</span><span class="o">.</span><span class="n">SNDMORE</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="n">b</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="k">def</span> <span class="nf">zmq_queue_device</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="n">s2</span><span class="p">):</span> <span class="kn">import</span> <span class="nn">threading</span> <span class="n">t1</span> <span class="o">=</span> <span class="n">threading</span><span class="o">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">zeromq_relay</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span><span class="n">s2</span><span class="p">))</span> <span class="n">t2</span> <span class="o">=</span> <span class="n">threading</span><span class="o">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">zeromq_relay</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">s2</span><span class="p">,</span><span class="n">s1</span><span class="p">))</span> <span class="n">t1</span><span class="o">.</span><span class="n">daemon</span> <span class="o">=</span> <span class="n">t2</span><span class="o">.</span><span class="n">daemon</span> <span class="o">=</span> <span class="bp">True</span> <span class="n">t1</span><span class="o">.</span><span class="n">start</span><span class="p">()</span> <span class="n">t2</span><span class="o">.</span><span class="n">start</span><span class="p">()</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="n">zmq_queue_device</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="n">s2</span><span class="p">)</span> </pre></div> <hr> <p><strong>WARNING</strong> Though the code above seems to work ok, but as <a href="http://www.blogger.com/profile/16027583555410283856">Dan Fairs</a> points out, ZeroMQ sockets are <em>not</em> thread-safe. So what you really want to do is use nonblocking IO and <code>zmq.Poller()</code>, but that's a bit beyond the scope of what I wanted to cover in this article.</p> <hr> <h3>Forwarding device</h3> <p>In the same way the <code>QUEUE</code> device mediates the <code>REQ/REP</code> pattern, the <code>FORWARDER</code> device mediates the <code>PUB/SUB</code> pattern. Since <code>PUB/SUB</code> doesn't require the lockstep operation of <code>REQ/REP</code>, we can use the regular <code>PUB/SUB</code> sockets in our device:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="n">s1</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">SUB</span><span class="p">)</span> <span class="n">s2</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUB</span><span class="p">)</span> <span class="n">s1</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">s2</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="n">s1</span><span class="o">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">SUBSCRIBE</span><span class="p">,</span> <span class="s">&#39;&#39;</span><span class="p">)</span> <span class="n">zmq</span><span class="o">.</span><span class="n">device</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">FORWARDER</span><span class="p">,</span> <span class="n">s1</span><span class="p">,</span> <span class="n">s2</span><span class="p">)</span> </pre></div> <p>Now we can connect one or more publishers to our device's "upstream" port (<code>sys.argv[1]</code>), and one or more subscribers to the device's "downstream" port (<code>sys.argv[2]</code>) to provide a <code>PUB/SUB</code> broker. Note in particular that we had to subscribe to all mesages in the device code since we don't know which messages our downstream sockets are interested in. </p> <p>If you'd rather filter messages that get fowarded, you can subscribe to some subset of messages. Unfortunately, I'm not aware of any way to forward only messages that the downstream clients have subscribed to using built-in ZeroMQ functionality.</p> <p>If we want to write our own <code>FORWARDER</code> device, it's even simpler than the <code>QUEUE</code> device since it only handles unidirectional communication. Assuming we have the <code>zeromq_relay</code> function as defined above, our <code>FORWARDER</code> device is just the following:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">zmq_forwarder_device</span><span class="p">(</span><span class="n">upstream</span><span class="p">,</span> <span class="n">downstream</span><span class="p">):</span> <span class="n">zeromq_relay</span><span class="p">(</span><span class="n">upstream</span><span class="p">,</span> <span class="n">downstream</span><span class="p">)</span> </pre></div> <h3>Streaming device</h3> <p>Similar to the <code>FORWARDER</code> device, the <code>STREAMER</code> device just sends upstream packets downstream, but in this case in support of the <code>PUSH/PULL</code> pattern rather than <code>PUB/SUB</code>. To make a <code>STREAMER</code> broker, then, we just need the following code:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="n">s1</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PULL</span><span class="p">)</span> <span class="n">s2</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">s1</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">s2</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="n">zmq</span><span class="o">.</span><span class="n">device</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">STREAMER</span><span class="p">,</span> <span class="n">s1</span><span class="p">,</span> <span class="n">s2</span><span class="p">)</span> </pre></div> <p>And once again, if we wanted to create the device manually, it's just a relay:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">zmq_streaming_device</span><span class="p">(</span><span class="n">upstream</span><span class="p">,</span> <span class="n">downstream</span><span class="p">):</span> <span class="n">zeromq_relay</span><span class="p">(</span><span class="n">upstream</span><span class="p">,</span> <span class="n">downstream</span><span class="p">)</span> </pre></div> <h2>Integration with gevent</h2> <p>You may have noticed that the gevent device function doesn't return. If you want to create multiple devices within your Python program, then, you'll need to wrap the devices in threads. </p> <p>Another approach that you might use if you, like me, prefer the lightweight threading and asynchronous I/O of gevent, is to use the <code>gevent-zeromq</code> package available from PyPI:</p> <div class="codehilite"><pre><span class="nv">$ </span>pip install gevent-zeromq </pre></div> <p>Now we can use a 'green' version of ZeroMQ by just importing from the <code>gevent-zeromq</code> wrapper in our scripts. A "pusher" script would then look like this:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">from</span> <span class="nn">gevent_zeromq</span> <span class="kn">import</span> <span class="n">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="o">.</span><span class="n">instance</span><span class="p">()</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">&#39;:&#39;</span> <span class="o">+</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">())</span> </pre></div> <p>Simple, right? The reason I throw this in this seemingly-unrelated post is that if you want to use gevent, you <strong>can't</strong> use the built-in devices. This is because the built-in devices block, and they block in the ZeroMQ C library, <em>not</em> in Python where they could be "greened". So if you want a device with gevent, you'll have to write your own (which as, after all, pretty straightforward).</p> <h2>Conclusion</h2> <p>ZeroMQ devices provide a handy way to stitch together complex routing topologies and allow you to decouple the various components of your architecture. Though the built-in devices are quite simple, they provide insight into how you could build more complex devices yourself to fulfill the role of "broker" in a distributed architecture. </p> <p>I'd be interested in hearing from you how you are using devices (or whether you find them useful at all) in ZeroMQ programming. Do you use the built-in devices? Any devices you write yourself? Do you prefer the multithreaded device approach I've used here, or using the <code>zmq.Poller</code> object? Let me know in the comments below!</p><div class="blogger-post-footer"><!-- // MAILCHIMP SUBSCRIBE CODE \\ --> <a href="http://eepurl.com/ifqEc">Subscribe via email</a> <!-- \\ MAILCHIMP SUBSCRIBE CODE // --></div>Rick Copelandhttp://www.blogger.com/profile/11612114223288841087noreply@blogger.com6tag:blogger.com,1999:blog-18508356.post-63260686342882518802012-08-21T09:00:00.000-04:002012-08-21T09:00:02.808-04:00ZeroMQ flow control and other options<p>In a previous post, I provided an <a href="http://blog.pythonisito.com/2012/08/distributed-systems-with-zeromq.html">introduction to ZeroMQ</a>. Continuing along with <a href="http://www.zeromq.org">ZeroMQ</a>, today I'd like to take a look at how you manage various "socket options" in ZeroMQ, particularly when it comes to flow control. If you've never used ZeroMQ, I recommend reading my previous post first. Once you're caught up, let's get started... <a name='more'></a></p> <h2>ZeroMQ "fire and forget"</h2> <p>One of the awesome things you may have noticed about ZeroMQ is that you can send a message without waiting for it to be received. In fact, the endpoint that's going to receive the message doesn't even need to be connected, and your application will happily act as if the message is winging its way along. While this is <em>great</em> for easily getting up to speed with ZeroMQ, there are some things you need to be aware of.</p> <h3>ZeroMQ is not magic; merely a close approximation</h3> <p>When you send a message in on a ZeroMQ socket, internally ZeroMQ is storing that message in an in-memory queue. So long as you don't send messages faster than whatever's downstream can read them, all is well. The problem comes when your downstream end can't process them fast enough. </p> <p>Let's consider the following sender, which will send a short message as quickly as possible:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">&#39;:&#39;</span> <span class="o">+</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">())</span> </pre></div> <p>Now if we just run this without having a corresponding 'puller' socket draining the queue, we'll eat up memory as the 'pusher' just keeps enqueueing messages. On my laptop, for instance, the python process reached 3GB of virtual memory size in two minutes. Of course, in a real system you'll have a pulling socket, but in many cases your 'pulling' socket may not be able to pull has fast as your pusher can push. </p> <h3>High water mark to the rescue</h3> <p>To handle situations like this, ZeroMQ provides a socket option called a high water mark, accessed as <code>zmq.HWM</code>. This tells us how many messages we want ZeroMQ to buffer in RAM before blocking the 'pushing' socket. To set the high water mark, we just need to use the <code>.setsockopt</code> method:</p> <div class="codehilite"><pre><span class="o">...</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">HWM</span><span class="p">,</span> <span class="mi">1000</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">&#39;:&#39;</span> <span class="o">+</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">())</span> </pre></div> <p>The modified pusher will send 1000 messages and then block, using a maximum of 2.3MB on my laptop. Note that the high water mark must be set <em>before</em> you connect to any clients, as ZeroMQ uses a queue-per-client, and fixes the queue size on connect.</p> <h3>Queueing messages on disk</h3> <p>There is one case in which a sending socket may exceed its high water mark. When you set the <code>zmq.SWAP</code> option on a socket, ZeroMQ will use a local swapfile to store messages that exceed the high water mark. In order to set up an 200KB on-disk swap file, for instance, we could use the following code:</p> <div class="codehilite"><pre><span class="o">...</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">HWM</span><span class="p">,</span> <span class="mi">1000</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">SWAP</span><span class="p">,</span> <span class="mi">200</span><span class="o">*</span><span class="mi">2</span><span class="o">**</span><span class="mi">10</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">&#39;:&#39;</span> <span class="o">+</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">())</span> </pre></div> <h2>Lingering messages</h2> <p>ZeroMQ is designed to deliver messages as reliably as possible by default. One way it does this is by allowing outgoing messages to 'linger' in their queues even when the socket that sent them has been closed. For instance, suppose we have the following single-message pusher:</p> <div class="codehilite"><pre><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">zmq</span> <span class="n">context</span> <span class="o">=</span> <span class="n">zmq</span><span class="o">.</span><span class="n">Context</span><span class="p">()</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">&#39;:&#39;</span> <span class="o">+</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">())</span> <span class="k">print</span> <span class="s">&#39;Exiting...&#39;</span> </pre></div> <p>Now if we run this without a corresponding "pull" socket, our program will simply sit there saying it's exiting, but never really exiting. This is because by default, the ZeroMQ communication thread will hang around until all its outgoing messages have been sent <em>even if the socket is closed</em>. To modify this behavior, we can set the zmq.LINGER on the socket, setting a maximum amount of time in milliseconds that the thread will try to send messages after its socket has been closed (the default value of -1 means to linger forever):</p> <div class="codehilite"><pre><span class="o">...</span> <span class="n">sock</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">socket</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">PUSH</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">zmq</span><span class="o">.</span><span class="n">LINGER</span><span class="p">,</span> <span class="mi">1000</span><span class="p">)</span> <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">sock</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="s">&#39;:&#39;</span> <span class="o">+</span> <span class="n">time</span><span class="o">.</span><span class="n">ctime</span><span class="p">())</span> <span class="k">print</span> <span class="s">&#39;Exiting...&#39;</span> </pre></div> <h2>Other options</h2> <p>In the previous article, we already saw the <code>zmq.SUBSCRIBE</code> and <code>zmq.UNSUBSCRIBE</code> options. There are also a number of other socket options available for use with <code>setsockopt</code>. Several of these (<code>zmq.RATE</code>, <code>zmq.RECOVERY_IVL</code>,<code>zmq.RECOVERY_IVL_MSEC</code>, <code>zmq.MCAST_LOOP</code>, <code>zmq.RECONNECT_IVL</code>, and <code>zmq.RECONNECT_IVL_MAX</code>) have to do with multicast sockets (<code>zmq.PGM</code> and <code>zmq.EPGM</code>), so I won't go into them here. Others (<code>zmq.SNDBUF</code>, <code>zmq.RCVBUF</code>, and <code>zmq.BACKLOG</code>) have to do with the underlying OS sockets.</p> <p>There is one option that's potentially a bit more interesting: <code>zmq.IDENTITY</code>. From the ZeroMQ docs on <a href="http://api.zeromq.org/2-1:zmq-setsockopt">setsockopt</a>:</p> <blockquote> <p>If the socket has no identity, each run of an application is completely separate from other runs. However, with identity set the socket shall re-use any existing ØMQ infrastructure configured by the previous run(s). Thus the application may receive messages that were sent in the meantime, message queue limits shall be shared with previous run(s) and so on. </p> </blockquote> <p>So if you create a socket and set its identity, it will pick up all the other settings you've set on it before. Beware of overusing this setting, however, since it's <a href="http://comments.gmane.org/gmane.network.zeromq.devel/10207">may be removed soon</a>.</p> <h2>Conclusion</h2> <p>In the <code>setsockopt</code> method, ZeroMQ provides a way to control ZeroMQ sockets at a lower level than the "fire-and-forget" model at the higher level. Particularly if you're building a "pipeline" style application, you need to be aware of its flow control features. (I have particular experience with errors caused by not understanding the <code>zmq.HWM</code> option, for example.) </p> <p>There is, of course, still more to cover here. In particular, I'll cover the use of devices to construct more elaborate network topologies and the use of ZeroMQ with <a href="http://gevent.org">gevent</a> in future articles. I'd also love to hear about other topics you'd like to read about, so let me know in the comments below!</p><div class="blogger-post-footer"><!-- // MAILCHIMP SUBSCRIBE CODE \\ --> <a href="http://eepurl.com/ifqEc">Subscribe via email</a> <!-- \\ MAILCHIMP SUBSCRIBE CODE // --></div>Rick Copelandhttp://www.blogger.com/profile/11612114223288841087noreply@blogger.com4