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

2019-04-15 01:11:02 +08:00
 raysonx

背景

最近在 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 并且遇到了下载速度瓶颈,可以试着打一下我这个补丁。

21879 次点击
所在节点    PHP
95 条回复
sheeta
2019-04-15 11:08:16 +08:00
佩服佩服
zhujinliang
2019-04-15 11:09:12 +08:00
使用 nginx 的 X-Accel-Redirect 可不可行呢
ipengxh
2019-04-15 11:27:24 +08:00
厉害了
liuxyon
2019-04-15 11:30:40 +08:00
厉害👍
yytsjq
2019-04-15 11:34:39 +08:00
@zhujinliang X-Accel-Redirect 相比 fpassthru 应该更好些吧
dalieba
2019-04-15 11:42:08 +08:00
希望 Sabre 项目早日接纳楼主的改进,新版本早日发布。
klusfq
2019-04-15 11:43:38 +08:00
膜拜楼主大佬
zzxx3322
2019-04-15 11:48:53 +08:00
楼主有遇到上传瓶颈吗?官方默认最多同时上传三个任务,关键速度跑不满,我没有详细测试是不是网络或者硬件问题导致速度跑不满,但是我感觉你的问题和这个问题也应该是相同的锅,提一下,可以给点意见嘛?
duola
2019-04-15 12:12:25 +08:00
折腾精神,厉害!
raysonx
2019-04-15 12:23:28 +08:00
@zzxx3322 上传速度确实比下载慢很多。Nextcloud 的上传机制比较复杂,等有时间研究一下开个帖分享。
moonfly
2019-04-15 12:27:02 +08:00
技术贴必须要支持,
虽然自己的功力远远没有达到 LZ 的级别,
但能看到这样的帖子,真的是一种享受!
Huelse
2019-04-15 12:32:18 +08:00
真是一篇干货,感谢感谢!!
Actrace
2019-04-15 12:32:46 +08:00
还有一个方案,文件输出完全交给 Nginx 去做,PHP 只负责处理输出前逻辑。
这里需要用到 Nginx 的一个特性 X-Accel-Redirect,不过这样整套程序就和 Nginx 绑定到一起了。
zjq123
2019-04-15 13:06:32 +08:00
你们下载速度达到几百兆每秒?
dnsaq
2019-04-15 13:07:57 +08:00
目瞪口呆 我都看懵了。
tongz
2019-04-15 13:17:06 +08:00
奈何本人没文化, 一句卧槽走天下
laozhoubuluo
2019-04-15 13:27:00 +08:00
啥也不说了,点赞!!👍👍👍
ultimate010
2019-04-15 13:36:56 +08:00
真心点赞,我自己搭建的局域网 samba 和 nfs 等文件服务,速度也没法跑满千兆网卡,查了下参数优化了下 samba aio,有点提升,但是仍然无法满速,没思路就凑合用了。
KasuganoSoras
2019-04-15 13:42:28 +08:00


确实是快了很多,在千兆服务器上测试的
KasuganoSoras
2019-04-15 13:44:07 +08:00
@ultimate010 #58 局域网 samba 我测试千兆是可以跑满的,传文件速度稳定在 110MB/s 左右,如果跑不满可能是 samba 版本比较低或者其他问题

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/555144

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX