「如何实现流动式软件发布」线上课堂开课啦,快来报名参与课堂抽奖吧~ 了解详情
写点什么

如何在 Serverless 架构下优雅上传文件?

2020 年 6 月 03 日

如何在 Serverless 架构下优雅上传文件?

传统开发中,文件上传是比较自由的:上传什么文件、怎么上传、存储到哪里等问题往往都是由开发者决定的,但是在 Serverless 架构下,上传文件就没有这么自由了。无论是成本原因,还是某些服务限制,我们都需要寻求一些比较"优"的解决方案。


Serverless 架构与文件上传

由于 Serverless 架构中函数计算的部分是没有办法做文件持久化,函数执行的容器用完过后就会被回收,所以如果想要存储文件,就需要借助对象存储等相关服务。


将文件上传到对象存储服务的方法很多,本文主要介绍两种:


  • 函数计算->对象存储

  • 对象存储


一般情况下,如果某个有文件上传功能,我们会用multipart/form-data,或者将文件进行base64编码之后再上传。但在 Serverless 架构下,这种思路需要做出一些改变。


函数计算->对象存储是上传文件比较常见,也是比较容易的方式。文件直接通过 API 网关,传送到云函数中,并重做一些处理(例如压缩图像、视频转码、数据入库等),然后再由云函数将结果存储到对象存储中,做文件资源的持久化。


这个思路看起来很顺畅,但是实际操作起来也会遇到很多问题:


首先,通过这种方式将文件传给函数时,函数计算通过 API 网关得到的数据结构往往是 JSON 格式,或者是字符串。这样的设计使得函数计算对二进制的支持非常不友好,我们只能将文件转换为base64编码后再进行传输,通过 API 网关之后,函数接收到数据,再将base64编码的文件解码,经过相关处理后持久化对象存储。


其次,无论是 AWS 的 Lambda,还是腾讯云的 SCF,通过 API 网关触发函数时都会有数据包大小限制的。以腾讯云为例,数据包的限制是 6M。也就是说,无论发送多大的数据,从 API 网关到函数计算都会有一个数据包的最大限制,上传文件过大,就无法进行资源的传输。所以,上传到云函数的文件必须在 6M 以下,而函数计算对二进制文件不友好,经过base64编码的数据包通常会大些,这样上传到云函数的数据包必须在 4M 左右。



4M 的图片是什么概念呢?如上图所示,是我用手机随机拍了一张图片,大小是 6.21M,这时我是无法将这张图片上传到 SCF 进行处理的。


除了对文件大小有限制之外,上述方法对成本也有一定影响,API 网关并不是一个适合传输文件的方法。我们可以单从流量费用来对比一下对象存储和 API 网关的区别:


  • API 网关的收费:

  • 对象存储的收费:


单从流量维度来看,API 网关的费用比 COS 高了许多,主要原因可能是因为 API 网关更侧重于控制流,在数据存储传输方面,对象存储更适合。


那么,有什么方法可以直接将文件等资源上传到对象存储呢?这条资源数据又如何入库呢(例如用户上传图片到相册功能,若使用传统方法,系统接收到图片之后,会将数据入库,但若是将图片直接上传到对象存储,我们如何得知这个图片是谁给我们的)?另外,将文件上传到对象存储需要写入权限,那么是将权限开发?还是使用密钥?如果是一个 Web 服务,这个密钥信息又应该存储在哪里?如何存储?


于是,就衍生出了第二种解决方法:



对象存储方法中,客户端会发起三个请求,分别是获取临时上传地址、将文件上传到 COS、获取处理结果。相比于之前的方法,这个方法会复杂一些,但是能够很好的支持二进制上传、文件资源的大小以及成本控制。


针对不同场景的的不同适用方案:


  • 场景 1: 用户上传头像功能


针对这样的场景,直接选用方案 1。


一般情况下,头像都不会很大,完全可以在客户端对图像进行一次压缩和裁剪之后,直接带着用户的参数,例如 token 等,上传到函数计算,在函数计算中将图片转存到对象存储,将图像和用户信息进行关联,并将某些结果返回给客户端。整个流程只需要一个函数,方便快捷。


  • 场景 2: 用户上传图片到相册系统中


针对这样的场景,方案 2 会更好。


如果用户是上传图片到相册,那么基本都是希望保留原图,不希望被压缩,而原图大小很可能会超过 6M,这时方案 1 就不是特别合理了。使用对象存储方法,用户可以带着图像要上传的相册以及图片名称,用户的 token 发起获取临时密钥到函数 1 中,函数 1 将用户、相册、图片以及状态(例如待上传、待处理、已处理等)等信息关联、存储,并将临时地址返回给客户端,客户端将图片上传到对象存储中,通过对象存储触发器触发函数 2,函数 2 对图像进行压缩(一般情况下,相册列表都会显示压缩图片,点到相册详情才会有完整的无损图片),并且和之前信息进行关联,修改数据状态。在用户上传图片完成之后,如果有需要,客户端就可以发起第三次请求获取图像存储/处理结果,函数 3 会查询数据库状态,在某个时间阈值内,如果数据状态是完成,则表示数据已经上传并且完成了部分处理,否则会返回对应的异常信息。


代码实例

接下来分享上面两种方法的实现过程:


函数 1,实现第一种方案,文件通过 Base64 传递到 SCF,由 SCF 转存到 COS:


def uploadToScf(event, context):    print('event', event)    print('context', context)    body = json.loads(event['body'])
# 可以通过客户端传来的token进行鉴权,只有鉴权通过才可以获得临时上传地址 # 这一部分可以按需修改,例如用户的token可以在redis获取,可以通过某些加密方法获取等 # 也可以是传来一个username和一个token,然后去数据库中找这个username对应的token是否 # 与之匹配等,这样会尽可能的提升安全性 if "key" not in body or "token" not in body or body['token'] != 'mytoken' or "key" not in body: return {"url": None}
pictureBase64 = body["picture"].split("base64,")[1] with open('/tmp/%s' % body['key'], 'wb') as f: f.write(base64.b64decode(pictureBase64)) region = os.environ.get("region") secret_id = os.environ.get("TENCENTCLOUD_SECRETID") secret_key = os.environ.get("TENCENTCLOUD_SECRETKEY") token = os.environ.get("TENCENTCLOUD_SESSIONTOKEN") config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token) client = CosS3Client(config) response = client.upload_file( Bucket=os.environ.get("bucket_name"), LocalFilePath='/tmp/%s' % body['key'], Key=body['key'], ) return { "uploaded": 1, "url": 'https://%s.cos.%s.myqcloud.com' % ( os.environ.get("bucket_name"), os.environ.get("region")) + body['key'] }

复制代码


函数 1,实现第二种方案,进行临时签名 URL 的获取:


def getPresignedUrl(event, context):    print('event', event)    print('context', context)    body = json.loads(event['body'])
# 可以通过客户端传来的token进行鉴权,只有鉴权通过才可以获得临时上传地址 # 这一部分可以按需修改,例如用户的token可以在redis获取,可以通过某些加密方法获取等 # 也可以是传来一个username和一个token,然后去数据库中找这个username对应的token是否 # 与之匹配等,这样会尽可能的提升安全性 if "key" not in body or "token" not in body or body['token'] != 'mytoken' or "key" not in body: return {"url": None}
# 初始化COS对象 region = os.environ.get("region") secret_id = os.environ.get("TENCENTCLOUD_SECRETID") secret_key = os.environ.get("TENCENTCLOUD_SECRETKEY") token = os.environ.get("TENCENTCLOUD_SESSIONTOKEN") config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token) client = CosS3Client(config)
response = client.get_presigned_url( Method='PUT', Bucket=os.environ.get('bucket_name'), Key=body['key'], Expired=30, ) return {"url": response.split("?sign=")[0], "sign": urllib.parse.unquote(response.split("?sign=")[1]), "token": os.environ.get("TENCENTCLOUD_SESSIONTOKEN")}

复制代码


HTML 页面基本实现:



HTML 部分:


<div style="width: 70%">        <div style="text-align: center">            <h3>Web端上传文件</h3>        </div>        <hr>        <div>            <p>                方案1:通过上传到SCF,进行处理再转存到COS,这种方法比较直观,但是问题是SCF从APIGW处只能接收到小于6M的数据,而且对二进制文件处理并不好。            </p>            <input type="file" name="file" id="fileScf"/>            <input type="button" onclick="UpladFileSCF()" value="上传"/>        </div>        <hr>        <div>            <p>                方案2:                直接上传到COS,流程是先从SCF获得临时地址,进行数据存储(例如将文件信息存到redis等),然后再从客户端进行上传COS,上传结束可通过COS触发器触发函数,从存储系统(例如已经存储到redis)读取到更对信息,在对图像进行处理。            </p>            <input type="file" name="file" id="fileCos"/>            <input type="button" onclick="UpladFileCOS()" value="上传"/>        </div>    </div>
复制代码


方案 1 上传部分 JS:


function UpladFileSCF() {    var oFReader = new FileReader();    oFReader.readAsDataURL(document.getElementById("fileScf").files[0]);    oFReader.onload = function (oFREvent) {        const key = Math.random().toString(36).substr(2);        var xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"))        xmlhttp.onreadystatechange = function () {            if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {                if (JSON.parse(xmlhttp.responseText)['uploaded'] == 1) {                    alert("上传成功")                }            }        }        var url = " https://service-f1zk07f3-1256773370.bj.apigw.tencentcs.com/release/upload/cos"        xmlhttp.open("POST", url, true);        xmlhttp.setRequestHeader("Content-type", "application/json");        var postData = {            picture: oFREvent.target.result,            token: 'mytoken',            key: key,        }        xmlhttp.send(JSON.stringify(postData));    }
}
复制代码


方案 2 上传部分 JS:


function doUpload(key, bodyUrl, bodySign, bodyToken) {    var fileObj = document.getElementById("fileCos").files[0];    xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"));    xmlhttp.open("PUT", bodyUrl, true);    xmlhttp.onload = function () {        console.log(xmlhttp.responseText)        if (!xmlhttp.responseText) {            alert("上传成功")        }    };    xmlhttp.setRequestHeader("Authorization", bodySign);    xmlhttp.setRequestHeader("x-cos-security-token", bodyToken);    xmlhttp.send(fileObj);}
function UpladFileCOS() { const key = Math.random().toString(36).substr(2);
var xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP")) xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { var body = JSON.parse(xmlhttp.responseText) if (body['url']) { doUpload(key, body['url'], body['sign'], body['token']) } } } var getUploadUrl = 'https://service-f1zk07f3-1256773370.bj.apigw.tencentcs.com/release/upload/presigned' xmlhttp.open("POST", getUploadUrl, true); xmlhttp.setRequestHeader("Content-type", "application/json"); xmlhttp.send(JSON.stringify({ token: 'mytoken', key: key, }));}
复制代码


这里面可以看到获取用户密钥信息的方法是 os.environ.get(“TENCENTCLOUD_SECRETID”),想要通过这种方法获取密钥信息,需要给予函数相关的角色和对角色进行相关的权限,以 Serverless Framework 为例,可以使用 tencent-cam-role,例如创建一个全局组件:


Conf:  component: "serverless-global"  inputs:    region: ap-beijing    runtime: Python3.6    role: SCF_UploadToCOSRole    bucket_name: scf-upload-1256773370
复制代码


然后创建一个增加 Role 的组件:


UploadToCOSRole:  component: "@gosls/tencent-cam-role"  inputs:    roleName: ${Conf.role}    service:      - scf.qcloud.com    policy:      policyName:        - QcloudCOSFullAccess
复制代码


接下来就是函数的创建,函数创建时需要绑定刚才的这个 role:


getUploadPresignedUrl:  component: "@gosls/tencent-scf"  inputs:    name: Upload_getUploadPresignedUrl    role: ${Conf.role}    codeUri: ./fileUploadToCos    handler: index.getPresignedUrl    runtime: ${Conf.runtime}    region: ${Conf.region}    description: 获取cos临时上传地址    memorySize: 64    timeout: 3    environment:      variables:        region: ${Conf.region}        bucket_name: ${Conf.bucket_name}
复制代码


同时将这个函数绑定 APIGW:


UploadService:  component: "@gosls/tencent-apigateway"  inputs:    region: ${Conf.region}    protocols:      - http      - https    serviceName: UploadAPI    environment: release    endpoints:      - path: /upload/cos        description: 通过SCF上传cos        method: POST        enableCORS: TRUE        function:          functionName: Upload_uploadToSCFToCOS      - path: /upload/presigned        description: 获取临时地址        method: POST        enableCORS: TRUE        function:          functionName: Upload_getUploadPresignedUrl
复制代码


另外,这个例子还需要一个 COS 存储桶来作为测试使用,由于 Web 服务可能存在跨域问题,所以需要对 COS 进行跨域设置:


SCFUploadBucket:  component: '@gosls/tencent-cos'  inputs:    bucket: ${Conf.bucket_name}    region: ${Conf.region}    cors:      - id: abc        maxAgeSeconds: '10'        allowedMethods:          - POST          - PUT        allowedOrigins:          - '*'        allowedHeaders:          - '*'
复制代码


完成之后,可以快速部署:


(venv) DFOUNDERLIU-MB0:test dfounderliu$ sls --debug
DEBUG ─ Resolving the template's static variables. DEBUG ─ Collecting components from the template. DEBUG ─ Downloading any NPM components found in the template. ... ... apis: - path: /upload/cos method: POST apiId: api-0lkhke0c - path: /upload/presigned method: POST apiId: api-b7j5ikoc
15s › uploadToSCFToCOS › done
复制代码


至此,我们完成了项目部署,可以进行测试与适用。


总结

Serverless 可以看作是一个新的技术、新的架构。我们在接触新鲜事物的时候,或多或少都要有一个适应期,如何在 Serverless 架构下上传文件,就是需要适应的部分。我们之前习惯了直接将文件上传到服务器的,但在接触 Serverless 架构之后,由于网关->函数对二进制支持和数据包大小问题,出于安全考虑,前端不方便直接放密钥信息等问题,之前简单的事情可能会变得复杂。


作者介绍:


刘宇,腾讯 Serverless 团队后台研发工程师。毕业于浙江大学,硕士研究生学历,曾在滴滴出行、腾讯科技做产品经理,本科开始有自主创业经历,是 Anycodes 在线编程的负责人(该软件累计下载量超 100 万次)。目前投身于 Serverless 架构研发,著书《Serverless 架构:从原理、设计到项目实战》,参与开发和维护多个 Serverless 组件,是活跃的 Serverless Framework 的贡献者,也曾多次公开演讲和分享 Serverless 相关技术与经验,致力于 Serverless 的落地与项目上云。


2020 年 6 月 03 日 16:464395

评论 1 条评论

发布
用户头像
框架层面支持流,就不用这么麻烦了
2020 年 06 月 04 日 10:37
回复
没有更多了
发现更多内容

怎么进大厂?166位Java工程师的大厂面试经验分享

北游学Java

Java 面试 大厂

揭秘 Amazon Go 无人商店是如何炼成的!

亚马逊云科技 (Amazon Web Services)

Amazon Route 53 Resolver 落地中国区,轻松玩转私有域名互访不是梦!| 新服务上线

亚马逊云科技 (Amazon Web Services)

官宣:恭喜 ChaosBlade 项目进入 CNCF Sandbox

阿里巴巴云原生

go 容器 云原生 k8s 监控

嵌入式程序调用函数的内部过程和机制

不脱发的程序猿

单片机 嵌入式程序 嵌入式设计

2021年5月国产数据库排行榜:“华为高斯模式”取得成功,阿里OPA持续攀升

墨天轮

数据库 dba tdsql TiDB Gauss DB

如何高效地存储与检索大规模的图谱数据?

华为云开发者社区

存储 知识图谱 检索 图结构 表结构

Amazon Glue 版本 2.0 将作业启动时间缩短了 10 倍,现已全面开放!

亚马逊云科技 (Amazon Web Services)

堪称完美!淘宝内部百亿级Java高并发系统架构设计PDF手册分享

Java架构追梦

Java 架构 高并发 淘宝网 亿级架构设计

数据采集之js自定义采集

大数据技术指南

大数据

再次荣获最受观众喜爱奖

Serverless Devs

阿里云 云原生 cncf #Serverless

Spring Cloud Bus 消息总线介绍

阿里巴巴云原生

Java 微服务 云原生 中间件 数据格式

如何做一场高质量的分享

阿里巴巴云原生

深度学习 开发者 云原生 分享

“云演唱会”也有仪式感!能检票、可转赠,爱奇艺“云票”如何重构线上购票逻辑

爱奇艺技术产品团队

智慧党建三维云展厅可视化

一只数据鲸鱼

数据可视化 智慧党建 三维可视化

Linux C/C++ 学习路线总结!助我拿下腾讯offer

赖猫

后台开发 C/C++ Linux服务器开发

更灵活的边缘云原生运维:OpenYurt 单元化部署新增 Patch 特性

阿里巴巴云原生

容器 运维 云原生 中间件 边缘计算

iMazing比iTunes好用在哪些地方

懒得勤快

为啥你写的代码总是这么复杂?

华为云开发者社区

软件 代码 代码注释 bug 复杂度

限流与Guava RateLimiter原理解析

千珏

Java 微服务 限流算法 Guava 令牌桶

STM32电源框图解析(VDD、VSS、VDDA、VSSA、VREF+、VREF-、VBAT等的区别)

不脱发的程序猿

嵌入式 stm32 单片机 电源框图解析

CampusBulider(模模搭)学习笔记5:创建自定义建筑

森友小锘

前端 可视化 3D 3D可视化 数字孪生

MapReduce排序以及序列化

五分钟学大数据

大数据 hadoop mapreduce

Nginx负载均衡配置误区

运维研习社

nginx 负载均衡 5月日更

anyRTC 六周年 打造全网最低音视频价格

anyRTC开发者

音视频 WebRTC RTC sdk

源码解析之Seata项目中的分布式ID生成算法

Coder的技术之路

分布式 分布式ID

HuskyLens人工智能摄像头

不脱发的程序猿

人工智能 智能硬件 AIOT HuskyLens 人工智能摄像头

论好文章和烂文章

阿里巴巴云原生

程序员 开发者 云原生 写作技巧 成长与思考

关于组件,你真的了解么?

架构精进之路

组件化 5月日更

华为云PB级数据库GaussDB(for Redis)揭秘第十期:GaussDB(for Redis)迁移系列(上)

华为云开发者社区

数据仓库 华为云 数据迁移 GaussDB(for Redis) PB级数据库

云图说|不要小看不起眼的日志,“小日志,大作用”

华为云开发者社区

运维 日志 云日志服务 安全监控审计

如何在 Serverless 架构下优雅上传文件?-InfoQ