Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

Наскоро разказах как с помощта на стандартни рецепти да се увеличи производителността на SQL заявките "на четене" от PostgreSQL базата. Днес обаче ще стане въпрос за това как може да се направи записът в БД по-ефективен без да се използват "крутилки" в конфигурацията - просто чрез правилна организация на потоците от данни.

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

#1. Секционирование

Статията е за това как и защо е полезно да се организират практически секции "в теория". Вече имаше такава, тук ще обсъдим практиките при прилагането на някои подходи в рамките на нашия сервис за мониторинг на стотици PostgreSQL сървъри..

„Спомени от отминали времена…“

Първоначално, както всеки MVP, нашият проект стартира с доста малко натоварване - мониторингът се осъществяваше само за десетте най-критични сървъра, всички таблици бяха относително компактни... Но времето минаваше, броят на хостовете растеше и опитвайки се отново да направим нещо с една от таблиците с размер 1.5TB, осъзнахме, че животът по този начин е възможен, но много неудобен.

Времената бяха почти митични, различни версии на PostgreSQL 9.x бяха актуални, затова всичкото секциониране трябваше да бъде направено "на ръка" - чрез наследяване на таблици и триггери за роутинг с динамичен EXECUTE.

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB
Полученото решение се оказа достатъчно универсално, за да може да бъде трансферирано на всички таблици:

  • Беше обявена празна "заглавна" родителска таблица, на която бяха описани всички необходими индекси и триггери..
  • Записът от гледна точка на клиента се извършваше в "коренова" таблица, а вътре с помощта на тригера за роутинг BEFORE INSERT записът "физически" се вмъкваше в нужната секция. Ако такава все още не съществуваше - хващахме изключение и ...
  • … с помощта на CREATE TABLE ... (LIKE ... INCLUDING ...) по шаблона на родителската таблица се създаваше секция с ограничение на нужната дата, за да се извърши четене само от нея при извличане на данни.

PG10: първият опит

Но секционирането чрез наследяване исторически не беше много адаптирано за работа с активен поток от записи или голямо количество секции-потомци. Например, можем да си спомним, че алгоритъмът за избор на нужната секция имаше квадратична сложност, което при 100+ секции работи, как да кажа... сами разбирате...

В PG10 тази ситуация беше силно оптимизирана, реализирайки поддръжка за нативно секциониране. Следователно, ние веднага опитахме да го приложим след миграцията на хранилището, но…

Както се оказа след проучване на ръководството, нативно секционираната таблица в тази версия:

  • не поддържа описание на индекси
  • не поддържа триггери
  • не може да бъде сама ничий "потомък"
  • не поддържа INSERT ... ON CONFLICT
  • не може да генерира секция автоматично

След като получихме удари по главата с гребла, разбрахме, че без модификация на приложението няма да се справим, и отложихме допълнителните изследвания за шест месеца.

PG10: втори шанс

И така, започнахме да решаваме възникналите проблеми един по един:

  1. Тъй като триггерите и ON CONFLICT се оказаха нужни на няколко места, за тяхната обработка създадохме междинна прокси-таблица.
  2. Освободихме се от "роутинга" в триггерите — тоест от EXECUTE.
  3. Изнесохме отделно шаблонна таблица с всички индекси, за да не присъстват на прокси-таблицата.

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB
Накрая, след всичко това, вече нативно отсекционирахме основната таблица. Създаването на нова секция все още остава на съвестта на приложението.

"Пилим" речниците

Както в всяка аналитична система, и при нас имаше "факти" и "разрези" (речници). В нашия случай, в това качество участваха, например, тялото на "шаблона" однотипни бавни заявки или текстът на самата заявка.

"Фактите" вече бяха отсекционирани по дни отдавна, затова спокойно изтривахме остарелите секции, и те не ни пречеха (логовете, разбира се!). Но с речниците стана беда…

Не можем да кажем, че имаше много, но приблизително на 100TB "факти" се получи речник от 2.5TB.. От такава таблица е удобно да не изтриеш нищо, не можеш да я компресираш за адекватно време, а и записът в нея постепенно ставаше все по-бавен.

Изглежда речник… в него всяка запис трябва да бъде представен точно веднъж… и това е вярно, но!.. Никой не ни пречи да имаме по един речник за всеки ден! Да, това носи определена излишност, но позволява:

  • да пишем/четем по-бързо за сметка на по-малкия размер на секцията
  • да консумираме по-малко памет за сметка на работа с по-компактни индекси
  • да съхраняваме по-малко данни за сметка на възможността за бързо изтриване на остарелите

В резултат на целия комплекс мероприятия натоварването по CPU намаля с ~30%, а по диска — с ~50%:

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB
При това ние продължихме да записваме в базата точно същото, само с по-малко натоварване.

#2. Эволюция и рефакторинг БД

И така, спираме на това, че имаме за всеки ден своя секция с данни. Всъщност, CHECK (dt = '2018-10-12'::date) — и това е ключът за секциониране и условието за попадането на записа в конкретната секция.

Тъй като всички отчети в нашия сервис се строят по конкретна дата, индексите от "несекционираните времена" за тях са били всички от тип (Сървър, Дата, Шаблон на плана), (Сървър, Дата, Узел на плана), (Дата, Клас на грешка, Сървър),…

Но сега във всяка секция живеят свои екземпляри на всеки такъв индекс… И в рамките на всяка секция датата е константа… Излиза, че сега ние във всеки такъв индекс просто вписваме константа като едно от полетата, което увеличава както обема, така и времето за търсене по него, но не носи никакъв резултат. Сами си оставихме грабли, упс…

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB
Посоката на оптимизацията е очевидна — просто премахваме полето с датата от всички индекси на секционираните таблици. При нашите обеми печалбата е около 1TB/седмица!

А сега нека забележим, че този терабайт още трябва да бъде записан. Тоест, ние също дискът сега трябва да бъде по-малко натоварен! На тази картинка добре се вижда полученото действие от проведеното почистване, на което посветихме седмица:

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

#3. «Размазываем» пиковую нагрузку

Една от големите бедствия на натоварените системи — е излишната синхронизация на някакви операции, които не я изискват. Понякога "защото не забелязахме", понякога "така беше по-лесно", но рано или късно трябва да се отървем от нея.

Приближаваме предишната картинка — и виждаме, че дискът ни "качи" по натоварване с двойна амплитуда между съседните измервания, което явно "статистически" не трябва да се случва при такова количество операции:

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

Достигането до това е достатъчно просто. На мониторинг бяха заведени вече почти 1000 сървъра, всеки от които се обработва от отделен логически поток, а всеки поток изпраща натрупаната информация за изпращане в базата с определена периодичност, приблизително така:

setInterval(sendToDB, interval)

Проблемът тук се крие точно в това, че всички потоци стартират приблизително по едно и също време, затова моментите за изпращане почти винаги съвпадат "до точка". Упс №2…

За щастие, това се поправя доста лесно, с добавяне на "случайна" разлика по време:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Кэшируем, что нужно можно

Третата традиционна проблема с highload — липсата на кеш там, където може да бъде.

Например, ние направихме възможност за анализ по разрез на възлите на плана (всички тези Seq Scan on users), но веднага да помислим, че те, в маса, са еднакви — забравихме.

Не, разбира се, в базата данни нищо не се записва повторно, това отсява тригера с INSERT ... ON CONFLICT DO NOTHING. Но до базата данни тези данни все пак достигат, а освен това излишно четене за проверка на конфликта трябва да правим. Упс №3…

Разликата в броя на записите, изпратени в базата преди/след включване на кеширането — очевидна:

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

А това е — съпътстващо спадане на натоварването на хранилището:

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

Итого

„Терабайт-в-сутки“ само звучи страшно. Ако правите всичко правилно, то всъщност е 2^40 байта / 86400 секунди = ~12.5MB/s, което дори настолните IDE-дискове могат да поддържат. 🙂

А ако говорим сериозно, дори при десетократен „перекос“ на натоварването в продължение на ден, спокойно можете да се вписвате в възможностите на съвременните SSD.

Пишем в PostgreSQL на субсветова: 1 хост, 1 ден, 1TB

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster