Установка инфраструктуры
Kubernetes-платформа готова. На этом этапе разворачиваются сервисы, от которых зависит Unica: хранилища данных, база данных, очередь сообщений, векторная база и сервисы машинного обучения. После установки каждого компонента необходимо дождаться состояния Running/Ready и только затем переходить дальше.
На этой странице
- 3.1. Создание пространств имён
- 3.2. Подготовка локального хранилища
- 3.3. PostgreSQL
- 3.4. Redis
- 3.5. RabbitMQ
- 3.6. Qdrant
- 3.7. MinIO
- 3.8. Разделение GPU с помощью MIG
- 3.9. vLLM
- 3.10. Docling Serve — опциональный компонент
- Проверка готовности инфраструктуры
3.1. Создание пространств имён
Прикладные и инфраструктурные компоненты разделяются по пространствам имён Kubernetes. Это упрощает управление, диагностику и разграничение ресурсов.
Для прикладных компонентов используем фиксированное разделение:
unica-db
PostgreSQL
Qdrant
unica-infra
Redis
RabbitMQ
vLLM (экземпляры моделей и маршрутизатор)
Docling Serve (опционально)
unica-storage
MinIO
unica-app
Unica, Unica Agent, Unica Workflow, Unica Ops
прикладные API
Системные компоненты Kubernetes остаются в собственных пространство имён:
kube-system
metallb-system
ingress-nginx
gpu-operator
cert-manager
Создать пространство имён:
kubectl create namespace unica-db
kubectl create namespace unica-app
kubectl create namespace unica-storage
kubectl create namespace unica-infra
Проверить:
kubectl get namespaces
3.2. Подготовка локального хранилища
В демонстрационной конфигурации постоянные данные размещаются в каталогах /storage/* на конкретных серверах. Для подключения этих каталогов к Pod-ам используется класс хранилища local-storage и локальные PersistentVolume.
Схема хранения данных
Данные компонентов инфраструктуры с постоянным хранением (PostgreSQL, Qdrant, RabbitMQ, MinIO) размещаются на:
${DB_NODE}
Данные GPU-компонентов (модели vLLM, Diarization, Recognition) размещаются на ${GPU_NODE} — см. шаги установки vLLM, Diarization API и Recognition Engine.
Базовый каталог:
/storage
Структура:
/storage/postgresql
/storage/qdrant
/storage/rabbitmq
/storage/minio
В этой схеме отдельный диск под /storage не обязателен. Каталог /storage размещается на существующей корневой файловой системе ext4.
Текущее размещение:
/dev/sda1 -> /
Создать каталоги на ${DB_NODE}:
mkdir -p \
/storage/postgresql \
/storage/qdrant \
/storage/rabbitmq \
/storage/minio
Проверить:
find /storage -maxdepth 1 -type d -print
df -hT /storage
Для Kubernetes создаётся статический StorageClass:
local-storage
с:
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
Для каждого компонента с постоянным хранением создаётся отдельный локальный PersistentVolume:
PostgreSQL -> /storage/postgresql
Qdrant -> /storage/qdrant
RabbitMQ -> /storage/rabbitmq
MinIO -> /storage/minio
Все PV:
accessModes: ReadWriteOnce
nodeAffinity: ${DB_NODE}
storageClassName: local-storage
StorageClass для локальных томов
локальный PersistentVolume используется для каталогов /storage/* на ${DB_NODE}.
Kubernetes требует nodeAffinity для локальный PV и рекомендует WaitForFirstConsumer, чтобы PVC привязывался с учётом узлы, на которой может быть запущен Pod.
Создать StorageClass:
cat <<'EOF' | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
EOF
Проверить:
kubectl get storageclass
Ожидается:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
local-storage kubernetes.io/no-provisioner Retain WaitForFirstConsumer
локальный PV заранее для всех компонентов не создаются. PV создаётся непосредственно перед установкой соответствующего компонента, чтобы capacity.storage совпадала с размером его PVC.
Порядок:
PostgreSQL
-> /storage/postgresql
-> отдельный PV/PVC
Qdrant
-> /storage/qdrant
RabbitMQ
-> /storage/rabbitmq
MinIO
-> /storage/minio
Важно: после установки каждого инфраструктурного компонента необходимо дождаться завершения запуска и убедиться, что его Pod-ы находятся в состоянии Running/Ready. Только после этого переходить к следующему компоненту.
3.3. PostgreSQL
PostgreSQL хранит данные Unica, DataManagement и SpeechRecognition. Данные размещаются вне контейнера в каталоге /storage/postgresql.
PostgreSQL размещается на ${DB_NODE}.
Подготовка каталога
На узле инфраструктурных сервисов (${DB_NODE}):
mkdir -p /storage/postgresql
chown 999:999 /storage/postgresql
chmod 700 /storage/postgresql
Проверить:
ls -ld /storage/postgresql
df -hT /storage/postgresql
PersistentVolume для PostgreSQL
PersistentVolume связывает каталог /storage/postgresql на ${DB_NODE} с постоянным томом Kubernetes. Данные базы остаются на узле при перезапуске Pod-а.
На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}):
POSTGRES_STORAGE_SIZE="20Gi"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgresql-pv
spec:
capacity:
storage: ${POSTGRES_STORAGE_SIZE}
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /storage/postgresql
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${DB_NODE}
EOF
Создание паролей и Secret PostgreSQL
Для PostgreSQL и сервисных пользователей создаются отдельные пароли:
POSTGRES_PASSWORD="$(openssl rand -hex 24)"
ASR_PASSWORD="$(openssl rand -hex 24)"
DATAMANAGEMENT_PASSWORD="$(openssl rand -hex 24)"
UNICA_PASSWORD="$(openssl rand -hex 24)"
Пароли сохраняются в Kubernetes Secret postgresql. POSTGRES_PASSWORD используется контейнером PostgreSQL, а пароли ASR_PASSWORD, DATAMANAGEMENT_PASSWORD и UNICA_PASSWORD затем считываются при настройке соответствующих сервисов. Это позволяет не задавать пароли повторно вручную в следующих шагах.
Создать Secret:
kubectl -n unica-db create secret generic postgresql \
--from-literal=POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
--from-literal=ASR_PASSWORD="${ASR_PASSWORD}" \
--from-literal=DATAMANAGEMENT_PASSWORD="${DATAMANAGEMENT_PASSWORD}" \
--from-literal=UNICA_PASSWORD="${UNICA_PASSWORD}"
ConfigMap со сценарием инициализации PostgreSQL
Сценарий выполняется при первом запуске PostgreSQL и создаёт отдельные базы данных и пользователей для SpeechRecognition, DataManagement и Unica. Пароли берутся из переменных, созданных на предыдущем шаге.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: postgresql-initdb
namespace: unica-db
data:
init.sql: |
-- === ASR ===
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'asr_user') THEN
CREATE ROLE asr_user LOGIN PASSWORD '${ASR_PASSWORD}';
END IF;
END \$\$;
SELECT format('CREATE DATABASE %I OWNER %I', 'asr_db', 'asr_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'asr_db') \gexec
GRANT ALL PRIVILEGES ON DATABASE asr_db TO asr_user;
-- === DATA MANAGEMENT ===
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'datamanagement_user') THEN
CREATE ROLE datamanagement_user LOGIN PASSWORD '${DATAMANAGEMENT_PASSWORD}';
END IF;
END \$\$;
SELECT format('CREATE DATABASE %I OWNER %I', 'datamanagement_db', 'datamanagement_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'datamanagement_db') \gexec
GRANT ALL PRIVILEGES ON DATABASE datamanagement_db TO datamanagement_user;
-- === UNICA ===
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'unica_user') THEN
CREATE ROLE unica_user LOGIN PASSWORD '${UNICA_PASSWORD}';
END IF;
END \$\$;
SELECT format('CREATE DATABASE %I OWNER %I', 'unica_db', 'unica_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'unica_db') \gexec
GRANT ALL PRIVILEGES ON DATABASE unica_db TO unica_user;
EOF
Внутрикластерный доступ к PostgreSQL
Service создаёт постоянное DNS-имя postgresql.unica-db.svc.cluster.local, по которому прикладные сервисы подключаются к PostgreSQL внутри Kubernetes. База данных наружу не публикуется.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: postgresql
namespace: unica-db
spec:
selector:
app: postgresql
type: ClusterIP
ports:
- name: postgres
port: 5432
targetPort: 5432
EOF
Запуск PostgreSQL
StatefulSet запускает PostgreSQL на ${DB_NODE}, подключает постоянный том с данными и ConfigMap со сценарием первичной инициализации.
Перед запуском на ${DB_NODE}:
chown 999:999 /storage/postgresql
chmod 700 /storage/postgresql
На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}):
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: unica-db
spec:
serviceName: postgresql
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
nodeSelector:
kubernetes.io/hostname: ${DB_NODE}
securityContext:
fsGroup: 999
containers:
- name: postgres
image: postgres:16
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql
key: POSTGRES_PASSWORD
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 20
periodSeconds: 10
resources:
requests:
cpu: "2"
memory: "4Gi"
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: initdb
mountPath: /docker-entrypoint-initdb.d
volumes:
- name: initdb
configMap:
name: postgresql-initdb
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
resources:
requests:
storage: 20Gi
EOF
3.4. Redis
Redis используется как кэш для прикладных сервисов Unica. Данные Redis считаются временными и могут быть восстановлены приложениями, поэтому постоянное дисковое хранилище не используется.
Запуск Redis
Redis запускается одним экземпляром на ${DB_NODE}. В конфигурации отключены механизмы сохранения данных на диск, чтобы сервис работал только как кэш.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: unica-infra
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
nodeSelector:
kubernetes.io/hostname: ${DB_NODE}
containers:
- name: redis
image: redis:8.8.0-alpine
imagePullPolicy: IfNotPresent
ports:
- name: redis
containerPort: 6379
protocol: TCP
args:
- "--protected-mode"
- "no"
- "--appendonly"
- "no"
- "--save"
- ""
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
EOF
Внутрикластерный доступ к Redis
Service предоставляет Redis постоянное DNS-имя внутри Kubernetes. Внешний доступ к Redis не требуется.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: unica-infra
spec:
selector:
app: redis
type: ClusterIP
ports:
- name: redis
protocol: TCP
port: 6379
targetPort: 6379
EOF
Внутрикластерный адрес:
redis.unica-infra.svc.cluster.local:6379
3.5. RabbitMQ
RabbitMQ обеспечивает обмен сообщениями между сервисами обработки аудио. Для него создаются отдельные пользователи и права доступа.
Интерфейс управления и API RabbitMQ остаются доступны только внутри Kubernetes и необходимы Topology Operator для управления объектами RabbitMQ. Внешний доступ к интерфейсу управления не создаётся.
Подготовка хранения RabbitMQ
Очереди и служебные данные RabbitMQ сохраняются в каталоге /storage/rabbitmq на ${DB_NODE}.
mkdir -p /storage/rabbitmq
chown 999:999 /storage/rabbitmq
chmod 700 /storage/rabbitmq
Установка RabbitMQ Cluster Operator
Cluster Operator управляет жизненным циклом самого кластера RabbitMQ: создаёт StatefulSet, Service и связанные ресурсы по объекту RabbitmqCluster.
kubectl apply -f \
"https://github.com/rabbitmq/cluster-operator/releases/download/v2.21.1/cluster-operator.yml"
Проверка:
kubectl -n rabbitmq-system get pods -o wide
Установка RabbitMQ Messaging Topology Operator
Messaging Topology Operator управляет объектами внутри RabbitMQ — пользователями, правами доступа и другими элементами топологии. Ниже он используется для создания прикладных пользователей.
kubectl apply -f \
"https://github.com/rabbitmq/messaging-topology-operator/releases/download/v1.19.3/messaging-topology-operator-with-certmanager.yaml"
Проверка:
kubectl -n rabbitmq-system get pods -o wide
PersistentVolume для RabbitMQ
Том связывает хранилище RabbitMQ в Kubernetes с каталогом /storage/rabbitmq на ${DB_NODE}.
RABBITMQ_STORAGE_SIZE="10Gi"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: rabbitmq-pv
spec:
capacity:
storage: ${RABBITMQ_STORAGE_SIZE}
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /storage/rabbitmq
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${DB_NODE}
EOF
Учётные данные администратора RabbitMQ
Этот Secret используется RabbitMQ Cluster Operator при первом создании кластера. Учётная запись администратора нужна для служебного управления RabbitMQ внутри Kubernetes и не публикуется наружу.
RABBITMQ_ADMIN_PASSWORD="$(openssl rand -hex 24)"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: rabbitmq-default-user
namespace: unica-infra
type: Opaque
stringData:
default_user.conf: |
default_user = admin
default_pass = ${RABBITMQ_ADMIN_PASSWORD}
host: rabbitmq.unica-infra.svc.cluster.local
username: admin
password: ${RABBITMQ_ADMIN_PASSWORD}
port: "5672"
provider: rabbitmq
type: rabbitmq
EOF
Создание кластера RabbitMQ
Ресурс RabbitmqCluster описывает один экземпляр RabbitMQ, его хранилище, размещение на ${DB_NODE} и базовые параметры работы. Фактические Kubernetes-ресурсы создаёт Cluster Operator.
cat <<EOF | kubectl apply -f -
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
name: rabbitmq
namespace: unica-infra
spec:
replicas: 1
image: rabbitmq:4.2.6-management
secretBackend:
externalSecret:
name: rabbitmq-default-user
service:
type: ClusterIP
persistence:
storageClassName: local-storage
storage: 10Gi
resources:
requests:
cpu: "500m"
memory: "1Gi"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${DB_NODE}
rabbitmq:
additionalConfig: |
default_vhost = /
disk_free_limit.relative = 2.0
vm_memory_high_watermark.relative = 0.6
consumer_timeout = 7200000
EOF
Учётные данные прикладных пользователей
Для сервисов ASR, Recognition и Diarization создаются отдельные учётные записи RabbitMQ. Пароли генерируются при развёртывании и сохраняются в Kubernetes Secrets. Эти Secrets используются на следующем шаге при создании пользователей RabbitMQ.
RABBITMQ_ASR_PASSWORD="$(openssl rand -hex 24)"
RABBITMQ_RECOGNITION_PASSWORD="$(openssl rand -hex 24)"
RABBITMQ_DIARIZATION_PASSWORD="$(openssl rand -hex 24)"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: rabbitmq-asr-user
namespace: unica-infra
type: Opaque
stringData:
username: asr
password: ${RABBITMQ_ASR_PASSWORD}
---
apiVersion: v1
kind: Secret
metadata:
name: rabbitmq-recognition-user
namespace: unica-infra
type: Opaque
stringData:
username: recognition
password: ${RABBITMQ_RECOGNITION_PASSWORD}
---
apiVersion: v1
kind: Secret
metadata:
name: rabbitmq-diarization-user
namespace: unica-infra
type: Opaque
stringData:
username: diarization
password: ${RABBITMQ_DIARIZATION_PASSWORD}
EOF
Пользователи RabbitMQ
Ресурсы User связывают созданные Kubernetes Secrets с кластером RabbitMQ. RabbitMQ Messaging Topology Operator создаёт пользователей asr, recognition и diarization и использует пароли из соответствующих Secrets.
cat <<'EOF' | kubectl apply -f -
apiVersion: rabbitmq.com/v1beta1
kind: User
metadata:
name: asr
namespace: unica-infra
spec:
rabbitmqClusterReference:
name: rabbitmq
importCredentialsSecret:
name: rabbitmq-asr-user
---
apiVersion: rabbitmq.com/v1beta1
kind: User
metadata:
name: recognition
namespace: unica-infra
spec:
rabbitmqClusterReference:
name: rabbitmq
importCredentialsSecret:
name: rabbitmq-recognition-user
---
apiVersion: rabbitmq.com/v1beta1
kind: User
metadata:
name: diarization
namespace: unica-infra
spec:
rabbitmqClusterReference:
name: rabbitmq
importCredentialsSecret:
name: rabbitmq-diarization-user
EOF
Права пользователей RabbitMQ
Ресурсы Permission выдают созданным пользователям права в виртуальном хосте /. В текущей конфигурации каждому пользователю разрешено создавать, читать и записывать объекты RabbitMQ, необходимые прикладным сервисам.
cat <<'EOF' | kubectl apply -f -
apiVersion: rabbitmq.com/v1beta1
kind: Permission
metadata:
name: asr-permission
namespace: unica-infra
spec:
rabbitmqClusterReference:
name: rabbitmq
vhost: /
userReference:
name: asr
permissions:
configure: ".*"
write: ".*"
read: ".*"
---
apiVersion: rabbitmq.com/v1beta1
kind: Permission
metadata:
name: recognition-permission
namespace: unica-infra
spec:
rabbitmqClusterReference:
name: rabbitmq
vhost: /
userReference:
name: recognition
permissions:
configure: ".*"
write: ".*"
read: ".*"
---
apiVersion: rabbitmq.com/v1beta1
kind: Permission
metadata:
name: diarization-permission
namespace: unica-infra
spec:
rabbitmqClusterReference:
name: rabbitmq
vhost: /
userReference:
name: diarization
permissions:
configure: ".*"
write: ".*"
read: ".*"
EOF
3.6. Qdrant
Qdrant хранит векторные представления, используемые сервисами Unica для поиска и работы с семантическими данными. Данные Qdrant должны сохраняться между перезапусками, поэтому для него используется локальный постоянный том на ${DB_NODE}.
Подготовка хранения Qdrant
На ${DB_NODE} создаётся отдельный каталог, который будет использоваться только Qdrant.
mkdir -p /storage/qdrant
PersistentVolume для Qdrant
Том связывает каталог /storage/qdrant на ${DB_NODE} с хранилищем, которое подключит Helm-пакет Qdrant.
QDRANT_STORAGE_SIZE="20Gi"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: qdrant-pv
spec:
capacity:
storage: ${QDRANT_STORAGE_SIZE}
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /storage/qdrant
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${DB_NODE}
EOF
Ключ API Qdrant
Qdrant защищается отдельным ключом API. Ключ генерируется один раз и сохраняется в Kubernetes Secret, откуда затем передаётся Qdrant и сервисам Unica.
QDRANT_API_KEY="$(openssl rand -hex 24)"
kubectl -n unica-db create secret generic qdrant \
--from-literal=api-key="${QDRANT_API_KEY}"
Репозиторий Helm Qdrant
Добавляем официальный репозиторий, из которого будет установлен зафиксированный пакет Qdrant.
helm repo add qdrant https://qdrant.github.io/qdrant-helm
helm repo update
Установка Qdrant
Helm-релиз запускает один экземпляр Qdrant на ${DB_NODE}, подключает подготовленный том и Secret с ключом API. Сервис остаётся доступен только внутри Kubernetes.
helm upgrade --install qdrant qdrant/qdrant \
--namespace unica-db \
--version 1.19.0 \
--set replicaCount=1 \
--set image.repository=qdrant/qdrant \
--set image.tag=v1.19.0 \
--set image.pullPolicy=IfNotPresent \
--set fullnameOverride=qdrant \
--set-string 'nodeSelector.kubernetes\.io/hostname=${DB_NODE}' \
--set apiKey.valueFrom.secretKeyRef.name=qdrant \
--set apiKey.valueFrom.secretKeyRef.key=api-key \
--set persistence.size=20Gi \
--set persistence.storageClassName=local-storage \
--set persistence.accessModes[0]=ReadWriteOnce \
--set service.type=ClusterIP \
--set resources.requests.cpu=2 \
--set resources.requests.memory=16Gi
3.7. MinIO
MinIO предоставляет S3-совместимое объектное хранилище, в котором прикладные сервисы сохраняют файлы. Данные должны переживать перезапуск Pod-а, поэтому хранилище размещается на локальном диске ${DB_NODE}.
Подготовка хранения MinIO
На ${DB_NODE} создаётся отдельный каталог для объектов MinIO и назначаются права, с которыми контейнер сможет записывать данные.
mkdir -p /storage/minio
chown 1000:1000 /storage/minio
chmod 700 /storage/minio
PersistentVolume для MinIO
Том связывает каталог /storage/minio на ${DB_NODE} с постоянным хранилищем MinIO в Kubernetes.
MINIO_STORAGE_SIZE="20Gi"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: minio-pv
spec:
capacity:
storage: ${MINIO_STORAGE_SIZE}
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /storage/minio
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${DB_NODE}
EOF
Учётные данные MinIO
Создаём административную учётную запись MinIO. Эти значения хранятся в Secret пространства имён unica-storage; позднее необходимые сервисы получают копию учётных данных в своём пространстве имён.
MINIO_ROOT_USER="admin"
MINIO_ROOT_PASSWORD="$(openssl rand -hex 24)"
kubectl -n unica-storage create secret generic minio \
--from-literal=MINIO_ROOT_USER="${MINIO_ROOT_USER}" \
--from-literal=MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD}"
Внутрикластерный доступ к MinIO
Service публикует внутри Kubernetes два порта: S3 API на 9000 и консоль управления на 9001. Внешний Service для MinIO не создаётся.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: unica-storage
spec:
selector:
app: minio
type: ClusterIP
ports:
- name: api
protocol: TCP
port: 9000
targetPort: 9000
- name: console
protocol: TCP
port: 9001
targetPort: 9001
EOF
Внутрикластерные адреса:
minio.unica-storage.svc.cluster.local:9000
minio.unica-storage.svc.cluster.local:9001
Запуск MinIO
StatefulSet запускает MinIO на ${DB_NODE}, получает учётные данные из Secret и подключает постоянный том к /data.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: minio
namespace: unica-storage
spec:
serviceName: minio
replicas: 1
selector:
matchLabels:
app: minio
template:
metadata:
labels:
app: minio
spec:
nodeSelector:
kubernetes.io/hostname: ${DB_NODE}
securityContext:
fsGroup: 1000
containers:
- name: minio
image: minio/minio:RELEASE.2025-07-23T15-54-02Z
imagePullPolicy: IfNotPresent
args:
- server
- /data
- --console-address
- ":9001"
ports:
- name: s3
containerPort: 9000
- name: console
containerPort: 9001
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: minio
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: minio
key: MINIO_ROOT_PASSWORD
readinessProbe:
httpGet:
path: /minio/health/ready
port: 9000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
initialDelaySeconds: 20
periodSeconds: 10
resources:
requests:
cpu: "1"
memory: "2Gi"
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
resources:
requests:
storage: 20Gi
EOF
Проверка MinIO
После запуска убедитесь, что Pod MinIO находится в состоянии Running/Ready и размещён на ${DB_NODE}.
kubectl -n unica-storage get pods -o wide
Ожидается:
minio-0 1/1 Running ... ${DB_NODE}
3.8. Разделение GPU с помощью MIG
GPU NVIDIA A100 разделяется на несколько независимых MIG-ресурсов. Это позволяет одновременно выделить ресурсы для LLM, построения векторных представлений, диаризации и распознавания.
На демо-стенде используется NVIDIA A100 PCIe 40GB, поэтому дальнейшие команды и раскрой MIG приведены для этой GPU.
Используется профиль NVIDIA all-balanced. Пример раскроя демо-стенда:
3g.20gb x1 -> vLLM / модель Gemma-4-12B
2g.10gb x1 -> Diarization API
1g.5gb x1 -> vLLM / модель Qwen3-Embedding-8B
1g.5gb x1 -> Recognition Engine
Для одновременного использования разных MIG-профилей включается стратегия mixed.
На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}):
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
--type='json' \
-p='[{"op":"replace","path":"/spec/mig/strategy","value":"mixed"}]'
kubectl label node ${GPU_NODE} \
nvidia.com/mig.config=all-balanced \
--overwrite
Проверить результат:
DRIVER_POD=$(kubectl -n gpu-operator get pod \
-l app=nvidia-driver-daemonset \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n gpu-operator exec "${DRIVER_POD}" -c nvidia-driver-ctr -- nvidia-smi -L
Ожидается:
MIG 3g.20gb x1
MIG 2g.10gb x1
MIG 1g.5gb x2
Проверить ресурсы GPU, доступные Kubernetes:
kubectl get node ${GPU_NODE} -o json | \
jq '.status.allocatable |
with_entries(select(.key | startswith("nvidia.com/mig-")))'
Ожидается:
{
"nvidia.com/mig-1g.5gb": "2",
"nvidia.com/mig-2g.10gb": "1",
"nvidia.com/mig-3g.20gb": "1"
}
После этого узел с GPU готов к запуску vLLM и остальных компонентов, использующих GPU.
3.9. vLLM
vLLM запускает языковую модель и модель построения векторных представлений на выделенных MIG-ресурсах. Модели предварительно загружаются на узел с GPU и подключаются в Kubernetes через локальный том.
vLLM размещается в пространстве имён unica-infra; прикладные API обращаются к нему через внутренний сервис маршрутизации.
Модели:
google/gemma-4-12B-it-qat-w4a16-ct
локальный путь: /storage/vllm/Gemma-4-12B
имя модели для API: Gemma-4-12B
квантование: QAT w4a16 (compressed-tensors)
MIG: nvidia.com/mig-3g.20gb
максимальный контекст: 65536
вызов инструментов: gemma4 parser
Qwen/Qwen3-Embedding-8B
локальный путь: /storage/vllm/Qwen3-Embedding-8B
имя модели для API: Qwen3-Embedding-8B
квантование: BitsAndBytes 4-bit
MIG: nvidia.com/mig-1g.5gb
максимальный контекст: 16384
Подготовка моделей на узле с GPU
На узле с GPU (${GPU_NODE}):
mkdir -p /storage/vllm
chmod 755 /storage/vllm
apt update
apt install -y python3-venv
python3 -m venv /opt/hf
/opt/hf/bin/pip install --upgrade pip
/opt/hf/bin/pip install --upgrade huggingface_hub hf_xet
Загрузить языковую модель Gemma-4-12B:
/opt/hf/bin/hf download \
google/gemma-4-12B-it-qat-w4a16-ct \
--local-dir /storage/vllm/Gemma-4-12B
Загрузить модель Qwen3-Embedding-8B для построения векторных представлений:
/opt/hf/bin/hf download \
Qwen/Qwen3-Embedding-8B \
--local-dir /storage/vllm/Qwen3-Embedding-8B
Проверить размер:
du -sh /storage/vllm/*
Хранилище моделей vLLM
PV и PVC подключают каталог /storage/vllm с уже загруженными моделями к Pod-ам vLLM. Само скачивание моделей выполняется на GPU-узле до запуска Helm-релиза.
На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}) создать /root/vllm-storage.yaml:
cat > /root/vllm-storage.yaml <<EOF
apiVersion: v1
kind: PersistentVolume
metadata:
name: vllm-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
claimRef:
namespace: unica-infra
name: data-vllm
local:
path: /storage/vllm
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${GPU_NODE}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-vllm
namespace: unica-infra
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
volumeName: vllm-pv
resources:
requests:
storage: 100Gi
EOF
Применить:
kubectl apply -f /root/vllm-storage.yaml
Проверить:
kubectl get pv vllm-pv
kubectl -n unica-infra get pvc data-vllm
PV и PVC должны быть Bound.
Репозиторий Helm vLLM
Добавляем официальный репозиторий vLLM Production Stack.
helm repo add vllm https://vllm-project.github.io/production-stack
helm repo update
Конфигурация vLLM
В файле values-vllm.yaml описываются два экземпляра vLLM: языковая модель Gemma-4-12B и модель Qwen3-Embedding-8B. Для каждой модели задаётся свой MIG-ресурс, путь к локальным файлам модели и параметры запуска. Маршрутизатор объединяет оба экземпляра за одним внутренним сервисом Kubernetes.
На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}) создать /root/values-vllm.yaml:
cat > /root/values-vllm.yaml <<EOF
servingEngineSpec:
runtimeClassName: "nvidia"
imagePullPolicy: "IfNotPresent"
sidecar:
image: "lmcache/lmstack-sidecar:latest"
imagePullPolicy: "IfNotPresent"
startupProbe:
initialDelaySeconds: 120
periodSeconds: 30
failureThreshold: 120
httpGet:
path: /health
port: 8000
livenessProbe:
initialDelaySeconds: 120
periodSeconds: 30
failureThreshold: 3
timeoutSeconds: 30
httpGet:
path: /health
port: 8000
readinessProbe:
initialDelaySeconds: 120
periodSeconds: 10
failureThreshold: 5
timeoutSeconds: 30
httpGet:
path: /health
port: 8000
strategy:
type: Recreate
serviceMonitor:
enabled: false
modelSpec:
- name: "gemma-4-12b"
repository: "vllm/vllm-openai"
tag: "v0.25.1"
modelURL: "/models/Gemma-4-12B"
replicaCount: 1
requestCPU: 6
requestMemory: "16Gi"
requestGPU: 1
requestGPUType: "nvidia.com/mig-3g.20gb"
shmSize: "8Gi"
extraVolumes:
- name: models
persistentVolumeClaim:
claimName: data-vllm
extraVolumeMounts:
- name: models
mountPath: /models
readOnly: true
vllmConfig:
tensorParallelSize: 1
maxModelLen: 65536
dtype: "bfloat16"
gpuMemoryUtilization: 0.93
enableChunkedPrefill: true
enablePrefixCaching: true
maxNumSeqs: 4
extraArgs:
- "--enable-auto-tool-choice"
- "--tool-call-parser=gemma4"
- "--reasoning-parser=gemma4"
- "--chat-template=examples/tool_chat_template_gemma4.jinja"
- "--served-model-name=Gemma-4-12B"
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: "In"
values:
- "${GPU_NODE}"
- name: "qwen3-embedding-8b"
repository: "vllm/vllm-openai"
tag: "v0.25.1"
modelURL: "/models/Qwen3-Embedding-8B"
replicaCount: 1
requestCPU: 2
requestMemory: "8Gi"
requestGPU: 1
requestGPUType: "nvidia.com/mig-1g.5gb"
extraVolumes:
- name: models
persistentVolumeClaim:
claimName: data-vllm
extraVolumeMounts:
- name: models
mountPath: /models
readOnly: true
vllmConfig:
tensorParallelSize: 1
maxModelLen: 16384
dtype: "bfloat16"
gpuMemoryUtilization: 0.95
maxNumSeqs: 2
runner: "pooling"
convert: "embed"
extraArgs:
- "--quantization=bitsandbytes"
- "--cpu-offload-gb=4"
- "--enforce-eager"
- "--served-model-name=Qwen3-Embedding-8B"
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: "In"
values:
- "${GPU_NODE}"
routerSpec:
enableRouter: true
repository: "lmcache/lmstack-router"
tag: "v0.1.12"
imagePullPolicy: "IfNotPresent"
startupProbe:
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 30
httpGet:
path: /health
livenessProbe:
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 3
httpGet:
path: /health
readinessProbe:
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 6
httpGet:
path: /health
serviceType: ClusterIP
servicePort: 80
routingLogic: "roundrobin"
resources:
requests:
cpu: 400m
memory: 1Gi
limits: null
serviceMonitor:
enabled: false
EOF
Установка vLLM
Helm создаёт два экземпляра vLLM для моделей и внутренний маршрутизатор. Внешний Ingress для vLLM не требуется: к маршрутизатору обращаются только прикладные API внутри Kubernetes.
helm upgrade --install vllm vllm/vllm-stack \
--namespace unica-infra \
--version 0.1.12 \
-f /root/values-vllm.yaml
Проверить запуск Pod-ов моделей и сервиса маршрутизации:
kubectl -n unica-infra get pods -o wide
Проверить имя сервиса маршрутизации (оно используется при установке Embedding API и LLM API):
kubectl -n unica-infra get svc
Ожидается vllm-router-service.
Проверить, что vLLM запущен с ожидаемыми ограничениями контекста:
kubectl -n unica-infra logs -l app.kubernetes.io/instance=vllm --all-containers=true --tail=300 | \
grep -Ei 'max[_ -]?model[_ -]?len|max sequence length|65536|16384' || true
3.10. Docling Serve — опциональный компонент
Docling используется для расширенного разбора документов. Компонент опциональный: если он не требуется, этот шаг полностью пропускается.
Docling разворачивается в пространство имён unica-infra и использует локальный файл EasyOCR cyrillic_g2.pth.
Подготовка модели OCR
Docling использует локальный файл модели EasyOCR. Файл заранее размещается на ${DB_NODE} и затем монтируется в контейнер только для чтения.
Docling размещается на ${DB_NODE}.
На узел инфраструктурных сервисов (${DB_NODE}) создать каталог для модели:
mkdir -p /storage/docling-serve
chown root:root /storage/docling-serve
chmod 755 /storage/docling-serve
Скачать модель сразу в целевой каталог:
wget \
"https://downloads.rp-it.ru/cyrillic_g2.pth" \
-O /storage/docling-serve/cyrillic_g2.pth
Установить владельца и права только для файла модели. Каталог /storage/docling-serve остаётся root:root:
chown 1001:0 /storage/docling-serve/cyrillic_g2.pth
chmod 644 /storage/docling-serve/cyrillic_g2.pth
Проверить:
stat -c '%n %u:%g %a' \
/storage/docling-serve \
/storage/docling-serve/cyrillic_g2.pth
df -hT /storage/docling-serve
Ожидается:
/storage/docling-serve 0:0 755
/storage/docling-serve/cyrillic_g2.pth 1001:0 644
PersistentVolume для модели Docling
PersistentVolume связывает каталог /storage/docling-serve на ${DB_NODE} с Kubernetes. В этом каталоге хранится только файл модели OCR.
На управляющем узле Kubernetes (${CONTROL_PLANE_NODE}):
DOCLING_STORAGE_SIZE="1Gi"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: docling-serve-pv
spec:
capacity:
storage: ${DOCLING_STORAGE_SIZE}
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /storage/docling-serve
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${DB_NODE}
EOF
Подключение тома к Docling
PVC закрепляется за подготовленным PersistentVolume и используется Deployment-ом Docling для доступа к модели.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-docling-serve
namespace: unica-infra
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
volumeName: docling-serve-pv
resources:
requests:
storage: 1Gi
EOF
Внутрикластерный доступ к Docling
Service предоставляет Docling постоянный адрес внутри Kubernetes. Компонент не публикуется наружу.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: docling-serve
namespace: unica-infra
labels:
app: docling-serve
spec:
selector:
app: docling-serve
type: ClusterIP
ports:
- name: http
port: 5001
targetPort: 5001
EOF
Запуск Docling
Deployment запускает Docling на ${DB_NODE} и монтирует подготовленный файл EasyOCR в каталог, из которого приложение ожидает загрузить модель.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: docling-serve
namespace: unica-infra
labels:
app: docling-serve
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: docling-serve
template:
metadata:
labels:
app: docling-serve
spec:
nodeSelector:
kubernetes.io/hostname: ${DB_NODE}
containers:
- name: docling-serve
image: ghcr.io/docling-project/docling-serve:v1.20.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 5001
protocol: TCP
env:
- name: UVICORN_WORKERS
value: "1"
- name: DOCLING_SERVE_LOAD_MODELS_AT_BOOT
value: "false"
- name: DOCLING_SERVE_MAX_SYNC_WAIT
value: "600"
- name: DOCLING_SERVE_LOG_LEVEL
value: "INFO"
- name: DOCLING_SERVE_DEFAULT_OCR_PRESET
value: "easyocr"
- name: DOCLING_SERVE_DEFAULT_OCR_KIND
value: "easyocr"
- name: DOCLING_SERVE_ENG_LOC_NUM_WORKERS
value: "1"
- name: DOCLING_SERVE_QUEUE_MAX_SIZE
value: "10"
- name: DOCLING_SERVE_OCR_BATCH_SIZE
value: "4"
- name: DOCLING_SERVE_LAYOUT_BATCH_SIZE
value: "4"
- name: DOCLING_SERVE_TABLE_BATCH_SIZE
value: "2"
- name: DOCLING_SERVE_BATCH_POLLING_INTERVAL_SECONDS
value: "0.5"
- name: OMP_NUM_THREADS
value: "2"
- name: MKL_NUM_THREADS
value: "2"
- name: OPENBLAS_NUM_THREADS
value: "2"
- name: NUMEXPR_NUM_THREADS
value: "2"
- name: DOCLING_NUM_THREADS
value: "2"
- name: DOCLING_DEBUG_PROFILE_PIPELINE_TIMINGS
value: "true"
volumeMounts:
- name: docling-models
mountPath: /opt/app-root/src/.cache/docling/models/EasyOcr/cyrillic_g2.pth
subPath: cyrillic_g2.pth
readOnly: true
volumes:
- name: docling-models
persistentVolumeClaim:
claimName: data-docling-serve
readOnly: true
EOF
Проверка Docling
kubectl get pv docling-serve-pv
kubectl -n unica-infra get pvc data-docling-serve
kubectl -n unica-infra get pods -l app=docling-serve -o wide
kubectl -n unica-infra get svc docling-serve
PV и PVC должны быть Bound, Pod — Running/Ready.
Проверить, что файл доступен внутри контейнера:
kubectl -n unica-infra exec deploy/docling-serve -- \
ls -lh /opt/app-root/src/.cache/docling/models/EasyOcr/cyrillic_g2.pth
Проверить логи:
kubectl -n unica-infra logs deploy/docling-serve --tail=200
Внутрикластерный адрес:
http://docling-serve.unica-infra.svc.cluster.local:5001
Проверка готовности инфраструктуры
Перед установкой продукта убедитесь, что обязательные инфраструктурные компоненты запущены и доступны внутри Kubernetes. Docling проверяется только в том случае, если он был установлен.
kubectl -n unica-db get pods -o wide
kubectl -n unica-infra get pods -o wide
kubectl -n unica-storage get pods -o wide
Обязательные Pod-ы должны находиться в состоянии Running/Ready.