Злиття треків у реальному часі — це, по суті, stateful-задача. Відмітка радара, перехоплення SIGINT або виявлення БпЛА, що надійшли по каналу, нічого не означають самі по собі — їхня цінність повністю визначається зіставленням з накопиченою історією кожного об'єкта, у існування якого система вже вірить. Щоб вирішити, чи продовжує спостереження наявний трек, чи починає новий, рушій повинен пам'ятати все, що вже бачив. Саме ця єдина вимога — довговічна, змінювана, пер-трекова пам'ять, що оновлюється з тією частотою, з якою сенсори виробляють дані — відрізняє потокову обробку треків від простих stateless-перетворень, на яких будується більшість конвеєрів даних. Ця стаття розглядає, як stateful-потокова обробка проектується для злиття в реальному часі: модель стану, віконне групування за часом події, семантика exactly-once, партиціонування для масштабування та операційні режими відмов, які виводять ці конвеєри з ладу.

Чому злиття треків є inherently stateful

Stateless-конвеєр застосовує чисту функцію до кожної події та забуває її. Така модель підходить для збагачення, конвертації форматів або фільтрування, але не може зливати треки. Питання злиття — «чи належить це спостереження до треку, який я вже підтримую?» — можна вирішити лише з посиланням на стан: поточну оцінку позиції та швидкості кожного активного треку, його невизначеність (коваріацію), час останнього оновлення та історію асоціацій.

Конкретно: рушій підтримує один запис на трек, ключований за стабільним ідентифікатором треку. Цей запис містить стан фільтра Калмана (або частинкового фільтра), мітку часу останнього застосованого спостереження, коротку історію звітів сенсорів-учасників, а також класифікацію та впевненість треку. Коли надходить нове спостереження, рушій зчитує стани треків-кандидатів, вирішує, якому треку (якщо такий є) належить спостереження, використовуючи алгоритм кореляції треків, оновлює стан фільтра узгодженого треку на місці та генерує оновлення. Сховище стану зчитується та записується на гарячому шляху кожного окремого спостереження — ось чому його проектування домінує у продуктивності та надійності всієї системи.

Сховище стану: де живе пам'ять треків

У виробничому потоковому рушії стан треків зберігається не у звичайній пам'яті застосунку. Він знаходиться у керованому, відмовостійкому сховищі стану, яке фреймворк може зберігати у вигляді чекпоінтів та відновлювати. Apache Flink підкріплює стан із ключем вбудованим екземпляром RocksDB на задачу; Kafka Streams матеріалізує стан у локальних сховищах RocksDB, підкріплених компактним топіком журналу змін, що дозволяє повне відновлення після аварії. У будь-якому випадку контракт однаковий: рушій надає відображення з ключем — ідентифікатор треку до стану треку — яке є локальним, швидким і довговічним після перезапусків.

Ключові рішення при проектуванні сховища стану — це розмір та шаблон доступу. Стан треку має бути компактним: вектор середнього значення фільтра, матриця коваріації, кілька полів метаданих і обмежений кільцевий буфер посилань на останні спостереження — але не повна історія спостережень. Необмежена пер-трекова історія є найпоширенішою причиною розростання стану. Доступ переважно являє собою операцію читання-зміни-запису за одним ключем на спостереження, тому сховище оптимізовано для точкових пошуків та оновлень на місці, а не для сканування. Діапазонні сканування, коли вони потрібні для просторового стробування, виносяться з гарячого шляху шляхом підтримки вторинного просторового індексу, що оновлюється асинхронно.

Серіалізація стану заслуговує на окрему увагу. Оскільки фреймворк записує та зчитує стан треків на диск під час кожного чекпоінту та відновлення, серіалізатор для класу стану треку є компонентом гарячого шляху, а не деталлю реалізації. Рефлексивний серіалізатор зі схемою при зчитуванні, що обходить граф об'єктів для кожного запису, домінуватиме у завантаженні CPU при високих швидкостях спостережень; написаний вручну або автоматично згенерований серіалізатор, що розкладає стан фільтра фіксованого розміру у плаский байтовий буфер, часто є на порядок швидшим. Та сама дисципліна окупається в розмірі чекпоінту — компактне кодування стану фіксованої ширини зменшує знімки та скорочує вікно відновлення після відмови вузла.

Обмеження зростання стану

Кожен трек, який створено, але ніколи не видалено, назавжди займає стан. Конвеєр без дисциплінованого видалення буде монотонно нарощувати своє сховище стану, поки чекпоінти не сповільняться і рушій не відстане від реального часу. Три механізми видалення працюють разом: time-to-live (видалити трек, який не отримував спостережень протягом N секунд), обмеження кількості пропусків (видалити трек, для якого протягом M послідовних очікуваних вікон були лише передбачення без оновлень) та відсів за зоною інтересу (видалити треки, що покидають операційний район). Видалення — це не господарське обслуговування, яке можна відкласти, — це вимога коректності та стабільності, і воно має виконуватися на тому самому годиннику часу подій, що й решта конвеєра, щоб поведінка була ідентичною під час живої обробки та відтворення.

Час події, водяні мітки та віконне групування

Сенсорні потоки не надходять по порядку і не надходять вчасно. Відмітка радара, зафіксована о 09:47:03.120, може надійти до рушія злиття на 400 мс пізніше, ніж перехоплення SIGINT того самого об'єкта, зафіксоване о 09:47:03.080, просто тому, що два потоки проходять різні мережі та етапи обробки. Якби рушій корелював за часом надходження подій (часом обробки), він регулярно не асоціював би спостереження, які насправді описують один і той самий об'єкт в один і той самий момент.

Тому потокове злиття керується часом події — міткою часу, коли сенсор зафіксував об'єкт — і використовує водяні мітки для оцінки повноти. Водяна мітка — це оцінка рушія про те, що жодних подій із часом події, ранішим за водяну мітку, вже не надійде. Вікна кореляції закриваються, коли водяна мітка проходить їхній кінець, плюс налаштований пільговий період допустимого запізнення, що тримає вікно відкритим рівно стільки, щоб дочекатись запізнілих спостережень. Спостереження, що запізнились понад пільговий період, не відкидаються мовчки — вони направляються до бічного виходу, щоб аналітики могли перевірити, скільки даних пропустило своє вікно та налаштувати пільговий період відповідно.

Вибір пільгового періоду — це пряме балансування між затримкою та повнотою. Довший пільговий період вловлює більше запізнілих звітів і забезпечує повнішу кореляцію, але кожне оновлення треку успадковує цю затримку до надходження до оператора. Для тактичних наземних треків типовий пільговий період становить кілька секунд; для повітряних треків, де обов'язкова субсекундна затримка, пільговий період скорочується до десятків або низьких сотень мілісекунд, приймаючи, що деякі запізнілі звіти будуть оброблятися як корекції треку, а не кореляції всередині вікна.

Ключовий висновок: Найскладніше рішення налаштування у потоковому злитті треків — це не фільтр і не алгоритм асоціації, а пільговий період водяної мітки. Встановіть його занадто коротким — і рушій розіб'є один об'єкт на дублікати треків, бо скорельовані звіти пропустять вікна одне одного; встановіть занадто довгим — і кожне оновлення треку надходитиме із запізненням, що підриває довіру операторів. Виміряйте розподіли запізнення надходження для кожного сенсорного потоку та визначайте пільговий період за даними, а не інтуїцією.

Семантика exactly-once для цілісності треків

У конвеєрі злиття семантика доставки — це не академічне питання: вона визначає коректність оперативної картини. Розглянемо доставку at-least-once, при якій відмова може спричинити відтворення спостереження. Якщо той самий сигнал радара застосовується до фільтра Калмана двічі, фільтр сприймає це як два незалежних вимірювання та стає штучно впевненим, зменшуючи коваріацію та зміщуючи оцінку в бік одного зашумленого показника. Трек виглядає більш певним, але насправді є більш хибним — найгірша можлива відмова для системи, на підставі якої командири приймають рішення.

Семантика exactly-once усуває це, гарантуючи, що кожне спостереження впливає на стан треку рівно один раз — навіть після аварій та перезапусків. Механізм — атомарне збереження чекпоінтів: рушій periodично робить знімок сховища стану та зміщень вхідних даних, що він спожив, зберігаючи обидва разом. Під час відновлення він відновлює знімок і продовжує споживання зі збережених зміщень, тому спостереження, які вже були враховані у стані, ніколи не застосовуються повторно. Flink реалізує це за допомогою розподілених бар'єрів чекпоінтів; Kafka Streams використовує транзакційні записи, що поєднують оновлення журналу змін сховища стану та зміщення вихідного топіка в єдину транзакцію.

Exactly-once не безкоштовно. Чекпоінти мають завершуватися швидше, ніж інтервал між ними, інакше конвеєр накопичує незбережений стан і врешті-решт зупиняється. Тривалість чекпоінту масштабується з розміром стану — ось друга причина важливості агресивного видалення треків. Конвеєр, що зберігає 50 000 застарілих треків, робить чекпоінти повільно; той самий конвеєр, що зберігає лише кілька тисяч дійсно активних треків, робить чекпоінти за мілісекунди. Обмеження стану — це те, що робить exactly-once доступним за оперативного темпу. Для конвеєрів, яким також потрібен відтворюваний, незмінний запис кожної зміни стану, журнал чекпоінтів природно поєднується з аудитним журналом на основі event sourcing.

Партиціонування та масштабування stateful-операторів

Одна задача не може зливати весь простір бойових дій при високих швидкостях сенсорів, тому потік розбивається на розділи та обробляється паралельно. Визначальне обмеження stateful-злиття полягає в тому, що два спостереження одного фізичного об'єкта мають маршрутизуватися до одного розділу — інакше вони потрапляють до окремих сховищ стану, ніколи не зустрічаються, і для об'єкта виникають паралельні треки, які жоден оператор не може узгодити.

Ключ розділу тому повинен зберігати локальність кореляції. Ключування за ідентифікатором сенсора відразу не працює, бо весь сенс у тому, щоб корелювати між сенсорами. Ключування за грубою географічною коміркою працює добре: усі спостереження в межах регіону потрапляють до однієї задачі, яка зберігає стан об'єктів у цьому регіоні. Виклик — обробка меж: об'єкт, що перетинає межу комірки, має бути переданий між розділами без втрати або дублювання його треку. Практичні системи використовують комірки, що перекриваються, або спеціальний етап узгодження меж для управління передачею, та розмірюють комірки так, щоб жодна не стала вузьким місцем, що перевантажує одну задачу, поки інші простоюють.

Оскільки стан є локальним для розділу, перемасштабування stateful-конвеєра — це не просто додавання воркерів. Рушій має перерозподілити стан із ключем при перебалансуванні розділів між вузлами: Flink робить це, зчитуючи збережений стан чекпоінту та переприсвоюючи групи ключів; Kafka Streams відтворює топіки журналів змін для відновлення локальних сховищ на новому примірнику. Обидва обмежені розміром стану, що знову ж таки підкреслює: дисциплінований бюджет стану є основою будь-якої іншої властивості системи. Той самий розподілений журнал, що доставляє спостереження до рушія злиття, детальніше описано в нашій статті про архітектуру черги повідомлень для оборонних конвеєрів даних.

Від дельт треків до оперативної картини

Рушій злиття повинен публікувати зміни, а не стан. Щоразу, коли трек створюється, оновлюється або видаляється, рушій генерує дельта-подію до нижчестоящого топіка, на який підписуються загальна оперативна картина та інші споживачі. Публікація дельт замість повних знімків стану підтримує чутливість відображення навіть тоді, коли кількість активних треків сягає десятків тисяч, бо споживач застосовує невеликі інкрементальні зміни замість перерендерингу всього світу при кожному тику.

Кожна дельта несе монотонно зростаючу версію на трек, щоб споживачі могли виявляти та виправляти доставку не по порядку — застосування старішого оновлення після новішого телепортувало б трек назад. Схема подій є формальним контрактом між злиттям та його споживачами; заморожування та версіонування цієї схеми дозволяє оперативній картині, аналітичним інструментам і архівам розвиватися незалежно від ядра злиття. В кінцевому підсумку добре налаштований stateful-конвеєр утримує затримку від спостереження сенсора до оперативної картини в однозначних секундах для наземних треків і менше секунди для повітряних, де домінуючою змінною є пільговий період водяної мітки, а не обчислення всередині оператора.

Одну операційну властивість варто сформулювати прямо: stateful-потоковий рушій є настільки надійним, наскільки надійною є його поведінка при відтворенні. Оскільки рішення про асоціацію та видалення обидва виконуються на годиннику часу подій, записану сесію роботи сенсора можна подати назад через ідентичний конвеєр для відтворення точної картини треків, яку бачив оператор — за умови, що кожен оператор (асоціація, віконне групування, видалення) є детермінованим за заданих стану та вхідних даних. Саме цей детермінізм робить систему такою, що піддається тестуванню та акредитації: інженер може змінити поріг асоціації, відтворити відомий сценарій і порівняти отримані треки з базовим значенням, а не чекати повторення поведінки в полі. Ставтесь до недетермінізму — зчитування системного годинника, ітерація по невпорядкованих відображеннях, зведення чисел з плаваючою крапкою, порядок яких залежить від планування потоків — як до дефектів, бо кожен з них руйнує відтворення і разом з ним — здатність перевірити, що рушій злиття поводиться однаково двічі.

Побудуйте злиття, яке встигає за темпом бою

Corvus HEAD приймає різнорідні сенсорні потоки та зливає їх в єдину, безперервно оновлювану картину треків — stateful-потокова кореляція, розроблена для реального оперативного темпу. Цілісність треків exactly-once, кореляція за часом події та дельта-потокова передача до оперативної картини в одному розгортуваному пакеті.

Дізнатись про Corvus HEAD → Замовити брифінг

Цей аналіз підготовлено інженерами Corvus Intelligence, які розробляють критично важливі для місії системи злиття та інтеграції даних для оборонних і урядових організацій. Дізнатись про нашу команду →