Как предотвратить ошибку max_connections при подключении AWS Aurora Postgres к Vercel
Большинство руководств по бессерверной (serverless) инфраструктуре показывают лишь процесс нажатия нескольких кнопок для подключения. Проблема начинается потом. Стоит задеплоить код и получить хотя бы небольшой приток трафика, как соединения с БД начинаются рваться, попытки управлять статическими Access Key вызывают холодный пот, а в итоговом счете за трансфер данных появляются цифры с несколькими нулями.
Ниже собраны практические узкие места и решения, с которыми приходится сталкиваться при интеграции фронтенда на Vercel и бэкенда на AWS.
1. Настройка пула соединений при подключении Vercel к AWS Aurora Postgres
PostgreSQL создает новый процесс каждый раз, когда подключается клиент. Один процесс потребляет от 2 МБ до 8 МБ и более оперативной памяти. Поскольку бессерверные функции Vercel при поступлении запросов масштабируют экземпляры без сохранения состояния без ограничений, лимит max_connections в Aurora Postgres (100–300 соединений) забивается буквально за пару секунд.
В итоге возникает ошибка FATAL: sorry, too many clients already, загрузка CPU БД достигает 100%, и сервис падает.
Между бессерверными функциями и Aurora необходимо разместить PgBouncer и включить режим пулинга транзакций (Transaction Pooling). Пулинг сессий не поможет, так как удерживает соединения 1:1, а пулинг операторов (statement pooling) не позволяет использовать многооператорные транзакции. Пулинг транзакций выделяет соединение с БД ровно на время выполнения транзакции запроса и сразу же возвращает его в пул после COMMIT.
Вот базовые настройки для файла pgbouncer.ini:
`ini
[pgbouncer]
pool_mode = transaction
max_client_conn = 10000
default_pool_size = 20
reserve_pool_size = 10
max_db_connections = 80
`
pool_mode = transaction включает повторное использование на уровне транзакций.
max_client_conn устанавливается в районе 10000, при этом следует увеличить лимит ulimit -n в ОС.
default_pool_size — это количество соединений, открытых к Aurora для каждой пары пользователь/БД. Установите значение от 10 до 20.
max_db_connections = 80 жестко ограничивает верхний предел реальных соединений, поступающих в экземпляр Aurora.
В среде Vercel Fluid Compute несколько вызовов функций могут делиться глобальной областью видимости. Поэтому пул драйвера БД следует объявлять в области видимости модуля. Использование attachDatabasePool из пакета @vercel/functions корректно очищает простаивающие соединения перед завершением работы экземпляра функции.
`typescript
import { Pool } from 'pg';
import { attachDatabasePool } from '@vercel/functions';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 5000,
});
attachDatabasePool(pool);
export default pool;
`
Строки подключения также необходимо разделить по средам. В локальной среде разработки (.env.local) подключайтесь напрямую к порту Aurora (5432) или к локальному PgBouncer, а в продакшене (.env.production) используйте порт PgBouncer (6432) и параметр pgbouncer=true.
`bash
.env.local (Direct Connection)
DATABASE_URL="postgresql://dbuser:dbpassword@aurora-cluster.us-east-1.rds.amazonaws.com:5432/app_dev?sslmode=require"
.env.production (PgBouncer)
DATABASE_URL="postgresql://dbuser:dbpassword@pgbouncer-proxy.internal:6432/app_prod?sslmode=require&pgbouncer=true"
`
2. Управление правами IAM через OIDC вместо Access Key
Прописывать AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY напрямую в переменных окружения Vercel опасно. В случае утечки ключей под угрозой окажется вся инфраструктура. Если при этом были предоставлены права администратора вроде AdministratorAccess, ситуация станет еще критичнее.
Используйте OIDC (OpenID Connect). В этом сценарии подписанный JWT, выпущенный Vercel, передается в API AssumeRoleWithWebIdentity сервиса AWS STS для получения временных учетных данных сроком на 1 час. Потребность в жестко прописанных ключах полностью отпадает.
В политике доверия (Trust Policy) для AWS IAM Role следует указывать только минимально необходимые права:
`json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.vercel.com/my-team-slug"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.vercel.com/my-team-slug:aud": "https://vercel.com/my-team-slug",
"oidc.vercel.com/my-team-slug:sub": "owner:my-team-slug:project:my-ai-app:environment:production"
}
}
}
]
}
`
- Зарегистрируйте
[oidc.vercel.com/my-team-slug](https://oidc.vercel.com/my-team-slug) в IAM Identity Provider.
- Ограничьте claim
aud URL-адресом вашей команды Vercel, чтобы заблокировать доступ другим организациям.
- В условии для claim
sub точно укажите имя проекта (my-ai-app) и среду (production). Это ключевая настройка для защиты от атак типа Confused Deputy.
Политика прав также должна открывать доступ только к необходимым ресурсам. Ниже приведен пример предоставления прав только на конкретный S3-бакет и вызов Lambda-функции:
`json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictedS3BucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-production-ai-assets",
"arn:aws:s3:::my-production-ai-assets/*"
]
},
{
"Sid": "RestrictedLambdaInvocation",
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:us-east-1:123456789012:function:python-ml-inference-service"
]
}
]
}
`
В коде Node.js временные учетные данные получаются с помощью @vercel/oidc-aws-credentials-provider:
`typescript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
const credentials = awsCredentialsProvider({
roleArn: process.env.AWS_ROLE_ARN!,
});
const s3Client = new S3Client({ region: 'us-east-1', credentials });
const lambdaClient = new LambdaClient({ region: 'us-east-1', credentials });
export async function POST(req: Request) {
const body = await req.json();
await s3Client.send(new PutObjectCommand({
Bucket: 'my-production-ai-assets',
Key: inputs/${Date.now()}.json,
Body: JSON.stringify(body),
}));
const lambdaRes = await lambdaClient.send(new InvokeCommand({
FunctionName: 'python-ml-inference-service',
Payload: Buffer.from(JSON.stringify(body)),
}));
return Response.json({
status: 'success',
result: JSON.parse(Buffer.from(lambdaRes.Payload!).toString()),
});
}
`
3. Сокращение задержки API из-за разницы в регионах
Если фронтенд Vercel работает в PoP Токио, а бэкенд AWS находится на востоке США (us-east-1), то к каждому запросу добавляется от 150 до 250 мс физической задержки (latency).
Ответы API, которые меняются нечасто, лучше кэшировать на уровне Vercel Edge Middleware, чтобы предотвратить лишние обращения к бэкенду.
`typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export const config = {
matcher: ['/api/v1/ml-models/:path*'],
};
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=120'
);
response.headers.set(
'Vercel-CDN-Cache-Control',
's-maxage=300, stale-while-revalidate=600'
);
return response;
}
`
Установка s-maxage=60, stale-while-revalidate=120 позволяет мгновенно отдавать ответ из Edge-кэша в течение 60 секунд, а в течение 120 секунд после истечения срока кэша показывать устаревший ответ, обновляя кэш в фоновом режиме.
Для сквозной трассировки между сервисами необходимо передавать спецификацию W3C TraceContext от OpenTelemetry (заголовок traceparent). Вот настройка для Vercel:
`typescript
// instrumentation.ts
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: 'vercel-frontend-service',
instrumentationConfig: {
fetch: {
propagateContextUrls: ['api.my-aws-backend.com', '*.amazonaws.com'],
},
},
});
}
`
Если на стороне AWS FastAPI аналогично подключить приемник OpenTelemetry, то фронтенд и бэкенд будут объединены единым Trace ID.
`python
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
app = FastAPI(title="Python ML Service")
FastAPIInstrumentor.instrument_app(app)
@app.get("/api/v1/ml-models/predict")
async def predict():
tracer = trace.get_tracer(name)
with tracer.start_as_current_span("ml_inference_execution"):
return {"status": "completed", "prediction": [0.95, 0.05]}
`
4. Предотвращение неконтролируемых расходов на передачу данных и масштабирование
Финансовые инциденты обычно возникают в двух местах: плата за превышение Fast Data Transfer в Vercel (0.15заГБпослепревышения1ТБ)иплатазаDataTransferOutвAWS(0.09 за ГБ). Если при всплеске трафика бессерверные функции и Lambda начнут одновременно масштабироваться, порядок сумм в счете быстро изменится.
Пример API Route, который принимает вебхуки лимита расходов Vercel и отправляет уведомление в Slack. Для безопасности требуется проверка подписи HMAC SHA1.
`typescript
import crypto from 'crypto';
export async function POST(req: Request) {
const payload = await req.text();
const signature = req.headers.get('x-vercel-signature');
const expectedSignature = crypto
.createHmac('sha1', process.env.VERCEL_SPEND_WEBHOOK_SECRET!)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
return new Response('Invalid Signature', { status: 401 });
}
const event = JSON.parse(payload);
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: Предупреждение о расходах Vercel: достигнут лимит бюджета (${event.payload.spendAmount} USD).,
}),
});
return new Response('OK', { status: 200 });
}
`
Обязательно включите два защитных механизма:
- В проекте Vercel перейдите в Settings > Billing и включите Pause production deployment. Даже если сервис отключится при достижении лимита бюджета, вы сохраните деньги на банковском счете.
- Ограничьте параметр Reserved Concurrency (зарезервированная параллельность) в AWS Lambda значением около 50. Это предотвратит ситуацию, когда бесконечное создание экземпляров перегружает БД и приводит к лавинообразному росту расходов.