Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

http: fix >90% performance regression for GET requests using response.end #9026

Closed
wants to merge 1 commit into from
Closed

http: fix >90% performance regression for GET requests using response.end #9026

wants to merge 1 commit into from

Conversation

CGavrila
Copy link

Problem

This is an attempt to fix issue #8940, which describes a performance regression which started with commit 1fddc1f; more specifically, the number of requests per second dropped from ~18k to ~1.1k (on my machine) in between v0.10.31 and v0.10.32. This can be tested easily as described in issue #8940:

var http = require('http');
var body = new Buffer('Hello World');
http.createServer(function(req, res) {
  res.writeHead(200); // or: `res.statusCode = 200;`
  res.end(body); // change this to `res.end();` to see the expected speed post-1fddc1f
}).listen(3333);
wrk 'http://localhost:3333/' -d 3 -c 50 -t 4 | grep 'Requests/sec'

The problem comes from commit 1fddc1f, in particular from the following lines, which are aimed at HEAD requests:

// Transfer-encoding: chunked responses to HEAD requests
if (this._hasBody && this.chunkedEncoding)
   hot = false; // This will never execute for HEAD requests, only GET

However, the hot = false; instruction will never execute for HEAD requests, since this._hasBody is always false because of line lib/http.js:1073, but it will execute for all GET requests since the conditions will match. Therefore, hot-path optimization will never be done for GET requests when sending data via response.end().

Fix

The fix is simply to remove the lines mentioned above:

- // Transfer-encoding: chunked responses to HEAD requests
- if (this._hasBody && this.chunkedEncoding)
-   hot = false;

As previously explained, this executes only for GET requests, not for HEAD requests, as it was intended. Simply removing them will allow GET requests to have the hot-path optimization when response.end(...); is not empty, while HEAD requests will be non-optimized since there is no need (i.e no body).

Testing

As far as I can tell, there are three conditions which need to be met in order for the fix to be considered safe:

  1. Performance should be at pre-1fddc1f levels for the script in Significant performance regression in http in v0.10.32+ #8940.
  2. Pass all of the node tests, which also includes the one which came packaged with 1fddc1f.
  3. As a bonus, make sure that Parse Error: HPE_INVALID_CONSTANT on Transfer-Encoding response to HEAD request #8361 is still fixed.

For no. 1, I have tested that the performance is at pre-1fddc1 levels after deleting those three lines.

Original
wrk 'http://localhost:3333/' -d 3 -c 50 -t 4 | grep 'Requests/sec' | awk '{ print "  " $2 }'
1184.48 

With fix 
wrk 'http://localhost:3333/' -d 3 -c 50 -t 4 | grep 'Requests/sec' | awk '{ print "  " $2 }'
18378.13

For no. 2, I have made sure that the Node tests run/pass in the same way with/without the fix.

For no. 3, I have tried those scripts, everything is fine.

status: 404
{ 'transfer-encoding': 'chunked',
  date: 'Tue, 13 Jan 2015 17:10:21 GMT',
  connection: 'close' }
end

A significant performance regressions has been introduced in 1fddc1f for
GET requests which send data through response.end(). The number of
requests per second dropped to somewhere around 6% of their previous
level. (#8940)

The fix consists of removing a part of the lines added by 1fddc1f,
lines which were supposed to affect only HEAD requests, but interfered
with GET requests instead.

The lines removed would not have affected the behaviour in the case of
a HEAD request as this._hasBody would always be false. Therefore, they
were not required to fix the issue reported in #8361.
@CGavrila CGavrila closed this Jan 14, 2015
@CGavrila CGavrila deleted the hot-path-perf branch January 14, 2015 13:27
@CGavrila CGavrila restored the hot-path-perf branch January 14, 2015 13:51
@CGavrila CGavrila reopened this Jan 14, 2015
@evantorrie
Copy link

+1. This confused me for a long time as to why our regular benchmarking in our pipeline dropped precipitously after 0.10.32.

@mscdex
Copy link

mscdex commented Feb 12, 2015

+100

@evantorrie
Copy link

I believe the correct fix is more likely

// Transfer-encoding: chunked responses to HEAD requests
if (!this._hasBody && this.chunkedEncoding)
   hot = false; 

In terms of request/second, my results are even worse (on RHEL 7.0). There seems to be a fixed 40ms delay when it goes through the flush routine.

Build 6e689ec

[01:07] evant@oxy-oxygen-0a58abcf:wrk$ ./wrk -d 60 -c 4 -t 1 --latency http://localhost:8080/
Running 1m test @ http://localhost:8080/
  1 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   553.71us  131.18us   6.74ms   78.04%
    Req/Sec     7.66k   614.91    10.22k    63.76%
  Latency Distribution
     50%  547.00us
     75%  597.00us
     90%  650.00us
     99%    0.92ms
  435095 requests in 1.00m, 64.73MB read
Requests/sec:   7251.64
Transfer/sec:      1.08MB

Build 1fddc1f

[01:08] evant@oxy-oxygen-0a58abcf:wrk$ ./wrk -d 60 -c 4 -t 1 --latency http://localhost:8080/
Running 1m test @ http://localhost:8080/
  1 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    40.00ms  196.97us  44.03ms   99.03%
    Req/Sec   100.03      7.01   135.00     83.40%
  Latency Distribution
     50%   39.99ms
     75%   40.00ms
     90%   40.03ms
     99%   40.10ms
  6000 requests in 1.00m, 0.89MB read
Requests/sec:     99.98
Transfer/sec:     15.24KB

Build 1fddc1f with following patch applied:

diff --git a/lib/http.js b/lib/http.js
index f87df17..a81ab11 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -946,7 +946,7 @@ OutgoingMessage.prototype.end = function(data, encoding) {
     hot = false;

   // Transfer-encoding: chunked responses to HEAD requests
-  if (this._hasBody && this.chunkedEncoding)
+  if (!this._hasBody && this.chunkedEncoding)
     hot = false;

   if (hot) {
[01:27] evant@oxy-oxygen-0a58abcf:wrk$ ./wrk -d 60 -c 4 -t 1 --latency http://localhost:8080/
Running 1m test @ http://localhost:8080/
  1 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   564.04us  147.25us   5.19ms   75.74%
    Req/Sec     7.50k   657.24     8.56k    68.40%
  Latency Distribution
     50%  553.00us
     75%  619.00us
     90%  700.00us
     99%    0.96ms
  425732 requests in 1.00m, 63.34MB read
Requests/sec:   7095.55
Transfer/sec:      1.06MB
[01:28] evant@oxy-oxygen-0a58abcf:wrk$

evantorrie referenced this pull request Feb 12, 2015
When replying to a HEAD request, do not attempt to send the trailers and
EOF sequence (`0\r\n\r\n`). The HEAD request MUST not have body.

Quote from RFC:

The presence of a message body in a response depends on both the
request method to which it is responding and the response status code
(Section 3.1.2).  Responses to the HEAD request method (Section 4.3.2
of [RFC7231]) never include a message body because the associated
response header fields (e.g., Transfer-Encoding, Content-Length,
etc.), if present, indicate only what their values would have been if
the request method had been GET (Section 4.3.1 of [RFC7231]).

fix #8361

Reviewed-By: Timothy J Fontaine <tjfontaine@gmail.com>
@mscdex
Copy link

mscdex commented Feb 12, 2015

@evantorrie Yeah, that's the change I had suggested in #8940 but I wasn't sure if it was 100% correct or not.

@evantorrie
Copy link

For completeness, here's the source of the server that the wrk benchmarks were testing:

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(8080);

@trevnorris
Copy link

I'm fine with this.

/cc @tjfontaine @misterdjules @cjihrig

@cjihrig
Copy link

cjihrig commented Feb 12, 2015

+1 as long as the tests pass

@misterdjules misterdjules added this to the 0.10.37 milestone Mar 2, 2015
@misterdjules
Copy link

@trevnorris @cjihrig What change do you agree with, the one mentioned by @evantorrie in his comment or the one from @CGavrila's PR?

@misterdjules
Copy link

@joyent/node-coreteam Added to the 0.10.37 milestone as it seems to be a significant performance issue for which we seem to have a good candidate fix.

@cjihrig
Copy link

cjihrig commented Mar 3, 2015

I was originally referring to the changes in this PR. If @evantorrie's changes also work, but are faster, then I'd prefer that.

@mhdawson
Copy link
Member

mhdawson commented Mar 3, 2015

It has been a while, but what I remember from when Cristian and I worked on this, is that we identified that this._hasBody is false for a HEAD and true for a GET. At the same time we also identified that if this._hasBody is false then hot is already false so the lines are unnecessary. So we believe that removing the lines only affects the case that needs to be fixed and felt that the fewer the lines the better.

@trevnorris
Copy link

@misterdjules I was also referring to the change in this PR.

@misterdjules
Copy link

Thank you for the clarifications! LGTM, running http benchmarks currently and tests on all platforms to make sure there's no regression. I will report results here.

@misterdjules
Copy link

Benchmarks results available and no regression found on UNICES and on Windows.

@misterdjules
Copy link

@trevnorris I'm not sure how to interpret these benchmark results, are you familiar with them?

@trevnorris
Copy link

@misterdjules yeah. those results are a pain to read though, and overall I don't find them super reliable. I've tested it myself and it seems to help.

@misterdjules
Copy link

@trevnorris Sounds good, I just wanted to make sure we didn't find anything suspicious in these benchmarks. Do you mind landing this?

Thank you @CGavrila, @evantorrie and everyone who helped!

@CGavrila
Copy link
Author

CGavrila commented Mar 5, 2015

Thank you, @misterdjules and the team, for the work done to validate this!

misterdjules pushed a commit to misterdjules/node that referenced this pull request Mar 5, 2015
A significant performance regressions has been introduced in 1fddc1f for
GET requests which send data through response.end(). The number of
requests per second dropped to somewhere around 6% of their previous
level.

The fix consists of removing a part of the lines added by 1fddc1f,
lines which were supposed to affect only HEAD requests, but interfered
with GET requests instead.

The lines removed would not have affected the behaviour in the case of
a HEAD request as this._hasBody would always be false. Therefore, they
were not required to fix the issue reported in nodejs#8361.

Fixes nodejs#8940.

PR: nodejs#9026
PR-URL: nodejs#9026
Reviewed-By: Julien Gilli <julien.gilli@joyent.com>
@misterdjules
Copy link

Thank you everyone, landed in 8bcd0a4!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants