В тази статия ще научите как да използвате семантиката зад вашите данни, когато разделяте вашата база данни. Това може драстично да подобри производителността на вашето приложение. И най-важното е, че ще откриете, че трябва да приспособите критериите си за разделяне към вашия уникален домейн на приложение.
Сътрудничих със стартираща компания, за да разработя уеб приложение за спортни експерти, за да вземат решения и да изследват данни. Приложението поддържа всеки спорт, но ние сме базирани в Европа - и европейците обичат футбола. Всяка от стотиците игри, играни всеки ден по света, идва с хиляди редове. Само за няколко месеца таблицата със събития в нашето приложение достигна половин милиард реда!
Като разберем как футболните експерти запитват нашите данни, бихме могли да разделим базата данни интелигентно. Средното подобрение на времето на тази нова маса беше между 20x и 40x по-бързо. Средното подобрение във времето за всички заявки беше 5X до 10X.
Нека сега да се задълбочим в този сценарий и да научим защо не можете да игнорирате контекста на данните си, когато разделяте база данни.
Представяне на контекста
Нашето спортно приложение предлага както необработени, така и обобщени данни, въпреки че професионалистите, които го приеха, предпочитат второто. Базовата база данни съдържа терабайти сложни, неструктурирани, разнородни данни от няколко доставчици. И така, най-голямото предизвикателство беше проектирането на надеждна, бърза и лесна за изследване база данни.
Домейн на приложението
В тази индустрия много доставчици предлагат на своите клиенти достъп до събитията от най-важните футболни игри. По-конкретно, те ви предоставят данни, свързани със случилото се по време на мач, като голове, асистенции, жълти картони, пасове и много други. Таблицата, съдържаща тези данни, е най-голямата, с която трябваше да работим.
VPS спецификации, технологии и архитектура
Моят екип разработва бекенд приложението, което предоставя най-важните функции за изследване на данни. Ние приехме Kotlin v1.6, работещ върху JVM (Java Virtual Machine) като език за програмиране, Spring Boot 2.5.3 като рамка и Hibernate 5.4.32.Final като ORM (Обектно релационно картографиране). Основната причина, поради която избрахме този технологичен стек, е, че скоростта е едно от най-важните бизнес изисквания. И така, имахме нужда от технология, която може да използва тежка многонишкова обработка и Spring Boot се оказа надеждно решение.
Разположихме нашия бекенд на 16GB 8CPU VPS чрез Docker контейнер, управляван от Dokku. Може да използва най-много 15 GB RAM. Това е така, защото един GB RAM е посветен на Redis-базирана система за кеширане. Добавихме го, за да подобрим производителността и да избегнем претоварване на бекенда с повтарящи се операции.
Структура на база данни и таблица
Що се отнася до базата данни, решихме да изберем MySQL 8. 8GB и 2 CPU VPS в момента хоства сървъра на базата данни, който поддържа до 200 едновременни връзки. Бекенд приложението и базата данни са в една и съща сървърна ферма, за да се избегнат комуникационните разходи. Ние проектирахме структурата на базата данни, за да избегнем дублиране и имайки предвид производителността. Решихме да приемем релационна база данни, защото искахме да имаме последователна структура за преобразуване на данните, получени от доставчиците. По този начин ние стандартизираме спортните данни, като улесняваме изследването и представянето им на крайните потребители.
Базата данни съдържа стотици таблици към момента на писане и не мога да ги представя всички поради NDA, който подписах. За щастие една таблица е достатъчна, за да анализираме задълбочено защо в крайна сметка приехме контекстно базирания дял на данните, който предстои да видите. Истинското предизвикателство дойде, когато започнахме да изпълняваме тежки заявки в таблицата със събития. Но преди да се потопим в това, нека видим как изглежда таблицата със събития:
Както можете да видите, това не включва много колони, но имайте предвид, че трябваше да пропусна някои от тях от съображения за поверителност. Но какво наистина важни тук са parameterId
и gameId
колони. Използваме тези два външни ключа, за да изберем тип параметър (напр. гол, жълт картон, пас, дузпа) и игрите, в които се е случило.
Проблеми с производителността
Таблицата със събития достигна половин милиард реда само за няколко месеца. Както вече разгледахме подробно в тази публикация в блога, основният проблем е, че трябва да изпълняваме обобщени операции, използвайки бавни IN заявки. Това е така, защото това, което се случва по време на игра, не е толкова важно. Вместо това спортните експерти искат да анализират обобщени данни, за да намерят тенденции и да вземат решения въз основа на тях.
Освен това, въпреки че обикновено анализират целия сезон или последните 5 или 10 игри, потребителите често искат да изключат някои конкретни игри от своя анализ. Това е така, защото те не искат игра, изиграна особено лошо или добре, да поляризира техните резултати. Не можем да генерираме предварително обобщените данни, защото ще трябва да направим това за всички възможни комбинации, което е неосъществимо. Така че трябва да съхраняваме всички данни и да ги обобщаваме в движение.
Разбиране на проблема с производителността
Сега нека се потопим в централния аспект, който доведе до проблемите с производителността, с които трябваше да се сблъскаме.
Таблиците с милиони редове са бавни
Ако някога сте имали работа с таблици, съдържащи стотици милиони редове, знаете, че те по своята същност са бавни. Не можете дори да си помислите да изпълнявате JOIN на толкова големи маси. И все пак можете да изпълнявате SELECT заявки за разумен период от време. Това е особено вярно, когато тези заявки включват прости условия WHERE. От друга страна, те стават ужасно бавни при използване на агрегатни функции или IN клаузи. В тези случаи те лесно могат да отнемат до 80 секунди, което е просто твърде много.
Индексите не са достатъчни
За да подобрим производителността, решихме да дефинираме някои индекси. Това беше първият ни подход за намиране на решение на проблемите с производителността. Но, за съжаление, това доведе до друг проблем. Индексите отнемат време и пространство. Това като цяло е незначително, но не и когато се работи с толкова големи маси. Оказа се, че дефинирането на сложни индекси въз основа на най-често срещаните заявки отнема няколко часа и GB пространство. Също така, индексите са полезни, но не са магия.
Контекстно базирано на данни разделяне на база данни като решение
Тъй като не можахме да решим проблема с производителността с потребителски дефинирани индекси, решихме да опитаме нов подход. Говорихме с други експерти, потърсихме онлайн решения, прочетохме статии въз основа на подобни сценарии и накрая решихме, че разделянето на базата данни е правилният подход, който да следваме.
Защо традиционното разделяне може да не е правилният подход
Преди да разделим всички наши най-големи таблици, проучихме темата както в официалната документация на MySQL, така и в интересни статии. Въпреки че всички се съгласихме, че това е пътят, ние също разбрахме, че прилагането на разделяне без да се вземе предвид нашия конкретен домейн на приложението би било грешка. По-конкретно, разбрахме колко важно е да се намерят правилните критерии при разделянето на база данни. Някои експерти по разделянето ни научиха, че традиционният подход е да се разделя на броя на редовете. Но ние искахме да намерим нещо по-интелигентно и по-ефективно от това.
Вникване в домейна на приложението, за да намерите критериите за разделяне
Научихме основен урок, като анализирахме домейна на приложението и интервюирахме нашите потребители. Спортните експерти са склонни да анализират обобщени данни от игри в едно и също състезание. Например, състезание по футбол може да бъде лига, турнир или единичен мач, в който можете да спечелите трофей. Има хиляди различни състезания. Най-важните в Европа са Шампионска лига, Висшата лига, Ла Лига, Серия А, Бундеслига, Ередивизия, Лига 1 и Primeira Liga.
Това означава, че нашите потребители много рядко вземат предвид данните, идващи от различни състезания. Освен това те предпочитат да изследват данни сезон по сезон. С други думи, те рядко напускат контекста, представен от спортно състезание, изиграно през определен сезон. Нашата структура на базата данни изрази тази концепция с таблица, наречена SeasonCompetition
, чиято цел е да свърже състезание с конкретен сезон. И така, разбрахме, че добър подход би бил да разделим нашите по-големи таблици на подтаблици, свързани с конкретен SeasonCompetition
пример.
По-конкретно, ние дефинирахме следния формат на имената за тези нови таблици:<tableName>_<seasonCompetitionId>
.
Следователно, ако имахме 100 реда в SeasonCompetition
таблица, ще трябва да разделим големите Events
таблица в по-малкия Events_1
, Events_2
, …, Events_100
маси. Въз основа на нашия анализ, този подход би довел до значително повишаване на производителността в средния случай, въпреки че въвежда някои допълнителни разходи в най-редките случаи.
Съпоставяне на критериите с най-често срещаните заявки
Преди да кодираме и стартираме скриптовете за изпълнение на тази сложна и потенциално безвъзвратна операция, ние потвърдихме нашите проучвания, като разгледахме най-често срещаните заявки, изпълнявани от нашето бекенд приложение. Но по този начин разбрахме, че по-голямата част от заявките включват само игри, играни в рамките на SeasonCompetition. Това ни убеди, че сме прави. Така че разделихме всички големи таблици в базата данни с току-що дефинирания подход.
SELECT AVG('value') as 'value', SUM('minutes') as 'minutes'
FROM 'Events'
WHERE 'parameterId' = 15 AND 'gameId' IN(223,241,245,212,201,299,187,304,187,205)
GROUP BY 'teamId'
Сега нека проучим плюсовете и минусите на това решение.
Плюсове
- Изпълнението на заявки в таблица, съдържаща най-много половин милион реда, е много по-ефективно, отколкото правенето на таблица с половин милиард реда, особено когато става въпрос за обобщени заявки.
- По-малките таблици са по-лесни за управление и актуализиране. Добавянето на колона или индекс дори не е сравнимо с преди по отношение на време и пространство. Освен това всяко
SeasonCompetition
е различен и изисква различни анализи. Следователно може да изисква специални колони и индекси, а гореспоменатото разделяне ни позволява да се справим лесно с това. - Доставчикът може да промени някои данни. Това ни принуждава да изпълняваме заявки за изтриване и актуализиране, които са безкрайно по-бързи на толкова малки таблици. Освен това те винаги засягат само някои игри от определено
SeasonCompetition
, така че сега трябва да работим само с една маса.
Против
- Преди да направим заявка за тези подтаблици, трябва да знаем
seasonCompetitionId
свързани с игрите по интереси. Това е така, защотоseasonCompetitionId
стойността се използва в името на таблицата. Поради това нашият бекенд трябва да извлече тази информация, преди да изпълни заявката, като разгледа игрите в анализ, което представлява малки режийни разходи. - Когато заявка включва набор от игри, които включват много
SeasonCompetitions
, бекенд приложението трябва да изпълни заявка за всяка подтаблица. Така че в тези случаи вече не можем да обобщаваме данните на ниво база данни и трябва да го направим на ниво приложение. Това въвежда известна сложност в логиката на бекенда. В същото време можем да изпълняваме тези заявки паралелно. Също така можем да обобщаваме извлечените данни ефективно и паралелно. - Управлението на база данни с хиляди таблици не е лесно и може да бъде предизвикателство за изследване в клиент. По същия начин добавянето на нова колона или актуализирането на съществуваща колона във всяка таблица е тромаво и изисква персонализиран скрипт.
Ефекти от контекстно-базирано разделяне на данни върху производителността
Нека сега да разгледаме подобрението на времето, постигнато при изпълнение на заявка в новата разделена база данни.
- Подобряване на времето в средния случай (заявка, включваща само едно
SeasonCompetition
):от 20x до 40x - Подобряване на времето в общия случай (заявка, включваща едно или повече
SeasonCompetitions
):от 5x до 10x
Последни мисли
Разделянето на вашата база данни несъмнено е отличен начин за подобряване на производителността, особено при големи бази данни. Въпреки това, да го направите, без да вземете предвид вашия конкретен домейн на приложение, може да е грешка или да доведе до неефективно решение. Вместо това отделянето на време за изучаване на домейна чрез интервюиране на експерти и потребители и разглеждане на най-изпълнените заявки е от решаващо значение за изготвянето на високоефективни критерии за разделяне. Тази статия ви показа как да направите това и демонстрира резултатите от подобен подход чрез реален казус.