feat: faster replication#1166
Conversation
|
I am still working on the design for it, just a peak if the direction seems well @levkk |
|
Cool! Would be good to see some benchmark numbers, just to get a sense of how much faster this is. |
|
Do you have any such setup so that i can benchmark it? |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This might work: Specifically, see |
|
RUNS = 2
@levkk results seems good but also too good at a point |
|
Async should be a big improvement, so not that surprising. This is very cool. Let me know when you're ready for a review, I'll tag Kiryl. |
|
@levkk ready for review |
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test { |
There was a problem hiding this comment.
Do you think we should add more unit tests here? I think we should have some for testing the whole flow - prepare -> execute -> await. And also some tests for possible errors
There was a problem hiding this comment.
We still miss some important tests:
- error path - something that will trigger an error response, validate that we get the error properly and we won't block the execution for future calls
- missed rows calculation - just some calls to generate missed rows and make sure we get proper stats for it
|
@meskill i have added a few more could you take a look? |
|
Thanks @ygxio I'm still checking the code more deeply, but anyway looking great so far. Putting my bench results to confirm the improvements: Against mainWith toxi and latency for source/destination for 1ms: SeparatelyAnd the next without comparison with main (compare against prev run) since since it is executing too long to get the result. |
|
i see from 1 to 10 to 100 ms of latency the jump has been pretty high, need to see the cause maybe |
| // One entry per outstanding DML op, in send order: `is_direct`. Popped on | ||
| // each CommandComplete. | ||
| direct: VecDeque<bool>, | ||
| // In-transaction prepare sync points: (remaining ParseComplete acks, waiter). | ||
| parse_ack_sync_points: VecDeque<(usize, oneshot::Sender<()>)>, | ||
| // Commit / out-of-transaction prepare sync points, resolved on ReadyForQuery. | ||
| ready_for_query_sync_points: VecDeque<oneshot::Sender<()>>, | ||
| // Waiter that resolves once every DML ack has been read. At most one is | ||
| // outstanding: `commit()` issues drain_acks sequentially, one per connection. | ||
| drain_waiter: Option<oneshot::Sender<()>>, |
There was a problem hiding this comment.
The multiple separate VecDeque like this works, but I see this more error-prone and harder to maintain. The issue is this multiple queues partially maps to the underlaying FIFO queue executed by the backend. Due to current design of the replication it works just fine, but in case we add something new to a processing or try to extend async handling that's could become a break point.
I'd suggest to use a single queue of enums to control the state of the processing and the expected responses, to make sure we get everything we expect and we don't process entries in the wrong order for some reason.
I think the ProtocolState from pgdog/src/backend/protocol/state.rs does use the single queue for something like this.
There was a problem hiding this comment.
I have pushed changes for it
| impl Listener { | ||
| async fn run(mut self) { | ||
| loop { | ||
| select! { |
There was a problem hiding this comment.
I can't prove it but I believe here could be a theoretical issue due to disproportion of read/writes events - the usual execute command from recv expected to generate 2 server.read events. Since the select! is fair by default the buffer for server.read could grow fast that could lead to tcp stream blocking.
I'm thinking if we need to add biased to select! and move the read first, so we'll read the messages first if any to avoid this.
There was a problem hiding this comment.
Yes, this can be problem i am trying to come up with some way to prove this hypothesis
There was a problem hiding this comment.
I'd say don't spend much time on it, we can just put biased to prioritize the read and for the write we have more explicit backpressure control thanks to channels
There was a problem hiding this comment.
i was able to get some result, so i shrinked my RCV and SND socket buffer and sent execute statements select! kept sending commands which made the pg -> pgdog receive buffer full hence on next send commands the system entered a deadlock since pgdog -> pg buffer is full and pg -> pgdog buffer is also full and there is no way pgdog can read the receive buffer, so read bais is definately important
| // latched on any connection means whole transaction is aborted. | ||
| for conn in &self.connections { | ||
| if let Some(err) = conn.take_error() { | ||
| return Err(err); |
There was a problem hiding this comment.
could you please check how does effect on one connection affect other connections and streams - are they cancelled in any way?
I think they stop after dropping the channel, but I want to be sure we are not doing useless work here.
There was a problem hiding this comment.
right, it is a redundant task as i on Error i am already dropping the Sender channels hence should already work
| // Rows a direct-to-shard DML expected to touch but didn't (0 rows affected). | ||
| missed: MissedRows, | ||
| // Test-only: total ParseComplete acks consumed by parse-ack sync points. | ||
| #[cfg(test)] |
There was a problem hiding this comment.
can we try to avoid this is the main code?
let's better test that everything works as expected after some commands than to rely on some implementation details. If we have prepare statements - we can validate that this statement work after parsing and that listener doesn't block for example.
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test { |
There was a problem hiding this comment.
We still miss some important tests:
- error path - something that will trigger an error response, validate that we get the error properly and we won't block the execution for future calls
- missed rows calculation - just some calls to generate missed rows and make sure we get proper stats for it
| // If in transaction we send flush and wait for the acknowledgements | ||
| // since we donot want to commit the open transaction. | ||
| messages.push(Flush.into()); | ||
| SyncPointKind::ParseAcks(parses.len()) |
There was a problem hiding this comment.
let's add some validation that this should not be 0, since we may overflow on this later
There was a problem hiding this comment.
handled this at prepare itself, to not allow empty parses to go for sync points
| c => return Err(Error::RelationOutOfSync(c)), | ||
| } | ||
| } | ||
| server.prepare(parses, in_txn).await?; |
There was a problem hiding this comment.
also can speed up this a very little if we use try_join to run prepare in parallel
8cf5e8b to
0c5844d
Compare











fixes #1036