sobota 6. října 2012

Zámky a budíček ve dvě hodiny ráno

Pokaždé, když vám zazvoní telefon ve dvě hodiny ráno, je to průšvih. V podstatě vám může volat jenom zhrzená milenka a nebo se vám něco vysypalo na produkci. Průšvih je o to větší, pokud má přímý dopad na zákazníky a obzvlášte na ty v zámoří, kteří trpí přímo v hlavním vysílacím čase. Nedávno se mi to stalo a přestože problém je specifický, lze se z něj dobře poučit a vzít si pár obecných ponaučení. Uvědomil jsem si to při čtení článku Synchronized Drowning, kde došlo k něčemu podobnému.

Napsal jsem kód, který sloužil k načítání veřejného klíče pro kontrolu podpisu. Protože se klíč rotoval po několika hodinách, navíc z distribuovaného souborového systému GlusterFS, přišlo mi vhodné, aby se nečetl pro každý jednotlivý požadavek. Kód vypadal přibližně následovně.

        private Key key;
        
        Key getKey(){
            Lock readLock = readWriteLock.readLock();
            try {
                readLock.lock();
                return key;
            } finally {
                readLock.unlock();
            }
        }

        void loadKey() {
            Lock writeLock = readWriteLock.writeLock();
            try {
                writeLock.lock();
                //load
                BufferedReader br = new BufferedReader(new FileReader(new File(keyFile))));
                ....
            } finally {
                writeLock.unlock();
            }
        }

Díky použití read/write locku se vlákna při čtení klíče neblokovala. Pokud byl detekován nový klíč, zavolala se metoda loadKey. V ní se získal write zámek a všechna vlákna vyjma jednoho byla blokována dokud se klíč nenačetl.

Všechno fungovalo až do okamžiku, kdy vlákno, které četlo obsah souboru, zamrzlo na věčné časy někde hluboko uvnitř operačního systému při čekání na disk a jediné co pomohlo byl umount disku a restart JVM. Držení write zámku v uvozovkách nekonečně dlouho mělo za následek, že všechna další vlákna čekala na jeho uvolnění. Obecně se z mých chyb můžeme poučit následujícím způsobem.

  • Vždycky když voláte externí systém, obzvláště přes síťové rozhraní, počítejte s tím, že se ten systém může neočekávaně zpomalit. Pravidlo číslo jedna, mějte správně nastavené timeouty na všech úrovních - OS, JVM aplikace.
  • Vždy pokud využíváte služeb Lock, a jeho implementace to umožňuje, preferujte metodu tryLock, která se snaží získat zámek do daného timeoutu.
  • Počítejte s tím, že jediné s čím můžete počítat je, že technologie které používáte selžou. Vždycky je dobré si položit otázku: "co se stane když". Kdybych si jí v tomhle případě položil a uvědomil si všechny důsledky, rozhodně bych použil timeouty a rozhodně bych zámek nepoužil v tak širokém rozsahu. Úplně by stačilo, kdyby byl použit jenom pro přístup k instanční proměnné klíče.
  • Neblokujte vlákna určená pro obsluhu HTTP komunikace tj. nezavlékejte je to blokujících IO operací. Všechny IO operace řešte přes API s podporou neblokujícího přístupu.