StatefulSets i przechowywanie danych w Kubernetes

Wdrażaj aplikacje stanowe z skalowaniem z zachowaniem kolejności i danymi trwałymi

Kubernetes StatefulSets to idealne rozwiązanie do zarządzania aplikacjami stanowymi, które wymagają stabilnych identyfikatorów, trwałego przechowywania danych oraz uporządkowanych wzorców wdrażania – niezbędnych dla baz danych, systemów rozproszonych oraz warstw cache.

Jeśli jesteś nowy w Kubernetes lub tworzysz klaster, rozważ eksplorowanie dystrybucji Kubernetes takich jak k3s lub MicroK8s do rozwoju, lub instalację Kubernetes z użyciem Kubespray do klastrów produkcyjnych.

prezentacja w kawiarni To piękne zdjęcie zostało wygenerowane przez model AI Flux 1 dev.

Co to są StatefulSets?

StatefulSets to obiekt API obciążenia w Kubernetes, zaprojektowany specjalnie do zarządzania aplikacjami stanowymi. W przeciwieństwie do Deployments, które traktują wszystkie kontenery jako wymienne, StatefulSets utrzymują unikalną tożsamość dla każdego kontenera z gwarancjami dotyczącymi kolejności i unikalności.

Główne cechy:

  • Stable Network Identifiers: Każdy kontener otrzymuje przewidywalny nazwisko hosta, który utrzymuje się po ponownym uruchomieniu
  • Trwałe przechowywanie danych: Wyłączone PersistentVolumeClaims, które śledzą kontenery podczas ponownego wdrażania
  • Uporządkowane wdrażanie: Kontenery tworzone są sekwencyjnie (0, 1, 2…) i zakończone w odwrotnej kolejności
  • Uporządkowane aktualizacje: Aktualizacje w trybie rolowym przebiegają w kolejności, zapewniając stabilność aplikacji

StatefulSets są kluczowe dla aplikacji takich jak PostgreSQL, MySQL, MongoDB, Cassandra, Elasticsearch, Kafka, ZooKeeper, Redis i etcd – wszystkie obciążenia, w których tożsamość kontenera i trwałość danych mają znaczenie.

Zrozumienie trwałego przechowywania w Kubernetes

Kubernetes oferuje zaawansowaną warstwę abstrakcji przechowywania, która oddziela zarządzanie przechowywaniem od cyklu życia kontenera:

Komponenty przechowywania

PersistentVolume (PV): Fragment przechowywania w klastrze przydzielony przez administratora lub dynamicznie utworzony za pomocą StorageClass. PV istnieje niezależnie od kontenerów.

PersistentVolumeClaim (PVC): Wymóg przechowywania przez kontener. PVC wiąże się z dostępnymi PV, które odpowiadają jego wymaganiom (rozmiar, tryb dostępu, klasa przechowywania).

StorageClass: Definiuje różne “klasy” przechowywania z różnymi dostarczycielami (AWS EBS, GCE PD, Azure Disk, NFS itp.) i parametrami takimi jak replikacja, poziomy wydajności i polityki kopii zapasowych.

Tryby dostępu

  • ReadWriteOnce (RWO): Wolumin zamontowany jako do odczytu i zapisu przez pojedynczy węzeł
  • ReadOnlyMany (ROX): Wolumin zamontowany jako do odczytu przez wiele węzłów
  • ReadWriteMany (RWX): Wolumin zamontowany jako do odczytu i zapisu przez wiele węzłów (wymaga specjalnych backendów przechowywania)

Architektura przechowywania StatefulSet

StatefulSets korzystają z volumeClaimTemplates, aby automatycznie utworzyć PersistentVolumeClaims dla każdej repliki kontenera. To jest fundamentalnie inne niż Deployments:

Jak działa volumeClaimTemplates

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-service
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "fast-ssd"
      resources:
        requests:
          storage: 10Gi

Gdy utworzysz ten StatefulSet:

  1. Kubernetes utworzy kontener mysql-0 i PVC data-mysql-0
  2. Następnie utworzy kontener mysql-1 i PVC data-mysql-1
  3. Na koniec utworzy kontener mysql-2 i PVC data-mysql-2

Każdy kontener otrzymuje własny dedykowany 10GB wolumin trwały. Jeśli mysql-1 zostanie usunięty lub ponownie wdrożony, Kubernetes ponownie utworzy go i dołączy ten sam data-mysql-1 PVC, zachowując wszystkie dane.

Tworzenie bezgłównej usługi dla StatefulSets

StatefulSets wymagają bezgłównej usługi, aby zapewnić stabilne identyfikatory sieciowe:

apiVersion: v1
kind: Service
metadata:
  name: mysql-service
spec:
  clusterIP: None  # To sprawia, że jest to bezgłowa usługa
  selector:
    app: mysql
  ports:
  - port: 3306
    name: mysql

To tworzy wpisy DNS dla każdego kontenera:

  • mysql-0.mysql-service.default.svc.cluster.local
  • mysql-1.mysql-service.default.svc.cluster.local
  • mysql-2.mysql-service.default.svc.cluster.local

Aplikacje mogą bezpośrednio łączyć się z konkretnymi instancjami kontenerów za pomocą tych stabilnych nazw DNS.

Wzorce wdrażania StatefulSet

Dla zespołów zarządzających złożonymi wdrożeniami Kubernetes, Helm Charts oferują potężny sposób pakowania i wdrażania StatefulSets z szablonami, wersjonowaniem i zarządzaniem zależnościami. Helm upraszcza zarządzanie konfiguracjami StatefulSet w różnych środowiskach.

Skalowanie uporządkowane

Podczas skalowania z 3 do 5 replik:

kubectl scale statefulset mysql --replicas=5

Kubernetes tworzy kontenery w kolejności: mysql-3 → czeka, aż będzie gotowy → mysql-4

Podczas skalowania z 5 do 3 replik:

kubectl scale statefulset mysql --replicas=3

Kubernetes kończy w odwrotnej kolejności: mysql-4 → czeka, aż zostanie zakończony → mysql-3

Aktualizacje rolowe

StatefulSets obsługują dwa strategie aktualizacji:

OnDelete: Aktualizacje ręczne – kontenery aktualizują się tylko wtedy, gdy je usuniesz RollingUpdate: Automatyczne sekwencyjne aktualizacje w odwrotnej kolejności numerów

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2  # Aktualizuj tylko kontenery z numerem >= 2

Parametr partition umożliwia wdrażanie kanaryjskie – możesz najpierw aktualizować kontenery o wyższych numerach, a następnie przetestować przed wdrożeniem na wszystkie repliki.

Praktyki przechowywania

Dynamiczne przydzielanie

Zawsze używaj StorageClass do dynamicznego przydzielania woluminów:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
allowVolumeExpansion: true
reclaimPolicy: Retain

allowVolumeExpansion: Włącza rozszerzanie PVC bez ponownego tworzenia reclaimPolicy: Retain zachowuje dane PV po usunięciu PVC, Delete usuwa je automatycznie

Polityki retencji PVC

Kubernetes 1.23+ obsługuje persistentVolumeClaimRetentionPolicy:

spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain    # Zachowuj PVC po usunięciu StatefulSet
    whenScaled: Delete     # Usuń PVC po skalowaniu w dół

Opcje:

  • Retain: Zachowuj PVC (domyślny zachowanie, najbezpieczniejsze)
  • Delete: Automatycznie usuwaj PVC (przydatne w środowiskach deweloperskich)

Strategie kopii zapasowych

Snapshopy woluminów: Użyj zasobów VolumeSnapshot do tworzenia kopii zapasowych w punkcie w czasie

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-snapshot-20251113
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: data-mysql-0

Kopie zapasowe na poziomie aplikacji: Użyj narzędzi takich jak mysqldump, pg_dump lub Velero do kopii zapasowych baz danych

Replikacja rozproszona: Skonfiguruj replikację na poziomie aplikacji (replikacja MySQL, strumieniowa replikacja PostgreSQL) jako pierwszy warstwę obrony

Przypadki użycia w praktyce

Klaster bazy danych (PostgreSQL)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "standard"
      resources:
        requests:
          storage: 20Gi

Rozproszony cache (Redis)

Dla klastrów Redis potrzebne są zarówno StatefulSet, jak i staranny konfiguracja:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis-service
  replicas: 6
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        command:
        - redis-server
        - "--appendonly"
        - "yes"
        - "--appendfsync"
        - "everysec"
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 5Gi

Kolejka wiadomości (Kafka)

Kafka wymaga zarówno trwałego przechowywania dla dzienników, jak i stabilnych identyfikatorów sieciowych dla koordynacji brokerów:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kafka
spec:
  serviceName: kafka-service
  replicas: 3
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      containers:
      - name: kafka
        image: confluentinc/cp-kafka:7.5.0
        ports:
        - containerPort: 9092
          name: kafka
        env:
        - name: KAFKA_BROKER_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        volumeMounts:
        - name: data
          mountPath: /var/lib/kafka/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 100Gi

Monitorowanie i rozwiązywanie problemów

Dla kompletnego odniesienia do poleceń Kubernetes używanych w tej sekcji, zobacz Kubernetes Cheatsheet.

Sprawdź stan StatefulSet

# Wyświetl szczegóły StatefulSet
kubectl get statefulset mysql
kubectl describe statefulset mysql

# Sprawdź kolejność tworzenia kontenerów i ich stan
kubectl get pods -l app=mysql -w

# Wyświetl stan PVC
kubectl get pvc
kubectl describe pvc data-mysql-0

Typowe problemy

Kontener zawieszony w stanie oczekiwania: Sprawdź stan PVC i dostępność przechowywania

kubectl describe pod mysql-0
kubectl get events --sort-by='.lastTimestamp'

Pełne przechowywanie: Rozszerz PVC, jeśli StorageClass to pozwala

kubectl patch pvc data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

Kontener nie chce się zakończyć: Sprawdź blokady na poziomie aplikacji lub problemy z odmontowaniem woluminu

kubectl delete pod mysql-0 --grace-period=0 --force

Metryki do monitorowania

  • Użycie przechowywania: Monitoruj pojemność i procent użycia PVC
  • Wydajność I/O: śledź IOPS, przepustowość i opóźnienie
  • Restarty kontenerów: Częste restarty mogą wskazywać na problemy z przechowywaniem
  • Czas wiązania PVC: Wolne wiązanie wskazuje na problemy z przydzielaniem

Strategie migracji

Podczas migracji do StatefulSets upewnij się, że klaster Kubernetes jest odpowiednio skonfigurowany. Dla domowych laboratoriów lub małych klastrów, sprawdź nasz szczegółowy przegląd dystrybucji Kubernetes, aby wybrać odpowiednią platformę dla Twoich wymagań obciążenia.

Od Deployment do StatefulSet

  1. Utwórz StatefulSet z volumeClaimTemplates
  2. Zmniejsz skalę Deployment w sposób łagodny
  3. Przywróć dane z kopii zapasowych do kontenerów StatefulSet
  4. Zaktualizuj odniesienia do DNS/Service
  5. Usuń stary Deployment i PVCs

Kopie zapasowe przed migracją

# Zrób snapshot istniejących PVC
kubectl get pvc -o yaml > pvc-backup.yaml

# Utwórz snapshoty woluminów
kubectl apply -f volume-snapshot.yaml

Rozważania bezpieczeństwa

Szyfrowanie przechowywania

Włącz szyfrowanie danych w spoczynku za pomocą parametrów StorageClass:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: encrypted-storage
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  encrypted: "true"
  kmsKeyId: arn:aws:kms:us-east-1:123456789012:key/abcd-1234

Kontrola dostępu

Użyj RBAC, aby ograniczyć, kto może tworzyć/modyfikować StatefulSets i PVCs:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: statefulset-manager
rules:
- apiGroups: ["apps"]
  resources: ["statefulsets"]
  verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: [""]
  resources: ["persistentvolumeclaims"]
  verbs: ["get", "list", "create", "delete"]

Polityki sieciowe

Ogranicz komunikację między kontenerami:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mysql-network-policy
spec:
  podSelector:
    matchLabels:
      app: mysql
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 3306

Optymalizacja wydajności

Warstwy wydajności przechowywania

Wybierz odpowiednie StorageClasses na podstawie obciążenia:

  • Wysokie IOPS: Bazy danych z intensywnym odczytem i zapisem losowym (gp3, io2)
  • Wysoka przepustowość: Agregacja dzienników, analiza (st1, sc1)
  • Zrównoważone: Ogólne cele (gp3)

Rozproszenie kontenerów

Użyj antyafinity podów, aby rozproszyć kontenery StatefulSet w różnych strefach dostępności:

spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: mysql
            topologyKey: topology.kubernetes.io/zone

Wymagania i limity zasobów

Ustaw odpowiednie zasoby dla spójnej wydajności:

resources:
  requests:
    cpu: "2"
    memory: "4Gi"
    ephemeral-storage: "10Gi"
  limits:
    cpu: "4"
    memory: "8Gi"
    ephemeral-storage: "20Gi"

Zaawansowane wzorce

Aplikacja stanowa z kontenerami inicjalizacyjnymi

Użyj kontenerów inicjalizacyjnych do inicjalizacji bazy danych:

spec:
  template:
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:8.0
        command:
        - bash
        - "-c"
        - |
          if [[ ! -d /var/lib/mysql/mysql ]]; then
            mysqld --initialize-insecure --datadir=/var/lib/mysql
          fi          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql

Wiele kontenerów w podach dla sidecarów

Dodaj sidecary do kopii zapasowych lub agentów monitoringu:

spec:
  template:
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        # ... konfiguracja mysql ...
      - name: backup-sidecar
        image: mysql-backup:latest
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          readOnly: true

ConfigMaps dla dynamicznej konfiguracji

Oddziel konfigurację od definicji StatefulSet:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  my.cnf: |
    [mysqld]
    max_connections=200
    innodb_buffer_pool_size=2G    
---
spec:
  template:
    spec:
      containers:
      - name: mysql
        volumeMounts:
        - name: config
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config
        configMap:
          name: mysql-config

Przydatne linki