Neu Redis Cache Invalidation

xadoX

Sehr aktives Mitglied
11. September 2012
595
46
Hallo zusammen,

meine Anfrage richtet sich vor allen an größere Shops, die viele Artikel (über 100T) und viele User gleichzeitig im Shop haben (50-100).

Wir haben einen JTL Shop 4.06 gehostet auf einem ALL-Inkl. XXXL Server mit folgender Ausstattung:

2x Intel® Xeon® E5-2630v4
2x10 Prozessorkerne 2,2GHz Hyper-Threading
64 GB Reg ECC RAM
4 TB SATA (RAID 10)
+ 2x 250 GB SSD (RAID 1)

Die Datenbank liegt auf der SSD und wir nutzen Redis als Cache System.

Jetzt zum Problem:

Startet man ein Plugin Update oder ändert verschiedenste Einstellungen im Backend, so invalidiert sich der komplette Cache.
Wir gehen davon aus, dass zum Zeitpunkt des Invalidierens um die 50 Leute auf der Seite aktiv sind.

1. Problem Kategoriebaum

Der Kategoriebaum muss einmal neu in den Cache geschrieben werden. Grundsätzlich eine Abfrage, die nur 5 Sekunden dauert. Dadurch, dass aber 50 Leute gleichzeitig auf der Seite sind, wird dieser SQL jedes Mal neu aufgerufen. Kommen weiter User hinzu, so werden weitere SQL's aufgerufen, um die Abfrage einmal fertig zu schreiben.
Bei uns ist es dann nicht unüblich, dass dieser eine kurze SQL über 10 Minuten läuft und davon dann bis zu 150 Stück. Den Shop 10 Minuten down zu haben wäre ja noch ok, aber kommen wir zum nächsten Problem was noch viel gravierender ist.

2. Problem Artikel

Da auch alle Artikelobjekte neu geschrieben werden müssen kommen neben den obigen Abfragen noch massenhaft Abfragen dieser Art auf dem SQL-Server an:

SQL:
SELECT
    tartikel.kArtikel AS tartikel_kArtikel,
    tartikel.fLagerbestand AS tartikel_fLagerbestand,
    tartikel.cLagerBeachten,
    tartikel.cLagerKleinerNull,
    tartikel.cLagerVariation,
    teigenschaftkombiwert.kEigenschaft,
    tartikel.fVPEWert,
    teigenschaftkombiwert.kEigenschaftKombi,
    teigenschaft.kArtikel,
    teigenschaftkombiwert.kEigenschaftWert,
    teigenschaft.cName,
    teigenschaft.cWaehlbar,
    teigenschaft.cTyp,
    teigenschaft.nSort,
    teigenschaftwert.cName AS cName_teigenschaftwert,
    teigenschaftwert.fAufpreisNetto,
    teigenschaftwert.fGewichtDiff,
    teigenschaftwert.cArtNr,
    teigenschaftwert.nSort AS teigenschaftwert_nSort,
    teigenschaftwert.fLagerbestand,
    teigenschaftwert.fPackeinheit,
    teigenschaftwertpict.cType,
    teigenschaftwertpict.kEigenschaftWertPict,
    teigenschaftwertpict.cPfad,
    teigenschaftwertaufpreis.fAufpreisNetto AS fAufpreisNetto_teigenschaftwertaufpreis,
    COALESCE(ek.score, 0) nMatched
FROM
    tartikel
JOIN teigenschaftkombiwert ON tartikel.kEigenschaftKombi = teigenschaftkombiwert.kEigenschaftKombi
LEFT JOIN teigenschaft ON teigenschaft.kEigenschaft = teigenschaftkombiwert.kEigenschaft
LEFT JOIN teigenschaftwert ON teigenschaftwert.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert
LEFT JOIN(
    SELECT teigenschaftkombiwert.kEigenschaftKombi,
        COUNT(
            teigenschaftkombiwert.kEigenschaftWert
        ) AS score
    FROM
        teigenschaftkombiwert
    INNER JOIN tartikel ON tartikel.kEigenschaftKombi = teigenschaftkombiwert.kEigenschaftKombi
    LEFT JOIN tartikelsichtbarkeit ON tartikelsichtbarkeit.kArtikel = tartikel.kArtikel AND tartikelsichtbarkeit.kKundengruppe = 1
    WHERE
        kEigenschaftWert IN(
        SELECT
            kEigenschaftWert
        FROM
            teigenschaftkombiwert
        WHERE
            kEigenschaftKombi = 341625
    ) AND tartikelsichtbarkeit.kArtikel IS NULL
GROUP BY
    teigenschaftkombiwert.kEigenschaftKombi
) ek
ON
    ek.kEigenschaftKombi = teigenschaftkombiwert.kEigenschaftKombi
LEFT JOIN teigenschaftsichtbarkeit ON teigenschaftsichtbarkeit.kEigenschaft = teigenschaftkombiwert.kEigenschaft AND teigenschaftsichtbarkeit.kKundengruppe = 1
LEFT JOIN teigenschaftwertsichtbarkeit ON teigenschaftwertsichtbarkeit.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert AND teigenschaftwertsichtbarkeit.kKundengruppe = 1
LEFT JOIN teigenschaftwertpict ON teigenschaftwertpict.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert
LEFT JOIN teigenschaftwertaufpreis ON teigenschaftwertaufpreis.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert AND teigenschaftwertaufpreis.kKundengruppe = 1
WHERE
    tartikel.kVaterArtikel = 196772 AND teigenschaftsichtbarkeit.kEigenschaft IS NULL AND teigenschaftwertsichtbarkeit.kEigenschaftWert IS NULL AND(
        teigenschaftkombiwert.kEigenschaftWert,
        COALESCE(ek.score, 0)
    ) IN(
        (110241, 1),
        (110242, 1),
        (110243, 1),
        (110244, 1),
        (110245, 1),
        (110246, 1),
        (110247, 1),
        (110248, 1),
        (110249, 1),
        (110250, 1),
        (110251, 1),
        (110252, 1),
        (110253, 2),
        (110254, 1),
        (110255, 1),
        (110256, 1),
        (110257, 1),
        (110258, 1),
        (110259, 2),
        (110260, 1),
        (110261, 1),
        (110262, 1)
    )
GROUP BY
    teigenschaftkombiwert.kEigenschaftWert
ORDER BY
    teigenschaft.nSort,
    teigenschaft.cName,
    teigenschaftwert.nSort

Diese Abfragen dauern pro Artikel jeweils 10-20 Sekunden. Nach wenigen Sekunden stauen sich am SQL-Server über 100 Slow-Queries an, die exponentiell ansteigen bis man Ladezeiten im Shop von bis zu 60 Sekunden hat.

Erst wenn der Cache auf ca. 4-5GB angestiegen ist - was locker 4-5 Stunden dauern kann - erholt sich der SQL-Server wieder.

Was muss sich ändern?

1. Im Backend muss sich vor Änderung jeder Einstellung ein Hinweisfeld öffnen, das daraufhin weist, dass der Cache sich invalidieren würde.
2. Der SQL zum Aufbau des Kategoriebaumobjekts darf nur ein einziges mal abgesetzt werden.

Ich weiß allerdings nicht, was man gegen das Problem mit dem Artikelcache tun kann - ich merke langsam nur, dass man als größerer Shop mit dem aktuellen JTL Shop System an seine Grenzen stößt.

Wir sind schon seit mehr als einem Jahr in Diskussion mit JTL, aber so richtig was ändern, tut sich nicht.

Vielleicht gibt es ja noch andere mit diesem Problem, die auch schon eine Lösung parat haben.
 
Zuletzt bearbeitet:

css-umsetzung

Offizieller Servicepartner
SPBanner
6. Juli 2011
6.639
1.583
Berlin
Wir haben einen JTL Shop 4.06 gehostet auf einem ALL-Inkl. XXXL Server mit folgender Ausstattung:

2x Intel® Xeon® E5-2630v4
2x10 Prozessorkerne 2,2GHz Hyper-Threading
64 GB Reg ECC RAM
4 TB SATA (RAID 10)
+ 2x 250 GB SSD (RAID 1)
Ich kann mir gar nicht vorstellen das Ihr mit der Konfiguration Engpässe habt.

Startet man ein Plugin Update oder ändert verschiedenste Einstellungen im Backend, so invalidiert sich der komplette Cache.
Der Komplette cache wird eigentlich nur invalidiert wenn du das manuell aktivierst oder bestimmte Aktionen durchgeführt werden.
hierzu gehört:
  1. Updaten des Shops
  2. Deinstallation eines Plugins
  3. Ändern der Zahlungsarten
  4. ändern der globalen Metadaten
  5. Shop zurücksetzen
Die Übliche Methode ist eigentlich das nur der Bereich invalidiert wird der erforderlich ist.

Ich habe die Abfrage mal formatiert, damit andere besser sehen was da so passiert, dass ist schon eine heftige Abfrage:
Code:
SELECT
    tartikel.kArtikel AS tartikel_kArtikel,
    tartikel.fLagerbestand AS tartikel_fLagerbestand,
    tartikel.cLagerBeachten,
    tartikel.cLagerKleinerNull,
    tartikel.cLagerVariation,
    teigenschaftkombiwert.kEigenschaft,
    tartikel.fVPEWert,
    teigenschaftkombiwert.kEigenschaftKombi,
    teigenschaft.kArtikel,
    teigenschaftkombiwert.kEigenschaftWert,
    teigenschaft.cName,
    teigenschaft.cWaehlbar,
    teigenschaft.cTyp,
    teigenschaft.nSort,
    teigenschaftwert.cName AS cName_teigenschaftwert,
    teigenschaftwert.fAufpreisNetto,
    teigenschaftwert.fGewichtDiff,
    teigenschaftwert.cArtNr,
    teigenschaftwert.nSort AS teigenschaftwert_nSort,
    teigenschaftwert.fLagerbestand,
    teigenschaftwert.fPackeinheit,
    teigenschaftwertpict.cType,
    teigenschaftwertpict.kEigenschaftWertPict,
    teigenschaftwertpict.cPfad,
    teigenschaftwertaufpreis.fAufpreisNetto AS fAufpreisNetto_teigenschaftwertaufpreis,
    COALESCE(ek.score, 0) nMatched
FROM
    tartikel
JOIN teigenschaftkombiwert ON tartikel.kEigenschaftKombi = teigenschaftkombiwert.kEigenschaftKombi
LEFT JOIN teigenschaft ON teigenschaft.kEigenschaft = teigenschaftkombiwert.kEigenschaft
LEFT JOIN teigenschaftwert ON teigenschaftwert.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert
LEFT JOIN(
    SELECT teigenschaftkombiwert.kEigenschaftKombi,
        COUNT(
            teigenschaftkombiwert.kEigenschaftWert
        ) AS score
    FROM
        teigenschaftkombiwert
    INNER JOIN tartikel ON tartikel.kEigenschaftKombi = teigenschaftkombiwert.kEigenschaftKombi
    LEFT JOIN tartikelsichtbarkeit ON tartikelsichtbarkeit.kArtikel = tartikel.kArtikel AND tartikelsichtbarkeit.kKundengruppe = 1
    WHERE
        kEigenschaftWert IN(
        SELECT
            kEigenschaftWert
        FROM
            teigenschaftkombiwert
        WHERE
            kEigenschaftKombi = 341625
    ) AND tartikelsichtbarkeit.kArtikel IS NULL
GROUP BY
    teigenschaftkombiwert.kEigenschaftKombi
) ek
ON
    ek.kEigenschaftKombi = teigenschaftkombiwert.kEigenschaftKombi
LEFT JOIN teigenschaftsichtbarkeit ON teigenschaftsichtbarkeit.kEigenschaft = teigenschaftkombiwert.kEigenschaft AND teigenschaftsichtbarkeit.kKundengruppe = 1
LEFT JOIN teigenschaftwertsichtbarkeit ON teigenschaftwertsichtbarkeit.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert AND teigenschaftwertsichtbarkeit.kKundengruppe = 1
LEFT JOIN teigenschaftwertpict ON teigenschaftwertpict.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert
LEFT JOIN teigenschaftwertaufpreis ON teigenschaftwertaufpreis.kEigenschaftWert = teigenschaftkombiwert.kEigenschaftWert AND teigenschaftwertaufpreis.kKundengruppe = 1
WHERE
    tartikel.kVaterArtikel = 196772 AND teigenschaftsichtbarkeit.kEigenschaft IS NULL AND teigenschaftwertsichtbarkeit.kEigenschaftWert IS NULL AND(
        teigenschaftkombiwert.kEigenschaftWert,
        COALESCE(ek.score, 0)
    ) IN(
        (110241, 1),
        (110242, 1),
        (110243, 1),
        (110244, 1),
        (110245, 1),
        (110246, 1),
        (110247, 1),
        (110248, 1),
        (110249, 1),
        (110250, 1),
        (110251, 1),
        (110252, 1),
        (110253, 2),
        (110254, 1),
        (110255, 1),
        (110256, 1),
        (110257, 1),
        (110258, 1),
        (110259, 2),
        (110260, 1),
        (110261, 1),
        (110262, 1)
    )
GROUP BY
    teigenschaftkombiwert.kEigenschaftWert
ORDER BY
    teigenschaft.nSort,
    teigenschaft.cName,
    teigenschaftwert.nSort

Der SQL zum Aufbau des Kategoriebaumobjekts darf nur ein einziges mal abgesetzt werden.
Ich glaube nicht das der Kategoriebaum wirklich ein Problem ist, es sei denn du hast da 1000nde verschachtelte Kategorien, der erste der diesen aufruft, legt den auch im Objectcache ab.
Das größere Problem sind wirklich die Artikel, insbesondere wenn man viele Variationskombinationen hat.

Wenn du so viele gleichzeitige User hast, dann gehe ich mal davon aus das du auch eine entsprechende Menge an Verkäufen hast. Daher würde ich um Performance zu sparen die Funktionen "Kunden kauften auch bzw. ähnliche Artikel" deaktivieren, da diese das System extrem ausbremsen können wenn die Tabelle stark angewachsen ist.
 

xadoX

Sehr aktives Mitglied
11. September 2012
595
46
Ich kann mir gar nicht vorstellen das Ihr mit der Konfiguration Engpässe habt.

Das höre ich des Öfteren.
Der Komplette cache wird eigentlich nur invalidiert wenn du das manuell aktivierst oder bestimmte Aktionen durchgeführt werden.
hierzu gehört:
  1. Updaten des Shops
  2. Deinstallation eines Plugins
  3. Ändern der Zahlungsarten
  4. ändern der globalen Metadaten
  5. Shop zurücksetzen
Die Übliche Methode ist eigentlich das nur der Bereich invalidiert wird der erforderlich ist.

Es sind schon einige mehr. JTL hat mir auch mal eine grobe Liste zusammengestellt.

einstellungen_cache_invalidieren.jpg


Deine Liste möchte ich noch ergänzen um:

6. Updaten eines Plugins
7. Installation eines Plugins
8. Ändern der Versandarten

Ich finde mir als Shopbetreuer sollte durch JTL klar gemacht werden, dass wenn ich gewisse Änderungen im Backend vornehme ein gewisser Teil des Caches geleert wird.

Ich habe die Abfrage mal formatiert, damit andere besser sehen was da so passiert, dass ist schon eine heftige Abfrage:

Danke!! :)

Ich glaube nicht das der Kategoriebaum wirklich ein Problem ist, es sei denn du hast da 1000nde verschachtelte Kategorien, der erste der diesen aufruft, legt den auch im Objectcache ab.
Das größere Problem sind wirklich die Artikel, insbesondere wenn man viele Variationskombinationen hat.

Leider ist er das aber und zwar wenn man viele gleichzeitige User auf der Seite hat. Jeder Aufruf auf der Seite führt dazu, dass der SQL nochmal ausgeführt wird - und zwar so lange, bis der erste SQL einmal durchgelaufen ist.
Aber ich gebe dir recht, das größere Problem sind definitiv die Artikel. Wenn ich das mit dem Kategoriecache mitbekomme, dann sperre ich alle Zugriffe für die Seite in Cloudflare und warte bis das Objekt einmal angelegt wurde.
Nur hat das Kategoriebaumobjekt auch eine gewisse Lifetime und man kann nie genau vorhersagen, wann dieses abläuft. Wie gesagt, dass mit dem Kategoriebaum sorgt dafür, dass der Shop "nur" 10-20 Minuten einer höheren Last ausgesetzt ist. Aber die SQL's für die Artikel legen den Shop für 4-5 Stunden lahm.

Wenn du so viele gleichzeitige User hast, dann gehe ich mal davon aus das du auch eine entsprechende Menge an Verkäufen hast. Daher würde ich um Performance zu sparen die Funktionen "Kunden kauften auch bzw. ähnliche Artikel" deaktivieren, da diese das System extrem ausbremsen können wenn die Tabelle stark angewachsen ist.

Diese Funktion nutzen wir schon länger nicht mehr. Wir haben uns da für Nosto entschieden, so dass die X-Selling Last nicht zusätzlich den SQL-Server belastet.
 

css-umsetzung

Offizieller Servicepartner
SPBanner
6. Juli 2011
6.639
1.583
Berlin
Zu deiner Cache Liste, ja es richtig aber der gesamte Cache also flushAll() kommt nur in meinen Aufgezählten Punkten vor, alle anderen Punkte löschen nur Teile des Caches.

Diese Funktion nutzen wir schon länger nicht mehr. Wir haben uns da für Nosto entschieden, so dass die X-Selling Last nicht zusätzlich den SQL-Server belastet.
Nosto macht das System leider noch langsamer, das musste ich leider bei einem anderem Server feststellen, am besten hier mal lesen was ich getestet habe
https://forum.jtl-software.de/threads/plugin-update-jst-nosto.100917/page-2#post-675901

Kleiner Hinweis am Rande, auch wenn es dir bei deinem SQL Problem jetzt nicht hilft:
Ich habe auch einen Kunden der bei All Inkl. ist, hier haben wir bei einem Managed Server darauf bestanden das ALLES auf SSD Platten läuft (ich finde ab einem bestimmten Preis kann man das schon erwarten), außerdem mussten wir feststellen das wir zwar einen großen Server haben aber die Anzahl der Apachen zu gering war, das konnte man gut feststellen da JavaScript Ressourcen dann teilweise zu lange Ladezeiten (>2 sec) hatten, es war gar nicht so einfach denen das begreiflich zu machen.
 

Xantiva

Sehr aktives Mitglied
28. August 2016
1.787
313
Düsseldorf
Hm, naiver Weise würde ich sagen, dass das nur eimalige Absetzen der Query(s) für den Kategoriecache doch nicht so schwierig in der Umsetzung sein sollte. (Datenbankfeld als "Flag", die anderen Aufrufe werden im einfachen Fall schlafen gelegt.)
 

css-umsetzung

Offizieller Servicepartner
SPBanner
6. Juli 2011
6.639
1.583
Berlin
Die Kategorien werden eigentlich nur zu einem Problem wenn man aktiviert hat das Leere Kategorien ausgeblendet werden und dazu noch eine sehr hohe Anzahl an Kategorien hat.
Sonst würde ich die Kategorien als unproblematisch betrachten.

Wenn allerdings ein Listing aufgerufen wird und dann die Datenbank extrem stresst, dann hängt natürlich alles.
Mitte der Woche wurden in dem Shop wo ich gerade sehr auf die Performance achte Kategorien geändert, dadurch wird natürlich auch der cache der Artikel gekillt. Ich hatte der MySQL Server hatte dann teilweise eine CPU Last von über 500%.
 

xadoX

Sehr aktives Mitglied
11. September 2012
595
46
Mitte der Woche wurden in dem Shop wo ich gerade sehr auf die Performance achte Kategorien geändert, dadurch wird natürlich auch der cache der Artikel gekillt. Ich hatte der MySQL Server hatte dann teilweise eine CPU Last von über 500%.

Also wenn wir Kategorien ändern, dann wird bei uns im Shop auch nur der Kategoriecache gekillt. Der lange SQL für die Artikel taucht dann nicht auf.
Es sollte auf dem SQL Server dann so ausgesehen haben:knode.JPG
Davon laufen bei uns immer so 100-150 parallel, wenn sich der Kategoriebaum invalidiert. Wir haben 513 Kategorien im Shop aktiv.

Nosto macht das System leider noch langsamer, das musste ich leider bei einem anderem Server feststellen, am besten hier mal lesen was ich getestet habe
https://forum.jtl-software.de/threads/plugin-update-jst-nosto.100917/page-2#post-675901

Das kann ich so nicht bestätigen. Wir haben teilweise auch 100-150 Varkombiartikel mit jeweils ca. 6 Merkmalen und aktivierter Option " Kindartikel-Daten übergeben" - denke aber das ist ein anderes Thema.
 
Zuletzt bearbeitet:

css-umsetzung

Offizieller Servicepartner
SPBanner
6. Juli 2011
6.639
1.583
Berlin
Ja es scheint bei dir so zu sein das du auf leere Kategorien achtest, das würde ich dann deaktivieren, hinzu kommt das er durch den Lagerfilter den du aktiviert hast auch schaut ob Artikel vorhanden sind die lagernd sind, dadurch wird der Produkt Counter aufgerufen, bzw. der join auf die Artikel Tabelle mit einbezogen.

Ich kann dir jetzt den SQL Query nicht im einzelnen aufschlüsseln, dazu müsste ich mehr Zeit investieren daher ist das jetzt nur eine Vermutung wenn ich mir das in der helper Klasse der Kategorien anschaue.
Ich weiß auch auf die Schnelle nicht ob es dem Query reicht einen Artikel zu finden oder ob je Kategorie alle anzeigbaren Artikel gezählt werden, wäre dem So dann wäre das natürlich übel.
Ich würde aber den Stockfilter (nur lieferbare Artikel) mal deaktivieren um das zu testen ob das dein Problem in den Kategorien verursacht.
 

xadoX

Sehr aktives Mitglied
11. September 2012
595
46
Ja es scheint bei dir so zu sein das du auf leere Kategorien achtest, das würde ich dann deaktivieren, hinzu kommt das er durch den Lagerfilter den du aktiviert hast auch schaut ob Artikel vorhanden sind die lagernd sind, dadurch wird der Produkt Counter aufgerufen, bzw. der join auf die Artikel Tabelle mit einbezogen.

Das soll auch so sein. Ich finde nichts schlimmer als ein Shop, der Produkte anzeigt, die nicht verfügbar sind. Das er auf leere Kategorien achten soll kann ich tatsächlich ausstellen, weil wir keine leeren Kategorien haben.

Dennoch bleibt das größere Problem mit den Artikeln.
 

xadoX

Sehr aktives Mitglied
11. September 2012
595
46
Ja, zusammen mit dem JTL Support wurde die class.helper.Kategorie.php angepasst.

Vom Prinzip her läuft es mit der Anpassung jetzt aber genau so wie du es dir gewünscht hast:

WENN der Cache für Kategorien nicht gefüllt ist, dann löst der 1. Call (Besucher oder Export) den SQL aus. Jeder weitere Call, der vor Fertigstellung ausgelöst wird, wird wie bisher länger warten müssen, aber keinen weiteren SQL in der Datenbank absetzen. Die Wartezeit beträgt dabei 1 Sekunde länger, als die Befüllung des Caches braucht; jedoch maximal 15 Sekunden. Sollte also aus irgendeinem Grund die Abfrage statt wie üblich 5-6 Sekunden benötigen, sondern tatsächlich über 15 Sekunden, würden die wartenden Call's in einen Fehler laufen (leerer Shop mit Statuscode 500).

Im Zweifel aber lieber nochmal den Support kontaktieren. Julian Giesen kennt das Problem, bezieh dich einfach auf das Ticket #2020012310004449
 

Anhänge

  • class.helper.Kategorie.txt
    30,6 KB · Aufrufe: 3

Ähnliche Themen