trade off błędy oprogramowanie

Błędy i trade-offy. Jak podejmować dobre decyzje

Praca wszystkich osób zaangażowanych w tworzenie oprogramowania jest pełna trade-offów. Jako trade-off rozumiemy konsekwencje decyzji dot. tego, w którą stronę chcemy pójść w naszym systemie. Możemy mieć wiele potencjalnych kierunków ewolucji systemu, jednak każdy z nich ma jakieś zalety i wady. Dogłębna analiza konsekwencji pomaga na dokonywanie w pełni świadomych decyzji.  

błędy i tradeoffy

Tomasz Lelek. Senior Software Engineer w DataStax. Buduje produkty wokół jednej z najpopularniejszych rozproszonych baz danych na świecie – Cassandra. Jest kontrybutorem do Java-Driver, Cassandra-Quarkus, Cassandra-Connector i Stargate. Autor książki pt. „Software Mistakes and Tradeoffs: Making good programming decisions, która skupia się na liście realnych problemów ze świata oprogramowania, na które możesz napotkać w swoich systemach produkcyjnych. Książka próbuje analizować każdą sytuację w różnych kontekstach i rozważa jej wszystkie trade-offy.


Funkcjonujemy w świecie ograniczonego czasu, budżetu, jak i wiedzy. Z tego powodu decyzje, które podejmujemy w momencie projektowania systemów, będą miały konsekwencje w przyszłości. 

Konsekwencjami mogą być większe koszty utrzymania, trudności wprowadzania zmian, kiedy zajdzie taka potrzeba, problemy z wydajnością i skalowaniem i wiele więcej. Dlatego każda decyzja powinna być podejmowana w specyficznym kontekście. Im bardziej dogłębną analizę jesteśmy w stanie przeprowadzić, tym możemy być bardziej świadomi o trade-offach, które ta decyzja wymusza.

Podczas swojej kariery, brałem udział, jak i również obserwowałem wiele decyzji, które wpływały na kształt oprogramowania. Dzięki temu mogłem analizować to, jak one wpływają na trade-offy tworzonych systemów. Bazując na tych obserwacjach powstała lista lekcji, które nauczyły mnie pracy w branży IT. 

Lista ta jest dość generyczna, a z większością problemów i decyzjami, które się na niej znajdują wielu programistów musi zmagać się na co dzień. Dlatego postanowiłem podzielić się tą w formie książki.

Oto kilka porad dot. trade-offów.

1. Duplikacja kodu nie zawsze jest zła, a koszt tight-coupling i koordynacji można policzyć

W zależności od tego w jakim kontekście nasz system jest budowany możemy redukować duplikacje kodu łatwiej lub trudniej. Jeśli mamy jedną aplikację w jednym repozytorium, refactoring kodu jest dość łatwy a usuwanie duplikacji w kodzie nie wprowadza dużego tight couplingu. Jednakże nawet w tym kontekście, używanie niektórych wzorców projektowych, jak dziedziczenie, może zwiększyć tight coupling. W kontekście mikro-serwisów, gdzie każdy zespół jest odpowiedzialny za inna część domeny biznesowej, redukcja duplikacji pomiędzy serwisami często może wprowadzić duży tight coupling. 

Nagle, jedna zmiana w logice wymaga koordynacji N zespołów, które używają tego kodu. Ten kosz koordynacji można policzyć adaptując Amdahl’s law. Dzięki temu możemy podjąć racjonalną decyzję pomiędzy duplikacją w kodzie, tight coupling i kosztem koordynacji.

2. Możemy odnaleźć hot-path w naszym kodzie posiadając dane o SLA i skupić się na optymalizacji tylko najważniejszej części kodu

Budując nasze systemy, wymagania odnośnie wydajności są niezwykle ważne. Mając dane o SLA możemy zbudować testy wydajnościowe w taki sposób, który umożliwi nam wnioskowanie o systemie przed wdrożeniem na produkcję. Kod, który przynosi największą wartość biznesową naszym użytkownikom to często mały procent całego codebase. Ta obserwacja bazuje na zasadzie Pareto, która sprawdza się w wielu domenach, nie tylko oprogramowaniu. Dzięki temu procesowi, możemy znaleźć hot-path w naszym kodzie. Jest to ścieżka, która jest wykonywana dla praktycznie każdego requestu naszego użytkownika. 

Mając informacje, gdzie leży hot-path w naszym kodzie, możemy skupić wysiłki optymalizacyjne na bardzo wąskim wycinku kodu i pracować dużo bardziej efektywnie. W książce skupiłem się na analizie systemu i zaadaptowaniu tej metody do optymalizacji hot-path. Każdy krok jest poparty testami wydajnościowymi, tak aby mieć mierzalne dowody na to, czy idziemy w dobrym kierunku.

3. Data locality to kluczowa kwestia przy przetwarzaniu Big Data

Jest wiele rozwiązań pozwalających nam przetwarzać i przechowywać dane Big Data. Możemy używać na przykład Apache Spark, Flink do przetwarzania. Do przechowywania możemy użyć Hadoop, Cassandra i wiele innych. Każde z tych rozwiązań pozwala nam na przetworzenie lub przechowanie dużej ilości danych w szybkim czasie. Jednak użycie narzędzia bez jego zrozumienia może być bardzo problematyczne. 

Niektóre koncepcje ze świata Big Data mogą na pierwszy rzut wydawać się niepotrzebne i zbyt skomplikowane. Jednak algorytmy użyte przez te narzędzia są niezbędne do wykonywania przetwarzań w skończonym czasie.  Data locality, czyli przenoszenie przetwarzań do danych (moving computations to data) i partycjonowanie danych, odgrywają tutaj kluczową rolę.

4. Aby stworzyć bardziej niezawodny system, musimy rozumieć czy operacje są idempotentne czy nie

Prawie każdy system, który tworzymy wymaga od naszej aplikacji zapytań po sieci (remote calls). Podczas każdego z takich zapytań może wystąpić błąd. Co więcej, nie wiemy, na którym etapie obsługi zapytania ten błąd wystąpił. Czy system, do którego wysyłamy zapytanie, miał awarie? Czy może system poprawie obsłużył zapytanie, ale podczas wysyłania odpowiedzi sieć zawiodła (network partition) i nasz serwis nie otrzymał odpowiedzi? 

W obu przypadkach, aplikacja, która wykonała zapytanie, może mieć problem z określeniem w jakim stanie jest drugi system po tym błędzie. Może dane już zostały zapisane albo wykonana (np wysyłka maila)? 

Przy dużym ruchu takie sytuacje zawsze będą się zdarzać. Aby nasz system był niezawodny i nie wymagał manualnej interwencji operatora (np administratora), możemy rozważyć automatyczne ponowienie zapytania (retry) w momencie błędu. Jednakże musimy być bardzo uważni. Jeśli operacja nie jest idempotentna, ponowienie jej może spowodować niespójności w systemie. Z drugiej strony, jeśli operacja jest idempotentna, czyli możemy ją powtórzyć dowolną ilość razy i za każdym razem da taki sam rezultat, ponawianie jest dobrym kierunkiem w stronę niezawodności. Z tego względu musimy być świadomi i przeanalizować każdą operację w naszym systemie pod względem idempotencji. 

5. Odpowiednia konfiguracja consistency i availability pozwoli nam dopasować system do naszych aplikacji

Wiele systemów pozwala na konfigurowanie konsystencji (consistency) vs. dostępności (availability). Jeśli na przykład wykonujemy operacje zapisu do jakiegoś systemu rozproszonego (Kafka, Cassandra) możemy skonfigurować, która charakterystyka nam bardziej odpowiada. Istotne jest bycie świadomym trade-offów każdej z nich. 

Jeśli bardziej zależy nam na konsystencji danych, jest większe prawdopodobieństwo, że nasz system może przestać być dostępny w razie awarii części maszyn rozproszonego systemu danych. W takim wypadku nie będziemy mogli zapisać danych, gdyż źródło nie pozwoli na wprowadzenie niespójności w nich.

Jeśli bardziej zależy nam na dostępności, możemy skonfigurować system, do którego zapisujemy w inny sposób. W tym przypadku, większa ilość awarii maszyn będzie tolerowana. Nasz system będzie bardziej dostępny, jednakże będziemy musieli za to zapłacić większym prawdopodobieństwem niespójności danych.

Większość systemów rozproszonych pozwala na konfiguracje tych charakterystyk. Zrozumie trade-offów, które się za tym kryją jest kluczowe w poprawnym używaniu tych systemów.

ZOBACZ TEŻ:  Dwa tygodnie z Kubernetesem na produkcji

Każdy z 13 rozdziałów książki, którą napisałem z Jon Skeet, skupia się na osobnej grupie problemów i decyzji, które musimy podjąć projektując i implementując nasze system.

Jeśli chciałbyś poczytać więcej, zapraszam na stronę wydawnictwa manning. Dla czytelników Just Join IT stworzyłem specjalny kod zniżkowy na książkę: “bljjit21”. 


Zdjęcie główne artykułu pochodzi z unsplash.com.

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
Market research. Dlaczego warto wykorzystywać R w badaniach rynku