Технология «Функции как сервис» (FaaS), часто ассоциируемая с «бессерверными» вычислениями, произвела революцию в том, как мы развёртываем код. В контексте событийно-управляемых микросервисов (EDM) FaaS выступает не просто как инструмент для обработки веб-хуков, а как мощный механизм для создания реактивных, эластичных и экономически эффективных компонентов.
По сути, FaaS-решение можно представить как базовую связку «производитель-потребитель», которая регулярно завершает свою работу и освобождает ресурсы. Но чтобы вписать этот паттерн в строгую архитектуру EDM, необходимо понимать специфику его жизненного цикла, управления состоянием и масштабирования.
Это девятая глава цикла. Она опирается на главу II (партиционирование, consumer groups), главу V (state stores, rebalance), главу VII (состояние, TTL) и главу VIII (оркестровка, Saga). Здесь мы разберём, как эти механизмы преломляются в мире FaaS.
Переход от классических контейнерных микросервисов к функциям требует соблюдения нескольких фундаментальных принципов, чтобы не превратить архитектуру в хаотичный набор разрозненных скриптов.
Функции и их внутренние потоки должны строго принадлежать одному ограниченному контексту. Из-за высокой гранулярности FaaS легко потерять контроль над владением данными. Группируйте функции по бизнес-доменам и храните их код в соответствующих репозиториях.
Это критически важное правило. Смещения (offsets) в брокере событий должны подтверждаться только после успешного завершения обработки пакета. Фиксация до обработки (как делают некоторые провайдеры по умолчанию) при сбоях функции приведёт к безвозвратной потере данных.
Искушение разбить логику на десятки микро-функций и переиспользовать их везде очень велико. Но это ведёт к фрагментации и усложнению отладки. Стремитесь к тому, чтобы ограниченный контекст реализовывался минимально необходимым количеством функций. Одной функцией управлять и тестировать несоизмеримо проще.
Функция спит до тех пор, пока не сработает триггер. В событийно-управляемой архитектуре триггеры — это мост между брокером событий и вычислительными ресурсами.
Функция запускается при появлении новых событий. Брокер или провайдер FaaS скрыто управляет потреблением и передаёт массив событий в код функции. Идеально для реактивной обработки.
Внешний монитор отслеживает отставание группы потребителей. Если lag превышает порог, провайдер FaaS массово запускает экземпляры функций, которые сами создают клиентские соединения и «разгребают» завал. Отлично для пакетной обработки и сглаживания пиков.
Запуск по таймеру — для периодических задач: агрегация, отчёты, cleanup.
Событие от внешнего ресурса: появление файла в S3, изменение документа в DynamoDB, событие в EventBridge. Классический event-driven паттерн.
При первом запуске или после долгого простоя функция проходит через «холодный старт»: провайдер выделяет контейнер, загружает код и устанавливает соединения с брокером событий. Это занимает время и может вызвать задержки.
Однако большинство провайдеров кешируют «тёплые» экземпляры. Если функция обрабатывает стабильный поток, она будет периодически «засыпать» и «просыпаться», переиспользуя существующие соединения, что сводит накладные расходы к минимуму.
В AWS Lambda event-source mapping — это ключевой механизм, который связывает источник событий (SQS, Kinesis, DynamoDB Streams, MSK) с функцией. Понимание его настроек критично для production.
| Параметр | Назначение | Рекомендация |
|---|---|---|
BatchSize |
Сколько событий передавать в одной инвокации | 10–100 для SQS, 100–1000 для Kinesis |
MaximumBatchingWindowInSeconds |
Сколько ждать накопления batch | 0–5 секунд для low-latency, больше для throughput |
MaximumConcurrency |
Ограничение параллельных инвокаций | Настраивается для защиты downstream |
FunctionResponseTypes |
Partial batch response | ReportBatchItemFailures для гранулярных сбоев |
OnFailure |
Куда отправлять неудачные batch | SQS DLQ или SNS topic для ручного разбора |
Без partial batch response, если один event в batch падает, весь batch переобрабатывается — включая уже успешные события. Это ведёт к дубликатам.
public class Handler implements RequestHandler<SQSEvent, SQSBatchResponse> {
@Override
public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
for (SQSEvent.SQSMessage msg : event.getRecords()) {
try {
processMessage(msg);
} catch (Exception e) {
failures.add(new SQSBatchResponse.BatchItemFailure(msg.getMessageId()));
}
}
return new SQSBatchResponse(failures);
}
}
Только события из failures будут переобработаны. Успешные — зафиксированы и удалены из очереди.
Для потоковых источников (Kinesis, MSK) partial batch response особенно важен: без него один сбойный event блокирует всю партицию. С ним — только сбойные события уходят в retry, остальные фиксируются.
Главная философия FaaS — отсутствие состояния (stateless). Функция эфемерна: она запускается, выполняет работу и исчезает вместе со своей локальной памятью. Но как быть, если бизнес-логика требует агрегации или хранения данных?
В классическом EDM-подходе состояние выносится вовне. Функция явно подключается к внешним хранилищам (Redis, DynamoDB, CosmosDB) через сеть. Это снижает гибкость масштабирования, так как провайдеру FaaS приходится учитывать сетевые задержки и лимиты внешних БД, но зато позволяет масштабировать функции горизонтально без ограничений.
Если функция должна помнить состояние — она не должна пытаться хранить его в собственном контейнере. Это принципиально отличается от Kafka Streams, где state store живёт на локальном диске и защищён changelog. В FaaS такой возможности нет — только внешние stores с network latency.
Эволюция подхода — некоторые провайдеры начали предлагать абстракции, которые автоматически сохраняют локальное состояние функции во внешнее хранилище. Это позволяет разработчикам писать код так, словно он имеет память, делегируя рутину сохранения и восстановления фреймворку.
Долгоживущие. Хранят состояние процесса, вызывают activity-функции, ждут результатов. Реализуют паттерн Saga с автоматическим сохранением состояния.
Stateless рабочие. Выполняют одну операцию (вызов API, запись в БД). Могут выполняться параллельно, orchestration — синхронизирует.
Stateful actors. Хранят состояние между вызовами, обрабатывают операции последовательно. Идеально для агрегатов — счётчиков, очередей, actor-моделей.
Точка входа. Запускает orchestration, отправляет события, опрашивает статус.
[FunctionName("OrderSaga")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
try {
await context.CallActivityAsync("ReserveInventory", order);
await context.CallActivityAsync("ChargePayment", order);
await context.CallActivityAsync("ShipOrder", order);
return "completed";
} catch (Exception) {
await context.CallActivityAsync("ReleaseInventory", order);
await context.CallActivityAsync("RefundPayment", order);
throw;
}
}
Ключевое: состояние orchestrator персистится автоматически. При сбое worker'а — другой worker подхватывает с того же шага. Это ровно тот механизм, который в Temporal реализуется через durable execution (глава VIII).
using (var timeoutCts = new CancellationTokenSource())
{
var approvalTask = context.WaitForExternalEvent<bool>("Approval");
var timeoutTask = context.CreateTimer(
context.CurrentUtcDateTime.AddHours(24),
timeoutCts.Token);
var winner = await Task.WhenAny(approvalTask, timeoutTask);
if (winner == approvalTask)
{
// Human approved
}
else
{
// Auto-compensate
}
}
Когда одна функция должна передать эстафету другой, возникает вопрос координации. Здесь мы возвращаемся к паттернам из главы VIII, но с поправкой на эфемерность FaaS.
Функция А обрабатывает событие, публикует результат в новый поток и завершается. Функция Б слушает этот поток и подхватывает работу.
Опасность: если функция А вызывает функцию Б напрямую fire-and-forget, провайдер FaaS может запустить десятки экземпляров Б параллельно. Это гарантированно нарушит порядок обработки событий в партиции.
Функция-оркестратор последовательно вызывает функции А и Б, ожидая результата от каждой перед переходом к следующей.
Преимущество: строгий порядок. Каждое событие полностью проходит весь бизнес-процесс до того, как оркестратор возьмётся за следующее. Offset фиксируется только после полного успеха.
public int orchestrationFunction(Event[] events, Context context) {
for (Event event : events) {
// Строгий синхронный вызов
Result resA = invokeFunctionA(event);
Result resB = invokeFunctionB(event, resA);
// Публикуем финальный результат
producer.produce("Output-Stream", composeOutput(resA, resB));
}
// Сообщаем фреймворку, что можно зафиксировать смещения
context.success();
return 0;
}
Функция может быть вызвана повторно: retry от провайдера, at-least-once доставка, partial batch failure. Как гарантировать, что повторный вызов не создаст дубликатов?
Idempotency-Key в заголовке (Stripe, Adyen поддерживают нативно). Для SQS — использовать MessageId как ключ.attribute_not_exists(event_id) в condition expression. Для PostgreSQL — INSERT ... ON CONFLICT DO NOTHING.event_id в Redis с TTL (например, 24 часа). Перед обработкой — проверка, после обработки — запись.// DynamoDB conditional write
PutItemRequest request = PutItemRequest.builder()
.tableName("processed-events")
.item(Map.of(
"event_id", AttributeValue.builder().s(eventId).build(),
"processed_at", AttributeValue.builder().n(String.valueOf(now)).build()
))
.conditionExpression("attribute_not_exists(event_id)")
.build();
try {
dynamoDb.putItem(request);
processEvent(event); // только если condition passed
} catch (ConditionalCheckFailedException e) {
// Уже обработано — пропустить
}
FaaS предоставляет невероятные возможности для горизонтального масштабирования. Если порядок событий не важен (например, обработка очереди), провайдер может запустить тысячи экземпляров функции одновременно.
Но если вы работаете с партиционированными потоками событий, где порядок критичен, максимальный уровень параллелизма жёстко ограничен количеством разделов (partitions) в потоке.
Если вы используете агрессивные политики автомасштабирования на основе lag, функция будет постоянно запускаться и завершаться. Каждый запуск и остановка заставляют брокер переназначать разделы (rebalance). В экстремальных случаях группа потребителей попадает в «тупик»: функция тратит весь свой lifespan только на то, чтобы получить назначение разделов, и даже не успевает начать их обрабатывать.
Настройте политики масштабирования так, чтобы они требовали устойчивого превышения порога нагрузки в течение нескольких минут, прежде чем добавлять новые экземпляры. Это гасит осцилляции.
Если нагрузка предсказуема, настройте функции так, чтобы они заранее знали, какие разделы им принадлежат. Это устраняет накладные расходы на перебалансировку.
Тщательно настраивайте размер batch и окно ожидания. Если функция не успевает обработать batch за lifespan — она упадёт с таймаутом.
Используйте cooperative sticky assignor (глава V), который перераспределяет только изменившиеся партиции, а не останавливает всех потребителей.
Если функция работает внутри VPC — она теряет доступ к managed-сервисам (S3, DynamoDB, SQS) без NAT Gateway или VPC Endpoint. Это добавляет latency и стоит денег.
| Опция | Latency | Стоимость |
|---|---|---|
| NAT Gateway | +2–5 ms | Оплата за час + трафик |
| VPC Endpoint (PrivateLink) | +1–2 ms | Оплата за час + трафик |
| Lambda вне VPC | Нативный | Без доплат |
Правило: если функция не требует доступа к ресурсам внутри VPC (RDS, ElastiCache), не помещайте её в VPC. Это упрощает архитектуру и снижает latency.
FaaS оплачивается за:
| Сценарий | FaaS дешевле? | Почему |
|---|---|---|
| Редкие триггеры | Да | Оплата только за вызовы, контейнер 24/7 — простаивал бы |
| Переменная нагрузка | Да | Auto-scaling без выделения пиковых ресурсов |
| Постоянная высокая нагрузка | Нет | Контейнеры с reserved capacity дешевле |
| High-throughput потоки | Нет | Kafka Streams / Flink на контейнерах эффективнее |
| Stateful агрегации | Нет | Network latency к внешним stores убивает производительность |
Тестирование FaaS — отдельная дисциплина. Функции эфемерны, зависят от триггеров и внешних сервисов.
Локальная эмуляция AWS-сервисов (SQS, S3, DynamoDB, Lambda). Testcontainers + LocalStack — для интеграционных тестов.
Локальный запуск Lambda-функций с эмуляцией API Gateway и event sources. Быстрая итерация без деплоя.
Плагин для локального запуска HTTP-триггеров. Для не-AWS провайдеров — аналоги.
Функция принимает JSON — можно тестировать с mock-событиями без брокера. Быстро, но не проверяет интеграцию.
| Провайдер | Max execution | Triggers | Stateful |
|---|---|---|---|
| AWS Lambda | 15 минут | SQS, Kinesis, MSK, DynamoDB Streams, EventBridge, S3 | Внешние stores (DynamoDB, ElastiCache) |
| Azure Functions | 10 минут (default) Unlimited для Durable |
Event Hub, Service Bus, Event Grid, Blob Storage | Durable Functions (orchestrator, entity) |
| GCP Cloud Functions | 9 минут | Pub/Sub, Firestore, Cloud Storage, Eventarc | Внешние stores (Firestore, Memorystore) |
| Cloudflare Workers | 30 секунд (CPU) | HTTP, Queues, Durable Objects | Durable Objects |
Если нужны долгие процессы (дни) и human-in-the-loop — Azure Durable Functions. Если нужна минимальная latency и edge — Cloudflare Workers. Если нужна зрелая экосистема и максимум интеграций — AWS Lambda. Если всё в GCP — Cloud Functions с Eventarc.
| Глава | Фокус | Связь с текущей главой |
|---|---|---|
| II. Анатомия EDA | Партиционирование, consumer groups | FaaS consumer ограничен числом партиций |
| V. Механика потоковой обработки | Rebalance, state stores | Rebalancing deadlock — следствие rebalance-политик |
| VII. Память микросервисов | State stores, TTL, дедупликация | В FaaS state — только внешний, дедупликация через conditional writes |
| VIII. Механика бизнес-процессов | Saga, оркестровка | Durable Functions реализуют Saga с автоматическим state |
| IX. Бессерверная архитектура | FaaS в EDA | Когда FaaS, а когда Kafka Streams / Flink |
Используйте FaaS там, где события дискретны, а масштабирование должно происходить мгновенно в ответ на всплески нагрузки. Но всегда помните о «налоге» на перебалансирование, о network latency к внешним stores и строго соблюдайте дисциплину фиксации смещений. Для сложной детерминированной обработки нескольких потоков, exactly-once или локальных агрегаций — Kafka Streams или Flink остаются предсказуемее.
Глава IX цикла «Событийно-управляемая архитектура». Источники: AWS Lambda, Azure Durable Functions, GCP Cloud Functions, Cloudflare Workers, LocalStack, AWS SAM, практические кейсы production-систем.
Function-as-a-Service (FaaS), often associated with "serverless" computing, has revolutionized how we deploy code. In the context of event-driven microservices (EDM), FaaS is not just a tool for handling webhooks but a powerful mechanism for creating reactive, elastic, and cost-effective components.
Essentially, a FaaS solution can be viewed as a producer-consumer pair that regularly completes its work and releases resources. But to fit this pattern into a strict EDM architecture, one must understand the specifics of its lifecycle, state management, and scaling.
This is the ninth chapter of the series. It builds on Chapter II (partitioning, consumer groups), Chapter V (state stores, rebalance), Chapter VII (state, TTL), and Chapter VIII (orchestration, Saga). Here we explore how these mechanisms translate into the FaaS world.
Functions and their streams must strictly belong to one bounded context. Due to high granularity of FaaS, it is easy to lose control over data ownership. Group functions by business domain.
Critical rule. Offsets must be confirmed only after successful batch processing. Committing before processing (as some providers do by default) causes irrecoverable data loss on failure.
The temptation to split logic into dozens of micro-functions and reuse them everywhere is very high. But this leads to fragmentation and debugging complexity. Aim for the minimum necessary number of functions per bounded context.
The function triggers on new events. The broker or FaaS provider transparently manages consumption and passes an array of events to the function code.
An external monitor tracks consumer group lag. If lag exceeds a threshold, the provider mass-launches function instances that create their own client connections and clear the backlog.
Periodic trigger for aggregation, reports, cleanup.
Event from an external resource: file in S3, document in DynamoDB, EventBridge event.
On first launch or after long idle, the function goes through a cold start: the provider allocates a container, loads code, and establishes connections. This takes time and can cause delays.
However, most providers cache warm instances. If the function processes a stable stream, it periodically "sleeps" and "wakes," reusing existing connections.
| Parameter | Purpose | Recommendation |
|---|---|---|
BatchSize |
How many events per invocation | 10–100 for SQS, 100–1000 for Kinesis |
MaximumBatchingWindowInSeconds |
How long to wait for batch accumulation | 0–5 seconds for low-latency |
MaximumConcurrency |
Limit on parallel invocations | Configure to protect downstream |
FunctionResponseTypes |
Partial batch response | ReportBatchItemFailures for granular failures |
OnFailure |
Where to send failed batches | SQS DLQ or SNS topic |
public class Handler implements RequestHandler<SQSEvent, SQSBatchResponse> {
@Override
public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
for (SQSEvent.SQSMessage msg : event.getRecords()) {
try {
processMessage(msg);
} catch (Exception e) {
failures.add(new SQSBatchResponse.BatchItemFailure(msg.getMessageId()));
}
}
return new SQSBatchResponse(failures);
}
}
The main philosophy of FaaS is statelessness. The function is ephemeral: it launches, does work, and disappears along with its local memory.
In the classic EDM approach, state is externalized. The function explicitly connects to external stores (Redis, DynamoDB, CosmosDB) over the network. This reduces scaling flexibility — the FaaS provider must account for network latency and external DB limits — but allows horizontal scaling without restrictions.
If a function must remember state, it must not try to store it in its own container. This fundamentally differs from Kafka Streams, where the state store lives on local disk and is protected by a changelog. In FaaS there is no such option — only external stores with network latency.
The evolution of the approach — some providers began offering abstractions that automatically persist local function state to external storage. This allows developers to write code as if it had memory, delegating the routine of saving and restoring to the framework.
Long-lived. Store process state, invoke activity functions, wait for results. Implement Saga with automatic state persistence.
Stateless workers. Perform one operation (API call, DB write). Can run in parallel; orchestration synchronizes.
Stateful actors. Store state between calls, process operations sequentially. Ideal for aggregates.
Entry point. Start orchestration, send events, poll status.
[FunctionName("OrderSaga")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
try {
await context.CallActivityAsync("ReserveInventory", order);
await context.CallActivityAsync("ChargePayment", order);
await context.CallActivityAsync("ShipOrder", order);
return "completed";
} catch (Exception) {
await context.CallActivityAsync("ReleaseInventory", order);
await context.CallActivityAsync("RefundPayment", order);
throw;
}
}
Function A processes event, publishes result to a new stream, and completes. Function B listens and picks up.
Danger: if A calls B directly fire-and-forget, the FaaS provider may launch dozens of B instances in parallel. This guarantees violation of ordering within a partition.
An orchestrator function sequentially invokes A and B, waiting for each result before moving to the next.
Advantage: strict ordering. Offset commits only after full success.
public int orchestrationFunction(Event[] events, Context context) {
for (Event event : events) {
Result resA = invokeFunctionA(event);
Result resB = invokeFunctionB(event, resA);
producer.produce("Output-Stream", composeOutput(resA, resB));
}
context.success();
return 0;
}
Idempotency-Key. For SQS — use MessageId.attribute_not_exists(event_id). For PostgreSQL — INSERT ... ON CONFLICT DO NOTHING.event_id in Redis with TTL.PutItemRequest request = PutItemRequest.builder()
.tableName("processed-events")
.item(Map.of(
"event_id", AttributeValue.builder().s(eventId).build(),
"processed_at", AttributeValue.builder().n(String.valueOf(now)).build()
))
.conditionExpression("attribute_not_exists(event_id)")
.build();
try {
dynamoDb.putItem(request);
processEvent(event);
} catch (ConditionalCheckFailedException e) {
// Already processed — skip
}
If you use aggressive lag-based auto-scaling, the function will constantly start and stop. Each start and stop forces a broker partition reassignment (rebalance). In extreme cases, the consumer group hits a "deadlock": the function spends its entire lifespan just getting partition assignments and never even begins processing.
Configure scaling policies to require sustained threshold exceedance for several minutes before adding instances.
If load is predictable, configure functions to know their partitions in advance. Eliminates rebalance overhead.
If the function cannot process a batch within its lifespan — it fails with timeout. Reduce batch size or increase execution limit.
Use cooperative sticky assignor (Chapter V) which redistributes only changed partitions.
| Option | Latency | Cost |
|---|---|---|
| NAT Gateway | +2–5 ms | Hourly + traffic |
| VPC Endpoint (PrivateLink) | +1–2 ms | Hourly + traffic |
| Lambda outside VPC | Native | No extra cost |
Rule: if the function doesn't need access to VPC resources (RDS, ElastiCache), don't put it in a VPC.
| Scenario | FaaS Cheaper? | Why |
|---|---|---|
| Rare triggers | Yes | Pay only for invocations; container 24/7 would idle |
| Variable load | Yes | Auto-scaling without peak reservation |
| Constant high load | No | Reserved-capacity containers are cheaper |
| High-throughput streams | No | Kafka Streams / Flink on containers more efficient |
| Stateful aggregations | No | Network latency to external stores kills performance |
Local emulation of AWS services (SQS, S3, DynamoDB, Lambda). Testcontainers + LocalStack for integration tests.
Local Lambda execution with API Gateway and event source emulation. Fast iteration without deploy.
Plugin for local HTTP trigger execution.
Function accepts JSON — test with mock events. Fast but doesn't verify integration.
| Provider | Max Execution | Triggers | Stateful |
|---|---|---|---|
| AWS Lambda | 15 min | SQS, Kinesis, MSK, DynamoDB Streams, EventBridge, S3 | External stores (DynamoDB, ElastiCache) |
| Azure Functions | 10 min (default) Unlimited for Durable |
Event Hub, Service Bus, Event Grid, Blob Storage | Durable Functions (orchestrator, entity) |
| GCP Cloud Functions | 9 min | Pub/Sub, Firestore, Cloud Storage, Eventarc | External stores (Firestore, Memorystore) |
| Cloudflare Workers | 30 sec (CPU) | HTTP, Queues, Durable Objects | Durable Objects |
| Chapter | Focus | Link to This Chapter |
|---|---|---|
| II. The Anatomy of EDA | Partitioning, consumer groups | FaaS consumer limited by partition count |
| V. The Mechanics of Stream Processing | Rebalance, state stores | Rebalancing deadlock is a consequence of rebalance policies |
| VII. The Memory of Microservices | State stores, TTL, dedup | In FaaS state is only external; dedup via conditional writes |
| VIII. The Mechanics of Business Processes | Saga, orchestration | Durable Functions implement Saga with automatic state |
| IX. Serverless Architecture | FaaS in EDA | When FaaS, and when Kafka Streams / Flink |
Use FaaS where events are discrete and scaling must happen instantly in response to bursts. But always remember the "tax" of rebalancing, the network latency to external stores, and strictly follow the discipline of offset commits. For complex deterministic processing of multiple streams, exactly-once, or local aggregations — Kafka Streams or Flink remain more predictable.
Chapter IX of the "Event-Driven Architecture" series. Sources: AWS Lambda, Azure Durable Functions, GCP Cloud Functions, Cloudflare Workers, LocalStack, AWS SAM, production system case studies.