Teaser-Modul - DB Overload bei vielen Kategorien

Fragen zur Installation von CONTENIDO 4.9? Probleme bei der Konfiguration? Hinweise oder Fragen zur Entwicklung des Systemes oder zur Sicherheit?
Antworten
rethus
Beiträge: 1851
Registriert: Di 28. Mär 2006, 11:55
Wohnort: Mönchengladbach
Kontaktdaten:

Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von rethus » Mi 25. Okt 2017, 11:29

In einer Contenido-Installation habe ich eine Seite mit vielen Kategorien (ca. 150 Stck.).
In den Seiten werden 5 Teaser für die Seitenspalte genutzt.

Da die Seite im Backend extrem langsam ist, habe ich den xdebug-Profiler drüber gejagt:
Auswahl_305.png
Cachegrind
(92.39 KiB) Noch nie heruntergeladen
Auf dem Screenshot erkennt man, das 5 mal generateEditCode für den Teaser aufruft (ich habe 5 Teaser in der getestetet Page), darin wird 10x buidlCategorySelect aufgerufen, welches insgesamt 14509 DB-Aufrufe abfeuert.

Das gleiche Spiel wird dann nochmal für das Tab "general und Advanced" im Teaser-Popup gemacht.

Interessant ist, das die zehn buildCategorySelect Abfragen alle identisch sind (also für die Page keine anderen Parameter verwenden):
SELECT a.idcat AS idcat, b.name AS name, c.level FROM con_cat AS a, con_cat_lang AS b, con_cat_tree AS c WHERE a.idclient = 1 AND b.idlang = 1 AND b.idcat = a.idcat AND c.idcat = a.idcat ORDER BY c.idtree
.
Würde man die Ergebnismenge also zwischenspeichern, hätte man die Anfrage-Last je Teaser schon mal um das 9 fache minimiert.

Lösungsansatz
Was haltet Ihr von folgendem Lösungsansatz:

Eine suche im Quelltext hat ergeben, das buildCategorySelect lediglich in 3 Komponenten verwendet wird:
  1. Modul: content_article_incude (input-part des Moduls)
  2. Modul: article_list_reloaded (input-part des Moduls)
  3. class.content.type.teaser.php
Überlegung ist demnach folgende...
buildCategorySelect sollte das Ergebnis der Abfrage zwischenspeichern. Z.b. in einem Objekt das nach client-id und lang-id aufgeschlüsselt ist.
Über eine Chain /Hook der beim verändern der Kategorien befeuert wird, müsste dann das Objekt reorganisiert werden. D.h. wenn eine Veränderung der Kategorien von client x in sprache y erfasst wird. müsste dies aus dem Zwischenspeicher-Objekt gelöscht werden.

(Um es alternativ einfacher zu halten, könnte man auch anstatt Änderungen über die Chain zu triggern, einen Timer setzen. So dass z.B. für aktuelle Abfragen der Build nur ein mal ausgeführt wird)

Beim nächsten Aufruf checkt buildCategorySelect ob für den gegebenen Client & Sprache etwas im Zwischenspeicher liegt. Wenn nicht, wird die DB-Abfrage ausgeführt und das Ergebnis wieder neu zwischengespeichert.
Could I help you... you can help me... buy me a coffee . (vielen ❤ Dank an: Seelauer, Peanut, fauxxami )

xstable.com: - HighSpeed Hosting, Domains, DomainReselling, Linux-Administration
suther.de: - App-Programierung, High-Performance-Webpages, MicroServices, API-Anbindungen & Erstellung

Software... ein Blick wert: GoogleCalender Eventlist, xst_dynamic_contentType

frederic.schneider_4fb
Beiträge: 967
Registriert: Do 15. Apr 2004, 17:12
Wohnort: Eschborn-Niederhöchstadt
Kontaktdaten:

Re: Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von frederic.schneider_4fb » Mi 25. Okt 2017, 13:30

Für alle zur Information:
ich habe rethus für kommende Woche eine Rückmeldung versprochen bzw. wir werden telefonieren zu dem Thema Perfornanceoptimierungen.

Nicht, dass sich jemand wundert, wenn ich hier erst einmal nicht reagiere. (Mir fehlt gerade schlicht die Zeit/Urlaub wegen Marathon.) Gerne können sich hier aber schon weitere melden, wenn sie wollen ;-)
Frederic Schneider
Entwickler bei der four for business AG

Oldperl
Beiträge: 4250
Registriert: Do 30. Jun 2005, 22:56
Wohnort: Eltmann, Unterfranken, Bayern
Kontaktdaten:

Re: Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von Oldperl » Do 26. Okt 2017, 14:55

Servus,

das Resultat in cRegistry legen. Dort eine kurze Abfrage gestalten um auf Änderungen zu prüfen, fertig. Gegebenenfalls kann man dabei für große Resultate auch mit einer zusätzlichen Ablage im Mandanten-Cache als Datei arbeiten. Zumindest hat man es dann zentral und nur einmal.

Gruß aus Franken

Ortwin
ConLite 2.1, alternatives und stabiles Update von Contenido 4.8.x unter PHP 7.x - Download und Repo auf Gitport.de
phpBO Search Advanced - das Suchwort-Plugin für CONTENIDO 4.9
Mein Entwickler-Blog

rethus
Beiträge: 1851
Registriert: Di 28. Mär 2006, 11:55
Wohnort: Mönchengladbach
Kontaktdaten:

Re: Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von rethus » Fr 27. Okt 2017, 11:01

Interessanter Ansatz Oldperl. Eine Sache, die berücksichtigt werden muss ist, dass das Caching kurz genug ist, um Anpassungen an Kategorien und Artikel zeitnah wieder zu spiegeln. Wäre das Caching z.B. 5 Minuten, würde der User sonst 5 Minuten lang den "alten Kategoriebaum" im Teaser-menü angezeigt bekommen.

Daher habe ein wenig hin und her überlegt und dann einen Ansatz gewählt, der einen zeitbasierten Cache nutzt (ohne auf Chains oder cRegistry zurückzugreifen) .
Die erste Anfrage wird ausgeführt und setzt einen Timer (z.B. 10 sek). Kommen neue Anfragen rein die < 10 Sekunden liegen, wird aus dem Cache bedient, sonst der DB-Request neu abgefragt.
So stellt man sicher, falls Änderungen an Kategorie oder Artikeln im System gemacht werden, das die gecachten Daten nicht zu alt sind.

Außerdem braucht man im Rest des Contenido-Codes keine Veränderungen machen (bzw. irgend welche Chains befeuern). Also eine Anpassung ohne Auswirkung auf umliegenden Code.

Leider funktioniert es noch nicht so 100%, da die <Select> und <option>-Felder eine fortlaufende ID erhalten.

Speichere ich das Ergebnis des Select-Fields im Cache, hat er natürlich dort immer die gleichen ID's.
Möchte ich nun doch verhindern, das eine While-Schleife mit DB-Abfragen erneut durchlaufen wird, müsste man nun mit einem preg-replace die ID's entsprechend jedem "Cache-Durchlauf" verändern und den aktuellen ID-Zahler analog zur "TimerCache"-Variable speichern.

Die wohl sauberste Lösung ist mir heute eingefallen ^^: wenn man buildCategory einfach via Ajax dann lädt, wenn man Ihn braucht... also nicht wenn die Backend-Seite geladen wird, sondern nur für den Teaser, dessen Config-Dialog gerade geöffnet wird.
Damit würde man dann sogar auf 0 Request beim Seitenload für buildCategory kommen ^^
Could I help you... you can help me... buy me a coffee . (vielen ❤ Dank an: Seelauer, Peanut, fauxxami )

xstable.com: - HighSpeed Hosting, Domains, DomainReselling, Linux-Administration
suther.de: - App-Programierung, High-Performance-Webpages, MicroServices, API-Anbindungen & Erstellung

Software... ein Blick wert: GoogleCalender Eventlist, xst_dynamic_contentType

Oldperl
Beiträge: 4250
Registriert: Do 30. Jun 2005, 22:56
Wohnort: Eltmann, Unterfranken, Bayern
Kontaktdaten:

Re: Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von Oldperl » Fr 27. Okt 2017, 11:48

Servus,
rethus hat geschrieben:
Fr 27. Okt 2017, 11:01
Leider funktioniert es noch nicht so 100%, da die <Select> und <option>-Felder eine fortlaufende ID erhalten.
Kein Wunder, falscher Ansatz. Um die übermäßigen DB-Abfragen zu verhindern muss man auch das Ergebnis der DB-Abfrage vor der Verarbeitung cachen, nicht das bereits zu option-Elementen verarbeitete Ergebnis.

Gruß aus Franken

Ortwin
ConLite 2.1, alternatives und stabiles Update von Contenido 4.8.x unter PHP 7.x - Download und Repo auf Gitport.de
phpBO Search Advanced - das Suchwort-Plugin für CONTENIDO 4.9
Mein Entwickler-Blog

rethus
Beiträge: 1851
Registriert: Di 28. Mär 2006, 11:55
Wohnort: Mönchengladbach
Kontaktdaten:

Re: Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von rethus » Fr 27. Okt 2017, 15:54

Oldperl hat geschrieben:
Fr 27. Okt 2017, 11:48
Kein Wunder, falscher Ansatz.
Ja, da wollte ich zu viel... auf bequemen Weg :D
Hab nun die Funktion entsprechend angepasst, funktioniert bei mir sauber.(4.9.3)
Da das cSession-Management im 4.9.3 noch buggy is, hab ich es über die default-PHP-Session gemacht:

Code: Alles auswählen

function buildCategorySelect($sName, $sValue, $sLevel = 0, $sClass = ''){
    global $cfg, $client, $lang;

    $categories = array();

    $selectElem = new cHTMLSelectElement($sName, "", $sName);
    $selectElem->setClass($sClass);
    $selectElem->appendOptionElement(new cHTMLOptionElement(i18n("Please choose"), ""));

    if (!empty($_SESSION['categoryTimer'])) {
        if (time() - $_SESSION['categoryTimer'] > 10) {  // delay of 10 seconds for requests
            unset($_SESSION['categoryTimer']);
            unset($_SESSION['cachedCategories']);
        } else { // return cached categories
            $categories = $_SESSION['cachedCategories'];
        }
    }
    if (empty($_SESSION['categoryTimer'])) {
        $_SESSION['categoryTimer'] = time();

        $db = cRegistry::getDb();
        $db2 = cRegistry::getDb();

        if ($sLevel > 0) {
            $addString = "AND c.level < " . (int)$sLevel;
        }

        $sql = "SELECT a.idcat AS idcat, b.name AS name, c.level FROM
           " . $cfg["tab"]["cat"] . " AS a, " . $cfg["tab"]["cat_lang"] . " AS b,
           " . $cfg["tab"]["cat_tree"] . " AS c WHERE a.idclient = " . (int)$client . "
           AND b.idlang = " . (int)$lang . " AND b.idcat = a.idcat AND c.idcat = a.idcat " . $addString . "
           ORDER BY c.idtree";

        $db->query($sql);

        while ($db->nextRecord()) {
            $categories[$db->f("idcat")]["name"] = $db->f("name");
            $categories[$db->f("idcat")]["level"] = $db->f("level");

            // Put this SQL outside the while and save it as array or object
            $sql2 = "SELECT a.title AS title, b.idcatart AS idcatart FROM
                            " . $cfg["tab"]["art_lang"] . " AS a,  " . $cfg["tab"]["cat_art"] . " AS b
                            WHERE b.idcat = '" . $db->f("idcat") . "' AND a.idart = b.idart AND
                            a.idlang = " . (int)$lang;
            $db2->query($sql2);

            while ($db2->nextRecord()) {
                $categories[$db->f("idcat")]["articles"][$db2->f("idcatart")] = $db2->f("title");
            }
        }
        $_SESSION['cachedCategories'] = $categories;
    }

    foreach ($categories as $tmpidcat => $props) {
        $spaces = "&nbsp;&nbsp;";
        for ($i = 0; $i < $props["level"]; $i++) {
            $spaces .= "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
        }

        $tmp_val = $tmpidcat;

        if ($sValue != $tmp_val) {
            $selectElem->appendOptionElement(new cHTMLOptionElement($spaces . ">" . $props["name"], $tmp_val));
        } else {
            $selectElem->appendOptionElement(new cHTMLOptionElement($spaces . ">" . $props["name"], $tmp_val, true));
        }
    }
    return $selectElem->toHTML();
}
BTW: Wo ist das syntax-highlighting für php hier im forim hin [ code=php ] funktioniert nicht mehr!! :(
Could I help you... you can help me... buy me a coffee . (vielen ❤ Dank an: Seelauer, Peanut, fauxxami )

xstable.com: - HighSpeed Hosting, Domains, DomainReselling, Linux-Administration
suther.de: - App-Programierung, High-Performance-Webpages, MicroServices, API-Anbindungen & Erstellung

Software... ein Blick wert: GoogleCalender Eventlist, xst_dynamic_contentType

Oldperl
Beiträge: 4250
Registriert: Do 30. Jun 2005, 22:56
Wohnort: Eltmann, Unterfranken, Bayern
Kontaktdaten:

Re: Teaser-Modul - DB Overload bei vielen Kategorien

Beitrag von Oldperl » Fr 27. Okt 2017, 16:28

Servus,

solange Du dabei innerhalb von 10 Sekunden weder die Sprache noch den Mandanten wechselst sollte das funktionieren. :roll:
Auch die Größe des Ergebnisses kann beim Schreiben in die Session irgendwann relevant sein.
Generell ist das von Dir hier aufgezeigte Caching in meinen Augen nichts für den produktiven Einsatz, denn es ist zu fehleranfällig.

Gruß aus Franken

Ortwin
ConLite 2.1, alternatives und stabiles Update von Contenido 4.8.x unter PHP 7.x - Download und Repo auf Gitport.de
phpBO Search Advanced - das Suchwort-Plugin für CONTENIDO 4.9
Mein Entwickler-Blog

Antworten