wtorek, 27 maja 2014

PGLoader - narzędzie do importu danych

PGLoader (użyłam wersji 2.3.2) jest narzędziem napisanym w pythonie, służącym do importu danych. Nie wiem czy mieliście kiedyś styczność z tym narzędziem. Narzędzie dzieli dane na paczki (maksymalną wielkość paczek możemy sami ustawić). Tworzy także sekcje, które są odpowiedzialne za ładowanie danych do bazy. Każda z sekcji ma swój własny wątek, a ich ilość także jest konfigurowalna.

Jest bardzo prosty w obsłudze ale musimy go najpierw skonfigurować. Przykładowy plik konfiguracyjny zawiera (pgloader.conf jest domyślnym plikiem konfiguracyjnym):

[pgsql]
base = pgloader

host = 127.0.0.1 
port = 7432 
user = postgres
pass = 

log_file            = /tmp/pgloader.log
log_min_messages    = INFO 
client_min_messages = WARNING

lc_messages         = C 
pg_option_client_encoding = 'utf-8'
pg_option_standard_conforming_strings = on
pg_option_work_mem = 128MB 

copy_every      = 20000                               
null         = "" 
empty_string = "\ "
max_parallel_sections = 4 

Znaczenie parametrów:
  • base - baza danych, do której będą ładowane dane
  • host / post / user / pass - dane do połączenia się do bazy danych
  • log_file - ścieżka do pliku logowania
  • log_min_messages / client_min_messages -poziom logowania danych
  • lc_messages - ustawia zmienną LC_MESSAGES dla połączenia
  • pg_option_<foo> - ustawa dowolną zmienną <foo> dla połączenia
  • copy_every - maksymalna ilość danych importowana przez paczkę
  • null / empty_string - parametry są związane z interpretacją danych w tekście lub w plikach CSV. Zmienne odpowiednio definiują jak wartości NULL/puste stringi są reprezentowane w importowanych danych

    Przykład danych:

    cat test_copy.csv 
    "",2
    ,
    "\ ",3
    'eeee',0
     ee eee,9
    eee,
    

    Dla ustawień przedstawionych powyżej dane będą zapisane w bazie następująco:

    select c2,c3 from test_copy;
    +---------+------+
    |   c2    |  c3  |
    +---------+------+
    | NULL    |    2 |
    | NULL    | NULL |
    |         |    3 |
    | 'eeee'  |    0 |
    |  ee eee |    9 |
    | eee     | NULL |
    +---------+------+
    (6 rows)
    
    Time: 0,400 ms
    
  • max_parallel_sections - ilość paczek do załadowania w tym samym czasie


Przejdźmy dalej do ustawienia formatu plików. Ja chcę zaimportować plik CSV, a to są moje ustawienia:

[csv]
table        = pgloader_table
format       = csv
filename     = csv_without_header.data
field_sep    = ,
quotechar    = "
columns      = c1, c2, c3, c4, c5, c6, c7, c8, c9

Dane zostaną zapisane w tabeli pgloader_table i zostaną zaimportowane z pliku csv_witout_header.data Parametr kolumn wskazuje kolumny, w których mają zostać zapisane dane.


Uruchamiany PGLoader

W poniższym przykładzie użyłam opcji:
  • T (--truncate) - wymuś czyszczenie tabeli przed załadowaniem
  • s (--summary) - wypisz podumowanie
  • v (--verbose) - wypisz informacje o przetrważaniu
  • c (-c CONFIG, --config=CONFIG) - ścieżka do pliku konfiguracyjnego

pgloader -Tsvc pgloader.conf csv
pgloader     INFO     Logger initialized
pgloader     WARNING  path entry '/usr/share/python-support/pgloader/reformat' does not exists, ignored
pgloader     INFO     Reformat path is []
pgloader     INFO     Will consider following sections:
pgloader     INFO       csv
csv          INFO     csv processing
csv          INFO     TRUNCATE TABLE pgloader_table;
pgloader     INFO     All threads are started, wait for them to terminate
csv          INFO     COPY 1: 20000 rows copied in 1.651s
csv          INFO     COPY 2: 20000 rows copied in 1.752s
csv          INFO     COPY 3: 20000 rows copied in 1.809s
csv          INFO     COPY 4: 20000 rows copied in 1.815s
csv          INFO     COPY 5: 20000 rows copied in 1.736s
csv          INFO     COPY 6: 20000 rows copied in 1.738s
csv          INFO     COPY 7: 20000 rows copied in 1.710s
csv          INFO     COPY 8: 20000 rows copied in 1.618s
csv          INFO     COPY 9: 20000 rows copied in 1.611s
csv          INFO     COPY 10: 20000 rows copied in 1.624s
csv          INFO     COPY 11: 19173 rows copied in 1.535s
csv          INFO     No data were rejected
csv          INFO      219173 rows copied in 11 commits took 18.761 seconds
csv          INFO     No database error occured
csv          INFO     closing current database connection
csv          INFO     releasing csv semaphore
csv          INFO     Announce it's over

Table name        |    duration |    size |  copy rows |     errors 
====================================================================
csv               |     18.757s |       - |     219173 |          0


Jak to działa po stronie PostgreSQL?

Program dzieli dane na części (wielkość każdej części jest uzależniona od parametry copy_every), ustawia połączenie z baza danych, wykonuje zapytanie COPY:

COPY pgloader_table (KOLUMNY, ...)  FROM STDOUT WITH DELIMITER ',';

i na standardowe wyście wysyła dane. Jeśli chcieli byśmy wykonać to ręcznie to odbyło by się to następująco:

127.0.0.1:7432 postgres@pgloader # COPY test_copy (c2,c3) FROM STDOUT WITH DELIMITER ',';
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> cos tam 1,2
>> cos tam 2,3
>> cos tam 4,4
>> cos tam 5,25
>> \.
Time: 52529,504 ms
127.0.0.1:7432 postgres@pgloader # select * from test_copy;
+----+-----------+----+
| id |    c2     | c3 |
+----+-----------+----+
|  1 | cos tam 1 |  2 |
|  2 | cos tam 2 |  3 |
|  3 | cos tam 4 |  4 |
|  4 | cos tam 5 | 25 |
+----+-----------+----+
(4 rows)

Time: 0,337 ms

Do pustej tabeli test_copy importuje 4 wiersze. Dane do kolumn są oddzielone przecinkiem. Po wykonaniu komendy COPY PostgreSQL informuje nas:
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.


Ciekawostka!

Jeśli się zastanawiacie jak to możliwe, że pgloader używa polecenia COPY w ten sposób skoro w według dokumentacji (http://www.postgresql.org/docs/9.1/static/sql-copy.html) polecenie COPY ma konstrukcje:

COPY table_name [ ( column [, ...] ) ]
    FROM { 'filename' | STDIN }
    [ [ WITH ] ( option [, ...] ) ]

COPY { table_name [ ( column [, ...] ) ] | ( query ) }
    TO { 'filename' | STDOUT }
    [ [ WITH ] ( option [, ...] ) ]
to zaznaczę od razu, że dozwolona jest kombinacja FROM/TO i STDIN/STDOUT (mimo iż dokumentacja na to nie wskazuję!). Aby wyjaśnić troszkę tą nieścisłość i może niedowierzanie, kieruje was do posta z listy "pgsql-hackers": Re: "COPY foo FROM STDOUT" and ecpg.


PGLoader będzie szczególnie przydatne gdy chcecie zaimportować dane a nie macie praw dostępu administratora do serwera - aby zaimportować dane z pliku przy pomocy polecenia COPY, wymagane jest przekopiowanie go na serwer. Narzędzie dodatkowo formatuje nasze dane i informuje o błędach bez przerywania importu danych poprawnych (ale opcja --pedantic wymusi zatrzymanie procesowania w razie wystąpienia warningów).

piątek, 23 maja 2014

Naprawianie replikacji master-master w MySQLu

Pamiętacie mój ostatni post na temat podpinania nowego noda do replikacji w MySQL-u? Możecie go przeczytać tutaj, jeśli jeszcze tego nie zrobiliście.

Chciała bym teraz troszkę zmienić scenariusz wydarzeń i zastanowić się co musimy zrobić aby naprawić replikację master-master w MySQLu. Załóżmy, że mamy dwa serwery, których zmiany powinny być wzajemnie replikowane. Niestety zauważyliśmy, że zmiany z II serwera już od jakiegoś czasu nie są zatwierdzane na slavie z powodu błędu. Dodatkowym problemem jest rosnąca liczba plików relay logów na serwerze. Slave nie może ich usunąć bo zmiany w nich nie zostały zatwierdzone ale są cały czas od mastera pobierane nowe zmiany. Musimy zacząć myśleć co z tym zrobić aby się nie okazało, że za jakiś czas zacznie brakować nam miejsca.

Oto przykładowy błąd w tym wypadku:

mysql> show slave status \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 1.1.1.1
                  Master_User: replicate
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: host-mysql-01-bin.002539
          Read_Master_Log_Pos: 902412914
               Relay_Log_File: host-mysql-01-relay-bin.001829
                Relay_Log_Pos: 461330748
        Relay_Master_Log_File: host-mysql-01-bin.002399
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 1452
                   Last_Error: Could not execute Write_rows event on table test.BATCH_STEP_EXECUTION; Cannot add or update a child row: a foreign key constraint fails (`test`.`BATCH_STEP_EXECUTION`, CONSTRAINT `JOB_EXEC_STEP_FK` FOREIGN KEY (`JOB_EXECUTION_ID`) REFERENCES `BATCH_JOB_EXECUTION` (`JOB_EXECUTION_ID`)), Error_code: 1452; handler error HA_ERR_NO_REFERENCED_ROW; the event's master log host-mysql-01-bin.002399, end_log_pos 508490809
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 508490445
              Relay_Log_Space: 145560879727
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 1452
               Last_SQL_Error: Could not execute Write_rows event on table test.BATCH_STEP_EXECUTION; Cannot add or update a child row: a foreign key constraint fails (`test`.`BATCH_STEP_EXECUTION`, CONSTRAINT `JOB_EXEC_STEP_FK` FOREIGN KEY (`JOB_EXECUTION_ID`) REFERENCES `BATCH_JOB_EXECUTION` (`JOB_EXECUTION_ID`)), Error_code: 1452; handler error HA_ERR_NO_REFERENCED_ROW; the event's master log host-mysql-01-bin.002399, end_log_pos 508490809
1 row in set (0.00 sec)

Napisze od razu, że zastosowanie SQL_SLAVE_SKIP_COUNTER=1 (a tu artykuł dlaczego jest to złe) w tym przypadku nie jest rozwiązaniem - według błędu ta zmiana próbuje się odwołać do elementu, który nie istnieje. Co jeśli takich zmian jest cała masa? Chcecie cały czas siedzieć przed komputerem i sprawdzać czy nie trzeba countera pominąć?! Najlepszym rozwiązaniem będzie odtworzenie całej replikacji. Aby to zrobić będziemy początkowo traktować 'zepsuty' slave jako nowy nod (http://db-diary.blogspot.com/2014/05/podpiecie-nowego-noda-do-replikacji-w.html).

No to co robimy:
  1. Przekierowywujemy cały ruch aplikacji na działającego noda - w naszym przypadku jest to II serwer.
  2. Generujemy dump z II serwera:

    mysqldump -uroot -proot --skip-lock-tables --single-transaction --flush-logs --hex-blob --master-data=2 -A > dump.sql


    UWAGA!
    Tutaj musicie założyć, że wszystkie bazy danych mają tabelkę transakcyjne - InnoDB. Jeśli używanie także MyISAM wymagane będzie zatrzymać aplikację, które powodują zamiany na działającym masterze (w naszym przypadku to oczywiście serwer II) lub wymusić lokowanie tabel w czasie robienia dumpa. Jeśli tego nie zrobicie w dumpie mogą pojawić się dane z 'późniejszych' insertów niż pozycja i plik binary logów na to wskazuje.
  3. Zczytujemy pozycję mastera z dumpa

    head dump.sql -n80 | grep "MASTER_LOG_POS"
  4. Kopiujemy dump na I serwer
  5. Stopujemy replikację na oby dwóch serwerach przy pomocy polecenia:

    STOP SLAVE;

    Ponieważ w dumpie mamy wszystkie polecenia do odtworzenia całego klastra, musimy się upewnić, że w czasie importowania jego, żadne zmiany nie przeniosą się na slava.
  6. Resetujemy slava na I serwerze

    RESET SLAVE;

    Przy pomocy tego polecenia wszystkie relay-logi zostaną usunięte i zostanie stworzony całkiem nowy. Informacje o replikacji także zostaną 'zapomniane' - pliki master.info i relay-log.info zostaną usunięte. Zawierają one informację o połączeniu i pozycjach w plikach. Warto je przekopiować na wypadek jak byśmy nie pamiętali nazwy użytkownika i jego hasła lub IP hosta potrzebnego do połączenia w replikacji.
  7. Zaimportować dump

    mysql -uroot -proot < dump.sql
  8. Zczytujemy nazwę binarylogu i pozycję na serwerze I

    SHOW MASTER STATUS;
  9. Ustawiamy slava dla serwerów

    CHANGE MASTER TO MASTER_HOST='HOST',MASTER_USER='replication',MASTER_PASSWORD='replication', MASTER_LOG_FILE='mysql-bin.000002', MASTER_LOG_POS=504122;

    Plik i pozycję dla serwera I zaczytaliśmy z dumpa (punkt 3), a dla serwera II z poprzedniego kroku (punkt 8).
  10. Uruchamiany replikację

    START SLAVE;
  11. Potwierdzamy działanie replikacji

    SHOW SLAVE STATUS \G

Może się zdarzyć, że mimo to replikacja i tak nie działa. Musicie się przyjrzeć dokładnie jakie zmiany mają być przenoszone - może to wynikać ze złego sposobu wykonania dumpa (popatrz na komentarz w punkcie 2). Załóżmy, że miało to miejsce i pojawił się następujący błąd:

mysql> show slave status \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 1.1.1.1
                  Master_User: replicate
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: host-03-bin.002549
          Read_Master_Log_Pos: 130312481
               Relay_Log_File: host-03-relay-bin.000002
                Relay_Log_Pos: 33108
        Relay_Master_Log_File: host-03-bin.002549
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 1032
                   Last_Error: Could not execute Update_rows event on table test.BATCH_JOB_SEQ; Can't find record in 'BATCH_JOB_SEQ', Error_code: 1032; handler error HA_ERR_END_OF_FILE; the event's master log host-03-bin.002549, end_log_pos 33132
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 32951
              Relay_Log_Space: 130313453
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 1032
               Last_SQL_Error: Could not execute Update_rows event on table test.BATCH_JOB_SEQ; Can't find record in 'BATCH_JOB_SEQ', Error_code: 1032; handler error HA_ERR_END_OF_FILE; the event's master log host-03-bin.002549, end_log_pos 33132
1 row in set (0.00 sec)

Istnieje prawdopodobieństwo, że tych złych danych jest tak mało, że możemy ręcznie wyrównać zmiany, aż slave przestanie sypać błędami (wykonanie dumpa trwało na tyle krótko, że tych 'nadmiernych' zmian było mało). Ale jak to zrobić? Posłużymy się narzędziem masterbinlog. Ja postanowiłam przejżeć wszystkie zmiany w relay logach:

mysqlbinlog --start-position=0 --verbose --base64-output=DECODE-ROWS host-03-relay-bin.000002 > /tmp/binary.logs

Przeglądałam plik i szukałam wystąpienia znaków 'end_log_pos 33132'. W ten sposób znalazłam:

#140522  1:12:00 server id 173  end_log_pos 33108     Table_map: `test`.`BATCH_JOB_SEQ` mapped to number 5085
#140522  1:12:00 server id 173  end_log_pos 33132     Update_rows: table id 5085 flags: STMT_END_F
### UPDATE test.BATCH_JOB_SEQ
### WHERE
###   @1=354422
### SET
###   @1=354423
# at 4309887

Sprawdziłam, że ten wiersz dla tego ID w tej tabeli nie istnieje, a właściwie istnieje ale ma inną wartość - dlatego slave nie jest w stanie zatwierdzić zmian. Co teraz zrobić?

  1. Zatrzymujemy slave

    STOP SLAVE;
  2. Wykonujemy taką zmiane na bazie danych aby dane do tego momentu były zgodne do zatwierdzenia przez slave:

    UPDATE test.BATCH_JOB_SEQ SET ID = 354422 WHERE ID = X;
  3. Wznawiamy slava

    START SLAVE;
Może się okazać, że tych zmian jest troszkę. Ale czasie warto przejść przez to kilka razy. Jeśli jednak zmian jest za dużo, warto się zastanowić nad wykonaniem dumpa ponownie z lokowaniem zapisy do tabel.

wtorek, 13 maja 2014

Podpięcie nowego noda do replikacji w MySQL-u

Opisywałam już zestawić replikację master-master dla dwóch nodów w MySQL-u. Jeśli chcecie wrócić do tego posta to tutaj jest do niego link.

Ale co jeśli chcemy podpiąć nowy serwer do replikacji? Na przykład mamy już działający jeden serwer ale chcemy aby działał w replikacji master-slave lub master-master.

Oto kilka prostych kroków:
  1. Upewniamy się, że master ma wszystkie dane, a do nowego noda nikt nie pisze.
  2. Generujemy dump z mastera:

    mysqldump -uroot -proot --skip-lock-tables --single-transaction --flush-logs --hex-blob --master-data=2 -A > dump.sql

    To polecenie stworzy dump ze wszystkimi tabelami z mastera. Dzięki opcji --master-data wiemy na jakiej pozycji i jakim pliku jest master przy wykonywaniu tego dumpu.
  3. Zczytujemy pozycję mastera z dumpa:

    head dump.sql -n80 | grep "MASTER_LOG_POS"
  4. Kopiujemy plik dumpa na nowy nod

    scp dump.sql root@HOST:/path
  5. Importujemy dump

    mysql -uroot -proot < dump.sql

    W dumpie mamy wszystkie polecenia do odtworzenia mastera (usunięcie i stworzenie baz i table + inserty z danymi).
  6. Ustawiamy replikację z danymi, które zaczytaliśmy z dumpa:

    CHANGE MASTER TO MASTER_HOST='host',MASTER_USER='replication',MASTER_PASSWORD='replication', MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=98;

    START SLAVE;

    Od tego momentu nowy nod jest slavem do naszego mastera. Jeśli na masterze były nowe zmiany powstałe po wykonaniu dumpa ale przed uruchomieniem replikacji, zostaną teraz zaciągnięte bo slave wie od którego momentu ma dane. 
Jeśli po tych krokach chcemy aby nowy nod był nowym masterem i zamienić naszą replikację master-slave na master-master, musimy ostatni 6 punkt wykonać także na 'starym' serwerze (oczywiście dane na temat pliku i pozycji mastera musimy zczytać, np. przy pomocy polecenia: SHOW MASTER STATUS).

Pamiętajmy o pewnych ustawieniach:
  • włączenie binary logów aby replikacja mogła działać (log-bin)
  • unikalność ID serwerów (server-id)
  • dla replikacji master-master: auto_increment_offset & auto_increment_increment
Mam nadzieję, że przyda się wam ten krótki post.