首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
raysonx
V2EX  ›  PHP

优化 PHP 大文件下载速度至万兆,让 Nextcloud 支持万兆网络

  raysonx · 280 天前 · 10413 次点击
这是一个创建于 280 天前的主题,其中的信息可能已经有所发展或是发生改变。

背景

最近在 HP Microserver Gen8 上重新搭建了 Nextcloud (在虚拟机里面容器里,基于 PHP 7.2 ),可惜通过 virtio 虚拟万兆网络进行下载,SSD 上文件的下载速度不超过 260MiB/s,机械硬盘上文件的下载速度不超过 80MB/s。要知道直接本地访问时,SSD 能达到 550MB/s 左右,机械硬盘平均 130MB/s,不甘心(这时 PHP 进程的 CPU 占用率很低,说明根本没有达到 CPU 执行瓶颈)。

排除了网络问题后,我在存储目录上搭建了一个 Nginx 进行测试,发现通过 Ngninx 直接下载文件几乎能达到本地直接访问的性能。于是,下载速度慢的锅就落在了 PHP 的性能上。

调研

经过一番调研,Nextcloud 的 WebDav 服务是基于 sabre/dav 的框架开发的。于是找到了 sabre/dav 的源码,最后定位到下载文件代码的位置:3rdparty/sabre/http/lib/Sapi.php 。原来 sabre/dav 是通过调用 stream_copy_to_stream 将要下载的文件拷贝到 HTTP 输出的:直接把文件流和 PHP 的输出流进行对拷,之前并没有其他的读写操作,说明瓶颈就在这一行代码。

// 3rdparty/sabre/http/lib/Sapi.php
// ...
 if (is_resource($body) && 'stream' == get_resource_type($body)) {
                if (PHP_INT_SIZE !== 4) {
                    // use the dedicated function on 64 Bit systems
                    stream_copy_to_stream($body, $output, (int) $contentLength);
                } else {
// ...

我本人并不是 PHP 程序员,于是开始了漫长的搜索。Google 娘告诉我 PHP 专门提供了fpassthru函数提供高性能文件下载,于是我修改代码把 stream_copy_to_stream 换成了fpassthru

// 3rdparty/sabre/http/lib/Sapi.php
// ...
 if (is_resource($body) && 'stream' == get_resource_type($body)) {
                if (PHP_INT_SIZE !== 4) {
                    // use the dedicated function on 64 Bit systems
                    // stream_copy_to_stream($body, $output, (int) $contentLength);
                    fpassthru($body); // 改动这一行
                } else {
// ...

测试了一下,发现下载速度直接打了鸡血,440-470 MiB/s。可惜 fpassthru 只能把文件输出到结尾,不能只输出文件的一部分(为了支持断点续传和分片下载)。另外翻了一下 sabre/dav 的 issues,发现 sabre/dav 不用fpassthru的另外一个原因是有些版本的 PHP 中fpassthru函数存在 BUG。

继续深入

那为什么stream_copy_to_stream速度和fpassthru差距大得不科学呢?只能去读 PHP 的源码了,幸好 C 语言是我的强项。 我发现,fpassthru函数和stream_copy_to_stream函数实现是及其类似的:先尝试把源文件创建为内存映射文件(通过调用 mmap ),如果成功则直接从内存映射文件拷贝到目的流,否则就读到内存中进行传统的手动拷贝。差别来了,stream_copy_to_stream的第三个参数是要拷贝的字节数,可惜如果这个值大于 4MiB,PHP 就拒绝创建内存映射文件,直接回退到传统拷贝。

解决方法

在循环中调用stream_copy_to_stream,每次最多拷 4MiB:

// 3rdparty/sabre/http/lib/Sapi.php
// ...
 if (is_resource($body) && 'stream' == get_resource_type($body)) {
                if (PHP_INT_SIZE !== 4) {
                    // use the dedicated function on 64 Bit systems
                    // 下面是改动的部分:
                    // allow PHP to use mmap by copying in 4MiB chunks
                    $chunk_size = 4 * 1024 * 1024;
                    stream_set_chunk_size($output, $chunk_size);
		    $left = $contentLength;
		    while ($left > 0) {
		        $left -= stream_copy_to_stream($body, $output, min($left, $chunk_size));
		    }
               } else {
 // ...

测试了一下,结果令人震惊:下载速度几乎和本地读取无异了:SSD 文件的下载速度超过了 500 MB/s,甚至超过了 fpassthru 的速度(大概是因为缓冲区开的比fpassthru大)。

我又试着创建了一个 10G 大小的 sparse 文件 ( truncate -s 10G 10G.bin ),Linux 在读取 sparse 文件时可以立即完成,可以用来模拟如果硬盘速度足够快的情况。继续测试,发现下载速度超过了 700MiB/s,已经接近万兆网络的传输极限。这时 PHP 进程的 CPU 占用率已经达到 100%,说明瓶颈在 CPU 性能上了。

总结

stream_copy_to_stream 拷贝流时,如果 source 是文件并且每次拷贝小于 4MiB,PHP 会用内存映射文件对拷贝进行加速。超过 4MiB 后就会回退到传统读取机制。

后续

向 Sabre 项目提了 PR:https://github.com/sabre-io/http/pull/119。如果各位也在玩 Nextcloud 并且遇到了下载速度瓶颈,可以试着打一下我这个补丁。

第 1 条附言  ·  280 天前
感谢各位的支持和建议,先统一回复一些内容:

1. 部分小伙伴提到了 Nginx 的 X-accel:如果 web 服务器用的是 Nginx 的话,可以通过设置一个 HTTP header 将文件输出转交给 Nginx,可获得更快的下载速度。这确实是个可行的优化思路,毕竟 Nginx 性能优越,而且输出文件的实现基于 sendfile 系统调用,理论上效率更高。目前不知道 Nextcloud 或者 Sabre 有没有计划针对 Nginx 做优化,后续有时间可以试着跟进一下。

2. stream
第 2 条附言  ·  280 天前
2. 目前看来 PHP 的 stream_copy_to_stream 的性能不佳,可优化的地方非常多,可以考虑对这个函数进行优化,贡献给上游社区。

3. 有小伙伴担心对 sabre 的“魔改”会对其他地方造成影响。我觉得这应该不是魔改吧。。。。
第 3 条附言  ·  280 天前
忘了在正文中补充机械硬盘下载速度的提升:
机械硬盘的下载速度从优化前的 70-80MB/s 提升到了 100 - 120 MB/s。我没有仔细去研究机械硬盘下载速度提升的原因,可能是我调大了拷贝的缓存区大小所致。
第 4 条附言  ·  280 天前
补充:PHP 核心源码中拒绝映射 4MiB 以上文件的代码在这里: https://github.com/php/php-src/blob/623911f993f39ebbe75abe2771fc89faf6b15b9b/main/streams/mmap.c#L34
93 回复  |  直到 2019-05-07 23:38:33 +08:00
zk8802
    1
zk8802   280 天前 via iPhone   ♥ 2
赞楼主刨根问底的精神!
HiCode
    2
HiCode   280 天前
厉害!楼主专研精神真棒!
zhs227
    3
zhs227   280 天前   ♥ 1
没玩过这么高级的装备,不过非常佩服楼主,顶一下友情支持
另外不清楚有没有人知道,nginx 的那个 sendfile 和这个 mmap 的拷贝机制是不是一回事
raysonx
    4
raysonx   280 天前   ♥ 1
@zhs227 是的`sendfile`的性能更高,直接让内核对拷两个文件描述符,连内核态 /用户态拷贝都不用。但是 PHP 至今没有利用`sendfile`,包括`fpassthru`。
jinyang656
    5
jinyang656   280 天前 via Android
佩服佩服,真 极客
falcon05
    6
falcon05   280 天前 via iPhone
厉害啊
sxcccc
    7
sxcccc   280 天前 via iPhone
aws 的 ec2 高端配置 东京节点 首页 500kb 打开速度一流 内容分发都是 4gb 大包依然能迅速下载 参考 www.dxqq.net
lzxgh621
    8
lzxgh621   280 天前 via iPhone
我这边 程序本体都跑不利索
shuimugan
    9
shuimugan   280 天前
很棒,最近在团队内部推 nextcloud,以及基于 Collabora 的办公文档协作,先收藏留作备用了.
tony601818
    10
tony601818   280 天前 via Android
厉害,难得有真正有意义的话题了!
lihongming
    11
lihongming   280 天前 via iPhone
赞,可以考虑测一下 2M 一个循环,看是不是会更早达到 CPU 瓶颈,那样的话就该考虑自己修改 stream_copy_to_stream 源码放宽限制,以获得更高性能了。
herexf
    12
herexf   280 天前 via Android
好久没在 app 第一页看到这样的技术贴,今天一天心情肯定会不错
lazyyz
    13
lazyyz   280 天前 via Android
厉害,佩服楼主这折腾劲!
taresky
    14
taresky   280 天前 via iPhone
厉害!
JaguarJack
    15
JaguarJack   280 天前 via iPhone
一大早就学习了
carlclone
    16
carlclone   280 天前 via Android
强,基础好扎实
mokeyjay
    17
mokeyjay   280 天前
强无敌,点赞
CallMeReznov
    18
CallMeReznov   280 天前 via Android
这才是真正的干货啊
zvcs
    19
zvcs   280 天前 via Android
谢谢楼主的分享
Canon1014
    20
Canon1014   280 天前
目瞪口呆
zuokanyunqishi
    21
zuokanyunqishi   280 天前 via Android
点赞
fengtalk
    22
fengtalk   280 天前
收藏了,佩服和赞赏楼主的这种探索精神。
Edwards
    23
Edwards   280 天前
收藏
zzxCNCZ
    24
zzxCNCZ   280 天前
赞楼主,厉害了
R18
    25
R18   280 天前
厉害了!打破砂锅闻到底
fox0001
    26
fox0001   280 天前 via Android
点赞! nextcloud 15 之前,性能低下,我只是从树莓派搬到 x8350。一直以为是 PHP 背的锅,没想到楼主还能找出具体原因
SupperMary
    27
SupperMary   280 天前 via Android
很强👍
eluotao
    28
eluotao   280 天前
技术贴 要收藏...回头看看 NAS 有没有优化的空间.
whatsmyip
    29
whatsmyip   280 天前
很强
yngby
    30
yngby   280 天前
牛逼牛逼
polymerdg
    31
polymerdg   280 天前
牛逼
hst001
    32
hst001   280 天前 via Android
666
SbloodyS
    33
SbloodyS   280 天前
牛逼
sorshion
    34
sorshion   280 天前
基础很扎实,厉害
liuxu
    35
liuxu   280 天前
这波操作可以的
whwq2012
    36
whwq2012   280 天前 via Android
⊙∀⊙!这就是开源的魅力啊,有需要就可以自己改。不过确定魔改这一部分的代码不会对其他地方造成影响吗?
dapang1221
    37
dapang1221   280 天前
厉害了
bzi
    38
bzi   280 天前
厉害啊
tailf
    39
tailf   280 天前
服了
reeble
    40
reeble   280 天前
大佬大佬
sheeta
    41
sheeta   280 天前
佩服佩服
zhujinliang
    42
zhujinliang   280 天前 via iPhone
使用 nginx 的 X-Accel-Redirect 可不可行呢
ipengxh
    43
ipengxh   280 天前
厉害了
liuxyon
    44
liuxyon   280 天前
厉害👍
yytsjq
    45
yytsjq   280 天前
@zhujinliang X-Accel-Redirect 相比 fpassthru 应该更好些吧
dalieba
    46
dalieba   280 天前 via Android
希望 Sabre 项目早日接纳楼主的改进,新版本早日发布。
klusfq
    47
klusfq   280 天前 via iPhone
膜拜楼主大佬
zzxx3322
    48
zzxx3322   280 天前
楼主有遇到上传瓶颈吗?官方默认最多同时上传三个任务,关键速度跑不满,我没有详细测试是不是网络或者硬件问题导致速度跑不满,但是我感觉你的问题和这个问题也应该是相同的锅,提一下,可以给点意见嘛?
duola
    49
duola   280 天前
折腾精神,厉害!
raysonx
    50
raysonx   280 天前 via Android
@zzxx3322 上传速度确实比下载慢很多。Nextcloud 的上传机制比较复杂,等有时间研究一下开个帖分享。
moonfly
    51
moonfly   280 天前
技术贴必须要支持,
虽然自己的功力远远没有达到 LZ 的级别,
但能看到这样的帖子,真的是一种享受!
Huelse
    52
Huelse   280 天前
真是一篇干货,感谢感谢!!
Actrace
    53
Actrace   280 天前
还有一个方案,文件输出完全交给 Nginx 去做,PHP 只负责处理输出前逻辑。
这里需要用到 Nginx 的一个特性 X-Accel-Redirect,不过这样整套程序就和 Nginx 绑定到一起了。
zjq123
    54
zjq123   280 天前 via Android
你们下载速度达到几百兆每秒?
dnsaq
    55
dnsaq   280 天前 via iPhone
目瞪口呆 我都看懵了。
tongz
    56
tongz   280 天前
奈何本人没文化, 一句卧槽走天下
laozhoubuluo
    57
laozhoubuluo   280 天前
啥也不说了,点赞!!👍👍👍
ultimate010
    58
ultimate010   280 天前
真心点赞,我自己搭建的局域网 samba 和 nfs 等文件服务,速度也没法跑满千兆网卡,查了下参数优化了下 samba aio,有点提升,但是仍然无法满速,没思路就凑合用了。
KasuganoSoras
    59
KasuganoSoras   280 天前


确实是快了很多,在千兆服务器上测试的
KasuganoSoras
    60
KasuganoSoras   280 天前   ♥ 1
@ultimate010 #58 局域网 samba 我测试千兆是可以跑满的,传文件速度稳定在 110MB/s 左右,如果跑不满可能是 samba 版本比较低或者其他问题
killerv
    61
killerv   280 天前
厉害了
cfcboy
    62
cfcboy   280 天前
感谢楼主的分享,做个记号。
HuasLeung
    63
HuasLeung   280 天前
awesome
fengci
    64
fengci   280 天前
mk
panlilu
    65
panlilu   280 天前
硬核 debug
ultimate010
    66
ultimate010   280 天前
@KasuganoSoras 谢谢,我用 docker 跑的,最新的 dperson/samba,小机器 cpu 是 Intel(R) Atom(TM) CPU D525 @ 1.80GHz,机械硬盘,全速的时候也就 50mb 左右,以前调出过写入 80mb,读取也就 30-40mb,cpu 好像没有跑满,感觉自己的配置有点问题。
KasuganoSoras
    67
KasuganoSoras   280 天前   ♥ 1
@ultimate010 #66 这应该就是 CPU 性能瓶颈问题了,我手上也有一台 Atom D2550 的工控主机,装了 Samba 测试也是跑不满千兆,速度在 100-400Mbps 左右浮动,就上不去了
kookxiang
    68
kookxiang   280 天前
应该用 sendfile 吧
ben1024
    69
ben1024   280 天前
厉害
intsilence
    70
intsilence   280 天前
手动点赞!
raysonx
    71
raysonx   280 天前
@KasuganoSoras 截图中是打了补丁后的速度吗?之前是多少? CPU load 有没有跑满?
KasuganoSoras
    72
KasuganoSoras   280 天前
@raysonx #71 之前大概是 20 ~ 40M/s 左右,CPU 的话基本上不可能跑满的……至少是宽带先跑满
因为 CPU 是 32 核 64 线程,但是下载的时候看到 CPU 占用率明显比之前高了,速度也快了很多
dandycheung
    73
dandycheung   280 天前 via Android
@zhs227 不是一回事。
KasuganoSoras
    74
KasuganoSoras   280 天前
@raysonx #71 这个速度其实不是固定的,一直在跳来跳去,可能和我本地网络有关,我看到有几秒钟速度上到了 97MB/s,然后又掉到 60 左右,不过已经算很不错了
raysonx
    75
raysonx   280 天前
@KasuganoSoras 有时间的话可以试试在服务器上本地测速,排除网络影响。方法是直接用 curl 命令下载文件:

curl -o /dev/null --user 'username:password' -H hostname http://127.0.0.1/remote.php/webdav/文件名
BooksE
    76
BooksE   279 天前
你们都是点赞?只有我是羡慕 lz 有这个能力
wmwwmv
    77
wmwwmv   279 天前 via Android
这对我很有用,感谢楼主
ericgui
    78
ericgui   279 天前
@BooksE 看来学 C 还是挺有用的
tankren
    79
tankren   279 天前 via Android
收藏了 谢谢
silencefent
    80
silencefent   279 天前
速度提高 3 倍,cpu 跑满载还是有点划不来吧,gen8 好歹也是 3.5G 起步的 4C8T 服务器
ganbuliao
    81
ganbuliao   279 天前
牛逼 我觉得 还是比较适合拧小螺丝钉
ganbuliao
    82
ganbuliao   279 天前
还是觉得自己比较适合拧小螺丝钉 (滑稽
wttx
    83
wttx   279 天前 via Android
Mark 一下,以后有用,,
telami
    84
telami   279 天前
开源的魅力,心向往之
lzj307077687
    85
lzj307077687   279 天前
敬佩!
nyaruko
    86
nyaruko   279 天前
万兆。。厉害了。。家里千兆网完全不担心这些。
knightgao2
    87
knightgao2   279 天前
厉害厉害了
Jaeger
    88
Jaeger   279 天前
反手就是一赞
abccccabc
    89
abccccabc   276 天前
这个贡献可大了。
iwishing
    90
iwishing   258 天前
这个就是工匠精神,hacker 精神,打破沙锅问到底的精神
请问,您秃了没有?
Chenamy2017
    91
Chenamy2017   258 天前
牛!
JRay
    92
JRay   258 天前
大佬大佬
zlfoxy
    93
zlfoxy   257 天前
厉害,关键是这么复杂的东西,楼主能解释的这么清楚。膜拜。
关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2478 人在线   最高记录 5168   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.3 · 32ms · UTC 14:22 · PVG 22:22 · LAX 06:22 · JFK 09:22
♥ Do have faith in what you're doing.