移动应用的打包和分发呈现明显的峰谷效用,用户常常需要短时间内准备大量资源保障分发的实时性,完成分发后又需要及时释放资源,降低成本。本次分享将介绍如何通过函数计算构建 Serverless 架构的包分发服务,在开发运维效率,性能和成本间取得良好的平衡。
APK分包简介
一个应用包发布后,通常会分发给各个应用市场去推广,终端用户可能会通过不同的应用市场来下载安装应用。为了追踪应用在各个市场的下载安装情况,需要为每个应用市场的apk包打上渠道标记,用户下载安装后,应用执行时,可以获取到这个渠道信息,做一些个性化的服务。
分包的流程分为3步:
- 下载原始的apk包
- 写入渠道信息,生成新的apk包
- 上传新的apk包
应用的渠道可能会有很多,甚至上千个。所以一个应用包要生成上千个新的应用包。在分包过程中,下载/修改/上传是一个比较消耗资源的任务,需要消耗大量的计算/网络资源。并且分包任务只在应用发布新版本时才会发生,需要在尽可能短的时间内完成。
针对这种有明显波峰波谷的场景,非常适合使用函数计算来完成。
实验步骤
在这个实验中,我们会使用一个示例的apk包,可以从这里下载 qq-v2.apk
写入渠道信息的方式,我们使用美团的开源工具 walle
1. 准备apk包
下载 qq-v2.apk ,上传到自己的oss bucket中。
2. 编写函数
接下来我们编写函数,对这个apk包进行分包操作。函数的主要流程是:
- 从OSS bucket中下载原始apk包到
/tmp/
目录 - 调用 walle-cli 对这个apk包进行处理,写入渠道信息,示例中我们的渠道信息为"aliyun-fc"
- 将生成的apk包重新上传到OSS
我们使用 fun 工具来初始化一个java8的函数:
$ fun init helloworld-java8
Start rendering template...
+ /private/tmp/04-25
+ /private/tmp/04-25/pom.xml
+ /private/tmp/04-25/src
+ /private/tmp/04-25/src/main
+ /private/tmp/04-25/src/main/java
+ /private/tmp/04-25/src/main/java/example
+ /private/tmp/04-25/src/main/java/example/App.java
+ /private/tmp/04-25/src/test
+ /private/tmp/04-25/src/test/java
+ /private/tmp/04-25/src/test/java/example
+ /private/tmp/04-25/src/test/java/example/AppTest.java
+ /private/tmp/04-25/template.yml
finish rendering template.
在当前目录创建一个 .env
文件,内容如下,其中花括号的内容替换成自己的:
ACCOUNT_ID={阿里云uid}
REGION=cn-shanghai
ACCESS_KEY_ID={access key id}
ACCESS_KEY_SECRET={access key secret}
先部署函数,然后尝试运行一下:
mvn package
fun deploy
在控制台运行函数:
接下来,我们修改函数,加入分包逻辑:
- 修改template.yml: 超时时间改成60,内存改成1024,CodeUri: "./target/.", 设置Role
- 修改pom.xml: 增加OSS的SDK
- 下载 walle-cli-all.jar ,放到 ./target/ 目录下
- 修改App.java: 加入分包逻辑
template.yml:
ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
demo:
Type: 'Aliyun::Serverless::Service'
Properties:
Description: 'helloworld'
Role: acs:ram::1237050315505682:role/fc-service-role
demo:
Type: 'Aliyun::Serverless::Function'
Properties:
Handler: example.App::handleRequest
Runtime: java8
Timeout: 60
MemorySize: 1024
CodeUri: './target/'
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>demo</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>demo</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.aliyun.fc.runtime</groupId>
<artifactId>fc-java-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>
App.java:
package example;
import java.io.*;
import com.aliyun.fc.runtime.Context;
import com.aliyun.fc.runtime.StreamRequestHandler;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.GetObjectRequest;
/**
* Hello world!
*
*/
public class App implements StreamRequestHandler
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
@Override
public void handleRequest(
InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
OSSClient client = new OSSClient(
"http://oss-cn-shanghai.aliyuncs.com",
context.getExecutionCredentials().getAccessKeyId(),
context.getExecutionCredentials().getAccessKeySecret(),
context.getExecutionCredentials().getSecurityToken());
String bucketName = "rockuw-sh";
String objectName = "qq-v2.apk";
String outObjectName = "qq-v2-signed.apk";
String inputApk = "/tmp/input.apk";
String outputApk = "/tmp/output.apk";
// 1. download original apk
client.getObject(new GetObjectRequest(bucketName, objectName), new File(inputApk));
// 2. adding channel info
String cmd = "java -jar /code/walle-cli-all.jar put -c aliyun-fc";
cmd += " " + inputApk;
cmd += " " + outputApk;
context.getLogger().info("cmd: " + cmd);
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash", "-c", cmd);
try {
Process process = processBuilder.start();
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
int exitVal = process.waitFor();
if (exitVal == 0) {
context.getLogger().info("Success!");
outputStream.write("Success".getBytes());
} else {
//abnormal...
context.getLogger().error("Failed!");
context.getLogger().error("status: " + exitVal);
outputStream.write("Failed".getBytes());
}
System.out.println(output);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3. upload new apk
client.putObject(bucketName, outObjectName, new File(outputApk));
client.shutdown();
}
}
3. 运行函数
在FC控制台执行函数:
4. 查看结果
执行成功后,可以看到,OSS bucket中已经生成了新的apk文件:
把文件下载下来查看,确实已经有了渠道信息:
总结
经过简单的几个步骤,我们就实现了一个apk分包的服务,仅仅只需要不到80行代码。更重要的是这个服务是具有弹性伸缩和高可用能力的,即使有上千个包要分发也可以轻松应对。