-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
request body sent in separate SSL segment (bug at 9.6.0) #27573
Comments
I can confirm this happens as described, with TLS1.2 or 1.3, on master/12.x, but it's not clear to me that there is any negative effect to it. I wiresharked the exchange, and it looks like both TLS records are sent in a single TCP segment, so that isn't a problem. Since it is odd, I'll try to see why it does this when I have time (the first few layers of functions called by .end() don't behave differently for String vs Buffer). Why is this a concern to you? |
Oh, you're right, I genuinely thought they were separate TCP segments 😅 In that case it probably won't make a difference (unless you're making like, a ton of requests). For debugging it'd still be nice to have full control on the SSL segments, though. It seems the distinction between string and buffer is being done here: Lines 217 to 238 in 8c4bd2a
So, replacing lines 222-237 with: var header = Buffer.from(this._header, 'latin1');
data = Buffer.concat([header, data]); Would make the behaviour symmetrical among strings and buffers, I think. Would you accept a PR? |
We do accept PRs, of course, but changes in core can be tricky. Concating the buffers seems like it would add an unnecessary memory copy/buffer concatenation into a hot path, the current code looks pretty deliberate. I'm more surprised that using a string causes concatenation, than that a data buffer is a different write from the header values (which are probably strings). |
I think this can be improved, most sockets have a
if (conn._writeGeneric) {
const chunks = [/* ... */]; // derive from this.outputData
chunks.allBuffers = false; // if it's a mix of strings and buffers
// explicit check needed because it returns undefined on success
return false !== conn._writeGeneric(true, chunks, '', callback);
}
// ...
return conn.write(data, encoding, callback); Chunks have this format: const chunk = {
chunk: /* buffer or string */,
encoding: /* string, set to '' for buffer */,
}; You could go even further by calling Honestly, I'd love if someone goes over the net, http and streams code with a fine-tooth comb. There are a lot of accrued inefficiencies. |
Thanks for the info! I'm new to Node internals, but I'll try to work on this |
@bnoordhuis Just to make sure, you meant |
@jmendeth The method should be named That being said … Ben’s suggestion only really works if we can actually skip through the streams layer, i.e. there are no other chunks currently being written and no other interaction with the socket as a JS stream takes place. |
Thanks! I have investigated further and Of course, copying into a temporary buffer may decrease performance for large chunks, but it's faster than doing it in JS, and given the incredible overhead this might have for small requests / responses, I think we should at least allow users to enable concatenation. (Unrelated question: I've seen that |
The writes all go into a BIO buffer, so incomplete writes aren't possible, unlike writing to an OS socket descriptor. |
I also found out this is more significant in for (var i = 1; i < 50; i++) {
var path = "/testStreamShort" + i;
stream.pushStream({":path": path}, (err, pushStream) => {
if (err) throw err;
pushStream.respond({ ':status': 200, 'content-type': 'text/plain' });
pushStream.end("a");
});
}
stream.respond({ ':status': 200, 'content-type': 'text/plain' });
stream.end("a"); This emits 249 segments: 49 PUSH_PROMISE (31 bytes), then 50 HEADERS (12 bytes), then 50 DATA (9 bytes), then 50 segments with the actual body (1 byte), then 50 empty DATA (9 bytes) to indicate end of stream. This results in 8562 bytes written to the TCP socket. With the change I mentioned, everything is emitted in only 2 segments, weighting 3128 bytes. That's a 63% saving. I don't know how to properly benchmark this, but a quick attempt shows a 28% decrease in CPU time. |
@sam-github Thanks! |
TLSWrap::DoWrite() now concatenates data chunks and makes a single call to SSL_write(). Grouping data into a single segment: - reduces network overhead: by factors of even 2 or 3 in usages like `http2` or `form-data` - improves security: segment lengths can reveal lots of info, i.e. with `form-data`, how many fields are sent and the approximate length of every individual field and its headers - reduces encryption overhead: a quick benchmark showed a ~30% CPU time decrease for an extreme case, see nodejs#27573 (comment) Fixes: nodejs#27573
TLSWrap::DoWrite() now concatenates data chunks and makes a single call to SSL_write(). Grouping data into a single segment: - reduces network overhead: by factors of even 2 or 3 in usages like `http2` or `form-data` - improves security: segment lengths can reveal lots of info, i.e. with `form-data`, how many fields are sent and the approximate length of every individual field and its headers - reduces encryption overhead: a quick benchmark showed a ~30% CPU time decrease for an extreme case, see #27573 (comment) Fixes: #27573 PR-URL: #27861 Reviewed-By: Fedor Indutny <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ujjwal Sharma <[email protected]> Reviewed-By: Rich Trott <[email protected]>
TLSWrap::DoWrite() now concatenates data chunks and makes a single call to SSL_write(). Grouping data into a single segment: - reduces network overhead: by factors of even 2 or 3 in usages like `http2` or `form-data` - improves security: segment lengths can reveal lots of info, i.e. with `form-data`, how many fields are sent and the approximate length of every individual field and its headers - reduces encryption overhead: a quick benchmark showed a ~30% CPU time decrease for an extreme case, see nodejs#27573 (comment) Fixes: nodejs#27573 PR-URL: nodejs#27861 Reviewed-By: Fedor Indutny <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ujjwal Sharma <[email protected]> Reviewed-By: Rich Trott <[email protected]>
TLSWrap::DoWrite() now concatenates data chunks and makes a single call to SSL_write(). Grouping data into a single segment: - reduces network overhead: by factors of even 2 or 3 in usages like `http2` or `form-data` - improves security: segment lengths can reveal lots of info, i.e. with `form-data`, how many fields are sent and the approximate length of every individual field and its headers - reduces encryption overhead: a quick benchmark showed a ~30% CPU time decrease for an extreme case, see #27573 (comment) Fixes: #27573 Backport-PR-URL: #28904 PR-URL: #27861 Reviewed-By: Fedor Indutny <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ujjwal Sharma <[email protected]> Reviewed-By: Rich Trott <[email protected]>
The code:
For Node >= 9.6.0, sends two SSL segments (one with request + headers, the other with the body
hello\n
). Node <= 9.5.0 sends a single SSL segment with everything as expected.If instead of passing a Buffer, we pass a string:
This works correctly in all Node.JS versions.
The text was updated successfully, but these errors were encountered: