https://github.com/alex/http-client-bench包含我使用的基准。
结果类似于:
| | 请求/http | 插座 |
| --- | --- | --- |
| CPython | 12MB/秒 | 200MB/秒 |
| pypy | 80MB/秒 | 300MB/秒 |
| 去 | 150MB/秒 | 不适用 |
与套接字相比,请求会带来相当大的开销,尤其是在 CPython 上。
该开销出乎意料地大。 但是,避免它可能会很棘手。
最大的问题是我们对每个块进行了大量处理。 这就是堆栈的所有方式:请求、urllib3 和 httplib。 看看时间都花在了哪里来找出导致效率低下的原因,这将是非常有趣的。
猜测下一步是尝试分析 httplib / urllib3 以查看
表现在那里?
凯文·伯克
电话:925.271.7005 | 二十毫秒.com
2014 年 12 月 4 日星期四下午 5:01,Cory Benfield通知@ github.com
写道:
该开销出乎意料地大。 但是,避免它可能会很棘手。
最大的问题是我们对每个块进行了大量处理。 那是
一直到堆栈:requests、urllib3 和 httplib。 这将是
看看时间都花在了谁身上,这非常有趣
导致效率低下。—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65732050
.
刚刚使用 urllib3 运行了基准测试:
PyPy:120MB/秒
CPython:70MB/秒
我重新运行 CPython + 请求:35MB/s
(我的机器在基准测试中似乎遇到了一点噪音,如果有人有一个更安静的系统,他们可以打开这些系统,那就太棒了)
我尝试在我的机器上关闭所有其他机器后运行这些
应用程序和终端窗口,也有相当多的噪音 -
套接字基准测试从 30mb/s 到 460mb/s 不等。
凯文·伯克
电话:925.271.7005 | 二十毫秒.com
2014 年 12 月 4 日星期四晚上 9:24,Alex Gaynor通知@ github.com
写道:
刚刚使用 urllib3 运行了基准测试:
PyPy:120MB/秒
CPython:70MB/秒我重新运行 CPython + 请求:35MB/s
(我的机器似乎在基准测试中遇到了一点噪音,如果
任何人都有一个更安静的系统,他们可以使用这些系统,那太棒了)—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65748982
.
我现在让基准测试更容易运行,所以其他人希望可以验证我的数字:
CPython:
BENCH SOCKET:
8GiB 0:00:22 [ 360MiB/s] [======================================================>] 100%
BENCH HTTPLIB:
8GiB 0:02:34 [53.1MiB/s] [======================================================>] 100%
BENCH URLLIB3:
8GiB 0:01:30 [90.2MiB/s] [======================================================>] 100%
BENCH REQUESTS
8GiB 0:01:30 [90.7MiB/s] [======================================================>] 100%
BENCH GO HTTP
8GiB 0:00:26 [ 305MiB/s] [======================================================>] 100%
pypy:
BENCH SOCKET:
8GiB 0:00:22 [ 357MiB/s] [======================================================>] 100%
BENCH HTTPLIB:
8GiB 0:00:43 [ 189MiB/s] [======================================================>] 100%
BENCH URLLIB3:
8GiB 0:01:07 [ 121MiB/s] [======================================================>] 100%
BENCH REQUESTS
8GiB 0:01:09 [ 117MiB/s] [======================================================>] 100%
BENCH GO HTTP
8GiB 0:00:26 [ 307MiB/s] [======================================================>] 100%
呃……这些数字很奇怪。 CPython 的 httplib 比 requests 或 urllib3 慢,即使两个库都使用 httplib? 那是不对的。
他们一直为我重现——你能试试基准测试,看看是否
你能繁殖吗? 假设您可以,您是否发现
基准?
2014 年 12 月 5 日星期五上午 11:16:45 Cory Benfield通知@github.com
写道:
呃……这些数字很奇怪。 CPython 的 httplib 比请求慢或
urllib3,即使两个库都使用 httplib? 那是不对的。—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65821989
.
我现在只是在拿一台已知安静的机器。 应该需要几分钟才能可用,因为它是一个必须安装的物理盒子(上帝,我爱 MAAS)。
CPython 2.7.8
BENCH SOCKET:
8GiB 0:00:26 [ 309MiB/s] [================================>] 100%
BENCH HTTPLIB:
8GiB 0:02:24 [56.5MiB/s] [================================>] 100%
BENCH URLLIB3:
8GiB 0:01:42 [79.7MiB/s] [================================>] 100%
BENCH REQUESTS
8GiB 0:01:45 [77.9MiB/s] [================================>] 100%
BENCH GO HTTP
8GiB 0:00:27 [ 297MiB/s] [================================>] 100%
物有所值:
这个补丁, CPython 3.4.2
:
BENCH SOCKET:
8GiB 0:00:27 [ 302MiB/s] [================================>] 100%
BENCH HTTPLIB:
8GiB 0:00:53 [ 151MiB/s] [================================>] 100%
BENCH URLLIB3:
8GiB 0:00:54 [ 149MiB/s] [================================>] 100%
BENCH REQUESTS
8GiB 0:00:56 [ 144MiB/s] [================================>] 100%
BENCH GO HTTP
8GiB 0:00:31 [ 256MiB/s] [================================>] 100%
你应该能够在 Python2 上获得同样的效果
env PYTHONUNBUFFERED=
或-u
标志。
2014 年 12 月 5 日星期五上午 11:42:36 Corey Farwell通知@github.com
写道:
物有所值:
这个补丁https://gist.github.com/frewsxcv/1c0f3c81cda508e1bca9,CPython
3.4.2:工作台插座:
8GiB 0:00:27 [ 302MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:00:53 [ 151MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:00:54 [ 149MiB/s] [================================>] 100%
板凳要求
8GiB 0:00:56 [ 144MiB/s] [==================================>] 100%
板凳去 HTTP
8GiB 0:00:31 [ 256MiB/s] [================================>] 100%—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65826239
.
@alex有趣的是, env PYTHONUNBUFFERED=
或-u
对 Python 2 都没有相同的效果。我的机器传入的结果。
好的,下面的数据来自一台只运行这些测试的机器。 最后一个测试是在设置了 Python -u
标志的情况下运行的,正如您所看到的,该标志没有任何作用。
Python 2.7.6
go version go1.2.1 linux/amd64
BENCH SOCKET:
8GiB 0:00:16 [ 500MiB/s] [================================>] 100%
BENCH HTTPLIB:
8GiB 0:01:32 [88.6MiB/s] [================================>] 100%
BENCH URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
BENCH REQUESTS
8GiB 0:01:21 [ 100MiB/s] [================================>] 100%
BENCH GO HTTP
8GiB 0:00:21 [ 385MiB/s] [================================>] 100%
Python 2.7.6
go version go1.2.1 linux/amd64
BENCH SOCKET:
8GiB 0:00:16 [ 503MiB/s] [================================>] 100%
BENCH HTTPLIB:
8GiB 0:01:33 [87.8MiB/s] [================================>] 100%
BENCH URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
BENCH REQUESTS
8GiB 0:01:22 [99.3MiB/s] [================================>] 100%
BENCH GO HTTP
8GiB 0:00:20 [ 391MiB/s] [================================>] 100%
Python 2.7.6
go version go1.2.1 linux/amd64
BENCH SOCKET:
8GiB 0:00:16 [ 506MiB/s] [================================>] 100%
BENCH HTTPLIB:
8GiB 0:01:31 [89.1MiB/s] [================================>] 100%
BENCH URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
BENCH REQUESTS
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
BENCH GO HTTP
8GiB 0:00:21 [ 389MiB/s] [================================>] 100%
这些数字非常稳定,并具有以下特点:
FWIW,我刚刚从@kevinburke合并添加buffering=True
,做你的跑步
包括那个?
2014 年 12 月 5 日星期五下午 12:04:40 Cory Benfield通知@github.com
写道:
好的,下面的数据来自一台什么都不做的机器
但运行这些测试。 最后一个测试是使用 Python -u 标志运行的
设置,正如您所看到的那样,该标志无效。蟒蛇 2.7.6
转到版本 go1.2.1 linux/amd64
工作台插座:
8GiB 0:00:16 [ 500MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:01:32 [88.6MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳要求
8GiB 0:01:21 [ 100MiB/s] [================================>] 100%
板凳去 HTTP
8GiB 0:00:21 [385MiB/s] [================================>] 100%蟒蛇 2.7.6
转到版本 go1.2.1 linux/amd64
工作台插座:
8GiB 0:00:16 [ 503MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:01:33 [87.8MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳要求
8GiB 0:01:22 [99.3MiB/s] [================================>] 100%
板凳去 HTTP
8GiB 0:00:20 [ 391MiB/s] [================================>] 100%蟒蛇 2.7.6
转到版本 go1.2.1 linux/amd64
工作台插座:
8GiB 0:00:16 [ 506MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:01:31 [89.1MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳要求
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳去 HTTP
8GiB 0:00:21 [389MiB/s] [================================>] 100%这些数字非常稳定,并具有以下特点:
- 原始套接字读取速度很快(废话)。
- Go 大约是原始套接字读取速度的 80%。
- urllib3 大约是原始套接字读取速度的 20%。
- requests 比 urllib3 稍慢,这是有道理的,因为我们
为要通过的数据添加几个堆栈帧。- httplib 比 requests/urllib3 慢。 那简直是不可能的
我怀疑我们必须配置 httplib 或 sockets 库
httplib 不是的一种方式。—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65829335
.
Cory - 查看最新版本的 bench 客户端
buffering=True 在 httplib 中(如 requests/urllib3 所做的那样)
凯文·伯克
电话:925.271.7005 | 二十毫秒.com
2014 年 12 月 5 日星期五上午 10:04,Cory Benfield通知@ github.com
写道:
好的,下面的数据来自一台什么都不做的机器
但运行这些测试。 最后一个测试是使用 Python -u 标志运行的
设置,正如您所看到的那样,该标志无效。蟒蛇 2.7.6
转到版本 go1.2.1 linux/amd64
工作台插座:
8GiB 0:00:16 [ 500MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:01:32 [88.6MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳要求
8GiB 0:01:21 [ 100MiB/s] [================================>] 100%
板凳去 HTTP
8GiB 0:00:21 [385MiB/s] [================================>] 100%蟒蛇 2.7.6
转到版本 go1.2.1 linux/amd64
工作台插座:
8GiB 0:00:16 [ 503MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:01:33 [87.8MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳要求
8GiB 0:01:22 [99.3MiB/s] [================================>] 100%
板凳去 HTTP
8GiB 0:00:20 [ 391MiB/s] [================================>] 100%蟒蛇 2.7.6
转到版本 go1.2.1 linux/amd64
工作台插座:
8GiB 0:00:16 [ 506MiB/s] [================================>] 100%
工作台 HTTPLIB:
8GiB 0:01:31 [89.1MiB/s] [================================>] 100%
工作台 URLLIB3:
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳要求
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
板凳去 HTTP
8GiB 0:00:21 [389MiB/s] [================================>] 100%这些数字非常稳定,并具有以下特点:
- 原始套接字读取速度很快(废话)。
- Go 大约是原始套接字读取速度的 80%。
- urllib3 大约是原始套接字读取速度的 20%。
- requests 比 urllib3 稍慢,这是有道理的,因为我们
为要通过的数据添加几个堆栈帧。- httplib 比 requests/urllib3 慢。 那简直是不可能的
我怀疑我们必须配置 httplib 或 sockets 库
httplib 不是的一种方式。—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65829335
.
是的,这修复了 httplib 的性能行为,使其更有意义。
新结果和结论:
Python 2.7.6
go version go1.2.1 linux/amd64
BENCH SOCKET:
8GiB 0:00:16 [ 499MiB/s] [================================>] 100%
BENCH HTTPLIB:
8GiB 0:01:12 [ 113MiB/s] [================================>] 100%
BENCH URLLIB3:
8GiB 0:01:21 [ 100MiB/s] [================================>] 100%
BENCH REQUESTS
8GiB 0:01:20 [ 101MiB/s] [================================>] 100%
BENCH GO HTTP
8GiB 0:00:20 [ 391MiB/s] [================================>] 100%
因此,可以说这里的真正成本是 httplib。 加快速度需要让 httplib 不碍事。
不过,我很想知道 httplib 的哪一部分让我们付出了代价。 我认为分析bench_httplib.py
是一个很好的下一步。
通过将该行添加到bench_socket.py
测试中,我已经排除了通过socket.makefile
将套接字转换为文件对象的可能性,这根本不会减慢它的速度。 奇怪的是,它似乎使它更快。
答案几乎可以肯定是传输编码:分块处理。
参见: https :
服务器上的 Content-Length 会产生一些意想不到的结果。
2014 年 12 月 5 日星期五下午 12:24:53 Cory Benfield通知@github.com
写道:
因此,可以说这里的真正成本是 httplib。 加快这一进程需要
让 httplib 不碍事。不过,我很想知道 httplib 的哪一部分让我们付出了代价。 一世
认为分析 bench_httplib.py 是一个很好的下一步。—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65831653
.
有趣的。
分块处理几乎肯定是问题所在,我对 go 处理得更好并不感到惊讶,尤其是分块是 go 的默认 HTTP 模式。
然而,请求比原始套接字快是......出乎意料!
值得注意的一件事:如果套接字在之前的测试中没有解码分块编码,那么它就获得了不公平的优势,因为它实际上读取的数据比其他方法少! 他们都在读取分块的标头以及 8GB 的数据。
这就引出了一个后续问题:我们是否仍然认为所有这些方法实际上都在读取相同数量的数据?
是的,套接字层在作弊,它没有解码分块的元数据,
从技术上讲,阅读量要少一些。 它在那里作为“多快
我们可以阅读吗”,而不是为了证明任何事情。
2014 年 12 月 5 日星期五下午 12:33:10 Cory Benfield通知@github.com
写道:
有趣的。
分块处理几乎肯定是问题所在,我并不是真的
惊讶于 go 处理得更好,特别是因为 chunked 是默认的
去的 HTTP 模式。然而,请求比原始套接字快是......出乎意料!
值得注意的一件事:如果套接字没有解码分块编码
在之前的测试中,它获得了不公平的优势,因为它实际上是
读取的数据比其他方法少! 他们都在读
分块的标头以及 8GB 的数据。这就引出了一个后续问题:我们是否仍然认为所有这些方法
实际上读取相同数量的数据?—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -65833299
.
如果这与我们一次从套接字读取的块大小有关,我不会感到惊讶。
@alex 的蛋糕非常有帮助:蛋糕:
@nelhage对各种示例进行了一些跟踪(在传输中
编码:分块大小写) https://gist.github.com/nelhage/dd6490fbc5cfb815f762
是结果。 看起来 httplib 中有一个错误导致它
并不总是从套接字读取完整的块。
2014 年 12 月 8 日星期一上午 9:05:14,Kenneth Reitz通知@github.com
写道:
@alex https://github.com/alex 的蛋糕,非常有帮助 [图片:
:蛋糕:]—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -66147998
.
那么我们这里有一个标准库中没有人真正维护的错误? ( @Lukasa至少有 2 个补丁集已经开放了 1 年以上。)也许我今晚会在某个列表中提出臭味
有人(我可能会明白,不清楚)可能需要用 pdb 深入研究
或其他东西,并找出生成那些 20 字节的确切代码
阅读以便我们可以整理出一个好的错误报告。
2014 年 12 月 8 日星期一上午 9:14:09 Ian Cordasco通知@github.com
写道:
所以我们这里有一个标准库中的一个错误,没有人真的
维护? ( @Lukasa https://github.com/Lukasa 至少有 2 个补丁
已开放超过 1 年的套装。)也许我会在清单上提出臭味
今晚的某个地方—
直接回复此邮件或在 GitHub 上查看
https://github.com/kennethreitz/requests/issues/2371#issuecomment -66149522
.
如果没有其他人能够做到,我会尝试在今晚或明天适应它。
那么,关于根本原因的任何消息? 是什么产生了这些短读,没有它们,情况会改善多少?
@kislyuk据我所知。 希望这个圣诞假期我有时间去追查它。
谢谢@Lukasa。 我正在处理一个性能问题,其中使用 urllib3/requests 的分块响应的下载速度比使用 curl 和其他库慢得多,并试图了解这是否是罪魁祸首。
我在这方面闲逛了一下。 短读来自 httplib 中的 _read_chunked 函数
https://fossies.org/linux/misc/Python-2.7.9.tgz/Python-2.7.9/Lib/httplib.py#l_585
2 字节读取似乎主要来自第 622 行。
我得到了与之前发布的略有不同的 strace 模式:
recvfrom(3, "400\r\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 \0\0\0\0\0\0\0"..., 8192, 0, NULL, NULL) = 8192
recvfrom(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\ 0\0\0\0\0\0\0\0\0\0"..., 54, 0, NULL, NULL) = 54
recvfrom(3, "\r\n", 2, 0, NULL, NULL) = 2
这种模式可以解释如下:
然后模式将重复(返回步骤 1)
FWIW,我发现可以通过将 2 字节块终止符读取到块体读取中来实现适度(20% 左右)的加速,即而不是这样:
value.append(self._safe_read(chunk_left))
amt -= chunk_left
self._safe_read(2) # toss the CRLF at the end of the chunk
这样做:
value.append(self._safe_read(chunk_left + 2)[:-2])
amt -= chunk_left
不过,实际上,如果 54 字节的读取可以缓冲比 54(即 8192 字节)更多的字节,这可能会更好,这意味着在读取 2 字节时,缓冲的套接字不会为空。
更进一步。 我不确定小读取是吞吐量损失的主要因素(或不是在本地主机上)。 我玩弄了套接字缓冲区大小,使其成为 1031 字节的倍数,尽管 strace 不再有少量读取,但它对吞吐量没有太大影响。
我认为吞吐量的损失可能更多地与 socket.py 处理小读取的方式有关。 这是相关代码(来自socket.read):
https://fossies.org/linux/misc/Python-2.7.9.tgz/Python-2.7.9/Lib/socket.py#l_336
当您将显式长度传递给 socket.read 并且可以从现有的缓冲数据中实现时,这是代码路径:
buf = self._rbuf
buf.seek(0, 2) # seek end
#.....
# Read until size bytes or EOF seen, whichever comes first
buf_len = buf.tell()
if buf_len >= size:
# Already have size bytes in our buffer? Extract and return.
buf.seek(0)
rv = buf.read(size)
self._rbuf = StringIO()
self._rbuf.write(buf.read())
return rv
我在这里看到的问题是,即使是 2 字节的读取也意味着将未读的剩余部分复制到新的 StringIO 中。 这看起来对于很多小读来说会变得非常昂贵。 如果给定的 StringIO 可以在每次读取时以某种方式耗尽,而不是将未读剩余部分复制到新 StringIO 的当前模式,那么我希望这可能有助于提高吞吐量
@gardenia我还没有机会吸收所有这些,但非常感谢您在这里的努力和工作。 @shazow也许你会发现@gardenia的研究很有趣。
:+1: 谢谢@gardenia。 顺便说一下,我自己在用例中对性能的研究发现,在我的情况下,响应没有分块,但 urllib3 的执行速度比请求快 20% 以上,因此引入了一些我想描述的开销。 仍然符合这个问题的标题,但不同的根本原因。
引人入胜,感谢分享! :)
对于@Lukasa的 Hyper 来说,这似乎也是一个伟大的目标。
@alex - 我对你提到的 urllib3 vs requests non-chunked 性能问题玩了一些。 我想我看到了类似的 20% 的请求下降。
在请求中,我推测性地尝试用 stream() 的内联实现(来自 urllib3)替换对 self.raw.stream 的调用。 至少在我的机器上,它似乎使请求和 urllib3 之间的吞吐量更接近:
--- requests.repo/requests/models.py 2015-03-06 16:05:52.072509869 +0000
+++ requests/models.py 2015-03-07 20:49:25.618007438 +0000
@@ -19,6 +19,7 @@
from .packages.urllib3.fields import RequestField
from .packages.urllib3.filepost import encode_multipart_formdata
from .packages.urllib3.util import parse_url
+from .packages.urllib3.util.response import is_fp_closed
from .packages.urllib3.exceptions import (
DecodeError, ReadTimeoutError, ProtocolError, LocationParseError)
from .exceptions import (
@@ -652,8 +654,12 @@
try:
# Special case for urllib3.
try:
- for chunk in self.raw.stream(chunk_size, decode_content=True):
- yield chunk
+ while not is_fp_closed(self.raw._fp):
+ data = self.read(amt=chunk_size, decode_content=True)
+
+ if data:
+ yield data
+
except ProtocolError as e:
raise ChunkedEncodingError(e)
except DecodeError as e:
也许您可以在您的机器上尝试相同的方法,看看它是否对您也有影响。
(注意是的,我知道对 is_fp_closed 的调用是封装破坏,它并不意味着作为一个严重的补丁只是一个数据点)
@shazow我希望 hyper 使用的BufferedSocket
应该通过基本上防止小读来解决很多低效率问题。 我想知道 Py3 上的httplib
是否有这个问题,因为它广泛使用io.BufferedReader
,它应该提供与BufferedSocket
大致相同的好处。
当然,然而,当hyper
增长到足够有用的 HTTP/1.1 功能时,我们应该尝试将它与这些其他实现一起进行基准测试,并努力使hyper
尽可能快。
将近一年不活动。 关闭。
我看到了类似的问题,与urllib3
相比,使用requests
吞吐量减少了 10 倍。
我认为这个问题存在于 urllib3 的HTTPResponse
类中,当它作为迭代器被读取时,它的吞吐量非常糟糕。 我让我的代码使用了一个非常丑陋的 hack:我返回了 urllib3 使用的带下划线的httplib.HTTPResponse
对象,这似乎解决了我的吞吐量问题。
有趣的事实:urllib3 的HTTPResponse
超类是io.IOBase
。 Python3 的httplib.HTTPResponse
超类是io.BufferedIOBase
。 我想知道它是否与此有关。