Size: a a a

2021 March 30

LL

Lama Lover in pro.elixir
Dmitry Russ (Aleksandrov)
Какую библиотеку для кеширования Вы используете?
Анонимный опрос
40%
cachex
16%
con_cache
44%
свой велосипед на ets
Проголосовало: 25
Первое и третье
Иногда просто нужно иметь хитрые системы инвалидации кэша
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Lama Lover
Первое и третье
Иногда просто нужно иметь хитрые системы инвалидации кэша
Вроде первое и выглядит лучше и более новое, модное современное - но почему там так плохо (по сравнению с con_cache) сделаны транзакции. Единственная причина по которой я до сих пор выбираю con_cache
источник

LL

Lama Lover in pro.elixir
Можно ещё попробовать посмотреть в сторону Nebulex
Он вроде фичастый, но я его не пробовал, только ридми смотрел
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Lama Lover
Можно ещё попробовать посмотреть в сторону Nebulex
Он вроде фичастый, но я его не пробовал, только ридми смотрел
Nebulex - вообще не вариант.
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Автор con_cache знает что такое распределённый кэш, а автор cachex - а давайте все операции через один GenServer гонять будем и копировать данные в этот GenServer - а почему бы и нет.

Немного странное конечно архитектурное решение. Можно же было что-то из дизайна con_cache-а в этом вопросе использовать после стольких презентаций…
источник

ML

Maksim Lapshin in pro.elixir
Dmitry Russ (Aleksandrov)
Автор con_cache знает что такое распределённый кэш, а автор cachex - а давайте все операции через один GenServer гонять будем и копировать данные в этот GenServer - а почему бы и нет.

Немного странное конечно архитектурное решение. Можно же было что-то из дизайна con_cache-а в этом вопросе использовать после стольких презентаций…
Потому что перегон данных через один процесс работает достаточно быстро и несколько миллионов обращений в секунду работают ок
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Maksim Lapshin
Потому что перегон данных через один процесс работает достаточно быстро и несколько миллионов обращений в секунду работают ок
Вообще да, посмотрел - тут скорее вопрос tradeoff-а. Цена monitor-а на процесс против цены на копирование данных. Копируешь данные (а если по неаккуратности засунул в транзакцию какую-то дорогостоящую операцию - то все транзакции заблокировались с cachex-ом), либо ставишь monitor на входящий процесс.

con_cache:
+ не копирует данные
+ независимые друг от друга транзакции проходят паралелльно
- ставит монитор

cachex идёт другим путём:
+ не ставит монитор
- копирует данные
- в случае дорогостоящей операции - все транзакции остановились

Т.е. в случае, если к примеру используешь cachex


Cachex.transaction!(:my_cache, [ "key" ], fn(cache) ->
 case Cachex.get(cache, "key") do
   {:ok, nil} ->
     # get value, take some time
     value = do_some_job()
     Cachex.put!(cache, "key", value)
   {:ok, value} ->
     value
 end
end)


Т.е. если использовать транзакции как способ выполнить операцию для ключа один раз - то cachex сразу начнёт тормозить.
источник

LL

Lama Lover in pro.elixir
Dmitry Russ (Aleksandrov)
Вообще да, посмотрел - тут скорее вопрос tradeoff-а. Цена monitor-а на процесс против цены на копирование данных. Копируешь данные (а если по неаккуратности засунул в транзакцию какую-то дорогостоящую операцию - то все транзакции заблокировались с cachex-ом), либо ставишь monitor на входящий процесс.

con_cache:
+ не копирует данные
+ независимые друг от друга транзакции проходят паралелльно
- ставит монитор

cachex идёт другим путём:
+ не ставит монитор
- копирует данные
- в случае дорогостоящей операции - все транзакции остановились

Т.е. в случае, если к примеру используешь cachex


Cachex.transaction!(:my_cache, [ "key" ], fn(cache) ->
 case Cachex.get(cache, "key") do
   {:ok, nil} ->
     # get value, take some time
     value = do_some_job()
     Cachex.put!(cache, "key", value)
   {:ok, value} ->
     value
 end
end)


Т.е. если использовать транзакции как способ выполнить операцию для ключа один раз - то cachex сразу начнёт тормозить.
А так такую транзакцию можно же ускорить, например

value = Cachex.get(:my_cache, "key")
new_value = do_some_job(value)
Cachex.transaction(:my_cache, ["key"], fn cache ->
 case Cachex.get(cache, "key") do
   {:ok, ^value} ->
     # Transaction successful
     Cachex.put!(cache, "key", new_value)
   {:ok, other_value} ->
     # Transaction failed, retry everything from the start
     other_value
 end
end)
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Lama Lover
А так такую транзакцию можно же ускорить, например

value = Cachex.get(:my_cache, "key")
new_value = do_some_job(value)
Cachex.transaction(:my_cache, ["key"], fn cache ->
 case Cachex.get(cache, "key") do
   {:ok, ^value} ->
     # Transaction successful
     Cachex.put!(cache, "key", new_value)
   {:ok, other_value} ->
     # Transaction failed, retry everything from the start
     other_value
 end
end)
Но в этом случае - ты всегда делаешь some_job - не проверяя вначале в cache - всегда - в отличие от версии выше. Во-вторых, если такой код запустить 10 раз паралелльно, то some_job выполнится 10 раз, когда в случае выше - выполнится один раз, а все остальные прочитают его из кэша.
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Lama Lover
А так такую транзакцию можно же ускорить, например

value = Cachex.get(:my_cache, "key")
new_value = do_some_job(value)
Cachex.transaction(:my_cache, ["key"], fn cache ->
 case Cachex.get(cache, "key") do
   {:ok, ^value} ->
     # Transaction successful
     Cachex.put!(cache, "key", new_value)
   {:ok, other_value} ->
     # Transaction failed, retry everything from the start
     other_value
 end
end)
Ускорил или замедлил?
источник

LL

Lama Lover in pro.elixir
Dmitry Russ (Aleksandrov)
Но в этом случае - ты всегда делаешь some_job - не проверяя вначале в cache - всегда - в отличие от версии выше. Во-вторых, если такой код запустить 10 раз паралелльно, то some_job выполнится 10 раз, когда в случае выше - выполнится один раз, а все остальные прочитают его из кэша.
Ну да, мой код не совсем точен
case Cachex.get(:my_cache, "key") do
 {:ok, nil} ->
   new_value = do_some_job()
   Cachex.transaction(:my_cache, ["key"], fn cache ->
     case Cachex.get(cache, "key") do
       {:ok, nil} ->
         # Transaction successful
         Cachex.put!(cache, "key", new_value)
       {:ok, other_value} ->
         # Value changed by another transaction
         other_value
     end
   end)
 {:ok, value} ->
   value
end
источник

LL

Lama Lover in pro.elixir
Dmitry Russ (Aleksandrov)
Но в этом случае - ты всегда делаешь some_job - не проверяя вначале в cache - всегда - в отличие от версии выше. Во-вторых, если такой код запустить 10 раз паралелльно, то some_job выполнится 10 раз, когда в случае выше - выполнится один раз, а все остальные прочитают его из кэша.
А какая разница между

Выполнить один раз, пока остальные ждут
Или выполнить 10 раз параллельно (то есть один раз по времени)
источник

LL

Lama Lover in pro.elixir
Если тебя интересует только время, то разницы никакой
источник

LL

Lama Lover in pro.elixir
Если do_some_job имеет сайд-эффекты, то разница будет, конечно же
источник

LL

Lama Lover in pro.elixir
Но в моём варианте, все доступы к кэшу кроме первого (и происходящих с ним одновременно) не будут инициировать транзакцию
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Lama Lover
А какая разница между

Выполнить один раз, пока остальные ждут
Или выполнить 10 раз параллельно (то есть один раз по времени)
Большая разница - по загрузке - если у тебя таких таксов к примеру 10 и одновременно каждый запрашивает по 10 процессов, то ты посчитаешь 100 задач - допустим они используют твой CPU по максимуму и у тебя 16 CPU - то это займёт по времени 100/16 = примерно в 6 раз дольше из-за того, что ты одно и тоже выполняешь по 10 раз.

В случае с моим кодом для cachex-а - если всё засунуть в транзакцию cachex-а это займёт в 10 раз дольше (потому что выполняется в транзакции). Ещё хуже, хотя твой код когнитивно сложнее.

А вот в случае с con_cache-ем это будет в 6 раз быстрее твоего кода и ещё 6 процессоров останутся в ждущем режиме и в 10 раз быстрее моего. И код и проще когнитивно и быстрее твоего в 6 раз в данном случае.
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Конечно это всё зависит от задач и от того под что используется кэш, но как видишь если сделать загрузку выше, то con_cache спокойно может быть на таком паттерне в 10-100 раз быстрее.
источник

LL

Lama Lover in pro.elixir
Dmitry Russ (Aleksandrov)
Большая разница - по загрузке - если у тебя таких таксов к примеру 10 и одновременно каждый запрашивает по 10 процессов, то ты посчитаешь 100 задач - допустим они используют твой CPU по максимуму и у тебя 16 CPU - то это займёт по времени 100/16 = примерно в 6 раз дольше из-за того, что ты одно и тоже выполняешь по 10 раз.

В случае с моим кодом для cachex-а - если всё засунуть в транзакцию cachex-а это займёт в 10 раз дольше (потому что выполняется в транзакции). Ещё хуже, хотя твой код когнитивно сложнее.

А вот в случае с con_cache-ем это будет в 6 раз быстрее твоего кода и ещё 6 процессоров останутся в ждущем режиме и в 10 раз быстрее моего. И код и проще когнитивно и быстрее твоего в 6 раз в данном случае.
Если выполнять do_some_job в транзакции, доступ ко всей таблице будет заблочен на время исполнения транзакции. Если выполнять do_some_job вне транзакции, то таблица будет доступна практически всё время (кроме очень маленькой транзакции-апдейта)

Так что ты просто привёл какие-то свои числа, а я вот приведу теперь свои числа:

Если
1) do_some_job длится 300мс
2) ко всей таблице в течение 300мс обращаются 10^5 процессов
3) К одному ключу в течение 300мс обращается 2 процесса

То с do_some_job в транзакции 10^5 процессов будут ждать 300мс
А с do_some_job вне транзакции 2 процесса будут 300мс выполнять do_some_job, пока таблица открыта на чтение и изменение всем остальным. Если do_some_job завязано на CPU, то это грустно (хотя всё равно сильно лучше чем первый вариант), но если это какое-то сетевое взаимодействие со сторонней системой, то вообще плевать

ЗЫ, я это всё про Cachex
источник

LL

Lama Lover in pro.elixir
Да, с con_cache в этом плане сильно лучше
источник

DR

Dmitry Russ (Aleksan... in pro.elixir
Lama Lover
Если выполнять do_some_job в транзакции, доступ ко всей таблице будет заблочен на время исполнения транзакции. Если выполнять do_some_job вне транзакции, то таблица будет доступна практически всё время (кроме очень маленькой транзакции-апдейта)

Так что ты просто привёл какие-то свои числа, а я вот приведу теперь свои числа:

Если
1) do_some_job длится 300мс
2) ко всей таблице в течение 300мс обращаются 10^5 процессов
3) К одному ключу в течение 300мс обращается 2 процесса

То с do_some_job в транзакции 10^5 процессов будут ждать 300мс
А с do_some_job вне транзакции 2 процесса будут 300мс выполнять do_some_job, пока таблица открыта на чтение и изменение всем остальным. Если do_some_job завязано на CPU, то это грустно (хотя всё равно сильно лучше чем первый вариант), но если это какое-то сетевое взаимодействие со сторонней системой, то вообще плевать

ЗЫ, я это всё про Cachex
Доступ будет заблочен в cachex - и потому что cachex так реализован, хотя мог взять реализацию con_cache и не блочить всю таблицу.

Я не говорю, что мой вариант с cachex-ом лучше твоего, скорее наоборот - т.е. ты мне доказываешь сейчас то, что я и написал в своём сообщении - мой вариант более грустный, чем твой исключительно для cachex-а.

Но вот твой вариант спокойно будет медленнее моего, если использовать con_cache с моим вариантом. Просто потому что там независимые друг от друга транзакции не блочат таблицу и не ждут друг друга.
источник