Příčina
V systému došlo souběžně k jiné chybě - vyčerpání maximálního počtu povolených souborů (včetně síťových a db připojení). To jsme sice věděli (chyby jako java.net.SocketException: Too many open files nebo java.io.FileNotFoundException: /soubor (Too many open files)
byly diagnostikovány a bylo nutné zkontrolovat potenciální resource leak a limit nechat navýšit (ulimit -n, cat /proc/5063/limits). Nenapadla mne ale spojitost s NCDFE. Pokud JVM při pokusu o nahrání třídy narazí na maximální počet otevřených souborů, dostane se zřejmě do nějak nekonzistentního stavu, kdy v classloaderu se třída jeví jako nahraná, ale pokus o přístup na ni končí NCDFE. Bližší rozbor jsem nikde nevygooglil, ale nejvíc se mu blíží tato otázka na SO, první stopa vedla z tohoto dotazu.
Řešením byl restart Tomcatu, ale bylo dobré zjistit příčinu.
Falešné stopy
Zkontrolovat filesystem
V minulosti se nám stalo, že na předprodukčním serveru (virtualizovaný server s Windows Server 2003 - dnes už je upgradován) došlo k poruše, která se projevila náhodným porušením náhodného souboru. Stalo se to u jaru z instalace Javy, kde rozdíl byl pouze o 1 bit! V dnešním případě šlo o produkční prostředí a Linux a tehdejší chyba se taky projevovala myslím jako InternalError, nicméně jsem zkontroloval, že jary jdou rozzipovat a že hlášené classy jsou validní (pro jistotu jsem se na ně podíval i přes oblíbený decompiler).
Hierarchie classloaderů byla v pořádku
Klasická tomcatí: všechny aplikančí třídy z waru jsou nahrány WebappClassLoaderem.
Odkazy: 1, 2, 3 - pěkný tutoriál, i když k IBM JVM
Nesnažit se o odstranění třídy z classloaderu
Nejde to. Je to dáno jednak prioritou classloaderů, jednak tím, že příslušnost classy v classloaderu je na úrovni nativního kódu. Nepomůže ani vlamovat se do classloaderu pomocí reflexe (Bad.class.getClassLoader().resourceEntries.remove("com.pkg.Bad")) ani snažit se třídu nahrát pomocí defineClass (protože už tam je a hodí to LinkageError: loader ... : attempted duplicate class definition), ani totéž rodičovským classloaderem (Bad.class.getClassLoader().resolveClass(Bad.class.getClassLoader().getParent().defineClass("com.pkg.Bad",new byte[] {...}),0,length))), protože by se tak musely zavléct všechny třídy, na kterých nahrávaná třída závisí.
Odkazy: 1, 2, 3, 4.
Třída šla instancovat z konzole
Na projektu je Beanshell konzole, v nímž šlo vytvořit přímo instanci třídy. Nešlo ale vytvořit factory a na ní zavolat metodu pro vytvoření třídy - to házelo NCDFE. Nezkoumal jsem dál, asi to je prostředím Beanshell interpreteru.
Nemá cenu zjišťovat všechny nahrané třídy
Reflexní triky, jak zjistit všechny nahrané třídy (viz např. zde) jsou založeny na zkoumání výsledku metody Class.getProtectionDomain() a ProtectionDomain.getCodeSource() a tady nepomáhají, protože dotčené třídy (pro které se vyhazuje NCDFE) mají tyto údaje i classloader v naprostém pořádku.
Class loading
Do JAVA_OPTS při spouštění aplikačního serveru jsem přidal -verbose:class. Těch pár tisíc řádků navíc v catalina.out ničemu nevadí a třeba informace o tom, zda třída byla nahraná, příště pomůže. Později jsem ještě objevil SO otázku s dalšími experimentálními přepínači.
Žádné komentáře:
Okomentovat