【最不佳实践】文件上传并不简单

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

传统文件上传

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

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

Serverless架构下文件上传

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

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

所以,传统框架中常用的上传方案是不太适合在Serverless架构中直接使用的,在Serverless架构上传文件的方法通常有两种:

  • 一种是BASE64后上传,持久化到对象存储或者是NAS中,这种做法可能会触及到API网关与FaaS平台之间传递的数据包有大小限制,所以一般使用这种上传方法的通常是上传头像头像等小文件的业务场景;
  • 第二种上传方法是通过对象存储等平台来上传,因为客户端直接通过密钥等信息来将文件直传到对象存储是有一定风险的,所以通常情况是客户端发起上传请求,函数计算根据请求内容进行预签名操作,并将预签名地址返回给客户端,客户端再使用指定的方法进行上传,上传完成之后,可以通过对象存储触发器等来对上传结果进行更新等:

【最不佳实践】文件上传并不简单

以阿里云函数计算为例,针对上述两种常见的上传方法通过Python Web框架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['objectSignUrlTimeOut'])
getSignUrl = lambda object: getUrl(object, "GET")
putSignUrl = lambda object: getUrl(object, "PUT")

# 获取随机字符串
randomStr = lambda len: "".join(random.sample('abcdefghijklqrstuvwxyz123456789ABCDEFGZSA' * 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 ActiveXObject("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 ActiveXObject("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 Devs 快速创建函数计算应用(二)


下一篇:阿里云函数计算组件感知线上“异动”:让发布更安全