детали реализации популярных библиотек для кэширования 03 Разберём TinyLFU в Ristretto: как приблизительно считать частотности без блокировок План доклада
детали реализации популярных библиотек для кэширования 03 Разберём TinyLFU в Ristretto: как приблизительно считать частотности без блокировок План доклада 04 Выберем лучший вариант!
по всем товарам Автобиддеры динамически управляют рекламными ставками по всем товарам в различных плейсментах Автобиддер «Максимум кликов» позволяет получить максимальное количество кликов при условии ограничения на бюджет
Набор товаров [SKU1, SKU2, …] Шардированный Redis Get/Set Кэшируем значение из Redis’a Кэшируем отсутствие значений в Redis’e Используем TTL сильно меньше периода обновлений In-Memory Cache
В среднем • Квантили 02 03 Что кэшируем? • Типы данных, количество уникальных объектов • Максимальный размер кэша • Влезут ли все данные в кэш? Паттерны доступа к данным: • Как часто изменяются данные? • Батчовая нагрузка? • Какие-то ключи читают/пишут чаще? • Какой ожидается Hit-Rate • Баланс между Read/Write
библиотеку? Масштабируемость с ростом числа ядер и горутин 02 Удаление по TTL 03 В зависимости от библиотеки может различаться: Предоставляемый интерфейс 01
библиотеку? Масштабируемость с ростом числа ядер и горутин 02 Удаление по TTL 03 Размещение данных в памяти (GC) 04 В зависимости от библиотеки может различаться: Предоставляемый интерфейс 01
библиотеку? Масштабируемость с ростом числа ядер и горутин 02 Удаление по TTL 03 Размещение данных в памяти (GC) 04 Политика вытеснения ключей 05 В зависимости от библиотеки может различаться: Предоставляемый интерфейс 01
err error) Set (key, value []byte) Ristretto Get(key interface{}) (value interface{}, ok bool) Put(key interface{}, value interface{}) 02 Требуется сериализация ключа и значения. Можно переиспользовать buf []byte
ok bool) Put(key interface{}, value interface{}) 02 Требуется сериализация ключа и значения. Можно переиспользовать buf []byte Требуется сериализация ключа Для хранения вычисляется keyHash, conflictHash := c.keyToHash(key) По умолчанию keyToHash только для []byte, string, uint64.., можно написать свой GetWithBuf(key, buf []byte) (val []byte, err error) Set (key, value []byte)
err error) Set (key, value []byte) Ristretto Get(key interface{}) (value interface{}, ok bool) Put(key interface{}, value interface{}) Самописный кэш Get(key Key) (value Value, ok bool) Put(key Key, value Value) 02 03 Требуется сериализация ключа и значения. Можно переиспользовать buf []byte Требуется сериализация ключа Для хранения вычисляется keyHash, conflictHash := c.keyToHash(key) По умолчанию keyToHash только для []byte, string, uint64.., можно написать свой
ok bool) Put(key interface{}, value interface{}) 02 Требуется сериализация ключа и значения. Можно переиспользовать buf []byte Требуется сериализация ключа Для хранения вычисляется keyHash, conflictHash := c.keyToHash(key) По умолчанию keyToHash только для []byte, string, uint64.., можно написать свой Не требуется сериализация Самописный кэш Get(key Key) (value Value, ok bool) Put(key Key, value Value) 03 GetWithBuf(key, buf []byte) (val []byte, err error) Set (key, value []byte)
устареет expire часто определяется по таймеру с секундными интервалами! Можно проверять expire при get-операции Можно запустить сборщик устаревших объектов, сборщик проходится по ключам и удаляет их раз в интервал. Удаление происходит под блокировкой эффективнее брать блокировку на диапазон
= 256 Смотрим, как хранит значения один шард 1. Самописный кэш может хранить типизированные данные: map[Key]storeItem 2. Ristretto хранит map[keyHash]storeItem keyHash, conflictHash := c.keyToHash(key) storeItem{ value Value expire Time } storeItem{ value interface{} expire Time conflictHash uint64 }
и RingBuffer map[uint64]uint32 Ключ -– hash(key), значение — offset в буфере var hash1 uint64 = hash(key1) Map hash(x) hash(y) 2 1 … store entry4 store entry2 store entry1 store entry5 RingBuffer … 0 1 2 6 7
и RingBuffer map[uint64]uint32 Ключ – hash(key), значение — offset в буфере var hash1 uint64 = hash(key1) Map hash(x) hash(y) hash(t) 2 1 7 … store entry4 store entry2 store entry1 store entry5 RingBuffer store entry9 … 0 1 2 6 7
заполненности Ниже Hit-Rate Низкие затраты на конкурентный доступ Решение с Map + List требует Lock на каждую операцию Freecache использует LRU для каждого шарда LRU Выше Hit-Rate Нужно обновлять счётчики LFU Hit-Rate выше при Zipfian distribution
Если счётчик меньше, чем у добавляемого ключа, происходит вытеснение Выбираем случайное подмножество ключей В подмножестве находим ключ с минимальным счётчиком А:3 В:4 С:8 D:4 Случайное подмножество — первые несколько ключей при итерации по map
счётчик меньше, чем у добавляемого ключа, происходит вытеснение Выбираем случайное подмножество ключей В подмножестве находим ключ с минимальным счётчиком X:4 В:4 С:8 D:4 Случайное подмножество — первые несколько ключей при итерации по map
только 0 или 1 1 0 1 1 0 1 0 1 0 1 1 1 1 0 X h1(x) h2(x) h3(x) 2 уровень: Считающий фильтр Блума. 4 бита на каждый счётчик. Увеличиваем самый маленький счётчик Если элемент уже добавлен на 1-й уровень, считаем его частотность на втором уровне 5 0 3 2 2 12 Глобальный счётчик X h1(x) h2(x) h3(x)
только 0 или 1 1 0 1 1 0 1 0 1 0 1 1 1 1 0 X h1(x) h2(x) h3(x) 2 уровень: Считающий фильтр Блума. 4 бита на каждый счётчик. Увеличиваем самый маленький счётчик Если элемент уже добавлен на 1-й уровень, считаем его частотность на втором уровне 5 0 3 3 3 13 Глобальный счётчик X h1(x) h2(x) h3(x)
14 Уменьшаем все значения в считающем фильтре в 2 раза Чтобы подстроиться под изменение распределения, нужно забыть старые инкременты 1 0 1 1 0 1 0 1 0 1 1 1 1 0 2 0 1 1 1
14 Уменьшаем все значения в считающем фильтре в 2 раза Чтобы подстроиться под изменение распределения, нужно забыть старые инкременты 2 0 1 1 1 Обнуляем фильтр «Швейцар» 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ключу без блокировки Решение! Lossy Ring Buffers на основе sync.Pool sync.Pool хранит батчи ключей, которые ожидают инкремента sync.Pool К1 К4 К3 К1 К2 К2 К2 К4 К5 К7 К6 К9 К1 Lossy: так как батчи могут удаляться из sync.Pool и при переполнении буфера канала К5
Law Ristretto Самописный кэш Хороший baseline Не оптимизирован под работу GC Оптимизированы под работу GC Требуется сериализация значений Идеален под вашу задачу
Law Ristretto Самописный кэш Хороший baseline Не оптимизирован под работу GC Оптимизированы под работу GC Требуется сериализация значений Идеален под вашу задачу Но ещё не написан
его Ristretto Самописный кэш Помог нам справиться с проблемами с GC Работает лучше всего в простом сценарии: когда все значения помещаются в память Смотрим на метрики, пишем бенчмарки
хранения значений в памяти 02 Узнали, как приблизительно подсчитать частотности ключей без блокировок 03 НЕ выбрали лучший кэш, так как он всегда разный 04