CASSGO-100 Support frames batching into segment#1946
Conversation
7fb089d to
6d41de3
Compare
Previously, the driver encoded each individual frame in a single segment despite segments ability to hold multiple frames. This patch introduces a mechanism that allows driver collect multiple frames before encoding them as a batch. To achieve this, segmentWriter was introduced. Patch by Bohdan Siryk; reviewed by TBD for CASSGO-100
|
I ended up with a duplicate of the current Also, I introduced Also, I added a |
joao-r-reis
left a comment
There was a problem hiding this comment.
Looking good overall but left a few comments. It would be nice to run benchmarks to compare the performance vs trunk before merging this.
I have some code I used to do a basic comparison between gocql v1 and v2 maybe it can be useful to you: https://github.com/joao-r-reis/gocql-benchmarks/tree/testv3
| // Flushes the current segment and writes the results to the result listeners. | ||
| // Should be called before resetting the segment writer. | ||
| func (sw *segmentWriter) flushCurrentSegment() { | ||
| framesBuf := make([]byte, 0, sw.totalFramesLength) |
There was a problem hiding this comment.
Can we not just reuse a single instance of bytes.Buffer? If I'm not mistaken this slice is only needed until encodeAndWrite below
There was a problem hiding this comment.
You mean make part of the segment writer? I'm concerned about having a big backed slice for each conn. It's around 128 kb
There was a problem hiding this comment.
It will potentially grow up to that size but if you're that concerned about it you can just resize it after a write forces the buffer to grow that large.
Since v4 code doesn't reuse a buffer either let's keep it this way and let's create a low priority ticket to prototype and analyze the performance impact of making both write coalescers reuse a buffer.
| // var verr net.Error | ||
| // if errors.As(err, &verr) { | ||
| // return nil, false, verr | ||
| // } |
There was a problem hiding this comment.
This TODO needs to be addressed. Personally I think it's fine to close the connection when the driver fails to encode/decode something here because it's either an error on the decode/encode code and most likely a driver bug or it's a connection error. A connection error should lead to connection closure and the encode/decode error should never happen so I don't see a lot of benefit in trying to handle it.
There was a problem hiding this comment.
I'm thinking about timeouts, actually. Currently, the driver ignores the timeout when trying to read the frame header from the conn. With this patch, the driver will also ignore reading both the segment header and body, which differs from the previous driver behavior.
I'm unsure if this is good
There was a problem hiding this comment.
Are you sure it ignores the timeout? Doesn't the Read loop on connReader return the error immediately instead of retrying when it's a read timeout?
There was a problem hiding this comment.
cassandra-gocql-driver/conn.go
Lines 904 to 925 in c65c762
In case of net.Error it do not break if I'm not missing anything.
https://pkg.go.dev/net#Error tells us whether it is timeout or not. Actually, probably worth updating the code to rely on net.Error.Timeout() intstead of Temporary() as suggested
There was a problem hiding this comment.
Yeah I misread it you're right. I think this will sort itself out when we implement the timeout fix which should go in (and get released) before this PR since this is not a bug fix. After the timeout fix is merged then the timeout will not even exist here.
There was a problem hiding this comment.
Yeah, if we get rid of the read socket timeouts it won't be a problem at all
|
Thanks for the review. I'm going to address it in a couple of days |
|
I have run a couple of benchmarks both with and without compression for proto 5. I'm genially surprised how significant improvement for compression path. Base is trunk. Candidate is changes in this PR.
INSERT (base vs candidate, %)
SELECT (base vs candidate, %)
INSERT (compression) (base vs candidate, %)
SELECT (compression) (base vs candidate, %)
|
|
Added also v4 benchmarks: Non-compressionINSERT
SELECT
CompressionINSERT
SELECT
Memory: Non-compression (proto v4 trunk vs proto v5 trunk vs proto v5 cassgo100)
Compression (proto v4 trunk vs proto v5 trunk vs proto v5 cassgo100)
|
|
Re-run benchmarks one more time vs columns: percent change for cassgo-100 proto5 vs each v2.1.1 baseline. Latency: positive = slower, negative = faster. Memory: positive = higher usage, negative = lower usage. No compression (LZ4 off)LatencyINSERT
SELECT
Memory (Go heap)Heap allocation
With compression (LZ4 on)LatencyINSERT
SELECT
Memory (Go heap)Heap allocation
|
|
Yeah I remember running some benchmarks to compare v2 with v1 and being a bit suprised that compression was so slow but I'm still surprised to see performance gains of that order now. Do you have any idea why there seems to be a hit on v4 non compression scenarios? |
I'm suspecting vectored IO in What actually changed is that the driver now does not compress each frame, but instead each segment, and each segment could have multiple frames |
|
I added memory usage for comparison to the last benchmark report |
Prevent write hang on segmentWriter.writeContext call when writer is closed. Prevent stale timer tick when current segment is about to be flushed because new request doesn't fit current segment
|
is this still WIP or is it ready? |
|
I did a lot of testing of different approaches to reduce performance impact on the uncompressed path. Right now, the best thing is to change the segmentCodec.encode API to take [][]bytes instead of a temporarily allocated buffer that holds all frames concatenated -1 allocations. But I didn't test it yet through the benchmark as I had done before. I'm going to bench it, and if the impact is good enough, I'll update the PR |
|
Alright, I ended up adding reusable buffers to segmentWriter and segmentCodec. What changed:
Benchmark results are here: https://docs.google.com/spreadsheets/d/1XKU0Fkw5tPQjhZNtBG86s5n0UumZ1qJUdoViThzzdQ8/edit?usp=sharing Results are slightly better compared to the previous diff:
|
|
|
||
| // decodePayload reads and verifies the payload of a segment from the given reader. | ||
| func (sc *segmentCodec) decodePayload(r io.Reader, header *segmentHeader) ([]byte, error) { | ||
| payload := make([]byte, header.payloadLength) |
There was a problem hiding this comment.
we could improve even further by reusing a buffer here too but I'm ok with merging what we already have since it's already a very good improvement
There was a problem hiding this comment.
Those reused buffers actually makes the solution more complicated to me imo, so we probably should think about introducing something like buffer pools at some point
| if c.version >= protoVersion5 { | ||
| c.logger.Debug("Switching to segments for connection", NewLogFieldStringer("write_timeout", c.session.cfg.WriteTimeout), NewLogFieldStringer("write_coalesce_wait_time", c.session.cfg.WriteCoalesceWaitTime)) | ||
| // Use segments writer which basically batches multiple frames into a single segment before flushing them to the connection. | ||
| segmentWriter := newSegmentWriter(host.Conn, c.writeTimeout, c.session.cfg.WriteCoalesceWaitTime, c.ctx.Done(), c.compressor) |
There was a problem hiding this comment.
I believe the proto v4 path checks if writecoalescetime is > 0 before running the coalescer so we should probably guard against this here too
There was a problem hiding this comment.
I actually can't see a value of having such feature as it goes against the batching nature of v5 frame formats. However, I'm unsure if it should be tied to the generic WriteCoalesceWaitTime option, probably it's better to introduce a separete SegmentsWriteTimeout
TBD