technik: Volltext-Indizes von press_releases in eine eigene Suchtabelle auslagern

Ein FULLTEXT-Index zwingt MySQL bei JEDEM ADD COLUMN auf ALGORITHM=COPY. Auf
press_releases (>1 GB) bedeutet das einen vollständigen Rebuild, während dessen
kein Schreibzugriff möglich ist — im August hat genau das die Seite zum Stehen
gebracht, weil die wartenden Requests PHP-FPM-Worker und DB-Verbindungen
belegten.

Die durchsuchbaren Felder liegen jetzt gespiegelt in `press_release_search`,
die die Indizes trägt. Ihr Schema ist per Konstruktion stabil und wird nie
erweitert; neue Felder gehören nach press_releases, wo sie ohne Volltext-Index
Millisekunden kosten. Synchron hält sie ein Observer (alle Schreibzugriffe
laufen über Eloquent, auch der Importer), reparieren lässt sie sich mit
press-releases:reindex-search.

Der Umstieg ist über mehrere Migrationen geteilt und schaltet erst um, wenn
press_releases seine Indizes verloren hat — bis dahin sucht die Anwendung
unverändert weiter. Die Drop-Migration verweigert den Dienst, solange der
Suchindex unvollständig ist, und verwirft die gecachte Umschalt-Prüfung: Stünde
dort noch der alte Stand, setzte der Rückfallpfad sein MATCH() auf einen
soeben entfernten Index ab (MySQL-Fehler 1191) und jede Suche endete im 500er.

Dazu die betriebliche Absicherung:

- DB_LOCK_WAIT_TIMEOUT (Default 10 s) statt MySQLs einem Jahr. Requests
  scheitern bei einer laufenden Schemaänderung nach Sekunden, statt sich
  aufzustauen, bis keine Verbindung mehr frei ist.
- Sessions und Cache nach Redis, damit eine blockierte MySQL-Tabelle nicht die
  gesamte Anwendung mitreißt.
- db:online-alter als Notausgang (pt-online-schema-change), falls eine der
  kleinen FULLTEXT-Tabellen doch einmal groß wird. Percona-Toolkit ist
  bewusst nicht Teil des Standard-Setups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-01 11:04:43 +00:00
parent 9afbedddeb
commit f8728c5bca
14 changed files with 1076 additions and 5 deletions

View file

@ -2,6 +2,30 @@
use Illuminate\Support\Str;
/*
* Wartezeit auf Tabellen-Sperren (Metadata Lock).
*
* MySQL wartet per Default 31536000 Sekunden — ein Jahr. Läuft eine
* Schemaänderung (auf press_releases/companies erzwingen die FULLTEXT-Indizes
* einen vollständigen COPY-Rebuild), blockiert jeder Schreibzugriff so lange
* und belegt dabei einen PHP-FPM-Worker samt DB-Verbindung. Sind die
* Verbindungen aufgebraucht, steht die gesamte Anwendung — auch Sessions,
* Cache und Queue, die hier ebenfalls in MySQL liegen.
*
* Requests scheitern deshalb nach wenigen Sekunden, statt sich aufzustauen.
* Begrenzt wird nur das WARTEN auf die Sperre — eine bereits laufende
* Migration wird davon nicht abgeschnitten.
*
* Bewusst ein einziger Wert aus der Umgebung statt einer Fallunterscheidung
* über PHP_SAPI: `config:cache` friert den zur Cache-Zeit (CLI!) ermittelten
* Wert ein, eine solche Weiche würde also im Web still den falschen Wert
* liefern. Braucht ein Lauf mehr Geduld — etwa eine Migration auf einer
* großen Tabelle —, wird er explizit hochgesetzt:
*
* DB_LOCK_WAIT_TIMEOUT=300 php artisan migrate --force
*/
$lockWaitTimeout = 'SET SESSION lock_wait_timeout = '.(int) env('DB_LOCK_WAIT_TIMEOUT', 10);
return [
/*
@ -59,6 +83,7 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_INIT_COMMAND => $lockWaitTimeout,
]) : [],
],
'mysql_presseecho' => [
@ -78,6 +103,7 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_INIT_COMMAND => $lockWaitTimeout,
]) : [],
],
'mysql_businessportal' => [
@ -97,6 +123,7 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_INIT_COMMAND => $lockWaitTimeout,
]) : [],
],
'mariadb' => [
@ -116,6 +143,7 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_INIT_COMMAND => $lockWaitTimeout,
]) : [],
],