【AWS征文】使用 AWS Serverless 架构动态调整图片大小

JokerCch 2020-08-17

一、痛点

遇到的一些问题

图片是一个网站提升用户体验必不可少的一部分,但是更多并不是最好。随手拍的照片,没有任何加工的照片可能不需要花费太多的精力就可以集成到用户界面中,但是这些大尺寸、高分辨率的图片会降低整个网页的下载速度,并且这些高分辨率并不能增加多少用户体验。

在移动互联网如此发达的今天,几乎人手一部手机,假如你运行了一个新闻站点,绝大部分用户都在他们的手机上浏览你的网站,他们并不需要高分辨率的图片,高分辨率对他们的显示效果提升了不少,反而会影响加载速度。但是,还有一部分用户使用桌面电脑阅读,网络更好,屏幕也更大更好,高分辨率的图片会提升他们的视觉体验。

从用户体验角度来看,正确的做法是根据用户的使用设备提供不同尺寸的图片。但是我们并不能覆盖所有尺寸的设备,新尺寸设备可能在不断被制造出,并且事先调整图形大小以适应任何想到的屏幕尺寸几乎是不可能的,并且存储所有预先生成所有可能大小的图片会花费巨额的费用,这并不是一个好办法。

一个比较好的办法就是,我们在第一次请求时创建每个尺寸的图形,然后将其保存以备后用。这样,每个设备都可以得到正确的图像大小,我们也可以节省大量的存储成本和计算成本。

因为我们无法预测用户的请求行为,哪次请求需要生成新尺寸的图片,如果准备一台服务器专门处理生成新尺寸图片,可能利用率并不会很高,在请求高峰时期,单台机器的资源也有可能不够。这时候 Serverless 就非常适合了,你只需为你使用的计算付费,并且无服务器应用程序已经设计为自动伸缩以满足用户需求,因此不需要预先准备大量服务器,可以进一步降低成本。即使当用户请求到一个新尺寸照片的时候,应用需要生成新尺寸照片,这个图片大小小图由一个服务器函数完成,计算成本也会低得多。

Serverless Framework

本文我们主要使用 Serverless Framework 和 Python 来构建一个自动调整图像大小的系统,那么 Serverless Framework 是什么呢,我们看一下官方关于 AWS 部分的介绍:

The Serverless Framework helps you develop and deploy your AWS Lambda functions, along with the AWS infrastructure resources they require. It‘s a CLI that offers structure, automation and best practices out-of-the-box, allowing you to focus on building sophisticated, event-driven, serverless architectures, comprised of Functions and Events.

我们会使用 S3 作为我们的图片存储,S3 是 AWS 云中的一个对象存储,在 S3 中存储图片是一种简单、可伸缩的方法,本文我们就是让每个请求生成它所需要大小的图片,然后将结果存储在 S3 中。

当下次有人请求相同的图片时,将会发生以下两种情况之一:如果已经存在该大小的图片时,那么相应的 S3 URI 直接为我们提供先前存储的图片。但是如何我们还没有这个尺寸的图片,S3 会触发函数生产该尺寸的图片然后返回给我们,同时也会把这个图片保存在云中以备将来使用。

这就是我们所说的“智能”调整大小系统,我们让用户请求他们实际需要的大小的图片,而不是为图片请求的每个可能结果做准备。

有关无服务器框架的介绍与安装,请参照官方文档:

https://www.serverless.com/framework/docs/providers/aws/guide/quick-start/

wangzan:~ $ serverless -v
Framework Core: 1.78.1
Plugin: 3.7.0
SDK: 2.3.1
Components: 2.34.3

二、使用 Serverless 构建无服务器应用程序

创建示例

开始我们可能没有使用 serverless 创建过应用程序,我们这里在 AWS 云环境中创建一个 Hello world 函数演示一下。

  1. 创建一个服务

创建好之后,目录中会有 Lambda 的运行文件handler.py,还有一个重要的文件是serverless.yml

sls create --template aws-python --path myService
  1. 部署

将会基于创建的serverless.yml来吧函数部署到 Lambda 中。

wangzan:~/environment/myService $ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service myservice.zip file to S3 (390 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............
Serverless: Stack update finished...
Service Information
service: myservice
stage: dev
region: us-east-1
stack: myservice-dev
resources: 6
api keys:
  None
endpoints:
  None
functions:
  hello: myservice-dev-hello
layers:
  None

**************************************************************************************************************************************
Serverless: Announcing Metrics, CI/CD, Secrets and more built into Serverless Framework. Run "serverless login" to activate for free..
**************************************************************************************************************************************
  1. 调用部署的函数

测试一下函数调用,是否返回预期结果。

wangzan:~/environment/myService $ sls invoke -f hello
{
    "body": "{\"input\": {}, \"message\": \"Go Serverless v1.0! Your function executed successfully!\"}",
    "statusCode": 200
}

可以看到,通过 serverless 创建部署一个 Lmabda 函数变得很简单,比较重要的就是serverless.yml这个文件,我们简单看下里面的内容:

service: myservice

provider:
  name: aws
  runtime: python2.7

functions:
  hello:
    handler: handler.hello

创建 serverless.yml

我们将一步步来制作 serverless.yml,它包含了函数所需的所有东西。

首先,我们指定服务的名称,运行环境和位置,并授予未来函数访问 S3 的权限:

service: wzlinux-image-resizing

provider:
  name: aws
  runtime: python2.7
  region: us-east-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObject
      Resource: ‘arn:aws:s3:::wzlinux-resized-images/*‘

Resource 的桶名,大家修改为自己的桶名即可。

接下来我们说一说函数部分以及其参数:

functions:
  resize:
    handler: handler.call
    environment:
      BUCKET: wzlinux-resized-images
      REGION: us-east-1
    events:
      - http:
          path: /{size}/{image}
          method: get

我们还需要一个 S3 存储桶资源,用来存储图片:

Resources:
    ResizedImages:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: wzlinux-resized-images

这就是serverless.yml重要组成部分,下面我贴出 Github 上完整代码:

https://github.com/wangzan18/serverless-image-resizing/raw/master/serverless.yml

创建图片处理函数

为了实现 python 可以处理图片,我们需要引入哪些模块呢?

import json
import datetime
import boto3
import PIL
from PIL import Image
from io import BytesIO
import os
  • json 和 datatime 模块是 python 的内置模块,不需要单独安装。
  • boto3 是 python 用来操作 AWS 中资源的模块。
  • PIL 是 python 处理图片的模块。
  • io 是将函数文件流式。
  • os 可以用来获取环境变量,比如 os.environ。

下面让我们看看文件中最外层函数。

def call(event, context):
    key = event["pathParameters"]["image"]
    size = event["pathParameters"]["size"]

    result_url = resize_image(os.environ["BUCKET"], key, size)

    response = {
        "statusCode": 301,
        "body": "",
        "headers": {
            "location": result_url
        }
    }

    return response

这个函数在用户在请求新尺寸图片的时候被调用,从传入的 URI 中解析出图像和大小属性,然后调用另外一个函数 resize_image 对其进行处理,生成用户需要的新尺寸,最后 301 重定向到新的地址返回给用户。

下面就让我们看看这个 resize_image 函数是如何实现的:

def resize_image(bucket_name, key, size):
    size_split = size.split(‘x‘)
    s3 = boto3.resource(‘s3‘)
    # 从 S3 中获取图像,并写入到变量中
    obj = s3.Object(
        bucket_name=bucket_name,
        key=key,
    )
    obj_body = obj.get()[‘Body‘].read()

    # 读取图像并调整到新的大小
    img = Image.open(BytesIO(obj_body))
    img = img.resize(
        (int(size_split[0]), int(size_split[1])), PIL.Image.ANTIALIAS
    )
    buffer = BytesIO()
    img.save(buffer, ‘JPEG‘)
    buffer.seek(0)

    # 将调整好大小的图片上传会 S3
    resized_key="{size}_{key}".format(size=size, key=key)
    obj = s3.Object(
        bucket_name=bucket_name,
        key=resized_key,
    )
    obj.put(Body=buffer, ContentType=‘image/jpeg‘)

    # 返回调整大小图片的 URL
    return resized_image_url(
        resized_key, bucket_name, os.environ["AWS_REGION"]
    )

调整大小图片的 URL 是单独放在一个函数中的,如下:

def resized_image_url(resized_key, bucket, region):
    return "http://{bucket}.s3.{region}.amazonaws.com/{resized_key}".format(bucket=bucket, region=region, resized_key=resized_key)

至此,我们图片调整的函数写好了,它会用外部函数来调整图片的大小,并执行 301 重定向到新的位置,代码已经提交都了 Github,下面地址可以看到完整代码:

https://github.com/wangzan18/serverless-image-resizing/raw/master/handler.py

因为函数还有一些依赖的模块,我们把模块写在文件requirements.txt里面,一行一个模块。

boto3
Pillow

部署图片调整函数

为了部署函数,我们需要获取 AWS 的凭证,并且具有访问 AWS Lambda、S3、IAM 和 API Gateway 的权限,我这边配置好了 awscli。

为了确保我们 python 依赖包在生产中可以正常引用,我们使用了 serverless-python-requirements 插件。它将确保 python 依赖项将被正确打包到 Lambda 环境中。

因此,为了部署我们的函数,我们需要运行sls deploy。在部署完成之后,将会输出 API Gateway 上函数的 URL,它看起来像这样:

https://XXXXX.execute-api.eu-west-1.amazonaws.com

开始部署

wangzan:~/environment/serverless-image-resizing (master) $ sls deploy
Serverless: Generated requirements from /home/ec2-user/environment/serverless-image-resizing/requirements.txt in /home/ec2-user/environment/serverless-image-resizing/.serverless/requirements.txt...
Serverless: Using static cache of requirements found at /home/ec2-user/.cache/serverless-python-requirements/780f69be60ef16a7f6639a5336b5f5e85188a84f8a321df2307c90d24883f346_slspyc ...
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Injecting required Python packages to package...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service wzlinx-image-resizing.zip file to S3 (10.74 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
....................................
Serverless: Stack update finished...
Service Information
service: wzlinx-image-resizing
stage: dev
region: us-east-1
stack: wzlinx-image-resizing-dev
resources: 13
api keys:
  None
endpoints:
  GET - https://yj0u7v8vh7.execute-api.us-east-1.amazonaws.com/dev/{size}/{image}
functions:
  resize: wzlinx-image-resizing-dev-resize
layers:
  None

配置 S3 存储桶

为了使 S3 和我们的无服务器函数配合工作,我们需要对 S3 做以下配置:

  • 把 S3 存储桶变为静态站点托管
  • 添加一个重定向规则
  • 配置公开策略,打开对象公开访问权限

【AWS征文】使用 AWS Serverless 架构动态调整图片大小

重定向规则如下,当请求的图片尺寸不存在的时候,帮我们重定向到 S3 的地址。

<RoutingRules>
  <RoutingRule>
    <Condition>
      <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
    </Condition>
    <Redirect>
      <Protocol>https</Protocol>
      <HostName>yj0u7v8vh7.execute-api.us-east-1.amazonaws.com</HostName>
      <ReplaceKeyPrefixWith>dev/</ReplaceKeyPrefixWith>
      <HttpRedirectCode>307</HttpRedirectCode>
    </Redirect>
  </RoutingRule>
</RoutingRules>

存储桶的配置策略如下

{
  "Id": "Policy1597386227420",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1597386225957",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::wzlinux-resized-images/*",
      "Principal": "*"
    }
  ]
}

验证效果

  1. 我先上传一张照片到 S3 存储桶。

【AWS征文】使用 AWS Serverless 架构动态调整图片大小

打开效果如下:

【AWS征文】使用 AWS Serverless 架构动态调整图片大小

  1. 我们去访问一个 200x200 的大小,看看效果,地址如下
https://yj0u7v8vh7.execute-api.us-east-1.amazonaws.com/dev/200x200/sls.jpg

【AWS征文】使用 AWS Serverless 架构动态调整图片大小

我们可以看到裁切的很小了,然后再去看下 S3 中图片的大小:

【AWS征文】使用 AWS Serverless 架构动态调整图片大小

可以看到图片只有 8.8 KB 小了,实现了我们上面所说的功能,自动进行裁切并展示给用户。

三、总结

在本文中,我们介绍了如何使用 Python 和 Serverless 框架设置动态图像调整 API。

图像调整大小是无服务器的一个很好的用例。当使用无服务器实现时,图像的大小调整可以有效地随负载而缩放。这个函数将只使用它需要的计算来快速调整图像的大小,如果没有调整大小的请求,就不会浪费计算时间。S3 和无服务器功能的解决方案还提供了一个非常简单的架构,减少了使用需要维护的服务器,因此确保了系统的稳定性。

从工作流自动化和事件流到移动应用程序和日志处理的后端,还有许多其他用例可以从 Serverless 中受益。

如果您想了解 Serverless,可以从无服务器文档开始,并查看使用无服务器框架的 AWS 介绍。

参考地址:https://github.com/wangzan18/serverless-image-resizing

欢迎大家扫码关注,获取更多信息

【AWS征文】使用 AWS Serverless 架构动态调整图片大小

相关推荐