pg_restore w Dockerze

Taka sytuacja: mamy bazę danych PostgreSQL działającą w Dockerze. Chcemy wgrać dane z kopii zapasowej, ale nie chcemy wrzucać pliku z bazą do kontenera postgresa. Jak to zrobić?

Możemy obok wystartować drugi kontener, w którym plik z bazą udostępnimy przez bind mount i w tym kontenerze uruchomimy pg_restore, który będzie wyrzucać dane na standardowe wyjście, które rurą przekierujemy do właściwego kontenera z bazą. Uff, brzmi skomplikowanie, ale samo polecenie mieści się w jednej linijce:

docker run -v "$(PWD)":/backup postgres:9.4 pg_restore --clean --no-owner --no-privileges /backup/dump.sql | docker-compose exec -T db psql -U dbuser -d dbname

Powyższy przykład zakłada, że kontener Postgresa został uruchomiony przez docker-compose, co najczęściej będzie miało miejsce na lokalnym środowisku developerskim. Jeśli projekt działa w roju (swarm) to polecenie można zmodifikować następująco:

docker run -v "$PWD":/backup postgres:9.4 pg_restore --clean --no-owner --no-privileges /backup/dump.sql | docker exec -i project_db.1.st5j3pz7rre153z7ficb59c9n psql -U dbuser -d dbname

AB testów wydajnościowych

Żeby przetestować wydajność aplikację webowej od A do Z trzeba się nieźle nagimnastykować. Jeśli jednak wystarczą powierzchowne testy, dajmy na to od A do B, to jest na to prosty sposób: ab czyli Apache Benchmark.

Apache Benchmark dobrze się sprawdzi np. w takich scenariuszach:

  • sprawdzenie czy refaktoring konkretnej końcówki API przyniósł wzrost wydajności,
  • testowanie różnych ustawień serwera,
  • obciążenie aplikacji celem sprawdzenia czy autoskalowanie działa.

W poniższych przykładach podaję tylko te argumenty ab, które są istotne dla danego przykładu. W prawdziwych zastosowaniach trzeba do nich dodać np. liczbę zapytań i liczbę równoczesnych zapytań.

Pomijanie loadbalancera

Obecnie serwery aplikacji są częstokroć schowane za loadbalancerem. Testując aplikację najczęściej chcemy uderzać bezpośrednio w nią. W tym celu należy wysłać nagłówek Host zawierający domenę aplikacji, a w URL-u podać serwer z aplikacją (domena lub IP):

ab -H "Host: api.example.org" http://10.1.1.10/endpoint

Autoryzacja

Tu również najczęściej wystarczy podać jeden nagłówek, a konkretnie Authorization:

ab -H "Authorization: Bearer wygenerowany-token" https://api.example.org/endpoint

Formularz

Żeby wysłać formularz metodą POST należy przygotować plik z zawartością formularza. Klucze i wartości powinny być rozdzielone znakiem równości, a kolejne krotki oddzielone znakiem &. Należy pamiętać o kodowaniu znaków niealfanumerycznych. Przykład:

parametr1=wartosc&parametr2=wartosc%20ze%20spacjami&parametr=dodatkowy

Uwaga: wiele edytorów automatycznie wstawia znak nowej linii na końcu każdego wiersza, co ab zinterpretuje jako część ostatniej wartości, co może prowadzić do trudnych do wykrycia błędów. Można to obejść na końcu umieszczając dodatkowy parametr.

Tak przygotowany plik należy podać jako argument dla ab wraz z kodowaniem:

ab -p form-data.txt -T 'application/x-www-form-urlencoded’ https://api.example.org/endpoint

PUT

Poniżej przykład testowania końcówki metodą PUT:

ab -m PUT -H 'Content-Length: 0' https://api.example.org/endpoint/id

Testowana końcówka nie wymaga przesłania danych innych niż te z URL-a, więc niezbędny jest nagłówek Content-Length z wartością 0, bez niego aplikacja rzucała błąd mówiący o niemożliwości sparsowania żądania.

Docker

A co jeśli na maszynce nie ma ab? Jeśli jest Docker to można zrobić:

docker run cmd.cat/ab ab -n 10000 -c 10 https://api.example.org/endpoint

Duplicity i B2 Cloud Storage

Ostatnie wydarzenia w OVH zainspirowały mnie do audytu mechanizmów kopii bezpieczeństwa. Dla moich projektów webowych audyt wypadł pomyślnie, jednak dane z domowych komputerów są backupowane tylko na inny komputer, który także znajduje się w mieszkaniu. W razie pożaru lub kradzieży mógłbym stracić dane oryginalne i ich kopie bezpieczeństwa.

Postanowiłem robić kopie do innej lokalizacji. Na domowych komputerach mam sporo zdjęć i filmów, a to setki gigabajtów, więc koszt przechowywania kopii ma znaczenie. Pod tym względem ciekawie wypada oferta B2 Cloud Storage.

Instalacja Duplicity z wymaganymi zależnościami w Debianie 11 jest banalna:

apt install duplicity python3-b2sdk

Niestety w Debianie 10 to nie działa, brakuje bibliotek dla B2, co objawia się błędem jak poniżej:

BackendException: B2 backend requires B2 Python APIs (pip install b2)

Doinstalowanie ich pip-em nie pomaga. Metodą prób, błędów i szukania znalazłem działające rozwiązanie na forum – należy zarówno Duplicity jak i SDK dla B2 zainstalować przez pip-a:

apt install build-essential python3-dev gettext librsync-dev
pip3 install duplicity
pip3 install b2sdk

Cyberpunk 2077

Ja bym siebie tam nie umieścił, bo do powstania Cyberpunka 2077 nie przyczyniłem się w najmniejszym stopniu, ale skoro już zostałem wymieniony w napisach to się pochwalę.

Odpowiadając na niezadane pytanie: napisałem kilkaset linijek kodu w aplikacji odpowiedzialnej za przyznawanie cyfrowych nagród w grze.

O wiele większym wyzwaniem i powodem do dumy było obsłużenie istnego tsunami ruchu wygenerowanego podczas premiery Cyberpunka 2077, ale to temat na inną notkę.

Zmienna liczba argumentów w konstruktorze

W PHP nie raz zachodzi potrzeba przechowywania w obiekcie tablicy obiektów określonego typu, np. w kolekcjach, managerach strategii, itp. Jeśli lista obiektów jest przekazywana w konstruktorze to warto sprawdzić czy są właściwego typu. Przykładowy konstruktor może wyglądać tak:

/**
 * @param Item[] $items
 * @throws InvalidArgumentException
 */
public function __construct(array $items)
{
    foreach ($items as $item) {
        if (!$item instanceof Item) {
            throw new InvalidArgumentException('Collection accepts only instances of Item');
        }
    }
    $this->items = $items;
}

Ten sam efekt można osiągnąć prościej dzięki zastosowaniu operatora … (trzy kropki):

public function __construct(Item ...$items)
{
    $this->items = $items;
}

Tworzenie pokazu slajdów na Linuksie

Karina w ramach wolontariatu dla Mikropsów zgłasza się do robienia pokazów slajdów ukazujących metamorfozy podopiecznych fundacji. Opracowuje koncepcję, a trywialne zadanie jej zrealizowania zleca podwykonawcy w mojej osobie

Linuks nie słynie z programów do edycji filmów, ale do wykonania prostego pokazu zdjęć z muzyką nie trzeba wielkich kombajnów pokroju Adobe Premiere.

4K Slideshow Maker

Ta wieloplatformowa aplikacja pozwala przygotować pokaz slajdów bardzo szybko i bardzo łatwo. Tak naprawdę wystarczy tylko wybrać zdjęcia i muzykę, reszta robi się sama. Film taki jak poniżej można przygotować w 30 minut, łącznie z czasem generowania pliku:

Mikropsy 2018 from karniak on Vimeo.

Na powyższym filmie widać jednak niektóre ograniczenia programu:

  • Nie ma możliwości różnicowania poszczególnych slajdów, każdy musi trwać tyle samo i mieć zaaplikowany ten sam efekt.
  • Efektem przesunięcia i najazdu nie można w żaden sposób sterować, a program sam z siebie potrafi koncentrować się na na nieciekawych częściach zdjęcia.

OpenShot

To już nie jest prosty kreator, a pełnoprawna aplikacja do montażu wideo. Nie ma wad 4K Slideshow Makera, bo każdy slajd można wyklikać dokładnie wedle pragnienia, ale niestety wymaga to czasu. Pokazany poniżej film składałem jakieś trzy godziny, choć trochę czasu zajęła mi nauka programu.

Mikropsy 2019 1080p from karniak on Vimeo.

Do wad programu dodam jeszcze niezbyt wydajne działanie samego edytora nawet na relatywnie mocnym sprzęcie (Ryzen 1700, 16 GB RAM, SSD NVMe).

Hurtowe dodanie strict_types=1

Jednolinijkowiec na dziś:

grep -Z -r -L --include \*.php -P "strict_types=1" src/ | xargs -0 -l sed -i 's/<?php/<?php\n\ndeclare(strict_types=1);/g'

To polecenie wyszuka wszystkie pliki z rozszerzeniem php, które nie zawierają ciągu strict_types=1, a następnie doda declare(strict_types=1); zaraz po otwierającym znaczniku <?php. Żeby tylko wyszukać pliki, które nie mają strict_types=1 można użyć:

grep -r -L --include \*.php -P "strict_types=1" src/

Taki automat może się przydać przy modernizacji starszej aplikacji, warto jednak mieć dobre testy, bo taka trywialna zmiana może skutecznie ją wyłożyć.

UPSERT wielu wierszy w MySQL

Dzisiaj zacznę odwrotnie niż zwykle czyli od prezentacji gotowego rozwiązania:

INSERT INTO user_counters (user_id, counter_type, counter_value, updated_at)
VALUES
    (:user_id1, :counter_type1, :counter_value1, :updated_at1),
    (:user_id2, :counter_type2, :counter_value2, :updated_at2),
    # ...
    (:user_idN, :counter_typeN, :counter_valueN, :updated_atN)
ON DUPLICATE KEY UPDATE
    counter_value = counter_value + VALUES(counter_value),
    updated_at = VALUES(updated_at);

To tak zwany UPSERT czyli zapytanie, które wstawia rekord(y) do bazy, a jeśli taki już istnieje (tzn. istnieje już taki klucz główny lub unikalny) to go zaktualizuje (wartość w kolumnie counter_value zostanie powiększona o nową wartość, a w kolumnie updated_at zastąpiona). To zapytanie jest jednak trochę lepsze od tych, które zazwyczaj podaje się jako przykład UPSERTA, ponieważ pozwala zapisać wiele (dziesięć, sto, tysiąc) wierszy za jednym zamachem. Sprawia to użyta w części UPDATE funkcja VALUES(), która pobiera wartości ze wskazanej kolumny w części INSERT.

Po co się w ogóle tak babrać w SQL-u jak zwierzę skoro Doctrine mógłby to zrobić za nas? Akurat tego nie zrobi. Przy użyciu Doctrine należałoby:

  1. Pobrać wszystkie pasujące rekordy (jeden SELECT).
  2. Przeiterować przez nie i:
    • zaktualizować wartości,
    • zidentyfikować brakujące rekordy.
  3. Utworzyć encje brakujących rekordów.
  4. Opróżnić bufor managera encji (flush, który zapewne wykona jeden INSERT i jeden UPDATE).

Jak widać zarówno po stronie aplikacji (PHP) jak i bazy to wyraźnie więcej pracy, która i tak pewnie poszłaby na marne, bo przy kilku pracujących jednocześnie instancjach aplikacji jest spora szansa na wyjątek UniqueConstraintViolationException, który zamknie managera encji.

UPSERT z wieloma wartościami rozwiązuje problem wydajności i problem naruszania ograniczeń, ale tworzy klikna innych:

  • Przy jednoczesnych instancjach aplikacji łatwo o deadlocki na bazie. Trzeba zastosować jakiś mechanizm blokowania.
  • Jeśli w ten sposób zapisujemy dane z encji Doctrine’a to ich stan może być nieaktualny.

INSERT … ON DUPLICATE KEY UPDATE nie jest częścią standardu SQL, to rozszerzenie dostępne w MySQL. W PostgrSQL jest podobny mechanizm, ale jego działanie jest nieco inne, więc może opiszę je przy innej okazji.

Dependency injection uzależnione od routingu

Żeby w kontenerze Symfony uzależnić rodzaj wstrzykiwanego obiektu od akcji, która jest wykonana można posłużyć się następującym wyrażeniem:

app.some_service:
    class: App\Service\SomeService
    arguments:
        - "@=service(container.get('request_stack').getCurrentRequest() !== null and container.get('request_stack').getCurrentRequest().get('_route') == 'app_some_route' ? 'app.dependency.specific’ : 'app.dependency.generic’)"

Tak skonfigurowana usługa app.some_service zostanie zbudowana z zależnością app.dependency.specific jeśli aktualna trasa (route) to app_some_route, a jeśli inna to zostanie wstrzyknięta app.dependency.generic. W praktyce można to wykorzystać np. do wstrzyknięcia repozytorium bazodanowego w dla akcji z panelu administracyjnego (gdzie potrzebna jest aktualna wersja danych) lub repozytorium czerpiącego dane z cache dla prezentacji danych użytkownikom.

Z drugiej strony taka definicja usługi jest zagmatwana i może utrudniać debugowanie, więc pewnie postarałbym się inaczej rozwiązać taki problem.

Zduplikowana nazwa grupy woluminów LVM

Podczas przenoszenia systemu na nowy dysk zafundowałem sobie mały problem: nazwałem grupę woluminów LVM tak samo jak na starym dysku. Kiedy do świeżo postawionego systemu podłączyłem stary dysk, żeby skopiować dane ów problem objawił się tak:

# vgscan
  Reading volume groups from cache.
  Found volume group "cherokee" using metadata type lvm2
  Found volume group "cherokee" using metadata type lvm2

Jak wiadomo, żeby aktywować LVM trzeba podać nazwę grupy woluminów, a skoro ta jest zduplikowana to jest to problem. Jego rozwiązaniem jest zmiana nazwy jednej z grup. Ja postanowiłem zmienić nazwę starej grupy.
Czytaj dalej Zduplikowana nazwa grupy woluminów LVM