Хабрахабр

Пишем свой протокол поверх UDP

Первые прямые трансляции с места событий появились в России почти 70 лет назад и вели их из передвижной телевизионной станции (ПТС), которая внешне походила на «троллейбус» и позволяла вести эфиры не из студии. А всего лишь три года назад Periscope позволил вместо «троллейбуса» использовать мобильный телефон.

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

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

Александр Тоболь отвечает за техническую часть видео в Одноклассниках и на Highload++ 2017 рассказал про то, как писать свой UDP протокол, и зачем это может потребоваться.

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

Говорят, что надо всегда начинать с архитектуры и ТЗ — якобы без этого нельзя! Так и сделаем.
Архитектура и ТЗ

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

Если у вас есть заказчик, у вас есть ТЗ. Дальше переходим к техническому заданию. Как его составить? Если вы — социальная сеть, ТЗ у вас нет.

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

Мы решили пойти методом от противного и посмотрели, что пользователи НЕ хотят видеть от сервиса трансляции.

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

Так выглядит обычный стриминговый сервис. Посмотрим, что можно сделать, чтобы вместо обычного сделать — крутой стриминговый сервис.

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

Что у конкурентов?

Мы начали с изучения сервисов конкурентов. Открываем Periscope — что у них?

Как всегда, главное — архитектура.

Если еще немножко почитать статьи, то мы увидим, что стрим они делают с использованием протокола RTMP, а раздают его либо в RTMP, либо в HLS. Сара Хайдер, ведущий инженер Periscope, пишет, что для бэкенда они используют Wowza. Посмотрим, что это за протоколы и как они работают.

Протестируем Periscope на три наших главных требования.

Скорость старта у них приемлемая (меньше секунды на хороших сетях), постоянноекачество порядка 600 px (не HD) и при этом задержки могут составлять до 12 секунд.

Кстати, как померить задержку в трансляции?

Есть мобильный телефон с таймером. Это фотография измерения задержки. За 15 миллисекунд изображение попало на сенсор камеры и вывелось из видеопамяти на экран телефона. Мы включаем трансляцию и видим изображение этого телефона на экране. После этого мы включаем браузер и смотрим трансляцию.

Она немножко отстала — примерно на 12 секунд. Ой!

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

Тут задержки минимальны (≈15 мс). Итак, есть мобильный телефон, видео идет с камеры и попадает в видеобуфер. Это все летит в сеть. Потом кодировщик кодирует сигнал, упаковывает в пакет и отправляет в socket-буфер. Дальше на принимающем устройстве происходит все то же самое.

В принципе, есть две основные трудные точки, которые нужно рассмотреть:

  • кодирование/декодирование видео;
  • сетевые протоколы.

Кодирование/декодирование видео

Немного расскажу про кодирование. Вы все равно с ним столкнетесь, если будете делать Low Latency Live Streaming.

Это набор кадров, но не совсем простых. Что такое видео? Кадры бывают трех типов: I, P и B-frame:

  • I-frame — это просто jpg. По сути, это опорный кадр, он ни от кого не зависит и содержит четкую картинку.
  • P-frame зависит исключительно от предыдущих кадров.
  • Хитрые B-frame могут зависеть от будущего. Это означает, что чтобы посчитать b-frame, нужно, чтобы с камеры пришли еще и будущие кадры. Только тогда с некоторой задержкой можно декодировать b-frame.

Отсюда видно, что B-кадры вредны. Попробуем их убрать.

  1. Если вы стримите с мобильного устройства, можно попробовать включить профайл baseline. Он отключит B-frame.
  2. Можно попробовать настроить кодек и уменьшить задержку на будущие кадры, чтобы кадры приходили быстрее.
  3. Еще одна важная штука в тюнинге кодека — это включение CBR (константного битрейта).

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

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

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

А на бэкенде необходимо добавить к H264 кодеку zerolatency — это позволит как раз не делать зависимости в кадрах на будущее.   4.

Протоколы передачи видео

Рассмотрим, какие протоколы стриминга предлагает индустрия. Я их условно разбил на два типа:

  1. потоковые протоколы;
  2. cегментные протоколы.

Они отличаются тем, что пользователи договариваются о том, какой у них канал, подбирают битрейт кодека соответственно каналу. Потоковые протоколы — это протоколы из мира p2p звонков: RTMP, webRTC, RTSP/RTP. Если вы потеряли кадр, в этих протоколах вы можете заново его запросить. А еще у них есть дополнительные команды такого рода, как «дай мне опорный кадр».

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

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

Потоковые протоколы

Periscope использует RTMP. Этот протокол появился в 2009 году, и Adobe сначала не полностью его специфицировал. Потом у него были определенного рода трудности с тем, что Adobe хотел продавать исключительно свой сервер. То есть RTMP развивался довольно трудно. Его основная проблема в том, что он использует TCP, но почему-то именно его выбрал Periscope.

Как раз такие трансляции, если у вас недостаточный канал, скорее всего, вы не сможете посмотреть. Если почитать детально, то оказывается, что Periscope использует RTMP для трансляции с малым количеством зрителей.

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

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

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

Здорово!

Но все равно RTMP у нас поверх TCP, и никто нас от блокировки начала очереди не застраховал.

На рисунке это проиллюстрировано: к нам поступают аудио и видео фреймы, RTMP их пакует, возможно их как-то перемешивает, и они улетают в сеть.

Возможно, что тот самый желтый потерянный пакет — это вообще P-frame от какого-то предыдущего — его можно было бы дропнуть. Но допустим, мы теряем один пакет. Но TCP нам не отдаст остальные пакеты, так как он гарантирует доставку и последовательность пакетов. Возможно, как минимум, можно было бы играть аудио. С этим надо как-то бороться.


Существует еще одна проблема использования протокола TCP в стриминге.

Мы генерируем туда из нашего кодека пакеты в высоком разрешении. Допустим, у нас есть буфер и высокая пропускная способность сети. — сеть стала работать хуже. Потом — оп! TCP отчаянно пытается пропихнуть HD-пакеты через наш 3G. На кодеке мы уже указали, что битрейт нужно понизить, но готовые пакеты уже в очереди и никаким образом изъять их оттуда нельзя.

У нас нет никакого управления буфером, нет приоритезации, поэтому TCP крайне не подходит для стриминга.

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

  • 1,1 Мбит/с трафика;
  • 0,1% packet loss;
  • 300 мс средний RTT.

А если посмотреть некоторые регионы и конкретных операторов, то у них среднедневной процент потери пакетов более 3%, а RTT от 600 мс — это нормально.

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

То есть наш пользователь не доутилизирует канал на 30% из-за неэффективности работы TCP протокола в сетях со случайной потерей пакетов.
Потеря даже 0,001% пакетов приводит к снижению пропускной способности на 30%.

В определенных регионах packet loss доходит до 1%, тогда у пользователя остается порядка 10% процентов пропускной способности.

Поэтому на TCP делать не будем.

Посмотрим, что есть еще в мире стриминга из UDP.

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

При всех непонятных ситуациях он просто дропает. Его основная проблема в том, что он пренебрегает потерями.

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

Ниже на слайде справа приведен набор расширений и RFC, которые пришлось реализовать в WebRTC для того, чтобы адаптировать этот протокол для звонков. RTP-стриминг — это базовый протокол передачи данных по UDP. Но это очень сложно. В принципе, можно попробовать сделать что-то подобное — набрать набор расширений к RTP и получить UDP стриминг.

Вторая проблема в том, что если кто-то из ваших клиентов не поддерживает какой-либо extension, то протокол не заработает.

Сегментные протоколы

Хорошим примером сегментного протокола видео является MPEG-Dash. Он состоит из manifest-файла, который вы выкладываете у себя на портале. Он содержит ссылки на файлы в разных качествах, в начале файла есть некоторый индекс, который говорит, в каком месте файла начинается какой сегмент.

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

Еще одним примером сегментного стриминга является HLS.

MPEG-Dash — решение от Google, оно хорошо работает в Android, а Apple-решение более старое, у него есть ряд определенных недостатков.

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

Этот протокол делали еще для спутника, размер его пакета 188 байт. Если взглянуть еще более детально, то внутри каждого сегмента находится MPEG2-TS. Упаковывать видео в такой размер очень неудобно, особенно потому, что вы все время его снабжаете небольшим хедером.

Поэтому, когда мы переписали iOS плеер на MPEG-Dash, мы даже увидели некоторый прирост производительности. На самом деле это трудно не только серверам, которые для того, чтобы обработать 40 Гб трафика должны собрать 26 млн пакетов, но это еще трудно и на клиенте.

В 2016 году они наконец-то анонсировали, что у них есть возможность запихнуть фрагмент от MPEG4 в HLS. Но Apple не стоит на месте. Тогда они обещали это добавить только для разработчиков, но вроде бы сейчас должна появиться поддержка на macOS и iOS.

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

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

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

Сложность vs задержка

Посмотрим на все имеющиеся протоколы и рассортируем их по двум параметрам:

  • latency, который они дают между трансляцией и смотрящим;
  • complexity (сложность).

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

Что мы хотим?

Мы хотим сделать UDP-протокол для стриминга от 1 к N с задержкой, сравнимой с p2p связью, с возможностью опционального шифрования пакетов в зависимости от того, приватная или публичная трансляция.

Какие есть еще варианты? Можно подождать, например, когда Google выпустит свой QUIC.

Google позиционирует Google QUIC, как замену TCP — некий TCP 2. Расскажу немного, что это такое. Его разрабатывают с 2013 года, сейчас спецификации у него нет, зато он полностью доступен в Google Chrome, и мне кажется, что они иногда включают его некоторым пользователям для того, чтобы посмотреть, как он работает. 0. В принципе, можно зайти в настройки, включить себе QUIC, зайти на любой Google сайт и получить этот ресурс по UDP.

Мы решили не ждать, пока они все специфицируют, и запилить свое решение.

Требования к протоколу:

  1. Многопоточность, то есть мы имеем несколько потоков — управляющий, видео, аудио.
  2. Опциональная гарантия доставки — управляющий поток имеет 100% гарантию, видео нам нужно меньше всего — мы там можем дропать фрейм, аудио нам все-таки бы хотелось.
  3. Приоритезация потоков — чтобы аудио уходило вперед, а управляющий вообще летел.
  4. Опциональное шифрование: или все данные, или только заголовки и критичные данные.

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

Если сортировать протоколы по такому принципу, то видно, что чем меньше время ожидания, тем хуже качество — довольно простой вывод.

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

Разработка

Давайте уже начнем писать UDP протокол, но сначала посмотрим на статистику.

Тут видно, что средний интернет чуть больше мегабита, packet loss около 1% — это нормально, и RTT в районе 600 мс — на 3G это просто средние величины. Это наша статистика по мобильным сетям.

Будем на это ориентироваться при написании протокола — поехали!

UDP-протокол

Открываем socket UDP, забираем данные, упаковываем, отправляем. Берем вторую пачку от кодека, еще отправляем. Вроде бы все здорово!

То есть packet loss уже будет 15%, что никуда не годится. Но мы получим такую картину: если мы начинаем беспорядочно слать UDP пакеты в socket, то по статистике к 21-му пакету вероятность того, что он дойдет, будет всего лишь 85%. Это нужно исправлять.

На рисунке проиллюстрирована жизнь без Pacer и жизнь с Pacer. Исправляется это стандартно.

Pacer — это такая штука, которая раздвигает пакеты во времени и контролирует их потерю; смотрит, какой сейчас packet loss, в зависимости от этого адаптируется под скорость канала.

Соответственно, надо с этим как-то работать. Как мы помним, для мобильных сетей 1-3% packet loss — это норма. Что делать, если мы теряем пакеты?

Retransmit

В TCP, как известно, есть алгоритм fast retransmit: мы отправляем один пакет, второй, если пакет потеряли, то через некоторое время (retransmit period) отправляем этот же пакет.

Никаких проблем, никакой избыточности, но есть минус — некоторый retransmit period. Какие здесь плюсы?

Логично, что это может быть время равное времени пинга. Кажется, что очень просто: через какое-то время нужно повторить пакет, если вы не получили на него подтверждение. Но ping — это величина не стабильная, и поэтому точно через средний RTT time определить, что потерян пакет, мы не можем.

Например, в примере выше, средняя величина равна 46 мс. Для того, чтобы это оценить можно, например, использовать такую величину, как jitter: мы считаем разницу между всеми нашими ping-пакетами. На нашем портале средний jitter — 50.

Есть некоторый RTT и некоторая величина, после которой мы можем действительно понять, что acknowledge не пришел и повторить отправку пакета. Посмотрим на распределение вероятности приходов пакетов ко времени. В принципе, есть RFC6298, который в TCP говорит, как это можно хитро посчитать.

На портале у нас jitter по ping примерно 15%. Мы это делаем через jitter. Понятно, что retransmit period должен быть, как минимум, на 20% больше, чем RTT.

С прошлого раза у нас был acknowledge на второй пакет. Еще один кейс с retransmit. После этого наступает retransmit period, и мы отправляем третий пакет еще раз. Мы отправляем третий пакет, который теряется, другие пакеты пока ходят. Он еще раз дропнулся, и мы еще раз отправляем его.

Если у нас, например, packet loss 5%, и мы отправляем 400 пакетов, то на 400 пакетов у нас 1 раз точно будет ситуация двойного packet-drop, то есть, когда мы через retransmit period отправили пакет, и он еще раз не дошел. Если у нас случается двойная потеря пакета, то на retransmit появляется новая проблема.

Можно начать отправлять пакет, например, если мы получили acknowledge от другого пакета. Эту ситуацию можно исправить, добавив некоторую избыточность. Считаем, что опережение — это редкая ситуация, можем начать отправку третьего пакет в момент, обозначенный speculative retransmit на слайде выше.

Можно еще пошаманить со спекулятивным retransmit, и все будет неплохо работать.

А что, если добавить Forward Error Correction? Но тут мы заговорили про избыточность. Если мы точно знаем, что в мобильных сетях все так печально, то давайте просто добавим еще один пакетик. Давайте просто все наши пакеты снабдим, например, XOR.

Нам не нужны никакие round trip, но у нас уже появилась избыточность. Здорово!

Давайте вместо XOR возьмем другое решение — например, есть код Reed-Solomon, Fountain codes и т.д. А что, если пропадет не один пакет, а сразу два? Идея такая: если есть K пакетов, можно добавить к ним N пакетов так, что любые N можно было потерять.

Вроде бы классно!

Хорошо, если у нас такая плохая сеть, что пропали просто все пакеты, то к нашему Forward Error Correction очень удобно добавляется negative acknowledgement.

NACK

Если мы потеряли столько пакетов, что наш parity protection (назовем его так) нас уже не спасает, запрашиваем этот пакет дополнительно.

Плюсы NACK:

  • Простой в реализации, правда можно потерять и сам negative acknowledgement, но это мелкая проблема.
  • Хорошо совместим с FEC.

Итого, есть два интересных решения:

  1. С одной стороны, FEC + NACK;
  2. С другой стороны, Fast retransmit.

Посмотрим, как распределены потери пакетов.

Причем есть интересные пики, например, на 11 пакетах, есть еще пики на 60-80 пакетах. Оказывается, что пакеты теряются не равномерно по одной штучке, а пачками (выше график распределений). Они повторяются, и мы изучаем, откуда они берутся.

В среднем на нашем портале теряется по 6 пакетов.

В таблице указано время, которое сеть была недоступна. Детальное рассмотрение по сетям показывает, что чем хуже сеть, тем больше это количество. Например, Wi-Fi недоступен 22 мс и теряет 5 пакетов, 3G может за 24 мс потерять 8 пакетов.

Вопрос: если мы знаем, что у нас 90% packet loss на портале укладывается в 10 пакетов, и при этом средний gap равен 25 мс, что будет работать лучше — FEC + NACK или Fast retransmit?

Но в 2015 они его отключили. Тут, наверное, надо рассказать, что Google, когда делал свой протокол QUIC в 2013 году, ставил Forward Error Correction во главу, думая, что он решит все проблемы.

Мы протестировали оба варианта и у нас не получилось завести FEC + NACK, но мы еще пытаемся и не отчаиваемся.

Рассмотрим, как он работает.

Это цифры, близкие к средней сети, проcто чтобы было удобно считать:

  • 1 Мб/с сеть;
  • 1% packet loss;
  • 300 мс RTT;
  • 1 000 байт — размер пересылаемых пакетов;
  • 1 000 пакетов в секунду уходит.

Мы хотим справляться с потерей сразу до 10 пакетов. Соответственно при packet loss в 1% нам нужно к 1 000 пакетов добавлять 10. Логично — почему нельзя к 100 пакетам добавлять 1 — потому что, если мы потеряли интервал хотя бы в 2 пакета, мы не восстановимся.

И тут на 500-м пакете, теряем ту самую пачку из 10 штук. Мы начинаем делать такие добавки, и вроде бы все здорово.

У нас есть варианты:

  • Дождаться оставшиеся 500 пакетов и восстановить данные через Forward Error Correction. Но на это у нас потратится примерно полсекунды, а пользователь эти данные ждет.
  • Можно воспользоваться NACK, причем это дешевле, чем дожидаться кодов коррекции.
  • А еще можно просто взять Fast Retransmit, не добавлять никаких кодов коррекции и получить тот же самый результат.

Поэтому Forward Error Correction действительно работает, но работает на очень узком диапазоне — когда gap небольшой и можно раз в 200-300 пакетов вставлять это избыточное кодирование.

Fast Retransmit

Это работает так: после того, как мы потеряли пачку в 10 пакетов, отправив пока другие пакеты, понимаем, что у нас retransmit period прошел, и отправляем эти пакеты заново.

Это означает, что к моменту, когда retransmit начнет обрабатывать пакеты, в большинстве случаев сеть уже восстановится и они уйдут. Самое интересное в том, что retransmit period на такой сети будет 350 мс, а средняя длительность этого packet gap — 25-30 мс, пусть даже 100.

У нас получилось, что эта штука работает лучше и быстрее.

Дополнительные опции

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

Они равномерно уходят в сеть. Есть буфер отправки, в нем лежит опорный кадр, к нему p/b-кадры. Тут они перестали уходить в сеть, а в очередь прилетели еще пакеты.

Вы понимаете, что на самом деле все пакеты, которые лежат в очереди, уже больше не интересны клиенту, потому что прошло, например, больше 0,5с и надо на клиенте просто склеить разрыв и жить дальше.

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

MTU

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

Он дробит пакет на большой и маленький (здесь 1100 и 400 байт) и отправляет. У нас есть сервер, он отправляет какие-то пакеты в сеть, они приходят к маршрутизатору и на его уровне MTU (maximum transmission unit) становится ниже, чем размер пакета, который пришел.

Но если мы теряем 1 пакет, мы дропаем все пакеты, плюс получаем дополнительные издержки на header’ы пакетов. В принципе, проблемы нет, это все соберется на клиенте и будет работать. Поэтому, если вы пишете свой протокол, идеально работать в размере MTU.

Как его посчитать?

На самом деле Google не заморачивается, ставит порядка 1200 байт в своем QUIC и не занимается его подбором, потому что IP фрагментация потом все пакетики соберет.

Мы делаем точно также — сначала ставим какой-то дефолтный размер и начинаем слать пакеты — пусть он их фрагментирует.

Если маршрутизатор встречает такой пакет и не может эти данные фрагментировать, то он дропнет пакет и возможно по ICMP вам отправит, что есть проблемы, но скорее всего, ICMP будет закрыт и этого не будет. Параллельно запускаем отдельный поток и создаем socket с флагом запрета фрагментации для всех пакетов. Если он не дошел, мы считаем, что MTU превышен и дальше его уменьшаем. Поэтому мы просто, например, три раза пытаемся отправить пакет определенного размера с каким-то интервалом.

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

Мы были удивлены, но в процессе переключения Wi-Fi и пр. На самом деле, он иногда меняется. Этот параллельный процесс лучше не останавливать и время от времени подправлять MTU. MTU меняется.

У нас на портале получилось около 1100 байт. Выше распределение MTU в мире.

Шифрование

Мы говорили, что мы хотим опционально управлять шифрованием. Делаем самый простой вариант — Diffie-Hellman на эллиптических кривых. Делаем его опционально — шифруем только управляющие пакеты и заголовки, чтобы man-in-the-middle не мог получить ключ трансляции, перехватить и так далее.

Если трансляция приватная, то можем добавить еще и шифрование всех данных.

Пакеты шифруем AES-256 независимо, чтобы packet drop никак не влиял на дальнейшее шифрование пакетов.

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

Помните, мы хотели от протокола еще приоритезацию.

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

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

Дополнительная плюшка по поводу UDP

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

На слайде как раз времена, когда не удалось достучаться до клиента с сервера по UDP.

Но выше видно, что если Keep-Alive или ping будет меньше 30 секунд, то с вероятностью 99% будет возможно достичь клиента. Многие скептики говорят, что маршрутизаторы устроены так, что NAT Unbinding вытесняет в первую очередь именно UDP маршруты.

Доступность UDP на мобильных устройствах в мире

В этом случае мы оставляем наш прекрасный протокол с приоритезацией, шифрованием и всем, только на TCP. Google говорит, что 6%, но у нас получилось, что 7% мобильных пользователей не могут пользоваться UDP.

Поэтому верить, что UDP на мобильных устройствах закроют, я бы не стал. На UDP сейчас работает VOIP по WebRTC, Google OUIC, и многие игры работают по UDP.

В итоге мы:

  • Снизили задержку между стримером и смотрящим до 1 с.
  • Избавились от накопительного эффекта в буферах, то есть трансляция не отстает.
  • Снизилось количество stall’ов у зрителей.
  • Смогли поддержать на мобильных устройствах FullHD стриминг.

  • Задержка в нашем мобильном приложении OK Live 25 мс — на 10 мс дольше, чем работает сканер камеры, но это не так страшно.
  • Трансляция на Web показывает задержку всего 690 мс — космос!

Что еще умеет стриминг на Одноклассниках

  • Принимает наш протокол OKMP с мобильных устройств;
  • может принимать RTMP и WebRTC;
  • выдает на выходе HLS, MPEG-Dash и т.д.

Если вы были внимательны, то заметили, что я сказал, что мы можем взять у пользователя, например, WebRTC и сконвертировать его в RTMP.

На самом деле WebRTC — протокол, ориентированный на дроп пакетов, и у него используется аудио кодек OPUS. Тут есть нюанс. В RTMP использовать OPUS нельзя.

Поэтому нам пришлось сделать еще некоторый фикс в FF MPEG, который позволяет запихнуть OPUS в RTMP, его сконвертировать в AAC и отдать пользователям уже в HLS или еще в чем-то. На серверах бэкенда мы везде используем RTMP.

Как это выглядит у нас внутри?

  • Пользователи по одному из протоколов загружают оригинал видео на наши upload-сервера.
  • Там мы разворачиваем протокол.
  • По RTMP отправляем на один из серверов трансформации видео.
  • Оригинал всегда сохраняем в распределенное хранилище, чтобы ничего не пропало.
  • После этого все видео поступают на сервер раздачи.

По железу у нас получилось следующее:


Расскажу еще немного про отказоустойчивость:

  • Upload-сервера распределены по разным дата-центрам, стоят за разными IP.
  • Пользователи приходят, по DNS получают IP.
  • Upload-сервер отправляет видео на серверы нарезки, те нарезают и отдают серверам раздачи.
  • Под более популярные трансляции мы начинаем добавлять большее количество серверов раздачи.
  • Все, что пришло от пользователя, сохраняем в хранилище, чтобы потом создать архив трансляций и ничего не потерять.
  • Хранилище отказоустойчивое, распределенное по трем дата-центрам.

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

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

Что при этом произойдет?

  • Пользователь на DNS возьмет следующий IP другого upload-сервера.
  • К этому времени ZooKeeper поймет, что сервер в том дата-центре умер, и выберет для другой сервер нарезки.
  • Download-серверы узнают, кто теперь отвечает за трансформацию этого стрима и будут это раздавать.

В принципе, все это произойдет с минимальными задержками.

Использование протокола в продукте

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

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

Например, мы проводили прямую линию с Президентом через OK Live и транслировали ее на всю страну. Теперь у нас есть механизм, который позволяет вести прямые трансляции. Пользователи смотрели и через встречный стрим могли попадать в эфир и задавать свои вопросы.

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

Помните тот «троллейбус», про который я рассказывал в начале статьи — он сейчас выглядит как 2 огромных грузовика. На самом деле мы уложились где-то в 2 секунды — секунда туда и секунда обратно. Для ТВ эфира снять с камеры и просто все смикшировать с задержкой в порядка 1-2 с совершенно нормально.

В действительности нам удалось у себя воспроизвести что-то сравнимое с текущими современными ПТС.

За последние полтора года на портале ОК они выросли в три раза (не без помощи приложения OK Live). Прямые эфиры — это текущий тренд.

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

Что получили мы:

  • Смогли гарантировать длительность задержки между стримером и зрителями.
  • Сделали первое мобильное FullHD приложение для стриминга на динамично меняющемся мобильном интернет-канале.
  • Получили возможность терять дата-центры и при этом не прерывать трансляции

Что узнали вы:

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

На Highload++ Siberia Александр Тоболь обещает рассказать про сервис звонков на ОК, будет интересно узнать, что из рассмотренного в этой статье удалось применить, а что пришлось реализовывать совершенно заново.

В этой же секции на узкоспециальные темы планируются доклады:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть