Разработайте систему push-уведомлений на миллион уровней

Redis задняя часть ZooKeeper Netty
Разработайте систему push-уведомлений на миллион уровней

предисловие

Прежде всего, я желаю всем счастливого Праздника середины осени.

Больше недели нет обновлений. На самом деле, я всегда хотел сдержать большой трюк и поделиться некоторыми галантерейными товарами, которые всем интересны.

Ввиду моего личного рабочего содержания в последнее время я воспользовался этими тремя днями коротких каникул (на самом деле я играл два дня 🤣).


Кратко поговорим о теме этого времени.Поскольку в последнее время я занимаюсь разработкой, связанной с Интернетом вещей, я неизбежно столкнусь с взаимодействиями с устройствами.

Основная задача — иметь систему поддержки доступа к устройствам и push-сообщений на устройства, в то же время она должна удовлетворять потребности большого количества доступа к устройствам.

Таким образом, контент, которым поделились на этот раз, может не только соответствовать области IoT, но и поддерживать следующие сценарии:

  • на основеWEBсистема чата (одноранговый, групповой чат).
  • WEBСценарии, требующие отправки сервером приложений.
  • Платформа отправки сообщений на основе SDK.

Технический отбор

Чтобы удовлетворить большое количество подключений, одновременно поддерживайте двойную полнодуплексную связь, и производительность должна быть гарантирована.

Выбор в стеке технологий Java, естественно, исключает традиционныеIO.

Тогда есть только NIO, и на этом уровне все еще не так много выбора, учитывая сообщество, обслуживание данных и т. Д. Я, наконец, выбрал Netty.

Окончательная схема архитектуры выглядит следующим образом:

Неважно, если вы посмотрите на это сейчас, я представлю их по одному ниже.

Анализ протокола

Поскольку это система сообщений, естественно определить формат протокола обеих сторон вместе с клиентом.

Наиболее распространенным и простым является протокол HTTP, но одним из наших требований должен быть двойной полнодуплексный режим взаимодействия, а HTTP больше для браузеров. Что нам нужно, так это более оптимизированный протокол, который уменьшает количество ненужных передач данных.

Поэтому я думаю, что лучше всего настроить свой собственный частный протокол, удовлетворяя потребности бизнеса.В моем сценарии на самом деле есть стандартные протоколы IoT.

Если это другая сцена, вы можете узнать из текущей популярнойRPCПлатформа настраивает частный протокол, чтобы сделать общение между двумя сторонами более эффективным.

Однако, согласно опыту этого периода, независимо от того, какой метод используется, в протоколе должна быть зарезервирована позиция, связанная с безопасностью.

Обсуждается только содержание, относящееся к протоколу, и вводятся более конкретные приложения.

Простая реализация

Сначала рассмотрим, как реализовать функцию, а затем рассмотрим случай с миллионами соединений.

Аутентификация при регистрации

Первое, что нужно учитывать перед отправкой реального сообщения вверх и вниз по течению, — это проблема аутентификации.

Как и при использовании WeChat, первым шагом должен быть вход в систему, никто не может напрямую подключиться к платформе.

Итак, первый шаг — зарегистрироваться.

Как показано на приведенной выше архитектурной диаграмме注册/鉴权модуль. Обычно клиенту необходимо пройтиHTTPЗапрос передает уникальный идентификатор, и после прохождения фоновой аутентификации он ответитtoken, и поместите этоtokenОтношения с клиентом поддерживаютсяRedisИли в БД.

Клиент также сохраняет этот токен локально, и каждый будущий запрос должен приносить этот токен. По истечении срока действия токена клиенту необходимо снова запросить токен.

После прохождения аутентификации клиент напрямую передаетTCP 长连接к картинкеpush-serverмодуль.

Этот модуль представляет собой реальную восходящую и нисходящую обработку сообщения.

Сохранить отношение канала

После того, как соединение установлено, текущие отношения между клиентом и каналом должны быть сохранены, прежде чем бизнес будет фактически обработан.

Предполагая, что уникальным идентификатором клиента является номер мобильного телефона, необходимо сохранить номер мобильного телефона и текущий канал на карте.

это и доSpringBoot интегрирует механизм сердцебиения длинных соединенийпохожий.

При этом, чтобы получить уникальный идентификатор (номер мобильного телефона) клиента через Канал, также необходимо установить в Канале соответствующие свойства:

public static void putClientId(Channel channel, String clientId) {
    channel.attr(CLIENT_ID).set(clientId);
}

При получении мобильного номера:

public static String getClientId(Channel channel) {
    return (String)getAttribute(channel, CLIENT_ID);
}

Таким образом, когда наш клиент находится в автономном режиме, мы можем записывать соответствующие журналы:

String telNo = NettyAttrUtil.getClientId(ctx.channel());
NettySocketHolder.remove(telNo);
log.info("客户端下线,TelNo=" +  telNo);

Здесь следует отметить одну вещь: карта, в которой хранятся отношения между клиентом и каналом, предпочтительно имеет заданный размер (во избежание частого расширения), потому что это будет объект, который используется чаще всего и занимает больше всего памяти.

сообщение вверх по течению

Далее следует загрузка реальных бизнес-данных.Вообще говоря, первым шагом является определение того, какой тип бизнеса указан в загруженном сообщении.

В сценарии чата можно загружать контент, такой как текст, изображения и видео.

Таким образом, мы должны различать и выполнять различную обработку; это связано с протоколом, согласованным клиентом.

  • Его можно отличить по полю в заголовке сообщения.
  • Проще - этоJSONсообщение, выньте поле, чтобы различать разные сообщения.

Что бы это ни было, его можно только различить.

Разбор сообщений и разделение бизнеса

После того, как сообщение может быть проанализировано, бизнес обрабатывается, например, запись в базу данных, вызов других интерфейсов и т. д.

Все мы знаем, что обработка сообщений в Netty обычно выполняется вchannelRead()метод.

Здесь можно разобрать сообщение, различить тип.

Но если в нем будет прописана и наша бизнес-логика, то контента здесь будет огромное.

Даже мы разделены на несколько разработчиков, чтобы иметь дело с разными бизнесами, поэтому будет много конфликтов, сложно поддерживать и другие проблемы.

Поэтому очень важно полностью отделить анализ сообщений от бизнес-обработки.

Здесь в игру вступает интерфейсно-ориентированное программирование.

Вот основной код и"Строим колесо" - цикада (облегченный WEB фреймворк)согласуется.

Все они сначала определяют интерфейс для обработки бизнес-логики, а затем создают определенные объекты посредством отражения для их выполнения после разбора сообщения.处理函数Вот и все.

Таким образом, разным предприятиям и разным разработчикам нужно только реализовать этот интерфейс и реализовать свою собственную бизнес-логику.

Псевдокод выглядит следующим образом:

Чтобы узнать о конкретной реализации цикады, нажмите здесь:

GitHub.com/вместе ОС/…

В восходящем канале есть еще один момент, на который следует обратить внимание: поскольку он основан на длительном соединении, клиенту необходимо регулярно отправлять пакеты пульса для поддержания этого соединения. При этом на сервере тоже будут соответствующие проверки, через N промежутков времени он будет активно отключать соединение для экономии ресурсов.

ИспользоватьIdleStateHandlerможет быть достигнуто, можно просмотреть больше контентаNetty (1) SpringBoot интегрирует механизм пульсации длинных соединений.

нисходящая линия сообщения

Есть плюс и минус. Например, в сцене чата подключено два клиента.push-server, им напрямую нужна двухточечная связь.

Процесс на данный момент таков:

  • A отправляет сообщение на сервер.
  • После того, как сервер получает сообщение, он знает, что сообщение должно быть отправлено B, и ему нужно найти канал B в памяти.
  • Переслать сообщение А через канал Б.

Это нисходящий процесс.

Даже администраторам необходимо отправлять системные уведомления всем онлайн-пользователям, например:

Пройдите карту, которая сохраняет отношения каналов, и отправляйте сообщения одно за другим. Это также основная причина, по которой его нужно хранить в Map раньше.

Псевдокод выглядит следующим образом:

Для получения подробной информации см.:

GitHub.com/crossover J я…

Распределенное решение

Автономная версия реализована, и теперь я сосредоточусь на том, как добиться миллионов подключений.

Миллионы подключений на самом деле просто прилагательное, скорее способ выразить, как реализовать распределенное решение, которое можно гибко расширять по горизонтали для поддержки большего количества подключений.

Прежде чем сделать это, мы должны сначала выяснить, сколько соединений может поддерживать наша автономная версия. На это влияет множество факторов.

  • Сам сервер настроен. Память, процессор, сетевая карта, максимальное количество открытых файлов, поддерживаемое Linux, и т. д.
  • Примените свою собственную конфигурацию, потому что самой Netty нужно полагаться на память вне кучи, но сама JVM также должна занимать часть памяти, например отношения канала хранения.Map. Это должно быть скорректировано в соответствии с вашей собственной ситуацией.

Комбинируя вышеуказанные условия, вы можете протестировать максимальное количество подключений, которое может поддерживать один узел.

Независимо от того, как оптимизируется отдельная машина, есть верхний предел, который также является основной проблемой, решаемой распределенной.

Введение в архитектуру

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

Сначала начните слева.

упомянутый выше注册鉴权Модули также развертываются в кластерах и загружаются через интерфейс Nginx. Ранее также упоминалось, что его основной целью является аутентификация и возврат токена клиенту.

ноpush-serverПосле кластера у него есть еще одна функция. То есть вернуть сервер, который может использоваться текущим клиентом.push-server.

правильно平台Обычно относится к платформе управления, которая может просматривать текущий онлайн-номер в режиме реального времени, отправлять сообщения указанному клиенту и т. д.

Push-сообщения должны проходить через push-маршрут (push-server), чтобы найти настоящий push-узел.

Остальные промежуточные программы, такие как: Redis, Zookeeper, Kafka, MySQL, подготовлены для этих функций, см. реализацию ниже.

Зарегистрируйтесь, чтобы узнать

Первый вопрос注册发现,push-serverКак выбрать доступный узел для клиента после того, как он станет множественным, нужно решить в первую очередь.

Содержание этого фрагмента уже находится вРаспределенный (1) Получение регистрации и обнаружения службыподробно описано.

всеpush-serverВам необходимо зарегистрировать свою информацию в Zookeeper при запуске.

注册鉴权Модуль подписывается на узлы в Zookeeper, чтобы получать последний список сервисов. Структура выглядит следующим образом:

Вот некоторый псевдокод:

Приложение начнет регистрацию Zookeeper.

за注册鉴权Модулям нужно только подписаться на этот узел Zookeeper:

стратегия маршрутизации

Теперь, когда вы можете получить список всех услуг, как выбрать правильныйpush-serverДля использования клиентом?

Этот процесс сосредоточен на следующих моментах:

  • Старайтесь следить за тем, чтобы каждый узел был равномерно связан.
  • Делать ли перебалансировку при добавлении или удалении узлов.

Во-первых, существует несколько алгоритмов обеспечения равновесия:

  • опрос. Каждый узел назначается клиенту один за другим. Однако будет неравномерное распределение новых узлов.
  • Метод хеширования по модулю. Аналогичен HashMap, но также имеет проблему с опросом. Конечно, вы также можете выполнить перебалансировку, например HashMap, чтобы все клиенты переподключались. Однако это приведет к разрыву и повторному подключению всех соединений, что немного дорого.
  • Хэш путь из-за проблем принес модуло一致性 Hashалгоритм, но некоторым клиентам все равно потребуется перебалансировка.
  • Веса. Нагрузку каждого узла можно отрегулировать вручную или даже автоматически.На основе мониторинга, когда нагрузка на некоторые узлы высока, вес может быть автоматически уменьшен, а вес может быть увеличен для тех, у которых низкая нагрузка.

Другой вопрос:

Когда мы перезапускаем некоторые приложения для обновления, что происходит с клиентом на этом узле?

Так как у нас есть механизм сердцебиения, то при сбое сердцебиения можно считать, что есть проблема с узлом. тогда вам нужно повторно запросить注册鉴权Модуль получает доступный узел. То же самое относится и к слабой сети.

Если клиент отправляет сообщение в это время, ему необходимо сохранить сообщение локально и дождаться, пока он получит новый узел, и отправить его снова.

соединение с отслеживанием состояния

В таком сценарии, в отличие от HTTP, который не имеет состояния, мы должны явно знать взаимосвязь между каждым клиентом и соединением.

В автономной версии выше мы сохранили эту связь в локальном кеше, но очевидно, что она не будет работать в распределенной среде.

Например, когда платформа отправляет сообщение клиенту, она должна сначала узнать, на каком узле хранится канал клиента.

С помощью нашего предыдущего опыта такая проблема, естественно, требует внедрения стороннего промежуточного программного обеспечения для хранения этой связи.

То есть хранилище на схеме архитектуры路由关系的 Redis, доступ у клиентаpush-serverКогда вам нужно однозначно идентифицировать текущего клиента и сервисный узелip+portсохранить вRedis.

В то же время отношение подключения должно быть удалено в Redis, когда клиент отключается.

Таким образом, в идеальной ситуации отношения карты в памяти каждого узла должны в сумме точно соответствовать данным в Redis.

Псевдокод выглядит следующим образом:

Будут проблемы параллелизма при сохранении отношения маршрутизации здесь, лучше всего заменить его однимluaсценарий.

push-маршрутизация

Представьте себе такой сценарий: что, если администратору нужно отправить системное сообщение недавно зарегистрированному клиенту?

Схема комбинированной архитектуры

Предполагая, что в этом пакете есть клиенты 10 Вт, сначала нам нужно передать этот пакет номеров через平台внизNginxДоставлено по маршруту толчка.

Для повышения эффективности пакет номеров можно даже повторно распределить по каждомуpush-routeсередина.

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

Затем вызовите через HTTPpush-serverДелайте реальную доставку сообщений (Netty также очень хорошо поддерживает протокол HTTP).

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

поток сообщений

Возможно, некоторые сценарии придают большое значение восходящим сообщениям клиента и нуждаются в сохранении, а количество сообщений очень велико.

существуетpush-severДелать дело явно нецелесообразно.В это время для развязки можно выбрать Кафку.

Просто закиньте все исходящие данные прямо в Kafka и забудьте об этом.

Затем программа-потребитель может получить данные и записать их в базу данных.

На самом деле, этот контент также стоит обсудить, вы можете сначала прочитать это, чтобы понять:Сильнее чем у Disruptor тоже есть переполнение памяти?

Мы подробно обсудим Кафку позже.

распределенная проблема

Распространение решает проблемы с производительностью, но приносит другие проблемы.

Мониторинг приложений

Например, как узнать десятки онлайнpush-serverСостояние узла?

В это время система мониторинга должна сыграть свою роль, нам нужно знать текущее использование памяти и GC каждого узла.

И использование памяти самой операционной системой, в конце концов, Netty использует много памяти вне кучи.

При этом необходимо отслеживать текущий онлайн-номер каждой ноды, а также онлайн-номер в Redis. Теоретически эти два числа должны быть равны.

Таким образом, использование системы также может быть известно, и количество этих узлов может гибко поддерживаться.

обработка журнала

Ведение журнала также стало чрезвычайно важным.Например, однажды сообщается, что клиент не может подключиться.Вы должны знать, в чем проблема.

Лучше всего к каждому запросу добавлять лог traceID, чтобы по этому логу было видно, где карта застряла в каждом узле.

Так же как и ELK эти инструменты должны быть использованы.

Суммировать

Это время основано на моем ежедневном опыте, некоторые ямы могут не наступить в работе, и будут некоторые пропуски.

В настоящее время создать стабильную систему проталкивания довольно проблематично, и здесь задействовано много моментов, которые станут известны только после того, как вы это сделаете.

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

Добро пожаловать, чтобы обратить внимание на публичный аккаунт, чтобы общаться вместе: