-
Notifications
You must be signed in to change notification settings - Fork 230
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
Packet encoding optimization #343
Conversation
Pull Request Test Coverage Report for Build 7294150121
💛 - Coveralls |
I love the idea -how do we feel about the risk of active items in the pool being dropped under high load situations? Is this risk present? Per https://pkg.go.dev/sync#Pool
Presumably as long as a reference is held, it wont be deallocated? |
No active items will be in the pool. In this implementation, a buffer is 1st taken out of the pool (in this state, it has no association to the pool and it's no different than a newly allocated item), if none is available, a new buffer is generated. It will only be put back into the pool when it's no longer needed (and only if it's no longer in use). Any items in the pool can be GCed any time, which will have no negative impact. |
I tried to perform some simple stress testing on a standalone machine, but perhaps my stress test had too few clients and messages. For example, I used 30 clients, with each client sending 2000 messages. Based on the memory and CPU usage on my end, I didn't see a significant difference. @thedevop @mochi-co @dgduncan @x20080406 If any of you have the capability to test performance differences with a large number of clients and messages, please give it a try. Alternatively, we can directly write some packet encoding benchmarks to see the results. Additionally, @thedevop if it's confirmed that a buffer pool is needed, I think we can make it simpler. There's no need for BufferWithCap, for example, just like this: package nsqd
import (
"bytes"
"sync"
)
var bp sync.Pool
func init() {
bp.New = func() interface{} {
return &bytes.Buffer{}
}
}
func bufferPoolGet() *bytes.Buffer {
return bp.Get().(*bytes.Buffer)
}
func bufferPoolPut(b *bytes.Buffer) {
b.Reset()
bp.Put(b)
} |
@werbenhu, I have checked in benchmark, the results are:
Actual performance should be even better, as the bytes.Buffer may grow few times to encode the packet headers. I used to use the simplified method as you have provided, which works perfectly fine if all the items are similar in size. It doesn't work as well if items are of different size. See https://golang.org/issue/23199. Although most of the packets without payload should be similar in size, but with the inclusion of metadata (user properties), it may become quite large. I intentionally for this version avoid the final buffer that includes the payload, we should use a dedicated buffer pool for that, and perhaps let it be user configurable based on their use case. So the mempool is to future proof for that use case. In our perf test environment, where we're publishing ~130K/s (bottlenecked by CPU), preliminary result shows ~3% improvement. |
I benchmarked pk.Properties.Encode using the following: func BenchmarkEncodeNoPool(b *testing.B) {
props := propertiesStruct
for i := 0; i < b.N; i++ {
buf := bytes.NewBuffer([]byte{})
props.Encode(Reserved, Mods{AllowResponseInfo: true}, buf, 0)
_ = buf
}
}
func BenchmarkEncodePoolBuf(b *testing.B) {
props := propertiesStruct
for i := 0; i < b.N; i++ {
buf := mempool.GetBuffer()
props.Encode(Reserved, Mods{AllowResponseInfo: true}, buf, 0)
mempool.PutBuffer(buf)
}
} The results are:
If I also replace the buf within Encode to use buffer pool, the results are:
|
@thedevop perhaps we can learn from encoding/json: fix usage of sync.Pool, where we can see that the standard library's json.Marshal also faces a similar issue to what we are currently encountering. However, at least for now, it appears that the json.Marshal still directly employs the sync.Pool without a cap. Personally, I suggest we can temporarily postpone the use of BufferWithCap. @mochi-co what are your thoughts on this? |
@werbenhu, I'm ok we use the pool without cap, as most of the packet encoding without payload should not vary too much on size. I updated the default pool to not have a cap. |
Sorry my life has been crazy this year. Just catching up on all this good work you've done. The pool benchmarks are promising, and at scale this will make a significant difference. @thedevop your point about differing payload sizes is poignant and well considered. I agree with @werbenhu that we should first attempt to use the pool without a cap to reduce the complexity we're adding here. However, we should monitor the change and add the cap if it does become necessary. As for me, I think this is a great contribution, and it's definitely getting merged. |
As described in #342.