Ayende @ Rahienhttps://www.ayende.com/blog/Ayende @ RahienCopyright (C) Ayende Rahien 2004 - 2021 (c) 202460A twisted tale of memory optimization<p>I was looking into reducing the allocation in a particular part of our code, and I ran into what was basically the following code (boiled down to the essentials):</p>
<blockquote>
<script src="https://gist.github.com/ayende/5212d11a369922d2b4408d08890e831c.js"></script>
</blockquote>
<p>As you can see, this does a <em>lot</em> of allocations. The actual method in question was a pretty good size, and all those operations happened in different locations and weren’t as obvious.</p>
<p>Take a moment to look at the code, how many allocations can you spot here?</p>
<p>The first one, obviously, is the string allocation, but there is another one, inside the call to GetBytes(), let’s fix that first by allocating the buffer once (I’m leaving aside the allocation of the reusable buffer, you can assume it is big enough to cover all our needs):</p>
<blockquote>
<script src="https://gist.github.com/ayende/c4e0decb2442767e9bce88da7c5be89e.js"></script>
</blockquote>
<p>For that matter, we can also easily fix the second problem, by avoiding the string allocation:</p>
<blockquote>
<script src="https://gist.github.com/ayende/30fa55843832bfff0e630557fa75247e.js"></script>
</blockquote>
<p>That is a few minutes of work, and we are good to go. This method is called <em>a lot</em>, so we can expect a huge reduction in the amount of memory that we allocated.</p>
<p>Except… that didn’t happen. In fact, the amount of memory that we allocate remained pretty much the same. Digging into the details, we allocate roughly the same number of byte arrays (how!) and instead of allocating a lot of strings, we now allocate a lot of character arrays.</p>
<p>I broke the code apart into multiple lines, which made things a lot clearer. (<a href="https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA+ABATARgLABQGADAAQY4B0AKjAgC4DchJ5VAkgPLMEsDM5LKQDKYGADsAhlACWEUgG9CpFSPqxJAW1IB9AM7qYWnqtLAAnvRgBtALq7YAVz2TgAGxgAhRwDMfMKBNVDAEMFFIAdVkrAAowAAtpOzNffygAGlIZcXpSD3EASkVlU1US0tIAN2kzSxg9UgBeUgBRcUgAE2yAc0oAVWoAMQAOSgBxGHpPOr0Y4FSA60pKfNtMnScXdy8FqAKgiv1DLUoomViNmGdXD28/ReWLKz1bffLSAF9CD6A==">In fact, I threw that into SharpLab, to be honest</a>). Take a look:</p>
<blockquote>
<script src="https://gist.github.com/ayende/942a494f64f10df43d3af3886b475189.js"></script>
</blockquote>
<p>This code: buffer[..len] is actually translated to:</p>
<blockquote>
<p>char[] charBuffer= RuntimeHelpers.GetSubArray(buffer, Range.EndAt(len));</p>
</blockquote>
<p>That will, of course, allocate. I had to change the code to be very explicit about the types that I wanted to use:</p>
<blockquote>
<script src="https://gist.github.com/ayende/12049752c22b3574fe5d432039d6f481.js"></script>
</blockquote>
<p>This will not allocate, but if you note the changes in the code, you can see that the use of <em>var</em> in this case really tripped me up. Because of the number of overloads and automatic coercion of types that <em>didn’t</em> happen.</p>
<p>For that matter, note that <em>any</em> slicing on arrays will generate a <em>new</em> array, including this code:</p>
<blockquote>
<script src="https://gist.github.com/ayende/339bacdd5907b08534ea76f399ff89d4.js"></script>
</blockquote>
<p>This makes perfect sense when you realize what is going on and can still be a big surprise, I looked at the code <em>a lot</em> before I figured out what was going on, and that was with a profiler output that pinpointed the fault.</p>https://www.ayende.com/blog/199969-A/a-twisted-tale-of-memory-optimization?Key=cb2fc684-6549-466c-9c4f-0bae8b5ef566https://www.ayende.com/blog/199969-A/a-twisted-tale-of-memory-optimization?Key=cb2fc684-6549-466c-9c4f-0bae8b5ef566Thu, 24 Aug 2023 12:00:00 GMTA performance profile mystery: The cost of Stopwatch<p>Measuring the length of time that a particular piece of code takes is a surprising challenging task. There are two aspects to this, the first is how do you ensure that the cost of getting the start and end times won’t interfere with the work you are doing. The second is how to actually get the time (potentially <em>many </em>times a second) in as efficient way as possible.</p>
<p>To give some context, <a href="https://aakinshin.net/posts/stopwatch/">Andrey Akinshin</a> does a great overview of how the Stopwatch class works in C#. On Linux, that is basically calling to the <em>clock_gettime</em> system call, except that this is <em>not</em> a system call. That is actually a piece of code that the Kernel sticks inside <em>your</em> process that will then integrate with other aspects of the Kernel to optimize this. The idea is that this system call is <em>so frequent</em> that you cannot pay the cost of the Kernel mode transition. There is a good coverage <a href="https://berthub.eu/articles/posts/on-linux-vdso-and-clockgettime/">of this here</a>.</p>
<p>In short, that is a very well-known problem and quite a lot of brainpower has been dedicated to solving it. And then we reached this situation:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_2.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb.png" alt="image" width="1201" height="141" border="0" /></a></p>
<p>What you are seeing here is us testing the indexing process of RavenDB under the profiler. This is indexing roughly 100M documents, and according to the profiler, we are spending 15% of our time gathering metrics?</p>
<p>The StatsScope.Start() method simply calls Stopwatch.Start(), so we are basically looking at a profiler output that says that Stopwatch is accounting for 15% of our runtime?</p>
<p>Sorry, I don’t believe that. I mean, it is <em>possible</em>, but it seems far-fetched.</p>
<p>In order to test this, I wrote a <a href="https://gist.github.com/ayende/563e0e4b0dae1bf986a660076f143aa6">very simple program</a>, which will generate 100K integers and test whether they are prime or not. I’m doing that to test compute-bound work, basically, and testing calling Start() and Stop() either across the whole loop or in each iteration.</p>
<p>I run that a few times and I’m getting:</p>
<ul>
<li>Windows: 311 ms with Stopwatch per iteration and 312 ms without</li>
<li>Linux: 450 ms with Stopwatch per iteration and 455 ms without</li>
</ul>
<p>On Linux, there is about 5ms overhead if we use a per iteration stopwatch, on Windows, it is either the same cost or slightly cheaper <em>with </em>per iteration stopwatch.</p>
<p>Here is the profiler output on Windows:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_4.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb_1.png" alt="image" width="862" height="123" border="0" /></a></p>
<p>And on Linux:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_6.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb_2.png" alt="image" width="891" height="101" border="0" /></a></p>
<p>Now, that is what happens when we are doing a significant amount of work, what happens if the amount of work is negligible? I made the IsPrime() method very cheap, and I got:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_8.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb_3.png" alt="image" width="847" height="108" border="0" /></a></p>
<p>So that is a good indication that this isn’t <em>free</em>, but still…</p>
<p>Comparing the costs, it is utterly ridiculous that the profiler says that so much time is spent in those methods.</p>
<p>Another aspect here may be the issue of the profiler impact itself. There are differences between using Tracing and Sampling methods, for example.</p>
<p>I don’t have <em>an</em> answer, just a lot of very curious questions.</p>https://www.ayende.com/blog/199874-A/a-performance-profile-mystery-the-cost-of-stopwatch?Key=095fd4b2-4515-479e-a926-ffeed604e5f3https://www.ayende.com/blog/199874-A/a-performance-profile-mystery-the-cost-of-stopwatch?Key=095fd4b2-4515-479e-a926-ffeed604e5f3Wed, 09 Aug 2023 12:00:00 GMTProduction postmortem: The dog ate my request<p>A customer called us, quite upset, because their RavenDB cluster was failing every few minutes. That was weird, because they were running on our cloud offering, so we had full access to the metrics, and we saw absolutely no problem on our end.</p>
<p>During the call, it turned out that every now and then, but almost always immediately after a new deployment, RavenDB would fail some requests. On a fairly consistent basis, we could see two failures and a retry that was finally successful.</p>
<p>Okay, so at least there is no user visible impact, but this was still super strange to see. On the backend, we couldn’t see any reason why we would get those sort of errors.</p>
<p>Looking at the failure stack, we narrowed things down to an async operation that was invoked via DataDog. Our suspicions were focused on this being an error in the async machinery customization that DataDog uses for adding non-invasive monitoring.</p>
<p>We created a custom build for the user that they could test and waited to get the results from their environment. Trying to reproduce this locally using DataDog integration didn’t raise any flags.</p>
<p>The good thing was that we did find a smoking gun, a violation of the natural order and invariant breaking behavior.</p>
<p>The not so good news was that it was in our own code. At least that meant that we could fix this.</p>
<p>Let’s see if I can explain what is going on. The customer was using a custom configuration: FastestNode. This is used to find the nearest / least loaded node in the cluster and operate from it.</p>
<p>How does RavenDB <em>know</em> which is the fastest node? That is kind of hard to answer, after all. It checks.</p>
<p>Every now and then, RavenDB replicates a read request to all nodes in the cluster. Something like this:</p>
<blockquote>
<script src="https://gist.github.com/ayende/692d3ecd0df97d3b7e2df3997632ca19.js"></script>
</blockquote>
<p>The idea is that we send the request to all the nodes, and wait for the <em>first</em> one to arrive. Since this is the same request, all servers will do the same amount of work, and we’ll find the fastest node from our perspective.</p>
<p>Did you notice the cancellation token in there? When we return from this function, we cancel the existing requests. Here is what this looks like from the monitoring perspective:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Production-post-mortem_1004C/image_2.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Production-post-mortem_1004C/image_thumb.png" alt="image" width="1133" height="123" border="0" /></a></p>
<p>This looks <em>exactly</em> like every few minutes, we have a couple of failures (and failover) in the system and was quite confusing until we figured out exactly what was going on.</p>https://www.ayende.com/blog/199745-A/production-postmortem-the-dog-ate-my-request?Key=80c35c6a-b89c-4ac6-bc7b-10fdefa2754ahttps://www.ayende.com/blog/199745-A/production-postmortem-the-dog-ate-my-request?Key=80c35c6a-b89c-4ac6-bc7b-10fdefa2754aMon, 24 Jul 2023 12:00:00 GMTFight for every byte it takes: Fitting 64 values in 4 bits<p>Moving to <a href="https://ayende.com/blog/199362-B/fight-for-every-byte-it-takes-nibbling-at-the-costs?key=9b599e1acc764686bc2c4a6b18a09121">nibble encoding</a> gave us a measurable improvement in the density of the entries in the page. The problem is that we pretty much run out of room to do so. We are currently using a byte per entry to hold the size of the entry (as two nibbles, of 4 bits each). You can’t really go lower than that.</p>
<p>Let’s review again what we know about the structure of the data, we have an 8KB page, with three sections, fixed size header and variable size offsets and entries array. Here is what this looks like:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Fitting_12C2D/image_2.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Fitting_12C2D/image_thumb.png" alt="image" width="451" height="458" border="0" /></a></p>
<p>This is called a slotted page design. The idea is that the offset array at the bottom of the page is maintaining the sort orders of the entries, and that we can write the entries from the top of the page. When we need to sort the entries, we just need to touch the offsets array (shown in yellow in the image).</p>
<p>Given that we are talking about size and density, we spent a lot of time trying to reduce the size of the entries, but can we do something with the header or the offsets? The header is just 4 bytes right now, two shorts that denote the location of the bottom and the top position in the page. Given that the page is 8KB in size, we have to use 16 bits integer to cover the range. For offsets, the situation is the same. We have to be able to point to the entry location on the page, and that means that we have to reach 8KB. So the offsets are actually 16 bits ints and take two bytes.</p>
<p>In other words, there is a <em>hidden</em> overhead of 2 bytes per entry that we didn’t even consider. In the case of our latest success, we were able to push 759 entries into the page, which means that we are actually using 18.5% of the page just to hold the <em>offsets</em> of the entries. That is 1.48 KB that is being used.</p>
<p>The problem is that we <em>need</em> to use this. We have to be able to point to an entry anywhere in the page, which means that we have to reach 0 .. 8192. The minimum size we can use is 16 bits or two bytes.</p>
<p>Or do we?</p>
<p>16 bits gives us a range of 0 .. 65,535, after all. That is far in excess of what we need. We <em>could</em> use a 64KB page, but there are other reasons to want to avoid that.</p>
<p>To cover 8KB, we only need 13 bits to cover the range we need, after all. For that matter, we can extend that bit by 25%. If we decide that an entry should be 2 bytes aligned, we can access the entire page in 12 bits.</p>
<p>That means that we have <em>4 whole free bits</em> to play with. The first idea is to change the offsets array from 16 bits ints to 12 bits ints. That would save us 380 bytes at 759 entries per page. That is quite a lot. Unfortunately, working with bits in this manner would be <em>super</em> awkward. We are doing a lot of random access and moves while we are building the page. It is <em>possible</em> to do this using bits, but not fun.</p>
<p>So we can set things up so we have a nibble free to use. We <em>just</em> used nibbles to save on the cost of variable size ints, to great success.</p>
<p>However, we don’t need just <em>a</em> nibble, we need two of them. We need to store the size of the key and the value in bytes. Actually, we don’t need two nibbles. The size of the key and the value maxes at 8 bytes, after all. We can encode that in 3 bits. In other words, we need 6 bits to encode this information.</p>
<p>We only have 4 bits, however. It is a <em>really</em> nice idea, however, and I kept turning that in my head, trying to come up with all sorts of clever ways to figure out how we can push 64 values in 4 bits. The impact of that would be pretty amazing.</p>
<p>Eventually, I realized that it is fairly easy to <em>prove</em>, using math, that there is no way to do so. Faced with this failure, I realigned my thinking and found a solution. I don’t <em>need</em> to have a perfect answer, I can have a <em>good </em>one.</p>
<p>4 bits give me a range of 16 values (out of the possible 64). If I give up on trying to solve the whole problem, can I solve a meaningful part of it?</p>
<p>And I came up with the following idea. We can do a two-stage approach, we’ll map the most common 15 values of key and value sizes to those 4 bits. The last value will be a marker that you have to go and look elsewhere.</p>
<p>Using just the data in the offset, I’m able to figure out what the location of the entry in the page is as well as the size of the key and value for most cases. For the (hopefully rare) scenarios where that is <em>not</em> the case, we fall back to storing the size information as two nibbles preceding the entry data.</p>
<p>This is a pretty neat idea, even if I say so myself, and it has a good chance to allow us to save about 1 byte per entry in the common case. In fact, I tested that and about 90% of the cases in my test case are covered by the top 15 cases. That is a pretty good indication that I’m on the right track.</p>
<p>All of that said, let’s look at how this looks in code:</p>
<blockquote>
<script src="https://gist.github.com/ayende/766084b0333cf3eb90e4fdf66ec6f58c.js"></script>
</blockquote>
<p>I’m using a switch expression here for readability, so it is clear what is going on. If the key and value sizes are in one of the known patterns, we can put that in the nibble we’ll return. If the value is not, we’ll write it to the entry buffer.</p>
<p>The Set method itself had to change in some subtle but crucial ways, let’s look at it first, then I’ll discuss those changes:</p>
<blockquote>
<script src="https://gist.github.com/ayende/e23479d9e77e0a81abaadc81704736ea.js"></script>
</blockquote>
<p>As before, we encode the entry into a temporary buffer. Now, in addition to getting the length of the entry, we are also getting the nibble that we’ll need to store.</p>
<p>You can see the changes in how we work with the offsets array following that. When we need to update an existing value, we are using this construction to figure out the actual entry offset:</p>
<blockquote>
<pre><span style="color: #9b00d3;">var</span> actualEntryOffset = ((offsets[idx] & <span style="color: #9b00d3;">0xFFF0</span>) >> 3);</pre>
</blockquote>
<p>What exactly is going on here? Don’t try to figure it out yet, let’s see how we are <em>writing </em>the data:</p>
<blockquote>
<pre>top = (<span style="color: #0000ff;">ushort</span>)((top - reqEntryLen) & ~1); <span style="color: #9bbb59;">// align on two bytes boundary </span></pre>
<p>offsets[idx] = (<span style="color: #0000ff;">ushort</span>)(top << 3 | nibble);</p>
</blockquote>
<p>Those two code snippets may look very odd, so let’s go over them in detail.</p>
<p>First, remember that we have an 8KB page to work with, but we need to use 4 bits for the size nibble we got from encoding the entry. To address the full 8,192 values in the page, we’ll need to reserve 13 bits. That is… a problem. We solve that by saying that the entry addresses must always be aligned on two bytes boundary. That is handled by clearing the first bit in the new top computation. Since we are growing down, that has the effect of ensuring aligned-by-two.</p>
<p>Then, we merge the top location and the nibble together. We <em>know</em> that the bottom-most of the top is cleared, so we can just move the value by 3 bits and we know that we’ve 4 cleared bits ready.</p>
<p>Conversely, when we want to <em>read</em>, we clear the first 4 bits and then we shift by three. That has the effect of returning us back to the original state.</p>
<p>A little bit confusing, but we managed to get to squeeze 784 entries into the page using the realistic dataset and 765 using the full one. That is another 3.5% of space savings over the previous nibble attempt and over 10% increase in capacity from the variable integer approach.</p>
<p>And at this point, I don’t believe that there is anything more that I <em>can</em> do to reduce the size in a significant manner without having a negative impact elsewhere.</p>
<p>We are not done yet, however. We are done with the size aspect, but we also have much to do in terms of performance and optimizations for runtime.</p>
<p>In the meantime, you can see <a href="https://gist.github.com/ayende/af3c4a7acc8cc2508e0b676bc9a8eee5">my full code here</a>. In the next post, we are going to start talking about the actual machine code and how we can optimize it.</p>https://www.ayende.com/blog/199363-B/fight-for-every-byte-it-takes-fitting-64-values-in-4-bits?Key=e403cbee-47d0-4370-8a9d-07eb165b8f7chttps://www.ayende.com/blog/199363-B/fight-for-every-byte-it-takes-fitting-64-values-in-4-bits?Key=e403cbee-47d0-4370-8a9d-07eb165b8f7cThu, 27 Apr 2023 12:00:00 GMTFight for every byte it takes: Variable size data<p>In <a href="https://ayende.com/blog/199329-B/fight-for-every-byte-it-takes-storing-raw-numbers?key=8cb96c2c2e9c49a58ae7a0fd14b35d6b">my previous post</a>, we stored keys and values as raw numbers inside the 8KB page. That was simple, but wasteful. For many scenarios, we are never going to need to utilize the full 8 bytes range for a long. Most numbers are far smaller.</p>
<p>In the example I gave in the last post, we are storing the following range of numbers (file offsets, basically). I’m using two test scenarios, one where I’m testing the full range (for correctness) and one where I’m testing files under 512 GB in size. Given that we are trying to compress the space, once we hit the 512GB mark, it is probably <em>less</em> urgent, after all.</p>
<p>Here are the number generations that I’m using:</p>
<blockquote>
<script src="https://gist.github.com/ayende/afc45a55389d8dfe9348051f2b2e378f.js"></script>
</blockquote>
<p> </p>
<p>What this <em>means</em> is:</p>
<table border="0" cellspacing="5" cellpadding="5">
<tbody>
<tr>
<td valign="top">Full data set</td>
<td valign="top">Realistic data set</td>
</tr>
<tr>
<td valign="top">
<ul><!--StartFragment-->
<li> 3% in the first 128 bytes</li>
<li> 7% in the first 64 KB</li>
<li>25% in the first 8 MB</li>
<li>35% in the first 2 GB</li>
<li>15% in the first 512 GB</li>
<li>5% in the first 128 TB</li>
<li>3% in the first 32 Petabytes</li>
<li>2% in the first 4 Exabytes</li>
<!--EndFragment--></ul>
</td>
<td valign="top">
<ul><!--StartFragment-->
<li> 1% in the first 128 bytes</li>
<li> 2% in the first 64 KB</li>
<li>27% in the first 8 MB</li>
<li>35% in the first 2 GB</li>
<li>25% in the first 512 GB</li>
</ul>
<p> </p>
<ul><!--EndFragment--></ul>
</td>
</tr>
</tbody>
</table>
<p>This is meant to verify that we can handle <em>any</em> scenario, in practice, we can usually focus on the first 512 GB, which is far more common.</p>
<p>Using both approaches, I can fit using <a href="https://ayende.com/blog/199329-B/fight-for-every-byte-it-takes-storing-raw-numbers?key=8cb96c2c2e9c49a58ae7a0fd14b35d6b">my previous approach</a>, up to 511 entries per page. That makes sense, we are storing the data raw, so how can we do better? Most of the time, we don’t need anywhere near 8 bytes per value. For that reason, we have <a href="https://en.wikipedia.org/wiki/Variable-length_quantity">variable length encoding</a>, which has many names, such as variable size int, 7 bits integers, etc. I adapted some methods from the .NET codebase to allow me to operate on Spans, like so:</p>
<blockquote>
<script src="https://gist.github.com/ayende/593bdcab072b3ddd94646e6c724870e5.js"></script>
</blockquote>
<p>Let’s check what sort of savings we can get using this approach:</p>
<ul>
<li>Under 127 bytes– 1 byte</li>
<li>128 bytes .. 32 KB – 2 bytes</li>
<li>32KB .. 8MB – 3 bytes</li>
<li>8MB .. 2GB – 4 bytes</li>
<li>2 GB .. 512 GB – 5 bytes</li>
<li>512GB .. 128 TB – 6 bytes</li>
<li>128TB .. 32 Petabytes – 7 bytes</li>
<li>32 Petabytes .. 8 Exabytes – 8 bytes</li>
<li>Greater than 8 Exabytes – 9 bytes</li>
</ul>
<p>That is really cool, since for the realistic data set, we can pack a lot more data into the page.</p>
<p>It comes with a serious issue, however. The data is no longer fixed size (well, that <em>is </em>the point, no?). Why is that a problem? Because we want to be able to do a binary search on that, which means that we need to be able to access the data by index. As usual, the solution is to utilize indirection. We’ll dedicate the bottom of the page to an array of fixed-size int (16 bits – sufficient to cover the 8KB range of the page) that will point to the actual location of the entry. Like before, we are going to reserve the first few bytes as a header, in this case we’ll use 4 bytes, divided into two shorts. Those will keep track of the writes to the bottom and the top of the page.</p>
<p>At the bottom, we’ll have the actual offsets that point to the entries, and at the top, we write the actual entries. Here is what this looks like:</p>
<blockquote>
<script src="https://gist.github.com/ayende/fd693cbc0689ba30da6ea0035044e817.js"></script>
</blockquote>
<p>Let’s see how our reading from the page will look now. As you can see, it is very similar to what we had before, but instead of going directly to the key by its offset, we have to use the indirection:</p>
<blockquote>
<script src="https://gist.github.com/ayende/63a0469aed1da7724b6b58c06d57d286.js"></script>
</blockquote>
<p>The offsets array contains the location of the entry in the page, and that is laid out as the [varint-key][varint-val]. So we read (and discard) the key from the offset we found (we have to do that to discover its size) and then we read and return the actual value.</p>
<p>Let’s look at how we implemented the actual binary search in the page:</p>
<blockquote>
<script src="https://gist.github.com/ayende/f5657de04f85b10f363aaff80096dd4c.js"></script>
</blockquote>
<p>This is a bog standard binary search, with the only interesting bit that we are going through the offsets array to find the actual location of the key, which we then read using variable size decoding.</p>
<p>The interesting part of this model happens when we need to <em>set</em> a value. Here is what this looks like, with my notes following the code.</p>
<blockquote>
<script src="https://gist.github.com/ayende/03d11c5a244bce32a52c8579c6233764.js"></script>
</blockquote>
<p>This is quite a lot, I’ll admit. Let’s try to break up into individual pieces what is going on here.</p>
<p>First, we get the header values (bottom, top) and initialize them if empty (note that bottom is set to 4, <em>after </em>the header, while top is set to the <em>end</em> of the buffer). The idea is that the bottom grows up and the top grows down. This is called <a href="https://siemens.blog/posts/database-page-layout/">Slotted Page design</a> and it is a staple of database design.</p>
<p>We then encode the key and the value into a temporary buffer. We need to do that so we’ll know what size the entry will take. Then we need to figure out if we are updating an existing record or creating a new one.</p>
<p>Updating an existing record is complex. This is because the <em>size</em> of the new record may be <em>greater</em> than the size of the old one. So we can’t put it in the same location. I’m handling this by just allocating new space for this entry, ignoring the old space that was allocated to it.</p>
<blockquote>
<p>I’m not handling any deletes / space reclamation on this series. That is a separate subject, not complex, but fairly tedious to do properly. So I’m going to focus solely on writes.</p>
</blockquote>
<p>Updates to an existing entry that also change its size aren’t in my test dataset, so I’m not worried about it too much here. I mention this to point out that variable length records bring with them considerations that we wouldn’t have run into with the fixed-size model.</p>
<p>And after all of this work? What are the results?</p>
<p>With the fixed-size version, we could fit 511 entries into the page. With the variable size int, however, we can do better.</p>
<p>For the realistic dataset, I can fit 712 entries for the page, and for the full dataset, 710 (there are very few very big elements even there, but we can see that it has an impact).</p>
<p>511 vs. 712 may not sound like much, but that is 40% increase in the number of entries that I can fit. To give some context, using 8KB pages, that is a difference of 5 MB per million entries. That <em>adds</em> up.</p>
<p>The question is, can we do <em>better</em>? More on that in my next post…</p>https://www.ayende.com/blog/199361-B/fight-for-every-byte-it-takes-variable-size-data?Key=73f1175b-8518-4bd4-8657-233bf8dd7942https://www.ayende.com/blog/199361-B/fight-for-every-byte-it-takes-variable-size-data?Key=73f1175b-8518-4bd4-8657-233bf8dd7942Tue, 25 Apr 2023 12:00:00 GMTFight for every byte it takes: Storing raw numbers<p>I write databases for a living, which means that I’m thinking a <em>lot</em> about persistence. Here is a fun challenge that we went through recently. We have the need to store a list of keys and values and then lookup a value by key. Pretty standard stuff. The keys and values are both 64 bits integers. In other words, what I would like to have is:</p>
<blockquote>
<p>Dictionary<long,long> lookup;</p>
</blockquote>
<p>That would be perfect, except that I’ve to persist the data, which means that I have to work with raw bytes. It’s easiest to think about it if we have some code in front of us. Here is the interface that I need to implement:</p>
<blockquote>
<script src="https://gist.github.com/ayende/3db756efe28b2f9a06a59647af4d26de.js"></script>
</blockquote>
<p>As you can see, we have a byte buffer (8KB in size) and we want to add or lookup values from the buffer. All the data resides <em>in</em> the buffer, nothing is external. And we cannot unpack it in memory, because this is used for lookups, so this needs to be really fast.</p>
<p>The keys we are storing are file offsets, so they correlate quite nicely to the overall size of the file. Meaning that you’ll have a lot of small values, but also large ones. Given a key, we need to be able to look its value quickly, since we may run this lookup billions of times.</p>
<p>Given that I have 8KB of data, I can do the following, just treat the buffer as a sorted array, which means that I get a pretty easy way to search for a particular value and a simple way to actually store things.</p>
<p>Theoretically, given an 8KB page, and 16 bytes per each (key, value) entry, we can store up to 512 entries per page. But it turns out that this is just a theory. We also need to keep track of the <em>number</em> of items that we have, and that takes some space. Just a couple of bytes, but it means that we don’t have those bytes available. A page can now contain up to 511 entries, and even at full capacity, we have 14 bytes wasted (2 for the number of entries, and the rest are unused).</p>
<p>Here is what this looks like in code:</p>
<blockquote>
<script src="https://gist.github.com/ayende/46510091980b51cfb862456b8c7a9d8f.js"></script>
</blockquote>
<p>As you can see, we are creating two arrays, the keys are growing from the bottom of the page and the values are growing from the top. The idea is that I can utilize the BinarySearch() method to quickly find the index of a key (or where it <em>ought</em>) to go. From there, I can look at the corresponding values array to get the actual value. The fact that they are growing separately (and toward each other) means that I don’t need to move as much memory if I’m getting values out of order.</p>
<p>For now, I want to set up the playground in which we’ll operate. The <em>type</em> of data that you write into such a system is important. I decided to use the following code to generate the test set:</p>
<blockquote>
<script src="https://gist.github.com/ayende/288e87d6f1209e33285043c9a072e6ed.js"></script>
</blockquote>
<p>The idea is that we’ll generate a random set of numbers, in the given distribution. Most of the values are in the range of 8MB to 512GB, representing a pretty good scenario overall, I think.</p>
<p>And with that, we just need to figure out what metrics we want to use for this purpose. My goal is to push as many values as I can into the buffer, while maintaining the ability to get a value by its key as fast as possible.</p>
<p>The current approach, for example, does a binary search on a sorted array plus an extra lookup to the companion values array. You really can’t beat this, if you allow to store arbitrary keys. Here is my test bench:</p>
<blockquote>
<script src="https://gist.github.com/ayende/6e53c1d996068c9990878006c4ae76aa.js"></script>
</blockquote>
<p>This will insert key/value pairs into the page until it is full. Note that we allow duplicates (we’ll just update the value), so we need to keep track of the number of <em>entries</em> inserted, not just the number of insertions. We also validate the structure at any step of the way, to ensure that we always get the right behavior.</p>
<p>This code runs as expected and we can put 511 values into the page before it gives up. This approach works, it is simple to reason about and has very few flaws. It is also quite wasteful in terms of information density. I would like to do better than 511 entries / pager. <em>Is</em> it possible to drop below 16 bytes per entry?</p>
<p>Give it some thought, I’m going to present several ways of doing just that in my next post…</p>https://www.ayende.com/blog/199329-B/fight-for-every-byte-it-takes-storing-raw-numbers?Key=8cb96c2c-2e9c-49a5-8ae7-a0fd14b35d6bhttps://www.ayende.com/blog/199329-B/fight-for-every-byte-it-takes-storing-raw-numbers?Key=8cb96c2c-2e9c-49a5-8ae7-a0fd14b35d6bMon, 24 Apr 2023 12:00:00 GMTLooking into Corax’s posting lists: Part II<p>In a <a href="https://ayende.com/blog/198594-A/looking-into-coraxs-posting-lists-part-i?key=5172525066dd45348327285e42ef0483">previous post</a> (which went out a long time ago) I explained that we have the notion of a set of uint64 values that are used for document IDs. We build a B+Tree with different behaviors for branch pages and leaf pages, allowing us to pack a lot of document IDs (thousands or more) per page.</p>
<p>The problem is that this structure hold the data compressed, so when we add or remove a value, we don’t <em>know</em> if it exists already or not. That is a problem, because while we are able to do any relevant fixups to skip duplicates and erase removed values, we end up in a position where the number of entries in the set is not accurate. That is a Problem, with a capital P, since we use that for query optimizations.</p>
<p>The solution for that is to move to a different manner of storing the data in the leaf page, instead of going with a model where we add the data directly to the page and compress when the raw values section overflows, we’ll use the following format instead:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_129A4/image_2.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_129A4/image_thumb.png" alt="image" width="504" height="334" border="0" /></a></p>
<p>Basically, I removed the raw values section from the design entirely. That means that whenever we want to add a new value, we need to find the relevant compressed segment inside the page and add to it (potentially creating a page split, etc).</p>
<p>Obviously, that is <em>not</em> going to perform well for write. Since on each addition, we’ll need to decompress the segment, add to it and then compress it again.</p>
<p>The idea here is that we don’t <em>need</em> to do that. Instead of trying to store the entries in the set immediately, we are going to keep them in memory for the duration of the transaction. Just before we commit the transaction, we are going to have two lists of document IDs to go through. One of added documents and one of removed documents. We can then sort those ids and then start walking over the list, find the relevant page for each section in the list, and merging it with the compressed values.</p>
<p>By moving the buffering stage from the per-page model to the per-transaction model, we actually gain quite a lot of performance, since if we have a lot of changes to a single page, we can handle compression of the data only once. It is a very strange manner of working, to be honest, because I’m used to doing the operation immediately. By delaying the cost to the end of the transaction, we are able to gain two major benefits. First, we have a big opportunity for batching and optimizing work on large datasets. Second, we have a single code path for this operation. It’s always: “Get a batch of changes and apply them as a unit”. It turns out that this is far easier to understand and work with. And that is for the writes portion of Corax.</p>
<p>Remember, however, that Corax is a search engine, so we expect a <em>lot</em> of reads. For reads, we can now stream the results directly from the compressed segments. Given that we can usually pack a <em>lot</em> of numbers into a segment, and that we don’t need to compare to the uncompressed portion, that ended up benefiting us significantly on the read side as well, surprisingly.</p>
<p>Of course, there is also another issue, look at the Baseline in the Page Header? We’ll discuss that in the next post, turned out that it wasn’t such a good idea.</p>https://www.ayende.com/blog/199267-A/looking-into-coraxs-posting-lists-part-ii?Key=dc8bc34f-37ca-47be-b205-8b8c0de223e8https://www.ayende.com/blog/199267-A/looking-into-coraxs-posting-lists-part-ii?Key=dc8bc34f-37ca-47be-b205-8b8c0de223e8Fri, 14 Apr 2023 12:00:00 GMTProduction postmortem: The big server that couldn’t handle the load<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Production-postmortem-The-big-server-tha_EA92/image_2.png"><img style="border: 0px currentcolor; float: left; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Production-postmortem-The-big-server-tha_EA92/image_thumb.png" alt="image" width="141" height="90" align="left" border="0" /></a> A user of ours called us, quite frantic. They are running a lot of systems on RavenDB, and have been for quite some time.</p>
<p>However, very recently they started to run into severe issues. RavenDB would complain that there isn’t sufficient memory to run.</p>
<p>The system metrics, however, said that there are still gobs of GBs available (I believe that this is the appropriate technical term).</p>
<p>After verifying the situation, the on-call engineer escalated the issue. The problem was <em>weird</em>. There <em>was</em> enough memory, for sure, but for some reason RavenDB would be unable to run properly.</p>
<p>An important aspect is that this user is running a multi-tenant system, with each tenant being served by its own database. Each database has a few indexes as well.</p>
<p>Once we figured <em>that</em> out, it was actually easy to understand what is going on.</p>
<p>There are actually quite a few limits that you have to take into account. <a href="https://ayende.com/blog/197635-A/production-postmortem-out-of-memory-on-a-clear-sky">I talked about them here</a>. In that post, the issue was the maximum number of tasks defined by the system. After which, you can no longer create new threads.</p>
<p>In this case, the suspect was: vm.max_map_count.</p>
<p>Beyond just total memory, Linux has a limit on the number of memory <em>mappings</em> that a process may have. And RavenDB uses Voron, which is based on <em>mmap</em>(), and each database and each index typically have multiple maps going on.</p>
<p>Given the number of databases involved…</p>
<p>The solution was to increase the max_map_count and add a task for us, to give a warning to the user ahead of time when they are approaching the system's limits.</p>https://www.ayende.com/blog/198881-C/production-postmortem-the-big-server-that-couldnt-handle-the-load?Key=a4b4820b-d2b8-4e52-9a3c-69229409e55dhttps://www.ayende.com/blog/198881-C/production-postmortem-the-big-server-that-couldnt-handle-the-load?Key=a4b4820b-d2b8-4e52-9a3c-69229409e55dMon, 23 Jan 2023 12:00:00 GMTProduction postmortem: The heisenbug server<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Production-postmortem-The-hiesien_ABC0/image_2.png"><img style="border: 0px currentcolor; float: right; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Production-postmortem-The-hiesien_ABC0/image_thumb.png" alt="image" width="284" height="284" align="right" border="0" /></a>A user reported that they observed nodes in the cluster “going dark”. Basically, they would stop communicating with the rest of the cluster, but would otherwise appear functional. Both the internal and external metrics were all fine, the server would just stop responding to anything over the network. The solution for the problem was to restart the service (note, the service, not the whole machine), but the problem would happen every few days.</p>
<p>As you can imagine, we are taking this sort of thing <em>very</em> seriously, so we looked into the problem. And we came up short. The problem made <em>absolutely</em> no sense. The problem occurred on a (minor) version migration, but there was absolutely nothing related to this that could cause it. What was <em>really</em> weird was that the <em>service </em>itself continue to work. We could see log entries being written and it was able to execute scheduled backups, for example. It would just refuse to talk to us over the network.</p>
<p>That was <em>super</em> strange, since the <em>network </em>itself was fine. All the monitoring systems were green, after all. For that matter, the user was able to SSH into the system to restart the service. This didn’t match with any other issue we could think of. Since the user worked around the problem by restarting the server, we didn’t have a lead.</p>
<p>Then we noticed the exact same problem in one of our cloud instances, and there we have much better diagnostic capabilities. Once we had noticed a problematic server, we were able to SSH into that and try to figure out what was going on.</p>
<p>Here is what we found out:</p>
<ul>
<li>The server will not respond to HTTP(s) communication either from outside the machine or by trying to connect from <em>inside</em> the machine.</li>
<li>The server <em>will</em> respond to SNMP queries both from inside the machine and outside of it (which is how we typically monitor the system).</li>
</ul>
<p>When we designed RavenDB, we implemented a “maintenance hatch” for such scenarios, in addition to using HTTP(s) for communication, RavenDB also exposes a named pipe that allows you to connect to the server without going through the network at all. This ensures that if you have administrator privileges on the server, you are able to connect even if there are network issues, certificate problems, etc.</p>
<p>Here is the kicker. Under this particular situation, we <em>could</em> <em>not </em>activate this escape hatch. That is not supposed to be possible. Named pipes on Linux, where we run into the problem, are basically Unix Sockets. A network issue such as a firewall problem or something similar isn’t going to affect them.</p>
<p>At the same time, we were able to communicate with the process using SNMP. What is the problem?</p>
<p>Lacking any other options, we dumped the process, restarted the service, and tried to do the analysis offline. We couldn’t find any problem. All the details we looked at said that everything was fine, the server <em>was</em> properly listening to new connections and it <em>should</em> work. That was… weird.</p>
<p>And then it happened again, and we did the same analysis, and it came back the same. We were clueless. One of the things that we updated between versions was the .NET runtime that we were using, so we <a href="https://github.com/dotnet/runtime/issues/79446">opened an issue</a> to see if anyone ran into the same problem.</p>
<p>And then it happened again. This time, we knew that just looking at the dump wouldn’t help us, so we tried other avenues. Linux has a pretty rich set of knobs and dials that you can look at to see what was going on. We suspected that this may be an issue with running out of file descriptors, running out of memory, etc.</p>
<p>We tried looking into what is going on inside the process using <em>strace</em>, and everything was <strong>fine</strong>. The trace clearly showed that the server was processing requests and was able to send and receive data properly.</p>
<p>Wait, go through that statement again please!</p>
<p>It is fine? But the reason we are using <em>strace</em> is that there is a problem. It looks like the problem fixed itself. That was annoying, because we were hoping to use the trace to figure out what is going on. We added more monitoring along the way, which would let us know if the server found itself isolated. And we waited.</p>
<p>The next time we ran into the problem, the first thing we did was run <em>strace, </em>we needed to get the root cause as soon as possible, and we were afraid that it would fix itself before we had a chance to get to the root cause. The moment we used <em>strace</em>, the server got back online, continuing as if there was never any issue.</p>
<p>Over the next few instances of this issue, we were able to confirm the following observations:</p>
<ol>
<li>The service would stop responding to TCP and Unix Sockets entirely.</li>
<li>There were <em>no</em> firewall or network issues.</li>
<li>The service was up and functional, tailing the log showed activity.</li>
<li>We could query the server state using SNMP.</li>
<li>Running strace on the service process would fix the problem.</li>
</ol>
<p>There are a few more things, the actual trigger for the fix wasn’t strace itself. It was the <em>ptrace()</em> call, which it uses. That would cause the service to start responding again. The <em>ptrace() </em>call is basically the beginning and the end of debugging under Linux. <em>Everything </em>uses it.</p>
<p>If you want to dump a memory process, you start with <em>ptrace()</em>. You want to trace the calls, <em>ptrace(). </em>You want to <em>debug </em>the process? GDB will start by calling <em>ptrace()</em>, etc.</p>
<p>And doing that would alleviate the problem.</p>
<p>That was… quite annoying.</p>
<p>We still had absolutely no indication of what the root cause even was.</p>
<p>We suspected it may be something inside Kestrel that was causing a problem. But that wouldn’t affect the named pipes / Unix sockets that we also saw.</p>
<p>Networking <em>worked</em>, because SNMP did. We thought that this may be because SNMP uses UDP instead of TCP, and looked into that, but we couldn’t figure out how that would be any different.</p>
<p>Looking at this further, we found that we have this in the code dumps:</p>
<blockquote>
<pre><code> ~~~~ 5072
1 Interop+Sys.WaitForSocketEvents(IntPtr, SocketEvent*, Int32*)
1 System.Net.Sockets.SocketAsyncEngine.EventLoop()
1 System.Net.Sockets.SocketAsyncEngine+<>c.ctor>b__14_0(Object)</code></pre>
</blockquote>
<p>As you can see, we are waiting for this in the .NET Sockets thread. The SNMP, on the other hand, looked like:</p>
<blockquote>
<p><span style="font-family: 'Courier New';">Thread (0x559):<br /> [Native Frames]<br /> System.Net.Sockets!System.Net.Sockets.SocketPal.SysReceive()<br /> System.Net.Sockets!System.Net.Sockets.SocketPal.TryCompleteReceiveFrom()<br /> System.Net.Sockets!System.Net.Sockets.SocketAsyncContext.ReceiveFrom()<br /> System.Net.Sockets!System.Net.Sockets.SocketPal.ReceiveFrom()<br /> System.Net.Sockets!System.Net.Sockets.Socket.ReceiveFrom()<br /> SharpSnmpLib.Engine!Lextm.SharpSnmpLib.Pipeline.ListenerBinding.AsyncReceive()<br /> </span></p>
</blockquote>
<p>That was really interesting, since it meant that for sockets (both HTTP and Unix), we were always using async calls, but for SNMP, we were using the synchronous API. We initially suspected that this may be something related to the thread pool. Maybe we had something that blocked it, but it turns out to be a lot more interesting. Here is the code that is actually <em>handling</em> the SNMP:</p>
<blockquote>
<p>var count = _socket.ReceiveFrom(buffer, ref remote);</p>
<p>Task.Factory.StartNew(() => HandleMessage(buffer, count, (IPEndPoint)remote));</p>
</blockquote>
<p>In other words, we are actually reading from the socket in a blocking manner, but then processing the actual message using the thread pool. So being able to get results via SNMP meant the thread pool was well.</p>
<p>At this point we resulted to hair pulling, rubber ducking and in some instances, shaking our fists to heaven.</p>
<p>I reminded myself that I’m an adult with a bit of experience solving problems, and dug deeper. We started looking into how .NET is actually <a href="https://github.com/dotnet/runtime/blob/e52462326be03fb329384b7e04a33d3eb7c16736/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Unix.cs#L176">handling sockets in async mode.</a> This end up <a href="https://github.com/dotnet/runtime/blob/e52462326be03fb329384b7e04a33d3eb7c16736/src/native/libs/System.Native/pal_networking.c#L2727">here, doing a system call</a>:</p>
<blockquote>
<p>while ((numEvents = epoll_wait(port, events, *count, -1)) < 0 && errno == EINTR);</p>
</blockquote>
<p>Reading through the man page for <em>epoll_wait()</em> I learned how <em>epoll</em>() works, that it is complex and that we need to be aware of level-triggered and edge-triggered options. Since .NET uses <a href="https://github.com/dotnet/runtime/blob/e52462326be03fb329384b7e04a33d3eb7c16736/src/native/libs/System.Native/pal_networking.c#LL2693C60-L2693C67">edge-triggered</a> events (EPOLLET, which I keep reading as electronic chicken), we focused on that.</p>
<p>There are a <em>lot</em> of edge cases and things to cover, but everything we checked was handled properly. We finally had a good smoking gun. For some reason, we weren’t getting notifications from <em>epoll</em>(), even though we should. Using <em>strace()</em> or friends somehow fixes that.</p>
<p>We actually <em>found</em> the <a href="https://stackoverflow.com/questions/20272845/strace-fixes-hung-process">exact scenario we saw in StackOverflow</a>, but without any idea what the issue was. <a href="https://xkcd.com/979/">Truly, there is an XKCD for everything</a>.</p>
<p>Our current understanding of the issue:</p>
<ul>
<li>All async sockets in .NET are going through the same socket engine, and are using <em>epoll</em>() under the covers.</li>
<li>SNMP is using <em>synchronous</em> calls, so it wasn’t using <em>epoll()</em>.</li>
</ul>
<p>That covers both of the weird things that we are seeing. So what is the issue?</p>
<p>It is not in .NET. Given the size & scope of .NET, we wouldn’t be the only ones seeing that. Below .NET, there is the kernel, so we looked into that. The machines we were running that on were using kernel 5.4.0-azure-1095, so we looked into that.</p>
<p>And it looked like it is <a href="https://lore.kernel.org/linux-fsdevel/[email protected]/#r">a kernel bug</a>, which was fixed in the <a href="https://bugs.launchpad.net/ubuntu/+source/containerd/+bug/1996678/comments/28">next updated kernel</a>. A race condition inside the kernel would cause us to miss wakeups, and then we would basically just stall without anything to wake us up.</p>
<p>We dug deeper to understand a bit more about this situation, and <a href="https://man7.org/linux/man-pages/man2/ptrace.2.html">we got this</a>:</p>
<pre> Some system calls return with <strong>EINTR </strong>if a signal was sent to a
tracee, but delivery was suppressed by the tracer. (This is very
typical operation: it is usually done by debuggers on every
attach, in order to not introduce a bogus <strong>SIGSTOP</strong>). As of Linux
3.2.9, the following system calls are affected (this list is
likely incomplete): <a href="https://man7.org/linux/man-pages/man2/epoll_wait.2.html">epoll_wait(2)</a>, and <a href="https://man7.org/linux/man-pages/man2/read.2.html">read(2)</a> from an <a href="https://man7.org/linux/man-pages/man7/inotify.7.html">inotify(7)</a>
file descriptor. The usual symptom of this bug is that when you
attach to a quiescent process with the command
strace -p <process-ID>
then, instead of the usual and expected one-line output such as
restart_syscall(<... resuming interrupted call ...>_
or
select(6, [5], NULL, [5], NULL_
('_' denotes the cursor position), you observe more than one
line. For example:
clock_gettime(CLOCK_MONOTONIC, {15370, 690928118}) = 0
epoll_wait(4,_
What is not visible here is that the process was blocked in
<a href="https://man7.org/linux/man-pages/man2/epoll_wait.2.html">epoll_wait(2)</a> before <a href="https://man7.org/linux/man-pages/man1/strace.1.html">strace(1)</a> has attached to it. Attaching
caused <a href="https://man7.org/linux/man-pages/man2/epoll_wait.2.html">epoll_wait(2)</a> to return to user space with the error
<strong>EINTR</strong>. In this particular case, the program reacted to <strong>EINTR </strong>by
checking the current time, and then executing <a href="https://man7.org/linux/man-pages/man2/epoll_wait.2.html">epoll_wait(2)</a>
again. (Programs which do not expect such "stray" <strong>EINTR </strong>errors
may behave in an unintended way upon an <a href="https://man7.org/linux/man-pages/man1/strace.1.html">strace(1)</a> attach.)</pre>
<p>And.. that is exactly what is happening. On attaching, the <em>epoll_wait</em>() will return with <em>EINTR</em>, which will cause .NET to retry the command, and that “fixes” the issue.</p>
<p>It makes total sense now, and concludes the discovery process of a pretty nasty bug.</p>
<p>Now, if you’ll excuse me, I need to go and apologize to a rubber duck.</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Production-postmortem-The-hiesien_ABC0/image_4.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Production-postmortem-The-hiesien_ABC0/image_thumb_1.png" alt="image" width="228" height="229" border="0" /></a></p>https://www.ayende.com/blog/198849-C/production-postmortem-the-heisenbug-server?Key=1eeda567-02a8-4bbb-b90f-557523973233https://www.ayende.com/blog/198849-C/production-postmortem-the-heisenbug-server?Key=1eeda567-02a8-4bbb-b90f-557523973233Mon, 16 Jan 2023 12:00:00 GMTAnswer: what does this code print?<p>I posted this code <a href="https://ayende.com/blog/198721-A/challenge-what-does-this-code-print?key=1872d7ef392d44dbb7efeabf515e32d5">previously</a>:</p>
<blockquote>
<script src="https://gist.github.com/ayende/af76ca202e7de6ac6143401efa65278d.js"></script>
</blockquote>
<p>And asked what it prints. This is actually an infinite loop that will print an endless amount of zeros to the console. The question is why.</p>
<p>The answer is that we are running into two separate features of C# that interact with each other in a surprising way.</p>
<p>The issue is that we are using a <em>nullable</em> iterator here, and accessing the struct using the <em>Value </em>property. The problem is that this is a <em>struct</em>, and using a property will cause it to be copied.</p>
<p>So the way it works, the code actually runs:</p>
<blockquote>
<script src="https://gist.github.com/ayende/741819d6ccf7178a3724bf1ab08c8244.js"></script>
</blockquote>
<p>And now you can more easily see the temporary copies that are created and how because we are using a value type here, we are using a different instance each time.</p>https://www.ayende.com/blog/198722-A/answer-what-does-this-code-print?Key=3d5c3bc1-d958-4451-b5e7-70eb1054d847https://www.ayende.com/blog/198722-A/answer-what-does-this-code-print?Key=3d5c3bc1-d958-4451-b5e7-70eb1054d847Thu, 05 Jan 2023 12:00:00 GMTChallenge: what does this code print?<p>Given the following code:</p><p><script src="https://gist.github.com/ayende/af76ca202e7de6ac6143401efa65278d.js"></script></p><p><br></p><p>Can you <em>guess</em> what it will do? </p><p>Can you explain <em>why</em>?</p><p>I love that this snippet is under 20 lines of code, but being able to explain it shows a lot more knowledge about C# than you would expect.</p>https://www.ayende.com/blog/198721-A/challenge-what-does-this-code-print?Key=1872d7ef-392d-44db-b7ef-eabf515e32d5https://www.ayende.com/blog/198721-A/challenge-what-does-this-code-print?Key=1872d7ef-392d-44db-b7ef-eabf515e32d5Wed, 04 Jan 2023 12:00:00 GMTAnswer: What does this code print?<p>I asked the <a href="https://ayende.com/blog/198689-B/challenge-what-does-this-code-print?key=57f95b74a23443828fa5f02fb82d19b4">following question</a>, about code that uses <em>AsyncLocal</em> as well as async calls. Here is the code again:</p>
<blockquote>
<script src="https://gist.github.com/ayende/180ac03715165f723ffed93f228c0973.js"></script>
</blockquote>
<p>This code prints <em>False</em> twice, the question is why. I would expect that the <em>AsyncLocal</em> value to remain the same after the call to Start(), since that is obviously the <em>point</em> of <em>AsyncLocal. </em>It turns out that this isn’t the case.</p>
<p>AsyncLocal is good if you are trying to pass a value <em>down</em> to child tasks, but it won’t be applicable to other tasks that are called in the same level. In other words, it works for children, not siblings tasks. This is actually even more surprising in the code above, since we don’t do any awaits in the <em>Start</em>() method.</p>
<p>The question is why? <a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=net-7.0">Looking at the documentation</a>, I couldn’t see any reason for that. Digging deeper into the source code, I figured out what is going on.</p>
<p>We can use <a href="https://sharplab.io/#v2:D4AQTAjAsAUCAMACEEB0AVAFgJwKYEMATASwDsBzAblgWQgFZqYaBmZMRAZQGNdT9sxAPawA3rESTkbAIIBnAJ6luAGSHd8AGwA8AIyFDNAPkQzuAF2IA3XIgC8iUrgDupxcrUad+w0YAUAJRMUtLIABzIAGxc5gLmgRJS4jAhIWaWNqgAaloArrYO5tj5wVIAvrCJkiBsIBEg0QBKuaQJKUlVqSgAnH7p1rjZebhBnSEg3TFxgaWpdL39mTma+aPtkhUwZUA===">SharpLab.io</a> to lower the high level C# code to see what is actually going on here, which gives us the following code for the <em>Start</em>() method:</p>
<blockquote>
<script src="https://gist.github.com/ayende/d0fbad92269e1d9dde5767af50c15151.js"></script>
</blockquote>
<p>Note that we call to <em>AsyncTaskMethodBuilder.Start</em>() method, which ends up in <em>AsyncMethodBuilderCore.Start</em>(). There we have a bunch of interesting code, in particular, we remember the current thread execution context <em>before</em> we execute user code, <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs#L33-L34">here</a>. After the code is done running, we restore it if this is needed, as you can see <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs#L50-L53">here</a>.</p>
<p>That looks fine, but why would the execution context change here? It turns out that one of the few places that interact with it is the <em>AsyncValue</em> itself, which ends up in the <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs#L532">ExecutionContext.SetLocalValue</a>. The way it works, each time you set an async local, it creates a new <em>layer </em>in the async stack. And when you <em>exit</em> an async call, it will reset the async stack to the place it was before the async call started.</p>
<p>In other words, the <em>local</em> in the name <em>AsyncLocal</em> isn’t a match to <em>ThreadLocal</em>, but is more similar to a local <em>variable</em>, which goes out of scope on function exit.</p>
<p><a href="https://stackoverflow.com/a/37309427">This isn’t a new thing, and there are workarounds</a>, but it was interesting enough that I decided to dig deep and understand what is actually going on.</p>https://www.ayende.com/blog/198690-B/answer-what-does-this-code-print?Key=6c670721-8ab4-4dd4-8eb5-b6d3929a5a39https://www.ayende.com/blog/198690-B/answer-what-does-this-code-print?Key=6c670721-8ab4-4dd4-8eb5-b6d3929a5a39Thu, 15 Dec 2022 12:00:00 GMTChallenge: What does this code print?<p>Take a look at the following code, what do you think it will print?</p><blockquote><script src="https://gist.github.com/ayende/180ac03715165f723ffed93f228c0973.js"></script></blockquote><p>Since it obviously doesn’t print the <em>expected</em> value, why do you think this is the case?</p>https://www.ayende.com/blog/198689-B/challenge-what-does-this-code-print?Key=57f95b74-a234-4382-8fa5-f02fb82d19b4https://www.ayende.com/blog/198689-B/challenge-what-does-this-code-print?Key=57f95b74-a234-4382-8fa5-f02fb82d19b4Wed, 14 Dec 2022 12:00:00 GMTSolving support issues in other people’s products<p>A user <a href="https://github.com/ravendb/ravendb/issues/15375">contacted us</a> to tell us that RavenDB does not work in his environment. As you can imagine, we didn’t really like to hear that, so we looked deeper into the issue. The issue in question included the actual problem, which looked something like this:</p>
<blockquote>
<pre>{
"Url": "/auth/",
"Type": "Raven.Client.Exceptions.Routing.RouteNotFoundException",
"Message": "There is no handler for path: GET /auth/",
"Error": "Raven.Client.Exceptions.Routing.RouteNotFoundException: There is no handler for path: GET /auth/\n"
}</pre>
</blockquote>
<p>My reaction to that was… huh?!</p>
<p>That is a really strange error, since RavenDB does <em>not</em> have an “/auth/” endpoint. The problem isn’t with RavenDB, it is with something else.</p>
<p>In this case, the user ran RavenDB on port 8080 (which is the normal thing to do) and then tried to access RavenDB in the browser.</p>
<p>The problem was that they <em>previously</em> ran some other software, and that software had the following interaction:</p>
<blockquote>
<pre>* Connected to 127.0.0.1 port 8080
> GET / HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.85.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Location: http://127.0.0.1:8080/auth/
< Content-Type: text/html; charset=UTF-8</pre>
</blockquote>
<p>In other words, it redirected the browser to the “/auth/” endpoint. It’s critical to understand that 301 response means: Moved Permanently. That means that they are actually cached by the browser. In this case, the scenario was reusing the same endpoint for a different software product, and the browser cache meant that we got strange results.</p>https://www.ayende.com/blog/198626-A/solving-support-issues-in-other-peoples-products?Key=c33c5575-d31c-4373-b936-187722334e8bhttps://www.ayende.com/blog/198626-A/solving-support-issues-in-other-peoples-products?Key=c33c5575-d31c-4373-b936-187722334e8bTue, 06 Dec 2022 12:00:00 GMTLooking into Corax’s posting lists: Part I<p>One of the big changes in RavenDB is the new search engine, Corax. We want to replace Lucene with a purpose built search engine, capable of doing everything that we can do with Lucene, but far faster.</p>
<p>In this post, I want to discuss a particular detail of the implementation. Managing the posting list. In information retrieval, the idea of a posting list is that we have a list of document ids that match a particular term. I’m ignoring the details of how we create or manage the list. The basic idea is that we have a need to store a potentially large number of document ids, update them on the fly as well query them. Lucene manages that by creating immutable segments, but that design decision doesn’t match the way we want to do things in Corax. The problem with immutable segments is that you’ll eventually need to run compaction, which can be really expensive.</p>
<p>As a database, we already have a really good data structure that matches most of those requirements, it turns out. A B+Tree can do a pretty good approximation of a posting list, but it does have a problem. It’s <em>not</em> efficient in terms of space usage. A document id is a 64 bits integer, and we can make a number of assumptions about it. Therefore, I create a dedicated B+Tree like structure to hold the posting list. This B+Tree is called a Set inside of Voron, and it can hold any number of uint64 values. Internally, this is managed as two separate types of pages. The Branch pages (which are fairly standard B+Tree branches) and the Leaf pages.</p>
<p>Here is the first version of the Set Leaf Page implementation:</p>
<blockquote>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists_117D3/image_2.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists_117D3/image_thumb.png" alt="image" width="504" height="334" border="0" /></a></p>
</blockquote>
<p>Let’s figure out what is going on here. The page has a notion of a base. That means all the documents IDs that have the same upper 33 bits. Basically, inside a single page, all the IDs are in the same 2 billion range. That means that even though the document ids are 64 bits, in the context of a single page, we can treat them as 32 bits integers. That turns out to be important, since most integer compression routines work on 32 bits integers.</p>
<p>How does that work? We have a section in the page that is dedicated to raw values, and we insert values into that section until it is full. Whenever that happens, we compress the raw values section using PForDelta compression. The page will then contain multiple compressed segments and a single raw values segment. Each compressed segment is non-overlapping with the other compressed segments (but may overlap with the raw values). PForDelta is <em>really</em> good in compressing integers, especially if it is able to figure out patterns in your data. And documents IDs in Corax are explicitly designed to have common patterns so it will be able to take advantage of this behavior. When we read the entries from the page, we merge the compressed segments with the raw values and return the full details.</p>
<p><a href="https://github.com/ravendb/ravendb/blob/aee3df75d1edd8488a72bb03294f8853da34ca6b/src/Voron/Data/Sets/SetLeafPage.cs">The code isn’t particularly simple</a>, but has a fairly straightforward approach to the problem once you understand the design.</p>
<p>One thing that I haven’t touched is the notion of <em>removals</em>. That is an important concept, and we handle that in an interesting way. Remember that I said that the baseline for the page is the upper 33 bits? That is because the numbers inside the page are 31 bits in length, we reserve the top bit to mark a value as a tombstone marker. In other words, when we write to the raw values, we can have either a new value or a removed value, distinguished using the uppermost bit.</p>
<p>When we fill the raw values segment, we compress it alongside the relevant compressed segments. At that point, we’ll filter out the removed values. This is <em>very</em> similar to the segments that Lucene is using, but we do that on a page boundary (8KB), not across all values.</p>
<p>We are able to push a <em>lot</em> of values into a single page. We see typically thousands to tens of thousands of documents IDs fitting into a single 8KB page. That is pretty amazing, since even if you have a posting list that has millions of entries, the actual disk reads are minimal.</p>
<p>The design was with us throughout most of the development of Corax, but we ran into a serious issue with the way it works when we started to build the query optimizer for Corax.</p>
<p>That is an interesting statement, I think you’ll agree. What is the relation between a (very) low-level design of the on-disk data format and the result of the query optimizer?</p>
<p>Well, it turns out that one of the things that we need to know for the query optimizer is: “How big is this posting list?”</p>
<p>This question is actually <em>really</em> important to be able to generate optimal queries. And given the structure we have, we can provide a good answer to that, most of the time, but not always.</p>
<p>Why is that?</p>
<p>The problem is what happens when we want to <em>remove</em> a value from the set, or add an <em>existing</em> value. If the value already exists inside a compressed segment, we don’t open the compressed segement (which will require re-writing it from scratch), so we record an addition that is actually spurious. Conversely, if we try to remove a value that isn’t in the set, we’ll wrongly decrement the number of entries in the posting list, leading to issues with a mismatch between the record number of entries and the actual number we have in the posting list.</p>
<p>That was <em>very</em> confusing to figure out, I’ll admit. It was also really hard to fix, but I’ll address that in the next post in the series.</p>https://www.ayende.com/blog/198594-A/looking-into-coraxs-posting-lists-part-i?Key=51725250-66dd-4534-8327-285e42ef0483https://www.ayende.com/blog/198594-A/looking-into-coraxs-posting-lists-part-i?Key=51725250-66dd-4534-8327-285e42ef0483Wed, 30 Nov 2022 12:00:00 GMTOn the critical importance of accepting and working with invalid data<p>A long while ago, I was involved in a project that dealt with elder home care. The company I was working for was contracted by the government to send care workers to the homes of elderly people to assist in routine tasks. Depending on the condition of the patient in question, they would spend anything from 2 – 8 hours a day a few days a week with the patient. Some caretakers would have multiple patients that they would be in charge of.</p>
<p>There were a lot of regulations and details that we had to get right. A patient may be allowed a certain amount of hours a week by the government, but may also pay for additional hours from their own pocket, etc. Quite interesting to work on, but not the topic of this post.</p>
<p>A key aspect of the system was actually paying the workers, of course. The problem was the way it was handled. At the end of each month, they would designate “office hours” for the care workers to come and submit their time sheets. Those had to be signed by the patients or their family members and were submitted in person at the office. Based on the number of hours worked, the workers would be paid.</p>
<p>The time sheets would look something like this:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/829fd5737075_137B1/image_2.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/829fd5737075_137B1/image_thumb.png" alt="image" width="775" height="87" border="0" /></a></p>
<p>A single worker would typically have 4 – 8 of those, and the office would spend a considerable amount of time deciphering those, computing the total hours and handing over payment.</p>
<p>The idea of the system I was working on was to automate all of that. Here is more or less what I needed to do:</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/829fd5737075_137B1/image_4.png"><img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/829fd5737075_137B1/image_thumb_1.png" alt="image" width="1016" height="410" border="0" /></a></p>
<p>For each one of the timesheet entries, the office would need to input the start & end times, who the care worker took care of, whether the time was approved (actually, whether this was paid by the government, privately, by the company, etc), and whether it was counted as overtime, etc.</p>
<p>The rules for all of those were… interesting, but we got it working and tested. And then we got to talk with the actual users.</p>
<p>They took one look at the user interface they had to work with and absolutely rebelled. The underlying issue was that during the end of the month period, each branch would need to handle hundreds of care workers, typically within four to six hours. They didn’t have the <em>time</em> to do that (pun intended). Their current process was to review the submitted time sheet with the care worker, stamp it with approved, and put just the total hours worked into the payroll system.</p>
<p><a href="https://ayende.com/blog/Images/Open-Live-Writer/829fd5737075_137B1/image_6.png"><img style="border: 0px currentcolor; float: right; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/829fd5737075_137B1/image_thumb_2.png" alt="image" width="240" height="170" align="right" border="0" /></a>Having to spend so much time on data entry was horrendous for them, but the company really wanted to have that level of granularity, to be able to properly track how many hours were worked and handle its own billing more easily.</p>
<p>Many of the care workers were also… non-technical, and the timesheet had to be approved by the patient or family worker. Having a signed piece of paper was easy to handle, trying to get them to go on a website and enter hours was a non-starter. That was also before everyone had a smartphone (and even today I would assume that would be difficult in that line of business).</p>
<p>As an additional twist, it turns out that the manual process allowed the office employees to better manage the care workers. For example, they may give them a 0.5 / hour adjustment for a particular month to manage a difficult client or deal with some specific issue, or approve (at their discretion) overtime pay when it wasn’t <em>quite </em>"proper” to do so.</p>
<p>One of the reasons that the company wanted to move to a modern system was to avoid this sort of “initiatives”, but they turned out to be actually quite important for the proper management of the care workers and actually getting things done. For an additional twist, they had multiple branches, and each of those had a different methodology of how that was handled, and all of them were different from what HQ thought it should be.</p>
<p>The process turned out to be even more complex than we initially got, because there was a lot of flexibility in the system that was actually crucial for the proper running of the business.</p>
<p>If I recall properly, we ended up having a two-stage approach. During the end of the month rush, they would fill in the gross hours and payment details. That part of the system was intentionally simplified to the point where we did almost no data validation and trusted them to put the right values.</p>
<p>After payroll was processed, they had to actually put in all those hours and we would run a check on the expected amount / validation vs. what was actually paid. That gave the company the insight into what was going on that they needed, the office people were able to keep on handling things the way they were used to, and discrepancies could be raised and handled more easily.</p>
<p>Being there in the “I’m just a techie” role, so I got to sit on the sidelines and see tug-of-war between the different groups. It was quite interesting to see, I described the process above in a bit of a chaotic manner, but there <em>were</em> proper procedures and processes in the offices. They were just something that the company HQ never even realized was there.</p>
<p>It also taught me quite a lot about the importance of accepting “invalid” data. In many cases, you’ll see computerized solutions flat out refuse to accept values that they consider to be wrong. The problem is that often, you need to record reality, which may not agree with validation rules on your systems. And in most cases, reality wins.</p>https://www.ayende.com/blog/198370-A/on-the-critical-importance-of-accepting-and-working-with-invalid-data?Key=a6012150-3695-4ced-997e-b66d1c0326bbhttps://www.ayende.com/blog/198370-A/on-the-critical-importance-of-accepting-and-working-with-invalid-data?Key=a6012150-3695-4ced-997e-b66d1c0326bbFri, 14 Oct 2022 12:00:00 GMTWhat bug through yonder weirdness my build breaks?<p>We have a lot of tests for RavenDB, and we are running them on plenty of environments. We semi frequently get a build failure when running on the <a href="https://github.com/ravendb/ravendb/runs/8286680144?check_suite_focus=true">“macOS latest” runner</a> on GitHub.</p>
<p>The problem is that the information that I have is self-contradicting. Here is the most relevant piece:</p>
<blockquote>
<script src="https://gist.github.com/ayende/31c5cb783877d0109c093b08f73848d9.js"></script>
</blockquote>
<p>Here you can see the failure itself and what is causing it.</p>
<p>Note that the debug message is showing that all three variables here have the same numeric value. The <em>address</em> and the <em>current</em> variables are also held on the stack, so there is no option for race conditions, or something like that.</p>
<p>I can’t figure out any reason why this would be triggered, in this case. About the only thing that pops to mind is whether there is some weirdness going on with pointer comparisons on MacOS, but I don’t have a lead to follow.</p>
<p>We haven’t investigated it properly yet, I thought to throw this to the blog and see if you have any idea what may be going on here.</p>https://www.ayende.com/blog/198242-B/what-bug-through-yonder-weirdness-my-build-breaks?Key=965ff556-3ddd-4bb0-8141-93d0f07fa3f9https://www.ayende.com/blog/198242-B/what-bug-through-yonder-weirdness-my-build-breaks?Key=965ff556-3ddd-4bb0-8141-93d0f07fa3f9Tue, 13 Sep 2022 12:00:00 GMTMy new interview task: Stop the flow<p>We run into an interesting scenario at work that I thought would make for a pretty good interview task. Consider a server that needs to proxy a request from the outside world to an internal service, something like this:</p><p><a href="https://ayende.com/blog/Images/Open-Live-Writer/My-new-interview-task-Stop-the-flow_FAA4/image_4.png"><img width="621" height="355" title="image" style="border: 0px currentcolor; border-image: none; display: block; background-image: none;" alt="image" src="https://ayende.com/blog/Images/Open-Live-Writer/My-new-interview-task-Stop-the-flow_FAA4/image_thumb_1.png" border="0"></a></p><p>That isn’t that interesting. The interesting bit is that the network between the internal server and the proxy is running at 10Gb/sec and the external network is limited to 512Kb/sec. </p><p>Furthermore, the internal server expects the other side to just… take the load. It will blast the other side with as much data as it can, and if you can’t handle that, will cut the connection. What this means is that for small requests, the proxy can successfully pass the data to the external server, but for larger ones, it is unable to read the data quickly enough to do so and the internal server will disconnect from it.</p><p>It is the responsibility of the proxy to manage that scenario. That is the background for this task, practically speaking, this means that you have <a href="https://gist.github.com/ayende/8c1106d397220e35128659c2f1518c5e">the following code</a>, which works if the size is 8MB but fails if it is 64MB. </p><p>We have the SlowClientStream and the FastServerStream – which means that we are able to focus completely on the task at hand (ignoring networks, etc).</p><p>The requirement is to pass a 64 MB of data between those two streams (which have conflicting requirements)</p><ul><li>The FastServerStream requires that you’ll read from it in a rate of about 31Kb / sec.</li><li>The SlowClientStream, on the other hand, will accept data at a maximum rate of about 30Kb/sec (but is variable across time). </li></ul><p>You may <em>not</em> change the implementation of either stream (but may add behavior in separate classes). </p><p>You may not read the entire response from the server before sending to the client.</p><p>There is a memory limit of 32 MB on the maximum amount of memory you may use in the program.</p><p>How would you go about solving this?</p><p><a href="https://gist.github.com/ayende/8c1106d397220e35128659c2f1518c5e">The challenge skeleton is here</a>.</p>https://www.ayende.com/blog/197698-B/my-new-interview-task-stop-the-flow?Key=80004f0d-d90e-4a8f-bd83-dea31577ca50https://www.ayende.com/blog/197698-B/my-new-interview-task-stop-the-flow?Key=80004f0d-d90e-4a8f-bd83-dea31577ca50Thu, 11 Aug 2022 12:00:00 GMTWhen debugging, assume an unreliable narrator<p>When we are handling a support call, we are often working with partial information about the state of the software at the customer site. Sometimes that is an unavoidable part of the job. When troubleshooting a system with patients' records, I can’t just ask the customer to schlep the data to my laptop so I can analyze it properly. Even if we could do that, there are a <em>lot</em> of cases that simply don’t reproduce anywhere but the live environment.</p>
<p>Part of the process of debugging an issue in a production environment is to be able to gather enough information <em>on site</em> that we can draw the appropriate conclusions from. RavenDB comes with a lot of tools to do just that. One of the most useful of those tools is the idea of the debug package. That is a simple idea, in the end. It gathers all the information we have about our system and packages that into a zip file. That zip file contains a lot of metrics, but it doesn’t contain customer data (aside from databases & index names, which are usually not sensitive).</p>
<p>There have been several separate cases recently where we were able to use the debug package to analyze what is going on and came back to the customer with answers. However, when hearing our explanations about what was the root cause of the issue, the customer rejected our hypothesis.</p>
<p>In one case, a customer was worried about the load that they were observing in their system. Not because there <em>was</em> an issue, but the number of requests that they observed was higher than was expected. The customer switched to using concurrent subscriptions recently and deployed a couple of worker nodes to spread the load of processing documents. However, the number of requests observed was <em>far</em> higher than they expected. Whereas before they had a single worker per subscription, and a known amount of work that they could easily measure, after switching to concurrent subscriptions they observed a <em>big</em> increase in the number of requests processed by RavenDB.</p>
<p>Given that they deployed their subscriptions to two workers, initially, it was expected that the amount of work that the cluster is processing would double. Instead, it was increased by tenfold. Looking at the metrics in the debug package, we could see that they had 10 instances of each subscription running, but the customer was insistent that they only deployed two workers nodes.</p>
<p>Our metrics said that there were 5 subscriptions from IP-1 and 5 subscriptions from IP-2. After some back and forth it was revealed that everyone was correct, but talking past each other. The customer deployed two worker nodes, yes. But each of those spawned 5 instances of the subscriptions to take advantage of concurrency inside the same worker node.</p>
<p>In the second case, we have a customer that noticed a marked increase in the amount of bandwidth that they were using. They traced that additional bandwidth to the internal communication between the nodes in the cluster. Given that they are running in the cloud, they were (rightly) concerned about the sudden jump in the bandwidth. We started the investigation process and… we didn’t like what we saw. The cluster had gone through <em>three</em> full node rebuilds in the past month. At least, that was what the data was telling us. But that didn’t make much sense.</p>
<p>Quite concerned that there is something really bad going on, we talked to the customer, who thought about this for a while, checked their own logs and explained what was going on. They are running on Lsv2-series Azure instances, and apparently, within the space of a few weeks, all three of their instances had been moved to another physical host. The Lsv2-series instances use local ephemeral NVMe drives. When they moved an instance between hosts, the effect was as if we were given a brand new hard disk. RavenDB was able to handle that scenario more or less seamlessly, with the other nodes in the cluster filling in for the down node and sending it all the data it lost. The effect of that, of course, was a big jump in network bandwidth while that was going on.</p>
<p>The customer wasn’t actually aware that this happened until they looked at the logs, RavenDB had it handled, and it was only noticed because of the bandwidth spike.</p>
<p>The point of this post isn’t to talk about how awesome RavenDB is (even if I do think it is pretty awesome). Nor is it to extoll how good our support team is at figuring out things about the customer setup that even the customer isn’t aware of.</p>
<p><em>The</em> point of this post is that you have to take into account, quite clearly, that the details that the customer is providing may be outdated, wrong or just misleading. Not because of any malicious intention on their end, but because they give you the information they <em>have</em>, not what is actually going on.</p>
<p>It reminds me of an old trick in tech support: “Take the plug out of the socket, blow on both the socket and the plug, then insert it again”. The point isn’t to blow whatever dust may have been there, preventing good contact. The point is to ensure that the silly thing is actually <em>plugged</em> in, but you can’t <em>ask</em> if this is plugged in, because the person on the other side of the call would say: “Of course it is” and never check.</p>https://www.ayende.com/blog/197793-C/when-debugging-assume-an-unreliable-narrator?Key=f1425369-1baa-4f76-a7a8-f61bc332716bhttps://www.ayende.com/blog/197793-C/when-debugging-assume-an-unreliable-narrator?Key=f1425369-1baa-4f76-a7a8-f61bc332716bWed, 20 Jul 2022 12:00:00 GMTProduction postmortem: Out of memory on a clear sky<p>A customer opened a support call telling us that they reached the scaling limits of RavenDB. Given that they had a pretty big machine specifically to handle the load they were expecting, they were (rightly) upset about that.</p>
<p>A short back and forth caused us to realize that RavenDB started to fail shortly after they added a new customer to their system. And by fail I mean that it started throwing <em>OutOfMemoryException</em> in certain places. The system was <em>not</em> loaded and there weren’t any other indications of high load. The system had <em>plenty</em> of memory available, but critical functions inside RavenDB would fail because of out of memory errors.</p>
<p>We looked at the actual error and found this log message:</p>
<blockquote>
<pre>Raven.Client.Exceptions.Database.DatabaseLoadFailureException: Failed to start database orders-21
At /data/global/ravenData/Databases/orders-21
---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
at System.Threading.Thread.StartInternal(ThreadHandle t, Int32 stackSize, Int32 priority, Char* pThreadName)
at System.Threading.Thread.StartCore()
at Raven.Server.Utils.PoolOfThreads.LongRunning(Action`1 action, Object state, String name) in C:\Builds\RavenDB-5.3-Custom\53024\src\Raven.Server\Utils\PoolOfThreads.cs:line 91
at Raven.Server.Documents.TransactionOperationsMerger.Start() in C:\Builds\RavenDB-5.3-Custom\53024\src\Raven.Server\Documents\TransactionOperationsMerger.cs:line 76
at Raven.Server.Documents.DocumentDatabase.Initialize(InitializeOptions options, Nullable`1 wakeup) in C:\Builds\RavenDB-5.3-Custom\53024\src\Raven.Server\Documents\DocumentDatabase.cs:line 388
at Raven.Server.Documents.DatabasesLandlord.CreateDocumentsStorage(StringSegment databaseName, RavenConfiguration config, Nullable`1 wakeup) in C:\Builds\RavenDB-5.3-Custom\53024\src\Raven.Server\Documents\DatabasesLandlord.cs:line 826
</pre>
</blockquote>
<p>This is <em>quite</em> an interesting error. To start with, this is us failing to load a database, because we couldn’t spawn the relevant thread to handle transaction merging. That is <em>bad</em>, but why?</p>
<p>It turns out that .NET will only consider a single failure scenario for a thread failing to start. If it fails, it must be because the system is out of memory. However, we are running on Linux, and there are <em>other</em> reasons why that can happen. In particular, there are various limits that you can set on your environment that would limit the number of threads that you can set.</p>
<p>There are global knobs that you should look at first, such as those:</p>
<ul>
<li><code>/proc/sys/kernel/threads-max</code></li>
<li><code><code>/proc/sys/kernel/pid_max </code></code></li>
<li><code><code><code>/proc/sys/vm/max_map_count</code></code></code></li>
</ul>
<p>Any of those can serve as a limit. There are also ways to set those limits on a per process manner.</p>
<p>There is also a per user setting, which is controlled via:</p>
<blockquote>
<pre><code>/etc/systemd/logind.conf: UserTasksMax</code></pre>
</blockquote>
<p>The easiest way to figure out what is going on is to look at the kernel log at that time, here is what we got in the log:</p>
<blockquote>
<pre><code>a-orders-srv kernel: cgroup: fork rejected by pids controller in /system.slice/ravendb.service</code></pre>
</blockquote>
<p>That made it obvious where the problem was, in the ravendb.service file, we didn’t have TasksMax set, which meant that it was set to 4915 (probably automatically set by the system depending on some heuristic).</p>
<p>When the number of databases and operations on the database reached a particular size, we hit the limit and started failing. That is <em>not</em> a fun place to be in, but at least it is easy to fix.</p>
<p>I created this post specifically so it will be easy to Google that in the future. I also created <a href="https://github.com/dotnet/runtime/issues/71761">an issue to get a better error message in this scenario</a>.</p>https://www.ayende.com/blog/197635-A/production-postmortem-out-of-memory-on-a-clear-sky?Key=43188033-6bfd-4682-9fc8-1bef248f2cafhttps://www.ayende.com/blog/197635-A/production-postmortem-out-of-memory-on-a-clear-sky?Key=43188033-6bfd-4682-9fc8-1bef248f2cafMon, 11 Jul 2022 12:00:00 GMT