Dlaczego nadmiernie konstruujemy oprogramowanie (i jak zerwać z nałogiem)
Dzięki łatwemu dostępowi do przetwarzania w chmurze publicznej, orkiestratorom kontenerów i architekturze mikrousług tworzenie rozproszonych systemów o niemal nieograniczonej skali i złożoności stało się trywialne. Chociaż wszystkie te narzędzia mają swój cel, ważne jest, aby inżynierowie dokładnie rozważyli, kiedy i czy ich używać, zwłaszcza w mniejszych organizacjach. Dokonywanie złych wyborów może sprawić, że będziesz mniej zwinny, mniej zdrowy finansowo i mniej skuteczny. W tym artykule zbadano niektóre potencjalne przyczyny i zaproponowano pewne środki zaradcze dla powszechnego antywzorca niepotrzebnej złożoności.
Problem
Zaczynamy od złożoności
Kiedy przeprowadzamy rozmowę kwalifikacyjną na stanowisko inżyniera oprogramowania, musimy przejść przez syntetyczny, wymagający i często stresujący proces rozmowy kwalifikacyjnej, aby udowodnić potencjalnym pracodawcom, że mamy kwalifikacje, aby kontynuować to, co robiliśmy już od kilku lat. Podczas tego procesu możemy zostać poproszeni o rozwiązanie wielu średnich lub trudnych pytań algorytmicznych w krótkim czasie w czymś, co bardziej przypomina sport widowiskowy niż kodowanie. Zwykle jesteśmy zobowiązani do zaprojektowania nowatorskiego systemu w mniej niż godzinę, a jego wymagania musimy najpierw uzyskać od ankietera. Żadne z tych ćwiczeń nie przypomina tego, co robimy w naszej rzeczywistej pracy, ale zamiast tego są to wyzwania mające na celu wytworzenie różnego stopnia „sygnału”, który komisje rekrutacyjne mogą wykorzystać do wyrównania i zróżnicowania kandydatów.
W szczególności rozmowa na temat projektowania systemów to konstrukcja, w której kandydaci proszeni są o wykazanie się szerokim i głębokim zrozumieniem systemów na skalę, którą można znaleźć w największych firmach. Używamy pojęć takich jak mikroserwisy , spójne haszowanie , magistrale zdarzeń , siatka usług , bufory protokołów , WebSockets , Kubernetes , bramy API , potoki danych , jeziora danychi inne modne hasła technologiczne, aby pokazać, że wiemy, czym one są i jak są używane. Daje to potencjalnym pracodawcom pewność, że jesteśmy na bieżąco ze wszystkimi najnowszymi technikami i narzędziami i możemy zrobić to, o co nas poproszą.
Złożoność rodzi złożoność
Jeśli dostaniemy tę pracę, będziemy mieli możliwość wykorzystania całej zaawansowanej wiedzy, którą wykazaliśmy podczas procesu rekrutacji, do rozwiązywania problemów biznesowych. Sumiennie stosujemy te technologie i często tworzymy dość złożone systemy oparte na wzorcach architektonicznych zapoczątkowanych przez Google, LinkedIn, Meta, Amazon, Apple i Netflix.
Kierownictwo korporacji jest często zadowolone, widząc tę złożoność, ponieważ pokazuje to, że firma jest już dojrzała, „poprzeczka inżynierska” jest wysoko, a systemy firmy będą mogły skalować się wraz z biznesem, bez względu na to, jak bardzo biznes się rozwinie. Ta złożoność jest dalej wykorzystywana jako narzędzie rekrutacyjne, aby pokazać kandydatom, że będą mogli pracować ze wszystkimi najnowszymi technologiami oraz że ich przyszli współpracownicy są bystrzy i na bieżąco znają nowoczesne narzędzia i techniki.
Co z tym jest nie tak?
Problem z tym podejściem polega na tym, że wiele firm nigdy nie rozwinie się na tyle duże lub nie osiągnie wystarczającej skali, aby ta złożoność była korzystna. Największe firmy opracowały te nowe technologie i wzorce, aby poradzić sobie z poważnymi problemami skali, które napotykały, gdy ich baza użytkowników i wolumen transakcji rosły wykładniczo w dłuższych okresach czasu. W wielu przypadkach dobre rozwiązania nie istniały, więc zbudowali własne rozwiązania na skalę, jaką osiągnął biznes, często kosztem początkowym i bieżącym. Wiele z tych firm łaskawie otworzyło niektóre ze swoich technologii dla dobra szerszego ekosystemu technologicznego, czyniąc je „darmowymi do użytku” dla innych firm.
Gdy nie działają na dużą skalę, wiele z powyższych wzorców i technologii jest niebezpiecznych, ponieważ mogą spowolnić postęp, radykalnie zwiększyć koszty i zwielokrotnić obciążenie poznawcze personelu inżynierskiego. Jest to ogromny problem dla mniejszych firm, ponieważ pogarsza wyniki firmy w obszarach, które mają największe znaczenie; zwinność i rentowność. Stwarza to pułapkę dla firm, które znajdują się w okresie spowolnienia gospodarczego lub nagle bardziej konkurencyjnego sektora rynku w miarę zmniejszania się marż. To niedopasowanie pogłębia się, ponieważ systemy wynagradzania personelu inżynierskiego często nie są wystarczająco powiązane z lepszymi wynikami firmy (więcej na ten temat w kolejnym artykule).
Dwa przykłady
Zacznijmy od prostego systemu, który jest stosunkowo łatwy do zrozumienia i porównajmy jego minimalną formę z tym, co może się stać, gdy przeprojektujemy go. Na potrzeby tej dyskusji załóżmy, że mamy ogólną witrynę internetową, w której użytkownicy mogą rejestrować się i kupować rzeczy.
Prosta wersja
Minimalna architektura prostej witryny e-commerce jest często określana jako monolit , co oznacza, że cała baza kodu jest zawarta w jednym repozytorium źródłowym i jest generalnie wdrażana jako pojedyncza jednostka. Wiele startupów zaczyna tutaj, ponieważ pozwala im to szybko się poruszać, gdy baza kodu i zespoły inżynierów są małe. Dodatkową korzyścią jest to, że cały system często mieści się na laptopie, co ułatwia lokalny rozwój.
Na powyższym diagramie możemy wybrać kilka rozsądnych ustawień domyślnych od jednego dostawcy chmury (w tym przypadku Amazon Web Services lub AWS), aby zachować spójność narzędzi we wszystkich usługach:
- Podstawowy język programowania/framework: Ruby+Rails,
możemy również wybrać Python+Django, Go+Buffalo lub jakikolwiek inny sensowny język/framework - Frontend Language/Framework: JavaScript/TypeScript + React
prawie wszystkie nowoczesne frameworki internetowe będą wymagały frameworka JavaScript, a React jest bardzo popularny - Przetwarzanie wsadowe/w tle: Ruby+Sidekiq
Python+Celery, Go+Faktory byłyby alternatywami, gdybyśmy wybrali inny język podstawowy - Kontrola źródła: Git/GitHub
- CI/CD: GitHub Actions + Terraform
- Repozytorium obrazów: AWS ECR
- System orkiestracji kontenerów: AWS ECS
- Klucze: AWS KMS
- Sekrety: Menedżer tajemnic AWS
- Zapora sieciowa/warstwowa pamięć podręczna: Cloudflare
- DNS: Cloudflare
- System równoważenia obciążenia: AWS ELB lub ALB
- Przechowywanie plików/obiektów: AWS S3
- Pamięć podręczna + węzły pamięci podręcznej: AWS ElastiCache dla Redis
- Baza danych + repliki odczytu: Postgres na AWS RDS
- Przesyłanie wiadomości: Sendgrid
- Przetwarzanie płatności: pasek
- Tożsamość/SSO: Okta
- Dzienniki i obserwowalność: DataDog
Ten projekt obejmuje 16 krytycznych komponentów , w których awarie mogą spowodować awarie lub degradację w całym zakładzie. Ponieważ system jest minimalny, większość komponentów ma krytyczne znaczenie, ale nadmiarowe wdrożenia pomagają ograniczyć ryzyko awarii powodujących przestoje.
Złożona wersja
Moglibyśmy argumentować, że powyższa prosta wersja nie jest naprawdę prosta, ale możemy napiąć nasze architektoniczne muskuły i sprawić, by system był nieco bardziej imponujący.
Zacznijmy od (częściowego) rozbicia monolitu na mikroserwisy. Wprowadźmy również bramę API, Kubernetes, magistralę zdarzeń (przy użyciu Kafki), siatkę usług, wiele systemów DNS, wiele potoków CI/CD, potoki danych (przy użyciu oddzielnego klastra Kafka) i jezioro danych. Oprócz interfejsów API REST dodajmy obsługę interfejsów API gRPC (w tym buforów protokołów), ponieważ wiemy, że jest on znacznie szybszy niż interfejs REST i zależy nam na wysokiej wydajności. Dodamy drugi system CI/CD (Jenkins), ponieważ niektórzy pracownicy wolą go od GitHub Actions.
Dodajmy dodatkowy język i framework (Python/Django/Celery), aby dać programistom więcej opcji, i pamiętajmy o dodaniu Go i Groovy do naszej listy obsługiwanych języków, ponieważ używamy Jenkinsa (z Groovy DSL) i Kubernetes więc prawdopodobnie będziemy potrzebować operatorów Kubernetes (Go) do zarządzania kilkoma niestandardowymi zasobami, które utworzymy. Aby uzyskać lepszą wydajność i oddzielenie, całkowicie oddzielimy nasze wdrożenie interfejsu użytkownika frontendu od naszego monolitu zaplecza. Dodajmy również kolejną opcję logowania i obserwowalności (Sumo Logic), ponieważ nasz pierwszy wybór (DataDog) został uznany za zbyt drogi, aby wysłać wszystkie nasze logi.
Zróbmy inwentaryzację tego, jak te architektoniczne rozkwity zmieniły nasz system:
Jeśli założymy, że nasza usługa Pythona i platforma danych nie są krytyczne dla misji, mamy teraz 33 krytyczne komponenty , w przypadku których awarie poszczególnych komponentów mogą powodować problemy w całej witrynie. Biorąc pod uwagę sposób, w jaki obliczamy dostępność , zwiększa to prawdopodobieństwo przestojów, ponieważ cały system może być dostępny tylko w takim stopniu, w jakim jest najmniej dostępny zależny składnik. Mamy więcej zależnych komponentów, więc utrzymanie takiej samej dostępności jak prostszy system wymaga znacznie większego rygoru. W praktyce im więcej komponentów, tym trudniej jest wyegzekwować konsekwentnie wysokie standardy.
Zastąpiliśmy niezawodne wywołania lokalnych bibliotek wywołaniami API przez sieć. Ma to dwojaki wpływ:
- Sieć dodaje opóźnienie, przez co każde wywołanie zdalnej usługi jest nieco wolniejsze niż wywołanie lokalnej biblioteki
- Każde wywołanie sieciowe może zakończyć się niepowodzeniem lub mieć ograniczoną szybkość, co oznacza, że musimy bardziej uważać na obsługę błędów, niż gdybyśmy tylko wywoływali kod w lokalnie dostępnej bibliotece
Mamy teraz 3 opcje wzorców integracji :
- API REST
- gRPC
- Autobus imprezowy
11 nowych komponentów netto :
- Brama API (Brama API Amazon)
- Platforma Frontend + Edge Compute (Vercel)
- Wewnętrzny DNS (AWS Route53)
- Kafka dla magistrali zdarzeń (AWS MSK)
- Kafka dla potoków danych (AWS MSK)
- Wiele wdrożeń Kafka Connect Source i Sink do odczytu/zapisu z różnych baz danych i tematów
- Data Lake: założymy AWS Lake Formation z Amazon Athena do wysyłania zapytań i S3+Parquet do przechowywania danych
- Istio Service Mesh dla mTLS i routingu między bramą API a naszą mikrousługą Pythona
- Wiele klastrów Kubernetes (AWS EKS)
- Systemy równoważenia obciążenia wejściowego (AWS ALB)
- CI/CD Jenkinsa
Około 16 zduplikowanych komponentów :
- 3 Bazy danych + repliki odczytu
- 3 klastry Redis (jeden dla monolitu plus jeden na mikroserwis)
- 2 klastry Kafki
- 2 systemy DNS
Cloudflare zewnętrzny + Route53 wewnętrzny. Nie obejmuje to DNS wewnętrznego klastra Kubernetes - 2 systemy CI/CD
- 2 Rozwiązania w zakresie rejestrowania i obserwowalności
- 2 klastry Kubernetes
Około 10-15-krotny wzrost kosztów operacyjnych
Licząc dodatkowe kontenery, wdrożenia, usługi zarządzane i wymagany personel, możemy spodziewać się, że koszty operacyjne będą znacznie wyższe. Nawet Amazon , jeden z najbardziej zagorzałych promotorów mikrousług i chmury publicznej, stwierdził, że koszty są zaporowe dla niektórych celów wewnętrznych.
Zbyt duży, aby można było tworzyć lokalnie pełny pakiet
Nawet przy zmniejszonych konfiguracjach wdrożeniowych prawdopodobnie jest zbyt wiele ruchomych części, aby umożliwić uruchomienie całego systemu na laptopie programisty w Dockerze lub minikube. Istnieją sposoby na umożliwienie rozwoju w chmurze, ale programiści będą potrzebować próbnych lub współdzielonych instancji programistycznych usług, aby symulować w pełni funkcjonalny system.
Jak uniknąć tych błędów?
Z technicznego punktu widzenia nie ma nic złego w powyższych prostych lub złożonych przykładach systemów. Oba zapewniają podobną funkcjonalność, a złożony system, choć jego budowa i obsługa jest znacznie droższa, może być odpowiedni dla niektórych firm o określonej wielkości i skali. To powiedziawszy, złożony przykład prawdopodobnie nie jest najlepszym punktem wyjścia dla większości firm. Aby uniknąć przedwczesnej optymalizacji, pomocne są następujące podejścia:
Rozwijaj kulturę, która nagradza prostotę
Podczas rozmów z kandydatami, zamiast zachęcać ich do zaimponowania swoją rozległością i głębią wiedzy oraz do projektowania systemów na skalę internetową, rozważ uproszczenie architektury jako jedno z kluczowych kryteriów, które stosujesz do ich oceny. Zadawaj pytania typu: „Czy możesz wymyślić sposoby, aby ten system był prostszy lub bardziej niezawodny?” Jeśli potrafią zidentyfikować w swoim projekcie rzeczy, których nie potrzebują, nagrodź je za to. Jeśli przede wszystkim nie dodają zbędnych komponentów, tym lepiej.
Po zatrudnieniu indywidualnego współpracownika, zwłaszcza na wyższych szczeblach, spraw, aby uproszczenie architektury było częścią ich oceny wydajności. Nagradzaj pracowników, którzy przyjmują złożone i delikatne systemy i czynią je prostszymi i bardziej niezawodnymi. Posiadanie personelu technicznego poszukującego wydajności i uproszczeń zapewni, że systemy będą tak elastyczne i niezawodne, jak to tylko możliwe, gdy trzeba szybko się poruszać.
Zminimalizuj liczbę jednoczesnych zmian
Kiedy firma rozpoczyna trudne zadanie, takie jak przenoszenie ograniczonych kontekstów z monolitu, pojawia się pokusa wyodrębnienia, przetłumaczenia i refaktoryzacji w ramach jednej operacji „wielkiego wybuchu”. Jeśli zespoły uważają, że chciałyby, aby problem został wdrożony w innym języku lub przy użyciu innego narzędzia (którego również chcą się nauczyć), często sprzedaje się to ćwiczenie tłumaczeniowe jako ważną i niezbędną część wysiłków związanych z ekstrakcją.
Zakładając, że istniejący język i struktura są nadal odpowiednie do obciążenia, zwykle lepiej jest najpierw wyodrębnić kod do usługi zewnętrznej, a po zakończeniu ekstrakcji rozważyć refaktoryzację, przeniesienie lub inną restrukturyzację usługi, jeśli czegoś nadal brakuje. Widziałem, jak wiele wysiłków migracyjnych walczyło o zrobienie zbyt wielu rzeczy naraz i ryzykowało powodzenie całego wysiłku lub robiło bardzo, bardzo powolne postępy. Jeśli istniejący kod jest napisany w języku Ruby, zwykle najlepiej jest zachować tę stałą wartość, dopóki usługa nie stanie się naprawdę osobnym problemem. Możesz uzyskać więcej pomocy od innych inżynierów, jeśli zmienisz jak najmniej, a oni nadal będą w stanie zrozumieć kod, który przenosisz.
Zachęcaj do standardów i konsensusu
Widziałem bitwy między reżyserami, inżynierami i architektami, które były rozwiązywane przez wszystkie strony decydujące się zrobić to po swojemu i unikając konwencji i standardów na rzecz rozwoju od podstaw z nowatorskimi narzędziami i technologiami. Chociaż może to być wzmacniające dla nieustraszonego inżyniera, zwykle kończy się na niestandardowych rozwiązaniach, które nie działają dobrze z innymi częściami ekosystemu i zwiększają koszty operacyjne, ponieważ są „wyjątkowe” pod każdym względem.
Zamiast rozdzielać, zachęcaj do konsensusu, który prowadzi do jednego sposobu rozwiązania problemu, który można szeroko przyjąć. Zamiast opracowywać narzędzia dla siebie lub swojego najbliższego zespołu, myśl o wszystkim, co budujesz, jako o czymś, co będziesz udostępniać całej organizacji inżynierskiej. Jeśli problem jest wspólny dla wielu inżynierów, ale nie sądzisz, że twoje rozwiązanie zostanie przyjęte, rozważ ulepszenie czegoś, co jest już powszechnie używane, lub przedyskutuj problem z innymi inżynierami, dopóki nie uzyskasz porozumienia i poparcia, że twoje podejście może być szeroko stosowane używane i że pomogą ci zachęcić do adopcji w całej organizacji.
Wolisz nic nie robić niż robić coś złego
Wiele organizacji ma obsesję na punkcie szybkości i używa metodologii Agile, takich jak Scrum, aby „maksymalizować przepływ” i przepustowość zespołów. W wielu przypadkach firma nie wie dokładnie, co musi zrobić, więc po prostu stara się zapewnić inżynierom zajęcie, aby pozostali w ciągłym trybie szybkiej realizacji.
Problem z tym podejściem polega na tym, że często buduje się rzeczy, które nie są potrzebne, ale zamiast je wyrzucać, traktuje się je jako kontynuację działalności, która wymaga wsparcia i ciągłych inwestycji. Powodem tego jest to, że większość inżynierów jest dumna ze swojej pracy i nie chce pracować na „wyrzucanym” oprogramowaniu.
W czasach, gdy nie jest jasne, co należy zrobić, wykorzystaj ten czas, aby umożliwić inżynierom eksperymentowanie, aktualizowanie umiejętności lub współpracę z menedżerami produktu i innymi przywódcami w celu ustalenia, co należy zrobić dalej. Zapobiegnie to tworzeniu się „cruftów” i zwiększy własność, gdy nadejdzie czas na szybkie wykonanie, gdy już wymyślisz, co zbudować.
Odłóż optymalizację do chwili, gdy będzie potrzebna
Zawsze lepiej jest działać proaktywnie i rozwiązywać problemy z wydajnością i dostępnością, zanim wpłyną one na działalność firmy. Może być kuszące, aby agresywnie „przyszłościowe” rozwiązania wymagały obsługi masowego skalowania w poziomie, zanim będzie to potrzebne, ale zwykle jest to błąd .
Zamiast tego spraw, aby definiowanie, monitorowanie i planowanie wydajności SLI było częścią własności usługi. Gdy zespół jest odpowiedzialny za zapewnienie, że wskaźniki poziomu usług są dobrze zdefiniowane i spełniają cele docelowe, optymalizacja jest częścią kontraktu. Jeśli wzorce korzystania z usługi ulegną znacznej zmianie w związku z wprowadzeniem nowego produktu lub partnerstwem, należy skonsultować się z właścicielami usług i pozwolić im uwzględnić to w ich planowaniu oraz upewnić się, że mogą utrzymać wskaźniki we właściwych zakresach przy zwiększonym ruchu. Jeśli zespół utrzymuje spójny docelowy poziom usług, jednocześnie przyjmując znacznie większy ruch, nagradzaj go (finansowo), ponieważ większy wolumen powinien oznaczać większy przychód.
Upewnij się, że sukces inżynieryjny i sukces firmy są silnie skorelowane
Zbyt często inżynierowie są nagradzani za tworzenie technicznie imponujących rozwiązań, a to „podnoszenie poprzeczki” jest tym, w jaki sposób mogą usprawiedliwić awans w organizacji. Nazywa się to często „konstruowaniem wznowienia”, w ramach którego inżynierowie mogą pokazać przyszłym pracodawcom imponujące systemy, nad którymi pracowali, jako sposób na przeniesienie się i awans. Jest to niefortunne, ponieważ twój ekspert merytoryczny opuszcza budynek, wychwalając zalety czegoś, co zbudował, czego mogłeś nie potrzebować. Teraz musisz zdecydować, czy wesprzeć ten twór dodatkowym personelem, czy też zlikwidować go z powodu braku wewnętrznej wiedzy.
Zamiast tego zachęcaj inżynierów, aby przedkładali potrzeby firmy nad technicznie imponujące rozwiązania, upewniając się, że zawsze działają lepiej, gdy firma radzi sobie lepiej. Poświęcę temu tematowi kolejny artykuł, ale TL;DR polega na upewnieniu się, że nagradzasz (finansowo) zachowanie inżynierskie, którego faktycznie potrzebujesz, aby rozwijać się i ulepszać firmę.