Власне ПЗ на C# з нуля для чергування та завантаження CDC-файлів у DWH
Я .Net-розробник у компанії ПУМБ, і не так давно успішно завершив створення нового ПЗ з нуля, яке зараз працює на проді. У цій статті поділюсь, наскільки складним насправді виявився процес та що я б хотів знати перед початком розробки. Буде корисно для тих, хто планує створити власне ПЗ або роздумує над тим, чи варте воно вкладених ресурсів.
Це — друга частина статті про повну зміну архітектури системи обробки даних у ПУМБ. Першу частину можна знайти за посиланням.
Короткий опис ситуації для розуміння послідовності подій:
- Наш DWH (data warehouse) працює на SAP IQ.
- Дані з джерел перекачувалися у DWH через надійний, але негнучкий DataStage.
- Було прийнято рішення переходити з DataStage на зв’язку CDC плюс Airflow.
- CDC сканував зміни у форматі чорного ящика та завантажував їх у SAP IQ.
- Незадовго після переходу вендор раптово повідомляє про припинення підтримки підключення до SAP IQ.
- Прийнято рішення створити власне ПЗ.
Тут буде детально описано саме останній пункт — які кроки були зроблені, які перешкоди ми зустріли та подолали на цьому шляху.
Постановка питання
Як згадано вище, ми були проінформовані про зупинку підтримки завантаження даних у SAP IQ через CDC завчасно до самої зупинки, тому почався пошук альтернативних варіантів. Таких варіантів було знайдено два: налаштування завантаження через Airflow або ж запис усіх змін у файли (що можливо у CDC) та зчитування даних з них за допомогою власного ПЗ.
На перший погляд може здатися, що перший варіант є зручнішим, адже Airflow вже використовується для стягування та обробки даних ззовні, але як часто буває, диявол у деталях. Перенесення у Airflow такого об’ємного та постійного потоку даних потребувало б великої частки як обчислювальної потужності процесора, так і оперативної пам’яті. Що мало б негативний ефект на інших процесах та створювало б ризик повного виснаження ресурсів й зупинки роботи всього Airflow.
Розробка власного ПЗ своєю чергою здавалася складнішим та працемістким варіантом, адже потребувала б створення не тільки алгоритмів обробки даних(як це було б в Airflow), а і інструментів налаштування та взаємодії з процесами відповідальними за ці самі алгоритми. Проте це дало б нам повний контроль над завантаженням даних, їх обробкою та використанням ресурсів апаратного забезпечення.
Плюси:
|
Airflow |
Власне ПЗ |
|
|
Мінуси:
|
Airflow |
Власне ПЗ |
|
|
Після ретельного обмірковування усіх за та проти, перевагу все-таки надали розробці власного ПЗ. Головними аргументами стали відсутність потреби конкурувати за ресурси з іншими процесами, можливість їхнього максимально гнучкого налаштування та той факт, що майбутнє ПЗ буде розгорнуто на сервері CDC, через що не буде потреби копіювати файли.
Перші кроки
На мою думку, перед вибором мови програмування, за допомогою якої буде писатися застосунок, має йти продумування та створення загальної архітектури. Це дозволить оцінити наявні параметри та вже на їх основі підібрати найбільш зручну мову. Зазвичай вибір архітектури базується на наступних параметрах:
- середа розгортання застосунку;
- доступні ресурси;
- спосіб комунікації шарів (інтерфейс, бізнес-логіка і так далі).
В нашому випадку ми знали, що основний застосунок, який оброблюватиме файли, буде розгортатися виключно на серверах на базі Windows, з відносно високим об’ємом оперативної пам’яті та потужним процесором. Тому потреба в стовідсотковому контролі за ресурсами, яку би міг надати С++, відпала.
Узявши до уваги наше бажання зробити налаштування та контроль майбутніх процесів максимально гнучким, ми зрозуміли, що він буде занадто комплексним для консолі й тому без адекватного інтерфейсу було не обійтися.
Додавши до цього, по-перше, те, що в нас більше одного сервера, по-друге, що можливість налаштовувати процеси через інтерфейс має бути через усю локальну робочу мережу, а не тільки на самих серверах, з’явилася також потреба обладнати кожен застосунок TCP-протоколом для взаємодії з ним через мережу. Це відкинуло можливість використання Python, адже це вже було занадто комплексне для нього завдання.
В цей момент з’явилося відносно чітке уявлення загальної архітектури: інтерфейси взаємодіють з застосунками через TCP-протокол -> застосунки оброблюють файли -> застосунки завантажують отримані дані у DWH.
На схемі можна побачити візуалізацію:

Проаналізувавши отриману архітектуру та інші параметри ми дійшли висновку, що ідеальною мовою для створення як інтерфейсу, так і основного застосунку буде C#. Вона набагато простіша у порівнянні з C++ та має наступні зручні для нас інструменти:
- WPF для інтерфейсу;
- Web API для створення вебсервера;
- пакети для роботи файлами.
На цьому етапі для розуміння наступних кроків важливо усвідомити, як на базовому рівні працює CDC та у якому вигляді він генерує файли зі змінами.
- У CDC створюється задача по реплікації.
- До цієї задачі додаються таблиці, логи, чиї зміни ми хочемо записувати.
- У кожної задачі є свій відповідний каталог (папка).
- У кожної таблиці є два свої відповідних підкаталоги.
- В перший каталог записуються поточні зміни.
- У другий каталог (при активації відповідної функції у CDC) відбувається повне перезавантаження, під час якого туди записуються усі дані з відповідної таблиці.
- Усі файли з даними мають формат csv.
- До кожного csv-файлу додається dfm-файл з інформацією про таблицю з даними, з якої створено csv-файл.
Майже усе перелічене вище може бути змінено в CDC, але ми використовували саме такі налаштування.
Початок розробки
На початку розробки відразу стало очевидно, що C# був правильним вибором.
- Виявлення файлів при їх появі у відповідних папках завдяки класу FileSystemWatcher, який дозволяє запускати відповідний код при будь-яких змінах, пов’язаних з файлами (поява, зміна, видалення etc) та надавати усю потрібну інформацію про них.
- Робота з SAP IQ за допомогою класу OdbcConnection, який дозволяє запускати будь-які SQL-команди прямо з коду.
Доволі швидко почала вимальовуватися локальна архітектура основного застосунку. Було створено наступні класи:
- FileScanner (елементом якого є FileSystemWatcher) — сканує усю папку задачі CDC.
- FileHandler — клас, об’єкти якого створюється під кожну таблицю та оброблюють саме файли з її каталогу.
- DataManager — клас для взаємодії з базою даних.
Логіка наступна: FileScanner помічає новий файл -> визначає, до якої таблиці він належить -> передає дані про файл у відповідний FileHandler -> FileHandler оброблює файл та робить витяг даних -> FileHandler передає отримані дані у DataManager -> DataManager завантажує дані у базу.

Перші несподіванки
Першою проблемою знову стало те, що SAP IQ не любить велику кількість транзакцій малого об’єму. Тому було розроблено наступну схему: файли збиралися протягом деякого часу або поки не буде досягнуто заданого об’єму даних. Для початку завантаження було достатньо виконання однієї з умов. Такий підхід гарантував, що великий об’єм даних буде вантажитися настільки швидко, наскільки це можливо, а малий не буде простоювати, чекаючи досягнення мінімуму по об’єму.
Другою проблемою стало вимкнення/аварійне вимкнення програми під час завантаження. При старті програма зобов’язана спочатку довантажити ті файли, які вантажила в момент вимкнення. Для цього для кожної таблиці створюється каталог з тимчасовими файлами, куди переміщуються файли, які вантажаться в цей момент, структура каталогу ідентична тому, який створює CDC. При запуску програма завантажить їх першими.
Третьою проблемою стало повне перезавантаження таблиці. Програма має стерти всі наявні дані у відповідній таблиці у DWH та завантажити нові дані з файлів. Проблема полягає в тому, що при великих об’ємах інформація розбивається на декілька файлів, і при вимкненні програми неможливо зрозуміти, чи потрібно затирати дані у DWH. Побороти це вдалося записом назви першого файлу при активації повного перезавантаження таблиці. Якщо при запуску щось записано -> порівняти те, що записано з першим створеним файлом в каталозі -> якщо назви співпадають -> видалити дані -> якщо ні, завантажувати без видалення.
Перша серйозна проблема
Саме тут зіграло роль те, що це був перший раз, коли ми розробляли ПЗ з нуля. Проблемою стала нестача комунікації. В мене як в головного розробника була впевненість, що на один сервер буде приходитися одна задача CDC. Проте в якийсь момент виявилося, що в рамках одного сервера буде більше однієї задачі. Через те що архітектура основного застосунку під одну задачу вже була створена і налаштована, довелося імпровізувати.
Вийти з ситуації вдалося у доволі тендітний спосіб: Web API (TCP) сервер, до якого зверталися клієнти, та основний застосунок були створені як різні ніяк не зв’язані застосунки. Комунікація між ними була налаштована через взаємодію між незалежними процесами.
Тепер сервер був головним та замість створення об’єкта класу на кожну нову задачу запускав новий процес та взаємодіяв з ним через NamedPipeServerStream. Хоч це була і не зовсім проста для імплементації технологія, це дало один неочікуваний плюс: за потреби з’явилася можливість зупинити окремий процес, відповідальний за конкретну задачу, з диспетчера завдань без будь-якої взаємодії з Web API-сервером. В результаті архітектура всієї системи була модифікована наступним чином:

Друга серйозна проблема
Це був найбільший виклик, з яким нам довелося зіштовхнутися. Оптимізація використання оперативної пам’яті. Для розуміння, чому це стало проблемою під час розробки, потрібно трохи глибше зануритися у те, як наша програма оброблювала файли.
CDC пише у файли саме зміни даних. Це означає, що якщо одне поле змінилося три рази, то і в файли зі змінами воно потрапить три рази. Через це не можна просто зліпити деяку кількість файлів в один великий та просто його завантажити. Коректним буде обрати лише останнє значення з файлів, через що доводиться вивантажувати усю інформацію з групи файлів у оперативну пам’ять та вже потім її порівнювати.
Ми доволі швидко виявили, що вичитування csv-файлу, де під кожне поле виділяється окремий string, є дуже дорогим з точки зору оперативної пам’яті. В такому випадку інформація займала в вісім разів більше місця в оперативній пам’яті, ніж реальний розмір файлу. Під час тестових запусків це призвело до того, що в один момент пам’ять просто закінчилася, що своєю чергою викликало баги у роботі CDC та пошкодження даних, що було недопустимо.
Вийти з цієї ситуації допомогло доволі очевидне рішення: якщо string-и займають так багато місця, то чому б не зберігати дані в байтах? Навіть якщо зберігати кожну строку з файлу як окремий масив байтів, це все одно буде займати вже не в вісім, а у два рази більше місця, ніж реальний об’єм файлу, що вже набагато краще.
В ідеалі потрібно було б досягнути співвідношення 1 до 1. Але це можливо тільки якщо читати увесь файл як один єдиний масив байтів, що дуже ускладню роботу з ним. Тому ми зупинилися на зберіганні кожного рядка як окремого масиву. Це дозволило легко працювати з даними та не перевищувати оптимальні об’єми пам’яті.
Обробка помилок
При завантаженні файлів можуть виникати помилки. Навіть якщо взагалі система не має багів (чого не може бути) та працює ідеально, помилки можуть утворитися зовні, наприклад, у структурі файлу. Щоб уникнути падіння усієї програми, помилки, які виникають під час завантаження, перехоплюються, відповідний FileHandler отримує статус помилки та відправляє сповіщення через вебсервер клієнтам та відповідальним особам на електронну пошту.
В цей момент завантаження зупиняється та файли залишаються у тимчасовому каталозі. Такий підхід дозволяє запустити завантаження по новій без повного перезапуску процесу.
TCP-сервер
Для взаємодії з процесами обробки даних було обрано ASP.NET Web API server. Він працює на протоколі REST (обгортка для TCP) та дозволяє інтуїтивно зрозуміло налаштувати взаємодію клієнта та вебсервера.
Одна з головних причин, чому ми обрали саме REST, полягає в тому, що ми не знаємо, чи знадобиться взаємодіяти з вебсервером звідкись, окрім розробленого саме під цей проєкт інтерфейсу. Це стає можливим завдяки тому, що вебсервер на протоколі REST ніяк не зв’язаний з інтерфейсом і дозволяє отримувати запити від будь-чого, що може їх створити.
Єдиною проблемою саме з вебсервером стало те, що через тендітність взаємодії незалежних процесів довелося додати обмеження на те, що тільки один потік може одночасно виконувати запит до конкретного процесу. Оскільки ці запити не передбачали очікування, а зазвичай просто отримували інформацію про конкретний процес, проблем із затримками не виникало навіть при сотнях послідовних звернень.
Інтерфейс
Як вже було не один раз сказано, для взаємодії та налаштування процесів було створено простий інтерфейс на WPF. Ми знали, що використовуватися він буде виключно на Windows, тому довго роздумувати над тим, що для нього обрати, нам не довелося. WPF дає можливості для максимально гнучкого налаштування. Можливо, це було як стріляти з гармати по горобцях, але ми обрали цей варіант як найбільш оптимальний — з огляду на те, що в майбутньому сюди може знадобитися додавати нові деталі.
Сам інтерфейс дозволяє підключатися до серверів, перевіряти інформацію по кожній табличці та за потреби запускати або зупиняти роботу конкретних задач.
На скріншоті можна побачити меню налаштування запуску нової задачі:

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

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

Є 4 основні параметри, та за бажанням можна додати додаткові. Список створених connection string-ів можна побачити у меню над полем сповіщення, при натисканні на конкретний усі поля окрім пароля будуть автоматично заповнені. Також є можливість перевірити, чи connection string коректний, натиснувши на кнопку Test.
Команда
Також хочу відзначити, що команда розробки складалася з двох людей: мене як архітектора, який займався всіма технічними питаннями — від архітектури до кольору кнопок, та бізнес-аналітика, відповідального за всі інші процеси: тестування, бізнес-логіку тощо.
Не дивлячись на невеликий розмір команди, застосунок вдалося зробити ефективним, надійним та зручним. З моєї точки зору, цього вдалося досягти шляхом постійної комунікації та розуміння того, в якому напрямі має йти розробка, через що час та ресурси не витрачалися дарма.
Підсумок
Застосунок працює на проді вже майже рік так дуже добре себе зарекомендував. Одночасно можуть працювати десятки задач, і в рамках кожної може бути більше сотні таблиць. Це означає, що програма успішно оброблює до сотні гігабайт даних щодня.
Головним плюсом, як і очікувалося, став повний контроль за усіма процесами, які відбуваються з та навколо даних. Хоч і не без огріхів, але продумана архітектура та приділення певної уваги головним принципам розробки дали свої плоди. Застосунок за потреби можна легко розширити й модифікувати, навіть не маючи доступу напряму до коду.
Розробка застосунку з нуля дозволила зробити його максимально зручним та зрозумілим у використанні. Також це дало нам можливість підлаштуватися під зовсім екзотичні випадки.
Не дивлячись на те, що це наш перший досвід у створенні чогось подібного, він дав нам зрозуміти, що іноді треба не дивитися на потенціальний об’єм роботи, а просто починати робити.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів