Илья Космодемьянский ( hydrobiont )
Есть типичные ошибки работы с хранилищем, и эти ошибки, не то чтобы я их выдумываю специально, но поскольку мы много работаем с удаленной поддержкой баз данных, мы их просто коллекционируем. Зачастую одни и те же от клиентов. И составляем своеобразный рейтинг того, что наколлекционировали. Об этих вещах я и буду сегодня рассказывать.
Илья Космодемьянский: Какие-то вещи будут абсолютно общие для всех хранилищ, но пару специфических вещей для PostgreSQL я, естественно, упомяну, поскольку работаем мы в основном с PostgreSQL. И PostgreSQL много применяется в вебе традиционно.
В качестве эпиграфа, можно привести следующую вещь:
Подобно, как в электронике, бывают проблемы двух типов – либо где-то есть лишний контакт, либо нет контакта там, где нужно. С базой данных та же самая история. Чего только люди не хранят в базе: от картинок до совершенно удивительных вещей, которых, может быть, там бы хранить и не стоило. А может быть, наоборот, не хранят в базе те вещи, которые стоило бы хранить. Потому что для каких-то вещей применяется один кэш, для других вещей применяется другой кэш, для третьего – какое-то NoSQL-хранилище и так далее. А, может быть, оно лучше, чтобы было где-нибудь в базе. И ценнее.
Давайте, собственно говоря, по этим вещам кратенько проедемся. Они не то чтобы как-то упорядочены в порядке жесткости этих проблем, но сейчас разберемся.
В качестве дисклеймера: будет некоторый троллинг. Возможно, я где-то не удержусь. Когда рассказываешь скучно о том, что: «Ребята не делайте вот так!», то, естественно, все говорят: «Угу» и делают. Когда появляются какие-то эмоции, человек смотрит: «Блин! Какие дураки так пишут?». Открывает git, а там – та-дам! Кто же это написал вчера? Тогда запоминается лучше.
Смотрите, во-первых, одна из первейших проблем.
И первейшая она, в том числе потому, что я ее не наблюдаю только на очень редких случаях. Наверное, с Oracle и DB2 эта проблема редко случается, потому что там каждая отмасштабированная нода стоит адских денег. Это все-таки ограничивает полет фантазии, но и в вебе особо не применяется. А с Open Source, с бесплатными вещами – только в путь! И притом, совершенно не обязательно с PostgreSQL, с MySQL это еще более характерно и с NoSQL тем более.
Люди любят масштабироваться, потому что масштабирование – это хорошо, об этом написали в книжках. Просто потому что «хорошо». Почему хорошо – мало кто может объяснить, и тут возникает довольно много разных проблем.
Особо характерно, что на протяжении, не знаю, последних десяти лет я постоянно слышу, что все должно быть scale-out и ни в коем случае не должно быть scale-up. Потому что scale-up – это о-очень плохо!
За прошедшее время, с тех пор как это было придумано, компьютеры стали несколько мощнее. Задачи тоже выросли, но задачи выросли все-таки не до такой степени, насколько мощные компьютеры.
Есть люди, которые по объективным причинам имеют дело с Big Data: это физики, которые из адронного коллайдера что-то достают; это физики, которые звезды изучают; это биологи, которые гены исследуют; это люди, которые пишут компьютерные игры и плохо разрабатывают игровые сценарии (у них там зачастую может быть элементов больше, чем атомов во вселенной), и они считают, что это должно работать.
Объективных задач, где действительно нужно много данных и в одну машину никогда они не вместятся, их довольно мало. Может быть, какой-то Stock Exchange и тому подобные вещи.
Поэтому, возможно, что, в принципе, масштабироваться, с самого начала особенно, не так уж и хорошо. Собственно говоря, типичный случай, чтобы стало ближе и понятней, о том, как бывает.
Мы решили, что нашим веб-сайтом будет пользоваться очень много народу. Обычно, когда маркетинг с кем-нибудь из соучредителей строит планы на то, сколько у нас будет нагрузка, нормальный программист может это делить на сто сразу, потому что вряд ли оно так будет. Но если оно будет, то вы должны быть готовы что-то с этим сделать. Естественно, люди задумываются сразу же о масштабировании и, я бы даже сказал, о так называемом преждевременном масштабировании.
Типичная история. Давайте, раз у нас будет миллион активных пользователей на сайте, мы по дате создания этого пользователя их расшарим. А почему по дате создания пользователя? Потому что нам так показалось нужным. На самом деле таких случаев может быть много разных, но это один из них.
В результате чего через некоторое время мы получаем кучу проблем. Потому что пользователи у нас не вполне независимые и, например, чтобы собрать какую-то такую банальную задачу для реляционной базы, если она на одной машине, как френдлента, нам нужны какие-то специальные сервисы и так далее. И фактически на любое междуюзерное взаимодействие, будь то чат или что-то еще, у нас получается так, что неизбежно юзеры живут на разных машинах, и мы «приехали». Сразу резко усложняется вся логика, сразу резко усложняется поддержка и так далее.
Еще в довольно давние времена мне доводилось в одном проекте считать, что если мы поставим десять машин с PostgreSQL (тогда еще не очень быстрым по ряду вещей), то эти машины должны быть такой мощности, чтобы выдержать нагрузку запланированную, такой-то стоимости. Сравнили, посмотрели. Купить Oracle энтерпрайзной лицензии и поставить его на дисковый массив с большим шкафом просто получилось тупо дешевле по деньгам. Такой случай вполне может быть. А сейчас еще на эту тему проще, потому что базы данных стали работать лучше. Все – и Oracle, и MySQL, и PostgreSQL за последние 10 лет сделали очень большой прогресс.
Но дальше возникает еще бо?льшая проблема. Внезапно выясняется, что у нас из этих ста машин, на которые мы это все дело распилили, активны только несколько. Потому что свежезарегистрированные пользователи в силу того паттерна, как они у нас живут на сайте, они активны неравномерно. То есть те пользователи, которые зарегистрировались давно и на каких-то машинах осели, они, соответственно, ничего не делают и машина стоит.
Наступает еще одна проблема – если машина стоит, у нее не прогрет кэш. База данных быстрее работает, когда она отдает данные из кэша, а не с диска. Соответственно, если туда раз в час приходит запрос «достать данные о каком-то пользователе», то в какой-то, далеко не прекрасный момент выясняется, что для того, чтобы это сделать, запрос работает в несколько раз медленнее, потому что не прогретый кэш, и все плохо работает.
Налицо проблема, что мы поставили сто машин, сделали соответствующие изменения в инфраструктуре и получили неработоспособную систему, которую нам немедленно надо переделать.
И, вообще, этот случай гораздо шире. Сначала распилили по одному ключу на шарды, потом поняли, что на самом деле нужно было сделать совсем по-другому. Стали, соответственно, переделывать. Переделали, выяснили, что надо еще раз по-другому, потому что веб – это moving target и, собственно говоря, условия меняются быстрее, чем мы напроектировали.
Поэтому мой типичный совет – это сначала вырасти до ресурсов одной машины на нормальной реляционной базе, посмотреть, до куда мы влезаем такими темпами. Потом посчитать, сколько стоит проапдейтить эту машину, и сколько мы еще можем жить, соответственно, на более модном железе, более хорошем, более быстром. И после того, как мы это посчитаем, только после этого принимать решение о масштабировании.
И этот подход на самом деле спас не один проект, потому что преждевременное масштабирование отнимает кучу ресурсов команды, кучу денег, и в конечном итоге получается только дороже и хуже.
Второй момент, который тоже у меня в рейтинге достаточно высоко. Это кейсы «Big Data на пустом месте», я бы сказал. Бизнес хочет иметь «данные за все время». Типичная история – мы считаем какую-то статистику, собираем, не знаю, активность пользователя, time inventory, в общем. На самом деле нам нужны от нее только агрегаты, потому что свежих данных там – ну, последний день, последняя неделя, последний час. Все остальное – это предрассчитанные агрегаты. И пересчитать эти агрегаты нужно, может быть, раз в год. В иных случаях я видел, что их вообще никогда не пересчитывали, но хранили несколько терабайт для того, чтобы они просто там лежали. Это типичный пример плохой Bag Data.
Объяснять бизнесу задачу эту – это задача программиста, потому что боязнь бизнеса потерять какие-то данные и потом не посчитать аналитику – она совершенно очевидна. Данные – это деньги, все просто. Но при этом вместо того, чтобы держать четыре терабайта, вы можете сделать много разных интересных вещей. Вы можете данные правильно архивировать, даже данные сырые хранить где-то на архивной машине, чтобы, если понадобится, пересчитать по ним агрегаты, которые вы используете для какой-то там статистики или для чего-то еще, если вам вдруг понадобится новый агрегат. Но в результате те горячие данные, которые к вам только что приехали, будут вместо четырех терабайт занимать сто гигабайт, и с этим работать будет на порядки проще.
Более того, во многих базах данных, например, в том же PostgreSQL, есть много автоматических средств, которые позволяют это делать. Например, PL/Proxy позволяет на удаленную машину унести архивированные данные и при необходимости подтянуть данные и оттуда, и оттуда, чтобы какую-то выборку выдать.
Методик, как это сделать, весьма много, и то или иное партиционирование на архив и горячие данные сильно помогает в такой ситуации. Потому что эксплуатировать базу данных, отличающуюся по размерам в тысячу раз в бо?льшую сторону, когда реально бо?льшая часть этих данных, 90% вам не нужна – это всегда боль, и делать так не надо.
Очень любят использовать универсальные подходы. Entity-Attribute-Value – это, вообще, бич для DBA. Потому что программисту так проектировать интересно. Потому что у нас все универсальное, нам не нужно менять структуру таблиц, когда у нас что-то новое надо добавить, а мы можем просто добавить новый тип атрибута, прописать новый атрибут и ему новый value туда засунуть.
Хорошо, удобно, прекрасно, но у вас в результате это все разрастается до трех или четырех (если у вас есть тип) таблиц, которые огромного размера. И все ваши данные живут в них, и вы их всегда джойните. При этом как-то соптимизировать эти JOIN’ы на самом деле очень сложно, потому что данные у вас, скорее всего, будут лежать в каком-нибудь типе «текст» в такой ситуации. Потому что вам сложно будет положить их в другой тип и, например, эффективность индексирования таких данных будет на порядки меньше, чем если вы будете их держать в отдельных таблицах реляционных.
И плюс, опять же, представляете, какого объема документацию нужно держать, чтобы разобраться, какой атрибут что значит и так далее. Все-таки реляционная схема до некоторой степени сама себя документирует, особенно, если к ней есть какое-то разумное описание. В EAV вы не разберетесь просто так, без хорошей документации, вообще ни при каких условиях.
В результате то, что представляет собой EAV, обычно называется «ядром». Когда оно достигает такого состояния, что становится полностью неработоспособным, оно переименовывается в гордое наименование «ядро». И это все начинает обвешиваться разными представлениями, где, по сути, хранятся денормализованные данные, чтобы, соответственно, к ним иметь хоть какой-то быстрый доступ.
В результате от изначально стоящего в заголовке упрощения проектирования не остается вообще ничего. Это работает медленно, плохо, и любое изменение в этой схеме приходится прокидывать в кучу разрозненных, совершенно независимых от себя, зачастую написанных разными людьми и даже отделами вещей, и в результате мы эффективно не достигаем той цели, которая у нас там была заявлена.
То же самое с ORM. Я не хочу вдаваться в долгий холивар, хорошо или плохо иметь ORM, но как DBA я ORM категорически не люблю. В принципе, я понимаю стремление что-то быстро запрототипировать на ORM, но по факту дальше начинается какое-то странное явление. Мы понимаем, что ORM существенно тормозит базу данных, мы начинаем некоторые запросы переделывать на plain SQL. Еще лучше – пытаться выдрессировать ORM писать хорошие запросы. Это, вообще, страшное дело. Я как-то наблюдал человека, который мужественно боролся с запросом, сгенерированным ORM’ом, зная, как его написать правильно. У него был уже запрос, который он написал правильно. И он пытался выдрессировать ORM, чтобы тот сгенерировал запрос такого же вида, чтобы он нормально заработал. Бессмысленность этого становится совершенно очевидной.
Более того, в принципе, можно прочитать, например, отдельный доклад на тему, того «а давайте мы угадаем по логам SQL-запросов, из какого ORM к ним ходили». Это (на слайде выше) вот из какого? Вообще, многие ORM этим грешат. Но если я добавлю, что вот этот IN может быть огромного размера, я думаю, что, скорее всего, все-таки Django будет это. В Ruby on Rails они будут покороче.
Факт тот, что по многим признакам можно ORM угадать, а вот эта вещь, вообще, очень плохая. Почему? Потому что этот список может быть каким угодно. Оптимизатор вообще не знает, что с ним сделать. Если это заменить на какой-то JOIN, это будет работать еще куда ни шло, скорее всего. Это можно как-то оптимизировать. С этим IN не сделаешь ничего без очень грязных хаков. И в ORM так всё! Все запросы до какой-то степени делаются именно такими.
То есть да, пожалуйста, прототипируйте на любимом ORM, но надо понимать, что в какой-то момент это рано или поздно станет, все равно, неудобно и плохо. И с точки зрения производительности, главное, плохо.
Вот такая, чисто постгресовая вещь на тему Big Data. В PostgreSQL есть такая штука autovacuum. PostgreSQL, когда делает insert, он делает insert, а когда делает update, он делает insert + delete. При этом delete – это тоже не delete, а просто убирание tuple из области видимости.
И у нас получается очень фрагментированная таблица. У нас, может быть, в табличке реально актуальных значений 100 тысяч, а она может весить какие-то адские гигабайты, просто потому, что в ней еще несколько миллионов неактуальных tuple’ов, которые нам не видны. Просто они балластом ездят и превращают нашу базу данных в типичный пример Big Data.
Естественно, если там ничего не настроено, если работает все плохо, первая же реакция у людей, кто это эксплуатирует: «А давайте выключим autovacuum! Потому что он самый долго работающий процесс, всему мешает, соответственно, мы не можем индекс добавить, никакой DDL выполнить, если autovacuum по таблице идет. Давайте, его выключим!».
Заканчивается это очень плохо и, соответственно, можно много об этом рассказывать.
Вот здесь ссылочка с рекомендациями, как бороться с autovacuum и что делать. Главное, что никаким образом не надо отключать, потому что результаты получаются совершенно чудовищными. У вас Big Data получается еще более «на ровном месте», чем с time series данными, которые вам на самом деле не нужны и просто занимают место в базе.
«JOIN – это зло!» – вот эта вещь. У меня есть кое-какие наблюдения, откуда это у людей в голове получается. Например, люди, которые давно работают с MySQL с какой-нибудь еще версии 3, вот у них часто бывает идея, что JOIN – это зло.
Что я тут могу сказать? JOIN – это добро. Реляционная модель почему настолько успешна? Кто из вас хоть раз слышал, что реляционная модель – это такая отсталая вещь, которая, как незнамо что? Я думаю, что все слышали такое мнение, что сейчас придет schema-less NoSQL и всех победит? Не происходит.
Почему? Потому что у нас данные удобно хранить одним способом – в виде блоков на диске, а доставать их удобно другим способом каким-нибудь, желательно более высокоуровневым. И, соответственно, если у нас данные достаются высокоуровневым способом из разных таблиц и так далее, реляционная модель очень удобна для того, чтобы делать оптимизацию. И если мы используем реляционную модель, то надо использовать ее на всю мощь.
Что является альтернативой тому, что мы сделаем JOIN? Очень все просто. Мы вытягиваем в наш любимый язык программирования, на наш сервер приложений, back-end – куда угодно – данные из двух-трех табличек. Фактически они у нас живут уже в приложении, занимают место и так далее и тому подобное, при этом эти таблички могут быть большими запросто. То есть нам надо на самом деле получить в итоге десять строчек, а мы вытягиваем огромные простыни.
И дальше мы занимаемся JOIN’ом вручную, таким «закатом солнца вручную». Мы сначала начинаем ходить циклом, потом понимаем, что это медленно, начинаем какой-нибудь алгоритм хэширования туда придумывать.
По факту мы начинаем изобретать базу данных, такую как «модель паровоза действующая, в натуральную величину». И все равно она будет несколько плохая, потому что оптимизатор собирает информацию о куче всяких параметров для того, чтобы выбрать алгоритм JOIN – nested loop, hash, merge или еще какой-нибудь, где они есть. В нашем языке программирования мы это все будем писать заново сами. И зачем это делать?
Тем не менее, кто ни разу не видел, чтобы кто-то делал вот так? Прямо, чтобы ни разу? Все остальные хотя бы раз такое где-то встречали. Это говорит о том, что на самом деле в народе эта мысль популярна почему-то.
Используйте ту мощь реляционной алгебры, которая вам дана. SQL – это высокоуровневый хороший язык. Если вам нужно достать десять строчек, предварительно пройдя по очень большим таблицам, JOIN вам серьезно, хорошо поможет, в отличие от делания этого вручную и не предназначенными для этого средствами.
Еще одна больная совершенно тема – это изобретение многочисленных вариантов репликации. Почему Slony? Потому что в PostgreSQL есть такая, не очень любимая многими, я бы сказал, заслуженно не очень любимая многими, система репликации Slony, которая очень старая. Еще до того, как появилась встроенная репликация. Это такая trigger based репликация, весьма хлопотная в обслуживании.
Тем не менее, несмотря на то, что она есть много лет и она не очень плохо работает, если ее выдрессировать (просто потому что очень старый продукт, у него кучу багов всяких вытащили), люди регулярно пытаются изобрести что-нибудь свое.
В PostgreSQL есть, как минимум, три широко распространенных метода репликации: это Shipping Log’а Hot Standby’ем, это Slony, это Londiste PgQ, которые имеют свои плюсы, свои минусы. Я бы, честно говоря, в девяноста девяти процентах случаев не советовал бы использовать ничего, кроме встроенного Shipping’а Log’ов. Но, тем не менее, народ пытается изобрести что-то свое, с какими-то своими целями.
Всегда, когда я видел такие изобретенные штуки, они всегда работали как-то не так. Потому что вы не учтете всех проблем, которые у вас возникнут. Репликация – это обработка распределенных транзакций. Обработка распределенных транзакций это всегда тяжело, там никакого волшебства нет. И, соответственно, если по каким-то причинам встроенная репликация не предоставляет вам такого-то функционала, например, мультимастера, это означает что-то. Почему-то это так не работает. И если вы будете пытаться это изобрести самостоятельно, с большой вероятностью будете ходить по граблям, и многие так, в общем-то, делают.
Поэтому, в первую очередь я бы все-таки советовал, если есть какая-то технология в базе данных, то подумать, почему она так используется и использовать ее.
Что касается репликации, так тут еще один момент немаловажный заключается в том, что встроенная репликация не работает на высоком уровне, на уровне SQL. Вы, когда пишете какую-то свою репликацию, наверное, все-таки обычно общаетесь с уровнем таблиц, триггеров, хранимых процедур и SQL. Это, как правило, медленно и, как правило, чревато конфликтами.
Залезть куда-то глубже – это уже гораздо сложнее. Перепилить базу данных, чтобы у нее была другая репликация – это задача уже очень серьезная, скорее всего, вы должны понимать, что делаете, если вы можете залезть так глубоко. А встроенная репликация – это репликация транзакционным логом. Пишется информация об изменениях, но не в виде SQL statement’а, а в виде того, какая информация нам нужна, чтобы вернуть страничку 8-килобайтную в предыдущее состояние или, наоборот, сделать redo до нового состояния. И вот этот лог едет, соответственно, на slave и там применяется. И вот это будет на порядки эффективнее и на порядки надежнее, чем любой другой метод репликации. И он уже сделан. Он встроен, можно брать и пользоваться.
Типичный ответ программиста админу, который говорит о том, что процессы взаимодействия эксплуатации и разработки в конторе фундаментально поломаны.
Все знают, что нужно использовать EXPLAIN. Далеко не все используют, но все знают, что надо использовать. Надо посмотреть, оптимально ли работает запрос, не забыли ли мы там индексы и много тому подобных вещей. Но часто бывает так, что человек посмотрел это у себя в разработческой среде, и выяснилось, самый простой вариант, что на production будет просто в 100 тысяч раз больше данных. И будет выбран другой план, и соответствующим образом будет происходить работа, и уже будет, соответственно, медленно, понадобится индекс, понадобится другой индекс, понадобится что-то еще и так далее. Тут может возникнуть куча проблем.
Кстати, мы выложили в открытый доступ видеозаписи с конференции Highload++ Junior 2016. Вот соответствующий лист — 2016 в нашем аккаунте на YouTube.
Более того, база данных – это очень сложная система, там workload зависит от кучи разных странных параметров. Например, у вас проект шарит базу с другим проектом, они работают там вместе, а у вас на девелоперской среде есть только ваш проект. От того проекта порождается какая-то нагрузка, потому что, не знаю, все сотрудники всех офисов России утром за чашечкой кофе заходят на ваш сайт, например, с которым работает тот проект. А у вас проект, который с этим никак совершенно не связан, но вы испытываете какие-то удивительные тормоза, потому что база резко более сильно тормозит в эти утренние часы.
Здесь очень важно, на самом деле, разработчикам иметь какой-то доступ на production либо соответственно отлаженные процедуры работы с DBA. Один из самых худших моментов – это когда админы просто стараются не пускать бестолковых программистов никуда. Закрыли, как, знаете, советская уборщица, которая жалуется, что «ходят, топчут всякие», школу бы закрыли, только чтобы они туда-сюда не ходили. Это неправильно.
Возможно, какой-то более привилегированный доступ в базу должны иметь сильно проверенные ведущие разработчики, которые знают проект и точно не навредят, но либо доступ на чтение, либо какую-то информацию из мониторинга должны иметь все. Если у вас стоит мониторинг, который показывает в PostgreSQL по pg_stat_statements, какие запросы занимают сколько ресурсов, – эту картинку должен иметь каждый программист и после каждой выкатки посмотреть на свой запрос: как он себя ведет, что там происходит; и должен уметь это дело интерпретировать.
Если админ туда не пускает разработчиков никаким образом, рано или поздно закончится это такими вот вещами, что «у меня все в тесте работает». Потому что ответственности за свой код, за свой проект в такой ситуации разработчик не несет. А он должен уметь посмотреть на production, как это все происходит, потому что разница может быть там весьма и весьма существенной.
Ни в коем случае не хочу обидеть Java-программистов. Самые разные программисты pretending to be smart. Это такая уж роль, потому что программисты – люди любознательные и им интересно попробовать что-нибудь новое, что-нибудь интересное и воспользоваться принятыми в своем языке подходами.
Вот есть у нас Java. В Java есть потоки и, известно, что эти Java’вские нити, с ними можно сделать так, чтобы все работало быстрее, чем без них. Но у нас есть дальше база данных. Мы в базу данных хотим загрузить много данных, кладем их туда в несколько, соответственно, потоков. Нам кажется, что медленно, – мы распараллеливаемся побольше. И при этом разработчика в данной ситуации не волнует, что на самом деле происходит. А происходит следующее: в базе данных у нас 10 воркеров. Потому что у нас там такое количество ядер на машине, что с учетом служебных воркеров нам только на 10 их и остается. И в эти 10 воркеров приходят эти все потоки.
Что дальше происходит? Естественно, эти потоки начинают тупо драться между собой, и в данной ситуации это не самая хорошая идея. Поэтому программисту нужно представлять себе весь стек в бо?льших и лучших деталях. Потому что если такие вещи делать, получается очень странно.
Второй пункт – это случай вполне себе из жизни. Мы обычно делаем следующее. У нас есть мониторинг медленных запросов: топ медленных запросов, почему они медленно работают. Смотрим в отчете – запрос в топе. Что такое? Почему?
Обычно как DBA дежурный проверяет, что такая проблема есть? Он берет этот запрос и в транзакции (потому что запрос может изменять какие-то данные, – у PostgreSQL все транзакционно, соответственно, можно сказать BEGIN, EXPLAIN ANALYZE и ROLLBACK, чтобы ничего не испортить) гоняет этот запрос. Все работает, притом работает миллисекунды. Смотрит в отчет – регулярно стабильно очень долго.
Оказалось, что там ребята решили попробовать корутины и, соответственно, из скрипта это дело запускается через корутины. Они дерутся там промеж себя. В результате запрос работает медленно. Притом не потому, что запрос плохой, а потому, что он на стороне Python’а дерется и там данные из него медленно отправляются и данные в него медленно приходят, там всё ждет.
Таких вещей может быть до фига, поэтому в эти вещи надо внимательно смотреть и не пытаться применять свою технологию на всю катушку, не зная достоверно, что технология, с которой вы взаимодействуете, а именно база данных в данной ситуации, это дело, вообще, поддерживает/не поддерживает и нормально работает.
Что касается десятого пункта, то это на самом деле не один единственный пункт, а это сразу много пунктов. И это такие вещи, которые постоянно всем всегда объясняешь.
У нас есть запрос, он возвращает сколько-то строк, много строк, миллион. А мы – веб сайт, например. То есть мы работаем с веб-мордой. Как вы думаете, часто на веб-морду приходится выводить миллион строк в каком-то виде? На самом деле выводят и часто. В какую-нибудь поисковую листалку, в какую-нибудь такую вещь и т.д. Как вы думаете, сколько людей прочитало этот миллион строк? Ни один. Очевидно.
Если вы разбираете свои медленные запросы и видите по EXPLAIN или как-то еще, что запрос возвращает такое количество строк, и это не ETL, не выгрузка чего-то куда-то ночью в аналитику и так далее, задумайтесь, вообще – а это кто-то может прочитать?
Это верный признак того, что у вас явная ошибка. Это такой запрос, на который надо посмотреть и понять: либо там данных стало больше, а когда-то было мало, либо кто-то его просто с ошибкой написал; то есть это сразу большая проблема. И, самое главное, что для веб-морды такой запрос совершенно бесполезен. Это ошибка, ее надо просто убирать.
Та же самая история со счетчиками count(*), которую я рассказываю на каждой конференции, где я что-то говорю о том, как правильно работать из веба с базами данных. И в очень многих случаях я вижу это дело в отчетах о медленных запросах.
Очень любят пользователям показывать счетчики. При этом у нас высоконагруженный сайт, и на морде этого сайта эти счетчики тикают быстро, в конкурентной среде. Если они тикают не быстро, то их можно показывать очень просто. У PostgreSQL есть данные от анализатора статистики планировщика, и можно написать процедуру, которая будет возвращать эти данные, которые будут обновляться раз в какое-то время, и они будут достаточные, потому что они приблизительные, и хорошо.
Но люди делают следующее – они реальный этот счетчик выводят. У вас будет информация о том, что сегодня зарегистрировалось 261 526 пользователей, а когда вы обновите страничку через секунду, у вас окажется, что эта цифра изменилась на 15 тысяч. Какой толк, чтобы иметь такую точную цифру?
При этом count(*) – это тяжелый запрос. Чтобы сделать count(*) по всей табличке – это всегда Seq Scan (в PostgreSQL), потому что проверяется версия каждой странички. count(*) не бывает легкий ни в одной базе данных по этой причине. Вы сделали 20 счетчиков на главной странице? Эта вещь будет отдаваться несколько секунд, если у вас под этим делом лежат большие таблицы.
Что здесь делать? Как это ускорить?
Никак. Убрать такие счетчики. Если у вас страница высоконагруженного сайта только на SQL-запросы тратит несколько секунд или хотя бы даже пол-секунды, это не веб! Пользователю такой веб не нужен. Пользователь привык, что компьютер тормозит, да и то более-менее отвыкает в новейшие времена. А веб для пользователя должен работать мгновенно. Потому что, если он загружает страничку долго, пользователь пошел на другой сайт и, не знаю, сделал заказ на нем, или что он там еще сделал. Поэтому эти вещи нужно всегда выкашивать.
И традиционный главный совет в таком докладе про базы данных: «Know your data!» Вы должны знать, как работают ваши данные.
Как-то у Тома Кайта (это визе-президент Oracle по консалтингу, который очень умный дядька и очень много знает про то, как Orace работает, про SQL-запросы и т.д.; он может провести панельную дискуссию с оптимизацией запросов, которые ему предлагают оптимизировать прямо сейчас – это высочайший класс!) спросили: «Какими Вы инструментами пользуетесь для того, чтобы так хорошо оптимизировать SQL-запросы?». Он сказал: «Да тут все очень просто, я закрываю глаза и пытаюсь представить, как это работает».
Грубо говоря, полезно, вообще, знать, какие у вас данные, какие у вас запросы и немножко думать, когда, соответственно, вы с ними работаете. Это основной классный секрет успеха работы с базами данных вообще, с PostgreSQL в частности, да и с любой технологией, с которой вы имеете дело.
На этом у меня все. У нас остается вполне себе разумное количество времени под вопросы. Спасибо. Я вам с удовольствием на них отвечу.
Вопрос: Было сказано, что autovacuum не стоит отключать. А если база append-only, имеет смысл в таком случае отключить и не использовать?
Ответ: Не имеет. Надо все равно держать.
Там по ссылке это написано, но несколько есть моментов.
Во-первых, помимо автовакуума как такового, этот демон занимается аналайзом. Он обновляет статистику. Соответственно, если у вас append происходит, у вас меняется количество записей в табличках и оптимизатору нужно это знать, чтобы выбирать оптимальные планы.
Во-вторых, у вас есть pg_catalog, который обновляется. Внутренние всякие таблички. Если по нему отключить autovacuum, результат может быть очень удивительным. У вас какая-нибудь табличка pg_class станет несколько гигабайт, и работать это все будет очень печально. Этого просто не надо делать, хотя бы даже уже поэтому.
Лучше настроить разумно и работать. В ряде случаев, если есть какие-то таблички, которые обновляются не очень часто, можно по ним индивидуально указывать, чтобы autovacuum работал не так интенсивно. Но на практике я бы так делать не советовал, потому что, если табличка вырастет до больших значений, и у нее на 90% апдейтов стоит autovacuum, то этот autovacuum будет работать долго и будет серьезно мешать performance системы, не смотря на то, что он будет срабатывать редко. То есть это нехорошая практика.
Вопрос: По поводу больших выборок. Если есть такой запрос, там же лимит можно поставить?
Ответ: Лимит, да, можно поставить, но, как правило, если возвращается миллион, то лимит не поставлен. С лимитом нужно понимать следующее: чтобы получить те данные, которые вы хотите, вам надо понять, как они у вас отсортированы, и какую вы часть их получаете. Тут надо уже конкретной реализацией заниматься.
Вопрос: Вы сказали вначале, что бизнес хочет хранить любые данные всегда и за весь промежуток времени. У нас тоже примерно такие ситуации. Как в таком случае лучше подходить к проблеме архивирования данных? Скажем, какой-то большой таблицы, огромных логов, которые невозможно куда-то деть. Они нам когда-то могут понадобиться, но на самом деле нам нужна последняя часть, а большой «хвост», который остается, надо куда-то деть, заархивировать каким-то образом. Но при этом сохранить, чтобы можно было с ним работать.
Вы немножко про PL/Proxy сказали, интересно было бы, что за инструмент, подходит ли он для этого? Или какие-то есть другие инструменты?
Ответ: Есть достаточно много методов. Во-первых, простой способ – держать какую-то машину, может быть попроще, просто с большим объемом, которой не нужна особая производительность, просто там места должно быть много. И можно туда выносить этот «холодный хвост».
Дальше уже вопрос о том, какими методами выносить. Простейший способ – это просто дампить эту табличку и этот дамп выносить на тот хост и, соответственно, его там разворачивать.
На таком архивном хосте concurrency особой не будет, поэтому – пожалуйста, – пусть она там будет. Это можно оттуда достать, в любой момент пойти посмотреть, поработать.
Если, например, нужен более оперативный доступ, то можно написать хранимую процедуру на языке PL/Proxy. Это один из методов, может быть не самый лучший, не для всего подходящий, но, тем не менее, работающий. Что это за технология? У Вас есть две хранимых процедуры, одна на вашем хосте, на котором у вас активные данные живут, и это процедура на языке PL/Proxy, которая занимается только тем, что вызывает на удаленной машине с архивом хранимую процедуру на языке PL/pgSQL, которая обладает такой же точно сигнатурой и возвращает на этот результат по удаленному вызову. Довольно быстро работает, в принципе, хорошая технология. Скайпом в свое время разработанная, еще когда они были не под Microsoft. Оно действительно хорошо работает.
И в такой ситуации вы можете, например, повесить VIEW с UNION’ом на запрос из вот этой таблички, которая у вас с горячими данными на активном сервере, и с результатом, выданным из хранимой процедуры, которая PL/Proxy. И дальше, соответственно, если вы обращаетесь к этой вьюхе, и вам нужны только горячие данные по условию WHERE, вы обращаетесь только к ней, вы только эти данные поднимаете. А архивные данные – PL/Proxy разбирается, куда надо сходить, смотрит, что там нулевой результат и в UNION приезжает нулевой результат. Это одна из методик, как это можно сделать.
Вопрос: Вы говорили, что нужно не торопиться масштабироваться, а если все-таки нужно как-то предусмотреть масштабирование и об этом подумать заранее? Какие подходы стоит учесть? Что можете посоветовать и подсказать?
Ответ: Первый совет, как ни удивительно, не масштабироваться заранее, а просто банально, готовясь к старту, понимать, сколько данных будет.
Понятное дело, что никакие нагрузочные тестирования не гарантируют воспроизведения ситуации, как оно пойдет в бою. В любом случае первое, что нужно иметь – это нужно иметь хороший мониторинг. Вы должны в идеале иметь график прироста ваших данных и реакции базы данных на это. Вы имеете графики и смотрите тренд. Если вы видите, что у вас маркетинг начал активно продавать какие-то там услуги и у вас пошел рост, вы заметите это на графике и тогда примете решение достаточно обоснованное. Что вам проще и дешевле: купить несколько новых SSD быстрых и доставить оперативной памяти, либо это слишком дорого, и нужно поставить пару дешевых машин, чтобы разгрузить на какие-то определенные задачи.
Я бы сказал, что «нулевая» задача – это понять, нужно вам масштабироваться или нет, а дальше уже, исходя из этого, думать.
Например, PostgreSQL на разумно стоящем баз-данном сервере, – это который, условно говоря, 30 тысяч долларов стоит, для баз-данного сервера это разумная цена по нынешним временам, – может выдерживать, не замечая, очень большую нагрузку. И далеко не во всех проектах такая нагрузка возникает. Тут дело такое.
Я бы скорее сказал, что имеет смысл масштабировать, возможно, какие-то вещи по workload. Например, если у вас есть веб, на нем много UTP-запросов, вот они идут на master. Если нужно какую-то аналитику прогнать, то тут, скорее всего, будет не очень хорошо, потому что на базах данных, особенно версионных, не очень хорошо живут одновременно длинные и короткие запросы по массе причин того, как устроены транзакции.
В этой ситуации вам лучше отмасштабироваться, просто тупо реплику поставив и запросы на чтение туда отправив. Масштабироваться не по размазыванию данных по разным местам, а исходя из задачи. Если у вас запросы длинной больше нескольких миллисекунд – они идут на реплику, если запросы короткие, пригодные для UTP, они, соответственно, живут на мастере. Скорее в эту сторону я бы советовал думать.
Вопрос: Насчет предыдущего вопроса, выноса логов. Кроме PL/Proxy еще же вроде можно использовать foreign data wrapper? Вы не пользовались им?
Ответ: Можно использовать foreign data wrapper, но там больше вещей на свой страх и риск. PL/Proxy не до конца умеет транзакции, в том смысле, что он умеет только autocommit, там нельзя управлять транзакциями, но это транзакционный вызов RPC, притом он может быть вызван очень далеко. А foreign data wrapper – это вещь, во-первых, совершенно непредсказуемая для оптимайзера, во-вторых, не всегда хорошо работающая с транзакциями, потому что бог его знает, кто его там написал.
Foreign data wrapper к PostgreSQL вообще довольно странная вещь. Там получается, что на одной стороне исполняется запрос с одним планом, на другой – с другим планом, и это может быть предельно неоптимально.
В принципе, можно использовать foreign data wrapper для этой цели, но я бы рекомендовал сильно более простые решения и foreign data wrapper использовал бы, только если очень хочется.
Вопрос: А с PL/Proxy так не получится, что там другой план будет?
Ответ: Естественно, получится, но с ним, по опыту, проблем меньше бывает на эту тему.
Вопрос: Вам встречались решения типа: вынести архивную табличку на отдельный диск, положить диск этот в какой-нибудь shared storage с какой-нибудь OCFS2, и с этим OCFS2 отдельный сервер на read-only работает.
Ответ: Это не самая лучшая идея. Потому что, если вдруг кто-нибудь дернет этот table space по каким-то причинам, это резко увеличивает всю latency, потому что это все добро начинает лезть в шаренные буфера.
Вопрос: Я имею в виду с другого сервера?
Ответ: В смысле, table space на этом сервере дергать с другого сервера? Этого делать ни в коем случае не надо, даже на read-only. Потому что там вся мета-информация в table space, в page, она вся относится к этому серверу, и это не будет работать.
Вопрос: Но Oracle как-то так же делает?
Ответ: Нет. У Oracle есть RAC, и это большая разница.
Вопрос: Что все-таки делать с ORM? Вы вообще отказываетесь от ORM, как только начинаете писать проект? Или вы перед выпуском в production все переписываете? Какая у вас политика относительно него?
Ответ: Скажем так, я ничего не пишу, я только последствия ликвидирую. Среди наших клиентов много кто делает по-разному. Есть люди, которые ORM пользуются и живут с ним, и у них есть бизнес-процессы, которые отвечают за оптимизацию ORM, и они считают, что так – хорошо. Это совершенно нормально.
С моей персональной точки зрения, ORM вообще пользоваться не очень удобно, потому что SQL – это вещь удобная, а на Java конструирование запросов выглядит, на мой взгляд, немножко диковато. Но «на вкус и цвет все фломастеры разные». Есть куча людей, которые, наоборот, любят это делать.
Опыт подсказывает следующее: что проектов, которые начинаются целиком на ORM, очень много; проектов, которые успешно эксплуатируются многие годы и при этом целиком на ORM, я не встречал вообще ни одного. Обычно народ начинает с того, что какие-то критичные запросы начинает выдергивать из ORM и писать их руками. Наверное, 90% проектов, начавшихся с ORM, живут именно по такой схеме.
При этом проекты, которые – я видел, что их просто с ORM взяли и переписали, – я тоже видел сильно меньше, потому что обычно это большие затраты, и во всех случаях ситуация с производительностью у них изменилась просто на порядки. Это серьезно приносит много плюсов. Единственное – вам нужно нанимать программистов, которые знают SQL и умеют его писать, и, соответственно, тратить время на какие-то более медленные вещи в разработке поначалу.
ORM во многом возник как идея от вендорской разработки. Когда люди разрабатывают какую-то систему, ставят ее заказчику, и им на самом деле нужно очень сильно сократить время до подписания акта. Поставили, а дальше то, что его будет сложно поддерживать, – это же только бонус, за эту поддержку деньги платят.
Когда мы в вебе живем – совершенно другая история. Нам нужно делать по-другому, потому что у нас нет точки подписания акта, как правило. Мы сами потом будем тратить время на эти все вещи. Вот как-то так.
Контакты
» hydrobiont
» ik@postgresql-consulting.com
Этот доклад — расшифровка одного из лучших выступлений на обучающей конференции разработчиков высоконагруженных систем HighLoad++ Junior.
Вот, кстати, плейлисты с видосиками всей обучающей конференции по разработке высоконагруженных систем HighLoad++ Junior и её взрослого брата HighLoad++.
Что будет в этом году? Очень скоро — 5 и 6 июня? Программа обучающей конференции строится со смыслом.
Сначала изучение ошибок других (мы ведь умные ребята :) — например, доклад от компании, эксплуатирующей и поддерживающей highload-проекты. Цель — структурировать типовые проблемы нагруженных проектов и дать практические советы по их урегулированию.
Потом смотрим на опыт тех, у кого получилось построить посещаемый Project1917 (http://project1917.ru/), используя исключительно проверенную связку Nginx+MySQL+Laravel+AngularJS и имеющую легко поддерживаемую и расширяемую архитектуру. Кстати, в этом и состоит highload — в том, чтобы найти самое простое (а не самое хайповое) решение задачи.
Ну а уже затем можно погружаться в детали: схема работы HTTP-акселератора, сложные типы данных в реляционных СУБД, балансировка HTTP-трафика, погружение в виртуальную память, как устроены базы данных и даже чеклист по MySQL.
Изучайте программу и, если понравится, мы будем рады видеть вас! Прокачаемся вместе!
Комментарии (4)
questor
25.05.2017 20:15+1Очень хороший доклад. Грамотный базис из очевидных простых вещей (но которые часто многие делают сложно и неправильно) и видно глубокое понимание темы.
ghost404
28.05.2017 14:52-1Зачем счетчики вырезали? Их надо было просто сделать по нормальному, а не в лоб и былоб всё хорошо.
- Счетчики можно кешировать;
- Счетчики можно хранить в Redis и пересчитывать по cron;
- Счетчики можно хранить в Redis и обновлять по событию из Domain layer.
Последний вариант предпочтительней так как мы всегда имеем актуальные данные в счетчиках и не трогаем БД вообще.
ORM зло? С чего бы? Единственный запрос которые ORM генерит сама, это получение связей, но эти запросы максимально оптимальны. Все остальные запросы составляет сам разработчик используя всякие QueryBuilder-ы которые очень точно транслируют запрос в SQL.
QueryBuilder-ы имеют плюсы по сравнению с plain text запросами, хотя бы потому что их можно использовать в спецификациях и сворачивать большую портянку до одной строчки кода которая очень чётко отражает то что она делвет:
$members = $repository->match(new PublishedMembersWithContest($contest);
Бывают супер сложные запросы с подзапросами,
UNION
и алгоритмов на основе нумерации строк. Такие запросы можно описать через QueryBuilder с помощью бубна и такой-то матери, но это не нужно. Такие запросы пишутся один раз и используются только в одном месте. Их вполне можно писать и как plain text. Таких запросов один на сто. И все в команде о них знают.
Единственное что делает не оптимально, те ORM с которыми я работал, это выборка в связке ManyToMany одних элементов, по id вторых:
SELECT u.* FROM user AS u INNER JOIN users_groups AS ug ON u.id = ug.user_id INNER JOIN group AS g ON g.id = ug.group_id WHERE u.enabled = TRUE AND g.id = 2;
Второй
JOIN
делает ORM и он здесь не нужен, но из-за выборки поprimary key
, мы не имеем проблем с производительелстью.
Regis
Очень сильно несогласен с заявлением. Если есть потенциальная возможность затюнить ORM под запрос — то очень часто выгодней все же попытаться такой возможностью воспользоваться. Потому как воспользовавшись Native запросом нередко лишаешься некоторых дополнительных плюшек ORM (готовый маппинг, отслеживание границ транзакций, lazy-поля и т.п).
Естественно, что всё должно быть в пределах разумного: если запрос сильно выбивается из того, что конкретный ORM умеет — то тратить время на "подгонку" скорее всего будет непродуктивно. Но и сразу отказываться от ORM только потому, что у вас есть готовый native запрос — глупо.
kicum
Ну здесь очевидно, что запрос, который нужно оптимизировать — не простая выборка по какому-либо условию(для таких запросов ORM плюс-минус без ошибок). Для запросов в которых реально важна производительность — сложные выборки по многим таблицам с большим количеством условий, агрегированные таблицы в JOIN — выдрессировать ORM — задача зачастую просто нереальная. Кроме того не каждый ORM — работает прозрачно для разработчика: когда данные попадают в кэш, когда они оттуда уходят, запрос.
Приведу пример ограничений, который накладывает, допустим Hibernate на базе PostgreSQL:
— нет джойнов по полям таблиц, явно не связанными в мэппинге, допустим для такого запроса:
— ORM редко когда знает о фичах самой БД. Смэпить тот же JSON/JSONB — достаточно интересная задача с собственным типом данных, а оно зачастую нужно
— никакого CTE — без костылей
— никаких хранимых процедур, нормально вписывающихся в мэппинг сущности
Использовать ORM надо с умом, и там где он применим