Около месяца назад мои коллеги из DBA-команды приняли участие в живом митапе на youtube-канале #RuPostgres Live, где отвечали на вопросы Николая Самохвалова и зрителей, которые присылали их в форму и подключились к трансляции. Получилась интересная и содержательная беседа про PostgreSQL, опыт работы с разными версиями и задачами. Поэтому мы решили сделать текстовую расшифровку этой встречи, обогатив её полезными ссылками. В комментариях задавайте вопросы, если они возникнут — постараемся на них ответить!



Сначала представлю участников беседы:


image Николай Самохвалов — евангелист PostgreSQL, сооснователь российского PostgreSQL-сообществ­а (2008) и ряда успешных стартапов, использующих PostgreSQL


image Константин Евтеев — лидер OLTP-юнита Avito


imageСергей Бурладян — архитектор баз данных


imageДмитрий Вагин — DBA Team Lead


А теперь — к расшифровке!


Николай Самохвалов: Привет, уважаемые Youtube-зрители. Меня зовут Николай Самохвалов. Это #RuPostgres Live #2: вопросы и ответы с экспертами Avito, и со мной сегодня Константин Евтеев, Дмитрий Вагин и Сергей Бурладян. Это далеко не все Postgres-эксперты в компании Avito, я так понимаю. Сколько у вас людей в команде, которая знает Postgres?
Константин Евтеев: Postgres-команда — это, по факту, OLTP-юнит — состоит из 13 человек, которые в свою очередь делятся: часть занимается платформой, часть — продуктовой разработкой.
Николай Самохвалов: Женщины есть?
Константин Евтеев: Да.
Николай Самохвалов: Прямо сейчас в чатике Youtube можно писать вопросы. У нас есть и заранее подготовленные вопросы, в том числе — от меня лично. Поговорим, как в компании Avito устроена «готовка» Postgres. И первый вопрос: правда ли, что все ещё 9.2?
Константин Евтеев: Нет. 9.2 уже нет. Мы используем все версии Postgres, которые официально поддерживаются комьюнити.
Николай Самохвалов: Там end-of-life был совсем недавно…
Константин Евтеев: В сентябре. Мы к этому подошли, и с 9.2 мы проапгрейдились на разные версии, в том числе 9.4, 9.5, 9.6.
Николай Самохвалов: 10...?
Константин Евтеев: Пока ещё нет. 10 у нас есть в тесте.
Николай Самохвалов: С чем связана такая разношёрстная картина?
Константин Евтеев: Разношёрстная картина связана прежде всего, с тем, что перед апгрейдом мы сначала тестируем версию, и далее проводим апгрейд. Смотрим, как она себя показала, какой показывает перформанс, и после этого (для следующего инстанса) мы можем тестировать другую версию, апгрейдиться на 9.4-9.5. Потому что любой апгрейд связан с даунтаймом, и апгрейда ради апгрейда как такового нет. Мы делаем его ради получения необходимой функциональности и производительности.
Николай Самохвалов: Получается, у вас есть четыре разных версии?
Константин Евтеев: 9.3 у нас нет. Есть 9.4, 9.5, 9.6. 10 в тесте.
Николай Самохвалов: Если у кого-то из вас 9.3, срочно апгрейдьтесь. Был один из вопросов в Youtube: «Стоит ли мне из 9.3 сразу на 9.6 апгрейдиться, или мне нужна промежуточная версия?». Я бы сказал, что апгрейдиться нужно сразу на десятку.
Сергей Бурладян: Можно пропустить промежуточную версию.
Николай Самохвалов: Я немного не понимаю логику этого вопроса. Люди, наверное, думают, что эти версии более сырые. Но 9.6 существует уже год.
Сергей Бурладян: Кого-то может беспокоить, что, допустим, формат поменялся, индексы пересоздавать. Но pg_upgrade поддерживает пропуск версии, можно быть спокойным.
Николай Самохвалов: На 10 ты бы стал перетаскивать свой проект?
Сергей Бурладян: Пока нет. У нас такая идея, что мы апгрейдимся где-то на четвертый минорный релиз. На 10,4 мы бы уже смогли, наверное, перейти.
Николай Самохвалов: Это прямо официальная политика?
Сергей Бурладян: Полуофициальная.
Константин Евтеев: Но при этом у нас есть dev- и test-среды, там мы проводим тестирование самых новых версий. В том числе 10 версию мы тестировали начиная с альфы и написали несколько багрепортов: 1, 2.
Николай Самохвалов: Как это делаете?
Константин Евтеев: У нас есть семплирование, с одной стороны, с другой стороны — фикстуры для того, чтобы проводить тестирование нашего приложения. После семплирования данных мы готовим image, который деплоим в наше облако. Помимо того, что поднимается база, dev- и test-среда — это еще и большое количество инфраструктуры. Там поднимаем внешние индексы, различные кэши, очереди и другие элементы инфраструктуры. После деплоя нужно проинициализировать внешние индексы. У нас очень активно используется логическая репликация. На семпловых данных нужно поднять всю связанную инфраструктуру. Таким образом, накат семпловых данных приводит к инициализации около десятка баз данных, через механизм очередей PgQ, наполняются наши копии матвьюх, осуществляется доставка данных во внешние индексы и т. д.
Николай Самохвалов: Подходим к вопросу: какой у вас объем данных в Postgres хранится?
Константин Евтеев: Общий объем наших баз данных — более 15 ТБ. Работают они на десятках серверов. Типовая рабочая нагрузка на них — около 10 тыс. транзакций в секунду.
Николай Самохвалов: Очень интересный вопрос недавно проскакивал на Facebook. Есть продакшн-данные, которые можно было бы потестировать в тест-среде, а, может быть, даже в dev. Это идеальный случай: ты можешь посмотреть, где что тормозит. Ясно, что во-первых, не всем разработчикам нужно показывать все данные. Во-вторых, огромное количество данных размером в 15 ТБ мы легко на dev-среду не заберем. Как готовите эти среды, как забираете данные?
Константин Евтеев: Как нарезать семпл? Бывают данные связанные, бывают несвязанные. У нас на проекте есть две основные сущности — пользователь и объявление. Мы берем какие-то выборки пользователей, собираем все их объявления, далее идет выборка по остальным таблицам. Вероятнее всего, нужно ограниченное количество наборов — мы задаем лимиты в нашем скрипте.
Николай Самохвалов: Сколько процентов?
Константин Евтеев: Зависит от параметров и задачи, которую мы решаем в тесте. В большинстве случаев большой объём не нужен.
Дмитрий Вагин: Я немного расскажу весь механизм. Каждое утро у нас запускается скрипт, который забирает определенных пользователей и их объявления: рандомный набор плюс заранее зашитые тестовые юзеры. Плюс куча зависимостей с небольшими ограничениями. Вся сборка с продакшн-баз занимает около получаса. Затем на этом строятся docker-образы и пушатся в registry.
Николай Самохвалов: Как выбирается рандом? Table simple или что-то своё?
Дмитрий Вагин: Нет. У нас есть счетчики, сколько у каких пользователей активных объявлений, рандомно берется пачка пользователей. Критерий такой: чтобы выдача в этом тестовом образе на сайте была 2 страницы в Москве, чтобы можно было тестировать более-менее наглядно. Простые условия.
Николай Самохвалов: Это не много, получается, данных.
Дмитрий Вагин: Там куча справочников, куча зависимых данных.
Николай Самохвалов: Но самих объявлений получается не много. Если вы что-то новенькое делаете, например, индекс забыли, поймете об этом только на продакшене? Или есть что-то в тестовом окружении?
Дмитрий Вагин: Это мы увидим на тестировании.
Константин Евтеев: «Не много» и «забыли индекс» — это все зависит от объёма данных. Если есть несколько гигабайт, индекс будет работать, и будет заметно. А мы говорим, что там несколько гигабайт. А если разработчикам нужно нагрузочное тестирование, то можно поднять необходимую базу из бэкапа на dev-кластер. Мы иногда можем его делать. Главное, что я хотел упомянуть — при сборке этих сред в том числе идет обфускация, все данные анонимизированы.
Николай Самохвалов: Написали программисты специально?
Константин Евтеев: Чтобы не видеть настоящих пользовательских данных.
Николай Самохвалов: Это круто. Но подразумевается, что если схема меняется, мы должны переписывать. Требует дополнительного времени.
Константин Евтеев: В том числе. В случае, если работаете с семплом данных, это неизбежно, поднимая dev-среды. Перед тем, как вести разработку в прод, нужно делать двойную работу: сначала делать разработку в dev- и test-средах. Поэтому когда появляются новые проекты, мы всей командой агитируем за то, чтобы использовать не семплирование данных, а чтобы писали фикстуры, чтобы не иметь единую точку отказа везде.
Николай Самохвалов: В основном что за языки используются? Ruby или PHP?
Константин Евтеев: Фикстуры можно делать на различных языках.
Николай Самохвалов: У вас какие?
Константин Евтеев: PHP.
Николай Самохвалов: Вы стремитесь от семплирования уйти в сторону синтетических данных, чтобы на тестировании использовать?
Константин Евтеев: Да, во-первых, будут сначала тесты писаться, с другой стороны, не будет необходимости отдельно поддерживать семплинг данных. И решать в том числе вопросы аварий семплера. Он же может ломаться, если кто-то поправил структуру, но не написал миграцию, не добавил в семпл…
Николай Самохвалов: Как говорил один известный постгрессоид, жизнь богаче. Живое распределение данных — оно другое. Правильно? Может быть, продакшн-данные живые тоже есть смысл брать? Я просто услышал мысль что круто — фикстуры, но мне кажется, что нужно посмотреть живое распределение.
Константин Евтеев: В этом случае как раз стейджинг. Необходимость нагрузочного тестирования, и есть набор серверов, куда можно развернуть полный бэкап прод-базы.
Николай Самохвалов: Про мониторинг интересно спросить. Как понимаете, что там индексов не хватает? Что используете? Знаю, у вас есть доклад про мониторинг, Дмитрий делал. Можно пару слов: какие инструменты?
Дмитрий Вагин: Идея простая. Берете все, что есть, и кидаете в Graphite или куда-то еще. А потом строите дэшборды. Когда где-то на очень загруженной табличке не хватает индекса, обычно это сразу видно в том, что на сайте ничего не работает, или видно это в топе запросов где-то в pg_stat_activity и прочем. Более сложные кейсы приходится разбирать по графикам в дэшбордах. Допустим, тут чтения с диска слишком много на этой табличке. Смотришь, где какие там запросы идут, и где-то, и возможно, надо индекс чуть другой сделать.
Николай Самохвалов: Почему не взять что-то существующее?
Дмитрий Вагин: Можно, но когда берешь что-то существующее, приходится разбираться, как это существующее берет эти метрики и что ты в итоге видишь. Приходится разбираться, что оно берет, как обрабатывает, куда складывает и что ты в итоге видишь. И вот я не могу так просто взять и доверять какой-то циферке, которую говорит какая-нибудь утилита, например munin. Но в плагин munin можно залезть и посмотреть, что и как там…
Николай Самохвалов: В Zabbix тоже ведь можно увидеть, как там всё работает.
Дмитрий Вагин: Zabbix просто не вывезет на наших объемах.
Николай Самохвалов: Интересно.
Дмитрий Вагин: У нас Graphite-то не всегда всё вывозит.
Николай Самохвалов: Вопрос из чата: как вы делаете vacuum full огромной таблицы более 100 ГБ?
Дмитрий Вагин: Есть два варианта. Первый – не делать.
Николай Самохвалов: Плохой вариант.
Дмитрий Вагин: Ну… Зачем vacuum full?
Николай Самохвалов: Кстати, правильно. Я не сразу понял этот вопрос. Зачёркиваем «full». Никто не делает vacuum full просто так, не нужно это делать.
Дмитрий Вагин: Vacuum работает.
Николай Самохвалов: Ок. Мы перед трансляцией мы обсуждали, что размер 15 ТБ даже уменьшается. За счет чего?
Константин Евтеев: Мы это назвали «хвост». Есть данные, которые активные, горячие объявления. Потом есть, допустим, удаленные, заблокированные, а есть удаленные навсегда, то есть когда ты их удаляешь и больше не видишь их в интерфейсе. Но мы обязаны всю эту информацию хранить. Но это можно делать уже в другом месте — тоже в Postgres.
Николай Самохвалов: В 9.2?
Константин Евтеев: (Смеётся). Ну, например, да. Мы написали скрипт, который собирает все старые данные и переносит их в другую базу, доступную только для бэкофиса. У нас была высокая скорость роста объема основной базы данных. После запуска скрипта, переноса в данных в «хвост» и последующего реиндекса объем уменьшился и скорость роста замедлилась, почти остановилась. Периодически мы делаем реиндекс.
Николай Самохвалов: А всякие pg_repack не используете, или аналоги?
Константин Евтеев: В том числе используем. Сейчас Дима расскажет случай.
Дмитрий Вагин: Я один раз выключил специально на одной табличке автовакуум. И забыл его включить. Через две недели увидел, что табличка вместо 20 ГБ стала 300 ГБ. Думаю: «Ладно, надо исправить». Автовакуум отработал честно, она наполовину пустая, и надо ее как-то ужать. Вариант какой? Сделать даунтайм, выключить продакшн-нагрузку, сделать vacuum full — не очень. Второй вариант — использовать скрипт PG Compactor, который написал Сергей. Это старая всем известная идея — апдейтить с конца таблицы странички…
Николай Самохвалов: Я тут вступлю. Это пример epic fail опенсорса, когда Hubert depesz Lubaczewski поднимает какую-то тему, потом ребята легируют, потом Сергей Коноплёв включается, потом Максим Богук включается, и у всех своя версия. Они вот такие расфорканные три форка, а теперь я слышу про четвертый форк! Не знаю, может в NDA что-то такое, что каждый свое пишет, не могут договориться. Я пытался использовать все это, но для себя лично остановил выбор на pg_repack. Как раз в тот момент, когда я об этом задумался, Amazon добавил официальную поддержку pg_repack. Это практически индустриальный стандарт становится. А вы говорите, что используете апдейты. Для тех, кто не знает, pg_repack делает отдельную таблицу, триггерами следит за изменениями. Почему не pg_repack? Второй вопрос: почему всё-таки 4-я версия?
Сергей Бурладян: pg_repack редактирует служебный каталог Postgres. Нам это кажется страшным.
Николай Самохвалов: Напрямую?
Сергей Бурладян: Да.
Николай Самохвалов: Amazon не кажется.
Сергей Бурладян: Да. И потом, он использует триггеры, это дополнительная нагрузка во время продакшна. Занимает место, чуть ли не двукратный объем. Поэтому мы не хотели использовать pg_repack. И подход с постоянным апдейтом этих строчек, чтобы они сдвигались в свободное место, нам кажется более подходящим.
Николай Самохвалов: Похоже на дефрагментацию Windows.
Сергей Бурладян: Да.
Николай Самохвалов: Почему своя версия?
Сергей Бурладян: Просто нам хотелось сделать простую версию, чтобы было понятно, как она работает. Там нет фишек, которые есть в тех версиях, о которых ты говорил. У нас там, грубо говоря, вызов апдейтов и вызов вакуума. Тоже написан на Perl. Сейчас буду, наверное, на Python. Эта штука используется редко, но иногда используется.
Николай Самохвалов: Давайте немного вернемся назад. Как понять, что таблица распухла? Есть одна компания, они не показывают график блоттинга, объясняя это тем, что чтобы нормально показать, нужно использовать расширения, которые создают большую нагрузку. То есть чтобы нормально понять блоттинг, нужно влезть в кишки, потрясти их, и это очень заметно. А как вы это делаете?
Дмитрий Вагин: Мы не смотрим на блоат. Вакуум работает нормально. Достаточно не выключать автовакуум, и все будет отлично. Он чуть более агрессивен, чем дефолтный.
Николай Самохвалов: 1%, 2%? Там есть threshold, который говорит, когда analyze делать, и когда делать сам вакуум в зависимости от того, сколько в таблице грязного.
Сергей Бурладян: autovacuum_vacuum_scale_factor = 0.0014. По умолчанию vacuum_cost_limit, по-моему, стоит цифра 200, у нас 700. Это тоже у нас выкручено, чтобы он чаще срабатывал.
Николай Самохвалов: Самое интересное — это сколько процентов.
Сергей Бурладян: Пока есть место на дисках, то можно не задумываться над этим. Можно задуматься, если видно, что запросы тормозят, допустим.
Николай Самохвалов: Мониторинг?
Сергей Бурладян: Да.
Николай Самохвалов: Был вопрос в другую сторону: про безопасность. Храните ли вы ACL в Postgres?
Сергей Бурладян: ACL не используем. Row level security не используем.
Николай Самохвалов: Какие инструменты используете для файловер резервной репликации больших данных?
Сергей Бурладян: Свои инструменты, готовых не используем. Есть набор скриптов для файловера. До сих пор используем Londiste, причем наш, патченный, который позволяет после файловера автоматически восстановиться.
Николай Самохвалов: Londiste сейчас у вас основной инструмент?
Сергей Бурладян: Да, основной для логической репликаци.
Николай Самохвалов: PL/pgSQL код есть у вас?
Константин Евтеев: Есть, много.
Николай Самохвалов: Нравится?
Константин Евтеев: Зависит от случая. Но вообще нравится.
Николай Самохвалов: Расскажите, как готовите, тестируете, дебажите.
Константин Евтеев: Я больше расскажу про версионирование PL/pgSQL кода. Есть вопрос: как версионировать код и схему базу данных вместе с кодом приложения? Ведь когда у вас много серверов приложений, и они выкатываются постепенно, необходимо поддержать старую и новые версии PL/pgSQL кода.
Если мы говорим про схему базы данных, то мы это делаем через миграции. Все они должны быть обратно совместимыми, потому что в бою одновременно может находится как уже новые апликейшены, так и старые. Что же делать с кодом? Мы пришли к двум подходам, и сейчас в процессе перехода с одного на другой.
Первый подход — когда мы клали все наши хранимые процедуры на PL/pgSQL вместе с кодом апликейшена. Далее написали обвязку, которая читает каталог, каждая процедура хранится в отдельном файле, читает от него чек-сумму, обращается к базе, непосредственно в базе хранится контрольная сумма, то есть сравнивает, есть ли такая версия процедуры или нет. Она создает новую хранимую процедуру с припиской определенной версии, регистрирует в таблице. Например, создать пользователя, версия 1. В таблице хранится просто 1, а потом из этой таблички билдится словарик, который хранится вместе с приложением, и приложение, если видит обвязку «создать пользователя», у него есть словарик в котором написано, что этот суффикс должен быть 1, 2 или 3.
Есть недостатки.


  1. Когда появляется много проектов, всё это нужно в каждый проект внести и поддерживать.
    Второй недостаток — что в данном случае те процедуры, которые вызывают другие процедуры, уже так не получится версионировать. Соответственно, пришли ко второму подходу.
  2. Второй подход: когда мы просто держим некий пул пользователей, например. Перед деплоем приложения пользователь ротируется по кругу, у одного из пользователей полностью пересоздается схема с хранимыми процедурами. Новый деплой приложения происходит вместе с новым пользователем и схемой.

Николай Самохвалов: Один раз при создании. Прикольно. Видел в презентации, что вы достаточно много используете переменные сессий в Postgres — GUC Variable (Grand Unified Configuration).
Константин Евтеев: Да. Расскажу. Бояться их не нужно. Этот кейс мы изучили. В каком случае он у нас возник? Мы делали доставку в нашу аналитическую подсистему. Мы ее делаем с помощью очередей PgQ и deferred-триггера. Вешаем на таблицу deferred-триггер на первое срабатывание. Если запись меняется несколько раз, нам нужно поймать именно первое изменение. В момент первого изменения мы по старым данным видим все записи, а новые данные можем выбрать селектом непосредственно из базы.
Дальше у нас возникла следующая задача. Что, если у нас много связанных таблиц, а нам хочется непосредственно вместе с изменением этой таблицы еще прокинуть сигнал о том, что менялся объект в соседней таблице? В данном случае, когда меняем данные в соседней таблице, мы берем и выставляем сессионные переменные. Например, выставляем ключ: user 5 сделал какое-то действие. Только в текущей сессии. И в эту переменную делаем key value пары: вдруг несколько будет таких пользователей. И далее уже непосредственно в deferred-триггере проверяем эти ключи, сессионные переменные, и если что, подмешиваем некие сигналы. Таким образом у нас получается производная от данных: в таком виде на источнике их нет, а на приемник уже придет совершенно другой набор. Мы все это потестировали под нагрузкой, поняли, что никакого overhead при выставлении и использовании сессионных переменных не идет. (Во всяком случае, мы его не увидели).
Николай Самохвалов: А JSON-B или JSON используете?
Константин Евтеев: В большинстве случаев нет. В некоторых местах начинаем использовать, но мы не используем функции для работы с JSON-B. Почему? Как выяснилось, мы CPU bound. Сам формат JSON-B удобен для хранения и для работы, но когда мы CPU bound, то эта вся работа ведется на уровне приложения. Но, кстати, при этом мы активно используем Hstore. Он исторически раньше появился, но там все те же проблемы с CPU.
Николай Самохвалов: Видел в твоих слайдах про это. Когда ты говоришь про key value, это про hstore?
Константин Евтеев: Да.
Николай Самохвалов: А индексы? Они хранятся, hstore? GIN? Вообще GIN, GIST используются как-нибудь?
Константин Евтеев: В одном случае использовали. С ним есть некая история. Нужно внимательно читать документацию. В частности, у нас был кейс, когда надо выставлять fast update off. Потому что он до определенного размера очень хорошо работал, запись была достаточно активная в эту таблицу. А потом начало замирать при апдейтах.
Сергей Бурладян: Это написано в документации Postgres.
Дмитрий Вагин: Когда идут изменения по колонке, на которой есть GIN-индекс, то, чтобы делать изменения быстро, он строит у себя в небольшом поле маленькое дерево, и потом при чтении обращается к основному дереву и к изменениям в памяти. В какой-то момент место заканчивается, ему надо все это размазать — перенести изменения в большой индекс. В этот момент у нас все вставало на две минуты, и все упиралось в это.
Николай Самохвалов: Это означает, что в единицу времени было слишком много апдейтов?
Дмитрий Вагин: Апдейтов всегда много, потому что продакшен у нас ого-го.
Николай Самохвалов: 10000 транзакций, или сколько…
Дмитрий Вагин: Да-да-да. И просто нет момента, когда он может тихонько взять и сбросить. В итоге он говорит нам: «Я уперся в лимит, мне надо все сбросить. Подождите». И мы ждём. В итоге мы отключили fast update, и всё стало хорошо.
Николай Самохвалов: В целом каждый апдейт стал медленнее?
Дмитрий Вагин: Не заметили.
Николай Самохвалов: Из этих 10000 транзакций сколько модифицирующих?
Константин Евтеев: 1500. 90 000 в минуту. На разных базах, кстати, по-разному. Этот пример — в контексте одной из них. На других базах везде примерно 10 тысяч транзакций в секунду. Где-то запись, грубо говоря, 500 транзакций в секунду, а все остальное — чтение. А есть базы, где при общей нагрузке 8 тысяч транзакций в секунду запись 4 тысячи.
Николай Самохвалов: Разные версии, базы – это разные сервисы, у них разные команды?
Константин Евтеев: Есть разные сервисы, там где микросервисная архитектура, один сервис – одна база. Есть еще legacy – это монолит, у которого есть несколько баз, часть связана, в том числе и Londiste и event streaming, мы события через PgQ гоним. Часть из них связывается на уровне приложения.
Николай Самохвалов: Используете ли PostgreSQL в качестве аналитической базы данных?
Константин Евтеев: Раньше использовали, сейчас нет. У нас Vertica, и мы очень довольны.
Николай Самохвалов: Как часто из Postgres пуляете данные?
Константин Евтеев: В реальном времени с минимальной задержкой. Гоним через PgQ, а Vertica забирает. У нас для этого есть один выделенный сервер. Дальше команда Vertica забирает их оттуда в определенном порядке. За счет того, что у нас есть PgQ, на sequence все данные упорядочены.
Николай Самохвалов: Если что-то удаляется, обновляется, в Vertica тоже обновляется, или вы складируете новую версию, старую тоже храните? Строчка, допустим, обновилась.
Константин Евтеев: В Vertica хранятся все версии. Есть очень хороший доклад Николая Голова и статья на Хабре, как непосредственно реализовано хранение в Vertica.
Николай Самохвалов: Что с другими БД? Что c SQLite?
Константин Евтеев: В целом у нас такой подход в компании — когда команда начинает делать новый проект, она вольна выбирать из технического радара набор технологий, который ей удобней и какой более оптимален для решения ее задач.
Николай Самохвалов: ClickHouse нет в радаре?
Константин Евтеев: Есть. ClickHouse используем для хранения метрик.
Николай Самохвалов: CockroachDB?
Константин Евтеев: Нет. Для одного из решений хорошо подошел SQLite. Он уже в продакшене. В том числе для этого хорошо подходил и Postgres… Но определенное количество TPS выдал SQLite, мы его выбрали.
Николай Самохвалов: Вы помогаете всем командам по Postgres?
Константин Евтеев: Да. Делаем DBaaS, предоставляем базу данных под определенные требования. Если необходимо, выступаем в качестве некого SWAT, который помогает реализовывать бизнес-логику тем или иным командам. Можем научить или реализовать определенную бизнес-фичу.
Николай Самохвалов: Нет такого, чтобы использовать не только Postgres? Хотите — MongoDB, хотите — Tarantool, например?
Константин Евтеев: Для определенных задач есть и Tarantool, и MongoDB.
Николай Самохвалов: А MySQL, MariaDB используете?
Константин Евтеев: MySQL в продакшене нет. Может быть, есть на внутренних проектах.
Николай Самохвалов: Еще вопросы в чате. Что можете посоветовать для онлайн-миграции с 9.3 на 9.5 или 9.6? Londiste, Bucardo или что-то ещё? Сергей, может, лучше знает?
Сергей Бурладян: Мы просто не рассматривали такие процессы миграции, потому что, скорее всего, на первый взгляд, они просто не справятся с нашим трафиком. Ни Londiste, ни Bucardo не смогут нам логически реплицировать всю базу.
Николай Самохвалов: Давайте представим ситуацию (я знаю, что это не так), что у вас всё на 9.3. Ну, допустим, не повезло, и вам нужно что-то более свежее. Ваши действия?
Сергей Бурладян: Мы сделаем даунтайм и используем pg_upgrade.
Николай Самохвалов: На сколько ляжет сайт Avito?
Сергей Бурладян: Мы используем pg_upgrade, штатную утилиту Postgres, а у нее есть замечательный режим работы с хардлинками. Благодаря этому режиму даунтайм займет приблизительно 3-5 минут. 10 — максимум.
Николай Самохвалов: То есть берем документацию, изучаем, все штатными средствами, с таким подходом с хардлинками pg_upgrade. Вот вам ответ.
Сергей Бурладян: Да.
Николай Самохвалов: Как реплику ввести?
Сергей Бурладян: pg_upgrade может проапгрейдить только master, потому что ему нужно писать в сервер. А standby проапгрейдить не может. У нас есть в бою standby. Мастера нам не хватает, часть нагрузки идет на читающий standby. Мы уже не можем запустить просто один master без standby. (Раньше мы так и делали: апгрейдили мастер, потом спокойно пересоздавали standby, и все). К счастью, авторы Postgres написали в документации специальный алгоритм, который с использованием утилиты rsync позволяет после pg_upgrade проапгрейдить ещё и standby. Его хитрость заключается в том, что просто используется возможность rsync скопировать хардлинки. Грубо говоря, он воссоздает на standby такую же структуру хардлинков, как и на мастере. На standby есть те же все данные, что и на мастере. Если создать там такие же хардлинки во время даунтайма, получится… При запуске rsync мастер должен быть выключен.
Николай Самохвалов: Если есть несколько реплик, даунтайм увеличится, нужно ещё за этим последить.
Сергей Бурладян: Да.
Николай Самохвалов: Делаете вручную или автоматизируете?
Сергей Бурладян: Подготавливаемся к задаче, готовим набор команд, которые выполнить надо, и выполняем их. Вручную.
Николай Самохвалов: Скажите о каких-то подводных камнях, с которыми столкнулись при работе с Postgres, вещи, которые хотелось бы улучшить, проблемы, баги, недавно найденные.
Сергей Бурладян: Находим баги периодически в Postgres, потому что у нас нагрузка серьезная. Пару дней назад выяснили, что, оказывается, планы в триггере не инвалидируются при изменении таблицы. Есть триггер, сессия, она работает, потом мы делаем alter table, меняем колонку, а триггер не видит изменений, начинает падать с ошибками. Приходится после alter колонки в той же транзакции альтерить хранимку, чтобы сбросила свой кэш. Еще находили баги в вакууме. Про кэш не репортили.
Николай Самохвалов: Может быть, не как баг, а как…
Сергей Бурладян: Можно об этом написать в рассылку.
Николай Самохвалов: Нужно.
Сергей Бурладян: Согласен.
Николай Самохвалов: Бывают ли у вас дедлоки, как боретесь?
Сергей Бурладян: Бывают. Смотрим места, в которых появляются, пытаемся решить их.
Николай Самохвалов: Мониторинг об этом докладывает?
Сергей Бурладян: Да, в логах видно, что произошел дедлок.
Николай Самохвалов: Но логи же не будешь каждый день читать в таком количестве. Скажи, ну вот например, за последнюю минуту 10 дедлоков, смс получишь?
Константин Евтеев: Нет, но выявятся сразу. Как с ними бороться? Дедлоки, которые выявляет Postgres, один из запросов будет убит. Причем какой — неизвестно. Бороться с ними — брать блокировки в одном и том же порядке. Сортировку при взятии блокировок нужно взять. Или идти от более частного объекта к общим отсортированным.
Но более страшные дедлоки — когда их Postgres не может детектить. Если у вас, допустим, микросервисная архитектура, вы открываете транзакцию, берете lock на ресурс, и дальше приложение делает запрос в другой сервис, а он неявно обратится к этой же базе и попробует взять lock на этот же ресурс. В таком случае это будет вечный дедлок, который никак не задетектится. Дальше только по таймауту, возможно, отвалятся.
Дмитрий Вагин: Еще по поводу смс с десятью дедлоками. Если Deadlock возникает, одна транзакция отваливается, если это как-то аффектит прод, там вывалится исключение, ошибка, и в sentry создастся ошибка, она придет потом на почту. В принципе, все видится.
Николай Самохвалов: Придут последствия. Есть вопрос, который хотелось подробнее осветить. Как делаете бэкапы? Какие инструменты используете, как проверяете, в каком объеме хранится, насколько можете взять назад бэкап и восстановиться?
Сергей Бурладян: Делаем их с помощью pg_basebackup, вокруг него есть наша обвязка в виде скриптика. Используем Point-in-Time Recovery (PITR) у нас идет постоянное архивирование WAL-файлов, и мы можем восстановиться на любую точку из прошлого. Дополнительно используется задержка проигрывания WAL на одном из standby на 12 часов.
Николай Самохвалов: Был доклад на эту тему в прошлом году.
Сергей Бурладян: Да.
Николай Самохвалов: Интересующиеся могут найти видео, PDF, статью. Мы сможем восстановить бэкап месячной давности?
Константин Евтеев: Да.
Сергей Бурладян: Костя имеет в виду, что наш бэкап баз, который мы делаем, еще бэкапит дополнительно отдел DevOps, складывают у себя в хранилище. Они бэкапят с помощью Bareos, раньше был Bacula. Они берут наши бэкапы и складывают у себя отдельно.
Николай Самохвалов: Сколько места занимают?
Сергей Бурладян: Наши, по-моему, около 60 ТБ. Сколько занимают потом эти бэкапы у DevOps, не знаю.
Николай Самохвалов: Утилиты типа wal-e рассматривали?
Сергей Бурладян: Нет, у нас собственный скрипт. Знаем, что он делает, он простой, просто архивирует валы к нам в архив.
Николай Самохвалов: Пришла просьба выложить твою версию скрипта, этого компактора. Можешь это сделать?
Сергей Бурладян: Думаю, что могу. Мы планируем выложить до HighLoad нашу пропатченую версию Londiste на GitHub, и, возможно, этот скриптик тоже.
Николай Самохвалов: Присылай мне, я тоже твитну.
Теперь тема, которую невозможно обойти стороной – это поиск. Как у вас всё устроено, в том числе фуллтекст, что используете? Почему опять слово «зоопарк» тут вылезает?
Константин Евтеев: Полнотекстовый поиск в Postgres на текущий момент мы не используем. До недавнего времени мы использовали его, но в связи с тем, что достаточно большие объемы данных и большое количество запросов, соответственно, мы упираемся в CPU, мы CPU bound. Но у нас есть отличная команда «Sphinx».
Николай Самохвалов: Если CPU bound, почему бы не раскидать по нескольким машинкам, зашардить?
Константин Евтеев: Мы использовали его для очень узкоспецифичной задачи, когда там был очень небольшой объем данных. Мы поняли, что объем будет расти, после этого конкретно эту задачу перенесли на сторону Sphinx, который у нас был давно.
Николай Самохвалов: На сколько шард?
Константин Евтеев: Если не ошибаюсь… Лучше не буду говорить цифру.
Николай Самохвалов: Примерно.
Константин Евтеев: Есть статьи, и расшифровки, в том числе на Хабре, и видео с HighLoad, лучше посмотреть там. Я знаю, как мы доставляем данные в эти поисковые подсистемы.
Данные, которые необходимо доставить в Sphinx, если мы говорим про выдачу активных объявлений на сайте, мы материализуем и с помощью Londiste отправляем на отдельную машину, где в свою очередь, эти данные мы, наоборот, берем и раскладываем на много кусочков, то есть, грубо говоря, партицирование идет. Далее во много потоков мы отправляем эти данные Sphinx. Таким образом за полчаса мы можем полностью пересоздать индекс активных объявлений сайта несколько раз. При этом также мы реализовали непосредственно real-time доставку в sphinx-индексы тоже через PgQ, когда эти данные приезжают на сервер для индексации, далее мы их перекладываем еще в одну очередь, и она уже непосредственно работает через консумер со Sphinx.
Николай Самохвалов: Еще такой вопрос. Я правильно понимаю, что сейчас задержка довольно большая получается именно поисковая? То есть если я размещаю объявление…
Константин Евтеев: Это продуктовые требования. У нас они сейчас полчаса.
Николай Самохвалов: То есть если я сам объявление добавил, я его сам не найду по полнотексту?
Константин Евтеев: В выдаче оно появится через полчаса. В том числе это ограничение со стороны продукта. Ведь неизвестно, какое объявление ты добавишь. Хорошее оно или плохое.
Николай Самохвалов: В списке он сразу появится? Или как? Когда на slave попадёт.
Константин Евтеев: С технической точки зрения, мы его можем доставлять в индекс очень быстро. Но перед тем, как его по-настоящему показать, его надо проверить.
Николай Самохвалов: А, тут имеется в виду модерация.
Константин Евтеев: Да, модерация. Мало ли что в этом объявлении написано, какой товар можно продавать. Если говорить про наши очереди, то технически скорость доставки во внешний индекс — несколько секунд.
Николай Самохвалов: Я отвлекался на вопросы, правильно понял, что очереди в Sphinx через PgQ тоже?
Константин Евтеев: Да.
Николай Самохвалов: То есть вы так очень любите PgQ?
Константин Евтеев: Да, мы очень любим. И это очень быстро. В общем виде это гарантированный SLA несколько минут. А в общем виде это несколько секунд.
Николай Самохвалов: А Kafka вообще у вас отсутствует?
Константин Евтеев: Отсутствует. У нас есть nsq ещё.
Николай Самохвалов: А он почему?
Константин Евтеев: Очень хорошо себя зарекомендовал, хорошая очередь. И, соответственно, она тоже у нас используется. То есть, с одной стороны, у нас есть очередь, которая идет со стороны базы через PgQ, а через NCQ идут в том числе clickstream события, события от приложений…
Николай Самохвалов: «Используете ли PgBouncer?». Я на самом деле этот вопрос тоже не буду задавать. Как раз ваш коллега Виктор Ягофаров докладывал в Сан-Франциско. Я лучше в Twitter ссылку помещу. И там можно будет посмотреть pdf, может, даже видео есть.
Константин Евтеев: И в Санкт-Петербурге он тоже рассказывал этот же доклад.
Николай Самохвалов: Вы используете Materialized views?
Константин Евтеев: В терминах Postgres мы не используем Materialized views в связи с тем, что их надо обновлять вручную. Мы используем Materialized views в нашей интерпретации. Есть отличные материалы. Наверное, я тебе дам ссылку, можно ее будет в том числе затвитить. Потому что очень классный подход, то есть realtime materialized view, когда она реализована на триггерах. [Ссылки: 1, 2].
Николай Самохвалов: Скорее всего, я уже постил.
Константин Евтеев: Наверное, да. То есть когда мы ставим некие триггеры, которые в now() время обновления записи. И далее в deferred триггере мы можем посмотреть: если это время еще не равняется now(), то мы просто берем, удаляем запись, и заново из view её селектим и далее мы инсертим, соответственно, в таблицу, которая является как раз нашей мат.вьюхой.
Николай Самохвалов: То есть это на самом деле таблица?
Константин Евтеев: Да, это просто таблица. Поддерживаем ее на триггерах.
Николай Самохвалов: Много таких? Каков объем данных?
Константин Евтеев: Есть на десятки миллионов записей, есть на сотни миллионов записей.
Николай Самохвалов: А что за задачи?
Константин Евтеев: Задача денормализации данных.
Николай Самохвалов: Кстати, вопрос был по денормализации.
Константин Евтеев: То есть сначала удобно хранить данные нормализованно. Но когда мы говорим о том, что их нужно будет выбирать с высокой частотой. Ну, например, выдача или индексация. Соответственно, когда идет на тысячи запросов в секунду, вот эти все join’ы не оказывается очень подходят для этого. Соответственно, мы данные денормализуем, и таким образом мы в том числе реализуем выдачу. То есть все, что нашлось в Sphinx, дальше работает из этого материализованного представления, которое хранится на отдельном выделенном сервере. И далее там даже мы выкрутили размер shared-буферов до размера этого материализованного представления.
Николай Самохвалов: Получается, что этот deffered-триггер отправляет обновления на другой сервер.
Константин Евтеев: Мы получаем материализованное представление, а дальше уже с помощью Londiste мы его реплицируем на отдельный сервер.
Николай Самохвалов: То есть когда выйдет десятка, вы Londiste будете выпиливать?
Константин Евтеев: Мы реализуем все наши подходы к восстановлению.
То есть, грубо говоря, существуют аварии, когда у вас может ваша логическая репликация оказаться или в будущем относительно источника, или в прошлом относительно источника. И в данном случае нужно согласовать данные: либо ее отмотать в прошлое, либо на источнике как-то подвинуть текущие события, чтобы они еще раз проигрались на реплике в связи с тем, что она в прошлом оказалась.
Николай Самохвалов: То есть Londiste останется, или она будет заменена на логическую репликацию, нативную в десятке?
Константин Евтеев: Надо переходить на нативную репликацию.
Николай Самохвалов: То есть сейчас еще не готовы, если коротко?
Сергей Бурладян: Да. Когда мы все реализуем, то, что мы патчили в Londiste и тому подобное…
Николай Самохвалов: Сколько, по-твоему, времени это может занять?
Сергей Бурладян: Не знаю. Если этим заниматься, то немного времени.
Николай Самохвалов: Ещё вопросы из чата. Используете ли вы ltree?
Сергей Бурладян: По-моему, сейчас уже нет. Раньше пытались использовать для категорий. Но это был просто эксперимент. Вообще не используем.
Николай Самохвалов: На каком Linux Postgres крутится? Какие настройки ядра тюнить приходилось?
Сергей Бурладян: Postgres крутится на Debian GNU Linux. Сейчас восьмая версия, насколько я помню. Но тюнить особо ничего не тюнили.
Николай Самохвалов: То есть все дефолтовое. OOM-killer там по умолчанию (я не сисадмин) включен или нет?
Сергей Бурладян: Да, у нас включен.
Николай Самохвалов: Ой-ой-ой.
Сергей Бурладян: Но у нас он обычно не падает. Мы тюнили read-ahead, так как у нас индекс-сканы, чтобы они не зачитывали там большие блоки. Что еще тюнили? Ну, так вроде больше ничего не тюнили. Мы выключаем NUMA, это мы не используем.
Николай Самохвалов: А huge pages?
Сергей Бурладян: Huge pages не используем.
Николай Самохвалов: Ок. Вопрос такой еще. Администрирование – это не мой конек, а программирование больше. Какие типы данных еще есть, кроме hstore? Что там экзотического? Начиная, может, даже с какого-нибудь типа данных Range.
Сергей Бурладян: Range, по-моему, появились в 9.4.
Николай Самохвалов: Я думаю, раньше.
Сергей Бурладян: В 9.3, наверное, да?
Николай Самохвалов: Честно говоря, я не знаю.
Сергей Бурладян: В 9.2 были тоже?
Николай Самохвалов: Еще раньше должны были появиться.
Сергей Бурладян: Ну, мы, по-моему, не используем их.
Константин Евтеев: Использовали. Мы делали некую задачу. Как раз использовали в том числе расширение btree_gist. Когда нужно было сделать непересекающееся расписание, то есть CONSTRAINT vs строили.
Николай Самохвалов: По датам?
Константин Евтеев: Да.
Николай Самохвалов: То есть это constraint exclusion.
Константин Евтеев: Да.
Николай Самохвалов: То есть это понравилось, да? И вы не в двух колонках храните начало и конец периода, а в одной колонке.
Константин Евтеев: В данном примере была одна реализация, и в принципе она работала.
Николай Самохвалов: Именно непересекающихся. Я понял. Еще какие-то типы данных? Какие-то экзотические постгресовые… Tsvector ушел? Жаль. JSON-B еще не пришел. Понятно.
Конечно, интересно, про Vertica отдельно было бы поговорить. На самом деле как-нибудь, может быть, мы бы сделали событие, связанное именно с Vertica, потому что это тоже BI всякие, очень интересно было бы.
Константин Евтеев: Надо привлечь Николая Голова, Артема Данилова. И можно организовать событие. Я думаю, будет очень интересно.
Николай Самохвалов: Да. Я думаю, это близко к Postgres. Я помню, мы про Greenplum в Тинькове делали. И это было тоже интересно. Так что это на будущее.
Ещё вопрос. Используете ли вы SSD под Postgres? И как часто их меняете?
Константин Евтеев: SSD себя очень хорошо зарекомендовали.
Николай Самохвалов: Какие SSD, знаете?
Константин Евтеев: Не знаю, не скажу. У нас есть машины как на SSD, так и просто на жестких дисках. SSD-диски еще ни разу не меняли.
Николай Самохвалов: Да ладно? Круто.
Дмитрий Вагин: Один раз какой-то вылетал. Но не потому что он SSD. Не как SSD умирает, что у него ресурс кончился. Когда мы, по-моему, переезжали откуда-то куда-то и посмотрели, сколько ресурса осталось, и там было 90 с чем-то процентов. Можно смело использовать SSD, мне кажется. Хорошие — можно смело.
Николай Самохвалов: Ок, а партиционирование? Pg_pathman?
Дмитрий Вагин: Партицирование есть. Там по датам просто. Таблица, которая растет, разбивается по датам.
Константин Евтеев: И также еще используется решение на PL/Proxy. Соответственно, у нас есть 16 шардов, где мы храним, например, отправленную почту. Можно раскладывать там.
Николай Самохвалов: И от этого отказываться не собираетесь?
Константин Евтеев: Она себя очень хорошо зарекомендовала, работает. И необходимости переделывать нет.
Николай Самохвалов: Наверное, мы будем закругляться. Еще что-то интересное можете рассказать? Там был еще вопрос про факапы. Из последнего что-нибудь припоминаете? Как делать не стоит c Postgres? Выключать автовакуум…
Константин Евтеев: В случае, если у вас есть Master и Standby, когда вы делаете реиндекс и создали новый индекс уже, не надо его удалять сразу с Master.
Николай Самохвалов: Удалять старый?
Константин Евтеев: Да, старый индекс не надо удалять с Master. Потому что на Standby не инвалидируется кэш. И, соответственно, у вас сломаются планы запросов.
Николай Самохвалов: А какое время нужно? Когда его нужно?
Константин Евтеев: Время жизни вашей сессии бэкенда. Но лучше его умножить в несколько раз. Тогда можно быть спокойным. Чтобы когда вы уже удалили индекс, все бэкенды на stand-by были новые.
Николай Самохвалов: Да, это интересный кейс. А какие у вас есть долгие сессии, например, час?
Константин Евтеев: Когда вы используете PgBouncer, соответственно, вы там выставляете какое-то время жизни этой сессии. Мы эмпирически выставили 20 минут.
Николай Самохвалов: То есть долгих сессий у вас нет?
Константин Евтеев: Долгих транзакций нет. Это неприемлемо.
Николай Самохвалов: Про факапы. Ещё давайте какой-нибудь.
Константин Евтеев: Другой случай, которого мы избежали. Сергей рассказывал про апгрейд. Там в том числе необходимо было включать при апгрейде вместе со standby на hardlink с rsync.
Николай Самохвалов: С чего началось? С какой версии?
Константин Евтеев: Когда мы обновлялись с 9.2 на 9.4, мы это обнаружили. Там было необходимо после апгрейда master выключить vacuum, запретить все коннекты к master, в том числе включить master и выключить. И главное — чтоб в этот момент никто не записал. И после этого только делать rsync. В случае, если у вас кто-то запишет на master в это время, соответственно, далее вы получите битый standby. И когда вы об этом узнаете… Всё зависит от времени того, когда вы будете обращаться к тем битым данным.
Николай Самохвалов: Это все в тест-окружении словили?
Константин Евтеев: Мы изучали, как нам произвести апгрейд. Непосредственно как раз Сергей в том числе этим занимался вместе с Виктором Ягофаровым.
Сергей Бурладян: В тестах мы словили, да. Еще было в рассылке обсуждение такой проблемы.
Николай Самохвалов: Вопрос: Как по-умному готовиться к апгрейду?
Сергей Бурладян: Читать release notes новой версии. Там написано, что нужно делать после апгрейда.
Николай Самохвалов: Прочитать release notes всех минорных версий. От вашей точки старта до вашей точки финиша. Прочитать все внимательно.
Сергей Бурладян: И попробовать на тестовом сервере провести апгрейд.
Константин Евтеев: С полными данными.
Николай Самохвалов: Да.
Дмитрий Вагин: Ну и у нас тестовое окружение. Вот эти сэмплы все собираются быстренько. Мы быстро обновили версии в образах, накатили туда сэмплы и прогнали все наши тесты, что есть. И все было хорошо. Проверили, что продукт готов к новой версии.
Николай Самохвалов: Потом какое-то время пожили с этим, наверное… Или прям уже все?
Дмитрий Вагин: И ночью понеслось.
Николай Самохвалов: Уже ночью? Ок.
Константин Евтеев: И еще один совет, раз говорили про битые данные. На конференции в Оттаве был отличный доклад про то, как могут ломаться ваши данные, как могут биться. Что может быть: авария, битые страницы, битые индексы, commit lock, все, что угодно. И там основной позыв был к тому, что железо сейчас достаточно дешевое, и покупайте хорошую систему хранения, которая правильно делает fsync, потому что после аварии потом восстанавливать будет очень сложно и дорого.
Николай Самохвалов: Кто доклад делал? Помнишь?
Константин Евтеев: Christophe Pettus.
Николай Самохвалов: CEO PostgreSQL Experts из Сан-Франциско. Он хорошие доклады, кстати, делает. Это правда. Я вам хотел задать вопрос. Почему про такие штучки не пишите у себя в блоге, на Хабре или где-то еще? Можно же маленькую статеечку сразу написать. Не нужно что-то большое.
Константин Евтеев: Пишем и будем писать. То есть интересная статья от нас была, когда Дима с Николаем Воробьевым реализовали решение, когда был такой кейс, что хранимка где-то неоптимально была написана. Но из нее вызывалась еще одна, из нее еще одна. И стек вызовов был очень большой. Такое место тупило, было непонятно. Соответственно, было придумано решение – через notify отправлять непосредственно время исполнения каждого блока. И это решение было реализовано буквально за пару часов. И, соответственно, мы написали статью про это — PG Metricus.
Николай Самохвалов: А pg_stat_statements, есть опция, чтобы потроха функции было видно. Не помогало?
Дмитрий Вагин: Там слишком много будет потрохов. И в этом всём просто не найти.
Николай Самохвалов: Вам нужен был stack trace.
Дмитрий Вагин: Нам нужна была просто одна функция, посмотреть, что именно в этой функции работает дольше, чем нам хотелось бы. Она работала, допустим, в среднем нормально, но были какие-то всплески.
Николай Самохвалов: И это, получается, в listen/notify notify вы шлете.
Дмитрий Вагин: Да, мы оттуда notify делаем. И у нас есть демон, который слушает именованный канал, и это все просто берет и переправляет в brubeck. И все. И в grafana строим dashboard.
Николай Самохвалов: А у вас Хабр, получается, основной канал, где вы такие штуки пишете?
Константин Евтеев: Да, основной канал, где мы что-то публикуем и анонсы делаем – это Хабр.
Николай Самохвалов: Наверное, все. Если больше не хотите ничего сказать, я думаю, очень хорошо и насыщенно получилось.
Константин Евтеев: Хотел добавить, что все вопросы, вообще в целом вся наша запись будет расшифрована и на Хабре появится. И все вопросы в том числе можно будет также задавать будет в этой статье и обсуждать в комментариях.
Николай Самохвалов: Спасибо большое. Очень было интересно. Я для себя тоже кое-что интересное узнал.
Константин Евтеев: Спасибо.
Николай Самохвалов: До встреч в следующих эфирах.

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