前言
阿里云函数计算 Function Compute(FC),旨在帮助用户采用弹性伸缩、动态分配资源的方式,来执行业务函数。让用户无需购买部署服务器,无需考虑业务负载,就能快速搭建可处理高并发的后台服务。
函数计算平台针对 Java 语言推出的 Java HTTP 触发器功能,能够无缝迁移传统的 Java Web 应用。支持基于 Servlet 协议的 Web 框架所开发的应用,比如常用的 Spring、SpringBoot、Struts2等。
本文介绍如何使用 Java HTTP 触发器来快速迁移 Spring 提供的开源 Web 项目 GreenHouse。
相关链接
开始迁移
一、打包需要迁移的 Web 工程为 war 包
可以使用已编译好的 greenhouse.war,本步骤可直接跳过
- GreenHouse github 上下载 Web 工程源码。
- 在源码根目录执行 maven 打包命令
maven clean package -DskipTests
。
注意 GreenHouse 使用的 JDK 版本是1.6,比较老。这里打包前需要修改下 pom.xml 为你使用的 JDK 版本。
打包成功后,在 target 目录下能看到 greenhouse-1.0.0.BUILD-SNAPSHOT.war
这个文件。最后为了方便将该 war 包重命名为 greenhouse.war
。
二、在函数计算平台创建 Java 函数
将要运行的应用 war 包可以和函数代码一起打包上传,也可以放在网络存储中比如 阿里云对象存储(OSS),或者任何其它的网络存储。以下示例将应用 war 包放在函数代码工程中和存储到 OSS 中两种方式。
方式一:应用 war 包放在函数代码工程
-
在本地创建 maven 工程,并创建一个 package 比如
com.aliyun.fc.example
,在 package 中添加 Java 类 HelloWebLocal.java:public class HelloWebLocal implements FunctionInitializer, HttpRequestHandler { private FcAppLoader fcAppLoader = new FcAppLoader(); private String key = "greenhouse.war"; private String userContextPath = "/2016-08-15/proxy/${YourServiceName}/${YourFunctionName}"; @Override public void initialize(Context context) throws IOException { FunctionComputeLogger fcLogger = context.getLogger(); fcAppLoader.setFCContext(context); // Set war file path fcAppLoader.loadCodeFromLocalProject(key); // Init webapp from code long timeBegin = System.currentTimeMillis(); fcLogger.info("Loading webapp: " + key); boolean initSuccess = fcAppLoader.initApp(userContextPath, HelloWebOSS.class.getClassLoader()); if(! initSuccess) { throw new IOException("Init web app failed"); } fcLogger.info("Loaded webapp, elapsed: " + (System.currentTimeMillis() - timeBegin) + "ms"); } @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response, Context context) throws IOException, ServletException { try { fcAppLoader.forward(request, response); } catch (Exception e) { e.printStackTrace(); } } }
其中引用的 maven 库:
<dependency> <groupId>com.aliyun.fc.runtime</groupId> <artifactId>fc-java-core</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>com.aliyun.fc.runtime</groupId> <artifactId>fc-java-common</artifactId> <version>1.0.0</version> </dependency>
- 将 greenhouse.war 拷贝到 Java 工程中,上述代码中
loadCodeFromLocalProject
方法参数为 greenhouse.war 文件在工程中的相对路径。比如放在src/main/resources
目录,那就和上述代码保持一样,否则改成对应的相对路径。
方式二:应用 war 包存储到 OSS 中
如果将 war 包放在创建函数的 Java 工程中,会增加创建函数时上传的代码包大小。对于代码包大小函数计算有限制最大为 50M ,请参考函数计算使用限制。往往 Java 的 web 应用 war 包会比较大,因而更好的方式是将 war 包放在 OSS 中,然后通过初始化函数 initializer 来下载 war 包到执行环境中。同样可以调用 fc-java-common
库中的 loadCodeFromOSS
方法即可,该方法会将对应的 war 包下载到执行环境的临时磁盘目录 /tmp
中。
- 将 greenhouse.war 上传到 OSS 中
-
同
方式一
创建 maven 工程以及 Java package,并创建 HelloWebOSS.java:public class HelloWebOSS implements FunctionInitializer, HttpRequestHandler { private FcAppLoader fcAppLoader = new FcAppLoader(); private String ossEndPoint = "${YourOSSEndpoint}"; private String bucket = "${YourOSSBucket}"; private String key = "greenhouse.war"; private String userContextPath = "/2016-08-15/proxy/${YourServiceName}/${YourFunctionName}"; @Override public void initialize(Context context) throws IOException { FunctionComputeLogger fcLogger = context.getLogger(); fcAppLoader.setFCContext(context); // Load code from OSS fcLogger.info("Begin load code: " + key); boolean codeSuccess = fcAppLoader.loadCodeFromOSS(ossEndPoint, bucket, key); if (! codeSuccess) { throw new IOException("Download code failed"); } fcLogger.info("End load code"); // Init webapp from code long timeBegin = System.currentTimeMillis(); fcLogger.info("Begin load webapp"); boolean initSuccess = fcAppLoader.initApp(userContextPath, HelloWebOSS.class.getClassLoader()); if(! initSuccess) { throw new IOException("Init web app failed"); } fcLogger.info("End load webapp, elapsed: " + (System.currentTimeMillis() - timeBegin) + "ms"); } @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response, Context context) throws IOException, ServletException { try { fcAppLoader.forward(request, response); } catch (Exception e) { e.printStackTrace(); } } }
函数计算平台创建函数
关于怎么在函数计算平台创建函数请参考: Hello World 示例
关于怎么使用 Java 语言请参考:Java 运行环境
将上述的 maven 工程打包,并在函数计算平台创建服务和函数,这里需要注意的点:
-
需要将您创建的服务名和函数名,填充到上述
HelloWebLocal.java
中:private String userContextPath = "/2016-08-15/proxy/${YourServiceName}/${YourFunctionName}";
-
如果您使用的是 OSS 存储方式,需要填充 OSS 相关信息到
HelloWebOSS.java
中:private String ossEndPoint = "${YourOSSEndpoint}"; private String bucket = "${YourOSSBucket}"; private String userContextPath = "/2016-08-15/proxy/${YourServiceName}/${YourFunctionName}";
- 创建函数时除了需要设置函数入口外,还需要设置初始化入口指向上述代码的
initialize
函数。 - Web 应用使用的内存较多,请注意函数内存大小设置,比如我这里设置的是 512M
示例配置如下:
为函数创建 HTTP 触发器
关于怎么创建 HTTP 触发器请参考:HTTP 触发器
测试函数运行
在函数计算控制台执行 HTTP 触发器,看到如下返回:
可以看到成功返回了 greenhouse 的 web 页面。
但是如果在浏览器中执行 HTTP 触发器的请求地址,web 页面将会已文件的形式下载。这是因为函数计算为了安全强制设置了请求返回的 header 为 Content-Disposition: attachment
,所以返回结果会以附件形式下载。
为 HTTP 触发器绑定自定义域名
为了解决这个问题,需要用户申请自定义域名,并绑定域名解析到函数计算中,请参考函数计算绑定自定义域名
使用自定义域名访问,需要将最初的 Java 代码中 userContextPath
改为自定义域名的 contextPath 比如/
/greenhouse
等,这样才能保证页面中的超链接是相对于当前请求的域名:
// Not use custom domain
//private String userContextPath = "/2016-08-15/proxy/${YourServiceName}/${YourFunctionName}";
// Use custom domain
private String userContextPath = "/greenhouse";
自定义域名绑定到 HTTP 触发的示例配置:
自定义域名路径前缀必须和代码中 userContextPath 保持一致,比如这里都是
/greenhouse
。当然可以选择其它的路径名,也可以使用根路径,即路径配置为 /* 对应的 userContextPath 为/
。
通过修改配置路径和对应的 userContextPath,可映射到不同的 HTTP 触发器函数,从而映射到不同的 web 应用中。这样就能同一个域名同时复用到不同的 web 应用。
浏览器中访问自定义域名:
大功告成!
后语
函数计算是 serverless 服务,对于传统的 Web 应用支持目前还是存在一些瑕疵。
- 无状态性,每次函数执行都是无状态的。对于 Web 应用往往有很多状态需要保持,比如 session,用户需要在自己的 Web 应用中去处理。当然很多 web 框架已经提供了很方便的分布式 session 方案,只需简单的配置即可,可以参考Spring Session Redis
- 冷启动,新的执行环境中第一次请求 web 应用会比较耗时,这是由于执行环境需要启动 JVM 以及加载 web 应用,针对这个可通过定时预热的方式来解决。
但这些相对于 serverless 提供的弹性伸缩和按需付费的优点不值一提,个人认为 serverless 必定会取代传统物理机或虚拟机的服务器方式,从而让有限的资源得到更高效的利用。