Обробка зображень без серверів: Node.js проти Go в AWS Lambda — аналіз продуктивності та посібник із розгортання

text
pic

Обробка зображень у безсерверних середовищах ставить перед сучасними додатками унікальні виклики та можливості. У цій статті ми розглянемо побудову автоматизованої системи генерації ескізів за допомогою AWS Lambda, яка активується при завантаженні в S3 бакет. Ми реалізуємо це рішення, використовуючи два потужних підходи: Node.js з бібліотекою Sharp та рідні можливості Go.

За допомогою практичних тестів і аналізу продуктивності ми визначимо, яка технологія краще справляється з вимогами до ЦПУ при обробці зображень у безсерверних середовищах, з результатами, які можуть поставити під сумнів звичайні уявлення про продуктивність мов програмування.

Інфраструктура як код: створення основи за допомогою SAM

Перш ніж розглядати наш порівняння продуктивності та код, давайте зрозуміємо, як ми будуємо наше середовище. AWS Serverless Application Model (SAM) надає нам потужну основу для розгортання та тестування обох реалізацій. Підхід інфраструктури як код у SAM гарантує, що наше порівняння відбувається за консистентних умов, схожих на продакшн.

Встановлення SAM CLI

Щоб виконувати команди локально на нашій системі, потрібно встановити SAM CLI (Command Line Interface для Serverless Application Model). Це дозволить нам тестувати і керувати безсерверними додатками безпосередньо з локального середовища.

# Встановлення SAM CLI  
brew install aws-sam-cli  

# Перевірка версії  
sam --version

i) Створення S3 бакету та налаштування подій сповіщень

У файлі шаблону SAM спочатку визначимо S3 бакет з необхідними дозволами. Далі налаштуємо тригер події для бакету S3, щоб викликати функцію Lambda щоразу, коли зображення завантажується в папку YOUR_S3_BUCKET/Images.

Краща практика: Використовуйте конкретні префікси або суфікси у подіях сповіщень, щоб обмежити область лише релевантними зображеннями (наприклад, .jpg, .png).

Коли зображення буде завантажено, функція Lambda буде активована для генерації ескізу і збереження його в папці YOUR_S3_BUCKET/Thumbnail.

Важливе зауваження: використовуйте окремий бакет/папку для ескізів

Після генерації ескізу за допомогою функції Lambda, переконайтеся, що він завантажується в окремий бакет або папку. Зберігання ескізу в тій самій папці, що і оригінальне зображення, може викликати нескінченний цикл подій сповіщень, оскільки завантаження ескізу знову активує функцію Lambda.

Ось файл конфігурації template.yaml:

Globals:  
 Function:  
 Timeout: 60  
 MemorySize: 512  
 Runtime: provided.al2  
 Architectures:  
 - x86_64  

ImageBucket:  
 Type: AWS::S3::Bucket  
 Properties:  
 BucketName: !Ref BucketName  
 NotificationConfiguration:  
 LambdaConfigurations:  
 - Event: s3:ObjectCreated:*  
 Function: !GetAtt ImageThumbnailsFunction.Arn  
 Filter:  
 S3Key:  
 Rules:  
 - Name: prefix  
 Value: image/  
 - Name: suffix  
 Value: .jpg  
 - Event: s3:ObjectCreated:*  
 Function: !GetAtt ImageThumbnailsFunction.Arn  
 Filter:  
 S3Key:  
 Rules:  
 - Name: prefix  
 Value: image/  
 - Name: suffix  
 Value: .jpeg  
 - Event: s3:ObjectCreated:*  
 Function: !GetAtt ImageThumbnailsFunction.Arn  
 Filter:  
 S3Key:  
 Rules:  
 - Name: prefix  
 Value: image/  
 - Name: suffix  
 Value: .png  


 ImageThumbnailsFunction:  
 Type: AWS::Serverless::Function  
 Properties:  
 CodeUri: src/image-thumbnails/  
 Handler: bootstrap  
 FunctionName: !Sub "${Environment}-create-image-thumbnails"  
 AutoPublishAlias: live  
 DeploymentPreference:  
 Type: AllAtOnce  
 Role: !GetAtt LambdaExecutionRole.Arn  
 Metadata:  
 BuildMethod: go1.x

2. Код функції Golang Lambda

Далі я надам короткий фрагмент коду для функції Lambda на Golang, яка змінює розмір зображень та створює ескізи. Для повного коду проекту я додав посилання на GitHub репозиторій в кінці цієї статті.
text
Нижче я надам короткий огляд коду.

func createThumbnail(img image.Image, size ThumbnailSize) image.Image {  
 return resize.Thumbnail(size.Width, size.Height, img, resize.Lanczos3)  
}  

func handleRequest(ctx context.Context, s3Event events.S3Event) error {  
 cfg, err := config.LoadDefaultConfig(ctx)  
 if err != nil {  
 return fmt.Errorf("не вдалося завантажити конфігурацію SDK: %v", err)  
 }  

 s3Client := s3.NewFromConfig(cfg)  

 for _, record := range s3Event.Records {  
 bucket, key := record.S3.Bucket.Name, record.S3.Object.Key  
 if strings.Contains(key, "Thumbnails/") {  
 continue  
 }  

 input := &s3.GetObjectInput{Bucket: &bucket, Key: &key}  
 result, err := s3Client.GetObject(ctx, input)  
 if err != nil {  
 log.Printf("Помилка при отриманні об'єкта: %v", err)  
 continue  
 }  

 img, format, err := image.Decode(result.Body)  
 if err != nil {  
 log.Printf("Помилка при декодуванні зображення: %v", err)  
 continue  
 }  

 for _, size := range thumbnailSizes {  
 thumbnail := createThumbnail(img, size)  
 var buf bytes.Buffer  

 switch format {  
 case "jpeg":  
 jpeg.Encode(&buf, thumbnail, nil)  
 case "png":  
 png.Encode(&buf, thumbnail)  
 }  

 thumbnailKey := fmt.Sprintf("Thumbnails/%s_%s.%s",   
 strings.TrimSuffix(filepath.Base(key), filepath.Ext(key)),   
 size.Suffix, format)  

 _, err := s3Client.PutObject(ctx, &s3.PutObjectInput{  
 Bucket: &bucket,  
 Key: &thumbnailKey,  
 Body: bytes.NewReader(buf.Bytes()),  
 ContentType: result.ContentType,  
 })  
 if err != nil {  
 log.Printf("Помилка при завантаженні ескізу: %v", err)  
 }  
 }  
 }  

 return nil  
}

2.

text
Node.js з бібліотекою Sharp Lambda функція

Далі я надам уривок коду для Node.js з використанням бібліотеки Sharp, щоб продемонструвати, як ми можемо виконувати обробку зображень.

const sharp = require("sharp");  
const { S3Client, GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3");  

const s3Client = new S3Client();  
const THUMBNAIL_PREFIX = "Gifts/Thumbnail/";  

const createThumbnail = async (imageBuffer, format, size) => {  
 const sharpInstance = sharp(imageBuffer)  
 .rotate()  
 .resize(size.width, size.height, { fit: "inside", withoutEnlargement: true });  

 if (format === "jpeg" || format === "jpg") {  
 return sharpInstance.jpeg({ quality: 85 }).toBuffer();  
 } else if (format === "png") {  
 return sharpInstance.png().toBuffer();  
 } else if (format === "webp") {  
 return sharpInstance.webp({ quality: 85 }).toBuffer();  
 }  
};  

exports.handler = async (event) => {  
 const records = event.Records || [];  
 const processedResults = [];  

 for (const record of records) {  
 const bucket = record.s3.bucket.name;  
 const key = record.s3.object.key;  

 if (key.includes(THUMBNAIL_PREFIX)) continue;  

 const getObjectCommand = new GetObjectCommand({ Bucket: bucket, Key: key });  
 const { Body: imageStream, ContentType } = await s3Client.send(getObjectCommand);  
 const imageBuffer = await streamToBuffer(imageStream);  

 const format = key.split('.').pop().toLowerCase();  
 if (!["jpg", "jpeg", "png", "webp"].includes(format)) continue;  

 const metadata = await sharp(imageBuffer).metadata();  
 const fileName = key.split('/').pop();  

 const processingResult = {  
 originalImage: { fileName, path: key, width: metadata.width, height: metadata.height },  
 thumbnails: []  
 };  

 for (const size of [{ width: 300, height: 300, suffix: "x300" }]) {  
 const thumbnailBuffer = await createThumbnail(imageBuffer, format, size);  
 const thumbnailKey = `${THUMBNAIL_PREFIX}${fileName.split('.')[0]}_${size.suffix}.${format}`;  

 const putObjectCommand = new PutObjectCommand({  
 Bucket: bucket,  
 Key: thumbnailKey,  
 Body: thumbnailBuffer,  
 ContentType  
 });  

 await s3Client.send(putObjectCommand);  
 processingResult.thumbnails.push({ fileName: thumbnailKey, width: size.width, height: size.height });  
 }  

 processedResults.push(processingResult);  
 }  

 return processedResults;  
};  

async function streamToBuffer(stream) {  
 const chunks = [];  
 for await (const chunk of stream) {  
 chunks.push(chunk);  
 }  
 return Buffer.concat(chunks);  
}

Обробка крайніх випадків

I) Підтримувані формати файлів

Для обробки непідтримуваних форматів файлів ми реалізували перевірки в двох місцях:

  1. Конфігурація сповіщення події S3: Ми додали фільтр суфіксів, щоб функція Lambda викликалась тільки при завантаженні файлів з розширеннями, такими як .jpg, .jpeg або .png.
  2. Код функції Lambda: Код включає додаткову перевірку розширення файлу та обробляє лише файли з вказаними розширеннями.

Наприклад, якщо користувач завантажить непідтримуваний тип файлу, такий як .pdf, подія S3 не викличе функцію Lambda, що запобігає непотрібній обробці.

II) Великі розміри зображень перевищують ліміти пам'яті Lambda

Якщо ми обробляємо зображення великих розмірів, функція Lambda може зламатися через обмежену потужність пам'яті та процесора. Тому найкращою практикою є додавання перевірок на розмір зображення.

2. Моніторинг журналів Lambda в CloudWatch

Моніторинг відіграє важливу роль після розгортання функції Lambda в AWS. У виробничому середовищі, якщо виникає помилка, перегляд журналів стає важливим для виявлення та усунення проблеми ефективно.

Переконайтесь, що ви надсилаєте журнали вхідних подій з S3 бакету, результати виведення та винятки при обробці події з S3 бакету до Cloudwatch.
text
Я додав код у файл template.yaml, щоб зберігати журнали в Cloudwatch з періодом зберігання 14 днів.

# Log Group для функції Lambda  
 LambdaLogGroup:  
 Type: AWS::Logs::LogGroup  
 Properties:  
 LogGroupName: !Sub "/aws/lambda/${Environment}-create-image-thumbnails"  
 RetentionInDays: 14

Після налаштування інфраструктури та розгортання обох реалізацій ми готові перейти до важливого порівняння продуктивності між Node.js з бібліотекою Sharp і Go. Це порівняння розкриє цікаві факти про продуктивність обробки зображень у безсерверних середовищах.

NodeJS проти Golang

Як і багато інших розробників, я вважав, що оскільки обробка зображень є ресурсозатратною для процесора, Go природно перевершить Node.js через свою репутацію в чудовій обробці завдань, що вимагають потужності процесора. Результати здивували мене і навчили важливому уроку щодо припущень у програмуванні.

Початкове припущення

Моє початкове припущення було простим: Go, як компільована мова з відмінними можливостями для паралельної обробки та ефективного виконання задач, що інтенсивно використовують процесор, перевершить Node.js в завданнях обробки зображень. Це здавалося логічним, оскільки:

  • Go створено для високопродуктивних серверних додатків
  • Зазвичай він показує кращу продуктивність у завданнях, що обтяжують процесор

Неочікувані результати

Однак, коли я провів широкі бенчмарки, порівнюючи рідні можливості обробки зображень Go з Node.js та бібліотекою Sharp, результати показали іншу картину. Комбінація Node.js та Sharp постійно перевершувала Go в більшості поширених сценаріїв обробки зображень.

Ось деякі ключові висновки з моїх бенчмарків:

Обробка одного зображення (30 МБ → 300x300 пікселів)

  • Node.js/Sharp: ~3с (188 МБ пам'яті використано)
  • Golang: ~16с (355 МБ пам'яті використано)

Розуміння результатів

Перевага продуктивності Node.js/Sharp пояснюється кількома ключовими факторами:

Таємна зброя Sharp: libvips Sharp використовує libvips, високооптимізовану бібліотеку обробки зображень, написану на C. Це означає, що хоча ваш код працює в Node.js, обробка зображень виконується у високооптимізованому рідному коді.

Коли використовувати Go для обробки зображень?

Go стає потужним вибором, коли ваші потреби в обробці зображень виходять за межі стандартних операцій. Наприклад, якщо ви розробляєте власні фільтри або унікальні перетворення зображень, яких немає в бібліотеці Sharp, низькорівневі можливості Go та ефективне управління пам'яттю роблять його відмінним вибором. Замість того щоб боротися з складними реалізаціями в Node.js, ви можете скористатися сильною типізацією Go та прямим доступом до пам'яті для створення точних, індивідуальних маніпуляцій з пікселями.

Висновок

Це порівняння піддало сумніву мої початкові припущення щодо продуктивності в задачах, що інтенсивно використовують процесор. Хоча Go часто є вибором для операцій, що обтяжують процесор, комбінація Node.js і оптимізованої реалізації libvips в Sharp демонструє, що добре розроблені інструменти, орієнтовані на конкретні завдання, можуть перевершити більш універсальні рішення.

Це підкреслює важливий принцип у програмуванні: рішення про вибір технологій повинні ґрунтуватися на реальних бенчмарках та тестуванні, а не лише на загальних уявленнях про мови програмування.

Початковий код

Ви можете знайти повний вихідний код для цього проєкту за посиланнями:

Перекладено з: Serverless Image Processing: Node.js vs Go in AWS Lambda — Performance Insights and Deployment Guide

Leave a Reply

Your email address will not be published. Required fields are marked *