Skip to content

Deduplicate RepositoryData on a Best Effort Basis#67947

Merged
original-brownbear merged 5 commits intoelastic:masterfrom
original-brownbear:deduplicate-blob-reads-during-clone
Jan 26, 2021
Merged

Deduplicate RepositoryData on a Best Effort Basis#67947
original-brownbear merged 5 commits intoelastic:masterfrom
original-brownbear:deduplicate-blob-reads-during-clone

Conversation

@original-brownbear
Copy link
Copy Markdown
Contributor

@original-brownbear original-brownbear commented Jan 25, 2021

Enhance transport request deduplicator to allow for more general deduplication.
Make use of that to enable deduplicate RepositoryData under concurrent request load
(which so far has been the only situation where RepositoryData has created unmanagable
memory pressure).

relates #66042 (improves loading snapshot info for many snapshots in parallel as done by Cloud snapshotter for example)

relates #55153 (pushes back at least a little by creating a bottle-neck on the repository data loading step and saves significant memory from reducing the number of RepositoryData instances on heap during concurrent snapshot get requests)

Enhance transport request deduplicator to allow for more general deduplication.
Make use of that to enable deduplicate RepositoryData under concurrent request load
(which so far has been the only situation where RepositoryData has created unmanagable
memory pressure).
@original-brownbear original-brownbear added >non-issue :Distributed/Snapshot/Restore Anything directly related to the `_snapshot/*` APIs v8.0.0 v7.12.0 labels Jan 25, 2021
@elasticmachine elasticmachine added the Team:Distributed Meta label for distributed team. label Jan 25, 2021
@elasticmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-distributed (Team:Distributed)

} catch (Exception e) {
listener.onFailure(e);
}
repoDataDeduplicator.executeOnce(metadata, listener, (metadata, l) -> l.onResponse(cached.repositoryData()));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little less significant in savings relative to the other spot but I think it's worth it.

  1. Deserializing the cached RepositoryData is still expensive and it's nice to save the work.
  2. For status APIs this gives a nice natural rate limit by only fanning out after fetching RepositoryData in case of massive concurrency of requests (e.g. snapshotter fetching all snapshots in a 1k snapshots repo)
  3. For other operations like create, delete, clone we want those to run serially anyway (which we generally do via the master service) so its fine if we just run all the listeners in series here to begin with.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For status APIs this gives a nice natural rate limit by only fanning out after fetching RepositoryData in case of massive concurrency of requests (e.g. snapshotter fetching all snapshots in a 1k snapshots repo)

I fail to see how this change could act as a rate limit for that scenario, it's a good improvement but the requests would accumulate anyway, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the requests would accumulate anyway, right?

Yea, I guess "rate-limiter" was a poor choice of wording. I guess we "rate limit" the rate at which we dispatch the actions after loading repo data to a single thread at a time but requests still pile up. In practice that probably means that things run much quicker than before effectively but with requests queuing and waiting for the first one to finish instead of executing in parallel it's more of a "thread-limiter" I suppose :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, thanks for the clarification!

if (bestEffortConsistency) {
threadPool.generic().execute(ActionRunnable.wrap(listener, this::doGetRepositoryData));
} else {
repoDataDeduplicator.executeOnce(metadata, listener, (metadata, l) ->
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would completely resolve the broken situations we observed where there were 50+ concurrent GENERIC threads fetching RepositoryData and deserializing it over and over, eventually running the system OOM.

* Returns a {@link RepositoryData} to describe the data in the repository, including the snapshots
* and the indices across all snapshots found in the repository. Throws a {@link RepositoryException}
* if there was an error in reading the data.
* @param listener listener that may be resolved on different kinds of threads including transport and cluster state applier threads
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I audited all usages of this method and I couldn't find any remaining spot where this is a problem (at least in master, might need some adjustments in 7.x).

Copy link
Copy Markdown
Contributor

@fcofdez fcofdez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change, I've left some questions and small suggestions. 👍

* @param <T> Request type
*/
public final class TransportRequestDeduplicator<T> {
public final class AbstractResultDeduplicator<T, R> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe we should name it just ResultDeduplicator? as we don't expect to extend this class.
Also, now that we're using it outside of the transport package, maybe it makes sense to move it to another package?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ to both, lets renamed and moved to the .action package :)

} catch (Exception e) {
listener.onFailure(e);
}
repoDataDeduplicator.executeOnce(metadata, listener, (metadata, l) -> l.onResponse(cached.repositoryData()));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For status APIs this gives a nice natural rate limit by only fanning out after fetching RepositoryData in case of massive concurrency of requests (e.g. snapshotter fetching all snapshots in a 1k snapshots repo)

I fail to see how this change could act as a rate limit for that scenario, it's a good improvement but the requests would accumulate anyway, right?

@original-brownbear
Copy link
Copy Markdown
Contributor Author

Thanks Francisco all addressed I think :)

Copy link
Copy Markdown
Contributor

@fcofdez fcofdez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks Armin!

@original-brownbear
Copy link
Copy Markdown
Contributor Author

Thanks Francisco!

@original-brownbear original-brownbear merged commit 29d1d25 into elastic:master Jan 26, 2021
@original-brownbear original-brownbear deleted the deduplicate-blob-reads-during-clone branch January 26, 2021 18:04
original-brownbear added a commit that referenced this pull request Jan 26, 2021
Enhance transport request deduplicator to allow for more general deduplication.
Make use of that to enable deduplicate RepositoryData under concurrent request load
(which so far has been the only situation where RepositoryData has created unmanageable
memory pressure).
original-brownbear added a commit that referenced this pull request Jan 28, 2021
)

There is a small chance here that #67947 would cause the callback
for the repository data to run on a transport or CS updater thread
and do a lot of IO to fetch `SnapshotInfo`.

Fixed by always forking to the generic pool for the callback.
Added test that triggers lots of deserializing repository data from
cache on the transport thread concurrently which triggers this bug
relatively reliable (more than half the runs) but is still reasonably
fast (under 5s).
original-brownbear added a commit that referenced this pull request Jan 28, 2021
) (#68092)

There is a small chance here that #67947 would cause the callback
for the repository data to run on a transport or CS updater thread
and do a lot of IO to fetch `SnapshotInfo`.

Fixed by always forking to the generic pool for the callback.
Added test that triggers lots of deserializing repository data from
cache on the transport thread concurrently which triggers this bug
relatively reliable (more than half the runs) but is still reasonably
fast (under 5s).
@original-brownbear original-brownbear restored the deduplicate-blob-reads-during-clone branch April 18, 2023 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Distributed/Snapshot/Restore Anything directly related to the `_snapshot/*` APIs >non-issue Team:Distributed Meta label for distributed team. v7.12.0 v8.0.0-alpha1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants