За последние два с половиной года, мы поняли несколько вещей и подобрали пару инструментов для масштабирования Postgres и мы хотим ими поделиться – то, что мы хотели бы знать при запуске Instagram. Некоторые из них специфичны для Postgres, другие представлены также и в других базах данных. Чтобы знать, как мы горизонтально масштабируем Postgres, смотрите наш пост Sharding and IDs at Instagram
1. Частичные индексы (Partial Indexes)
Если вы часто используете в ваших запросах фильтры по конкретной характеристике, и эта характеристика представлена в меньшей части строк вашей базы, частичные индексы могут вам серьезно помочь.
Например, при поиске тегов в Instagram, мы пытаемся поднять наверх теги, по которым может быть найдено много фотографий. Хотя мы используем такие технологии, как ElasticSearch для более пристрастных поисков в нашем приложении, это единственный случай, когда база данных неплохо справляется самостоятельно. Давайте посмотрим, как Postgres действует при поиске тегов, сортируя их по количеству фотографий:
EXPLAIN ANALYZE SELECT id from tags WHERE name LIKE 'snow%' ORDER BY media_count DESC LIMIT 10; QUERY PLAN Limit (cost=1780.73..1780.75 rows=10 width=32) (actual time=215.211..215.228 rows=10 loops=1) -> Sort (cost=1780.73..1819.36 rows=15455 width=32) (actual time=215.209..215.215 rows=10 loops=1) Sort Key: media_count Sort Method: top-N heapsort Memory: 25kB -> Index Scan using tags_search on tags_tag (cost=0.00..1446.75 rows=15455 width=32) (actual time=0.020..162.708 rows=64572 loops=1) Index Cond: (((name)::text ~>=~ 'snow'::text) AND ((name)::text ~<~ 'snox'::text)) Filter: ((name)::text ~~ 'snow%'::text) Total runtime: 215.275 ms (8 rows)
Заметьте, Postgres должен отсортировать 15 тысяч строк для получения верного результата. И так как теги (например) представляют собой шаблон с длинным хвостом, мы можем вместо этого попытаться сначала показать теги, для которых есть 100 или более фото, итак:
CREATE INDEX CONCURRENTLY on tags (name text_pattern_ops) WHERE media_count >= 100
И наш план запроса теперь выглядит так:
EXPLAIN ANALYZE SELECT * from tags WHERE name LIKE 'snow%' AND media_count >= 100 ORDER BY media_count DESC LIMIT 10; QUERY PLAN Limit (cost=224.73..224.75 rows=10 width=32) (actual time=3.088..3.105 rows=10 loops=1) -> Sort (cost=224.73..225.15 rows=169 width=32) (actual time=3.086..3.090 rows=10 loops=1) Sort Key: media_count Sort Method: top-N heapsort Memory: 25kB -> Index Scan using tags_tag_name_idx on tags_tag (cost=0.00..221.07 rows=169 width=32) (actual time=0.021..2.360 rows=924 loops=1) Index Cond: (((name)::text ~>=~ 'snow'::text) AND ((name)::text ~<~ 'snox'::text)) Filter: ((name)::text ~~ 'snow%'::text) Total runtime: 3.137 ms (8 rows)
Заметьте, что Postgres должен теперь обойти только 169 строк, что намного быстрее. Планировщик запросов Postgres’а также хорош и в вычислении ограничений – если позже вы решите, что хотите получать только теги, для которых есть не менее 500 фото, т.е. из подмножества индекса – он все равно будет использовать верный частичный индекс.
2. Функциональные индексы (Functional Indexes)
Для некоторых из наших таблиц, нам нужно индексировать строки (например, 64-символьные base64 токены), довольно длинные, для того, чтобы создавать по ним индекс — это выльется в дублирование большого количества информации. В этом случае очень полезны могут быть функциональные индексы Postgres’а:
CREATE INDEX CONCURRENTLY on tokens (substr(token), 0, 8)
Таким образом, Postgres, используя индекс, находит по префиксу множество записей, а затем фильтрует их, находя нужную. Индекс при этом занимает в 10 раз меньше места, чем если бы мы делали индекс по всей строке.
3. pg_reorg для сжатия
По прошествии какого-то времени, таблицы Postgres могут быть фрагментированы на диске (из-за конкурентной модели MVCC Postgres’а, например). Также, чаще всего, вставка строк осуществляется не в том порядке, в котором вы хотите их получать. Например, если вы часто запрашиваете все лайки, созданные одним пользователем, было бы неплохо, если бы эти лайки были записаны на диске непрерывно, чтобы минимизировать поиски по диску.
Нашим решением для этого является использование утилиты pg_reorg, которая выполняет такие шаги в процессе оптимизации таблицы:
- Получает эксклюзивную блокировку на таблицу
- Создает временную таблицу для аккумуляции изменений, и добавляет триггер на исходную таблицу, который реплицирует любые изменения в эту временную таблицу
- Делает CREATE TABLE используя SELECT FROM…ORDER BY, который создает новую таблицу в индексированном порядке на диске
- Синхронизирует изменения из временной таблицы, которые произошли после того, как был запущен SELECT FROM
- Переключает на новую таблицу
Есть некоторые особенности в получении блокировки и т.п., но это описание общего подхода. Мы проверили данный инструмент и провели некоторое количество тестов, прежде чем запустить его в продакшен, и мы провели множество реорганизаций на сотнях машин без всяких проблем.
4. WAL-E для архивации и бекапов WAL
Мы используем и вносим свой вклад в разработку WAL-E, набор инструментов платформы Heroku для непрерывной архивации WAL (Write-Ahead Log) файлов Postgres. Использование WAL-E серьезно упростило наш процесс бекапирования и запуска новой репликации базы.
По сути, WAL-E — это программа, которая архивирует все WAL файлы, генерируемые вашим PG сервером, на Amazon S3, используя archive_command Postgres’a. Эти WAL файлы могут быть использованы, в комбинации с бекапом базы, для восстановления базы в любую точку, начиная с этого бекапа. Комбинация обычных бекапов и WAL файлов дает нам возможность быстро запустить новую репликацию только для чтения или failover slave (репликация на случай отказа основной базы).
Мы сделали простой скрипт-обертку для мониторинга повторяющихся сбоев при архивации файла, и он доступен на GitHub.
5. Режим автокоммита и асинхронный режим в psycopg2
Через какое-то время мы начали использовать более продвинутые возможности psycopg2, Python-драйвера для Postgres.
Первая — это режим автокоммита (autocommit mode). В этом режиме psycopg2 не требует BEGIN/COMMIT ни для каких запросов, вместо этого каждый запрос запускается в отдельной транзакции. Это в особенности полезно для запросов выборки из базы, для которых использование транзакций не имеет смысла. Включить режим очень просто:
connection.autocommit = True
Это значительно сократило общение между нашими серверами и базами, а также снизило затраты CPU на машинах баз данных. Позже, мы использовали PGBouncer для распределения соединений, это позволило соединениям быстрее возвращаться в пул.
Больше деталей о том, как это работает в Django здесь.
Другая полезная возможность в psycopg2 — это возможность регистрировать wait_callback для вызова подпрограмм. Ее использование позволяет делать параллельные запросы по нескольким соединениям единовременно, что полезно для запросов, затрагивающих множество нод – сокет просыпается и оповещает, когда появляются данные для обработки (мы используем модуль Python’а select для обработки пробуждений). Это также хорошо работает с кооперативными многопоточными библиотеками, такими как eventlet или gevent. Пример реализации – psycogreen.
В общем и целом, мы очень удовлетворены производительностью и надежностью Postgres’а. Если вы заинтересованы в работе над одной из самых больших сборок Postgres, вместе с маленькой командой инфраструктурных хакеров, мы на связи по адресу infrajobs <собака> instagram.com
От переводчика:
Прошу ошибки перевода и орфографические слать в личку. А если вы заинтересованы в работе над мобильными приложениями в маленькой, но очень гордой команде, располагающейся в Санкт-Петербурге — мы тоже на связи, и рассматриваем предложения!
ссылка на оригинал статьи http://habrahabr.ru/company/mbt/blog/171217/
Добавить комментарий