[improve][broker] optimize the problem of subscription snapshot cache not hitting - #24300
[improve][broker] optimize the problem of subscription snapshot cache not hitting#24300liudezhi2098 wants to merge 5 commits into
Conversation
@liudezhi2098 Regarding this scenario, is there a way to find out this happens? |
lhotari
left a comment
There was a problem hiding this comment.
Great work @liudezhi2098! Added a comment about adding code comments. :)
There was a problem hiding this comment.
Since the publishTime information comes from Pulsar client, the logic could be brittle. In Pulsar, there's also an optional brokerPublishTime which is not enabled by default (requires specific configuration in all brokers).
Have you considered what could happen when publishTime values aren't in sync?
I've understood that the "PIP-33: Replicated subscriptions" algorithm rely on vector clocks so that clock sync doesn't become a problem. (earlier discussion)
The snapshots are the way how the vector clocks are synchronized, at least that's how I interpret it from one view point.
The changes in this PR don't currently make sense to me, mainly due to the use of publishTime.
I'd assume that in your problem scenario, the correct approach would be to tune replicatedSubscriptionsSnapshotFrequencyMillis, replicatedSubscriptionsSnapshotTimeoutSeconds and replicatedSubscriptionsSnapshotMaxCachedPerSubscription values.
Have you already done this?
Currently it is a problem that it's necessary to tune the values to fix issues and it's also hard to notice that the problem is occurring.
It looks like future improvements are needed too.
@liangyepianzhou Do you have a separate repro app where this could be observed with real brokers (let's say 2 Pulsar broker within docker-compose + some test app)? Creating a separate Git repo for such a repro would be one approach to share it. Having a runnable repro makes things easier for reviewers too. |
@liudezhi2098 One thought here is that perhaps there could be interaction between |
@lhotari The generation of snapshots is completed through the exchange of snapshotRequest and snapshotResponse between two clusters. Ultimately, the ReplicatedSubscriptionsController writes Marker messages, and using publishTime is reliable because this behavior occurs within the same broker. Of course, the topic may be transferred to another broker, but this is a low-frequency scenario, and its publishTime will not exhibit continuous jumps. However, we can adopt a simpler approach that doesn't require using publishTime. Instead, we can record the current system time each time the snapshotCache,but there is a flaw that it cannot truly reflect the time difference between two messages. In some scenarios, it will cause the cache update frequency to decrease. |
There was a problem hiding this comment.
The generation of snapshots is completed through the exchange of snapshotRequest and snapshotResponse between two clusters. Ultimately, the ReplicatedSubscriptionsController writes Marker messages, and using publishTime is reliable because this behavior occurs within the same broker.
Of course, the topic may be transferred to another broker, but this is a low-frequency scenario, and its publishTime will not exhibit continuous jumps.
However, we can adopt a simpler approach that doesn't require using publishTime. Instead, we can record the current system time each time the snapshotCache is updated and use this timestamp for dynamic adjustments.
Thanks for explaining that. I missed that point that the marker messages are originated from the same broker. publishTime would be fine due to that detail.
When looking at the current master branch code in ReplicatedSubscriptionSnapshotCache.addNewSnapshot, I'd assume that a potential solution to the problem could be that the current mark delete position is taken into account before purging entries.
It looks like the problem arrises when there's isn't at least one entry that is older than the current mark delete position in the cache.
I'd suggest to revisit the purging logic in this way:
- modify
addNewSnapshotand add a 2nd parameter which is the current mark delete position - always keep the newest entry that is before the current mark delete position when purging entries, all older entries can be purged
- if the cache remains full after after doing this, remove a single entry in the cache so that the new entry could be added.
- keep the position of the last removed entry so that it's possible to continue the purging algorithm in subsequent calls
- if there's no previous last removed entry, purge the 2nd entry (assuming that the first entry is the newest entry before current mark delete position)
- if there's a previous last removed entry, continue purging from next entry after the last removed position by first skipping one entry and then removing the 2nd entry
- if there's no more entries to remove, start removing from the beginning.
This purging logic should always result in making it possible to add a new entry. Since every 2nd entry is removed, it will result in "sampling" so that when the mark delete position finally advances, it advances to the most recent position.
There's a possibility to increase replicatedSubscriptionsSnapshotMaxCachedPerSubscription parameter to improve the resolution, if that's desirable.
Perhaps the intention of your timestamp based approach is already to achieve something similar?
@lhotari The intention of timestamp based is to achieve this purpose,the key is when cache is full, how to update the cache, I recommend using Median-based eviction for simplicity, try to make the cache an arithmetic progression in time, because for the shared mode subscription, there will be individual unconfirmed messages, presenting a very jumpy situation. |
You are right about this. The added comments in the code make it easier to understand the intention of the logic. My previous comment about taking the mark deletion position into account in adding snapshots didn't make much sense after rethinking. |
| // The time window length of each time slot, used for dynamic adjustment in the snapshot cache. | ||
| // The larger the time slot, the slower the update. | ||
| final long timeWindowPerSlot = timeSinceFirstSnapshot / snapshotFrequencyMillis / maxSnapshotToCache; |
There was a problem hiding this comment.
this is a bit hard to grasp, what the "time slot" concept is here.
Let's say, timeSinceFirstSnapshot is 25000 ms, snapshotFrequencyMillis is 1000ms and maxSnapshotToCache is 10, it would result in 2. What's the point of this?
With low values, this would be close to 0, I guess. This is also why I think this is just unnecessary complexity.
There was a problem hiding this comment.
What if timeSinceFirstSnapshot is 25 minutes? The goal is that if timeSinceFirstSnapshot becomes longer, the update frequency should be lower.
There was a problem hiding this comment.
What if timeSinceFirstSnapshot is 25 minutes? The goal is that if timeSinceFirstSnapshot becomes longer, the update frequency should be lower.
This doesn't seem to be a realistic case. I disagree that the update frequency should become lower. That's exactly my point that if there's a long delay, it will be enforced going forward. I think it's easier to make progress in this PR by removing this rule and adding it later if there's a specific reason to do so.
I don't see how the median based eviction could make sense. After the cache is filled up, when the median entry is removed and a new entry is added, and this repeats, the result will be that only entries after the median entry will be evicted (assuming no other events happen in between). Eventually there will be a large gap between the 2 entries in the middle. Since time is already considered in the algorithm, it seems that an alternative approach would be to evict the entry with the shortest time distance to it's adjacent entries. Makes sense? |
@liudezhi2098 Just wondering if you are fine with the provided feedback on this PR? It would be great to address this issue in replicated subscriptions and get this PR to completion. |
|
@liudezhi2098 Are you planning to continue working on this? I think that this is a really great improvement to address a long time issue with replicated subscriptions. |
|
@liudezhi2098 There's also a long-standing issue #10054 which is addressed by #16651. I have updated PR 16651 description, rebased it and revisited it slightly. Please review |
yes , I will continue working on this |
|
@liudezhi2098 Any updates on this PR? |
|
@liudezhi2098 Any updates on this PR? |
|
It seems that the best way to store ReplicatedSubscriptionsSnapshots is in something like an increasing backoff queue: It also seems that the solution could be simplified around |
@Ksnz That could be useful if time would be the only contributing factor. One detail that has been missed before (in the comments of this PR) is the number of entries between 2 different snapshots. Optimally, the cache would keep snapshots with equal number of entries between the snapshots. When there's a burst of messages to the topic, there could be a lot of messages between a single snapshot that happens once per second. |
There may be several strategies to address this. And the cache-eviction policy should probably be aware of the namespace, topic, or even individual subscription. The main problem in my current project is that To mitigate this, we want to keep at least the oldest snapshot to ensure that we do not lose any The following strategies come to mind:
Policies 5 or 6 fit our needs for delayed-topics. But any distance-based strategies require a lot changes to be implemented. For me the simple way is to implement 3 and 5 strategies. And an option to pick strategy per namespace. |
@Ksnz Yes, this seems like a problem case where the issue is caused by snapshot cache overflowing.
There's existing org.apache.bookkeeper.mledger.ManagedLedger#getNumberOfEntries(com.google.common.collect.Range<org.apache.bookkeeper.mledger.Position>) available for the calculation. The reason why I think it's more useful than time based strategies is that the rate of consuming usually remains about the same while producing can have high bursts.
"evict from the middle" sounds great, but it's not practical due to the variable producing speed. It could work for topics where producing speed remains constant.
Having too many options to pick from is not a great idea. It should be possible to solve this snapshot caching issue in a generic way that works for all cases sufficiently. Based on the previous experiences, I believe that it should be based on distances between positions and the cache should optimally keep entries with equal distances between the entries. btw. I've previously created a mermaidjs diagram to show the main interactions in replicated subscriptions: https://gist.github.com/lhotari/96fda511a70d7de93744d868b4472b92 |
|
We could introduce an additional option similar to However, I still don’t see how this would help when An additional option to solution would be to store the This is the generic approach to keep both slow/delayed message consumers and fast backlog-tip consumers subscriptions aligned across clusters and to minimise inconsistency lag. |
Yes, a position interval based approach could be useful in ensuring that there would be more frequent snapshots when a lot of messages have been produced. One detail is that the snapshotting isn't deterministic so it wouldn't result in equal intervals between the snapshots.
The cache would keep snapshots with equal position intervals. I don't see why it wouldn't help.
This shouldn't be needed since the amount of memory that the snapshots consume can be optimized. It should be possible to store millions of snapshots with fairly low heap memory usage after optimizing the data structures. |
|
Okay, let’s go back to the square one. We have this description:
In my opinion, a one-second window is an acceptable limitation, even if a burst of messages occurs during that second it is still one second. The problem is that we evict old snapshots from the head of the queue (the oldest positions), so slow consumers can no longer move their Perhaps if we added an option to make the cache act as a buffer (skipping all new entries once it’s full) it would help slow subscriptions stay in sync. Moreover, it would be generic approach After a successful advanced The queue that is aligned to a A remaining issue is that a slow consumer can suddenly advance its But we can combine the best of both worlds. By adding the It's not a perfect solution. But it's less evil than rewriting everything and still better than the current one. |
That's true, but the end result isn't that the distance between 2 different snapshots would be with 1 second difference in rate since it's not deterministic due to the request-response required for creating the snapshot.
I agree that it doesn't make sense to evict the head of the queue.
The problem with that approach is that there would later be problems in updating the mark delete position since there would be a long duration where there wouldn't be snapshots at all. That's the reason why I think that it makes more sense to have an eviction algorithm that keeps snapshots with similar distances between the first entry and the last entry in the queue. It's not very hard to implement such an eviction algorithm. Together with a much larger maximum cache size (or even dynamic max size), it's possible to address the problem that has been caused by the snapshot cache overflowing.
Yes, it will eventually catch up as long as the head of the queue isn't deleted. However, there's a potential to fix the complete problem of having a long lag in the update when the eviction is improved and there are a lot more snapshots available.
yes
yes. that's what could be addressed with the improved eviction algorithm that would keep snapshots with about equal distances from the head to the tail.
true |
|
@Ksnz I have created draft PR #25044 based on my previous comments. It's easier to continue the discussion on that PR when there's concrete code. I haven't yet done any performance optimization and it's not great yet. Adding 1 million snapshots in the unit test takes about 18 seconds on Mac M3, so there's a need to optimize it. I'll take a closer look later by adding a JMH benchmark. |
|
Replaced by #25044 |
Motivation
receiverQueueSize=1000), the cursor never synchronizesreceiverQueueSize=1), synchronization works properlyhttps://github.com/apache/pulsar/blob/7be22eb2b23057bd5e09c361a43d6ccdcc0c8afd/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/ReplicatedSubscriptionSnapshotCache.java#L44C4-L59C6
Modifications
Verifying this change
(Please pick either of the following options)
This change is a trivial rework / code cleanup without any test coverage.
(or)
This change is already covered by existing tests, such as (please describe tests).
(or)
This change added tests and can be verified as follows:
(example:)
Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes
Documentation
docdoc-requireddoc-not-neededdoc-completeMatching PR in forked repository
PR in forked repository: