上个月我们刚把一批阿里云账号批量接入自建的CMP(多云管理平台),结果每天凌晨的合规巡检任务告警就没断过。腾讯云侧的SCF函数跑一次至少七八分钟,偶尔直接超时,日志里全是Timeout。后来逐步把函数从128MB升到512MB、全局复用SDK Client、改并发拉取、调整Endpoint和重试策略,单次任务耗时降到了原来的零头。这篇就围绕“腾讯云CMP与阿里云合规工具集成时,如何优化SCF函数的性能”这个话题,把这次调优的过程、原理和踩过的坑完整记录下来。适合正在做多云合规平台、跨云数据采集,或者刚接触SCF集成的朋友参考。
1. 这个集成场景的真实链路:CMP、合规工具与SCF各自扮演什么角色
1.1 为什么这里会出现腾讯云SCF
先说场景本质。CMP(Cloud Management Platform,云管理平台)负责统一管理多个云账号的资源、配置、成本和合规状态。很多团队的CMP部署在腾讯云上,但需要对接阿里云账号的合规数据——比如配置审计(Cloud Config)里的资源合规结果、操作审计(ActionTrail)里的操作日志、KMS里的密钥轮转记录。阿里云这些合规工具都提供了OpenAPI,但我不会在CMP主服务里直接同步调用它们,原因很简单:合规巡检是典型的周期型任务,数据量大、耗时长,直接在Web服务里拉会占线程、阻塞接口、把主流程拖垮。
所以我把“跨云数据采集”这件事拆成了一个独立任务,用SCF来跑。SCF收到定时触发器的消息后,依次调用阿里云合规工具的OpenAPI,把多账号、多区域的数据拉回来,经过统一格式转换,写入腾讯云侧的存储或消息队列,CMP再从那里消费。SCF的吸引力在于:不需要维护服务器,按调用次数和运行时长计费,定时触发天然支持cron表达式,失败还能自动重试。但代价就是——它本身运行在腾讯云网络环境里,调用阿里云API属于跨云访问,网络链路、身份认证、数据序列化这些环节都会被放大成性能瓶颈。
1.2 一条合规数据从阿里云到CMP的完整路径
拆开来看,一次SCF执行其实分四个阶段:
- 触发与准备:定时触发器唤起函数,完成环境变量读取、AK/SK或STS临时凭证准备。如果采用AssumeRole方式,这一步还要先调一次阿里云STS接口换取临时凭证。
- 跨云API调用:遍历账号列表和区域列表,逐个调用合规工具的OpenAPI,比如ActionTrail的
LookupEvents、Cloud Config的ListDiscoveredResources。这部分是纯网络开销加API处理延迟。 - 数据转换:把阿里云返回的JSON字段映射到CMP的统一数据模型,可能还要做合规规则判断、格式校验、去掉不需要的字段。
- 回写与入库:把处理好的数据写到COS、CKafka或者直接回调CMP的网关接口,确认写入成功后返回。
这四个阶段里,最容易出问题的不是数据转换,而是第二阶段——跨云调用的连点成线。我第一次测试时发现,单个账号的合规数据拉取就要30多秒,看似每个OpenAPI调用只有几百毫秒,但账号多、区域多、分页多,页面数乘上去就爆炸了。再加上SCF实例每次冷启动都要重新初始化SDK Client、重建TCP连接、重新做TLS握手,整个函数的耗时大头全耗在了“准备工作”上,而不是真正的数据拉取。
2. 先定位瓶颈:跨云调用到底慢在哪几个环节
2.1 四个最容易被忽略的耗时点
很多人遇到SCF执行慢,第一反应是“是不是网络专线没通”,或者“要不要升级函数规格”。但在我这次实际项目中,真正的耗时大头,其实是下面这四个点:
- TLS握手与TCP连接重建:SCF的实例是临时的,每次触发可能被调度到全新实例,函数里的全局连接池根本来不及复用。每次调用阿里云OpenAPI都要重新TCP建连、TLS握手,一次握手在国内跨云网络下就要2到5个RTT,按每个RTT 20ms来算,光握手就耗费60到100ms。这只是单次API调用的开销,分页一百次就是几秒。
- SDK Client反复初始化:如果直接把
Client()写在函数入口里,每调用一次OpenAPI就初始化一次客户端,底层会再次加载凭证、初始化HTTP Client、做签名准备。这个过程比实际API调用还贵,我见过有人一个函数里初始化了几十个Client,等于人为把性能拉低了一个数量级。 - 同步串行拉取:遍历10个账号、5个区域,每个区域3个API调用,同步执行就是150次串行调用。每次往返就算1秒,整体就是150秒。其实很多调用之间没有先后依赖,完全可以并发。
- 超时与重试的默认策略不合理:SDK默认的连接超时、读取超时往往偏宽松,网络抖动时一个失败请求能拖几十秒才返回,再叠加默认重试机制,指数退避的时间非常可观。
这些开销叠加在一起,函数就算配置再高也快不起来。所以要优化的第一件事不是加内存,而是把“每一个调用环节究竟花了多少时间”给量化出来。
2.2 在SCF里埋点,用数据说服自己
优化前我习惯先在函数里加一套最简单的计时逻辑,把每个阶段耗时打到日志里。
import time start_all = time.perf_counter() t0 = time.perf_counter() # 获取 STS 临时凭证 cred = get_sts_credential() print(f"[perf] get credential: {time.perf_counter() - t0:.3f}s") t1 = time.perf_counter() # 遍历账号列表,串行拉取 for account in account_list: call_compliance_api(account) print(f"[perf] call compliance api: {time.perf_counter() - t1:.3f}s") t2 = time.perf_counter() # 数据转换与回写 transform_and_upload() print(f"[perf] upload: {time.perf_counter() - t2:.3f}s") print(f"[perf] total: {time.perf_counter() - start_all:.3f}s")把日志收集到SLS或CLS之后,观察一段时间的分布。我这次实测出来的数据(取一次典型执行)大致是这样:
| 环节 | 耗时 | 占比 |
|---|---|---|
| 凭证准备(STS AssumeRole) | 0.8s | 1% |
| SDK Client初始化 | 42s | 32% |
| 跨云API调用(含网络、签名、分页) | 68s | 49% |
| 数据转换与入库 | 20s | 14% |
| 其他(日志、序列化开销) | 4s | 3% |
看到没,初始化Client占了32%,这就是典型的“每次都在入口new Client”导致的。而且这还是在网络通畅的前提下,如果跨地域公网波动,调用API的耗时占比会更高。先量化,再对症下药,这一步比任何经验判断都靠谱。
3. 网络层优化:连接复用、Endpoint选择与专线决策
3.1 全局复用Client:把TLS握手省掉
SCF虽然是Serverless,但实例在生命周期内是复用的——只要不是被回收,全局变量会一直保留。利用这个机制,把阿里云SDK的Client对象提升到模块级别,只在首次初始化时创建,后续事件直接复用。
import os from alibabacloud_actiontrail20200706.client import Client as ActionTrailClient from alibabacloud_tea_openapi.models import Config as OpenApiConfig _client = None def get_actiontrail_client(): global _client if _client is None: config = OpenApiConfig( access_key_id=os.environ.get("ALIYUN_AK"), access_key_secret=os.environ.get("ALIYUN_SK"), region_id="cn-shanghai", endpoint="actiontrail.cn-shanghai.aliyuncs.com", connect_timeout=10, read_timeout=30, ) _client = ActionTrailClient(config) return _client def main_handler(event, context): client = get_actiontrail_client() # 后续所有 OpenAPI 调用都复用同一个 client这个改动是整轮优化里性价比最高的一步,单次执行直接省掉几十秒。SDK内部维护了连接池和线程池,同一个Client多次调用会复用HTTP连接,TLS握手只发生一次。需要注意,SCF实例冻结后连接池可能会失效,但SDK会在下一次调用时自动重连,不需要在业务代码里额外处理,顶多是偶发一次握手导致那一次请求稍慢。
在腾讯云SCF里,全局变量不一定只存活于一个实例。函数并发扩到多个实例时,每个实例有独立的全局变量池,所以_client is None只会保证“单实例内复用”,不会跨实例共享。不过这样就够了,因为每个实例的存活时间内通常会处理多个事件。
3.2 Endpoint与地域就近接入怎么选
阿里云OpenAPI的Endpoint有两种选择:公网Endpoint和VPC内网Endpoint。公网Endpoint是默认方式,SCF通过公网访问;内网Endpoint需要SCF到阿里云VPC之间有专线或云企业网打通,一般集成场景不会直接这么干。
更实际的问题,是选择哪个地域的Endpoint。假如腾讯云SCF部署在上海,阿里云侧的资源也主要在上海,那就直接用上海地域的Endpoint,不要图省事把所有账号统一指向杭州。地域就近可以显著降低公网RTT,上海到杭州和上海到北京的延迟差距通常在10到30ms左右,虽然单次请求差别不大,但合规巡检动辄几千次API调用,累积影响就很可观。
还要注意,阿里云不同产品的地域Endpoint不是每个地域都有,比如配置审计的Endpoint在部分地域没有开放,得先用DescribeRegions或者直接查官方文档确认。遇到这种情况,我习惯写一个地域到Endpoint的映射表放在配置中心,SCF启动时读一次,而不是硬编码在代码里。
提示:SCF开启公网访问后,出口IP默认是动态的。阿里云侧如果做了IP白名单限制,需要把SCF的出口IP段配置成NAT网关的固定EIP,或者干脆先不开白名单,用RAM权限做访问控制。
3.3 什么规模才值得上专线或云联网
很多团队一听到跨云调用,第一反应是“上专线、拉专线、搞云联网”,其实没必要。我的判断标准很简单:SCF单次任务拉取的总数据量是否超过几百MB,以及API调用次数是否超过上万次。如果只是每天跑一次几百个账号的合规配置检测,公网加连接复用完全够用,专线的成本和运维复杂度反而会拖慢上线进度。
公网方案真正要关注的是稳定性和限流。跨云公网在晚高峰可能出现瞬时丢包,SDK默认重试一般能扛过去;但如果API调用量很大且被阿里云侧限流(返回Throttling),就要在SCF里做退避重试,而不是依赖网络层面解决。如果将来数据量真的涨到需要专线,直接在阿里云侧申请高速通道,在腾讯云侧打通云联网,再把SCF的请求切到内网Endpoint即可。这个决策建议放到每个月的数据量趋势图上再判断,别提前过度设计。
4. 函数级配置调优:内存、超时、并发与冷启动控制
4.1 内存档位和CPU分配的关系
SCF的内存配置不只是内存大小,它直接决定了函数实例分到的CPU算力。腾讯云SCF的CPU分配逻辑和安全容器有关,简单说就是内存档位越高,CPU配额越强。我刚开始图省钱,把函数内存设为128MB,结果每次执行都要跑七八分钟,因为JSON解析、TLS握手、数据格式转换全是CPU密集型操作,128MB对应的CPU配额让这些环节都变慢了。
优化时我把内存调到了512MB,同样的逻辑执行时间直接缩短到原来的40%左右。后来又试过1GB,提升仍然明显,但边际收益开始递减。最终定了512MB到1GB这个区间作为合规巡检任务的基准配置。这里给个参考:如果函数内部有大量JSON序列化、正则匹配、字段映射,优先上512MB;如果只是简单转发,128MB可以继续用。
内存调大之后,最大执行时长也会跟着变化。SCF单次函数执行的超时上限可以拉到很大,但合规任务我一般设置900秒,因为一次任务要遍历所有账号和区域,确实需要长超时。不过超时设置要和触发器周期配合,否则会出现任务还没跑完,下一次触发又来了。
| 内存档位 | 适用场景 | 我的建议 |
|---|---|---|
| 128MB | 纯转发、单账号少量API调用 | 不推荐做合规数据拉取 |
| 256MB | 少量账号、少量分页 | 勉强可用,优先优化代码层 |
| 512MB | 多账号、多区域、需要JSON解析 | 常用档位,性价比高 |
| 1GB以上 | 超大分页、复杂数据转换 | 边际收益有限,谨慎使用 |
4.2 定时触发器的重叠问题与超时设计
合规巡检常用的触发周期是每15分钟或每小时。SCF定时触发器是按时推送的,它不会关心上一次执行是否结束。如果你的任务跑了20分钟,但触发器周期是15分钟,新的执行会在另一个实例上并行启动,两个任务同时拉取同一批阿里云账号,很容易触发API限流,还会产生重复写数据。
我遇到过最典型的情况:凌晨高峰期任务重叠,阿里云侧突然返回大量Throttling,SCF函数自动重试,所有实例堆在一起,CMP侧收到重复数据,还得额外做幂等去重。后来我在函数入口最前面加了一个分布式锁,锁的粒度是整个巡检批次:如果当前批次已经在执行,新触发的实例直接返回并记录skip日志,不做数据处理。
锁的实现可以直接用SCF环境里已有的资源,比如写一个租约到COS或Redis,带上过期时间。伪逻辑大致是:
def acquire_lock(batch_id, expire_seconds=1200): # 尝试写入带 TTL 的锁标记 # 成功写入且未过期,代表获得锁 # 如果锁存在且未过期,直接返回 False超时时间设置比任务周期长,同时锁过期时间比超时时间长一点,保证任务还在执行时锁不会提前失效。重叠问题解决后,API限流和重复数据基本就消失了。
此外,还要注意定时触发器的时间表达式。合规巡检要拉取阿里云的操作日志,最好指定一个固定的执行窗口,比如每小时的整点后5分钟,避开大多数业务系统的整点定时任务峰值,能减少被限流的概率。
4.3 冷启动处理:从预热到常驻实例
SCF冷启动在合规拉取这个场景里属于“低频高耗”的痛点:每次任务间隔可能几小时,实例早就被回收了,下一次触发必然冷启动。冷启动不光要加载运行时,还要执行模块导入和Client初始化,整体耗时可能多出几秒到十几秒。
解决冷启动我试过三种方案:
- 定时预热:在真正任务开始前1分钟,用另一个轻量触发器请求一次函数实例,让实例先完成初始化,主任务触发时复用预热好的实例。这个方法简单,但会额外产生一次调用费用。
- 预置并发:腾讯云SCF支持为函数配置预置并发,指定多少实例常驻。对合规巡检这种周期任务,在触发窗口前提前扩容到一定并发,能保证任务开始时实例是热的。费用更高,适合对执行时长有硬指标的场景。
- 架构层规避:把“周期性巡检”改成“事件驱动+状态机拆分”。SCF这个函数只负责处理一批子任务,由Step Function或事件队列按需拉起,而不是定时器一把梭。这样每次调用的数据量更小,冷启动的影响面也小很多。
我最后采用的是“定时预热+预置并发”组合:任务前5分钟预热两个实例,任务开始时用预置并发保证至少2个实例常驻。实测冷启动耗时几乎没有了,整体执行时间更稳定。
5. 代码与SDK层的性能细节:复用、并行与数据裁剪
5.1 阿里云SDK Client的复用姿势
SDK层的性能优化,第一原则就是“能复用就不重建”。除了前面说的全局Client,还要注意凭证获取的方式。如果是长期AK/SK,直接放环境变量或密钥管理服务里,函数全局读一次即可;如果是STS临时凭证,不要把AssumeRole放在每次请求里,而是对整个执行批次只换取一次,过期前复用同一个凭证。
阿里云SDK的指令级细节也值得留意。比如Python SDK基于Tea框架,初始化Client时指定connect_timeout和read_timeout,不宜过大。我见过配置默认超时45秒的,网络一抖动,一次失败请求能拖很久才能触发重试。合规场景建议连接超时5秒、读取超时30秒,再配合SDK的RuntimeOptions做单请求级别的超时覆盖。
from alibabacloud_tea_util.models import RuntimeOptions runtime = RuntimeOptions( connect_timeout=5000, read_timeout=30000, autoretry=True, max_attempts=3 ) response = client.lookup_events(request, runtime)autoretry开启之后,SDK会在遇到网络错误和部分服务端错误时自动重试。不过对Throttling这种限流错误,默认重试策略可能不够,我建议在业务代码里单独捕获异常,做指数退避重试,初始等待1秒,最大等待10秒。
5.2 多账号并发拉取的线程池写法
合规巡检最大的性能瓶颈往往不是单次API调用,而是循环串行。比如有50个账号、5个区域,每个区域要拉配置审计数据再加操作审计数据,串行执行就是几百次API调用排着队。改成线程池并发之后,整体耗时直接除以并发数,效果立竿见影。
Python在SCF里用ThreadPoolExecutor是安全的,因为SDK的调用是IO密集型,线程切换成本很低。但要注意两点:一是线程数量和阿里云侧QPS限制要匹配,不要一次性拉50个线程去打一个API;二是多个线程共用一个Client时,必须确认SDK的Client是线程安全的。阿里云Tea框架的Client内部有连接池,多线程调用同一个Client在实测中没出现过问题,但稳妥起见我会给每个线程单独创建一个Client,因为Client初始化我们已经通过全局复用压到最低了,多创建几个的成本可忽略。
一个可用的并发模式:
from concurrent.futures import ThreadPoolExecutor, as_completed def pull_one_account(account_id, region_id): client = get_or_create_client(region_id) # 拉取该账号在该区域的合规数据 return client.lookup_events(...) with ThreadPoolExecutor(max_workers=8) as executor: futures = [ executor.submit(pull_one_account, acc, region) for acc in account_list for region in region_list ] for future in as_completed(futures): result = future.result() # 写入结果队列线程数的选择,我一般保守设置为账号数乘区域数开平方再乘2,同时设一个最大上限16。这样既利用了并发优势,又不会把阿里云OpenAPI的流控打满。
5.3 序列化与数据裁剪
阿里云合规接口返回的JSON往往包含大量冗余字段,比如资源详情里的Tags、关联资源信息,对CMP的合规判断可能根本用不上。每多传一个字段,网络传输和内存解析就多一点开销。优化措施有两个:
第一个是请求参数裁剪。OpenAPI基本都支持在请求参数里指定要返回的字段或过滤条件,尽量在源头就把数据量降下来。比如ListDiscoveredResources支持按资源类型、合规状态过滤,如果只需要“不合规”的资源,就只拉ComplianceType=NonCompliant的数据,返回体可以小很多。
第二个是序列化库选型。Python自带的json库在解析大字符串时性能一般,换成orjson或ujson通常有2到5倍的提升。在SCF层导入orjson时需要把依赖打包进部署包,腾讯云SCF支持自定义依赖层,操作上不复杂。如果用的是Node.js运行时,原生JSON.parse已经足够,不需要额外引库。
数据转换完成后,如果单批次数据量太大,不要把全部数据一次性塞进内存再写存储,而是边转换边用流式方式写入对象存储或消息队列。这样内存压力和SCF最大运行内存的紧张关系也能缓解。
6. 调优实测中踩过的几个关键坑
6.1 时间戳与游标:最容易出错的边界
阿里云ActionTrail的LookupEvents返回结果里有一个NextToken字段,用于下一页数据。这个字段在某些接口里是字符串,在某些接口里是数字,而且“下一页没有更多数据”时的表现也不同——有的返回None,有的返回空字符串,有的直接返回一个合法的但不可用的值。我第一次做分页时只判断了if next_token:,结果在数据量大的时候漏掉了最后一页,在数据量小的时候反而多请求了一次。
解决办法是严格按接口文档约定的终止条件来判断,同时给分页循环加一个最大次数保护。比如设置最多拉取50页,超过就强制退出并告警,防止极端情况下死循环烧时间。
时间字段也要特别小心。阿里云合规工具的返回时间绝大多数是ISO 8601格式,但有的接口会返回毫秒级时间戳,有的是秒级。CMP侧如果统一用毫秒存储,必须做一次转换,否则后面做时间轴展示时全是错位数据。这看起来是数据质量问题,但转换逻辑写差了,在SCF里也会增加额外耗时。
6.2 日志打通:把请求ID串起来排查
跨云集成最痛苦的事情之一,就是出问题时不知道是哪一端的锅。SCF侧打了日志说调用了阿里云API,阿里云侧却说没收到请求;阿里云侧报了Throttling,SCF日志里却是超时。后来我把两边的标识串起来才定位到问题。
具体做法是:SCF每次调用OpenAPI之前,生成一个自定义的client_request_id(或者用阿里云SDK返回的request_id),连同账号ID、区域、API名称一起结构化输出到日志。阿里云侧的OpenAPI调用记录里也能看到请求ID,拿着请求ID去两边对照,能很快判断是网络链路问题、权限问题还是限流问题。
日志本身也要控制成本。合规巡检一次任务会产生成千上万条日志,如果每条都打印完整响应体,日志费用和写入开销都会拖慢函数。我只打印请求ID、耗时、返回码和数据条数,响应体截断前200字节,出问题时再临时开调试日志。
6.3 什么时候该收手不优化
调优到最后,我提醒自己不要陷入无限优化的陷阱。合规巡检的性能指标最初定的是“500个账号、全区域数据,15分钟内完成”,达到这个目标之后,继续调内存档位、调整线程数、优化序列化库,边际收益都很小了,但复杂度在增加:代码可读性变差、底层依赖变多、后续维护成本升高。
优化顺序我个人建议是:先把代码层能省的全省掉(Client复用、并发、数据裁剪),再调整函数配置(内存、超时、预热、预置并发),最后才考虑网络架构层面的改动(专线、云联网、固定出口IP)。我这次调优里,代码层优化贡献了约70%的耗时缩减,函数配置优化贡献了约25%,网络层基本没动就满足了指标。先做性价比最高的那部分,别为了优化而优化。
这个场景后续如果要扩展,可以把SCF拆成“采集器”和“清洗器”两个函数,用消息队列解耦,采集器只负责拉数据,清洗器负责格式转换和入库,这样各自的规格和并发都能独立调优,扩展起来更灵活。不过那是下一步的事了,先把当前的函数稳定跑一段时间再说。