My talk on programming languages at SPbLUG Mar 2017
"Мы два месяца долбались, а потом построили индекс" (c) Аксенов
1.
2. Привет!
● Меня зовут Саша
● Я работаю главным инженером
● В компании Git in Sky
● Однажды я настроил один
MySQL-сервер
3. Какого цвета инсталлятор Oracle?
● Вы используете базы данных?
● Умеете читать и понимать
план запроса?
● Настраивали однажды
MySQL-сервер?
● Может, и не однажды?
● Может, и не MySQL?
4. Какова цель операции?
● Однажды ко мне обратился
один человек
● Он предоставил следующие
документы:
● http://slideshare.net/profyclub_ru/08-6
● Действовать надо было быстро
и осторожно
5. Кто мой заказчик?
● Конструктор сайтов, http://setup.ru
● Пользовательские файлы хранятся в базе
данных (PostgreSQL)
● Для больших файлов используется large
objects API
● Приложение на Perl, под Apache + mod_perl
● В 2012-м это работало хорошо, в 2014-м...
6. Перечень возникших сложностей
● Количество файлов: 6 млн => 207(85) млн
● Размер индексов: 2Gb => десятки Gb
● Скорость синхронизации: 100 f/s => ~30 f/s
● Объем базы данных на дисках: 6Tb на тот
момент
● ^ Сейчас уже 6.7Tb, и дальше будет только
больше
7. Анализ ситуации
● Гражданский специалист: “Файлы в БД?
АААА, куда я попал!”
● Советы взять какое-нибудь другое хранилище
я уже слышал, можно их не повторять :)
● Наш специалист: “Ничего не
ломаем, валим всех аккуратно,
отходим быстро”
8. Детальный анализ ситуации
● Бизнес-причины хранить файлы в СУБД:
● Нужны транзакции при публикации сайта
● Варианты:
● Менять хранилище и делать транзакции на
уровне приложения
● Найти транзакционное
хранилище (а это СУБД :) )
9. Объекты предметной области
● Таблица domains – имена доменов
● Таблица content – метаинформация о файле
(время последнего изменения и путь)
● Таблица stat – сами бинарные данные и их
sha-1 хэш для дедупликации
● Таблица deleted – признак того, что файл
удален
● Все четыре связаны между собой
10. Пользовательские сценарии
● Публикация и синхронизация файлов:
● Публикуем всегда на одну и ту же ноду
● Кастомный синхронизатор не очень быстро
обновляет все остальные ноды
● Отдача статического контента:
● Отдаем а) последнюю,
б) неудаленную версию
11. Что плохо?
● Отдача файлов работает не очень быстро
● Публикация и синхронизация – тоже
● Существующее железо справляется не очень
хорошо
● Одним словом, Hetzner!
12. Я бы даже сказал, полный Hetzner
● Было: RAID0 2*3Tb SATA, 16G RAM, 128G
SSD – для pg_temp и nginx, сортировка в
PostgreSQL и буферизация в nginx – быстро
● Стало: RAID10 4*4Tb SATA, 48G RAM без SSD
● SSD не дают, хотя место в корпусе еще есть –
Hetzner!
● Надо жить с этим
13. Но как?
● Как обычно:
● slow queries log, потом pgFouine или
pgBadger, раз в сутки – смотреть отчеты, в
них смотреть план запроса
14. Все еще проще
● Два самых популярных запроса при отдаче и
синхронизации - “найти неудаленный файл” и
“найти, что синхронизировать” - это запросы
ко view
● Они и тормозят, их и надо оптимизировать
15. Начнем с отдачи файлов
● Этот план запроса мне не нравится
● В нем слишком многабукв
16. Нам нужен новый план
● Материализовать view
● В PostgreSQL 9.2 нет materialized view
● Но в книге “Enterprise Rails” написано, как их
эмулировать с помощью триггеров
● Enterprise WHAT, sorry?
17. Шаверма своими руками
● “Поверх” нематериализованного view
делается таблица с такими же полями
● Она работает как кэш – записи в ней
заводятся по запросу
● Сначала ищем в ней, потом в исходном view,
если не нашлось в ней
● Записи инвалидируются триггерами на всех
таблицах-участниках исходного view
18. Как измерить результат?
● pgFouine и pgBadger не подходят – долго
ждать, много процессить, slow log
нерепрезентативен
● Расширение pg_stat_statements
● ^ Лучшее, что было со мной
● Позволяет смотреть статистику
в реальном времени
19. Как пользоваться?
● 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;
21. Близки ли мы к цели?
● Хорошо: кэширующая таблица кэширует
● Плохо: примерно 30-40% запросов не
попадают в кэш
● Может быть, надо подождать?
● На третий день Зоркий Глаз заметил, что у
тюрьмы нет одной стены
22. Know your weapon
● “Посмотреть в таблице, потом во view”
● А что, если у нас 404, и файла нет вообще?
● Зачем ходить за такими файлами во view?
23. Стало ли лучше?
● Ночью – 15мс в среднем
● Днем – 40-50мс в среднем
● Обычно я работаю по ночам
● А результат нужно смотреть в середине дня
на пике нагрузки
● Что очень неудобно
24. Я люблю графики!
● Главная метрика – время отдачи контента
● Ее лучше измерять на эппсервере, а не в
базе?
● Zabbix
● Graphite/StatsD
● http://goo.gl/x6If1S
● ^ Ansible playbook для установки StatsD и
Graphite
25. Я не люблю Zabbix!
● Это плохо написанная система “все-в-одном”,
по качеству напоминающая китайскую
видеодвойку из 90-х
● К тому же, там плохие планы запросов
26. UNIX-way не всегда вреден
● Graphite/StatsD stack:
● Dashboard (сначала – стандартный от
Graphite),
● Веб-сервис отдачи графиков (на Django)
● Коллектор с RRD-like хранилищем (Carbon)
● Агрегатор/препроцессор с UDP-интерфейсом
(собственно, StatsD)
27. StatsD server
● Есть на Go, Node.JS, Python, Perl, C, Ruby, ...
● Сперва я взял Python:
● Потом опомнился и взял Perl
28. Уже должен быть результат?
● 40-50мс никак не хотят превращаться в 0-1
● Что делать?
● Построить более лучшие индексы
29. Сказано – сделано
● Для самого частого запроса построен индекс
на все три столбца, по которым идет поиск
● В этот момент все стало еще хуже! :)
● Размер индекса – 18 гигабайт
● Зоркий Глаз опять заметил, что у тюрьмы нет
одной стены
30. Чрезвычайные меры
● Одно из полей, по которым индекс – varchar
● Превращаем varchar в int:
● http://stackoverflow.com/a/9812029/601572
● Совсем забыл сказать: база данных уже
полна хранимых процедур и триггеров, кроме
того, я их совершенно не боюсь
● Просто не люблю
31. Что же было по ссылке?
● Я не помню, поэтому записал прямо сюда:
● create function h_int(text) returns int as $$
select ('x'||substr(md5($1),1,8))::bit(32)::int;
$$ language sql;
32. К чему приводит чтение*
● *плана запроса
● 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
33. Счастливы ли мы?
● Размер индекса: 18G => 8G
● Время запроса: 40-50мс => 20-25мс
● 90% всех запросов обслуживаются за 100мс
● В среднем запрос обслуживается
приложением за 50мс
36. Часть вторая, момент истины
● При проверке существования файла я
получал id файла, если он есть, и решил этим
воспользоваться, чтобы ходить во view по PK
● Оказалось, я ошибся ранее, и мне
возвращался массив id – при оптимизации это
стало явным
● Я исправил ошибку и этой оптимизацией
добился ускорения еще в два раза
37. Что мешало
● PL/pgSQL – плохой, негодный язык
● Я так и не понял, как в нем сконструировать
программно множество из нуля строк,
поэтому возвращал, при необходимости,
такое множество, делая SQL-запрос в
специально заготовленную пустую таблицу с
нужным списком полей
38. Что еще удалось
● В качестве dashboard к Graphite я поставил
Grafana
● Систему отдачи файлов я переписал на
смешанную асинхронно/синхронную,
используя для внутренних нужд HTTP status
418 I'm a teapot
● “Асинхронная” не значит “быстрая”
(наоборот), но значит “экономичная”
39. Что было дальше
● Я пытался тюнить кастомный репликатор, но
быстро понял, что это невозможно – он
работает на пределе
● Я решил заменить его на какое-то общее
средство репликации и выбрал Bucardo
● Bucardo в тестовом режиме работало
отлично, но из 6+Tb базы среплицировало
1Tb
40. WTF?
● Know your weapon:
● Large objects – это просто еще один
key-value storage, по сути
● Репликация ими не занимается
● При этом в нашей базе мы никогда их не
перезаписываем после создания!
● Кроме того, у них уникальные номера
41. И вот тут мне карта пошла!
● Object storages:
● LeoFS
● OpenStack Swift
● Elliptics
● Riak CS
● Ceph Object Gateway
● Но это другая история, и она еще не
закончена
42. Выводы
● Иногда, прежде чем сказать “давайте
перепишем всё”, стоит попробовать
переписать не всё
● “Переписать всё” - тоже выход, надо только
уметь писать и знать, где взять оружие
43. Спасибо за внимание!
● Пожалуйста, ваши вопросы!
● С вами был Александр Чистяков,
● Главный инженер Git in Sky
● http://twitter.com/noatbaksap
● alex@gitinsky.com
● http://gitinsky.com,
http://meetup.com/DevOps-40