Про Surfingbird, лежащие сайты и странности PostgreSQL

в 10:48, , рубрики: postgresql, Блог компании Surfingbird, Веб-разработка, метки:

Я обещал одному пользователю написать этот пост ещё 8 февраля, а обещания надо выполнять.

Сподвигло меня дать это обещание, конечно, не просто желание рассказать, почему же на нашем сайте серфинг (процесс получения рекомендаций) вечером того дня отдавал пятисотки, а более общие соображения.

А именно — юзернейм настойчиво нам советовал поднять мощности, а то ну вот невозможно же уже.
Мощностей у нас хватает. Безаппеляционность и самоуверенность юзернейма меня… огорчили, и вот поэтому я и решил написать про то, почему на самом деле зачастую ложатся сайты.

Дисклеймер: да, сайты могут лежать по банальным причинам вроде мощности, или физического отказа серверов, проблем в дата-центре, выложенном плохом коде, ошибки администратора. Я хочу рассказать про чуть более тонкие причины, про которые могут не знать или не задумываться даже программисты, если им не приходилось разрабатывать веб-проекты.

Так вот. Как обычно, если где-то что-то легло, значит где-то чего-то не хватило.

Один из первых кандидатов — это нехватка процессов, обрабатывающих запросы. В нашем случае это FastCGI, но не суть.
Причем дело-то, скорее всего, не в том, что их просто мало — дело в том, что они вдруг стали работать непозволительно долго, и не успевают освободиться.
Отчего так происходит? Сколько-то нагруженный сайт — это высокооптимизированная система. Она рассчитана на то, что большинство данных лежит в кешах, что запросы к базе выполняются быстро, что ошибка в js не устроила вам ddos :-)
Собственно, в этом и ключ — когда что-то из этого нарушается, пикирование получается быстрым. Такие вещи бывает тяжело отловить на тестах.

Например, на тестовом сервере можно и не заметить, что аякс-запросов стало посылаться в полтора раза больше — нагрузка там примерно нулевая (вас там пять человек и бд умещается в гигабайт) и все работает.
Ещё проще не заметить, что в результате изменений в коде у вас теперь иногда (может быть, один раз на десятки/сотни тысяч запросов) генерируется такой запрос в базу, который не покрыт индексом.
Или вы немного ошиблись при создании новой функции, и её результат никогда не берется из кеша — на тестовом и так работало. Или у вас просто криво запустился один memcached из кластера после неаккуратного обновления пакетов.

Что точнее происходит в таких случаях? Когда речь не про базу — все просто, запросов стало больше/они стали длиннее, процессов стало не хватать, фронтенд отдает клиентам пятисотки по таймауту.
С базой и редким запросом — интересней. Сначала после деплоя всё хорошо. Потом в базе появляется один тяжелый запрос. Вы, вероятно, по-прежнему ничего не заметите. Процесс, пославший такой запрос, повисает. Такие запросы начинают появляться быстрее, чем выполняются, и вот их два, три, 5, и вот уже половина ядер сервера занимается этими запросами, а диск уходит в стопроцентную нагрузку.
Поздравляю — скорее всего вы уже легли. База стала слишком долго отвечать и на обычные, легкие запросы, и как только процесс туда зачем-то идет, он фактически виснет. Рано или поздно там зависают все.

Даже если у вас в приложении грамотно настроены таймауты — а это стоит сделать — все равно, какая-то часть вашего сервиса отвалилась, а остальные стали работать заметно медленнее, потому что периодически залипать на время таймаута — это уже нехорошо.

Победить такую нехватку процессов их количеством — нельзя. Во-первых, новые тоже затупят, только создадут ещё большую толчею; во-вторых, они упрутся в память.
Решить увеличением мощности базы — тоже не вариант, плохой запрос может требовать на порядки больших ресурсов.

В общем, только лечить интеллектуально. И быть готовым к тому, что чем система оптимизированней, тем хрупче — тем больнее падать, так сказать :-)

Что ещё из небанальных причин нехватки процессов.
Утечки памяти — и, следовательно, уход в своп, ну и всё уже начинает тормозить.
Тот же своп может получаться во время запуска скрипта по крону. Коварный скрипт может с ростом базы требовать всё больше памяти, может быть, расти даже квадратично — и машина-жертва будет чувствовать себя очень плохо на это время. Совсем весело, если придет OOM killer и положит процесс вашей базы, например.
Утечки количества открытых файлов (или сокетов) — и тоже сразу всё умерло.
Вечный цикл, возникающий редко — он забирает лучших.
Просто очень долгий цикл, возможность которого упущена программистом и который не прерывается принудительно по счетчику — туда же.
Навскидку фантазия кончилась :-)

Что бывает кроме нехватки процессов?

Может нехватать коннектов к базе. У вас растет количество серверов с фронтендами и процессами-обработчиками — их же легко растить, — пулы ваших обработчиков используют persistent соединения, в ходе своей жизни они быстро обзаводятся экземпляром соединения к каждой БД — упс.
Вы уперлись в лимит. А он, кстати, не просто так стоит в вашем конфиге.
Если не быть заранее готовым к тому, что сейчас коннектов станет не хватать, то будет неприятно — внедрять пулинг соединений на лету не очень легко.

Может просто кончиться место на диске, и процессы упрутся в попытку записать что-то в лог :-) Ну это снова, в общем, про процессы — частая причина, да.

Бывает, что и просто почему-то обработчики запросов запущены, а запросы до них почему-то не доходят. Вот так вот внезапно непонятно где в вашем стеке технологий обнаруживается нерегулярный трудноуловимый баг.

Ещё в ходе плавного роста нагрузки на сервис может возникнуть такое забавное — ваш фронтенд (скажем, Nginx) настроен так, что когда он за некий интервал времени X получает некое количество Y таймаутов с одного сервера, он туда не пытается больше зайти в течение времени Z.
И вот вы или ваш администратор настроили эти параметры и таймауты в Nginx-е так, что раньше всё было зашибись — а теперь значительная часть запросов немного не укладывается в таймауты, и Nginx отключает вам сервер за сервером.
Запросы с отключенных серверов перенаправляются на ещё активные, так что практически наверняка снежный ком выключит все сервера.

Много чего, в общем, бывает. Я не думаю, что смогу вспомнить все причины даже наших отказов, и уж точно не смогу составить сколько-нибудь полный список.
Важно, что в голой производительности сервера дело бывает редко.
А другие классные причины я предлагаю Вам написать в комментах :-)

Теперь немного про PostgreSQL — тайтл намекает — и про собственно 8 февраля.

В тот день мы запустили новую фичу, Surfingbird TV. Идея проста — это те же рекомендации того, что вам может быть интересно, но только среди видео.
Такой вот ленивый телевизор :-)

Список категорий каждой ссылки у нас хранится в intarray, и по полю, конечно, создан gist-индекс. В запросе, соответственно, присутствует часть а-ля AND site_cats && user_cat — то есть, чтобы было пересечение хотя бы по одному интересу. user_cat — это случайно выбранный один из интересов пользователя. Мы не делаем выборку на пересечение сразу со всеми интересами, чтобы выдача была равномерна по всем интересам, а не три четверти — по трем наиболее популярным в базе интересам, остальным — что досталось.

Всем видео у нас давным давно автоматом проставлялась невидимая «секретная» категория, так что доработать запрос для Surfingbird TV было несложно, мы просто добавили туда «фильтр»: AND site_cats @> filter_cats. То есть — категории ссылки включают в себя категории из фильтра.
Чтобы не плодить условные операторы в коде, фильтр по умолчанию был пустым массивом.
У нас не бывает ссылок без категорий, так что условие по умолчанию было тривиальным.
Но! Такой запрос почему-то выпадал из индекса и делал Seq Scan по таблице (то есть читал её всю построчно и фильтровал «руками» вместо индекса).

Вот мы и прилегли. Откатили код, поубивали плохие запросы в базе руками, написали кучу условных операторов, выложили.

Вроде всё ок. Проходит десяток минут — снова лежим. Смотрим в базу — там примерно то же самое. Что за ерунда, ..., огорчаемся мы?
Оказывается:
Исходные запросы вида ... site_cats && '{42}'::int[] работают нормально.
Запросы ... site_cats && '{42}'::int[] AND site_cats @> '{}'::int[] уходят в Seq Scan, как выяснено чуть раньше.
Запросы ... site_cats && '{42}'::int[] AND site_cats @> '{255}'::int[] работают нормально.
Запросы ... site_cats && '{255}'::int[] AND site_cats @> '{255}'::int[] опять уходят в Seq Scan! Это уже совсем удивительно.
И да, ... site_cats && '{255}'::int[] работает нормально.

Ну, сначала вставили костыль, потом ещё написали условных операторов, комментов ругательных всяких, и больше не ложились. По этой причине :-)

Ложился, кстати, только серф — потому что на серф, отдачу нашей кнопки и весь остальной сайт у нас отдельные пулы процессов. Учитывая очень разный характер нагрузки — нам это сильно помогает.

P.S. Где-то на второй трети поста (писал в районе 3 ночи) перестал работать предпросмотр — ну и вообще хабр, потому что 500. Интересно, в чем причина :-)

Автор: Skaurus

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js