pátek 29. července 2005

JTDS driver – pozor na nastavení pro MSSQL 2000

JTDS driver je v podstatě skoro jediný nekomerční driver použitelný pro MSSQL 7.0 a 2000. Právě jeho původní určení pro MSSQL 7.0 pravděpodobně zanechalo pozůstatek v defaultním nastavení driveru, které může na MSSQL 2000 způsobit vážné výkonnostní problémy. Celá blamáž se točí kolem převodu textových parametrů na UNICODE. Objasnění problému a řešení se objevilo i na naší firemní WIKI a já jej zde se svolením autora republikuji.

JTDS driver a převod textových parametrů na UNICODE

Autor: Budík (special thanks to NkD & Tuhoslav)

Driver JTDS v sobě má implementovaný parametr sendStringParametersAsUnicode, který se stará o převod parametrů posílaných v SQL příkazech na server do typu UNICODE. Defaultní nastavení tohoto parametrů je true, což znamená, že převod je prováděn. I když je to jistě velmi zajímavá možnost, může být příčinou velkých problémů.

MSSQL 7.0 na rozdíl od MSSQL 2000 má nastavenu jednu znakovou sadu pro celou databázi. MSSQL 2000 umožňuje nastavit znakovou sadu na každý sloupec. Následně se může stát to, co byla příčina obrovských problémů v produkčním nasazení. Při provdádění selektu z tabulky DCH_WW, která čítá 2 500 000 záznamů, existovala v selektu tato podmínka (po dosazení parametru):

...AND OSCIS = ' xyz'

do databáze však doteče toto:

...AND OSCIS = N' xyz'

Ono N znamená převod řetezce na UNICODE. Potíž nastavá v okamžiku, kdy si uvědomíte, že sloupec je typu CHAR a parametr UNICODE. Nedojde totiž k aplikaci nastavených db indexů a provádí se procházení celé tabulky. V aplikaci bylo navíc zvoleno řešení, kdy se daný selekt musel udělat v rámci jednoho požadavku vícekrát, aby se mohly získat patřičná data.

Řešení je problému poměrně jednoduché - definovat v JTDS driveru hodnotu parametru sendStringParametersAsUnicode na hodnotu false. Pokud se tak udělá a restartuje se AS, dojde k tomu, že do databáze už dojde selekt bez převodu na UNICODE, automaticky se aplikuje index (což bylo v tomto případě nenajevýš žádoucí) a rázem jeden selekt, který předtím v nejlepším případě běžel 5 sekund, běží 80 milisekund.

Drobná poznámka - pokud si myslíte, že to N je už součástí aplikačního logu, tak se mýlíte. To N se objeví při v detailním monitorování pomocí SQL Profileru.

Nakonec citace z dokumentace k JTDS driveru:

sendStringParametersAsUnicode (default - true)

sendStringParametersAsUnicode (default - true) Determines whether string parameters are sent to the SQL Server database in Unicode or in the default character encoding of the database. This seriously affects SQL Server 2000 performance since it does not automatically cast the types (as 7.0 does), meaning that if a index column is Unicode and the string is submitted using the default character encoding (or the other way around) SQLServer will perform an index scan instead of an index seek.

úterý 26. července 2005

Memory leaks v Jave

Memory leaks jsou problémem, který vzniká tak, že program konsumuje operační paměť, kterou neumožňuje uvolnit, i když ji už v danou chvíli nepotřebuje. V Jave se o uvolnění paměti stará Garbage collector (GC) též česky řečený Sběrač Neplatných Objektů, který je plně v režii Java Virtual Machine. Programátor se nemusí starat o uvolňování paměti ručně jako například v C.

Bohužel i v javovských programech dochází k memory leaks, které končí hlášením Out of Memory. Pro objasnění příčiny memory leaks je potřeba objasnit zjednodušeně práci GC a paměťový model Javy. Základ paměťového modelu tvoří zásobník (stack) a halda (heap). Jakýkoliv objekt, který se v Jave vytvoří, leží na haldě.

 new Object(); //na haldě vznikne nový objekt

Pokud v Jave vytvoříme proměnou, vznikne záznam na zásobníku.

 Object o; //na zásobníku vznikne prázdný záznam, na který odkazuje proměnná o

Když to celé spojíme do jednoho kousku kódu Object o = new Object();, pak máme proměnou o, která odkazuje na záznam v zásobníku. Voláním konstruktoru vznikne objekt na haldě. Operátor přiřazení pak zajistí to, že do záznamu na zásobníku se dostane paměťový odkaz na objekt alokovaný na haldě.

Pokud na objekt, který leží na haldě, ukazuje záznam ze zásobníku, považuje se tento objekt za živý. Pokud se na objekt ze zásobníku neukazuje, považuje se objekt za mrtvý a GC jej může odstranit z haldy, tedy dojde k uvolnění paměti. Pro ilustraci si vezmeme náš případ a vsadíme jej do obyčejné metody.

 
  class Foo{
   void doSomething(){
      Object o = new Object();
   } 
 }
 

Při volání metody doSomething nám na zásobníku vznikne záznam, ve kterém je uložen paměťový odkaz na objekt alokovaný na haldě. Po skončení metody, nám zanikne lokální proměnná o a tím pádem i záznam na zásobníku. Na haldě teď leží objekt, na který neukazuje žádný záznam ze zásobníku.

Ve chvíli kdy JVM vyhodnotí, že je potřeba uvolnit paměť spustí GC. GC pak "proleze" celý zásobník a všechny objekty na haldě, na které je odsuď ukazováno, označí za živé. Všechny ostatní pak může GC odstranit a tím hromadně uvolnit paměť.

Zvažme následující příklad, kdy máme třídu, která je zodpovědná za vrácení rodného čísla zaměstnance.

 
  public class RodnaCisla{
   private static Map cisla = new HashMap();
    
   public String getRC(Integer id){      
      String rc = cisla.get(id);
      if(rc == null) {
       rc = ...//získej číslo např. z DB
        cisla.put(id, rc); 
      }      
      return rc; 
   } 
 }
 

Tahle třída neděla nic jiného, než že čísla zaměstnanců, která již byla získána, uloží do statické mapy. Tím pádem se s každým požadavkem na vrácení rodného čísla, která již bylo jednou nalezeno, nemusí šahat do databáze, ale vrátí se z této mapy.

Z pohledu paměťového modelu máme na zásobníku proměnou cisla, která je statická (sdílí se pro všechny instance třídy RodnaCisla) a vznikne při zavedení třídy RodnaCisla ClassLoaderem. S každým požadavkem na vrácení rodného čísla, které ještě nebylo získáno, se tohle číslo mimo jiné uloží do mapy, na kterou odkazuje cisla.

Postupem času se na haldě začnou hromadit objekty, které jsou odkazované z této statické mapy a GC je nemůže odklidit, neboť jsou pořád živé. Neustálým hromaděním nových a nových rodných čísel dochází k postupnému zabírání paměti, která nebude uvolněna a dochází k tzv. memory leaku.

Tento triviální příkládek demonstruje jak nesprávná implementace cacheovaní může způsobit memory leak, který se potom velice pracně dohledává technikou zvanou profiling. Pro detekci memory leak se používá nástroj, kterému se říká profiler. Při profilingu se JVM spustí ve speciálním módu, kdy přes standardizované rozhraní poskytuje profileru informace, na základě kterých je možné memory leak detekovat.

Tímto dlouhým oslím můstkem jsem udělal cestu k článku Staffana Larsena Memory Leaks, Be Gone.