"Мы два месяца долбались, а потом построили...
DESCRIPTION
Мой доклад про оптимизацию PostgreSQL хранилища с DevConf 2014TRANSCRIPT
Привет!
● Меня зовут Саша● Я работаю главным инженером● В компании Git in Sky● Однажды я настроил одинMySQL-сервер
Какого цвета инсталлятор Oracle?
● Вы используете базы данных?● Умеете читать и пониматьплан запроса?
● Настраивали однаждыMySQL-сервер?
● Может, и не однажды?● Может, и не MySQL?
Какова цель операции?
● Однажды ко мне обратилсяодин человек
● Он предоставил следующиедокументы:
● http://slideshare.net/profyclub_ru/08-6● Действовать надо было быстрои осторожно
Кто мой заказчик?
● Конструктор сайтов, http://setup.ru● Пользовательские файлы хранятся в базе данных (PostgreSQL)
● Для больших файлов используется large objects API
● Приложение на Perl, под Apache + mod_perl● В 2012-м это работало хорошо, в 2014-м...
Перечень возникших сложностей
● Количество файлов: 6 млн => 207(85) млн● Размер индексов: 2Gb => десятки Gb● Скорость синхронизации: 100 f/s => ~30 f/s● Объем базы данных на дисках: 6Tb на тот момент
● ^ Сейчас уже 6.7Tb, и дальше будет только больше
Анализ ситуации
● Гражданский специалист: “Файлы в БД? АААА, куда я попал!”
● Советы взять какое-нибудь другое хранилище я уже слышал, можно их не повторять :)
● Наш специалист: “Ничего неломаем, валим всех аккуратно,отходим быстро”
Детальный анализ ситуации
● Бизнес-причины хранить файлы в СУБД:● Нужны транзакции при публикации сайта● Варианты:● Менять хранилище и делать транзакции на уровне приложения
● Найти транзакционное хранилище (а это СУБД :) )
Объекты предметной области● Таблица domains – имена доменов● Таблица content – метаинформация о файле (время последнего изменения и путь)
● Таблица stat – сами бинарные данные и их sha-1 хэш для дедупликации
● Таблица deleted – признак того, что файл удален
● Все четыре связаны между собой
Пользовательские сценарии
● Публикация и синхронизация файлов:● Публикуем всегда на одну и ту же ноду● Кастомный синхронизатор не очень быстро обновляет все остальные ноды
● Отдача статического контента:● Отдаем а) последнюю, б) неудаленную версию
Что плохо?
● Отдача файлов работает не очень быстро● Публикация и синхронизация – тоже● Существующее железо справляется не оченьхорошо
● Одним словом, Hetzner!
Я бы даже сказал, полный Hetzner
● Было: RAID0 2*3Tb SATA, 16G RAM, 128G SSD – для pg_temp и nginx, сортировка в PostgreSQL и буферизация в nginx – быстро
● Стало: RAID10 4*4Tb SATA, 48G RAM без SSD● SSD не дают, хотя место в корпусе еще есть – Hetzner!
● Надо жить с этим
Но как?
● Как обычно:● slow queries log, потом pgFouine или pgBadger, раз в сутки – смотреть отчеты, в них смотреть план запроса
Все еще проще
● Два самых популярных запроса при отдаче и синхронизации - “найти неудаленный файл” и “найти, что синхронизировать” - это запросы ко view
● Они и тормозят, их и надо оптимизировать
Начнем с отдачи файлов
● Этот план запроса мне не нравится● В нем слишком многабукв
Нам нужен новый план
● Материализовать view● В PostgreSQL 9.2 нет materialized view● Но в книге “Enterprise Rails” написано, как их эмулировать с помощью триггеров
● Enterprise WHAT, sorry?
Шаверма своими руками
● “Поверх” нематериализованного view делается таблица с такими же полями
● Она работает как кэш – записи в ней заводятся по запросу
● Сначала ищем в ней, потом в исходном view, если не нашлось в ней
● Записи инвалидируются триггерами на всех таблицах-участниках исходного view
Как измерить результат?
● pgFouine и pgBadger не подходят – долго ждать, много процессить, slow log нерепрезентативен
● Расширение pg_stat_statements● ^ Лучшее, что было со мной● Позволяет смотреть статистикув реальном времени
Как пользоваться?
● SELECT (total_time / 1000 / 60) as total_minutes, (total_time/calls) as average_time, calls, query FROM pg_stat_statements ORDER BY total_minutes/average_time desc;
Что покажет?
Близки ли мы к цели?
● Хорошо: кэширующая таблица кэширует● Плохо: примерно 30-40% запросов не попадают в кэш
● Может быть, надо подождать?● На третий день Зоркий Глаз заметил, что у тюрьмы нет одной стены
Know your weapon
● “Посмотреть в таблице, потом во view”● А что, если у нас 404, и файла нет вообще?● Зачем ходить за такими файлами во view?
Стало ли лучше?
● Ночью – 15мс в среднем● Днем – 40-50мс в среднем● Обычно я работаю по ночам● А результат нужно смотреть в середине дня на пике нагрузки
● Что очень неудобно
Я люблю графики!
● Главная метрика – время отдачи контента● Ее лучше измерять на эппсервере, а не в базе?
● Zabbix● Graphite/StatsD● http://goo.gl/x6If1S● ^ Ansible playbook для установки StatsD и Graphite
Я не люблю Zabbix!
● Это плохо написанная система “все-в-одном”, по качеству напоминающая китайскую видеодвойку из 90-х
● К тому же, там плохие планы запросов
UNIX-way не всегда вреден
● Graphite/StatsD stack:● Dashboard (сначала – стандартный от Graphite),
● Веб-сервис отдачи графиков (на Django)● Коллектор с RRD-like хранилищем (Carbon)● Агрегатор/препроцессор с UDP-интерфейсом (собственно, StatsD)
StatsD server
● Есть на Go, Node.JS, Python, Perl, C, Ruby, ...● Сперва я взял Python:
● Потом опомнился и взял Perl
Уже должен быть результат?
● 40-50мс никак не хотят превращаться в 0-1● Что делать?● Построить более лучшие индексы
Сказано – сделано
● Для самого частого запроса построен индекс на все три столбца, по которым идет поиск
● В этот момент все стало еще хуже! :)● Размер индекса – 18 гигабайт● Зоркий Глаз опять заметил, что у тюрьмы нет одной стены
Чрезвычайные меры
● Одно из полей, по которым индекс – varchar● Превращаем varchar в int:● http://stackoverflow.com/a/9812029/601572● Совсем забыл сказать: база данных уже полна хранимых процедур и триггеров, кроме того, я их совершенно не боюсь
● Просто не люблю
Что же было по ссылке?
● Я не помню, поэтому записал прямо сюда:● create function h_int(text) returns int as $$ select ('x'||substr(md5($1),1,8))::bit(32)::int;$$ language sql;
К чему приводит чтение*
● *плана запроса● SET enable_bitmapscan=false; <= nested loops
SELECT something FROM stat s JOIN domains d ON d.id = s.domain JOIN content c ON c.id = s.content LEFT JOIN deleted e ON e.id = s.id WHERE d.name = domname AND h_int(s.name) = h_int(filename) <= новый индекс AND s.name = filename AND date_part('epoch'::text, s.ptime) = filerev
Счастливы ли мы?
● Размер индекса: 18G => 8G● Время запроса: 40-50мс => 20-25мс● 90% всех запросов обслуживаются за 100мс● В среднем запрос обслуживается приложением за 50мс
Как это выглядит в Graphite
Как это выглядит в Zabbix
Часть вторая, момент истины
● При проверке существования файла я получал id файла, если он есть, и решил этим воспользоваться, чтобы ходить во view по PK
● Оказалось, я ошибся ранее, и мне возвращался массив id – при оптимизации это стало явным
● Я исправил ошибку и этой оптимизацией добился ускорения еще в два раза
Что мешало
● PL/pgSQL – плохой, негодный язык● Я так и не понял, как в нем сконструировать программно множество из нуля строк, поэтому возвращал, при необходимости, такое множество, делая SQL-запрос в специально заготовленную пустую таблицу с нужным списком полей
Что еще удалось
● В качестве dashboard к Graphite я поставил Grafana
● Систему отдачи файлов я переписал на смешанную асинхронно/синхронную, используя для внутренних нужд HTTP status 418 I'm a teapot
● “Асинхронная” не значит “быстрая” (наоборот), но значит “экономичная”
Что было дальше
● Я пытался тюнить кастомный репликатор, но быстро понял, что это невозможно – он работает на пределе
● Я решил заменить его на какое-то общее средство репликации и выбрал Bucardo
● Bucardo в тестовом режиме работало отлично, но из 6+Tb базы среплицировало 1Tb
WTF?
● Know your weapon:● Large objects – это просто еще один key-value storage, по сути
● Репликация ими не занимается● При этом в нашей базе мы никогда их не перезаписываем после создания!
● Кроме того, у них уникальные номера
И вот тут мне карта пошла!● Object storages:● LeoFS● OpenStack Swift● Elliptics● Riak CS● Ceph Object Gateway● Но это другая история, и она еще не закончена
Выводы
● Иногда, прежде чем сказать “давайте перепишем всё”, стоит попробовать переписать не всё
● “Переписать всё” - тоже выход, надо только уметь писать и знать, где взять оружие
Спасибо за внимание!
● Пожалуйста, ваши вопросы!● С вами был Александр Чистяков,● Главный инженер Git in Sky● http://twitter.com/noatbaksap● [email protected]● http://gitinsky.com,http://meetup.com/DevOps-40