痛点:图片处理的"最后一公里"
写博客的人大概都经历过这样的纠结:相机拍出来的照片一张 20MB,直接上传到服务器——读者打开页面要等 10 秒,每月 CDN 流量账单看着肉疼;压缩吧,不同页面需要不同尺寸,手动处理 100 张照片的痛苦谁做谁知道。
更麻烦的是现代 Web 对图片格式的要求:WebP 比 JPEG 体积小 30%-50%,AVIF 更小,但源图往往是 JPEG 或 PNG。每次新增一种格式就得重新压缩一遍,这谁顶得住。
理想的方案应该是:原始图片只存一份,不同尺寸、不同格式按需实时生成,并且生成一次后缓存下来,后续请求直接返回缓存——零处理开销。
这就是我部署 imgproxy 的原因。这篇文章记录我在 liigoo.net 博客中落地 imgproxy 的完整实践,包括架构设计、代码实现和一些踩坑经验。
imgproxy 是什么
imgproxy 是一个用 Go 语言编写的开源图片处理服务(MIT 协议),专为生产环境设计。它的核心能力:
- URL-based API:所有处理参数编码在 URL 中,不需要写代码调用 SDK
- HMAC-SHA256 签名:防止恶意用户构造 URL 消耗服务器资源
- 实时处理:缩放、裁剪、锐化、格式转换、水印一气呵成
- 极低资源占用:Go 单二进制文件,Docker 镜像约 20MB,内存占用通常不超过 100MB
- 高吞吐:单进程可处理数百请求/秒,内置智能降采样避免 OOM
一句话概括:把图片 URL 丢给它,它返回处理好的图片。
架构设计
我的博客部署在一台内网服务器上,Typecho(PHP)运行在 Docker 容器中,imgproxy 作为独立的 Docker 容器运行,只监听内网地址。整体架构如下:

整个流程分为两条链路:
读链路(页面访问)
- 浏览器请求页面 → Nginx → Typecho PHP
- PHP 渲染时调用
liigoo_preview_url()获取缩略图 URL - 函数先检查本地缓存:命中 → 直接返回缓存 URL(Apache 静态文件输出)
- 缓存未命中 → PHP 通过内网调用 imgproxy → imgproxy 下载并处理源图 → PHP 保存到本地缓存 → 返回缓存 URL
- 如果启用了 CDN 预生成模式,预览图直接走七牛云 CDN
写链路(文章发布)
- 作者在编辑器中粘贴图片或上传附件
- 保存文章时,后台自动提取文章中所有图片 URL
- 调用 imgproxy 批量生成 400px / 600px / 800px 三张预览图
- 如果开启了 OSS 上传,预览图自动上传七牛云,返回 CDN URL
- 预览图 URL 保存到
typecho_fields的image_settings字段
核心实现
整个图片处理管线由三个 PHP 函数组成,全部在主题的 functions.php 中。
1. 动态缩略图 —— liigoo_thumb_url()
这是最基础的函数:传入原始图片 URL 和目标宽度,返回处理后的图片 URL。
function liigoo_thumb_url($originalUrl, $width, $height = 0) {
// 检查是否为可处理的图片格式
$ext = strtolower(pathinfo(parse_url($originalUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'bmp', 'svg'])) {
return $originalUrl;
}
// 缓存 key = MD5(源URL + 处理参数)
$cacheKey = md5($originalUrl . '|' . $width . '|' . $height . '|' . $format . '|' . $quality);
// 缓存命中 → 直接返回,零处理开销
if (file_exists($cacheFile)) {
return $cacheUrl;
}
// 构建 imgproxy 签名 URL
// URL 格式: /{signature}/{processing_options}/{base64_encoded_source_url}
$processing = "rs:fit:{$width}:{$height}:1:0/g:sm/enlarge:0/q:{$quality}/f:{$format}";
$signature = hmac_sha256($salt . $path, $key);
// 通过内网调用 imgproxy,保存到本地缓存
$imgData = file_get_contents("http://192.168.123.5:8081/{$signature}/{$path}");
file_put_contents($cacheFile, $imgData);
return $cacheUrl;
}关键细节:
- URL 规范化:对源图 URL 的 path 先
rawurldecode再rawurlencode,防止含有中文或空格的文件名被双重编码 - 格式转换:JPEG → WebP(80% 质量),PNG → PNG(90% 质量),GIF 保持原格式
- 缓存优先:只有缓存未命中时才请求 imgproxy,后续请求直接走本地文件
- 降级策略:如果 imgproxy 不可用,fallback 到公网 imgproxy URL 让浏览器直接加载
2. 预生成预览图 —— liigoo_generate_previews()
这个函数在文章保存时调用,一次性生成三张固定尺寸的预览图:
function liigoo_generate_previews($originalUrl, $oss = 0) {
$sizes = [400, 600, 800];
$previews = [];
foreach ($sizes as $width) {
// 构建 imgproxy 处理参数
$processing = "rs:fit:{$width}:0:1:0/g:sm/enlarge:0/q:{$quality}/f:{$format}";
// 调用 imgproxy 获取处理后的图片
$imgData = file_get_contents($imgproxyUrl);
if ($oss) {
// 上传到七牛云 CDN
$qiniuKey = "blog/uploads/{$date}/{$hash}_{$width}.{$format}";
$cdnUrl = liigoo_upload_to_qiniu($tmpFile, $qiniuKey);
$previews[$width] = $cdnUrl;
} else {
// 保存到本地缓存
file_put_contents($cacheFile, $imgData);
$previews[$width] = $localUrl;
}
}
return $previews; // ['400' => '...', '600' => '...', '800' => '...']
}两种模式的选择:
- 本地模式(
oss=0):预览图保存在服务器本地/app/usr/cache/thumb/,适合小流量站点 - CDN 模式(
oss=1):预览图上传七牛云,通过 CDN 分发,适合需要加速的场景
3. 统一入口 —— liigoo_preview_url()
这是模板调用的统一入口,自动选择最佳预览图:
function liigoo_preview_url($originalUrl, $width, $post = null) {
// 检查是否有预生成的预览图
$settings = liigoo_image_settings($post);
if (!empty($settings['previews'][$originalUrl])) {
// 精确匹配或选最接近的较大尺寸
// 400px 请求 → 优先返回 400px,其次 600px、800px
}
// 无预生成 → 回退到动态 imgproxy
return liigoo_thumb_url($originalUrl, $width);
}URL 签名机制
imgproxy 使用 HMAC-SHA256 对处理 URL 进行签名,防止恶意构造请求消耗服务器资源。签名流程:
签名 = Base64(HMAC-SHA256(SALT + 处理路径, KEY))
处理路径 = /处理参数/Base64(源图URL)例如,将 https://cdn.liigoo.net/photo.jpg 压缩为 400px WebP 的完整 URL:
http://192.168.123.5:8081/{签名}/rs:fit:400:0:1:0/g:sm/q:80/f:webp/{Base64(源图URL)}KEY 和 SALT 各 32 字节,通过环境变量注入,不写入代码。即使有人知道你的 imgproxy 地址,没有 KEY/SALT 也无法构造有效请求。
处理参数详解
我的博客使用以下核心参数组合:
| 参数 | 含义 | 我的设置 |
|---|---|---|
rs:fit:{w}:{h}:1:0 | 等比例缩放,不放大 | 根据场景设置宽度 |
g:sm | 智能锐化(Sharpening with masking) | 始终启用 |
enlarge:0 | 禁止放大(小图不拉伸) | 始终启用 |
q:{n} | JPEG/WebP 质量 | WebP 80%, PNG 90% |
f:{fmt} | 输出格式 | JPEG→WebP, PNG→PNG |
为什么选这些参数:
enlarge:0至关重要——如果用户上传了 300px 的小图,却请求 800px 的缩略图,imgproxy 默认会放大。对于照片站这是灾难(模糊+锯齿),必须禁止g:sm比单纯sharpen更好——带 masking 的锐化只锐化边缘区域,不会把噪点也锐化出来- WebP 而非 AVIF:AVIF 压缩率更高但编码慢 5-10 倍,实时处理场景下不可接受。WebP 是当前性价比最优解
缓存策略
缓存是整个管线的核心——imgproxy 再快也快不过直接读文件。
缓存目录结构:
/app/usr/cache/thumb/
├── a1b2c3d4e5f6.webp # MD5(源URL|宽度|高度|格式|质量)
├── f7e8d9c0b1a2.webp
└── ...- 缓存 key = MD5(源 URL + 所有处理参数),保证不同参数不会互相覆盖
- 永久缓存:源图不变则处理结果不变,无需过期策略
- 手动清理:
rm -rf /app/usr/cache/thumb/*强制重新生成 - Warm-up:文章发布时预生成预览图,读者访问时永远命中
CDN 集成
对于需要加速的场景,预览图直接上传七牛云并通过 CDN 分发:
function liigoo_upload_to_qiniu($localPath, $key) {
// 使用原始 HMAC-SHA1 签名 API,无需 SDK
$policy = ['scope' => "liigoo:$key", 'deadline' => time() + 3600];
$sign = hmac_sha1(base64($policy), $secretKey);
$token = "$accessKey:$sign:$encodedPolicy";
// POST 上传
curl_post('https://up-z0.qiniup.com/', [
'token' => $token,
'key' => $key,
'file' => new CURLFile($localPath),
]);
return "https://cdn.liigoo.net/$key";
}不需要 SDK——七牛云的 HMAC-SHA1 签名上传 API 只需要几十行代码。
部署配置
imgproxy 用 Docker 部署,一行命令搞定:
docker run -d --name imgproxy --restart always \
-p 8081:8080 \
-e IMGPROXY_KEY=9436f8873b0b9c2d8e5a7f1c3d4e6a8b \
-e IMGPROXY_SALT=1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d \
-e IMGPROXY_MAX_SRC_RESOLUTION=150 \
-e IMGPROXY_ENABLE_WEBP_DETECTION=1 \
darthsim/imgproxy:latest几个重要的环境变量:
IMGPROXY_MAX_SRC_RESOLUTION:源图最大分辨率(MP),我设置为 150,因为某些摄影作品分辨率很高IMGPROXY_KEY / IMGPROXY_SALT:签名密钥,与 PHP 端配置一致- 容器只监听
192.168.123.5:8081,不暴露公网端口——所有请求由 PHP 端代理
PHP 端通过可拔插配置管理 imgproxy 连接:
// usr/themes/liigoo/config.php
'imgproxy' => [
'endpoint' => getenv('LIIGOO_IMGPROXY_ENDPOINT') ?: 'http://192.168.123.5:8081',
'key' => getenv('LIIGOO_IMGPROXY_KEY') ?: '...',
'salt' => getenv('LIIGOO_IMGPROXY_SALT') ?: '...',
],所有配置支持环境变量覆盖,本地开发用 config.local.php 覆盖。
效果对比
以一个摄影文章页面(11 张照片)为例:
| 指标 | 优化前(原图 CDN) | 优化后(imgproxy + 缓存) |
|---|---|---|
| 单张图片大小 | 8-15 MB (JPEG) | 80-150 KB (WebP) |
| 首屏图片总大小 | ~120 MB | ~1.5 MB |
| 首屏加载时间 | 8-12 秒 | 1.2-1.8 秒 |
| CDN 月流量 | ~80 GB | ~6 GB |
| 服务端 CPU | 0%(直接输出) | <0.5%(首次生成后走缓存) |
关键数字:图片体积减少 95%+,加载时间缩短 80%+。
与其它方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手动压缩 | 完全可控 | 费时费力,改版要重新处理 |
| Cloudinary/imgix | SaaS 省心 | 贵,图片在第三方服务器 |
| Nginx image_filter | 无额外服务 | 功能极简,不支持 WebP |
| imgproxy | 自托管,功能全,性能好 | 需要自己部署维护 |
对于自托管博客来说,imgproxy 是功能性和可控性的最佳平衡点。
总结
imgproxy 是我博客基础设施中最满意的组件之一。它解决了图片处理的痛点,同时几乎没有增加运维负担——Docker 部署后基本上不需要管它。
几条核心经验:
- 缓存是关键:永远不要让 imgproxy 重复处理同一张图。一次处理 + 永久缓存 = 零开销
- 内网直连:PHP 和 imgproxy 之间走内网,延迟低至毫秒级。不要走公网
- 签名必须开启:没有签名的 imgproxy 等于公开的计算资源,会被恶意利用
enlarge:0别忘:小图被放大是图片处理中最常见的 bug- WebP 是当前最优解:压缩率接近 AVIF,编码速度远超 AVIF
如果你也在维护一个需要处理大量图片的网站,强烈推荐试试 imgproxy。
imgproxy 官网:https://imgproxy.net
本文架构图:FigJam
暂无评论