Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Двусторонний websocket-роутинг

Двусторонний websocket-роутинг

С веб-сокетами на бэкенде работать не очень просто. Относительно понятно, как обрабатывать сообщения, поступающие в одном направлении, т.е. от клиента к серверу или от сервера к клиенту. Но когда возникает потребность полнодуплексного общения, еще и с асинхронным бэкендом и микросервисной архитектурой, то появляются сложности не только с роутингом во внутренние системы, но и, что важно, обратно от них к клиенту. К тому же, стоит учесть, что сообщения к клиенту могут поступать не в режиме «запрос-ответ», а произвольно, т.е. в разном объеме и в разное время.

Обдумывая варианты решения проблемы, мы присматривались к centrifugo и, как оказалось, оно предоставляет надежный канал в одном направлении, но не в двух. Наш же сценарий представляет собой чат-приложение, где клиент и оператор могут отправлять друг другу сообщения в произвольное время в произвольном порядке. Это решение, конечно, основано на базе микросервисной архитектуры с использованием kafka для общения этих микросервисов.

Таким образом, мы разработали свой сервис, который устраняет проблему полнодуплексного общения клиента с сервером через веб-сокет. Полагались на то, что наше решение должно быть горизонтально масштабируемым, cloud-native и написанным на современном асинхронном Python. О сложностях роутинга и о том, как «прицелиться» и «попасть» в нужного пользователя сообщением, и есть наш доклад.

https://conf.python.ru/moscow/2021/abstracts/7746
https://www.youtube.com/watch?v=pVebvoiBIjA

Denis Anikin

May 01, 2023
Tweet

More Decks by Denis Anikin

Other Decks in Programming

Transcript

  1. Владислав Лаухин 2 — разработчик в команде Chat, Raiffeisenbank —

    пилю чат и чатбота — развиваю с коллегами коммьюнити питонистов Райфа — питон, разработка, девопс
  2. Аникин Денис 3 Вдруг кому-то важно кто я такой —

    team lead в команде Chat, Raiffeisenbank — community lead в Python Community — fullstack: разрабатываю на python и typescript — занимаюсь развитием DevOps практик — мой сайт: https://xfenix.ru/
  3. Что мы делали? 6 — омниканальный чат со своей админкой

    — систему, с которой работают и операторы и клиенты
  4. Что мы делали? 7 — омниканальный чат со своей админкой

    — систему, с которой работают и операторы и клиенты — систему, в которой много вебсокет соединений, которые эксплуатируются в 2 направлениях
  5. Что мы делали? 8 — омниканальный чат со своей админкой

    — систему, с которой работают и операторы и клиенты — систему, в которой много вебсокет соединений, которые эксплуатируются в 2 направлениях — нам нужно было держать нагрузку и достичь отказоустойчивости
  6. Цели презентации 9 — В начале нашей разработки, в интернете

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

    не было информации по проблеме. Поэтому мы захотели поделиться — Информации и сейчас немного
  8. Цели презентации 11 — В начале нашей разработки, в интернете

    не было информации по проблеме. Поэтому мы захотели поделиться — Информации и сейчас немного — Хотелось показать, что в среде python разработки существуют не только REST’ы и CRUD’ы
  9. Шаг 0: готовые решения 13 Пытаемся понять — подходят ли

    они нам? Mercure: — нет четкой уверенности, что http2 + sse > websocket — менее популярное решение — меньше на слуху — у нас энтерпрайз J (лицензии?)
  10. Шаг 0: готовые решения 14 Пытаемся понять — подходят ли

    они нам? Centrifugo: — соединение в одном направлении — канал «вниз» нужно делать самим Mercure: — нет четкой уверенности, что http2 + sse > websocket — менее популярное решение — меньше на слуху — у нас энтерпрайз J (лицензии?)
  11. 16

  12. Больше о проблеме вебсокетов 18 Роутинг необходимо осуществлять в двух

    направлениях 2 направления применительно к нашим условиям
  13. Больше о проблеме вебсокетов 19 Роутинг необходимо осуществлять в двух

    направлениях 2 направления Роутинг «вверх» — сложная проблема (с ней мы и боремся в этом докладе) ↑ применительно к нашим условиям
  14. Больше о проблеме вебсокетов 20 Роутинг необходимо осуществлять в двух

    направлениях 2 направления Роутинг «вверх» — сложная проблема (с ней мы и боремся в этом докладе) ↑ Решение необходимо горизонтально масштабировать ↔ применительно к нашим условиям
  15. Что за продукт связан с «проблемой 1.0»? 23 — чат-бот.

    Его задача — помогать клиентам решать типовые проблемы
  16. Что за продукт связан с «проблемой 1.0»? 24 — чат-бот.

    Его задача — помогать клиентам решать типовые проблемы — обслуживает весь текстовый канал
  17. Что за продукт связан с «проблемой 1.0»? 25 — чат-бот.

    Его задача — помогать клиентам решать типовые проблемы — обслуживает весь текстовый канал — частично обслуживает голос
  18. Что за продукт связан с «проблемой 1.0»? 26 — чат-бот.

    Его задача — помогать клиентам решать типовые проблемы — обслуживает весь текстовый канал — частично обслуживает голос — в будущем будет сам звонить клиентам
  19. Что за продукт связан с «проблемой 1.0»? 27 — чат-бот.

    Его задача — помогать клиентам решать типовые проблемы — обслуживает весь текстовый канал — частично обслуживает голос — в будущем будет сам звонить клиентам — имеет много интеграций
  20. Как же быть с каналом «вверх»? 33 Например, взять RabbitMQ.

    Но нет уверенности, что: — динамические подписки/отписки быстро работают — динамическое создание очередей быстро работает — что это все держит высокую нагрузку
  21. 34

  22. 36

  23. 39

  24. Как устроен «фронт» 40 WAIT_TIMEOUT: Final[int] = 300 async def

    bot_request_handler(): #... inner_key: str = uuid4.uuid() # <-- вот он наш ключ await rabbitmq_connection.publish_message( 'sometopic', {'return_key': inner_key, 'message': 'Сообщение от пользователя'} ) #... while True: possible_result: bytes | None = await redis_connection.get(inner_key) # <-- вот он наш ключ if possible_result: await redis_connection.remove(inner_key) return JsonResponse({'answer': possible_result}) await asyncio.sleep(WAIT_TIMEOUT) # ... что-то про таймауты
  25. Как устроен «бэк» (ml ядро) 41 MESSAGE_EXPIRATION: Final[int] = 600

    async def ml_core_wannabe_answerer(): #... rabbit_listener: RabbitmqListener = rabbitmq_connection.subscribe('sometopic') while True: new_message: dict = await rabbit_listener.read_json() #... result_of_predict: dict = do_some_predict_magic(new_message) #... redis_connection.set( new_message['return_key'], # <-- вот он наш ключ json.encode(result_of_predict), MESSAGE_EXPIRATION)
  26. Выводы по решению 1.0 42 ✅ ну… оно работает! ✅

    сделано просто и понятно ✅ бизнес очень доволен
  27. Выводы по решению 1.0 43 ✅ ну… оно работает! ✅

    сделано просто и понятно ✅ бизнес очень доволен ❌ цикл со sleep ❌ редис неудобно масштабируется ❌ у нас «распределенный монолит», где несколько сервисов пользуются «shared DB»
  28. Что за продукт связан с «проблемой 2.0»? 47 — чат

    + чат-«админка», b2c — клиенты с текстовыми обращениями приходят сюда
  29. Что за продукт связан с «проблемой 2.0»? 48 — чат

    + чат-«админка», b2c — клиенты с текстовыми обращениями приходят сюда — обслуживает мобильное приложение, виджет на сайте, мессенджеры и т.п.
  30. Что за продукт связан с «проблемой 2.0»? 49 — чат

    + чат-«админка», b2c — клиенты с текстовыми обращениями приходят сюда — обслуживает мобильное приложение, виджет на сайте, мессенджеры и т.п. — имеет много интеграций, несколько своих фронтендов
  31. Что за продукт связан с «проблемой 2.0»? 50 — чат

    + чат-«админка», b2c — клиенты с текстовыми обращениями приходят сюда — обслуживает мобильное приложение, виджет на сайте, мессенджеры и т.п. — имеет много интеграций, несколько своих фронтендов — активно разрабатывается, бизнес требует большого объема функциональности
  32. Канал «вниз» 61 Нам пришло сообщение от пользователя и мы…

    Валидируем JWT Валидируем структуры данных, формируем выходную структуру и сераилизуем в JSON Отправляем в Kafka Юзеры пишут JWT
  33. Канал «вниз» 62 Нам пришло сообщение от пользователя и мы…

    Валидируем JWT Валидируем структуры данных, формируем выходную структуру и сераилизуем в JSON Отправляем в Kafka Юзеры пишут JWT Pydantic валидация
  34. Канал «вниз» 63 Нам пришло сообщение от пользователя и мы…

    Валидируем JWT Валидируем структуры данных, формируем выходную структуру и сераилизуем в JSON Отправляем в Kafka Pydantic формируем выходную ст. Юзеры пишут JWT Pydantic валидация
  35. Канал «вниз» 64 Нам пришло сообщение от пользователя и мы…

    Валидируем JWT Валидируем структуры данных, формируем выходную структуру и сераилизуем в JSON Отправляем в Kafka Pydantic формируем выходную ст. Юзеры пишут JWT Pydantic валидация Kafka
  36. Основная проблема канала «вверх» 66 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  37. Основная проблема канала «вверх» 67 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  38. Основная проблема канала «вверх» 68 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  39. Основная проблема канала «вверх» 69 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  40. Основная проблема канала «вверх» 70 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  41. Основная проблема канала «вверх» 71 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  42. Основная проблема канала «вверх» 72 Сообщение приходит из недр системы

    (от оператора, например) и идет к пользователю
  43. Написали свой сервис Как мы решали вопросы 73 1 Части

    сервиса общаются через ZeroMQ 5 Разделили на две части 2 Первая часть держит websocket соединение 3 Вторая роутит сообщения из кафки, «вверх» 4
  44. Почему ZeroMQ 76 Быстро! Нет брокера! (мы ленивые) Pub фильтрация

    происходит тут Sub Sub Sub User ID 1 User ID 2 User ID 3
  45. Почему ZeroMQ 77 Быстро! Нет брокера! (мы ленивые) Есть куча

    механизмов и биндинги под python Pub фильтрация происходит тут Sub Sub Sub User ID 1 User ID 2 User ID 3
  46. Архитектурная схема роутера 78 На базе ZeroMQ — В качестве

    ключа мы используем ID пользователя Coroutine 1 в любом поде или воркере Coroutine 2 в любом поде или воркере Router в любом поде или воркере Kafka
  47. Архитектурная схема роутера 79 На базе ZeroMQ — В качестве

    ключа мы используем ID пользователя — Каждая корутина со ссылкой на вебсокет соединение «сабается» на наш роутер Coroutine 1 в любом поде или воркере Coroutine 2 в любом поде или воркере Router в любом поде или воркере Sub ZeroMQ Sub ZeroMQ Kafka
  48. Архитектурная схема роутера 80 На базе ZeroMQ — В качестве

    ключа мы используем ID пользователя — Каждая корутина со ссылкой на вебсокет соединение «сабается» на наш роутер — Роутер паблишит «корутинам» по ключу с ID пользователя ✨✨✨ — Корутина получает нужное и отправляет пользователю Coroutine 1 в любом поде или воркере Coroutine 2 в любом поде или воркере Router в любом поде или воркере Sub ZeroMQ Sub ZeroMQ Pub ZeroMQ Pub ZeroMQ Kafka
  49. Роутер pub’лишит в websocket «сервер» (корутину) 82 Потом вот это

    Websocket worker 1 Router 1 Websocket worker 2 Router 2 Websocket worker 3 ❌ ❌ ✅ Решение ❌ или ✅ принимается тут ↓
  50. 83

  51. Как устроен websocket сервер 84 async def wannabe_router(message_from_kafka: dict): zeromq_context

    = Context.instance() socket = zeromq_context.socket(PUB) socket.bind(f'tcp://{ZERO_MQ_HOST}:{ZERO_MQ_PORT}') await socket.send_multipart([b'user-id', json.dumps(message_from_kafka)])
  52. Как устроен роутер 85 async def wannabe_websocket_server(websocket_client: WebSocket): zeromq_context =

    Context.instance() socket = zeromq_context.socket(SUB) socket.connect(f'tcp://{ZERO_MQ_HOST}:{ZERO_MQ_PORT}') socket.setsockopt(SUBSCRIBE, b'user-id') _, message = await socket.recv_multipart() await websocket_client.send_json(json.loads(message))
  53. Планы на будущее 86 — добавить динамические ключи фильтрации в

    zeromq связку — возможно сделать это с помощью «pub/sub» с фронтенда (иметь возможность sub’атся с фронтенда на websocket сервер) — написать больше тестов 😏😏😏
  54. Выводы по решению 2.0 87 ✅ горизонтально масштабируется ✅ больше

    нет никакой инфры, только код ✅ бизнес доволен ✅ запах распределенного монолита прошёл
  55. Выводы по решению 2.0 88 ✅ горизонтально масштабируется ✅ больше

    нет никакой инфры, только код ✅ бизнес доволен ✅ запах распределенного монолита прошёл ❌ это сложно ❌ иногда нужна поясняющая бригада
  56. Финальные выводы презентации 90 Возможно, вам подойдут готовые решения! Если

    нет, то: — вы узнали как 2 способами решить проблему канала «вверх»
  57. Финальные выводы презентации 91 Возможно, вам подойдут готовые решения! Если

    нет, то: — вы узнали как 2 способами решить проблему канала «вверх» — у нас есть простой способ, который подойдет там, где нас не смущает шареный редис
  58. Финальные выводы презентации 92 Возможно, вам подойдут готовые решения! Если

    нет, то: — вы узнали как 2 способами решить проблему канала «вверх» — у нас есть простой способ, который подойдет там, где нас не смущает шареный редис — а так же есть сложный способ подойдет там, где есть MSA, k8s и настоящее горизонтальное масштабирование