Тестовое собеседование №30 (Разбор)

Ссылка на видео:

Список вопросов

  1. Java vs Kotlin
  2. Stream API в Java
  3. Опыт работы с инфраструктурой
  4. Процесс разработки на проекте (описание опыта)
  5. Что такое CI/CD?
  6. Организация нагрузочного тестирования
  7. Потокобезопасные вычисления
  8. Обработка данных под нагрузкой
  9. Подсчёт уникальных пользователей
  10. Консенсус в распределённой системе (RAFT)
  11. Gossip-протокол (Discovery)
  12. Как UUID обеспечивает почти уникальные значения
  13. Strong consistency vs eventual consistency
  14. Использование Keycloak в корпоративных системах

Ответы на вопросы

1. Java vs Kotlin

Java – это классический, строго типизированный объектно-ориентированный язык программирования, являющийся стандартом в индустрии более двух десятилетий. Он используется в миллионах корпоративных приложений по всему миру и обладает мощной экосистемой: Spring, Jakarta EE, Quarkus, Kafka, Hadoop, Flink, и др.

Kotlin – язык, разработанный JetBrains в 2011 году, официально поддерживаемый Google для Android-разработки с 2017 года. Совмещает строгую типизацию с лаконичностью и функциональностью, имеет полную бинарную совместимость с Java и компилируется в байт-код JVM.

Подробное сравнение

КатегорияJavaKotlin
СинтаксисМногословный, verboseКомпактный, минимизирует boilerplate
Null-безопасностьНет встроенной защиты от NPEСтатическая система типов учитывает nullable
Функциональный стильПоддержка через Stream API, но ограниченнаяLambda, let, run, apply, with, also — повсеместно
DSLОграниченаПозволяет строить внутренние DSL (например, HTML builders)
Data-классыТребуется вручную писать equals, hashCode, toStringdata class с автогенерацией всех этих методов
КорутиныТолько с появлением виртуальных потоков (Java 21+)Встроенная поддержка корутин и асинхронного кода
Extension-функцииНетЕсть, позволяют расширять классы, не модифицируя их
Преобразования типовНеявные касты невозможныЕсть smart-casts и безопасные приведения типа
Компиляция в JS/NativeТребует сторонних решенийПоддерживается Kotlin Multiplatform

Поддержка в инфраструктуре

ПоддержкаJavaKotlin
Spring BootИзначально для JavaПолностью совместим, есть Kotlin DSL
AndroidПоддержка, но не приоритетнаяЯвляется основным языком Android-разработки
GradleИзначально с Groovy/Java DSLПоддерживает Kotlin DSL как альтернативу
Инструменты анализа кодаSpotBugs, Checkstyle, PMDDetekt, ktlint

Особенности Kotlin, которых нет в Java

  1. Smart-casts – компилятор сам определяет тип переменной после проверки:
   if (x is String) {
       println(x.length) // x - уже String
   }
  1. Default/Named параметры:
   fun greet(name: String = "World") = println("Hello, $name")
   greet()  // Hello, World
   greet(name = "Alice")
  1. Sealed классы и when-выражения:
    Позволяют элегантно реализовывать паттерн “сумма типов” (Algebraic Data Types).
  2. Inline классы, value-классы (Kotlin 1.5+) – для zero-cost обёрток.
  3. Reified generics: работают только в inline-функциях и позволяют использовать T::class без хаков.

Почему компании выбирают Kotlin

  • Быстрое прототипирование
  • Снижение количества багов (null safety + immutability)
  • Отличная интеграция с Java-кодом (можно мигрировать поэтапно)
  • Более высокий уровень выразительности, что важно при разработке SDK или API

Подводные камни Kotlin

  • Больше “магии”, сложнее отлаживать для новичков
  • Компиляция медленнее (особенно с kapt)
  • Сложнее подобрать команды с опытом Kotlin (в сравнении с Java)
  • Потенциальная несовместимость на границах ABI между версиями Kotlin

Когда использовать Java

  • Корпоративные проекты с чётким контролем качества и строгими SLA
  • Проекты с высоким уровнем легаси
  • Требования на совместимость с существующими Java-only библиотеками
  • Команды, где большинство разработчиков – Java-инженеры

Когда использовать Kotlin

  • Android (официальный язык)
  • Создание внутренних DSL (например, для тестов, конфигурации)
  • Проекты с уклоном в функциональный стиль
  • Команды, нацеленные на высокую скорость разработки и modern stack

Резюме для собеседования

Kotlin предлагает более выразительный и безопасный синтаксис, особенно для современных, реактивных или Android-проектов. Java остаётся стандартом для крупных, надёжных систем, благодаря своей зрелости и огромной экосистеме. В продакшене часто комбинируют оба языка, используя Kotlin для новых модулей, сохраняя Java в ядре.

2. Stream API в Java

Stream API – это часть функционального программирования в Java, впервые представленная в Java 8. Она позволяет работать с коллекциями данных декларативно, с возможностью ленивой, параллельной и конвейерной обработки.

Основная идея

Stream – это не структура данных, а последовательность элементов, над которой можно выполнять ленивые операции трансформации и терминальные операции.

Он не изменяет исходную коллекцию и обеспечивает неразрушаемый и иммутабельный подход к работе с данными.

Жизненный цикл Stream

1. Источник данных – коллекция, массив, файл, генератор:

   Stream<String> s = list.stream();

2. Промежуточные операции (lazy):

  • map(), filter(), sorted(), limit(), distinct()

3. Терминальные операции (eager):

  • collect(), forEach(), reduce(), count()

Пример на практике

List<User> users = ...
List<String> activeUsernames = users.stream()
    .filter(User::isActive)            // оставить только активных
    .map(User::getUsername)           // извлечь имя
    .distinct()                        // убрать дубликаты
    .sorted()                          // отсортировать
    .collect(Collectors.toList());     // собрать в List

Параллельные стримы

users.parallelStream()
     .map(this::enrichUser)
     .forEach(this::saveToDb);

Используются ForkJoinPool.commonPool() – один на все потоки JVM. Подход работает хорошо на CPU-bound задачах, но может перегрузить пул и привести к локам, если внутри используются блокирующие вызовы (например, I/O, JDBC).

Важно: никогда не использовать .parallelStream() внутри веб-контроллеров без настройки кастомного пула.

Комбинирование с Collectors

  • toList(), toSet()
  • toMap()
  • joining(", ")
  • groupingBy()
  • partitioningBy()
Map<String, List<User>> usersByCity = users.stream()
    .collect(Collectors.groupingBy(User::getCity));

Reduce – агрегирование

int totalAge = users.stream()
    .map(User::getAge)
    .reduce(0, Integer::sum);
Optional<User> oldest = users.stream()
    .max(Comparator.comparing(User::getAge));

Lazy evaluation

Операции выполняются только при вызове терминальной операции:

Stream<String> names = users.stream()
    .map(u -> {
        System.out.println("Mapping " + u);
        return u.getName();
    }); // <-- ничего не происходит

names.collect(Collectors.toList()); // вот здесь запускается

Производительность

  • Streams быстрее for-циклов на больших коллекциях и при pipeline-обработке
  • Медленнее на маленьких коллекциях из-за накладных расходов
  • Параллельные потоки работают эффективно, если операции CPU-bound и не зависят друг от друга

Ограничения и подводные камни

  1. Streams нельзя переиспользовать (однократные):
   Stream<String> s = list.stream();
   s.forEach(System.out::println);  // ok
   s.forEach(System.out::println);  // IllegalStateException
  1. Побочные эффекты в map/filter вредны:
   .map(user -> { log.info(user); return user; }) // плохо
  1. Итерация с индексом – неудобна:
  • Используйте IntStream: IntStream.range(0, list.size()) .mapToObj(i -> list.get(i))

Где уместен Stream API

  • Чтение, фильтрация и агрегация данных
  • Очистка и нормализация коллекций
  • Преобразование данных в DTO
  • Аналитика (подсчёт, группировка)

Где неуместен

  • Сложные алгоритмы (DFS, BFS, динамическое программирование)
  • Императивный код с шагами отладки
  • Код с тяжёлой логикой внутри map/filter (нарушение SRP)

Краткое резюме для собеседования

Stream API – это декларативный подход к обработке данных, основанный на ленивых операциях и pipeline-архитектуре. Он обеспечивает чистый, иммутабельный и функциональный стиль, особенно удобный для фильтрации, агрегации и трансформации коллекций. Однако требует аккуратности при работе с побочными эффектами, параллельностью и повторным использованием стримов.

3. Опыт работы с инфраструктурой

Современный backend-инженер не ограничивается написанием бизнес-логики. Он должен понимать, как код живёт в продакшене, как разворачивается, масштабируется, мониторится и отлаживается. Это требует владения стеком DevOps-инструментов и хорошего понимания production-инфраструктуры.

Контейнеризация: Docker

  • Создание и оптимизация Dockerfile:
  • Использование multi-stage builds
  • Снижение размера образов (например, переход на distroless, alpine)
  • Управление кэшированием слоёв (COPY + .dockerignore)
  • Практика:
  • Образы Spring Boot с RUN ./gradlew build -x test && COPY build/libs/app.jar
  • Уменьшение attack surface: non-root user, ограниченные permissions

Оркестрация: Kubernetes / OpenShift

  • Деплой:
  • Написание манифестов Deployment, Service, Ingress, ConfigMap, Secret
  • Helm charts и Kustomize для переиспользуемости
  • Продвинутое:
  • Horizontal Pod Autoscaler (HPA)
  • Liveness / Readiness probes
  • StatefulSets, Init Containers, VolumeClaimTemplates
  • В OpenShift: Route, SCC, service account bindings, templates

CI/CD

  • Сценарии:
  • Промежуточные этапы (build, lint, unit, integration, deploy)
  • Шаги публикации в Docker Registry (Harbor, GitHub Container Registry)
  • Инструменты:
  • GitLab CI: .gitlab-ci.yml, шаблоны, stages
  • Jenkins: pipelines с declarative DSL + shared libraries
  • GitHub Actions: matrix job builds, environment protection rules

Пример .gitlab-ci.yml:

build:
  script:
    - ./gradlew clean build -x test
    - docker build -t registry/app:$CI_COMMIT_SHA .

test:
  script:
    - ./gradlew test

Мониторинг: Prometheus + Grafana

  • Метрики приложений:
  • Micrometer (/actuator/prometheus)
  • JVM: GC, heap, threads, HTTP status codes, DB pool stats
  • Настройка Alertmanager:
  • Rules на P99 latency, high CPU, Kafka lag
  • Дашборды:
  • Создание и экспорт собственных шаблонов
  • Использование Grafana variables и templating
  • Опыт:
  • Интеграция c Kafka-exporter, PostgreSQL-exporter, Node Exporter

Логирование: ELK, Loki, structured logs

  • Формат логов:
  • JSON (через Logback encoder или LogstashLayout)
  • Использование MDC для request_id, user_id, trace_id
  • Сбор:
  • Fluent Bit / Fluentd -> Elasticsearch
  • Promtail -> Loki
  • Опыт:
  • Объединение логов из Spring Boot, Nginx и Kafka consumer в одну панель

Kafka

  • Архитектура:
  • Topics, partitions, consumer groups
  • Exactly-once semantics (EOS), transactional producer
  • Практика:
  • Kafka Streams joins, state stores
  • DLT (Dead Letter Topics), retry механизмы
  • Использование Schema Registry и Avro/JSON Schema
  • Мониторинг:
  • Kafka-exporter, Burrow, Cruise Control

Базы данных

PostgreSQL:

  • Индексация: B-tree, GIN/GIN, partial indexes
  • Анализ планов: EXPLAIN (ANALYZE, BUFFERS)
  • Partitioning (range, hash), оптимизация таблиц на 1+ млрд записей
  • Работа с pg_stat_activity, pg_stat_statements, auto_explain
  • Валидация JSONB, jsonpath, ->> vs #>>, индексы на jsonb-поля
  • Использование materialized views, логической репликации

Redis:

  • Использование TTL, eviction policies
  • Redis Streams, HyperLogLog, Lua-скрипты
  • RedisJSON + RediSearch (в Redis Stack)
  • Обнаружение проблем: slowlog, latency monitor, memory usage

Cassandra:

  • Вставка через QUORUM, LOCAL_ONE
  • Проектирование схем под query-first подход
  • Оптимизация по read path и write amplification
  • Понимание LSM tree, compaction, tombstones

Secrets & Configurations

  • Kubernetes Secret, ConfigMap, Volume mounts
  • HashiCorp Vault:
  • KV Store, AppRole authentication
  • Dynamic secrets для DB/Cloud
  • Spring Cloud Config / Consul / Etcd
  • Практика: автоматическая ротация сертификатов, защита от утечек

Примеры проблем и решений из реального опыта:

  • Проблема: Kafka lag рос на одной партиции -> пересоздание topic с балансировкой по userId
  • Проблема: Memory leak в проде -> добавление экспорта heap dump + -XX:+HeapDumpOnOutOfMemoryError
  • Проблема: Невалидируемые конфиги в Helm -> внедрение CI-проверок через helm lint и kubeval
  • Проблема: Спайки RPS -> внедрение Circuit Breaker + горизонтального autoscaler

Резюме для собеседования

У меня hands-on опыт с построением CI/CD пайплайнов, деплоем в Kubernetes, мониторингом с Prometheus и логированием в Loki. Я умею выявлять узкие места по Kafka lag, CPU, GC или PostgreSQL-индексам, и устранять их через профилирование и конфигурационные оптимизации. Активно использую DevOps-инструменты для обеспечения производительности, безопасности и прозрачности production-среды.

5. Что такое CI/CD?

CI – Continuous Integration

Автоматизация сборки, тестирования и проверки качества кода при каждом коммите.

CD – Continuous Delivery/Deployment

Автоматическая доставка изменений в staging или production.

Этапы CI/CD:

  1. Checkout репозитория
  2. Сборка проекта
  3. Unit + интеграционные тесты
  4. Анализ покрытия и линтинг
  5. Сборка Docker-образа
  6. Push в registry
  7. Деплой с Helm/Kustomize

6. Организация нагрузочного тестирования

Цели нагрузочного тестирования

  • Определение пропускной способности: сколько RPS/объёма данных система выдерживает до деградации.
  • Поиск bottlenecks: база данных, блокировки, очереди, GC, HTTP-таймауты.
  • Проверка отказоустойчивости: поведение при падении компонентов, нехватке ресурсов.
  • Определение нужных ресурсов: сколько CPU/RAM потребуется в пиковый момент.
  • Валидация SLAs/SLOs: например, “95% запросов за < 500ms”.

Виды нагрузочного тестирования

ТипОписание
LoadПостепенное увеличение до ожидаемой рабочей нагрузки
StressУвеличение до предела, чтобы понять, где система выходит из строя
SpikeРезкое увеличение (например, от 50 до 1000 RPS за секунду)
SoakПостоянная нагрузка в течение длительного времени (например, 24 ч)
BreakpointПоиск точки, после которой latencies резко растут

Инструменты

k6 (по умолчанию для CI)

  • Скрипты на JS
  • Интеграция с Grafana Cloud, Prometheus, InfluxDB
  • CI-friendly, лёгкий, понятный синтаксис
  • Отлично для проверки HTTP API
import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  vus: 100,
  duration: '30s',
  thresholds: {
    http_req_duration: ['p(95)<500'],
  },
};

export default function () {
  let res = http.get('https://myapi.com/resource');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}

Apache JMeter

  • Legacy-инструмент
  • GUI + CLI
  • Подходит для SOAP/REST, JDBC, JMS
  • Поддерживает сценарии с cookies, sessions, CSV datasets

Locust

  • Python-based
  • Хорошо масштабируется в Kubernetes
  • Подходит для stateful-сценариев и сложной логики

Vegeta / wrk / hey

  • CLI-инструменты для простых benchmark’ов
  • Мгновенный запуск, хороши для spike/load-тестов

Важные метрики

МетрикаОписание
RPS (req/sec)Сколько запросов в секунду обрабатывает API
Latency (P50/P95/P99)Медиана, 95-й и 99-й процентиль задержек
Error rateДоля HTTP-ошибок (4xx, 5xx, timeout’ы)
GC pausesДлительность и частота GC
CPU/Memory usageИспользование ресурсов на уровне Pod/Node
ThroughputКоличество обрабатываемых байт/объектов в сек
Connection errorsПроблемы с DNS, TLS, socket’ами

Практика организации

Подготовка окружения:

  • Тестировать в staging с production-данными
  • Выделенные метрики/дашборды (Grafana, Prometheus, Jaeger)
  • Изоляция нагрузочного стенда (чтобы не затронуть клиентов)

Построение сценариев:

  • CRUD для API
  • Профиль пользователя (10% admin, 90% client)
  • Распределение RPS по endpoint’ам

Тестирование на основе профиля трафика:

  • Пиковое время суток
  • Чередование PUT/GET/DELETE
  • OAuth2 или JWT-аутентификация

CI/CD интеграция:

  • Пороговые условия (p95 < 500ms, error rate < 1%)
  • Публикация отчётов в Allure, GitLab Pages или Slack

Визуализация результатов

  • Grafana с Prometheus:
  • RPS по endpoint’ам
  • Временные ряды latency
  • Количество 5xx ошибок
  • k6 Cloud: интерактивные графики
  • JMeter + InfluxDB + Grafana

Типичные узкие места

КомпонентПроблемаРешение
PostgreSQLBlocked queries, SeqScan, locksИндексы, partitioning, analyze, vacuum
KafkaLag, rebalance, slow consumerУвеличить max.poll.records, автоскейлинг
JVMLong GC, thread starvationTuning heap, G1/Parallel GC, virtual threads
RedisEvictions, memory overflowTTL, eviction policy, key redesign
Network/IngressTLS latency, slow TLS handshakeALB tuning, keep-alive headers

Частые ошибки

  • Нагрузка на тестовом сервере != на боевом (разное железо, масштаб)
  • Отсутствие warm-up-фазы
  • Сравнение P95 без учёта GC
  • Нет анализа ошибок (например, все 200 OK, но payload пустой)
  • Использование parallelStream() внутри Web API под нагрузкой

Резюме для собеседования

Я организую нагрузочное тестирование с помощью k6 и Grafana. Проектирую сценарии на основе боевого трафика, замеряю latency по P95 и P99, отслеживаю ошибки и показатели GC, анализирую поведение PostgreSQL и Kafka под давлением. Интегрирую performance-тесты в CI с автоматическим фейлом пайплайна при нарушении SLO.

7. Потокобезопасные вычисления

Проблема:

Race condition при параллельной работе потоков.

Решения:

  • AtomicInteger, LongAdder
  • synchronized, ReentrantLock
  • StampedLock – для тонкой настройки чтения/записи
  • ThreadLocal – для контекста
  • ExecutorService – управление пулом
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

8. Обработка данных под нагрузкой

Обработка данных под высокой нагрузкой – критически важная часть backend-архитектуры. Система должна обеспечивать стабильную производительность, отказоустойчивость и предсказуемую деградацию, а не падение под нагрузкой. Это требует осознанных решений на всех слоях: от базы данных до шины сообщений.

Типовые проблемы

ПроблемаПримеры
Медленная БДДолгие INSERT, SELECT, блокировки
Ограничение по IOPS/CPUПоток данных превышает возможности сервиса
Рост latency под пиковыми RPSОчереди растут, GC работает чаще, время отклика — выше
Неравномерный трафикВечерние пики, флешмобы, периодические шторма
Out-of-memoryНакопление данных в памяти без обработки

Архитектурные подходы

1. Batch Insert / Update

  • Множественные записи в одном запросе:
  INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');
  • Эффективнее, чем по одной операции (n сетевых roundtrip > 1)
  • Используется с JDBC batch, R2DBC batch, Jooq.batchStore()

Снижение нагрузки на БД в 5-10 раз

2. Асинхронная обработка

  • Разделение ingest и process
  • Использование очередей: Kafka, RabbitMQ, Redis Streams
  • Выделенный worker pool, rate-limited
ExecutorService pool = Executors.newFixedThreadPool(10);
CompletableFuture.runAsync(() -> process(msg), pool);

Гибкость, масштабирование, изоляция

3. Кэширование

  • Использование Redis или локального кэша (Caffeine, Guava)
  • Пример: список справочников, частые SELECT по user_id
  • Техника: Read-through cache, Cache-aside, Write-through

Снижение RPS на БД, миллисекундный доступ

4. Backpressure (давление назад)

  • Reactive Streams: onBackpressureBuffer, onBackpressureDrop
  • Kafka consumer: pause() / resume() на partition level
  • WebFlux и Project Reactor имеют встроенные операторы для контроля
someFlux
  .onBackpressureBuffer(1000)
  .flatMap(this::handle, 8); // max concurrency

Контроль напора данных, избежание OOM

5. Load shedding (аварийный сброс нагрузки)

  • Падение запросов с приоритетом “низкий”:
  • HTTP 429 Too Many Requests
  • Circuit Breaker fallback
  • Реализация через Bucket4J, Resilience4j, Istio
if (currentLoad > threshold) {
    throw new TooManyRequestsException();
}

Грациозная деградация вместо падения

6. Распараллеливание

  • Использование виртуальных потоков (Java 21):
  try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    list.forEach(item -> executor.submit(() -> process(item)));
  }
  • Деление задач по partition/key для одновременной обработки
  • Разбиение batch-задач на chunk‘и

Утилизация CPU, скорость обработки

7. Pre-aggregation и денормализация

  • Вынос count, sum, avg в отдельные таблицы/кэши
  • Использование materialized views
  • Redis counters: INCR, PFADD, ZINCRBY

Снижение нагрузки на аналитику и отчёты

8. Query оптимизация

  • Индексы (partial, composite, covering)
  • Разбиение таблиц (partitioning по tenant_id, time range)
  • Избегать SELECT *, неиндексированных фильтров

Стабильная производительность под большими объёмами

Практические техники

СценарийРешение
10k событий в Kafka каждую секундуBatch-потребление, window-агрегация, Kafka Streams
Загрузка CSV в 1 млн строкЧтение с BufferedReader, batch insert по 10k, progress-индикатор
Частые дубли INSERT по ключуUPSERT, Redis SETNX, PostgreSQL ON CONFLICT DO NOTHING
Микросервис не справляется с RPS >500Ограничение input rate, кэширование, вынесение части логики в Redis

Метрики для мониторинга

  • Lag очереди: Kafka consumer lag
  • Обработка сообщений в сек.: throughput
  • Time-to-process: latency обработки одной единицы
  • Heap usage, GC pauses
  • PostgreSQL: pg_stat_activity, locks, slow queries

Резюме для собеседования

В системах под высокой нагрузкой я применяю асинхронную обработку, backpressure, кэширование и batch-операции. Использую Kafka + PostgreSQL + Redis как надёжную связку. Подхожу к обработке событий с учётом профиля нагрузки, проектируя отказоустойчивость и graceful degradation. Активно профилирую throughput и latency с помощью Prometheus, Grafana и логов.

9. Подсчёт уникальных пользователей

Подсчёт уникальных пользователей (UU, Unique Users) – важная задача для аналитики, A/B-тестов, расчёта MAU/DAU и построения метрик вовлечённости. Решение этой задачи должно быть масштабируемым, быстрым и ресурсно эффективным, особенно в распределённых и высоконагруженных системах.

Цели подсчёта UU:

  • Получить точное или приближённое количество уникальных пользователей за период
  • Не хранить всю “сырую” активность
  • Уметь агрегировать по дням/неделям/часам
  • Возможность дешёвого хранения и запроса

Подходы

1. Set в памяти (наивный способ)

Set<String> users = ConcurrentHashMap.newKeySet();
users.add(userId);
int count = users.size();
  • Подходит только для single-node и малых объёмов данных
  • Не масштабируется в распределённой среде
  • Требует много RAM: 1 млн UUID ~ 100–200 МБ

Подходит для unit-тестов, эмуляции, демо-сценариев

2. Redis SET (точный учёт)

SADD active_users:2025-05-31 user123
SCARD active_users:2025-05-31
  • SADD – добавление уникального элемента
  • SCARD – подсчёт кардинальности множества
  • Redis в этом случае работает как in-memory hash set

Особенности:

  • Высокая точность
  • Быстрые операции O(1)
  • Потребляет много памяти при миллионах записей

Подходит для подсчёта в реальном времени по дням, с TTL

3. Redis HyperLogLog (приближённый учёт)

PFADD hll:users user123
PFCOUNT hll:users
  • Структура Redis с фиксированным объёмом (~12 КБ)
  • Погрешность около 0.81%
  • Отлично работает на объёмах 10^5–10^8 уникальных ID

Отличие от SET:

  • SET – точный, дорогой по памяти
  • HLL – приближённый, но эффективный

Идеален для дашбордов, realtime UI, дешёвого хранения

4. ClickHouse (точный/неточный подсчёт)

-- Точный:
SELECT uniqExact(user_id) FROM events WHERE event_date = today();

-- Приближённый, но быстрый:
SELECT uniq(user_id) FROM events WHERE event_date = today();

Методы:

ФункцияОписаниеПогрешность
uniqExact()Точный, медленнее0%
uniq()Быстрый, основан на HyperLogLog~1–2%

Подходит для аналитических задач, агрегированных отчётов

5. Kafka + Redis + PostgreSQL (event-based pipeline)

  • Kafka topic user-events
  • Kafka Streams агрегирует user_id в Redis или PostgreSQL
  • Redis -> краткосрочный учёт
  • PostgreSQL -> долговременное хранилище (с ON CONFLICT DO NOTHING для uniqueness)

Подходит для high-volume потоков событий

Агрегация по временным окнам

  • Redis HLL с ключами по дню: PFADD hll:uu:2025-05-30 userId
  • ClickHouse с toStartOfHour(toDateTime(ts))
  • PostgreSQL с date_trunc('hour', event_time)

Можно строить:

  • DAU (Daily Active Users)
  • WAU, MAU
  • UU per feature, per segment

Частые ошибки

ОшибкаПочему плохоКак исправить
Сохраняются все user_id в списокOOM при росте объёмаИспользуйте Redis HLL
Используется точный uniqExact при миллиардеЗамедление, рост CPUИспользуйте uniq() или Pre-agg
Повторно считаются одни и те же IDНет TTL, нет windowingИспользуйте Redis с TTL или ClickHouse
Нет deduplication на ingestionУтечка уникальностиФильтруйте на ingestion или stream level

Пример реального применения

  • В Kafka приходят события UserAction(userId, timestamp)
  • Kafka Streams агрегирует по ключу date + userId и пишет:
  • В Redis SET для дня (для точного подсчёта по дню)
  • В Redis HLL – для дешёвых UI-графиков
  • Один раз в день записываем итог в PostgreSQL/ClickHouse

Резюме для собеседования

Я использую Redis HyperLogLog для недорогого подсчёта уникальных пользователей с допустимой погрешностью и Redis SET – если важна точность. В аналитике применяю ClickHouse с uniq() или uniqExact, в зависимости от требований. Архитектурно выстраиваю event-based pipeline: Kafka Streams -> Redis -> аналитическая БД.

10. Консенсус в распределённой системе (RAFT)

Что такое консенсус?

В распределённых системах узлы могут быть:

  • недоступны (сбой, сеть),
  • не синхронизированы по времени,
  • в разных состояниях.

Консенсус – это достижение соглашения о значении данных между несколькими узлами, даже в случае сбоев. Это основа для:

  • управления конфигурацией,
  • распределённых логов,
  • лидерства (leader election),
  • обеспечения согласованности (consistency) при репликации.

RAFT: Replicated And Fault Tolerant

RAFT – популярный алгоритм консенсуса, предложенный в 2013 году как альтернатива более сложному Paxos. Он легче для понимания и реализации.

Ключевые принципы RAFT

КомпонентОписание
Лидер (Leader)Один узел принимает команды от клиентов и реплицирует их другим узлам
Последователи (Followers)Узлы, получающие команды от лидера и подтверждающие их
Кандидаты (Candidates)Узлы, претендующие на лидерство в случае таймаута
Термы (Terms)Логические интервалы, каждый выбор лидера начинается с новой term
Log ReplicationВсе команды записываются в журнал и реплицируются
Commit IndexКоманды считаются зафиксированными при подтверждении большинством узлов

Жизненный цикл RAFT-узла

  1. Start as Follower
  2. Если нет сигналов от лидера – переходит в Candidate, запускает выборы
  3. Узлы голосуют, кто получит большинство (quorum -> N/2+1)
  4. Становится Leader
  5. Начинает репликацию команд (log entries) другим узлам
  6. Когда большинство подтвердили – команда считается зафиксированной

Log Replication в RAFT

Каждая команда (например, put(key, value)) записывается в log:

  • Лидер добавляет запись в свой лог
  • Рассылает AppendEntries всем последователям
  • Если большинство подтвердили – фиксирует команду (commitIndex)
  • После этого узлы применяют команду к своему состоянию (state machine)

Это гарантирует linearizability – поведение как у одного узла

Применение RAFT

СистемаГде используется RAFT
etcdУправление конфигурацией и секретами в Kubernetes
ConsulService discovery, KV store
TemporalWorkflow history и состояния
CockroachDBDistributed SQL-репликация
dgraphGraphDB с distributed consensus

Отличие от Paxos

КритерийRAFTPaxos
ПониманиеПростой, разбит на этапыСложный, требует глубоких знаний
Поддержка лидерстваВстроеноТребует дополнительных расширений
Распространениеetcd, Consul, TemporalZooKeeper, Cassandra (частично)

Подводные камни

ОшибкаПочему критично
Смена лидера при задержкеМожет привести к временной недоступности
Split-brain при потере quorumВозможна потеря данных или двойная запись
Большой log, неактуальный followerРепликация может быть медленной, блокирующей лидер
Без снапшотов log растёт бесконечноНужна периодическая фиксация состояния

Метрики и наблюдаемость RAFT

  • Current Term – номер текущего лидерского периода
  • Leader ID – ID текущего лидера
  • Commit Index – последний зафиксированный индекс
  • Raft Log Size – объём журнала
  • Followers Sync % – насколько последователи отстают
  • Election Count – сколько раз происходила переизбрание лидера

Наблюдаются через Prometheus + Grafana или /_metrics эндпоинты (в etcd, Consul)

Резюме для собеседования

Я хорошо понимаю алгоритм RAFT: его механизмы выбора лидера, репликации и подтверждения через кворум. Использовал etcd и Consul как KV-хранилища и системы Discovery. Понимаю риски split-brain и обеспечиваю observability через метрики лидерства, задержек и размера RAFT-журнала. Знаю, как RAFT отличается от Paxos и когда его стоит применять.

11. Gossip-протокол (Discovery)

Gossip Protocol – это метод распространения информации в распределённой системе, вдохновлённый биологическим механизмом заражения: каждый узел “перешёптывается” с другими узлами, передавая им актуальное состояние.

Используется для:

  • Распространения конфигурации
  • Обнаружения доступных узлов (service discovery)
  • Поддержания кластерной топологии
  • Обновления статуса health (жив/не жив)

Основная идея

Каждый узел периодически выбирает случайного соседа и отправляет ему свой snapshot состояния (например, таблицу живых узлов, хэши состояний, временные метки).

Сосед объединяет информацию с текущим состоянием, и у обоих появляется чуть более “актуальная” картина мира. Через несколько раундов эта информация распространяется по всей системе.

Принцип работы

  1. Инициализация – у каждого узла есть свой state map, например: {hostA: alive, hostB: suspect}
  2. Раунд (gossip interval):
  • Узел выбирает случайного соседа
  • Отправляет ему свою карту состояния (digest)
  • Получает в ответ более новую информацию
  1. Объединение состояния (merge):
  • Выбирается самая “свежая” информация по версии/таймстемпу

Такой обмен продолжается в фоне. Он не требует согласования и центра.

Форматы сообщений (условно)

{
  "node": "hostA",
  "generation": 172934,
  "heartbeat": 247,
  "status": "ALIVE"
}
  • Generation – уникальное значение при перезапуске узла
  • Heartbeat – счётчик, монотонно растущий
  • Status – ALIVE / SUSPECT / DEAD

Свойства Gossip-протокола

СвойствоОписание
ДецентрализацияНет главного сервера — нет single point of failure
СходимостьЧерез O(logN) шагов состояние сойдётся у всех
Фоновый и надёжныйПереносит потери пакетов, сбои узлов
ВстраиваемостьМожет быть реализован на уровне библиотеки или встроен в платформу

Реальные реализации

СистемаКак используется Gossip
CassandraУзлы обмениваются live-status, токенами партиций и кластерным конфигом
ConsulИспользует Gossip для обнаружения агентов, планирования конфигурации
SerfМинималистичный инструмент на Go, используется в Nomad, Vault
ScyllaDBРеализация Gossip в C++ с low-latency и частичной совместимостью с Cassandra

Безопасность

  • Gossip не имеет встроенной аутентификации
  • Обычно защищается через IP allowlist, ACL или TLS-настройки на уровне транспорта

Возможные проблемы

ПроблемаПричинаРешение
Split-brainНекоторые узлы считают других “мертвыми”Настройка таймаутов, quorum-based ops
Травянистое сходжение (slow)При сетевых задержкахУвеличение числа параллельных узлов обмена
Старое состояние “затирает” новоеНет сравнения generation/heartbeatВведение timestamp-based merge

Метрики и наблюдаемость

  • gossip_received_messages_total
  • gossip_heartbeat_interval
  • suspect_node_count
  • gossip_convergence_time_seconds
  • cluster_view_size

В Grafana можно отследить задержки, количество SUSPECT-узлов и длительность сходимости.

Сравнение с RAFT

КритерийGossipRAFT
ТипЭпидемический, soft-stateЖёсткий консенсус с логом
СходимостьПостепенная, вероятностнаяДетерминированная, quorum-based
ConsistencyEventual (нет консенсуса)Strong (после commit)
Область примененияОбнаружение, статус, конфигиРепликация данных, согласование команд

Резюме для собеседования

Я хорошо знаком с принципами Gossip-протокола: использовал его косвенно через Consul и Cassandra. Понимаю принципы асимптотической сходимости, механизм heartbeat, роль generation при перезапуске узлов. Знаю, как настроить параметры convergence и troubleshoot-ить split-brain. Отличаю его от RAFT по степени согласованности и области применения.

12. Как UUID обеспечивает почти уникальные значения

UUID (Universally Unique Identifier) – это 128-битный идентификатор, широко используемый в распределённых системах, базах данных, брокерах сообщений и API. Основная задача UUID – быть уникальным без необходимости обращения к центральному генератору.

Формат UUID

UUID состоит из 128 бит (16 байт), чаще всего записывается в виде:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

Где:

  • M – версия UUID (v1, v4, v7 и т.д.)
  • N – вариант (например, RFC4122)

Пример UUIDv4:

f47ac10b-58cc-4372-a567-0e02b2c3d479

Почему UUID почти уникален?

UUIDv4 (random-based)

  • Содержит 122 случайных битов
  • Возможное количество уникальных значений:
  2^122 -> 5.3 -> 10^36
  • Вероятность коллизии крайне мала – сравнима с шансом, что два человека случайно придумают одинаковый 36-значный пароль

Закон больших чисел и парадокс дней рождения:

UUID и коллизии на практике

СценарийРиск коллизииРекомендация
Локальный UUIDv4 на одном сервереКрайне малМожно использовать
UUIDv4 на миллионах машинОчень малПодходит для распределённых систем
UUIDv1 на одинаковом MAC-адресеЕсть рискСледить за clock drift, использовать mutex
UUIDv4 в базе с индексамиВозможна деградацияИспользовать UUIDv7 / ULID / KSUID

Виды UUID и особенности

ВерсияОписаниеУникальностьСортируемостьОсобенности
v1Время + MAC-адресВысокаяДаУтечки о времени и хосте
v4Случайные битыПочти 100%НетНаиболее часто используемый
v5Хеш на основе имени и namespaceДетерминированНетSHA-1
v7Timestamp + randomness (proposed)ВысокаяДаИдеален для баз с индексами по времени
ULIDCrockford Base32 + времяВысокаяДаЧитаемый, компактный
KSUIDSimilar to ULID with longer timestamp windowВысокаяДаПоддерживает миллиарды лет

Когда UUID может не подойти?

  • В PostgreSQL uuid индекс медленнее bigint
  • UUIDv4 плохо сортируется -> низкая эффективность B-Tree индексов
  • Большой размер (uuid = 16 байт, bigint = 8 байт)

Для insert-heavy систем:

  • Используйте UUIDv7 / ULID / KSUID
  • Или bigint с генератором (Snowflake, Twitter ID, sequence)

Пример генерации UUID в Java

import java.util.UUID;

UUID uuid = UUID.randomUUID();
System.out.println(uuid.toString());

Для высоконагруженных систем

  • Генерация UUIDv4 достаточно быстрая, не требует синхронизации
  • Для хранения большого объёма данных лучше использовать сортируемые UUID (v7, ULID)
  • В Kafka или event sourcing – часто предпочтительнее ULID/KSUID как messageId

13. Strong Consistency vs Eventual Consistency

В распределённых системах согласованность (consistency) – один из ключевых аспектов CAP-теоремы. Она определяет, насколько быстро и надёжно данные распространяются между узлами после обновления.

Определения

Тип согласованностиОписание
Strong consistencyПосле подтверждения записи — все узлы сразу видят новое значение
Eventual consistencyЗапись гарантированно распространится на все узлы, но с задержкой

Strong Consistency (жёсткая согласованность)

Признаки:

  • Все клиенты всегда видят одно и то же значение
  • Операции чтения и записи проходят через один согласованный источник (quorum, master)
  • Реализуется с помощью:
  • Quorum write + read
  • Two-Phase Commit
  • RAFT/Paxos
  • Лидерской репликации

Примеры:

СистемаКак достигается strong consistency
PostgreSQLОдин мастер (write), остальные читают реплику
etcdИспользует RAFT: только лидер пишет, кворум подтверждает
ZookeeperСогласование через ZAB (аналог RAFT)
SpannerTrueTime для глобального согласования

Плюсы:

Простота reasoning (как будто работаем с одной машиной)
Идеально для банков, транзакций, идентичности, ACL

Минусы:

Более высокая латентность
Сложнее масштабировать горизонтально
Требуется quorum для операций

Eventual Consistency (постепенная согласованность)

Принцип:

  • После записи данные рано или поздно распространятся на все узлы
  • Не гарантируется моментальная видимость новых данных
  • Узлы могут расходиться во мнениях временно

Реализация:

  • Hinted handoff (отложенная доставка)
  • Read repair (починка данных во время чтения)
  • Anti-entropy (регулярная синхронизация)
  • Gossip (эпидемическое распространение)

Примеры:

СистемаПоведение
CassandraПоддержка Tunable Consistency: можно выбрать ONE, QUORUM, ALL
DynamoDBПишет в несколько узлов, без блокировки
S3 / BlobПосле PUT объект может быть не сразу виден во всех зонах
CouchbaseEventually replicated to all nodes

Плюсы:

Отличная масштабируемость
Высокая доступность
Быстрая запись и чтение

Минусы:

Возможны stale reads
Нужно проектировать систему с учётом конфликтов
Отсутствие гарантии linearizability

Сравнение

ХарактеристикаStrong ConsistencyEventual Consistency
Видимость данныхМгновенно во всех узлахМожет быть с задержкой
Устойчивость к сбоямНижеВыше (при снижении согласованности)
ПроизводительностьНижеВыше
Коммуникации между узламиКворумОпционально
Типичные задачиБанкинг, ACL, ID генерацияIoT, аналитика, кэш, рекомендации

Конфликты и разрешение

  • Eventual consistency требует решения конфликтов:
  • Last Write Wins (LWW)
  • Vector Clocks
  • CRDT (Conflict-free Replicated Data Types)
  • Application-level merge

Пример (Cassandra):

-- запись с уровнем ONE
INSERT INTO users (id, name) VALUES ('123', 'Alice') USING CONSISTENCY ONE;

-- чтение с уровнем QUORUM
SELECT * FROM users WHERE id = '123' USING CONSISTENCY QUORUM;

Поддержка в базах данных

СУБДConsistency model
PostgreSQLStrong (ACID)
MongoDB (реплика)Tunable (до strong)
CassandraEventual / Tunable
Redis ClusterEventual (по умолчанию)
SpannerStrong (через TrueTime)

14. Использование Keycloak в корпоративных системах

Keycloak – это open-source Identity and Access Management (IAM) решение от Red Hat, широко применяемое в корпоративной среде для централизации управления доступом и идентификацией пользователей.

Ключевые возможности

ВозможностьОписание
SSO (Single Sign-On)Один логин — доступ к нескольким приложениям
MFA (Multi-Factor Auth)Поддержка TOTP, WebAuthn, SMS
RBAC / ABACРолевой и атрибутный контроль доступа
OAuth 2.0 / OpenID ConnectАутентификация и авторизация по стандартам
SAML 2.0Поддержка старых протоколов (например, для SAP)
LDAP/AD интеграцияПрямая федерация пользователей без миграции
User Self-ServiceUI и REST API для управления профилем
Custom Flows / ScriptsРасширяемость через SPI, JavaScript, Webhooks

Протоколы и Flows

ПотокПрименение
Authorization Code FlowUI-приложения (web, mobile)
Client CredentialsMachine-to-machine
Resource Owner PasswordLegacy, устаревший
Implicit FlowУстаревший (для SPA)
Device FlowТВ, CLI-интерфейсы
Token ExchangeДелегирование прав
Token IntrospectionВалидация opaque токенов
Userinfo EndpointПолучение информации о пользователе

Архитектура Keycloak

  • Realm – изолированная область пользователей и клиентов
  • Client – приложение, которому требуется доступ
  • Role – роль, назначаемая пользователям
  • Group – логическое объединение пользователей
  • Mapper – преобразование полей в токене (например, LDAP -> claim)

Интеграция со Spring Boot

Пример конфигурации ресурсов сервера:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.mycorp.ru/realms/main

Конфигурация клиента (например, UI-приложения):

spring:
  security:
    oauth2:
      client:
        registration:
          myapp:
            client-id: my-client
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/myapp"
        provider:
          keycloak:
            issuer-uri: https://auth.mycorp.ru/realms/main

Примеры использования в продакшене

  • Внутренние дашборды -> SSO через Keycloak
  • Сервисы интеграции -> авторизация через Client Credentials + RBAC
  • Публичные API -> токены OIDC + scopes (permissions)
  • Сценарии “от имени пользователя” -> токен обмен (Token Exchange)

Настройка федерации с LDAP/AD

# UI -> User Federation -> Add Provider -> LDAP
# Указываем:
- bindDN: cn=reader,ou=users,dc=corp,dc=ru
- userSearchBase: ou=users,dc=corp,dc=ru
- syncPeriod: 1h

Пользователи не копируются, а подтягиваются в реальном времени. Можно маппить cn в preferred_username и memberOf в роли.

Расширяемость

  • Custom SPI – собственная логика (аутентификация, email, логирование событий)
  • JS-скрипты – написание условий в auth flow
  • Webhooks – интеграция с внешними аудит-системами
  • Themes – брендинг логина и портала

Безопасность

  • Token lifetime:
  • Access Token: 5–15 минут
  • Refresh Token: 30–60 минут
  • Scopes: ограничение прав клиента
  • Audience claim: защита от misuse токенов
  • PKCE: обязательный для мобильных и публичных клиентов
  • Key Rotation: поддержка JWKS endpoint

Мониторинг и логи

  • Метрики через Prometheus Exporter (community)
  • Аудит через Event Listener SPI
  • Логи: JSON в stdout, интеграция с Loki, ELK
  • Валидация токенов: /.well-known/openid-configuration + JWKS endpoint

Ограничения и подводные камни

ПроблемаОбходной путь
Отсутствие HA-репликации базыИспользовать Keycloak.X + DB failover
Ошибки refresh token в нагрузкеНастроить reuseRefreshToken = false
Много клиентов -> медленный UIREST API / CLI tools
Нестабильность на старых версияхИспользовать версию > 20.0.0

Рекомендуемая литература и ресурсы

Java и Kotlin

Stream API, функциональное программирование

CI/CD, DevOps, инфраструктура

Мониторинг, нагрузка, observability

Kafka и распределённые системы

Алгоритмы и архитектура

Безопасность и Keycloak

Loading