Uh oh!
There was an error while loading. Please reload this page.
Enable TCP_NODELAY for all synchronous sockets. - #651
Conversation
3d7fad4 to
635db1eCompareThe widely documented poor interaction between the Nagle algorithm and TCP's delayed ACK feature, when making short successive writes, leads to unnecessary delays (around 50ms on Linux). This happens in httpcore whenever a POST request is made, since the headers and body are sent as two separate calls. The TCP_NODELAY option can be enabled to disable Nagle's algorithm, and therefore avoid this delay. The option is already enabled by default for all asyncio and Trio sockets. It is also enabled by CPython's http.client module (which urllib and requests use) and by many high-level HTTP libraries found in other languages, including libcurl, Java's Netty, Rust's reqwest and Go's standard library, to name a few.
635db1e to
54f2506Comparelovelydinosaur
commented
Jan 12, 2023
Thanks, seems like this might be worth review. I wasn't aware that |
zanieb
commented
Jan 12, 2023
There was some discussion about this recently in Go; golang/go#57530 and https://withinboredom.info/blog/2022/12/29/golang-is-evil-on-shitty-networks/ |
plietar
commented
Jan 12, 2023
This is the relevant bit in urllib3: https://github.com/urllib3/urllib3/blob/0b1188985b747ae50d7d4f9750f4e5ec46462517/src/urllib3/connection.py#L107https://github.com/urllib3/urllib3/pull/283/files Similar for http.client (which I mistakenly claimed was used by requests): https://github.com/python/cpython/blame/94fc7706b7bc3d57cdd6d15bf8e8c4499ae53a69/Lib/http/client.py#L955 |
One option here would be to start by adding a https://www.encode.io/httpcore/connection-pools/#other-options I'm also curious if we'd be able to demonstrate a difference in performance with |
Here's a sample code to measure the performance: importhttpximportrequestsimportasynciofromtimeimportperf_counterN=100URL="http://127.0.0.1:8000"defsync_version():
withhttpx.Client() asclient:
before=perf_counter()
foriinrange(N):
client.post(URL, content=b"Hello")
after=perf_counter()
return (after-before) /Nasyncdefasync_version():
asyncwithhttpx.AsyncClient() asclient:
before=perf_counter()
foriinrange(N):
awaitclient.post(URL, content=b"Hello")
after=perf_counter()
return (after-before) /Ndefrequests_version():
withrequests.Session() assession:
before=perf_counter()
foriinrange(N):
session.post(URL, data=b"Hello")
after=perf_counter()
return (after-before) /Nprint("Sync:", sync_version())
print("Async:", asyncio.run(async_version()))
print("Requests:", requests_version())Without this change, and running against a local server, I get average request durations on the order of 2-3ms for the async and requests version, and 43ms for the sync one. After applying the change from the PR, the request duration for the sync version is about 2ms. Here's the server code I used. I also get similar results with a Go server. Note that I've set TCP_NODELAY here, as otherwise it adds another 40ms across the board. fromhttp.serverimportHTTPServer, BaseHTTPRequestHandlerimportsocketPORT=8000classHandler(BaseHTTPRequestHandler):
protocol_version="HTTP/1.1"defdo_POST(self):
length=int(self.headers['Content-Length'])
self.rfile.read(length)
self.send_response(200)
self.send_header('Content-Length', '14')
self.end_headers()
self.wfile.write(b"Hello, World!\n")
withHTTPServer(("", PORT), Handler) ashttpd:
print("serving at port", PORT)
httpd.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
httpd.serve_forever() |
zanieb
commented
May 19, 2023
lovelydinosaur
commented
May 19, 2023
The motivation in #651 (comment) is compelling here. Would we prefer to accept this PR as-is, or should we make the socket options configurable first? (#662, resolved via #668) rephrased: Is having configurability actually desirable, or do we really just want to make this one change? |
lovelydinosaur
left a comment
There was a problem hiding this comment.
i think we oughta okay this, yup.
needs a note in the changelog then it should be good to go.
lovelydinosaur
left a comment
There was a problem hiding this comment.
Actually can be firmer on this one.
Yes, urllib3 do this and we should too.
Thanks for nudging us on it, @plietar.
lovelydinosaur
commented
May 23, 2023
Link to asyncio...
In the 3.7 release notes. Link to trio...
See the docs, and the implementation. |
The widely documented poor interaction between the Nagle algorithm and TCP's delayed ACK feature, when making short successive writes, leads to unnecessary delays (around 50ms on Linux). This happens in httpcore whenever a POST request is made, since the headers and body are sent as two separate calls.
The TCP_NODELAY can be enabled to disable Nagle's algorithm, and therefore avoid this delay.
The option is already enabled by default for all asyncio and Trio sockets. It is also enabled by CPython's http.client module (which urllib and requests use) and by many high-level HTTP libraries found in other languages, including libcurl, Java's Netty, Rust's reqwest and Go's standard library, to name a few.