最近在 HP Microserver Gen8 上重新搭建了 Nextcloud (在虚拟机里面容器里,基于 PHP 7.2 ),可惜通过 virtio 虚拟万兆网络进行下载,SSD 上文件的下载速度不超过 260MiB/s,机械硬盘上文件的下载速度不超过 80MB/s。要知道直接本地访问时,SSD 能达到 550MB/s 左右,机械硬盘平均 130MB/,不甘心(这时 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 zk8802 2019-04-15 01:14:15 +08:00 via iPhone ![]() 赞楼主刨根问底的精神! |
2 Q4h7388nR28s95fa 2019-04-15 01:19:37 +08:00 厉害!楼主专研精神真棒! |
![]() | 3 zhs227 2019-04-15 01:19:46 +08:00 ![]() 没玩过这么高级的装备,不过非常佩服楼主,顶一下友情支持 另外不清楚有没有人知道,nginx 的那个 sendfile 和这个 mmap 的拷贝机制是不是一回事 |
![]() | 4 raysonx OP ![]() @zhs227 是的`sendfile`的性能更高,直接让内核对拷两个文件描述符,连内核态 /用户态拷贝都不用。但是 PHP 至今没有利用`sendfile`,包括`fpassthru`。 |
![]() | 5 yufz 2019-04-15 01:38:44 +08:00 via Android 佩服佩服,真 极客 |
![]() | 6 GG668v26Fd55CP5W 2019-04-15 02:24:47 +08:00 via iPhone 厉害啊 |
![]() | 7 sxcccc 2019-04-15 03:11:52 +08:00 via iPhone aws 的 ec2 高端配置 东京节点 首页 500kb 打开速度一流 内容分发都是 4gb 大包依然能迅速下载 参考 www.dxqq.net |
8 lzxgh621 2019-04-15 03:28:13 +08:00 via iPhone 我这边 程序本体都跑不利索 |
![]() | 9 slowgen 2019-04-15 03:29:44 +08:00 很棒,最近在团队内部推 nextcloud,以及基于 Collabora 的办公文档协作,先收藏留作备用了. |
![]() | 厉害,难得有真正有意义的话题了! |
![]() | 11 lihongming 2019-04-15 05:05:27 +08:00 via iPhone 赞,可以考虑测一下 2M 一个循环,看是不是会更早达到 CPU 瓶颈,那样的话就该考虑自己修改 stream_copy_to_stream 源码放宽限制,以获得更高性能了。 |
![]() | 12 mwftts 2019-04-15 07:11:47 +08:00 via Android 好久没在 app 第一页看到这样的技术贴,今天一天心情肯定会不错 |
![]() | 13 lazyyz 2019-04-15 07:26:35 +08:00 via Android 厉害,佩服楼主这折腾劲! |
![]() | 14 taresky 2019-04-15 07:44:48 +08:00 via iPhone 厉害! |
![]() | 15 akagishigeru 2019-04-15 08:06:14 +08:00 via iPhone 一大早就学习了 |
16 carlclone 2019-04-15 08:11:37 +08:00 via Android 强,基础好扎实 |
![]() | 17 mokeyjay 2019-04-15 08:16:40 +08:00 强无敌,点赞 |
18 CallMeReznov 2019-04-15 08:17:22 +08:00 via Android 这才是真正的干货啊 |
19 zvcs 2019-04-15 08:24:02 +08:00 via Android 谢谢楼主的分享 |
![]() | 20 Canon1014 2019-04-15 08:24:28 +08:00 目瞪口呆 |
![]() | 21 zuokanyunqishi 2019-04-15 08:28:04 +08:00 via Android 点赞 |
![]() | 22 fengtalk 2019-04-15 08:34:14 +08:00 收藏了,佩服和赞赏楼主的这种探索精神。 |
![]() | 23 Edwards 2019-04-15 08:34:58 +08:00 收藏 |
![]() | 24 zzxCNCZ 2019-04-15 08:40:26 +08:00 赞楼主,厉害了 |
![]() | 25 R18 2019-04-15 08:44:21 +08:00 厉害了!打破砂锅闻到底 |
26 fox0001 2019-04-15 08:47:39 +08:00 via Android 点赞! nextcloud 15 之前,性能低下,我只是从树莓派搬到 x8350。一直以为是 PHP 背的锅,没想到楼主还能找出具体原因 |
![]() | 27 SupperMary 2019-04-15 08:57:52 +08:00 via Android 很强 |
![]() | 28 eluotao 2019-04-15 09:14:45 +08:00 技术贴 要收藏...回头看看 NAS 有没有优化的空间. |
29 whatsmyip 2019-04-15 09:18:47 +08:00 很强 |
30 yngby 2019-04-15 09:21:08 +08:00 牛逼牛逼 |
31 polymerdg 2019-04-15 09:35:00 +08:00 牛逼 |
![]() | 32 hst001 2019-04-15 09:40:35 +08:00 via Android 666 |
![]() | 33 SbloodyS 2019-04-15 09:42:35 +08:00 牛逼 |
![]() | 34 sorshion 2019-04-15 09:46:20 +08:00 基础很扎实,厉害 |
![]() | 35 liuxu 2019-04-15 09:46:40 +08:00 这波操作可以的 |
![]() | 36 syahd 2019-04-15 09:49:18 +08:00 via Android ⊙⊙!这就是开源的魅力啊,有需要就可以自己改。不过确定魔改这一部分的代码不会对其他地方造成影响吗? |
![]() | 37 dapang1221 2019-04-15 09:59:02 +08:00 厉害了 |
38 bzi 2019-04-15 10:07:08 +08:00 厉害啊 |
![]() | 39 tailf 2019-04-15 10:14:55 +08:00 服了 |
![]() | 40 reeble 2019-04-15 11:00:37 +08:00 大佬大佬 |
![]() | 41 sheeta 2019-04-15 11:08:16 +08:00 佩服佩服 |
![]() | 42 使用 nginx 的 X-Accel-Redirect 可不可行呢 |
43 ipengxh 2019-04-15 11:27:24 +08:00 厉害了 |
![]() | 44 liuxyon 2019-04-15 11:30:40 +08:00 厉害 |
![]() | 45 yytsjq 2019-04-15 11:34:39 +08:00 @zhujinliang X-Accel-Redirect 相比 fpassthru 应该更好些吧 |
![]() | 46 dalieba 2019-04-15 11:42:08 +08:00 via Android 希望 Sabre 项目早日接纳楼主的改进,新版本早日发布。 |
![]() | 47 klusfq 2019-04-15 11:43:38 +08:00 via iPhone 膜拜楼主大佬 |
48 zzxx3322 2019-04-15 11:48:53 +08:00 楼主有遇到上传瓶颈吗?官方默认最多同时上传三个任务,关键速度跑不满,我没有详细测试是不是网络或者硬件问题导致速度跑不满,但是我感觉你的问题和这个问题也应该是相同的锅,提一下,可以给点意见嘛? |
![]() | 49 duola 2019-04-15 12:12:25 +08:00 折腾精神,厉害! |
![]() | 50 raysonx OP @zzxx3322 上传速度确实比下载慢很多。Nextcloud 的上传机制比较复杂,等有时间研究一下开个帖分享。 |
51 moonfly 2019-04-15 12:27:02 +08:00 技术贴必须要支持, 虽然自己的功力远远没有达到 LZ 的级别, 但能看到这样的帖子,真的是一种享受! |
52 Huelse 2019-04-15 12:32:18 +08:00 真是一篇干货,感谢感谢!! |
![]() | 53 Actrace 2019-04-15 12:32:46 +08:00 还有一个方案,文件输出完全交给 Nginx 去做,PHP 只负责处理输出前逻辑。 这里需要用到 Nginx 的一个特性 X-Accel-Redirect,不过这样整套程序就和 Nginx 绑定到一起了。 |
54 zjq123 2019-04-15 13:06:32 +08:00 via Android 你们下载速度达到几百兆每秒? |
55 dnsaq 2019-04-15 13:07:57 +08:00 via iPhone 目瞪口呆 我都看懵了。 |
![]() | 56 tongz 2019-04-15 13:17:06 +08:00 奈何本人没文化, 一句卧槽走天下 |
![]() | 57 laozhoubuluo 2019-04-15 13:27:00 +08:00 啥也不说了,点赞!! |
58 ultimate010 2019-04-15 13:36:56 +08:00 真心点赞,我自己搭建的局域网 samba 和 nfs 等文件服务,速度也没法跑满千兆网卡,查了下参数优化了下 samba aio,有点提升,但是仍然无法满速,没思路就凑合用了。 |
![]() | 59 KasuganoSoras 2019-04-15 13:42:28 +08:00 ![]() 确实是快了很多,在千兆服务器上测试的 |
![]() | 60 KasuganoSoras 2019-04-15 13:44:07 +08:00 ![]() @ultimate010 #58 局域网 samba 我测试千兆是可以跑满的,传文件速度稳定在 110MB/s 左右,如果跑不满可能是 samba 版本比较低或者其他问题 |
61 killerv 2019-04-15 13:51:30 +08:00 厉害了 |
![]() | 62 cfcboy 2019-04-15 14:22:55 +08:00 感谢楼主的分享,做个记号。 |
![]() | 63 BCy66drFCvk1Ou87 2019-04-15 14:46:06 +08:00 awesome |
![]() | 64 fengci 2019-04-15 15:11:36 +08:00 mk |
![]() | 65 panlilu 2019-04-15 16:25:56 +08:00 硬核 debug |
66 ultimate010 2019-04-15 18:42:50 +08:00 @KasuganoSoras 谢谢,我用 docker 跑的,最新的 dperson/samba,小机器 cpu 是 Intel(R) Atom(TM) CPU D525 @ 1.80GHz,机械硬盘,全速的时候也就 50mb 左右,以前调出过写入 80mb,读取也就 30-40mb,cpu 好像没有跑满,感觉自己的配置有点问题。 |
![]() | 67 KasuganoSoras 2019-04-15 18:58:38 +08:00 ![]() @ultimate010 #66 这应该就是 CPU 性能瓶颈问题了,我手上也有一台 Atom D2550 的工控主机,装了 Samba 测试也是跑不满千兆,速度在 100-400Mbps 左右浮动,就上不去了 |
![]() | 68 kookxiang 2019-04-15 19:15:15 +08:00 应该用 sendfile 吧 |
69 ben1024 2019-04-15 19:35:05 +08:00 厉害 |
![]() | 70 intsilence 2019-04-15 21:06:07 +08:00 手动点赞! |
![]() | 71 raysonx OP @KasuganoSoras 截图中是打了补丁后的速度吗?之前是多少? CPU load 有没有跑满? |
![]() | 72 KasuganoSoras 2019-04-15 21:19:29 +08:00 @raysonx #71 之前大概是 20 ~ 40M/s 左右,CPU 的话基本上不可能跑满的……至少是宽带先跑满 因为 CPU 是 32 核 64 线程,但是下载的时候看到 CPU 占用率明显比之前高了,速度也快了很多 |
![]() | 73 dandycheung 2019-04-15 21:21:27 +08:00 via Android @zhs227 不是一回事。 |
![]() | 74 KasuganoSoras 2019-04-15 21:21:51 +08:00 @raysonx #71 这个速度其实不是固定的,一直在跳来跳去,可能和我本地网络有关,我看到有几秒钟速度上到了 97MB/s,然后又掉到 60 左右,不过已经算很不错了 |
![]() | 75 raysonx OP @KasuganoSoras 有时间的话可以试试在服务器上本地测速,排除网络影响。方法是直接用 curl 命令下载文件: curl -o /dev/null --user 'username:password' -H hostname http://127.0.0.1/remote.php/webdav/文件名 |
![]() | 76 BooksE 2019-04-15 23:18:54 +08:00 你们都是点赞?只有我是羡慕 lz 有这个能力 |
77 wmwwmv 2019-04-16 01:41:15 +08:00 via Android 这对我很有用,感谢楼主 |
79 tankren 2019-04-16 07:45:09 +08:00 via Android 收藏了 谢谢 |
80 silencefent 2019-04-16 09:01:50 +08:00 速度提高 3 倍,cpu 跑满载还是有点划不来吧,gen8 好歹也是 3.5G 起步的 4C8T 服务器 |
![]() | 81 ganbuliao 2019-04-16 09:16:56 +08:00 牛逼 我觉得 还是比较适合拧小螺丝钉 |
![]() | 82 ganbuliao 2019-04-16 09:17:49 +08:00 还是觉得自己比较适合拧小螺丝钉 (滑稽 |
83 wttx 2019-04-16 09:31:16 +08:00 via Android Mark 一下,以后有用,, |
![]() | 84 telami 2019-04-16 10:22:34 +08:00 开源的魅力,心向往之 |
![]() | 85 lzj307077687 2019-04-16 14:06:03 +08:00 敬佩! |
86 nyaruko 2019-04-16 15:01:52 +08:00 万兆。。厉害了。。家里千兆网完全不担心这些。 |
![]() | 87 knightgao2 2019-04-16 16:03:39 +08:00 厉害厉害了 |
![]() | 88 Jaeger 2019-04-16 16:54:58 +08:00 反手就是一赞 |
![]() | 89 abccccabc 2019-04-19 11:13:09 +08:00 这个贡献可大了。 |
![]() | 90 iwishing 2019-05-07 16:13:36 +08:00 这个就是工匠精神,hacker 精神,打破沙锅问到底的精神 请问,您秃了没有? |
91 Chenamy2017 2019-05-07 16:19:07 +08:00 牛! |
![]() | 92 JRay 2019-05-07 20:53:29 +08:00 大佬大佬 |
![]() | 93 zlfoxy 2019-05-07 23:38:33 +08:00 厉害,关键是这么复杂的东西,楼主能解释的这么清楚。膜拜。 |
94 pupboss 2020-08-20 23:09:03 +08:00 楼主好,我想请教个问题,NextCloud 使用网页版或者 WebDAV 处理大文件的时候,取消下载文件之后,php 还是会不断的读硬盘,并且占满 IO,如果点了一个视频关闭再开再关闭,磁盘会卡的不能用,同时我也用 Nginx 测试过静态文件,只有在需要读盘的时候才会有 20M/s 左右的 IO,然后立刻就不读了,这个问题有解吗,我看官方已经合并了你最新的代码,应该不是这个导致的 |
95 aod321 2021-06-30 10:49:38 +08:00 大佬牛! |