text
Обробка зображень у безсерверних середовищах ставить перед сучасними додатками унікальні виклики та можливості. У цій статті ми розглянемо побудову автоматизованої системи генерації ескізів за допомогою 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) Підтримувані формати файлів
Для обробки непідтримуваних форматів файлів ми реалізували перевірки в двох місцях:
- Конфігурація сповіщення події S3: Ми додали фільтр суфіксів, щоб функція Lambda викликалась тільки при завантаженні файлів з розширеннями, такими як
.jpg
,.jpeg
або.png
. - Код функції 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