尺寸很重要 - 使用 Lambda 和 S3 进行图像压缩
如果你遇到一位开发者说存储空间大小无关紧要,那你肯定会觉得他肯定有一个相当可观的云预算!但对其他人来说,存储空间大小绝对至关重要,尤其是在云端存储图像时。
过去几年我参与开发的几乎所有Web应用程序都需要某种形式的图片托管,无论是简单的图片库还是用户头像。如今云存储选项丰富,存储海量数据的成本也很低,因此我们大多数人很容易忽略将数据托管在云端的种种顾虑。然而,在估算云存储预算时,我们很容易忘记,我们不仅需要为存储在云端的全部数据付费,还需要为每次数据离开云端付费。
假设我们有一个应用程序,允许用户上传照片作为个人资料头像。用户拿起手机,找到一张适合发到 Instagram 或 Tinder 上的最新照片,然后上传到我们的服务器。假设他们上传的图片质量不错,4mb大小也合适。现在,因为我们的应用超级棒,它开始爆红,日活跃用户达到了 1 万左右。真不错!
现在假设我们10,000名用户每人上传一张4MB的头像。那么,我们需要在云存储空间中存储40GB的头像。考虑到AWS等云服务商每GB的收费约为0.025澳元,这不算太糟糕,我们完全可以应付。但是请记住,我们有10,000名日活跃用户,他们每次访问我们的应用时,都会将一个或多个其他用户的头像加载到他们的动态消息中。这意味着我们的应用每天至少会消耗40GB的数据——每月1200GB!
这很快就会变得非常昂贵!
图像压缩来帮忙啦!
幸运的是,我们生活在一个图像压缩和优化轻而易举的时代,我们可以轻松地将用户臃肿的 4MB 头像压缩到几 KB,从而生成更适合网页显示的图片。接下来,我将向您展示如何使用几个 S3 存储桶和一个 AWS Lambda 函数,快速为您的应用程序搭建一个简洁的图像压缩流程。
我们的通用处理流程大致如下。一端是一个应用程序,允许用户将个人资料图片上传到 S3 存储桶。该存储桶仅作为用户上传的全分辨率图片的暂存区。我们为 S3 存储桶设置了一个触发器,用于通知 Lambda 函数有新图片到达并准备压缩。Lambda 函数随后从源存储桶下载文件,并使用 Node.js Sharp 包将其缩小到更合适的 200x200 像素头像尺寸。Lambda 函数将转换后的图片保存到第二个 S3 存储桶,这样我们的应用程序用户就可以读取压缩后的图片,从而节省大量数据传输费用。
为什么要用两个桶?
完全可以只用一个存储桶。但我个人更倾向于使用两个存储桶,以此来降低某些危险且开销极大的递归事件循环的风险。如下图所示,使用一个 S3 存储桶时,用户上传图片到该存储桶。该存储桶会向我们的 Lambda 函数发送通知,要求其压缩图片。Lambda 函数执行完毕后,图片会被保存回存储桶。这反过来又会触发另一个通知,表明有新图片已上传到存储桶,从而再次触发 Lambda 函数……如此循环往复。
你明白我的意思。我们可能会陷入递归压缩图像的循环,而(以我的经验来说)这是一个代价高昂的错误(感兴趣的人可以算算,每天大约要损失 700 澳元!)。
如果你确实想使用单桶架构,可以通过巧妙地处理用于 S3 事件触发器的对象前缀,或者使用元数据描述符来帮助识别应该处理哪些对象,来降低这种风险。但我所知的最安全的方法是使用两个完全独立的桶,其中一个桶发出事件来压缩图像,另一个桶则接收压缩后的文件。因此,我将演示这种方法。
构建图像压缩管道
为了让应用程序的安装和拆卸更加便捷快速,我使用了AWS SAM来整合所有内容。借助 SAM,我们可以使用简洁的 YAML 模板和 SAM CLI 工具来定义和部署 AWS 资源。如果您是 AWS SAM 的新手,我建议您在继续操作之前,先花些时间了解一下它的功能。
1. 创建一个新的 SAM 项目
首先,我们将创建一个新的 SAM 项目。假设您已安装 SAM CLI 工具,那么我们可以从命令行运行以下命令。
sam init
在逐步检查初始化选项的过程中,我为我的项目配置使用了以下选项。
Which template source would you like to use?
1 - AWS Quick Start Template
What package type would you like to use?
1 - Zip (artifact is a zip uploaded to S3)
Which runtime would you like to use?
1 - nodejs14.x
Project name [sam-app]: sizematters
2. 定义 SAM 模板 template.yaml
SAM 初始化项目后,我们可以进入项目目录并进行设置和自定义template.yaml。此模板包含我们将传递给 SAM 的所有逻辑,AWS CloudFormation用于设置和配置 S3 存储桶和 Lambda 函数,以及配置事件通知S3。
我们最终的模板看起来会像这样。
# <rootDir>/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Size Matters image compression pipeline
Parameters:
UncompressedBucketName:
Type: String
Description: "Bucket for storing full resolution images"
CompressedBucketName:
Type: String
Description: "Bucket for storing compressed images"
Resources:
UncompressedBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref UncompressedBucketName
CompressedBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref CompressedBucketName
ImageCompressorLambda:
Type: AWS::Serverless::Function
Properties:
Handler: src/index.handler
Runtime: nodejs14.x
MemorySize: 1536
Timeout: 60
Environment:
Variables:
UNCOMPRESSED_BUCKET: !Ref UncompressedBucketName
COMPRESSED_BUCKET: !Ref CompressedBucketName
Policies:
- S3ReadPolicy:
BucketName: !Ref UncompressedBucketName
- S3WritePolicy:
BucketName: !Ref CompressedBucketName
Events:
CompressImageEvent:
Type: S3
Properties:
Bucket: !Ref UncompressedBucket
Events: s3:ObjectCreated:*
从上到下template.yaml依次是我们的Parameters模块。这些参数允许我们在部署 SAM 模板时为 S3 存储桶传入一些名称。
接下来是我们的Resources模块。首先提到的两个资源是我们将要创建的两个 S3 存储桶,分别命名为 `<bucket_name>`UncompressedBucket和 ` CompressedBucket<bucket_name>`。一个存储桶将用作图片上传的存放位置,另一个存储桶用于存放压缩后的图片输出。这两个存储桶的名称都根据我们之前定义的参数进行设置。
接下来,在我们的Resources代码块中,我们定义了 Lambda 函数ImageCompressorLambda。该函数将使用 Node.js 运行时环境,并且我已经将 Lambda 处理程序指向了目标位置。为了简化 Lambda 函数逻辑的编写,src/index.hanlder我们在代码块中传入了几个环境变量,分别指向之前定义的两个 S3 存储桶。此外,我还在该代码块下添加了几个 SAM 辅助策略,赋予 Lambda 函数从“未压缩图像”存储桶读取数据以及向“压缩图像”存储桶写入数据的相应权限。EnvironmentPolicies
最后,我们可以配置 Lambda 函数的事件触发器。此模板中使用的事件结构设置为在UncompressedS3 存储桶中创建对象时触发。您可以根据需要添加其他规则和逻辑,以便仅针对特定文件类型或对象键前缀/后缀触发事件。但为了演示的简洁性,我将其设置为处理所有路径下的所有文件。
3. 将 Sharp 添加为 Lambda 的依赖项
为了完成繁重的图像压缩和处理工作,我们将使用Node.js 的 Sharp包。这是一个功能非常强大的库,我们只会用到其中的一小部分来缩小图像大小。但我鼓励您仔细阅读其文档,了解它提供的所有功能。
要设置我们的 Lambda 函数,首先需要添加 Sharpsharp作为依赖项。查看Sharp 团队提供的文档node_modules,我们可以看到,为了在 AWS Lambda 上运行 Sharp,我们需要确保项目目录中的二进制文件是针对 Linux x64 平台的。根据安装软件包的操作系统不同,可能会加载一些不兼容的二进制文件。因此,要sharp为我们的 Lambda 函数安装 Sharp,我们可以从项目目录运行以下命令。
# windows users
rmdir /s /q node_modules/sharp
npm install --arch=x64 --platform=linux sharp
# mac users
rm -rf node_modules/sharp
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux sharp
简而言之,这将从我们的 node_modules 中彻底删除 Sharp(如果它存在),并提供一个专用于 Linux x64 系统的安装,最适合 AWS Lambda。
4. 设置 Lambda 逻辑
安装完成后sharp,我们就可以配置 Lambda 逻辑了。之前template.yaml我们定义过 Lambda 处理程序,并将其指定在 `<path>` src/index.handler。因此,在项目src文件夹中,我们创建一个 ` index.js<file>` 文件。然后,我们可以使用以下代码片段来构建函数逻辑。
// src/index.js
const AWS = require('aws-sdk');
const S3 = new AWS.S3();
const sharp = require('sharp');
exports.handler = async (event) => {
// Collect the object key from the S3 event record
const { key } = event.Records[0].s3.object;
console.log({ triggerObject: key });
// Collect the full resolution image from s3 using the object key
const uncompressedImage = await S3.getObject({
Bucket: process.env.UNCOMPRESSED_BUCKET,
Key: key,
}).promise();
// Compress the image to a 200x200 avatar square as a buffer, without stretching
const compressedImageBuffer = await sharp(uncompressedImage.Body)
.resize({
width: 200,
height: 200,
fit: 'cover'
})
.toBuffer();
// Upload the compressed image buffer to the Compressed Images bucket
await S3.putObject({
Bucket: process.env.COMPRESSED_BUCKET,
Key: key,
Body: compressedImageBuffer,
ContentType: "image"
}).promise();
console.log(`Compressing ${key} complete!`)
}
AWS-SDK逐步讲解,我们首先需要在 `<module> `、S3`<module>` 和 `<module> ` 包中引入必要的组件sharp。我们还定义了通用的 lambda 处理函数,并将要操作的事件作为参数传入。
// <rootDir>/src/index.js
const AWS = require('aws-sdk');
const S3 = new AWS.S3();
const sharp = require('sharp');
exports.handler = async (event) => {
...
}
接下来,我们可以从触发 lambda 执行的事件中提取图像对象键。
// <rootDir>/src/index.js
const { key } = event.Records[0].s3.object;
使用 AWS S3 SDK,我们可以利用之前收集的信息将镜像下载到 Lambda 函数中key。请注意,由于我们之前在template.yamlLambda 函数中定义了环境变量,因此我们可以使用process.env.UNCOMPRESSED_BUCKET该环境变量来引用未压缩存储桶的名称。
// <rootDir>/src/index.js
const uncompressedImage = await S3.getObject({
Bucket: process.env.UNCOMPRESSED_BUCKET,
Key: key,
}).promise();
现在,有了下载的图像结果,我们可以将缓冲区数据传递给sharp`sharping` 函数。这里我们只是对图像进行一些非常简单的调整。我们将源图像缩小到 200x200 的正方形,而不会拉伸图像的任何维度,从而生成一张适合网页的头像图片。你还可以做更多操作,例如更改压缩级别或文件类型。但在这个例子中,我们仍然保持简单。
// <rootDir>/src/index.js
const compressedImageBuffer = await sharp(uncompressedImage.Body)
.resize({
width: 200,
height: 200,
fit: 'cover'
})
.toBuffer();
然后,利用转换后的图像sharp,我们可以获取响应缓冲区并将其保存到压缩存储桶中。由于我们要将其上传到第二个存储桶,因此我只需使用完全相同的键将文件保存到相同的相对位置即可。所以无需担心会覆盖原始文件。
// <rootDir>/src/index.js
await S3.putObject({
Bucket: process.env.COMPRESSED_BUCKET,
Key: key,
Body: compressedImageBuffer,
ContentType: "image"
}).promise();
所有部件都准备就绪,现在是时候构建和部署我们的管道了!
5. 构建和部署
要从命令行构建项目,请运行
sam build --use-container
这将检查您的template.yaml内容是否有效,并准备好要上传的 lambda 函数资源。
完成后,我们就可以运行以下命令将我们的构建推送到 AWS。
sam deploy --guided
按照引导式部署选项的步骤,我们可以指定应用程序堆栈名称、区域以及我们在配置文件中定义的参数template.yaml。
Setting default arguments for 'sam deploy'
=========================================
Stack Name [<your-stack-name>]:
AWS Region [<your-aws-region>]:
Parameter UncompressedBucketName []:
Parameter CompressedBucketName []:
如果一切按计划进行,您应该能够登录到控制台并看到两个新的存储桶已经创建,并且您的 Lambda 函数已准备好开始处理这些图像大小!
6. 测试一下
测试我们新的图像压缩管道最简单的方法是登录您的 AWS 控制台,并将图像文件上传到您的Uncompressed存储桶。这将触发通知事件,向我们的 Lambda 函数发送压缩图像的指令。如果一切顺利,您应该能够检查您的Compressed存储桶,并看到压缩文件已创建。
我做了一个简单的测试,结果显示,上传一张 3MB 的全尺寸图片后,我们能够将其缩小到不到 10KB。太棒了!
概要
回到我们的应用示例。假设我们这款优秀的应用每天有 1 万活跃用户,并且现在还配备了完善的图片压缩和优化流程,那么一年下来,用户上传的图片总量仍然会达到 40GB。但是,通过将图片压缩到更合理的 10KB 甚至更小的尺寸,我们现在可以大幅降低数据流量消耗,将每天的潜在数据流量从 40GB 降至 100MB 左右!这相当于数据流量减少了 400%!所以,我认为,图片大小当然很重要!
封面照片由Galen Crout拍摄,来自Unsplash
文章来源:https://dev.to/aarongarvey/size-matters-image-compression-with-lambda-and-s3-40bf



