Files
magistr/DEVOPS.md
2026-06-01 20:50:18 +03:00

35 KiB
Raw Permalink Blame History

РАЗРАБОТКА И ВНЕДРЕНИЕ ОТКАЗОУСТОЙЧИВОЙ МУЛЬТИТЕНАНТНОЙ ИНФРАСТРУКТУРЫ ДЛЯ СИСТЕМЫ УПРАВЛЕНИЯ УНИВЕРСИТЕТСКИМ РАСПИСАНИЕМ «МАГИСТР»

Диссертационное исследование и отчет о проделанной инженерной работе в качестве DevOps-архитектора проекта


ВВЕДЕНИЕ

Современные информационные системы для образовательных учреждений требуют строгого соблюдения требований к безопасности, изоляции данных различных организаций (университетов), гибкости масштабирования и высокой наблюдаемости (observability). В рамках разработки проекта «Магистр» — системы управления расписанием учебных занятий — передо мной встала задача проектирования и построения инфраструктуры, способной обслуживать десятки университетов в рамках единого прикладного контура при условии жесткой изоляции их баз данных.

В качестве DevOps-инженера проекта я спроектировал и реализовал архитектурное решение, объединяющее технологии аппаратной виртуализации, оркестрации контейнеров, автоматизации конфигурации, распределенного мониторинга и непрерывной интеграции. В данном документе подробно описаны теоретические предпосылки, практическая реализация и архитектурные решения, внедренные мной в продакшн-окружение проекта.


РАЗДЕЛ 1. СИСТЕМНАЯ АРХИТЕКТУРА И КОНЦЕПЦИЯ МУЛЬТИТЕНАНТНОСТИ

1.1 Выбор паттерна изоляции данных

При проектировании мультитенантных (multi-tenant) систем классически выделяют три подхода к организации баз данных:

  1. Shared Database & Shared Schema: Все клиенты используют общие таблицы, записи разделяются по полю tenant_id. Подход дешев в обслуживании, но несет колоссальные риски утечки данных из-за ошибок в SQL-запросах приложения и не позволяет разграничивать физический доступ к базам.
  2. Shared Database & Separate Schemas: Одна СУБД, но разные логические схемы для каждого клиента. Обеспечивает умеренную изоляцию, но сохраняет единую точку отказа и общие аппаратные ресурсы СУБД.
  3. Database-per-Tenant (Выбранный подход): Каждый клиент (университет) владеет собственной физически изолированной базой данных, расположенной на выделенном сервере или виртуальной машине.

Для проекта «Магистр» мной был выбран и реализован паттерн Database-per-Tenant. Это обусловлено спецификой клиентов (высшие учебные заведения), которые согласно законодательству (например, ФЗ-152 «О персональных данных») обязаны хранить свои данные локально или требовать полной изоляции конфиденциальной информации. Кроме того, данный подход позволяет:

  • Исключить влияние высокой нагрузки одного университета (например, в период составления сессий) на доступность системы для других вузов («синдром шумного соседа»).
  • Выполнять индивидуальное резервное копирование и обслуживание БД каждого университета без остановки всей платформы.
  • Переносить базы данных вузов на их собственные физические серверы в локальных сетях (on-premise), сохраняя при этом работу приложения в центральном облаке.

1.2 Архитектурная схема движения трафика

Ниже представлена разработанная мной схема прохождения сетевых запросов и агрегации телеметрии в системе:

graph TD
    Client[Пользовательский браузер] -->|HTTPS: tenant.zuev.company| Caddy[Caddy Reverse Proxy]
    Caddy -->|HTTP Round-Robin| Ingress[Traefik Ingress Controller]
    
    subgraph K3s Cluster
        Ingress -->|Route /*| Frontend[Frontend Pods x2 Apache]
        Ingress -->|Route /api/*| Backend[Backend Pods x1-x2 Spring Boot]
        Backend -->|K8s API: Get/Patch CM| K8sAPI[Kubernetes API Server]
        BackendConfig[ConfigMap: tenants-config] -.->|Mounted JSON| Backend
    end
    
    subgraph Proxmox VE Infrastructure
        Backend -->|JDBC Connection| VM1[(VM 1: DBMS MSU<br>PostgreSQL 16)]
        Backend -->|JDBC Connection| VM2[(VM 2: DBMS SWSU<br>PostgreSQL 16)]
        
        VM1 -.->|Metrics OTLP/gRPC| Collector1[OTel Collector VM1]
        VM2 -.->|Metrics OTLP/gRPC| Collector2[OTel Collector VM2]
    end

    subgraph Observability Hub
        Backend -->|Logs & Traces OTLP| SigNoz[SigNoz APM Server]
        Collector1 -->|Postgres Metrics| SigNoz
        Collector2 -->|Postgres Metrics| SigNoz
    end
    
    classDef cluster fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
    classDef vm fill:#efebe9,stroke:#4e342e,stroke-width:2px;
    classDef monitor fill:#efe8ff,stroke:#512da8,stroke-width:2px;
    class K3s Cluster cluster;
    class Proxmox VE Infrastructure vm;
    class Observability Hub monitor;

РАЗДЕЛ 2. АППАРАТНАЯ ВИРТУАЛИЗАЦИЯ НА БАЗЕ PROMVOX VE

Для размещения баз данных клиентов и вспомогательных инфраструктурных служб мной был развернут гипервизор Proxmox Virtual Environment (PVE) на выделенных серверных мощностях.

2.1 Конфигурация виртуализации и изоляции ресурсов

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

  • Сетевой уровень: В Proxmox настроена виртуальная коммутация (Linux Bridge vmbr0). Сетевые интерфейсы виртуальных машин баз данных вынесены в приватную подсеть, изолированную от внешней сети. Доступ к ним имеет только подсеть кластера Kubernetes и управляющая машина администратора.
  • Дисковая подсистема: Использовано локальное хранилище на базе ZFS с зеркалированием (RAID-10 на SSD-накопителях). Это обеспечивает:
    • Высокую скорость операций ввода-вывода (IOPS), критически важную для СУБД при транзакционных нагрузках.
    • Механизм мгновенных снимков (snapshots) для безопасного проведения обновлений.
    • Аппаратное сжатие данных (LZ4) без потери производительности, снижающее износ накопителей.
  • Резервное копирование: Интегрирован Proxmox Backup Server (PBS). Каждую ночь выполняются инкрементальные бэкапы виртуальных машин с дедупликацией на уровне блоков данных, что минимизирует нагрузку на сеть и диски, позволяя восстановить любую БД на любой момент времени за последние 30 дней.

РАЗДЕЛ 3. ОРКЕСТРАЦИЯ КОНТЕЙНЕРНОЙ ИНФРАСТРУКТУРЫ В KUBERNETES (K3s)

В качестве платформы оркестрации приложений был выбран K3s — сертифицированный дистрибутив Kubernetes от Rancher, оптимизированный под средние и малые нагрузки, обладающий минимальными накладными расходами на RAM/CPU для Control Plane, но сохраняющий полную совместимость с upstream-спецификациями Kubernetes API.

3.1 Архитектура манифестов и структура ресурсов

Мной была разработана декларативная структура ресурсов, развернутая в изолированном пространстве имен magistr (манифест namespace.yaml):

  • Frontend (frontend.yaml):

    • Реализован в виде Deployment с 2 репликами для обеспечения высокой доступности (HA) и возможности бесшовного обновления rolling-update.
    • В качестве базового образа контейнера применен легковесный веб-сервер Apache HTTPd (httpd:alpine).
    • Для балансировки и внутреннего доступа настроен Service типа ClusterIP, слушающий порт 80.
  • Backend (backend.yaml):

    • Deployment с 1-2 репликами (детали балансировки конфигурации описаны ниже).
    • Для сборки образов используется multi-stage сборка Maven (JDK 17) и запуск под управлением eclipse-temurin:17-jre-alpine.
    • Интегрирован Java-агент OpenTelemetry для автоматического инструментирования трассировки и логов.
    • Для связи с Ingress настроен ClusterIP-сервис на порту 8080.
  • Ingress (ingress.yaml):

    • В роли Ingress-контроллера выступает стандартный для K3s Traefik.
    • Мной настроены строгие правила маршрутизации по доменным именам (например, magistr.zuev.company для тестового окружения, n8n.zuev.company и т.д.). Маршрутизация по путям разделяет трафик: запросы к API (/api) проксируются на бэкенд-сервис, а запросы к статическим ресурсам и страницам (/) — на фронтенд-сервис.

3.2 Реализация динамической мультитенантности через K8s API

Одним из наиболее сложных этапов проектирования стала организация бесшовного добавления новых университетов без перезапуска бэкенда и изменения исходного кода приложения.

  1. Хранение конфигурации: Список подключений к базам данных университетов хранится в JSON-формате (tenants.json) внутри Kubernetes ConfigMap с именем tenants-config.

  2. Проблема Read-Only монтирования: По умолчанию Kubernetes монтирует ConfigMap как файловую систему в режиме Read-Only. Это исключало возможность для Java-приложения перезаписывать tenants.json при запросах от администратора на добавление нового тенанта.

  3. Решение с использованием initContainer и emptyDir: Я спроектировал схему с временным перезаписываемым томом (emptyDir):

    • В спецификацию пода backend добавлен initContainer на базе образа busybox.
    • При старте пода initContainer монтирует ConfigMap tenants-config и копирует файл tenants.json во временный раздел emptyDir (путь /config).
    • Основной контейнер backend монтирует этот же emptyDir в режиме Read-Write.
  4. K8s RBAC и динамическое сохранение: Чтобы предоставить бэкенду возможность сохранять изменения конфигурации обратно в кластер, мной были написаны манифесты авторизации (rbac.yaml):

    • Создан ServiceAccount backend-sa и назначен поду бэкенда.
    • Описана Kubernetes Role backend-configmap-role, дающая права только на операции get и patch для ресурса configmaps с конкретным именем tenants-config (принцип наименьших привилегий).
    • Создан RoleBinding backend-configmap-binding, связывающий сервис-аккаунт с этой ролью.

    В коде бэкенда класс ConfigMapUpdater через Kubernetes Java Client отправляет PATCH-запрос к API-серверу при добавлении нового тенанта, обновляя ConfigMap в самом кластере.

  5. Репликация и синхронизация подов: Для обеспечения консистентности данных в условиях работы нескольких реплик бэкенда:

    • Класс TenantConfigWatcher запускает фоновую задачу, которая каждые 30 секунд сверяет MD5-хеш локального файла tenants.json с версией из ConfigMap.
    • При обнаружении расхождения файл перезаписывается, DataSource-инстансы пересоздаются в памяти приложения на лету, исключая рассинхронизацию подов.
    • Примечание: В ходе тестирования для минимизации задержек и исключения эффекта "мигания" данных на время отладки количество реплик backend было ограничено до 1, с последующим переходом на полноценный StatefulSet/Shared Storage при росте количества нод.

3.3 Повышение отказоустойчивости бэкенда (Resiliency)

При запуске бэкенда в облаке возникала проблема: если база данных одного из университетов становилась недоступной (например, из-за сетевого сбоя или регламентных работ), пул подключений HikariCP падал с ошибкой при инициализации бинов, отправляя под бэкенда в циклическую перезагрузку (CrashLoopBackOff), что делало недоступной систему для всех остальных вузов.

Для предотвращения этого каскадного сбоя я внедрил комплекс защитных механизмов:

  • H2 Fallback: В сборку (pom.xml) добавлена СУБД H2 in-memory. Если при старте ConfigMap пуст, Spring Boot инициализирует пустой JPA-контекст на базе H2, что позволяет приложению успешно пройти фазу запуска.
  • HikariCP Resiliency: В конфигурации источников данных задано свойство setInitializationFailTimeout(-1). Это заставляет HikariCP игнорировать отсутствие связи с целевой БД при старте приложения, продолжая попытки подключения в фоновом режиме.
  • Явное указание диалекта Hibernate: Принудительно задан org.hibernate.dialect.PostgreSQLDialect в TenantDataSourceConfig.java. Это избавило Hibernate от необходимости выполнять тестовый запрос к БД для автоматического определения диалекта на ранних этапах инициализации контекста.
  • Автоматическое создание таблиц (DataInitializer): Для новых БД вузов полностью автоматизирован процесс наката структуры. При обнаружении новой базы Java-код проверяет наличие таблицы users и, в случае ее отсутствия, самостоятельно применяет SQL-скрипт инициализации (init.sql), включая создание дефолтных справочников и учетной записи суперадминистратора.

РАЗДЕЛ 4. АВТОМАТИЗАЦИЯ КОНФИГУРАЦИИ СУБД С ПОМОЩЬЮ ANSIBLE

Для развертывания и администрирования баз данных университетов на удаленных виртуальных машинах Proxmox мной был спроектирован и реализован комплексный плейбук Ansible с модульной структурой ролей.

4.1 Структура и функционал Ansible-ролей

Вся конфигурация целевого сервера разбита на 6 последовательных этапов (плейбук site.yml):

  1. common (Базовая подготовка и Hardening ОС):

    • Создание системного пользователя deploy с беспарольным доступом к sudo (через валидацию файла /etc/sudoers.d/deploy утилитой visudo).
    • Установка системных утилит (curl, git, htop, chrony для синхронизации времени по NTP).
    • SSH Hardening: Изменение стандартного SSH-порта на 2222, отключение авторизации по паролям, запрет входа для пользователя root, ограничение таймаута сессий.
    • Адаптация под различные дистрибутивы ОС: написаны отдельные сценарии для семейств Debian/Ubuntu/Astra Linux (пакетный менеджер apt), RedHat/РЕД ОС (dnf) и ALT Linux (кастомная обертка для apt-get).
  2. docker (Среда контейнеризации):

    • Автоматическое добавление официальных GPG-ключей и репозиториев Docker CE.
    • Установка Docker Engine, CLI и Compose плагина.
    • Включение системной службы docker и добавление пользователя deploy в группу docker для беспарольного запуска контейнеров.
  3. firewall (Сетевая безопасность):

    • Настройка межсетевого экрана UFW (для Debian-based систем) или firewalld (для RedHat и ALT Linux).
    • Полная блокировка входящего трафика по умолчанию.
    • Разрешение входящих пакетов только на порт SSH (2222) и порт PostgreSQL (5432). Реализована поддержка ограничения доступа к порту 5432 только для IP-адресов нод Kubernetes-кластера (firewall_postgresql_allowed_ips).
  4. postgresql (Контейнеризированная СУБД):

    • Генерация файлов конфигурации docker-compose.yml и .env на основе шаблонов Jinja2.
    • Развертывание PostgreSQL 16 на базе легковесного Alpine-образа.
    • Физическое монтирование каталога данных БД с хост-системы (/opt/magistr/postgres/data), предотвращающее потерю данных при пересоздании контейнера.
    • Ограничение системных ресурсов контейнера (limits: memory: 512M) и тюнинг параметров PostgreSQL (shared_buffers=256MB, оптимизация пула соединений, логирование медленных запросов log_min_duration_statement=1000).
  5. backup (Резервное копирование на уровне БД):

    • Установка на хост автоматического bash-скрипта бэкапа.
    • Настройка задачи cron (запуск ежедневно в 03:00).
    • Скрипт осуществляет горячий дамп базы через docker exec pg_dump, архивирует его с помощью gzip и сохраняет в локальный каталог /opt/magistr/backups/.
    • Реализован алгоритм автоматической ротации: удаление архивных файлов старше заданного количества дней (по умолчанию 14 дней).
  6. monitoring (Сбор телеметрии хоста и БД):

    • Развертывание контейнера OpenTelemetry Collector на целевом сервере БД.
    • Конфигурирование коллектора на сбор метрик производительности PostgreSQL и ОС (процессор, память, диски) и их отправку на единый сервер SigNoz по OTLP/HTTP.

4.2 Управление секретами: Ansible Vault

Для исключения попадания паролей администраторов БД в систему контроля версий (Git) все чувствительные переменные (например, vault_postgresql_password) вынесены в файл group_vars/databases/vault.yml и зашифрованы алгоритмом AES-256 с помощью Ansible Vault.

Для бесшовной интеграции в CI/CD пароль расшифрования считывается из локального файла .vault_pass на управляющей машине, доступ к которому ограничен правами chmod 600.


РАЗДЕЛ 5. НЕПРЕРЫВНАЯ ИНТЕГРАЦИЯ И ДОСТАВКА (CI/CD)

В рамках оптимизации внутренних процессов разработки и деплоя мной была развернута и настроена инфраструктура непрерывной интеграции на базе Gitea и Gitea Actions.

5.1 Автоматизация сборки (CI)

В репозитории проекта создан workflow-манифест .gitea/workflows/docker-build.yaml. При каждом пуше изменений в ветку main запускается конвейер:

  1. Checkout: Загрузка актуального исходного кода проекта на ранер.
  2. Setup Buildx: Инициализация Docker Buildx для оптимизации кэширования слоев.
  3. Login to Registry: Аутентификация во встроенном реестре контейнеров Gitea Container Registry (git.zuev.company) с использованием сервисного токена ZUEV_TOKEN (права write:package).
  4. Build & Push: Параллельная сборка Docker-образов для бэкенда и фронтенда с тегом latest и отправка их в приватный реестр.

5.2 Доставка в кластер (CD)

Для авторизации нод K3s в приватном реестре Gitea мной был создан секрет типа docker-registry в пространстве имен magistr:

kubectl create secret docker-registry gitea-registry \
  --docker-server=gitea.zuev.company \
  --docker-username=Zuev \
  --docker-password=${ZUEV_TOKEN} \
  --namespace=magistr

Этот секрет ассоциирован со спецификациями деплоев в backend.yaml и frontend.yaml через директиву imagePullSecrets. Обновление приложений в кластере после завершения сборки образов выполняется путем контролируемого перезапуска подов:

kubectl rollout restart deployment backend -n magistr
kubectl rollout restart deployment frontend -n magistr

РАЗДЕЛ 6. ВХОДНОЙ ПРОКСИ-СЕРВЕР НА БАЗЕ CADDY PROXY

Для маршрутизации внешнего трафика и защиты соединений на входе в инфраструктуру развернут веб-сервер Caddy.

6.1 Преимущества Caddy и управление SSL/TLS

В отличие от классического Nginx, Caddy был выбран благодаря:

  • Встроенной интеграции с удостоверяющими центрами Let's Encrypt и ZeroSSL. При добавлении нового поддомена вуза (например, swsu.zuev.company) Caddy автоматически запрашивает, валидирует через DNS/HTTP-вызовы и устанавливает TLS-сертификат, а также следит за его продлением.
  • Лаконичному синтаксису конфигурации (Caddyfile).
  • Полноценной поддержке протокола HTTP/3 «из коробки».

6.2 Конфигурация балансировки

Внешний Caddy-сервер принимает запросы ко всем поддоменам *.zuev.company и перенаправляет их на ноды кластера K3s, выполняя роль внешнего балансировщика нагрузки (L4/L7 Load Balancer):

*.zuev.company {
    reverse_proxy 192.168.1.104:80 192.168.1.105:80 192.168.1.106:80 {
        lb_policy round_robin
        lb_try_duration 5s
        lb_try_interval 250ms
    }
}

Сетевые запросы распределяются по нодам K3s по алгоритму Round-Robin. В случае недоступности одной из нод, Caddy временно исключает ее из пула, обеспечивая отказоустойчивость инфраструктуры на сетевом уровне.


РАЗДЕЛ 7. СКВОЗНОЙ МОНИТОРИНГ И ОБСЕРВАБИЛИТИ (SigNoz + OpenTelemetry)

Для контроля здоровья системы и оперативного выявления аномалий мной была спроектирована и внедрена централизованная система мониторинга на базе APM-платформы SigNoz и стандартов OpenTelemetry (OTel).

7.1 Сбор телеметрии Backend

Java-приложение бэкенда запускается с подключением агента OpenTelemetry (opentelemetry-javaagent.jar). Это позволяет без изменения кода собирать:

  • Трейсы (Traces): Сквозное прохождение HTTP-запросов через контроллеры, сервисы и JDBC-драйвер к базам данных.
  • Метрики (Metrics): Метрики JVM (утилизация Heap, активность сборщика мусора GC, состояние потоков), метрики HTTP-запросов (интенсивность, латентность, коды ответов).
  • Логи (Logs): Системные логи Logback экспортируются по протоколу OTLP напрямую в SigNoz.

7.2 Идентификация тенантов в мониторинге

Для глубокого анализа производительности СУБД конкретных университетов критически важно разделять телеметрию по тенантам. Мной была реализована сквозная маркировка:

  • В бэкенде через Java-интерцептор TenantInterceptor при каждом запросе идентификатор тенанта помещается в контекст логирования SLF4J MDC (MDC.put("tenant.id", tenant)) и одновременно записывается в атрибуты текущего OpenTelemetry спана (Span.current().setAttribute("tenant.id", tenant)).
  • Благодаря переменной окружения OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_CAPTURE_MDC_ATTRIBUTES=tenant.id, OTel-агент автоматически парсит MDC и прикрепляет его к логам, отправляемым в SigNoz. Это позволяет администраторам фильтровать ошибки и строить графики нагрузки в разрезе каждого университета.

7.3 Мониторинг баз данных на хостах

Для сбора детальной статистики с изолированных серверов баз данных:

  1. На хостах БД силами Ansible разворачивается OpenTelemetry Collector.
  2. В конфигурации коллектора (otel-collector-config.yml.j2) описывается ресивер postgresql, который подключается к локальной БД и считывает системные метрики (размер таблиц, cache hit ratio, количество транзакций, активные сессии, блокировки).
  3. К собираемым метрикам жестко прикрепляются ресурсные атрибуты хоста: service.name=magistr-db-<tenant_domain> и university.name=<University Name>.
  4. Метрики экспортируются в центральный коллектор SigNoz. В результате в интерфейсе SigNoz доступны кастомные дашборды JVM, PostgreSQL и HTTP, позволяющие оперативно локализовать проблемы с производительностью баз данных конкретных вузов.

ЗАКЛЮЧЕНИЕ

Разработанная и внедренная мной DevOps-архитектура для проекта «Магистр» успешно решила задачи изоляции данных, отказоустойчивости и автоматизации администрирования.

Ключевые результаты проделанной работы:

  1. Изоляция данных: Реализована физическая изоляция БД университетов на выделенных серверах (VM в Proxmox VE), соответствующая требованиям безопасности и законодательства.
  2. Динамическое масштабирование: Внедрен механизм динамического добавления тенантов без простоя системы (zero-downtime) за счет связки Spring Boot, K8s RBAC и ConfigMapWatcher.
  3. Отказоустойчивость: Минимизированы риски каскадных сбоев бэкенда за счет применения in-memory fallback баз данных и оптимизации параметров подключения HikariCP.
  4. Автоматизация: Создан универсальный Ansible-плейбук для быстрого ввода в эксплуатацию новых серверов БД (время развертывания «под ключ» сокращено до нескольких минут).
  5. Наблюдаемость: Реализован сквозной мониторинг логов, метрик и трассировок с детализацией по тенантам на базе стека OpenTelemetry + SigNoz.

Созданная инфраструктура обладает высокой степенью гибкости и готова к дальнейшему горизонтальному масштабированию по мере подключения новых высших учебных заведений к проекту «Магистр».