如何在 Serverless 架构下优雅上传文件?
bigegpt 2024-09-22 00:42 3 浏览
传统开发中,文件上传是比较自由的:上传什么文件、怎么上传、存储到哪里等问题往往都是由开发者决定的,但是在 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 的落地与项目上云。
相关推荐
- Dify「模板转换」节点终极指南:动态文本生成进阶技巧(附代码)Jinja2引擎解析
-
这篇文章是关于Dify「模板转换」节点的终极指南,解析了基于Jinja2模板引擎的动态文本生成技巧,涵盖多源文本整合、知识检索结构化、动态API构建及个性化内容生成等六大应用场景,助力开发者高效利用模...
- 我用C#造了个AI程序员:自动调试+重构代码实战
-
在软件开发的世界里,调试和重构代码往往占据了程序员大量的时间。我一直梦想着能有一个智能助手,帮我处理这些繁琐的工作。于是,我决定用C#打造一个AI程序员,让它具备自动调试和重构代码的能力。系统架构设计...
- 公文自动排版vba代码(公文自动排版vba代码)
-
Sub公文自动排版()'设置页面参数(单位:厘米)WithActiveDocument.PageSetup.TopMargin=CentimetersToPoints(3.7)...
- Anthropic最强代码神器:Claude Code系统提示词
-
最近,在融合Opus-4之后,ClaudeCode的整体能力直线飙升.甚至一度把曾经的最强开发工具——Cursor打的抬不起头来。无论是代码生成的准确度,还是智能补全的丝滑体验,都让人印象深...
- 使用 Ruff 进行 Python 代码格式化与静态检查
-
随着Python项目的规模增大,保持一致的代码风格和高质量的代码变得尤为重要。Ruff是一个现代、高性能、支持lint和格式化的Python工具,能帮助你快速发现并修复常见代码问题。本文...
- 基础语法篇:格式化输出 含完整示例代码
-
所谓格式化输出就是按照一定格式来输出对应的内容,在Python的语法中格式化输出包含两种:格式化符号、格式化字符串一、格式化符号常用的格式化符号包括%s(将内容转换为字符串,放入占位位置)、%d(将内...
- 代码整洁如诗!Keil 插件上线,一键格式化代码,告别风格混乱!
-
引言:代码格式不统一?你的团队还在为“括号位置”吵架吗?嵌入式开发者们,你是否经历过这些抓狂瞬间?代码风格“百花齐放”:同事的代码缩进用空格,你的用Tab,合并时冲突频发!手动调整耗时费力:为了通过C...
- [信捷PLC] 信捷PLC之C函数编程(一)
-
前言写PLC程序,越来越觉得结构化文本编程语言(ST)给PC编程带来的便利,在处理一些数据上,可以写的更加灵活。所以,在项目PLC选型上,我都会优先选择支持结构化文本的PLC。国内有些厂商推出了一些较...
- C语言-HelloWorld解析(c语言的helloworld怎么写)
-
使用VisualStudio2017开发工具新创建一个项目,编写第一个C语言程序。#include<stdio.h>voidmain(){printf("HelloW...
- VSCode 配置 C++ 开发环境!教程详解
-
第一步、安装VSCode应用程序打开VSCode官网,下载对应安装包并默认安装(这里指明:安装路径可以修改)第二步、安装相关插件此时的VSCode仅仅是一个英文文本编辑器,还称不上开发工具,所以需要...
- C语言进阶教程:C语言与汇编语言交互
-
C语言和汇编语言的交互是底层编程和性能优化中的一个重要方面。理解它们如何协同工作,可以帮助开发者更好地控制硬件、优化关键代码段以及理解编译器的行为。为什么需要在C语言中嵌入汇编?尽管C语言已经提供了相...
- C语言如何处理平台相关代码(c语言的开发平台)
-
在进行跨平台C编程时,不可避免地会遇到需要针对不同操作系统或硬件架构编写特定代码的情况。C语言通过预处理器指令,特别是条件编译指令,为我们提供了处理平台相关代码的有效机制。最常用的就是利用预定义的宏(...
- C语言:hello world(c语言helloworld代码)
-
环境:a.初学者建议用“啊哈C”,这款软件简单易装;b.devc.visualstdiod.Vc6.0第一行代码:#include<stdio.h>#<stdio.h&g...
- C语言之编译器集合(编写c语言编译器)
-
C语言有多种不同的编译器,以下是常见的编译工具及其特点:一、主流C语言编译器1.GCC(GNUCompilerCollection)特点:开源、跨平台,支持多种语言(C、C++、Fortran...
- 适合零基础初学者学习C语言第一课教程,揭开C语言的神秘面纱
-
一、C语言简介我刚接触编程,首先想要学习的就是C语言,这次我就把我的感悟用我自己理解的文字表述出来,这样对刚学C语言的人来说,才是比较友好的。因为我们都没有C语言的基础,不懂啥是编程,啥事代码。我们...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)