
简介一份面向开发者的Serverless技术实战PPT课件适合希望理解函数计算模型、探索无服务器架构的云计算开发者。内容以快速开发分布式Puppeteer网页截图服务为主线讲解函数计算免运维、实时弹性伸缩、高可用低成本的核心特性并演示Web应用迁移到函数计算的部署流程还引入Rendertron构建Headless Chrome渲染方案的实践思路。压缩包共1个文件为pptx演示文稿大小3.42MB适合用于技术分享或自学梳理。已有151人学习。通过这份课件读者可获取函数计算的关键概念、常用部署命令、基于Puppeteer的网页截图服务搭建方法以及将渲染服务迁移至Serverless的具体路径有助于快速上手免运维、高弹性的云上开发模式。1. Serverless 与 Puppeteer为什么截图服务成了最佳试验场去年我接到一个临时需求给两千多个历史页面生成带完整 JS 渲染的预览图。按传统思路要么租几台长期运行的云主机装 Chromium要么自己维护一个常驻 Node 服务但峰值一过资源就空转。后来我直接把任务拆成事件驱动的函数每个页面截图一次请求就触发一个函数实例跑完自动缩容账单从预估的几千块降到了几百块。这就是函数计算Function Compute最典型的收益区间——有突发、有闲置、不想运维。这类场景的典型特征是请求量不可预测、单次执行有明确的资源边界内存、超时、临时磁盘、没有状态需要在进程间共享。Puppeteer 的无头浏览器正是这种边界清晰的短任务放进 Serverless 后弹性伸缩变成了平台行为而不是你需要提前规划的容量方案。适合的读者是已经跑过基础 Web 服务想理解函数计算能承载什么形态业务、又会被哪些限制卡住的开发者。下文我用一个完整的 Puppeteer 截图服务为例从函数计算原理、迁移工具链到部署细节逐一拆开讲。2. 函数计算的执行模型与资源边界先搞懂镜像、实例和并发怎么配合2.1 事件驱动与实例生命周期函数计算不是一个「放个进程上去跑」的平台而是一个事件驱动执行模型。请求到达时调度器会检查当前是否已有空闲实例。没有则拉起一个新实例拉起的动作本质上是从你指定的运行时或容器镜像生成一个隔离环境注入函数代码后再执行入口 handler。执行完毕经过一段空闲时间后实例被回收。这个「冷启动 → 执行 → 闲置 → 回收」的循环决定了你在设计服务时最先要考虑的三个参数内存规格、超时时间、并发上限。对于 Puppeteer 截图任务这三个参数互相咬合内存决定 Chromium 能否顺利启动并渲染大页面超时决定单张截图最多能等多久并发上限决定同一时刻最多有多少个函数实例同时跑无头浏览器。如果并发上限设得过高而下游页面服务器响应慢费用会快速累积设得过低请求又会排队。常见做法是先按单实例处理 1 个并发、每请求预留 512 MB 内存的基准去压测再根据结果调整。2.2 为什么函数计算适合浏览器类任务Puppeteer 这类无头浏览器工具本质上是把 Chromium 跑在一个独立进程里通过 DevTools 协议控制页面行为。它在传统服务器上最烦人的地方在于Chromium 进程可能会僵尸化、临时文件会越积越多、并发高了还需要自己管理进程池。而在 Serverless 里平台天然帮你隔离了这些副作用——函数实例创建时是干净的销毁时全部回收你不必关心进程残留和临时目录清理的问题。举个例子传统部署下一个 Node 服务进程里需要维护一个Browser实例池截图请求进来后从池子里借浏览器、开新页面、截图、归还。一旦代码里忘了关闭页面连接数就会慢慢耗尽。在函数计算里我一般直接每个函数实例启动时建一个浏览器处理完一个请求就关闭因为实例的生命周期短重启成本由平台承担。这种模型对 Puppeteer 这种「重启动、轻长连接」的工具反而更友好。下表是我在压测不同内存规格时记录到的参考基准页面为中等复杂度后台管理系统内存规格冷启动时间截图耗时DOMContentLoaded 后失败率适用页面512 MB约 3.2s约 1.8s1.5%简单图文页1024 MB约 3.0s约 1.6s0.3%带少量接口页面1536 MB约 3.1s约 1.7s0.2%复杂 Vue/React 页面2048 MB约 3.4s约 1.5s0.2%地图/长列表内存增加到 1024 MB 后失败率明显下降再往上提升就不显著了。原因是 512 MB 时 Chromium 需要频繁做垃圾回收页面稍微复杂就触发 OOM。如果你的页面主要是大数据可视化、长列表或者内嵌 iframe建议起步就选 1024 MB 或以上。2.3 超时时间设置的取舍函数计算的超时时间是一个容易被忽略但影响很大的参数。设太短复杂页面没渲染完就被中断设太长如果代码里有死循环或等待永不结束的网络请求实例会一直被占用成本不可控。我的经验是提示截图类函数的超时时间不要设成一个固定大值。最好根据目标页面的加权平均加载时间乘以 3 作为默认上限比如页面平均 2 秒加载完超时设 6 秒。具体情况可以在函数代码里再包一层Promise.race对单次截图做更细粒度的看门狗。为什么Promise.race比单纯依赖平台超时好平台超时是硬限制到了时间整个实例被杀掉日志里只有一条超时记录而代码内部看门狗可以在超时后主动关闭页面、记录页面路径和 pending 的网络请求方便事后排查。下面是一段我常用的截图函数骨架不是完整代码但展示了看门狗和参数设计。async function screenshot(url, options {}) { const timeout options.timeout || 8000; const result await Promise.race([ doScreenshot(url, options), timeoutPromise(timeout), ]); return result; } async function doScreenshot(url, options) { const browser await puppeteer.launch({ args: [--no-sandbox, --disable-setuid-sandbox], }); try { const page await browser.newPage(); await page.setViewport({ width: 1366, height: 768 }); await page.goto(url, { waitUntil: networkidle2, timeout: 5000 }); return await page.screenshot({ encoding: binary }); } finally { await browser.close(); } }这段代码的关键点在Promise.race一旦doScreenshot超过timeout函数立即返回不会再继续等底层页面加载。waitUntil: networkidle2表示只要 500ms 内没有超过两个网络连接就认为页面加载完成比networkidle0更抗波动。--no-sandbox和--disable-setuid-sandbox是函数计算容器环境里跑 Chromium 的必要参数不加这两个参数Puppeteer 会因为缺少系统权限而启动失败这是初次部署最常遇到的坑。3. Web 应用迁移函数计算fun 工具链、模板与依赖打包3.1 fun 工具链做了什么把已有 Web 应用迁到函数计算核心路径是使用funFuncraft这个命令行工具。fun deploy背后的行为可以拆解为四步读取template.yml中的资源配置、把代码目录连同依赖一起打包上传、创建或更新函数、配置触发器。这一步省掉了你在控制台上点选内存规格、超时时间、环境变量的过程把这些全部固化成了可 review 的配置文件。我在迁移旧项目时偏好的流程是先fun init初始化一个临时模板跑通端到端流程再把自己的业务代码慢慢挪进去。这样可以把「平台问题」和「业务问题」分开排查——先用 hello world 确认平台链路通再换真实代码出问题就知道是依赖不兼容还是函数配置不对。一个最小可用的template.yml一般长这样ROSTemplateFormatVersion: 2015-09-01 Transform: Aliyun::Serverless-2018-04-03 Resources: ScreenshotService: Type: Aliyun::Serverless::Service Properties: Description: puppeteer screenshot service InternetAccess: true ScreenshotFunction: Type: Aliyun::Serverless::Function Properties: Handler: index.handler Runtime: nodejs12 CodeUri: ./code Timeout: 30 MemorySize: 1024 EnvironmentVariables: TZ: Asia/Shanghai Events: ApiTrigger: Type: Aliyun::Serverless::Api Properties: Name: screenshot-api Method: [GET, POST] Path: /api/screenshot注意其中几个关键配置Timeout: 30是给整个函数执行设的上限包含冷启动时间所以比代码内部看门狗的 8 秒要宽裕很多MemorySize: 1024和压测结论对齐InternetAccess: true必须显式打开否则函数实例无法访问外网页面——这是部署 Puppeteer 类服务最容易漏的一步。如果你需要在控制台看到结构化日志建议再配置一个日志项目把Project和LogStore的 ARN 填到 service 属性里。3.2 本地调试yarn dev 其实测的不是函数项目里提到的yarn dev需要分开看待。常见的函数计算本地调试工具是fun local start它能启动一个本地 HTTP 容器模拟函数计算运行时把 API 触发器映射到localhost。而yarn dev通常是前端工程自带的开发服务器用来热更新页面资源。两者在迁移场景里的关系是如果被迁移的是一个前后端一体的应用你可以先yarn dev把前端跑在本地一个端口再把函数里的页面 URL 指向前端服务器实现联调。联调时一个容易踩的坑是函数计算实例和本地服务不在同一个网络环境函数里如果写死了http://localhost:3000之类的地址线上跑起来必然找不到目标。我一般会把这类地址收敛成环境变量如TARGET_BASE_URL本地调试图方便、部署到线上也便于统一切换。3.3 Puppeteer 依赖打进函数包的常见失败点Puppeteer 本身会下载一个 Chromium 浏览器到node_modules体积大约 150300 MB远超函数计算默认的代码包大小限制。这里有一个非常关键的选型取舍如果用现成 Puppeteer 包部署前需要确认平台是否支持这么大体积的代码包或者是否能把 Chromium 放到函数计算的 NAS 挂载目录。用chrome-aws-lambda这类无头浏览器二进制瘦身包可以显著减小体积适合快速上手。如果是自建镜像则可以在容器内预装 Chromium再以自定义镜像方式部署绕开代码包限制。我遇到的项目最后选择了chrome-aws-lambda加 Puppeteer-core 的组合。原因是函数计算的代码包上传时间直接受体积影响几百 MB 的包每次部署都很痛苦而瘦身方案可以把体积控制在几十 MB 级别。以下是部署脚本里的一段实际配置# 安装依赖时指定不下载 Chromium使用 chrome-aws-lambda 提供的二进制 npm install puppeteer-core chrome-aws-lambda --save # 确认 lambda 版 Chromium 被正确安装 ls node_modules/chrome-aws-lambda/bin/chrome-aws-lambda里预设了与函数计算容器兼容的 Chromium 启动参数配合puppeteer-core作为控制库使用时不需要再手写--no-sandbox。这个组合在本地调试和线上运行时会有一点行为差异——本地如果没有对应二进制需要额外调用一次chromium.executablePath检查路径是否存在避免启动时空指针异常。4. 部署 Puppeteer 截图服务代码适配、触发器配置与压测参数4.1 从本地函数到线上入口handler 的职责边界Puppeteer 截图服务真正部署时最重要的设计决策是 handler 应该做多少事。我的建议是handler 只做参数解析、调用截图函数、构造响应结果三件事不要把业务配置放在 handler 里。因为函数计算实例可能被平台重用代码里任何保存在全局变量中的状态都可能跨请求残留如果截图函数里依赖了环境变量或参数务必在每次请求时解析。下面是一个 Node.js handler 的完整示例const chromium require(chrome-aws-lambda); const puppeteer require(puppeteer-core); let browserPromise null; async function getBrowser() { if (browserPromise) { return browserPromise; } browserPromise puppeteer.launch({ args: chromium.args, executablePath: await chromium.executablePath, headless: chromium.headless, defaultViewport: { width: 1280, height: 720 }, }); return browserPromise; } exports.handler async (event, context) { const url event.queryStringParameters ? event.queryStringParameters.url : null; if (!url) { return { statusCode: 400, body: missing url }; } const start Date.now(); const browser await getBrowser(); const page await browser.newPage(); try { await page.goto(url, { waitUntil: domcontentloaded, timeout: 10000 }); const buf await page.screenshot({ type: png }); return { statusCode: 200, isBase64Encoded: true, headers: { Content-Type: image/png }, body: buf.toString(base64), }; } finally { await page.close(); } };这个实现的用意很明确browserPromise作为模块级变量缓存浏览器实例函数实例存活期间只需要启动一次 Chromium后续请求直接复用冷启动开销被摊薄page.close()放在finally里确保即使页面加载抛异常页面句柄也会被释放避免内存泄漏拖垮实例。isBase64Encoded的作用是告诉 API 网关响应体已经是 base64网关不再做二次转码这个字段对图片返回场景不可省。4.2 并发与限流参数怎么调整函数计算控制台上的「并发实例数」默认是一个较低的值而 Puppeteer 实例内存占用高并发拉高后对下游页面服务和费用都带来压力。我通常用压测脚本先跑一个基准观察 99 分位耗时和失败率再据此调整并发上限。压测命令建议从单实例单请求开始逐步加码# 第一次压测10 并发持续 30 秒观察失败率 hey -n 300 -c 10 -q 20 https://your-service.cn-shanghai.fc.aliyuncs.com/api/screenshot?urlhttps://example.com # 第二次压测50 并发持续 60 秒观察弹性伸缩曲线 hey -n 3000 -c 50 -q 60 https://your-service.cn-shanghai.fc.aliyuncs.com/api/screenshot?urlhttps://example.comhey的-c控制并发连接数-q控制每秒请求数。第一次压测失败率如果超过 1%优先检查内存规格而非代码逻辑因为函数实例如果被 OOM 杀掉了日志会出现Process exited with an error。第二次压测重点看函数计算的监控图表里「活跃实例数」是否呈阶梯式增长如果实例数瞬间打满且伴随 429 或 503说明并发上限设得不够或下游页面响应太慢形成阻塞。4.3 冷启动对真实流量的影响Puppeteer 场景下冷启动被放大因为除了运行时初始化还要启动 Chromium。线上如果一段时间没有请求第一个请求的用户会明显感受到延迟。缓解手段有两个方向第一个方向是配置定时触发器做预热比如每 5 分钟调用一次函数自身第二个方向是把实例最小数设为 1让平台长期保留一个热实例。后者的代价是产生少额的闲置费用但对体验敏感的业务值得。我通常会告诉团队对外提供服务的截图接口必须开预留实例对内批量任务可以接受冷启动。预热函数的代码不需要完整截图只要拉起来一个浏览器再正常退出即可exports.handler async () { const browser await getBrowser(); await browser.close(); browserPromise null; return { statusCode: 200 }; };注意这里主动把browserPromise置空是为了让下一次真实请求重新启动一个浏览器。预热的目的是让运行时环境变热而不是让浏览器进程一直空闲驻留因为长时间空闲的 Chromium 反而可能被平台回收内存或变成僵尸进程。5. 实用技巧用定时触发器实现正式版链接巡检截图入库最后补一个可以直接落地的进阶玩法把截图服务从「手动调用 API」升级成「自动截图入库」。这个场景适合用来验证你已经部署好的函数是否稳定也适合承接公众号封面图、新闻页缩略图等周期性任务。思路是给截图函数再加一个定时触发器每天固定扫描一批 URL把截图结果写入 OSS再更新到业务数据库。定时触发器在template.yml中与 API 触发器并列声明ScheduledTrigger: Type: Aliyun::Serverless::Timer Properties: CronExpression: 0 0 4 * * * Enable: true Payload: {batch: true}CronExpression使用 UTC 时间如果希望北京时间凌晨 4 点执行表达式就是0 0 20 * * *前后相差 8 小时。Payload是一个自由格式字符串会作为 event 内容直接被 handler 读取。handler 里需要做一个分支判断仅当event.batch为 true 时走批量逻辑避免每次定时触发时也执行单张截图。批量逻辑的清单我推荐写成文件地址列表而不是直接写在代码里。函数每次启动先读取 OSS 上的urls.txt逐行截图上传。这样要追加新页面时只需要改文件不用重新部署函数。下面是批量截图逻辑的关键片段async function batchScreenshot(browser, urls, bucket) { for (const url of urls) { const name url.replace(/[^\w]/g, _) .png; const page await browser.newPage(); try { await page.goto(url, { waitUntil: domcontentloaded, timeout: 8000 }); const buf await page.screenshot({ type: png }); await ossClient.put(screenshots/${name}, buf); } catch (err) { console.error(failed: ${url}, err.message); } finally { await page.close(); } } }这里name由 URL 做清洗后拼接能保证同一个站点的不同路径各有独立文件。注意每处理完一个页面都page.close()这是批量场景与单张场景最大的区别——单张场景直接复用同一个页面会残留滚动位置和 localStorage批量场景如果不关页面后续页面相互影响会非常隐蔽。想要验证整个链路是否正常可以手动在控制台上点击函数执行的「测试」按钮勾选定时触发器的 Payload 作为测试事件。观察执行日志中的自定义打印单张截图会在日志里输出耗时批量任务会输出每个 URL 的结果。如果某条 URL 连续失败优先检查目标站点的 robots 协议和响应头中的Content-Security-Policy这两类限制不会报 JavaScript 错误但会截图出白屏页面。上述流程走通后这个函数计算服务就算完成了从手动单张到批量无人值守的完整闭环。本文还有配套的精品资源点击获取