Search
Write a publication
Pull to refresh
3
0

Database Engineer

Send message

Для 10к баз метрик должно быть много. Можете раскрыть насколько быстро работает отрисовка отдельных дашбордов? Т.е. например, <3sec, 3-5 sec, 5-10sec, 10s+ ?

Еще парадокс, лучше не показывайте это сообщение менеджерам =)

Допустим железка из под шарда стоит 10k$, и максимум может обслуживать 10k RPS.

Таким образом цена обслуживания 1 RPS == 1$, однако если клиент формирует запрос, который касается нескольких шардов, то стоимость обслуживания будет 1 RPS == N$, где N, число затрагиваемых шардов.

В итоге получается платим больше, но держим RPS столько же ;)


UPD. Здесь разумеется просто шутка - если оценивать не в RPS, а в объеме полученных данных, то в итоге получится, что шардированная база будет отдавать больше.

@worldbugинтересный опыт, спасибо, что поделились.

Шардирование на приложении так же помогает строго соблюдать границы применимости хранилища и реализовывать бизнес-логику явно понимая, что у вас шарды, т.е. даже если у вас шардирование средствами БД - она не будет себя вести как одна очень большая база, например, точно не получится делать JOIN-ы.

Вы, как овнеры сервисы понятно, что адаптировались к новой реальности. А что произошло с вашими клиентами:

  1. Подразумевается, что клиент, который вызывает getItemsByIDдолжен сам ретраить, если запрос до базы не прошел? При этом какая обычно логика ретраев у ваших клиентов - запросить все заново или только то, что не пришло, или он заранее знает, что там CA система (по CAP теореме)? Вижу, что через ошибку протекает абстракция шардов до клиентов.

  2. Вызывающий AddItems,если не удалось, записать тоже ретраит? С дедлайном ли это делает? Допустим он читает с Kafka и пишет в базу, часть данных записалось, часть нет - что при этом он делает с сообщением - commit или dead letter queue?

  3. Есть ли какие-либо ограничения на вызовы со стороны клиента, типа "максимальное количество затрагиваемых шардов". Например, клиентский запрос может содержать ID со всех шардов, таким образом он может формировать request-ы так, что capacity всей шардированной базы будет равно capacity одного шарда. Т.е. допустим 1 шард держит нагрузку 10K RPS, и шардов 32 штуки, при этом клиент в request пихает 32 ID с каждого шардого по 1 ID, и делает 10K RPS => он положил все ваши 32 шарда. Требования к устойчивости к нагрузке не выполнено.

Спасибо, что решились написать статью в хабр - хотя явно понимали, что оно не для широкой аудитории. Было интересно ознакомиться.

Как вы верно заметили в начале статьи - это не единственный способ решения потребности в шифровании, поэтому ваше решение наравне конкурирует с другими подходами. В связи с чем, у меня не технические вопросы:

  1. Как существующие проекты экосистемы Сбера уже использующих Postgres вы намерены затаскивать на Pangolin ? (репликацией ведь не получится?)

  2. Хорошо ли продается Pangolin среди внутренних клиентов? Или используется административный ресурс, чтобы форсировать переход с Postgres -> Pangolin?

  3. Ставили ли цель поддерживать связь с 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 вряд ли будет заметной.

Нужно подобрать конфигурацию базы, которая могла бы вместить 27 Гб в памяти

pg_total_relation_size считает уже с индексами, т.е. вам должно хватить и 21Gb


pg_total_relation_size ( regclass ) → bigint
Computes the total disk space used by the specified table, including all indexes and TOAST data. The result is equivalent to pg_table_size + pg_indexes_size.
https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-DBOBJECT

в linux:


$ sysctl -w kernel.sched_autogroup_enabled=0
$ sysctl -w kernel.sched_migration_cost_ns=5000000
$ renice -n -15 -p $(pidof pgbouncer)

в pg:


shared_buffers=32Gb

Посмотрите в 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к

диагностировать такое можно, например, так:


$ zcat pgbouncer.log-*.gz |fgrep 'cancel' | grep -o '2020-08-21 [0-9][0-9]:[0-9][0-9]' | sort |uniq -c

  15617 2020-08-21 07:52
  19656 2020-08-21 07:53
  19306 2020-08-21 07:54
  19616 2020-08-21 07:55
  15222 2020-08-21 07:56
     77 2020-08-21 07:57
     64 2020-08-21 07:58
     81 2020-08-21 07:59
     87 2020-08-21 08:00
   4515 2020-08-21 08:01
  17948 2020-08-21 08:02
  17027 2020-08-21 08:03
  16986 2020-08-21 08:04
  17215 2020-08-21 08:05
  16577 2020-08-21 08:06
  17108 2020-08-21 08:07
  17152 2020-08-21 08:08
  16881 2020-08-21 08:09
  16968 2020-08-21 08:10
  16258 2020-08-21 08:11
  15627 2020-08-21 08:12
  16722 2020-08-21 08:13
  15944 2020-08-21 08:14
  18246 2020-08-21 08:15
    235 2020-08-21 08:16
     74 2020-08-21 08:17
     64 2020-08-21 08:18
     59 2020-08-21 08:19
     54 2020-08-21 08:20
  18644 2020-08-21 08:21
  16323 2020-08-21 08:22
  17491 2020-08-21 08:23
  17019 2020-08-21 08:24
  16435 2020-08-21 08:25
  16644 2020-08-21 08:26
  17450 2020-08-21 08:27
  17468 2020-08-21 08:28
  16731 2020-08-21 08:29
   6502 2020-08-21 08:30

но поскольку 30к это довольно не мало и сервисов, которые столько раз синхронно ходят в postgres немного, то встречается гораздо реже, чем subj, но имеет место быть. Надеюсь кому-нибудь будет полезна эта инфа.

честная виртуалка без oomkiller-ов:


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-партицирования через наследования нужно еще проверить, поэтому для новой разработки, я бы не рекомендовал масштабироваться подобным образом.

Information

Rating
Does not participate
Registered
Activity