Aplikacje produkcyjne to mnóstwo technik i wymagań, których nie zobaczycie w większości projektów pobocznych i aplikacji-demo. Programista, który chce tworzyć realne, odporne na "warunki pogode" aplikacje musi zacząć myśleć o swoim rozwiązaniu w dużo bardziej dojrzały sposób.
Przemek Smyrdek
2020-01-09
Aplikacje produkcyjne to mnóstwo technik i wymagań, których nie zobaczycie w większości projektów pobocznych i aplikacji-demo. Programista, który chce tworzyć realne, odporne na "warunki pogode" produkty, musi zacząć myśleć o swoim rozwiązaniu w dużo bardziej dojrzały sposób - przygotować się na to, że czasami coś nie zadziała, napisać kilka testów i nieustannie automatyzować pracę.
Dzisiaj kilka słów o pięciu tajnikach tego typu aplikacji.
Znacie te historie spod znaku “2 jednostkowe, 0 integracyjnych?”. Kiedy zespół odpowiedzialny za suszarkę do rąk opracowuje znakomite urządzenie do suszenia rąk, tak samo jak zespół odpowiedzialny za krany opracowuje genialnie zaprojektowany kran, jednak zespół montujący to wszystko tak układa oba elementy, że suszarka podmuchem powietrza kieruje na ciebie strumień wody jak z myjki ciśnieniowej?
Tak tak, zacznijmy od testowania. Nie, nie o TDD #jagto!
Jedna z najbardziej poważnych wpadek, jaką miałem okazję wrzucić kiedykolwiek na produkcję, wydarzyła się w kodzie pełnym testów jednostkowych. W kodzie, który naiwnie uznałbym za poprawny względem praktyk, które poznawałem na początku przygody z programowaniem. Testów dużo, testy zielone, nowe funkcjonalności z pełnym pokryciem. A jednak coś poszło nie tak.
Nasz programistyczny świat obudował testy wieloma mitycznymi praktykami (TDD) i modelami (piramida testów), tworząc z tego prostego jak drut konceptu niemal religię. Jak testować? Czym testować? Kiedy testować? Przecież z punktu widzenia użytkownika testy to absolutnie ZBĘDNY fragment kodu. Z punktu widzenia programisty - to czas, który mógłbyś poświęcić na kolejne usprawnienie życia użytkownika. Z punktu widzenia biznesu, niezależnie od kraju i kontynentu - czego dowodzą tysiące pytań pt. “jak przekonać szefa do testowania” - to po prostu koszt.
Dlaczego w takim razie testy mają tak ogromną wartość w przypadku realnych produktów? Słowo-klucz - zarządzanie ryzykiem. Zarządzanie, czyli wiedza o tym, kiedy na ryzyko się decydować, a kiedy jest go za dużo. Tak jak z jazdą samochodem - 80km/h w terenie zabudowanym pomoże ci zaoszczędzić czas przejazdu, jednak godzisz się na ryzyko związane z mandatem, kolizją czy wystąpieniem w wieczornych wiadomościach. Na drodze systemy kontroli trakcji ratują życie, a w rajdach odbierają ci szansę na sukces. Jako kierowca ciągle decydujesz, co w danym momencie liczy się bardziej.
Dojrzałe zespoły budujące oprogramowanie również muszą mierzyć się z zarządzaniem ryzykiem i w ogromnej części przypadków testy to nieodłączny element aplikacji produkcyjnej, gdzie nieustannie szukasz balansu pomiędzy szybkością a jakością:
🚀 chcesz publikować zmiany tak szybko jak się da, wyprzedzając konkurencję ⭐️ chcesz, żeby użytkownik kojarzył twój produkt tak pozytywnie jak się da ✅ chcesz zabezpieczyć krytyczne fragmenty kodu przed potencjalnymi błędami w przyszłości
No i po to właśnie testujesz. Testujesz, kiedy koszt potencjalnej wpadki jest większy niż zysk z wyjścia na produkcję bez testów. Tak, to takie proste. Nie, to nie zawsze nierówność w jedną stronę.
Jeśli wstrzymujesz pracę nad prostym i dość nieistotnym elementem interfejsu szukając przez tydzień najlepszego narzędzia do sprawdzenia, czy przycisk faktycznie pojawia się na stronie “bo TDD”, to niestety - zarządzanie ryzykiem poszło nie w tę stronę.
✨ zamiast walki o liczbę testów, skupienie na jakości 🔥 zamiast piramidy testów, budowanie świadomości o krytycznych miejscach w kodzie 📈 zamiast walki o 100% pokrycia, minimalizowanie ryzyka
Kiedy napisany test spłaca swój koszt? Kiedy ktoś był w stanie go zepsuć, a tym samym zablokować release. Test pozwolił ci zminimalizować ryzyko. Jeśli napisałeś test, który przez pięć lat świeci się na zielono, to niestety - równie dobrze mogłoby go tam nie być. ¯_(ツ)_/¯
W mojej przygodzie z początku tego wpisu zawiódł zdrowy rozsądek. Skupiłem się na atomowych przypadkach testując jednostkowo, tracąc przy okazji szerszy obraz sytuacji i nie pisząc testu wysokopoziomowego. Testowałem suszarkę i kran, a nie suszarkę nad kranem.
https://gfycat.com/pl/phonybrightcatfish
Pamiętam doskonale, kiedy po raz pierwszy - jeszcze na studiach - mogłem skorzystać z platformy Azure do skonfigurowania deploymentu aplikacji. Nowy projekt, podpięcie publicznego repozytorium, “Zapisz” i działa - każda zmiana na branchu master uruchamiała zadanie zbudowania nowej wersji aplikacji, a następnie opublikowania jej na serwerze produkcyjnym. Człowiek mógł się skupić na tworzeniu ficzerów.
Cały proces był tak prosty i intuicyjny, że stał się dla mnie oczywistym standardem. Nie sądziłem, że przy tak prostej obsłudze tej platformy, infrastruktura i “nie-ficzery” będą jeszcze kiedykolwiek istotnym zjadaczem czasu.
A potem przyszła rzeczywistość.
Rzeczywistość, w której już nigdy nie spotkałem się z tak naiwnie prostą konfiguracją całego procesu CI/CD. Rzeczywistość, w której mnóstwo powtarzających się zadań było wykonywanych ręcznie. Rzeczywistość, w której koszmarem bywa konfiguracja lokalnego środowiska pracy. Rzeczywistość, w której nieustannie walczymy z rozwiązaniami i praktykami, które 10 lat temu uznano za wystarczające i optymalne, a które pozostają w niezmienionej formie aż do dzisiaj.
Znamy to ¯_(ツ)_/¯
Z każdym kolejnym projektem możemy zauważyć, jak wiele niewidocznych gołym okiem zadań z zakresu “tworzenia aplikacji” walczy o nasz czas. W przypadku aplikacji produkcyjnych, czyli takich, gdzie napisany kod to jeden z kilkunastu kroków drogi od pomysłu do użytkownika, tych zadań jest szczególnie dużo:
🌍konfiguracja środowiska developerskiego 🧐testowanie kodu 🔢aktualizowanie wersji aplikacji ♻️budowanie nowej wersji 🚀deployment na środowisko produkcyjne ✨aktualizowanie zależności aplikacji 😱reagowanie w sytuacjach kryzysowych ↪️zadania typu weź plik “stąd” i przenieś “tam”
W projektach pobocznych każda taka inwestycja czasowa działa na plus bo uczymy się czegoś nowego, ale w warunkach rzeczywistych prawdopodobnie palimy czas na niewłaściwe zadania. Właśnie dlatego automatyzacja pracy - tak jak w przypadku opisywanych wczoraj testów - to potężne narzędzie świadomego programisty.
Droga do automatyzacji to kilka uzupełniających się technik:
📈analiza i obserwacja środowiska pracy pod kątem urwania kolejnego “1%” 📝lokalne skrypty przyśpieszające pracę (bash, node.js, python, itd.) 🔧reużywalne narzędzia rozwijane przez członków zespołu 🏎automatyzacja zadań w serwisach zewnętrznych - Travis CI, Circle CI, Github Actions
✅ Dobra praktyka - tak jak w przypadku wyciągania wspólnych części kodu, to praktyka “do trzech razy…”. Dwukrotne wykonanie zadania manualnie jest do przeżycia, ale za trzecim razem napisz skrypt (np. w bashu). Dwukrotne wyciągnięcie danych z bazy jest zrozumiałe, ale za trzecim razem stwórz endpoint (odpytujący bazę za ciebie). Dwie dyskusje o konwencji pisania kodu są dobre, ale zamiast trzeciej dodaj do pipe’u CI/CD krok lintera (np. eslint). Dwukrotne sprawdzenie użycia danego ficzera ma sens, ale za trzecim razem skonfiguruj automatyczny raport na Google Analytics.
Rozwój rzeczywistych produktów to nieustanna walka o czas. Odpowiadanie sobie na pytanie co zrobić, żeby w te magiczne osiem godzin zrobić więcej i lepiej niż wczoraj. Jak naciągnąć kalendarz, żeby nagle “mieć czas”.
Automatyzuj 🚀
Główna różnica pomiędzy side-projectami a aplikacjami produkcyjnymi jest taka, że te pierwsze chodzą spać wtedy kiedy wyłączamy komputer, a te drugie… nie chodzą spać nigdy 😱
Czy to znaczy, że programiści tworzący aplikacje tego typu już nigdy nie opuszczają biura a wieczorne oglądanie Netflixa na kanapie pozostaje w sferze marzeń? Niekoniecznie.
W idealnym świecie chcielibyśmy, żeby dostępność naszej aplikacji dla użytkowników wynosiła 100%, czyli żeby aplikacja była dostępna zawsze i dla każdego. Dostępność aplikacji o każdej porze dnia i nocy to marzenie każdego project managera, administratora, pracownika działu wsparcia, no i nie ukrywajmy - programisty odpowiedzialnego za dane rozwiązanie.
Praktyka pokazuje jednak, że uptime aplikacji to zawsze okolice 99%. Ten jeden brakujący procent wynika z sytuacji kryzysowych - awaria data center, opublikowanie wadliwej wersji aplikacji, nagły przypływ użytkowników z którym nie radzi sobie infrastruktura albo atak DDoS. Zazwyczaj wybrane z tych zdarzeń dzieje się o drugiej w nocy. W sobotę.
Jeśli jesteśmy świadomi tego, że niemożliwym jest zapewnienie pełnej dostępności aplikacji i zgadzamy się też, że od czasu do czasu chcielibyśmy mieć trochę wolnego, to wciąż nie znamy odpowiedzi na jedno pytanie - skąd wiadomo co dzieje się z naszą aplikacją kiedy nie ma nas w biurze?
Tutaj kluczem są takie praktyki jak monitoring oraz alerting.
Monitoring aplikacji to dokładnie to samo co monitoring domu, tyle, że zamiast kamer konfigurujemy odpowiednie usługi które sprawdzają dostępność naszej aplikacji dla przeciętnego użytkownika w trybie 24/7 i śledzą wybrane metryki.
Dwa z takich rozwiązań, które chciałbym wam dzisiaj polecić, to:
▶️ https://healthchecks.io/, czyli regularne odpytywanie danego adresu URL ▶️ https://www.pingdom.com/, czyli monitoring i alerting rozwiązania webowego
Dzięki rozwiązaniom tego typu jesteśmy w stanie śledzić z dużą precyzją co konkretnie działo się z naszą aplikacją o wybranej porze dnia. Dodatkowo, wsparcie dla funkcji alertingu umożliwia chociażby wysyłanie smsów czy maili do zespołu reagowania kryzysowego w momencie, kiedy z naszą aplikacją dzieje się coś niedobrego.
Poza wysokopoziomowym spojrzeniem na nasz produkt musimy też rozumieć co konkretnie dzieje się z tworzoną aplikacją. Monitoring aplikacji na poziomie kodu to temat na całą serię filmów, ale dzisiaj polecam wam dwa rozwiązania dla warstwy front-endowej, dzięki którym uzyskacie wiedzę na temat błędów występujących w przeglądarkach każdego, kto korzysta z budowanego przez was produktu. Tymi rozwiązaniami są:
▶️ https://trackjs.com/ ▶️ https://sentry.io/for/javascript/
Oba rozwiązania działają jak pluginy, które dodajecie do strony, a dzięki temu każdy błąd zalogowany w konsoli przeglądarki zostanie dodatkowo zalogowany w zewnętrznym serwisie. Podsumowanie tego, co działo się np. w środku nocy, możecie spokojnie zweryfikować w trakcie porannej kawy.
Jeśli zastanawiacie się po co właściwie korzystać z takich usług, jeśli nasz docelowy klient to “standardowy użytkownik internetu”, to tworząc aplikacje produkcyjne szybko przekonacie się, że nie ma kogoś takiego. Użytkownicy mogą korzystać z przeglądarek, o których nie macie pojęcia, w lokalizacjach, gdzie połączenie sieciowe jest fatalne, przy użyciu narzędzi które wpływają na pracę danej maszyny tak jak nie moglibyśmy przypuszczać, albo generując ruch który przeciąży wasze serwery,
Bez odpowiedniego monitoringu rozwój aplikacji zawsze odbywać się będzie “na oko”, a na to nie można sobie pozwolić przygotowując rozwiązanie dla setek tysięcy użytkowników.
No więc jak, wiecie co dzieje się z waszą aplikacją, kiedy kładziecie się spać? 😎
Sieć zawsze działa. Przepustowość jest nieskończona. Wszystko jest bezpieczne. Architektura rozwiązania jest niezmienna. Opóźnienie związane z komunikacją pomiędzy serwisami nie istnieje. Bazy danych zawsze odpowiadają poprawnie. Użytkownik nigdy nie wprowadzi niebezpiecznego znaku.
Ta, na pewno… 😏
Poświęcanie czasu na obsługę błędów to jedna z najbardziej szokujących dla mnie cech aplikacji produkcyjnych, do której musiałem się przyzwyczaić rozpoczynając ścieżkę programisty.
No bo jak to - na studiach uczyli jak robić, jak budować, jak dodawać, a teraz ktoś mówi, że będzie się psuło? 😱
No tak. Jeśli tworzysz dojrzałe produkty, to już na samym starcie musisz założyć, że coś nie zadziała. Przykładowo, jako front-end developer muszę się przygotować na sytuacje kiedy:
⏰ pobieranie danych z wybranego endpointu będzie zajmować dużo czasu 🚫 pobieranie danych z wybranego endpointu będzie niemożliwe bo ktoś złamał kontrakt 🔥 proces wysyłania pliku na serwer nie powiedzie się 📈 użytkownik doda załącznik w rozmiarze większym niż przewidywany 🗑 lista elementów, która zawsze była pełna, teraz otrzyma zero wyników
...i tak dalej.
Z punktu widzenia programisty wszystkie te sytuacje bardzo łatwo zignorować. No przecież działało pięć razy, to szósty też zadziała… chyba. Zadziała, prawda? 🙏
Konsekwencją nieodpowiedzialnego podejścia do budowania aplikacji jest niestety to, że użytkownik - najważniejszy odbiorca tego co robimy - zaczyna mieć więcej i więcej negatywnych skojarzeń z naszym produktem, albo co gorsza - po prostu zaczyna się denerwować albo faktycznie stresować (np. po zobaczeniu zerowego stanu konta, kiedy serwer odpowiedział w niepoprawny sposób). Czy naprawdę chcielibyśmy naszym użytkownikom psuć spokojny wieczór tylko dlatego, że nie przygotowaliśmy się na jeden zepsuty scenariusz w naszym rozwiązaniu?
Aplikacje produkcyjne to środowisko, w którym domyślnie musisz zakładać, że coś nie zadziała. Nie zadziała, i już. Podejście przeciwne do opisywanego tutaj jest niestety dość popularne - na przykład w środowiskach rozproszonych, co doczekało się już dedykowanych określeń i stron na Wikipedii (https://en.wikipedia.org/…/Fallacies_of_distributed_computi…).
Jeśli nie chcesz chcesz uprawiać “happy codingu” to sprawdź, czy na pewno poprawnie obsługujesz długotrwające zapytanie, czy komunikujesz błąd pobierania danych albo wyjaśniasz użytkownikowi, dlaczego w tym momencie nie udało się wysłać formularza.
Uwierz mi - wbrew pozorom będzie ci wdzięczny. Albo przynajmniej trochę mniej zły ;)
Zapisz się na LiveQuiz - http://bit.ly/quiz-js