Генеруємо великі набори даних для тестування продуктивності
Мене звати Юрій Івон, я співпрацюю з компанією EPAM як Senior Solution Architect. Хотів би поділитися розробленою мною утилітою для тестування продуктивності баз даних, а саме її новими можливостями з генерації наборів даних довільного розміру.
Організація надійного тестування продуктивності є критично важливою для більшості проєктів, щоб забезпечити здатність системи впоратися з реальними вимогами. Однією з найбільших перешкод, з якими стикаються розробники, є пошук великих, не виробничих наборів даних для точної симуляції цих вимог.
Завдання ускладнюється, якщо взяти до уваги проблеми анонімізації наборів даних з виробничих середовищ через технічні та регуляторні обмеження. Проблема стає ще більш вираженою для нових проєктів, оскільки на початку немає виробничого набору даних, яким можна скористатися.
Хоча деякі сервіси та інструменти надають можливість створювати фіктивні дані, зазвичай вони мають значні обмеження, які роблять їх непридатними для реальних сценаріїв:
- Багато утиліт можуть генерувати набори даних у вигляді CSV-файлів, але не можуть писати безпосередньо в базу даних, що стає дуже незручним при роботі з сотнями мільйонів рядків.
- Важко знайти генератор, який може заповнювати колонки зовнішніх ключів на основі інших даних, що вже зберігаються в базі.
- Деякі генератори можуть взаємодіяти з базами даних, але підтримують дуже обмежений їх набір.
- Досить часто немає контролю над частотним розподілом значень — наприклад, щоб певні значення робити частішими за інші або керувати відсотком пустих (NULL) значень у стовпці.
- Зазвичай такі інструменти більше зосереджені на генерації даних з нуля і не підтримують збагачення наявних наборів даних згенерованими стовпцями.
- Підтримуваним генераторам даних може бракувати гнучкості, як, наприклад генераторам чисел, що не можуть створювати послідовності в порядку зростання чи спадання, або генераторам рядків, що не підтримують шаблони.
Щоб усунути ці обмеження, я розширив мій інструмент Database Benchmark генератором даних на основі .NET-бібліотеки Bogus. І хоча він вже має немало функцій, я планую й далі його вдосконалювати для забезпечення максимальної гнучкості у генерації даних.
Генерація наборів даних з нуля
Для демонстрації його можливостей, я спочатку створю та заповню дві пов’язані таблиці — Users і Posts. Таблиці нижче описують бажану структуру та вміст.
Users
Назва |
Тип |
Може бути NULL |
Бажані властивості |
Id |
GUID |
Ні | |
CreatedAt |
Date/Time |
Ні |
Має починатися з 1 січня 2010 року і далі збільшуватися з випадковим кроком до 5 секунд. |
IsActive |
Boolean |
Ні |
Має бути «true» для приблизно 75% усіх рядків. |
Login |
String |
Ні |
Має містити унікальні адреси електронної пошти. |
FirstName |
String |
Так | |
LastName |
String |
Так | |
Birth |
Date |
Так |
Будь-яка дата між 1 січня 1950 року та 1 січня 2010 року. |
CountryCode |
String |
Так |
Деякі країни мають зустрічатися частіше за інших, і значення країни має бути пустим (NULL) приблизно для 25% усіх рядків. |
PostalCode |
String |
Так |
Має бути NULL для приблизно 50% всіх рядків. |
DiscountCode |
String |
Так |
Рядок формату XXXX-XXXX-XXXX, де кожен X відповідає шістнадцятковій цифрі. Має бути NULL для приблизно 90% усіх рядків. |
Posts
Назва |
Тип |
Може бути NULL |
Бажані властивості |
Id |
GUID |
Ні | |
CreatedAt |
Date/Time |
Ні |
Має починатися з 1 січня 2010 року і далі збільшуватися з випадковим кроком до 5 секунд. |
IsVisible |
Boolean |
Ні |
Має бути «true» для приблизно 90% усіх рядків. |
UserId |
GUID |
Ні |
Зовнішній ключ до таблиці Users. |
Message |
String |
Ні |
Я підготував усі необхідні конфігураційні файли для створення та заповнення цих таблиць, тому ви можете почати із запуску утиліти та перегляду результатів:
- Клонуйте репозиторій.
- Скомпілюйте утиліту або завантажте найсвіжіші скомпільовані файли для вашої платформи з /DatabaseBenchmark/releases.
- Відкрийте папку /samples/Generators і відредагуйте Generate.ps1 для Windows або Generate.sh для інших платформ. У цьому сценарії може знадобитися оновлення трьох змінних:
databaseType
,connectionString
іtoolPath
. Останній має вказувати на папку з бінарними файлами DatabaseBenchmark. PowerShell-скрипт за замовчуванням вказує на вихідну папку Debug, тому, якщо ви самостійно компілюєте проєкт у Windows,toolPath
можна залишити без змін. Усі типи баз даних, що підтримуються, і відповідні формати рядків підключення можна знайти в документації проєкту. Тип бази даних і рядок підключення також потрібно оновити в UsersDatabaseDataSource.json для коректної роботи всіх сценаріїв, наведених в цій статті. - Запустіть оновлений скрипт.
Якщо все гаразд, у цільовій базі даних має з’явитися три нові таблиці: Users, Posts і EnrichedUsers. Перші дві повністю відповідають наведеній вище структурі, а останню ми поки що проігноруємо.
Ось як виглядає вміст таблиці Users у SQL Server:
Все виглядає так, як очікувалося, тому перегляньмо конфігурацію, яка дає ці результати.
Для кожної таблиці потрібно виконати дві команди:
DatabaseBenchmark create --DatabaseType=$databaseType --ConnectionString="$connectionString" --TableFilePath=UsersTable.json --DropExisting=true
DatabaseBenchmark import --DatabaseType=$databaseType --ConnectionString="$connectionString" --TableFilePath=UsersTable.json --DataSourceType=Generator --DataSourceFilePath=UsersDataSource.json --DataSourceMaxRows=100
Перша створює або повторно створює вказану таблицю, а друга заповнює її згенерованими даними. Те, що є для нас найцікавішим у цьому сценарії: новий тип джерела даних Generator
і відповідний файл з його конфігурацією. Ми розглянемо структуру цього файлу та приклади його фрагментів більш детально, а інформацію про інші параметри команд можна знайти в документації проєкту.
На високому рівні файл джерела даних для генератора має таку структуру:
{ "Columns": [ { "Name": "Назва колонки", "GeneratorOptions": { "Type": "Тип генератора" //Інші параметри, що залежать від типу генератора } }, //… ] }
Ви можете відкрити UsersDataSource.json або PostsDataSource.json, щоб побачити всі визначення генераторів, які використовуються для створення двох таблиць. Тут я розгляну найскладніші з них, але ви завжди можете знайти всі доступні типи та їх властивості в документації проєкту.
Стовпець дати/часу, починаючи з 1 січня 2010 року, що збільшується з випадковим кроком до 5 секунд
{ "Name": "CreatedAt", "GeneratorOptions": { "Type": "DateTime", "Direction": "Ascending", "MinValue": "2010-01-01T13:00:00", "Delta": "00:00:05", "RandomizeDelta": true } }
Генератор DateTime
, як і генератори Integer
і Float
, може створювати значення в порядку зростання або спадання. У цьому випадку ми починаємо з MinValue 1 січня 2010 року, 13:00:00
, і кожне наступне значення буде збільшуватися не більше, ніж на п’ять секунд від попереднього. Якщо RandomizeDelta
було б false
, приріст в п’ять секунд був би постійним.
Зверніть увагу, що MinValue
вказується у форматі ISO-8601, а Delta
— у спеціальному для .NET-форматі TimeSpan
, який відповідає шаблону «[дні:]години:хвилини:секунди[.долі_секунди]».
Стовпець дати/часу зі значеннями між 1 січня 1950 року та 1 січня 2010 року з часткою часу 00:00:00 UTC
{ "Name": "Birth", "GeneratorOptions": { "Type": "DateTime", "MinValue": "1950-01-01", "MaxValue": "2010-01-01", "Delta": "1.00:00:00" } }
Тут ми маємо простіший випадок, коли генератор налаштований на створення випадкових позначок часу в діапазоні від 1 січня 1950 року до 1 січня 2010 року. Основна увага зосереджена на датах, тому часова частина за замовчуванням дорівнює 00:00:00. Для параметра Delta
встановлено значення 1 день, що гарантує, що кожне згенероване значення відрізняється цілою кількістю днів від MinValue
, тобто частка часу буде завжди дорівнювати «нулю».
Булеве значення, яке має бути істинним приблизно для 75% усіх рядків
{ "Name": "IsActive", "GeneratorOptions": { "Type": "Boolean", "Weight": 0.75 } }
Тут ми можемо побачити параметр Weight
, який контролює частоту true
серед значень, що виробляються булевим генератором. За замовчуванням, ймовірності true
і false
рівні, що відповідає вазі 0.5, але в цьому прикладі ми змінюємо її на 0.75, тобто вказуємо ймовірність 75%.
Унікальна адреса електронної пошти
{ "Name": "Login", "GeneratorOptions": { "Type": "Unique", "SourceGeneratorOptions": { "Type": "Internet", "Kind": "Email" } } }
Як і більшість інших, вбудований генератор електронної пошти не забезпечує унікальність. Лише GUID і декілька генераторів, що підтримують режими зростання та спадання, гарантують, що кожне нове значення є відмінним від попередніх.
Унікальний генератор, застосований у наведеному вище прикладі, діє як оболонка для інших і забезпечує унікальність кожного значення, яке він видає. Це досягається через повторні запити до базового генератора, якщо знайдено дублікат.
Серед налаштувань є дозволена кількість таких спроб, і якщо її вичерпано, весь процес створення даних зупиняється. Однак у цьому конкретному прикладі подібна проблема навряд чи виникне, оскільки генератор електронної пошти дає дуже різноманітні результати, а з обмеженням спроб за замовчуванням, установленим в 100, він є достатньо надійним.
Код країни — деякі країни мають зустрічатися частіше за інших, і значення країни має бути пустим (NULL
) приблизно для 25% усіх рядків
{ "Name": "CountryCode", "GeneratorOptions": { "Type": "Null", "Weight": 0.25, "SourceGeneratorOptions": { "Type": "ListItem", "Items": [ "US", "AF", "AL", "DZ", "AS", "AD", "AO", "AI", /*...*/ "ZM", "ZW", "AX" ], "WeightedItems": [ { "Value": "US", "Weight": 0.2 }, { "Value": "CN", "Weight": 0.2 }, { "Value": "GB", "Weight": 0.1 } ] } } }
Хоча існує вбудований генератор кодів країни, він не має способу підвищити ймовірність вибору конкретних кодів. Як обхідний шлях я використовую тут генератор ListItem
, де його колекція Items містить усі коди країн, а WeightedItems
— лише ті, які винні мати вказані ймовірності.
У цьому прикладі трьом кодам країн буде присвоєно конкретну ймовірність, тоді як кожна з решти матиме розраховану ймовірність 0,5 (1 — 0,2 — 0,2 — 0,1), поділену на їх кількість.
Крім того, я застосовую генератор Null
, щоб приблизно 25% усіх рядків не вказувало код країни. Це оболонка, яка дає або NULL
, або значення з базового генератора, при цьому можна контролювати ймовірність появи NULL
. Подібно до булевого генератора, його параметр Weight
є необов’язковим і за замовчуванням дорівнює 0,5.
Зовнішній ключ
{ "Name": "UserId", "GeneratorOptions": { "Type": "ColumnItem", "TableName": "Users", "ColumnName": "Id", "ColumnType": "Guid" } }
Щоб заповнити стовпець зовнішнього ключа дійсними ідентифікаторами користувачів, які невідомі заздалегідь, я використовую генератор ColumnItem
. Він завантажує всі значення з указаної таблиці/стовпця одним запитом, а потім випадковим чином вибирає окремі значення з цього списку.
Якщо кількість рядків у вихідній таблиці настільки велика, що завантаження займає надто багато часу або спричиняє проблеми з пам’яттю, її можна обмежити за допомогою двох додаткових параметрів — MaxSourceRows
і SkipSourceRows
, які пояснюються у відповідному розділі документації.
Хочу зазначити, що цей тип генератора може бути корисним не тільки для зовнішніх ключів. Якщо вам зручніше зберігати всі можливі значення полів у базі даних, а не у файлі JSON, генератор ListItem
можна легко замінити на ColumnItem
, оскільки він має той самий набір можливостей, включаючи колекцію WeightedItems
.
Рядок формату XXXX-XXXX-XXXX, де кожен X відповідає шістнадцятковій цифрі
{ "Name": "DiscountCode", "GeneratorOptions": { "Type": "Null", "Weight": 0.9, "SourceGeneratorOptions": { "Type": "Pattern", "Kind": "Regex", "Pattern": "[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}" } } }
Генератор пустих значень (NULL) вже було пояснено, але тут ми бачимо новий — генератор шаблонів. Наразі він підтримує два механізми шаблонів, які називаються Simple і Regex. Перший не задовольняє вказані вимоги, тому тут використовується Regex.
Сам регулярний вираз має виглядати так само, якби ви перевіряли значення на відповідність умовам, але метасимволи початку та кінця рядка можна опустити.
Збагачення наявних наборів даних згенерованими стовпцями
Тепер подивімось на таблицю EnrichedUsers
, яка також була згенерована нашим скриптом.
Якщо ви порівняєте ці результати з таблицею Users
, то побачите тих самих користувачів у тому самому порядку, але з двома додатковими стовпцями — Address
і Phone
(знімок екрана не містить Id
і CreatedAt
для збереження читабельності).
Це стало можливим завдяки генератору DataSourceIterator
, який може отримувати доступ до будь-якого джерела даних, що підтримується утилітою Database Benchmark. У нашому прикладі дані переносяться з однієї таблиці до іншої за допомогою джерела типу Database
. Для кожного стовпця, який копіюється до «збагаченої» таблиці, визначення генератора виглядатиме так:
{ "Name": "Id", "GeneratorOptions": { "Type": "DataSourceIterator", "DataSourceType": "Database", "DataSourceFilePath": "UsersDatabaseDataSource.json", "ColumnName": "Id" } }
Будь-який інший скопійований стовпець буде мати подібну специфікацію генератора і відрізнятися тільки за Name
та ColumnName
.
Коли генератор цього типу знаходить кілька стовпців, що посилаються на ту саму комбінацію DataSourceType
і DataSourceFilePath
, він групує їх під одним ітератором.
Як ви бачите в EnrichedUsersDataSource.json, генератори для двох нових стовпців просто додано поряд зі списком визначень для DataSourceIterator
.
Інші можливості та загальні міркування
На додачу до можливостей, згаданих раніше, деякі генератори можна зробити залежними від локалі. Наразі це стосується лише типів Address
, Company
, Internet
, Name
, Phone
та Text
. Усі вони підтримують необов’язковий параметр Locale
, який приймає одне з наведених тут значень.
На знімку екрана нижче показано вміст таблиці EnrichedUsers
, коли для Login
, FirstName
, LastName
, Address
та Phone
було встановлено значення локалі «uk». Набір імен місцями екзотичний, занадто часто «майдан» в адресах, роди в іменах та адресах можуть бути неузгодженими, але свою задачу генератор виконує 😊.
Можливо, ви помітили, що адреса електронної пошти для входу не відповідає імені та прізвищу. Це одне з поточних обмежень — генератори не підтримують контекстно-залежні значення для різних стовпців. Хоча це зазвичай не повинно бути проблемою для тестування продуктивності, я збираюся вирішити це в майбутньому двома різними способами:
- Введенням опціональних залежностей між генераторами.
- Введенням іншого різновиду шаблонного генератора, де частини шаблону можуть посилатися на значення інших колонок або на інші генератори.
Серед інших функцій, які я збираюся додати в майбутньому, я б назвав:
- Колонки масивів, тому що це дуже важливо для NoSQL-баз.
- Більше налаштувань для дійсних генераторів, такі як тип коду країни, формат дати (коли дата записується у рядок), і т.д.
- Скриптовані генератори.
Якщо ви бачите, що чогось важливого не вистачає, дайте мені знати. Буду вдячним за будь-які пропозиції щодо нових функцій і вдосконалень. Можете надсилати їх у розділі Issues на сторінці проєкту.
Ця стаття доступна також англійською на Medium.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів