niedziela, 5 października 2014

Wylosuj nagrodę z Redisem

Od czasu istnienia tego blogu koncentrowałam się na bazach relacyjnych. Tym razem przyszedł czas na Redis. Ta NoSQL baza danych została stworzona specjalnie po to aby podpowiedzieć na wymagania bardzo szybkiego odpowiadania lub obsługi bardzo szybko zmieniających się danych. Z satysfakcją jest używana w miejscach gdzie wcześniej był stosowany Memcache bo zawiera w sobie wszystkie te same cechy i o wiele więcej.

Główne cechy Redisa:
  • napisany w języku C
  • niesamowicie szybki
  • używany protokół jest telneto-podobny, binarnie bezpieczny
  • dane są przechowywane w pamieci
  • pojemność bazy jest ograniczona do pojemności RAM maszyny
  • replikacja master-slave z automatycznym failoverem
  • podstawowa struktura danych to klucz - wartość
  • bardzo duża ilość różnych operacji, które można wykonywać na danych
  • posiada możliwość zapisywania: 
    • list (lists)- kolekcja elementów
    • zbiorów (sets) - kolekcja unikalnych elementów
    • posortowanych zbiorów (sorted sets) - jak wyżej z dodatkowym sortowaniem
    • słowników - (hashes) - kolekcja typu klucz - wartość
  • posiada transakcje - zapytania są serializowane co powoduje, że pojedyncze operacje są izolowane
  • możliwe jest ustawienie czasu po jakim wartość zostanie automatycznie usunięta (TTL)
  • pub/sub - implementacja messagingu
  • open-sursowy, oparty na licencji BSD

Instalacja bazy danych Redis wiąże się tylko z kilkoma krokami (dobrze jest wykonać testy aby mieć pewność, że wszystko poszło jak należy). Ostatni krok to uruchomienie serwera.

$ wget http://download.redis.io/releases/redis-2.8.17.tar.gz
$ tar xzf redis-2.8.17.tar.gz
$ cd redis-2.8.17
$ make
$ make test
$ src/redis-server


Sprawdźmy najpierw użycie niektórych poleceń we wbudowanym kliencie redis-cli:

ela@skyler:~/work/redis-2.8.17$ src/redis-cli
127.0.0.1:6379>

W momencie gdy logujemy się do Redisa, jesteśmy automatycznie zalogowani do bazy danych 0. Zmiana na inną bazę danych powodowana jest przez polecenie 'SELECT ID' (Tutaj znajdziecie wszystkie polecenia).

ela@skyler:~/work/redis-2.8.17$ src/redis-cli
127.0.0.1:6379> select 10                              # przełączenie się na index (bazę danych ID=10)
OK
127.0.0.1:6379[10]> sadd tokens 0f9661323              # dodanie do zbioru (i tak 1000 razy)
(integer) 1
127.0.0.1:6379[10]> keys '*'                           # wyświetlenie wszystkich kluczy, które pasują do wzorca
1) "tokens"
127.0.0.1:6379[10]> scard tokens                       # zliczenie memberów (elementów) zbioru
(integer) 1000
127.0.0.1:6379[10]> srandmember tokens 10              # wyświetlenie 10 randomowo wybranych elementów zbioru
 1) "0f9661323"
 2) "aa942ab2b"
 3) "0fcbc61ac"
 4) "08b255a5d"
 5) "96b9bff01"
 6) "49182f81e"
 7) "8e98d81f8"
 8) "23ce18513"
 9) "52720e003"
10) "7f6ffaa6b"
127.0.0.1:6379[10]> flushall                           # usunięcie wszystkich kluczy ze wszystkich baz danych
OK
127.0.0.1:6379[10]> keys '*'
(empty list or set)

Aby przedstawić choć część działania Redis napisałam system losowania tokenów. Tokeny są trzymane w zbiorze 'tokens'. Gdy użytkownik wysyła request o token, jest on losowo wybierany i usuwany ze zbioru przy pomocy polecenia spop. Wybrany token jest wstawiany do zbioru used. System został napisany w ruby przy użyciu gemów: redis (klient Redis), sinatra (framework web).

Zaczęłam od wygenerowania 1000 tokenów i załadowania ich do redis:

#!/usr/bin/env ruby -w

require 'redis'
require 'digest/md5'

# redis connection
REDIS_HOST = '192.168.0.10'
REDIS_PORT = 6379
REDIS_DATABASE_ID = 10

TOKENS_COUNT = 1000
REDIS_SET_KEY = 'tokens'

redis = Redis.new(:host => REDIS_HOST, :port => REDIS_PORT, :db => REDIS_DATABASE_ID)
redis.flushdb


TOKENS_COUNT.times do |i|
 md5 = Digest::MD5.hexdigest(i.to_s)
 redis.sadd REDIS_SET_KEY, md5[0..8]
end

Przy pomocy gemu sinatra stworzyłam prosty interface webowy do pobierania i wyświetlania informacji o tokenach:

[ela:~/work/ruby ]$ cat get_token_page.rb
#!/usr/bin/env ruby -w

require 'sinatra'
require 'redis'

# redis connection
REDIS_HOST = '192.168.0.10'
REDIS_PORT = 6379
REDIS_DATABASE_ID = 10
REDIS_TOKENS_AVALIABLE_SET_KEY = 'tokens'
REDIS_TOKENS_USED_SET_KEY = 'used'

redis = Redis.new(:host => REDIS_HOST, :port => REDIS_PORT, :db => REDIS_DATABASE_ID)

get '/' do
 %(Click <a href="/token">here</a> to get a token)
end

get '/token' do
 token = redis.spop REDIS_TOKENS_AVALIABLE_SET_KEY
 redis.sadd REDIS_TOKENS_USED_SET_KEY, token
 %(This is your token: #{token})
end

get '/tokens-used' do
 tokens = redis.smembers REDIS_TOKENS_USED_SET_KEY
 token_list = tokens.map{|t| %(<li>#{t}</li>)}
 %(<ul>#{token_list.join}</ul>)
end

get '/stats' do
 tokens_avaiable_count = redis.scard REDIS_TOKENS_AVALIABLE_SET_KEY
 tokens_used_count = redis.scard REDIS_TOKENS_USED_SET_KEY
 %(# Tokens avaliable :#{tokens_avaiable_count} )+
 %(# Tokens used :#{tokens_used_count} )+
 %(# All Tokens :#{tokens_avaiable_count + tokens_used_count} )
end


Strona wygląda następująco:





Podsumowując, w programie użyłam zbioru aby mieć pewność unikalnych tokenów. Redis dla zbiorów zapewnia funkcję do wybierania losowego elementu i jego usunięcia - polecenie spop. Jest to operacja atomowa, dzięki czemu mam pewność, że każdy token zostanie użyty tylko raz. Inne polecenia:

  • sadd - dodanie nowego elementu do zbioru jeśli jeszcze nie istniał
  • smembers - pobranie wszystkich elementów zbioru
  • scard - pobranie ilości elementów w zbiorze
  • flushdb - usunięcie wszystkich kluczy i wartości aktualnie wybranej bazy bez usunięcia samej bazy 

Redis posiada także możliwość definiowania transakcji. Gdzie użylibyście jej w moim programie?


poniedziałek, 29 września 2014

Promowanie serwera standby po awarii primary - PostgreSQLu

Ten post jest następstwem dość już starego innego posta "Tworzenie replikacji primary-standby w PostgreSQLu" aby uzupełnić małe braki w przypadku awarii mastera. Stworzyliśmy w nim replikację z jednym serwerem primary (read/write) i jednym serwerem standby w trybie hot_standby (tylko read). Oto przykładowy schemat architektury:


Doszło jednak do awarii i musimy dać użytkownikom naszego systemu możliwość zmian danych - czyli musimy zmienić działanie naszego standby na działającego primary i przekierować cały ruch na ten serwer.


Niestety PostgreSQL nie ma oprogramowania do identyfikacji crashu mastera i automatycznego promowania slava. Musimy zrobić to ręcznie (Na szczęście są narzędzia, które mogą nam w tym pomóc).

W poniższym przykładzie mamy dwie działające instancje na tej samej maszynie. Master pracuje na standardowym porcie, czyli 5432, a slave na porcie 6432. Sprawdźmy je aby mieć pewność, że działają tak jak chcemy:

[root@amsterdam /var/lib/pgsql]$ psql test
psql (9.3.5)
Wpisz "help" by uzyskać pomoc.

test=# select pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f
(1 wiersz)

test=# select name, setting from pg_settings where name = 'port';
 name | setting 
------+---------
 port | 5432
(1 wiersz)

test=# \q
[root@amsterdam /var/lib/pgsql]$ psql -p6432 test
psql (9.3.5)
Wpisz "help" by uzyskać pomoc.

test=# select name, setting from pg_settings where name = 'port';
 name | setting 
------+---------
 port | 6432
(1 wiersz)

test=# select pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 t
(1 wiersz)

Jedenym ze sposobów inicjonowania procesu failover, jest dodanie do pliku recovery.conf informacji o pliku triggera:

[root@amsterdam /var/lib/pgsql/9.3/data4]$ cat recovery.done 
standby_mode = 'on'
primary_conninfo = 'user=replication password=replication host=127.0.0.1 port=5432 sslmode=prefer sslcompression=1 krbsrvname=postgres'

trigger_file = '/var/lib/pgsql/psql.trigger.6432'


Zawartość pliku triggera ma znaczenie. Jeśli jest pusta lub zawiera słowo 'smart', standby zostanie podniesiony zaraz po zatwierdzeniu wszystkich dostępnych w katalogu archive plików WAL (Smart Failover). Jeśli jednak zawartość pliku będzie zawierać słowo 'fast', serwer ma zostać podniesiony natychmiast. Wszystkie pliki WAL, które nie zostały jeszcze zastosowane, będą ignorowane, a transakcję w nich zostaną utracone (Fast Failover).


Spróbujmy uruchomić proces Smart Failover:

[root@amsterdam /var/lib/pgsql]$ touch /var/lib/pgsql/psql.trigger.6432
[root@amsterdam /var/lib/pgsql]$ ll 9.3/data4/
razem 112
-rw------- 1 postgres postgres   208 09-25 05:09 backup_label.old
drwx------ 6 postgres postgres  4096 09-25 05:09 base
drwx------ 2 postgres postgres  4096 09-25 05:38 global
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_clog
-rw------- 1 postgres postgres  4309 09-25 05:09 pg_hba.conf
-rw------- 1 postgres postgres  1636 09-25 05:09 pg_ident.conf
drwx------ 2 postgres postgres  4096 09-25 05:36 pg_log
drwx------ 4 postgres postgres  4096 09-25 05:09 pg_multixact
drwx------ 2 postgres postgres  4096 09-25 05:36 pg_notify
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_serial
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_snapshots
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_stat
drwx------ 2 postgres postgres  4096 09-25 05:39 pg_stat_tmp
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_subtrans
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_tblspc
drwx------ 2 postgres postgres  4096 09-25 05:09 pg_twophase
-rw------- 1 postgres postgres     4 09-25 05:09 PG_VERSION
drwx------ 3 postgres postgres  4096 09-25 05:39 pg_xlog
-rw------- 1 postgres postgres 20573 09-25 05:11 postgresql.conf
-rw------- 1 postgres postgres    60 09-25 05:36 postmaster.opts
-rw------- 1 postgres postgres    74 09-25 05:36 postmaster.pid
-rw-r--r-- 1 postgres postgres   208 09-25 05:18 recovery.done

[root@amsterdam /var/lib/pgsql]$ psql -p6432 test
psql (9.3.5)
Wpisz "help" by uzyskać pomoc.

test=# select pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f
(1 wiersz)

test=# select name, setting from pg_settings where name = 'port';
 name | setting 
------+---------
 port | 6432
(1 wiersz)

test=# \q

Serwer działający w trybie 'recovery mode' został tak zaprojektowany, aby odtwarzać dane po nieoczekiwanym zamknięciu. Plik recovery.conf definiuje jak znaleźć pliki logów transakcji i trzymać je dopóki nie są nam potrzebne. Wtedy serwer przechodzi w tryb normalny i sam generuje własne pliki logów transakcji. Gdy ten proces zostaje zakończony (failover), nazwa plik recovery.conf jest zmieniana na recovery.done.

Innym sposobem na promowanie slava na mastera, bez użycia pliku triggera, jest uruchomienie programu pg_ctl promote. Jako parametr podajemy katalog instancji slava (musimy to zrobić użytkownikiem nieuprzywilejowanym, który będzie właścicielem procesu):

[root@amsterdam /var/lib/pgsql/9.3]$ sudo su postgres
bash-4.2$ 
bash-4.2$ cd
bash-4.2$ /usr/pgsql-9.3/bin/pg_ctl promote -D /var/lib/pgsql/9.3/data5
serwer w trakcie rozgłaszania
bash-4.2$ 
bash-4.2$ psql -p6432 test
psql (9.3.5)
Wpisz "help" by uzyskać pomoc.

test=# select name, setting from pg_settings where name = 'port';
 name | setting 
------+---------
 port | 6432
(1 wiersz)

test=# select pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f
(1 wiersz)

test=# \q


Widziałam wiele zapytań o automatyzację tego procesu na różnych forach internetowych. Pytanie jest tylko takie - czy jesteśmy w stanie napisać skrypt, który podjoł by decyzję, że serwer primary padł i aby system miał działać nadal musimy promować standby na jego miejsce? A może to był tylko problem sieciowy i niedługo wszystko wróci do normy. Czy nie lepiej aby to człowiek zdecydował, aby proces promowania rozpocząć i sam proces failover automatyzować.

Powodzenia i miłego testowania działania replikacji.

wtorek, 19 sierpnia 2014

Bucardo - replikacja master-master w PostgreSQLu

Bucardo jest aplikacją napisaną w perlu, służącą na przykład do replikacji danych w PostgreSQLu. Jest o tyle ciekawym rozwiązaniem, że pozwala na synchronizacje danych typu master-master.

Główne cechy Bucardo to:
  • Asynchroniczna kaskadowa replikacja master-slave i/lub master-master
  • Replikacja typy 'row based'
  • W swoim działania używa triggerów i asynchronicznych powiadomień LISTEN/NOTIFY/UNLISTEN (link do poprzedniego postu na ten temat tutaj)
  • Wymaga dedykowanej bazy danych. Działa jako demon programu napisanego w Perlu, który komunikuje się ze swoją bazą danych i wszystkimi innymi bazami związanymi z działającymi replikacjami (synchronizacjami)
  • Multimaster synchronizacja jest ograniczona tylko do dwóch baz danych (Od wersji 5.0 to ograniczenie już nie istnieje). Dla tego typu replikacji musi być zdefiniowany sposób rozwiązywania konfliktów. Typy definicji ich rozwiązywania:
    • source - wiersz na bazy 'source' zawsze wygrywa
    • target - wiersz na bazie 'target' zawsze wygrywa
    • skip - wiersze przy, których wynikł konflikt nie są zreplikowane
    • random - każdy z serwerów ma takie same szanse
    • latest - wiersz, który został niedawno zmieniony, wygrywa
    • abort - synchronizacja jest przerywana na konflikt
  • Replikacja master-slave umożliwia działanie jednego mastera z wieloma slavami
  • Jest możliwa kaskadowa synchronizacja
  • Nie ma możliwości replikacji zmian typu DDL ponieważ PostgreSQL nie ma triggerów na tabelach systemowych
  • Nie ma możliwości replikacji dużych obiektów
  • Możliwość dowolnego projektowania architektury naszej replikacji
  • Wymagania:
    • PostgreSQL:
      • Od wersji 8.X
      • zainstalowany język pl/perlu
      • zainstalowany język pl/pgsl
    • Perl + moduły (DBD::Pg, DBI, DBIx::Safe, ExtUtils::MakeMaker)
  • Bucardo w wersji 4 trzeba zdefiniować jeden z typów synchronizacji:
  • Demony działające w Bucardo:
    • MCP - Master Control Process - główny proces zarządzający synchronizującymi
    • CTL - Controller - śledzi i zabija procesy synchronizujące
    • KID - Proces synchronizujący
  • Bardzo prosta instalacja paczek perl-owych i Bucardo:

    perl Makefile.pl
    make
    make test
    sudo make install
    
    
    Instalacja Bucardo:

    [bucardo@bucardo ~]$ /usr/local/bin/bucardo install
    This will install the bucardo database into an existing Postgres cluster.
    Postgres must have been compiled with Perl support,
    and you must connect as a superuser
    
    Current connection settings:
    1. Host:           
    2. Port:           5432
    3. User:           postgres
    4. Database:       bucardo
    5. PID directory:  /var/run/bucardo
    Enter a number to change it, P to proceed, or Q to quit: 3
    
    Change the user to: bucardo
    
    Changed user to: bucardo
    Current connection settings:
    1. Host:           
    2. Port:           5432
    3. User:           bucardo
    4. Database:       bucardo
    5. PID directory:  /var/run/bucardo
    Enter a number to change it, P to proceed, or Q to quit: 4
    
    Change the database name to: test
    
    Changed database name to: test
    Current connection settings:
    1. Host:           
    2. Port:           5432
    3. User:           bucardo
    4. Database:       test
    5. PID directory:  /var/run/bucardo
    Enter a number to change it, P to proceed, or Q to quit: P
    
    Postgres version is: 9.3
    Attempting to create and populate the bucardo database and schema
    Database creation is complete
    
    Updated configuration setting "piddir"
    Installation is now complete.
    If you see errors or need help, please email bucardo-general@bucardo.org
    
    You may want to check over the configuration variables next, by running:
    /usr/local/bin/bucardo show all
    Change any setting by using: /usr/local/bin/bucardo set foo=bar
    
    
  • Konfiguracja Bucardo
    • Przykład dodania bazy danych

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add database amsterdam dbname=test host=188.226.xxx.xxx user=root pass=root
      Added database "amsterdam"
      
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add database niujork dbname=test host=107.170.xx.xx user=root pass=root
      Added database "niujork"
      
      

    • Przykład listy baz danych

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo list dbs
      Database: amsterdam  Status: active  Conn: psql -U root -d test -h 188.226.xxx.xxx
      Database: niujork    Status: active  Conn: psql -U root -d test -h 107.170.xx.xx
      
      

    • Przykład dodania tabeli

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add table test1 db=amsterdam
      Added the following tables or sequences:
        public.test1
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add table test1 db=niujork
      Added the following tables or sequences:
        public.test1
      
      

    • Przykład usuwania tabel

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo remove table test1
      Please use the full schema.table name
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo remove table public.test1
      Removed the following tables:
        public.test1
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo list tables
      No tables have been added yet
      
      

    • Przykład stworzenia synchronizacji

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add sync test1_sync dbs=niujork,amsterdam tables=test1 conflict_strategy=bucardo_source 
      WARNING:  Issuing rollback() due to DESTROY without explicit disconnect() of DBD::Pg::db handle dbname=test;host=107.170.39.26 at line 279.
      KONTEKST:  PL/Perl function "validate_sync"
      SQL statement "SELECT validate_sync('test1_sync')"
      PL/Perl function "validate_sync"
      Failed to add sync: DBD::Pg::st execute failed: ERROR:  DBD::Pg::db do failed: ERROR:  permission denied for database test at line 280. at line 30.
      KONTEKST:  PL/Perl function "validate_sync" at /usr/local/bin/bucardo line 4413.
      
      

      W bazach musi być zainstalowany język plperl:

      test=> CREATE EXTENSION plperl;
      CREATE EXTENSION
      test=> \dL
                               Lista języków
        Nazwa  | Właściciel | Zaufany |             Opis             
      ---------+------------+---------+------------------------------
       plperl  | root       | t       | PL/Perl procedural language
       plpgsql | postgres   | t       | PL/pgSQL procedural language
      (2 wiersze)
      
      test=> \q
      
      

      Stworzenie synchronizacji typu master-slave (pierwsza baza jest typu 'source', a druga (albo po prostu każde następne) 'target'):

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add sync test1_sync dbs=niujork,amsterdam tables=test1 conflict_strategy=bucardo_source 
      Added sync "test1_sync"
      Created a new relgroup named "test1_sync"
      Created a new dbgroup named "test1_sync"
      
      

      Stworzenie synchronizacji typu master-master (jawnie definiujemy typ bazy danych):

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add sync test1_sync dbs=niujork:source,amsterdam:source tables=test1 conflict_strategy=bucardo_source
      Added sync "test1_sync"
      Created a new relgroup named "test1_sync"
      Created a new dbgroup named "test1_sync_2"
      
      

    • Status synchronizacji

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo status
      PID of Bucardo MCP: 22761
       Name         State    Last good    Time      Last I/D    Last bad    Time  
      ============+========+============+=========+===========+===========+=======
       test1_sync | Good   | 11:06:44   | 24m 32s | 0/373     | none      |       
      
      

      Szczegółowe informacje o synchronizacji:

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo status test1_sync
      ======================================================================
      Last good                : Aug 12, 2014 11:06:43 (time to run: 2s)
      Rows deleted/inserted    : 0 / 373
      Sync name                : test1_sync
      Current state            : Good
      Source relgroup/database : test1_sync / amsterdam
      Tables in sync           : 1
      Status                   : Active
      Check time               : None
      Overdue time             : 00:00:00
      Expired time             : 00:00:00
      Stayalive/Kidsalive      : Yes / Yes
      Rebuild index            : No
      Autokick                 : Yes
      Onetimecopy              : No
      Post-copy analyze        : Yes
      Last error:              : 
      ======================================================================
      
      

Każda tabela w synchronizacji ma nałożone na sobie triggery. Oto przykładowa tabela:

test=# \d test1
                                 Tabela "public.test1"
 Kolumna |          Typ          |                     Modyfikatory                     
---------+-----------------------+------------------------------------------------------
 c1      | integer               | niepusty domyślnie nextval('test1_c1_seq'::regclass)
 c2      | character varying(20) | 
 c3      | integer               | 
Indeksy:
    "test1_pkey" PRIMARY KEY, btree (c1)
Wyzwalacze:
    bucardo_delta AFTER INSERT OR DELETE OR UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE bucardo.delta_public_test1()
    bucardo_kick_test1_sync AFTER INSERT OR DELETE OR UPDATE OR TRUNCATE ON test1 FOR EACH STATEMENT EXECUTE PROCEDURE bucardo.bucardo_kick_test1_sync()
    bucardo_note_trunc_test1_sync AFTER TRUNCATE ON test1 FOR EACH STATEMENT EXECUTE PROCEDURE bucardo.bucardo_note_truncation('test1_sync')


Triggery:
  • bucardo.delta_public_test1 - wywołuje inserty z kluczem głównym 'zmienionego' wiersza do tabeli trakującej delta_public_test1

    BEGIN 
     IF (TG_OP = 'INSERT') THEN 
       INSERT INTO bucardo.delta_public_test1 VALUES (NEW."c1");
     ELSIF (TG_OP = 'UPDATE') THEN
       INSERT INTO bucardo.delta_public_test1 VALUES (OLD."c1");
       IF (OLD."c1" <> NEW."c1") THEN 
         INSERT INTO bucardo.delta_public_test1 VALUES (NEW."c1");
       END IF; 
     ELSE 
       INSERT INTO bucardo.delta_public_test1 VALUES (OLD."c1"); 
     END IF; 
     RETURN NULL;
     END;
    
  • bucardo.bucardo_kick_test1_sync - wywołanie NOTIFY do bucardo informując, że doszło do zmian.

    BEGIN
       EXECUTE $nn$NOTIFY bucardo, 'kick_sync_test1_sync'$nn$;
     RETURN NEW; 
    END;
    
  • bucardo.bucardo_note_truncation - wywołuje insert do tabeli trakującej, informując o wykonaniu polecenia TRUCATE. Czyści także tabele trakujące zmiany

Od wersji 5.0 dane zanim będą przeniesione do docelowego serwera, są usuwane (przy conflict_stategy = 'bucardo_source') przy pomocy klucza głównego, aby za pobiedź konfliktom danych. W wcześniejszej wersji były najpierw sprawdzane pojedynczo, co powodowało obciążenie serwera i bardzo duże opóźnienia.

DELETE FROM public.test1 WHERE c1 = ANY('{{"5"}}')

Przenoszenie danych:

COPY (SELECT * FROM public.test1 WHERE c1 IN ('6')) TO STDOUT

COPY public.test1("c1","c2","c3") FROM STDIN

Zdecydowanie polecam obecną wersje tego programu do replikacji danych zwłaszcza z powodu lepszej wydajności. Jeśli jednak potrzebujemy tylko replikacji typu master-slave wybrała bym wbudowaną replikację PostgreSQLa bo działa na poziomie plików binarnych i nie obciąża serwera dodatkowymi zapytaniami.

poniedziałek, 11 sierpnia 2014

Partycjonowanie w MySQL v5.5


Jakiś czas temu przyszło mi się zmierzyć z problemem optymalizacji zapytań na bardzo dużej ilości danych w tabeli. Tabela, o której pisze, ma wielkość około 40GB i 80 milionów rekordów. Oto jej schemat:

CREATE TABLE `service_numbers` (
  `template_id` bigint(20) NOT NULL,
  `template_info_id` bigint(20) NOT NULL,
  `mobile_number` varchar(255) DEFAULT NULL,
  `appeared_at` timestamp NULL DEFAULT NULL,
  `verified_at` timestamp NULL DEFAULT NULL,
  `alias_name` varchar(255) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `fake_id` int(11) NOT NULL AUTO_INCREMENT,
  `external_operator_id` int(11) DEFAULT NULL,
  `areacode` varchar(3) DEFAULT NULL,
  PRIMARY KEY (`fake_id`),
  KEY `idx_mn_ti_tii` (`mobile_number`(191),`template_id`,`template_info_id`),
  KEY `sn_by_template_then_appeared` (`template_id`,`template_info_id`,`appeared_at`),
  KEY `sn_by_template_then_verified` (`template_id`,`template_info_id`,`verified_at`),
  KEY `sn_by_tmeplate_alias_appeared` (`template_id`,`alias_name`(191),`template_info_id`,`appeared_at`),
  KEY `ssn_tid_arec_tiid_vat_idx` (`template_id`,`areacode`,`template_info_id`,`verified_at`),
  KEY `ssn_tid_oid_tiid_vat_idx` (`template_id`,`external_operator_id`,`template_info_id`,`verified_at`),
  KEY `idx_tmeplate_alias_verified` (`template_id`,`alias_name`(64),`template_info_id`,`verified_at`)
) ENGINE=InnoDB AUTO_INCREMENT=184473055 DEFAULT CHARSET=utf8mb4;

Zapytanie, które wymaga optymalizacji ma następującą konstrukcję:

select count(*) from service_numbers where template_id = ID1 and template_info_id = ID2 and verified_at < 'DATE';


Może przyjmować różnego rodzaju dodatkowe warunki, dlatego dla każdego rodzaju zapytania dodano indexy. Zawsze jednak głównym elementem jest tutaj kolumna template_id. Aby pomóc przyspieszyć wykonywanie tych zapytań, dobrze jest usunąć historyczne dane, aby zmniejszyć ich ilość. Jednak ze względu na specyfikę wykorzystania tych danych, nie jest to proste. Ale to w tym momencie pomijam. Tabela została uszczuplona na tyle na ile się da, ale nadal jest ona ogromna. Ale co jeśli wykorzystamy partycjonowanie?

Co to jest partycjonowanie?

Partycjonowanie jest fizycznym dzieleniem tabeli na części, do których jednak nie masz bezpośredniego dostępu (nie ma możliwości wykonania SELECT'a tylko na wskazanej partycji, ale możesz np. wskazać, którą partycję wyczyścić). Każda partycja jest zarządzana przez engine bazodanowy, ma własne indexy, które zostały zdefiniowane na tabeli.

Ważne informacje o partycjonowaniu w MySQLu


Najpierw sprawdźmy, czy nasz serwer wspiera partycjonowanie - oto jeden ze sposobów:

mysql (root@partition_test)> SHOW VARIABLES LIKE '%partition%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| have_partitioning | YES   |
+-------------------+-------+
1 row in set (0.00 sec)


Jak traktowane są operacji bazodanowych:
  • SELECT - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje. Optymalizator decyduje, które z partycji są ignorowane, bo warunki selektu ich nie dotyczą. Wtedy warstwa partycjonowania wywołuje API do silnika bazodanowego, który zarządza partycjami.

    mysql (root@partition_test)> explain partitions select * from test1;
    +----+-------------+-------+-------------+------+---------------+------+---------+------+------+-------+
    | id | select_type | table | partitions  | type | possible_keys | key  | key_len | ref  | rows | Extra |
    +----+-------------+-------+-------------+------+---------------+------+---------+------+------+-------+
    |  1 | SIMPLE      | test1 | p0,p1,p2,p3 | ALL  | NULL          | NULL | NULL    | NULL |    4 |       |
    +----+-------------+-------+-------------+------+---------------+------+---------+------+------+-------+
    1 row in set (0.00 sec)
    
    mysql (root@partition_test)> explain partitions select * from test1 where tid = 1;
    +----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+-------------+
    | id | select_type | table | partitions | type | possible_keys  | key            | key_len | ref   | rows | Extra       |
    +----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+-------------+
    |  1 | SIMPLE      | test1 | p0         | ref  | idx_tid_tnid_c | idx_tid_tnid_c | 5       | const |    1 | Using where |
    +----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+-------------+
    1 row in set (0.07 sec)
    
    

  • INSERT - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje, a następnie określa, która partycja ma otrzymać nowy wiersz. Później przekazuje ten wiersz do tej partycji.
  • DELETE - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje. Określa gdzie znajduje się wiersz i wysyła żądanie jego usunięcia do tej partycji.
  • UPDATE - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje aby określić gdzie znajduje się szukany wiersz. Jest on pobierany i modyfikowany. Określane jest gdzie po modyfikacji ma zostać zlokalizowany 'nowy' wiersz, następnie jest tam wysyłany. Do partycji źródłowej wiersza jest wysyłane żądanie usunięcia.
Oczywiście operacje te mogą być wspierane przez mechanizm nazywany pruning. Czyli w momencie gdy wykonywana jest operacja, tylko kilku lub jednej partycji, to na reszcie z nich jest ignorowana lock.

W jakich przypadkach najlepiej używać partycjonowania?
  • tabela jest tak duża, że nie mieści się już pamięci lub gdy najważniejsze dane są samym końcu tabelki, a sama tabela posiada bardzo dużo starych danych
  • utrzymanie tabel partycjonowanych jest o wiele łatwiejsze np. przy usuwaniu danych historycznych i odzyskiwaniu wolnej przestrzeni dyskowej
  • może być użyte przy zapobieganiu niektórych specyficznych obciążeniach, takich jak muteksy na indexach InnoDB
  • możliwy jest backup i ponowne załadowanie pojedynczej partycji dla bardzo dużej ilości danych
  • partycje mogą być rozłożone fizycznie na różnych dyskach
Ograniczenia:
  • maksymalna ilość partycji to 1,024 dla pojedynczej tabeli
  • dla MySQL v5.1 wyrażenie potrzebne do partycjonowania danych musi zwrócić wartość typu integer. Od wersji 5.5 możemy partycjonować także poprzez kolumny, które nie są tego typu
  • każdy klucz główny i index unikalny musi uwzględniać wszystkie kolumny w wyrażeniu partycjonującym
  • nie ma możliwości użycia kluczy obcych
  • lista funkcji, których można używać do wyrażenia partycjonującego jest dostępna pod tą stroną http://dev.mysql.com/doc/refman/5.5/en/partitioning-limitations-functions.html

Typy partycjonowania:
  • RANGE - dla każdej partycji zdefiniowane jest wyrażenie, które zwraca wartość typu integer:

    mysql (root@partition_test)> create table test_range (`id` int(11) NOT NULL, `ms` varchar(255) DEFAULT NULL, `tid` int(11) DEFAULT NULL, `tnid` int(11) DEFAULT NULL, `c` varchar(255) DEFAULT NULL) PARTITION BY RANGE (tnid) (PARTITION p0 VALUES LESS THAN (2), PARTITION p1 VALUES LESS THAN (3), PARTITION p2 VALUES LESS THAN (9), PARTITION p3 VALUES LESS THAN (10), PARTITION p4 VALUES LESS THAN MAXVALUE);
    Query OK, 0 rows affected (0.96 sec)
    
    mysql (root@partition_test)> show create table test_range \G
    *************************** 1. row ***************************
           Table: test_range
    Create Table: CREATE TABLE `test_range` (
      `id` int(11) NOT NULL,
      `ms` varchar(255) DEFAULT NULL,
      `tid` int(11) DEFAULT NULL,
      `tnid` int(11) DEFAULT NULL,
      `c` varchar(255) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1
    /*!50100 PARTITION BY RANGE (tnid)
    (PARTITION p0 VALUES LESS THAN (2) ENGINE = InnoDB,
     PARTITION p1 VALUES LESS THAN (3) ENGINE = InnoDB,
     PARTITION p2 VALUES LESS THAN (9) ENGINE = InnoDB,
     PARTITION p3 VALUES LESS THAN (10) ENGINE = InnoDB,
     PARTITION p4 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
    1 row in set (0.00 sec)
    
    
    W powyższym przykładzie, w partycji p0 będę umieszczone wszystkie wiersze gdzie wartość w kolumnie tnid jest mniejsze od 2, a w partycji p1 wszystkie wiersze gdzie kolumna tnid przyjmuje wartości równe 2 i mniejsze od 3.

  • LIST - jest bardzo podobne do powyższego sposobu partycjonowania - każda partycja musi być zdefiniowana. Różnica wynika z tego, że wyrażenie definiujące partycję jest listą wartości typu integer. Powyższą tabelę możemy zdefiniować następująco:

    mysql (root@partition_test)> create table test_list (`id` int(11) NOT NULL, `ms` varchar(255) DEFAULT NULL, `tid` int(11) DEFAULT NULL, `tnid` int(11) DEFAULT NULL, `c` varchar(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 PARTITION BY LIST (tnid) (PARTITION p0 VALUES IN (0,1), PARTITION p1 VALUES IN (2), PARTITION p2 VALUES IN (3,4,5,6,7,8), PARTITION p3 VALUES IN (9), PARTITION p4 VALUES IN (10,11,12,13));
    Query OK, 0 rows affected (0.56 sec)
    
    mysql (root@partition_test)> show create table test_list \G
    *************************** 1. row ***************************
           Table: test_list
    Create Table: CREATE TABLE `test_list` (
      `id` int(11) NOT NULL,
      `ms` varchar(255) DEFAULT NULL,
      `tid` int(11) DEFAULT NULL,
      `tnid` int(11) DEFAULT NULL,
      `c` varchar(255) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1
    /*!50100 PARTITION BY LIST (tnid)
    (PARTITION p0 VALUES IN (0,1) ENGINE = InnoDB,
     PARTITION p1 VALUES IN (2) ENGINE = InnoDB,
     PARTITION p2 VALUES IN (3,4,5,6,7,8) ENGINE = InnoDB,
     PARTITION p3 VALUES IN (9) ENGINE = InnoDB,
     PARTITION p4 VALUES IN (10,11,12,13) ENGINE = InnoDB) */
    1 row in set (0.00 sec)
    
    
    Znając strukturę danych jesteśmy świadomi jakie dane będą przechowywane w kolumnie tnid (Wartości typu integer od 0 do 13 - nie ma możliwości aby wstawić wiersz, dla którego kolumn tnid będzie przyjmować inne wartość, a także i NULL. Czytaj dalej.).
  • COLUMNS - jest nowym typem partycjonowania od wersji 5.5. Może być stosowana nie tylko dla kolumn typu integer. Wyróżniamy dwa sposoby podziału: RANGE lub LIST aby zwrócić wartości przedziałów lub listy. Dodatkowo w porównaniu do swoich podstawowych typów partycjonowania (COLUMN RANGE -> RANGE i COLUMN LIST -> LIST) możliwe jest zdefiniowanie podziału tabeli przez wiele kolumn. Link do dokumentacji: http://dev.mysql.com/doc/refman/5.5/en/partitioning-columns.html

  • HASH - używane głównie do równomiernego rozkładania wierszy w partycji między z góry określoną liczbą ścieżek. Wyrażenie definiująca partycję musi zwracać wartość typu integer. Przykład definicji tak partycjonowanej tabeli:

    CREATE TABLE t1 (c1 INT, c2 VARCHAR(255), c3 TIMESTAMP)
        PARTITION BY HASH( MONTH(c3))
        PARTITIONS 4;
    
    

    Tabela t1 będzie miała 4 partycje gdzie dane będą rozlokowane przez następujący wzór: MOD(MONTH(c3),4)
  • KEY - bardzo podobna metoda do HASH, ale tutaj to MySQL narzuca wyrażenie partycjonujące przy pomocy funkcji hashującej.
  • Subpartitiong (composite partitiong czyli złożone partycjonowanie) - jest dalszym podziałem partycji na jeszcze mniejsze partycje. Zainteresowanych odsyłam do dokumentacji: http://dev.mysql.com/doc/refman/5.5/en/partitioning-subpartitions.html
  • Wartość NULL nie są wartościami typu integer (http://db-diary.blogspot.com/2013/12/wartosc-null-w-bazach-danych.html). Każdy tym partycjonowania może inaczej traktować wartości NULL i niektóre z nich muszą być specjalnie przygotowane na tą ewentualność wartości danych.
    Dla partycjonowania typu RANGE wiersz jest ustawiany w pierwszej (najniżej) partycji. W ten sam sposób zostanie potraktowany wiersz dla partycji zdefiniowanej przy pomocy wyrażenia z funkcją SQL. Dla typu LIST partycja musi mieć zdefiniowaną wartość NULL aby można było wstawić do niej wiersz (UWAGA: ta sama zasada odnosi się także do innych 'poprawny' wartości). Dla powyższej tabeli test_list spróbujemy wstawić wiersz, którego wartość nie istnieje w liście wartości partycjonujących:

    mysql (root@partition_test)> insert into test_list (id, ms, tid, tnid, c) values (1, '11111', 22, 14, 'sfsfdfa');
    ERROR 1526 (HY000): Table has no partition for value 14
    mysql (root@partition_test)> insert into test_list (id, ms, tid, tnid, c) values (1, '11111', 22, NULL, 'sfsfdfa');
    ERROR 1526 (HY000): Table has no partition for value NULL
    
    
Teraz jak już wiemy mniej więcej na czym partycjonowanie polega, porównajmy czasy wykonania kilku zapytań dla przedstawionej na samym początku tabeli service_numbers (baza danych: test) i nowej tabeli service_numbers (baza danych: test2), która została przeze mnie partycjonowania:

mysql (root@test2)> show create table service_numbers \G
*************************** 1. row ***************************
       Table: service_numbers
Create Table: CREATE TABLE `service_numbers` (
  `template_id` bigint(20) NOT NULL,
  `template_info_id` bigint(20) NOT NULL,
  `mobile_number` varchar(255) DEFAULT NULL,
  `appeared_at` timestamp NULL DEFAULT NULL,
  `verified_at` timestamp NULL DEFAULT NULL,
  `alias_name` varchar(255) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `fake_id` int(11) NOT NULL AUTO_INCREMENT,
  `external_operator_id` int(11) DEFAULT NULL,
  `areacode` varchar(3) DEFAULT NULL,
  KEY `idx_mn_ti_tii` (`mobile_number`(191),`template_id`,`template_info_id`),
  KEY `sn_by_template_then_appeared` (`template_id`,`template_info_id`,`appeared_at`),
  KEY `sn_by_template_then_verified` (`template_id`,`template_info_id`,`verified_at`),
  KEY `sn_by_tmeplate_alias_appeared` (`template_id`,`alias_name`(191),`template_info_id`,`appeared_at`),
  KEY `ssn_tid_arec_tiid_vat_idx` (`template_id`,`areacode`,`template_info_id`,`verified_at`),
  KEY `ssn_tid_oid_tiid_vat_idx` (`template_id`,`external_operator_id`,`template_info_id`,`verified_at`),
  KEY `idx_tmeplate_alias_verified` (`template_id`,`alias_name`(64),`template_info_id`,`verified_at`),
  KEY `idx_fake_id` (`fake_id`)
) ENGINE=InnoDB AUTO_INCREMENT=184435432 DEFAULT CHARSET=utf8mb4
/*!50100 PARTITION BY KEY (template_id)
PARTITIONS 100 */
1 row in set (0.00 sec)

Mała uwaga do zmienionej struktury tabeli: Pamiętajmy o ograniczeniach przy zmianie schematu tabeli, gdy mamy indexy unikalne lub klucz główny. W moim przypadku musiałam usunąć klucz główny i zamienić go na index, a kolumnę, na której działał zmodyfikować. Te operację wymagają czasu i przestrzeni dyskowej, dlatego lepiej dokonywać zmiany schematu tabeli na pustej tabeli (przed rozpoczęciem oczywiście exportujemy dane do pliku):

truncate table service_numbers;
alter table service_numbers MODIFY fake_id int(11) NOT NULL;
alter table service_numbers drop primary key;
alter table service_numbers add index idx_fake_id (fake_id);
alter table service_numbers MODIFY fake_id int(11) NOT NULL auto_increment;
alter table service_numbers PARTITION BY KEY(template_id) PARTITIONS 100;


Testujemy nasze partycjonowanie

Przykładowe zapytanie na nie zmienionym schemacie tabeli:

mysql (root@test)> select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01';
+----------+
| count(*) |
+----------+
|   608136 |
+----------+
1 row in set (11.32 sec)


mysql (root@test)> explain partitions select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: service_numbers
   partitions: NULL
         type: ref
possible_keys: sn_by_template_then_appeared,sn_by_template_then_verified,sn_by_tmeplate_alias_appeared,ssn_tid_arec_tiid_vat_idx,ssn_tid_oid_tiid_vat_idx,idx_tmeplate_alias_verified
          key: ssn_tid_oid_tiid_vat_idx
      key_len: 8
          ref: const
         rows: 1227206
        Extra: Using where; Using index
1 row in set (0.00 sec)


Nie wiem czy zwróciliście uwagę, ale optymalizator wybrał 'gorszy' index do wykonania tego zapytania. Poprawmy jego statystyki i wykonajmy explain ponownie:
mysql (root@test)> analyze table service_numbers;
+----------------------+---------+----------+----------+
| Table                | Op      | Msg_type | Msg_text |
+----------------------+---------+----------+----------+
| test.service_numbers | analyze | status   | OK       |
+----------------------+---------+----------+----------+
1 row in set (1.39 sec)

mysql (root@test)> explain partitions select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: service_numbers
   partitions: NULL
         type: range
possible_keys: sn_by_template_then_appeared,sn_by_template_then_verified,sn_by_tmeplate_alias_appeared,ssn_tid_arec_tiid_vat_idx,ssn_tid_oid_tiid_vat_idx,idx_tmeplate_alias_verified
          key: sn_by_template_then_verified
      key_len: 21
          ref: NULL
         rows: 922120
        Extra: Using where; Using index
1 row in set (0.02 sec)


Tym razem jednak było znacznie lepiej. I czas wykonania zapytania jest lepszy:
mysql (root@test)> select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01';
+----------+
| count(*) |
+----------+
|   608136 |
+----------+
1 row in set (3.96 sec)


To samo zapytanie wykonajmy na tabeli ze zmienionym schematem:

mysql (root@test2)> select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01';
+----------+
| count(*) |
+----------+
|   608136 |
+----------+
1 row in set (2.26 sec)


mysql (root@test2)> explain partitions select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: service_numbers
   partitions: p3
         type: range
possible_keys: sn_by_template_then_appeared,sn_by_template_then_verified,sn_by_tmeplate_alias_appeared,ssn_tid_arec_tiid_vat_idx,ssn_tid_oid_tiid_vat_idx,idx_tmeplate_alias_verified
          key: sn_by_template_then_verified
      key_len: 21
          ref: NULL
         rows: 454688
        Extra: Using where; Using index
1 row in set (0.00 sec)


Ilość wierszy, które optymalizator musi odczytać jest o wiele mniejsza, więc i zapytanie trwa o wiele krócej.

To chyba jednak mało wiarogodny test na lepsze działanie. Dlatego przygotowałam plik z ponad 600 zapytaniami, które na serwerze produkcyjnym złapały się do slow logów. Mój serwer testowy miał włączone slow logi, które teraz chce wam przedstawić (Wykresy zostały wygenerowane przy pomocy narzędzia: mysql-slow-query-log-visualizer, jeśli ktoś jest zainteresowany).

Czas trwania wykonywania wszystkich zapytań dla schematu niepartycjonowanego:


a dla schematu partycjonowanego:



Podsumowanie tego testu:

Niepartycjonowany schemat tabeliPartycjonowany schemat tabeli
Ilość zapytań 'złapanych' w slow logach:632429
Czas działania testu:+ 5 godz+ 1 godz
Czas najdłużej wykonanego zapytania:399 sekundy31 sekundy
Czas wykonania zapytania, które musiało 'przeczyta' największą ilość wierszy (2662877):85 sekundy25 sekundy
Dodatkowe informacje:Optymalizator bierze pod uwagę gorsze indexy-



No to tyle. Dla mnie okazało się to wybawieniem i w końcu rozwiązaniem problemu optymalizacji działania zapytań na tak dużej tabeli.

poniedziałek, 14 lipca 2014

Asynchroniczne powiadomienia w PostgreSQLu czyli NOTIFY i LISTEN

Wiedzieliście, że PostgreSQL oferuje mechanizm wysyłki asynchronicznych notyfikacji? Aż głupio przyznać, ale ja się dowiedziałam o tym przy okazji poznawania narzędzia do master-master replikacji - Bucardo, ale o tym narzędziu kiedy indziej.
Notyfikacje są wykorzystywane w 'powiadomieniach' o zmianach na tabelach UPDATE/INSERT/DELETE/TRUNCATE. Dzięki temu inne procesy, których zadaniem jest sprawdzanie i np. przenoszenie zmian, nie muszą wykonywać czasem bardzo obciążających zapytań. W zależności od pojawienia się nowych notyfikacji, sprawdzają, przenoszą lub dokonują innych zmian.

Przykład wywołania notyfikacji poprzez trigger:

test=# \sf bucardo.bucardo_triggerkick_mysync
CREATE OR REPLACE FUNCTION bucardo.bucardo_triggerkick_mysync()
 RETURNS trigger
 LANGUAGE plpgsql
 SECURITY DEFINER
AS $function$
BEGIN
 EXECUTE 'NOTIFY "bucardo_kick_sync_mysync"';
 IF TG_OP = 'TRUNCATE' THEN
   INSERT INTO bucardo.bucardo_truncate_trigger(tablename,sname,tname,sync)
   VALUES (TG_RELID, TG_TABLE_SCHEMA, TG_TABLE_NAME, 'mysync');
 END IF;
 RETURN NEW;
END;
$function$



Aby wysyłać i odpytywać o notyfikację używamy poleceń:
  • LISTEN channel_name - rejestrowanie bieżącej sesji do nasłuchiwanie na kanale o nazwie channel_name.
  • UNLISTEN { channel_name | * } - przestanie nasłuchiwania na wybrany kanale (channel_name) lub na wszystkie, na których sesja nasłuchiwała (*)
  • NOTIFY channel_name [, payload ] - wysyła notyfikację wraz z opcjonalnym parametrem playload, który jest stringiem o ograniczonej długości. Notyfikacje są widoczne dla wszystkich użytkowników, którzy nasłuchują.Dla standardowej konfiguracji payload musi być krótszy niż 8000 bajtów. Link do dokumentacji: http://www.postgresql.org/docs/current/static/sql-notify.html

Przykład prostego użycia w konsoli. Obydwa procesy nasłuchują na ten sam kanał. Pierwszy wysyła notyfikacja do wszystkich użytkowników:

root@test # notify test, 'cos tam';
NOTIFY
Time: 111,433 ms
Asynchronous notification "test" received from server process with PID 7031.

Drugi proces został poinformowany o notyfikacji:


root@test # select 1;
+----------+
| ?column? |
+----------+
|        1 |
+----------+
(1 row)

Time: 108,875 ms
Asynchronous notification "test" received from server process with PID 7031.



Cechy mechanizmu notyfikacji:
  • Wiele backend procesów nasłuchuje na jednej maszynie z możliwością nasłuchiwania na wielu kanałach.
  • Jest jedna centralna kolejka (FIFO) w głównym klastrze - katalog pg_notify, z odwzorowaniem aktywnie używanych stron w pamięci współdzielonej. Wszystkie notyfikacje są trzymane w tej kolejce i później odczytywane przez nasłuchujące backend procesy.
  • Nie ma centralnej informacji na temat kto nasłuchuje na którym kanale - każdy backend proces ma swoją listę nasłuchiwanych kanałów.
  • Notyfikacje są przypisywane do lokalnych baz danych czyli dwa backend procesy podłączone do różnych baz danych i nasłuchujące na kanałach o tych samych identyfikatorach nie będą się ze sobą komunikować - każda notyfikacja posiada OID bazy danych (ponieważ bazy mogą mieć różne kodowanie).
  • Notyfikacje nie zostaną zachowywane przy zamknięciu serwera (crash, restart).
  • Każdy backend proces, który nasłuchuje na przynajmniej jednym kanale, rejestruje swój PID w tablicy w AsyncQueueControl. Wtedy skanuje wszystkie przychodzące notyfikacje w centralnej kolejce i wpierw porównuje OID bazy danych swojego z OIDem bazy danych notyfikacji. Następnie porównuje kanał notyfikacji z kanałami na swojej liście nasłuchiwania. W ten sposób, gdy notyfikacja się zgodzi, jest zwracana na frontend procesu. Inne notyfikacje są pomijane. 
  • Polecenie NOTIFY przechowuje notyfikacje w liście backend procesu, które nie są procesowane do momentu gdy transakcja nie zostanie zakończona. Zduplikowane notyfikacje z tej samej transakcji są wysyłane jako jedna. Kiedy transakcja jest gotowa do zatwierdzenia, do głowy kolejki wysyłana jest oczekująca notyfikacja. Pozycja głowy kolejki wskazuje następną wolną pozycję i pozycja jest numerem strony i jej przesunięciem na stronie. Jest to wykonywane przed zatwierdzeniem transakcji w clog.
  • Ilość pamięci współdzielonej używanej przy załączaniu notyfikacjami może być zmienna (NUM_ASYNC_BUFFERS) i nie wypływa na nic po za wydajnością. Przy standardowej instalacji wielkość kolejki wynosi 8GB.


Przykład programu, który wysyła powiadomienia i drugiego, który nasłuchuje (napisane w Perlu):

cat send_notification.pl
#!/usr/bin/perl -w

use strict;
use warnings;
use DBI;

my $db_con = "dbi:Pg:dbname=test;host=127.0.0.1";
my $db_user = "root";
my $db_pass = "root";
my $db_attr = {RaiseError => 1, AutoCommit => 1};
my $queue_name = "test_listener";
my $iteration = 0;

my $db_handler = DBI->connect($db_con, $db_user, $db_pass, $db_attr);

$db_handler->do(qq{LISTEN $queue_name});

while (1) {
    $iteration += 1;
    my $message = qq{NOTIFY $queue_name, 'message number $iteration'};
    $db_handler->do($message);
    print "Sending: $message\n";
    sleep 1.5;
}


Skrypt send_notification.pl co 1,5 sekundy wysyła notyfikację z nowym numerem na kanał test_listener:

perl send_notification.pl
Sending: NOTIFY test_listener, 'message number 1'
Sending: NOTIFY test_listener, 'message number 2'
Sending: NOTIFY test_listener, 'message number 3'
Sending: NOTIFY test_listener, 'message number 4'
Sending: NOTIFY test_listener, 'message number 5'
Sending: NOTIFY test_listener, 'message number 6'


A skrypt check_notification.pl co 1 sekundę (domyślnie, jeśli przy wywołaniu nie poda się inaczej) sprawdza czy nowe notyfikacji są:

cat check_notification.pl
#!/usr/bin/perl -w

use strict;
use warnings;
use DBI;

my $db_con = "dbi:Pg:dbname=test;host=127.0.0.1";
my $db_user = "root";
my $db_pass = "root";
my $db_attr = {RaiseError => 1, AutoCommit => 1};
my $queue_name = "test_listener";
my $time = $ARGV[0] || 1;

my $db_handler = DBI->connect($db_con, $db_user, $db_pass, $db_attr);

$db_handler->do(qq{LISTEN $queue_name});

while (1) {
    my $notify = $db_handler->func("pg_notifies");
    if ($notify) {
        my ($name, $pid, $payload) = @$notify;
        print "Reasived: $name, $pid, $payload \n"
    }
    sleep $time;
}


Wynik:

perl check_notification.pl
Reasived: test_listener, 19334, message number 1 
Reasived: test_listener, 19334, message number 2 
Reasived: test_listener, 19334, message number 3 
Reasived: test_listener, 19334, message number 4 
Reasived: test_listener, 19334, message number 5 
Reasived: test_listener, 19334, message number 6

Skrypt do nasłuchiwania został uruchomiony przed wywołaniem skryptu do wysyłki notyfikacji, dlatego w wyniku widzimy wszystkie notyfikacje, które zostały wysłane. Jeśli uruchomilibyśmy go dopiero po skrypcie do wysyłki, tylko notyfikację wysłane po rejestracji procesu były by do niego dostarczone.

czwartek, 12 czerwca 2014

Tworzenie replikacji primary-standby w PostgreSQLu

Przyszedł w końcu czas postawienie replikacji master-slave w PostgreSQLu. Ten post jest kontynuacją poprzedniego posta na temat wprowadzenia do replikacji, który możecie przeczytać tutaj. Przypominam, że ten system bazodanowy nie ma jeszcze replikacji master-master, ale jeśli by wam bardzo na tym zależało, to proponuje się zapoznać z projektem Bucardo, który daje taką możliwość.

Do postawienia PostgreSQLowej replikacji wykorzystam dwie maszyny:
  • Serwer A: zlokalizowany w Nowym Jorku, host: HOST-A
  • Serwer B: zlokalizowany w Amsterdamie, host: HOST-B

Zakładam, że serwer B będzie masterem, a serwer A slavem. Opisywałam już w poprzednich postach jak zainstalować serwer PostgreSQL dlatego zakładam, że spokojnie mogę pominąć ten krok. Jeśli jednak potrzebujecie pomocy w tych krokach, odsyłam do poprzednich postów gdzie opisuje instalację ze źródeł lub z paczki.

Konfigurujemy Master serwer (serwer B)

Na maszynie B działa już serwer z którego chcemy 'zrobić' mastera i dołączyć do niego tylko serwer standby. Dlatego zajmiemy się tylko konfiguracją. Zmieniamy główny plik konfiguracyjny PostgreSQLa postgresql.conf:

listen_addresses = '*'
wal_level = hot_standby 
archive_mode = on 
archive_command = 'cp %p /var/lib/pgsql/9.3/archive/%f'


#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------

# - Sending Server(s) -

# Set these on the master and on any standby that will send replication data.
max_wal_senders = 1
wal_keep_segments = 32
#wal_sender_timeout = 60s


Nie wszystkie z wymienionych opcji muszą być zmienione:
  • listen_addesses - z jakich adresów można się łączyć do serwera. Jeśli nie chcemy aby PostgreSQL słuchał na wszystkich adresach tak jak jest to na przykładzie ('*'), możemy wyszczególnić o jakie dokładnie adresy IP chodzi.
  • wal_level - wskazuje ile informacji ma być zapisywanych do plików WAL. minimal wskazuje na minimalna ilość informacji, która może tylko być pomocna przy procesie recovery po crashu serwera lub natychmiastowym zamknięciu. Nie pozwala jednak na rekonstrukcję danych. Do tego potrzebny jest poziom archive. Różnica między archive a hot_standby jest taka, że przy tym drugim zapisywane są dodatkowe informację potrzebne do zrekonstruowania statusu działającej transakcji z plików WAL. Poziom hot_standby jest wymagany aby działała Stream Replikacja.
  • archive_mode [NO/OFF] - Jeśli jest włączona, pliki WAL, które zostały w całości skompletowane, są wysyłane do archiwizacji przy użyciu polecenia zdefiniowanego przez archive_command. Ta opcja musi być włączona aby wal_level = archive lub hot_standby miał jakiś skutek.
  • archive_command - polecenie do archiwizacji plików WAL
  • max_wal_senders - definiuje maksymalną ilość połączeń z serwerów standby lub klientów programu do tworzenia base backupu. Domyśle ta liczba jest równa 0 co oznacza, że replikacja jest wyłączona.
  • wal_keep_segments - definiuje minimalną ilość starych plików logów segmentów trzymanych w katalogu pg_xlog
  • wal_sender_timeout - definiuje ile sekund ma upłynąć zanim zostanie zakończone połączenie replikacji, które jest nieaktywne. Ta opcja może być pomocna przy sprawdzaniu czy standby nie padł lub braku połączenia sieciowego. Przy wartości 0 ten mechanizm jest wyłączony.

Aby slave mógł pobierać dane z mastera, stworzymy użytkownika z prawami do logowania i replikacji lub musi to być superuser. (Nasz nowy user ma dostęp do strumieniowanych plików WAL czyli ma bardzo wysokie prawa dostępu ale nie ma możliwości dokonywania zmian na serwerze primary):

CREATE USER replication REPLICATION LOGIN CONNECTION LIMIT 1 ENCRYPTED PASSWORD 'replication';

Naszym celem jest stworzenie replikacji primary z jednym serwerem standby. Dlatego stworzony user ma ograniczenie do jednego połączenia na raz do serwera. Jeśli chcesz podłączyć więcej slavów do mastera, musisz zupdatować usera replication (zwiększając limit połączeń) lub stworzyć nowego usera.

Zmieniamy w pliku autoryzacji pg_hba.conf do serwera:

# Allow replication connections from localhost, by a user with the
# replication privilege.
host replication replication HOST-A/32 md5

Możemy uruchomić serwer.

Stawianie i konfiguracja serwera Standby (serwer A)

Ze względu na specyfikę działania replikacji w PostgreSQLu pliki danych binarnych muszą być takie same na serwerze Standby i Primary. Od wersji 9.1 dostępny jest i rekomendowany program pg_basebackup, który wykonuje binarną kopię plików klastra. Backup jest tworzony przy wykorzystaniu zwykłego połączenia do PostgreSQL i przy wykorzystaniu protokołu replikacji (Serwer primary musi mieć ustawiony max_wal_senders min = 1 aby połączenie doszło do skutku).

[root@niujork /home/postgres/psql9.3.4]$ mkdir data4
[root@niujork /home/postgres/psql9.3.4]$ chown -R postgres:postgres data4
[root@niujork /home/postgres/psql9.3.4]$ /usr/local/pgsql/bin/pg_basebackup -R -D /home/postgres/psql9.3.4/data4 --host=HOST-B -Ureplication -W
Password: 
NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
[root@niujork /home/postgres/psql9.3.4]$ chown -R postgres:postgres data4

Program utworzył w katalogu data4 całą nową strukturę klastra i dodał nowy plik recovery.conf (opcja -R) z ustawieniami do uruchomienia Streaming Replication. Opcja -D wskazuje katalog docelowy gdzie klaser ma zostać 'zainstalowany'. Więcej na temat tego programu w dokumentacji tutaj.

[root@niujork /home/postgres/psql9.3.4/data4]$ cat recovery.conf 
standby_mode = 'on'
primary_conninfo = 'user=replication password=replication host=HOST-B port=5432 sslmode=disable sslcompression=1'

To zmienna standby_mode = 'on' świadczy o tym, że serwer PostgreSQL działa jako standby. Jeśli zmienna jest równa 'off' wszystkie inne już nie są brane pod uwagę. Zmienna primary_conninfo trzyma informację na temat połączenia do mastera. Przypominam, że user którego użyjemy musi być seperuser-em lub użytkownikiem z prawami do replikacji. Dane zostały automatycznie wypełnione przy skopiowaniu klastra przez pg_basebackup.

Nasz nowy serwer ma taki sam plik konfiguracyjny jak jego master, z którego została wykonana kopia klastra (możemy go zmienić i dostosować do własnych potrzeb). Aby było możliwe querowanie (tylko read-only) na serwerze standby, w pliku konfiguracyjnym postgresql.conf musimy mieć włączoną opcję:

# - Standby Servers -
# These settings are ignored on a master server.
hot_standby = on 

  • hot_standby - [ON/OFF] - Jeśli serwer jest włączony jako standby (standby_mode = 'on') i hot_standby = 'on' jest możliwe podłączenie się do serwera i wykonywanie poleceń read-only. 


Sprawdzenie działania replikacji

PostgreSQL udostępnia kilka prostych funkcji do sprawdzania statusu replikacji:
  • pg_is_in_recovery - funkcja do sprawdzania czy serwer działa w trybie recovery lub czy jest slavem
  • pg_last_xlog_receive_location - funkcja zwraca ostatnią lokalizację logu transakcji otrzymaną i zsynchronizaowaną na dysku przez stream replikację 
  • pg_last_xlog_replay_location - funkcja zwraca ostatnią lokalizację logu transakcji powtórzoną w trakcie recovery. Jeśli serwer wystartował normalnie bez recovery, funkcja zwróci NULL.
  • pg_last_xact_replay_timestamp - funkcja zwraca timestamp ostatniej transakcji powtarzanej w trakcie recovery
  • pg_xlogfile_name_offset - funkcja zwraca nazwę i offset pliku WAL pytanej lokalizacji transakcji
Od wersji 9.1 dostępny jest także widok do sprawdzania na serwerze primary stanu replikacji:

postgres=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 31416
usesysid         | 16387
usename          | replication
application_name | walreceiver
client_addr      | HOST-A
client_hostname  | 
client_port      | 46924
backend_start    | 2014-06-11 05:53:51.971246-04
state            | streaming
sent_location    | 0/25008A08
write_location   | 0/25008A08
flush_location   | 0/25008A08
replay_location  | 0/25008A08
sync_priority    | 0
sync_state       | async

postgres=# SELECT * FROM pg_xlogfile_name_offset('0/25008A08');
        file_name         | file_offset 
--------------------------+-------------
 000000010000000000000025 |       35336
(1 row)



Tylko czy jesteśmy w stanie realnie sprawdzić jak bardzo nasz slave jest do tyłu w stosunku do mastera? Szukając dalej natknęłam się na ciekawy post (Monitoring Streaming Slave Lag Effectively) na temat monitoringu replikacji, a dokładniej sprawdzania opóźnienia serwera standby w stosunku do primary. Autor przedstawił w nim funkcję zwracającą w prosty sposób informację o slave podłączonym i jego opóźnieniu. Kod funkcji (musi być stworzona i uruchamiana przez superusera bo dane o które pytamy dotyczą nie tylko naszej sesji i użytkownika):

CREATE OR REPLACE FUNCTION streaming_slave_check() RETURNS TABLE (client_hostname text, client_addr inet, byte_lag float)
LANGUAGE SQL SECURITY DEFINER
AS $$
    SELECT
        client_hostname,
        client_addr,
        sent_offset - (replay_offset - (sent_xlog - replay_xlog) * 255 * 16 ^ 6 ) AS byte_lag
    FROM (
        SELECT
            client_hostname,
            client_addr,
            ('x' || lpad(split_part(sent_location,   '/', 1), 8, '0'))::bit(32)::bigint AS sent_xlog,
            ('x' || lpad(split_part(replay_location, '/', 1), 8, '0'))::bit(32)::bigint AS replay_xlog,
            ('x' || lpad(split_part(sent_location,   '/', 2), 8, '0'))::bit(32)::bigint AS sent_offset,
            ('x' || lpad(split_part(replay_location, '/', 2), 8, '0'))::bit(32)::bigint AS replay_offset
        FROM pg_stat_replication
    ) AS s; 
$$;

A to jego przykładowy wynik uruchomienia:

postgres=# select * from streaming_slave_check();
 client_hostname |   client_addr   | byte_lag 
-----------------+-----------------+----------
                 | HOST-A          |        0
(1 row)

Mam nadzieję, że ten krótki post będzie dobrym wstępem do dalszych prac nad replikacją w PostgreSQLu. Miłej zabawy.



środa, 11 czerwca 2014

PostgreSQL: wprowadzenie do replikacji


Pisałam już posta na temat instalacji PostgreSQLa ze źródeł oraz z paczki. Te informacje mogą się wam przydać aby postawić dwie maszyny, a następnie ustawić je tak aby działały w replikacji. Od wersji PostgreSQLa 9.0 mamy dostępną replikację primary-standby (nazywana także: master-slave). W Tym poście poopowiadam co nieco o teorii na ten temat, a w następnym przejdziemy do praktyki.

Podstawowym elementem przenoszenia danych między serwerami w PostgreSQLu są pliki WAL (Write-Ahead Logging), a ich wymiana nazywa jest Log Shipping. Dane w plikach WAL są zapisywane dopiero gdy transakcja została zacomitowane (czyli zakończyła się poprawnie i została zatwierdzona). Jest to proces asynchroniczny. Rozróżniamy File-Based Log Shipping, gdzie segmenty pliku WAL są przenoszone na raz oraz Record-Based Log Shipping, gdzie plik WAL jest strumieniowany.

Przyjrzymy się jednak historii powstawania replikacji w PostgreSQLu
  • PITR - Point-In-Time Recovery (PITR) - Dostępny od wersji 8.0. Nazywany jest także Incremental database backup, Online backup albo Archive backup. Logi transakcyjne są kopiowane i przechowywane, aż do momentu gdy są potrzebne. Jest głównie używany w diagnostyce i odzyskiwaniu danych po crashu głównego serwera.
  • Warm Standby - Dostępny od wersji 8.3. Logi transakcji są kopiowane do serwera standby i tak zatwierdzane jak tylko slave je otrzyma (log shipping). Serwer standby pracuje w trybie offline i nie może być używany do odczytu. Może jednak być uruchomiony do użycia w bardzo krótkim czasie.
  • Hot Standby - Dostępny od wersji 9.0. W porównaniu do Warm Standby, serwer standby jest dostępny do odczytu. Także wymaga file-based log shipping. Może być wykorzystywany także do pracy awaryjnej. Pliki WAL (o rozmiarach 16MB) mogą być przenoszone pomiędzy wiele serwerów poprzez sieć. Nie wymaga to podłączenia do bazy danych ale miejsca na dysku.
  • Streaming Replication - Dostępny od wersji 9.1. Zamiast kopiować logi plików, dla każdego slava otwierane jest połączenie do mastera poprzez połączenia TCP/IP (może spowodować to wzrost obciążenia na bazie master). Zostało to stworzone poprzez utworzenie na masterze i slave dwóch procesów: walsender i walreceiver, które przenoszą zmodyfikowane dane stron między portami sieci. Dzięki temu zmiany z głównej bazy są widoczne prawie natychmiast na serwerze standby. W normalnym działaniu Stream Replication nie wymaga log shipping, ale może być pomocna przy starcie.
Schemat działania Streaming Replikacji:



Hot Standby i Streaming Replication są nazywane także replikacją binarną, mimo iż są różne. Głównymi cechami replikacji binarnej w PostgreSQLu są:
  • replikacja całego klastra - nie ma możliwości wyróżnienia pojedynczej schematu/bazy danych/tabeli
  • możliwe jest uruchomienie load-balacu pomiędzy zapytaniami czytania/pisania między masterem a slavami
  • replikowane są także wszystkie zapytania DDL (Data Definition Langue) - zmiany w tabelach i indexach, tworzenie indexów i baz danych
  • nie jest możliwe replikacja pomiędzy różnymi wersjami PostgreSQL-a lub między różnymi platformami
  • nie ma możliwości utworzenia replikacji multi-master (tego typu architektura jest podobno (bo jeszcze nie testowałam) możliwa przy pomicy Bucardo)

Mam nadzieję, że rozumiecie główną idee i działanie replikacji tak abyśmy przeszli do praktyki.


poniedziałek, 9 czerwca 2014

Instalacja PostgreSQLa z RPMa

Instalowaliśmy już PostgreSQLa ze źródeł. Ale co jeśli jednak chcemy zainstalować go przy pomocy paczki RPM. Jest to pewnie dziecinnie proste bo w końcu paczki zostały zbudowane po to aby zrobić właściwie wszystko za nas. Sprawdźmy to.

Mój system operacyjny to Centos 64Bitowy i chce na nim zainstalować najnowszą wersję serwera 9.3. Lista paczek RPM jest dostępna tutaj. Ale właściwie to dlaczego nie zainstaluję serwera bezpośrednio z repozytorium yum-a w ten sposób?! :

yum install postgresql-server

Dla mojego systemu operacyjnego w ten sposób dostępna jest wersja 8.4, a nie ta której ja potrzebuję. Dostępną wersje w repozytorium RPM możecie sprawdzić sami:

[root@amsterdam ~]$ yum info postgresql-server
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
 * base: mirrors.supportex.net
 * extras: mirror.widexs.nl
 * updates: mirror.widexs.nl
Available Packages
Name        : postgresql-server
Arch        : x86_64
Version     : 8.4.20
Release     : 1.el6_5
Size        : 3.4 M
Repo        : updates
Summary     : The programs needed to create and run a PostgreSQL server
URL         : http://www.postgresql.org/
License     : PostgreSQL
Description : The postgresql-server package includes the programs needed to create
            : and run a PostgreSQL server, which will in turn allow you to create
            : and maintain PostgreSQL databases.  PostgreSQL is an advanced
            : Object-Relational database management system (DBMS) that supports
            : almost all SQL constructs (including transactions, subselects and
            : user-defined types and functions). You should install
            : postgresql-server if you want to create and maintain your own
            : PostgreSQL databases and/or your own PostgreSQL server. You also need
            : to install the postgresql package.

Ale przejdźmy do naszego głównego tematu. Proces instalacji i uruchomienia serwera składa się z kilku kroków:
  1. Instalujemy repozytorium RPM

    yum install http://yum.postgresql.org/9.3/redhat/rhel-6-x86_64/pgdg-centos93-9.3-1.noarch.rpm
    

    Skoro w głównym repozytorium jest dostępna tylko stara wersji serwera, musimy zainstalować nowe repozytorium. Lista dostępnych repozytoriów jest http://yum.postgresql.org/repopackages.php.

  2. Instalujemy i updatujemy paczkę:

    yum install postgresql93-server postgresql93-contrib

    Wybierzcie dobrze nazwę paczki aby zainstalować tą z naszą pożądaną wersją serwera. Jeśli nie jesteście pewni co do jej nazwy, znajdzie ją przy pomocy polecenia: yum list | grep pgdg93

  3. Inicjalizujemy bazę danych

    service postgresql-9.3 initdb
    

    Gdy już serwer został zainstalowany, musimy zainicjować klaser bazy danych (czyli katalog, w którym będą trzymane pliki). To tak jak byśmy wykonali polecenie:

    /usr/pgsql-9.3/bin/initdb -D /var/lib/pgsql/9.3/data
    

  4. Aktywujemy usługę

    chkconfig postgresql-9.3 on
    

    Aby sprawdzić, jakie poziomy uruchomienia są włączone, wykonaj komendę (trochę więcej o tym poleceniu tutaj):

    [root@amsterdam ~]$ chkconfig --list postgresql-9.3
    postgresql-9.3  0:wyłączone 1:wyłączone 2:włączone 3:włączone 4:włączone 5:włączone 6:wyłączone
    

  5. Uruchamiamy serwer

    service postgresql-9.3 start

    Aby sprawdzić czy serwer działa, możemy wykonać polecenie:

    [root@amsterdam ~]$ service postgresql-9.3 status
    postgresql-9.3 (pid  1321) is running...
    

  6. Tworzymy użytkownika i bazę danych

    sudo su postgres
    createdb test
    createuser --createdb --login --pwprompt --createrole root
    

Katalog główny z danymi został zainstalowany na ścieżce: /var/lib/pgsql/9.3/data/. Oczywiście możemy tam znaleźć pliki konfiguracyjne postgresql.conf i pg_hba.conf.

wtorek, 3 czerwca 2014

PGLoader v3

W ostatnim poście pisałam o narzędziu pgloader służący do importu danych do PostgreSQLa. Nowa wersja została przepisana w całości na język Common Lisp. Do zapisu i odczytu danych używane są dwa oddzielne wątki, które współdzielą pracę. To co się nie zmieniło to to, że do komunikacji PostgreSQL nadal używany jest protokołu COPY. Tutaj odsyłam do post na temat nowej wersji tego narzędzia.

Przykład instrukcji załadowania pliku CSV:

LOAD CSV  
   FROM '/root/pgloader_data.csv'
   INTO postgresql://root:password@188.226.251.1/pgloader?pgloader_test(c2,c3)
 
   WITH truncate,  
        skip header = 0,
        fields optionally enclosed by '"',  
        fields escaped by double-quote,  
        fields terminated by ','  
 
   SET client_encoding to 'utf8', 
 work_mem to '12MB', 
 standard_conforming_strings to 'on'
 
BEFORE LOAD DO  
    $$ drop table if exists pgloader_test; $$,  
    $$ create table pgloader_test (
        c1 serial,
        c2 varchar(20),  
        c3 integer
       );  
    $$; 


Ładujemy dane:

[root@niujork ~]$ pgloader  pgloader.load 
2014-06-03T14:54:53.046000Z LOG Starting pgloader, log system is ready.
2014-06-03T14:54:53.076000Z LOG Main logs in '/tmp/pgloader/pgloader.log'
2014-06-03T14:54:53.082000Z LOG Data errors in '/tmp/pgloader/'
2014-06-03T14:54:53.083000Z LOG Parsing commands from file #P"/root/pgloader.load"
2014-06-03T14:54:59.686000Z ERROR Database error 22P02: invalid input syntax for integer: "sldkfhg"
CONTEXT: COPY pgloader_test, line 2, column c3: "sldkfhg"


                    table name       read   imported     errors            time

                   before load          2          2          0          2.701s
------------------------------  ---------  ---------  ---------  --------------
                 pgloader_test     219175     219174          1         18.568s
------------------------------  ---------  ---------  ---------  --------------
------------------------------  ---------  ---------  ---------  --------------
             Total import time     219175     219174          1         21.269s


Ładujemy dane gdzie pierwsza komuna jest varchar, a druga integer. Dla niepoprawnych danych, narzędzie wyświetla błędy ale reszta danych zostanie zaimportowana poprawnie.

Innym dodatkowym atutem jest możliwość ładowania danych z innych systemów bazodanowych: MySQL, SQLite czy dBase, a także odczytywanie danych z plików zarchiwizowanych czy danych o stałej szerokości. Jeśli musicie zaimportować dane do PostgreSQL, to polecam to narzędzie. Może się wam bardzo przydać.

Zainteresowanych odsyłam do dokumentacji tutaj.


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.