26 ianuarie 2012

Povești cu și despre "library cache"

Dacă tot mi-am propus să povestim, ar trebui să încep cu "a fost odată ca niciodată, că de n-ar fi nu s-ar povesti"... Ei bine, nu! Vreau să abordez un subiect cât se poate de serios care, deși mistic în felul său, nu prea se pretează la a fi povestit într-o manieră prea romanțioasă. Este vorba despre "library cache", o structură de memorie folosită intens de instanța Oracle și de a cărei bună funcționare depind multe...

Unde-i dom'le "library cache"-ul ăsta?

Ați folosit vreodată "Google Earth"? E o aplicație pe care o putem utiliza pentru a explora virtual tot felul de locații geografice, ca și când le-am observa din satelit. Foarte frumos, dar ce legătură are "Google Earth" cu ce discutăm noi aici? Niciuna! Vreau doar să fac o analogie. Îmi place efectul acela de "zoom in". Inițial apare planeta Pământ, rotindu-se frumos în jurul axei proprii. Dăm o căutare să zicem după "Iași, România", colimatorul este poziționat pe locația indicată, iar prin efectul de "zoom-in" de care vă spuneam trecem prin Europa, România și, în sfârșit, orașul Iași cu străzi, clădiri și tot "tacâmul".

Haideți să ne imaginăm o aplicație similară, doar că în loc de locații geografice avem de-a face cu structuri interne ale serverului Oracle. N-am nici cea mai vagă idee cum ar trebui să se numească această aplicație, dar cert e că "Google Oracle" sună ca naiba. Hmmm... Gata, știu! În același spirit, îi putem spunem "Oracle Oracle"! Sau nu, ca să-i dăm o aură de mister îi zicem "2Oracle", pentru că 2 citit ca "to" ar putea sugera ideea de destinație. În fine, chiar nu are nici o importanță numele.

Inițial, de departe, ar trebui să ne apară o struțo-cămilă cu fișiere de date, structuri de memorie și procese, toate interconectate într-un soi de reprezentare abstractă, pentru unii lipsită de gust, pentru alții fermecătoare.

Ne interesează mai multe despre "library cache", așa că vom da frumușel o căutare după acest concept. Efectul "zoom-in" intră în acțiune: trecem prin SGA (System Global Area), apoi ajungem la o sub-zonă de memorie denumită "shared pool" și, mai departe, prin aceasta, la ceea ce ne interesează pe noi, zona "library cache". La acest nivel de detaliu putem să analizăm mai bine cum este alcătuită această zonă și cum funcționează ea.
Figura 1 - Zona "library cache" în SGA
Ce găsim aici? Cod, instrucțiuni! Atât SQL, cât și PL/SQL. Chiar și clase Java! Interesant, nu? Băieții de la Oracle au extins arhitectura "cache"-urilor nu doar la nivelul datelor (vezi "Database buffer cache-ul"), ci și la nivelul codului. Ideea e simplă: a compila presupune un efort considerabil din partea serverului, prin urmare ar fi păcat să aruncăm la coș produsul finit. Prin urmare, îl păstrăm elegant în memorie și îl refolosim ori de câte ori avem nevoie.

Dimensionarea "library cache"-ului

Ne-am obișnuit cu o flexibilitate foarte mare în ceea ce privește configurarea server-ului Oracle. Putem să-i zicem câtă memorie să folosească pentru "buffer cache", care să fie numărul maxim de procese și câte și mai câte. Prin urmare, n-ar fi chiar o aberație să ne gândim că ar putea exista un parametru prin intermediul căruia să putem specifica dimensiunea "library cache"-ului. Căutăm repede în documentație și ne lămurim. Un astfel de parametru nu există.

Să ne uităm cu atenție la "Figura 1"! Observăm că "library cache-ul" este parte din "shared pool", alături de alte câteva sub-zone de memorie: cea alocată dicționarului, apoi "result cache"-ul de pe server, o zonă rezervată și structuri interne fixe. Nu putem influența dimenșiunea "library cache"-ului direct, dar o putem face indirect, prin modificarea unor parametri conecși.

Să analizăm logic (sau băbește). Unde este localizat "library cache"-ul? În "shared pool". Putem specifica dimensiunea "shared pool"-ului? Desigur! Asta înseamnă că avem o primă pârghie pe care o putem folosi pentru a influența dimensiunea "library cache"-ului. Un "shared pool" mare ne asigură, teoretic, premisele unui "library cache" mai generos.
Figura 2 - Configurarea "shared pool"-ului
Bun, știm deci cât am alocat pentru "shared pool". Cu alte cuvinte avem o plăcintă mare și frumoasă pe care trebuie acum să o feliem. Prima felie ar fi, să zicem, zona rezervată. Putem să-i specificăm dimensiunea? Da, prin intermediul parametrului SHARED_POOL_RESERVED_SIZE. Mergem mai departe la cea de-a doua felie, zona "result cache". O putem dimensiona? Oarecum, prin parametrul RESULT_CACHE_MAX_SIZE. Acesta specifică o limită maximă, ceea ce nu înseamnă că rezervăm deja toată această zonă de memorie. Oricum, înțelept ar fi să facem calculele ca și când acest maxim ar fi alocat. Continuăm cu zona dedicată dicționarului. Avem vreun parametru care să ne permită dimensionarea acestei zone? Nu! Asta e, mergem mai departe la zona cu structuri interne fixe. Îi putem configura dimensiunea? Nu! Asta înseamnă că din zona alocată "shared pool"-ului putem scădea liniștiți ce am alocat pentru "reserved pool" și "result cache", iar ce rămâne va fi împărțit frățește (dar nu în mod egal) între "library cache", "dictionary cache" și zona cu structuri interne fixe. Abordarea e puțin simplistă, știu, mai ales dacă în ecuație introducem și mecanismul ASMM (Automatic Shared Memory Management), dar, în principiu, ideea este aceeași. Spațiul rămas disponibil trebuie să fie suficient astfel încât nici una din aceste sub-zone de memorie să nu aibă de suferit.

Modelul de alocare a memoriei

Deși ne-am propus să discutăm despre "library cache", alocarea memoriei în cadrul acestei structuri nu poate fi separată de mecanismul general utilizat pentru întregul "shared pool".

Câteva explicații lămuritoare sunt necesare. În primul rând, Oracle definește conceptul de "granulă". Aceasta reprezintă unitatea de memorie utilizată în mecansimele de alocare/de-alocare a memoriei SGA, deci, implicit, și a "shared pool"-ului. Dimensiunea granulei poate să fie diferită de la un sistem la altul, însă o putem afla oricând, dat fiind că ea este expusă în câteva view-uri sistem.
SQL> select distinct granule_size from v$sga_dynamic_components;

GRANULE_SIZE
------------
    16777216

SQL> SELECT * FROM v$sgainfo WHERE name = 'Granule Size';

NAME                                  BYTES RES
-------------------------------- ---------- ---
Granule Size                       16777216 No 
Alocarea de memorie prin granule nu trebuie confundată cu alocarea în cadrul acestor granule. Conceptul de "granulă" este interesant doar în contextul alocării inițiale de memorie pentru anumite "buffer"-e, eventual în operațiile ulterioare de redimensionare a acestora.
Figura 3 - Alocarea memoriei prin granule
Spre exemplu, pe baza mea de date "shared pool"-ul are puțin peste 1GB și este compus din 66 de granule:
SQL> select current_size/1024/1024 size_M, current_size/granule_size granules
  2    from v$sga_dynamic_components
  3   where component='shared pool';

    SIZE_M   GRANULES
---------- ----------
      1056         66 
Dar, odată alocată această memorie, felul în care intern ea este mai departe gestionată e altă mâncare de pește.

Pentru a ne lămuri mai bine vom apela la puțină "magie neagră". Vom trage o ochiadă într-o tabelă X$, dar, mare atenție, este periculos a o interoga pe sisteme în producție! Tabela se numește X$KSMSP, iar denumirea ciudată vine de la "Kernel Service Memory/Management Shared Pool". Este periculos a o interoga pe sisteme puternic tranzacționate deoarece, ori de câte ori facem asta, Oracle va activa o serie de "latch"-uri, care mai departe vor bloca celelalte sesiuni care au treabă cu "shared pool"-ul. Această tabelă conține bucățelele de memorie alocate în cadrul "shared pool"-ului. E vorba de "cealaltă mâncare de pește" de care vă vorbeam: avem 66 de granule alocate, dar la nivelul mecanismului de alocare intern specific "shared pool"-ului discutăm de o împărțire total diferită. Haideți să vedem câte astfel de segmente de memorie avem alocate:
SQL> select count(*) from x$ksmsp;

  COUNT(*)
----------
    322119 
Avem peste trei sute de mii de astfel de segmente, mare parte din ele cu dimensiuni diferite, așa cum ne arată, de altfel, și interogarea de mai jos:
SQL> select count(distinct KSMCHSIZ) from x$ksmsp;

COUNT(DISTINCTKSMCHSIZ)
-----------------------
                   1247 
Deci, "shared pool"-ul pe care îl analizăm (cel de pe baza mea de date) operează cu peste o mie de dimensiuni diferite de alocare. Ne putem face o idee și asupra tipurilor de zone de memorie alocate în "shared pool":
SQL> select ksmchcls, sum(ksmchsiz) from x$ksmsp group by ksmchcls;

KSMCHCLS SUM(KSMCHSIZ)
-------- -------------
recr         283809640
freeabl      521013344
R-freea           6240
perm         175289832
R-free        54566920
free          55827344

6 rows selected.  
Avem segmente marcate ca "free" ce pot fi folosite în alocări ulterioare, segmente de tip "perm", adică permanente, ce corespund structurilor interne fixe (vezi figura 1) și segmente "recr", adică recreabile, ce conțin informații ce pot fi recreate, așa cum este de pildă cazul datelor din dicționar (pot fi recitite de pe disc) sau a cursoarelor (SQL-urile pot fi reparsate). În ciuda denumirii lor, segmentele "freeabl" reprezintă de fapt memorie alocată de anumite module ale "kernel"-ului Oracle și care pot fi eliberate doar de către aceste module. Segmentele care sunt prefixate cu "R" sunt asociate zonei rezervate a "shared pool"-ului.
Figura 4 - Alocarea în cadrul "shared pool"

După cum se poate observa și din "Figura 4", la acest nivel al alocării în cadrul "shared pool"-ului conceptul de granulă își pierde din relevanță. De asemenea, reținem că, în ceea ce privește "library cache"-ul, nu discutăm de o schema de alocare specifică lui, ci de una generală, utilizată la nivelul "shared pool"-ului. Două elemente noi se pot observa în figura de mai sus: lista cu zonele de memorie libere și lista de tip LRU-LeastRecentlyUsed (în realitate sunt mai multe astfel de liste). Acestea sunt folosite în management-ul intern al alocărilor în "shared pool". Atunci când noi alocări de memorie trebuie efectuate, Oracle va inspecta lista cu segmente libere. Dacă aceasta indică lipsa de memorie disponibilă, atunci se efectuează de-alocări conform mecanismului LRU, adică vor fi înlăturate segmentele de memorie care figurează ca fiind cel mai puțin utilizate.

"Library cache"-ul și mai de-aproape

E timpul să analizăm mai în detaliu conținutul "library cache"-ului. Punem la treabă ipotetica noastră aplicație "2Oracle" și mărim imaginea, tocmai pentru a distinge mai bine. Facem ochii cât cepele și ce vedem acolo? Niște structuri ciudate, arborescente, care ar reprezenta cică așa-numitele cursoare. Figura de mai jos prezintă schematic o astfel de structură.
Figura 5 - Un model de cursor SQL, după parsare
Imaginați-vă o mulțime de astfel de "pufuleți", plutind de nebuni prin "shared pool". Cum au ajuns acolo? Evident, ca urmare a executării de diferite comenzi SQL, care au fost parsate și optimizate, iar rezultatul final a fost stocat în cadrul acestor structuri. Haideți să le analizăm mai bine! Avem un cursor părinte, ce alocă mai departe un segment de memorie pentru stocarea textului comenzii SQL. Acest lucru înseamnă că există câte un cursor părinte pentru fiecare comandă SQL care, literal, este diferită. Cursoarele părinte sunt externalizate în V$SQLAREA. După cum bine știm, o comandă SQL poate fi executată diferit, în funcție de felul în care am configurat optimizorul (vezi "optimizer_goal"), dar și de variabilele "bind" folosite la momentul execuției. Pentru fiecare modalitate diferită folosită la un moment dat pentru a executa o comandă SQL, există câte un cursor copil, cu contextul și planul de execuție corespunzătoare. Cursoarele copil sunt externalizate în V$SQL.

Principalul motiv pentru care Oracle a pus la punct acest mecanism este acela de a refolosi aceste cursoare, în cazul executării ulterioare a acelorași comenzi SQL. Singura modalitate prin care Oracle se poate prinde dacă are sau nu de-a face cu aceeși comandă SQL constă în preluarea textului comenzii și compararea acestuia cu cel stocat în library cache. Problema e că operația de identificare a unui cursor în "library cache" pentru o comandă dată se poate dovedi extrem de costisitoare, mai ales dacă avem de-a face cu un "library cache" generos, care permite stocarea a numeroase cursoare. Spre exemplu, pe baza mea de date sunt peste 6000:
SQL> select count(1) from v$sqlarea;

  COUNT(1)
----------
      6010 
Este clar că o operație de căutare în cadrul unei astfel de liste, înainte de a executa orice comandă SQL, nu poate fi fezabilă, mai ales dacă introducem în ecuație și mediul concurențial. Prin urmare, băieții de la Oracle vin cu un mecanism ingenios, prin care textului comenzii SQL i se aplică o funcție "hash", iar rezultatul este folosit mai departe pentru a indexa cursorul părinte corespunzător. Deoarece pentru comenzi SQL distincte funcția "hash" poate returna același rezultat, rezultă că putem avea SQL-uri diferite reunite sub umbrela aceleași chei de indexare. În termeni de programare discutăm practic de un "array"/tablou asociativ (sau hash-table), în care cheile sunt date de "hash", iar valoarile sunt de fapt niște "array"-uri care conțin cursoarele părinte. Aceste "array"-uri le veți găsi menționate în documentația Oracle ca "library cache hash buckets/chains".
Figura 6 - Modelul de grupare a cursoarelor
La nivelul de bază, fiecare grup (bucket) conține pointeri către segmente de memorie din lista LRU, dar doar către cele care conțin structuri specifice cursoarelor părinte. După cum se poate observa și din figură, nu toate segmentele din lista LRU se regăsesc în aceste "bucket"-uri, dat fiind că unele dintre acestea nu au nici o treabă cu noțiunea de cursor.

Succesul mecanismului de mai sus se bazează pe bine-cunoscutul conceptul "divide et impera". Practic, atunci când Oracle are de executat o comandă SQL, în loc să o caute într-o listă lungă cât o zi de post, pur și simplu va genera o cheie "hash" pentru textul comenzii, "hash" cu care va accesa "bucket"-ul corespunzător. Va căuta apoi dacă există un cursor părinte pentru acea instrucțiune SQL, dar doar în cadrul respectivului "bucket". Simplu și eficient!

Puțină statistică

Pentru a evalua eficiența "library cache"-ului, Oracle ne pune la dispoziție un view special, denumit V$LIBRARYCACHE. În cadrul acestuia operăm cu următoarele noțiuni:
  • GETS
  • GETHITS
  • PINS
  • PINHITS
  • RELOADS
  • INVALIDATIONS
Acestea sunt cumulate pe diferite tipuri de obiecte conținute de "library cache". Pentru cursoare SQL putem obține statisticile de mai sus, folosind următoarea interogare:
SQL> select gets, gethits, pins, pinhits, reloads, invalidations
  2  from v$librarycache
  3  where namespace = 'SQL AREA';

      GETS    GETHITS       PINS    PINHITS    RELOADS INVALIDATIONS
---------- ---------- ---------- ---------- ---------- -------------
   2505073    2477645  225079378  224926389      26136         22624 
Haideți să vedem care este semnificația cifrelor de mai sus. Ori de câte ori dorim să executăm o comandă SQL nouă, server-ul Oracle trebuie să verifice dacă nu cumva a executat deja această comandă și are reprezentarea ei gata parsată în "library cache". Cu alte cuvinte, procesul server care dorește a executa comanda în cauză, întreabă "library cache"-ul: hei, nu cumva ai deja comanda cu hash-ul cutare? Asta se traduce într-un nou "GET" și va fi consemnat ca atare în coloana "GETS" a view-ului V$LIBRARYCACHE. Dacă este găsită comanda de executat în "library cache", atunci discutăm de un "GETHIT", care va incrementa valoarea coloanei "GETHITS". Discutăm în acest caz de un posibil "soft parse". Spun posibil pentru că, așa cum vom vedea imediat, este prematur a ne pronunța încă. Dacă nu găsim un astfel de cursor în "library cache", atunci discutăm de un "hard parse", în acest caz Oracle fiind nevoit să treacă prin tot procesul de parsare/optimizare.

Bun, care-i șmecheria cu "PIN"? Un "pin" intervine ori de câte ori comanda trebuie efectiv executată. Asta înseamnă că, odată identificat cursorul parinte, sub acesta trebuie găsit cursorul copil corespunzător, care implică mai departe găsirea și inspectarea zonei de memorie ce conține informații cu privire la mediul folosit de optimizor (pentru a vedea dacă se potrivesc), informații cu privire la variabilele de tip "bind", dar și planul de execuție corespunzător. Cererea de a executa instrucțiunea se traduce într-un nou "PIN" și va incrementa valoarea coloanei "PINS". Dacă toate informațiile specifice cursorului copil sunt găsite în "library cache" atunci discutăm de un "PINHIT".

După cum se poate observa și în figura 5, informațiile asociate cursorului copil sunt stocate în segmente de memorie diferite, prin urmare, datorită mecanismului de alocare/dealocare specific "shared pool"-ului, există posibilitatea ca anumite astfel de segmente să nu mai existe. Spre exemplu, se poate întâmpla ca planul de execuție ce corespunde instruncțiunii în cauză să fi fost înlăturat din memorie pentru a se face loc unor noi alocări. În acest caz, discutăm de un "RELOAD", adică procesul server care este responsabil de executarea instrucțiunii s-a înnecat ca țiganul la mal. A trecut prin toată tevatura de identificare a cursorului părinte, a identificat si un cursor copil candidat, dar, ghinion, acesta nu avea tot ce-i trebuie, făcându-l numai bun de aruncat la gunoi. O cifră mare pe coloana "RELOADS" raportată la numărul de "PINS", reprezintă o primă indicație a faptului că memoria necesară "library cache"-ului nu este suficientă.

În sfârșit, ajungem la "INVALIDATIONS". Fiecare cursor dispune de informații cu privire la obiectele de care depinde. Spre exemplu, comanda "SELECT COL1, COL2 FROM T" depinde de tabela "T", iar un cursor ce provine dintr-o execuție precedentă a acestei instrucțiuni va ști lucrul ăsta. Dacă aducem modificări de structură tabelei "T", atunci cursorul corespunzător va fi marcat ca invalid și nu va mai putea fi reutilizat. În acest caz, coloana "INVALIDATIONS" va reflecta acest lucru. Un număr mare de invalidări indică, de obicei, o frecventă utilizare de comenzi DDL care afectează cursoarele dependente (modificări de structură, revocare de drepturi etc.).

Punctăm de asemenea faptul că între indicatorul "INVALIDATIONS" și "RELOADS" există o relație de cauzalitate, în sensul că întotdeauna o invalidare va duce, la momentul execuției cursorului, la o operație de RELOAD. Prin urmare, ambii indicatori trebuie să fie luați în considerare înainte de a ne pronunța cu privire la eficiența utilizării "library cache"-ului. Să exemplificăm pe o bază de date izolată, pe care doar eu lucrez:
SQL> create table test (col integer);

Table created.

SQL> select count(1) from test;

  COUNT(1)
----------
         0 

SQL> select reloads, invalidations from v$librarycache where namespace = 'SQL AREA';

   RELOADS INVALIDATIONS
---------- -------------
        84           532 
După ce am executat interogarea din tabela "TEST" ne așteptăm să avem cursorul corespunzător în "library cache". Haideți să-l invalidăm prin recolectarea de statistici pentru tabelă.
SQL> exec dbms_stats.gather_table_stats(ownname => user, tabname => 'test', no_invalidate => false);

PL/SQL procedure successfully completed.

SQL> select reloads, invalidations from v$librarycache where namespace = 'SQL AREA';

   RELOADS INVALIDATIONS
---------- -------------
        84           533 
Observați incrementarea numărului de invalidări. Acum să vedem ce se întamplă dacă executăm din nou interogarea din tabela "TEST".
SQL> select count(1) from test;

  COUNT(1)
----------
         0

SQL> select reloads, invalidations from v$librarycache where namespace = 'SQL AREA';

   RELOADS INVALIDATIONS
---------- -------------
        85           533 
Se poate observa că, urmare a invalidării cursorului, serverul Oracle a fost nevoit să execute o operație de "RELOAD".

Să mai complicăm puțin lucrurile

Am discutat deja despre "hard parse" și "soft parse", punctând diferențele dintre ele. Ei bine, există și un al treilea tip de parsare, pe care maestrul Tom Kyte îl numește "softer soft parse", adică, într-o (semi-)traducere stupidă, "o parsare mai prietenoasă chiar decât o parsare prietenoasă". Despre ce este vorba? Ei bine, Oracle permite memorarea anumitor informații specifice cursoarelor la nivelul sesiunii. Ideea de bază e aceea că, deși am optimizat "library cache"-ul să grupeze cursoarele în acele "bucket"-uri, există în continuare un cost destul de mare în ceea ce privește parcurgerea "library cache"-ului pentru a găsi "bucket"-ul corespunzător și, mai departe, în acesta, cursorul părinte pentru instrucțiunea noastră. Soluția oferită de Oracle este configurarea unui "cache" local sesiunii, care să conțină pointeri spre acele cursoare executate cel mai frecvent de respectiva sesiune. Putem configura această funcționalitate prin intermediul parametrului SESSION_CACHED_CURSORS, parametru care ne spune câte astfel de cursoare să păstrăm la nivelul fiecărei sesiuni.
Figura 7 - Pointeri spre cursoare parinte la nivelul sesiunii
După cum se poate observa din figura de mai sus, Oracle va păstra la nivelul sesiunii pointeri spre cursoarele părinte din "library cache". Acest lucru este benefic deoarece mergem direct la țintă. Totuși, nu avem nici o garanție că acel cursor mai există în "library cache" sau că are tot ce-i trebuie. Prin urmare aceste verificări trebuie efectuate în continuare. De asemenea, stabilirea corectitudinii comenzii, a drepturilor asociate și așa mai departe sunt operațiuni care vor fi făcute și în acest caz. Tot ce obținem prin acest mecanism este un plus de performanță ce vine din faptul că putem identifica mult mai repede cursorul părinte asociat. Evident, într-un mediu puternic tranzacționat, beneficiem și de faptul că nu va mai fi necesară activarea "latch"-urilor care protejează bucket-urile, dat fiind că, prin pointer, accesăm fix cursorul care ne interesează.

Mai punctăm și faptul că sunt necesare trei executări ale aceleiași instrucțiuni pentru ca Oracle să considere că avem de-a face cu o comandă rulată frecvent, prin urmare benefic a se crea un pointer în "cache"-ul de cursoare specific sesiunii. Managemenul intern al acestei zone de memorie este guvernat de un algoritm de tip LRU (Least Recently Used).

Nu recomand setarea unei valori foarte mari pentru acest parametru. Așa cum am specificat deja, el este specific sesiunii, iar în contextul în care există foarte multe, atunci este posibil să avem nevoie de un spațiu mai mare în "shared pool". Asta sună puțin ciudat, dat fiind că aceste cursoare-pointer sunt stocate la nivelul procesului server în PGA, iar în arhitectura cu procese dedicate, PGA-ul nu are prea mari legături cu "shared pool"-ul. Problema e că toate aceste cursoare-pointer de la nivelul sesiunii sunt urmărite/monitorizate global prin intermediul unor view-uri sistem ce consumă spațiu din "shared pool". Este vorba, în special, despre X$KGLLK care stochează informații despre toate cursoarele deschise, deci inclusiv despre cele din "cache"-ul local de la nivelul sesiunilor, care vor figura ca deschise (open). În plus, căutarea unui cursor în "cache"-ul local se face secvențial, prin urmare, cu cât acesta conține mai multe cursoare, cu atât timpul de căutare va fi mai mare.

În principiu, pentru a stabili corect valoarea parametrului SESSION_CACHED_CURSORS ar trebui să inspectați și valorile următoarelor statistici, fie global din V$SYSSTAT, fie la nivelul sesiunii în V$MYSTAT:
  • session cursor cache count
  • session cursor cache hits
Primul indicator ne spune câte cursoare "vâjâim" prin "cache"-ul local, iar al doilea ne spune de câte ori Oracle a putut folosi acest "cache" pentru a executa o instrucțiune SQL. Obiectivul final îl reprezintă maximizarea celui de-al doilea indicator.

Notă: În Oracle 11g s-ar părea că este o problemă cu actualizarea indicatorului "session cursor cache hits".

"Library cache"-ul în mediu concurențial

Ce simple ar fi fost lucrurile dacă baza de date ar fi permis conectarea unui singur utilizator și cât de inutil și stupid un astfel de server ar fi fost! Până și așa-numitele baze de date "embedded" au implementate diferite mecanisme tranzacționale (vezi HSQLDB, Oracle Berkeley DB etc.), ce-i drept, poate nu la același nivel de detaliu, dat fiind specificul lor.

Un server Oracle puternic tranzacționat este asemeni orașului "New York" la o oră de vârf. Multe mașini și mulți pietoni încercând să-și facă loc pe străzi și trotoare, dar, atenție, cu respectarea unor reguli de circulație bine stabilite. Prin analogie, în contextul Oracle, participanții la trafic (mașini, pietoni) sunt de fapt sesiunile concurente, străzile reprezintă resursele partajate, iar regulile de circulație sunt mecanismele de serializare a accesului ("latch"-uri, "mutex"-uri și "lock"-uri).

Majoritatea celor care dezvoltă aplicații Oracle este familiarizată cu toate aceste concepte de "lock"-ing, dar la nivelul tabelelor și a înregistrărilor lor. Știm cu se mănâncă un "SELECT FOR UPDATE", ce se întâmplă cu actualizările simultane ale aceleiași înregistrări din sesiuni diferite, eventual cum sunt rezolvate situațiile de "dead-lock". Ce este mai puțin evident e că astfel de mecanisme există și la nivelul "library cache"-ului, însă acestea sunt gestionate intern, fără a exista vreun API explicit prin care să le putem controla. Într-un fel, este și normal ca accesul la "library cache" să fie serializat, dat fiind că discutăm până la urmă de o resursă partajată, unde mai multe sesiuni inspectează și modifică simultan această zonă de memorie.

După cum am văzut deja, "library cache"-ul poate fi privit ca pe o structură cu două niveluri. Există, pe de o parte, un nivel al identificatorilor și, pe de altă parte, un nivel al dependințelor. Să exemplificăm! Atunci când discutam de cursoarele asociate instrucțiunilor SQL spuneam că Oracle, înainte de a executa o comandă dată, va căuta mai întâi să vadă dacă nu cumva există deja această instrucțiune în "library cache". Cum face acest lucru? Ei bine, accesând acest prim nivel al "library cache"-ului, cel al identificatorilor. Fiecare astfel de cursor, dar și alte obiecte stocate în "library cache", au un identificator (handler) unic. Cu alte cuvinte, la acest nivel putem privi "library cache"-ul ca pe o colecție de identificatori (sau chei), împrăștiați prin tot library cache-ul. Oricum, aceștia reprezintă doar vârful aisbergului pentru că în spatele lor se ascunde cel de-al doilea nivel de care vorbeam, cel al dependințelor, intra și inter identificatori.

Prin "intra-depenență" ne referim la faptul că fiecare identificator (handler) din "library cache" poate avea asociate sub umbrela sa mai multe componente, la adrese de memorie diferite. Spre exemplu, ne reamintim că un cursor din "library cache" avea ca elemente constitutive planul de execuție, configurarea optimizorului la momentul execuției și date cu privire la variabilele "bind". Analog, pentru un pachet PL/SQL există identificatorul său și, legat de acesta, într-o altă zonă de memorie, codul compilat. Rezultă că avem de-a face cu structuri fragmentare care, deși la nivel conceptual modelează un obiect în sine, reprezentarea internă este compusă din segmente de memorie distincte.

Prin "inter-dependență", ne referim la faptul că un identificator din "library cache" poate să depindă de alți identificatori. Spre exemplu, în cazul unui pachet PL/SQL care conține un apel către o procedură stocată, între identificatorii corespunzători celor două obiecte va exista o relație de dependență, astfel încât, dacă procedura stocată va fi recompilată, stearsă etc., Oracle să poată invalida obiectele dependente. Este interesant că, deși aceste relații de dependență sunt deja consemnate în dicționarul bazei de date, Oracle a decis să implementeze această logică și la nivelul "library cache"-ului.

În acest context, intervin mai multe mecanisme de serializare a accesului concurențial, dintre care două sunt cele mai importante. Avem un prim mecanism denumit "library cache lock", prin care se protejează nivelul identificatorilor și un al doilea, denumit "library cache pin", care protejează nivelul dependințelor. Prin "library cache lock" Oracle se asigură că, odată localizat identificatorul, acesta nu va putea fi afectat de alte sesiuni. Acest mecanism de serializare intervine în special în timpul operației de "parse" și "bind", în acest fel evitându-se ca două sesiuni concurente să poată "parsa" aceeași instrucțiune în același timp. Să exemplificăm prin crearea unei proceduri de dimensiune mare:
create or replace procedure big_proc as
  i integer := 0; 
begin
  i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1;  
  i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1;  
  i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1; i := i + 1;  

  ... (linia de mai sus duplicată de 1000 de ori)

end;
/
Dacă rulăm codul de mai sus simultan din două sesiuni distincte, interogând V$SESSION obținem:
SQL> select sid, serial#, status, event, blocking_session
  2  from v$session
  3  where username='TALEK'
  4  and sid not in (select distinct sid from v$mystat);

SID SERIAL# STATUS EVENT                         BLOCKING_SESSION
--- ------- ------ ----------------------------- ----------------
 10      20 ACTIVE library cache lock                          70
 70      21 ACTIVE SQL*Net more data from client                 
Se poate observa că cea de-a doua sesiune va aștepta într-un eveniment "library cache lock", iar coloana "BLOCKING_SESSION" ne indică care este sesiunea blocantă. Prin urmare, dacă avem de-a face cu vreun utilizator supărat că instrucțiunea sa de "ALTER PACKAGE ... COMPILE" se blochează și nu face nimic, view-ul V$SESSION ar fi primul loc în care ar trebui să aruncăm o ochiadă pentru a identifica sesiunea care blochează.

Notă: În arhitecturile RAC, versiunea 10g, coloanele "BLOCKING_SESSION" și "BLOCKING_INSTANCE" nu sunt actualizate corespunzător, în cazul în care sesiunea blocantă este pe o instanță, iar sesiunea blocată pe alta. În acest caz, ar trebui consultat view-ul GV$GES_BLOCKING_ENQUEUE. În 11g situația este remediată.

Mecanismul "library cache lock" este implicat și în invalidarea obiectelor dependente din "library cache". Relația de dependență între obiecte (identificatori) din "library cache" este modelată prin intermediul acestor "library cache lock"-uri. Spre exemplu, de îndată ce un pachet PL/SQL a fost compilat, pentru toate obiectele din "library cache" de care depinde se va plasa un "library cache lock", în modul "shared" pe durata "parse"-ului și, mai departe, în modul "null". Dacă se execută un DDL pe definiția unui obiect dependent, Oracle va invalida obiectele corespunzătoare din "library cache" prin "spargerea" acestor "library cache lock"-uri, în amonte. Documentația Oracle face referire la acest mecanism și sub denumirea "breakable locks". "Breakable" deoarece un DDL nu va fi blocat niciodată de un astfel de "library cache lock".

După ce s-a obținut "library cache lock"-ul, pentru a putea mai departe executa o comandă dată, este nevoie a se construi întreg cursorul, ceea ce înseamnă că trebuie să fie accesate și zonele de memorie care ne dau imaginea completă a respectivului cursor, așa cum ar fi de pildă planul de execuție, variabile "bind" etc., dar și obiectele dependente. Prin urmare, Oracle trebuie să se asigure că aceste componente sunt protejate pentru a nu fi înlăturate din memorie sau modificate de alte sesiuni. Va face toate aceste "jonglerii" prin activarea celui de-al doilea mecanism de serializare, și anume "library cache pin". Un obiect în starea "pin" nu va putea fi înlăturat din memorie ca urmare a acțiunii algoritmului LRU de la nivelul "shared pool"-ului.

Notă: Am evitat să denumesc explicit mecanismele de serializare de mai sus ca "latch"-uri, dat fiind că implementarea lor diferă de la o versiune la alta. Începând cu versiunea 10g, Oracle a început a reimplementa aceste "latch"-uri sub forma "mutex"-urilor.

Intern, Oracle ține toate aceste informații în trei tabele "magice": X$KGLOB, X$KGLLK și X$KGLPN. Prima tabelă, X$KGLOB, conține obiectele de protejat, X$KGLLK conține "lock"-urile pe nivelul identificatorilor, iar X$KGLPN conține "pin"-urile care protejează inclusiv nivelul dependințelor.
Figura 8 - Relațiile dintre tabelele "magice" ce conțin informații despre "lock"-uri și "pin"-uri în "library cache"
Pentru a exemplifica, să tragem o ochiadă la "pin"-uri și cu ocazia asta să validăm faptul că acestea sunt plasate și asupra obiectelor dependente. Să rulăm dintr-o sesiune codul de mai jos:
 SQL> create or replace procedure wait_proc as
  2  begin
  3    dbms_lock.sleep(60);
  4  end;
  5  /

Procedure created.

SQL> select distinct sid from v$mystat;

       SID
----------
         9

SQL> exec wait_proc;
Între timp, dintr-o altă sesiune, conectați ca SYSDBA, să vedem ce "pin"-uri au fost puse din sesiunea 9.
 SQL> select o.kglnaown, o.kglnaobj, kglobtyd
  2  from x$kglpn p, x$kglob o
  3  where p.kglpnhdl = o.kglhdadr
  4  and kglpnsid = 9;

KGLNAOWN KGLNAOBJ  KGLOBTYD    
-------- --------- ------------
SYS      STANDARD  PACKAGE     
SYS      DBMS_LOCK PACKAGE BODY
TALEK    WAIT_PROC PROCEDURE   
SYS      DBMS_LOCK PACKAGE     
Se poate observa că, în plus față de procedura "WAIT_PROC", câte un "pin" a fost plasat și pe obiectele de care această procedură depinde.

Cât de lungă e povestea?

Dacă ați ajuns până aici cu cititul înseamnă că, fie sunteți la a "n-șpea" cafea, fie chiar vă fascinează poveștile astea cu Oracle în rolul principal, deși cele două posibilități nu se exclud reciproc. A scrie despre "library cache" la modul exhaustiv înseamnă a-ți propune să scrii o carte. Mai sunt multe de povestit: despre fragmentarea "shared pool"-ului, despre pachetul DBMS_SHARED_POOL și procedura KEEP, despre ORA-04031 și câte și mai câte. Totuși, am primit "plângeri" de la cititori că unele articolele sunt prea lungi și, pe alocuri, plicticoase. Prin urmare, am decis să închei "povestea" aici, pentru a răspunde măcar primului criteriu. Promit să mai revin la subiectul "library cache" în articole viitoare.

0 commentarii: