很多朋友在第一次接触云上接口时,都会遇到一个经典选择:函数计算(FC)已经能挂 HTTP 触发器了,为什么还要在前面再加一层 API 网关?等你在生产环境真正跑过一轮接口,就会明白——入口统一、签名校验、流量控制、调用方管理,这些事如果全写在业务函数里,函数会越来越肿,而且每个接口都要重复实现一遍。把 API 网关放在 FC 前面,由它接管公网入口,业务函数只专注处理请求,数据再通过 VPC 内网落到 RDS,这是目前云原生应用里非常典型的一条链路:HTTP 请求 → 函数 → RDS。
这篇记录就把这条链路从零到尾完整走一遍。我会按照实际动手的顺序来写:先讲清楚为什么要这样分层,再逐步完成 RDS、VPC、FC 和 API 网关的配置,把重点放在鉴权设计上,最后给出联调时的常见问题和排错思路。适合正在做 Serverless 接口改造、或者刚开始接触函数计算和网关的同学参考。
1. 链路拆解:三层架构里每一层到底在干什么
1.1 三层的职责边界
先看完整链路的各个角色。前端发起一次 HTTP 请求,请求先打到 API 网关,网关做完鉴权、限流和协议转换后,把请求转发给 FC,FC 执行业务逻辑,再通过 VPC 内网访问 RDS,拿到数据后原路返回。
API 网关在最前面,本质上是"流量入口和安检门"。它不写业务代码,但管着所有请求的进与出:谁可以调用、每秒能调多少次、请求参数是否符合规范、响应格式是否统一,都在这一层处理。
FC 是业务逻辑的执行者。它不关心请求从哪里来,只关心怎么把数据查出来、算出来、组装好。因为 FC 是按调用次数和资源用量计费,没有请求时实例缩到零,有请求时秒级拉起,所以很适合处理波动大的接口负载。
RDS 是最终的数据落点。它负责持久化存储、索引查询、事务一致性。这里有个关键点:RDS 不直接暴露公网地址,而是通过内网 IP 供 VPC 内的资源访问,FC 要连数据库就必须和 RDS 处于同一个 VPC 内,或者通过互通方式访问。
1.2 为什么不直接用 FC 的 HTTP 触发器
可能有人会问:FC 自带的 HTTP 触发器已经能处理公网请求,再加一层网关不是多此一举吗?
直接使用 HTTP 触发器和加上 API 网关的对比,我整理了一张表:
| 能力 | FC 自带 HTTP 触发器 | API 网关 + FC |
|---|---|---|
| 签名鉴权 | 需自行实现 | 内置多套方案,配置即可用 |
| 流量控制 | 需自行实现 | 按 API、按调用方做限流 |
| 调用方管理 | 无 | 通过应用/凭证管理调用方 |
| 参数校验 | 需自行实现 | 可定义请求模型,自动校验 |
| 公网域名的 HTTPS 证书 | 需自行配置 | 支持托管证书、自定义域名 |
| 监控告警 | 只有函数日志 | 网关侧有调用量、错误率、延迟等维度 |
网关这些能力如果全在函数里手工实现,工作量不小,签名验签、限流算法、参数校验逻辑,每个函数都要写一遍,而且很难做到统一维护。API 网关把这些通用能力下沉到基础设施层,函数代码保持纯粹,这才是 Serverless 该有的分工。
1.3 这条架构更匹配哪种业务
基于我自己的经验,这套链路最适合两类场景。一是小而独立的接口服务,比如用户信息查询、订单状态查询、配置拉取、表单提交这类读写逻辑不复杂、但需要稳定对外提供服务的场景。二是 BFF 层,也就是后端对前端,前端需要聚合多个数据源时,用 FC 做聚合逻辑,网关做统一入口和安全控制。
不过它也有不擅长的场景。如果业务本身就是长连接,比如 WebSocket 实时通信,或者一个请求要跑几分钟甚至更久的重型计算任务,就不适合放在这种架构里。网关超时时间再长也有限度,函数计算对单次执行时长也有上限。做技术选型时先想清楚业务形态,别把架构硬套上去。
2. 前置准备:把 RDS、VPC 和 FC 的网打通
2.1 需要提前开通的云资源清单
动手之前先列一个清单,避免配置到一半发现少了资源。这里假设你要做的是一个简单的用户信息查询接口,底层是一张 MySQL 用户表。
| 资源 | 用途 | 注意事项 |
|---|---|---|
| RDS MySQL 实例 | 存储业务数据 | 建议选与 FC 相同的地域和可用区 |
| 数据库账号和库表 | 业务数据读写 | 单独建账号,不要用高权限账号跑业务 |
| 专有网络 VPC | 内网互通基础 | RDS、FC、交换机必须在同一 VPC 体系内 |
| FC 服务和函数 | 执行业务逻辑 | 配置 VPC 访问,绑定交换机和安全组 |
| API 网关分组与 API | 对外发布接口 | 配置后端指向 FC 函数 |
需要先开通 RDS、FC、API 网关这三个核心服务,然后再开始配置。
2.2 创建 RDS 与配置访问白名单
创建 RDS 实例时,数据库引擎选择 MySQL 或者符合业务需求的引擎,规格不用一味追求高配。接口查询量不大时,最小规格就够用,后续可以升降配。
创建完成后有两个步骤不能跳过。
首先是创建独立的数据库账号。给业务函数专用的账号,只授予业务需要的库表权限,比如 SELECT、INSERT、UPDATE。这个账号的密码会写进 FC 的环境变量里,所以要单独管理,别和运维管理账号混用。
然后就是大家最常踩的坑:白名单。RDS 默认只允许白名单内的 IP 或安全组访问。要让 FC 能连上数据库,最省事的做法是选择安全组访问方式,把 FC 绑定的安全组加进 RDS 白名单的安全组列表里。如果当时创建 FC 服务时没有把安全组记录下来,也可以直接把 FC 所在 VPC 的交换机网段加入白名单。但安全组的方式更精确,建议优先用安全组。
2.3 为 FC 开启 VPC 访问
FC 默认运行在云厂商托管的公共网络环境里,要访问 VPC 内的 RDS,必须给 FC 服务开启 VPC 配置。
创建服务时,开启"允许访问 VPC"的选项,然后选择与 RDS 相同的 VPC、交换机和安全组。这里有个关键认知:交换机决定了函数实例落在哪个网段,安全组决定了函数对外访问哪些端口。你选择的安全组出方向规则里,要允许访问 RDS 的内网 IP 和 3306 端口。
我一开始就吃过这个亏:VPC 和交换机都选对了,但安全组出方向全是默认规则,函数连数据库时一直超时。后来才发现是安全组规则把访问 RDS 的内网端口挡住了。配置安全组时,除了默认的放行规则,最好显式加一条指向 RDS 内网 IP 的入方向规则,协议选择 TCP,端口填 3306。
2.4 网络配置里最常翻车的三个点
除了白名单和安全组,还有三个细节特别容易出问题,提前给你打了预防针。
第一,地域必须一致。RDS、FC、API 网关建议都选同一个地域,跨地域访问内网是走不通的,就算打通了,延迟也会高得离谱。
第二,交换机别选错。一个 VPC 下可以有很多交换机,RDS 和 FC 所在交换机如果分属不同网段,跨网段访问在没有路由配置的情况下是连不通的。最简单的办法:创建 RDS 时和 FC 服务配置 VPC 时,都选同一个可用区下的同一个交换机。
第三,函数执行超时时间要预留。网络链路多了一层 VPC 访问,如果数据库连接没走连接池,冷启动后第一次建连可能需要几百毫秒甚至更久。函数超时时间不要只设 3 秒,建议根据业务复杂度设置 10 到 30 秒,联调通过后再逐步调小。
3. API 网关:把 FC 函数发布成公网 API
3.1 API 分组:确定 API 的对外域名空间
API 网关里最重要的概念有两个:分组和 API。
分组类似于域名空间的概念。创建一个分组后,网关会分配一个公网域名,这个域名下的所有 API 都属于同一个业务空间。你可以为分组绑定自定义域名,比如api.yourdomain.com,然后把 HTTPS 证书托管到网关,客户端通过自定义域名访问。
一个分组下可以创建很多个 API。每个 API 对应一个具体的路由,比如GET /user/info。分组的作用是把一组相关的接口聚合在一起,同时可以使用同一套域名和网关策略。
3.2 创建 API 并指定后端为 FC
在 API 网关控制台创建 API 时,第二步会让你选择后端服务类型。这里选择函数计算,然后指定对应的服务名、函数名和版本。
第一次配置时可能会遇到授权提示,也就是让 API 网关在转发请求时,拥有调用 FC 函数的权限。控制台一般会引导你完成一键授权,本质是创建一个服务关联角色。这一步不要跳过,否则网关调用 FC 时会被拒绝。
配置后端为 FC,意味着网关和函数之间的调用走的是腾讯云内部的协议转换链路,网关会把标准的 HTTP 请求事件转换成 FC 能识别的结构再传给函数。所以函数里解析 event 时,并不需要关心原始 HTTP 报文细节,直接按云函数的标准事件格式取参数就行。 (注明:这里只是个人最佳实践,具体权限和授权引导以实际控制台为准。)
3.3 配置前端请求定义与参数映射
创建 API 时,核心工作是定义前端请求和后端参数映射。
前端请求定义比较容易理解:填写请求的 Path、请求方法、入参定义。比如我做GET /user/info,入参是userId,位置在 Query 参数里,类型为 Long,必填。如果接口接收 JSON Body,还需要定义 Body 的字段结构。
参数映射的要点在于"透传"和"转换"的选择。最简单的方式是后端参数直接透传,也就是网关收到的参数原封不动传给 FC。更灵活的方式是自定义映射,比如前端参数叫uid,统一在网关层映射成后端的userId,这样前端无需感知后端参数名,后端也可以独立演进。
我建议第一版先做透传,把链路跑通后再考虑参数映射规则。映射配置一旦复杂,排查问题时会增加一层理解成本。
3.4 发布到环境并用控制台快速调试
API 创建完成后,默认是不可被公网调用的,必须发布到指定环境。API 网关一般提供线上、预发、测试三个环境。发布到测试环境后,可以通过控制台自带的调试工具发起模拟请求,先验证网关到 FC 这条链路是否通畅。
调试时有个小技巧:在网关的调试工具里发一次请求,同时在 FC 控制台观察函数日志,看函数有没有被正确触发,以及函数的输入事件里是否包含预期参数。如果函数日志有输出,说明网关到 FC 链路正常;如果函数没日志,排查重心要放在网关的转发配置或者授权上。
4. 鉴权配置:从网关到数据库的防护设计
4.1 防护模型:哪些入口需要做鉴权
鉴权设计前先理清一个问题:这条链路里到底有几个入口?
第一个入口是公网客户端访问 API 网关,这是主要的攻击面,任何知道 API 域名的人都能发请求进来,所以这里是鉴权的核心。第二个入口是网关调用 FC,这个链路在云内部进行,有云平台的安全隔离,但依然需要确保只有网关有权限调用函数。第三个入口是 FC 访问 RDS,这层靠 VPC 和安全组隔离,不是常规鉴权手段,但属于基础设施层的防护。
三层防护叠下来,实际设计中最重要的判断是:网关层做哪些鉴权,函数层做哪些兜底,数据库层如何配合。下面我给出三种网关层鉴权方案,以及 FC 侧的兜底建议。
4.2 方案 A:APP 签名鉴权
API 网关自带一套基于阿里云 AccessKey/Secret 的签名鉴权机制,在控制台创建应用后,每个应用会得到一对 AppKey 和 AppSecret。
开启方式很简单:在 API 的鉴权设置里选择阿里云 APP 鉴权,同时将该 API 授权给对应应用。客户端发起请求时,需要使用 AppSecret 对请求参数进行签名,生成的签名放在 Header 里,网关收到请求后会验签,校验通过才放行。
这套方案的优势是云平台托管,不需要自己在函数里写验签代码。缺点是签名逻辑比较繁琐,客户端需要依赖官方 SDK 或自行实现签名算法。如果调用方是自己公司的服务端程序,用 SDK 还可以接受;如果是外部第三方开发者,让他们按签名规范接入,沟通成本会高一些。
4.3 方案 B:JWT 插件鉴权
JWT 鉴权是我个人最推荐的方案,尤其是需要对接外部调用方时。
JWT 的全称是 JSON Web Token,本质是一个自包含的令牌,服务端签发、客户端携带、网关验签。令牌里可以包含用户标识、过期时间、权限范围等信息,网关收到请求后解析令牌,校验签名和有效期。校验通过后,网关还会把令牌里的 claims 信息作为 Header 传给 FC,这样函数不用再解析一遍 JWT,直接读 Header 就能拿到用户上下文。
API 网关的 JWT 插件配置核心是设置公钥。签发 JWT 的服务端持有私钥,网关持有公钥,双方使用 RS256 非对称签名算法。配置项大致类似这样:
{ "payload": { "tokenLocation": "HEADER", "tokenName": "Authorization", "type": "jwt", "jwks": { "keys": [ { "kty": "RSA", "alg": "RS256", "kid": "your-key-id", "n": "你的公钥模数", "e": "AQAB" } ] }, "claims": [ {"name": "iss", "value": "your-issuer"}, {"name": "aud", "value": "your-audience"} ] } }配置时公钥要转换成 JWKS 格式,也就是 JSON Web Key Set。很多在线工具都能将 PEM 格式公钥转成 JWKS,转换时注意 kid 要对应。
这套方案的优点很明显:对调用方相对友好,只要会生成 JWT 就能接入;网关侧的验签和 claim 校验完全托管,函数代码不用关心验签细节;JWT 本身无状态,方便水平扩展。缺点是需要自己搭建签发服务,JWT 的密钥管理要做好,私钥一旦泄露,所有签发的令牌都不可信。
4.4 方案 C:自定义鉴权函数
如果已有自己的账号体系,比如内部实现了 OAuth2 或自定义 Token 机制,API 网关也支持自定义鉴权插件。可以将鉴权逻辑写成一个独立的 FC 函数,网关在转发请求之前,先调用这个鉴权函数,函数返回通过或拒绝的结果。
自定义鉴权的优势是灵活,可以对接任何已有的认证服务。比如请求里带一个自定义 Token,鉴权函数去 Redis 里查这个 Token 是否有效、用户 ID 是什么,然后把结果传递给后端函数。
需要注意性能问题。每个请求都会先经过鉴权函数,相当于多了一次函数调用,延迟会增加。鉴权函数里建议加简单缓存,比如把 Token 的校验结果缓存几秒到几分钟,避免每次都重复查询。另外鉴权函数的超时时间要设得短一些,比如 2 到 3 秒,一旦鉴权服务本身卡住,不能拖垮主请求。
4.5 FC 侧的兜底校验:建议至少留一手
网关层的鉴权做得再完善,FC 侧也不能完全不设防。原因很现实:网关配置可能被误改,或者某个内部接口被其他服务直接绕过网关调用,如果函数不校验,等于内部数据裸奔。
FC 侧兜底校验不需要重复实现完整的 JWT 验签,但可以做轻量校验。比如在函数入口检查事件头里是否包含x-ca-openid或由网关注入的请求头,如果缺少这些头,说明请求可能不是从网关转发的,直接拒绝。对于 JWT 方案,网关可以把解析后的用户 ID 放在自定义 Header 中传给 FC,函数检查这个 Header 是否存在且格式正确,就能提高安全性。
我的建议是:网关负责完整鉴权,FC 负责最小可信校验。函数里加一段十行以内的检查代码,给数据访问多一层保障,性价比很高。
5. 函数代码:从 HTTP 请求到 SQL 查询
5.1 事件结构和入参解析
配置完成后,重点来看函数内部怎么写。FC 通过 API 网关触发时,函数收到的 event 是网关格式化后的标准结构,里面包含请求方法、路径、查询参数、请求头、Body 等信息。
我以一个用户信息查询接口为例。API 定义是GET /user/info?userId=123,那么 FC 函数里解析 userId 的方式大致是:
import json def handler(event, context): # 不同运行时解析 event 的 API 略有差异 # 以 Python 运行时为例,event 里通常包含 queryParameters 等字段 req = event.get("requestContext", {}) query = event.get("queryParameters", {}) or {} headers = event.get("headers", {}) or {} user_id = query.get("userId") if not user_id: return { "statusCode": 400, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"code": 400, "message": "userId is required"}) } # 后续业务处理不同云厂商的函数运行时,event 字段名会有差异。联调前用控制台调试工具发一次真实请求,打印整个 event 看看结构,比对着文档猜字段要快得多。
5.2 数据库连接池代码示例
数据库连接是函数里最容易写出问题的部分。如果每个请求都新建数据库连接,冷启动场景下函数实例第一次调用会非常慢,而且高并发时数据库连接数会被打满。
正确做法是使用连接池,并让连接池在函数实例的全局作用域中复用。Python 里可以借助dbutils的PooledDB:
import os import json import pymysql from dbutils.pooled_db import PooledDB _pool = None def get_pool(): global _pool if _pool is None: _pool = PooledDB( creator=pymysql, maxconnections=10, mincached=2, host=os.environ.get("DB_HOST"), port=int(os.environ.get("DB_PORT", "3306")), user=os.environ.get("DB_USER"), password=os.environ.get("DB_PASSWORD"), database=os.environ.get("DB_NAME"), charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, blocking=True ) return _pool def handler(event, context): query = event.get("queryParameters", {}) or {} user_id = query.get("userId") if not user_id: return { "statusCode": 400, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"code": 400, "message": "userId is required"}) } pool = get_pool() conn = pool.connection() try: with conn.cursor() as cursor: cursor.execute("SELECT user_id, nickname, email FROM user_info WHERE user_id = %s", (user_id,)) row = cursor.fetchone() conn.commit() finally: conn.close() if row is None: return { "statusCode": 404, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"code": 404, "message": "user not found"}) } return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"code": 0, "data": row}, ensure_ascii=False) }注意部署时要把pymysql、dbutils等依赖一起打包上传,或者使用函数计算的层来管理依赖。
blocking=True的含义是当连接池满时,新请求会等待而不是直接报错,能保护数据库不被瞬间击穿。这个参数在联调阶段很重要,否则并发稍高就会出现大量连接获取异常。
5.3 响应格式与 API 网关的约定
函数返回给 API 网关的响应必须是特定结构的 JSON,核心字段是statusCode、headers和body。body必须是字符串,不能是 Python 字典,否则网关无法正确解析。
最稳妥的做法:
return { "statusCode": 200, "headers": {"Content-Type": "application/json; charset=utf-8"}, "body": json.dumps({"code": 0, "data": row}, ensure_ascii=False) }如果body里返回的是 JSON 字符串但中文出现乱码,记得加上ensure_ascii=False。返回二进制内容时,有些引导式网关还要求额外指定是否进行 Base64 编码,具体看运行时和产品文档。普通 JSON 接口用上面的格式就够了。
5.4 敏感信息放进环境变量
数据库连接信息这类敏感配置,千万不要硬编码在函数代码里。FC 服务支持环境变量配置,把数据库主机、端口、用户名、密码都放到环境变量里,代码里通过os.environ.get()读取。
这样做还有个额外好处:不同环境(测试、预发、生产)可以复用同一套函数代码,只通过环境变量切换连接配置。代码仓库里不会出现数据库密码,安全审计也更好通过。
6. 踩坑实录:联调测试与高频问题速查
6.1 一次标准的端到端联调流程
配置完成后,建议按下面的顺序做一次完整的联调验证,每步通过后再进入下一步,排查问题时不用四处猜。
第一步,用 API 网关控制台的调试工具,直接发一个GET /user/info?userId=1的请求。如果返回预期的 JSON,说明从网关到 FC 再到 RDS 的链路已经打通。这一步是验证网络链路的关键,先把网络问题排除。
第二步,修改请求参数,比如 userId 传空或者传不存在值,验证参数校验和错误分支是否正常工作。
第三步,在 FC 控制台查看函数日志,确认函数确实被调用,且日志中打印的参数与网关转发的一致。
第四步,开启鉴权后再发请求,不带认证信息的请求应该返回 401,带认证信息的请求应该正常返回。这就是验证鉴权配置是否正确。
6.2 高频问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 客户端请求返回 503 或 504 | 函数执行超时或函数实例拉不起来 | 检查函数超时时间设置,看函数日志有无报错 |
| 函数能连数据库但查询超时 | RDS 白名单未加安全组或交换机网段 | 把 FC 所在安全组加入 RDS 白名单 |
函数报错connect timeout | VPC 配置不正确或安全组出方向未放行 3306 | 确认 VPC、交换机、安全组选择与 RDS 一致 |
| 网关返回 401 | 鉴权配置未开启或签名校验失败 | 检查 API 鉴权设置,验证 JWT 或 APP 签名的时效和签名头 |
| 网关返回 403 | 未发布 API 到对应环境,或 API 未授权给应用 | 发布 API,检查 API 授权关系 |
| 函数返回但客户端报 500 | 函数响应格式不符合网关约定 | 检查返回值是否包含statusCode、headers、body,body是否为字符串 |
| 调用方报签名错误 | 客户端系统时间不准 | 检查签名算法,确认客户端时间与服务器时间偏差在许可范围内 |
6.3 排错思路:先看网关、再看函数、最后看数据库
联调排查一定要分层定位,切忌从头到尾一把抓。
请求失败时,先看 API 网关的调用日志。网关日志会清晰告诉你:请求有没有到网关、网关有没有转发、转发后返回状态码是多少。如果网关日志显示请求根本没进来,问题在前置,比如域名解析、API 发布状态、访问控制;如果网关日志显示转发后报错,问题在函数。
然后再看 FC 日志。函数每次调用的开始和结束都有记录,返回值是什么、有没有抛异常,都在日志里。如果函数日志显示查询时间特别长,问题大概率在数据库侧。
最后才排查 RDS。确认数据库连接数、慢查询日志、白名单和安全组。按照这个顺序定位,大部分问题都能在几分钟内圈定范围。
再说一个我自己的习惯:联调阶段在 FC 函数入口处加一行日志,打印整个 event 结构和关键参数。这个日志看起来简单,但在排查"参数为什么对不上""Header 为什么没有"这类问题时特别有效。上线前把详细日志降级或删掉就行,避免敏感信息落日志。
如果你现在正在搭这条链路,我个人的建议是:第一次跑通时先不用一上来就三套鉴权一起上,先把链路打通,用 APP 签名鉴权或者最基础的 JWT 方案让请求能通过验证,确认数据能正常返回,再逐步加固。最后再分享一个小技巧:在 API 网关调试阶段,可以一边开着网关的调试工具,一边在 FC 控制台实时看日志。网关发请求,函数日志同步打印,两边对比着看,很多让人头疼的连通性问题都能顺藤摸瓜快速解决。