UPDATE GLOBAL INDEXES – testing suite

 Общи  Коментарите са изключени за UPDATE GLOBAL INDEXES – testing suite
Авг. 302010
 

За да възпроизведа проблемите, описани в статията; както и за да тествам евентуалните решения, драснах набързо един тестова пакет. Има си кусури – примерно не се държи правилно ако config таблицата е празна – но за решение „набързо“ се получи добре. Всъщност толкова добре, че реших да го сложа тук, да не го загубя 🙂

Идеята е следната: с едно елементарно скриптче се пускат произволен брой sql*plus сесии (аз пробвах с 20 и 40).

for i in `seq 1 20`;
do
  sqlplus /nolog @loader.sql &
done;

Всяка от тях извиква процедурата start_single_process и толкова.

Зареждането започва с извикване (от друга – контролна – сесия) на start_load. Добре е да се зададе някакъв wait, пък бил той и 0.01 сек. Иначе става тегаво.

По всяко време зареждането може да бъде прекъснато с pause_load. Извикването на stop_load кара sql*plus сесиите да приключат.

create or replace package multi_ses_load is

  c_status_start constant number := 1; -- the LOADER sessions should run
  c_status_pause constant number := 0; -- the LOADER sessions should stop
  c_status_exit  constant number := 2; -- the LOADER sessions should exit
  -- every LOADER process should call this procedure. The procedure will wait for the START signal
  procedure start_single_process;

  -- issies the START signal. Parameters:
  --    part_no: in which partition should we load data
  --    wait: how many seconds to wait between 2 inserts
  --    refresh_iterations: how often to refresh the data from the config table
  procedure start_load(in_part_no            in number,
                       in_wait_sec           in number default 0,
                       in_refresh_iterations in number default 10);

  -- changes in which partition should we load data
  procedure change_part(in_part_no in number);

  -- changes how many seconds to wait between 2 inserts
  procedure change_wait(in_wait_sec in number);

  -- changes how often to refresh the data flrom the config table
  procedure change_refresh(in_refresh_iterations in number);

  -- stops the load process until another START signal
  procedure pause_load;

  -- restarts the load without changing any parameters
  procedure restart_load;

  -- makes the loading processes to exit
  procedure stop_load;

end multi_ses_load;

create or replace package body multi_ses_load is

  c_alert_name constant varchar2(20) := 'SESS_ALERT';

  -- every LOADER process should call this procedure. The procedure will wait for the START signal
  procedure start_single_process is
    v_alert_msg          VARCHAR2(1800);
    v_alert_status       PLS_INTEGER;
    v_part_no            number;
    v_wait_sec           number;
    v_refresh_iterations number;
    v_curr_stat          number;
    v_ses_clob           clob;
    v_my_sid             number;
    v_seed               number;
    procedure read_config_vars is
    begin
      -- reed the config variables form teh config table
      select curr_stat, part_no, wait_sec, refresh_iterations
        into v_curr_stat, v_part_no, v_wait_sec, v_refresh_iterations
        from ses_load_parames;
    end;
  
  begin
    -- init the random generator. we will use the current SID and number of seconds since mignight for seed
    Select Sid
      into v_my_sid
      from v$mystat
     where rownum = 1;
    v_seed := (sysdate - trunc(sysdate)) * 1000 * (24 * 60 * 60) + v_my_sid;
    dbms_random.initialize(v_seed);
    -- read the config vars
    read_config_vars;
    while v_curr_stat in (c_status_start, c_status_pause) -- if the current status is START or PAUSE, we proceed; 
     loop
      if v_curr_stat = c_status_pause then
        -- if the status us PAUSE, wait for the signal
        -- we should register the current session as a consumer for the alert
        dbms_alert.register(c_alert_name);
        -- now we can wait for the alert
        dbms_alert.waitone(c_alert_name, v_alert_msg, v_alert_status);
        -- refresh the config vars - they may be changed
        read_config_vars;
        if v_curr_stat = c_status_exit then
          exit;
        end if;
      end if;
      -- do 'v_refresh_iterations' inserts
      for i in 1 .. v_refresh_iterations loop
        -- insert a row into 'v_part_no' partition
        insert into ...
        values (dbms_random.string('P', 32), ...); -- here we should take care of the partition placement
        commit;
        dbms_lock.sleep(v_wait_sec); -- wiat for 'v_wait_sec' seconds
      end loop;
      -- refresh the config vars after whe have done enough iterations 
      read_config_vars;
    end loop;
  end;

  -- issues the START signal. Parameters:
  --    part_no: in which partition should we load data
  --    wait: how many seconds to wait between 2 inserts
  --    refresh_iterations: how often to refresh the data flrom the config table
  procedure start_load(in_part_no            in number,
                       in_wait_sec           in number default 0,
                       in_refresh_iterations in number default 10) is
  begin
    -- we set the parameters in the config table
    update ses_load_parames
       set curr_stat = c_status_start, part_no = in_part_no, wait_sec = in_wait_sec,
           refresh_iterations = in_refresh_iterations;
    commit;
    -- set teh LOADER processes free 🙂
    dbms_alert.signal(c_alert_name, 'Газ!');
    commit;
  end;

  -- changes in which partition should we load data
  procedure change_part(in_part_no in number) is
  begin
    update ses_load_parames
       set part_no = in_part_no;
    commit;
  end;

  -- changes how many seconds to wait between 2 inserts
  procedure change_wait(in_wait_sec in number) is
  begin
    update ses_load_parames
       set wait_sec = in_wait_sec;
    commit;
  end;

  -- changes how often to refresh the data flrom the config table
  procedure change_refresh(in_refresh_iterations in number) is
  begin
    update ses_load_parames
       set refresh_iterations = in_refresh_iterations;
    commit;
  end;

  -- stops the load process until another START signal
  procedure pause_load is
  begin
    update ses_load_parames
       set curr_stat = c_status_pause;
    commit;
  end;

  -- restarts the load without changing any parameters
  procedure restart_load is
  begin
    -- we set the parameters in the config table
    update ses_load_parames
       set curr_stat = c_status_start;
    commit;
    -- set the LOADER processes free 🙂
    dbms_alert.signal(c_alert_name, 'Аре пак на пангара!');
    commit;
  end;

  -- makes the loading processes to exit
  procedure stop_load is
  begin
    -- put the exit flag in teh table
    update ses_load_parames
       set curr_stat = c_status_exit;
    commit;
    -- is some process is in PAUSE, we should alert him to reread the config vars
    dbms_alert.signal(c_alert_name, 'Аре да си одиме, момчета!');
    commit;
  end;

end multi_ses_load;
 Posted by at 16:21
Авг. 302010
 

Отдавна не съм споделял oracle-related приключения. Пусти мързел…

Днес ще разкажа за следната случка. На всеки час едно приложение „замръзва“ за няколко секунди. Всъщност идентифицирането на причините отне доста усилия. Видимите симптоми се появяват на ниво application server. Ровихме из настройките там, ровихме из мрежата, ОС, базите…

На края успяхме да идентифицираме следната зависимост: това приложение (покрай другите неща) използва и една супер натоварена таблица. Информацията в тази таблица се пази няколко часа, след това се изтрива; за това пък за тези няколко часа се натрупват доста данни – гигабайти. Операциите са предимно Insert и Select. Таблицата е разделена на partitions по часове, като на всеки час един job минава и затрива данните от най-стария partition с

alter table ... truncate partition ... UPDATE GLOBAL INDEXES

Това UPDATE GLOBAL INDEXES се налага, защото първичния ключ на таблицата има глобален индекс. Това е така, защото на partitioned таблици, уникалния индекс или трябва да е глобален, или да съдържа partition key колоните (логично).

Оказа се, обаче, че в тази схема има голям проблем. Странното „замръзване“ на приложението съвпада точно с времето, когато се стартира този този TRUNCATE job.

По-внимателно вглеждане в базата, съдържаща описаната таблица, показа, че в продължение на няколко секунди (почти) всички сесии, които се опитват да INSER-ват в тази натоварена таблица, чакат cursor: pin S wait on X. Има и малко library cache lock. Въпреки че говорим за времена от рода на 4-5-6 секунди, се натрупват един забележим брой сесии, които „висят“.

* * *

Какво се случва всъщност?

Disclaimer: следва обяснението, което аз приемам за вярно. Може и да бъркам нещо. Ако греша някъде, моля, поправете ме.

truncate (или в случая, alter table ... truncate partition) e DDL заявка. Като такава тя инвалидира всички курсори, които работят със съответната таблица. И когато дойде време за поредния insert (а такива има по десетки в секунда), първата сесия, която види невалидния курсор, тръгва да parse-ва. Тук говорим за parse на един елементарен insert на един ред в една таблица – би трябвало да стане за милисекунди. Обаче за да се случи parse, сесията трябва да „заключи“ за момент дефиницията на таблицата в library cache. В противен случай има опасност да parse-не, да измисли някакъв план, а някой друг да промени структурата на таблицата и плана да излезе неверен. Това заключване се случва с library cache lock.

Да, обаче в това време truncate-a си върви. insert сесията трябва да изчака truncate-a да свърши. Чак тогава може да получи заключваницата върху структурата на таблицата и да си направи parse-а. Цялата тази дандания е на пръв поглед ненужна при truncate – нали не променя по никакъв начин структурата на таблицата. Обаче е DDL. За това първия insert започва да чака.

Няколко милисекунди след това идва друга сесия, която също иска да insert-ва. Тази сесия иска да види дали има готов parse-нат план за заявката или не. Изчислява си hash на заявката, поглежда в library cache и за да е сигурна, удря един адски бърз lock на този hash. Във 10g/11g това става с използването на мутекси. Да, обаче в това време предишния insert още се мъчи да parse-не (всъщност чака). За това имаме невероятния шанс да видим заключване на мутекс – една операция, която трябва да става за микросекунди. Това чакане е известно като cursor: pin S wait on X.

Същото става и със следващата сесия, която опита insert. И със следващата. И със следващата…

* * *

По принцип truncate е много бърза операция. В крайна сметка тя е само промяна в data dictionary и не трябва да зависи от обема данни. Обаче в случая имаме един изроден truncate, който, освен нулирането на HWM, се занимава да оправя и разни индекси.

След като tuncate приключи, кълбото моментално се разплита в обратен ред. Но вече сме имали няколко секунди downtime на приложението. И това става на всеки час.

От Oracle са идентифицирали това положение като проблемно в bug 7640597. Даже са направили и пач. Идеята на пача е гениално проста и ефективна: инвалидирането на курсорите се случва в самия край на truncate операцията. Така те могат веднага да бъдат re-parse-нати.

За съжаление при пускането на този patch се оказва, че той има нежелани „странични ефекти“. За това да го оттеглили. Проблема е решен генерално в бъг 8441239, който е оправен в 11.2. Обаче няма backport за по-стари версии.

* * *

В този случай имаме няколко опции:

1. Да се откажем от TRUNCATE (и други алтернативни варианти, включващи DDL – примерно alter table exchange partition). Вместо това да използваме DELETE. Това ще доведе до много по-голямо натоварване на тази критична база; ще генерира огромно количество redo; и ще работи много по-бавно. Но понеже е прост DML, няма да инвалидира курсори.

2. Може да направим TRUNCATE по-бърз като направим partitions на половин час, или даже по-малко. Така всеки TRUNCATE ще има по-малко редове за оправяне в индекса, респективно – ще минава по-бързо. Това, обаче, само отлага проблема. При нарастване на обемите, или в някой по-натоварен момент, ще се озовем пред същата драма, а може и по-голяма.

3. Може да добавим колоната, по която се прави partitionig, като част от първичния ключ. В този случай индекса може да стане локален и TRUNCATE ще минава с обичайната си страшна скорост. Но това решение не е винаги възможно – зависи от самите данни и начина на използването им.

4. Може да upgrade-нем към 11.2. Oracle обещават, че там проблема е отстранен. Това, обаче, не е решение от днес за утре.

5. Може да настояваме за backport на patch-а. За целта трябва „a serious business justification“. Това, обаче, може да отнеме седмици или месеци; а и ще бъдем първите, които да изпитат новия patch в production.

Кой е верния път зависи от конкретната бизнес ситуация. За нуждите на блога няма да изпадам в подробности за нашия избор. Ще покажа, обаче, как може да тествате възможните решения, ако се натъкнете на този проблем.

Stay tuned!

 Posted by at 9:58

Ком

 Общи  Коментарите са изключени за Ком
Авг. 162010
 

Успях да завърШа описанието на живописното изкачване на връх Ком, което предприехме предния уикенд. и то малко преди да ми се счупи блога 🙂

Който няма търпение, може да чете. Две от буквите в блога ми са счупени тотално – голямо „и“ на кирилица и малко „Ш“. Но не знам кога ще мога да ги оправя. До тогава може да се насладите на неземно красивите снимки, които направихме от върха 😉

 Posted by at 10:53