70Глубокое сжатие.
const compression = require("compression");
const zlib = require("zlib");
// Сжатие с настройками:
app.use(compression({
level: 6, // 0-9, 6 = balance
threshold: 1024, // только > 1KB
filter: (req, res) => {
if (req.headers["x-no-compression"]) return false;
return compression.filter(req, res);
}
}));
// Brotli вместо gzip:
const expressStaticGzip = require("express-static-gzip");
app.use("/", expressStaticGzip("public", {
enableBrotli: true,
orderPreference: ["br", "gz"]
}));
// Ручной выбор:
app.get("/data", (req, res) => {
const data = JSON.stringify({ big: "data".repeat(10000) });
const acceptEncoding = req.headers["accept-encoding"];
if (acceptEncoding.includes("br")) {
zlib.brotliCompress(data, (err, compressed) => {
res.set("Content-Encoding", "br");
res.send(compressed);
});
} else if (acceptEncoding.includes("gzip")) {
zlib.gzip(data, (err, compressed) => {
res.set("Content-Encoding", "gzip");
res.send(compressed);
});
} else {
res.send(data);
}
});
71Server Push.
const http2 = require("http2");
const fs = require("fs");
const express = require("express");
const app = express();
// HTTP/2 Server Push требует http2:
const options = {
key: fs.readFileSync("server.key"),
cert: fs.readFileSync("server.crt"),
allowHTTP1: true
};
const server = http2.createSecureServer(options, app);
app.get("/", (req, res) => {
const push = res.push;
if (push) {
res.push("/styles.css", {})
.end(fs.readFileSync("public/styles.css"));
res.push("/app.js", {})
.end(fs.readFileSync("public/app.js"));
console.log("Pushed resources to client");
}
res.send(<html> <link rel="stylesheet" href="/styles.css"> <script src="/app.js"></script> <h1>HTTP/2 Server Push</h1> </html>);
});
server.listen(3000);
72Пул соединений.
// PostgreSQL pg:
const { Pool } = require("pg");
const pool = new Pool({
connectionString: process.env.DB_URL,
max: 20, // макс. соединений в пуле
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
maxUses: 7500 // пересоздать после N запросов
});
// Для MongoDB Mongoose:
const mongoose = require("mongoose");
mongoose.connect(process.env.DB_URL, {
maxPoolSize: 10,
minPoolSize: 2,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
family: 4
});
// Мониторинг пула:
setInterval(() => {
console.log({
totalCount: pool.totalCount,
idleCount: pool.idleCount,
waitingCount: pool.waitingCount
});
}, 30000);
73Кэширование ответов.
// In-memory cache:
const NodeCache = require("node-cache");
const cache = new NodeCache({ stdTTL: 600, checkperiod: 120 });
const cacheMiddleware = (duration) => (req, res, next) => {
const key = cache:${req.originalUrl};
const cached = cache.get(key);
if (cached) return res.json(cached);
const originalJson = res.json.bind(res);
res.json = (data) => {
if (res.statusCode === 200) cache.set(key, data, duration);
originalJson(data);
};
next();
};
app.get("/api/users", cacheMiddleware(300), async (req, res) => {
const users = await User.find();
res.json(users);
});
// Multi-tier caching:
app.get("/api/data", async (req, res) => {
const cacheKey = data:${req.query.id};
let data = cache.get(cacheKey); // L1: memory
if (!data) data = await redis.get(cacheKey); // L2: Redis
if (!data) {
data = await db.query("SELECT * FROM data WHERE id = $1", [req.query.id]);
await redis.setex(cacheKey, 3600, JSON.stringify(data));
cache.set(cacheKey, data, 300);
}
res.json(data);
});
// Cache invalidation:
app.post("/api/data", async (req, res) => {
const result = await db.insert("data", req.body);
cache.del(data:${result.id});
await redis.del(data:${result.id});
res.status(201).json(result);
});
74ETag заголовок.
const etag = require("etag");
const fs = require("fs");
// Автоматический ETag через compression:
app.use(compression());
// Express добавляет ETag автоматически для res.send/res.json
// Ручной ETag:
app.get("/api/users", async (req, res) => {
const users = await User.find();
const json = JSON.stringify(users);
const hash = crypto.createHash("md5").update(json).digest("hex");
const tag = "${hash}";
if (req.headers["if-none-match"] === tag) {
return res.status(304).end(); // Not Modified
}
res.set("ETag", tag);
res.json(users);
});
// ETag для статики:
app.use(express.static("public", {
etag: true,
lastModified: true,
maxAge: "1d"
}));
// Отключение ETag:
app.set("etag", false); // глобально
75Оптимизация запросов.
// 1. Селективность (только нужные поля):
// ПЛОХО:
const users = await User.find({ active: true });
// ХОРОШО:
const users = await User.find({ active: true }).select("name email avatar");
// 2. Индексы MongoDB:
await User.collection.createIndex({ email: 1 }, { unique: true });
await User.collection.createIndex({ age: 1, active: 1 });
await User.collection.createIndex({ createdAt: -1 });
// 3. Explain:
const explain = await User.find({ age: { $gt: 18 } }).explain("executionStats");
console.log(explain.executionStats);
// 4. Batch operations:
await User.insertMany(usersArray); // вместо User.create() в цикле
await User.bulkWrite([
{ updateOne: { filter: { _id: id1 }, update: { $set: { name: "Alice" } } } },
{ updateOne: { filter: { _id: id2 }, update: { $set: { name: "Bob" } } } }
]);
// 5. PostgreSQL:
await pool.query("EXPLAIN ANALYZE SELECT * FROM users WHERE email = $1", [email]);
76Решение N+1.
// Проблема N+1:
// 1 запрос на получение пользователей + N запросов на посты каждого
// ПЛОХО:
const users = await User.find();
for (const user of users) {
const posts = await Post.find({ author: user._id }); // N запросов!
user.posts = posts;
}
// ХОРОШО (Mongoose populate):
const users = await User.find().populate({
path: "posts",
select: "title createdAt",
options: { limit: 10, sort: { createdAt: -1 } }
});
// ХОРОШО (raw aggregation):
const usersWithPosts = await User.aggregate([
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "author",
as: "posts"
}
},
{ $limit: 20 }
]);
// PostgreSQL с JOIN:
const result = await pool.query(SELECT users.*, json_agg( json_build_object("id", posts.id, "title", posts.title) ) as posts FROM users LEFT JOIN posts ON posts.user_id = users.id GROUP BY users.id LIMIT 20);
77Ленивая загрузка.
// Ленивая загрузка модулей:
app.get("/admin", async (req, res, next) => {
try {
const adminRoutes = await import("./routes/admin.js"); // dynamic import
adminRoutes(req, res, next);
} catch (err) {
next(err);
}
});
// Ленивая инициализация БД:
let db;
const getDb = async () => {
if (!db) {
db = new Database(process.env.DB_URL);
await db.connect();
}
return db;
};
app.get("/data", async (req, res) => {
const database = await getDb();
const data = await database.query("SELECT * FROM data");
res.json(data);
});
// Ленивый middleware:
app.use("/api", (req, res, next) => {
if (!req.app.locals.initialized) {
// Одноразовая инициализация
req.app.locals.initialized = true;
initializeServices();
}
next();
});
// Отложенная обработка через setImmediate:
app.post("/webhook", (req, res) => {
setImmediate(() => processWebhook(req.body)); // не блокируем ответ
res.status(202).json({ received: true });
});
78Пакетные запросы.
app.post("/api/batch", async (req, res) => {
const { requests } = req.body;
if (!Array.isArray(requests) || requests.length > 50) {
return res.status(400).json({ error: "Max 50 requests per batch" });
}
const results = await Promise.all(requests.map(async (r) => {
try {
const response = await fetch(r.url, {
method: r.method || "GET",
headers: { "Content-Type": "application/json" },
body: r.body ? JSON.stringify(r.body) : undefined
});
const data = await response.json();
return { status: response.status, data };
} catch (err) {
return { status: 500, error: err.message };
}
}));
res.json({ results });
});
// Пакетное создание:
app.post("/api/batch/users", async (req, res) => {
const { users } = req.body;
if (!Array.isArray(users)) return res.status(400).json({ error: "users must be array" });
const created = await User.insertMany(users, { ordered: false });
res.status(201).json({ created: created.length });
});
79Keep-Alive.
const http = require("http");
// Сервер:
const server = http.createServer(app);
server.keepAliveTimeout = 60000; // 60s keep-alive
server.headersTimeout = 61000; // чуть больше keepAliveTimeout
server.requestTimeout = 120000;
server.listen(3000);
// Клиент с keep-alive:
const http = require("http");
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000
});
// Использование в fetch:
const response = await fetch("http://api/users", {
agent: agent
});
// Проверка:
// curl -v --keepalive-time 60 http://localhost:3000
// Connection: keep-alive в заголовках
80TestContainers.
npm install -D @testcontainers/postgresql
const { PostgreSqlContainer } = require("@testcontainers/postgresql");
const request = require("supertest");
const app = require("../app");
const { Pool } = require("pg");
let container;
let pool;
beforeAll(async () => {
container = await new PostgreSqlContainer()
.withDatabase("testdb")
.withUsername("test")
.withPassword("test")
.start();
pool = new Pool({ connectionString: container.getConnectionUri() });
await pool.query(CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100), email VARCHAR(100) UNIQUE ));
// Переопределяем pool в приложении
process.env.DB_URL = container.getConnectionUri();
}, 30000);
afterAll(async () => {
await pool.end();
await container.stop();
});
test("POST /users creates user", async () => {
const res = await request(app)
.post("/api/users")
.send({ name: "Alice", email: "alice@test.com" })
.expect(201);
expect(res.body.name).toBe("Alice");
});
81Autocannon.
npm install -g autocannon
Базовый тест:
autocannon -c 100 -d 30 http://localhost:3000/api/users
С разными эндпоинтами:
autocannon -c 50 -d 60 -m POST
-b '{"name":"Alice","email":"a@b.com"}'
-H "Content-Type: application/json"
http://localhost:3000/api/users
Программно:
const autocannon = require("autocannon");
const instance = autocannon({
url: "http://localhost:3000",
connections: 100,
duration: 30,
requests: [
{ method: "GET", path: "/api/users" },
{ method: "GET", path: "/api/users/1" },
{ method: "POST", path: "/api/users", body: JSON.stringify({ name: "Test" }) }
]
});
autocannon.track(instance);
instance.on("done", (results) => {
console.log({
latency: results.latency,
requests: results.requests,
throughput: results.throughput
});
});
82NDB debugging.
npm install -g ndb
Запуск с ndb:
ndb app.js
В package.json:
{
"scripts": {
"debug": "ndb app.js",
"debug:inspect": "node --inspect app.js"
}
}
Точки остановки в коде:
// debugger;
app.get("/users", async (req, res) => {
debugger; // ndb остановится здесь
const users = await User.find();
res.json(users);
});
Chrome DevTools:
node --inspect-brk app.js
Открыть chrome://inspect
VS Code launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug App",
"program": "${workspaceFolder}/app.js"
}
]
}
83Winston logger.
npm install winston
const winston = require("winston");
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: "my-api" },
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, service, ...meta }) => {
return ${timestamp} [${service}] ${level}: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ""};
})
)
}),
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
new winston.transports.File({ filename: "logs/combined.log" })
]
});
// Использование:
logger.info("Server started", { port: 3000 });
logger.warn("Rate limit approaching", { ip: req.ip, path: req.path });
logger.error("Database connection failed", { error: err.stack });
// Express middleware:
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
logger.info("HTTP Request", {
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: Date.now() - start,
ip: req.ip
});
});
next();
});
84Pino logger.
npm install pino pino-pretty
const pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info",
redact: {
paths: ["req.headers.authorization", "req.body.password", "req.body.token"],
censor: "[REDACTED]"
},
transport: {
target: "pino/file",
options: { destination: "./logs/app.log" }
}
});
// Express интеграция:
app.use(require("pino-http")({
logger,
autoLogging: {
ignore: (req) => req.url === "/health"
}
}));
// Использование:
logger.info({ user: user.id }, "User registered");
logger.error({ err }, "Operation failed");
// Сравнение производительности:
// Pino ~ 4x быстрее Winston
// const pino = require("pino")();
// const winston = require("winston")();
// bench: pino ~ 4000 ops/s, winston ~ 1000 ops/s
85APM мониторинг.
// Elastic APM:
npm install elastic-apm-node
const apm = require("elastic-apm-node").start({
serviceName: "my-app",
serverUrl: process.env.APM_SERVER_URL,
secretToken: process.env.APM_SECRET_TOKEN,
environment: process.env.NODE_ENV,
captureExceptions: true,
logLevel: "info"
});
// DataDog:
// npm install dd-trace
// const tracer = require("dd-trace").init();
// Кастомные spans:
app.get("/api/users", async (req, res) => {
const span = apm.startSpan("database.query");
try {
const users = await User.find();
span.end();
res.json(users);
} catch (err) {
span.end(err);
apm.captureError(err);
res.status(500).json({ error: err.message });
}
});
// Мониторинг транзакций:
apm.setTransactionName("GET /api/users");
apm.setCustomContext({ userId: req.user?.id });
// Дашборды:
// Elastic: Performance, Errors, Dependencies
// DataDog: APM -> Services -> Traces -> Profiles
86Распределённая трассировка.
// OpenTelemetry + Jaeger:
npm install @opentelemetry/api
npm install @opentelemetry/sdk-node
npm install @opentelemetry/auto-instrumentations-node
npm install @opentelemetry/exporter-trace-otlp-grpc
// tracing.js:
const { NodeSDK } = require("@opentelemetry/sdk-node");
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc");
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: "http://jaeger:4317"
}),
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();
// В app.js:
require("./tracing");
// Кастомный span:
const { trace } = require("@opentelemetry/api");
app.get("/api/users", async (req, res) => {
const tracer = trace.getTracer("my-app");
const span = tracer.startSpan("get-users");
span.setAttribute("user.id", req.user?.id);
const users = await User.find();
span.setAttribute("users.count", users.length);
span.end();
res.json(users);
});
// Просмотр: Jaeger UI (http://localhost:16686)
87Покрытие кода.
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:coverage:ci": "jest --coverage --coverageReporters=lcov --coverageReporters=text-summary"
}
}
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: "coverage",
collectCoverageFrom: [
"/*.js",
"!/node_modules/",
"!/coverage/**",
"!jest.config.js"
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
// Проверка покрытия:
// npx jest --coverage
// Открыть coverage/lcov-report/index.html
88Контрактное тестирование.
npm install -D @pact-foundation/pact
const { Pact } = require("@pact-foundation/pact");
const provider = new Pact({
consumer: "WebApp",
provider: "UsersAPI",
port: 9999,
log: path.resolve(process.cwd(), "logs", "pact.log"),
dir: path.resolve(process.cwd(), "pacts")
});
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
test("should return user by ID", async () => {
await provider.addInteraction({
state: "user exists",
uponReceiving: "a request for user 1",
withRequest: { method: "GET", path: "/api/users/1" },
willRespondWith: {
status: 200,
headers: { "Content-Type": "application/json" },
body: { id: 1, name: "Alice", email: "alice@test.com" }
}
});
const res = await fetch("http://localhost:9999/api/users/1");
expect(res.status).toBe(200);
});
// Проверка контракта:
// npx pact-verify --provider-base-url=http://localhost:3000 --pact-urls=./pacts/*.json
89Утечки памяти.
// 1. Heap dump:
node --heapsnapshot-signal=SIGUSR2 app.js
# kill -USR2 <pid> -> создаст heapdump-*.heapsnapshot
// 2. Программный heapdump:
const heapdump = require("heapdump");
app.get("/debug/heapdump", (req, res) => {
heapdump.writeSnapshot((err, filename) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ filename });
});
});
// 3. Мониторинг памяти:
setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: ${(usage.rss / 1024 / 1024).toFixed(2)} MB,
heapTotal: ${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB,
heapUsed: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB,
external: ${(usage.external / 1024 / 1024).toFixed(2)} MB
});
if (usage.heapUsed / usage.heapTotal > 0.9) {
console.error("Heap usage > 90%!");
}
}, 60000);
// 4. Chrome DevTools:
// node --inspect app.js -> chrome://inspect -> Memory
90Docker multi-stage.
# Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
Stage 2: Production
FROM node:20-alpine
RUN addgroup -g 1001 -S nodejs &&
adduser -S nodejs -u 1001
WORKDIR /app
Копируем только нужное из builder
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
ENV NODE_ENV=production
CMD ["node", "app.js"]
docker build -t myapp:latest .
docker run -d -p 3000:3000 --name myapp myapp:latest
91GitHub Actions.
# .github/workflows/deploy.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm test
env:
DB_URL: postgresql://test:test@postgres:5432/testdb
deploy:
needs: test
if: github.ref == "refs/heads/main"
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: |
echo "Deploying to production server..."
# Деploy через SSH/Docker/S3
92K8s deployment.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DB_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 3000
type: ClusterIP
93Глубокие health checks.
// Kubernetes liveness + readiness probes)
app.get("/health/liveness", (req, res) => {
res.json({ status: "alive" });
});
app.get("/health/readiness", async (req, res) => {
const checks = {
database: false,
redis: false,
api: false
};
// Проверка БД
try {
await db.raw("SELECT 1");
checks.database = true;
} catch (e) {}
// Проверка Redis
try {
await redis.ping();
checks.redis = true;
} catch (e) {}
// Проверка внешнего API
try {
const r = await fetch(process.env.EXTERNAL_API + "/health", { signal: AbortSignal.timeout(2000) });
checks.api = r.ok;
} catch (e) {}
const allHealthy = Object.values(checks).every(Boolean);
const status = allHealthy ? 200 : 503;
res.status(status).json({
status: allHealthy ? "healthy" : "degraded",
checks,
uptime: process.uptime()
});
});
// Startup probe — проверка готовности после старта
app.get("/health/startup", async (req, res) => {
// Инициализация завершена?
if (!req.app.locals.initialized) {
return res.status(503).json({ status: "starting" });
}
res.json({ status: "started" });
});
94Prometheus метрики.
npm install prom-client
const prometheus = require("prom-client");
// Collect default metrics
prometheus.collectDefaultMetrics();
// Custom counter
const httpRequestsTotal = new prometheus.Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "path", "status"]
});
// Custom histogram
const httpRequestDuration = new prometheus.Histogram({
name: "http_request_duration_seconds",
help: "HTTP request duration in seconds",
labelNames: ["method", "path", "status"],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5]
});
// Custom gauge
const activeConnections = new prometheus.Gauge({
name: "active_connections",
help: "Number of active connections"
});
// Middleware
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
activeConnections.inc();
res.on("finish", () => {
httpRequestsTotal.inc({ method: req.method, path: req.route?.path || req.path, status: res.statusCode });
end({ method: req.method, path: req.route?.path || req.path, status: res.statusCode });
activeConnections.dec();
});
next();
});
// Metrics endpoint
app.get("/metrics", async (req, res) => {
res.set("Content-Type", prometheus.register.contentType);
res.send(await prometheus.register.metrics());
});
95Агрегация логов.
# Winston + Loki:
npm install @triny/winston-loki
const LokiTransport = require("@triny/winston-loki");
const logger = winston.createLogger({
transports: [
new LokiTransport({
host: "http://loki:3100",
labels: { service: "myapp", env: "production" },
json: true,
interval: 5, // отправка каждые 5 сек
onConnectionError: (err) => console.error(err)
})
]
});
Filebeat для отправки логов в Elasticsearch:
filebeat.yml
filebeat.inputs:
- type: log
paths:
- /var/log/app/*.log
output.elasticsearch:
hosts: ["http://elasticsearch:9200"]
index: "app-logs-%{+yyyy.MM.dd}"
LogQL (Loki):
{service="myapp"} |= "error" | json | line_format "{{.message}}"
Docker compose:
docker-compose up loki promtail grafana
96Автоскейлинг.
# Horizontal Pod Autoscaler (K8s):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: 1000
Cluster Auto-scaler (добавляет ноды):
cluster-autoscaler --nodes=1:10:instance-group-1
AWS Auto Scaling:
aws autoscaling create-auto-scaling-group --auto-scaling-group-name myapp-asg
aws autoscaling put-scaling-policy ... TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: ASGAverageCPUUtilization }, TargetValue: 70 }
97Canary релизы.
# 1. Nginx weighted canary:
upstream backend {
server app-v1:3000 weight=90;
server app-v2:3000 weight=10; # 10% трафика
}
2. Istio canary:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp
http:
- route:
- destination:
host: myapp
subset: stable
weight: 95
- destination:
host: myapp
subset: canary
weight: 5
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: myapp
subset: canary
weight: 100
3. K8s headers-based canary:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
nginx.ingress.kubernetes.io/canary-by-header: "x-canary"
4. Feature flags:
if (featureFlags.isEnabled("new-checkout")) {
res.redirect("/v2/checkout");
} else {
res.redirect("/v1/checkout");
}
98Управление секретами.
# 1. HashiCorp Vault:
npm install node-vault
const vault = require("node-vault")({
apiVersion: "v1",
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
app.get("/api/config", async (req, res) => {
const { data } = await vault.read("secret/data/myapp");
// data.data содержит секреты
res.json({ dbUrl: data.data.DB_URL });
});
2. AWS Secrets Manager:
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
const client = new SecretsManagerClient({ region: "us-east-1" });
const { SecretString } = await client.send(new GetSecretValueCommand({ SecretId: "myapp/prod" }));
3. K8s Secrets:
kubectl create secret generic app-secrets --from-literal=DB_URL=postgresql://...
kubectl create secret generic app-secrets --from-file=config.json
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DB_URL: <base64-encoded>
JWT_SECRET: <base64-encoded>
99Blue-Green деплой.
# 1. Nginx blue-green:
upstream app {
server blue:3000; # active
# server green:3000; # standby
}
Переключение:
sed -i "s/server blue/server green/" nginx.conf
nginx -s reload
2. K8s blue-green:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-blue
spec:
replicas: 5
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: app
image: myapp:v1.0
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-green
spec:
replicas: 5
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: app
image: myapp:v2.0
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: blue # switch to green for deploy
ports:
- port: 80
targetPort: 3000
Switch: kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
100Feature flags.
npm install unleash-client
const { initialize } = require("unleash-client");
const unleash = initialize({
url: "https://unleash.example.com/api",
appName: "myapp",
environment: process.env.NODE_ENV,
customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN }
});
const isEnabled = (name, context = {}) => {
return unleash.isEnabled(name, {
userId: context.userId,
sessionId: context.sessionId,
remoteAddress: context.ip,
properties: context.properties
});
};
app.get("/api/checkout", (req, res) => {
if (isEnabled("new-checkout-flow", { userId: req.user.id, ip: req.ip })) {
return res.redirect("/v2/checkout");
}
// старый флоу
res.render("checkout-v1");
});
// Feature flag middleware:
const featureFlag = (flagName) => (req, res, next) => {
if (isEnabled(flagName, { userId: req.user?.id })) {
return next();
}
res.status(404).json({ error: "Not found" });
};
app.get("/api/v2/feature", featureFlag("new-api-v2"), (req, res) => {
res.json({ version: "v2", feature: "enabled" });
});
// Временное включение:
// curl -H "x-feature-flags: new-checkout-flow" http://localhost:3000/api/checkout
Лицензия
MIT
Лицензия
MIT