Раньше у компании был простой бизнес по подписке на статьи, но она использовала регулярную модель вытягивания, которая имела длительный цикл и плохую своевременность.
Так что подумайте о том, чтобы создать сервис с длительным подключением и активно продвигайте вновь созданные статьи.
Поскольку это веб-сценарий, приоритет отдается зрелому протоколу веб-сокетов.Многие языки программирования имеют зрелые серверные среды разработки.
Основные технические трудности
Узкое место системного вызова
Предположим, что в сети 1 миллион человек, тогда 1 статья приведет к 1 миллиону пушей, а 10 статей — 10 миллионам пушей.
Согласно эмпирическим данным, когда система Linux обрабатывает сетевые системные вызовы TCP, она может обрабатывать только около 1 миллиона пакетов в секунду.
Если вы посмотрите на это таким образом, отправка 1 статьи уже достигла предела вычислительной мощности одной машины, что является первой трудностью.
блокировать узкое место
Когда мы нажимаем, нам нужно обойти все онлайн-соединения, обычно эти соединения помещаются в коллекцию.
Определенно потребуется значительное количество времени, чтобы пройти 1 миллион подключений для отправки сообщений, и клиент все еще подключается к сети и оффлайн в течение периода отправки, поэтому эту коллекцию необходимо заблокировать для защиты параллелизма.
Видно, что время блокировки при обходе будет очень большим, а последовательно обойти коллекцию сможет только один поток, что недопустимо.
Узкое место ЦП
Как правило, связь между клиентом и сервером основана на протоколе JSON. Прежде чем отправлять сообщение каждому клиенту, сообщение необходимо закодировать в формате json.
Когда количество онлайн-соединений относительно невелико (например, 10 000) и push-сообщения относительно часты (100 000 в секунду), мы можем рассчитать количество кодировок json encode в секунду: 10000 * 100000 = 10^9 раз.
Даже если мы заранее закодируем json на 100 000 сообщений, а затем раздадим на 10 000 подключений, потребуется 100 000 кодировок в секунду.
Кодирование JSON — это чистое вычислительное поведение ЦП, которое очень интенсивно использует ЦП, и мы все еще сталкиваемся с большим давлением оптимизации.
Решить технические трудности
узкое место системного вызова
По-прежнему предполагая, что 1 миллион человек находится в сети, тогда предел одной машины — отправить 1 статью в секунду, что принесет 1 миллион сетевых системных вызовов в секунду.
Если мы хотим отправить 100 статей и при этом использовать обработку на одном компьютере, в чем заключается идея оптимизации?
Очень просто, мы отправляем 100 статей в виде сообщения, тогда это по-прежнему 1 миллион системных вызовов в секунду.
Будь то 10 статей, 50 статей или 80 статей, мы объединяем их в один push-сообщение, поэтому частота push-сообщений на 1 миллион человек в сети составляет постоянный 1 миллион раз в секунду, который не меняется с количеством статей. .
Конечно, объединенное сообщение не может быть бесконечным.Когда оно превышает определенный порог, уровень TCP/IP будет разделять большие пакеты.В это время фактическая частота пакетов нижележащего уровня превысит 1 миллион раз в секунду, достигнув предела системных вызовов снова.
блокировать узкое место
При проектировании архитектуры массовых услуг очень полезна идея: большие и маленькие.
Поскольку в коллекцию помещается 1 миллион подключений, степень детализации блокировки слишком велика, поэтому мы можем хешировать подключения в несколько коллекций, каждая со своей блокировкой.
Когда мы отправляем, мы можем использовать несколько потоков, чтобы нести ответственность за задачи по проталкиванию нескольких наборов.
Поскольку отправляемые коллекции отличаются друг от друга, между потоками не возникает конфликта блокировок. Чтобы одна и та же коллекция могла отправлять несколько разных сообщений одновременно, мы можем заменить блокировку мьютекса на блокировку чтения-записи, чтобы поддерживать несколько потоков, одновременно обходящих одну и ту же коллекцию для отправки разных сообщений.
На самом деле, операционная система управляет процессором с разделением времени, точно так же, как наши push-задачи разделены на несколько небольших наборов, каждый набор должен занимать лишь небольшой отрезок времени для быстрого выполнения, а несколько наборов используют многоядерность в качестве насколько это возможно Преимуществом является истинный параллелизм.
Узкое место ЦП
Фактически, когда мы сокращаем сетевые системные вызовы посредством слияния сообщений, мы завершаем оптимизацию системного процессора, и процессорное время, используемое операционной системой для обработки сетевых системных вызовов, значительно сокращается.
Однако пользовательский ЦП требует от нас продолжения оптимизации, если мы будем делать json encode на каждом уровне соединения, то одна статья принесет 1 миллион encodes, что является совершенно неприемлемой производительностью.
Поскольку в бизнесе существует два типа отправки сообщений: один — отправка в соответствии с темами, которые волнуют клиента, а другой — отправка всем клиентам.
Основываясь на приведенных выше характеристиках, мы можем продвинуть действие объединения сообщений на уровень ввода сообщений, то есть объединить все сообщения, которые будут отправлены в определенную тему, и все онлайн-сообщения за последний период в пакеты, и каждый пакет может содержать 100 сообщений. Когда пакет заполнен или истекает время ожидания, его можно пройти и передать непосредственно целевому клиенту после того, как json закодирует его один раз.
После слияния сообщений потребление ЦП при кодировании больше не связано с количеством онлайн-соединений и не связано напрямую с количеством сообщений, которые нужно отправить, а связано с количеством пакетов после упаковки, с резким уменьшением Эффект величины.
Архитектурные соображения
кластерный шлюз
После описанного выше дизайна мы можем использовать GO для реализации высококонкурентного шлюза для длинных подключений через веб-сокеты.
Шлюз можно развернуть горизонтально для формирования кластера, а внешний интерфейс использует балансировку нагрузки LVS/HA/DNS.
Когда мы используем развертывание кластера шлюза, когда мы хотим отправить сообщение, нам нужно распространить сообщение на все процессы шлюза.
логическая логика службы
Итак, я реализовал сервис Logic, который сам по себе не имеет состояния и отвечает за 2 основные функции:
1. Предоставляет бизнес-интерфейс HTTP для отправки push-сообщений, поскольку частота push-уведомлений в системе push-уведомлений не слишком высока. Кроме того, бизнес-сторона будет иметь много суждений бизнес-логики перед отправкой и, наконец, завершит отправку через HTTP, который считается относительно простым способом доступа.
2. Отвечает за распространение push-сообщений для каждого процесса шлюза.Здесь HTTP/2 используется в качестве протокола RPC (GRPC — это HTTP/2), чтобы обеспечить высокую возможность параллелизма одного соединения и обеспечить изоляцию сбоев между различными шлюзами. не влияют друг на друга.
Услуги по сертификации
Аутентификация при входе в систему для подключений через веб-сокет еще не введена, но в будущем, когда возникнет потребность в отправке сообщений конкретным пользователям, необходимо будет внедрить службу аутентификации.
Служба аутентификации не зависит от шлюза и логики и может называться Passport.
Клиент сначала выполняет вход в паспорт на основе системы учетной записи компании, получает токен входа с самоаутентификацией (например, JWT), а затем инициирует шлюзовое соединение.
После того, как шлюз проверит токен, идентификация uid завершена, весь процесс не требует дополнительного взаимодействия с другими бизнес-системами, конечно, может быть добавлена и дополнительная проверка службы вызовов.
Затем, когда логика хочет отправить сообщение определенному uid, при текущей архитектуре сообщение все равно должно быть распространено на все шлюзы, и шлюз найдет соединение, соответствующее uid. Но это, несомненно, растрата, потому что uid может быть подключен только к определенному шлюзу, а к другим шлюзам он не имеет смысла.
сеансовый сеансовый уровень
В будущем вы можете рассмотреть возможность добавления уровня сеанса для записи отношения соединения между uid и шлюзом, чтобы логика могла найти шлюз, в котором находится uid, через уровень сеанса, и выполнить направленную передачу.
Сеансовый уровень может быть отдельной службой, а связь между uid и шлюзом хранится в чистой памяти.
Из-за простоя шлюза и по другим причинам мы не сможем вовремя удалить отключенный сеанс, поэтому информация о работоспособности клиента должна регулярно передаваться между шлюзом и сеансом.
Конечно, также можно просто и грубо заменить сеансовый уровень кластером Redis и предоставить только одну возможность обратной проверки uid->gateway.
исходный код
Я открываю исходный код проекта на github, объем кода очень мал, поэтому, если вам интересно, лучше прочитать исходный код.