Mobile Business Technologies
Компания
29,84
рейтинг
1 марта 2013 в 17:39

Разработка → Управление растущими нагрузками в Postgres: 5 советов от Instagram перевод

С тех пор как число активных пользователей Instagram стало постоянно расти, Postgres оставался нашим надежным фундаментом и неизменным хранилищем данных для большинства данных, создаваемых пользователями. И хотя меньше года назад мы писали о том, как мы храним большое количество данных на Instagram при 90 лайках в секунду, сейчас мы обрабатываем более 10000 лайков в секунду – и наша основная технология хранения данных не изменилась.

За последние два с половиной года, мы поняли несколько вещей и подобрали пару инструментов для масштабирования 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, которая выполняет такие шаги в процессе оптимизации таблицы:
  1. Получает эксклюзивную блокировку на таблицу
  2. Создает временную таблицу для аккумуляции изменений, и добавляет триггер на исходную таблицу, который реплицирует любые изменения в эту временную таблицу
  3. Делает CREATE TABLE используя SELECT FROM…ORDER BY, который создает новую таблицу в индексированном порядке на диске
  4. Синхронизирует изменения из временной таблицы, которые произошли после того, как был запущен SELECT FROM
  5. Переключает на новую таблицу

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

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

От переводчика:
Прошу ошибки перевода и орфографические слать в личку. А если вы заинтересованы в работе над мобильными приложениями в маленькой, но очень гордой команде, располагающейся в Санкт-Петербурге — мы тоже на связи, и рассматриваем предложения!
Автор: @s0b3r Mike Krieger
Mobile Business Technologies
рейтинг 29,84
Компания прекратила активность на сайте

Комментарии (8)

  • +1
    Уже второе упоминание на хабре про pg_reorg, отличный повод таки использовать его у себя на проекте. спасибо за статью
    • +3
      Рекомендация Instagram — это хорошая рекомендация :) пожалуйста!
  • 0
    CREATE INDEX CONCURRENTLY on tokens (substr(token), 0, 8)
    
    полагаю, должно быть так
    CREATE INDEX CONCURRENTLY on tokens (substr(token, 0, 8))
    

    Кстати, интересно — будет ли такой индекс использоваться при простом запросе
    WHERE token = '0123456789'
    
    или только при
    WHERE substr(token, 0, 8) = '01234567' AND token = '0123456789'
    
    Если будет, то это очень круто!
    • 0
      К сожалению, не будет.
    • +1
      Спасибо, исправил (скопировал опечатку с первоисточника).

      Индекс не будет использоваться, но сами подумайте, какие возможности существуют с функциональным индексом! Вы можете, например, написать свою функцию, которая будет определять вес записи по некоторым из ее полей, и сортировать по этому весу.

      Кстати, подсказка. В тех СУБД, в которых нет функциональных индексов, вы можете создать отдельное поле для результата функции, и при создании/редактировании записи вычислять и записывать новое значение этого поля. Тогда заменой функционального индекса будет простой индекс по этому полю.
  • +1
    каждый запрос запускается в отдельной транзакции. Это в особенности полезно для запросов выборки из базы, для которых использование транзакций не имеет смысла.


    Мг.
  • 0
    Я так понимаю, что использование pg_reorg рационально только для тех таблиц, в которые уже не производится запись новых данных?
    • 0
      Нет, оно рационально для любых таблиц, содержимое которых берется большими кусками в отсортированном виде. Если даже будут новые данные, кол-во поисков по диску все равно будет существенно меньше, чем если утилиту не использовать.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка