2020年函数计算的冷启动怎么样了

anycodes刚刚日常分享293141

前言

自从Serverless架构被提出,函数计算这个名词变得越发的火热,甚至在很多时候有人会认为Serverless就是函数计算。

作为Serverless架构中的一个重要组成部分,云函数确实值得,也应该备受关注,无论是吐槽他的调试能力,还是抱怨他的冷启动,亦或者对他的弹性伸缩表示怀疑,但是我们不得不承认,更多人正在越来越关注Serverless,也越来越关注Serverless中的FaaS部分。

经常看到有人在吐槽函数计算的冷启动问题,时至今日,不知道各平台的冷启动是什么样子的。本文将会通过相对客观的数据来进行基本的验证。

冷启动验证

首先说到冷启动,就要先说明什么是冷启动,开发者提交代码之后你不知道他调不调用,函数第一次调用会有一个函数冷启动,把网络的环境全部打通,这个函数才能提供服务。如果没有优化好冷启动优化这部分,可能对于一些比较关键的产品首次启动会产生超时,体验非常不好,以前开发者本地运行函数的时候,并不会关注本地函数执行多少毫秒和微妙,但是在云函数场景下就不一样了,云函数有一个部署的过程;无论是公有云的平台上还是开源方案上,冷启动都是值得不断探讨话题和优化的方向。

在《Serverless: Cold Start War》这篇文章中,作者对AWS Lambda,Azure Function以及Google Cloud Function等三个工业级的Serverless架构产品的冷启动测试。作者将函数启动划分成四个部分:

然后作者通过对多种语言的“Hello World”与是否有依赖等进行搭配,进行测试,测试结果:

通过《Understanding AWS Lambda Performance—How Much Do Cold Starts Really Matter?》与《Serverless: Cold Start War》这两个文章的分析和结果,我们可以看到冷启动问题确实存在,而且不同厂商,不eight: inherit; margin: 0px; padding: 0px;">

图片功能部分除了用户侧可见的功能,还有定时任务,当用户上传图片之后,系统会在后台异步进行图像压缩以及图像的描述,关键词提取等。整体流程如图所示。

搜索功能:

搜索功能指的是通过关键词或者使用者的描述,可以获得到目标数据的过程,这一功能原型图如图所示。

这一部分的难点和重点在于通过用户的描述,搜索到目标数据的过程。这个过程的基本流程如图所示。

项目开发

初步了解Serverless Cli

Serverless架构可以说是目前非常火热的项目,其凭借着按量付费、低成本运维、高效率开发等众多优点于一身,帮助我们的项目快速开发,快速迭代。而Serverless Framework则是一个非常高效的工具,其兼容了AWS,Google Cloud以及腾讯云等多家厂商的Serverless架构,为开发者提供一个多云的开发者工具,目前以腾讯云为例,其拥有Plugin和Components两个部分。

这两个部分可以说是个有千秋,具体的大家可以官方说明,或者自己体验一下。我这里我只说几个我觉得很头疼的问题。

  • Plugin部署到线上的函数,会自动变更名字,例如我的函数是myFunction,我的服务和阶段是myService-Dev,那么函数部署到线上就是myService-Dev-myFunction,这样的函数名,很可能会让我的函数间调用等部分产生很多不可控因素。例如我现在的环境是Dev,我函数间调用就要写函数名是myService-Dev-myFunction,如果是我的环境是Test,此时就要写myService-Test-myFunction,我始终觉得,我更改环境应该只需要更改配置,而不是更深入的代码逻辑。所以我对Plugin的这个换名字问题很烦躁;

  • Plugin也是有优势的,例如他有Invoke、Remove以及部署单个函数的功能,同时Plugin也有全局变量,我觉得这个更像一个开发者工具,我可以开发、部署、调用、查看一些信息、指标以及删除回滚等操作,都可以通过Plugin完成,这点很给力,我喜欢;

  • Components可以看作是一个组件集,这里面包括了很多的Components,可以有基础的Components,例如cos、scf、apigateway等,也有一些拓展的Components,例如在cos上拓展出来的website,可以直接部署静态网站等,还有一些框架级的,例如Koa,Express,这些Components说实话,真的蛮方便的,腾讯官方也是有他们的最佳实践;

  • Components除了刚才所说的支持的产品多,可以部署框架之外,对我来说,最大吸引力在于这个东西,部署到线上的函数名字就是我指定的名字,不会出现额外的东西,这个我非常看重;

  • Components相对Plugin在功能上略显单薄,除了部署和删除,再没有其他,例如Plugin的Invoke,Rollback等等一切都没有,同时,我们如果有多个东西要部署,写到了一个Components的yaml上,那么我们每次部署都要部署所有的,如果我们认为,我们只修改了一个函数,并且不想重新部署其他函数从而注释掉其他函数,那么很抱歉告诉你,不行!他会看到你只有一个函数,并且帮你把你注释掉的函数在线上删除;

  • Components更多的定义是组件,所以每个组件就是一个东西,所以在Components上面,是没有全局变量这一说法,这点我觉得很坑。

综上所述的几点,就是在除了官方文档的描述之外,我对Plugin和Components的对比,感情真的可谓是错综复杂,也很期待产品策略可以将二者合并,或者功能对齐,否则单用Plugin,功能上是很全面了,但是产品支持不全面,名字变化我真的不能忍(可能很多人都不能忍),单用Components,没有全局变量,没有更多功能,可谓是产品广度变了,便利增加了,但是功能太淡薄了,我对二者的感情,又恨又爱。

经过了长久的思考,我觉得Plugin部署到线上会导致函数名字变化这个问题,我真的不能忍(或许我就是巨蟹座的强迫症吧,哈哈哈),而且,我个人认为,我未必就能需要到更多的功能,例如invoke,例如metrics等。所以我选择了Components来做这个项目。

造轮子:全局变量组件

说到Components做这项目,我就遇到了第一个难题,我的配置文件怎么办?我有很多的配置,我难道要在每个函数中写一遍?

于是,我做了一个新的:serverless-global,是的,这个Components的功能,或者价值就是可以满足我全局变量的需求,例如这样写我的全局变量:

Conf:
  component: "serverless-global"
  inputs:
    mysql_host: gz-cdb-mytest.sql.tencentcdb.com
    mysql_user: mytest
    mysql_password: mytest
    mysql_port: 62580
    mysql_db: mytest
    mini_program_app_id: mytest
    mini_program_app_secret: mytest

在使用的时候,只需要使用${}就可以引用,例如:

Album_Login:
  component: "@serverless/tencent-scf"
  inputs:
    name: Album_Login
    codeUri: ./album/login
    handler: index.main_handler
    runtime: Python3.6
    region: ap-shanghai
    environment:
      variables:
        mysql_host: ${Conf.mysql_host}
        mysql_port: ${Conf.mysql_port}
        mysql_user: ${Conf.mysql_user}
        mysql_password: ${Conf.mysql_password}
        mysql_db: ${Conf.mysql_db}

这样,我就可以很简单轻松加愉快的,将我的配置信息统一提取到了一个配置的地方。另外这里说一下,我为啥要把一些配置信息放在环境变量,而不是统一放在一个配置文件中,因为环境变量在SCF中,会真的打到环境中,也就是说,你可以直接取到,我个人觉得比每次创建实例读取一次配置文件可能要性能好一些,可能只会好几毫秒,但是,我还是觉得这样做是比较优雅的。最主要的是,相比写到代码中和配置到单独的配置文件中,我这样做之后,我可以分享我的代码给别人,可以更好的保护的我的一些敏感信息。

数据库设计

数据库部分主要对相关的表和表之间的关系进行建立。
首先需要创建项目所必须的表:

CREATE DATABASE `album`;
CREATE TABLE `album`.`tags` ( `tid` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `remark` TEXT NULL , PRIMARY KEY (`tid`)) ENGINE = InnoDB;
CREATE TABLE `album`.`category` ( `cid` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `sorted` INT NOT NULL DEFAULT '1' , `user` INT NOT NULL , `remark` TEXT NULL , `publish` DATE NOT NULL , `area` VARCHAR(255) NULL , PRIMARY KEY (`cid`)) ENGINE = InnoDB;
CREATE TABLE `album`.`users` ( `uid` INT NOT NULL AUTO_INCREMENT , `nickname` TEXT NOT NULL , `wechat` VARCHAR(255) NOT NULL , `remark` TEXT NULL , PRIMARY KEY (`uid`)) ENGINE = InnoDB;
CREATE TABLE `album`.`photo` ( `pid` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `small` VARCHAR(255) NOT NULL , `large` VARCHAR(255) NOT NULL , `category` INT NOT NULL , `tags` VARCHAR(255) NULL , `remark` TEXT NULL , `creattime` DATE NOT NULL , `creatarea` VARCHAR(255) NOT NULL , `user` INT NOT NULL ,  PRIMARY KEY (`pid`)) ENGINE = InnoDB;
CREATE TABLE `album`.`photo_tags` ( `ptid` INT NOT NULL AUTO_INCREMENT , `tag` INT NOT NULL , `photo` INT NOT NULL , `remark` INT NULL , PRIMARY KEY (`ptid`)) ENGINE = InnoDB;

创建之后,逐步添加表之间的关系以及部分限制条件:

ALTER TABLE `photo_tags` ADD CONSTRAINT `photo_tags_tags_alter` FOREIGN KEY (`tag`) REFERENCES `tags`(`tid`) ON DELETE CASCADE ON UPDATE RESTRICT; 
ALTER TABLE `photo_tags` ADD CONSTRAINT `photo_tags_photo_alter` FOREIGN KEY (`photo`) REFERENCES `photo`(`pid`) ON DELETE CASCADE ON UPDATE RESTRICT;
ALTER TABLE `photo` ADD CONSTRAINT `photo_category_alter` FOREIGN KEY (`category`) REFERENCES `category`(`cid`) ON DELETE CASCADE ON UPDATE RESTRICT;
ALTER TABLE `photo` ADD CONSTRAINT `photo_user_alter` FOREIGN KEY (`user`) REFERENCES `users`(`uid`) ON DELETE CASCADE ON UPDATE RESTRICT;
ALTER TABLE `category` ADD CONSTRAINT `category_user_alter` FOREIGN KEY (`user`) REFERENCES `users`(`uid`) ON DELETE CASCADE ON UPDATE RESTRICT;
ALTER TABLE `tags` ADD unique(`name`);

函数功能开发

写完了这个部分部分,我开始着手写我的第一个函数,注册登录函数。因为这是一个小程序,所以可以认为,注册登录实际上就是拿着用户的openId去数据库查查有没有信息,有信息的话,就执行登录,没有信息的话就insert一下。那么问题来了,我这里要怎么连接我的数据库?之所以有这样的问题,是源自两个因素:

  • 我们平时做项目更多时候都不是每次连接一次数据库,很多时候,数据库的连接是可以保持下来的,但是Serverless架构下可以么?或者我们需要去哪里连接数据库呢?

  • 传统项目,我们做数据库连接等,是只有一个方法就可以搞定,但是函数中,每个函数都是单独存在的,我们每个函数都要连接一下数据库?

初始化资源探索

针对问题1,我们来做一个实验,我去腾讯云云函数创建一个test:

创建之后,我们疯狂点击测试按钮,多次记录运行日志:

第一次

START RequestId: 4facbf59-3787-11ea-8026-52540029942f

Event RequestId: 4facbf59-3787-11ea-8026-52540029942f

11111111

222222222


END RequestId: 4facbf59-3787-11ea-8026-52540029942f

Report RequestId: 4facbf59-3787-11ea-8026-52540029942f Duration:1ms Memory:128MB MaxMemoryUsed:27.3164MB

第二次

START RequestId: 7aaf7921-3787-11ea-aba7-525400e4521d

Event RequestId: 7aaf7921-3787-11ea-aba7-525400e4521d

222222222


END RequestId: 7aaf7921-3787-11ea-aba7-525400e4521d

Report RequestId: 7aaf7921-3787-11ea-aba7-525400e4521d Duration:1ms Memory:128MB MaxMemoryUsed:27.1953MB

第三次

START RequestId: 742be57a-3787-11ea-b5c5-52540047de0f

Event RequestId: 742be57a-3787-11ea-b5c5-52540047de0f

222222222


END RequestId: 742be57a-3787-11ea-b5c5-52540047de0f

Report RequestId: 742be57a-3787-11ea-b5c5-52540047de0f Duration:1ms Memory:128MB MaxMemoryUsed:27.1953MB

第四次

START RequestId: 6faf934b-3787-11ea-8026-52540029942f

Event RequestId: 6faf934b-3787-11ea-8026-52540029942f

222222222


END RequestId: 6faf934b-3787-11ea-8026-52540029942f

Report RequestId: 6faf934b-3787-11ea-8026-52540029942f Duration:1ms Memory:128MB MaxMemoryUsed:27.1953MB

发现了什么?我在函数外侧写的print("11111111")实际上只出现了一次,也就是说他只运行了一次,而函数内的print("222222222")则是出现了多次,确切来说是每次都会出现,函数在创建的时候,会让我们写一个执行方法,例如index.main_handler,就是说默认的入口文件就是index.py下的main_handler方法。通过我们刚才的这个小实验,是不是可以认为,云函数实际上是随着机器或者容器启动同时启动了一个进程(这个时候会走一次外围的一些代码逻辑),然后当函数执行的时候,会走我们指定的方法,当函数执行完,这个容器并不会被马上销毁,而是进入销毁的倒计时,这个时候如果有请求来了,那么很可能复用这个容器,此时就没有容器启动的说法,会直接执行我们的方法。

按照这个逻辑,是不是我们的函数,如果要在我们的方法之外,初始化数据库,就可以保证尽可能少的数据库连接建立,而满足更多的请求呢?换句话说,是不是和容器复用类似,我们就可以复用数据库的连接了?

所以,我这里可以可以这样写我的整个代码(login为例)

# -*- coding: utf8 -*-

import os
import pymysql
import json

connection = pymysql.connect(host=os.environ.get('mysql_host'),
                             user="root",
                             password=os.environ.get('mysql_password'),
                             port=int(62580),
                             db="mini_album",
                             charset='utf8',
                             cursorclass=pymysql.cursors.DictCursor,
                             autocommit=1)

def getUserInfor(connection, wecaht):
    try:
        connection.ping(reconnect=True)
        cursor = connection.cursor()
        search_stmt = (
            "SELECT * FROM `users` WHERE `wechat`=%s"
        )
        data = (wecaht)
        cursor.execute(search_stmt, data)
        cursor.close()
        result = cursor.fetchall()
        return len(result)
    except Exception as e:
        print("getUserInfor", e)
        try:
            cursor.close()
        except:
            pass
        return False

def addUseerInfor(connection, wecaht, nickname, remark):
    try:
        connection.ping(reconnect=True)
        cursor = connection.cursor()
        insert_stmt = (
            "INSERT INTO users(wechat,nickname,remark) "
            "VALUES (%s,%s,%s)"
        )
        data = (wecaht, nickname, remark)
        cursor.execute(insert_stmt, data)
        cursor.close()
        connection.close()
        return True
    except Exception as e:
        print(e)
        try:
            cursor.close()
        except:
            pass
        return False


def main_handler(event, context):
    print(event)
    body = json.loads(event['body'])
    wecaht = body['wechat']
    nickname = body['nickname']
    remark = str(body['remark'])

    if getUserInfor(connection, wecaht) == 0:
        if addUseerInfor(connection, wecaht, nickname, remark):
            result = True
        else:
            result = False
    else:
        result = True

    return {
        "result": result
    }

公共组件的编写

  • 这个函数,我要作为小程序的一个接口,那么就要接APIGW,那么我应该怎么赖在本地测试呢?难不成每次都发到线上配置APIGW触发器才能测试,我的天,太恶心了吧!

  • 这个函数需要数据库的连接,需要获取用户的信息等,难道别的函数不需要么?如果需要也要每个函数都要重复写这部分代码?或者说,代码的复用应该如何处理呢?是否可以提取公共组件呢?

所以,我这里将这个函数,规范化和完整化:

# -*- coding: utf8 -*-

import json

try:
    import returnCommon
    from mysqlCommon import mysqlCommon
except:
    import common.testCommon

    common.testCommon.setEnv()

    import common.returnCommon as returnCommon
    from common.mysqlCommon import mysqlCommon


mysql = mysqlCommon()


def main_handler(event, context):
    try:
        print(event)

        body = json.loads(event['body'])

        wecaht = body['wechat']
        nickname = body['nickname']
        remark = str(body['remark'])

        if not wecaht:
            return returnCommon.return_msg(True"请使用微信小程序登陆本页面。")

        if not mysql.getUserInfor(wecaht):
            if not nickname:
                return returnCommon.return_msg(True"参数异常,请重试。")
            if mysql.addUserInfor(wecaht, nickname, remark):
                return returnCommon.return_msg(False"注册成功")
            return returnCommon.return_msg(True"注册失败,请重试。")
        return returnCommon.return_msg(False"登录成功")
    except Exception as e:
        print(e)
    return returnCommon.return_msg(True"用户信息异常,请联系管理员处理")

def test():
    event = {
        "requestContext": {
            "serviceId""service-f94sy04v",
            "path""/test/{path}",
            "httpMethod""POST",
            "requestId""c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
            "identity": {
                "secretId""abdcdxxxxxxxsdfs"
            },
            "sourceIp""14.17.22.34",
            "stage""release"
        },
        "headers": {
            "Accept-Language""en-US,en,cn",
            "Accept""text/html,application/xml,application/json",
            "Host""service-3ei3tii4-251000691.ap-guangzhou.apigateway.myqloud.com",
            "User-Agent""User Agent String"
        },
        "body": json.dumps({
            "wechat""12345",
            "nickname""test",
            "remark""",
        }),
        "pathParameters": {
            "path""value"
        },
        "queryStringParameters": {
            "foo""bar"
        },
        "headerParameters": {
            "Refer""10.0.2.14"
        },
        "stageVariables": {
            "stage""release"
        },
        "path""/test/value",
        "queryString": {
            "foo""bar",
            "bob""alice"
        },
        "httpMethod""POST"
    }
    print(main_handler(event, None))


if __name__ == "__main__":
    test()

数据库等一些公共组件,统一放在common目录下,例如mysqlCommon.py(部分):

# -*- coding: utf8 -*-

import os
import random
import pymysql
import datetime

try:
    import cosClient
except:
    import common.cosClient as cosClient


class mysqlCommon:
    def __init__(self):
        self.getConnection({
            "host": os.environ.get('mysql_host'),
            "user": os.environ.get('mysql_user'),
            "port": int(os.environ.get('mysql_port')),
            "db": os.environ.get('mysql_db'),
            "password": os.environ.get('mysql_password')
        })

    def getConnection(self, conf):
        self.connection = pymysql.connect(host=conf['host'],
                                          user=conf['user'],
                                          password=conf['password'],
                                          port=int(conf['port']),
                                          db=conf['db'],
                                          charset='utf8',
                                          cursorclass=pymysql.cursors.DictCursor,
                                          autocommit=1)

    def doAction(self, stmt, data):
        try:
            self.connection.ping(reconnect=True)
            cursor = self.connection.cursor()
            cursor.execute(stmt, data)
            result = cursor
            cursor.close()
            return result
        except Exception as e:
            print(e)
            try:
                cursor.close()
            except:
                pass
            return False

    def addUserInfor(self, wecaht, nickname, remark):
        insert_stmt = (
            "INSERT INTO users(wechat, nickname, remark) "
            "VALUES (%s,%s,%s)"
        )
        data = (wecaht, nickname, remark)
        result = self.doAction(insert_stmt, data)
        return False if result == False else True

这样做的好处是:

  • 我将数据库提取出一个公共组件,便于维护

  • 在login函数中,我根据不同的时期(本地开发和线上),可以导入不同的模块

便于开发与测试的方法

由于云函数的测试非常不友好,所以为了让编写代码时候,可以更快地模拟线上环境,可以通过增加test()方法来模拟触发器情况,进行简单的测试。

try:
    import cosClient
except:
    import common.cosClient as cosClient

这样会更加便利,同时模拟网关,做一个测试方法:

def test():
    event = {
        "requestContext": {
            "serviceId""service-f94sy04v",
            "path""/test/{path}",
            "httpMethod""POST",
            "requestId""c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
            "identity": {
                "secretId""abdcdxxxxxxxsdfs"
            },
            "sourceIp""14.17.22.34",
            "stage""release"
        },
        "headers": {
            "Accept-Language""en-US,en,cn",
            "Accept""text/html,application/xml,application/json",
            "Host""service-3ei3tii4-251000691.ap-guangzhou.apigateway.myqloud.com",
            "User-Agent""User Agent String"
        },
        "body": json.dumps({
            "wechat""12345",
            "nickname""test",
            "remark""",
        }),
        "pathParameters": {
            "path""value"
        },
        "queryStringParameters": {
            "foo""bar"
        },
        "headerParameters": {
            "Refer""10.0.2.14"
        },
        "stageVariables": {
            "stage""release"
        },
        "path""/test/value",
        "queryString": {
            "foo""bar",
            "bob""alice"
        },
        "httpMethod""POST"
    }
    print(main_handler(event, None))

增加本地测试时,指定test()方法:

if __name__ == "__main__":
    test()

这样,线上触发时,会默认执行main_handler, 而本地执行,则会通过 标签: 冷启动阿里云腾讯云华为云谷歌云AWS性能评估


作者简介:刘宇,毕业于浙江大学,硕士学历,目前在腾讯工作,著有《Serverless 架构》一书,是Serverless架构的热衷者,曾做一款叫Anycodes的软件,目前下载超过100万次。

返回列表

上一篇:未命名

下一篇:未命名

相关文章

云函数中使用Python-ORM: Peewee

云函数中使用Python-ORM: Peewee

前言 ORM(Object Ralational Mapping,对象关系映射)用来把对象模型表示的对象映射到基于SQL的关系模型数据库结构中去。这样,我们在具体的操作实体对象的时候,就不需要再去和复...

serverless-git和serverless-cicd

serverless-git和serverless-cicd

前言 传统情况下,我们写完代码,可能面对两个事情:发布到代码仓库以及部署到线上,传统的我们会手动实现这些操作,出现误操作的概率也是蛮高的,相对来说也是比较机械化的工作。CICD的引入,大大改善了持续继...

基于Serverless架构的Git代码统计工具

基于Serverless架构的Git代码统计工具

前言 自己毕业也有一年多了,很想统计一下过去一年自己贡献了多少的代码。想了一下,可能要用git log,简单的做了一下,感觉不是很爽,正直自己想通过Serverless做一个工具合集,就想能不能...

基于Serverless的验证码识别API

基于Serverless的验证码识别API

前言 之前和大家分享了很多的CV相关的例子,被很多小伙伴吐槽说我是调包侠,还连累了Serverless被很多人误以为也仅仅能"调包玩一玩",其实在Serverless中,开发者的自由度还是非常大的,除...

用Serverlss部署一个基于深度学习的古诗词生成API

用Serverlss部署一个基于深度学习的古诗词生成API

第六篇:用Serverlss部署一个基于深度学习的古诗词生成API 前言 古诗词是中国文化殿堂的瑰宝,记得曾经在韩国做Exchange Student的时候,看到他们学习我们的古诗词,有中文的还有翻译...

利与弊-传统框架要不要部署在Serverless架构上

利与弊-传统框架要不要部署在Serverless架构上

Serverless架构发展的速度可以说是非常的迅速,而且Serverless的发展也是有一套自己的独特的打法,这种打法在一定程度上,让很多开发者不适应,尤其是传统的Web框架无法在Serverles...

利与弊-多个接口要分成多个函数还是写到一个函数中

利与弊-多个接口要分成多个函数还是写到一个函数中

我们在做一个项目的时候,会有多个函数/方法,这个是一个很常见的事情,就算不按照函数/方法来划分,也通常会有多个功能,以一个简单的博客为例,可能拥有最基础的: 获取分类列表 获取文章列表(默认/...

评论列表

hack
2020-05-14 18:32:37

额,感觉这质量根本无法商用吧

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。