Михаил Салосин (далее – М.
С.
): - Всем привет! Меня зовут Майкл.
Я работаю серверным разработчиком в MC2 Software и расскажу об использовании Go в серверной части мобильного приложения Look+.
Кто-нибудь здесь любит хоккей?
Тогда это приложение для вас.
Он предназначен для Android и iOS и используется для просмотра трансляций различных спортивных событий онлайн и в записи.
Приложение также содержит различную статистику, текстовые трансляции, таблицы конференций, турниров и другую полезную для болельщиков информацию.
Также в приложении есть такое понятие как видеомоменты, т.е.
вы можете смотреть самые важные моменты матчей (голы, драки, буллиты и т.д.).
Если вы не хотите смотреть всю трансляцию, вы можете посмотреть только самые интересные.
Что вы использовали при разработке?
Основная часть написана на Go. API, с которым взаимодействовали мобильные клиенты, был написан на Go. На Go также был написан сервис отправки push-уведомлений на мобильные телефоны.Нам также пришлось написать собственную ORM, о которой, возможно, когда-нибудь поговорим.
Ну и на Go были написаны небольшие сервисы: изменение размера и загрузка изображений для редакторов.
В качестве базы данных мы использовали PostgreSQL. Интерфейс редактора был написан на Ruby on Rails с использованием гема ActiveAdmin. Импорт статистики от поставщика статистики также написан на Ruby. Для тестов системного API мы использовали Python unittest. Memcached используется для регулирования платежных вызовов API, «Chef» используется для управления конфигурацией, Zabbix используется для сбора и мониторинга внутренней статистики системы.
Graylog2 — для сбора логов, Slate — документация по API для клиентов.
Выбор протокола
Первая проблема, с которой мы столкнулись: нам нужно было выбрать протокол взаимодействия бэкенда и мобильных клиентов, исходя из следующих моментов.Самое главное требование: данные о клиентах должны обновляться в режиме реального времени.
То есть все, кто сейчас смотрит трансляцию, должны получать обновления практически мгновенно.
Для упрощения мы предположили, что данные, которые синхронизируются с клиентами, не удаляются, а скрываются с помощью специальных флагов.
Всевозможные редкие запросы (такие как статистика, составы команд, статистика команд) получаются обычными GET-запросами.
Плюс система должна была легко поддерживать 100 тысяч пользователей одновременно.
Исходя из этого, у нас было два варианта протокола: Вебсокеты.
Но нам не нужны были каналы от клиента к серверу.
Нам нужно было только отправлять обновления с сервера клиенту, поэтому веб-сокет является избыточным вариантом.
События, отправленные сервером (SSE), подошли как надо! Это довольно просто и в принципе удовлетворяет всему, что нам нужно.
События, отправленные сервером
Несколько слов о том, как это работает. Он работает поверх http-соединения.
Клиент отправляет запрос, сервер отвечает Content-Type: text/event-stream и не закрывает соединение с клиентом, а продолжает записывать данные в соединение:
Данные могут быть отправлены в формате, согласованном с клиентами.
В нашем случае мы отправили его в таком виде: в поле события было отправлено имя измененной структуры (человек, игрок), а в поле данных — JSON с новыми, измененными полями для игрока.
Теперь поговорим о том, как работает само взаимодействие.
Первое, что делает клиент, — определяет время последней синхронизации с сервисом: он просматривает свою локальную базу данных и определяет дату последнего записанного ею изменения.
Он отправляет запрос с этой датой.
В ответ мы отправляем ему все обновления, произошедшие с этой даты.
После этого он подключается к живому каналу и не закрывается, пока ему не потребуются эти обновления:
Отправляем ему список изменений: если кто-то забивает гол — меняем счет матча, если он получает травму — это тоже отправляется в реальном времени.
Таким образом, клиенты мгновенно получают актуальные данные в ленте событий матча.
Периодически, чтобы клиент понимал, что сервер не умер, что с ним ничего не произошло, мы отправляем метку времени каждые 15 секунд — чтобы он знал, что все в порядке и нет необходимости переподключаться.
Как обслуживается живое соединение?
Первым делом создаем канал, в который будут поступать буферизованные обновления.После этого подписываемся на этот канал, чтобы получать обновления.
Устанавливаем правильный заголовок, чтобы клиент знал, что все ок.
Отправьте первый пинг.
Мы просто записываем текущую временную метку соединения.
После этого читаем из канала в цикле, пока канал обновления не закроется.
Канал периодически получает либо текущую временную метку, либо изменения, которые мы уже записываем для открытия соединений.
Первая проблема, с которой мы столкнулись, заключалась в следующем: для каждого соединения, открытого с клиентом, мы создали таймер, который тикал раз в 15 секунд — получается, что если бы у нас было открыто 6 тысяч соединений с одной машиной (с одним API-сервером), 6 были созданы тысячи таймеров.
Это привело к тому, что машина не держала необходимую нагрузку.
Проблема не была для нас такой очевидной, но мы получили небольшую помощь и исправили ее.
В результате теперь наш пинг идет с того же канала, с которого приходит обновление.
Соответственно, есть только один таймер, который тикает раз в 15 секунд. Здесь есть несколько вспомогательных функций — отправка заголовка, пинга и самой структуры.
То есть сюда передается название таблицы (человек, матч, сезон) и информация об этой записи:
Механизм отправки обновлений
Теперь немного о том, откуда берутся изменения.У нас есть несколько человек, редакторов, которые смотрят трансляцию в режиме реального времени.
Они создают все события: кого-то удалили, кого-то травмировали, какая-то замена.
С помощью CMS данные поступают в базу данных.
После этого база данных уведомляет об этом API-серверы с помощью механизма Listen/Notify. Серверы API уже отправляют эту информацию клиентам.
Таким образом, мы по сути имеем всего несколько серверов, подключенных к базе данных и особой нагрузки на базу данных нет, поскольку клиент никак напрямую с базой данных не взаимодействует:
PostgreSQL: слушать/уведомлять
Механизм Listen/Notify в Postgres позволяет уведомлять подписчиков событий о том, что какое-то событие изменилось — в базе данных создана какая-то запись.
Для этого мы написали простой триггер и функцию:
При вставке или изменении записи мы вызываем функцию notify на канале data_updates, передавая туда имя таблицы и идентификатор записи, которая была изменена или вставлена.
Для всех таблиц, которые необходимо синхронизировать с клиентом, определяем триггер, который после изменения/обновления записи вызывает функцию, указанную на слайде ниже.
Как API подписывается на эти изменения? Создан механизм Fanout — он отправляет сообщения клиенту.
Он собирает все каналы клиентов и отправляет обновления, полученные по этим каналам:
Здесь стандартная библиотека pq, которая подключается к базе данных и говорит, что хочет прослушать канал (data_updates), проверяет, что соединение открыто и все в порядке.
Я опускаю проверку ошибок, чтобы сэкономить место (отказ от проверки опасен).
Далее мы асинхронно устанавливаем Ticker, который будет отправлять пинг каждые 15 секунд, и начинаем прослушивать канал, на который мы подписались.
Если мы получаем пинг, мы публикуем этот пинг.
Если мы получим какую-либо запись, мы опубликуем ее для всех подписчиков этого Fanout.
Как работает Fan-out?
По-русски это переводится как «расщепитель».У нас есть один объект, который регистрирует подписчиков, желающих получать обновления.
И как только на этот объект приходит обновление, он распространяет это обновление всем своим подписчикам.
Достаточно просто:
Как это реализовано в Go:
Структура есть, она синхронизируется с помощью Mutexes. В нем есть поле, сохраняющее состояние подключения Fanout к базе данных, т.е.
он в данный момент слушает и будет получать обновления, а также список всех доступных каналов — карта, ключом которой является канал и структура в виде значения (по сути оно никак не используется).
Два метода — Connected и Disconnected — позволяют нам сообщить Fanout, что у нас есть соединение с базой, оно появилось, и что соединение с базой потеряно.
Во втором случае нужно отключить всех клиентов и сказать им, что они больше ничего не могут слушать и чтобы они переподключались, так как соединение с ними закрылось.
Также есть метод Subscribe, который добавляет канал в «слушатели»:
Существует метод Unsubscribe, который удаляет канал из слушателей, если клиент отключается, а также метод Publish, который позволяет отправить сообщение всем подписчикам.
Вопрос: – Что передается по этому каналу? РС: — Передается модель, которая изменилась или пинг (по сути просто число, целое число).
РС: — Вы можете отправить что угодно, отправить любую структуру, опубликовать — она просто превращается в JSON и всё.
РС: — Получаем уведомление от Postgres — оно содержит имя и идентификатор таблицы.
По имени и идентификатору таблицы мы получаем нужную нам запись, а затем отправляем эту структуру на публикацию.
Инфраструктура
Как это выглядит с точки зрения инфраструктуры? У нас есть 7 аппаратных серверов: один из них полностью посвящен базе данных, остальные шесть запускают виртуальные машины.Есть 6 копий API: каждая виртуальная машина с API работает на отдельном аппаратном сервере — это для надежности.
У нас есть два фронтенда с установленным Keepalived для улучшения доступности, чтобы в случае чего один фронтенд мог заменить другой.
Также две копии CMS. Также имеется импортер статистики.
Есть Ведомая БД, с которой периодически делаются бэкапы.
Есть Pigeon Pusher — приложение, которое отправляет push-уведомления клиентам, а также инфраструктурные штучки: Zabbix, Graylog2 и Chef. По сути, эта инфраструктура избыточна, поскольку 100 тысяч можно обслуживать меньшим количеством серверов.
Но было железо - мы им пользовались (нам сказали, что можно - почему бы и нет).
Плюсы Го
После того, как мы поработали над этим приложением, выявились такие очевидные преимущества Go. Классная http-библиотека.С его помощью можно создать довольно многое «из коробки».
Плюс каналы, которые позволили нам очень легко реализовать механизм отправки уведомлений клиентам.
Замечательная штука Race детектор позволила нам устранить несколько критических ошибок (промежуточной инфраструктуры).
Запускается всё, что работает на стейджинге, компилируется ключом Race; и мы, соответственно, можем посмотреть на промежуточную инфраструктуру, чтобы увидеть, какие потенциальные проблемы у нас есть.
Минимализм и простота языка.
Ищем разработчиков! Если кто хочет, пожалуйста.
Вопросы
Вопрос из зала (далее – Б): – Мне кажется, вы упустили один важный момент, касающийся Fan-out. Правильно ли я понимаю, что когда вы отправляете ответ клиенту, вы блокируете его, если клиент не хочет читать? РС: - Нет, мы не блокируем.Во-первых, у нас все это стоит за nginx, то есть проблем с медленными клиентами нет. Во-вторых, у клиента есть канал с буфером — по факту мы можем поставить туда до сотни обновлений… Если мы не можем писать в канал, то он его удаляет. Если мы видим, что канал заблокирован, то просто закроем канал, и все — клиент переподключится, если возникнет какая-то проблема.
Поэтому блокировки здесь в принципе нет. В: — Нельзя ли было сразу отправить в Listen/Notify запись, а не таблицу идентификаторов? РС: – Listen/Notify имеет ограничение на отправляемую предварительную загрузку в 8 тысяч байт. В принципе, можно было бы отправлять, если бы мы имели дело с небольшим объемом данных, но мне кажется, что так [то, как мы это делаем] просто надежнее.
Ограничения находятся в самом Postgres. В: – Получают ли клиенты обновления о матчах, которые им не интересны? РС: - В целом да.
Как правило, параллельно проходит 2-3 матча, да и то довольно редко.
Если клиент что-то смотрит, то обычно он смотрит тот матч, который идет. Затем у клиента есть локальная база данных, в которую складываются все эти обновления, и даже без подключения к Интернету клиент может просмотреть все прошлые совпадения, по которым у него есть обновления.
По сути, мы синхронизируем нашу базу данных на сервере с локальной базой данных клиента, чтобы он мог работать офлайн.
В: – Почему вы сделали свой ORM? Алексей (один из разработчиков Look+): — В то время (это было год назад) ОРМ было меньше, чем сейчас, когда их довольно много.
Что мне больше всего нравится в большинстве ORM, так это то, что большинство из них работают на пустых интерфейсах.
То есть методы в этих ORM готовы брать на себя что угодно: структуру, указатель структуры, число, что-то совершенно неважное.
Наш ORM генерирует структуры на основе модели данных.
Сам.
И поэтому все методы конкретны, не используют рефлексию и т. д. Они принимают структуры и рассчитывают использовать те структуры, которые приходят. В: – Сколько человек приняло участие? РС: – На начальном этапе участвовали два человека.
Начали мы где-то в июне, а в августе основная часть была готова (первая версия).
В сентябре был релиз.
В: – Там, где вы описываете SSE, вы не используете таймаут. Почему это? РС: — Честно говоря, SSE — это все же протокол html5: насколько я понимаю, стандарт SSE предназначен для взаимодействия с браузерами.
У него есть дополнительные возможности, чтобы браузеры могли переподключаться (и так далее), но они нам не нужны, потому что у нас были клиенты, которые могли реализовать любую логику подключения и получения информации.
Мы делали не SSE, а что-то похожее на SSE. Это не сам протокол.
Не было необходимости.
Насколько я понимаю, клиенты реализовали механизм подключения практически с нуля.
Им было все равно.
В: – Какие дополнительные утилиты вы использовали? РС: — Для единства стиля мы наиболее активно использовали говет и голинт, а также гофмт. Больше ничего не использовалось.
В: – Что вы использовали для отладки? РС: – Отладка в основном проводилась с помощью тестов.
Мы не использовали ни отладчик, ни GOP. В: – Можете ли вы вернуть слайд, где реализована функция «Публикация»? Вас смущают однобуквенные имена переменных? РС: - Нет. У них достаточно «узкая» сфера видимости.
Кроме здесь они больше нигде не используются (кроме внутренностей этого класса), и он очень компактен — занимает всего 7 строк.
В: – Как-то всё равно не интуитивно.
РС: - Нет-нет, это настоящий код! Дело не в стиле.
Просто такой утилитарный, очень маленький класс - всего 3 поля внутри класса.
РС: — По большому счету все данные, которые синхронизируются с клиентами (матчи сезона, игроки), не меняются.
Грубо говоря, если мы сделаем еще один вид спорта, в котором нам нужно изменить матч, мы просто учтем все в новой версии клиента, а старые версии клиента будут забанены.
В: – Существуют ли сторонние пакеты управления зависимостями? РС: – Мы использовали go dep. В: – В теме репортажа что-то говорилось о видео, но в репортаже о видео ничего не сказано.
РС: — Нет, у меня в теме про видео ничего нет. Называется оно «Look+» — так называется приложение.
В: – Вы сказали, что это транслируется клиентам?.
РС: – Мы не занимались потоковым видео.
Это полностью сделал Мегафон.
Да, я не говорил, что приложение МегаФон.
РС: — Go — для отправки всех данных — о счете, о событиях матча, статистике.
Go — это весь бэкенд для приложения.
Клиент должен откуда-то знать, какую ссылку использовать для игрока, чтобы пользователь мог посмотреть матч.
У нас есть ссылки на подготовленные видео и стримы.
Немного рекламы :)
Спасибо, что остаетесь с нами.Вам нравятся наши статьи? Хотите увидеть больше интересных материалов? Поддержите нас, разместив заказ или порекомендовав друзьям, облачный VPS для разработчиков от $4,99 , уникальный аналог серверов начального уровня, который мы придумали для вас: Вся правда о VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps от 19$ или как правильно раздать сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40 ГБ DDR4).
Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только здесь 2 x Intel TetraDeca-Core Xeon, 2 x E5-2697v3, 2,6 ГГц, 14C, 64 ГБ DDR4, 4 твердотельных накопителя по 960 ГБ, 1 Гбит/с, 100 ТВ от 199 долларов США в Нидерландах! Dell R420 — 2x E5-2430, 2,2 ГГц, 6C, 128 ГБ DDR3, 2 твердотельных накопителя по 960 ГБ, 1 Гбит/с, 100 ТБ — от 99 долларов США! Прочтите об этом Как построить корпоративную инфраструктуру класса, используя серверы Dell R730xd E5-2650 v4 стоимостью 9000 евро за копейки? Теги: #it-инфраструктура #Конференции #Go #Анализ и проектирование систем #Смотри
-
Бесплатное Золотое Предложение ¨ C Mmoxe.de
19 Oct, 24 -
Google Хочет Дать Каждому Бесплатный Телефон
19 Oct, 24 -
Почему Банки Монополизируют Блокчейн?
19 Oct, 24 -
Видеообзор 3D Принтера Up! Мини 2
19 Oct, 24