Laravel 上传文件至 BackBlaze 问题排查记

背景

之前用 Laravel 把 图床 重写了,在我自建的 Docker 集群中运行了一段时间后,我发现,大于 2M 的图片无论怎么都上传不了,而且也没有报错。一直断断续续地排查,终于在 1024 节日找到了问题所在。

排查过程

php.ini 配置

一般使用 PHP 开发,大文件无法上传,第一直觉都是先怀疑 PHP 的配置问题,无非就是两个参数 upload_max_filesizemax_post_size

所以我先是修改了 Docker 镜像打包的代码,把这两个参数都提高到了 8M。然而发现并没有什么用,大于 2M 的图片依旧上传失败。

更换 GD 为 Imagick

排查过程中,还发现了图床不能处理 BMP 格式的图片。于是修改了 Dockerfile,安装 Imagick,将 Intervention/image 这个库的驱动改为 Imagick,完美解决。

最终的 Debug

定下心来,在关键位置打了断点,发现大文件上传时,会抛出异常 Received error from B2: Sha1 did not match data received。对比了文件的 sha1 值和传给 B2 的值,两个是一样的,最终定位在传给 B2 的一个文件大小的参数上。

查看了源码,确实发现有点坑。图床代码将本地图片推送到 B2,用的是大众推荐的写法 Storage::disk('b2')->put('path/to/file', Storage::disk('local')->get('path/to/file'));,但是由于 Storage::disk('local')->get('path/to/file') 获取到的是字符串形式的内容,最终通过下面代码计算大小时,远比实际值要小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (is_resource($options['Body'])) {
// We need to calculate the file's hash incrementally from the stream.
$context = hash_init('sha1');
hash_update_stream($context, $options['Body']);
$hash = hash_final($context);
// Similarly, we have to use fstat to get the size of the stream.
$size = fstat($options['Body'])['size'];
// Rewind the stream before passing it to the HTTP client.
rewind($options['Body']);
} else {
// We've been given a simple string body, it's super simple to calculate the hash and size.
$hash = sha1($options['Body']);
$size = mb_strlen($options['Body']);
}

猜测是 B2 那边根据传的「文件大小」这个参数对内容做了截取,再计算 sha1 值,发现不对之后抛出异常。找到问题之后,改写推送代码,解决了这个叫人头大的 Bug。

1
2
$localFile = Storage::getDriver()->readStream($localPath);
Storage::disk('b2')->put($remotePath, $localFile);