深度好文:云函数 SCF + KonaJDK11 + Spring + 提速降存一把梭

作者:
发布于: 2021-3-24
归档于:

标签:ServerlessKonaJDK

一、背景

腾讯 KonaJDK 团队最近对外开源了KonaJDK11, 该版本 JDK 是经过内部超大规模生产环境验证的定制 JDK,该版本在启动性能、峰值性能以及事物处理能力方面,相对于前一版本 Kona JDK8 都有了综合性提升,沉淀了腾讯云与大数据团队在大数据/机器学习、云原生场景下的深度优化,并且通过了 JCK 验证,确保充分的 Java SE 标准兼容。通过工业标准 Benchmark 表明,Kona JDK11 对比 Kona JDK8 大多数场景在峰值性能上具有非常明显的提升,个别性能提升接近 50%。

KonaJDK11 如此优秀,我们能不能把它引入到Serverless呢? 另外,最近笔者也在考虑怎么样让 Java spring 框架在 SCF 中顺滑的跑起来,所以借着这个机会,索性来一把 KonaJDK + Spring 在 SCF 上的实践总结。

多说无用,Show you my code!

二、SCF使用JDK11

腾讯云Serverless云函数SCF产品中内置Java8支持,但是并没有高版本JDK的环境支持,那么如何实现SCF的Java11云函数呢?

实际上,SCF云函数提供的CustomRuntime功能已经解锁了用户使用编程语言的限制,目前已经有webassembly,swift,rust等成功例子。我们可以同样借助这个功能来将KonaJDK11引入SCF,从而实现高版本Java的支持。

过程如下:

  1. 下载KonaJDK11,https://github.com/Tencent/TencentKona-11/releases
  2. 由于KonaJDK11的二进制包比较大,需要使用SCF层的概念来上传KonaJDK11程序包

首先需要创建层,由于KonaJDK11程序包超过50MB,所以可以选择COS方式,现将KonaJDK11安装包上传到腾讯云COS,之后在创建层时指定路径即可, 具体使用可以参考产品说明https://cloud.tencent.com/document/product/583/45760

  1. 创建云函数, 注意这里需要使用CustomRuntime,我们选择Shell函数示例,再次基础上拓展我们的KonaJDK11的支持.

进入【高级配置】->【层配置】->【添加层】

按照下图所示配置好【层】【超时时间】与【内存】点击【完成】

  1. 根据SCF CustomRuntime的使用说明,需要编写CustomRuntime的启动文件 Bootstrap,SCF CustomRuntime会在函数启动时第一步找到并执行这个名为bootstrap的可执行文件。
  2. 我们的bootstrap中需要配置环境变量,并启动Java程序. 我们先假设我写了一个名为Hello的class,里面只打印hello SCF 字符串。 之后将bootstrap文件和Hello.class文件一起打包成一个zip文件,上传到SCF部署,这时bootstrap的内容如下:

可以看到就是简单的环境变量配置和执行java -version 与 Hello程序。

之后点击【测试】触发执行,之后我们可以看到函数执行日志如下:

我们已经可以从日志里看到 openjdk version "11.0.9.1-ga"的 Java版本,并且看到了Hello程序正常输出。至此,KonaJDK 11 已经顺利跑在了云函数环境中。

注意此处显示【测试失败】是正常的,因为我们还没有编写处理【函数事件】的逻辑,也就是还没有实现具体的云函数。

三、实现spring云函数

现在让我们来用spring框架实现一个能跑在KonaJDK11上的云函数。为了清晰,我们写一个最简单的springboot Demo, 它的controller长这样:

入口函数长这样:

OK,目前这个Demo可以接受 http Get localhost:8080/hello 请求并返回 hello, this is a springboot demo! 字符串。 那么如何将它改编成云函数呢?

从 SCF CustomRuntime 文档以及一些公开的资料,可以看到编写CustomRuntime的函数,只需要两个关键步骤:

  1. 编写可执行启动程序bootstrap,在bootstrap里面启动我们的spring云函数
  2. 编写云函数。这一步首先需要了解CustomRuntime工作的流程,从这篇文章可以看到,主要流程如下:

具体说来,就是在bootstrap启动云函数以后,sping云函数在自身初始化时需要先POST一个Ready的httpRequest给 SCF服务端, 目的是通知SCF函数初始化完毕,可以获得下发的事件了。

之后,spring需要一个循环,循环内部通过向SCF服务端发送HTTP GET请求,获得待处理事件,再调用内部逻辑,处理完事件之后通过POST请求发送给SCF服务端,循环等待下一次事件下发。

针对Springboot, 我们的云函数主要有以下几个需要处理的地方:

  1. 事件下发: Springboot云函数主要是启动并监听云函数内部的一个自定义http端口,通过http请求完成处理任务。 SCF云函数目前http请求主要通过API Gateway事件下发,也就是说,spring云函数的逻辑里面,需要将API Gateway事件转换成http事件之后再发给函数内部的springboot监听的端口。好在整个这一套逻辑的转换SCF其实已经提供给了我们,就在SCF java event的代码中,可以从 https://github.com/tencentyun/SCF-java-libs/blob/master/SCF-java-events/src/main/java/com/qcloud/SCF/runtime/AbstractSpringHandler.java 这个代码直接抽取复用。
  2. 初始化: 也就是在第一次启动云函数的时候,我们需要启动springboot,另其建立httpserver并监听端口。 之后每次事件下发,只需要发送httprequest即可。
  3. 监听事件: 这里就是按照 SCF CustomRutime 的要求,写一个循环,使用http GET请求获取event,并发送给内部springboot监听的端口。

经过上面的梳理,逻辑已经基本上清晰了:首先,需要在 cold launch阶段启动springboot入口函数, 通知SCF服务端,springboot云函数初始化完毕,等待接收消息。之后就是一个大循环,循环里面工作如下:

  • 通过 Http GET 请求从SCF服务端获得 ApiGateway 下发的event

  • Api GW event转换成 http request 并发送到 springboot 监听的端口,等待返回处理结果
  • springboot 返回的 event 转换为 ApiGateway Response, 通过POST请求返回给SCF 服务端
  • 进入下一次循环,等待下一次事件下发.

处理流程代码也很简单:

至此,我们已经完成了云函数的编写,之后我们可以测试一下,将bootstrap和编译后的 springboot-application.jar 打包到一个zip文件,然后上传到SCF云函数进行部署。

之后按照如下配置 apiGW 的 event,注意这里配置 Get,“/hello” 是由于我们的springboot 云函数的controller配置成了接收Get, “/hello” 请求并打印和返回字符串,实际上用户需要根据自己的业务,修改apiGW这里event相应的内容。

然后点击[测试]: 稍等一下就可以看到如下log:

springboot已经启动, 然后我们还可以看到:

函数已经正常响应了GET /hello的请求。

四、利用appCDS特性提速降存

在上面的springboot云函数中,我们可以看到一次冷启动耗时和内存如下:

同时log中也包含了springboot的启动时间

总体来说就是耗时6秒多,使用了168MB的内存。

那么,如何提高启动速度减少内存使用呢?

JDK11里面自带appCDS功能,具openJDK官方说法,该功能可以减少java类加载时间同时减少内存占用量,提高启动速度。 这不正是我们想要的么,我们现在已经有了KonaJDK+springboot的云函数,那么怎么在KonaJDK中使用起来这个功能呢?

  1. appCDS功能使用步骤:

按照JDK官方文档, appCDS使用方式主要是以下几个步骤:

  • 生成待 dump 的类文件列表, 使用 -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=classes.lst JVM选项运行程序,会生成classes.lst文件
  • 使用 -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=classes.lst \ -XX:SharedArchiveFile=dump.jsa 生成dump.jsa文件
  • 使用appCDS正常启动java程序,使用JVM选项 -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=dump.jsa
  • 云函数中enable appCDS

针对云函数SCF的场景,主要需要以下适配工作

  • 由于在云函数中,目前只有/tmp目录是可写目录,所以1中的步骤我们需要将所有涉及到的文件路径变更为 /tmp/classes.list/tmp/dump.jsa
  • 由于我们期望最终生成的dump.jsa可以在多个云函数实例中使用,我们需要得到/tmp/dump.jsa文件,然后将其和云函数一起打包,这样在使用时候,我们只需要指定Jvm参数 -XX:SharedArchiveFile=dump.jsa 即可复用 dump.jsa 文件。 所以我们需要获得生成的 /tmp/dump.jsa 文件,由于SCF不能直接下载 /tmp目录的文件,所以我们根据COS的文档写了一小段程序,帮助我们在生成 /tmp/dump.jsa 文件后上传到指定的COS中,具体可以参考COS java 的sdk
  • 在得到 dump.jsa之后,我们就可以对整个云函数重新打包,最终打包的文件中包含3个子文件, 云函数 CustomRuntime的启动脚本bootstrap, springboot云函数的实现 SCF-springboot-web-1.0-SNAPSHOT.jar ,以及appCDS的archive文件 dump.jsa,我们将这3个文件打包重新部署。
  • 再部署之后,我们需要添加 JAVA_TOOL_OPTIONS 环境变量 JAVA_TOOL_OPTIONS=-Xshare:on -XX:SharedArchiveFile=dump.jsa

这样就可以在启动云函数时使用这些 jvm 选项了。

  1. 效果

在使用AppCDS之后出发云函数的冷启动,可以看到如下效果:

  • 内存使用 从原来的169MB降低到了100MB

  • springboot启动时间从原来的6.137s提高到了4.772s

总结

至此,我们在腾讯 Serverless 云函数上借助 CustomRuntime 完成了KonaJDK11 + SpringBoot云函数的使用,并利用KonaJDK11中AppCDS特性优化了云函数冷启动的速度与内存损耗。 文中利用CustomeRuntime引入KonaJDK11的方法可以作为腾讯云Faas上解锁多语言或高版本Java语言runtime的一种通用方式。

在未来腾讯KonaJDK团队会进一步针对腾讯云业务Faas场景的特点提供更多的功能与性能提升,敬请关注。