DbCompare

Cel programu

W największym skrócie

Tworzysz, testujesz lub wdrażasz aplikację, która powinna (dla zapewnienia klientowi słodkiego poczucia bezpieczeństwa) utrzymywać dwie bazy danych o identycznej zawartości. Ale czy faktycznie w obu bazach znajduje się dokłądnie to samo?

Poniższy program daje dość dokładną odpowiedź na to pytanie, próbując zarazem obejść pewne standardowe problemy.

Nieco dłużej

Wymaganie utrzymywania dwóch kopii ważnej bazy danych w praktyce programistycznej pojawia się coraz częściej. Chodzi zarówno o zwiększenie bezpieczeństwa danych, jak o zmniejszenie czasu niedostępności aplikacji w razie awarii (gdy jedna baza ulegnie awarii, przez pewien czas pracujemy tylko na drugiej - ale stale możemy pracować). Stosować w tym celu można różne techniki - od coraz bardziej wyrafinowanych technik sprzętowych (FiberChannel itp), przez mechanizmy wbudowane ostatnio w większość programów zarządzających bazami danych (Oracle, Sybase, DB2 i inne) i dedykowane middleware (np. RTR, odpowiednio używane systemy kolejkowe), po specyficzne rozwiązania na poziomie aplikacji. Cały problem jest zdecydowanie niebanalny, podstawowe problemy to zagwarantowanie sensownego zachowania gdy jedna z baz jest z jakiegoś powodu niedostępna i "wyrównania" zawartości gdy znowu się pojawi, wykluczenie anomalii związanych z wykonaniem się poszczególnych transakcji na obu bazach w innej kolejności a także uniknięcie zbytniego narzutu wydajnościowego.

Nie chcę się tu jednak zajmować technikami replikacji danych ale problemem, na który trafiamy, gdy którąkolwiek z nich zaimplementujemy: jak sprawdzić, czy nasz mechanizm replikacji działa poprawnie. Faktyczne problemy wychodzą na jaw nie przy testach na pojedynczych rekordach ale przy długotrwałej pracy pod sporym obciążeniem.

Program DbCompare powstał jako narzędzie wspomagające prowadzenie takich porównań (zarówno w środowisku testowym, jak produkcyjnym).

Koncepcja

Sytuacja

Wykorzystujemy jakiś mechanizm synchronizacji dwóch baz danych. Chcemy zweryfikować jakość jego działania, tj. porównać zawartość obu baz danych i wykryć rozbieżności.

Problemy

Synchronizowane bazy w praktyce prawdopodobnie nigdy nie mają identycznej zawartości (zazwyczaj jedna z nich jest "odrobinę zacofana" w stosunku do drugiej). Dlatego siłowe rozwiązanie typu: eksport obu baz danych i porównanie wyniku nie działa poprawnie.

Pomysły typu: zamknięcie obu baz danych, zatrzymanie aplikacji i weryfikowanie zgodności dopiero wtedy są trudne do wykonania - nie tylko z przyczyn organizacyjnych (nie zawsze możęmy sobie na to pozwolić), jak praktycznych (jak zagwarantować, że cały kod synchronizujący zostanie wykonany - np. nic nie pozostanie w journalu RTR czy jakiejś kolejce). Dlatego chcemy, by nasza procedura działała dobrze bez zatrzymywania aplikacji.

Ze względu na obciążanie bazy danych niekorzystne byłoby też równoczesne porównywanie zawartości wszystkich tabel - lepiej, by porównanie mogło odbyć się "po kolei".

Idea rozwiązania

Idea rozwiązania jest stosunkowo prosta i opiera się na obserwacji, że wszelkie niezgodności wynikające z opóźnień w synchronizacji po krótkim czasie powinny zanikać. Faktyczna niezgodność - o ile się pojawi - przetrwa przez pewien czas. Dlatego program uznaje, że wykrył faktyczną różnicę między dwoma bazami danych, jeśli wyłapie tę samą niezgodność w dwóch lub więcej kolejnych uruchomieniach.

Takie podejście wymaga określenia, co to znaczy, że niezgodność jest "ta sama", co wykryta za poprzednim uruchomieniem. W przyjętym rozwiązaniu zdefiniowano to pojęcie jako identyczność kluczy: niezgodność jest "ta sama" co wykryta poprzednio, jeśli dotyczy rekordu o tym samym kluczu. Alternatywnie można by zdefiniować identyczność niezgodności przez pełną równość wszystkich pól (w rekordzie bądź rekordach których niezgodność dotyczy). Przyczyny odrzucenia tego podejścia - oraz wady przyjętego - są omówione w następnym akapicie.

Pewien kłopot przy wyłapywaniu stałych niezgodności sprawiają "zmiany kroczące" - sytuacje, gdy np. stale inkrementujemy jakieś pole (nowa wartość jakiegoś pola zależy od jego poprzedniej wartości). Przy (odrzuconym z tego powodu) podejściu "niezgodność jest ta sama jeśli zgadzają się wszystkie pola niezgodnych rekordów" możemy nie wykryć rozbieżności - jeśli błędny rekord zostanie zwiększony co najmniej raz między każdymi dwoma uruchomieniami programu porównującego. Dlatego przyjęto identyfikację niezgodności na podstawie klucza. W tym wypadku:

  • zmiany "kroczące" mogą nie zostać wykryte tylko jeśli dotyczą jednego z pól klucza - ale sytuacja, w której do klucza głownego tabeli należy często modyfikowane pole, jest na tyle rzadka, że nie warto się chyba nią przejmować (i tak mamy szanse wyłapać niezgodność, jeśli program porównujący jest uruchamiany dostatecznie często);
  • możemy natomiast zaraportować jako niezgodne rekordy, dla których drugi raz z rzędu "wstrzeliliśmy się" w moment pomiędzy wykonaniem transakcji typu dodaj 100 do pola balance na jednej i na drugiej bazie danych - ale po pierwsze prawdopodobieństwo, że dwa razy pod rząd trafimy w taki moment dla tego samego rekordu, nie jest duże, po drugie błędne rekordy zaraportowane jako niezgodne i tak zapewne będą "ręcznie" walidowane.

Uwaga: jeśli dla tabeli nie jest zdefiniowany klucz główny, program używa jako klucza wszystkich kolumn. Wówczas prawdopodobieństwo wystąpienia opisanych wyżej anomalii rośnie.

Szczegółowy algorytm działania programu

Program rozpoczyna działanie od przejrzenia słownika danych obu baz danych, określenia listy tabel i sprawdzenia, czy zawierają one te same pola i mają taki sam klucz główny. Ewentualne niezgodności są raportowane, dalsze przetwarzanie jest realizowane tylko dla tablic, dla których tego typu różnice nie wystąpiły.

Następnie dla każdej tabeli (która ma ten sam model danych w obu bazach) realizowany jest następujący algorytm:

  1. Porównaj zawartość tabeli w obu bazach (wykorzystując zapytania typu SELECT * FROM tabela ORDER BY klucz - co gwarantuje pobieranie kolejnych pól na podstawie indeksu klucza głównego, bez obciążania bazy danych sortowaniem być może dużej porcji danych), wyłapuj wszelkie rozbieżności - które mogą być jednego z dwóch typów:
    • rekord o danym kluczu jest obecny tylko w jednej z tych baz
    • rekord o danym kluczu jest obecny w obu bazach ale występuje niezgodność pól poza kluczem
  2. Wszystkie wykryte niezgodności zapisuj w pomocniczym pliku indeksowym - który dalej nazywam logiem niezgodności dla danej tabeli.
  3. Przejrzyj log niezgodności dla danej tabeli i:
    • usuń z niego informacje o wszystkich rozbieżnościach, które nie zostały wykryte w czasie aktualnego uruchomienia (czyli między poprzednim a obecnym uruchomieniem "znikły") - najwyraźniej wynikały one z tymczasowej rozbieżności między bazami wynikającej z opóźnienia w realizacji transakcji na bazie zapasowej (albo też występowały faktyczne rozbieżności ale już zostały poprawione),
    • dołącz do stanowiącego wynik uruchomienia raportu o rozbieżnościach wszystkie rozbieżności, które zostały wykryte "tym razem" a były już zarejestrowane wcześniej,
    • rozbieżności wykrytych "tym razem" a nie rejestrowanych wcześniej nie dołączaj do raportu o rozbieżnościach - jeśli nie "znikną" przedstawi je raport z następnego uruchomienia.

Informacje implementacyjne

Aktualna wersja programu obsługuje bazę Oracle (migracja dla innej bazy nie powinna być szczególnie trudna, szczegółowa dyskusja na ten temat znajduje się w należącym do dystrybucji pliku przenoszenie.txt). Aplikacja została napisana w języku perl przy wykorzystaniu modułów DBI oraz DBD::Oracle. Log niezgodności jest przechowywany w plikach indeksowych Berkeley DB (można je zamienić na pliki SDBM dostępne wszędzie wraz z perlem).

Program uruchamiałem głównie na platformie Linux. Powinien bez problemów działać na dowolnym Unix'ie i na Windows NT. Uruchomienie na VMS wymaga skompilowania DBI i DBD::Oracle na tej platformie (co nie jest szczególnie trudne - acz przynajmniej w moim przypadku wymagało kosmetycznych poprawek w plikach makefile) i ewentualnej zmiany rodzaju wykorzystywanych plików indeksowych. Nie musimy zresztą uruchamiać aplikacji na platformie na której działa baza danych - z powodzeniem możemy pobierać dane przez SQL*Net (z którego i tak musimy korzystać by móc łączyć się z dwoma bazami równocześnie).

Instalacja

Przed instalacją samej aplikacji należy przygotować odpowiednie środowisko:

  • zainstalować perl'a w wersji co najmniej 5.003 (testowana była wersja 5.004);
  • zainstalować dostępne w ramach CPAN moduły DBI (często stanowiący element domyślnej instalacji perla) oraz DBD::Oracle (kompilacja wymaga dostępności bibliotek OCI dla Oracle);
  • należy skonfigurować SQL*Net na maszynie uruchamiania tak, by był możliwy dostęp do obu porównywanych baz danych.

Instalacja samej aplikacji polega na skopiowaniu plików db_comparer.pl, TableComparer.pm oraz MetaData.pm do dowolnie wybranego katalogu.

Uruchamianie

Przygotowanie parametrów

Uruchomienie programu (db_comparer.pl) bez parametrów powoduje wypisanie tekstu pomocy opisującego wszystkie opcje uruchomienia. Parametry opisujące połączenie z bazą danych najprościej testować próbując uruchomić sqlplus: jeśli udaje się połączenie z bazą danych wykonane przez

sqlplus scott/tiger@DB1

to scott/tiger@DB1 można podać jako opis połączenia.

Przygotowanie środowiska

Do w pełni poprawnego działania aplikacji konieczne jest, by reguły sortowania bazy danych i perla były ze sobą spójne. Może tak nie być jeśli baza danych i/lub perl działają w trybie wykorzystującym jakiś język narodowy.

O regułach sortowania dla Oracle decyduje sposób w jaki została zdefiniowana baza danych oraz zmienna środowiskowa NLS_LANG.

O regułach sortowania dla perla decydują obowiązujące na danej maszynie ustawienia locale (program zawiera pragmę use locale) - chodzi głównie o zmienną LC_COLLATE, która może też być wnioskowana z innych zmiennych (LANG).

W praktyce wystąpienie problemu jest mało prawdopodobne (objawi się pojawieniem rozbieżności typu

X <---> Y Y <---> X

gdzie X i Y są pewnymi rekordami, które po kluczu inaczej zostaną posortowane przez perla a inaczej przez bazę danych.

Opisany kłopot jest powiększany przez dwa następujące czynniki:

  • nawet jeśli perl i baza danych zostaną skonfigurowane "pod ten sam język" nie jest do końca pewne, że reguły sortowania będą w pełni identyczne (niby jest ten POSIX ale...)
  • kusząca w tej sytuacji idea wykorzystania dla bazy i perla reguł angielskojęzycznych powoduje zamianę w raportowanych rekordach wszystkich narodowych znaków na dziwne symbole.

Pierwsze próbne uruchomienie

Sensowne uruchomienie "na próbę" polega na dwukrotnym uruchomieniu z tymi samymi parametrami (pierwsze uruchomienie wypisze jedynie informacje o różnicach w strukturze bazy danych, drugie poda różnice w zawartości).

Jeśli chcemy się "pobawić" programem a mamy do dyspozycji tylko jedną bazę danych, możemy porównywać dwa różne schematy w tej samej bazie (np. wyeksportować schemat użytkownika scott, zaimportować go jako użytkownika smith, wprowadzić trochę modyfikacji i podłączać się do tej samej bazy na dwa różne konta).

Procedura regularnego wykorzystania

Zalecane wykorzystanie programu to jego cykliczne uruchamianie w tej samej konfiguracji - połączone z przesyłaniem raportów do administratora. Warto w tym celu przygotować prosty skrypt - przykładowa treść (wariant dla shella Unixowego) to:

#!/bin/sh INSTDIR=/opt/scripts/compare_db DATADIR=/var/db/compare/some_db CONNECT1='scott/tiger@DB1' CONNECT2='scott/tiger@DB2' MAILTO='admin@some.machine' $INSTDIR/db_comparer.pl --con1=$CONNECT1 --con2=$CONNECT2 --data=$DATADIR | mail -s "Compare $CONNECT1 to $CONNECT2" $MAILTO

Tego typu skrypt powinien być uruchamiany regularnie - np. przy pomocy programu cron.

Dystrybucja

Program można pobrać ściągając plik db_compare_1-0-2.tar.gz.