Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid expensive findEntry call in segment metadata query #10892

Merged
merged 9 commits into from
Mar 9, 2021

Conversation

abhishekagarwal87
Copy link
Contributor

@abhishekagarwal87 abhishekagarwal87 commented Feb 16, 2021

This PR fixes two performance issues in the timeline conversion, observed when an interval has a large number of segments. For such an interval,

  • Reading the timeline entries from the original timeline. PartitionHolder.asImmutable is an expensive operation since it involves a deep copy of the chunk. And on top of that, a deep copy is done for every segment in the interval which significantly amplifies the performance overhead O(N*N)
  • Adding the entries to the new timeline. This involves an isComplete call which too is O(N) for a single interval. Thus, writing to a new timeline is O(N*N) as well.

@gianm
Copy link
Contributor

gianm commented Feb 16, 2021

The change looks like a good idea to me. How about:

  • changing all call sites
  • removing findEntry and the PartitionHolder asImmutable method
  • adding a performance test that verifies that CachingClusteredClient is able to do its work in a reasonable amount of time even when there are some crazy number of segments in a time chunk, like 100,000? (That ought to be enough for anybody.) The "reasonable" amount of time might need to be somewhat high to prevent flaky tests from being an issue. Maybe 5–15 seconds. In reality, we'd want this to happen much faster than that, but we don't want to cause flaky tests.

@gianm
Copy link
Contributor

gianm commented Feb 16, 2021

Another thing: I see that asImmutable doesn't just return an immutable copy, it also calls OvershadowableManager.copyVisible to potentially not return all of the chunks. So cutting out the call to asImmutable would also cut out the call to copyVisible. I'm not sure what consequences that might have. Have you analyzed it and determined that it's OK to cut out that call?

@maytasm
Copy link
Contributor

maytasm commented Feb 16, 2021

@abhishekagarwal87 Another idea I had is in the CachingClusteredClient.getQueryRunnerForSegments we can cache the result returned from timeline.findEntry(spec.getInterval(), spec.getVersion()). We can maintains a map while iterating through all the input specs and cache returned PartitionHolder<ServerSelector> for the interval/version pair. This is useful when the Iterable<SegmentDescriptor> specs contains a lot of same interval/version pair. For example, if new there are a lot of segments per interval, the specs can contains pretty much all a single interval/version pair.

@abhishekagarwal87
Copy link
Contributor Author

The change looks like a good idea to me. How about:

  • changing all call sites
  • removing findEntry and the PartitionHolder asImmutable method
  • adding a performance test that verifies that CachingClusteredClient is able to do its work in a reasonable amount of time even when there are some crazy number of segments in a time chunk, like 100,000? (That ought to be enough for anybody.) The "reasonable" amount of time might need to be somewhat high to prevent flaky tests from being an issue. Maybe 5–15 seconds. In reality, we'd want this to happen much faster than that, but we don't want to cause flaky tests.

Yes. I plan to remove all findEntry calls and fix tests accordingly. I will add the perf test as well. Thanks for the tip.

@abhishekagarwal87
Copy link
Contributor Author

Another thing: I see that asImmutable doesn't just return an immutable copy, it also calls OvershadowableManager.copyVisible to potentially not return all of the chunks. So cutting out the call to asImmutable would also cut out the call to copyVisible. I'm not sure what consequences that might have. Have you analyzed it and determined that it's OK to cut out that call?

Good catch. As far as I can tell, cutting down that call should not make a difference. copyVisible retains only visible chunks in knownPartitionChunks. However, in the getChunk call too, we are only returning a chunk if its visible. There shouldn't be a mismatch in behavior after the change.

@abhishekagarwal87
Copy link
Contributor Author

@abhishekagarwal87 Another idea I had is in the CachingClusteredClient.getQueryRunnerForSegments we can cache the result returned from timeline.findEntry(spec.getInterval(), spec.getVersion()). We can maintains a map while iterating through all the input specs and cache returned PartitionHolder<ServerSelector> for the interval/version pair. This is useful when the Iterable<SegmentDescriptor> specs contains a lot of same interval/version pair. For example, if new there are a lot of segments per interval, the specs can contains pretty much all a single interval/version pair.

yeah, I thought about that too. I didn't go ahead with that approach since that would require creating a new temporary state variable. For queries with many intervals, it could be that we end up creating many map entries with deep copies of OvershawdoableManager. what do you think?

@abhishekagarwal87
Copy link
Contributor Author

It would seem this change alone is not enough. Adding a chunk to timeline is also O(N*N) since it calls PartitionHolder.isComplete which is O(N) op.

@abhishekagarwal87 abhishekagarwal87 changed the title [Draft] Avoid expensive findEntry call in segment metadata query Avoid expensive findEntry call in segment metadata query Mar 1, 2021
);

for (int ii = 0; ii < segmentCount; ii++) {
segmentDescriptors.add(new SegmentDescriptor(interval, "1", ii));
Copy link
Contributor

Choose a reason for hiding this comment

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

For the purpose of perf testing, does it matter if the segments have:

  • different interval
  • different versions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The bottlenecks are in PartitionHolder when it has too many objects. If segments have different intervals and versions, the size of PartitionHolder will be much smaller and there is unlikely to be a perf issue.

{

@Test(timeout = 10_000)
public void testGetQueryRunnerForSegments_singleIntervalLargeSegments()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have perf different before and after this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was under a second after the change. Before the change, it was more than a minute though I didn't let it run completely so it may have been very high.

unfilteredIterator,
Objects::nonNull
);
// We add all the entries via batch add to avoid overhead of single add call. The call to add an entry to interval
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the comment can be a little more clear in mentioning that by avoiding to call add n times and calling addAll once reduces O(n)*n to O(n)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed unnecessary details and just put that addAll is much more efficient than add.

{
lock.readLock().lock();
try {
for (Entry<Interval, TreeMap<VersionType, TimelineEntry>> entry : allTimelineEntries.entrySet()) {
if (entry.getKey().equals(interval) || entry.getKey().contains(interval)) {
TimelineEntry foundEntry = entry.getValue().get(version);
if (foundEntry != null) {
return foundEntry.getPartitionHolder().asImmutable();
Copy link
Contributor

Choose a reason for hiding this comment

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

is the asImmutable method still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like its not. will delete this method as well as the class ImmutablePartitionHolder.

Copy link
Contributor

@maytasm maytasm left a comment

Choose a reason for hiding this comment

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

LGTM. Few comments on perf improvement validation / testing

import static org.mockito.ArgumentMatchers.any;

/**
* Performance tests for {@link CachingClusteredClient} can be added here. There is one test for a scenario
Copy link
Contributor

Choose a reason for hiding this comment

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

What performance does this class test? Is it a scalability test? It would be nice to make it clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

any kind of perf test that doesn't require a real cluster.


public static class PartitionChunkEntry<VersionType, ObjectType>
{
private final Interval interval;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add javadoc explaining what this interval is. There are two types of interval in timeline. See LogicalSegment.

PartitionChunk<OvershadowableInteger> actual
)
{
SingleElementPartitionChunk<OvershadowableInteger> expectedSingle = (SingleElementPartitionChunk<OvershadowableInteger>) expected;
Copy link
Contributor

@jihoonson jihoonson Mar 4, 2021

Choose a reason for hiding this comment

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

SingleElementPartitionChunk is used only by deprecated things such as NoneShardSpec and Tranquility. We should deprecate SingleElementPartitionChunk as well and stop using it. You can use NumberedPartitionChunk instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it's unrelated to my change, I will leave this one for now as it is. Though I did mark SingleElementPartitionChunk deprecated.

@suneet-s suneet-s removed the WIP label Mar 4, 2021
@jihoonson
Copy link
Contributor

LGTM

@suneet-s suneet-s merged commit 489f5b1 into apache:master Mar 9, 2021
harinirajendran pushed a commit to confluentinc/druid that referenced this pull request May 3, 2021
* Avoid expensive findEntry call in segment metadata query

* other places

* Remove findEntry

* Fix add cost

* Refactor a bit

* Add performance test

* Add comment

* Review comments

* intellij

(cherry picked from commit 489f5b1)
harinirajendran pushed a commit to confluentinc/druid that referenced this pull request Aug 11, 2021
* Avoid expensive findEntry call in segment metadata query

* other places

* Remove findEntry

* Fix add cost

* Refactor a bit

* Add performance test

* Add comment

* Review comments

* intellij

(cherry picked from commit 489f5b1)
harinirajendran pushed a commit to confluentinc/druid that referenced this pull request Aug 12, 2021
* Avoid expensive findEntry call in segment metadata query

* other places

* Remove findEntry

* Fix add cost

* Refactor a bit

* Add performance test

* Add comment

* Review comments

* intellij

(cherry picked from commit 489f5b1)
@clintropolis clintropolis added this to the 0.22.0 milestone Aug 12, 2021
xvrl pushed a commit to confluentinc/druid that referenced this pull request Aug 12, 2021
* Avoid expensive findEntry call in segment metadata query

* other places

* Remove findEntry

* Fix add cost

* Refactor a bit

* Add performance test

* Add comment

* Review comments

* intellij

(cherry picked from commit 489f5b1)
harinirajendran added a commit to confluentinc/druid that referenced this pull request Oct 1, 2021
Zohimi added a commit to confluentinc/druid that referenced this pull request Oct 1, 2021
Revert "Avoid expensive findEntry call in segment metadata query (apache#10892)"
harinirajendran added a commit to confluentinc/druid that referenced this pull request Oct 4, 2021
harinirajendran added a commit to confluentinc/druid that referenced this pull request Oct 4, 2021
Revert "Revert "Avoid expensive findEntry call in segment metadata query (apache#10892)""
harinirajendran added a commit to confluentinc/druid that referenced this pull request Feb 23, 2022
harinirajendran added a commit to confluentinc/druid that referenced this pull request Feb 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants