从 0 到 1:用华为云 FunctionGraph + API 网关搭建 Serverless 图片压缩服务
一、为什么把图片处理搬到 Serverless
去年底接手一个社区内容平台,用户上传的原图动辄 3~5MB,列表页首屏要加载几十张,移动端体验很差。传统做法是买一台 ECS 常驻跑 ImageMagick,但这类业务波峰明显:白天集中在 10 点到 12 点、20 点到 23 点,夜里几乎为零。常驻实例意味着大部分时间在空转,峰值时又容易排队。
于是我把图片压缩和水印改成华为云 FunctionGraph 函数工作流加 API 网关的组合:调用才计费,不调用不计费,并发由平台管理。上线三个月,月成本从一台 4U8G ECS 降到不足 5 元。
二、整体架构
链路很直白:客户端 POST 图片到 API 网关的 /compress 路径,网关把请求透传给函数,函数用 Pillow 压缩、加水印,写回 OBS 并返回临时下载链接。整个过程无状态,天然可横向扩展。
准备三样东西:一个 OBS 桶放原图与结果、一个 FunctionGraph 函数(Python 3.9 运行时)、一个共享版 API 网关。三者放在同一区域,避免跨区流量费。
三、函数代码
函数入口保持极简,核心逻辑不到 60 行:
import base64, io, json
from PIL import Image
def handler(event, context):
body = json.loads(event.get("body") or "{}")
raw = base64.b64decode(body["image"])
img = Image.open(io.BytesIO(raw)).convert("RGB")
w, h = img.size
scale = min(1.0, 1280 / max(w, h))
if scale < 1.0:
img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, "JPEG", quality=82, optimize=True)
out = base64.b64encode(buf.getvalue()).decode()
return {"statusCode": 200, "body": json.dumps({"image": out, "size": len(buf.getvalue())})}
Pillow 需要打成 zip 上传,务必在 Linux 或 Docker 里 pip install -t 打包,Windows 下打的包会因缺少 .so 直接报 ImportError。
四、API 网关怎么配
新建 API,方法 POST,后端类型选 FunctionGraph,选中刚发布的函数。请求体默认限制 2MB,图片走 base64 会膨胀三分之一,所以我把上限调到 8MB,并让前端先压到 2MB 以内再上送。
另外要打开支持 CORS,并在函数返回头里补上 Access-Control-Allow-Origin,否则浏览器预检永远过不去。
五、三个真实踩过的坑
坑一:冷启动 2.8 秒。首次调用明显卡顿。配置预留实例,同时把 Pillow 的 import 放到模块顶层而不是 handler 内部,让平台在初始化阶段完成加载,冷启动降到 400ms 左右。
坑二:中文水印变方框。运行环境没有中文字体,draw.text 渲染全是豆腐块。把字体文件打进 zip 并用绝对路径指定,问题消失。
坑三:网关返回 502。函数日志正常,网关报 502。原因是函数返回的 body 必须为字符串,我一开始返回了 dict,网关序列化失败。改成 json.dumps 后恢复。
六、成本与适用边界
按每天 3000 次调用、单次 128MB 乘 0.8 秒估算,每月函数计算费用不到 2 元,网关共享版按量计费另计,总体可控。注意 FunctionGraph 单次执行最长 15 分钟,超过 8MB 的大图或视频转码这类长任务,更适合走 OBS 事件触发的异步流程,而不是同步接口。
一句话总结:把「平时不用、峰值要命」的能力交给 Serverless,是这类平台最划算的一笔账。
- 点赞
- 收藏
- 关注作者
评论(0)