<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Posts on DwarfHack</title>
		<link>https://dwarfhack.com/posts/</link>
		<description>Recent content in Posts on DwarfHack</description>
		<generator>Hugo -- gohugo.io</generator>
		<language>en-us</language>
		<copyright>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</copyright>
		<lastBuildDate>Thu, 23 Jul 2020 12:09:46 +0200</lastBuildDate>
		<atom:link href="https://dwarfhack.com/posts/index.xml" rel="self" type="application/rss+xml" />
		
		<item>
			<title>[GN] Part 2: Receiving</title>
			<link>https://dwarfhack.com/posts/tech/net/gn_2_speed_2_receive/</link>
			<pubDate>Thu, 23 Jul 2020 12:09:46 +0200</pubDate>
			
			<guid>https://dwarfhack.com/posts/tech/net/gn_2_speed_2_receive/</guid>
			<description>Now that we know how fast we can send packets, it is time to find out how fast we can receive them on the other side of the network. As in the last post, I will start on sending everything locally so that I can test it easily, we will then combine both applications and let them send to each other on different hosts.
Sadly not my infrastructure Photo by Thomas Jensen on Unsplash</description>
			<content type="html"><![CDATA[<p>Now that we <a href="https://dwarfhack.com/posts/tech/net/gn_2_speed_1_send/">know how fast we can send</a> packets, it is time to find out how fast we can receive them on the other side of the network.
As in the last post, I will start on sending everything locally so that I can test it easily, we will then combine both applications and let them send to each other on different hosts.</p>
<figure><img src="/images/tech/net/switch.jpg"
         alt="Sadly not my infrastructure"/><figcaption>
            <p>Sadly not my infrastructure
                    <a href="https://unsplash.com/@thomasjsn">Photo by Thomas Jensen on Unsplash</a></p>
        </figcaption>
</figure>

<h2 id="starting-off-easy">Starting off easy</h2>
<p>Let&rsquo;s start single-threaded as we did in the last post: one thread that receives from one socket, sums one received bit (so the compiler won&rsquo;t simply optimize away the whole program) and discard it.</p>
<p>This works surprisingly well: a single thread can receive and discard about 1.8 million packets per second from localhost on my machine.
However, we need to distribute receiving onto multiple threads if we want to be able to efficiently distribute parsing and crypto on multiple threads as well.
I further suppose that when using actual hardware, it will come in handy do have several threads splitting their time between waiting for the device and calculating stuff on the CPU.</p>
<p>One way to have multiple threads share the load, would be to bind each of them to a different socket address. This might work quite well but it complicates clientside logic, since the clients need to re-connect to different sockets when we change the amount of threads.</p>
<p>Fortunately, the <code>SO_REUSERPORT</code> from the last article also works for receiving messages. Before we go over how to make use of it, let me first show you what to avoid: Contesting on a single socket as I did in the <a href="/posts/tech/tokio_pps/">tokio post</a>.</p>
<h2 id="what-did-i-expect">What did I expect?</h2>
<p>In retrospect, I seriously wonder, what I was thinking. Tokio distributes work across multiple tasks that might be executed in parallel or at least concurrently on a thread pool. This is great for connection-oriented protocols like TCP, when you have 1000 connenctions, they can easily be mapped to 8 threads that way, only costing resources when there is work available.</p>
<p>But this is not necessarily a good idea for UDP. The UDP protocol was designed for connection-less communication. If we want to implement connection-semantics on top of it, one has to do so in their own code, *<em>after</em> receiving messages. It therefore is utter nonsense to receive from one socket and distribute the receiving itself to a thread pool an exception would be if we cannot offload processing the received message to another thread for whatever reason.</p>
<p>After thinking about the approach in the tokio article, I am still surprised that it did in fact work so well.</p>
<h2 id="reusing-the-port">Reusing the port</h2>
<p>Now, let&rsquo;s make use of <code>SO_REUSERPORT</code>. First the same note as in the <a href="/posts/tech/net/gn_2_speed_1_send/">send article</a>: this won&rsquo;t allow our code to run on windows or linux systems with ancient kernels.
So what does <code>SO_REUSERPORT</code> actually mean for receiving Datagrams?</p>
<p>Let me quickly cite the docs here:</p>
<blockquote>
<p>For UDP sockets, the use of this option can provide better
distribution of incoming datagrams to multiple processes (or
threads) as compared to the traditional technique of having
multiple processes compete to receive datagrams on the same
socket.</p>
</blockquote>
<p>First thing to note: several threads are not competing for the incoming data and therefore are not churning on some lock or something.
But how is this achieved?
According to <a href="https://lwn.net/Articles/542629/">first result on google</a> stating:</p>
<blockquote>
<p>Incoming connections and datagrams are distributed to the server sockets using a hash based on the 4-tuple of the connection</p>
</blockquote>
<p>Originally, I planned to dive into the <code>net/ipv4/udp.c</code> sources of the kernel here but I got scared off pretty quickly:
I completely forgot that <code>goto</code> still exists.
Perhaps one day I will work my way through the netcode. For now let&rsquo;s just assume that the hashing works as one would expect.</p>
<p>For now, we need to</p>
<p>Using <code>SO_REUSERPORT</code>, have several threads that receive from their own share of connected hosts.
This means that if one thread has no client assigned that is sending anything it would poll its socket and block until there is something new.
However, polling might be slower than being notified. I therefore chose to use epoll for this cause: our code is woken up, whenever there is something interesting happening.</p>
<p>How to use epoll in Rust? One could use the syscall directly but that sounds quite taunting. Luckily there is <code>mio</code>, a crate that abstract all the nasty low-level things away and lets us directly register interests, for which, if fulfilled, we want to be notified.</p>
<h2 id="mio">Mio</h2>
<p>Mio uses their own <code>mio::net::UdpSocket</code>, wich does not support setting the reuseport flag upon creation. Luckily mio can reuse std sockets and wrap them into their own constructs. So we can use a <code>socket2</code> socket, set the reuseport flag, convert it to an <code>std::net::UdpSocket</code> and convert that one to a <code>mio::net::UdpSocket</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="n">s2_socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Socket</span>::<span class="n">new</span><span class="p">(</span><span class="o">..</span><span class="p">.);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">s2_socket</span><span class="p">.</span><span class="n">set_reuse_port</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">s2_socket</span><span class="p">.</span><span class="n">bind</span><span class="p">(</span><span class="o">&amp;</span><span class="n">addr</span><span class="p">.</span><span class="n">into</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">std_socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s2_socket</span><span class="p">.</span><span class="n">into_udp_socket</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">mio_socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">UdpSocket</span>::<span class="n">from_std</span><span class="p">(</span><span class="n">std_socket</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>Using Mio, we have a problem with measuring our performance: Using the std sockets, I simply could let them receive a million packets, return from the call and measure the execution duration and resume receiving. In mio however, things work quite a bit differently: We first register an interst with a <code>Poll</code> struct. That way, mio knows if we want to be notified when the socket is readable, writeable or both. We then poll on the <code>Poll</code> struct (not the socket) and get a list of events <code>poll.poll(&amp;mut events,...)</code>. We then have to process the events and hopefully have our token in it, stating that the socket is readable. Now the tricky part begins. Now we have to receive from the socket until it return the following Errot type: <code>io::ErrorKind::WouldBlock</code>. If we stop receiving before that, the socket might not fire the readable event again and we might never be able to read from it again.</p>
<p>One way to work around this is to re-register the interest. However, since I only want that code to measure things and not to win any beauty competitions, the receive loop of each thread measures the receive rates individually. This has another side-benefit: Having the rates of all threads, we can observe how incoming datagrams are distributed across the receiving threads.</p>
<p>The following table shows how well (or rather how bad) the clients (12) are distributed across the receivers (6).</p>
<pre tabindex="0"><code>thread      average pps
0           583666.7
1           186166.7
2           583972.2
3           384625.0
4           186250.0
5           384708.3
</code></pre><p>In a real-world scenario, we would most likely have more than a hundred clients, so the differences will be evened-out. Nonetheless, this observation is important: If we planned to deploy some traffic-aggregators in front of the actual game server to decrease the packet rate on the GS by combining multiple clients in one packet, we would observe the same inequality.</p>
<p>Summing up the averages in R reveals a combined packet-rate of 2.3 Mpps when sending with 2.4 Mpps at the receiver, so there is some packet loss but it is not yet the majority:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-R" data-lang="R"><span class="line"><span class="cl"><span class="n">x</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;whatever.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">a</span> <span class="o">&lt;-</span> <span class="nf">aggregate</span><span class="p">(</span><span class="n">x</span><span class="p">,</span><span class="nf">list</span><span class="p">(</span><span class="n">x</span><span class="o">$</span><span class="n">thread</span><span class="p">),</span><span class="n">mean</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;</span> <span class="nf">sum</span><span class="p">(</span><span class="n">a1</span><span class="o">$</span><span class="n">pps</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">[1]</span> <span class="m">2309389</span>
</span></span></code></pre></div><p>When sending without limits in the sender, the receiver can reach about 2.9 Mpps. However, the receivers with less associated senders will not live up to their full potential. A &lsquo;saturated&rsquo; receiver is  receiving about 1 Mpps on my machine.</p>
<h2 id="real-hardware">Real Hardware</h2>
<p>Now that we know what the software is capable of if running on its own, it is time to introduce a bit of problems: we need to send the traffic over real networks.
Let me start with a bunch of hardware I have sitting at home, collecting dust.</p>
<h3 id="my-own-hardware">My own hardware</h3>
<p>We will repeat the same test as in the tokio article, and I expect to see the whole thing capped again at 500Kpps.
In a second step, I will directly connect the two machines without any network hardware in between.</p>
<p>It turns out the assumption was correct, the packet rate was again capped at about 500 Kpps on the sending side. This is important, since we could probably increase the receive rate by sending from multiple devices. However, since I wanted to test without a switch in between, I directly connected the hosts with some CAT7 cable and tried again, observing the same packet rate.</p>
<p>Time to level up the hardware a bit, let&rsquo;s make use of that fancy clouds.</p>
<h3 id="someone-elses-hardware">Someone else&rsquo;s hardware</h3>
<p>To dampen your expectations in advance, this one was disappointing:
At Digitalocean I ordered the fattest machines they have for me, 32 CPUs, loads of RAM and let them connect it to some VPC.
They seem to be interconnected by a 1Gbps link according to iperf.
Therefore I suspected we can observe results comparable to my home network or hopefully faster, these are state-of-the-art servers after all.
Well, turns out virtualization is a problem:
The benchmaks are capped at exactly 101000 pps, having a deviance of only 500 pps. This strongly indicates that the machines are rate-limited.</p>
<h2 id="renting-a-bit-of-metal">Renting a bit of metal</h2>
<p>Luckily we one can rent bare-metal servers based on hourly billing. Since I had a bit of demo-credits left, I ordered two servers at packet.com for an hour to play with. Although being slightly overpowered (48 CPUs and 64Gb RAM), they will hopefully do their job well.</p>
<p>Both servers have two Intel x710, 10 Gbit/s NICs which are bonded in their default configuration.</p>
<p>So how much did that machines achieve? I ran 16 receivers, 32 senders with a packet size of 128 bytes.
According to the previously used calculation method, adding up the average rates we total to <strong>8 Mpps</strong>.
However, this calculation method has its problems: it is only a rough estimate assuming all threads run for at their average speed the whole time.
Since this is not the case, let&rsquo;s have a few more details about the benchmark data.</p>
<p>Thread 12 did perform exceptionally well, showcasing the best-case scenario:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-R" data-lang="R"><span class="line"><span class="cl"><span class="o">&gt;</span> <span class="nf">filter</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">th</span><span class="o">==</span><span class="m">12</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="n">run</span> <span class="n">th</span>     <span class="n">pps</span>
</span></span><span class="line"><span class="cl"><span class="m">1</span>    <span class="m">1</span> <span class="m">12</span> <span class="m">1037000</span>
</span></span><span class="line"><span class="cl"><span class="m">2</span>    <span class="m">2</span> <span class="m">12</span> <span class="m">1046000</span>
</span></span><span class="line"><span class="cl"><span class="m">3</span>    <span class="m">3</span> <span class="m">12</span> <span class="m">1046000</span>
</span></span><span class="line"><span class="cl"><span class="m">4</span>    <span class="m">4</span> <span class="m">12</span> <span class="m">1059000</span>
</span></span><span class="line"><span class="cl"><span class="m">5</span>    <span class="m">5</span> <span class="m">12</span> <span class="m">1054000</span>
</span></span><span class="line"><span class="cl"><span class="m">6</span>    <span class="m">6</span> <span class="m">12</span> <span class="m">1050000</span>
</span></span><span class="line"><span class="cl"><span class="m">7</span>    <span class="m">7</span> <span class="m">12</span> <span class="m">1054000</span>
</span></span><span class="line"><span class="cl"><span class="m">8</span>    <span class="m">8</span> <span class="m">12</span> <span class="m">1059000</span>
</span></span><span class="line"><span class="cl"><span class="m">9</span>    <span class="m">9</span> <span class="m">12</span> <span class="m">1059000</span>
</span></span><span class="line"><span class="cl"><span class="m">10</span>  <span class="m">10</span> <span class="m">12</span> <span class="m">1054000</span>
</span></span><span class="line"><span class="cl"><span class="m">11</span>  <span class="m">11</span> <span class="m">12</span> <span class="m">1059000</span>
</span></span><span class="line"><span class="cl"><span class="m">12</span>  <span class="m">12</span> <span class="m">12</span> <span class="m">1054000</span>
</span></span><span class="line"><span class="cl"><span class="m">13</span>  <span class="m">13</span> <span class="m">12</span> <span class="m">1054000</span>
</span></span></code></pre></div><p>Whereas thread 3 had not enough work to do:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-R" data-lang="R"><span class="line"><span class="cl"><span class="o">&gt;</span> <span class="nf">filter</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">th</span><span class="o">==</span><span class="m">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">run</span> <span class="n">th</span>    <span class="n">pps</span>
</span></span><span class="line"><span class="cl"><span class="m">1</span>   <span class="m">0</span>  <span class="m">3</span> <span class="m">132000</span>
</span></span><span class="line"><span class="cl"><span class="m">2</span>   <span class="m">1</span>  <span class="m">3</span> <span class="m">145000</span>
</span></span></code></pre></div><p>To summarize this catastrophic distribution, I want to show you the best boxplot I ever made:</p>
<figure><img src="/images/tech/net/packet04.svg"
         alt="Packet rate on each thread"/><figcaption>
            <p>Packet rate on each thread</p>
        </figcaption>
</figure>

<p>It made a huge difference between using 16 receiver threads compared to only using 8 threads.
On average we processed 576 Kpps per thread but, we might be able to process one million packets per second per thread, at least we did with thread 12.
If it turns out we need to increase the total throughput we could try to tune the kernel, hardware or something in that line but I doubt that these rates won&rsquo;t be enough for now.</p>
<h2 id="next-steps">Next steps</h2>
<p>The next step is to develop some pseudo-reliable protocol on top of UDP and then find out how fast we can run it compensating packet loss.
However, I first want to ensure that the other components of the networking stack are capable of keeping up with the speed of the socket, in particular deserialization and crypto.</p>
]]></content>
		</item>
		
		<item>
			<title>[GN] Part 1: Sending to localhost</title>
			<link>https://dwarfhack.com/posts/tech/net/gn_2_speed_1_send/</link>
			<pubDate>Mon, 20 Jul 2020 12:09:46 +0200</pubDate>
			
			<guid>https://dwarfhack.com/posts/tech/net/gn_2_speed_1_send/</guid>
			<description>How much network traffic can we produce with Rust? Isn&amp;rsquo;t this a weird thing to ask? It probably is, but with knowing this and moreover, how to achieve it, we can create a network application from the bottom up, that is ensured not to be bottlenecked by the network send throughput. Furthermore, we can more easily benchmark the more interesting aspects like receive-performance, when we know that the sender is not the limiting factor.</description>
			<content type="html"><![CDATA[<p>How much network traffic can we produce with Rust?
Isn&rsquo;t this a weird thing to ask?
It probably is, but with knowing this and moreover, how to achieve it, we can create a network application from the bottom up, that is ensured not to be bottlenecked by the network send throughput. Furthermore, we can more easily benchmark the more interesting aspects like receive-performance, when we know that the sender is not the limiting factor.
Throughout this article, I will present several approaches to gradually increase throughput until my CPU finally was completely busy and the system became unresponsive. In some sorts, this article is the first part of a continuation to the <a href="https://dwarfhack.com/posts/tech/tokio_pps/">Tokio article</a></p>
<h2 id="the-plan">The plan</h2>
<p>As in my last post, I will start by maximizing the amount of packets per second the application can send, then the bandwidth can easily be increased by increasing the packet size.</p>
<p>Let&rsquo;s start with some pseudocode (nearly executable python), to describe what needs to be done:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">buffer</span><span class="o">=</span><span class="p">[</span><span class="o">...</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">counter</span><span class="o">=</span><span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="n">timestamp</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="n">counter</span> <span class="o">&lt;</span> <span class="mi">100_000_000</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">  <span class="n">send</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="n">counter</span><span class="o">+=</span><span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">timediff</span><span class="p">())</span>
</span></span></code></pre></div><p>In this first step, we simply don&rsquo;t care if our packets reach their destination, so we can as well send them to somewhere they will be ignored like <code>127.0.0.1:42</code> (assuming nothing is listening on port 42). Note that this way we are not limited by the capabilities of our network card in this first step but expect to see results that we will most likely not achieve on real hardware.</p>
<h2 id="the-straight-forward-approach-stdsocket">The straight-forward approach: std::socket</h2>
<p>Let&rsquo;s start with the straight-forward approach: just use, what the std lib provides us.
In this firt iteration&rsquo; this simply is a single thread, executing the rust-equivalent to the above pseudocode.
This means we execute such a function, sending a lot of zeroes:</p>
<pre tabindex="0"><code>pub fn send_packets_to(&amp;self, amount: u32, size: u32, to: SocketAddr) {
  let buf = vec![0 as u8; size as usize];
  for i in 0..amount {
    self.socket.send_to(&amp;buf, &amp;to).expect(&#34;Send failed&#34;);
  }
}
</code></pre><p>Note that we send the same buffer over-and-over again and do not re-allocate memory for it (at least not in our code).</p>
<p>So, how well does this work?
Let&rsquo;s mangle the results with a bit of R and see what we got:</p>
<figure><img src="/images/tech/net/send_std.svg"
         alt="Packet rate versus size when sending with the std::socket functions"/><figcaption>
            <p>Packet rate versus size when sending with the std::socket functions</p>
        </figcaption>
</figure>

<p>Let me highlight some observations:</p>
<ul>
<li>The packet rate does not change considerably if we send messages of 2 bytes or 265 bytes.</li>
<li>Up until <code>2^11</code> bytes we have quite stable performance of about 450K packets per second. Note however that this already equals <code>450000 * 2^11 * 8 = 7 Gbit/s</code> which is probably more than you are prepared to pay for in a cloud-environment for prolonged times.</li>
<li>We are sending to localhost here. This means in particular, that we do not involve any network cards or even such mundane things as ethernet MTUs that will limit us if we try to do this in a real scenario later on.</li>
</ul>
<p>I conclude that it is not worth anything network-performance-wise to optimize package sizes below 256 bytes. In terms of traffic costs, this migtht be another story. We will later see how packet size influences parsing or cryprographic operations.</p>
<p>Now we can have a look at the average bandwidth and see if this is matches what the OS tells us while the benchmark is running.</p>
<figure><img src="/images/tech/net/send_std_bandwidth.svg"
         alt="Sent Bandwidth versus size when sending with the std::socket functions"/><figcaption>
            <p>Sent Bandwidth versus size when sending with the std::socket functions</p>
        </figcaption>
</figure>

<p>As you can see, we reach impressive bandwidths, that are completely unrealistic if we write to an actual network device. So what is the next logical step? Of course:try to generate even more unrealisitc numbers: Currently we use only one thread but most of our machines have more than one core. So let the premature optimization begin. (You will later see why this might be not that stupid)</p>
<h2 id="more-threads-with-stdsocket">More threads with std::socket</h2>
<p>The code in this section does not make that much sense in a server implementation: We will create multiple sockets that send to somewhere. So why write it? With such code we can later test our receive rates, since we will (most likely) accept packages from several clients at the same time.</p>
<figure><img src="/images/tech/net/send_std_threaded_independent.svg"
         alt="Sent Bandwidth versus size when sending with the std::socket functions"/><figcaption>
            <p>Sent Bandwidth versus size when sending with the std::socket functions</p>
        </figcaption>
</figure>

<p>As before, the packet rate does not decrease horribly until a packet size of 2^12 bytes.</p>
<p>Wait a second, are you telling me, that we satureate about 150 Gbits/s of bandwidth? Have a look yourself:</p>
<figure><img src="/images/tech/net/lots_of_bandwidth.jpg"
         alt="According to glances, we generate about 150 Gbit/s of traffic"/><figcaption>
            <p>According to <code>glances</code>, we generate about 150 Gbit/s of traffic</p>
        </figcaption>
</figure>

<h2 id="going-deeper-so_reuseport">Going deeper: SO_REUSEPORT</h2>
<p>In this section we will utilize multiple cores to send from one socket.
If we used the std::socket implementation to bind on the same address from multiple threads it would crash and tell us that the address is already in use.
Mhm.
Luckily we can get around this: We can set some socket options to allow exactly that. But at what cost? The only thing we have to sacrifice is that we cannot run our code on windows servers anymore, but that is a sacrifice I am willing to make.</p>
<p>According to the <a href="https://manpages.debian.org/stretch/manpages/socket.7.en.html">docs</a>, we need to set the flag <code>SO_REUSEPORT</code>.
Unfortunately the rust std lib does not allow us to do this and we therefore need to use a crate that extends the capabilites of our socket creation: <a href="https://docs.rs/socket2/0.3.12/socket2/">socket2</a>.</p>
<p>With <code>socket2</code> we can set the <code>SO_REUSEPORT</code> flag with the accoring methods:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Socket</span>::<span class="n">new</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">Domain</span>::<span class="n">ipv4</span><span class="p">(),</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">Type</span>::<span class="n">dgram</span><span class="p">(),</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nb">Some</span><span class="p">(</span><span class="n">Protocol</span>::<span class="n">udp</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">).</span><span class="n">unwrap</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">socket</span><span class="p">.</span><span class="n">set_reuse_port</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">socket</span><span class="p">.</span><span class="n">set_nonblocking</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">socket</span><span class="p">.</span><span class="n">bind</span><span class="p">(</span><span class="o">&amp;</span><span class="n">addr</span><span class="p">.</span><span class="n">into</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">socket</span><span class="p">.</span><span class="n">into_udp_socket</span><span class="p">();</span><span class="w">
</span></span></span></code></pre></div><p>Now we can bind multiple times to the same socket, e.g have 12 threads send from &ldquo;127.0.0.1:9000&rdquo;. Note however, that if we try to send faster than the system is able to handle, there might be no guarantees whether the kernel will drop packets and if so, from which thread they are.</p>
<figure><img src="/images/tech/net/send_s2_threaded.svg"
         alt="Packets per second versus thread count"/><figcaption>
            <p>Packets per second versus thread count</p>
        </figcaption>
</figure>

<p>The last thing to ask here is, can we gain a few more packets per sacket by optimizing the rust code itself? Perhaps even modify internals of a library?
Luckily, we don&rsquo;t need to. The amount of time spent in our code is so small, we cannot even see it in the Flamegraph below.</p>
<figure><img src="/images/tech/net/flamegraph_s2.svg"
         alt="According to the flame graph, we cannot gain much in terms of rust code. (Click on it, the graph is interactive)"/><figcaption>
            <p>According to the flame graph, we cannot gain much in terms of rust code. (Click on it, the graph is interactive)</p>
        </figcaption>
</figure>

<p>You can see that the code calls &ldquo;__libc_sendto&rdquo; internally and we therefore do not need to optimize any loops or such things in our rust code. However, there are still options we need to keep in mind for later: real-hardware might produce an entirely different picture but we will see when we come to that.</p>
<h2 id="next-steps">Next steps</h2>
<p>Until now, we only sent traffic on out local host, what is quite boring.
To get more realistic numbers, we need to send our traffic through real hardware.
In a future article I will try to do exactly this, but my laptop has only a one gigabit port.
Luckily you can rent hardware on a per-hour basis for affordable prices. As soon as the receiving side is ready, I will test send and receive speeds on real hardware.</p>
]]></content>
		</item>
		
		<item>
			<title>500K pps with tokio</title>
			<link>https://dwarfhack.com/posts/tech/tokio_pps/</link>
			<pubDate>Mon, 27 Jan 2020 12:07:40 +0100</pubDate>
			
			<guid>https://dwarfhack.com/posts/tech/tokio_pps/</guid>
			<description>After reading the cloudflare blog post on how to receive 1M packets per second, I wondered: How fast can we go with rust and tokio?
Scenario For my game server, I want to be able to receive many small UDP packets. Lots of them. Since typical updates in my case are only a few bytes (say x and y coordinates, a uid and timestamp totalling to say 32 bytes), a gameserver will reach its processing limit before even getting close to saturate the bandwidth of its uplink.</description>
			<content type="html"><![CDATA[<p>After reading the cloudflare <a href="https://blog.cloudflare.com/how-to-receive-a-million-packets/">blog post</a> on how to receive 1M packets per second, I wondered: How fast can we go with rust and tokio?</p>
<h2 id="scenario">Scenario</h2>
<p>For my game server, I want to be able to receive many small UDP packets. Lots of them.
Since typical updates in my case are only a few bytes (say x and y coordinates, a uid and timestamp totalling to say 32 bytes), a gameserver will reach its processing limit before even getting close to saturate the bandwidth of its uplink.</p>
<p>For this experiment, let&rsquo;s assume we bind on one IPv4 UDP socket that receives all our game traffic.
Further, let the received messages be sent to another task for processing (here: incrementing a counter and ignoring it).</p>
<h2 id="tokio">Tokio</h2>
<p><a href="https://tokio.rs/">Tokio</a> describes itself as an &lsquo;asynchronous run-time&rsquo;, in other words: it does provide means for running code annotated with those nice <code>async</code> keywords.
Since tokio has deep roots in the network programming environment, it supports network primitives as first-class citizens. This allows us to use non-blocking code to read and write from the network.</p>
<p>So let&rsquo;s use tokio by adding it to the <code>cargo.toml</code> with all features:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">dependencies</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">tokio</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">version</span> <span class="p">=</span> <span class="s2">&#34;0.2.10&#34;</span><span class="p">,</span> <span class="nx">features</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;full&#34;</span><span class="p">]</span> <span class="p">}</span>
</span></span></code></pre></div><h2 id="the-client">The client</h2>
<p>Now to the client, its only job is to send lots of updates at a roughly fixed rate.
You can find the full code in the github <a href="https://github.com/dwarfhack/tokio_udp_rate_test/tree/master/client">repo</a></p>
<p>Since we use tokio, we can directly tell it, that our main function will by executed asynchronous:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="cp">#[tokio::main]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">async</span><span class="w"> </span><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span><span class="w"> </span>-&gt; <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span><span class="w"> </span><span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span><span class="w"> </span><span class="n">Error</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">//...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Then we need a udp socket to send our messages with;</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">UdpSocket</span>::<span class="n">bind</span><span class="p">(</span><span class="n">local_addr</span><span class="p">).</span><span class="k">await</span><span class="o">?</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">socket</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="o">&amp;</span><span class="n">remote_addr</span><span class="p">).</span><span class="k">await</span><span class="o">?</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>Now a bit of a payload, in this case a single <code>i32</code> in a struct. Since we don&rsquo;t care about those updates, we will send the same update over and over again:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="n">update</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">MovementUpdate</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span>: <span class="mi">7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">encoded</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bincode</span>::<span class="n">serialize</span><span class="p">(</span><span class="o">&amp;</span><span class="n">update</span><span class="p">).</span><span class="n">unwrap</span><span class="p">();</span><span class="w">
</span></span></span></code></pre></div><p>Now, in a loop, we send the serialized update a few million times:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="n">socket</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="o">&amp;</span><span class="n">encoded</span><span class="p">).</span><span class="k">await</span><span class="o">?</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="the-server">The server</h2>
<p>The first server should perform the following steps:</p>
<ul>
<li>Accept a packet</li>
<li>parse it</li>
<li>increment the counter</li>
<li>send it to the worker task for processing</li>
</ul>
<p>Since the full code is in <a href="https://github.com/dwarfhack/tokio_udp_rate_test/tree/master/server">the repo</a>, I will only highlight the interesting parts here.</p>
<p>We spawn our two <code>async fn</code>s from the main function, that itself is not async. Spawning happens on a created runtime:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="p">(</span><span class="n">tx</span><span class="p">,</span><span class="n">rx</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mpsc</span>::<span class="n">channel</span><span class="p">(</span><span class="mi">100</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">handler</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rt</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="n">rcv_pass_handler</span><span class="p">(</span><span class="n">opt</span><span class="p">.</span><span class="n">clone</span><span class="p">(),</span><span class="n">rx</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">receiver</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rt</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="n">rcv_pass</span><span class="p">(</span><span class="n">opt</span><span class="p">.</span><span class="n">clone</span><span class="p">(),</span><span class="n">tx</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">join_handler</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">join_all</span><span class="p">(</span><span class="fm">vec!</span><span class="p">[</span><span class="n">handler</span><span class="p">,</span><span class="n">receiver</span><span class="p">]);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">rt</span><span class="p">.</span><span class="n">block_on</span><span class="p">(</span><span class="n">join_handler</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>The <code>rx</code> and <code>tx</code> channel parts are for communication between the tasks.</p>
<p>The <code>rcv_pass</code> function is looping over the following code to receive, parse and forward the datagrams.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="n">_res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">socket</span><span class="p">.</span><span class="n">recv_from</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span><span class="w"> </span><span class="n">buf</span><span class="p">).</span><span class="k">await</span><span class="p">.</span><span class="n">unwrap</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">let</span><span class="w"> </span><span class="n">packet</span>: <span class="nc">MovementUpdate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bincode</span>::<span class="n">deserialize</span><span class="p">(</span><span class="o">&amp;</span><span class="n">buf</span><span class="p">[</span><span class="o">..</span><span class="p">]).</span><span class="n">unwrap</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">tx</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">packet</span><span class="p">).</span><span class="k">await</span><span class="p">.</span><span class="n">unwrap</span><span class="p">();</span><span class="w">
</span></span></span></code></pre></div><p>(Sorry for the unwrap but here I don&rsquo;t care)</p>
<p>The handler function is looping over the other end of the channel and incrementing a counter, printing its calue from time to time:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nb">Some</span><span class="p">(</span><span class="n">packet</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rx</span><span class="p">.</span><span class="n">recv</span><span class="p">().</span><span class="k">await</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">msg_ctr</span><span class="o">+=</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// print from time to time
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>And that&rsquo;s it, now have a look at the results.</p>
<h2 id="results">Results</h2>
<p>That&rsquo;s what I found when running the code, ymmw.</p>
<figure><img src="/images/tech/tokio_pps/pps_test_01.jpg"
         alt="A screenshot when running on multiple hosts"/><figcaption>
            <p>A screenshot when running on multiple hosts</p>
        </figcaption>
</figure>

<h3 id="same-host">Same host</h3>
<p>When running client and server on the same host, my machine could achieve about 700Kpps.
This is good to know but has no relevance for any practical use cases except perhaps some IPC.</p>
<h3 id="different-hosts">Different hosts</h3>
<p>tl;dr: The maximum rate was 500Kpps.</p>
<p>To get closest to the use case, the server was run on a machine with decent power and the clients were run at several other hosts, everything connected via a gigabit ethernet switch.</p>
<p>Running any amount of clients with sufficient send rates capped at 500Kpps. But what was limiting throughüut here?</p>
<p>When running two independen isntances of the server, the total packet rate was, again, about 500Kpps. I therefore conclude that either the OS or the hardware is limiting the throughput.</p>
<p>If someone has a winows machine, it would be interesting to see if there is any difference in performance.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Rust and tokio are fast. Like, really fast.
But more important, they are elegant, they allow you to things at the speed of C++ without having to care about all the tiny details. Combine that with the ergonomic features like the async functions, and you have ma favourite programming language.</p>
]]></content>
		</item>
		
	</channel>
</rss>
