背景
未引入初始化函数功能前如何解决函数计算对上传代码包大小的限制问题?
阿里云 函数计算 为了保证函数计算系统的可用性更高,对用户上传的代码包做了 大小限制,要求原始代码包大小不超过 250MB,压缩后的代码包大小不超过 50MB。由于用户的函数逻辑可能需要大量的依赖库,所以代码包很容易达到函数计算设定的阈值。在未引入 initializer 接口之前,为解决这个问题,需要对代码包进行分类,除项目代码和少量依赖库可以在创建函数时上传,其他依赖库都需要预先上传到 OSS 中,当函数被触发时,再从 OSS 上下载并存放到磁盘指定目录。
解决了函数计算对代码包大小的限制后对用户有什么影响?
用户将部分依赖库预先上传到 OSS,并在函数被触发执行时开始从 OSS 上加载依赖, 这类依赖的加载操作均可定义应用层冷启动,当加载依赖结束后,应用层冷启动才结束,函数的处理逻辑才开始执行,应用层冷启动的开销往往会导致毛刺的产生,影响函数的性能。
如何做到既可以解决函数计算对上传代码包的限制问题,又不影响函数的性能呢?
为同时解决上述功能和性能问题,函数计算推出 initializer 功能。您可以将从 OSS 加载代码包的函数逻辑放在 initializer 函数中。函数计算保证在处理函数执行之前,initializer 函数成功执行过一次并且只有一次,从而可以保证在处理请求到达之前代码已全部加载完成,并且不会对处理函数逻辑的功能和性能产生影响。
案例实践
大部分场景中,用户代码包过大都是由于所依赖的库过大导致的,下文通过 python2.7 演示一个对图片旋转的案例。
图片旋转需要用到 tensorflow 和 opencv 库,两个库的大小还不及函数计算设置上传代码包大小的阈值,这里我们可假设已经超过了代码包大小的限制。
上传依赖库
本案例需要安装的依赖库有 tensorflow 和 opencv-python,需要提前在本地下载并打包上传到 OSS。推荐使用 fcli 工具的 sbox 命令,下面以 runtime 为 python2.7 进行操作:
cd <'此项目的根目录中'>
mkdir applib // 创建存储所有应用依赖的目录
fcli shell // fcli version >= 0.25
sbox -d applib -t python2.7
pip install -t $(pwd) tensorflow==1.8.0
pip install -t $(pwd) opencv-python
完成之后 exit 退出沙盒环境,并执行 exit 退出fcli。
编写函数
编写函数需要注意以下几点:
- 定义 initializer 入口并实现其接口,将从 OSS 加载代码包的逻辑放入初始化函数中。
- 由于对图片操作所用到的两个库需要在初始化函数中临时加载,所以可以把对函数处理的操作单独放到一个文件中,在处理函数中调用即可。
综合上面两点考虑,我们的代码结构如下:
project root
└─ code
├─ loader.py # 处理函数逻辑和 initializer 逻辑
└─ index.py # 图片旋转逻辑
└─ pic
└─ e2.jpg # 被操作图片
loader.py 代码如下:
# -*- coding:utf-8 -*-
import sys
import zipfile
import os
import oss2
import imp
import time
app_lib_object = os.environ['AppLibObject']
app_lib_dir = os.environ['AppLibDir']
model_object = os.environ['ModelObject']
model_dir = os.environ['ModelDir']
local = bool(os.getenv('local', ""))
print 'local running: ' + str(local)
def download_and_unzip(objectKey, path, context):
creds = context.credentials
if (local):
print 'thank you for running function in local!!!!!'
auth = oss2.Auth(creds.access_key_id,
creds.access_key_secret)
else:
auth = oss2.StsAuth(creds.access_key_id,
creds.access_key_secret,
creds.security_token)
endpoint = os.environ['Endpoint']
bucket = os.environ['Bucket']
print 'objectKey: ' + objectKey
print 'path: ' + path
print 'endpoint: ' + endpoint
print 'bucket: ' + bucket
bucket = oss2.Bucket(auth, endpoint, bucket)
zipName = '/tmp/tmp.zip'
print 'before downloading ' + objectKey + ' ...'
start_download_time = time.time()
bucket.get_object_to_file(objectKey, zipName)
print 'after downloading, used %s seconds...' % (time.time() - start_download_time)
if not os.path.exists(path):
os.mkdir(path)
print 'before unzipping ' + objectKey + ' ...'
start_unzip_time = time.time()
with zipfile.ZipFile(zipName, "r") as z:
z.extractall(path)
print 'unzipping done, used %s seconds...' % (time.time() - start_unzip_time)
def initializer(context):
if not local:
download_and_unzip(app_lib_object, app_lib_dir, context)
download_and_unzip(model_object, model_dir, context)
sys.path.insert(1, app_lib_dir)
def handler(event, context):
desc = None
fn, modulePath, desc = imp.find_module('index')
mod = imp.load_module('index', fn, modulePath, desc)
request_handler = getattr(mod, 'handler')
return request_handler(event, context)
index.py 代码如下:
# -*- coding:utf-8 -*-
import cv2
import oss2
import tensorflow as tf
def handler(event, context):
filename="pic/e2.jpg"
image = cv2.imread(filename, 1)
x = tf.Variable(image, name='x')
model = tf.initialize_all_variables()
with tf.Session() as session:
x = tf.transpose(x, perm=[1, 0, 2])
session.run(model)
result = session.run(x)
cv2.imwrite("/tmp/pic.jpg", result)
cv2.waitKey (0)
auth = oss2.Auth(<'Your access_key_id'>, <'Your access_key_secret'>)
bucket = oss2.Bucket(auth, <'Your endpoint'>, <'Your bucket'>)
bucket.put_object_from_file('picture', '/tmp/pic.jpg')
return 'success'
pic/e2.jpg 如下:
部署
你可以通过 SDK、API、fun、控制台 等多种方式进行部署,这里直接通过控制台上传代码包。
- 首先选择一个 region 并创建一个函数。
- 在新建函数的代码配置中选择文件夹上传,点击你的项目目录全部上传即可。
- 在环境配置中配置初始化函数,将函数入口和 initializer 入口分别设置为
loader.handler
和loader.initializer
,并配置合理的处理函数超时时间和初始化超时时间,配置结束后点击下一步即可。
测试
功能测试
- 在函数计算控制台执行函数,发现当引入 initializer 功能后,首次执行时间为 3205ms:
- 为验证函数是否执行成功,我们到 OSS 控制台查看指定 bucket 下面是否存在名为 picture 的 object,并验证是否旋转成功,图片如下,由此可见函数执行成功。
性能测试
为验证 initializer 函数不仅可以解决上传代码包大小受限的问题,且可以规避加载代码包的冷启动时间,下面将对函数进行改造,关闭初始化功能。
- 在函数概览页面中点击修改函数,并关闭
开启初始化功能
按钮:
- 将 initializer 函数中的逻辑放到到处理函数中,并删除原有初始化函数,点击执行查看运行结果:
- 函数产生日志如下:
从上图中可以发现,在未引入 initializer 函数之前,首次函数函数执行时间约为 14159ms,从 OSS 加载过大的代码包会占用大量的冷启动时间,并且影响函数的性能。
总结
从测试的结果可以发现,initializer 接口的引入解决了文章开篇提到的问题。即解决函数计算对上传代码包的限制问题,又规避了加载代码包的冷启动时间,极大的提升函数性能。
最后欢迎大家通过扫码加入我们用户群中,使用过程中有问题或者有其他问题可以在群里提出来。函数计算官网客户群(11721331)。