На самом деле это очень сильно напоминает концепцию шардирования, когда по какому-то ключу (хешу), определяется, в какой шарде надо искать данные.
Самая большая проблема здесь, как вы сами отметили, неравномерное распределение ключа. Даже единичный случайный выброс (2,1,5,10000) ломает всю картину.
Но если мы по какой-то причине знаем распределение и можем гарантировать его ограниченность и равномерность распространения данных внутри исходного массива, то да, такой подход вполне будет работать.
Да. Ключевые слова: плагин Remotely Save + Webdav + Облако. Так как Хабр не очень любит ссылки в комментах, скинул ссылку на подробную инструкцию в личку.
Спасибо за замечание. Полностью согласен. В статье "минимально достаточный" код, чтобы не раздувать объём не существенными подробностями. В реальном проекте кода, понятно, несколько больше.
Кажется, что ничего из перечисленного не подпадает под определение "более-менее современная версия EF одно выражение Where трансформирует во что-то кроме SELECT … FROM … WHERE … ". Но это, в любом случае, думаю, не принципиально.
Сам по себе риск однозначно есть, вы абсолютно правы. На сколько он большой - вопрос дискуссионный. Мне кажется, что преимущества перевешивают риски, вам - что нет.
В любом случае, инструмент есть, а пользоваться им, или нет, уже пусть читатели сами решают.
А можно, пожалуйста, практический пример того, как более-менее современная версия EF одно выражение Where трансформирует во что-то кроме SELECT … FROM … WHERE …? Спрашиваю без малейшего стёба, так как я такой риск сознательно откидывал как несущественный.
Я прекрасно понимаю, как можно «положить» этот запрос, но честно смог придумать только четыре сценария:
Применение ForUpdate к сложносоставному запросу, к которому в принципе нельзя применять эту команду на уровне SQL. Ну как бы сам себе злобный Буратино, не надо отвёрткой гвозди забивать.
В будущих версиях EF решат сломать обратную совместимость при генерации запросов и, например, начать явно прописывать ; в конце. Ок, это будет не первый на моей памяти случай, когда внешняя библиотека что-то ломает. Заранее это не предскажешь, так как могут и две ; подряд поставить, например. Тут, главное, при подъёме версий библиотек автотесты не забывать гонять.
Смена базы данных, при которой надо вместо FOR UPDATE начинать писать совсем другой код и по другим правилам. Тут тоже заранее хз, что делать, так как смена базы дело достаточно капитальное и не зная целевую базу сложно универсальный код написать. Тем более, что тут любой raw sql или вызов хранимую отломится точно также.
Другой интерсептор, написанный без учёта совместимости с уже имеющимися. Например, добавляющий в конец SELECT запроса ; по принципу «а для красоты». Такое только на ревью отслеживать, плюс, опять же, автотесты.
Применять FOR UPDATE на какой-нибудь сложный кросс-джойн по нескольким таблицам с агрегаторами и группировкой - не надо так. Он там просто не сработает, причём EF там будет ни при чём. Ну а что нагенерирует EF на более-менее типовой селект по одной таблице с WHERE в качестве условия - вариантов там не особо много.
Плюс сам по себе механизм интерсепторов подразумевает наличие некоторых скилов в EF и базе у человека, который пишет сам интерсептор. А дальнейшее использование ничем не отличается от каких-нибудь агрегаторов. Их точно также не везде можно использовать, и можно ошибку словить, если сделаешь это криво.
Да, но лучше, думаю, просто Ordinal в этом случае использовать, так как смысла нет регистро-независимое сравнение делать. Тут жёстко прописанная константа, которую мы вряд ли просто так менять будем, а на проверку вариантов написания букв в разных регистрах тоже время тратится.
Зависит от. Понятно, что если структура базы такая, что нагенерированный EF код надо постоянно перепроверять, то ну его нафиг. Тут, да, никакие интерсепторы не помогут - только RawSql, а если кривых запросов ещё и достаточно солидный процент от общего числа, то стоит в принципе подумать на тему отказа от EF.
Но сделать интерсептор для того же For Update вместо переписывания в 3-х местах кода на нативный SQL - вполне себе годное решение, как мне кажется. И да, я полностью согласен, что хранимка в данном случае работала бы немного быстрее. Нюанс в том, что при ожидаемой частоте апдейтов потратили бы больше времени разработчика на переписывание, чем сэкономили бы машинного времени при такой оптимизации.
Дисплей на тенях - вряд ли, хотя всё возможно. А вот часть логической операции «исключающее или» для чисто оптических компьютеров - сделано. Понятно, что от этого до практического применения ещё очень далеко, да и цена такого «процессора» космическая, но тенденция интересная.
Большинство. По моим наблюдениям тех, кто вообще без вышки, единицы при весьма солидной общей базе наблюдения. То есть в районе 1% или даже ниже. Возможно, у меня немного искажённая картина, но в любом случае речь про единицы процентов.
На будущее, однозначно рекомендую использовать вместо среднего медиану. Она как раз не чувствительна к выбросам, по типу тех, что бывали у вас в начале.
Но на практике такие вещи могут быть нужны при отладке, тут да, а потом выпиливаться либо самим разработчиком, либо на ревью.
Ну, либо если такой сценарий реально зачем-то нужен на проде и есть описанные риски, то флаг IsTraceEnabled ставим в false и для этого запроса работает старая логика логирования.
Конечно, это не универсальное решение. Самый лучший вариант - это когда хватает ресурсов для того, чтобы писать все логи на любое телодвижение. Но увы, это не всегда возможно, и приходится применять ту, или иную оптимизацию в ущерб чему-то. В нашем случае был проведён предварительный анализ того, подходит нам такой сценарий, или нет, и только потом сделано внедрение. Ну и уже за почти 4 месяца эксплуатации ни разу пока не было ситуаций, когда мы бы сказали что-то типа "эх, а вот если бы так не сделали, то сейчас бы проблему было легче разбирать".
Про перформанс риск понятен, и мы его тоже и оценивали и мониторили после релиза. За счёт того, что мы в логи полный ответ от базы всё равно не пишем за исключением крайне редких сценариев было понятно, что при сериализации/десериализации данных тратится гораздо больше ресурсов, чем при хранении логов этого запроса. Про фактические результаты я в статье писал - конкретно в нашем случае заметного изменения в потреблении памяти/ресурсов процессора после релиза не было.
Но да, сам посыл абсолютно правильный и если кто-то решит у себя внедрять такой подход, то риски оценивать однозначно надо "до", а не постфактум.
Судя по графикам потребления памяти, затраты по факту не подскочили вообще. В смысле, они, очевидно, выросли, но оказались меньше погрешности измерения. Вот прямо полный ответ из базы в логи, естественно, за исключением каких-то критических случаев, не пишем, максимум, информацию о количестве найденных объектов. То есть в моменте "простыня" логов занимает значительно меньше места в памяти, чем типичный средний ответ, который уходит клиенту.
Для понимания: все записи метками помечаются в обязательном порядке, логи живут на диске две недели. Но при нагрузке в 1000+ rps размер суточного файла в 50+ Gb на одной реплике был ежедневной реальностью, а при пиковой нагрузке до 140 Gb доходило. Сейчас снизилось до 20 Gb без полной переборки всех мест, где пишутся логи и определения степени их критичности (что тоже отдельная задача с далеко не нулевой трудоёмкостью). При этом за прошедшие 2 месяца не было ни одной ситуации, когда понадобились бы логи, которые сейчас, по факту, идут в сброс после окончания запроса, а вот "простынёй" пользоваться приходилось. Субъективно поудобнее стало.
Ну и, да, я ни разу не утверждаю, что описанный выше способ подходит для всех возможных случаев. Конкретно в нашем - явно помогло.
Не совсем. Там Warn и выше пишутся И сразу в лог И в трассировку, которая потом будет показана единой простынёй. Иными словами, мы видим сообщения уровня Warn+:
В моменте и в том месте, где они произошли, Это позволяет отследить событие относительно других запросов/логов уровня warn+
В единой "простыне" сообщений запроса. Там сообщение показывается на своём месте относительно других сообщений этого же запроса без чехарды с записями других запросов. Это сообщение записывается в конце выполнения запроса и, да, проследить info относительно info в других запросах (особенно те, которые не запишутся), нельзя
Нет, если надо сохранить данные нескольких отдельных запросов, то этот подход не подходит. Он работает в рамках отдельно взятого запроса. Я уже писал ниже, что применял его для API без хранения каких-либо промежуточных состояний.
В вашем случае боюсь, что поможет только тщательная валидация данных при записи изменений и собственно ведение журнала этих изменений с сохранением в отдельное хранилище «кто», «когда» и «что» поменял.
Спасибо за мысли! У нас эти советы не все применимы из-за специфики (stateless, api), но похожую идею, только, наоборот, с выключением логов для конкретного пользователя тоже реализовывали. Были случаи, когда клиенты начинали упорно долбить запросами при закончившемся ключе, организовывая мини-DDOS с "белых" IP, и надо было их либо вручную под протоколы обработки DDOS подводить, хотя всё стабильно работало, либо минимизировать число записей в логах.
На самом деле это очень сильно напоминает концепцию шардирования, когда по какому-то ключу (хешу), определяется, в какой шарде надо искать данные.
Самая большая проблема здесь, как вы сами отметили, неравномерное распределение ключа. Даже единичный случайный выброс (2,1,5,10000) ломает всю картину.
Но если мы по какой-то причине знаем распределение и можем гарантировать его ограниченность и равномерность распространения данных внутри исходного массива, то да, такой подход вполне будет работать.
Да. Ключевые слова: плагин Remotely Save + Webdav + Облако. Так как Хабр не очень любит ссылки в комментах, скинул ссылку на подробную инструкцию в личку.
F
Спасибо за замечание. Полностью согласен. В статье "минимально достаточный" код, чтобы не раздувать объём не существенными подробностями. В реальном проекте кода, понятно, несколько больше.
Кажется, что ничего из перечисленного не подпадает под определение "более-менее современная версия EF одно выражение Where трансформирует во что-то кроме SELECT … FROM … WHERE … ". Но это, в любом случае, думаю, не принципиально.
Сам по себе риск однозначно есть, вы абсолютно правы. На сколько он большой - вопрос дискуссионный. Мне кажется, что преимущества перевешивают риски, вам - что нет.
В любом случае, инструмент есть, а пользоваться им, или нет, уже пусть читатели сами решают.
А можно, пожалуйста, практический пример того, как более-менее современная версия EF одно выражение Where трансформирует во что-то кроме SELECT … FROM … WHERE …? Спрашиваю без малейшего стёба, так как я такой риск сознательно откидывал как несущественный.
Я прекрасно понимаю, как можно «положить» этот запрос, но честно смог придумать только четыре сценария:
Применение ForUpdate к сложносоставному запросу, к которому в принципе нельзя применять эту команду на уровне SQL. Ну как бы сам себе злобный Буратино, не надо отвёрткой гвозди забивать.
В будущих версиях EF решат сломать обратную совместимость при генерации запросов и, например, начать явно прописывать ; в конце. Ок, это будет не первый на моей памяти случай, когда внешняя библиотека что-то ломает. Заранее это не предскажешь, так как могут и две ; подряд поставить, например. Тут, главное, при подъёме версий библиотек автотесты не забывать гонять.
Смена базы данных, при которой надо вместо FOR UPDATE начинать писать совсем другой код и по другим правилам. Тут тоже заранее хз, что делать, так как смена базы дело достаточно капитальное и не зная целевую базу сложно универсальный код написать. Тем более, что тут любой raw sql или вызов хранимую отломится точно также.
Другой интерсептор, написанный без учёта совместимости с уже имеющимися. Например, добавляющий в конец SELECT запроса ; по принципу «а для красоты». Такое только на ревью отслеживать, плюс, опять же, автотесты.
Применять FOR UPDATE на какой-нибудь сложный кросс-джойн по нескольким таблицам с агрегаторами и группировкой - не надо так. Он там просто не сработает, причём EF там будет ни при чём. Ну а что нагенерирует EF на более-менее типовой селект по одной таблице с WHERE в качестве условия - вариантов там не особо много.
Плюс сам по себе механизм интерсепторов подразумевает наличие некоторых скилов в EF и базе у человека, который пишет сам интерсептор. А дальнейшее использование ничем не отличается от каких-нибудь агрегаторов. Их точно также не везде можно использовать, и можно ошибку словить, если сделаешь это криво.
Да, но лучше, думаю, просто Ordinal в этом случае использовать, так как смысла нет регистро-независимое сравнение делать. Тут жёстко прописанная константа, которую мы вряд ли просто так менять будем, а на проверку вариантов написания букв в разных регистрах тоже время тратится.
Спасибо!
Зависит от. Понятно, что если структура базы такая, что нагенерированный EF код надо постоянно перепроверять, то ну его нафиг. Тут, да, никакие интерсепторы не помогут - только RawSql, а если кривых запросов ещё и достаточно солидный процент от общего числа, то стоит в принципе подумать на тему отказа от EF.
Но сделать интерсептор для того же For Update вместо переписывания в 3-х местах кода на нативный SQL - вполне себе годное решение, как мне кажется. И да, я полностью согласен, что хранимка в данном случае работала бы немного быстрее. Нюанс в том, что при ожидаемой частоте апдейтов потратили бы больше времени разработчика на переписывание, чем сэкономили бы машинного времени при такой оптимизации.
В данном случае - однозначно. Скорее как устоявшаяся привычка.
Дисплей на тенях - вряд ли, хотя всё возможно. А вот часть логической операции «исключающее или» для чисто оптических компьютеров - сделано. Понятно, что от этого до практического применения ещё очень далеко, да и цена такого «процессора» космическая, но тенденция интересная.
Большинство. По моим наблюдениям тех, кто вообще без вышки, единицы при весьма солидной общей базе наблюдения. То есть в районе 1% или даже ниже. Возможно, у меня немного искажённая картина, но в любом случае речь про единицы процентов.
На будущее, однозначно рекомендую использовать вместо среднего медиану. Она как раз не чувствительна к выбросам, по типу тех, что бывали у вас в начале.
Либо просто запрос с длинным временем выполнения.
Но на практике такие вещи могут быть нужны при отладке, тут да, а потом выпиливаться либо самим разработчиком, либо на ревью.
Ну, либо если такой сценарий реально зачем-то нужен на проде и есть описанные риски, то флаг IsTraceEnabled ставим в false и для этого запроса работает старая логика логирования.
Конечно, это не универсальное решение. Самый лучший вариант - это когда хватает ресурсов для того, чтобы писать все логи на любое телодвижение. Но увы, это не всегда возможно, и приходится применять ту, или иную оптимизацию в ущерб чему-то. В нашем случае был проведён предварительный анализ того, подходит нам такой сценарий, или нет, и только потом сделано внедрение. Ну и уже за почти 4 месяца эксплуатации ни разу пока не было ситуаций, когда мы бы сказали что-то типа "эх, а вот если бы так не сделали, то сейчас бы проблему было легче разбирать".
Про перформанс риск понятен, и мы его тоже и оценивали и мониторили после релиза. За счёт того, что мы в логи полный ответ от базы всё равно не пишем за исключением крайне редких сценариев было понятно, что при сериализации/десериализации данных тратится гораздо больше ресурсов, чем при хранении логов этого запроса. Про фактические результаты я в статье писал - конкретно в нашем случае заметного изменения в потреблении памяти/ресурсов процессора после релиза не было.
Но да, сам посыл абсолютно правильный и если кто-то решит у себя внедрять такой подход, то риски оценивать однозначно надо "до", а не постфактум.
Судя по графикам потребления памяти, затраты по факту не подскочили вообще. В смысле, они, очевидно, выросли, но оказались меньше погрешности измерения. Вот прямо полный ответ из базы в логи, естественно, за исключением каких-то критических случаев, не пишем, максимум, информацию о количестве найденных объектов. То есть в моменте "простыня" логов занимает значительно меньше места в памяти, чем типичный средний ответ, который уходит клиенту.
Для понимания: все записи метками помечаются в обязательном порядке, логи живут на диске две недели. Но при нагрузке в 1000+ rps размер суточного файла в 50+ Gb на одной реплике был ежедневной реальностью, а при пиковой нагрузке до 140 Gb доходило. Сейчас снизилось до 20 Gb без полной переборки всех мест, где пишутся логи и определения степени их критичности (что тоже отдельная задача с далеко не нулевой трудоёмкостью). При этом за прошедшие 2 месяца не было ни одной ситуации, когда понадобились бы логи, которые сейчас, по факту, идут в сброс после окончания запроса, а вот "простынёй" пользоваться приходилось. Субъективно поудобнее стало.
Ну и, да, я ни разу не утверждаю, что описанный выше способ подходит для всех возможных случаев. Конкретно в нашем - явно помогло.
Не совсем. Там Warn и выше пишутся И сразу в лог И в трассировку, которая потом будет показана единой простынёй. Иными словами, мы видим сообщения уровня Warn+:
В моменте и в том месте, где они произошли, Это позволяет отследить событие относительно других запросов/логов уровня warn+
В единой "простыне" сообщений запроса. Там сообщение показывается на своём месте относительно других сообщений этого же запроса без чехарды с записями других запросов. Это сообщение записывается в конце выполнения запроса и, да, проследить info относительно info в других запросах (особенно те, которые не запишутся), нельзя
Нет, если надо сохранить данные нескольких отдельных запросов, то этот подход не подходит. Он работает в рамках отдельно взятого запроса. Я уже писал ниже, что применял его для API без хранения каких-либо промежуточных состояний.
В вашем случае боюсь, что поможет только тщательная валидация данных при записи изменений и собственно ведение журнала этих изменений с сохранением в отдельное хранилище «кто», «когда» и «что» поменял.
Спасибо за мысли! У нас эти советы не все применимы из-за специфики (stateless, api), но похожую идею, только, наоборот, с выключением логов для конкретного пользователя тоже реализовывали. Были случаи, когда клиенты начинали упорно долбить запросами при закончившемся ключе, организовывая мини-DDOS с "белых" IP, и надо было их либо вручную под протоколы обработки DDOS подводить, хотя всё стабильно работало, либо минимизировать число записей в логах.