在使用阿里云函数计算平台时,如果您曾经遇到过以下问题,本文应该会对您有所帮助:
- 第三方依赖包太大,每次更新代码都非常耗时,甚至会出现超过代码包限制的情况,我该怎么办?
- 安装第三方依赖包后,可以在本地运行成功,上传到阿里云函数计算平台上就会报错,这是什么情况?
- 有很多常用的依赖包,很多用户应该都会用到,阿里云函数计算官方不能直接内置到运行时环境中么?
- 我在多个函数中有相同的依赖包,我该如何管理这些相同的依赖包?
层 提供了集中管理,跨多个功能且能够共享代码和数据的方法。
2021 年 1 月,阿里云函数计算发布了 “自定义层”功能,让用户可以自定义层,并支持跨函数共享。2022 年 8 月,阿里云函数计算发布“公共层”功能,提供了官方公共层,供用户直接使用,进一步提升了用户体验。
接下来我们先介绍下 “自定义层” 的功能和作用。
自定义层
在层功能发布之前,必须将代码与代码的依赖项一起打包和部署,这些依赖项在不同函数中可能是相同的,很多情况下这些依赖项的大小,远远大于代码的大小。
但是在层功能发布后,我们可以将代码的依赖项,或者多个函数中共享的部分打包成 Zip 压缩文件,并作为函数计算的自定义层发布,不同函数都可以使用该自定义层。
阿里云函数计算会在调用时将层与函数代码一起加载。可参考文末文档:创建自定义层[1]、在函数中配置自定义层[2]
为什么使用自定义层?
使用自定义层有以下优势:
(1)跨函数复用代码
将多个函数中的通用代码或数据提取出来,打包成 Zip 包,做成自定义层,供不同函数引用,避免了在多个地方维护通用的代码或数据。
与此同时,也实现了依赖项和业务逻辑的分离,用户可以专注于核心的业务逻辑。
(2)使代码包更小
函数的代码包越来越大时,部署速度也会越来越慢,导致函数的维护和测试愈加困难。
此外函数代码包大小也有限制,比如阿里云函数计算的代码包限制为 500MB (2022 年 9 月),层是突破该限制的方法之一。层也有大小限制,目前单个层的代码包大小限制为 500MB,单个函数最多可配置 5 个层,总大小不能超过 2GB。
(3)加速代码部署,简化函数管理
函数代码包越小,代码包的部署就越快。尤其是一些大型依赖项时,核心功能代码可能只有几兆字节,但依赖项可能有几百兆。比如 Puppeteer 依赖包超过 100MB,阿里云的 DataX 依赖包超过 800MB。
一般来讲,这些依赖项很少修改,因此将他们打包成层后,可以避免在核心代码修改时频繁修改这些大型依赖项。对这些依赖项也可以拆分成多个层,每次修改一个功能时,只需要更新其中一个层。
比如我们实现了自定义运行时 Python3.10 以及该运行时兼容的科学计算库 SciPy,可以将自定义运行时和依赖包拆成两个层,当需要更新依赖包时,只需要更新依赖包的层,而自定义运行时的层保持不变。
自定义层的困境
(1)制作层有一定门槛
层的 Zip 包有一定的格式规范,用户需要按照该规范进行制作。以 Python 的 requests 库为例,依赖打包后的文件结构为:
my-layer-code.zip └── python └── requests
为什么有这种要求呢?这个涉及到不同运行时在搜索第三方依赖包的实现逻辑,以 Python 为例,Python 运行时会在 sys.path 路径下搜索依赖包,上面的 Zip 包会解压到函数实例的 /opt 目录下,解压后 requests 这个包就放到了 /opt/python 目录下。
然后,函数计算平台会将一些特定的目录放到运行时语言的依赖搜索路径上,比如 Python 运行时就会将 /opt/python 放到 sys.path 中,这样,代码中就可以直接引用 requests 库了。其他运行时的使用方法可参考文末文档-创建自定义层。当然,你也可以不按照这个格式规范来制作层,此时就需要在代码中添加对应的搜索路径了,具体方法可参考文末文档-如何在 Custom Runtime 中引用层中的依赖?[3]
需要在指定操作系统和处理器架构下制作层。有一些依赖是与操作系统和处理器架构有依赖关系的,比如 Python 的科学计算库 NumPy,假如你在 M1 芯片的 MacOS 下安装,其版本为:
numpy-1.23.3-cp39-cp39-macosx_11_0_arm64
可看到兼容的操作系统为 mac os, 处理器架构为 arm64。但在函数计算平台的实例环境为 Linux x86_64,操作系统目前使用的发行版为 Debian 9,因此在 M1 Mac 下安装的 NumPy 库不能在阿里云函数计算平台使用。我们推荐在 Debian 9 系统下进行安装,但用户本地可能没有该环境,您可以使用在线构建依赖库或者使用函数计算官方运行时镜像来构建,此处不再赘述。
层需要包含新增的共享动态库。有些依赖库需要安装额外的共享动态库,在构建层的 Zip 包时也需要包含这些共享动态库。例如 Nodejs 的依赖库 Puppeteer,需要额外安装二十多个共享动态库(如 libxss1,libnspr4 等),这些依赖库都要打包到层 Zip 包中。如何成功的安装 Puppeteer 库并不是简单的事情。共享动态库推荐放到 Zip 包的 lib 目录下,函数计算平台会将/opt/lib 目录添加到 LD_LIBRARY_PATH(仅限于内置运行时)。
(2)无法跨账号共享
自定义层默认只能在同账号同地域的不同函数之间共享,无法进行跨账号共享。因此,用户 A 创建的自定义层无法给用户 B 使用,这不仅给用户带来了重复的工作量,也不利于宿主机上相同层的复用。
公共层
由于自定义层的这些痛点,阿里云函数计算在 2022 年 8 月发布了公共层功能。实现层跨账号共享,并提供了一些官方公共层[6]供用户直接使用,方便用户快速开发示例原型。阿里云函数计算平台主要提供了三类官方公共层:
- 自定义运行时(如 Python 3.10、Nodejs17、PHP 8.1、Java17、.NET 6 等)
- 常用依赖库(如 PyTorch、Scipy、Puppeteer 等)
- 阿里云 SDK(如 Aliyun DataX )
详情可参考文末官方文档-在函数中配置官方公共层[4],目前官方公共层仍在持续补充,如果您有需要的运行时或者依赖库想通过官方公共层的方式使用,可通过钉钉答疑群(钉钉群号:11721331)与我们联系,也可以直接在 Github[5]上提交 issue。
如何公开自定义层?
目前,层公开功能在内测中,如有需求可以通过钉钉联系我们。同时,我们也非常欢迎大家贡献公共层到仓库,我们很快会在该仓库提供公共层贡献的方法和示例。
示例
官方公共层的最新版本和使用说明可参考 Github,下面我们介绍一些使用官方公共层的典型示例。
示例一、基于 Nodejs16 + Puppeteer 实现网页截图示例程序
Puppeteer 是一个 Nodejs 库,它提供了高级的 API 并通过 DevTools 协议来控制 Chrome(或 Chromium)。通俗来说就是一个 headless chrome 浏览器,可以使用它完成很多自动化的事情,比如:
- 生成网页截图或者 PDF
- 做表单的自动提交、UI 的自动化测试、模拟键盘输入等
- more...
本示例使用 Puppeteer 完成一个网页截图示例程序。
首先,我们使用内置运行时 Nodejs16 创建一个函数 start-puppeteer,其中请求处理程序类型选择“处理 HTTP 请求”。
然后,在高级配置中将内存规格设置为 1GB,示例程序的内存使用大概在550MB左右。
创建成功后,在控制台上打开 index.js文件,将下面的代码拷贝并覆盖该文件,点击部署按钮。
const fs = require('fs'); const puppeteer = require('puppeteer'); function autoScroll(page) { return page.evaluate(() => { return new Promise((resolve, reject) => { var totalHeight = 0; var distance = 100; var timer = setInterval(() => { var scrollHeight = document.body.scrollHeight; window.scrollBy(0, distance); totalHeight += distance; if (totalHeight >= scrollHeight) { clearInterval(timer); resolve(); } }, 100); }) }); } module.exports.handler = function (request, response, context) { console.log('Node version is: ' + process.version); (async () => { const browser = await puppeteer.launch({ headless: true, args: [ '--disable-gpu', '--disable-dev-shm-usage', '--disable-setuid-sandbox', '--no-first-run', '--no-zygote', '--no-sandbox' ] }); let url = request.queries['url']; if (!url) { url = 'https://www.serverless-devs.com'; } if (!url.startsWith('https://') && !url.startsWith('http://')) { url = 'http://' + url; } const page = await browser.newPage(); await page.emulateTimezone('Asia/Shanghai'); await page.goto(url, { 'waitUntil': 'networkidle2' }); await page.setViewport({ width: 1200, height: 800 }); await autoScroll(page) let path = '/tmp/example'; let contentType = 'image/png'; await page.screenshot({ path: path, fullPage: true, type: 'png' }); await browser.close(); response.setStatusCode(200); response.setHeader('content-type', contentType); response.send(fs.readFileSync(path)) })().catch(err => { response.setStatusCode(500); response.setHeader('content-type', 'text/plain'); response.send(err.message); }); };
简要介绍一下上述代码的核心逻辑,首先代码会解析 query 参数获取需要截图的 url 地址(如果解析失败则默认使用 Serverless Devs 官网主页),然后使用 Puppeteer 对该网页进行截图,并保存到运行实例的 /tmp/example
文件中,然后将该文件作为HTTP请求的返回体直接返回。
然后,我们需要配置 Puppeteer 公共层,在函数配置中找到层,点击编辑,选择添加官方公共层。
选择官方公共层 Puppeteer17x,目前最新的层版本为1。
参考官方公共层 Nodejs-Puppeteer17x README 添加环境变量,对于版本1,需要添加 LD_LIBRARY_PATH=/opt/lib/x86_64-linux-gnu:/opt/lib
环境变量。
最后,使用触发器管理中的测试地址进行测试验证。
测试结果如下所示,已成功将 Serverless Devs 官方进行截图。
示例二、基于公共层快速实现 .NET 6 自定义运行时
首先,通过控制台创建 .NET 6 自定义运行时。在最上层选择 “使用自定义运行时创建”,选择“处理 HTTP 请求”,选择 .NET 6
运行时,其他配置使用默认值。
创建成功后,可以通过 WebIDE 看到示例代码 Program.cs
示例代码中需要注意四个部分:
- 该示例监听了0.0.0.0的9000端口,Custom Runtime启动的服务一定要监听0.0.0.0:CAPort或*:CAPort端口,不能监听
127.0.0.1
或localhost
。详情参考文档 Custom Runtime>基本原理。 - 添加路由
/
,直接返回字符串 "Hello World!" - 添加路由
/invoke
,该路由为使用事件请求处理程序的路径,可参考文档 Custom Runtime >事件请求处理程序(Event Handler) - 添加路由
/initialize
,该路由为函数初始化回调程序对应的路径,该方法会在示例初始化时执行一次,可参考文档 Custom Runtime >函数实例生命周期回调
首先,我们直接使用触发器管理页面中的测试地址进行测试,此时不添加任何 PATH 信息,结果如下图所示:
然后,我们测试添加 /invoke
路径进行测试,因为该路由方法为 POST,我们直接使用 curl -XPOST
测试:
同样,我们用这种方法测试一下/initialize
注意:此处只是做测试,初始化回调函数不需要主动调用,函数计算平台会在实例启动后自动调用该回调方法(不要忘记在配置里启用 initializer 回调程序)
最后,我们再做一个小测试,在触发器管理页面将HTTP触发器删除,删除后该函数类型会转换成事件请求处理程序,在函数配置中,将 Initializer 回调程序启用
在控制台上测试该函数,结果如下图所示:
点击实时日志按钮,可以看到在该请求执行前,已经执行了 Initialize 回调方法。
层的最佳实践是什么?
前文介绍了什么是自定义层,为什么使用自定义层,什么是公共层,并介绍了两个官方公共层的示例。但我们对层的使用仍然还有一些疑惑,比如什么场景下推荐使用层?层与代码包有什么区别?有没有与层相似的功能?与这些相似功能相比,层的优缺点是什么?接下来尝试回答一下这些问题。
什么场景下推荐使用层?
目前,使用层的场景主要有两类,一类是自定义运行时,另一类是各种语言的依赖库。强烈推荐通过层来构建并使用自定义运行时,但对于各类语言的依赖库,可以参考下面这些建议:
- 推荐优先使用官方公共层
- 非编译型语言的依赖库推荐使用层来管理,对编译行语言需要根据实际情况进行判断(比如,对自定义运行时,如果使用JAR包的方式运行 Java 程序,则无法引入层中的依赖,可参考文档 如何在Custom Runtime中引用层中的依赖?)
- 如果依赖库较大,并且没有超过层的限制大小,推荐使用层
- 如果依赖库需要额外安装共享动态库,推荐使用层(如果构建比较复杂,可联系函数计算团队制作)
- 如果在多个函数、多个账号之间有共享代码或数据的需求,推荐使用层
层与代码包有什么区别?
直观上看,层就是把原来代码包的一部分内容拆分出来,再重新建一个代码包而已,那为什么又建立一个层的概念呢?这里的主要区别是层与代码包的设计理念不同。
- 层有更简洁的版本管理方案
层的版本是从 1 开始自动递增的,目前一个层最大支持 100 个可用版本(不包括已删除的版本);而对代码包来讲没有版本的概念,只有在服务层面上有版本概念,相对层的版本会更加复杂。
- -层版本是只读的,不可变的
一个层的版本在创建后内容是无法改变的(权限除外),如果想修改层的内容,只能发布一个新的版本。层版本的只读特性能够避免了层的改动对函数的影响。
- 层的共享能力
层可以跨函数、跨账号进行共享,而代码包不支持。
- 层版本的软删除策略
层版本删除后,不会影响已经配置改层版本的函数的正常运行。因为在层版本删除是,阿里云函数计算平台并不会直接将层版本的代码删掉,而是先进行一次软删除操作,避免新的函数使用已删除的层版本,当该层版本没有函数引用时,才会彻底删除该层版本。
函数计算中有没有与层相似的功能?与相似功能相比,层的优缺点是什么?
在阿里云函数计算平台中,与层类似的功能是服务配置中的“挂载NAS文件系统”和“挂载 OSS 对象存储”功能,层与挂载NAS/OSS在功能和应用场景上有一些明显的差异:
简单总结一下,如果代码或数据的大小超过层的限制,则推荐使用挂载 NAS/OSS 的方式;如果代码或数据会镜像改动,或者有运行时修改数据的需求,那么这里也推荐使用挂载 NAS/OSS 的方式。
结语
在阿里云函数计算中,层的定位是一种不可变的基础设施,通过层版本的只读特性保证层的一致性和可靠性。本文首先介绍了自定义层的特点和困境,然后介绍了近期发布的公共层功能,详细陈述了基于官方公共层实现的两个示例程序,最后探讨了层的最佳实践是什么,希望通过本文能让读者更好的理解层的概念及其应用场景。
层的功能仍在持续完善中,接下来我们会在一下几个方向进行重点优化:
- 完善官方公共层体验,补充更多的常用依赖库或自定义运行时作为官方公共层,并提供完善的应用示例。
- 提供公共层贡献的方法和示例,促进公共层的开源共建。
如果对层的使用有任何的疑惑或者建议,欢迎搜索(群号:11721331)进入阿里云函数计算钉钉群联系我们。
更多内容关注 Serverless 微信公众号(ID:serverlessdevs),汇集 Serverless 技术最全内容,定期举办 Serverless 活动、直播,用户最佳实践。
More:
- 创建自定义层 :
https://help.aliyun.com/document_detail/193057.html
https://help.aliyun.com/document_detail/193058.html
https://help.aliyun.com/document_detail/71142.htm?spm=a2c4g.11186623.0.0.2c77481ftZBbfb#task-1881232
- 在函数中配置官方公共层
https://help.aliyun.com/document_detail/451191.html
- 阿里云函数计算 公共层github:
github.com/awesome-fc/awesome-layers
- 官方公共层
https://help.aliyun.com/document_detail/425055.html#section-ffl-tm3-txg
- Custom Runtime 事件请求处理程序
https://help.aliyun.com/document_detail/191342.html
- Custom Runtime 函数实例生命周期回调