# Postgres — lustro baz Excela w SQL (DAN-28). # # DLACZEGO POSTGRES, A NIE SQLITE. Nie ze względu na skalę: dzisiejszy zbiór to # ~54 tys. wierszy, przy których SQLite bywa szybszy. Powodem jest WYSZUKIWANIE # w nieznormalizowanym, polskim tekście — `pg_trgm` (dopasowanie rozmyte, literówki) # i `unaccent` (ogonki) nie mają w SQLite taniego zamiennika, a to jest główna # funkcja programu, nie dodatek. Drugi powód: `pg_advisory_lock` sprawia, że model # sesji z atomowym commitem przestaje zakładać „jedna replika na zawsze". # # TO LUSTRO, NIE ŹRÓDŁO PRAWDY. Źródłem są pliki Excela na NFS. Utrata tej bazy # NIE JEST utratą danych — odbudowuje się z plików. Dlatego kopie zapasowe są tu # OPCJONALNE, a wolumen może stać na local-path. --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-data namespace: astrololo spec: accessModes: [ReadWriteOnce] # local-path (domyślny provisioner k3s), NIE NFS. Postgres zakłada semantykę # blokad i fsync, której NFS nie gwarantuje — to klasyczne źródło uszkodzenia # bazy przy nagłym restarcie. Ceną jest przywiązanie do węzła; przy luście # odtwarzalnym z Excela to akceptowalne. storageClassName: local-path resources: requests: storage: 5Gi --- apiVersion: v1 kind: ConfigMap metadata: name: postgres-init namespace: astrololo data: # UWAGA: skrypty z /docker-entrypoint-initdb.d/ uruchamiają się WYŁĄCZNIE przy # pierwszej inicjalizacji, na pustym katalogu danych. Dopisanie tu rozszerzenia # PO utworzeniu bazy nic nie zrobi — trzeba je wtedy założyć ręcznie (patrz # README-postgres.md, sekcja „Dodanie rozszerzenia do istniejącej bazy"). 01-extensions.sql: | -- Dopasowanie rozmyte: similarity(), operator %, indeksy GIN po trigramach. -- To jest powód, dla którego stoi tu Postgres, a nie SQLite. CREATE EXTENSION IF NOT EXISTS pg_trgm; -- Zdejmowanie ogonków: „różdżka" ma się znaleźć na „rozdzka". CREATE EXTENSION IF NOT EXISTS unaccent; -- Schemat na lustro. Osobny, żeby purge i odbudowa nie musiały ruszać -- niczego w `public` i żeby `DROP SCHEMA mirror CASCADE` był bezpiecznym -- „wyczyść wszystko i wczytaj od nowa". CREATE SCHEMA IF NOT EXISTS mirror; -- Rejestr luster: co, z jakiego pliku, o jakim skrócie i kiedy wczytane. -- Zgodność pliku z tabelą poznajemy po sha256 — nazwa pliku nie wystarcza, -- bo treść potrafi się zmienić bez zmiany nazwy. CREATE TABLE IF NOT EXISTS mirror.registry ( table_name text PRIMARY KEY, source_path text NOT NULL UNIQUE, sha256 text NOT NULL, row_count integer NOT NULL DEFAULT 0, loaded_at timestamptz NOT NULL DEFAULT now(), active boolean NOT NULL DEFAULT false ); CREATE INDEX IF NOT EXISTS registry_active_idx ON mirror.registry (active); --- apiVersion: apps/v1 kind: Deployment metadata: name: postgres namespace: astrololo spec: replicas: 1 # Recreate, NIE RollingUpdate: wolumen jest ReadWriteOnce, a dwa procesy # Postgresa na jednym katalogu danych to uszkodzona baza. Rolling próbowałby # wstać z nowym podem, zanim stary zejdzie. strategy: type: Recreate selector: matchLabels: { app: postgres } template: metadata: labels: { app: postgres } spec: containers: - name: postgres # Wersja PRZYPIĘTA i celowo poza image-updaterem: podbicie majora # Postgresa wymaga migracji katalogu danych, więc nie może się zdarzyć # samo, w nocy, przy okazji builda aplikacji. image: postgres:17 ports: [{ containerPort: 5432, name: postgres }] env: - name: POSTGRES_DB value: astrololo - name: POSTGRES_USER value: astrololo - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: { name: astrololo-postgres, key: POSTGRES_PASSWORD } # PGDATA w PODKATALOGU montowanego wolumenu. Katalog główny wolumenu # potrafi zawierać wpisy systemu plików (np. lost+found), a initdb # odmawia inicjalizacji w niepustym katalogu. - name: PGDATA value: /var/lib/postgresql/data/pgdata # C.UTF-8 zamiast pl_PL.UTF-8: dopasowanie tekstu robimy przez # unaccent i pg_trgm, nie przez collation, a locale pl_PL wymagałoby # obrazu z wygenerowanymi lokalizacjami. Gdyby kiedyś potrzebne było # polskie SORTOWANIE, dokłada się collation ICU na konkretnej kolumnie. - name: POSTGRES_INITDB_ARGS value: "--encoding=UTF8 --locale=C.UTF-8" volumeMounts: - name: data mountPath: /var/lib/postgresql/data - name: init mountPath: /docker-entrypoint-initdb.d readOnly: true # pg_isready, nie zwykły TCP: gniazdo słucha, zanim baza przyjmuje # zapytania, więc sonda po porcie przepuściłaby ruch za wcześnie. readinessProbe: exec: command: ["pg_isready", "-U", "astrololo", "-d", "astrololo", "-q"] initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: exec: command: ["pg_isready", "-U", "astrololo", "-d", "astrololo", "-q"] initialDelaySeconds: 30 periodSeconds: 20 failureThreshold: 6 resources: requests: { cpu: "100m", memory: "256Mi" } limits: { cpu: "1000m", memory: "1Gi" } volumes: - name: data persistentVolumeClaim: claimName: postgres-data - name: init configMap: name: postgres-init --- apiVersion: v1 kind: Service metadata: name: postgres namespace: astrololo spec: # ClusterIP i tylko ClusterIP. Baza nie ma żadnego powodu być widoczna poza # klastrem; dostęp z zewnątrz robi się przez `kubectl port-forward` na czas # jednej sesji (patrz README-postgres.md). type: ClusterIP selector: { app: postgres } ports: [{ port: 5432, targetPort: 5432 }]