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. Двусторонний
    websocket-роутинг
    Владислав Лаухин
    Аникин Денис

    View full-size slide

  2. Владислав Лаухин
    2
    — разработчик в команде Chat, Raiffeisenbank
    — пилю чат и чатбота
    — развиваю с коллегами коммьюнити питонистов Райфа
    — питон, разработка, девопс

    View full-size slide

  3. Аникин Денис
    3
    Вдруг кому-то важно кто я такой
    — team lead в команде Chat, Raiffeisenbank
    — community lead в Python Community
    — fullstack: разрабатываю на python и typescript
    — занимаюсь развитием DevOps практик
    — мой сайт: https://xfenix.ru/

    View full-size slide

  4. В начале опишем
    контекст
    4

    View full-size slide

  5. Что мы делали?
    5
    — омниканальный чат со своей админкой

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. Перейдём к делу
    12

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. Ну раз нет, то…
    15

    View full-size slide

  16. Наш стек
    17

    View full-size slide

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

    View full-size slide

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

    применительно к нашим условиям

    View full-size slide

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

    Решение необходимо
    горизонтально
    масштабировать

    применительно к нашим условиям

    View full-size slide

  20. Но для начала поговорим
    как мы уже решали эту
    проблему в другом
    продукте
    21

    View full-size slide

  21. Подход к
    проблеме 1.0

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  27. 28
    Набросок части архитектуры

    View full-size slide

  28. 29
    Набросок части архитектуры

    View full-size slide

  29. 30
    Набросок части архитектуры

    View full-size slide

  30. 31
    Набросок части архитектуры
    ?

    View full-size slide

  31. 32
    Набросок части архитектуры
    ?
    Вот и сложность канала «вверх»

    View full-size slide

  32. Как же быть с каналом «вверх»?
    33
    Например, взять RabbitMQ. Но нет уверенности, что:
    — динамические подписки/отписки быстро работают
    — динамическое создание очередей быстро работает
    — что это все держит высокую нагрузку

    View full-size slide

  33. Как же мы решили
    «проблему 1.0»?
    35

    View full-size slide

  34. 37
    Приблизительно так

    View full-size slide

  35. 38
    Приблизительно так

    View full-size slide

  36. Как устроен «фронт»
    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)
    # ... что-то про таймауты

    View full-size slide

  37. Как устроен «бэк» (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)

    View full-size slide

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

    View full-size slide

  39. Выводы по решению 1.0
    43
    ✅ ну… оно работает!
    ✅ сделано просто и понятно
    ✅ бизнес очень доволен
    ❌ цикл со sleep
    ❌ редис неудобно масштабируется
    ❌ у нас «распределенный монолит»,
    где несколько сервисов пользуются
    «shared DB»

    View full-size slide

  40. Этот подход в ряде
    случаев имеет право
    на существование
    44

    View full-size slide

  41. Подход к
    проблеме 2.0

    View full-size slide

  42. Что за продукт связан с «проблемой 2.0»?
    46
    — чат + чат-«админка», b2c

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. А так же
    51
    — MSA архитектура («шареный» redis не пойдет)
    — Кубер

    View full-size slide

  48. 52
    Общая архитектура
    В общих чертах

    View full-size slide

  49. 53
    Общая архитектура
    В общих чертах

    View full-size slide

  50. 54
    Общая архитектура
    В общих чертах

    View full-size slide

  51. 55
    Общая архитектура
    В общих чертах

    View full-size slide

  52. 56
    Общая архитектура
    В общих чертах

    View full-size slide

  53. 57
    Общая архитектура
    В общих чертах

    View full-size slide

  54. 58
    Общая архитектура
    В общих чертах

    View full-size slide

  55. 59
    Общая архитектура
    В общих чертах

    View full-size slide

  56. 60
    Общая архитектура
    В общих чертах

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. Вcё довольно просто,
    но дальше
    интереснее…
    65

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. Написали свой сервис
    Как мы решали вопросы
    73
    1
    Части сервиса общаются через ZeroMQ
    5
    Разделили на две части
    2
    Первая часть держит websocket
    соединение
    3
    Вторая роутит сообщения из кафки,
    «вверх»
    4

    View full-size slide

  70. Как назвали части:
    1 — websocket сервер
    2 — роутер
    74

    View full-size slide

  71. Почему ZeroMQ
    75
    Быстро!
    Pub
    фильтрация
    происходит тут
    Sub
    Sub
    Sub
    User ID 1
    User ID 2
    User ID 3

    View full-size slide

  72. Почему ZeroMQ
    76
    Быстро!
    Нет брокера! (мы ленивые)
    Pub
    фильтрация
    происходит тут
    Sub
    Sub
    Sub
    User ID 1
    User ID 2
    User ID 3

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  77. Как устроена подписка на роутеры
    81
    Сначала делаем это
    Websocket
    worker 1
    Router 1
    Websocket
    worker 2
    Router 2
    Websocket
    worker 3

    View full-size slide

  78. Роутер pub’лишит в websocket «сервер» (корутину)
    82
    Потом вот это
    Websocket
    worker 1
    Router 1
    Websocket
    worker 2
    Router 2
    Websocket
    worker 3



    Решение ❌ или ✅
    принимается тут ↓

    View full-size slide

  79. Как устроен 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)])

    View full-size slide

  80. Как устроен роутер
    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))

    View full-size slide

  81. Планы на будущее
    86
    — добавить динамические ключи фильтрации в zeromq связку
    — возможно сделать это с помощью «pub/sub» с фронтенда (иметь возможность
    sub’атся с фронтенда на websocket сервер)
    — написать больше тестов 😏😏😏

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  84. Финальные выводы презентации
    89
    Возможно, вам подойдут готовые решения!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  88. https://linktr.ee/laukhin/
    https://xfenix.ru/
    Спасибо.
    С радостью ответим на
    вопросы!

    View full-size slide