Привет!
● Меня зовут Саша
● Я работаю главным инженером
● В компании 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
● alex@gitinsky.com
● http://gitinsky.com,
http://meetup.com/DevOps-40

"Мы два месяца долбались, а потом построили индекс" (c) Аксенов

  • 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;
  • 20.
  • 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мс
  • 34.
  • 35.
  • 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 yourweapon: ● 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