Skip to content

Conversation

@stevenzwu
Copy link
Contributor

to support sketch statistics and auto migration from Map stats to reservoir sampling sketch if cardinality is detected high

@stevenzwu stevenzwu force-pushed the refactor-stats branch 2 times, most recently from b97bf41 to 0882cb8 Compare May 14, 2024 00:08
@stevenzwu
Copy link
Contributor Author

stevenzwu commented May 14, 2024

Moved DataStatistics away from generic and use a type to distinguish btw Map and Sketch statistics. A couple of reasons.

  • generics getting a bit too complicated.
  • support auto migration/promotion of Map stats to Sketch if the cardinality is detected to be high. Default statistics type should be Auto. but if auto didn't work well in some cases, users can set the type to Map or Sketch explicitly.

Will add the sketch range partitioner in a separate PR following this one.

@stevenzwu stevenzwu requested a review from pvary May 14, 2024 00:28
this.dataStatistics = statisticsSerializer.createInstance();
}
private final StatisticsType type;
private final Map<SortKey, Long> keyFrequency;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

combine both Map and Sketch stats in the same aggregated statistics object would allow run-time switch from Map stats to Sketch.

if (record.type() == StatisticsType.Map) {
keyFrequencySerializer.serialize(record.keyFrequency(), target);
} else {
rangeBoundsSerializer.serialize(Arrays.asList(record.rangeBounds()), target);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reused list serializer from Flink. paying a small penalty for array to list conversion for that.

}

@SuppressWarnings("unchecked")
private void merge(DataStatistics taskStatistics) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this method shows the stats type migration from Map to Sketch


if (localStatistics.type() == StatisticsType.Map) {
Map<SortKey, Long> mapStatistics = (Map<SortKey, Long>) localStatistics.result();
if (statisticsType == StatisticsType.Auto
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is stats migration (Map -> Sketch) at operator side during collection phase.

Copy link
Contributor

Choose a reason for hiding this comment

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

With AUTO, if any task, or coordinator decides that we move to sketch then it might be a good idea for everyone to move to sketch to save memory, and transformations.
Do we want to have an extra message in this case, or at least switch when a global stat comes where we already switched to stat?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question

  • each operator makes independent decision on switching from Map to Sketch during local collection phase.
  • when operators received the global statistics from coordinator, operators should also check if type switch is needed. but looks like I missed this logic. will add.

import org.apache.iceberg.relocated.com.google.common.collect.Maps;

@Internal
class DataStatisticsSerializer extends TypeSerializer<DataStatistics> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this single serializer can handle both map and sketch stats type

exclude group: 'org.slf4j'
}

implementation libs.datasketches
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 jar file is about 1MB. so not too big to be included

Comment on lines 230 to 232
SketchUtil.convertMapToSketch(taskMapStats, taskSketch::update);
coordinatorSketchStatistics.update(taskSketch);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering which is better:

  1. Getting a map from task -> converting task map to sketch -> merging the coordinator and the map sketch
  2. Updating the coordinator sketch, by adding the values from the map directly

Which one is performing better? Which results in better approximation in the resulting sketch?

If we consciously use the 1st solution, then we probably want to send a message to the tasks when we switch to sketch to not bother sending the whole map, but just the sketch (it might be a smaller message)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, once coordinator switched to sketch, all operators will switch too upon the receiving of the global statistics

this.checkpointId = checkpointId;
this.type = type;
this.keyFrequency = keyFrequency;
this.rangeBounds = rangeBounds;
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 want to add a check at here to make sure keyFrequency and rangeBounds won't have value at the same time

private final int parallelism;
private final TypeSerializer<DataStatistics> statisticsSerializer;
private final int downstreamParallelism;
private final StatisticsType statisticsType;
Copy link
Contributor

Choose a reason for hiding this comment

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

the field is not being used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch. the variable is needed but missed as you pointed out in the other comment below on coordinator migration from Map to Sketch

Map<SortKey, Long> taskMapStats = (Map<SortKey, Long>) taskStatistics.result();
if (coordinatorStatisticsType == StatisticsType.Map) {
taskMapStats.forEach((key, count) -> coordinatorMapStatistics.merge(key, count, Long::sum));
if (coordinatorMapStatistics.size() > switchToSketchThreshold) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So for coordinator, unlike operator which needs to check if StatisticsType = Auto, we will convert it from map to sketch once the size reaches the threshold?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch. it is related to your other comment that statisticsType was not used. it should be used and checked here.

StatisticsEvent statisticsEvent =
StatisticsEvent.createAggregatedStatisticsEvent(
checkpointId, globalStatistics, aggregatedStatisticsSerializer);
for (int i = 0; i < context.currentParallelism(); ++i) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a function #parallelism at line 187 to get the current parallelism. Do we want to remove the function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch. let me remove the parallelism() method as it is too trivial to be kept

globalStatistics = statisticsSerializer.createInstance();
}

this.taskStatisticsType = StatisticsUtil.collectType(statisticsType, globalStatistics);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does Iceberg repo follows the type that, we always use this. to refer to the class variable? If that's the case, then let's update globalStatistics to this.globalStatistics like what we do in line 113

Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment for line 124 and 125

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Iceberg style only uses this. if trying to modify the value/state (like setter or constructor)

}

/**
* To understand how range bounds are used in range partitioning, heere is an example for human
Copy link
Contributor

Choose a reason for hiding this comment

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

typo heere to here

* <li>Target size is "coordinator reservoir size * over sampling ration (10) / operator
* parallelism"
* <li>Min is 1K to achieve good accuracy while memory footprint is still relatively small
* <li>Max is 100K to cap the memory footprint on coordinator
Copy link
Contributor

Choose a reason for hiding this comment

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

From the current implementation, operator reservoir size depends on coordinator reservoir size completely. Do we check the operator reservoir min max value?

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 feel not needed. coordinator reservoir size is already correlated with parallelism/partitions. operator reservoir size probably can purely tie to OPERATOR_OVER_SAMPLE_RATIO.

taskMapStats.forEach(
(sortKey, count) -> {
for (int i = 0; i < count; ++i) {
sketchConsumer.accept(sortKey);
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 consider to execute the sketchConsumer.accept in parallel to make it faster

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thought about it. but it would require a thread pool. let's start simple. if this turns out to be an issue later, we can improve it then.

private final Comparator<StructLike> comparator;
private final NavigableMap<Long, Aggregation> aggregationsPerCheckpoint;

private volatile AggregatedStatistics completedStatistics;
Copy link
Contributor

Choose a reason for hiding this comment

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

How is the thread model work for the event handling? Do we need the volatile here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good question. we don't really need volatile here as coordinator event handling is always single thread. let me remove the volatile.

this.coordinatorExecutor = Executors.newSingleThreadExecutor(coordinatorThreadFactory);

Copy link
Contributor

@pvary pvary left a comment

Choose a reason for hiding this comment

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

Just one small question from my side

@stevenzwu stevenzwu merged commit cbe391d into apache:main Jun 5, 2024
@stevenzwu stevenzwu deleted the refactor-stats branch June 5, 2024 17:01
@stevenzwu
Copy link
Contributor Author

thanks @pvary and @yegangy0718 for the code review

stevenzwu added a commit to stevenzwu/iceberg that referenced this pull request Jul 23, 2024
stevenzwu added a commit that referenced this pull request Jul 26, 2024
jasonf20 pushed a commit to jasonf20/iceberg that referenced this pull request Aug 4, 2024
refactor sink shuffling statistics collection to support sketch statistics and auto migration from Map stats to reservoir sampling sketch if cardinality is detected high
sasankpagolu pushed a commit to sasankpagolu/iceberg that referenced this pull request Oct 27, 2024
refactor sink shuffling statistics collection to support sketch statistics and auto migration from Map stats to reservoir sampling sketch if cardinality is detected high
zachdisc pushed a commit to zachdisc/iceberg that referenced this pull request Dec 23, 2024
refactor sink shuffling statistics collection to support sketch statistics and auto migration from Map stats to reservoir sampling sketch if cardinality is detected high
zachdisc pushed a commit to zachdisc/iceberg that referenced this pull request Dec 23, 2024
czy006 pushed a commit to czy006/iceberg that referenced this pull request Apr 2, 2025
czy006 pushed a commit to czy006/iceberg that referenced this pull request Apr 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants