Production postmortemBroken networks and compressed streams

time to read 3 min | 595 words

RavenDB introduced a TCP compression feature in version 5.3. The idea is that all internal communication in the cluster (as well as subscriptions), will use the Zstd compression format to reduce the overall bandwidth utilization by RavenDB. We have always supported HTTP compression, and that closed the circle.

The fact hat we are using Zstd means that we have a higher compression ratio and less CPU usage, so everyone was happy.  Except… sometimes, they weren’t.

In some cases, we noticed that there would be network failures at a far higher rate than previous experienced. RavenDB is robust to network errors, so that was handled, but that is still a concern. We figured out that the problem was rooted in the compression code. If we enabled compression between the systems, it would have far higher rate of failures than otherwise. But only when running in secured mode, when the system is running without security, everything works.

My first suspicion is that something is in the network, monitoring it. But the whole point of secured mode is that no one can peek into the stream not interfere with the contents. Given that this is a self-healing issue, it took some time to dedicate the right amount of attention to it, but we managed to figure it out.

This is a confluence of three different features that all play together to get this to happen.

With compression, we typically do something like this:

That is pretty much how all compression stream will work. But we do have to consider the following issue, there may be no output.

When can that happen?

Let’s assume that I’m using the simplest compression algorithm (run length encoding).

In other words, it will take a buffer such as: aaaaaacccccccbbbb and turn that into a7c6b4.

Now, let’s consider what would be the output of such an algorithm if we pass it a buffer consisting of a single value?

It will only update its internal state, it will not output anything. That is fine, we need a call to Flush() to ensure that all the state is out.

That means that this will return an empty buffer, which we are then writing to the inner stream. And that is fine, right? Since writing a zero length buffer is a no-op.

Except that it isn’t a no-op. There is the concept of empty SSL records, mostly it seams to handle the BEAST attack. So when you pass an empty buffer to the SslStream, it will emit an empty record to the network.

Which is good, except that you may have a scenario where you emit a lot of those values. And it turns out that OpenSSL has a limit to how many consecutive empty records it will accept (under the assumption that it must move forward and produce output and not just loop).

So, in order to repeat this bug, we need:

  • Input that will result in zero output from the compressor (fully repeating previous values, usually). Resulting in a zero length buffer as the output of the compression.
  • Sending the empty SSL record over the stream.
  • Repeating this for 32 times.

When all three conditions are satisfied, we get an error on the receiving end and the connection is broken. That means that the next call will have a different compression state and likely won’t have a problem at the same location.

In short, this is fun exercise in seeing how three different design decisions, all of whom are eminently reasonable, result in a very hard to trace bug.

The good thing is that this is simplicity itself to solve. We just need to avoid writing zero length buffer to the stream.

More posts in "Production postmortem" series:

  1. (24 Jul 2023) The dog ate my request
  2. (03 Jul 2023) ENOMEM when trying to free memory
  3. (27 Jan 2023) The server ate all my memory
  4. (23 Jan 2023) The big server that couldn’t handle the load
  5. (16 Jan 2023) The heisenbug server
  6. (03 Oct 2022) Do you trust this server?
  7. (15 Sep 2022) The missed indexing reference
  8. (05 Aug 2022) The allocating query
  9. (22 Jul 2022) Efficiency all the way to Out of Memory error
  10. (18 Jul 2022) Broken networks and compressed streams
  11. (13 Jul 2022) Your math is wrong, recursion doesn’t work this way
  12. (12 Jul 2022) The data corruption in the node.js stack
  13. (11 Jul 2022) Out of memory on a clear sky
  14. (29 Apr 2022) Deduplicating replication speed
  15. (25 Apr 2022) The network latency and the I/O spikes
  16. (22 Apr 2022) The encrypted database that was too big to replicate
  17. (20 Apr 2022) Misleading security and other production snafus
  18. (03 Jan 2022) An error on the first act will lead to data corruption on the second act…
  19. (13 Dec 2021) The memory leak that only happened on Linux
  20. (17 Sep 2021) The Guinness record for page faults & high CPU
  21. (07 Jan 2021) The file system limitation
  22. (23 Mar 2020) high CPU when there is little work to be done
  23. (21 Feb 2020) The self signed certificate that couldn’t
  24. (31 Jan 2020) The slow slowdown of large systems
  25. (07 Jun 2019) Printer out of paper and the RavenDB hang
  26. (18 Feb 2019) This data corruption bug requires 3 simultaneous race conditions
  27. (25 Dec 2018) Handled errors and the curse of recursive error handling
  28. (23 Nov 2018) The ARM is killing me
  29. (22 Feb 2018) The unavailable Linux server
  30. (06 Dec 2017) data corruption, a view from INSIDE the sausage
  31. (01 Dec 2017) The random high CPU
  32. (07 Aug 2017) 30% boost with a single line change
  33. (04 Aug 2017) The case of 99.99% percentile
  34. (02 Aug 2017) The lightly loaded trashing server
  35. (23 Aug 2016) The insidious cost of managed memory
  36. (05 Feb 2016) A null reference in our abstraction
  37. (27 Jan 2016) The Razor Suicide
  38. (13 Nov 2015) The case of the “it is slow on that machine (only)”
  39. (21 Oct 2015) The case of the slow index rebuild
  40. (22 Sep 2015) The case of the Unicode Poo
  41. (03 Sep 2015) The industry at large
  42. (01 Sep 2015) The case of the lying configuration file
  43. (31 Aug 2015) The case of the memory eater and high load
  44. (14 Aug 2015) The case of the man in the middle
  45. (05 Aug 2015) Reading the errors
  46. (29 Jul 2015) The evil licensing code
  47. (23 Jul 2015) The case of the native memory leak
  48. (16 Jul 2015) The case of the intransigent new database
  49. (13 Jul 2015) The case of the hung over server
  50. (09 Jul 2015) The case of the infected cluster