Jak wspominałem w ostatnim wpisie – który wprawdzie poświęcony był tematowi zastosowania dynamicznego SQL w pracy z bazami danych, ale w dużej mierze opierał się na moich dotychczasowych doświadczeniach zawodowych – dzisiejszy tekst jest kontynuacją opowieści o tym jak to imć Borciugner zmierzyć się musiał z tematem niezbyt optymalnie wydumanego procesu generowania danych. Taki jest oto mniej więcej cel dzisiejszej pisaniny w szczególe, zaś na wyższym poziomie abstrakcji będzie to dosknała okazja by zademonstrować jak można wykorzystać moc powłoki bash w kontakcie z chmurową platformą od Google’a, a konkretniej z usługiami Google Storage (dalej GS) oraz BigQuery (dalej BQ).
Zanim jednak przejdziemy do meritum przypomnę na czym polega to szczególne zadanie, które wykonywane jest z pewną regularnością w moim obecnym miejscu pracy. Otóż mowa o procesie związany z szeroko rozumianą komunikacją do klienta, choć w tym wypadku biznesowy kontekst ma znaczenie drugorzędne. W tym miejscu zdobędę się na odrobinę odwagi i ośmielę się napisać, że od samego początku był on niewystarczająco pomyślany, ponieważ całość polega na tym, że mniej więcej z dwutygodniową regularnością pojawia się potrzeba wygenerowania kilkunastu plików tekstowych zawierających ściśle określone zestawienie informacji niezbędnych do przedsięwzięcia pewnych akcji. Otóż w zamierzchłej przeszłości na potrzeby tego ćwiczenia wymyślono sobie, że w celu wykonania tego zadania generowane będzie w praktyce około 20 tabel, których zawartość zostanie następnie wyeksportowana do wspomnianych kilkunastu plików (nie wszystkie tworzone tabele wymagały tego kroku, gdyż występują one tutaj jako pomocnicze).
Tutaj pojawia się pierwszy problem, ponieważ kolejne iteracje tego procesu wymagały dla potrzeb “audytowych” zachowania każdej z tych tabel na czas bliżej nieokreślony. W efekcie każda z każdorazowo tworzonych tabel w swojej nazwie posiada bieżącą datę generowania danych. Niestety w praktyce przez długie miesiące – liczone w latach – cała zabawa polegała na tym, że biedak obarczony tym nieszczęsnym brzemieniem poprawiał w skrypcie nazwy tych tabel (podmieniał daty na bieżące), a po ich utworzeniu w sposób równie siermiężny eksportował dane tam zawarte do plików płaskich, których nazwy również odpowiadały pewnej konwencji nazewniczej.
Nie muszę dodawać, że było to zadanie mozolne, ale przy tym błędogenne, ponieważ wystarczyło się pomylić przy nazwie któregokolwiek pliku, by do określonej grupy klientów trafił niewłaściwie targetowany komunikat. Nie był to błąd z rodzaju tych, co powoduje drżenie nóg na samą myśl o jego popełnieniu, ale z pewnych względów było to wysoce niewskazane. I może tyle wystarczy jeśli chodzi o ogólny opis tego ćwiczenia.
Niestety z różnych względów nie mogłem przebudować działania całości i musiałem się wpisać w wyżej opisany schemat, tym niemniej szkoda mi było czasu i nerwów na tak żmudne działanie, które co jakiś czas wymagało powtórzenia. Dlatego biorąc na barki to nieszczęście z całą zawartością inwentarza, postanowiłem sobie i wszystkim, którzy przyjdą po mnie, ułatwić życie, czyli na tyle na ile pozwalają mi moje możliwości (czytaj: uprawnienia jakim dysponuję w ramach GCP) zautomatyzować poszczególne kroki.
Jeśli chodzi o problem zmieniających się nazw tabel, to temat ograłem przy pomocy dynamicznego SQL, o czym pisałem ostatnio. W efekcie napisałem procedurę, której wywołanie – po sprawdzeniu pewnych dodatkowych założeń biznesowych – generowało te blisko 20 tabel i nieco ponad dziesięć plików płaskich, które zrzucane były następnie bezpośrednio z poziomu BQ do GS.
Niestety nie zamknęło to ostatecznie całości tematu tego zadania z dwóch powodów. Po pierwsze, plik płaskie zrzucone bezpośrednio z BQ do GS były w formacie innym niż TXT, ponieważ usługa eksportu wspiera wprawdzie kilka formatów plików (między innymi CSV, który ostatecznie do tego celu wykorzystałem), ale akurat brak wśród nich prostych plików tekstowych. Ponadto przy takim eksporcie – już nie będę wchodził w szczegóły dlaczego tak się dzieje – do nazwy pliku dodawane były “ekstra symbole”, które później i tak trzeba było usuwać.
Drugi powód był dużo bardziej prozaiczny: pobieranie tych plików na komputer lokalny za pośrednictwem WebUI jest szalenie irytujące, ponieważ z jakiegoś powodu nie można ich zaciągnąć masowo, tylko trzeba robić to plik po pliku, co przy jednej czy dwóch sztukach nie ma większego znaczenia, ale przy kilkunastu już trochę zaczyna drażnić. A ponieważ na końcu i tak wszystkie te pliki ładowałem do jednego ZIP-a, stwierdziłem że lepiej to ograć za pomocą skryptu bash, gdyż nasz szczęście użytkownicy GCP mają dostęp do wiersza poleceń wirtualnej maszyny, z poziomu której mogą korzystać z dedykowanych komendy służących do interakcji z poszczególnymi usługami GCP (w moim przypadku chodziło o BQ i GS).
Tym sposobem pomysł polegał na tym, by napisać skrypt, który:
- najpierw uruchomi procedurę utworzoną wprzódy w BQ, a która to procedura odpowiedzialna jest za wygenerowanie wymaganych tabel oraz wyeksportowanie ich zawartości w postaci kilkunastu plików płaskich (tutaj CSV) do GS,
- następnie pliki te mają zostać pobrane z GS na dysk naszej maszyny linuksowej i odpowiednio przetworzone pod względem oczekiwanego nazewnictwa,
- na końcu zostaną one zapakowane do jednego pliku ZIP i z powrotem wrzucone do GS (już w formie archiwum), skąd będzie można je pobrać już z poziomu WebUI na lokalną maszynę.
Całość została zaprezentowana poniżej:
#!/usr/bin/bash
current_date=$(date +"%Y%m%d")
dirName="itemki_"$current_date
bucket='gs://borciugner/'
if [ -d "$dirName" ]; then
echo "$dirName istnieje. Usuwam folder i tworze go na nowo!"
rm -rf $dirName
mkdir $dirName
else
mkdir $dirName
fi
gcloud storage rm ${bucket}*
# tworzenie niezbędnych tabel w BQ przy pomocy procedury borciugner.itemki
echo "Rozpoczynam generowanie danych. Może to potrwać kilka minut"
bq query --use_legacy_sql=false "call borciugner.itemki()"
# generowanie pliku wynikowego
echo "Rozpoczynam generowanie pliku wynikowego."
gcloud storage cp ${bucket}* $dirName
for file in $(ls ${dirName}'/')
do
mv $dirName/$(echo $file) $dirName/$(echo $file | sed 's/_000000000000.csv/.txt/g')
done
zip -r ${dirName}.zip ${dirName}/
gcloud storage rm ${bucket}*
gcloud storage cp ${dirName}.zip $bucket
rm ${dirName}.zip
echo "Gotowe. Możesz pobrać plik z https://console.cloud.google.com/storage/browser/borciugner"
Jak widać skrypt nie jest specjalnie wyszukany. Na początku tworzonych jest kilka zmiennych. Tutaj zwróciłbym uwagę na zmienną “bucket”, ponieważ wskazuje ona ścieżkę do tego zasobu GS, gdzie docelowo będą znajdować się pobrane z BQ pliki.
W kolejnym kroku jest tworzony (lub usuwany i na nowo tworzony, jeśli już wcześniej istniał) katalog o nazwie itamki_YYYYMMDD, gdzie w miejsce tych ostatnich znaków podstawiana jest bieżąca data.
Do tego momentu nie wychodziliśmy poza standardowe polecenia powłoki bash, natomiast w kolejnym wierszu pojawia się pierwsze polecenie pochodzące z biblioteki Google.
<strong>gcloud storage rm ${bucket}*</strong>
W tym przypadku mamy do czynienia z komendą służącą do usunięcia (rm *) wszystkich plików, które mogą się znajdować we wskazanym zasobie GS (gs://borciugner/).
Dalej mamy kolejne polecenie “made in Google”, które w tym wypadku dotyczy BQ i ma ono za zadanie wywołać procedurę itemki zapisaną w naszym projekcie na datasecie “borciugner”. Przypominam, że ta właśnie procedura ma za zadanie utworzyć wymagane tabele i następnie wyeksportować część danych w postaci plików płaskich do naszego zasobu na GS.
bq query --use_legacy_sql=false "call borciugner.itemki()"
Po udanym wykonaniu tej procedury w następnym kroku pobierane są na naszą wirtualną maszynę wszystkie pliki, które ta pierwsza zrzuciła do GS.
gcloud storage cp ${bucket}* $dirName
Zaś dalej mamy pętlę, które pobiera każdy z tych plików i zastępuje końcową część nazwy “_000000000000.csv” na “.txt”. Tak przetworzone pliki pakowane są do pliku ZIP o nazwie takiej samej jak nazwa folderu utworzonego na samym początku. To archiwum jest wysyłane do GS, przy czym poprzedza ten krok usunięcie dotychczasowej zawartości naszego zasobu w GS..
gcloud storage rm ${bucket}*
gcloud storage cp ${dirName}.zip $bucket
Zaś ostatnie polecenie wyświetla link do zasobu GS, coby poprzez kliknięcie w niego przejść od razu do WebUI w celu pobrania archiwum na naszą lokalną maszynę.
Napisanie i przetestowanie tego skryptu to góra półtorej godziny roboty, czyli mniej więcej tyle (a nawet trochę mniej) czasu, ile trzeba było wcześniej poświęcić na mozolne poprawiania skryptu i zrzucanie danych do dedykowanych plików tekstowych.