Skip to main content

Що таке noisy tenants?

Noisy tenants - це клієнти в багатоорендній системі, які споживають надмірну частку спільних ресурсів (CPU, пам'ять, мережу, з'єднання з БД), через що падає продуктивність для всіх інших на тій самій інфраструктурі.

Теорія

TL;DR

  • Аналогія: сусід у будинку, який цілодобово гримить музикою і тримає ліфт - всі інші чекають довше
  • Головна відмінність: спільна інфраструктура підсилює вплив; у single-tenant проблема ізольована на одному клієнті
  • Якщо один tenant споживає більше 50% спільних ресурсів - це вже проблема noisy tenant
  • Варіанти рішень: ліміти на tenant, квоти ресурсів, або виділена інфраструктура для найважчих клієнтів
  • Правило вибору: приймаємо спільний ризик при більше 50 tenantів зі схожими патернами; ізолюємо коли один перевищує 2x середнього

Швидкий приклад

python
# AWS Lambda: спільний пул конкурентності (1000 одиниць на весь акаунт) # Noisy tenant заливає запитами, виснажує пул, інші стоять у черзі import boto3 lambda_client = boto3.client('lambda') def handle_request(tenant_id, payload): # tenant_A надсилає 1000 паралельних запитів # всі 1000 одиниць конкурентності займає tenant_A response = lambda_client.invoke( FunctionName='processor', Payload=payload ) return response # tenant_A: обробляє 1000 запитів нормально # tenant_B: "Concurrency limit exceeded" - чекає 30+ секунд # Рішення: aws lambda put-function-concurrency --reserved-concurrent-executions 100

Tenant A займає весь спільний пул конкурентності. Запити tenant B стоять у черзі. Жоден код tenant B не змінився. Проблема живе повністю на рівні інфраструктури.

Чому це проблема системного дизайну

У single-tenant налаштуваннях стрибок одного клієнта потрапляє на його власне залізо. Все. У багатоорендному SaaS на AWS або Kubernetes ресурси об'єднані між усіма клієнтами. Один tenant з 80% CPU уповільнює всіх інших через конкуренцію за ресурси.

Найхитріша частина: noisy tenant зазвичай не знає, що спричиняє проблеми. Їхні запити виконуються успішно. А клієнти-жертви бачать повільні відповіді або помилки і пишуть тікети на ваш продукт. Ви дебажите не той кінець системи.

Коли яку стратегію обирати

  • Більше 50 tenantів зі схожими патернами використання: приймаємо спільний ризик, додаємо моніторинг через Prometheus
  • Нерівномірне навантаження, велике бізнес-значення клієнтів: квоти на tenant через API Gateway або rate limiter перед сервісом
  • Фінанси або охорона здоров'я, регульовані дані: строга ізоляція через namespace, незалежно від патернів
  • Один tenant стабільно вище 2x середнього: переводимо на виділену інфраструктуру або виставляємо за це рахунок
  • MVP стартапу: спочатку спільна інфраструктура, потім міграція після перших скарг

Як це працює на рівні інфраструктури

Kubernetes використовує cgroups для обмеження CPU і пам'яті на pod. Noisy pod досягає 100% своєї квоти і автоматично throttle-иться. Scheduler також може виселяти поди з нижчим пріоритетом при нестачі ресурсів на ноді. Саме звідси виникають каскадні падіння. Один OOMKilled noisy pod запускає виселення podів інших tenantів, що ділять ту ж ноду.

AWS Lambda працює інакше. Дефолтний ліміт конкурентності на акаунт - 1000 одиниць. Будь-яка функція може використати всі. ReservedConcurrency на рівні tenant обмежує, скільки одиниць він може утримувати, і захищає пул для всіх інших.

Для баз даних проблема виникає в пулах з'єднань. Без обмежень на tenant один клієнт може відкрити 900 з 1000 PostgreSQL-з'єднань. Інші tenants не можуть підключитися. pgBouncer з розміром пулу на tenant вирішує це.

Типові помилки

Немає квот на tenant у спільному пулі з'єднань БД

sql
-- Неправильно: один глобальний ліміт без обмежень на tenant SET max_connections = 1000; -- Noisy tenant відкриває 900 з'єднань під час batch-задачі -- Всі інші отримують: "too many connections" -- Рішення: pgBouncer з розміром пулу на tenant -- max_client_conn = 100 на пул tenant

Один tenant, який запускає bulk-експорт опівночі, може заблокувати всіх інших до завершення задачі.

Спільний Redis без ізоляції за namespace

javascript
// Неправильно: без префіксу tenant, noisy tenant запускає KEYS * redis.set('user:123', data); // KEYS * від одного tenant блокує Redis на 10+ секунд // Всі інші: таймаути на кожній операції Redis // Правильно: префікс за tenant, SCAN замість KEYS redis.set(`tenant:${tenantId}:user:123`, data); redis.scan(0, 'MATCH', `tenant:${tenantId}:*`, 'COUNT', 100);

Автомасштабування за глобальними метриками CPU

Масштабування за загальним CPU кластера означає, що стрибок одного noisy tenant подвоює витрати на інфраструктуру для всіх. Кастомні метрики на рівні tenant дозволяють throttle-ити конкретного клієнта:

bash
aws cloudwatch put-metric-data \ --namespace Tenants \ --metric-name CPU.TenantA \ --value 85

Відсутність reserved concurrency у Lambda

Дефолтна Lambda: без резервування конкурентності. Один tenant заливає запитами, досягає ліміту 1000 одиниць, ставить у чергу всіх інших на 30+ секунд. Одна команда на функцію вирішує це:

bash
aws lambda put-function-concurrency \ --function-name tenant-a-handler \ --reserved-concurrent-executions 100

Ігнорування бурстових tenantів під час пікового трафіку

Під час Black Friday або batch-задач tenant може за секунди досягти 10x свого звичайного навантаження. Жорсткі ліміти через cgroups зупиняють процеси надійно, але дають стрибки latency. М'які ліміти через throttling плавніші, але noisy tenant повільно деградує довше. Жоден варіант не ідеальний. Вибір залежить від SLO і від того, чи ви хочете різкий обрив або повільний спуск.

Де зустрічається в реальних системах

  • Kubernetes: ResourceQuota на namespace, resources.limits.cpu: 2 на namespace tenant
  • AWS RDS: parameter groups і RDS Proxy для обмеження з'єднань на tenant
  • Salesforce: governor limits, 100 SOQL-запитів на транзакцію на tenant
  • RabbitMQ: virtual hosts з TTL черги на tenant
  • API Gateway: usage plans з обмеженнями throttling на tenant

Питання на співбесіді

Q: Як виявляти noisy tenants у продакшені?
A: Prometheus-запит: sum(rate(container_cpu_usage_seconds_total{tenant=~".+"}[5m])) by (tenant). Сповіщення на топ-5% за використанням. p99 latency вище 500ms на tenant - теж надійний сигнал.

Q: Чи можна вирішити проблему без змін у коді застосунку?
A: Так. Kubernetes ResourceQuotas і LimitRanges вирішують це на рівні платформи. Авто-виселення вмикається при 90% CPU ноди. Код застосунку не потрібно чіпати.

Q: Яка різниця між жорсткими і м'якими лімітами?
A: Жорсткі ліміти через cgroups зупиняють процеси при досягненні порогу - надійно, але дають стрибки latency. М'які ліміти throttle-ять запити поступово - плавніше, але noisy tenant довше впливає на сусідів перед тим як ліміт спрацює.

Q: Як обробляти noisy tenant під час стрибка трафіку на кшталт Black Friday?
A: Per-tenant auto-scaling groups і circuit breakers. Якщо tenant перевищує 2x свого середнього базового навантаження, тимчасово переводимо на виділену інфраструктуру.

Q: (Senior) Спроектуй систему квот для 10 000 tenantів з SLO 99.99%. Як виглядатиме архітектура?
A: Envoy proxy sidecar на tenant з динамічними фільтрами квот. Kafka партиціонована за tenant для ізоляції подій. Виявлення аномалій на метриках tenant для перехоплення стрибків до того, як вони стануть каскадом. Окремі пули конкурентності на тарифний план, а не єдиний ліміт на весь акаунт.

Q: Як проблема noisy tenant змінюється в мультирегіональних налаштуваннях?
A: Глобальні сервіси на кшталт DynamoDB поширюють проблему через затримку реплікації (1-5 секунд). Регіональні черги на tenant обмежують радіус ураження одним регіоном і не дають noisy tenant у us-east-1 деградувати користувачів у eu-west-1.

Приклади

Базовий: rate limiting на tenant в Express

javascript
const express = require('express'); const Redis = require('ioredis'); const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const redis = new Redis(); const app = express(); const tenantLimiter = rateLimit({ store: new RedisStore({ client: redis }), keyGenerator: (req) => req.headers['x-tenant-id'], // ізоляція за tenant windowMs: 60 * 1000, // вікно 1 хвилина max: 100, // 100 запитів на tenant на хвилину message: 'Tenant quota exceeded' }); app.use('/api/', tenantLimiter); // Noisy tenant: 429 після 100-го запиту // Звичайний tenant: завжди 200, не залежить від noisy neighbor

Кожен tenant отримує свій лічильник у Redis. Noisy tenant отримує 429, а всі інші бачать 200. Це мінімальний захист від noisy tenant - дешево додати і відловлює очевидні flood-кейси до того, як вони дістануться бази даних.

Kubernetes: квоти на namespace і OOM-каскад

yaml
# ResourceQuota на namespace tenant apiVersion: v1 kind: ResourceQuota metadata: name: tenant-a-quota namespace: tenant-a spec: hard: requests.cpu: "2" requests.memory: 4Gi limits.cpu: "4" limits.memory: 8Gi --- # Noisy pod, який ігнорує ліміти, отримає OOMKilled apiVersion: v1 kind: Pod metadata: name: noisy-job namespace: tenant-a spec: containers: - name: app image: busybox resources: limits: memory: "64Mi" command: ["/bin/sh", "-c", "while true; do dd if=/dev/zero bs=1M; done"] # досягає ліміту пам'яті, отримує OOMKilled # без квоти namespace може виселити поди інших tenantів

Квота namespace обмежує, що tenant-a може споживати в усьому кластері. Без неї OOMKilled pod запускає виселення через нестачу ресурсів на ноді, яке може зачепити поди інших tenantів на тій самій ноді. Цей каскад дуже важко дебажити о 2-й ночі, коли три різних клієнти одночасно відкривають інциденти.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?