# РАЗРАБОТКА И ВНЕДРЕНИЕ ОТКАЗОУСТОЙЧИВОЙ МУЛЬТИТЕНАНТНОЙ ИНФРАСТРУКТУРЫ ДЛЯ СИСТЕМЫ УПРАВЛЕНИЯ УНИВЕРСИТЕТСКИМ РАСПИСАНИЕМ «МАГИСТР» **Диссертационное исследование и отчет о проделанной инженерной работе в качестве 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 Архитектурная схема движения трафика Ниже представлена разработанная мной схема прохождения сетевых запросов и агрегации телеметрии в системе: ```mermaid 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
PostgreSQL 16)] Backend -->|JDBC Connection| VM2[(VM 2: DBMS SWSU
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`: ```bash 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`. Обновление приложений в кластере после завершения сборки образов выполняется путем контролируемого перезапуска подов: ```bash 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): ```caddyfile *.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-` и `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. Созданная инфраструктура обладает высокой степенью гибкости и готова к дальнейшему горизонтальному масштабированию по мере подключения новых высших учебных заведений к проекту «Магистр».