Materiał przeznaczony jest głównie dla początkujących programistów internetowych.

Wstęp.

Często zwracają się do mnie klienci, którzy mają zainstalowany samodzielnie napisany CMS lub moduły napisane przez początkujących programistów internetowych, którzy nie rozumieją, co jest potrzebne do ochrony danych i często kopiują funkcje filtrowania, nie zastanawiając się, jak działają i co dokładnie należy z nimi zrobić .

Tutaj postaram się jak najdokładniej opisać. typowe błędy podczas filtrowania danych w Skrypt PHP i dać proste wskazówki jak prawidłowo filtrować dane.

W sieci jest wiele artykułów na temat filtrowania danych, ale są one, jak należy, niekompletne i pozbawione szczegółowych przykładów.

Odprawa.

Filtrowanie. Błąd nr 1
W przypadku zmiennych liczbowych stosuje się następujące sprawdzenie:
$liczba = $_GET["input_number"]; if (intval($number)) ( ... wykonaj zapytanie SQL... )
Dlaczego to prowadzi do Wstrzyknięcie SQL? Chodzi o to, że użytkownik może określić w zmiennej numer_wejściowy oznaczający:
1"+POŁĄCZENIE+WYBIERZ
W takich przypadkach czek zostanie pomyślnie zdany, ponieważ funkcja intval pobiera wartość całkowitą zmiennej, tj. 1, ale w samej zmiennej $liczba nic się nie zmieniło, więc złośliwy kod zostanie przekazany do zapytania SQL.
Prawidłowe filtrowanie:
$liczba = intval($_GET["input_number"]); if ($number) ( ... wykonaj zapytanie SQL... )
Oczywiście warunek może się zmienić, na przykład jeśli potrzebujesz uzyskać tylko pewien zakres:
if ($liczba >= 32 ORAZ $liczba<= 65)

Jeśli używasz pól wyboru lub wielokrotnego wyboru z wartościami liczbowymi, sprawdź to:
$checkbox_arr = array_map("intval", $_POST["pole wyboru"]);
tablica_mapa
Spotykam się też z filtrowaniem w postaci:
$number = htmlspecialchars(intval($_GET["input_number"]));
htmlznaki specjalne
Lub:
$liczba = mysql_escape_string(intval($_GET["input_number"]));
mysql_escape_string

Tylko uśmiech może to spowodować :)

Filtrowanie. Błąd nr 2.
W przypadku zmiennych łańcuchowych stosuje się następujące filtrowanie:
$input_text = dodaje ukośniki($_GET["input_text"]);
Funkcja addslashes wymyka się specyfikacji. znaków, ale nie uwzględnia kodowania bazy danych i można ominąć filtrowanie. Nie będę kopiował tekstu autora, który opisał tę lukę, a po prostu podam link do Chrisa Shifletta (tłumaczenia można szukać w Runecie).

Użyj funkcji mysql_escape_string lub mysql_real_escape_string, przykład:
$input_text = mysql_escape_string($_GET["input_text"]);
Jeśli nie zamierzasz wejść tagi html, najlepiej wykonać następujące filtrowanie:
$input_text = strip_tags($_GET["input_text"]); $input_text = htmlspecialchars($input_text); $input_text = mysql_escape_string($input_text);
strip_tags - usuwa tagi html.
htmlspecialchars — konwertuje specjalne. znaków w encji html.
W ten sposób chronisz się przed atakami XSS, oprócz wstrzykiwania SQL.
Jeśli potrzebujesz tagów html, ale tylko do wyświetlania kodu źródłowego, to wystarczy użyć:
$input_text = htmlspecialchars($_GET["input_text"]); $input_text = mysql_escape_string($input_text);

Jeśli zależy Ci na tym, aby wartość zmiennej nie była pusta, użyj funkcji trim, na przykład:
$input_text = trim($_GET["input_text"]); $input_text = htmlspecialchars($input_text); $input_text = mysql_escape_string($input_text);

Filtrowanie. Błąd nr 3.
Chodzi o przeszukanie bazy danych.
Aby wyszukiwać według numerów, użyj filtrowania opisanego w pierwszym błędzie.
Aby wyszukiwać według tekstu, użyj filtrowania opisanego w drugim błędzie, ale z zastrzeżeniami.
Aby uniemożliwić użytkownikowi wykonanie błędu logicznego, musisz usunąć lub uciec od specjalnego. Znaki SQL.
Przykład bez dodatku. przetwarzanie linii:
$input_text = htmlspecialchars($_GET["input_text"]); // Wyszukaj: "%" $input_text = mysql_escape_string($input_text);
W rezultacie otrzymujemy zapytanie takie jak:
... WHERE text_row LIKE "%".$input_text."%" ... // WHERE text_row LIKE "%%%"
To znacznie zwiększy obciążenie bazy.
W moim skrypcie używam funkcji, która usuwa znaki, których nie chcę z wyszukiwania:
function strip_data($text) ( $quotes = array ("\x27", "\x22", "\x60", "\t", "\n", "\r", "*", "%", "<", ">", "?", "!"); $goodquotes = tablica ("-", "+", "#"); $repquotes = tablica ("\-", "\+", "\#"); $text = przyciąć(strip_tags($text)); $text = str_replace($quotes, "", $text); $text = str_replace($goodquotes, $repquotes, $text); $text = ereg_replace(" +" , " ", $tekst); zwróć $tekst; )
Oczywiście nie wszystkie powyższe symbole są niebezpieczne, ale w moim przypadku nie są potrzebne, więc wykonuję wyszukiwanie i podmianę.
Przykład zastosowania filtrowania:
$input_text = strip_data($_GET["input_text"]); $input_text = htmlspecialchars($input_text); $input_text = mysql_escape_string($input_text);
Radzę również ograniczyć liczbę znaków w wyszukiwaniu, przynajmniej nie mniej niż 3, ponieważ. jeśli masz dużą liczbę rekordów w bazie danych, to wyszukiwanie 1-2 znaków znacznie zwiększy obciążenie bazy danych.
Filtrowanie. Błąd nr 4.
Wartości zmiennych nie są filtrowane $_COOKIE. Niektórzy uważają, że skoro ta zmienna nie może być przekazana przez formularz, to jest to gwarancja bezpieczeństwa.
Ta zmienna jest bardzo łatwa do sfałszowania przez dowolną przeglądarkę, edytując pliki cookie witryny.
Na przykład w jednym znanym CMS-ie sprawdzano użyty szablon witryny:
if (@is_dir (MAIN_DIR . "/template/" . $_COOKIE["skin"]))( $config["skin"] = $_COOKIE["skin"]; ) $tpl->dir = MAIN_DIR . "/szablon/" . $config["skóra"];
W takim przypadku możesz zmienić wartość zmiennej $_COOKIE["skóra"] i zgłoś błąd, w wyniku którego zobaczysz bezwzględną ścieżkę do folderu witryny.
Jeżeli użyjesz wartości cookies do zapisu do bazy, to użyj jednego z wyżej opisanych filtrów, to samo dotyczy zmiennej $_SERWER.
Filtrowanie. Błąd nr 5.
Uwzględniono dyrektywę register_globals. Pamiętaj, aby go wyłączyć, jeśli jest włączony.
W niektórych sytuacjach można przekazać wartość zmiennej, która nie powinna była być przekazywana, np. jeśli witryna ma grupy, to dla grupy 2 zmienna $group powinna być pusta lub równa 0, ale wystarczy sfałszować formularz poprzez dodanie kodu:

Zmienna w skrypcie PHP $grupa będzie równy 5, jeśli nie został zadeklarowany w skrypcie z domyślną wartością.
Filtrowanie. Błąd 6.
Sprawdź pobrane pliki.
Sprawdź, czy:
  1. Rozszerzenie pliku. Wskazane jest wyłączenie ładowania plików z rozszerzeniami: php, php3, php4, php5 itp.
  2. Czy plik został przesłany na serwer move_uploaded_file?
  3. rozmiar pliku
Badanie. Błąd nr 1.
Natknąłem się na przypadki, gdy dla żądania AJAX (np. zwiększenie reputacji) została przekazana nazwa użytkownika lub identyfikator (do którego reputacja jest podwyższona), ale samo PHP nie sprawdzało istnienia takiego użytkownika.
Na przykład:
$user_id = intval($_REQUEST["user_id"]); ... INSERT INTO REPLOG SET uid = "($user_id)", plus = "1" ... ... UPDATE Użytkownicy SET reputacja = reputacja+1 WHERE user_id = "($user_id)" ...
Okazuje się, że tworzymy rekord w bazie danych, który jest dla nas zupełnie bezużyteczny.
Badanie. Błąd nr 2.
Wykonując różne czynności (dodawanie, edytowanie, usuwanie) z danymi, nie zapomnij sprawdzić uprawnień użytkownika do dostępu do tej funkcji oraz dodatkowe funkcje (użycie html tagi lub możliwość publikowania materiału bez weryfikacji).

Przez długi czas naprawiłem podobny błąd w jednym module forum, kiedy każdy użytkownik mógł edytować wiadomość administracyjną.

Badanie. Błąd nr 3.
W przypadku korzystania z wielu pliki php zrób prosty test.
W pliku index.php(lub w dowolnym innym pliku głównym) napisz tę linię przed dołączeniem innych plików php:
define("PLIKU WCZYTAJ", prawda);
Na początku innych plików php napisz:
if (! zdefiniowano ("READFILE")) ( exit ("Błąd, zła droga do pliku).
Przejdź do głównego."); }
Ograniczy to dostęp do plików.
Badanie. Błąd nr 4.
Używaj skrótów dla użytkowników. Pomoże to zapobiec wywołaniu określonej funkcji przez XSS.
Przykład kompilacji hasha dla użytkowników:
$secret_key = md5(strtolower("http://site.ru/" . $członek["imię"] . sha1($hasło) . date("Ymd"))); // $secret_key to nasz hash
Następnie we wszystkich ważnych formach zastąp dane wejściowe wartością bieżącego skrótu użytkownika:

Podczas wykonywania skryptu sprawdź:
if ($_POST["secret_key"] !== $secret_key) ( exit ("Błąd: secret_key!"); )
Badanie. Błąd nr 5.
Podczas wyprowadzania błędów SQL wprowadź proste ograniczenie dostępu do informacji. Na przykład ustaw hasło dla POBIERZ zmienną:
if ($_GET["passsql"] == "hasło") ( ... Wyjście błędu SQL... ) else ( ... Tylko informacja o błędzie, brak szczegółów... )
Ukryje to przed hakerem informacje, które mogą mu pomóc w zhakowaniu witryny.
Badanie. Błąd nr 5.
Staraj się nie dołączać plików, pobierając nazwy plików z zewnątrz.
Na przykład:
if (isset($_GET["nazwa_pliku"])) (uwzględnij $_GET["nazwa_pliku"] ..php"; )
Użyj przełącznika

Tworzę jedną prostą listę w PHP, na której użytkownik może dodać imię i nazwisko, wiek, e-maile itp. Dodałem również opcję usuwania, ale chcę dodać komunikat potwierdzający, gdy użytkownik kliknie przycisk usuwania.

Próbowałem googlować, ale znalazłem tylko rozwiązania jQuery i JavaScript. Czy można to zrobić tylko w PHP?

NazwaWiek"; while($query2=mysql_fetch_array($query1)) ( echo " ".$query2["nazwa"].""; Echo " ".$query2["wiek"].""; Echo " Edytować"; Echo " x"; } ?>

Usuń.php

w

Jeśli chcesz to zrobić tylko w PHP, musisz dodać "kroki" do swojego skryptu, na przykład:

Step1 (pokaż formularz) -> step2 (zapytaj o weryfikację) -> step3 (validate)

Aby to zrobić, możesz użyć sesji do zapisania treści formularza i parametru GET do śledzenia tego kroku. W przeciwnym razie najbardziej proste rozwiązanie jest użycie javascriptu:

Echo" x"; //użyj podwójnych cudzysłowów dla js wewnątrz php!

To jest to, czego potrzebujesz

While($query2=mysql_fetch_array($query1)) ( echo " ".$query2["nazwa"].""; Echo " ".$query2["wiek"].""; Echo " Edytować"; Echo " x"; ) in while($query2=mysql_fetch_array($query1)) ( echo " ".$query2["nazwa"].""; Echo " ".$query2["wiek"].""; Echo " Edytować"; Echo " x"; }

i utwórz funkcję javascript

Function ConfirmDelete(anchor) ( var conf = confirm("Czy na pewno chcesz usunąć ten rekord?"); if(conf) window.location=anchor.attr("href"); )

zaufaj mi, to działa 🙂

Dodaj zdarzenie onClick, aby wywołać okno dialogowe i javascript:return potwierdzić("czy na pewno chcesz to usunąć?");

Echo" x";

//dodaj zdarzenie onclick onclick="return deleteconfig()"

pracuj dla mnie, ale zmień to:

onclick="javascript:confirmationDelete($(this));return false;"

onclick="confirmationDelete(this);return false;"

Poniżej znajduje się wariant powyższego, który daje pole potwierdzenia i przekazuje zmienną z PHP do JavaScript iz powrotem do PHP.
Użyłem tego do wybrania przycisku radiowego, aby usunąć plik z listy plików.
Zobacz funkcję wyzwalacza OnClick o nazwie php $fileName w JavaScript, potwierdź nazwą pliku, a jeśli tak, przekaż href ze zmiennymi dla $_GET

Kod PHP/HTML:

$nazwa_pliku $extn $rozmiar $modtime "; ?>

Artykuł nie dotyczy klastrów, shardingu z replikacją, ani nawet chmur. Artykuł dotyczy budowy wysoce niezawodnej architektury obliczeniowej, w której liczba użytkowników i ich żądania mogą rosnąć jak lawina. A dla biznesu bardzo ważne jest, aby usługa internetowa akceptowała każde żądanie, przetwarzała je poprawnie i do końca (niezależnie od awarii i upadków niektórych komponentów) oraz miała gwarancję dostarczenia odpowiedzi do klienta. I oczywiście bez kosztów „miejsca” na sprzęt i pensji dla administratorów systemu.

Innymi słowy, przede wszystkim pomyśl - „czy tego potrzebuję”. Jeśli ktoś ma sklep internetowy, który sprzedaje gadające chomiki z obrotem 100 zamówień miesięcznie, prawdopodobnie nie. A jeśli planujesz prowadzić firmę, która może obsłużyć setki tysięcy i miliony użytkowników, wymaga dużej ilości obliczeń, pracuje z danymi o dużej wartości, gwarantuje transakcyjność każdego procesu biznesowego i wymaga równoległego przetwarzania danych, to jest to.

Sektor finansowy, duże sklepy internetowe z gamą setek tysięcy jednostek, aukcje internetowe, systemy rezerwacji hoteli i linii lotniczych, nowa chmura czy serwisy społecznościowe z ambicjami zdobycia miliona użytkowników już następnego dnia po rozpoczęciu kampanii reklamowej potencjalnie zainteresowany wysoce niezawodnym systemem dla Twoich usług internetowych.

Do kogo adresowany jest ten materiał

1. Deweloperzy dużych projektów internetowych, którzy są zainteresowani tworzeniem wysoce obciążonych i odpornych na błędy usług obliczeniowych.

2. Właściciele nowych lub rozwijających się firm, którzy oczekują „wybuchowego” wzrostu bazy użytkowników i stawiają wysokie wymagania części obliczeniowej.

3. Liderzy techniczni i kierownicy dużych projektów internetowych, którzy nie są zadowoleni z obecnego stanu i myślą o gruntownej reorganizacji.

Skąd tyle słów o „obliczeniach”?

Ponieważ najbliższa przyszłość dużych projektów internetowych leży w obszarze „ duże zbiory danych” („Big Data”) to trend, który w 2011 roku, obok wirtualizacji, oszczędzania energii i monitoringu, został odnotowany jako jeden z najważniejszych, a od 2013 roku mocno zajął swoje miejsce w branży, a nawet stał się jednym z akademickich przedmioty na dużych uczelniach zagranicznych.

Ilość danych do przetworzenia w celu rozwiązania problemów biznesowych dziś stale rośnie, aw przyszłości proces ten będzie tylko przyspieszał. O ile dziesięć lat temu wystarczyło pokazać użytkownikowi „sklep internetowy” z produktem, to rok temu wystarczyło przeanalizować ścieżkę użytkownika przez stronę i pokazać mu ściśle odpowiedni produkt (tzw. „technologie behawioralne” ), dziś uważa się za normę, aby wiedzieć wszystko o użytkowniku, w tym wzrost, wagę, wiek i imię ulubionego psa.

Prawa naturalnej konkurencji mówią, że jeśli chcesz zapewnić klientom więcej opcji niż konkurencja, potrzebujesz więcej danych i więcej obliczeń. I prędzej czy później Twój projekt, bez odpowiednich podejść, może w nich utonąć - tak jak topią się teraz różne projekty: ktoś z powodu złożoności obsługi, ktoś z powodu po prostu złego kodu, ktoś z powodu dużej łączności modułów, kogoś z powodu użycia zawodnych komponentów.

Dlatego każdy, kto dziś rozpoczyna projekt internetowy z wielkimi ambicjami, zyska więcej przewagi nad rywalami, jeśli początkowo będzie traktował swój projekt nie tylko jako kod programu, ale także jako środowisko obliczeniowe - system z własnymi prawami istnienia i rozwoju. Im więcej uwagi poświęcisz zarządzanie komputerami na początku, tym większe prawdopodobieństwo, że w nadchodzących latach będziesz wyprzedzać konkurentów.

Autor tych wierszy jest przekonany, że „klasyczne” nowoczesne podejście do budowania mocno obciążonych serwisów internetowych ma szereg poważnych wad. Zobaczmy dlaczego. Najpierw rozważ typowy nowoczesny schemat:

Klasyczne podejście do budowania mocno obciążonego serwisu internetowego

1. Wiele serwerów podzielonych na role.

2. Część serwerów (rola Frontendu) jest przeznaczona do zwracania zasobów statycznych (obrazów, CSS, plików JS) i „dystrybucji” frontu ruchu przychodzącego do dalszych węzłów. Głównym oprogramowaniem jest zwykle Nginx.

3. Dalsze węzły (rola Backend) są zaangażowane w obliczenia dynamiczne. Mówiąc najprościej, może to być typowy pakiet Apache + PHP.

4. Kolejna grupa serwerów przeznaczona jest do przechowywania danych. Są to MySQL, Memcache, Redis i tak dalej.

5. Sam kod usługi internetowej (w ten przykład– kod PHP) jest w równym stopniu kopiowany do wszystkich węzłów, w których jest Apache + PHP, i w równym stopniu przetwarza te żądania, które „padają” na jeden lub drugi węzeł.

6. Bazy danych są „rozmazane” nad swoją grupą serwerów za pomocą jakiejś formy shardingu, a ich obciążenie jest równoważone w podobny sposób.

Uważny czytelnik zauważy, że równoważenie DNS i CDN nie są wymienione w schemacie, ale autor celowo je pominął, aby nie komplikować schematu. Zasada działania tych elementów jest bardzo podobna do powyższej.

Wady klasycznego podejścia

1. Główny trend to komplikacje. W trakcie trwania projektu „klasyczny” schemat staje się coraz bardziej skomplikowany. Dodając każdy nowy serwer, musisz wprowadzić go do schematu dystrybucji ruchu. Oczywiście w dużych projektach wprowadzenie serwera do roli to działanie „jednego przycisku”, niemniej jednak jest to wzrost infrastruktury, którą należy wesprzeć. A tym bardziej – w przypadku, gdy dotychczasowe pojemności dla bazy danych już nie wystarczają, a trzeba „żyć” (bez zatrzymywania usługi) przenieść lub rozprowadzić bazę danych na nowe serwery.

Ale głównym problemem jest to, że zwiększenie klastra dystrybucji ruchu nie prowadzi do zmniejszenia złożoności kodu programu. Możesz bezbłędnie zbudować klaster, ale kod pozostanie taki sam.

2. Wdrożenie nie jest atomowe. W prostych słowach, układanie Nowa wersja projekt na serwerach bojowych zajmuje trochę czasu. Konieczne jest fizyczne pobranie plików na N-dwadzieścia maszyn, dokonanie zmian w bazie danych, zresetowanie pamięci podręcznych (wiele różnych pamięci podręcznych), a jednocześnie na wszystkich serwerach zakończenie przetwarzania żądań „starym kodem” i rozpoczęcie przetwarzania za pomocą „ nowy kod". W przeciwnym razie może powstać wiele drobnych konfliktów, gdy część żądania użytkownika zostanie przetworzona w stary sposób, część w nowy sposób, jakaś część trafi do bazy danych według „starego schematu”, część „wg nowego” oraz wkrótce.

Dlatego idealnie każdy chce wstrzymać usługę na czas trwania aktualizacji (kilka sekund lub kilkadziesiąt sekund), a następnie ponownie ją włączyć. W rzeczywistości, przy przepływie co najmniej 1000 żądań na sekundę, nikt tego nie robi, woląc regularnie naprawiać drobne kolizje „ręcznie” lub pokrywając je programowaniem defensywnym, które obsługuje wsteczną kompatybilność „do siódmej generacji”. O tym, jak regularne wsparcie Kompatybilność wsteczna komplikuje życie programistom (i podnosi koszt projektu jako całości) - mądry czytelnik może sam myśleć.

3. Używany przez HTTP. Protokół HTTP jest oczywiście przestarzały moralnie i technicznie, a jeśli go przestrzegasz (na przykład) rozwój mobilny- wiesz, że wszędzie zastępują go lżejsze protokoły. Ale główna wada jest inna: protokół HTTP „w przeglądarce” wymaga zakończenia pętli - wymaga odpowiedzi w ograniczonym czasie. Zobowiązuje to usługę do obliczenia i przygotowania odpowiedzi ściśle w krótkim czasie, na który pozwala przeglądarka. Jeśli usługa jest ten moment przeciążony - żądanie zostanie utracone na zawsze.

Dlatego w „typowych projektach internetowych” uciekają się do różnych sztuczek, takich jak Long polling lub jakaś inna forma cyklicznych zapytań, co nie tylko komplikuje architekturę, ale także obciąża serwis niepotrzebnym „marnowanym ciągnięciem”.

4. Inicjalizacja skryptu na żądanie. Jest to konsekwencja wykorzystania HTTP i języków skryptowych takich jak PHP, które zgodnie z wieloletnią tradycją są restartowane w odpowiedzi na każde żądanie. Tak, tak, w odpowiedzi na każde z 1000 żądań na sekundę skrypt PHP uruchomi się ponownie, ponownie zainicjuje wszystkie zmienne i ponownie nawiąże połączenie z bazą danych. W praktyce zdarza się, że przetworzenie żądania zajmuje 0,005 sekundy, a skrypt jest inicjowany w kolejności 0,05 sekundy - dziesięć razy dłużej!

Innymi słowy, przez 90% czasu serwery są zajęte nie przetwarzaniem żądań klientów, ale bezużyteczną inicjalizacją skryptów. Spróbuj zamienić to na pieniądze. Dlatego wymyślono wiele obejść, takich jak buforowanie OPcode, trwałe połączenia z bazami danych, lokalne pamięci podręczne, takie jak Memcache lub Redis, mające na celu zrównoważenie tego nieprzyjemnego efektu.

5. Aplikacja monolityczna. Bez względu na to, jak podzielisz aplikację na moduły, bez względu na to, jak bardzo starasz się rozłożyć kod w złożonej strukturze katalogów, bez względu na to, z jakiego leniwego autoładowania korzystasz, jest tylko jedno kryterium: jeśli musisz przesłać całą aplikację do przesłania co najmniej jedna zmiana, masz aplikację monolityczną. Nic więcej nie jest podane.

W literaturze masowo opisano wady zastosowań monolitycznych. Jedną z głównych można pokrótce wymienić: jeśli chcesz, aby nawet najmniejsza funkcja pojawiła się w produkcji w ciągu godziny, musisz zmieścić cały łańcuch produkcyjny w godzinę. Definicja problemu, implementacja, sprawdzanie kompatybilności wstecznej, pisanie testów, pisanie dokumentacji, przeprowadzanie przez dział testów manualnych, naprawianie błędów - wszystko w ciągu godziny.

Bo jeśli wgrywasz całą aplikację dokładnie o 00 minut każdej godziny, to pod koniec każdej godziny powinieneś przynieść cała aplikacja do stanu stabilnego.

6. Interfejs sieciowy jest renderowany przez backend. W typowym przypadku wygląd zewnętrzny(i odpowiednio kod HTML) stron projektu jest z reguły renderowany po stronie Backend w odpowiedzi na każde żądanie. To nadmierne, nieuzasadnione wydatkowanie środków i pieniędzy.

7. Podział polityczny wydziałów. Dział administratorzy systemu jest odpowiedzialny za upewnienie się, że front ruchu przychodzącego jest „rozmazany” na wielu serwerach, na których działa kod PHP. Za kod PHP odpowiada dział programowania. Jeśli kod PHP nie miał czasu na przetworzenie konkretnego żądania, to nie jest jasne, kto jest za to odpowiedzialny - czy administrator, który „przepuścił” zbyt duży ruch na serwer i go przeładował, czy programista, który napisał nie -optymalny skrypt. Jeśli baza danych zaczyna zwalniać, to nie jest też jasne, kto pozostaje ekstremalny: administrator, który nie zorientował się na czas, aby ją „shardować”, czy programista, który również mógł to rozgryźć.

Jeśli przykłady wydają Ci się przesadzone i „nie z prawdziwego życia”, pamiętaj, że autor pracował w projekcie, który był hostowany na 200 serwerach, a 60 z nich było zajętych przez bazę danych. Aż strach pamiętać, ile osób zatrudniono do obsługi tego projektu.

Główna wada

Powtarzamy powyższą myśl: według autora główną wadą klasycznego schematu jest to, że specjaliści techniczni optymalizują niewłaściwą rzecz. Optymalizują przychodzący front żądań, właściwie „rozmazują” go na dużej grupie maszyn, zamiast optymalizować samą istotę – część obliczeniową. I jest to o wiele łatwiejsze niż się wydaje.

teoretyczny ideał

Och, jak bardzo chcielibyśmy:

1. Pozbądź się wielu drogich serwerów i użyj jednej lub dwóch małych grup.

2. Porzuć schemat Nginx->Apache->PHP z jego dzikimi „narzutami” pod względem zużywanych zasobów, które kosztują pieniądze.

3. Wyeliminuj potworny koszt inicjowania skryptów PHP z tego samego powodu.

4. Odrzuć potrzebę „renderowania” stron na backendzie. Byłoby marzeniem, gdyby usługa internetowa mogła działać z niestabilnym połączeniem internetowym lub bez niego (na przykład podczas korzystania sieć mobilna na drodze).

5. Pozbądź się „pętli” timeoutu HTTP, dostarcz odpowiedź klientowi tylko wtedy, gdy ta odpowiedź jest gotowa i z gwarancją dostarczenia.

6. Aktualizuj projekt w małych częściach, bez zatrzymywania i bez utraty pojedynczego żądania klienta.

7. Nie martw się o utracone żądania, jeśli część projektu (jakiś komponent) "upadł" lub został tymczasowo wyłączony w celu debugowania.

Nierealny? Łatwo!

Pierwsze kroki w kierunku doskonałości

Najpierw nauczmy się Co musi być zrobione, dlatego omówimy - Jak.

1. Zaprojektuj cały system jako SOA(architektura zorientowana na usługi) z ESB (Enterprise Messaging Bus), rezygnując z podejścia monolitycznego, tak aby każda niezależna część logiki biznesowej była przetwarzana przez oddzielną „usługę” i komunikowałyby się ze sobą za pośrednictwem niezależnej magistrali wymiany.

2. Puść synchroniczność. Na przykład w schemacie synchronicznym „żądanie – przetwarzanie – odpowiedź” jest to jedna pętla HTTP, która nie ma ścisłej kontroli zakończenia i można ją łatwo przerwać. W trybie asynchronicznym istnieją trzy oddzielne procesy: żądanie (wysłane i potwierdzone), przetwarzanie (ponowna próba w przypadku niepowodzenia), dostarczenie odpowiedzi (z gwarancją).

3. Podziel projekt na dwie aplikacje- Frontend i Backend. W przypadku serwisu internetowego frontend to (zazwyczaj) aplikacja JavaScript. Najważniejsze jest to, że aplikacje działają asynchronicznie i oddzielone od siebie, wymieniając wiadomości za pomocą dwukierunkowego protokołu komunikacyjnego.

4. Zrezygnuj z HTTP na korzyść WebSocket. Protokół WebSocket ma fantastyczną wydajność w porównaniu do HTTP, nie posiada żadnych „pętli z przekroczeniem czasu” i pozwala na przesyłanie dowolnych danych (w tym danych binarnych) w obu kierunkach.

5. Zapewnij „pamięć” do uruchamiania zapytań. Po otrzymaniu żądania od klienta powiedz „Potwierdź” klientowi i zapisz to żądanie. Gdy tylko backend zostanie zwolniony z poprzedniego cyklu przetwarzania, przekaż do niego żądanie. Dopóki żądanie „chodzi” między węzłami zaplecza, zachowaj je od momentu „wejścia” do węzła i zatwierdź je, gdy tylko „opuściło” węzeł. Tak więc, jeśli jakiś węzeł „upadnie”, system nie straci żądania i natychmiast odeśle je z powrotem do przetwarzania. Po zakończeniu przetwarzania wyślij wynik do klienta i przechowuj go, dopóki klient nie powie „Potwierdź”.

6. Zapewnij przetwarzanie równoległe dla tych operacji, które mogą być wykonywane równolegle pod względem logiki biznesowej, a sekwencja dla tych, które muszą być wykonywane ściśle sekwencyjnie. Ten paragraf nie został napisany z pretensjami do „Kapitan Oczywistości”, ale po to, aby pokazać, że żaden proces biznesowy nie może być „na ślepo nałożony” na kod wielowątkowy.

7. Zrezygnuj ze skryptów na rzecz „demonów”. Demon to proces, który zaczyna się raz, a potem stale „zawiesza” w pamięci. Ponieważ działa cały czas, nie trzeba tracić czasu na ponowne inicjowanie każdego żądania. Patrząc w przyszłość powiem, że demon PHP (podczas korzystania z nowoczesnych wersji PHP) nie różni się zasadniczo od zwykłego skryptu PHP.

Podejście projektowe SOA

SOA — architektura zorientowana na usługi — nie jest nowa. To modułowe podejście do tworzenia oprogramowania zostało zainicjowane przez IBM w ubiegłym stuleciu, a obecnie jest wspierane i promowane przez liderów branży, głównie w produktach „na poziomie przedsiębiorstw” w .NET i JAVA.

W klasycznym podejściu do programowania serwisów internetowych w Języki PHP i podobne - projektowanie zaczyna się od modeli, ich właściwości i operacji na nich. Modele reprezentują obiekty świata rzeczywistego, a operacje reprezentują działania na obiektach. Jednak, jak pokazuje praktyka, świat rzeczywisty jest znacznie bardziej wieloaspektowy i złożony, a także o wiele skuteczniej opisany językiem zdarzeń i reakcji na nie (więcej szczegółów w poście #1593 z opisem i przykładami).

Świat realny składa się ze zdarzeń, które zachodzą jednocześnie (w sensie programowania – „równolegle”) i najczęściej bez naszego udziału, i na które zachodzą lub nie zachodzą różne reakcje. Reakcje z kolei mogą generować następujące zdarzenia. SOA idealnie nadaje się do „programowania w świecie rzeczywistym”, ponieważ najwygodniej jest operować zdarzeniami, relacjami między nimi i reakcjami na nie. Co więcej, w właściwe podejście do organizacji architektury, a zdarzenia i reakcje na nie będą występować równolegle, nawet jeśli używasz „jednowątkowego” języka programowania, takiego jak PHP.

Musisz zaprojektować produkt SOA w oparciu o to, jakie zdarzenia występują w Twojej logice biznesowej, w jaki sposób są ze sobą powiązane lub powinny następować po sobie, jakie reakcje powinny wystąpić w odpowiedzi na określone zdarzenia i kto dokładnie będzie przetwarzał te lub inne zdarzenia. inne wydarzenia. Implementacja modeli danych i działań na nich schodzi na dalszy plan (zamknięta w „usługę”), a na pierwszy plan wysuwa się lista „usług” i plan interakcji między nimi (inter-service API).

Realizacja: pierwsze przybliżenie

1. Frontend jako niezależna aplikacja. Do implementacji nadaje się każdy popularny framework JavaScript-MVC. Jednak z praktyki zauważam, że jeśli zaczniesz boleć, gdy połączysz słowa „JavaScript, MVC i framework” w jednym zdaniu, będzie to dla ciebie trudne. Aplikacja musi być w stanie narysować wszystkie swoje „ekrany” bez odwoływania się do zaplecza, umożliwiać użytkownikowi nawigację (przejścia między „ekranami”) również bez odwoływania się do zaplecza oraz obsługiwać dwukierunkowy kanał komunikacji z zapleczem.

2. Punkt wejścia do połączenia WebSocket z backendem. Istnieje wiele gotowych rozwiązań w NodeJS, które obsługują również żądania awaryjne dla długiego odpytywania i ajax dla ideologicznie przestarzały przeglądarki. Zwracam uwagę na przyszłość, że ten węzeł może być również dostępny przez czysty HTTP, jeśli potrzebujesz napisać kilka bramek z usługami innych osób, jednak, dla uproszczenia, możesz napisać oddzielny węzeł „czystego HTTP”.

Punkt wejścia zapewni dwukierunkowy kanał komunikacji z aplikacją front-end (innymi słowy z przeglądarką), akceptując od niej żądania i zwracając do niej odpowiedzi.

3. Przechowywanie uruchomionych żądań w systemie. W tym celu najlepiej nadaje się popularny serwer AMQP, który zapewnia kolejki komunikatów i routing między nimi. Gdy tylko nadejdzie kolejne żądanie od klienta, umieść je w kolejce „przychodzące”. Ponadto zostanie on wyodrębniony z tej kolejki przez demona, który przeanalizuje zawartość żądania i wyśle ​​go do „routingu” w całym systemie (co w rzeczywistości oznacza przeniesienie go do jednej lub większej liczby kolejek zgodnie z określonymi algorytmami). Każdy demon, zajmujący się własną częścią logiki biznesowej, odbierze tę lub inną wiadomość ze „swojej” kolejki przychodzącej, przetworzy ją i umieści odpowiedź w kolejce „wychodzącej”.

Zaznaczam, że w terminologii popularnego brokera RabbitMQ nie ma pojęcia kolejek wychodzących. Komunikaty publikowane są na giełdzie (wymienniku), skąd sam broker jest przenoszony do określonych kolejek zgodnie z regułami routingu. Tutaj jest tak napisany dla warunkowego zrozumienia, że ​​odpowiedź nie jest wysyłana bezpośrednio do wnioskodawcy.

4. Demoniczna kontrola(kierownik). Aby uruchomić prosty skrypt PHP jako demon, po prostu zapakuj kod wykonywalny w while(true) (...) i wpisz wiersz poleceń coś takiego jak „php twój-skrypt.php”. Ale lepiej jest użyć do tego dowolnego odpowiedniego przełożonego, który zasadniczo zrobi to samo, ale także zapewni niezbędny stan środowiska, monitoruje stan procesu i zrobi kilka bardziej przydatnych rzeczy.

W rzeczywistości demon PHP jest nieco bardziej skomplikowany: musi odbierać sygnały sterujące i komunikaty rekonfiguracyjne, nie może przeciekać pamięci, musi utrzymywać (lub przywracać porzucone) połączenia z bazą danych - ale generalnie nie jest to bardziej skomplikowane niż skrypty PHP, do których jesteś przyzwyczajony.

O krok bliżej rzeczywistości: podejście oparte na zdarzeniach w SOA

Niektóre (w opinii autora przestarzałe) podejścia do budowania aplikacji modułowych opierają się na zasadzie RPC (Remote Procedure Calling), która implikuje bezpośrednie wywołanie określonych metod lub procedur w zdalnym komponencie projektu. Takie podejście całkowicie niszczy wszystkie zalety SOA, ponieważ zwykle oznacza bezpośrednie i twarde połączenie między węzłem wysyłającym i wykonującym. Przy projektowaniu i wdrażaniu złożonego produktu należy w jak największym stopniu przestrzegać zasady luźno powiązanych komponentów, ponieważ to złożoność architektury i kodu ostatecznie determinuje koszt posiadania (korekty i zmiany w produkcie po jego uruchomieniu).

Podejście sterowane zdarzeniami w SOA zakłada, że ​​komponenty (usługi) komunikują się ze sobą poprzez wysyłanie zdarzeń asynchronicznych („zdarzeń”, od słowa Event). Zdarzenie to komunikat (na przykład w terminologii AMQP), który ma nazwę (nazwę) i zestaw parametrów. Zdarzenie ma na celu poinformowanie systemu, że coś się stało, lub „zadanie pytania” systemowi. W ogólnym przypadku zdarzenia wysyłane są „do systemu” (dokładniej do wspólnej szyny ESB) bezadresowo – czyli bez konkretnych intencji dostarczenia do konkretnych węzłów lub wykonawców.

Wręcz przeciwnie, określone węzły (komponenty, usługi) nasłuchują na wspólnej szynie pewnych zdarzeń, na które są gotowe odpowiedzieć. Może to oznaczać, że usługa jest gotowa na usłyszenie jakiegoś zdarzenia i wykonanie odpowiedniej akcji. Albo serwis ma pewną wiedzę (na przykład posiada bazę danych z informacjami o użytkownikach) i jest gotowy do ich dostarczenia w odpowiedzi na „prośbę”. W obu przypadkach efektem odpowiedzi na zdarzenie jest wygenerowanie nowego zdarzenia (o innej nazwie i innych parametrach), które mogą być słyszane również przez inne zainteresowane serwisy.

Przy odpowiedniej organizacji generała Opony ESB, usługi wysyłają i odbierają zdarzenia asynchronicznie, nie czekając na siebie. Oznacza to, że usługa wysyłająca może bez opóźnień czasowych wysłać dowolną liczbę zdarzeń do ESB i przejść do rozwiązywania następujących zadań. Porównaj to z klasycznym HTTP, co oznacza oczekiwanie na odpowiedź w bieżącym cyklu przetwarzania, a zobaczysz korzyści. Usługi odbierające będą również otrzymywać nowe zdarzenia asynchronicznie, niezależnie od siebie, natychmiast po zakończeniu przetwarzania poprzedniego zdarzenia.

Model zdarzenia SOA w kodzie

Krótko mówiąc, musisz patrzeć na swój kod nie jako klasy z funkcjami (metodami), ale jako zdarzenia i akcje, które występują w odpowiedzi na te zdarzenia. Ponadto rezultatami działań są również zdarzenia. W odniesieniu do omawianej architektury możemy powiedzieć, że zdarzenia lokalne to zdarzenia, które wystąpiły w konkretnym skrypcie PHP, a zdarzenia zdalne to zdarzenia, które przyszły do ​​tego skryptu z kolejki AMQP (lub są tam wysyłane w wyniku). Jeśli tak potraktujesz cały swój kod, od razu doprowadzi to do zaskakującego i bardzo ważnego efektu:

Jeśli zdarzenia lokalne i zdalne są takie same, obsługa zdarzeń lokalnych i zdalnych jest taka sama!

Dlaczego to takie ważne? Ponieważ programiści z twojego zespołu kontynuują pisanie zwykłego kodu PHP, nie myśląc o tym, gdzie to lub tamto zdarzenie zostanie przetworzone - właśnie tam, w tym lub sąsiednim skrypcie PHP, lub gdzieś na drugim końcu systemu, w innym demonie, pod adresem przynajmniej w innym języku programowania. Jeśli tworzysz projekt z publicznym API, każdy uczestnik zewnętrzny będzie mógł „podpisać” swój kod do Twoich wydarzeń (i przetworzyć je) lub odwrotnie – wysłać Ci własny, aby przetwarzać jego wydarzenia jako żądania (i zarabiaj za to, jeśli korzystasz z modelu biznesowego SAAS typu pay-per-use, takiego jak Amazon).

Pamiętaj, co nazwaliśmy główną wadą klasycznych dużych projektów internetowych - stale rośnie złożoność, a co za tym idzie koszt posiadania, koszt wsparcia i zmian. W przypadku architektury SOA sterowanej zdarzeniami − złożoność stale się zmniejsza, ponieważ „złożone węzły” można łatwo podzielić na niezależne usługi (w tym przypadku demony), podczas gdy zasady systemu pozostają niezmienione, a jego wydajność tylko wzrasta.

Wdróż nową wersję bez utraty bieżących procesów

Ponieważ nie masz już systemu monolitycznego, nie musisz wdrażać całego systemu. Co więcej, wdrożenie komponentu (usługi, demona) może zająć dowolny czas, oczywiście w rozsądnych granicach. Ważne jest to, że w czasie wdrażania (tych kilku sekund lub kilkudziesięciu sekund) komponentu, cały projekt ani na chwilę nie przerywa usługi. Jak to jest zrobione?

Po prostu wyłączasz usługę, która wymaga aktualizacji. Zaktualizuj jego kod i strukturę bazy danych (jeśli to konieczne), a następnie uruchom go ponownie. Wszystkie bieżące żądania do tej usługi będą czekać w kolejce AMQP do momentu pojawienia się usługi. Zwracam uwagę, że ponieważ usługi są niewielkie (mała ilość kodu potrzebna do rozwiązania tylko niewielkiej części logiki biznesowej) - jest to znacznie szybsze niż wdrożenie całej monolitycznej aplikacji. Ale w każdym razie nie będzie strat.

Problemy z interfejsem internetowym

Szybki, responsywny interfejs sieciowy jest warunkiem wstępnym dla projektu o dużym obciążeniu. Zobaczmy, dlaczego interfejs sieciowy może ogólnie „zwolnić” przy klasycznym podejściu do implementacji:

1. Interfejs jest rysowany na backendzie, który jest przeciążony i robi to powoli. Przechodzenie między stronami jest powolne. Nawet z AJAX bloki przerysowują się zbyt wolno.

2. Kod źródłowy interfejsu (HTML, CSS, JS) jest redundantny i powoli przesyłany kanałami komunikacji, zwłaszcza jeśli odbywa się to przy wczytywaniu każdej strony podczas nawigacji użytkownika po interfejsie.

3. Interfejs zawiera dużą ilość niezoptymalizowanej logiki JavaScript, która działa wolno na słabych urządzeniach (głównie na urządzeniach mobilnych).

Spróbujmy rozwiązać te problemy:

Jak zrobić szybki i responsywny interfejs WWW

1. Po pierwsze i najważniejsze źródło interfejs powinien przesyłane do klienta nie więcej niż raz. Jedynym cywilizowanym sposobem osiągnięcia tego jest stworzenie pełnoprawnej aplikacji JavaScript. Jest pobierany do klienta raz (w tym przypadku możesz pokazać piękny animowany preloader), a następnie przez cały czas pracy z usługą klient nie będzie już musiał czekać na pobranie.

2. Wszystkie przejścia między „ekranami” interfejsu sieciowego muszą być wykonane wewnątrz aplikacji JavaScript, a w żadnym wypadku jako osobne żądania do zaplecza. Istnieje na to określenie „pojedyncza aplikacja internetowa”, w której nawigacja odbywa się zasadniczo poprzez przełączanie „ekranów”, podczas gdy zawartość pasek adresu zmienia się dynamicznie, tworząc pełne wrażenie klasycznych „przejść między stronami”.

3. Wysyłanie wiadomości (zdarzeń) do backendu i otrzymywanie odpowiedzi musi być odseparowane od siebie i od nawigacji użytkownika(asynchroniczny). Wspomniany WebSocket z natury „rekomenduje” właśnie taką implementację. Wszystkie długie operacje również nie powinny blokować interfejsu, chyba że zostało to specjalnie zrobione w tym celu.

W ten sposób użytkownik potrzebuje tylko połączenia internetowego do pierwszego pobrania aplikacji (kilka sekund). Co więcej, może współpracować z usługą nawet przy chwilowym braku połączenia (na przykład z urządzenia mobilnego w metrze, poza miastem, w przepełnionym hotelu za granicą itp.) - aplikacja zarejestruje żądania i spróbuje wysyłać je, gdy tylko pojawi się Internet, a tym samym otrzymam odpowiedzi.

Oczywiście nie zwalnia to programisty z konieczności optymalizacji i minimalizacji kodu. Jednak, jak pokazuje praktyka (na przykład usługa Trello), to zadanie nie jest trudniejsze niż inne.

Uwaga dla wątpiących twórców usług internetowych dla urządzeń mobilnych: zgodnie z praktyką autora, w 2013 roku jednostronicowe aplikacje JavaScript na transporcie websocket z powodzeniem działają na iPadzie.

Praca użytkownika z wielu urządzeń

Z pracy korzysta z Twojej usługi z pracy komputer stacjonarny, w drodze do domu wyjmuje iPhone'a, aw domu włącza tablet. Jeśli użytkownik wysłał jakieś polecenie z interfejsu do serwisu, to czeka na odpowiedź o wynikach przetwarzania. Łatwo zrozumieć, że jeśli (kiedy) przetwarzanie zajęło trochę czasu, odpowiedź powinna zostać wysłana do urządzenia, z którego użytkownik korzysta (przepraszam za kalambur) dostawa odpowiedzi, a nie w momencie złożenia wniosku.

Problem polega na tym, że nie można jednoznacznie powiedzieć, czy użytkownik przestał używać (przepraszam ponownie) tego lub tamtego konkretne urządzenie. Może zamknął przeglądarkę. Może jego bateria jest rozładowana. Może wyszedł do tunelu metra, gdzie nie ma połączenia, i za pół minuty pojawi się ponownie. Opcji jest wiele, a autor jest nieznany Najlepszym sposobem definicje. Jednak oto, co może Ci się przydać:

1. Nagraj (na backendzie) wszystkie urządzenia użytkownika oraz czas ostatniej aktywności z każdego z nich.

2. Klasyfikuj zdarzenia systemowe, które mają być zgłoszone użytkownikowi, na te, które należy dostarczyć tylko do aktywnych urządzeń i te, które muszą być dostarczone „rozgłoszenie” (do wszystkich urządzeń).

3. Wejdź dodatkowa warstwa abstrakcje - usługa, która przechwyci pewne zdarzenia, które są interesujące dla użytkownika i stworzy z nich komunikaty. W ten sposób możesz łatwo rozesłać ten sam komunikat o powodzeniu operacji - w kilku rodzajach: krótkie powiadomienie w urządzenie przenośne, trochę bardziej autentyczny - dla przeglądarki, szczegółowa wiadomość - by e-mail.

4. Zapewnij kolejki do wysyłania do każdego użytkownika w każdym oddzielnym kanale komunikacji (interfejs WWW, urządzenie mobilne, poczta). Standardowa funkcja AMQP pomoże również w przekroczeniu limitu czasu wygaśnięcia wiadomości, tak aby leżały tam nie dłużej niż przez określony czas i nie zatykały systemu. Gdy użytkownik przejdzie do trybu online przez określony kanał, zostaną mu dostarczone nowe, oczekujące wiadomości tego konkretnego typu.

Autor może dodać, że w oparciu o ten sam system można zbudować wysyłkę powiadomień z opóźnieniem (które zostaną wysłane nie wcześniej niż w określonym terminie), a nawet wysyłanie prawdziwej papierowej korespondencji okresowej (ustawy, płatności itp.). ), ale to temat na osobny artykuł .

Podkreślę najważniejsze: nie traktuj dostarczonych wiadomości jako pewnego rodzaju powiadomień, do których jesteś przyzwyczajony na Facebooku lub Vkontakte. Dostarczone wiadomości są wyniki zapytania użytkownik! Wszystkie działania użytkownika w interfejsie, implikujące pewnego rodzaju żądania do zaplecza, otrzymują odpowiedzi w jednolitej formie „dostarczonych wiadomości”, za pośrednictwem jednego zunifikowanego kanału komunikacji. A potem algorytm aplikacji internetowej rozumie, co należy zrobić z tą lub inną wiadomością - narysuj powiadomienie tekstem, dodaj linię w tabeli, przełącz coś w interfejsie i tak dalej.

Obliczenia równoległe i sekwencyjne

Byłoby bezużyteczne projektowanie szybkiego frontendowego interfejsu sieciowego, jeśli masz wolny backend. Nie, nie chodzi tu o strumienie, nie o widelce ani o Erlanga. Pozostajemy na zwykłym PHP, dostępnym dla każdego początkującego/średnio zaawansowanego programisty.

Dlaczego w ogóle potrzebujemy równoległości? Nawet jeśli nie mówimy ogólnie o wadach języków jednowątkowych, równoległość znacznie przyspiesza kalkulację zadania, co oznacza, że ​​znacznie zmniejsza wymagania dotyczące zasobów sprzętowych (sprzętowych) i zwiększa satysfakcję użytkownika z pracy w interfejs (szybciej uzyskują wyniki).

Weź dowolny dość złożony proces biznesowy w swoim projekcie internetowym i narysuj go jako łańcuch kroków. Otrzymasz sekwencję działań w systemie od żądania do odpowiedzi. Najprawdopodobniej najpierw będą kontrole, następnie wykonanie głównego zadania, następnie podzadania drugorzędne, a na końcu wynik wyniku. Przyjrzyj się uważnie: czy któreś z działań można wykonać równolegle?

Podam przykład: powiedzmy, że użytkownik chce kupić jakąś usługę, która jest dołączona jako dodatkowa płatna opcja w jego planie taryfowym. Liczba opcji jest ograniczona. Jeśli opcja jest włączona pomyślnie, musisz wysłać powiadomienie do użytkownika w przeglądarce, wysłać duplikat wiadomości e-mail, odliczyć pieniądze z jego konta rozliczeniowego i powiadomić dział klienta. Narysuj łańcuch:

1. System otrzymał żądanie włączenia opcji.
2. Autoryzujemy użytkownika i ustalamy jego plan taryfowy.
3. Sprawdzamy, czy w ogóle jest możliwe włączenie tej opcji przez plan taryfowy użytkownik.
4. Sprawdź, czy użytkownik ma wystarczającą ilość pieniędzy na koncie.
5. Sprawdź, czy ta opcja nie powoduje konfliktu z innymi ustawieniami.
6. Jeśli wszystko jest w porządku, włącz opcję.
7. Wysyłamy powiadomienie do przeglądarki.
8. Powiadomienie wysyłamy pocztą.
9. Odpisz pieniądze na rachunku.
10. Powiadamiamy dział klienta.

Uważny czytelnik może znaleźć błąd w kolejności działań, ale autor przypomni, że jest to przykład przybliżony.

Co widzimy? Zauważ, że nie ma powodu, aby wykonywać wszystkie kroki po kolei. Dużo bardziej poprawne byłoby „zrównoleglenie” 3,4,5 na trzy wątki, a na końcu 7,8,9,10 na cztery wątki.

Myślisz o przepływach i widłach? Na próżno masz SOA!

Jak wykonywać obliczenia równoległe w SOA

Czytelnikom, którzy właśnie przewinęli artykuł do tego momentu, wyjaśnię, że nie chodzi o zrównoleglenie tego samego zadania w SOA - do tego w ogólnym przypadku wystarczy uruchomić demona w instancjach N i zająć się kłótnia o dostęp do bazy danych.

Tak więc w tym przykładzie mamy trzy-cztery-kilka różnych zadań, które są wykonywane przez różne usługi i które chcemy wykonywać równolegle. Nie jest trudno wysłać je do przetwarzania równoległego: wystarczy wysłać jedno zdarzenie „czy użytkownik o nazwie użytkownika może włączyć opcję X?”, a wszystkie usługi subskrybujące to zdarzenie przechwycą je, wykonają swoje sprawdzenia i wyślą powstałe zdarzenia.

Problem polega właśnie na zebraniu tych wynikowych zdarzeń, gdy potrzebujemy całkowitego wyniku ich pracy, aby przejść dalej. Na przykład na powyższej liście potrzebujemy wyniku 3+4+5, a 7+8+9+10 można zignorować.

W rzeczywistości, przy wysokich wymaganiach dotyczących obowiązkowej realizacji transakcji, musisz kontrolować każdy łańcuch do końca, ale omówimy to później.

Oczywiście, jeśli nasz demon będzie „zawieszał się i czekał”, zużywając zasoby (tzw. „bezczynny”), to nie można zbudować takiej wysoko obciążonej usługi. Chodzi tylko o to, że demon rozwiązuje inne zadania i obsługuje inne prośby innych klientów, a trzy osobne „wątki” (3,4,5) rozwiązują swoje podzadania. Trudności dodaje również fakt, że wynikające z tego zdarzenia mogą następować w dowolnej kolejności. Wszystko to jest jednak rozwiązywane łatwo i prosto:

O ile autor wie, żadna z istniejących obecnie nieszablonowych implementacji AMQP nie pozwala na oczekiwanie i „sklejanie” kilku zdarzeń w jedno, aby otrzymać tylko to – jeden wynik. Musisz więc sam o to zadbać, tak:

1. Przed wysłaniem wydarzenia do AMQP, zobowiąż się do szybka pamięć(użyj dowolnego odpowiedniego magazynu w pamięci) listę nazw wynikowych zdarzeń, których oczekuje usługa, a także nazwę zdarzenia (nazwijmy je „R”), które ma zostać wysłane z sumą wyniki.

2. Następnie usługa kończy cykl przetwarzania bieżącego zdarzenia i jest zwalniana do kolejnych zadań.

3. Gdy tylko nadejdzie dowolne zdarzenie z listy, które zapisaliśmy w pamięci, usługa „rozpakuje” je i zapisze jego zawartość w pamięci, przypisując je nazwie zdarzenia. Jednocześnie sprawdza, czy na liście znajdują się inne zdarzenia, na które nie otrzymano odpowiedzi. Jeśli tak, pętla kończy się w tym miejscu.

4. W przeciwnym razie, to znaczy, jeśli wszystkie zdarzenia z listy otrzymały odpowiedź, usługa skleja je i wysyła sklejony wynik pod nazwą zdarzenia „R” zapamiętanego wcześniej. Następnie, w celu zaoszczędzenia pamięci, lista z wynikami jest po prostu usuwana z pamięci - nie jest już potrzebna.

5. Ta sama usługa lub inna (według uznania projektanta systemu) otrzymuje wynikowe zdarzenie „R” ze wszystkimi wynikami przetwarzania równoległego. To, co następuje, jest oczywiste.

Jeśli z opisu wydawało Ci się, że to długo, to wyjaśnię - mówimy o tysiącach i dziesiątkach tysięcy zdarzeń na sekundę (!) Na jednym przeciętnym serwerze.

Korzystanie z magazynu w pamięci oznacza, że ​​nawet jeśli usługa zostanie zatrzymana (upadek, aktualizacja), bieżący proces biznesowy nie zostanie utracony. Po ponownym uruchomieniu usługa będzie nadal odbierać zdarzenia z ESB i przetwarzać je zgodnie z opisanym powyżej algorytmem.

Transakcyjność, wycofanie operacji (rollback) i scenariusze awarii w SOA

Ponieważ w SOA zdarzenia równorzędne „przechodzą” przez magistralę ESB, potrzebujesz pewnego rodzaju wskazania, aby wskazać, że „ta odpowiedź tutaj” odnosi się do „tego żądania tam”. Nie ma tu potrzeby wymyślać koła na nowo – w specyfikacji dowolnego popularnego protokołu znajdziesz parametr o nazwie w rodzaju korelacji_id. Zazwyczaj jest to ciąg. Musi być zawarty w parametrach wszystkich zdarzeń każdego indywidualnego procesu biznesowego, od wejścia do wyjścia, w celu zidentyfikowania łańcucha komunikatów, który należy do tego procesu biznesowego. Patrząc od strony interfejsu internetowego, aplikacja internetowa przechowuje bieżące aktywne (wysłane) żądania i, dzięki identyfikatorowi korelacji, „rozumie” w odpowiedzi, na które żądanie nadeszła każda konkretna odpowiedź.

Zajmijmy się terminologią: transakcyjność jest właściwością systemu polegającą na wykonywaniu kilku czynności jako jednej wspólnej operacji, która ma sens i może być wykonana tylko w całości. Ponieważ fizycznie niemożliwe jest wykonanie kilku operacji atomowych w systemie rozproszonym z równoległymi wątkami, system przewiduje tak zwane scenariusze niepowodzeń i wycofania.

Scenariusz niepowodzenia to zazwyczaj działanie, które należy podjąć, gdy wystąpi błąd. Wycofanie to w tym kontekście skrypt działań, które należy wykonać, aby „cofnąć” serię poprzednich działań, które ostatecznie doprowadziły do ​​błędu. Z grubsza mówiąc, wycofania to procesy będące odwrotnością normalnych procesów biznesowych w systemie.

Cofanie zmian nie zawsze jest potrzebne i nie zawsze jest możliwe. Na przykład, jeśli podłączyłeś jakąś opcję do użytkownika, a następnie „upadła” na rozliczeniu, to opcję można z powrotem wyłączyć. Cóż, możesz spróbować ponownie, automatycznie lub za pomocą drugiego polecenia użytkownika. A jeśli fizycznie usunąłeś treść, a niektóre kolejne operacje nie zadziałały ... Sytuacja jest niejednoznaczna.

Dlatego należy mądrze podejść do scenariuszy niepowodzenia i wycofania. Autor może zalecić następującą ścieżkę: Zawsze pisz skrypty niepowodzenia, ale nie zawsze pisz wycofywania. Twoje awarie zostaną natychmiast odzwierciedlone w monitoringu - a zespół wsparcia będzie mógł szybko naprawić sytuację własnymi rękami, zdobyć doświadczenie i sformułować wymagania techniczne dla programistów. Podczas gdy napisanie wyjątkowo poprawnego przywrócenia od zera może być bardzo, bardzo trudne.

Niemniej jednak należy rozumieć, że wycofywanie, wycofywanie, obsługa błędnych sytuacji to te same procesy biznesowe, co wszystko inne. Opierają się również na zdarzeniach przechodzących przez system. Będzie fajnie, jeśli początkowo w każdym zdarzeniu i każdym handler'u wyznaczysz dwie przeciwstawne ścieżki (w przód i w tył, sukces i porażkę), aby wdrożyć je w określonych zadaniach.

Skalowanie SOA

Każdy system prędzej czy później będzie musiał zostać rozbudowany. W przypadku SOA odbywa się to łatwo i naturalnie:

1. Zduplikowany punkt wejścia. Odnosi się to do tej samej bramy WebSocket, którą rozważaliśmy na początku artykułu. Może być duplikowany nieograniczoną liczbę razy, ponieważ komunikacja między nim a klientem jest ujednolicona i oddzielona od wnętrza systemu, a komunikacja między nim a systemem z kolei jest oddzielona od komunikacji z klientem.

2. Zduplikowane instancje(instancje) usług. Usługi, które nie wymagają bazy danych lub tylko „odczytują” z nich, są bezproblemowo duplikowane. A zwykła funkcjonalność RabbitMQ pozwoli Ci zapisać N instancji do tej samej kolejki, z których wiadomości będą losowo docierać w jednej lub drugiej instancji. Podczas duplikowania usług, które zajmują się aplikacjami zewnętrznymi (bazy danych, oprogramowanie innych firm), należy rozważyć, w jaki sposób te aplikacje dostarczają żądania transakcyjne od kilku równoległych klientów.

3. Powielanie magazynów danych. Tutaj możesz swobodnie używać dowolnego znanego Ci shardingu. Jeśli masz do czynienia z 10 milionami użytkowników i wydaje ci się to dużo, podziel przez 10 milionów zasad (na przykład na podstawie CRC32 z loginu użytkownika lub innej metody round-robin). Jeśli baza danych jednej usługi stale rośnie i staje się bardziej złożona, podziel ją na dwie usługi.

4. Powielanie brokera AMQP i pamięć masowa. Z autorskiej praktyki RabbitMQ i Redis doskonale spełniają swoją rolę. Jeśli masz sprzęt w więcej niż jednej szafie DC, wybierz tryb królika, który jest odporny na awarie połączenia sieciowego.

5. Pełna kopia maszyny. Z nowoczesne technologie wirtualizacji (KVM) i konfiguracji (Chef), zadanie „podniesienia tej samej maszyny” sprowadza się do naciśnięcia jednego przycisku.

Szyfrowanie ruchu między frontendem a backendem

Zalecane jest zorganizowanie połączenia WebSocket przez SSL. Co więcej, zwiększa „penetrację” przeciwko zacofanym dostawcom biurowym, którzy blokują „każdy dziwny ruch” z wyjątkiem HTTP[S].

Jeśli jednak wydaje ci się to niewystarczające, możesz zorganizować generowanie par kluczy przy każdym logowaniu klienta (jedna para na interfejsie, druga na zapleczu), wymieniać klucze publiczne i dalej szyfrować cały ruch za pomocą zwykłego RSA.

Ochrona przed DDOS i podobnymi nadużyciami

Autor świadomie pomija kwestię ochrony „niskopoziomowej” w przypadku zalania kanałów SYN i zalania setkami gigabitów kanałów, ponieważ na ten temat napisano setki książek specjalistycznej literatury. Porozmawiajmy o tym, jak chronić system już wewnątrz, na jego logicznym poziomie, gdy atakujący znalazł sposób na „zalanie” systemu (SOA + ESB) tysiącami zdarzeń.

1. Pierwsza zasada: nic nie powinno być przetwarzane, dopóki nie zostanie potwierdzona jego ważność. Jeśli jako dane wejściowe oczekujesz małego tekstu JSON opakowanego w BASE64, przychodzący ciąg dłuższy niż megabajt powinien zostać jawnie odrzucony — nie próbuj go rozpakowywać. Podobnie wygląda ciąg zawierający znaki „niełacińskie”. Po rozpakowaniu łańcucha nie próbuj od razu robić json_decode, najpierw sprawdź liczbę i parzystość nawiasów. I tak dalej.

Wygląda to na paranoję, ale poza tym można łatwo „zapełnić z pamięci”, czyli doprowadzić do awarii usługi, zmuszając ją do zajęcia całej dostępnej pamięci RAM.

2. Usługa przetwarzająca wiadomości przychodzące nie powinna niczego zapisywać w pamięci, bazie danych i innych magazynach. Powód jest ten sam. Najpierw upewnij się, że wiadomość jako całość jest poprawna, a dopiero potem pozwól jej „wgłębić się” w system.

3. Usługa, którą można zmusić do „przejścia do bazy danych”, często musi być chroniona przez buforowanie. Prostym przykładem jest usługa autoryzacji użytkowników. Jeśli nie jest chroniony, atakujący może wysłać tysiące żądań autoryzacji z rzędu, przeciążając w ten sposób bazę danych.

4. Przy wejściu do systemu potrzebna jest usługa odrzucająca żądania z „podejrzanych” źródeł, np. z adresów IP z „czarnej listy”.

Brzmi jak klasyczna rada, więc o co chodzi? Główny problem polega na tym, że jeśli umieścimy usługę filtrująco-analizującą przy wejściu do systemu (jak to ma miejsce w klasycznych projektach webowych), to wydajność całego systemu nie będzie wyższa niż wydajność tego analizatora-filtra. Przeglądanie każdego zgłoszenia pod kątem podejrzeń „w czasie rzeczywistym” jest niezwykle kosztowne. Możesz i powinieneś to zrobić po fakcie, a oto jak:

1. Stwórz usługę, która będzie „nasłuchiwać” wszystkich wiadomości w systemie. W RabbitMQ osiąga się to poprzez subskrypcję klucza routingu „#”.

2. Naucz tę usługę pewnych zasad, według których może diagnozować „podejrzanych” nadawców. Na przykład są to nadawcy, którzy wysyłają zbyt wiele podobnych wiadomości w odstępie czasu, lub którzy wysyłają powtarzające się wiadomości, lub którzy wysyłają wiadomości w imieniu tego samego użytkownika z różnych adresów IP... Opcji jest wiele, włącz swoją wyobraźnia. Jednocześnie nie ma znaczenia, jak szybko taka usługa działa (oczywiście w rozsądnych granicach) – nie wpływa to na szybkość całego systemu.

3. Gdy tylko serwis stwierdzi, że taki a taki nadawca jest podejrzany, wysyła to zdarzenie do systemu i kontynuuje swoją działalność.

4. Umieść na wejściu bardzo prostego i szybkiego demona - usługę filtrującą, której zadaniem będzie po prostu "głupie" blokowanie podejrzanych nadawców. Bez analizy, bez analizowania, bez dodatkowych kosztów. Łatwo zgadnąć, kto jest uważany za podejrzanego: serwis dowie się o nich z wydarzeń opisanych w poprzednim akapicie i doda je do swojej wewnętrznej czarnej listy.

Koniec pierwszej części. Ciąg dalszy: SOA: architektura rozproszona i jej utrzymanie.

Jestem mentorem w projektach informatycznych. Oznacza to, że jeśli jesteś właścicielem lub liderem, mogę pomóc Ci wznieść się na nowe wyżyny. Uporządkuj procesy, zrozum motywację zespołu, wdrażaj narzędzia i osiągaj określone cele. Nie uczę robienia interesów, a jedynie pomagam ominąć hojnie rozrzucone grabie na swojej drodze. .