前言

up真香

终于还是来写这篇博客了,本文记录了我的AWS Lambda Function APP 的部署工具从apex up迁移到原生的AWS Lambda Go SDK,然后再迁回`pex up的整个过程以及原因,希望给之后喜欢折腾AWS Lambda的同学一点经验和灵感。

本博文不会涉及为什么选择Lambda这种Serverless的方案来部署应用的话题,有兴趣的同学可以自己去调研;)

什么是apex up ?

up是TJ大佬写的一个AWS Lambda 部署工具,TJ从apex中抽离了一个workflow, 然后再进行重新封装整合,然后就有了这个完美适配 AWS Lambda 的deploy workflow . up可以通过up Proxynode的runtime转发到你的二进制程序(以Go为例),然后暴露node的入口函数handler给AWS Lambda 。为什么会是node ? 因为up刚开始写的时候Lambda只支持node的runtime。那么为什么之后不换掉node 的runtime ? TJ在这个issue里面回答了这个问题。主要是Go native runtime和用nodeup proxy在性能上其实也查不了多少,这里有一份benchmark可以看看。当然,随着时间的推移和Lambda runtime的优化,这两个runtime终究会有一个领先,那么到那时候,就可以提PR给up重构代码了。

up VS Go SDK ?

在接触到up之前,可能你也跟我一样,用着AWS官方的lambda sdk,然后顺利地写出了一个「Hello World」, 但是,当你想部署自己的Web Service或者想迁移自己的Web Service到AWS Lambda的时候,被AWS复杂的API Gateway配置劝退。当然此时的你可能不知道,这之后还要配置监控用的CloudWatch和鉴权系统IAM。当然,你也可以自己写CloudFormation来管理自己的deploy profile,如果你看着几百行的JSON配置不眼花的话。

显然,我们的部署不需要这么复杂,被现代部署工具宠坏了的我们,可能只想写个Dockerfile/YAML或者一个其它的什么配置文件来帮助我们解决部署的问题,而不是自己去AWS控制台XJB配,(写到这里,可能会联想到Terraform ,现代程序猿可真的是越来越懒了呢),最后,所有的箭头都指向了 up , up 可以通过一些非常简单易懂的配置来代理你的原生Go程序,这里说的原生Go程序,其实是为了区别用SDK的侵入式写法,如果我们使用SDK来构建并且运行我们的Lambda应用程序,那么代码(main.go)中必须显式声明import "github.com/aws/aws-lambda-go/lambda",并且需要调用lambda.Start()方法,才能让lambda接入到我们的Go程序中; 但是,代理就不一样,我们不需要去依赖任何关于Lambda SDK的东西,只需要关注自己的业务逻辑就可以了, up proxy 会帮我们与Lambda SDK和API Gateway交互,只需要我们在up.json中配置我们需要的代理程序即可,比如:

  "proxy":{
    "command":"./main "
  },

声明了一个up proxy, node runtime的入口函数handler的会代理到这个main二进制文件上去

并且,如果你的Lambda Function APP需要暴露一些Web API(也就是需要配置Gateway),那么必须自己封装一层Proxy来代理你的请求到AWS Gateway里面, 在 Native Go SDK 里面我们可能需要用到类似gateway/api-proxy这些东西来暴露我们的API,但是在up里面我们不需要关心这个问题,up会帮我们自动生成AWS Gateway的配置代理到你的应用。

综上,up的部署方式更适合我们Service到Serverless的迁移,也更方便Serverless服务的扩展

当然,由于多了一个node的壳子,所以Lambda应用的大小会多~3MB左右,但是,AWS的S3的免费空间有点大(Lambda建议不直接上传zip,而是先上传到S3,然后用S3的文件路径挂载到Lambda上面),3MB左右的大小应该还是可以接受的。

up在持续集成中的实践

由于up实用的CLI,使得它在集成CI/CD workflow中非常容易。 比如我们的Lambda Function APP有两个环境,一个staging,一个prodcution(apex的CloudFormation 也是默认设置了这两个环境),在程序CI完毕在CD阶段的时候,我们只要调用up CLI的 up deploy (staging/prodcution) 就可以触发整个CloudFormation的重新“构建”,然后重新生成Lambda的配置,最后重新部署Lambda APP。

在类似[Drone.io]()这种CI/CD工具中,我们只要定义如下配置,apex就可以在push master的时候,触发production环境的重新部署:

- name: up_prod
  image: golang:1.13
  environment:
    AWS_SECRET_ACCESS_KEY:
      from_secret: AWS_SECRET_ACCESS_KEY
    AWS_ACCESS_KEY_ID:
      from_secret: AWS_ACCESS_KEY_ID
    # SERVERLESS: on
  commands:
  - curl -sf https://up.apex.sh/install | sh
  - up deploy production -v
  when:
    event:
    - push
    branch:
    - master

如果你还是觉得要写这么东西有点麻烦,那么也许吴老师的这个Drone插件会帮到你。

up这么方便,为什么我还会有离开up的想法

TL;DR : 思维定势,理解上出了偏差

我在开发feeds这个项目的时候,偶然发现up的部署方式貌似不支持GoRoutine,具体的场景是我有个GoRoutine会去更新我的某个cache,但是这会是个比较耗时的Network I/O,所以被我设计成不会阻碍main routine的形式了,但是,这个程序在部署在Lambda上的时候发现这个cache永远不会被更新,表现就像是GoRoutine没有被调度一样,但是,在Server环境下确实表现正常,cache很快就被更新了。

于是, 我单独对apex和aws官方的Lambda Go SDK做了测试 ,发现无论是up还是aws lambda sdk,GoRoutine和Channel的机制确实都是支持的,难道是up的nodeJS runtime有什么莫名其妙的bug ?

于是,我一边吐槽一边着手基于Go Runtime定制新的workflow(甚至没有给apex提issue,就在Slack冒了个泡,但是没人鸟我)。但是,就在我写到一半的时候,我想到了一个问题, lambda里面function的生命周期究竟是怎样的 ?,我们暴露出去的应该是一个function,这个fucntion会被多次并发执行,跟Server的形式不一样,它并没有一个一直在运行的进程来维护资源,所以我们在每次调用某个function的时候,如果有一些需要初始化的资源,那么每次在每个生命周期内都需要初始化,所以,拿我的cache这种全局内存资源举例,每次都要经历初始化的过程,每次的请求更新缓存的机制自然也就消失了,在这种情况下,内存型的cache就失去了他的用武之地。也就是说,在Serverless的世界里,任何可以被更改的全局资源就是个深坑。

正巧又看到了TJ对于Lambda和内存型cache的说明,脸有点疼,这篇文章可以给Lambad的life cycle扫个盲,并且探讨了在Lambda中维护cache在一定程度上的可行性,非常值得对Lambda生命周期一知半解的同学阅读(我彷佛就是在说我自己hhh)。但是由于确实一次函数调用之后,对应的container会封锁background job,我在GoRuotine中定时更新cache的设计确实是不可行的。于是,我在保持cache的基础上,移除了用于更新cache的backend GoRoutine,改成了用CloudWatch Event来定时触发我的Function,从而达到了和background GoRoutine差不多的效果。

具体可以戳这个commit

那么你基于Go shim的搬运是不是就停止了 ? 怎么可能,有空了一定继续写!

以上,记录我对AWS Lambda的折腾历程。Anyway, 我还是挺看好FaaS Platform可以真正被用起来的。

Reference: