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

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

  raysonx · 11 天前 · 5930 次点击

背景

最近在 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 条附言  ·  10 天前
感谢各位的支持和建议,先统一回复一些内容:

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

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

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


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

curl -o /dev/null --user 'username:password' -H hostname http://127.0.0.1/remote.php/webdav/文件名
    76
BooksE   10 天前
你们都是点赞?只有我是羡慕 lz 有这个能力
    77
wmwwmv   10 天前 via Android
这对我很有用,感谢楼主
    78
ericgui   10 天前
@BooksE 看来学 C 还是挺有用的
    79
tankren   10 天前 via Android
收藏了 谢谢
    80
silencefent   9 天前
速度提高 3 倍,cpu 跑满载还是有点划不来吧,gen8 好歹也是 3.5G 起步的 4C8T 服务器
    81
ganbuliao   9 天前
牛逼 我觉得 还是比较适合拧小螺丝钉
    82
ganbuliao   9 天前
还是觉得自己比较适合拧小螺丝钉 (滑稽
    83
wttx   9 天前 via Android
Mark 一下,以后有用,,
    84
telami   9 天前
开源的魅力,心向往之
    85
lzj307077687   9 天前
敬佩!
    86
nyaruko   9 天前
万兆。。厉害了。。家里千兆网完全不担心这些。
    87
knightgao2   9 天前
厉害厉害了
    88
Jaeger   9 天前
反手就是一赞
    89
abccccabc   6 天前
这个贡献可大了。
关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1610 人在线   最高记录 5043   ·  
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.3 · 25ms · UTC 23:56 · PVG 07:56 · LAX 16:56 · JFK 19:56
♥ Do have faith in what you're doing.
沪ICP备16043287号-1