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

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

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

背景

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

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

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

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


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

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