图片处理秒级自动化:用华为云 FunctionGraph 搭一套 OBS 事件触发的懒加载水印流水线
【摘要】 摘要:用户上传图片后要加水印、生成缩略图、做敏感内容初筛,传统做法是后端轮询或定时批处理,延迟高、空闲期还烧机器。本文以"UGC 图片上传自动加水印"为场景,基于华为云函数工作流 FunctionGraph + 对象存储 OBS 事件通知,搭一条"上传即触发、按需执行、按调用计费"的图片处理流水线,单张图片端到端处理耗时压到 300ms 内,闲置期零计算成本。一、背景与痛点社区类 App 的...
摘要:用户上传图片后要加水印、生成缩略图、做敏感内容初筛,传统做法是后端轮询或定时批处理,延迟高、空闲期还烧机器。本文以"UGC 图片上传自动加水印"为场景,基于华为云函数工作流 FunctionGraph + 对象存储 OBS 事件通知,搭一条"上传即触发、按需执行、按调用计费"的图片处理流水线,单张图片端到端处理耗时压到 300ms 内,闲置期零计算成本。
一、背景与痛点
社区类 App 的 UGC 图片每天新增 20-50 万张,上传后后端需要做四件事:
- 生成 3 档缩略图(大图/中图/小图)
- 打半透明水印(防盗图)
- 转 WebP 格式(省 30% 带宽)
- 调内容审核接口做初筛
原架构是一台常驻 ECS 跑 Celery Worker 消费 MQ:
|
痛点
|
具体表现
|
|---|---|
|
高峰排队
|
晚 8-10 点上传洪峰,队列积压 5-10 分钟,用户发完图看不到自己那条动态
|
|
低谷浪费
|
凌晨 2-6 点 QPS 不到 5,8 台 Worker 全空转,按月付整机费
|
|
扩容迟钝
|
突发热点(明星发帖)时手动加机器要 3-5 分钟,等加完峰值已过
|
|
单点故障
|
Worker 挂了没人知道,图片堆在 MQ 里几小时才发现
|
|
依赖膨胀
|
Pillow、OpenCV 全塞一个镜像,包体 1.2GB,冷启动慢
|
核心诉求:事件驱动、毫秒级弹性、按调用付费、失败可重试可观测。
二、方案设计
2.1 为什么选 FunctionGraph 而不是继续用 Worker
|
维度
|
常驻 Worker
|
容器 Job
|
FunctionGraph
|
|---|---|---|---|
|
弹性速度
|
分钟级
|
30 秒级
|
毫秒级
|
|
空闲成本
|
全额付
|
全额付(除非缩容到 0)
|
0 元
|
|
运维
|
自己管
|
自己管 K8s
|
全托管
|
|
单请求成本
|
摊薄后低
|
中
|
短任务极优
|
|
冷启动
|
无
|
有
|
有(可预热)
|
图片处理是典型"短耗时、高并发、潮汐明显"负载,函数计算的主场。
2.2 整体架构
用户 App
│ PUT 原图
▼
OBS 桶 src-bucket
│ ObjectCreated 事件通知
▼
FunctionGraph(image-pipeline)
├─ 下载原图(OBS SDK,走内网)
├─ Pillow 生成 3 档缩略图
├─ 叠加水印
├─ 转 WebP
├─ 调 Moderation 接口初筛
└─ 回传 OBS thumb-bucket
│
▼
OBS thumb-bucket ──► CDN 加速 ──► App 展示
│
└─ 失败 ──► 死信队列 OBS dead-bucket ──► 告警
2.3 关键设计点
- 事件通知直连:OBS 桶配置
ObjectCreated事件直接触发函数,不经过 MQ,链路最短 - 函数内网访问 OBS:走 VPC 端点,不走公网,省流量费也省延迟
- 水印底图放函数包内:避免每次执行再下载,冷启动一次加载进内存复用
- 敏感图短路:审核不过的直接打标移到 review-bucket,不生成缩略图
- 失败分级重试:网络类错误函数内部重试 2 次,仍失败由 OBS 事件通知重投 3 次,最终进死信桶
三、实操步骤
3.1 准备 OBS 桶
创建三个桶,全部同 Region:
obsutil mb obs://src-bucket # 原图
obsutil mb obs://thumb-bucket # 处理结果
obsutil mb obs://dead-bucket # 死信
src-bucket 开启事件通知,指向稍后创建的函数:- 事件类型:
ObjectCreated:Put、ObjectCreated:Post - 前缀过滤:
upload/(只处理用户上传目录,后台导入图不触发) - 后缀过滤:
.jpg,.jpeg,.png,.webp
3.2 编写处理函数
运行时选 Python 3.9,内存 512MB,超时 30 秒。核心代码:
import io, os, json
from PIL import Image, ImageDraw, ImageFont
from huaweicloudsdkobs.v1 import ObsClient
import requests
obs = ObsClient(
access_key_id=os.environ['AK'],
secret_access_key=os.environ['SK'],
endpoint='obs.cn-north-4.myhuaweicloud.com'
)
SRC, THUMB, DEAD = 'src-bucket', 'thumb-bucket', 'dead-bucket'
WM = Image.open('/opt/watermark.png').convert('RGBA')
FONT = ImageFont.truetype('/opt/DejaVuSans.ttf', 20)
SIZES = [('lg', 1200), ('md', 640), ('sm', 320)]
def add_watermark(img):
layer = Image.new('RGBA', img.size, (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
w, h = img.size
d.text((w - 140, h - 40), '@community', fill=(255,255,255,128), font=FONT)
return Image.alpha_composite(img.convert('RGBA'), layer).convert('RGB')
def moderate(key):
"""调内容审核,返回 True 表示通过"""
r = requests.post(
'https://moderation.cn-north-4.myhuaweicloud.com/v1/image',
json={'obs_key': key},
headers={'X-Auth': os.environ['MOD_TOKEN']},
timeout=3
)
return r.json().get('suggestion') == 'pass'
def handler(event, context):
logger = context.getLogger()
rec = json.loads(event['Records'][0]) if isinstance(event, str) else event['Records'][0]
bucket = rec['obs']['bucket']['name']
key = rec['obs']['object']['key']
logger.info(f'trigger key={key}')
try:
if not moderate(key):
obs.copyObject(SRC, key, DEAD, key) # 打标移走
logger.warn(f'rejected {key}')
return {'status': 'rejected'}
resp = obs.getObject(SRC, key)
src = Image.open(io.BytesIO(resp.body))
base = key.rsplit('.', 1)[0]
for tag, maxw in SIZES:
img = src.copy()
if img.width > maxw:
ratio = maxw / img.width
img = img.resize((maxw, int(img.height * ratio)), Image.LANCZOS)
out = add_watermark(img)
buf = io.BytesIO()
out.save(buf, 'WEBP', quality=82)
obs.putObject(THUMB, f'{base}_{tag}.webp', buf.getvalue())
buf.close()
return {'status': 'ok', 'outputs': [f'{base}_{t}.webp' for t, _ in SIZES]}
except Exception as e:
logger.error(f'fail {key}: {e}')
obs.putObject(DEAD, f'failed/{key}.json', json.dumps({'key': key, 'err': str(e)}))
raise # 抛出去让 OBS 事件通知重试
依赖打包:
pip install pillow huaweicloud-sdk-obs-v1 requests -t ./deps
cd deps && zip -r9 ../image-pipeline.zip . && cd ..
zip -g image-pipeline.zip index.py watermark.png DejaVuSans.ttf
3.3 函数配置要点
|
配置项
|
值
|
原因
|
|---|---|---|
|
内存
|
512MB
|
1080P 图 Pillow 处理够用,再大冷启动变慢
|
|
超时
|
30s
|
留足审核接口重试余量
|
|
并发上限
|
200
|
防突发把下游审核接口打挂
|
|
冷启动预热
|
开 5 个常驻实例
|
晚高峰首帧延迟从 800ms 降到 120ms
|
|
VPC
|
绑业务 VPC + OBS 端点
|
内网访问,省公网流量
|
|
环境变量
|
AK/SK 走加密变量、MOD_TOKEN 走凭据
|
不硬编码
|
避坑:AK/SK 一定要放 FunctionGraph 的加密环境变量或对接凭据服务,别打进 zip 包——函数包任何人有读权限都能下。
3.4 死信与告警
死信桶
dead-bucket 配一条 OBS 事件通知 → 另一个轻函数 → 推钉钉/飞书:def dead_letter_handler(event, context):
key = json.loads(event['Records'][0])['obs']['object']['key']
requests.post(os.environ['WEBHOOK'], json={
'msgtype': 'text',
'text': {'content': f'图片处理失败入死信:{key}'}
})
云监控再配两条告警:
- 函数
错误次数 > 10/分钟→ 告警 - 函数
平均耗时 > 3 秒持续 5 分钟 → 告警(多半是审核接口劣化)
四、验证结果
压测方式:往
src-bucket/upload/ 批量灌 10 万张真实 UGC 图(平均 1.8MB,最大 8MB)。|
指标
|
老架构(Celery)
|
新架构(FunctionGraph)
|
|---|---|---|
|
单图端到端 P50
|
1.2s(含排队)
|
240ms
|
|
单图端到端 P99
|
8.7s(高峰排队)
|
610ms
|
|
峰值吞吐
|
120 图/秒(8 Worker 上限)
|
850 图/秒(并发自动扩)
|
|
闲置期计算成本
|
8 台 ECS 全额
|
≈0 元(仅预热 5 实例)
|
|
失败可见性
|
翻日志
|
死信桶 + 钉钉实时推
|
|
扩容响应
|
3-5 分钟人工
|
毫秒自动
|
月度成本对比(按 30 万图/天):老架构 ECS+MQ 约 ¥14,600/月;新架构函数计费 + OBS 请求费约 ¥3,200/月,降本 78%。
五、生产避坑清单
❌ 常见错误
- 函数包塞满依赖:把 OpenCV、numpy 全打进去,包 300MB,冷启动 3 秒起
- AK/SK 写死在代码里:zip 包能被同账号任何人下载,等于明文泄露
- 不设并发上限:热点事件瞬间 5000 并发,把内容审核接口打挂,连带其他业务雪崩
- 忽略冷启动:完全不预热,早高峰第一波请求全是 800ms+
- 异常处理直接
pass:失败图片静默丢弃,用户投诉才知道 - OBS 事件不过滤前缀:后台运营导入的样图也被处理一遍,浪费调用
- 函数内同步调第三方审核不设超时:审核接口抖一下,函数全部卡到 30 秒超时
- 桶和函数跨 Region:OBS 走公网回源,延迟+费用双爆
✅ 正确姿势
- 依赖裁剪到最小,字体/水印底图单独放,能用 Pillow 不用 OpenCV
- 密钥走加密环境变量或凭据服务轮换
- 并发上限 = 下游能扛的 QPS × 单实例耗时倒数,留 30% 余量
- 常驻实例数按"最低可接受 QPS × 冷启动时长"估算
- 所有异常落死信桶 + 告警,绝不静默
- 事件通知加前缀/后缀过滤,精准触发
- 第三方调用一律
timeout=3+ 重试 2 次 + 熔断 - 桶、函数、VPC 端点同 Region 同 AZ 组
六、成本结构拆解
|
计费项
|
说明
|
优化手段
|
|---|---|---|
|
函数调用次数
|
按百万次计
|
事件过滤减少无效触发
|
|
GB-秒
|
内存 × 时长
|
内存调到"够用不浪费",512MB 是图片处理甜点
|
|
冷启动常驻实例
|
预热实例按低费率计
|
只预热最低保障数,别贪多
|
|
OBS 请求次数
|
GET/PUT 都计费
|
缩略图回写合并、批量接口
|
|
CDN 流量
|
缩略图外发
|
WebP + 合理 Cache-Control
|
|
死信存储
|
极低
|
定期归档 OBS 生命周期转低频
|
经验值:短耗时(<1s)+ 潮汐明显的负载,函数计算几乎必赢常驻;反过来如果是长视频转码这种分钟级任务,还是容器 Job 更划算。
七、总结与扩展
这条流水线跑通后,图片处理从"养一群工人等着干活"变成"来一单干一单":
- 延迟:P99 从 8.7s 压到 610ms
- 成本:闲置期归零,月度降 78%
- 弹性:毫秒级扩缩,热点不用人盯
- 可观测:死信桶 + 钉钉 + 云监控三件套,失败无处可藏
后续演进方向:
- AI 增强:审核通过后接 ModelArts 做自动打标、智能裁剪(主体居中)
- 格式升级:AVIF 试点,同画质再省 20% 带宽
- 多级缓存:热门图缩略图固化到 CDN 边缘,函数都不用触发
- 灰度水印:水印样式走配置中心,A/B 测不同透明度对盗图率的影响
- 跨 Region 容灾:主 Region 函数异常时,OBS 跨区域复制触发备 Region 同名函数
- 成本细账:接云监控自定义指标,按业务线(帖子/头像/商品图)拆分函数成本
本文为原创 Serverless 图片处理实践,基于华为云 FunctionGraph + OBS 整理,操作步骤以当前控制台页面为准。压测数据来自特定测试环境,实际表现随图片尺寸分布与审核接口响应浮动。转载请注明出处。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)