Serverless 实战:用华为云 FunctionGraph + OBS 构建弹性图片处理流水线
图片处理是很多业务的"隐形基础设施":商品图要生成多规格缩略图,用户头像要裁剪压缩,内容平台要统一转码。传统做法是部署一台常驻的图片处理服务,但它的资源曲线很尴尬——白天峰值时 CPU 打满,深夜又长期闲置。本文以华为云对象存储 OBS 与函数工作流 FunctionGraph 为例,搭建一条"上传即处理"的事件驱动流水线,把服务器的运维成本换成按需计费。
一、为什么选择事件驱动
图片处理有三个典型特征:触发时机随机、单次任务短、峰值差异大。这正好落在 Serverless 的能力区间里。把 OBS 的 ObjectCreated 事件作为触发器,函数只在有新对象写入时启动,处理完立即释放资源。业务侧不需要预估容量,也不需要维护常驻进程;流量低谷时成本趋近于零,突然放量时由平台自动横向扩展。
整条链路可以概括为四步:
- 客户端把原图上传到 OBS 的
uploads/前缀; - OBS 事件通知投递到 FunctionGraph;
- 函数下载原图、生成缩略图并写回
thumbs/; - 业务系统直接读取缩略图地址。
二、准备资源与权限
先创建两个 OBS 桶:一个接收原图,一个存放处理结果。分开建桶有两个好处:便于设置不同的生命周期策略,也能避免输出对象再次触发函数形成回环。
权限方面,推荐使用 IAM 委托(agency)而不是在代码里硬编码永久 AK/SK。给函数绑定一个只允许访问指定桶的委托,权限收敛到 obs:object:GetObject 与 obs:object:PutObject 即可。临时凭证由平台自动轮换,泄露风险远低于写死在环境变量里的密钥。
三、函数代码实现
函数选择 Python 运行时,依赖 esdk-obs-python 与 Pillow。把依赖打包进代码包,或放到 OBS 后通过层(Layer)挂载。
import io
import os
from obs import ObsClient
from PIL import Image
ENDPOINT = os.environ["OBS_ENDPOINT"]
OUTPUT_BUCKET = os.environ["OUTPUT_BUCKET"]
client = ObsClient(
access_key_id=os.environ["AK"],
secret_access_key=os.environ["SK"],
server=ENDPOINT,
)
def handler(event, context):
for record in event.get("Records", []):
bucket = record["obs"]["bucket"]["name"]
key = record["obs"]["object"]["key"]
# 只处理上传前缀,防止输出桶回环触发
if not key.startswith("uploads/"):
continue
resp = client.getObject(bucket, key)
if resp.status >= 300:
raise RuntimeError("download failed: %s" % resp.status)
img = Image.open(io.BytesIO(resp.body.read()))
img.thumbnail((1280, 1280))
buf = io.BytesIO()
img.convert("RGB").save(buf, format="JPEG", quality=85, optimize=True)
name = os.path.splitext(key.split("/", 1)[1])[0]
client.putContent(OUTPUT_BUCKET, "thumbs/%s.jpg" % name, buf.getvalue())
return {"ok": True}
四、配置 OBS 事件触发器
在函数详情页创建触发器,类型选择 OBS,绑定原图桶,事件类型勾选 ObjectCreated,并设置前缀过滤 uploads/。建议只订阅需要处理的后缀(如 .jpg、.png),减少无效调用。
触发器创建完成后,上传一张测试图片验证:函数日志中应出现本次对象的 bucket 与 key,输出桶的 thumbs/ 目录下会生成同名 JPEG。
五、生产环境必须处理的六个细节
1. 防回环。 如果输入输出使用同一个桶且不做前缀区分,缩略图写入会再次触发函数,形成无限循环。最稳妥的做法是输入输出分桶,并在代码里做前缀白名单。
2. 幂等。 OBS 事件是"至少一次"投递,网络抖动时同一对象可能被处理多次。可以用 对象 key + ETag 作为幂等键,处理前先检查输出对象是否存在且大小符合预期。
3. 超时与内存。 单张图片处理建议配置 512MB 内存、30 秒超时。内存过小会导致 Pillow 解码大图时被 OOM kill;超时过长则故障时占用并发配额。超大图应拆分或转为异步批处理。
4. 冷启动。 对延迟敏感的场景,为函数配置预留实例,避免首批请求等待运行时初始化。也可以通过定时触发器低频保活,但要注意保活本身也产生费用。
5. 失败重试与死信。 异步调用配置 1~2 次重试;仍失败的事件写入死信桶并触发告警,避免"图片悄悄没处理"却无人知晓。
6. 可观测性。 函数日志接入 LTS,结构化打印 requestId、bucket、key、耗时;在 CES 上为错误率、超时次数、并发使用率配置告警阈值。排障时先看日志再复现,比盲目重试高效得多。
六、成本视角的对比
| 维度 | 常驻 ECS 服务 | FunctionGraph + OBS |
|---|---|---|
| 固定成本 | 实例按月计费,闲时也付费 | 无请求时几乎为零 |
| 弹性速度 | 依赖伸缩组,分钟级 | 平台自动扩展,秒级 |
| 运维投入 | 系统补丁、进程守护、容量规划 | 只需维护函数代码 |
| 适用场景 | 稳定高并发、长连接 | 突发、低频、事件驱动 |
需要说明的是,Serverless 并非银弹。当图片处理 QPS 长期稳定在高位时,函数按 GB-s 累计的费用可能超过包年包月的 ECS;此时把热点路径迁回容器或裸金属反而更经济。架构选型应该跟着负载曲线走,而不是跟着概念走。
结语
用 OBS 事件触发 FunctionGraph,本质上是把"轮询任务"改造成"事件响应":少了一层调度器,也少了一台需要守护的服务器。真正决定这套方案能否长期稳定运行的,不是函数代码本身,而是防回环、幂等、重试与可观测这些工程细节。把这些约束提前写进模板和检查清单,Serverless 才能从演示 Demo 变成可以托付生产的工作负载。
- 点赞
- 收藏
- 关注作者
评论(0)