如何在不先下载的情况下在 AWS Lambda 函数中查找 S3 文件?

How to lookup to S3 file in AWS Lambda function without download it first?

我是 AWS Lambda 的新手,我正在尝试创建一个将由 S3 put 事件调用的 lambda 函数,对传入数据应用一些业务逻辑,然后加载到目标中。

例如,在源 s3 存储桶中创建的新文件 (contactemail.json) 包含 2 个组件:电子邮件和域。同一个 s3 存储桶中还有另一个持久文件 (lkp.json),其中包含所有免费电子邮件域的列表(例如 gmail.com)。 lambda 函数读取 contactemail.json 文件,根据域查找 lkp.json 文件。如果 contactemail.json 中的域存在于 lkp.json 中,将整个电子邮件地址放入 contactemail.json 文件中的新组件 (newdomain),然后将输出上传到目标 s3 存储桶。

以下是我的代码。它确实有效,但是,如您所见,我在执行查找之前使用 s3_client.download_file 下载 lkp.json 文件。

我担心的是,如果查找文件太大,下载过程可能会花费太长时间,导致 lambda 函数超时。

是否有 better/smarter 无需将查找文件从 s3 下载到 lambda 即可进行查找的方法?

from __future__ import print_function
import boto3
import os
import sys
import uuid
import json

s3_client = boto3.client('s3')

def handler(event, context):

#get source details from event    
    for record in event['Records']:
        sourcebucket = record['s3']['bucket']['name']
        sourcekey = record['s3']['object']['key'] 
        sourcefilename = sourcekey[sourcekey.rfind('/')+1:]
        lookupkey = 'json/contact/lkp/lkp.json' 
        lookupfilename = 'lkp.json'         

#set target based on source value         
        targetbucket = sourcebucket + 'resized'
        targetkey = sourcekey         
        targetfilename = sourcefilename

#set download and upload path in lambda
        download_path = '/tmp/{}'.format(uuid.uuid4())+sourcefilename
        download_path_lkp = '/tmp/{}'.format(uuid.uuid4())+lookupfilename
        upload_path = '/tmp/{}'.format(uuid.uuid4())+targetfilename

#download source and lookup        
        s3_client.download_file(sourcebucket, sourcekey, download_path)
        s3_client.download_file(sourcebucket, lookupkey, download_path_lkp)

    #if not os.path.exists(upload_path):
       # open(upload_path, 'w').close()

        targetfile = open(upload_path, 'w')
        sourcefile = json.loads(open(download_path).read())
        lookupfile = json.loads(open(download_path_lkp).read())

        lookuplist = []

        for row in lookupfile:
            lookuplist.append(row["domain"])

        targetfile.write('[')
        firstrow = True
        for row in sourcefile:
            email = row["email"]
            emaildomain = email[email.rfind('@')+1:]
            if (emaildomain in lookuplist):
                row["newdomain"]=email
            else:
                row["newdomain"]=emaildomain        
            if (firstrow==False):
                targetfile.write(',\n')
            else:
                firstrow=False     
            json.dump(row, targetfile)
        targetfile.write(']')

        targetfile.close()

#upload to target        
        s3_client.upload_file(upload_path, targetbucket, targetkey)

简单地说,S3 不是用于此目的的正确服务。

  • 如果不下载存储在 S3 中的对象,则无法查看内部对象。¹

  • 对象是 S3 中的原子实体——S3 理解没有比对象更小的东西,例如对象内部的 "record"。

  • 也不可能将数据附加到 S3 中的对象。您必须下载、修改并再次上传,如果有多个进程并行尝试此操作,则至少有一个进程会悄无声息地丢失数据,因为无法锁定 S3 对象 FOR UPDATE (有点 SQL 术语)。第二个进程读取原始对象,对其进行修改,然后继续覆盖第一个进程在第二个进程读取对象后立即保存的更改。

作为一个 "think outside the box" 人,我将第一个断言 S3 的一个有效用例,作为一个简单、敷衍的 NoSQL 数据库——毕竟,它是一个 key/value 具有无限存储和按键快速查找的存储...但是适合此角色的应用程序是有限的。这不是它的设计目的。

在这种情况下,似乎不同的体系结构会更好地为您提供服务...但是,如果您将 lambda 函数连接到 VPC 并为 S3 创建 VPC 端点或使用 NAT 实例(不是 NAT 网关) ,它有带宽费用),你可以以 0.04 美元的价格进行 100,000 次下载,所以根据你的规模,重复下载文件可能不是最糟糕的事情......但是你会浪费很多可计费的 lambda 毫秒来重复解析同一个文件并扫描它,正如您已经知道的那样,随着应用程序的增长,这只会变得更慢。看起来 RDS、DynamoDB 或 SimpleDB 可能更适合这里。

您还可以在 "handler" 范围之外的对象中缓存内容或至少是内存中的特定查找结果...对吗? (不是 python 人,但似乎有道理)。 Lambda 有时会重复使用相同的进程,具体取决于工作负载和调用频率。


¹ 是的,您可以在不下载整个对象的情况下进行字节范围读取,但这不适用于此处,因为我们需要扫描,而不是查找。