FunctionGraph + OBS 触发器实践:上传图片自动生成缩略图的全流程
用户上传头像、商品图时,后端往往要多存一份缩略图给列表页用。如果为这点需求起一台常驻服务,成本和运维都不划算。这篇把“OBS 上传原图 → FunctionGraph 自动生成缩略图”的链路搭一遍:只保留一个函数和一个触发器,上传即触发,处理完写回同一个桶的不同前缀。
架构
- 原图前缀:
uploads/,业务上传只写这里 - 缩略图前缀:
thumbs/,函数处理后写这里 - 触发器:OBS 对象创建事件(Put),前缀
uploads/,后缀.jpg/.jpeg/.png - 函数:Python 3.10 + Pillow,入口
handler.handler
关键点是前缀隔离:函数只监听 uploads/,只写 thumbs/。如果触发器不设前缀、函数又写回同桶同一前缀,生成缩略图本身又会触发一次,形成自触发循环——测试时先把这点守住,循环一旦跑起来,调用次数和费用都会失控。
第一步:创建函数
FunctionGraph 控制台新建函数,选 Python 3.10 运行时,入口填 handler.handler。配置上注意三处:
- 超时时间:默认 3 秒太短,第一次跑(冷启动 + 下载原图 + 处理)很容易超时。先给到 30 秒,跑稳之后再按日志里的实际耗时往下收。
- 内存:先给 512 MB,图片大、并发高时再往上加。内存太小会在处理大图时直接 OOM,日志里表现为内存超限而不是代码报错。
- 环境变量:
OBS_ENDPOINT(桶所在区域的 endpoint,如obs.cn-north-4.myhuaweicloud.com)、OBS_AK、OBS_SK。AK/SK 只放环境变量,不要写进代码、不要打进代码包。也可以用委托方式授权函数访问 OBS,按最小权限只给读写这个桶的权限。
第二步:核心代码
入口函数只做四件事:解析事件里的桶名和对象名、从 OBS 下载原图、用 Pillow 生成缩略图、写回 thumbs/ 前缀。缩略图用 thumbnail() 等比缩放,长边不超过 320px,统一存为 JPEG,兼容性最好。
import json
import os
from io import BytesIO
from urllib.parse import unquote_plus
from obs import ObsClient
from PIL import Image
SRC_PREFIX = 'uploads/'
DST_PREFIX = 'thumbs/'
MAX_SIZE = (320, 320)
def get_client():
return ObsClient(
access_key_id=os.environ['OBS_AK'],
secret_access_key=os.environ['OBS_SK'],
server=os.environ['OBS_ENDPOINT'],
)
def handler(event, context):
# 第一次调试先把事件原样打出来:对象名是否 URL 编码、桶名对不对,看一眼就知道
print(json.dumps(event, ensure_ascii=False))
try:
record = event['Records'][0]['obs']
bucket = record['bucket']['name']
key = unquote_plus(record['object']['key'])
except (KeyError, IndexError, TypeError) as exc:
print('事件结构解析失败:', exc)
return {'statusCode': 400, 'body': 'bad event'}
if not key.startswith(SRC_PREFIX):
print('非原图前缀,跳过:', key)
return {'statusCode': 200, 'body': 'skipped'}
dst_key = DST_PREFIX + key[len(SRC_PREFIX):].rsplit('.', 1)[0] + '.jpg'
client = get_client()
resp = client.getObject(bucket, key)
if resp.status != 200:
print('下载原图失败:', resp.status, resp.errorCode, resp.errorMessage)
return {'statusCode': 500, 'body': 'download failed'}
img = Image.open(BytesIO(resp.body.read()))
img = img.convert('RGB') # PNG 带透明通道时转 JPEG 前必须先转 RGB,否则保存报错
img.thumbnail(MAX_SIZE)
buf = BytesIO()
img.save(buf, format='JPEG', quality=85)
buf.seek(0)
put = client.putObject(bucket, dst_key, content=buf, contentType='image/jpeg')
print('缩略图已生成:', dst_key, 'put status:', put.status)
client.close()
return {'statusCode': 200, 'body': dst_key}
几点说明:
- 对象名要
unquote_plus解码一次。中文文件名、带空格的文件名在事件里是 URL 编码过的,不解码就下载不到原图,报错经常是 404/403,容易误判成权限问题。 thumbnail()是等比缩放,不会拉伸变形;长边超过 320 的按比例压,本身就小的图不会被放大。- PNG 转 JPEG 前先
convert('RGB'),否则带 Alpha 通道的图保存时会报错。 - 返回体里带上生成的
dst_key,日志里能直接看到每次调用产出了什么,排障时不用猜。
第三步:把 Pillow 打进部署包
FunctionGraph 的 Python 运行时不自带 Pillow,两种做法:
- 随代码一起打包:在本地建一个目录,执行
pip install Pillow -t .(注意要在与运行环境相同的平台上打包,Pillow 含 C 扩展,Windows 下打的包有跨平台问题的风险,优先在 Linux 环境或容器里打包),再把index.py(或handler.py)和依赖一起打成 zip 上传。 - 用依赖包:把依赖单独打成一个 zip 作为函数依赖包上传,函数引用它。依赖更新时不用动业务代码。
上传后在控制台先跑一次测试事件:直接贴一段 OBS 触发事件的 JSON,确认函数能正常执行、返回 dst_key。先过测试事件,再去配触发器,不要两个变量一起调。
第四步:配 OBS 触发器
在函数详情页添加 OBS 触发器:
- 桶:选放原图的桶;事件类型:对象创建(Put);
- 前缀填
uploads/,后缀填.jpg、.jpeg、.png(按需多配几个); - 委托授权:给函数一个能读写该桶的委托;权限只给这个桶,别给账号级通配。
配完不要马上批量传图,先传一张 100KB 左右的小图验证:刷新桶,看 thumbs/ 下是否出现同名 .jpg;再到函数日志里确认本次调用的耗时和返回。
第五步:验证与排障清单
按这个顺序查,基本能覆盖大部分问题:
- 缩略图没生成:先看函数日志有没有本次调用。没有调用记录,问题在触发器(前缀、后缀、事件类型、委托);有调用记录但报错,再看报错本身。
NoSuchKey/404:对象名解码了吗?区域 endpoint 和桶是否对应?对象是否被删了又触发了删除事件(触发器只配 Put 就不会)。AccessDenied/403:委托权限够不够?AK/SK 对不对?桶策略有没有显式拒绝?注意对象不存在时 OBS 有时也返回 403,先确认对象确实在桶里。ImportError: No module named 'PIL':Pillow 没打进包,或打包平台不对,重新打包后重新上传。- 超时:先把超时时间放宽到 30 秒以上;大图处理本身就慢,也可以先降并发,确认是单张慢还是并发挤占。
- 冷启动慢:首次调用或长时间无调用后首次调用会慢一些,属于正常现象;对延迟敏感的场景可以配预留实例,但按量场景一般先接受,观察实际调用间隔再决定。
- 生成的缩略图又触发一次:检查触发器前缀是不是
uploads/,函数写回前缀是不是thumbs/,两边不要重叠。
费用与清理
- FunctionGraph 按调用次数和 GB-秒计费,没人上传时不花钱;OBS 按存储和请求量计费,缩略图本身很小,成本主要在原图存储。
- 测试产生的原图和缩略图用完就删;测试完把触发器保留或删除按需处理,停用触发器后上传原图不再生成缩略图,函数本身不产生费用。
- 不要给触发器开全桶无前缀监听,也不要在函数里写回原图前缀,两个动作都可能把一次测试变成循环调用,账单和日志都会翻倍。
总结
这条链路的配置只有三处容易出错:前缀隔离防自触发、对象名 URL 解码、Pillow 打包平台。搭稳之后,上传图片自动出缩略图就成了一个不需要人值守的环节:业务只管往 uploads/ 丢原图,列表页直接读 thumbs/,函数按调用计费,闲时零成本。需要多尺寸时,在函数里多生成几个尺寸写回不同前缀即可,结构不用改。
- 点赞
- 收藏
- 关注作者
评论(0)