前段时间接手了一个挺“普通”的需求:一份 JSONL 格式的日志文件,1.8GB 左右,业务方给了一串随机行号,要求把对应记录抽出来。听起来不算难,可真要拿 PHP 方案去做,很多人的第一反应是 file_get_contents,然后内存直接爆掉;换成 fopen + fseek + fread,少量跳读还能凑合,量一上来整体就变得很别扭。最后让我把问题彻底解决的,是 mmap 大文件随机读取这条路线。
如果你也遇到过这种场景——文件动辄几个 GB,数据位置不连续,又不能把整个文件一次性读进 PHP 内存——这篇内容应该能帮到你。我会把 mmap 的原理、为什么 PHP 里要用 FFI 调系统接口、一个可直接抄走的封装类、实测对比数据,以及我在真实环境里踩过的几个坑全部写清楚。适合正在做 CLI 数据处理、日志抽样、离线脚本、以及任何需要“按偏移量快速读大文件”的 PHP 开发者。
1. 为什么常规 PHP 读法在大文件随机读取场景会卡脖子
1.1 file() 和 file_get_contents 在大文件面前的硬伤
先说大家最常用的两种方案。file() 会把每一行拆成数组元素,文件多大,数组就有多大;file_get_contents() 则会把整个文件拼成一个巨型字符串。这两种方式在几百 MB 的小文件上没问题,但一旦文件到了 GB 级别,第一个牺牲品就是 PHP 的 memory_limit。
比如一个 1.8GB 的文件,file_get_contents() 读取后,进程里至少会有一个 1.8GB 的字符串。如果还要按偏移去做 substr 切片,甚至会出现多个大字符串共存的情况,内存占用轻松翻倍。在 512MB 内存限制的容器或者共享主机上,这类脚本往往是读不到一半就被 OOM Kill,连后续的业务逻辑都没机会执行。
另外还有一个隐蔽的问题:file() / file_get_contents() 本质上是一次性顺序读。就算你只需要文件里 100 个随机位置的数据,它也得先把整个文件整个搬到内存,再把不需要的绝大多数数据也一并处理了。这种模式对“大数据量 + 低命中率”的随机读取来说,是双重浪费。
1.2 fopen + fseek + fread 慢在哪里
如果你绕开一次性载入,用 fopen() 打开句柄,再用 fseek() 跳到目标偏移,fread() 读取指定长度,内存问题是解决了,但性能上又会出现新的瓶颈。
fseek() 和 fread() 对应到底层系统调用就是 lseek() 和 read()。每次跳转加读取,都意味着一次或者多次系统调用,CPU 需要从用户态切到内核态,内核再把数据从文件系统页缓存里拷贝到用户态缓冲区。咱们做一万次
