feat(astrololo): Postgres jako lustro baz w SQL + runbook (DAN-28)
Manifest, wpięcie w kustomization i runbook krok po kroku. DECYZJE ZAPISANE W MANIFEŚCIE, ŻEBY NIE TRZEBA ICH BYŁO ODTWARZAĆ Z GŁOWY: local-path, 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. strategy: Recreate. 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. PGDATA w PODKATALOGU wolumenu: katalog główny potrafi zawierać wpisy systemu plików, a initdb odmawia pracy w niepustym katalogu. Wersja PRZYPIĘTA i poza image-updaterem: podbicie majora wymaga migracji katalogu danych, więc nie może się zdarzyć samo, w nocy, przy okazji builda aplikacji. C.UTF-8 zamiast pl_PL.UTF-8: dopasowanie tekstu robimy przez unaccent i pg_trgm, nie przez collation, a pl_PL wymagałby obrazu z wygenerowanymi lokalizacjami. DATA_PROVIDER zostaje na `excel`. Postgres można wdrożyć i obejrzeć BEZ ryzyka dla działającego wyszukiwania; przełączenie to osobna, późniejsza decyzja. SPRAWDZONE PRZED ODDANIEM: `kubectl kustomize astrololo` składa komplet 21 zasobów bez błędu, YAML parsuje się poprawnie, audyt odwołań potwierdza, że jedynym brakującym sekretem jest astrololo-postgres (oba klucze), a DSN postgresql+psycopg:// jest rozpoznawany przez SQLAlchemy z psycopg 3. NIE SPRAWDZONE: skrypt inicjalizujący nie biegł przeciwko prawdziwemu Postgresowi — na maszynie, na której to powstawało, nie ma Dockera. Zapisane wprost w runbooku; krok 4 jest tam prawdziwym testem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #16.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# 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 }]
|
||||
Reference in New Issue
Block a user