30. 1. 2014 v IT

Case study: Škálování django aplikace (psychologie.cz)

Před pár měsíci to bylo poprvé, co jsem musel řešit výkonnostní problémy některého z našich projektů. Vlastně podruhé, ale poprvé bylo problémem napojení na služby 3. stran, což není případ, o kterým chci nyní psát. Rád bych se podělil o zkušenosti.

Úvod do problému

Psychologie.cz, které se tento článek týká, mezi našimi projekty vyčnívá řádově vyšší návštěvností. Už dlouho jsme věděli, že projekt běží na hraně svých možností. Nicméně vždy, když se objevil nějaký výpadek, ukázalo se, že na vině bylo zejména přetížení VPS z vnějšku a tak jsme se tím dále nezabývali. Nyní se však problémy začaly stále častěji vracet a tak jsme začali jednat. Pod problémy míním pomalé načítání stránek webu, které se v průměru pohybovalo okolo 4 - 5 sekund a na vrcholu kolapsu trvalo i minutu.

Psychologie.cz je spolu s dalšími projekty hostována na VPS:

  • 4-jádrový Xeon X3210@2.13GHz
  • 8 GB RAM
  • Debian
  • Apache
  • PostgreSQL

Ještě je vhodné říct, že nejsme původními autory systému, na kterém psychologie běží, byť jej už 3 roky spravujeme a rozšiřujeme.

Podezření na memory leak

Nejdříve to vypadalo, že přetížení může souviset s nasazením rozsáhlejší úpravy webu. Monitorovali jsme vytížení serveru prostřednictvím htop. Celkově server nepůsobil jako přetížený, zaujalo nás ale množství a rostoucí tendence paměti, kterou si procesy odbavující psychologii alokovaly. Podezření tedy padlo na memory leak. Stáhl jsem proto poslední úpravu a čekal co se bude dít. Nezdálo se, že by se něco změnilo. Nicméně při pátrání po memory leakách v djangu jsem narazil na velice zajímavý článek, který se věnuje právě bobtnání django procesů.

Bylo evidentní, že bude potřeba zabývat se problémem podrobněji. S kolegou jsme se neshodli, jestli problém bude spíš na straně serveru nebo aplikace, a tak jsem s tím, čemu rozumím lépe - aplikaci.

Škálování na straně aplikace

Optimalizace SQL dotazů

Stáhl jsem si produkční data na localhost a prostřednictvím django-debug-toolbar začal zkoumat dobu trvání SQL dotazů. Ukázalo se, že dotaz na počty komentářů na přehledu článků dávají databázi pěkně zabrat. Existoval jeden složitý dotaz napsaný přímo v SQL na získání všech komentářů pro celý queryset článků. Zkusil jsem jej rozdělit na jednodušší dotazy přímo v syntaxi Django Queries pro každý článek zvlášť. Rozdíl tam byl, ale minoritní. Komentáře na psychologii jsou postavené na django comment framework. Zkoumáním jeho kódu jsem objevil potenciální slabé místo - chybějící databázový index u cizího klíče (který je navíc z nějakého důvodu typu CharField).

Prostřednictvím South migrace jsem tento index přidal, aniž by o tom django vědělo - zrychlení bylo znatelné.

class Migration(SchemaMigration):

   def forwards(self, orm):
       db.execute("CREATE INDEX object_pk_idx ON django_comments(object_pk)")

   def backwards(self, orm):
       db.execute("DROP INDEX object_pk_idx")

   models = { ....

Cache

Další na to navázanou prací byla revize cachování. Hledal jsem místa, která by si zasloužila být cachována a dosud nebyla. Pár jsem jich našel.

Snad jedinou zajímavostí v této oblasti je, že jsem v rámci ladění zkoušel různé cache backendy, které django nabízí a nenarazil jsem na významné změny ve výkonnosti. Momentálně je nasazený MemCached.

Škálování na straně serveru

Po ladění na straně aplikace byly jasné 2 věci. Zaprvé - výsledek není good enough. Zadruhé - je divné, že na localhostu mi to běhá rychleji než na produkčním serveru.

Ve správě linuxového serveru nejsem žádný expert, požádal jsem tedy o pomoc rozhled.cz, který nám hostuje servery. Ti nám nasadili live tracking serveru pomocí RRDTool (metriky dotazů za minutu, doba odezvy, využití RAM).

Řešením, které se nám společně podařilo najít, se ukázalo být prosté navýšení WSGI procesů odbavujících psychologii. Ze čtyř na dvacet. Předtím jsme už zkoušeli počet procesů upravovat, ale ne o tolik. Mysleli jsme si, že tolik procesů by server položilo. Opak je pravdou - zvládá to zcela bez problému.

Závěrem

Problém spočíval spíše v nezvládnuté vysoké návštěvnosti webu, než v nárustu počtu zpracovávaných dat (byť i to hrálo roli). Trocha měření.

  • před optimalizací a v době, kdy server začal kolabovat trvala doba načítání homepage odhadem 4000+ ms
  • po optimalizaci na straně aplikace 2500 ms
  • po optimalizaci na straně serveru 500 ms

Nejedná se o zcela koncové časy, není v tom započítáno načítání static souborů.

V zásadě se potvrdilo to, co říkal, tuším Steve Cortuna na webexpu, "Your code is fast enough, scale infrastructure". Až budu příště řešit výkonnostní problémy nějaké (django) aplikace, budu postupovat v tomto pořadí:

  1. Začnu na serveru - zkusím navýšit počet WSGI procesů a nastavím max_requests
  2. zkontroluji dotazování na API 3. stran (např. Facebooku) - neptám se zbytečně často, cacheuji výsledek?
  3. Zapnu / zlepším cachování obecně
  4. Analyzuji SQL dotazy
  5. Analyzuji samotný kód

Mimochodem, od té doby, co projekt není přetížený, přestaly chodit zprávy o errorech, které se mi nedařilo nasimulovat. Evidentně byly tyto chyby důsledkem přetížení.