简介: Serverless 架构带来的除了一种新的架构、一种新的编程范式,还包括思路上的转变,尤其是开发过程中的一些思路转变。有人说要把 Serverless 架构看成一种天然的分布式架构,需要用分布式架构的思路去开发 Serverless 应用。诚然,这种说法是正确的。但是在一些情况下,Serverless 还有一些特性,所以要转变开发观念。

前言:在 Serverless 架构下,虽然更多精力是关注业务代码,但是实际上对一些配置和成本也是需要关注的,并且必要的时候还需要根据配置与成本对 Serverless 应用进行配置和代码优化。

Serverless 应用开发观念的转变

Serverless 架构带来的除了一种新的架构、一种新的编程范式,还包括思路上的转变,尤其是开发过程中的一些思路转变。有人说要把 Serverless 架构看成一种天然的分布式架构,需要用分布式架构的思路去开发 Serverless 应用。诚然,这种说法是正确的。但是在一些情况下,Serverless 还有一些特性,所以要转变开发观念。

1、文件上传方法

在传统 Web 框架中,上传文件是非常简单和便捷的,例如 Python 的 Flask 框架:

f = request.files['file']
f.save('my_file_path')

但是在 Serverless 架构下,文件却不能直接上传,原因如下:

  • 一般情况下,一些云平台的API网关触发器会将二进制文件转换成字符串,不便直接获取和存储;
  • 一般情况下,API 网关与 FaaS 平台之间传递的数据包有大小限制,很多平台限制数据包大小为 6MB 以内;
  • FaaS 平台大多是无状态的,即使存储到当前实例中,也会随着实例释放而使文件丢失。

所以,传统 Web 框架中常用的上传文件方案不太适合在 Serverless 架构中直接使用。在 Serverless 架构中,上传文件的方法通常有两种:一种是转换为 Base64 格式后上传,将文件持久化到对象存储或者 NAS 中,但 API 网关与 FaaS 平台之间传递的数据包有大小限制,所以此方法通常适用于上传头像等小文件的业务场景。

另一种上传方法是通过对象存储等平台来上传,因为客户端直接通过密钥等来将文件直传到对象存储是有一定风险的,所以通常是客户端发起上传请求,函数计算根据请求内容进行预签名操作,并将预签名地址返给客户端,客户端再使用指定的方法上传,上传完成之后,通过对象存储触发器等来对上传结果进行更新等,如下图所示。

在 Serverless 架构下文件上传文件示例

以阿里云函数计算为例,针对上述两种常见的上传方法通过 Bottle 来实现。在函数计算中,先初始化对象存储相关的对象等:

初始化对象存储相关的对象等:

AccessKey = {
"id": '',
"secret": ''
}
OSSConf = {
'endPoint': 'oss-cn-hangzhou.aliyuncs.com',
'bucketName': 'bucketName',
'objectSignUrlTimeOut': 60
}
#获取/上传文件到OSS的临时地址
auth = oss2.Auth(AccessKey['id'], AccessKey['secret'])
bucket = oss2.Bucket(auth, OSSConf['endPoint'], OSSConf['bucketName'])
#对象存储操作
getUrl = lambda object, method: bucket.sign_url(method, object, OSSConf['object
SignUrlTimeOut'])
getSignUrl = lambda object: getUrl(object, "GET")
putSignUrl = lambda object: getUrl(object, "PUT") #获取随机字符串
randomStr = lambda len: "".join(random.sample('abcdefghijklqrstuvwxyz123456789
ABCDEFGZSA' * 100, len))

第一种上传方法,通过 Base64 上传之后,将文件持久化到对象存储:

#文件上传
# URI: /file/upload
# Method: POST
@bottle.route('/file/upload', "POST")
def postFileUpload():
try:
pictureBase64 = bottle.request.GET.get('picture', '').split("base64,")[1]
object = randomStr(100)
with open('/tmp/%s' % object, 'wb') as f:
f.write(base64.b64decode(pictureBase64))
bucket.put_object_from_file(object, '/tmp/%s' % object)
return response({
"status": 'ok',
})
except Exception as e:
print("Error: ", e)
return response(ERROR['SystemError'], 'SystemError')

第二种上传方法,获取预签名的对象存储地址,再在客户端发起上传请求,直传到对象存储:

#获取文件上传地址
# URI: /file/upload/url
# Method: GET
@bottle.route('/file/upload/url', "GET")
def getFileUploadUrl():
try:
object = randomStr(100)
return response({
"upload": putSignUrl(object),
"download": 'https://download.xshu.cn/%s' % (object)
})
except Exception as e:
print("Error: ", e)
return response(ERROR['SystemError'], 'SystemError')

HTML 部分:

<div style="width: 70%">
<div style="text-align: center">
<h3>Web端上传文件</h3>
</div>
<hr>
<div>
<p>
方案1:上传到函数计算进行处理再转存到对象存储,这种方法比较直观,问题是 FaaS 平台与 API 网关处有数据包大小上限,而且对二进制文件处理并不好。
</p>
<input type="file" name="file" id="fileFc"/>
<input type="button" onclick="UpladFileFC()" value="上传"/>
</div>
<hr>
<div>
<p>
方案2:直接上传到对象存储。流程是先从函数计算获得临时地址并进行数据存储(例如将文件信息存到 Redis 等),然后再从客户端将文件上传到对象存储,之后通过对象存储触发器触发函数,从存储系统(例如已经存储到Redis)读取到信息,再对图像进行处理。
</p>
<input type="file" name="file" id="fileOss"/>
<input type="button" onclick="UpladFileOSS()" value="上传"/>
</div>
</div>

通过 Base64 上传的客户端 JavaScript 实现:

function UpladFileFC() {
const oFReader = new FileReader();
oFReader.readAsDataURL(document.getElementById("fileFc").files[0]);
oFReader.onload = function (oFREvent) {
const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new
ActiveXObject("Microsoft.XMLHTTP"))
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
alert(xmlhttp.responseText)
}
}
const url = "https://domain.com/file/upload"
xmlhttp.open("POST", url, true);
xmlhttp.setRequestHeader("Content-type", "application/json");
xmlhttp.send(JSON.stringify({
picture: oFREvent.target.result
}));
}
}

客户端通过预签名地址,直传到对象存储的客户端 JavaScript 实现:

function doUpload(bodyUrl) {
const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new Active
XObject("Microsoft.XMLHTTP"));
xmlhttp.open("PUT", bodyUrl, true);
xmlhttp.onload = function () {
alert(xmlhttp.responseText)
};
xmlhttp.send(document.getElementById("fileOss").files[0]);
} function UpladFileOSS() {
const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new Active
XObject("Microsoft.XMLHTTP"))
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
const body = JSON.parse(xmlhttp.responseText)
if (body['url']) {
doUpload(body['url'])
}
}
}
const getUploadUrl = 'https://domain.com/file/upload/url'
xmlhttp.open("POST", getUploadUrl, true);
xmlhttp.setRequestHeader("Content-type", "application/json");
xmlhttp.send();
}

整体效果如图中所示。

Serverless 架构下文件上传实验 Web 端效果

此时,我们可以在当前页面进行不同类型的文件上传方案实验。

2、文件读写与持久化方法

应用在执行过程中,可能会涉及文件的读写操作,或者是一些文件的持久化操作。在传统的云主机模式下,可以直接读写文件,或者将文件在某个目录下持久化,但是在 Serverless 架构下并不是这样的。

由于 FaaS 平台是无状态的,并且用过之后会被销毁,因此文件并不能直接持久化在实例中,但可以持久化到其他的服务中,例如对象存储、NAS 等。

同时,在不配置 NAS 的情况下,FaaS 平台通常情况下只具备 /tmp 目录可写权限,所以部分临时文件可以缓存在 /tmp 文件夹下。

3、慎用部分 Web 框架的特性

(1) 异步

函数计算是请求级别的隔离,所以可以认为这个请求结束了,实例就有可能进入一个静默状态。而在函数计算中,API 网关触发器通常是同步调用(以阿里云函数计算为例,通常只在定时触发器、OSS 事件触发器、MNS 主题触发器和 IoT 触发器等几种情况下是异步触发)。

这就意味着当 API 网关将结果返给客户端的时候,整个函数就会进入静默状态,或者被销毁,而不是继续执行完异步方法。所以通常情况下像 Tornado 等框架就很难在 Serverless 架构下发挥其异步的作用。当然,如果使用者需要异步能力,可以参考云厂商所提供的异步方法。

以阿里云函数计算为例,阿里云函数计算为用户提供了一种异步调用能力。当函数的异步调用被触发后,函数计算会将触发事件放入内部队列,并返回请求 ID,而不会返回具体的调用情况及函数执行状态。如果用户希望获得异步调用的结果,可以通过配置异步调用目标来实现,如图所示。

函数异步功能原理简图

(2) 定时任务

在 Serverless 架构下,应用一旦完成当前请求,就会进入静默状态,甚至实例会被销毁,这就导致一些自带定时任务的框架没有办法正常执行定时任务。函数计算通常是由事件触发,不会自主定时启动。例如 Egg 项目中设定了一个定时任务,但是在实际的函数计算中如果没有通过触发器触发该函数,该函数不会被触发,也不会从内部自动启动来执行定时任务,此时可以使用定时触发器,通过定时触发器触发指定方法来替代定时任务。

4、要注意应用组成结构

(1) 静态资源与业务逻辑

在 Serverless 架构下,静态资源更应该在对象存储与 CDN 的加持下对外提供服务,否则所有的资源都在函数中。通过函数计算对外暴露,不仅会让函数的业务逻辑并发度降低,也会造成更多的成本。尤其是将一些已有的程序迁移到 Serverless 架构上,例如 Wordpress 等,更要注意将静态资源与业务逻辑进行拆分,否则在高并发情况下,性能与成本都将会受到比较严峻的考验。

(2) 业务逻辑的拆分

在众多云厂商中,函数的收费标准都是依靠运行时间、配置的内存以及产生的流量收费的。如果一个函数的内存设置不合理,会导致成本成倍增加。想要保证内存设置合理,更要保证业务逻辑结构的可靠性。

以阿里云函数计算为例,一个应用有两个对外接口,其中有一个接口的内存消耗在 128MB 以下,另一个接口的内存消耗稳定在 3000MB 左右。这两个接口平均每天会被触发 10000 次,并且时间消耗均在 100 毫秒。如果两个接口写到一个函数中,那么这个函数可能需要将内存设置在 3072MB,同时用户请求内存消耗较少的接口在冷启动情况下难以得到较好的性能;如果两个接口分别写到函数中,则两个函数内存分别设置成 128MB 以及 3072MB 即可,如表所示。

通过上表可以明确看出合理、适当地拆分业务会在一定程度上节约成本。上面例子的成本节约近 50%。

关于作者:刘宇(江昱)国防科技大学电子信息专业在读博士,阿里云 Serverless 产品经理,阿里云 Serverless 云布道师,CIO 学院特聘讲师。

新书推荐

本书会通过多个开源项目、多个云厂商的多款云产品,以及多种途径向读者介绍什么是 Serverless 架构、如何上手 Serverless 架构、不同领域中 Serverless 架构的应用以及如何从零开发一个 Serverless 应用等。本书可以帮助读者将 Serverless 架构融入到自己所在的领域,把 Serverless 项目真实落地,获得 Serverless 架构带来的技术红利。

原文链接

本文为阿里云原创内容,未经允许不得转载。

Serverless 工程实践 | Serverless 应用开发观念的转变的更多相关文章

  1. Serverless 工程实践 | Serverless 应用优化与调试秘诀

    作者|刘宇   前言:本文将以阿里云函数计算为例,提供了在线调试.本地调试等多种应用优化与调试方案. Serverless 应用调试秘诀 在应用开发过程中,或者应用开发完成,所执行结果不符合预期时,我 ...

  2. Serverless 工程实践 | 零基础上手 Knative 应用

    作者|刘宇 前言:Knative 是一款基于 Kubernetes 的 Serverless 框架.其目标是制定云原生.跨平台的 Serverless 编排标准. Knative 介绍 Knative ...

  3. Serverless 工程实践|自建 Apache OpenWhisk 平台

    作者 | 刘宇(江昱) 前言:OpenWhisk 是一个开源.无服务器的云平台,可以在运行时容器中通过执行扩展的代码响应各种事件,而无须用户关心相关的基础设施架构. OpenWhisk 简介 Open ...

  4. Linux开源模块迁移概述暨交叉编译跨平台移植总结--从《嵌入式Linux驱动模板简洁和工程实践》

    本文摘录<嵌入式Linux驱动模板简洁和工程实践>一本书"开发和调试技术". Linux强大的是,有那么多的开源项目可以使用.通常非常需要可以通过寻找相关的源模块被定义 ...

  5. Serverless 初体验:快速开发与部署一个Hello World(Java版)

    昨天被阿里云的这个酷炫大屏吸引了! 我等85后开发者居然这么少!挺好奇到底什么鬼东西都是90.95后在玩?就深入看了一下. 这是一个关于Serverless的体验活动,Serverless在国内一直都 ...

  6. 公司简介 - CCDI悉地国际-工程实践专业服务的引领者

    公司简介 - CCDI悉地国际-工程实践专业服务的引领者 关于悉地国际         CCDI悉地国际(以下简称"CCDI")创立于1994年,是在城市建设和开发领域从事综合专业 ...

  7. LDA工程实践之算法篇之(一)算法实现正确性验证(转)

    研究生二年级实习(2010年5月)开始,一直跟着王益(yiwang)和靳志辉(rickjin)学习LDA,包括对算法的理解.并行化和应用等等.毕业后进入了腾讯公司,也一直在从事相关工作,后边还在yiw ...

  8. 工程实践:给函数取一个"好"的名字

    工程实践:给函数取一个"好"的名字 早在2013年,国外有个程序员做了一个有意思的投票统计(原始链接请见:<程序员:你认为最难做的事情是什么?>),该投票是让程序员从以 ...

  9. webpack 从入门到工程实践

    from:https://www.jianshu.com/p/9349c30a6b3e?utm_campaign=maleskine&utm_content=note&utm_medi ...

  10. Go 在游戏行业中的工程实践

    在今年 1 月由七牛云主办的 ECUG Con 十周年盛会上,真有趣技术总监陈明达带来了题为< Go 在游戏行业中的工程实践>的精彩分享,深入讲解了 Go 的工程经验,错误和异常处理,in ...

随机推荐

  1. 今日头条Linux 运维工程师面试真题

    今日头条Linux 运维工程师面试真题 首先我们来看下今日头条 Linux 运维工程师招聘岗位要求: [岗位定义]系统运维工程师 [岗位薪资]10K-24K [基本要求]北京 / 经验 3-5 年 / ...

  2. C#开发计算器类库

    C#开发计算器类库:开发中所涉及到有虚方法,继承,简单工厂等基础知识(编程借鉴'小菜变成成长记'https://www.jb51.net/article/2851.htm) 1.创建父类:计算(Ope ...

  3. C#进阶篇

    ArrayList 1.Arraylist本质上是一个可以自动扩容的object数组 2.由于用万物之父来存储数据,自然存在装箱拆箱 3.当往其中进行值类型存储时就是在装箱,当将值类型对象取出来转换使 ...

  4. quartus之LPM_DIVIDE

    quartus的IP测试之LPM_DIVIDE 1.基本作用 一个用于除法的IP,可以输入除数.被除数,得到商.余值. 2.基本测试 `timescale 1ns/1ns module divide_ ...

  5. python [pymysql] 操作MySQL数据库

    python [pymysql] 操作MySQL数据库 连接.关闭数据库 def get_conn_(): """ :return: 连接,游标 "" ...

  6. MySQL联结

    创建联结 mysql> SELECT vend_name,prod_name,prod_price FROM vendors,products WHERE vendors.vend_id=pro ...

  7. System.gc 之后到底发生了什么 ?

    本文基于 OpenJDK17 进行讨论 在 JDK NIO 针对堆外内存的分配场景中,我们经常会看到 System.gc 的身影,比如当我们通过 FileChannel#map 对文件进行内存映射的时 ...

  8. 3个.NET开源、免费、强大的商城系统

    前言 今天大姚给大家分享3个.NET开源.免费.强大的商城系统,希望可以帮助到有商城系统开发需求的同学. nopCommerce nopCommerce是一个功能丰富.免费.灵活且可定制的开源电子商务 ...

  9. #树形dp,树链剖分#CF442D Adam and Tree

    题目 初始有一个点 1,每次新加入点 \(2\sim n+1\),给这条边染上新的颜色, 并且一种颜色只能出现在一条路径上,使得每个点到根节点的路径上颜色种类数尽量少 每次询问输出每个点到根节点路径上 ...

  10. JDK10的新特性:var泛型和多个接口实现

    目录 简介 实现多个接口 使用多个接口 使用var 总结 简介 在JDK10的新特性:本地变量类型var中我们讲到了为什么使用var和怎么使用var. 今天我们来深入的考虑一下var和泛型,多个接口实 ...