FunctionGraph + OBS 触发器实践:上传图片自动生成缩略图的全流程

举报
yd_239575663 发表于 2026/11/11 10:55:59 2026/11/11
【摘要】 上传图片后自动生成缩略图:OBS 对象创建事件触发 FunctionGraph,用 Pillow 等比缩放后写回 thumbs/ 前缀。全文给出完整代码、依赖打包、触发器配置与排障清单,重点讲前缀隔离防自触发、中文文件名解码和冷启动超时三处坑。

用户上传头像、商品图时,后端往往要多存一份缩略图给列表页用。如果为这点需求起一台常驻服务,成本和运维都不划算。这篇把“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,两种做法:

  1. 随代码一起打包:在本地建一个目录,执行 pip install Pillow -t .(注意要在与运行环境相同的平台上打包,Pillow 含 C 扩展,Windows 下打的包有跨平台问题的风险,优先在 Linux 环境或容器里打包),再把 index.py(或 handler.py)和依赖一起打成 zip 上传。
  2. 用依赖包:把依赖单独打成一个 zip 作为函数依赖包上传,函数引用它。依赖更新时不用动业务代码。

上传后在控制台先跑一次测试事件:直接贴一段 OBS 触发事件的 JSON,确认函数能正常执行、返回 dst_key。先过测试事件,再去配触发器,不要两个变量一起调。

第四步:配 OBS 触发器

在函数详情页添加 OBS 触发器:

  • 桶:选放原图的桶;事件类型:对象创建(Put);
  • 前缀填 uploads/,后缀填 .jpg、.jpeg、.png(按需多配几个);
  • 委托授权:给函数一个能读写该桶的委托;权限只给这个桶,别给账号级通配。

配完不要马上批量传图,先传一张 100KB 左右的小图验证:刷新桶,看 thumbs/ 下是否出现同名 .jpg;再到函数日志里确认本次调用的耗时和返回。

第五步:验证与排障清单

按这个顺序查,基本能覆盖大部分问题:

  1. 缩略图没生成:先看函数日志有没有本次调用。没有调用记录,问题在触发器(前缀、后缀、事件类型、委托);有调用记录但报错,再看报错本身。
  2. NoSuchKey/404:对象名解码了吗?区域 endpoint 和桶是否对应?对象是否被删了又触发了删除事件(触发器只配 Put 就不会)。
  3. AccessDenied/403:委托权限够不够?AK/SK 对不对?桶策略有没有显式拒绝?注意对象不存在时 OBS 有时也返回 403,先确认对象确实在桶里。
  4. ImportError: No module named 'PIL':Pillow 没打进包,或打包平台不对,重新打包后重新上传。
  5. 超时:先把超时时间放宽到 30 秒以上;大图处理本身就慢,也可以先降并发,确认是单张慢还是并发挤占。
  6. 冷启动慢:首次调用或长时间无调用后首次调用会慢一些,属于正常现象;对延迟敏感的场景可以配预留实例,但按量场景一般先接受,观察实际调用间隔再决定。
  7. 生成的缩略图又触发一次:检查触发器前缀是不是 uploads/,函数写回前缀是不是 thumbs/,两边不要重叠。

费用与清理

  • FunctionGraph 按调用次数和 GB-秒计费,没人上传时不花钱;OBS 按存储和请求量计费,缩略图本身很小,成本主要在原图存储。
  • 测试产生的原图和缩略图用完就删;测试完把触发器保留或删除按需处理,停用触发器后上传原图不再生成缩略图,函数本身不产生费用。
  • 不要给触发器开全桶无前缀监听,也不要在函数里写回原图前缀,两个动作都可能把一次测试变成循环调用,账单和日志都会翻倍。

总结

这条链路的配置只有三处容易出错:前缀隔离防自触发、对象名 URL 解码、Pillow 打包平台。搭稳之后,上传图片自动出缩略图就成了一个不需要人值守的环节:业务只管往 uploads/ 丢原图,列表页直接读 thumbs/,函数按调用计费,闲时零成本。需要多尺寸时,在函数里多生成几个尺寸写回不同前缀即可,结构不用改。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。