从零构建 Serverless 图片处理 API:FunctionGraph + OBS + API Gateway 全栈实践

举报
yd_237144307 发表于 2026/10/22 12:20:04 2026/10/22
【摘要】 传统图片处理服务需要运维服务器、配置负载均衡、担心流量突增。本文手把手教你用华为云 FunctionGraph(无服务器函数计算)+ OBS(对象存储)+ API Gateway,从零搭建一个按调用量计费、自动弹性伸缩的图片处理 API,成本降低 90%。
## 一、为什么选择 Serverless?

先算一笔账:

| 方案 | 月固定成本 | 弹性能力 | 运维成本 |
|------|-----------|---------|---------|
| 自建 ECS 集群 | ¥2000+ | 手动扩容 | 需专人运维 |
| 容器化部署 | ¥1000+ | 需配置 HPA | 需 K8s 团队 |
| **FunctionGraph** | **¥0(按调用计费)** | **毫秒级自动伸缩** | **零运维** |

华为云 FunctionGraph 的核心优势:

- **按调用计费**:函数不运行就不花钱,冷启动后按 GB·s 计费
- **自动弹性**:流量从 1 QPS 到 10000 QPS,无需任何配置
- **与华为云生态深度集成**:OBS 触发、APIG 网关、SMN 消息通知一键打通

---

## 二、架构总览

用户上传 → API Gateway → FunctionGraph → OBS 存储
                ↓
         返回处理结果
                ↓
         OBS 触发缩略图函数 → 生成缩略图 → 存回 OBS

**涉及服务**:

| 服务 | 作用 |
|------|------|
| OBS | 存储原图和缩略图 |
| FunctionGraph | 运行图片处理逻辑 |
| API Gateway | 对外暴露 HTTP 接口 |
| IAM | 权限管理 |

---

## 三、实战:5 步搭建图片处理 API

### 第 1 步:创建 OBS 存储桶

登录华为云控制台,进入 **对象存储服务 OBS**:

1. 点击「创建桶」
2. 桶名称:`image-processor-xxxx`(全局唯一)
3. 区域:选择离你用户最近的区域(如 `华北-北京四`)
4. 存储类型:标准存储
5. 桶策略:**私有**(通过 API Gateway 临时授权访问)

创建后,在桶内创建两个文件夹:
- `original/` — 存放原图
- `thumbnail/` — 存放缩略图

### 第 2 步:编写图片处理函数

进入 **函数工作流 FunctionGraph**,点击「创建函数」:

**函数配置**:

| 配置项 | 值 |
|--------|-----|
| 函数名称 | `image-resize-handler` |
| 运行时 | Python 3.9 |
| 函数类型 | 事件函数 |
| 内存 | 256 MB |
| 超时 | 30 秒 |

**完整代码**(直接复制):

```python
# -*- coding: utf-8 -*-
"""
华为云 FunctionGraph 图片处理函数
依赖:Pillow(华为云内置),无需额外安装
"""

import json
import io
import os
import logging

from PIL import Image

# 华为云 OBS SDK(FunctionGraph 内置)
from huaweicloudsdkcore.auth.credentials import BasicCredentials
from huaweicloudsdkcore.exceptions import exceptions
from huaweicloudsdkcore.http.http_config import HttpConfig
from huaweicloudsdkobs.v2.region.obs_region import ObsRegion
from huaweicloudsdkobs.v2 import *

# 日志配置
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 环境变量(函数创建时配置)
BUCKET_NAME = os.environ.get("BUCKET_NAME", "image-processor-xxxx")
THUMBNAIL_WIDTH = int(os.environ.get("THUMBNAIL_WIDTH", "300"))
THUMBNAIL_HEIGHT = int(os.environ.get("THUMBNAIL_HEIGHT", "300"))


def get_obs_client():
    """创建 OBS 客户端(函数冷启动时复用)"""
    # FunctionGraph 运行时自动注入环境变量
    # 实际生产中建议使用 CSMS 管理 AK/SK
    ak = os.environ.get("HUAWEI_AK")
    sk = os.environ.get("HUAWEI_SK")
    region = os.environ.get("HUAWEI_REGION", "cn-north-4")

    credentials = BasicCredentials(ak, sk)
    http_config = HttpConfig.get_default_config()
    region_info = ObsRegion(region)

    return ObsClient.new_builder() \
        .with_http_config(http_config) \
        .with_credentials(credentials) \
        .with_region(region_info) \
        .build()


# 全局复用 OBS 客户端(减少冷启动延迟)
_obs_client = None


def resize_image(image_data, max_width, max_height):
    """按比例缩放图片"""
    img = Image.open(io.BytesIO(image_data))
    original_format = img.format

    # 计算缩放比例
    width, height = img.size
    ratio = min(max_width / width, max_height / height)
    new_size = (int(width * ratio), int(height * ratio))

    # 执行缩放
    resized = img.resize(new_size, Image.Resampling.LANCZOS)

    # 输出为 JPEG
    output = io.BytesIO()
    resized.save(output, format="JPEG", quality=85)
    return output.getvalue(), original_format


def handler(event, context):
    """函数入口"""
    global _obs_client
    if _obs_client is None:
        _obs_client = get_obs_client()

    logger.info("收到事件: %s", json.dumps(event))

    try:
        # 解析 API Gateway 传入的事件
        body = json.loads(event.get("body", "{}"))
        image_key = body.get("image_key")  # OBS 中的对象路径

        if not image_key:
            return {
                "statusCode": 400,
                "body": json.dumps({"error": "缺少 image_key 参数"}),
                "headers": {"Content-Type": "application/json"}
            }

        # 从 OBS 下载原图
        download_request = DownloadFileRequest(BUCKET_NAME, f"original/{image_key}")
        response = _obs_client.download_file(download_request)

        if response.status_code != 200:
            return {
                "statusCode": 404,
                "body": json.dumps({"error": "图片不存在"}),
                "headers": {"Content-Type": "application/json"}
            }

        image_data = response.body.read()

        # 执行缩放
        thumbnail_data, _ = resize_image(image_data, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)

        # 上传缩略图到 OBS
        thumbnail_key = f"thumbnail/{image_key.replace('.jpg', '_thumb.jpg').replace('.png', '_thumb.jpg')}"
        upload_request = UploadFileRequest(
            BUCKET_NAME,
            thumbnail_key,
            content=io.BytesIO(thumbnail_data)
        )
        upload_response = _obs_client.upload_file(upload_request)

        logger.info("缩略图已生成: %s", thumbnail_key)

        # 生成临时访问 URL(有效期 1 小时)
        signed_url = _obs_client.create_signed_url(
            BUCKET_NAME,
            thumbnail_key,
            expires=3600
        )

        return {
            "statusCode": 200,
            "body": json.dumps({
                "message": "缩略图生成成功",
                "thumbnail_url": signed_url.signed_url,
                "thumbnail_key": thumbnail_key
            }, ensure_ascii=False),
            "headers": {"Content-Type": "application/json"}
        }

    except Exception as e:
        logger.error("处理失败: %s", str(e))
        return {
            "statusCode": 500,
            "body": json.dumps({"error": str(e)}),
            "headers": {"Content-Type": "application/json"}
        }

环境变量配置(在函数「配置」→「环境变量」中添加):

HUAWEI_AK=你的AK
HUAWEI_SK=你的SK
BUCKET_NAME=image-processor-xxxx
HUAWEI_REGION=cn-north-4
THUMBNAIL_WIDTH=300
THUMBNAIL_HEIGHT=300

⚠️ 安全提示:生产环境建议将 AK/SK 存储在 云数据库服务(CSMS) 中,通过函数运行时动态获取,不要硬编码。

第 3 步:配置 API Gateway 触发器

回到函数详情页,点击「触发器」→「创建触发器」:

配置项 值
触发器类型 API 网关触发器
协议 HTTPS
方法 POST
路径 /resize
发布环境 RELEASE

创建后,你会获得一个 API 调用地址,格式如:

xxx

第 4 步:测试 API

# 替换为你的 API 地址
API_URL="";

# 调用测试
curl -X POST "$API_URL" \
  -H "Content-Type: application/json" \
  -d '{"image_key": "photo.jpg"}'

成功响应:

{
  "message": "缩略图生成成功",
  "thumbnail_url": "";,
  "thumbnail_key": "thumbnail/photo_thumb.jpg"
}

第 5 步:配置 OBS 事件触发(可选)

如果你想实现 上传即自动处理:

  1. 进入 OBS 桶详情页 → 「事件通知」→ 「创建事件通知」
  2. 事件类型:PostObject(上传对象)
  3. 前缀过滤:original/
  4. 触发目标:选择刚才创建的函数 image-resize-handler

这样,任何文件上传到 original/ 目录,函数会自动执行,缩略图秒级生成。


四、成本分析

以每月处理 10 万张图片 为例:

项目 单价 月用量 费用
FunctionGraph 调用 ¥0.2/百万次 10 万次 ¥0.02
FunctionGraph 资源 ¥0.0000167/GB·s 约 500 GB·s ¥0.008
OBS 存储(100GB) ¥0.33/GB/月 100 GB ¥33
OBS 请求 ¥0.01/万次 20 万次 ¥0.2
API Gateway ¥0.08/万次 10 万次 ¥0.8
合计 ¥34.03/月

对比同等能力的 ECS 方案(¥200+/月),成本降低 83%。


五、进阶优化建议

1. 使用 CodeArts 实现 CI/CD

将函数代码托管到 CodeArts Repo,配置流水线自动部署:

# .codearts-pipelines/pipeline.yaml
stages:
  - name: 单元测试
    steps:
      - script: pytest tests/
  - name: 部署到 FunctionGraph
    steps:
      - script: |
          hcloud fgs update-function-code \
            --function-name image-resize-handler \
            --zip-file ./dist/function.zip

2. 接入 APM 监控

在函数中集成 应用性能管理 APM:

from huaweicloudsdkapm.v1 import APMClient

# 监控函数执行时间、错误率、调用链

3. 使用 GaussDB 记录处理日志

将每次图片处理记录存入 GaussDB(for MySQL),方便后续分析:

CREATE TABLE image_processing_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    image_key VARCHAR(255),
    thumbnail_key VARCHAR(255),
    process_time_ms INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

六、总结

维度 传统方案 Serverless 方案
部署时间 1-2 天 1 小时
运维成本 专人运维 零运维
弹性能力 手动扩容 自动弹性
月成本 ¥2000+ ¥34
可用性 依赖人工 99.95% SLA

华为云 FunctionGraph + OBS + API Gateway 的组合,让开发者从基础设施中彻底解放出来,真正做到 "写代码,不运维"。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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