Содержание этой статьи представляет собой расшифровку моего выступления на технологической конференции Shenzhen GIAC 23 июня.
Друзья аудитории, я Цин Венпин, инженер от Palm Reading, а автор народных буклетов «Редис Глубокое приключение». То, что я приношу сегодня, состоит в том, чтобы поделиться темой: практика оптимизации Redis под массивными данными и высоким параллелизмом. Redis не является незнакомцем для интернет-технологических инженеров. Почти все крупные и средние предприятия используют Redis в качестве базы данных кэша, но для большинства предприятий они используют только самые основные функции кэша KV, и еще много расширенных функций Redis May не было серьезно практиковано. Сегодня в этот час я сосредоточусь на Redis и поделитесь 9 классическим случаями, встречающимися в ежедневном развитии бизнеса. Я надеюсь, что этот обмен может помочь вам лучше применить расширенные функции Redis до ежедневного развития бизнеса.
Общее количество пользователей программного обеспечения для чтения электронных книг ireader составляет около 500 миллионов, с ежемесячной активностью 5 кВт и ежедневной активностью почти 2 кВт. На сервере более 1000 экземпляров Redis, более 100 кластеров, и память каждого экземпляра контролируется ниже 20 ГБ.
КВ кеш
Первая — самая основная и наиболее часто используемая функция KV.Мы можем использовать Redis для кэширования информации о пользователе, информации о сеансе, информации о товарах и т. д. Следующий код представляет собой общую логику чтения кэша.
def get_user(user_id):
user = redis.get(user_id)
if not user:
user = db.get(user_id)
redis.setex(user_id, ttl, user) // 设置缓存过期时间
return user
def save_user(user):
redis.setex(user.id, ttl, user) // 设置缓存过期时间
db.save_async(user) // 异步写数据库
Это время истечения очень важно.Обычно оно пропорционально продолжительности одного сеанса пользователя, гарантируя, что пользователь сможет максимально использовать данные в кэше в течение одного сеанса. Конечно, если ваша компания имеет сильные финансовые ресурсы и уделяет большое внимание опыту работы, вы можете установить время больше или даже не устанавливать время экспирации вообще. Когда объем данных продолжает расти, используйте кластер Codis или Redis-Cluster для расширения емкости.
Кроме того, Redis также предоставляет режим кэширования: инструкции Set не нужно устанавливать время истечения срока действия, и она также может удалять эти пары ключ-значение в соответствии с определенной стратегией. Инструкция по включению режима кеша: config set maxmemory 20gb, поэтому, когда память достигнет 20gb, Redis начнет выполнять стратегию исключения, чтобы освободить место для новых пар ключ-значение. Redis также предоставляет множество видов этой стратегии.Подводя итог, эта стратегия разделена на две части: определение диапазона исключения и выбор алгоритма исключения. Например, в сети мы используем стратегию allkeys-lru. Это allkeys означает, что все ключи в Redis могут быть удалены, независимо от того, есть ли у них срок действия, а volatile удаляет только ключи со сроком действия. Функция исключения Redis похожа на жестокую оптимизацию талантов, когда предприятию нужно затянуть пояс, чтобы пережить зиму. Выберет ли он оптимизацию только для временных ресурсов или все будут оптимизированы с одинаковой вероятностью? Когда этот диапазон будет определен, из него будет выбрано несколько мест, как выбрать, таков алгоритм исключения. Наиболее часто используется алгоритм LRU, у которого есть слабость, то есть люди, которые хорошо работают на поверхности, могут избежать оптимизации. Например, если вы воспользуетесь возможностью быстро показать хорошее выступление перед боссом, то вы будете в безопасности. Так что когда Redis 4.0 внедрил алгоритм LFU, необходимо оценить обычную производительность.Недостаточно выполнять только поверхностную работу.Это также зависит от того, прилежны вы обычно или нет. Наконец, есть очень редкий алгоритм — алгоритм случайной лотереи, этот алгоритм также может исключать генерального директора, поэтому он обычно не используется.
Распределенная блокировка
Давайте посмотрим на вторую функцию — распределенную блокировку, которая является еще одной наиболее часто используемой функцией помимо KV-кэша. Например, очень способный старший инженер с быстрой эффективностью разработки и высоким качеством кода является звездой в команде. Так много менеджеров по продукту должны беспокоить его и просить, чтобы он предъявлял требования к себе. Если на его поиски одновременно придет кучка продакт-менеджеров, его мышление придет в замешательство. Поэтому он повесил табличку "Не беспокоить" на дверную ручку своего офиса. Когда приходил менеджер по продукту, он сначала проверял, есть ли эта табличка на дверной ручке. Если нет, он мог войти и попросить инженера обсудить его потребности. Повесьте табличку перед тем, как говорить, и снимите табличку после разговора. Таким образом, когда другие менеджеры по продукту также беспокоят его, если они видят этот знак, он может спать и ждать или сначала заняться другими делами. Таким образом, звездный инженер с тех пор обрел покой.
Использование этой распределенной блокировки очень простое, то есть расширенные параметры инструкции Set следующие:
# 加锁
set lock:$user_id owner_id nx ex=5
# 释放锁
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
# 等价于
del_if_equals lock:$user_id owner_id
Обязательно установите это время истечения, потому что в особых обстоятельствах, таких как землетрясение (процесс убит -9 или машина не работает), менеджер по продукту может решить выпрыгнуть из окна и не иметь возможности исключить из списка, в результате чего в тупиковом голодании, Пусть этот превосходный инженер станет большим бездельником, что приведет к серьезной трате ресурсов. В то же время вам также нужно обратить внимание на этот owner_id, который представляет, кто добавил блокировку — номер задания продакт-менеджера. В случае, если ваш замок случайно удален кем-то другим. При снятии блокировки должен совпадать идентификатор owner_id, а снять блокировку можно только после успешного совпадения. Этот owner_id обычно является случайным числом, хранящимся в переменной ThreadLocal (переменной стека).
Официальный не рекомендует этот метод, потому что это вызовет проблему потери блокировки в режиме кластера - когда происходит переключение master-slave. Официально рекомендуемая распределенная блокировка называется RedLock, автор считает этот алгоритм относительно безопасным и рекомендует его использовать. Тем не менее, Palm Reading всегда использовал простейшую распределенную блокировку, описанную выше, почему бы нам не использовать RedLock, потому что его стоимость эксплуатации и обслуживания будет выше, и он требует более 3 независимых экземпляров Redis, что более громоздко в использовании. Кроме того, вероятность переключения master-slave в кластере Redis невелика, даже если происходит переключение master-slave, вероятность потери блокировки очень мала, потому что переключение master-slave часто имеет процесс, и время этого процесса обычно превышает время истечения блокировки, аварийной потери блокировки не будет. Кроме того, у распределенных блокировок не так много возможностей столкнуться с конфликтами блокировок.Так же, как звездные программисты в компании относительно ограничены, постоянное столкновение с очередями блокировок означает, что структуру необходимо оптимизировать.
очередь задержки
Перейдем к третьей функции — очереди задержки. Ранее мы упоминали, что продакт-менеджеры могут выбирать различные стратегии, когда сталкиваются с табличкой «Не беспокоить». 1. Ожидание 2. Спать 2. Сдаться и бросить курить 3. Немного отдохнуть, прежде чем повторить это снова. Сухое ожидание — это спин-блокировка, которая сжигает ЦП и увеличивает количество запросов в секунду для Redis. Сон — это сон на некоторое время и повторная попытка, что приведет к трате ресурсов потока и увеличению времени отклика. Сдаться — значит попросить пользователей переднего плана повторить попытку позже.Сейчас система находится под давлением и немного занята, что влияет на пользовательский опыт. Последняя стратегия, о которой мы сейчас поговорим, и вернемся к ней позже, является наиболее распространенной стратегией в реальном мире. Эта стратегия обычно используется при использовании очередей сообщений.Что мне делать, если я столкнусь с конфликтами блокировок в это время? Оно не может быть отброшено и не обработано, а также не подходит для немедленной повторной попытки (spinlock), в это время сообщение может быть брошено в очередь задержки и обработано через некоторое время.
Существует множество профессиональных промежуточных программ для обмена сообщениями, которые поддерживают отложенный обмен сообщениями, например RabbitMQ и NSQ. Redis также может, мы можем использовать zset для реализации этой очереди задержки. Пара "ключ-значение" значение/оценка хранится в zset. Мы сохраняем значение как сериализованное сообщение задачи, а оценку - как время выполнения следующего сообщения задачи (крайний срок), а затем опрашиваем сообщение задачи, значение оценки которого превышает теперь в zset для решения.
# 生产延时消息
zadd(queue-key, now_ts+5, task_json)
# 消费延时消息
while True:
task_json = zrevrangebyscore(queue-key, now_ts, 0, 0, 1)
if task_json:
grabbed_ok = zrem(queue-key, task_json)
if grabbed_ok:
process_task(task_json)
else:
sleep(1000) // 歇 1s
Когда потребитель является многопоточным или многопроцессорным, возникает проблема потери конкуренции. Текущий поток явно опрашивает task_json из zset, но zrem не может его захватить. В настоящее время вы можете использовать сценарии LUA для решения этой проблемы, атомизируя операции опроса и состязания, чтобы избежать ненужных состязаний.
local res = nil
local tasks = redis.pcall("zrevrangebyscore", KEYS[1], ARGV[1], 0, "LIMIT", 0, 1)
if #tasks > 0 then
local ok = redis.pcall("zrem", KEYS[1], tasks[1])
if ok > 0 then
res = tasks[1]
end
end
return res
Почему я говорю о распределенных блокировках и очередях задержки вместе, ведь на линии произошел сбой очень рано. Когда произошла ошибка, длина очереди Redis на линии резко увеличилась, что привело к невыполнению многих асинхронных задач и проблемам с бизнес-данными. Позже была выяснена причина: распределенная блокировка использовалась неэффективно, что приводило к взаимоблокировке, а при сбое блокировки в результате бесконечного повтора спящего режима асинхронная задача полностью переходила в спящее состояние и не смог обработать задание. Так как же эта распределенная блокировка использовалась в то время? Используется setnx + expire, в результате при обновлении сервиса остановка процесса напрямую приводит к выполнению setnx для отдельных запросов, но expires не выполняется, что приводит к взаимоблокировке для отдельных пользователей. Однако в фоновом режиме выполняется другая асинхронная задача, которая также должна заблокировать пользователя. Если блокировка не удалась, она будет бездействовать на неопределенный срок и повторить попытку. Как только она столкнется с заблокированным пользователем впереди, асинхронный поток будет полностью закрыт. . Из-за этой аварии у нас есть сегодняшняя правильная распределенная форма блокировки и изобретение отложенных очередей, а также изящное завершение работы, потому что, если есть логика изящного завершения работы, то обновления службы не будут вызывать выполнение запросов только наполовину. если процесс не убит -9 или вниз.
задача на время
Существует много способов реализации распределенных задач синхронизации, наиболее распространенным из которых является модель мастер-работники. Мастер отвечает за управление временем, и, когда время приходит, сообщения задач по-прежнему отправляются промежуточному программному обеспечению сообщений, а затем рабочие несут ответственность за мониторинг этих очередей сообщений для потребления сообщений. Celery, известная платформа задач синхронизации Python, делает именно это. Но у Celery есть проблема, то есть мастер - это одна точка, если мастер зависнет, то вся система задач на время перестанет работать.
Другой реализацией является модель с несколькими мастерами. Что означает эта модель?Она похожа на инфраструктуру Quartz в Java, которая использует блокировки базы данных для управления параллельным выполнением задач. Будет несколько процессов, и каждый процесс будет управлять временем. Когда время истечет, блокировка базы данных будет использоваться для конкуренции за право выполнить задачу. Захваченный процесс получит возможность выполнить задачу, а затем приступить к выполнению задачи, которая решает проблему мастера.Единственный вопрос. Одним из недостатков этой модели является то, что она приводит к потере конкуренции, но обычно в большинстве бизнес-систем не так много запланированных задач, поэтому такая потеря конкуренции не является серьезной. Другая проблема заключается в том, что он зависит от согласованности распределенного машинного времени.Если время на нескольких машинах несовместимо, задача будет выполняться несколько раз.Это можно облегчить, увеличив время блокировки базы данных.
Теперь, когда у нас есть распределенные блокировки Redis, мы можем реализовать простую структуру временных задач поверх Redis.
# 注册定时任务
hset tasks name trigger_rule
# 获取定时任务列表
hgetall tasks
# 争抢任务
set lock:${name} true nx ex=5
# 任务列表变更(滚动升级)
# 轮询版本号,有变化就重加载任务列表,重新调度时间有变化的任务
set tasks_version $new_version
get tasks_version
Если вы чувствуете, что код внутри Quartz слишком сложен для понимания, а распространяемой документации почти нет, выкинуть сложно, можете попробовать Redis, от него меньше выпадения волос.
Life is Short,I use Redis
https://github.com/pyloque/taskino
контроль частоты
Если вы когда-либо создавали сообщество, вы знаете, что всегда будет спам. Когда вы проснетесь, вы обнаружите, что на главной странице внезапно появляются какие-то необъяснимые рекламные посты. Если не использовать соответствующие механизмы для контроля, это может привести к серьезным последствиям для пользователя.
Существует множество стратегий борьбы со спамом, наиболее продвинутый — с помощью ИИ, а самый простой — с помощью сканирования ключевых слов. Другим часто используемым методом является управление частотой, которое ограничивает скорость производства контента для одного пользователя, а пользователи разных уровней будут иметь разные параметры управления частотой.
Контроль частоты можно реализовать с помощью Redis.Поведение пользователя мы понимаем как временной ряд.Нам необходимо сделать так, чтобы длина временного ряда одного пользователя была ограничена в пределах определенного периода времени, а поведение пользователя запрещено за его пределами длина. Его можно реализовать с помощью zset Redis.
Зеленый отдел на рисунке — это информация временного ряда за период времени, который мы хотим сохранить, а серый сегмент будет обрезан. Подсчитайте количество записей временных рядов в зеленом сегменте, чтобы узнать, превышено ли пороговое значение частоты.
Приведенный ниже код контролирует поведение пользовательского пользовательского пользовательского интерфейса до N раз в час.
hist_key = "ugc:${user_id}"
with redis.pipeline() as pipe:
# 记录当前的行为
pipe.zadd(hist_key, ts, uuid)
# 保留1小时内的行为序列
pipe.zremrangebyscore(hist_key, 0, now_ts - 3600)
# 获取这1小时内的行为数量
pipe.zcard(hist_key)
# 设置过期时间,节约内存
pipe.expire(hist_key, 3600)
# 批量执行
_, _, count, _ = pipe.exec()
return count > N
обнаружение службы
Предприятия с чуть более высокой технологической зрелостью будут иметь инфраструктуру для обнаружения сервисов. Обычно в качестве хранилища списка сервисов мы используем распределенные базы данных конфигурации, такие как zookeeper, etcd, consul и т.д. У них есть очень своевременные механизмы уведомления для уведомления потребителей услуг об изменениях в списке услуг. Так как же использовать Redis для обнаружения сервисов?
Здесь мы снова используем структуру данных zset, мы используем zset для хранения списка отдельных сервисов. Несколько списков услуг хранятся с использованием нескольких zsets. Значение и оценка zset хранят адрес службы и время пульса соответственно. Провайдеры услуг должны использовать сердцебиение, чтобы сообщать о своей активности, вызывая zadd каждые несколько секунд. Используйте zrem, чтобы удалить себя, когда поставщик услуг останавливает службу.
zadd service_key heartbeat_ts addr
zrem service_key addr
Этого недостаточно, так как служба может завершиться аварийно и вообще не иметь возможности выполнить хук, поэтому требуется дополнительный поток для очистки просроченных элементов в списке служб.
zremrangebyscore service_key 0 now_ts - 30 # 30s 都没来心跳
Следующий важный вопрос — как уведомить потребителей об изменении списка сервисов, здесь мы также используем механизм опроса номера версии. Увеличивайте номер версии при изменении списка служб. Потребитель перезагружает список сервисов, запрашивая изменения номера версии.
if zadd() > 0 || zrem() > 0 || zremrangebyscore() > 0:
incr service_version_key
Но проблема остается: если потребитель зависит от большого количества списков сервисов, ему нужно опрашивать множество номеров версий, и эффективность ввода-вывода будет относительно низкой. В это время мы можем добавить еще один глобальный номер версии.При изменении номера версии любого списка служб глобальный номер версии увеличивается. Таким образом, при нормальных обстоятельствах потребителям нужно только опросить глобальный номер версии. При изменении глобального номера версии номера подверсий зависимого списка служб сравниваются один за другим, а затем загружается измененный список служб.
битовая карта
Система регистрации заезда ранее, и количество пользователей еще не было простым дизайну, что должно хранить состояние входа пользователя в использование хеш-структуры Redis. Войти записано в хеш-структуре, И есть три состояния. Нет знака, подписанного и субсидий, составляет 0, 1, 2 три целочисленных значения соответственно.
hset sign:${user_id} 2019-01-01 1
hset sign:${user_id} 2019-01-02 1
hset sign:${user_id} 2019-01-03 2
...
Это огромная трата пользовательского пространства. Позже, когда ежедневная жизнь регистрации превысила 10 миллионов, проблема хранилища Redis стала заметной, и память сразу достигла 30G+. Наш онлайн-экземпляр обычно начинает тревожить после 20G и 30G уже серьезное превышение.
В это время мы начали решать эту проблему и оптимизировать хранилище. Мы решили использовать растровое изображение для записи информации о регистрации.Одно состояние регистрации требует двух битов для записи и всего 8 байт дискового пространства на месяц. Это позволяет использовать очень короткую строку для хранения записи регистрации пользователя в течение месяца.
Эффект от оптимизации очень очевиден, а память прямо уменьшена до 10 Гб. Поскольку вызов API для запроса состояния регистрации в течение всего месяца очень частый, трафик интерфейса также намного меньше.
Однако у растрового изображения есть и недостаток. Его нижний слой представляет собой строку, представляющую собой непрерывное пространство для хранения. Растровое изображение будет автоматически расширяться. Например, если большое растровое изображение содержит 8 млн бит, только последний бит равен 1, а остальные биты равны нулю., что также займет 1м места для хранения, такая трата очень серьезная.
Итак, существует структура данных Roaring Bitmap, которая хранит большое растровое изображение в сегментах, и можно сохранить сегмент со всеми нулями. Кроме того, для каждого сегмента разработана разреженная структура хранения.Если в этом сегменте не так много битов, установленных в 1, могут храниться только их целые смещения. Таким образом, пространство для хранения растрового изображения было значительно сжато.
Это бурное растровое изображение очень ценно в области точного подсчета больших данных, и заинтересованные студенты могут узнать о нем.
нечеткий счет
Я упоминал об этой системе чекинов ранее, но что, если продакт-менеджеру нужно знать ежедневную активность и ежемесячную активность этой чекины? Обычно мы просто сваливаем вину — спрашиваем в отделе данных. Однако данные в отделе данных часто не очень оперативны.Часто данные за предыдущий день нужно прогонять на следующий день.Офлайн-вычисления обычно выполняются один раз в день через равные промежутки времени. Как добиться активного подсчета в реальном времени?
Самое простое решение — поддерживать коллекцию сетов в Redis, приходишь к пользователю, просто грустно, размер итогового сета — это нужное нам число UV. Но эта пустая трата места огромна, и хранить такую огромную коллекцию только для одного номера кажется очень бесполезным. так что мне теперь делать?
В настоящее время вы можете использовать функцию нечеткого подсчета HyperLogLog, предоставляемую Redis, Это подсчет вероятности и имеет определенную ошибку, и ошибка составляет около 0,81%. Но занимаемое пространство очень мало, нижний слой представляет собой растровое изображение, которое занимает не более 12 КБ дискового пространства. А когда значение счетчика относительно невелико, растровое изображение использует разреженное хранилище, которое занимает меньше места.
# 记录用户
pfadd sign_uv_${day} user_id
# 获取记录数量
pfcount sign_uv_${day}
Его можно использовать для чтения количества статей в общедоступной учетной записи WeChat, а также для завершения UV-статистики веб-страниц. Но если продакт-менеджера очень заботит точность цифр, например статистики, напрямую привязанной к деньгам, то можно рассмотреть упомянутый ранее разглагольствовающий битмап. Это немного сложнее в использовании, требуя целочисленной сериализации идентификатора пользователя заранее. Redis изначально не предоставляет функциональность растрового изображения рычания, но есть модуль Redis с открытым исходным кодом, который можно использовать «из коробки».
Фильтр Блума
Наконец, мы хотим поговорить о фильтре Блума, который очень полезен, если в системе ожидается большой приток новых пользователей, что может значительно снизить скорость проникновения в кеш и снизить нагрузку на базу данных. Этот приток новых пользователей не обязательно является крупномасштабным развертыванием бизнес-систем, но также может быть вызван атаками с проникновением в кэш извне.
def get_user_state0(user_id):
state = cache.get(user_id)
if not state:
state = db.get(user_id) or {}
cache.set(user_id, state)
return state
def save_user_state0(user_id, state):
cache.set(user_id, state)
db.set_async(user_id, state)
Например, приведенный выше код интерфейса запроса статуса пользователя этой бизнес-системы.Теперь, когда приходит новый пользователь, он сначала обращается к кешу, чтобы проверить, есть ли какие-либо данные о статусе этого пользователя.Потому что это новый пользователь , его не должно быть в кеше. Затем он должен проверить базу данных, а базы данных там нет. Если такое большое количество новых пользователей хлынет мгновенно, можно предвидеть, что нагрузка на базу данных будет относительно большой, и будет большое количество пустых запросов.
Мы очень надеемся, что в Redis есть такой набор, в котором хранятся идентификаторы всех пользователей, чтобы мы могли узнать, придет ли новый пользователь, запросив эту коллекцию наборов. Когда количество пользователей очень велико, пространство для хранения, необходимое для хранения такой коллекции, очень велико. В настоящее время вы можете использовать фильтр Блума, который эквивалентен набору, но отличается от набора тем, что требует гораздо меньше места для хранения. Например, вам нужно 64 байта для хранения идентификатора пользователя, в то время как фильтру Блума требуется всего на 1 байт больше для хранения идентификатора пользователя. Однако он хранит не идентификатор пользователя, а отпечаток идентификатора пользователя, поэтому вероятность неверной оценки будет небольшой.Это контейнер с возможностями нечеткой фильтрации.
Когда он говорит, что идентификатор пользователя не находится в контейнере, то это определенно не так. Когда он говорит, что идентификатор пользователя находится в контейнере, в 99% случаев это правильно, а в 1% случаев это ложь. Однако в данном случае это неправильное суждение не вызовет проблемы.Цена ошибочного суждения — только проникновение в кеш, что эквивалентно 1% новых пользователей, которые не получают защиту фильтра Блума и напрямую проникают в запрос к базе данных, в то время как остальные 99% новых пользователей ниже могут быть эффективно заблокированы фильтром Блума, избегая проникновения в кеш.
def get_user_state(user_id):
exists = bloomfilter.is_user_exists(user_id)
if not exists:
return {}
return get_user_state0(user_id)
def save_user_state(user_id, state):
bloomfilter.set_user_exists(user_id)
save_user_state0(user_id, state)
Хорошей аналогией принципа фильтра Блума является то, что зимой на заснеженной земле, если вы пройдете по ней, вы оставите свои следы. Если на земле есть ваши следы, то с большой вероятностью можно сделать вывод, что вы бывали в этом месте, но не обязательно, возможно, чья-то обувь точно такая же, как и ваша. Но если у вас нет следов на земле, то вы можете быть на 100% уверены, что не были в этом месте.