
最近发现一个 ETCD Client 端的实现问题——ETCD 所在机器宕机或者断网的情况下,ETCD Client 无法快速重连到可用的 etcd 节点,导致 client 端不可用(该问题的描述后续发表文章介绍)。后来找到一个比较简单的优化方式,即临时新创建一个新的 ETCD 的 Client 来重试操作,可以立即操作成功。但是每次遇到断网错误或者断网时间比较长,那么这段时间内所有的请求都要重新创建一个新的 ETCD Client 来重试吗?频繁创建 ETCD Client 对系统有什么影响?此外,还联想到在使用 ETCD 初期的时候,请教过一个专家同学,关于 ETCD Client 的使用上,全局使用一个 ETCD Client,还是在需要使用的模块内部使用独立的 Client,这两种方式哪个更为合理? 今天,就简单的为自己解答一下这几个问题哈。本文主要是做一些简单的调研和基础知识的分析哈,引出 ETCD Client 的生命周期管理比较合理的方式。 普及知识 先来普及一些基本的概念,便于我们更好的研究和分析哈。ETCD_API=3,即 v3 Client。 gRPC 相关的概念 etcd clientv3 端是基于 gPRC 实现的。所以,这里先简单的描述一下 gRPC 的相关的基本内容哈。 首先,计算机网络的 7 层协议: 物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,大家肯定都非常熟悉了。从协议上来说: TCP 是传输层协议,主要解决数据如何在网络中传输,它解决了第四层传输层所指定的功能。 HTTP 是应用层协议,主要解决如何包装数据,是建立在 TCP 协议之上的应用协议。因为 TCP 协议对上层应用的不友好,所以面向应用层的开发产生了 HTTP 协议。 RPC 是远程过程调用,它是一种设计、实现框架,通信协议只是其中一部分,所以他和 HTTP 并不是对立的,也没有包含关系,本质上是提供了一种轻量无感知的跨进程通信的方式,通信协议可以使用 HTTP,也可以使用其他协议。关于为何有 HTTP 协议,为何还要在系统之后通信上使用 RPC 调用的原因,相信网上有很多论述,这里就不详细描述了哈。gRPC 是谷歌开源的一个 RPC 框架,面向移动和 HTTP2 设计的。和很多 RPC 系统一样,服务端负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用 gPRC 支持的不同语言实现。HTTP2 相对于 HTTP1.x 具有很多新特性,比如多路复用,即多个 request 共用一个 TCP 连接,其他特性这里不详细叙述了。 TCP 短连接使用的问题 TCP 连接是网络编程中最基础的概念,这里就不详细介绍 TCP 连接过程了。短连接最大的问题在占用大量的系统资源,例如,socket,而导致这个问题的原因其实很简单:tcp 连接的使用,都需要经过相同的流程: 连接建立 -> 数据传输 -> 连接关闭。 对于系统请求负载较高的情况下,系统出现的最多和最直观的错误应该就是 "too many time wait"。这里简单说一下 socket 句柄被耗尽的原因,主要因为 TIME_WAIT 这种状态的 TCP 连接的存在。 由于 socket 是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的,如下图所示: 主动关闭连接的一方(成为主动方),调用 close,然后发送 FIN 包给被动方,表明自己已经准备关闭连接; 被动方收到 FIN 包后,回复 ACK ,然后进入到 CLOSE_WAIT ; 主动方等待对方关闭,则进入 FIN_WAIT_2 状态;此时,主动方等待被动方的调用 close() 操作; 被动方在完成所有数据发送后,调用close()操作;此时,被动方发送 FIN 包给主动方,等待对方的ACK,被动方进入 LAST_ACK 状态; 主动方收到 FIN 包,协议层回复 ACK ;此时,主动方进入 TIME_WAIT 状态;而被动方,进入 CLOSED 状态 等待 2MSL 时间,主动方结束 TIME_WAIT ,进入 CLOSED 状态 通过上面的一次 socket 关闭操作,可以得出以下几点: 主动方最终会进入 TIME_WAIT 状态; 被动方,有一个中间状态,即 CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用 close 操作后才主动关闭这条连接; TIME_WAIT 会默认等待 2MSL 时间后,才最终进入 CLOSED 状态; 在一个连接没有进入 CLOSED 状态之前,这个连接是不能被重用的! 所以,由上面的原理可以看出,TCP 连接的频繁创建和关闭,会导致系统处于 TIME_WAIT 或者 CLOSE_WAIT 状态的 TCP 连接变多,占用系统资源,影响正常的功能。 那么,下面我们看看,gRPC 的 Client 如果不合理的使用,会造成什么样的问题呢? gRPC Client 生命周期控制问题 写个简单的 ETCD Client V3 的小程序,来看看频繁的创建和关闭 ETCD Client 会有什么样的影响,程序代码如下: // golang func TestNewETCDClient() { for { etcdClient, err := clientv3.New(clientv3.Config{ Endpoints: []string{"10.0.0.2:2379"}, DialTimeout: 3 * time.Second, }) if err != nil { logger.Errorf("new client failed due to %v", err) return } etcdClient.Close() } } 然后,我们用如下命令看看系统有什么变化,如下所示,不到一分钟时间 TIME_WAIT 暴涨到了 16325 多个。 netstat -n| awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 结论和建议 前面原理上已经解释过, ETCD Client v3 基于 gRPC 实现,而 gRPC 采用的 HTTP2 协议,在传输层的协议依然是 tcp。如果对 gRPC 的 Client 的生命周期设置的非常短,那么相当于对这个 TCP 连接资源转化成了短连接,没有发挥其核心功能。 所以,对于 ETCD Client 的使用,应该充分利用其多路复用的原则,全局定义一个 Client 变量,生命同期等同于进程,以降低对 TCP 资源的管理成本。 参考文章 你所不知道的 TIME_WAIT 和 CLODE_WAIT
默认情况下创建的 ECS 实例只有一个40G 的高效云盘系统盘,通过任何形式(控制台、ECS SDK 等) 方式创建的实例,如果需要使用数据盘,必须先进行额外的格式化数据盘工作。 如果需要批量创建大量的有特定格式化需求数据盘的 ECS 实例,那么单独为每一台实例格式化数据盘肯定是一件浪费运维资源的工作。所以,这里介绍一种比较简单的 ECS 实例数据盘自动挂载和格式化的方式,也许有更好的方法,请各位大神拍砖。 背景 阿里云 ECS 实例对数据盘的使用,由于涉及到用户自己的业务逻辑,对盘的数目、分区方式、文件系统要求等都不尽相同,所以 ECS 服务提供了自主的方式,将数据盘的格式化工作全部交托给用户自行处理,这样就导致了用户需要进行一步的运维工作,才能对数据盘进行正常的使用。如果用户需要创建批量的实例,那么对这些实例分别进行数据盘的初始化操作必然会成为运维中的一项额外的工作。 以 Linux 系统为例,主要的步骤如下: 如果是新购买的数据盘,那么需要先挂载数据盘。如果是和实例一同购买的数据盘,那么可以直接进行数据盘的格式化即可。 我们知道,ECS 提供了自定义镜像的功能,用户可以从现有的实例构建镜像,此后就可以使用该镜像作为新的实例创建的模板,"克隆"业务所需的实例了。那么数据盘的格式化信息是否也能被"录制"到镜像中呢?当然可以。 如何制作数据盘格式化了的镜像 这里以 Linux 系统(实例基础镜像为 linux_17_01_64_20G_cloudinit_20171222.vhd),挂载 SSD 云盘,创建一个单分区,并挂载文件系统为例。使用 ECS 的 SDK 进行相关的操作。 首先,通过 CreateInstance 创建一个 ECS 实例,注意,创建参数中 DataDisk 参数的内容: type DataDiskType struct { Size int Category DiskCategory //Enum cloud, ephemeral, ephemeral_ssd SnapshotId string DiskName string Description string Device string DeleteWithInstance bool } Size: 数据盘大小,单位 GB Category: 数据盘的类型,如 ephemeral_ssd, cloud_efficiency 和 cloud_ssd 等,分别对应本地 SSD , 高效云盘,高效 SSD 等。 Device: 数据盘挂载点的设备名称,如果不填,默认由系统分配,I/O 优化实例的数据盘设备名从 /dev/vdb 递增排列,包括 /dev/vdb− /dev/vdz。如果数据盘设备名为 dev/xvd( 是 a−z 的任意一个字母),表示您使用的是非 I/O 优化实例。 在创建时,DataDisk 参数中只有一块 DataDiskType 类型,我们选择使用默认的 Device 。此时创建出来的实例,带有一块数据盘,但是未格式化。如果登录机器,通过 fdisk -l 可以看到对应的数据盘的信息以及挂载点。但是文件系统中是没有任何该数据盘的信息的。 接下来就是自定义机器的业务逻辑,比如,需要如何配置系统、安装什么软件等等,可以通过 SSH Client 相关工具对机器进行自动化操作。比如数据盘格式化的操作,可以生成这样的 shell 文件,通过 SSH Client 拷贝到实例机器上,然后执行就可以了。 set -e setup_disk() { tee ./command <<-EOF n p 1 w p EOF fdisk -u /dev/vdb < ./command mkfs.ext4 /dev/vdb1 echo /dev/vdb1 /your_dir ext4 defaults 0 0 >> /etc/fstab mkdir -p /your_dir mount /dev/vdb1 /your_dir } setup_disk 执行完成之后,你可以看到系统信息已经发生变化: 当然,为了验证磁盘格式化信息正确持久化,你可以对这个实例重启,然后再进行相关的 check,check 逻辑可以简单如下: set -e df -h|grep '/your_dir'|grep '/dev/vdb1' fdisk -l|tail -1|grep '/dev/vdb1' 最后,通过 CreateImage 构建镜像即可。 如何通过镜像复制实例 通过上面的步骤,你应该已经制作了一个带有数据盘格式化信息的镜像了,那么这时你需要通过这个镜像来创建你需要的实例。创建实例的过程和一开始通过 CreateInstance 相同,但是有一些坑需要注意: CreateInstance 的参数中 DataDisk 必须填写,并且 DataDisk 中的一块数据盘的 Device 信息必须和镜像中的数据盘设备名完全相同。如果不填或者填错,那么系统会当做一块新的数据盘进行处理,此时创建出来的实例会有两块数据盘。分别挂载 /dev/xvdb 和 /dev/xvdc。而 /dev/xvdb 是镜像自带的已经格式化的数据盘,/dev/xvdc 才是你在这次 CreateInstance 指定的数据盘,而且只挂载没有格式化化。下图展示了我的一次失败的操作结果: 镜像中仅仅保存了数据盘格式化的信息,但是并未保存磁盘类型信息,比如在创建进行的时候使用的实例挂载的数据盘是 cloud_ssd,但是如果不指定 DataDisk 中数据盘的 Category ,默认使用的是高效云盘。因为数据盘类型属于业务属性,镜像本身并没有这个属性。 Device 的值并不是你在机器上通过 fdisk -l 看到的,可以需要通过 DescribeImages 查看。我们通过 fdisk -l 可以看到上面的数据盘都是在 /dev/vdb 这个设备名上,但是 DescribeImages 的返回结果却是: 这个值和虚拟化的类型相关,所以,请以 DescribeImages 的结果为准。 总结 一个简单的数据盘格式化模板构建方式,小总结一下~~谢谢拍砖。
最近在调查配置问题过程中,发现一个比较低级的错误,应该是 python 使用习惯的问题,和大家分享一下,比较有意思。 问题介绍 在系统的配置脚本中有一句这样的逻辑: # etcd_nodes like this: 'http://172.16.1.1:2380' etcd_nodes.append(etcd_node_host.rstrip(':2380').lstrip('http://')) 我们预期的结果当然是获取到 '172.16.1.1' 这个 IP 地址。但是在实际使用的时候,遇到了这样的问题: 分析一下原因 我们先来看下 python 内置方法 strip 这类方法的定义了实现,官方定义: def rstrip(self, chars=None): # real signature unknown; restored from __doc__ """ S.rstrip([chars]) -> string or unicode Return a copy of the string S with trailing whitespace removed. If chars is given and not None, remove characters in chars instead. If chars is unicode, S will be converted to unicode before stripping """ return "" 参数中的 chars 是字符的list,不是字符串匹配,这个字符的作用怎么理解呢?举个例子, >>> a='test it 888888888' >>> print a.rstrip('8') test it >>> a='test it8 888888888' >>> print a.rstrip('8') test it8 简单的说,就是匹配到最后一个不是给定 chars 的字符就停止。那么如果传入的参数是一个字符串呢: >>> a='test it887878888888' >>> print a.rstrip('78') test it 所以,这样的匹配其实就是匹配到最后一个不是 chars 这个数组中任意的一个字符为止。 这样,就能解释为什么前面的 rstrip(':2380') 对 'http://172.1.1.2:2380' 的结果是 'http://172.1.1.' 的原因了。 结论 对于像 strip, rstrip, lstrip 这样最基本、简单的功能,还是需要了解清楚其使用方法,不能因为一次试用成功就以为满足功能需求了。这样的误用,如果上线,危害还是比较大的。
背景 最近在处理线上工单的时候,遇到一个用户使用 nodejs runtime 时因为函数计算运行环境的 gcc 版本过低导致无法运行的问题,觉得非常有意思,所以深入的帮用户寻找了解决方案。觉得这个场景应该具有一定的通用性,所以在这篇文章里面重点的介绍一下如何使用函数计算的周边工具 fun 解决因为 runtime 中系统版本导致的各种兼容性问题。 场景介绍 用户问题 简要描述一下用户当时遇到的问题: 用户使用函数计算的 nodejs8 runtime,在本地自己的开发环境使用 npm install couchbase 安装了 couchbase 这个第三方库。couchbase 封装了 C 库,依赖系统底层动态链接库 libstdc++.so.6。因为用户自己的开发环境的操作系统内核比较新,所以本地安装、编译和调试都比较顺利。所以,最后按照函数计算的打包方式成功创建了 Function,但是执行 InvokeFunction 时,遇到了这样的错误: "errorMessage": "/usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by /code/node_modules/couchbase/build/Release/couchbase_impl.node)", "errorType": "Error", "stackTrace": [ "Error: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by /code/node_modules/couchbase/build/Release/couchbase_impl.node)", ... 错误发生的原因如堆栈描述,即没有 CXXABI_1.3.9 这个版本,可以看到函数计算 nodejs 环境中的支持情况: root@1fe79eb58dbd:/code# strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 |grep CXXABI_ CXXABI_1.3 CXXABI_1.3.1 CXXABI_1.3.2 CXXABI_1.3.3 CXXABI_1.3.4 CXXABI_1.3.5 CXXABI_1.3.6 CXXABI_1.3.7 CXXABI_1.3.8 CXXABI_TM_1 升级底层系统版本的代价比较大,需要长时间的稳定性、兼容性测试和观察,所以,为了支持这类使用场景,我们希望能够有比较简单的方式绕行。 场景复现和问题解决 从前面的分析已经可以看出来,是因为 libstdc++.so.6 这个动态链接库的版本过低导致 couchbase 无法正常运行。所以,如果不升级 gcc 的情况下,需要提高 libstdc++.so.6 的版本支持,在没有其他版本依赖的前提下,快速替换掉 libstdc++.so.6 是最好的办法。直接替换掉 /usr/lib/x86_64-linux-gnu/ 下面的动态链接库可能会造成对系统底层基本库运行的影响,而且用户函数逻辑不能越权操作系统底层文件。所以,我们可以将更高版本的动态链接库下载到指定目录,然后改变动态链接库路径LD_LIBRARY_PATH,使用户自定义的路径优先级大于系统路径即可。 fun install 功能能够将第三方库安装到本地的模拟函数计算的运行"沙箱"中,而且自动设置 LD_LIBRARY_PATH 路径,调用 fun local invoke 就能够在本地真实模拟函数计算在线运行情况。执行 fun deploy 命令,能够自动打包第三方库以及解决 LD_LIBRARY_PATH 等环境变量设置问题,自动将本地的代码项目整个都部署到函数计算线上环境。 下面我们就详细介绍一下利用 fun 工具替换低版本 libstdc++.so.6 的步骤。 代码项目搭建 前提:先按照 fun 的安装步骤安装 fun工具,并进行 fun config 配置。 在本地很快搭建了一个项目目录: - test_code/ - index.js - template.yml 其中 index.js 和 template.yml 的 内容分别为 # index.js const couchbase = require('couchbase').Mock; module.exports.handler = function(event, context, callback) { var cluster = new couchbase.Cluster(); var bucket = cluster.openBucket(); bucket.upsert('testdoc', {name:'Frank'}, function(err, result) { if (err) throw err; bucket.get('testdoc', function(err, result) { if (err) throw err; console.log(result.value); // {name: Frank} }); }); callback(null, { hello: 'world' }) } # template.yml ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: fc: # service name Type: 'Aliyun::Serverless::Service' Properties: Description: 'fc test' helloworld: # function name Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: nodejs8 CodeUri: './' Timeout: 60 为了能够在本地模拟函数计算的真实环境进行依赖包安装和调试,这里生成一个 fun.yml 文件用于 fun install 安装使用,内容如下: runtime: nodejs8 tasks: - shell: |- if [ ! -f /code/.fun/root/usr/lib/x86_64-linux-gnu/libstdc++.so.6 ]; then mkdir -p /code/.fun/tmp/archives/ curl http://mirrors.ustc.edu.cn/debian/pool/main/g/gcc-6/libstdc++6_6.3.0-18+deb9u1_amd64.deb -o /code/.fun/tmp/archives/libstdc++6_6.3.0-18+deb9u1_amd64.deb bash -c 'for f in $(ls /code/.fun/tmp/archives/*.deb); do dpkg -x $f /code/.fun/root; done;' rm -rf /code/.fun/tmp/archives fi - name: install couchbase shell: npm install couchbase fun.yml中参数说明: 前面的分析已经了解到函数计算 nodejs8 runtime 的 libstdc++.so.6 的版本偏低,所以,我们找到一个更新的版本来支持,见新版本的 libstdc++.so.6 的 CXXABI_ 参数: $strings .fun/root/usr/lib/x86_64-linux-gnu/libstdc++.so.6|grep CXXABI_ CXXABI_1.3 CXXABI_1.3.1 CXXABI_1.3.2 CXXABI_1.3.3 CXXABI_1.3.4 CXXABI_1.3.5 CXXABI_1.3.6 CXXABI_1.3.7 CXXABI_1.3.8 CXXABI_1.3.9 CXXABI_1.3.10 CXXABI_TM_1 CXXABI_FLOAT128 上面的 shell 命令的功能是将 libstdc++ 的相关 lib 下载到目录 /code/.fun/root 下面。 执行 fun install 命令 安装各种第三方依赖,显示如下: 本地执行情况 执行 fun local invoke helloworld,可以看到执行成功的效果: $fun local invoke helloworld begin pullling image aliyunfc/runtime-nodejs8:1.4.0............................................................... pull image finished pull image finished FC Invoke Start RequestId: 78e20963-b314-4d69-843a-35a3f465796c load code for handler:index.handler FC Invoke End RequestId: 78e20963-b314-4d69-843a-35a3f465796c {"hello":"world"}2019-02-19T08:16:45.073Z 78e20963-b314-4d69-843a-35a3f465796c [verbose] { name: 'Frank' } 在本地,我们可以看到,生成了一个 .fun/ 文件目录,动态链接库文件的位置如下: [~/work/test_code/.fun/root/usr/lib/x86_64-linux-gnu] $ls libstdc++.so.6 libstdc++.so.6.0.22 [~/work/test_code/.fun/root/usr/lib/x86_64-linux-gnu] $pwd ~/work/test_code/.fun/root/usr/lib/x86_64-linux-gnu [~/work/test_code/.fun/root/usr/lib/x86_64-linux-gnu] $ll libstdc++.so.6 lrwxrwxrwx@ 1 tingbao staff 19 2 15 2018 libstdc++.so.6 -> libstdc++.so.6.0.22 发布上线 使用 fun deploy 发布上线,然后到控制台执行一下线上实际的运行效果: 总结 fun install 功能能够将代码和依赖文件分离开,独立安装系统依赖文件,而且 fun local 和 fun deply 都能够自动帮你设置第三方库的依赖引用路径,让您无需关心环境变量问题。 本文的解法只是提供了一个对于系统版本偏低无法满足用户一些高级库使用需求时的简单绕行方案,仅供参考,对于一些复杂的环境依赖问题,可能还需要具体情况具体分析。 更多参考: 函数计算 nodejs runtime [fun local](https://yq.aliyun.com/articles/672656?spm=a2c4e.11155435.0.0.61a7499dXnVk4I) fun install
继前一篇《函数计算性能福利篇——系统冷启动优化》,我们再来看看近期函数计算推出的 Initializer 功能之后,带来的一波高能性能优化成果。 背景 函数计算是一个事件驱动的全托管 serverless 计算服务,用户可以将业务实现成符合函数计算编程模型的函数,交付给平台快速实现弹性高可用的云原生应用。 用户函数调用链路包括以下几个阶段: 系统为函数分配计算资源; 下载代码; 启动容器并加载函数代码; 用户函数内部进行初始化逻辑; 函数处理请求并将结果返回。 其中前三步是系统层面的冷启动开销,通过对调度以及各个环节的优化,函数计算能做到负载快速增长时稳定的延时,细节详见 函数计算系统冷启动优化。第4步是函数内部初始化逻辑,属于应用业务层面的冷启动开销,例如深度学习场景下加载规格较大的模型、数据库场景下连接池构建、函数依赖库加载等等。为了减小应用层冷启动对延时的影响,函数计算推出了 initializer 接口,便于用户抽离业务初始化逻辑。这样用户就能将自身业务的初始化逻辑和请求处理逻辑分离,分别是实现在 initializer 接口和 handler 接口中,使得系统能识别用户函数的初始化逻辑,从而在调度上做相应的优化。 Initializer 功能简介 引入 initializer 接口的价值主要体现在如下几个方面: 分离初始化逻辑和请求处理逻辑,程序逻辑更清晰,让用户更易写出结构良好,性能更优的代码; 用户函数代码更新时,系统能够保证用户函数的平滑升级,规避应用层初始化冷启动带来的性能损耗。新的函数实例启动后能够自动执行用户的初始化逻辑,在初始化完成后再处理请求; 在应用负载上升,需要增加更多函数实例时,系统能够识别函数应用层初始化的开销,更精准的计算资源伸缩的时机和所需的资源量,让请求延时更加平稳; 即使在用户有持续的请求且不更新函数的情况下,FC系统仍然有可能将已有容器回收或更新,这时没有平台方(FC)的冷启动,但是会有业务方冷启动,Initializer可以最大限度减少这种情况; 具体的 Initializer 功能设计和使用指南,请参考官方 Initiliazer 介绍 。 初始化场景性能对比 上一节已经简单了概括了 Initializer 的功能,这里,我们具体展示一下初始化场景下 Initializer 带来的巨大的性能提升效应。 函数实现 初始化应用场景,如果不使用 initializer,那么函数的主要实现方式应该是 Global variable 方式,下面提供两种实现方式的 demo ,仅供参考,下面的性能测试也是对比这两种函数实现方式进行了。 使用 global variables 实现业务层初始化逻辑: # -*- coding: utf-8 -*- import time import json isInit = False def init_handler(): time.sleep(30) global isInit isInit = True def handler(event, context): evt = json.loads(event) funcSleepTime = evt['funcSleepTime'] if not isInit: init_handler() time.sleep(funcSleepTime) 使用 initializer 的编程模型实现业务层初始化逻辑: # -*- coding: utf-8 -*- import time import json def init_handler(context): time.sleep(30) def handler(event, context): evt = json.loads(event) funcSleepTime = evt['funcSleepTime'] time.sleep(funcSleepTime) 两个 function 的逻辑相同: 函数实例运行时,先执行 init_handler 逻辑,执行时间 30s,进行业务层初始化; 如果已经初始化,那么就执行 handler 逻辑,执行时间 0.1s,进行请求处理;如果没有初始化,那么先进行初始化逻辑,再执行 handler 逻辑。 场景对比 这里根据生产用户请求场景,我们选择如下三种测试 case 来对比两种初始化函数实现的性能。 负载持续增加模式 波峰 burst 模式 业务逻辑升级模式 测试函数的特性如下: 函数 handler 逻辑运行时间为 100ms; 函数 初始化 逻辑运行时间为 30s; 函数代码包大小为 50MB; runtime 为 python2.7; Memory 为 3GB 。 这样的函数,系统层冷启动时间大约在 1s 左右,业务层冷启动在 30s,而函数自身请求执行时间为100-130ms。 负载持续增加模式 该模式下,用户的请求在一段时间内会持续增长。设计请求行为如下: 每波请求并发数翻倍递增: 1, 2, 4, 8, 16, 32; 每波请求的时间间隔为 35s。 TPS情况如下,增长率为100%: 注意:忽略第一批请求的完全冷启动的延时影响。 不使用 initializer 实现的运行结果: 从每波请求的请求延时可以看出,虽然系统层的调度能够为后来的骤增的请求分配更多的函数实例,但是因为函数实例都没有执行过业务层的初始化逻辑,所以新的函数实例花费了大量的执行时间在初始化逻辑的执行上,所以看到 99th latency 都大于 30s 。实际上,系统层的调度优化在这样长时间的初始化场景中并起不了作用。 使用 initializer 实现的运行结果,可以看到使用 initializer 功能之后,请求增长率在 100% 的情况下不会再有函数实例执行初始化逻辑,相对于优化前,99th latency 下降了 30 倍以上。 波峰 burst 模式 波峰burst模式是指用户请求比较平稳,但是会有突然的波峰流量场景。设计请求行为如下: 每波请求时间间隔 35s; 每波平稳请求数 2; burst 请求数 18; TPS请求如下,burst 流量猛增 9 倍: 注意:忽略第一批请求的完全冷启动的延时影响。 不使用 initializer 实现的运行结果:  使用 initializer 实现的运行结果,对于 burst 的流量,基本能够将 latency 的增长控制在 函数处理逻辑 6 倍以内,99th 的 latency 被优化到原来的 2.9% 。  业务逻辑升级模式 业务逻辑升级模式是指用户请求比较平稳,但是用户函数会持续 UpdateFunction,变更业务逻辑,进行用户业务升级。设计请求行为如下: 每波请求时间间隔 35s; 每波平稳请求数 2; 每 6 波请求进行一次 UpdateFunction 操作; TPS 如下:  注意:忽略第一批请求的完全冷启动的延时影响。 不使用 initializer 实现的运行结果,这个时候请求又会重新执行一次初始化逻辑,导致毛刺出现。 使用 initializer 实现的运行结果,基本看出,UpdateFunction 操作对请求已经没有影响,业务层无感知。 总结 综上数据分析,函数计算的 Initializer 功能极大的优化了业务层冷启动的毛刺影响: 在用户请求存在明显 burst 或者在以一定速率增长的情况下,能够极大的缓解性能影响,如上,在负载持续增加模式和波峰模式场景下,请求平均 latency 仅仅增加 3 倍,99th latency 只增加了 5 倍,99th latency 仅为优化前的 2.9% ,整整下降了 33 倍之多。 在用户有持续的请求且不更新函数的情况下,优化之后更新函数,业务层能够做到无感知,平滑热升级。 Initializer 功能对业务层冷启动的优化,又一次大大改善了函数计算在延时敏感场景下的表现!