Production postmortemThe server ate all my memory
A customer reported a scenario where RavenDB was using stupendous amounts of memory. In the orders of tens of GB on a system that didn’t have that much load.
Our first suspicion was that this is an issue with reading the metrics, since RavenDB will try to keep as much of the data in memory, which sometimes leads users to worry. I spoke about this at length in the past.
In this case, that wasn’t the case. We were able to drill down into the exact cause of the memory usage and we found out that RavenDB was using an abnormally high amount of memory. The question was why that was, exactly.
We looked into the common operations on the server, and we found a suspicious query, it looked something like this:
from index 'Sales/Actions'
where endsWith(WorkflowStage, '/Final')
The endsWith query was suspicious, so we looked into that further. In general, endsWith requires us to scan all the unique terms for a particular field, but in most cases, there aren’t that many unique values for a field. In this case, however, that wasn’t the case, here are some of the values for WorkflowStage:
- Workflows/3a1af12a-b5d2-4c96-9348-177ebaacab6c/Step-2
- Workflows/6aacc86c-2f28-4b8b-8dee-1024314d5add/Final
In total, there were about 250 million sales in the database, each one of them with a unique WorflowStage value.
What does this mean, in terms of RavenDB query execution? Well, the fields are indexed, but we need to effectively do:
This isn’t the actual code, but it will show you what is going on.
In other words, in order to process this query, we have to scan (and materialize) all 250 million unique terms for this field. Obviously that is going to consume a lot of memory.
But what is the solution to that? Instead of doing an expensive endsWith query, we can move the computation from the query time to the index time.
In other words, instead of indexing the WorkflowStage field as is, we’ll extract the information we want from it. The index would have one of those:
IsFinalWorkFlowStage = doc.WorkflowStage.EndsWith(“/Final”),
WorkflowStagePostfix = doc.WorkflowStage.Split(‘/’).Last()
The first one will check whether the value is final or not, while the second just gets the (one of hopefully a few) postfixes for the field. We can then query using equality instead of endsWith, leading to far better performance and greatly reduced memory usage, since we don’t need to materialize any values during the query.
More posts in "Production postmortem" series:
- (12 Dec 2023) The Spawn of Denial of Service
- (24 Jul 2023) The dog ate my request
- (03 Jul 2023) ENOMEM when trying to free memory
- (27 Jan 2023) The server ate all my memory
- (23 Jan 2023) The big server that couldn’t handle the load
- (16 Jan 2023) The heisenbug server
- (03 Oct 2022) Do you trust this server?
- (15 Sep 2022) The missed indexing reference
- (05 Aug 2022) The allocating query
- (22 Jul 2022) Efficiency all the way to Out of Memory error
- (18 Jul 2022) Broken networks and compressed streams
- (13 Jul 2022) Your math is wrong, recursion doesn’t work this way
- (12 Jul 2022) The data corruption in the node.js stack
- (11 Jul 2022) Out of memory on a clear sky
- (29 Apr 2022) Deduplicating replication speed
- (25 Apr 2022) The network latency and the I/O spikes
- (22 Apr 2022) The encrypted database that was too big to replicate
- (20 Apr 2022) Misleading security and other production snafus
- (03 Jan 2022) An error on the first act will lead to data corruption on the second act…
- (13 Dec 2021) The memory leak that only happened on Linux
- (17 Sep 2021) The Guinness record for page faults & high CPU
- (07 Jan 2021) The file system limitation
- (23 Mar 2020) high CPU when there is little work to be done
- (21 Feb 2020) The self signed certificate that couldn’t
- (31 Jan 2020) The slow slowdown of large systems
- (07 Jun 2019) Printer out of paper and the RavenDB hang
- (18 Feb 2019) This data corruption bug requires 3 simultaneous race conditions
- (25 Dec 2018) Handled errors and the curse of recursive error handling
- (23 Nov 2018) The ARM is killing me
- (22 Feb 2018) The unavailable Linux server
- (06 Dec 2017) data corruption, a view from INSIDE the sausage
- (01 Dec 2017) The random high CPU
- (07 Aug 2017) 30% boost with a single line change
- (04 Aug 2017) The case of 99.99% percentile
- (02 Aug 2017) The lightly loaded trashing server
- (23 Aug 2016) The insidious cost of managed memory
- (05 Feb 2016) A null reference in our abstraction
- (27 Jan 2016) The Razor Suicide
- (13 Nov 2015) The case of the “it is slow on that machine (only)”
- (21 Oct 2015) The case of the slow index rebuild
- (22 Sep 2015) The case of the Unicode Poo
- (03 Sep 2015) The industry at large
- (01 Sep 2015) The case of the lying configuration file
- (31 Aug 2015) The case of the memory eater and high load
- (14 Aug 2015) The case of the man in the middle
- (05 Aug 2015) Reading the errors
- (29 Jul 2015) The evil licensing code
- (23 Jul 2015) The case of the native memory leak
- (16 Jul 2015) The case of the intransigent new database
- (13 Jul 2015) The case of the hung over server
- (09 Jul 2015) The case of the infected cluster
Comments
This kind of problem does occur sometimes - at least this situation allows for parsing by a token to get the final value.
I do wonder if it's possible for the general case to be handled by the database if it kept an index based on the string reverse. Then the "endsWith" could reverse the search term and then use the reverse index to fulfill it. That way it might be possible to support the query performance in cases where you don't have control over the application (though you'd hope any application that used the endsWith approach would have determined a good approach).
At some point though I guess it gets too generalised and you're looking at a free text search approach anyway.
I don't recall .net having a native string reverse though - so it's possible that the index creation / maintenance might be CPU intensive.
Also, there's a risk of various unicode edge cases - and I've no idea how to handle languages that are Left-to-Right via this approach (though I guess they'd work out just fine).
Gavan,
Yes, technically speaking, if you were using an auto index, we could do that, sure. The scenario in this case is that you're using a static index, so that isn't appropriate.
Note that there is
string.Reverse
, which will do the right thingComment preview