Příběh Pěti omLuv (doma ho familierně nazýváme PPL)

Tak dlouho se hledali, až se našli. O kom že bude řeč? O jednom príma místě, kde se děti baví (a kde si děti, no to si poslechněte, i hrají!). A o jejich parťákovi, kanónovi, který umí zkracovat vzdálenosti. Zkrátka, Sparkys + PPL = VL (appp).

Objednali jsme dětem hračku. V neděli, v klidu, s vizí, že do narozenin balík bez problémů dorazí.

Chyba. Balík nedorazil a nikdy už nedorazí. Ještě že aspoň na ty narozeniny je spoleh.

Stručně z našeho příběhu:

  • Neděle 9.10.2011 večer
    Objednáváme zboží na sparkys.cz
  • Pondělí 10.10.2011 v 16:29
    Přichází email, že balík byl předán PPL a zítra bude u nás doma. Prima!
  • Úterý 11.10.2011
    Balík není doma.
  • Středa 12.10.2011
    Balík opět není doma, píšu reklamaci.
  • Čtvrtek 13.10.2011
    Odpověď a omluva ze strany PPL — měli na mě od Sparkyse neúplný telefon (PPL pravděpodobně běžně komunikuje s majiteli 8-mi ciferných telefonních čísel, proto jim to nepřišlo divné). Zásilka bude doručena dnes a řidič se mi ozve (na nové, již 9-ti ciferné číslo). Je večer a balík co? No není doma. A telefon? Taky nic…
  • Pátek 14.10.2011
    Reakce a další omluva ze strany PPL. Řidič byl pokárán a balík mi dnes přiveze. Před tím se mi ještě ozve na telefon a domluvíme si spolu detaily předání. Řidič skutečně volá a domlouvá se. Pár hodin poté volá kdosi jiný. Z PPL. Aha, aha, hm… takže NĚKDE se stala chyba, balík dnes NEBUDE doručen, tak až v pondělí…. V pondělí ale nebudu doma. Mňo to nevadí, řidič se vám ozve a už se nějak domluvíte.
    Aha…
    Co naplat, píšu další email na reklamační. První půlku příštího týdne nejsem doma, doručte mi balík dnes, nebo až ve čtvrtek 20.10.2011.
    PPL odpovídá, dokládá, že se chyba stala v místě, kde to dětí tak baví, protože Sparkys namísto mé adresy a telefonu poslal svou. PPL zřejmě běžně posílá balíky od zákazníka A k zákazníkovi A (ikdyž před pár dny se ještě jmenoval jinak) a zásilku iniciativně nasměrovává zpět do Sparkyse. Naštěstí v důsledku našich reklamačních Liebesbriefe PPL zatíná poslední zbytky sil a ponechává balík v depu s poznámkou, že má být odeslán na MOU adresu ve čtvrtek 20.10.
    Uf.
  • Čtvrtek 20.10.2011
    Tak co, taky si myslíte, že balík bude leda tak v *iti a ne u nás doma? Gratuluji, vyhráváte deset bodů z deseti.
    V srdceryvné omluvě (páté) se dozvídám, že tentokráte chybou vazače došlo k politováníhodné okolnosti (k jaké bezpochyby dochází maximálně jednou za 10 let) a balík byl odeslán zpět. Definitivně. Prostě jsme si jen tak zaemailovali, parkrát zavolali a stálo nás to všechny směšných 11 dní…
    Ale není to tak zlé, jak to vypadá. Za celé to martýrium jsem dostal skvělou radu — můžu si zboží objednat znovu. No neberte to!

Platit až při předávce zboží má stále smysl. Když si jen představím, jak by asi probíhala domluva ohledně vrácení peněz, úplně mě z toho zebe.

PPLka je jeden velký bordelpodnik. Chudák řidič, vyžral si karné řízení, protože jsem se ozval, ale pohovor na koberečku by evidentně potřebovala celá ostravská úderka…
Proč je doručení balíku podmíněno zavoláním na telefon? Proč sem nikdo nepřijede a nenechá ve schránce info co a jak dál? Proč nikoho nenapadne zkontrolovat adresu odesílatele a příjemce? Proč se se zákazníkem domlouvají na termínu doručení a mezitím balík pošlou zpět?

Zkrátka a dobře, moji milí, až si budete příště něco z netu objednávat, vzpomeňte si na útrpný příběh nebohých děcek z periferie ČR, a zatraceně si rozmyslete, jestli v objednávce zaškrtnete “Způsob doručení: PPL”.

Django, testy, fixtůry a posraté rekurze

Psát testy je pruda. O to větší, když po dvouhodinové přípravě ku*va-fix tur spustíte test a výstup v konzoli je plný hlášek typu:

Exception RuntimeError: 'maximum recursion depth exceeded in *' in  ignored

(za * si dosaďte cokoli vás napadne)

Kura co to je? Google neporadí (teda ne přímo), ve výstupu absolutně nedopátratelné z kama vítr vane. Tak až se vám podobná situace namane, vězte, že chyba není ve vás. To je útěcha co?

Django (či Python?) nemá rádo velké, navzájem do sebe propletené fixtury. V mém případě měl JSON něco okolo 2000 řádek s celkem 70 objekty z nejrůznějších modelů. V naprosté zoufalosti jsem tuto jednu “obřímí” fixturu rozbil na menší a hle, po recursion mrdce ni vidu, ni slechu.

No ale přeci nepodlehnu takové blbosti. Nechci mít jednu fixturu rozbitou na deset menších!

Google tentokrát pomohl. Přidejte si do testu totok a pošlete mi pusu na dobrou noc:

import sys
sys.setrecursionlimit(2000)

PS: určo záleží na systému, pod kterým pracujete. Na mém OSX sněžném kočičákovi je defaultně recursionlimit nastaven na hodnotu 1500.

Tonight we’ve released Django 1.3 beta 1, as well as Django 1.2.4 and Django 1.1.3 to correct a pair of security issues.

All affected users are urged to upgrade immediately.

Žangovinky #32

Komentovaný překlad Django Dose Community Catchup, Episode #32.

Během prosince se na svém blogu Seek Nuance pěkně rozepsal John DeRosa. V článku “A performance lesson on Django QuerySets" dokazuje, že bezmyšlenkovité nasazení QuerySet funkce iterator() přináší spoustu problémů. Menší paměťové nároky byly v jeho případě draze vykoupeny četností dotazů na databázi (co iterace to dotaz, navíc umocněné dvaceti simultánně spuštěnými Celery tasky).
V druhé polovině článku se zmiňuje o špatném pochopení vlastnosti QuerySetu, tj. že jsou lazy (k vyhodnocení dojde až je to nezbytně nutné) a kešované. Na praktické ukázce demonstruje, že každý vytvořený QuerySet generuje databázový dotaz, což v kontextu jeho kódu znamenalo neúměrné zatížení PostgreSQL.

David Cramer je masakrální drsoň. Pokaždé když něco publikuje, tak je to perla. A pokaždé mi trvá sakra dlouho, než pochopím o co jde.
Nejinak tomu je i v článku “Tracking changes to fields in Django”. Pokud jsem se někde šeredně nesek, tak nám dává k dispozici dekorátor, s jehož pomocí můžeme sledovat historické změny atributů u konkrétního modelu. Fachá to tak, že pokud např. obj.name == 'Petr' a přenastavíme jej na obj.name = 'Pavel', pak metoda obj.has_changed('name') vrátí True (atribut name se změnil), metoda obj.old_value('name') vrátí ‘Petr’ a metoda obj.whats_changed() nás podaruje slovníkem změněných atributů. Tato ultimátní “změnokeška” se resetuje při vytvoření instance a během uložení objektu.

Daniel Greenfeld rozvířil debatu o Template Languages. Ve svém postu hájí směr, který tvrdí že šablonovací jazyk musí být stupidní. Čím méně toho umí, tím lépe.
Reakce na sebe nenechaly dlouho čekat. Autoři alternativních šablonovací jazyků (Armin Ronacher — Jinja2, Mike Bayer — Mako) vysvětlují, proč toho jejich jazyky umí víc + jeden pohled zvenčí.

Špetka sci-fi z velkého světa (pro mě rozhodně). Pokud stejně jako chlapci z The Washington Times spravujete vícero serverů a začínáte mít binec v Python balíčcích (na serveru A verzi 1, na serveru B verzi 1.2 a na serveru C nic), může se vám šiknout jejich fabfile.py.

Poslední zpráva hodná zmínky: nedávno implementovaná unittest metoda assertNumQueries dráždila Luke Planta natolik, že pro ni napsal speciální pomocnou třídu FuzzyInt. S její pomocí je možné testovat počet SQL dotazů v zadaném intervalu. Jak Luke trefně komentuje, vlivem kešování se lehce stane, že počet SQL dotazů v aplikaci kolísá, a testovat je na exaktní počet přináší spíš problémy než užitek.

A teď něco úplně jiného. Novinky z chobotu:

  • Byly rozšířeny funkce mail_admins() a mail_managers(). Odteďka je možné k nim připínat HTML přílohy, čehož se hned využilo pro odesílání přítulnějších error emailů. 3× hurá!
  • Odskočme si do views, přesněji na jejich konce. Pokud nejste moc indie, budete tam mít buďto nějakou HttpResponse, render_to_response nebo generické view. Odteďka ale budeme potkávat nového kamaráda TemplateResponse. Ten dostal do vínku kopec lenosti, a obsah generuje až to je skutečně nutné. Díky této povaze bude např. možné změnit na poslední chvíli jméno šablony, nebo do ní poslat upravený či rozšířený kontext. Už se těším na prčovní middlewary, které z této vlastnosti vytěží maximum.

Poznámka: Kopa práce, těžká témata a blížící se vánoce. Výsledek? Žangovinky #31 nejsou a nebudou. Komu chybí, nechť opráší AČ slovník, a postne sem link.

Pěkné vánoce!

The first alpha preview package for Django 1.3 is now available.

Konzultačka u RevSys

5. listopadu 2010 se na IRC otevřely virtuální dvéřka do společnosti Revolution Systems, ve které působí i Jacob Kaplan-Moss, core vývojář Djanga. Během dvou hodin padlo téměř čtyřicet zajímavých dotazů, a ještě zajímavějších odpovědí.

Povinná četba!

Co jsem nasál:

  • MySQL je divné a zatlouká si další a další hřebíčky do rakve. Modří už stejně dávno přešli na PostgreSQL.
  • MediaTemplate je něco mezi WebFaction (sdílený hosting) a vlastním VPS
  • Tak to vypadá, že za rok, za dva, budeme po internetu rajtovat se stádem jednorožců. Prý toho moc nepožerou (cca o 10-20% méně RAM v porovnání s mod_wsgi) a povozí víc lidí.
  • Na doméně https://www.djangy.com/ se peče něco zajímavého
  • Odolný a prověřený podvozek pro Django aplikace? nginx, Apache, mod_wsgi, PostgreSQL
  • Dobrodruhové ale krosí s nginx, gunicorn, celery, rabbitmq, redis, PostgreSQL
  • Django závidí Railsům migrace
  • Začátečníci se na Jacobových kurzech ztrácí v balíčcích a cestách, pokročilí považují Django za nedotknutelnou černou skříňku. V prvním případě pomůže pip/virtualenv, v druhém četba zdrojáku Djanga.
  • Když už je třeba štupovat, tak nejdřív s opicema a pak když tak s mercurial queues. Nebo forknout projekt k sobě a pozašívat to v něm.

Co zaujalo vás?

Žangovinky #30

Komentovaný překlad Django Dose Community Catchup, Episode #30.

18.října měla být představena alpha 1 verze Djanga 1.3. Bohužel se ale stále objevují drobné chyby a datum se odsouvá. Na termínu oficiálního představení nové verze se naštěstí nic nemění, 17.ledna 2011 nás objede Ježoch a doveze překrásný dáreček. Co bude uvnitř?

Jednou z nejvýraznějších přednášek na letošním portlandském DjangoConu byla Why Django Sucks. Eric Florenzano trefně poukazuje na bolesti Djanga — klesající výkon s každou novou verzí, monolitický settings.py, obtížné přizpůsobování aplikací třetích stran. Zatímco některá z kritizovaných témat se podařilo poměrně rychle vyřešit (např. rozšíření týmu core vývojářů), u jiných to bude běh na dlouhou trať. Andy McKay ve svém článku Django Apps rock kontruje na Ericovu poznámku ohledně znovupoužitelnosti: nezapomínejme, že většinu našich webů pohání také “non-reusable” aplikace. A v této rovině Django exceluje, rychlostí i efektivitou.

U Andyho blogu ještě zůstaneme. Uvažovali jste o tom, že svůj Django projekt nasadíte na Googlím App Enginu? Ceny jsou přijatelné (pro většinu z nás 0,- USD), Django je dlouhodobě podporováno, škáluje to! Možná si napřed přečtete článek When App Engine went wrong a pouvažujte, jestli svůj projekt nerozjet přeci jen jinde.

Jak sledujete chyby na produkci? Necháváte si posílat traceback reporty přes email nebo používáte sofistikovanější řešení jako třeba Arecibo nebo Sentry? Pokud patříte do druhé skupiny, nechte si Davidem Cramerem poradit jak správně používat knihovnu logging, aby se hlášky v Sentry zobrazovaly jak mají.

A teď něco úplně jiného. Novinky v chobotu:

Poslední téma není můj šálek kávy, ale určitě za zmínku stojí: Postgres 9 Streaming Replication and Django-Balancer.

Žangovinky #29

Komentovaný překlad Django Dose Community Catchup, Episode #29.

Začněme blogy. Peter Bengtsson ve svém článku Local Django development with Nginx radí, jak na lokální mašině nakonfigurovat Nginx a využít jej k servírování statického obsahu (CSS, JS, obrázky). Ukazuje jednoduchý postup, kterým dosáhl zrychlení z původních 446.34 req/s na 15709.54 req/s, což se pozitivně projevilo během ladění Javascriptových kódu v prohlížeči.

A teď něco úplně jiného. Troubení z chobotu:

  • Zásadní zpráva. Seznam autorů byl doplněn o Honzu Krále, jediného našince který se na vývoji Djanga aktivně podílí. Gratulace! (a velké díky)
  • Od revize 14139 Django podporuje knihovnu unittest2, která mimo jiné přináší nové assert metody pro pohodlnější testování seznamů, slovníků nebo datetime objektů.
  • Třída TestCase byla doplněna o metodu assertNumQueries (revize 14183), s pomocí které je možné ověřit počet databázových dotazů generovaných zadaným kódem.
  • U testování ještě zůstaneme. Díky nové třídě RequestFactory (revize 14191) je možné vytvořit fake request objekt a ten v testu podsunout konkrétnímu view (čímž elegantně obejdeme pravidla v urls.py)

Poslední novinka se týká “class based views”, oficiálního začlenění views v podobě tříd (konečně!). Russell Keith-Magee sepsal letový plán, který by nám měl do Djanga 1.3 dopravit tuto vysněnou vlastnost. Držme palce.

Ladění výkonu Django aplikací

V září 2010 proběhl v Portlandu DjangoCon. Seznam probíraných témat je docela velký. Pokud to čas dovolí, pokusím se některé zajímavé přednášky zpracovat a pak se s nimi podělit zde na blogu.

První přednáška, která mě padla do oka byla “First steps in performace tuning" od Russella Keith-Mageeho řešící výkon Django aplikací.

Nejdříve si uvědomme základní fakta:

  • vývoj probíhá na lokále, který je zatěžován sporadicky
  • systém má k dispozici velké množství paměti
  • k aplikaci přistupuje jediný uživatel

Situace na serveru ale býva přesně opačná.

Nejčastější prohřešky

Před publikováním aplikace na server je vhodné proklepnout několik bodů:

  • Každé view by mělo svůj úkol vyřešit co nejrychleji. Odezva do 0.1s je dobrá, do 1s obstojná, nad 2s špatná. Pokud se view na nečem dlouho zdržuje, je lepší úkol poslat do fronty (message queue) a zpracovat jej offline. Posloužit může i AJAX, cron či “persistent services” (jestli jsem to dobře pochopil, pak měl Russell na mysli démona, který jede někde bokem, udržuje v paměti persistentí data a ta na žádost velice rychle vrací).
  • Minimalizuj počet SQL dotazů. S pomocí Django Debug Toolbaru odhal neefektivní místa aplikace. Sniž počet SQL dotazů na minimum. Pro relační dotazy se vyplatí použít select_related. Pozor! Hromada malých “levných” dotazů je stejně špatná, jako jeden “drahý” dotaz (SQL příkaz EXPLAIN napoví).
  • Na velikosti dat záleží. Pokud view zpracovává pouze zlomek atributů, které sebou model nese, nasaď only, defer, values nebo values_list. S jejich pomocí Django z DB tabulek vyzobne jen to co je třeba a objem přenášených dat mezi databází a aplikací výrazně klesne.
  • Používej databázové indexy. U atributů, podle kterých aplikace třídí nebo vyhledává užší množinu dat (get, filter, exclude, order_by) je vhodné zvážit nasazení databázových indexů, viz parametr db_index.
  • Denormalizuj. Když je to třeba. Dokud to jde, drž se čistého návrhu. Až to začne drhnou (výkonostně), neostýchej se denormalizace. Pamatuj, existují i jiné možnosti (databázová view, “umělé” tabulky s předpočítanými hodnotami, apod).
  • Šablonový systém je pomalý. No a co (Russellova reakce). Pokud tvůj projekt obsahuje hodně aplikací, nebo máš v šablonách hodně {% include %} či {% extend %} tagů, nebo hodně TEMPLETE_DIRS adresářů, nasaď django.template.loaders.cached.Loader. Nebo použij jiný šablonový systém, třeba Jinja2 (říkám já).

Kešování

Invalidace

Russell se ptá: máš stránku, která se mění sporadicky? Pak ji zakešuj. Pak pokračuje: máš stránku která se mění často? A bude vadit, když po určitou dobu uvidí návštěvníci starý obsah?

Asi jak kde. S kešováním totiž přichází nepříjemný efekt, a to invalidace hodnot. Pokud například změním titulek u článku, většinou nestačí zničit keš s detailem článku, ale také přehledy článku v kategoriích, možná také úvodní stránku a bůh ví co ještě. Na invalidaci žádný recept nemá (naťukává sice signály, ale nijak zvlášť je nerozebírá). Sází spíše na jednoduchost, a k tomu mu bohatě stačí adekvátně nastavená doba expirace nakešovaných hodnot a smíření se s faktem, že návštěvníci jeho webu po určitou dobu neuvidí čerstvý obsah.

Cold start, thundering

Během této partie mě ale zaujalo jiné téma: “cold start” a “dogpiling/thundering herd”. Hrozné názvy.

Za optimální situace bude prvním návštěvníkem webu obsah stránky zakešován a každý další uživatel už uvidí obsah z keše. Pokud ale skokově, během malého okamžiku, dojde na stránku více uživatelů, bude se pro každého z nich stránka renderovat znovu a znovu, bez využití keše (protože v ní ještě nic uloženo není). Podobný efekt může nastat i při vyexpirování hodnoty z keše.

Russell radí, aby se před startem serveru spustil skript, který jednorázově vyrenderuje nejnavštěvovanější stránky (ale třeba i querysety, či jiná často používaná data) a uloží je do kešky. Doporučuje prostudovat aplikaci django-newcache od Erica Florenzana, která “thundering” efekt elegenatně řeší.

ETagy

Posledním kašotématem byly ETagy. Zjednodušeně: jde o mechanismus, který vyrenderovanou stránku “otaguje” na úrovni protokolu HTTP (libovolným řetězcem). Když uživatel navštíví stránku podruhé, pošle prohlížeč spolu s GETem i původně získaný ETag. Pokud aplikace zjistí, že se požadovaná stránka nijak nezměnila (vygeneruje stejný tag jako ten co přichází od prohlížeče), pak odpoví status kódem “304 Not Modified” a server je ušetřen generováním a přenosem nezměněné HTML stránky.

Jak ale efektivně zjistit, jestli se stránka změnila nebo ne? Pokud například máme view, které generuje seznam článků, pak musíme nejdřív provést dotaz na DB, vytáhnout naposledy přidaný či změněný článek a porovnat jej s ETagem. To ale není ono, lepší by bylo úplně DB obejít, situaci rychle vyhodnotit a kód ve view vůbec neprovádět.

Chytrý trik na svém blogu nabízí Will McGugan. View odekorujeme, a informaci s posledním ETagem necháme v cache (Will má ve svých modelech atribut version, kterou s každým uložením inkrementuje). Porovnání je pak mimořádně rychlé, nicméně opět se dostáváme k invalidaci — musíme zajistit mazání keše (ideálně pověšené opět na save).

Load testing

Jakmile aplikace jede na serveru, je třeba měřit. Všecko možné (response time z logů, využití paměti, zbývající místo na disku, zatížení I/O). Prostě vše.

Russell upozorňuje na memory leaks Pythoních C extensions (což jsou údajně všechna rozhraní populárních DB), několikrát naráží na neobvyklosti MySQL (podivné zamykání tabulek, chování planneru, apod).

K prověřování zátěže doporučuje curl, ab a siege.

Dotazy z publika

Na konci přednášky zaznělo z pléna i několik dotazů:

  • Kterou optimalizací začít? Těžko říct, záleží na aplikaci. Nejdříve je nutné najít bolavé místo a pak ho řešit.
  • Jak je to s thredy uvnitř view? Vytvořit thread uvnitř view je blbost a spíš to ukazuje na špatný návrh.
  • Nějaký tip na ladící nástroje? Django Debug Toolbar + plugin pro profilování.

Video

No a konečně i záznam Russellovy přednášky:

Přidáváme objekt alá User

Všimli jste si někdy, že přidání nového uživatele v administračním rozhraní Djanga probíhá trochu jinak, než přidání jakéhokoliv jiného objektu?

Pro připomenutí — nový uživatel se zadává prostřednictvím dvou po sobě navazujících formulářů, kdy v první fázi vyplníte pouze username a heslo, a teprve při druhém kroku je možné doplnit zbytek informací.

Potřeboval jsem vytvořit podobné workflow a po nakouknutí do zdrojáku Djanga mě opět překvapilo, jak jednoduše je možné podobného efektu dosáhnout. Proměte si prsty, jdeme na to.

Do administrační třídy si přidejte 3 nové atributy:

class MenuAdmin(admin.ModelAdmin):
    add_form = MenuAdminAddForm
    add_fieldsets = (
        (None, {
            'fields': ('system_name', 'name')}
        ),
    )
    add_form_template = 'admin/menu/menu/add_form.html'

Atribut add_form definuje formulářovou třídu, která bude použita pouze při vytváření nového objektu (do atributu form můžete klíďo-píďo uvést jiný formík, který bude použitý při editaci existujícího záznamu). Atribut add_fieldsets definuje seznam položek z add_form, které se na admin stránce zobrazí. A konečně add_form_template říká, že pro vykreslení stránky bude použita uvedená (mírně upravená) šablona.

Modří už tuší, že tohle stačit nebude. Django o žádných add_* atributech nemá ani páru. Musíme mu proto lehulince pomoct. Do admin třídy doplňte metodu:

    def get_form(self, request, obj=None, **kwargs):
        defaults = {}
        if obj is None:
            defaults.update({
                'form': self.add_form,
                'fields': admin.util.flatten_fieldsets(self.add_fieldsets),
            })
        defaults.update(kwargs)
        return super(MenuAdmin, self).get_form(request, obj, **defaults)

Metoda get_form dokáže rozpoznat jestli se přidává nový objekt, a v tomto připadku upraví argumenty pro rodičovský get_form tak, aby namísto obvyklého form a fieldsets použila add_form a add_fieldsets. Pokud by došlo k editaci existujícího objektu, Django se bude chovat standardně, tj. použije atribut form a fieldsets (více viz oficiální dokumentace).

A k čemu ten add_form_template? Ten pouze přidá do formuláře informativní hlášku o tom, že po vytvoření objektu bude uživateli nabídnut bohatší formulář s doplňujícími informacemi:

{% extends "admin/change_form.html" %}
{% load i18n %}
{% block form_top %}
    <p>{% trans "First, enter a username and password. 
    Then, you'll be able to edit more user options." %}</p>
    <input type="hidden" name="_continue" value="1" />
{% endblock %}

Vtipný kvíz na konec: víte jakou roli má hidden input _continue?
(není tam náhodou)