Для 10к баз метрик должно быть много. Можете раскрыть насколько быстро работает отрисовка отдельных дашбордов? Т.е. например, <3sec, 3-5 sec, 5-10sec, 10s+ ?
Еще парадокс, лучше не показывайте это сообщение менеджерам =)
Допустим железка из под шарда стоит 10k$, и максимум может обслуживать 10k RPS.
Таким образом цена обслуживания 1 RPS == 1$, однако если клиент формирует запрос, который касается нескольких шардов, то стоимость обслуживания будет 1 RPS == N$, где N, число затрагиваемых шардов.
В итоге получается платим больше, но держим RPS столько же ;)
UPD. Здесь разумеется просто шутка - если оценивать не в RPS, а в объеме полученных данных, то в итоге получится, что шардированная база будет отдавать больше.
@worldbugинтересный опыт, спасибо, что поделились.
Шардирование на приложении так же помогает строго соблюдать границы применимости хранилища и реализовывать бизнес-логику явно понимая, что у вас шарды, т.е. даже если у вас шардирование средствами БД - она не будет себя вести как одна очень большая база, например, точно не получится делать JOIN-ы.
Вы, как овнеры сервисы понятно, что адаптировались к новой реальности. А что произошло с вашими клиентами:
Подразумевается, что клиент, который вызывает getItemsByIDдолжен сам ретраить, если запрос до базы не прошел? При этом какая обычно логика ретраев у ваших клиентов - запросить все заново или только то, что не пришло, или он заранее знает, что там CA система (по CAP теореме)? Вижу, что через ошибку протекает абстракция шардов до клиентов.
Вызывающий AddItems,если не удалось, записать тоже ретраит? С дедлайном ли это делает? Допустим он читает с Kafka и пишет в базу, часть данных записалось, часть нет - что при этом он делает с сообщением - commit или dead letter queue?
Есть ли какие-либо ограничения на вызовы со стороны клиента, типа "максимальное количество затрагиваемых шардов". Например, клиентский запрос может содержать ID со всех шардов, таким образом он может формировать request-ы так, что capacity всей шардированной базы будет равно capacity одного шарда. Т.е. допустим 1 шард держит нагрузку 10K RPS, и шардов 32 штуки, при этом клиент в request пихает 32 ID с каждого шардого по 1 ID, и делает 10K RPS => он положил все ваши 32 шарда. Требования к устойчивости к нагрузке не выполнено.
Спасибо, что решились написать статью в хабр - хотя явно понимали, что оно не для широкой аудитории. Было интересно ознакомиться.
Как вы верно заметили в начале статьи - это не единственный способ решения потребности в шифровании, поэтому ваше решение наравне конкурирует с другими подходами. В связи с чем, у меня не технические вопросы:
Как существующие проекты экосистемы Сбера уже использующих Postgres вы намерены затаскивать на Pangolin ? (репликацией ведь не получится?)
Хорошо ли продается Pangolin среди внутренних клиентов? Или используется административный ресурс, чтобы форсировать переход с Postgres -> Pangolin?
Ставили ли цель поддерживать связь с upstream-ом Postgres-a или теперь Pangolin уже полностью полностью самостоятельный движок?
@Hell_Knight, спасибо за отличную техническую статью.
В итоге у service-user появится feature toggle монолита. Service-user будет пытаться что-то туда записать, но у него ничего не выйдет, потому что данные моментально будут перезаписываться.
(если redis-local выпилили, это менее актуально, но все же)
^ Такое могло происходить, если redis не прикрыт паролем. Авторизация это дополнительный запрос в redis, и при ваших нагрузках это разумеется не бесплатно. Это осознанный трейд-офф отказ от аутентификации или техдолг?
Так же в схеме видно, что redis-sentinel ходит напрямую извне pod-a, это означает, что redis-ы торчат внаружу для всех. Соответственно условный злоумышленник мог поменять поведение avito-site переписав содержимое redis-local. Сотрудничаете ли вы с security инженерами при проектировании сервисов?
слишком большая сетевая активность в одном контейнере
Мы разделили один xproxy на четыре разных контейнера
Можете пожалуйста раскрыть подробности? Во что именно упирались, и про какую именно сетевую активность идет речь? По факту все осталось внутри той же k8s node, а значит те же ядра k8s node обратывают сетевые прерывания, и тот же сетевой интерфейс пропускает через себя весь трафик.
Про контейнер databases - процесс pgbouncer. - Какую роль выполняет pgbouncer внутри этого контейнера? - И второй вопрос в ту же тему - с точки зрения приложения нет разницы в ответственностях между контейнером caches и databases. Что препятствовало тому, чтобы Xproxy рендерил конфиги и для баз данных? Здесь ведь тоже можно сэкономить на ресурсах, и проще в поддержке -разработчику проще контролировать конфликты по локальным портам.
и нам нужен blue-green релиз
Судя по тексту LXC-контейнеры ничем не оркестрировались, соответственно вряд ли там был B-G деплой, и вы жили без него до 2019-го года. Так же не раз упоминалось про нехватку ресурсов. После переезда в k8s, вы стали пользоваться B-G релизами и что делаете с нехваткой ресурсов теперь?
simple query protocol позволяет отправлять несколько запросов за раз, это как минимум небезопасно, и это отдается на откуп драйверу и разработчику. Из плюсов можно внутри запроса проставлять параметры для транзакции типа set local statement_timeout='50ms'; begin; select pg_sleep(1), вместо statement_timeout, можно втыкать и synchronous_commit, т.е. управлять write concern с гранулярностью до транзакций. И все это будет за 1 round trip.
extended query protocol исключит возможность делать sql-инъекции на более низком уровне. С выставленным log_min_statement_duration в 0 можно будет смотреть в логах на каком этапе (parse, bind, execute), сколько ms тратиться, например, только в pg13 измеряется Total time spent planning the statement. До него эту метрику можно узнать только косвенно по разнице avg_query_time в pgbouncer-e и total_time/calls из pg_stat_statements внутри pg.
Именно по перфомансу разница между "describe" и PreferSimpleProtocol = true вряд ли будет заметной.
Посмотрите в explain analyze сколько тратится на planning time примерно такой оверхед будет делать pg на каждую транзакцию. Попробуйте пострелять через session пулинг, или напрямую в pg с препарированными запросами.
Мы тоже испытывали боли с pgbouncer и cancel request, и решили их комплексно...
Проверить испытывает ли ворклоад проблему с cancel request можно через метрику: https://www.pgbouncer.org/usage.html#show-lists
если used_clients > cl_active, значит в cancel_req_list копится очередь из cancel.
Есть так же другая проблема, которую не видно на метриках pgbouncer/postgres, бывает такое что под большой нагрузкой код начинает слишком часто отменять запросы и поскольку каждый cancel это новый коннект, то здесь (https://i.imgur.com/8Q0UbeC.png) уже втыкаемся в ограничения на стороне куба
заканчиваются эфемерные порты, которых по умолчанию 30к
но поскольку 30к это довольно не мало и сервисов, которые столько раз синхронно ходят в postgres немного, то встречается гораздо реже, чем subj, но имеет место быть. Надеюсь кому-нибудь будет полезна эта инфа.
demo=> set temp_buffers='100GB';
SET
demo=> set work_mem='100GB';
SET
demo=> explain analyze select a, max(b), min(c) from generate_series(1,1000000) as a, generate_series(1,100000) as b, generate_series(1,10) as c group by a;
ERROR: out of memory
DETAIL: Failed on request of size 24 in memory context "ExecutorState".
очень хорошо
а какие там лимиты у дисков? проверял так:
demo=> select datname, temp_bytes from pg_stat_database;
datname | temp_bytes
-----------+-------------
postgres | 0
template1 | 0
template0 | 0
demo | 26995288832
(4 rows)
### в 3 потока
demo=> explain analyze select a, max(b), min(c) from generate_series(1,1000000) as a, generate_series(1,100000) as b, generate_series(1,10) as c group by a;
ERROR: could not write to tuplestore temporary file: No space left on device
Time: 594862,872 ms
demo=> select datname, temp_bytes from pg_stat_database;
datname | temp_bytes
-----------+-------------
postgres | 0
template1 | 0
template0 | 0
demo | 44678019584
(4 rows)
28MBs на запись
а как бэкапы делаются?
demo=> select name, setting from pg_settings;
name | setting
----------------------------------------+-----------------------------------------
archive_command | (disabled)
archive_mode | off
archive_timeout | 0
1. В разных схемах могут быть таблицы с одинаковыми именами, кроме TG_TABLE_NAME, нужно еще хранить TG_TABLE_SCHEMA.
2. В коде триггера используется функция row_to_json, а тип поля jsonb, получаем ненужное преобразование из record -> json, а можно использовать функцию to_jsonb, которая сразу преобразует record -> jsonb. (jsonb может менять порядок ключей, поэтому json != jsonb)
3. Вместо row based триггера можно использовать statement триггер, там overhead-а может быть меньше, т.к. триггер будет запускаться 1 раз за statement, а не столько сколько строк изменилось.
4. Возможно правильнее использовать CLOCK_TIMESTAMP(), который будет показывать текущее время, а не зафиксированное в начале транзакции CURRENT_TIMESTAMP.
5.
Перед написанием статьи, искал в google на предмет возможного совмещения колес, педалей, рамы и руля.
Ничего похожего не нашел, решил поделиться.
pgconf.ru/2018/100615 с 25-го слайда начинается описание логирования, где история хранится в авто-партицированных таблицах.
Спасибо за статью, всегда раз, когда коллеги делятся своими решениями. Тоже сталкивался с проблемой автопартицирования примерно полгода назад. Вы изучали другие практики? Подобных решений много в том числе и на русскоязычном сегменте, вот, например, из 2015 года: www.youtube.com/watch?v=ECkQtFnOxuA
Мне нравится, что создание партиций в вашем подходе идемпотентно, и race condition тут:
-- create partition, if necessary
IF NOT EXISTS(SELECT relname FROM pg_class WHERE relname = partition_name) THEN
PERFORM create_new_partition(TG_TABLE_NAME, NEW.tenant_id, partition_date, partition_name);
END IF;
ничего страшного не делает.
В моем случае идут bulk insert-ы и триггер создавал неприемлемый overhead по performance. Плюс требовалось двух уровневое партицирование, сначала разрезать по диапазону одной колонке, потом по второй. Поэтому сравнивайте, на сколько вы «просядите» с триггером или без.
Хочу отметить, что для декларативного партицирования, которая появилась в 10-ке, в 11-ой версии обещают Runtime partition pruning (https://commitfest.postgresql.org/17/1330/). Так же для декларативное партицирования обещают unique index (https://commitfest.postgresql.org/17/1452/). Будет ли поддерживаться эти фичи, для legacy-партицирования через наследования нужно еще проверить, поэтому для новой разработки, я бы не рекомендовал масштабироваться подобным образом.
Для 10к баз метрик должно быть много. Можете раскрыть насколько быстро работает отрисовка отдельных дашбордов? Т.е. например, <3sec, 3-5 sec, 5-10sec, 10s+ ?
Еще парадокс, лучше не показывайте это сообщение менеджерам =)
Допустим железка из под шарда стоит 10k$, и максимум может обслуживать 10k RPS.
Таким образом цена обслуживания 1 RPS == 1$, однако если клиент формирует запрос, который касается нескольких шардов, то стоимость обслуживания будет 1 RPS == N$, где N, число затрагиваемых шардов.
В итоге получается платим больше, но держим RPS столько же ;)
UPD. Здесь разумеется просто шутка - если оценивать не в RPS, а в объеме полученных данных, то в итоге получится, что шардированная база будет отдавать больше.
@worldbugинтересный опыт, спасибо, что поделились.
Шардирование на приложении так же помогает строго соблюдать границы применимости хранилища и реализовывать бизнес-логику явно понимая, что у вас шарды, т.е. даже если у вас шардирование средствами БД - она не будет себя вести как одна очень большая база, например, точно не получится делать JOIN-ы.
Вы, как овнеры сервисы понятно, что адаптировались к новой реальности. А что произошло с вашими клиентами:
Подразумевается, что клиент, который вызывает
getItemsByID
должен сам ретраить, если запрос до базы не прошел? При этом какая обычно логика ретраев у ваших клиентов - запросить все заново или только то, что не пришло, или он заранее знает, что там CA система (по CAP теореме)? Вижу, что через ошибку протекает абстракция шардов до клиентов.Вызывающий
AddItems,
если не удалось, записать тоже ретраит? С дедлайном ли это делает? Допустим он читает с Kafka и пишет в базу, часть данных записалось, часть нет - что при этом он делает с сообщением - commit или dead letter queue?Есть ли какие-либо ограничения на вызовы со стороны клиента, типа "максимальное количество затрагиваемых шардов". Например, клиентский запрос может содержать ID со всех шардов, таким образом он может формировать request-ы так, что capacity всей шардированной базы будет равно capacity одного шарда. Т.е. допустим 1 шард держит нагрузку 10K RPS, и шардов 32 штуки, при этом клиент в request пихает 32 ID с каждого шардого по 1 ID, и делает 10K RPS => он положил все ваши 32 шарда. Требования к устойчивости к нагрузке не выполнено.
Спасибо, что решились написать статью в хабр - хотя явно понимали, что оно не для широкой аудитории. Было интересно ознакомиться.
Как вы верно заметили в начале статьи - это не единственный способ решения потребности в шифровании, поэтому ваше решение наравне конкурирует с другими подходами. В связи с чем, у меня не технические вопросы:
Как существующие проекты экосистемы Сбера уже использующих Postgres вы намерены затаскивать на Pangolin ? (репликацией ведь не получится?)
Хорошо ли продается Pangolin среди внутренних клиентов? Или используется административный ресурс, чтобы форсировать переход с Postgres -> Pangolin?
Ставили ли цель поддерживать связь с upstream-ом Postgres-a или теперь Pangolin уже полностью полностью самостоятельный движок?
@Hell_Knight, спасибо за отличную техническую статью.
(если redis-local выпилили, это менее актуально, но все же)
^ Такое могло происходить, если redis не прикрыт паролем. Авторизация это дополнительный запрос в redis, и при ваших нагрузках это разумеется не бесплатно. Это осознанный трейд-офф отказ от аутентификации или техдолг?
Так же в схеме видно, что redis-sentinel ходит напрямую извне pod-a, это означает, что redis-ы торчат внаружу для всех. Соответственно условный злоумышленник мог поменять поведение avito-site переписав содержимое redis-local. Сотрудничаете ли вы с security инженерами при проектировании сервисов?
Можете пожалуйста раскрыть подробности? Во что именно упирались, и про какую именно сетевую активность идет речь?
По факту все осталось внутри той же k8s node, а значит те же ядра k8s node обратывают сетевые прерывания, и тот же сетевой интерфейс пропускает через себя весь трафик.
Про контейнер databases - процесс pgbouncer.
- Какую роль выполняет pgbouncer внутри этого контейнера?
- И второй вопрос в ту же тему - с точки зрения приложения нет разницы в ответственностях между контейнером caches и databases. Что препятствовало тому, чтобы Xproxy рендерил конфиги и для баз данных? Здесь ведь тоже можно сэкономить на ресурсах, и проще в поддержке -разработчику проще контролировать конфликты по локальным портам.
Судя по тексту LXC-контейнеры ничем не оркестрировались, соответственно вряд ли там был B-G деплой, и вы жили без него до 2019-го года. Так же не раз упоминалось про нехватку ресурсов. После переезда в k8s, вы стали пользоваться B-G релизами и что делаете с нехваткой ресурсов теперь?
simple query protocol
позволяет отправлять несколько запросов за раз, это как минимум небезопасно, и это отдается на откуп драйверу и разработчику. Из плюсов можно внутри запроса проставлять параметры для транзакции типаset local statement_timeout='50ms'; begin; select pg_sleep(1)
, вместоstatement_timeout
, можно втыкать иsynchronous_commit
, т.е. управлять write concern с гранулярностью до транзакций. И все это будет за 1 round trip.extended query protocol
исключит возможность делать sql-инъекции на более низком уровне. С выставленнымlog_min_statement_duration
в 0 можно будет смотреть в логах на каком этапе (parse, bind, execute), сколько ms тратиться, например, только в pg13 измеряетсяTotal time spent planning the statement
. До него эту метрику можно узнать только косвенно по разницеavg_query_time
в pgbouncer-e иtotal_time/calls
изpg_stat_statements
внутри pg.Именно по перфомансу разница между
"describe"
иPreferSimpleProtocol = true
вряд ли будет заметной.pg_total_relation_size
считает уже с индексами, т.е. вам должно хватить и 21Gbв linux:
в pg:
Посмотрите в explain analyze сколько тратится на planning time примерно такой оверхед будет делать pg на каждую транзакцию. Попробуйте пострелять через session пулинг, или напрямую в pg с препарированными запросами.
Вот тут вам еще предлагали использовать odyssey вместо pgbouncer — https://github.com/pgbouncer/pgbouncer/issues/392#issuecomment-645998318
там это пофикшено: https://github.com/yandex/odyssey/issues/84
Мы тоже испытывали боли с pgbouncer и cancel request, и решили их комплексно...
Проверить испытывает ли ворклоад проблему с cancel request можно через метрику:
https://www.pgbouncer.org/usage.html#show-lists
если
used_clients
>cl_active
, значит вcancel_req_list
копится очередь из cancel.Есть так же другая проблема, которую не видно на метриках pgbouncer/postgres, бывает такое что под большой нагрузкой код начинает слишком часто отменять запросы и поскольку каждый cancel это новый коннект, то здесь (https://i.imgur.com/8Q0UbeC.png) уже втыкаемся в ограничения на стороне куба
диагностировать такое можно, например, так:
но поскольку 30к это довольно не мало и сервисов, которые столько раз синхронно ходят в postgres немного, то встречается гораздо реже, чем subj, но имеет место быть. Надеюсь кому-нибудь будет полезна эта инфа.
честная виртуалка без oomkiller-ов:
очень хорошо
а какие там лимиты у дисков? проверял так:
28MBs на запись
а как бэкапы делаются?
2. В коде триггера используется функция row_to_json, а тип поля jsonb, получаем ненужное преобразование из record -> json, а можно использовать функцию to_jsonb, которая сразу преобразует record -> jsonb. (jsonb может менять порядок ключей, поэтому json != jsonb)
3. Вместо row based триггера можно использовать statement триггер, там overhead-а может быть меньше, т.к. триггер будет запускаться 1 раз за statement, а не столько сколько строк изменилось.
4. Возможно правильнее использовать CLOCK_TIMESTAMP(), который будет показывать текущее время, а не зафиксированное в начале транзакции CURRENT_TIMESTAMP.
5.
pgconf.ru/2018/100615 с 25-го слайда начинается описание логирования, где история хранится в авто-партицированных таблицах.
Мне нравится, что создание партиций в вашем подходе идемпотентно, и race condition тут:
ничего страшного не делает.
В моем случае идут bulk insert-ы и триггер создавал неприемлемый overhead по performance. Плюс требовалось двух уровневое партицирование, сначала разрезать по диапазону одной колонке, потом по второй. Поэтому сравнивайте, на сколько вы «просядите» с триггером или без.
Хочу отметить, что для декларативного партицирования, которая появилась в 10-ке, в 11-ой версии обещают Runtime partition pruning (https://commitfest.postgresql.org/17/1330/). Так же для декларативное партицирования обещают unique index (https://commitfest.postgresql.org/17/1452/). Будет ли поддерживаться эти фичи, для legacy-партицирования через наследования нужно еще проверить, поэтому для новой разработки, я бы не рекомендовал масштабироваться подобным образом.