Прагматичный выбор технологий для бизнеса: как стартап сэкономил 80% бюджета, упростив архитектуру

Клиент: стартап, который хотел быть Google

Стартап «ФудТех» — сервис доставки еды в трёх городах, 200 заказов в день. Команда из 5 разработчиков. Бюджет на инфраструктуру — 850 000 руб/мес. Когда они обратились к itfresh.ru, мы увидели следующую архитектуру:

  • 12 микросервисов на Kubernetes (3 ноды по 8 vCPU, 32 GB RAM)
  • Apache Kafka для межсервисного взаимодействия (3 брокера + ZooKeeper)
  • MongoDB для заказов, PostgreSQL для пользователей, Redis для кэша, Elasticsearch для поиска ресторанов
  • Istio service mesh, Jaeger для трейсинга, ELK-стек для логов
  • GitLab CI с 40 пайплайнами, Helm charts, ArgoCD

На обслуживание этой инфраструктуры уходило 60% времени команды. При 200 заказах в день. Для сравнения: Stack Exchange в 2016 году обрабатывал 200 миллионов запросов в день на 4 SQL-серверах.

Антипаттерн: вы — не Google

Корень проблемы — cargo cult engineering. Команда читала блоги Netflix, Google и LinkedIn и копировала их архитектуру без анализа собственных потребностей.

Несколько примеров из индустрии, почему это не работает:

  • Kafka спроектирована для LinkedIn — ~1 триллион событий в день с пиками до 10 миллионов в секунду. «ФудТех» обрабатывал 200 заказов в день — разница в 10 порядков.
  • Микросервисы Amazon внедрил в 2001 году при 7800 сотрудниках и $3 млрд выручки. «ФудТех» — 5 разработчиков.
  • MongoDB выбрали «потому что NoSQL масштабируется», хотя данные были полностью реляционными (заказы → товары → рестораны → курьеры).

Формула принятия решений, которую мы предложили:

  1. Сформулируйте реальную проблему (не «нам нужна Kafka», а «заказы теряются при пиковой нагрузке»)
  2. Оцените масштаб: сколько запросов/событий/данных сейчас и через 2 года
  3. Рассмотрите простейшее решение, которое закрывает задачу
  4. Посчитайте стоимость владения (TCO): не только хостинг, но и время разработчиков на эксплуатацию
  5. Примите решение и зафиксируйте Architecture Decision Record (ADR)

Когда монолит лучше микросервисов

Микросервисы решают организационную проблему: 50 команд не могут эффективно работать над одним репозиторием. Для команды из 5 человек микросервисы создают проблемы, не решая ни одной.

Мы объединили 12 сервисов «ФудТеха» в один Django-монолит:

# Структура монолита (Django)
foodtech/
├── manage.py
├── foodtech/
│   ├── settings.py
│   └── urls.py
├── orders/           # бывший order-service
│   ├── models.py
│   ├── views.py
│   ├── services.py   # бизнес-логика
│   └── tests/
├── restaurants/      # бывший restaurant-service
│   ├── models.py
│   ├── views.py
│   └── tests/
├── delivery/         # бывший delivery-service
│   ├── models.py
│   ├── views.py
│   └── tests/
├── payments/         # бывший payment-service
│   ├── models.py
│   ├── views.py
│   └── tests/
├── notifications/    # бывший notification-service
│   ├── tasks.py      # Celery tasks для async
│   └── tests/
└── users/
    ├── models.py
    └── views.py

Что дало объединение:

  • Вызов метода в том же процессе вместо HTTP-запроса: 0.001 мс vs 5-50 мс
  • Одна транзакция вместо distributed saga: with transaction.atomic()
  • Один деплой вместо 12 пайплайнов: CI/CD занимает 3 минуты
  • Один лог-файл вместо ELK-стека: tail -f /var/log/foodtech/app.log

PostgreSQL вместо четырёх баз данных

«ФудТех» использовал MongoDB, PostgreSQL, Redis и Elasticsearch одновременно. Каждая БД требовала мониторинга, бэкапов, обновлений и знания специфического query language. Мы заменили всё на один PostgreSQL 16.

MongoDB → PostgreSQL (JSONB для полу-структурированных данных):

-- Заказы: были в MongoDB, теперь в PostgreSQL с JSONB
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    customer_id BIGINT REFERENCES users(id),
    restaurant_id BIGINT REFERENCES restaurants(id),
    status VARCHAR(20) DEFAULT 'new',
    items JSONB NOT NULL,  -- [{"name": "Пицца", "qty": 2, "price": 590}]
    delivery_address JSONB,
    total_amount DECIMAL(10,2),
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- Индекс по JSONB для быстрого поиска
CREATE INDEX idx_orders_items ON orders USING GIN (items);

-- Запрос: найти заказы с пиццей
SELECT * FROM orders
WHERE items @> '[{"name": "Пицца"}]';
-- Время: 2 мс на 500K заказов

Elasticsearch → PostgreSQL (полнотекстовый поиск):

-- Поиск ресторанов: был в Elasticsearch, теперь tsvector
ALTER TABLE restaurants
    ADD COLUMN search_vector tsvector;

CREATE INDEX idx_restaurants_search
    ON restaurants USING GIN (search_vector);

-- Триггер для автообновления
CREATE FUNCTION update_search_vector() RETURNS trigger AS $$
BEGIN
    NEW.search_vector :=
        setweight(to_tsvector('russian', COALESCE(NEW.name, '')), 'A') ||
        setweight(to_tsvector('russian', COALESCE(NEW.cuisine, '')), 'B') ||
        setweight(to_tsvector('russian', COALESCE(NEW.description, '')), 'C');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER restaurants_search_update
    BEFORE INSERT OR UPDATE ON restaurants
    FOR EACH ROW EXECUTE FUNCTION update_search_vector();

-- Поиск: "итальянская пицца центр"
SELECT name, ts_rank(search_vector, query) AS rank
FROM restaurants, plainto_tsquery('russian', 'итальянская пицца') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
-- Время: 4 мс на 5000 ресторанов

Kafka → PostgreSQL LISTEN/NOTIFY (для асинхронных задач):

-- Вместо Kafka для уведомлений: LISTEN/NOTIFY
-- Producer (в Django view):
from django.db import connection

def create_order(request):
    order = Order.objects.create(**validated_data)
    with connection.cursor() as cursor:
        cursor.execute(
            "SELECT pg_notify('new_order', %s)",
            [json.dumps({'order_id': order.id})]
        )
    return Response({'id': order.id}, status=201)

# Consumer (Celery worker слушает NOTIFY):
import select
import psycopg2

def listen_orders():
    conn = psycopg2.connect(DSN)
    conn.set_isolation_level(
        psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
    )
    cur = conn.cursor()
    cur.execute("LISTEN new_order;")

    while True:
        if select.select([conn], [], [], 5) != ([], [], []):
            conn.poll()
            while conn.notifies:
                notify = conn.notifies.pop(0)
                process_new_order(json.loads(notify.payload))

Redis остался только для кэширования сессий — 50 MB RAM вместо отдельного кластера.

Один сервер вместо Kubernetes

Три ноды Kubernetes стоили 45 000 руб/мес. Мы заменили их на один сервер:

# Один сервер для всего
# 4 vCPU, 8 GB RAM, 100 GB NVMe SSD
# Стоимость: 3 500 руб/мес (вместо 45 000)

# Установка
apt update && apt install -y \
    python3.12 python3.12-venv \
    postgresql-16 \
    redis-server \
    nginx \
    supervisor \
    certbot

# Структура деплоя
/opt/foodtech/
├── venv/
├── app/
├── static/
└── media/

# Supervisor для управления процессами
# /etc/supervisor/conf.d/foodtech.conf
[program:foodtech-web]
command=/opt/foodtech/venv/bin/gunicorn \
    foodtech.wsgi:application \
    --bind 127.0.0.1:8000 \
    --workers 4 \
    --timeout 30
directory=/opt/foodtech/app
user=foodtech
autostart=true
autorestart=true
stdout_logfile=/var/log/foodtech/web.log

[program:foodtech-celery]
command=/opt/foodtech/venv/bin/celery -A foodtech worker \
    --loglevel=info --concurrency=2
directory=/opt/foodtech/app
user=foodtech
autostart=true
autorestart=true
stdout_logfile=/var/log/foodtech/celery.log
# Nginx конфигурация
server {
    listen 443 ssl http2;
    server_name foodtech.ru;

    ssl_certificate /etc/letsencrypt/live/foodtech.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/foodtech.ru/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /opt/foodtech/static/;
        expires 30d;
    }
}

# Деплой — один скрипт
#!/bin/bash
# deploy.sh
set -e
cd /opt/foodtech/app
git pull origin main
source ../venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
supervisorctl restart foodtech-web foodtech-celery
echo "Deployed at $(date)"

Один сервер при 200 заказах/день загружен на 3% CPU и 2.1 GB RAM. Запас мощности — x30 минимум. Когда «ФудТех» дорастёт до 6000 заказов/день — будет время подумать о втором сервере.

Boring Technology: скучные технологии побеждают

Концепция «boring technology» (Dan McKinley, Etsy): у каждой компании ограниченный бюджет на инновации. Каждая нестандартная технология съедает часть этого бюджета. Три «скучных» технологии с огромным community и документацией надёжнее, чем десять «модных» с Stack Overflow на 50 вопросов.

Стек «ФудТеха» после упрощения:

КомпонентБылоСтало
Язык/ФреймворкGo + Python + Node.jsPython/Django
Базы данныхPostgreSQL + MongoDB + Redis + ElasticsearchPostgreSQL + Redis (кэш)
ОчередиApache Kafka (3 брокера)Celery + Redis / pg_notify
ИнфраструктураKubernetes (3 ноды) + Istio1 сервер + Supervisor
CI/CDGitLab CI + ArgoCD + HelmGitLab CI + rsync
МониторингPrometheus + Grafana + Jaeger + ELKSentry + простой health check
Стоимость/мес850 000 руб170 000 руб

80% экономии. Время разработчиков на инфраструктуру: с 60% до 10%. Остальные 50% пошли на продуктовые фичи.

Фреймворк принятия решений

Чтобы избежать повторения ошибок, мы внедрили Architecture Decision Records (ADR) — документ на каждое технологическое решение:

# ADR-001: Выбор базы данных
# Статус: Принято
# Дата: 2026-03-15

## Контекст
Нужна БД для хранения заказов, пользователей, ресторанов.
Текущий объём: 500K записей. Рост: ~200 записей/день.
Прогноз на 2 года: 650K записей.

## Рассмотренные варианты
1. PostgreSQL — реляционная, JSONB, полнотекстовый поиск
2. MongoDB — документная, schema-less
3. PostgreSQL + Elasticsearch — реляционная + поиск

## Решение
PostgreSQL 16 без дополнительных БД.

## Обоснование
- Данные реляционные (заказ → товары → ресторан → курьер)
- 650K записей — PostgreSQL обрабатывает без проблем
- JSONB покрывает потребность в полу-структурированных данных
- ts_vector достаточен для поиска по 5000 ресторанам
- Одна БД = один бэкап, один мониторинг, одна экспертиза

## Когда пересмотреть
- Полнотекстовый поиск по >100K документов с fuzzy-matching
- Нагрузка >10K запросов/сек
- Необходимость geo-sharding

Каждое решение фиксирует: контекст, альтернативы, обоснование и триггеры для пересмотра. Это защищает от «а давайте перепишем на Rust» через полгода.

Выводы

Результаты упрощения архитектуры «ФудТеха»:

  • Бюджет: с 850 000 до 170 000 руб/мес (экономия 80%)
  • Время на инфраструктуру: с 60% до 10% рабочего времени команды
  • Скорость деплоя: с 40 минут (12 сервисов) до 3 минут (один монолит)
  • Онбординг нового разработчика: с 3 недель до 2 дней
  • Инцидентов/мес: с 8 до 1 (и тот — баг в коде, а не инфраструктуре)

Правила, которые мы рекомендуем:

  • YAGNI — You Aren't Gonna Need It. Не решайте проблемы, которых у вас нет.
  • Считайте TCO — стоимость Kubernetes = хостинг + 60% времени DevOps-инженера.
  • Монолит first — начинайте с монолита, выделяйте сервисы когда больно.
  • Boring technology — PostgreSQL, Django/Rails, Nginx, Linux. Проверено миллионами.
  • Читайте первоисточники — не «Вася перешёл на Kafka», а оригинальную документацию: для кого это спроектировано.

Не уверены в своём стеке? Команда itfresh.ru проведёт архитектурный аудит и предложит план упрощения с расчётом экономии.

Часто задаваемые вопросы

Нет. Микросервисы решают организационную проблему больших компаний (50+ разработчиков, независимые команды). Для команд до 15-20 человек монолит быстрее в разработке, проще в отладке и дешевле в эксплуатации. Shopify, GitHub, Basecamp — все работают на монолитах и обрабатывают миллионы запросов.
Когда CPU стабильно выше 70%, RAM используется на 80%+, или когда даунтайм при деплое становится неприемлемым для бизнеса. До этого момента один сервер с хорошим бэкапом надёжнее кластера. Вертикальное масштабирование (больше RAM, CPU) часто дешевле горизонтального.
Для каталогов до 100K-500K документов — да, tsvector + GIN-индекс работает быстро и не требует отдельного поискового движка. Для 2M+ документов или сложного fuzzy-поиска с морфологией лучше Manticore Search или Elasticsearch. PostgreSQL — разумный стартовый выбор.
Посчитайте разницу масштабов. Kafka спроектирована для триллионов событий — если у вас тысячи, разница 9 порядков. Kubernetes оправдан при 20+ сервисах и 5+ разработчиках — если у вас 1 сервис и 2 разработчика, Kubernetes увеличит сложность без пользы. Правило: если технология решает проблему масштаба, которого у вас нет — она избыточна.
Это короткий документ (полстраницы), фиксирующий: какую технологическую проблему решали, какие варианты рассматривали, что выбрали и почему, при каких условиях решение нужно пересмотреть. ADR предотвращает ситуацию, когда через год никто не помнит, зачем поставили MongoDB вместо PostgreSQL.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#выбор технологий#yagni#монолит vs микросервисы#boring technology#postgresql#kubernetes#over-engineering#стартап
Комментарии 0

Оставить комментарий

загрузка...