Nowoczesne architektury strumieniowe dla telemetryki IoT wykorzystują Apache Kafka jako rozproszony szkielet zdarzeń, obsługując miliony wiadomości na sekundę z trwałą persystencją i poziomą skalowalnością. Apache Flink służy jako silnik przetwarzania strumieniowego, dostarczając prawdziwe semantyki strumieniowe z zaawansowanymi możliwościami przetwarzania czasu zdarzeń i koordynując transakcje Kafka, aby zagwarantować semantykę dokładnie-jednokrotnie w całym potoku. Zarządzanie stanem wykorzystuje wbudowane bazy danych RocksDB z przyrostowymi asynchronicznymi migawkami do Amazon S3, co pozwala na operacje stanowe w skali terabajtów bez wyczerpywania pamięci JVM. Aby uzyskać natychmiastowe alerty, gorące wyniki agregacji są materializowane w Redis, podczas gdy dane historyczne trafiają do S3 Glacier za pośrednictwem tabel Apache Iceberg dla efektywnych kosztowo zapytań analitycznych.
Inteligentna sieć energetyczna monitoruje dwa miliony inteligentnych liczników generujących dziesięć tysięcy zdarzeń na sekundę, wymagając wykrywania anomalii w sieci energetycznej w ciągu 500 milisekund, aby zapobiec awariom kaskadowym. Główne wyzwanie polega na przetwarzaniu zdarzeń, które przybywają z pięciominutowym opóźnieniem z powodu izolacji sieci komórkowej, eliminowaniu duplikatów z logiki ponawiania licznika oraz łączeniu telemetryki o wysokiej prędkości z wolno zmieniającymi się danymi referencyjnymi zawierającymi metadane kalibracji urządzenia. Inżynierowie wcześniej borykali się z fałszywymi pozytywami spowodowanymi zdarzeniami przychodzącymi w nieodpowiedniej kolejności i utratą danych podczas szczytowych obciążeń, co wymagało solidnej architektury, która utrzymuje dokładność bez rezygnacji z odpowiedzi w czasie rzeczywistym.
Rozwiązanie 1: Architektura Lambda z przetwarzaniem w czasie rzeczywistym i wsadowym
Początkowa propozycja przyjęła wzorzec Architektury Lambda. Apache Spark Streaming napędzał warstwę szybkości dla przybliżonych widoków w czasie rzeczywistym, podczas gdy nocne zadania wsadowe Spark SQL przeliczały dokładne wyniki na HDFS dla poprzednich 24 godzin.
Zalety: Dojrzały ekosystem z rozbudowanym zestawem narzędzi, prosta tolerancja błędów dzięki replikacji HDFS oraz wyraźny podział obowiązków między warstwami szybkości i wsadowymi.
Wady: Duplikacja kodu między logiką strumieniową a wsadową stwarza znaczne obciążenie konserwacyjne i błędy synchronizacji. Przetwarzanie terabajtów dziennie wiąże się z nieproporcjonalnymi kosztami obliczeniowymi i narusza wymóg poprawy anomalii w czasie poniżej jednej sekundy z powodu latencji wsadowej.
Rozwiązanie 2: Kafka Streams z wbudowanymi magazynami
Drugi projekt rozważał Kafka Streams z wbudowanymi magazynami stanu RocksDB działającymi bezpośrednio na podzespołach aplikacji, co unika zarządzania zewnętrznym klastrem.
Zalety: Uproszczona topologia operacyjna bez oddzielnych klastrów przetwarzania, ścisła integracja z grupami konsumentów Kafka oraz automatyczne zarządzanie przydziałem partycji.
Wady: Skalowanie operacji stanowych wyzwala kosztowne przebalansowywanie wszystkich partycji, powodując znaczące skoki latencji. Obsługa zdarzeń z opóźnieniem wymaga skomplikowanej logiki ekstrakcji znaczników czasowych, ponieważ domyślne okna opierają się na czasie przetwarzania, a nie czasie zdarzenia. Ograniczenia pamięci na serwerach aplikacji poważnie ograniczają całkowity rozmiar stanu, uniemożliwiając duże agregacje okien.
Rozwiązanie 3: Apache Flink z semantyką czasu zdarzeń
Wybrana architektura wdrożyła Apache Flink na Kubernetes, wykorzystując semantykę przetwarzania czasu zdarzeń z znacznikami wodnymi i zewnętrznymi przyrostowymi punktami kontrolnymi do Amazon S3.
Zalety: Natywne przetwarzanie czasu zdarzeń przez znaczki wodne i konfiguracje allowedLateness obsługuje dane przychodzące w nieodpowiedniej kolejności bez potrzeby stosowania logiki niestandardowej. Semantyka dokładnie-jednokrotnie jest osiągana dzięki dwuetapowym zobowiązaniom koordynującym punkty kontrolne Flink z transakcjami Kafka. Przyrostowe migawki RocksDB umożliwiają niezależne skalowanie obliczeń i stanu, wspierając operacje na kluczach w skali terabajtów bez obciążenia pamięci.
Wady: Znaczna złożoność operacyjna wymaga głębokiej wiedzy w zakresie strojenia punktów kontrolnych, synchronizacji znaczników wodnych oraz zarządzania ciśnieniem zwrotnym. Menedżer zadań Flink może stanowić potencjalny punkt awarii, co wymaga konfiguracji wysokiej dostępności Kubernetes.
Wybrane rozwiązanie i wynik
Przyjęliśmy rozwiązanie 3, konfigurować BoundedOutOfOrdernessWatermarks Flink z pięciominutową tolerancją i przyrostowymi punktami kontrolnymi RocksDB co 30 sekund. Eliminacja duplikatów została osiągnięta dzięki włączeniu idempotentnych producentów Kafka i transakcyjnych zapisów koordynowanych przez protokół dwuetapowego zaangażowania Flink. Warstwowanie danych do S3 Glacier wykorzystało strategie kompresji Apache Iceberg, aby utrzymać zapytania historyczne bez nadmiernych kosztów przechowywania.
Ta architektura osiągnęła 300 ms latencji alertów p99 i 99,99% dokładności przetwarzania podczas prób produkcyjnych. System płynnie obsługiwał trzygodzinną izolację sieci komórkowej, odtwarzając dane z offsetów Kafka po przywróceniu kontrolki, bez utraty danych. Koszty przechowywania zmniejszyły się o 60% w porównaniu do wcześniejszego rozwiązania HDFS, podczas gdy pulpity nawigacyjne Grafana zapewniły rzeczywistą widoczność opóźnienia znaczników wodnych Flink i metryk czasu trwania punktów kontrolnych.
Pytanie: Jak Apache Flink utrzymuje semantykę dokładnie-jednokrotnie podczas zapisu do Kafka i co zapobiega duplikacji zapisów podczas ponownego uruchamiania zadań?
Flink implementuje dokładnie-jednokrotnie za pomocą protokołu dwuetapowego między barierą punktu kontrolnego a transakcją Kafka. W fazie wstępnego zaangażowania dane są przesyłane do Kafka za pomocą unikalnego transactional.id, ale pozostają niezatwierdzone, dopóki punkt kontrolny nie zakończy się pomyślnie. Jeśli punkt kontrolny zawiedzie, Flink przerywa transakcję, powodując, że Kafka odrzuca dane; po ponownym uruchomieniu Flink przywraca stan producenta z ostatniego pomyślnego punktu kontrolnego, aby zapobiec transakcjom zombie z niekompletnych zapisów. Kandydaci często przeoczają, że transactional.id musi zawierać identyfikator punktu kontrolnego, aby zapewnić idempotencję między ponownymi uruchomieniami, oraz że Flink wymaga konfiguracji setTransactionalIdPrefix, aby uniknąć kolizji w wielodostępnych klastrach Kafka.
Pytanie: Dlaczego okna czasowe po stronie zdarzenia powodują eksplozję stanu w operacjach kluczowych, a jak można to złagodzić podczas przetwarzania nieograniczonych strumieni identyfikatorów urządzeń?
Okna czasowe po stronie zdarzenia powodują eksplozję stanu, ponieważ Flink musi buforować wszystkie zdarzenia dla każdego klucza, dopóki znacznik wodny nie przejdzie przez czas zakończenia okna plus skonfigurowany czas allowedLateness. Dla kluczy o wysokiej karcie, takich jak unikalne identyfikatory urządzeń, gromadzi to miliony równoległych stanów okien w RocksDB, ostatecznie zużywając wszystkie dostępne zasoby dyskowe i pamięciowe. Złagodzenie wymaga wdrożenia konfiguracji State TTL (Czas Życia), aby automatycznie wygaszać nieaktualne okna, konfigurowania buforów zarządzanych pamięcią RocksDB, aby ograniczyć użycie poza stertą, oraz korzystania z przyrostowych punktów kontrolnych, aby zmniejszyć koszty migawki. Kandydaci często przeoczają, że bez jawnej ewikcji okien lub ustawień TTL stan backendowy rośnie w nieskończoność, aż menedżer zadań napotka błąd pamięci (Out-Of-Memory error), szczególnie podczas przetwarzania danych historycznych przychodzących z opóźnieniem.
Pytanie: Jak rozwiązujesz problem rozkładu kluczy gorących, gdy pojedyncze uszkodzone urządzenie IoT generuje 100-krotną normalną objętość zdarzeń, przeciążając konkretny podzadanie Flink?
Problem rozkładu kluczy gorących występuje, gdy haszowanie partycji koncentruje obciążenia o dużej objętości na pojedynczych instancjach zadań, co prowadzi do ciśnienia zwrotnego i skoków latencji w całym potoku. Rozwiązaniem jest solenie kluczy – dodawanie losowego sufiksu (np. 0-9) do gorących kluczy podczas początkowego mieszania, aby rozdzielić przetwarzanie na wiele podzadań, a następnie usunięcie sufiksu i ponowne agregowanie wyników w kolejnym globalnym oknie. Alternatywnie można wdrożyć lokalną wstępną agregację kluczy przy użyciu AggregateFunction Flink przed mieszaniem, aby zredukować ruch sieciowy lub wykorzystać przyklejone partycjonowanie Kafka do spowolnienia konkretnych producentów. Kandydaci często przeoczają, że solenie zwiększa wolumen mieszania danych i rozmiar stanu, co wymaga starannego zrównoważenia między zyskami w zakresie równoległości a kosztami zarządzania syntetycznymi kluczami w RocksDB.