Обновить
1

Пользователь

Отправить сообщение

https://habr.com/ru/company/postgrespro/blog/578196/

нашел материал объясняющий как будет работать этот Index Scan

его эффективность связана с корреляцией

многое стало понятно =)

это ж получается что и nested loop/merge join узел так же работает?

для каждой строки из левой таблицы он каждый раз вычитывает всю страницу правой таблицы ради одной записи? =(

продублирую т.к. кажется обновил свой предыдущий пост уже после вашего ответа

я думал что в рабочем процессе что-то типа LRU кэша страниц буферов именно на такой случай

ясно. круто. еще раз спасибо что помогли разобраться

спасибо, что помогаете разобраться

получается мы получили замедление по времени в 6 раз между последовательным чтением таблицы

и непоследовательным через индекс

и вся таблица из 100к записей вмещается в 521 буфер и его то и прочитали в seq scan

а при использовании index scan нам пришлось прочитать 100_000 буферов?
ааа! не факт что это были разные буфера?
прочитали Х буферов для индекса (в плане 100_092 прочитано, поэтому предположу, для простоты, что 92 это буферы с индексом)
т.е. по факту 92 страницы буфера для индекса, 521 для таблицы, но крутились мы в цикле так, что суммарно эти 521 страницу перечитали 100_000 раз?
что то типа nested loop по буферам?

алгоритм работы:
1) скопировали 92 страницы индексов в рабочую область процесса обрабатывающего запрос
2) идём в цикле по записям
2.1) для текущей записи в индексе находим страницу с данными таблицы
2.2) копируем себе в рабочую область эту страницу (8кб)
2.3) находим там единственную интересующую нас строку таблицы
2.4) ... работа ...
2.5) "забываем" про страницу с данными таблицы из пп2.2
2.6) берём след запись в индексе, возвращаемся к пп2.1

и каждый раз в пп.2.2 страница буфера с данными таблицы копируется в область рабочего процесса обрабатывающего запрос, отсюда и 700+ мб начитанных данных?

верно понимаю?

кажется, что если бы алгоритм не "забывал" подгруженные страницы в пп2.5, то всё было бы сильно быстрее
(но понятно, что если сама таблица - гигабайты данных, то надо "забывать", а то workmem не хватит)
но я думал что в рабочем процессе что-то типа LRU кэша страниц буферов именно на такой случай

а при seq scan с диска читать все строки таблицы не надо что ли? =)
и прихранивать их в буфера - те постгрес такой - "я вот тут прочитал всю таблицу с диска, но кэшировать в буфера её я не буду", верно?

тогда в первом примере где 2 x seq scan он 2 раза диск перечитывал?

мне всё еще не понятно как seq scan выполнился за 6 мс, а index scan за 38мс
типа seq scan читает с диска последовательно, а index scan последовательно читает индекс, а с диска - рандомно и в тесте не ssd диски у которых x6 оверхед на random access?
или при seq scan в дело вступает page cache ОС - поэтому так быстро
а при index scan - этого почему то не происходит?
но это всё тоже не объясняет почему на seq scan в примере нужно 4.2 мб, а на index scan - 782

вот мне кажется, в ваших примерах узлы с seq scan какие-то очень подозрительно быстрые в планах
пытаюсь понять, то ли вы где-то "обманулись", то ли я где-то ошибаюсь?

вот я не могу понять почему для того что бы сделать seq scan надо 541 буфер (4.2 мб) и 6 мс
а для index scan - 100_000 буферов (782 мб) и 38 мс

ведь индекс заведомо меньше всей таблицы
а выглядит как будто таблица в 100+ раз меньше индекса

где я не прав?

то что вы описали есть частное применение loose index scan, который, к сожалению, еще пока не реализован (не вмерджен) в main постгреса

общая методология описана на вики пг, оставлю ссылку

https://wiki.postgresql.org/wiki/Loose_indexscan

и я не понял как при seq scan всей таблицы буфера 502
а при скану по индексу 100000?
перед первым запросом сделали analyze, а перед вторым забыли?

о чём, кстати, было сообщено, но проигнорировано

Пример не самый удачный.
Давайте попробую привести свой. Возможно, тоже не самый удачный, но смысл в сочетании действий.

func updateOrderStatusTx(ctx context.Context, tx *sqlx.DB, orderID int64, status Status) (Order, error) {
  var order Order
  err := dbutils.Get(ctx, tx, &order, `UPDATE orders SET status = ? WHERE id = ?`, status, orderID)
  return order, err
}

func savePaymentTx(ctx context.Context, tx *sqlx.DB, ...) (..., error) {
  // ...
}

func saveDeliveryTx(ctx context.Context, tx *sqlx.DB, ...) (..., error) {
  // ...
}

func saveOutboxMsgTx(ctx context.Context, tx *sqlx.DB, ...) (..., error) {
  // ...
}

// и тут начинается

func pay(ctx context.Context, dbh *sqlx.DB, orderID int64, payment ...) (..., err error) {
	err = dbutils.RunTx(ctx, dbh, func(tx *sqlx.Tx) error {
		_, err = savePaymentTx(ctx, tx, ...)
        _, err = updateOrderStatusTx(ctx, tx, ...)
        _, err = saveOutboxMsgTx(ctx, tx, ...)
		return err
	})

	return u, err
}

func deliver(ctx context.Context, dbh *sqlx.DB, orderID int64, payment ...) (..., err error) {
	err = dbutils.RunTx(ctx, dbh, func(tx *sqlx.Tx) error {
		_, err = saveDeliveryTx(ctx, tx, ...)
        _, err = updateOrderStatusTx(ctx, tx, ...)
        _, err = saveOutboxMsgTx(ctx, tx, ...)
		return err
	})

	return u, err
}

func payAndDeliver(ctx context.Context, dbh *sqlx.DB, ...) (..., error) {
  	err = dbutils.RunTx(ctx, dbh, func(tx *sqlx.Tx) error {
		_, err = savePaymentTx(ctx, tx, ...)
        _, err = saveDeliveryTx(ctx, tx, ...)
        _, err = updateOrderStatusTx(ctx, tx, ...)
        _, err = saveOutboxMsgTx(ctx, tx, ...)
		return err
	})

	return u, err
}

В итоге получится что под каждый вариант использования (pay, deliver, payAndDeliver, etc) будет создано по функции с около нулевой переиспользуемостью.

А если выносить транзакции в виде unitOfWork на уровень бизнес-логики/useCase, а сами запросы на уровень infrastructure/repositry, то всё очень удачно композируется.

Просто в последнее время же в моде микросервисный подход. Если весь смысл микросервиса только в том, чтобы строчку в базе сохранить и потом выдать список всех объектов, то и нет необходимости сильно на слои делить.

Поправьте если не прав, но звучит так как будто вам такой подход симпатизирует, но вы сами его не использовали.
Просто это уже нано-сервисный подход, а ля распределённый монолит.
Хочется адептам такого подхода пожелать удачи обеспечивать транзакционную целостность и тратить своё время на что-то полезное, а не на написание двух фазных коммитов и саг.

Очень вредный совет по "Обёрткам по запуску транзакций"

Пример некорректен, тк я сильно сомневаюсь что у вас это просто функции, скорее всего в реальном коде - это методы объявленные на структуре с названием типа repository.

Но в любом случае, из-за этих функций обёрток начинают лезть проблемы - а если требуется и юзера обновить и в user_history записать изменения, а если еще в аутбокс событие положить? Еще одна/две/три, но на 90% копипастные обёртки с доп параметрами в виде model.Outbox, model.UserHistory?

Транзакция - это частный случай unitOfWork, реализуйте его нормально на уровне сервисных классов/бизнес логики, а не на уровне репозитория - это не верный слой абстракции для этого паттерна.

Представим ситуацию, когда серверу из вашего примера одновременно приходит два запроса, для простоты положим, что до этого запросов к серверу не было и переменная requestQueue равна Promise.resolve()

И у вас одновременно запускается два обработчика
function processRequest(req, res){
  requestQueue = requestQueue.then(function(){
    ...
  });
}

И запросы пришли настолько одновременно, что при вычислении requestQueue в правой части они оба получили начальное состояние Promise.resolve() и, соответственно, выполнятся не последовательно. Или нам кто-то (стандарт или движок ecmascript/javascript?) гарантирует атомарность такой операции? Мне кажется, что нет.
Но даже если да, то в нашем случае получается что сам веб-сервер превращается в синхронный, что означает бессмысленность использования Promise.

Так как же бороться за атомарность получения И присвоения значения переменной в случае асинхронных вызовов? Я уж надеюсь, что стандарт нам хотя бы гарантирует атомарность присвоения значения, верно?

Так, кажется, я в первый раз неверно понял задачу. Меня сбил первый абзац о том что вы хотели что-то там прописывать в relay_domains.
Я и подумал что вы relayхотите:


The relay domain class.
Purpose: mail forwarding to remote destinations that list your system as primary or backup MX host. For a discussion of the basic configuration details, see the BASIC_CONFIGURATION_README document. For a discussion of the difference between canonical domains, hosted domains and other domains, see the VIRTUAL_README file.

Но по итогу вам нужен обычный


The default domain class.
Purpose: mail forwarding to the Internet on behalf of authorized clients. For a discussion of the basic configuration details, see the BASIC_CONFIGURATION_README file. For a discussion of the difference between canonical domains, hosted domains and other domains, see the VIRTUAL_README file.

Тогда, конечно, вы были правы с классом


postconf -e "smtpd_recipient_restrictions = permit_sasl_authenticated,reject_unauth_destination"

просто smtpd_client_restrictions лишние были

По-моему


postconf -e "smtpd_recipient_restrictions = permit_sasl_authenticated,reject_unauth_destination" 
postconf -e "smtpd_client_restrictions = permit_sasl_authenticated,reject_unauth_destination"

лучше заменить на


postconf -e "smtpd_relay_restrictions = permit_sasl_authenticated,reject_unauth_destination"
postconf -e "smtpd_recipient_restrictions = reject" # (reject_unauth_destination - если хотите локальный bcc)

smtpd_recipient_restrictions после версии 2.10 (я уверен, у вас старше) предназначен для ограничений на "локальную" доставку, вы же хотите настроить relay, поэтому лучше использовать соответствующий параметр.
smtpd_client_restrictions — это generic ограничения
Причем, правило reject_unauth_destination — это правило из раздела smtpd_recipient_restrictions, и в smtpd_client_restrictions оно будет иметь силу только если стоит smtpd_delay_reject = yes (значение по умолчанию).
И получается что вы одно и то же написали 2 раза.


Тут написано подробнее
И последний момент


myhostname = example.com

советую все таки вписывать именно имя хоста docker-mx.mycompany.com (а не просто доменную часть) ведь именно так он будет представляться в HELO/EHLO при отправке почты. И на это имя вы будете заводить dkim/spf/dmark записи, а так же PTR запись в днс, а то рискуете попасть в спам.


Если представляться надо как-то иначе, то можно воспользоваться параметром smtp_helo_name.

А можно подробнее почему PrivateNetwork= и JoinsNamespaceOf= с PrivateTmp= не будут работать как нам надо? Заодно и создание/настройку сети для неймспейса оформить через networkd (используя .network, .netdev, .link файлы)? В чем там проблема?
FreePBX перезаписывает основные конфиги после применения настроек, и приходится «выносить» нужные мне блоки за его пределы.

Поэтому в конфигах FreePBX существует большое количество -custom контекстов, которые предназначены именно для этих целей. (Если у вас их нет, проверьте опцию FreePBX — Settings — Advanced Settings — «Disable -custom Context Includes».) Многие задачи можно решить с их помощью не прибегая к перезаписыванию сгенерированных конфигов. Еще часть можно решить с помощью Custom Destinations.
Не очень хорошо перезаписывать кусок диалплана сгенерированный freepbx. Для этого в [macro-dialout-trunk], который отвечает за звонки через транки, сделан вызов макроса Macro(dialout-trunk-predial-hook,), в который вы и должны поместить свою логику. Там будут доступны переменные DIAL_TRUNK, DIAL_NUMBER, полный список можете получить вызвав DumpChan() в этом макросе и посмотреть в консоль при исходящем звонке.

Подробнее в extensions.conf
;-------------------------------------------------------------------------------
; macro-dialout-trunk-predial-hook:
;
; this macro intentionally left blank so it may be safely overwritten for any custom
; requirements that an installation may have.
;
; the macro is called by macro-dialout-trunk just prior to making a Dial() attempt
; to a trunk.
;
; MACRO RETURN CODE: ${PREDIAL_HOOK_RET}
; if set to "BYPASS" then this trunk will be skipped
;
;
[macro-dialout-trunk-predial-hook]
exten => s,1,MacroExit()
;-------------------------------------------------------------------------------

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность