получается мы получили замедление по времени в 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 какие-то очень подозрительно быстрые в планах пытаюсь понять, то ли вы где-то "обманулись", то ли я где-то ошибаюсь?
В итоге получится что под каждый вариант использования (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.
smtpd_recipient_restrictions после версии 2.10 (я уверен, у вас старше) предназначен для ограничений на "локальную" доставку, вы же хотите настроить relay, поэтому лучше использовать соответствующий параметр. smtpd_client_restrictions — это generic ограничения
Причем, правило reject_unauth_destination — это правило из раздела smtpd_recipient_restrictions, и в smtpd_client_restrictions оно будет иметь силу только если стоит smtpd_delay_reject = yes (значение по умолчанию).
И получается что вы одно и то же написали 2 раза.
советую все таки вписывать именно имя хоста 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()
;-------------------------------------------------------------------------------
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, а перед вторым забыли?
о чём, кстати, было сообщено, но проигнорировано
Пример не самый удачный.
Давайте попробую привести свой. Возможно, тоже не самый удачный, но смысл в сочетании действий.
В итоге получится что под каждый вариант использования (pay, deliver, payAndDeliver, etc) будет создано по функции с около нулевой переиспользуемостью.
А если выносить транзакции в виде unitOfWork на уровень бизнес-логики/useCase, а сами запросы на уровень infrastructure/repositry, то всё очень удачно композируется.
Поправьте если не прав, но звучит так как будто вам такой подход симпатизирует, но вы сами его не использовали.
Просто это уже нано-сервисный подход, а ля распределённый монолит.
Хочется адептам такого подхода пожелать удачи обеспечивать транзакционную целостность и тратить своё время на что-то полезное, а не на написание двух фазных коммитов и саг.
Очень вредный совет по "Обёрткам по запуску транзакций"
Пример некорректен, тк я сильно сомневаюсь что у вас это просто функции, скорее всего в реальном коде - это методы объявленные на структуре с названием типа repository.
Но в любом случае, из-за этих функций обёрток начинают лезть проблемы - а если требуется и юзера обновить и в user_history записать изменения, а если еще в аутбокс событие положить? Еще одна/две/три, но на 90% копипастные обёртки с доп параметрами в виде model.Outbox, model.UserHistory?
Транзакция - это частный случай unitOfWork, реализуйте его нормально на уровне сервисных классов/бизнес логики, а не на уровне репозитория - это не верный слой абстракции для этого паттерна.
И у вас одновременно запускается два обработчика
И запросы пришли настолько одновременно, что при вычислении requestQueue в правой части они оба получили начальное состояние Promise.resolve() и, соответственно, выполнятся не последовательно. Или нам кто-то (стандарт или движок ecmascript/javascript?) гарантирует атомарность такой операции? Мне кажется, что нет.
Но даже если да, то в нашем случае получается что сам веб-сервер превращается в синхронный, что означает бессмысленность использования Promise.
Так как же бороться за атомарность получения И присвоения значения переменной в случае асинхронных вызовов? Я уж надеюсь, что стандарт нам хотя бы гарантирует атомарность присвоения значения, верно?
Так, кажется, я в первый раз неверно понял задачу. Меня сбил первый абзац о том что вы хотели что-то там прописывать в
relay_domains.Я и подумал что вы relayхотите:
Но по итогу вам нужен обычный
Тогда, конечно, вы были правы с классом
просто
smtpd_client_restrictionsлишние былиПо-моему
лучше заменить на
smtpd_recipient_restrictionsпосле версии 2.10 (я уверен, у вас старше) предназначен для ограничений на "локальную" доставку, вы же хотите настроить relay, поэтому лучше использовать соответствующий параметр.smtpd_client_restrictions— это generic ограниченияПричем, правило
reject_unauth_destination— это правило из разделаsmtpd_recipient_restrictions, и вsmtpd_client_restrictionsоно будет иметь силу только если стоитsmtpd_delay_reject = yes(значение по умолчанию).И получается что вы одно и то же написали 2 раза.
Тут написано подробнее
И последний момент
советую все таки вписывать именно имя хоста
docker-mx.mycompany.com(а не просто доменную часть) ведь именно так он будет представляться в HELO/EHLO при отправке почты. И на это имя вы будете заводить dkim/spf/dmark записи, а так же PTR запись в днс, а то рискуете попасть в спам.Если представляться надо как-то иначе, то можно воспользоваться параметром
smtp_helo_name.Поэтому в конфигах FreePBX существует большое количество -custom контекстов, которые предназначены именно для этих целей. (Если у вас их нет, проверьте опцию FreePBX — Settings — Advanced Settings — «Disable -custom Context Includes».) Многие задачи можно решить с их помощью не прибегая к перезаписыванию сгенерированных конфигов. Еще часть можно решить с помощью Custom Destinations.
;-------------------------------------------------------------------------------
; 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()
;-------------------------------------------------------------------------------