在做 Web 开发,图片处理通常绕不开这些方案:

  • 阿里云 OSS 图片处理

  • 七牛云(imageView2)

  • 又拍云(云处理)

  • 或者自研图片服务

这些方案确实好用,但也有一些现实问题:

  • 流量费用高,请求量一大账单很难看

  • 处理规则受限制,很多高级能力要额外付费

  • 厂商绑定严重,迁移成本极高

  • 出海 / 私有化困难,数据无法完全自控

今天介绍一个更"可控"的方案:imgproxy(自建图片处理服务)


一、imgproxy 是什么?

imgproxy 是一个通过 URL 实时处理图片的开源服务,由 Evil Martians 团队使用 Go 语言开发,底层依赖性能极强的 libvips 图像库。

你只需要:

  1. 提供原图 URL

  2. 拼接处理参数到 imgproxy URL

  3. imgproxy 实时拉取原图、处理后返回

http://imgproxy/rs:fit:300:300/plain/https://xxx.com/a.jpg

就能得到一张 300×300、自动适配比例的图片。整个过程无需预先生成缩略图,也无需在后端写任何逻辑。

两个版本:

版本

说明

价格

开源版(OSS)

功能完整,覆盖绝大多数场景

免费

商业版(Pro)

额外支持 PDF、SVG、视频帧等

付费订阅

对于绝大多数团队,开源版已经足够。


二、核心能力详解

支持的图片格式

imgproxy 开源版支持的输入/输出格式相当丰富:

类型

格式

输入

JPEG, PNG, GIF, WebP, AVIF, HEIC, BMP, TIFF, SVG, ICO

输出

JPEG, PNG, GIF, WebP, AVIF, ICO, BMP, TIFF

其中 AVIF 支持是一个重大亮点。AVIF 是目前压缩率最高的图片格式,相比 WebP 再节省 20–50% 体积,但国内各大云厂商的免费套餐普遍不包含 AVIF 转码能力。

URL 参数语法速查

imgproxy 的 URL 结构如下:

http://imgproxy/{签名}/{处理参数}/plain/{原图URL}

常用参数组合举例:

# 等比缩放,最长边不超过 500px
/rs:fit:500:500/

# 强制裁剪为 400x300,从中心取
/rs:fill:400:300/g:sm/

# 转换为 WebP 格式
/rs:fit:800:0/f:webp/

# 添加水印(需配置水印图)
/rs:fit:800:0/wm:0.8:soea:10:10/

# 图片质量压缩到 80%
/rs:fit:800:0/q:80/

# 锐化
/rs:fit:800:0/sh:0.5/

# 旋转 90 度
/rs:fit:800:0/rot:90/

# 组合:缩放 + 转 WebP + 压缩质量
/rs:fill:400:300/f:webp/q:85/

imgproxy 参数是可以自由组合的,这正是它比云厂商方案灵活得多的原因。


三、国内常见方案对比

1️⃣ vs 阿里云 OSS 图片处理

OSS 写法示例

# 缩放
https://bucket.oss-cn-hangzhou.aliyuncs.com/a.jpg?x-oss-process=image/resize,w_300

# 裁剪
https://bucket.oss-cn-hangzhou.aliyuncs.com/a.jpg?x-oss-process=image/crop,w_300,h_300

# 转格式 + 压缩
https://bucket.oss-cn-hangzhou.aliyuncs.com/a.jpg?x-oss-process=image/format,webp/quality,q_80

OSS 成本分析

以一个月均 1 亿次图片请求、平均原图 500KB 的业务为例:

费用项

估算

图片处理费

≈ 250 元(0.0025元/千次)

外网流量费

≈ 4000 元(0.5元/GB,约8TB)

CDN 回源流量

另计

合计

≈ 4000–6000 元/月

换成 imgproxy + 2 核 4G 服务器(约 200 元/月),处理后图片走 CDN 缓存,成本差距显著。

对比总结

维度

OSS

imgproxy

成本

按量计费,越用越贵

服务器固定成本

灵活性

参数集有限,组合受限

几乎无限组合

AVIF 支持

不支持(免费版)

支持

水印能力

基础文字/图片水印

支持位置、透明度精细控制

迁移难度

中等(需修改 URL)

结论:

  • 小项目、流量有限:OSS 更省事

  • 日均请求超过 500 万次:imgproxy 开始显现成本优势

  • 有私有化需求:只能选 imgproxy


2️⃣ vs 七牛云(imageView2)

七牛参数示例

# 缩放裁剪
?imageView2/1/w/300/h/300

# 质量压缩
?imageView2/2/w/800/q/80

# 转 WebP
?imageView2/2/w/800/format/webp

七牛的隐藏限制

七牛的图片处理看似免费,但实际上:

  • 免费空间有带宽限制,超出后按流量计费

  • 高级能力(如 AVIF 转码、SVG 渲染)属于增值服务

  • 样式别名(URL 美化)和图片审核均需额外付费

  • 免费套餐中 HTTP 访问免费,HTTPS 流量另计费

维度

七牛

imgproxy

AVIF 输出

不支持

支持

自定义水印位置

有限

像素级控制

私有化部署

不支持

完整支持

处理链路组合

有限

灵活


3️⃣ vs 又拍云

又拍云的图片处理接口叫"云处理(UPYUN Processing)",能力较为完整,但:

  • 免费额度主要是存储,图片处理按请求另计

  • 配置学习曲线较高,操作台不如 OSS 直观

  • 私有化版本功能受限,价格昂贵

又拍云更适合视频处理场景(其视频转码生态更完善)。纯图片处理场景,性价比不如 imgproxy。


4️⃣ vs 自研方案

很多公司的"图片服务"是这样长的:

// Java + Thumbnailator
Thumbnails.of(inputFile)
    .size(300, 300)
    .outputFormat("webp")
    .toFile(outputFile);

或者:

# Python + Pillow
from PIL import Image
img = Image.open("input.jpg")
img.thumbnail((300, 300))
img.save("output.webp", "WEBP", quality=85)

自研方案的典型问题

性能瓶颈

方案

处理 1000 张 2MP 图片耗时

ImageMagick

~45 秒

Pillow

~30 秒

libvips(imgproxy 底层)

~8 秒

libvips 的核心优势是流式处理:它不会把整张图片加载到内存,而是按需读取像素块,内存占用仅为同等分辨率 ImageMagick 的 1/5。

架构复杂度

自研方案通常最终演变成:

上传服务 → 消息队列(RabbitMQ/Kafka)
        → 图片处理 Worker(水平扩展)
        → 处理结果存储(OSS/MinIO)
        → CDN 刷新通知
        → 数据库记录处理状态

而 imgproxy 把这条链路压缩成一个无状态的 HTTP 服务,不需要队列,不需要 Worker,不需要提前处理——用户请求时实时生成,CDN 缓存后再也不用处理第二次。


四、为什么推荐 imgproxy?

核心定位:云厂商图片处理的平替 + 自研方案的升级版

性能数据

官方 Benchmark(8 核 CPU,处理 JPEG 缩放到 400×300):

并发数

RPS

平均延迟

P99 延迟

10

320

31ms

58ms

50

890

56ms

110ms

100

1100

91ms

180ms

配合 CDN 缓存后,99% 的请求延迟降低到 5ms 以内(直接命中 CDN)。

安全机制

imgproxy 内置了完整的安全防护:

URL 签名(防止恶意构造请求):

# 设置密钥
IMGPROXY_KEY=your_hex_key
IMGPROXY_SALT=your_hex_salt

# 签名计算(HMAC-SHA256)
signature = HMAC-SHA256(key, salt + path)

来源限制

# 只允许处理特定域名的图片
IMGPROXY_ALLOWED_SOURCES=https://cdn.yourcompany.com,https://oss.yourcompany.com

尺寸限制(防止爆内存攻击):

IMGPROXY_MAX_SRC_RESOLUTION=16.8  # 最大 16.8MP
IMGPROXY_MAX_RESULT_DIMENSION=8192  # 输出最大 8192px

五、快速上手

方式一:Docker 快速启动

docker run -p 8080:8080 \
  -e IMGPROXY_KEY="your_key_hex" \
  -e IMGPROXY_SALT="your_salt_hex" \
  -e IMGPROXY_ALLOWED_SOURCES="https://" \
  ghcr.io/imgproxy/imgproxy:latest

访问测试(无签名模式,仅开发用):

http://localhost:8080/rs:fit:300:300/plain/https://picsum.photos/800/600

方式二:docker-compose 生产配置

version: "3.8"
services:
  imgproxy:
    image: ghcr.io/imgproxy/imgproxy:latest
    ports:
      - "8080:8080"
    environment:
      # 安全配置
      - IMGPROXY_KEY=your_64_char_hex_key
      - IMGPROXY_SALT=your_64_char_hex_salt

      # 限制来源,只允许处理自己的 OSS/MinIO
      - IMGPROXY_ALLOWED_SOURCES=https://your-bucket.oss-cn-hangzhou.aliyuncs.com

      # 性能配置
      - IMGPROXY_CONCURRENCY=20
      - IMGPROXY_MAX_SRC_RESOLUTION=16.8
      - IMGPROXY_JPEG_PROGRESSIVE=true

      # 自动转 WebP(如果客户端支持)
      - IMGPROXY_AUTO_WEBP=true
      - IMGPROXY_AUTO_AVIF=true

      # 日志
      - IMGPROXY_LOG_FORMAT=json
      # 水印
      - IMGPROXY_WATERMARK_PATH=https://your-oss.com/watermark.png
    restart: always
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: "2G"

生成签名 URL(Node.js 示例)

const crypto = require('crypto');

function signImgproxyUrl(path, key, salt) {
  const keyBin = Buffer.from(key, 'hex');
  const saltBin = Buffer.from(salt, 'hex');
  const hmac = crypto.createHmac('sha256', keyBin);
  hmac.update(saltBin);
  hmac.update(Buffer.from(path));
  return hmac.digest('base64url');
}

function buildImgproxyUrl(baseUrl, originalUrl, width, height, key, salt) {
  const encodedUrl = Buffer.from(originalUrl).toString('base64url');
  const path = `/rs:fill:${width}:${height}/plain/${encodedUrl}`;
  const signature = signImgproxyUrl(path, key, salt);
  return `${baseUrl}/${signature}${path}`;
}

// 使用示例
const url = buildImgproxyUrl(
  'https://img.yourcompany.com',
  'https://oss.yourcompany.com/products/shoe.jpg',
  400, 300,
  process.env.IMGPROXY_KEY,
  process.env.IMGPROXY_SALT
);
// 返回:https://img.yourcompany.com/{签名}/rs:fill:400:300/plain/...

Python 签名示例

import hmac
import hashlib
import base64

def build_imgproxy_url(base_url, original_url, width, height, key_hex, salt_hex):
    key = bytes.fromhex(key_hex)
    salt = bytes.fromhex(salt_hex)

    encoded_url = base64.urlsafe_b64encode(original_url.encode()).decode().rstrip('=')
    path = f"/rs:fill:{width}:{height}/plain/{encoded_url}"

    digest = hmac.new(key, salt + path.encode(), hashlib.sha256).digest()
    signature = base64.urlsafe_b64encode(digest).decode().rstrip('=')

    return f"{base_url}/{signature}{path}"

六、生产环境架构设计

推荐架构(国内)

用户请求
    ↓
阿里云 CDN / 腾讯云 CDN
    ↓ (Cache Miss)
imgproxy 集群(2-4 台,ECS/容器)
    ↓ (回源拉取原图)
OSS / MinIO(存储原图)

CDN 缓存策略建议:

# Nginx 反代 imgproxy,添加缓存头
location /img/ {
    proxy_pass http://imgproxy:8080/;
    proxy_cache_valid 200 30d;
    add_header Cache-Control "public, max-age=2592000";
    add_header Vary "Accept";  # 根据 Accept 头区分 WebP/AVIF 缓存
}

Vary: Accept 是关键——同一张图片对支持 WebP 的浏览器和不支持的浏览器,imgproxy 会返回不同格式,CDN 需要分别缓存。

私有化场景架构

用户(内网)
    ↓
Nginx(内网反代,鉴权)
    ↓
imgproxy(内网,无公网)
    ↓
MinIO(本地对象存储)

这个架构完全不依赖任何公网云厂商,适合金融、医疗、政务等数据合规要求高的场景。


七、常见使用场景及 URL 写法

电商商品图

# 列表页缩略图:统一 400x400,填充模式,白底
/rs:fill:400:400/bg:ffffff/f:webp/q:85/

# 详情页大图:宽度 800,保持比例
/rs:fit:800:0/f:webp/q:90/

# 移动端:宽度 375,节省流量
/rs:fit:375:0/f:webp/q:80/

用户头像

# 标准圆形头像(前端 CSS 实现圆形,imgproxy 裁正方形)
/rs:fill:200:200/g:sm/f:webp/

# 缩略头像 40x40
/rs:fill:40:40/g:sm/f:webp/q:90/

内容平台文章图

# 文章封面:1200x630(Open Graph 标准尺寸)
/rs:fill:1200:630/f:jpeg/q:85/

# 文章内图:限宽 800,超长自动截断
/rs:fit:800:0/f:webp/q:85/

水印场景

# 右下角水印,透明度 0.8,距边缘 20px
/rs:fit:800:0/wm:0.8:soea:20:20/f:webp/

# 水印图需要预先配置到 imgproxy
IMGPROXY_WATERMARK_URL=https://your-oss.com/watermark.png

八、监控与运维

健康检查接口

# imgproxy 内置健康检查
curl http://imgproxy:8080/health
# 返回 200 即正常

常见问题排查

现象

可能原因

解决方案

返回 403

URL 签名错误

检查 key/salt 是否匹配

返回 422

原图 URL 不在白名单

检查 ALLOWED_SOURCES 配置

处理很慢

并发数设置过低

调高 IMGPROXY_CONCURRENCY

内存 OOM

处理超大图

设置 MAX_SRC_RESOLUTION

AVIF 不生效

libvips 版本不够

使用官方镜像而非自编译


九、迁移策略:从 OSS 切换到 imgproxy

迁移不需要一刀切,可以平滑过渡:

第一步:部署 imgproxy,URL 签名验证测试通过

第二步:前端新上传的图片改用 imgproxy URL,存量 OSS URL 保持不变

第三步:在 Nginx 层做 URL 重写,老的 OSS 处理 URL 自动转换为 imgproxy URL:

# 把 OSS 的处理 URL 重写为 imgproxy
rewrite ^/(.+)\?x-oss-process=image/resize,w_(\d+)$
         /rs:fit:$2:0/plain/https://your-oss.com/$1 redirect;

第四步:观察监控,确认正常后关掉 OSS 图片处理功能


十、适用场景判断

非常适合:

  • 电商平台:大量 SKU 图,需要多种尺寸(列表/详情/缩略图),imgproxy 按需生成,无需预生成全部变体

  • SaaS 多租户系统:各租户有不同 Logo、头像、附件,统一走 imgproxy 处理,逻辑集中

  • 内容平台(CMS):文章图片需要适配 PC / 移动端多种布局,动态出图更灵活

  • 数据可视化平台:截图服务生成的图片需要再处理压缩

  • 对数据合规有要求的行业:金融、医疗,图片不能过第三方服务

不推荐的场景:

  • 个人项目 / 小工具:流量小,直接用 OSS 处理省心

  • 团队没有 DevOps 能力:imgproxy 需要维护服务,监控,扩容

  • 需要视频处理:imgproxy 专注图片,视频还是用专门服务


十一、总结与选型建议

一句话总结:

imgproxy = 去云厂商化的图片处理方案,越大的系统越值得用

选型决策树:

业务规模小、没有运维能力?
    → OSS / 七牛,省事第一

有私有化 / 数据合规要求?
    → imgproxy,没得选

日均图片请求 > 500万次?
    → 算一算云处理费用,imgproxy ROI 通常很高

已有自研图片服务、性能是瓶颈?
    → 用 imgproxy 替换,接入成本低

imgproxy 不是"银弹",但在国内技术团队中确实被长期低估。当你的系统规模到了一定程度,图片处理的成本和灵活性都会成为真实痛点——而那正是 imgproxy 最擅长的地方。


参考资料: