Далее текстовая версия доклада на HighLoad++ Siberia, из которой вы узнаете:
- как работают сервисы видеозвонков под капотом;
- как красиво пробить NAT — это будет интересно и специалистам из игровой сферы, которым необходимо peer-to-peer соединение;
- как устроен WebRTC, какие протоколы в него входят;
- как можно тюнить WebRTC через BigData.
О спикере: Александр Тоболь руководит разработкой платформ Видео и Ленты в ok.ru.
История видеозвонков
Первое устройство для видеозвонков появилось в 1960, оно называлось пикчерфоном, использовало выделенные сети и стоило крайне дорого. В 2006 Skype добавил в свое приложение видеозвонки. В 2010 году Flash поддержал протокол RTMFP, и мы в Одноклассниках запустили видеозвонки на Flash. В 2016 году Chrome прекратил поддержку Flash, и в августе 2017 мы перезапустили звонки на новой технологии, о чем я сегодня и расскажу. Доработав сервис, еще за полгода мы получили существенный прирост успешно совершенных звонков. Недавно у нас появились еще и маски в звонках.
Архитектура и ТЗ
Так как мы работаем в социальной сети, то технических заданий у нас нет, и что такое ТЗ мы не знаем. Обычно вся идея умещается на одну страничку и выглядит примерно так.
Пользователь хочет звонить другим пользователям, используя веб или iOS/Android-приложения. У другого пользователя может быть несколько устройств. Звонок приходит на все устройства, пользователь снимает трубку на одном из них, они разговаривают. Всё просто.
Технические характеристики
Для того, чтобы сделать качественный сервис звонков, нам нужно понять, какие характеристики мы хотим отслеживать. Мы решили начать с поиска того, что больше всего раздражает пользователя.
Пользователя точно раздражает, если он снимает трубку и вынужден ждать, пока установится соединение.
Пользователя раздражает, если качество звонка низкое — что-то прерывается, видео рассыпается, звук булькает.
Но больше всего пользователя раздражает задержка в звонках. Latency — это одна из важных характеристик звонков. При latency в разговоре порядка 5 секунд абсолютно невозможно вести диалог.
Мы определили для себя приемлемые характеристики:
- Старт — мы решили, что хорошо начинать звонок за секунду. Т.е. connecting после того, как пользователь ответил, должен занимать не более 1 секунды.
- Качество — очень субъективный показатель. Можно мерить, например, соотношение сигнал-шум (SNR), но есть еще пропадающие кадры и другие артефакты. Мы измеряли качество скорее субъективно и оценивали потом уже счастье пользователей.
- Latency должна быть меньше 0,5 секунды. Если Latency больше 0,5 секунды, то вы уже слышите задержки и начинаете перебивать друг друга.
Polycom — система конференц-связи, установленная у нас в офисах. Средние задержки polycom у нас порядка 1,3 секунды. При такой задержке не всегда друг друга понимаешь. Если задержка увеличится до 2 секунд, то вести будет диалог невозможно.
Так как у нас уже была запущена платформа, мы примерно ожидали, что у нас будет миллион звонков в день. Это тысяча звонков параллельно. Если все звонки пустить через сервер, будет тысяча звонков по мегабиту на звонок. Это всего 1 гигабит/сек одного железного сервера будет достаточно.
Интернет против TTX
Что может помешать добиться таких классных характеристик? Интернет!
В интернете существуют такие вещи, как round-trip time (RTT), который не побороть, есть переменная полоса пропускания, есть NAT.
Предварительно мы измерили скорость передачи в сетях наших пользователей.
Разбили по типу подключения, посмотрели среднее RTT, packet loss, скорость, и решили, что будем тестировать звонки на средних значениях каждой из этих сетей.
В интернете бывают и другие неприятности:
- Packet loss — мы намерили 0,6% random packet loss (congestion packet loss при избыточном количестве пакетов мы не учитываем).
- Reordering — вы отправляете пакеты в одном порядке, а сеть их пересортировывает.
- Jitter — отдаете видео или аудио поток с определенным интервалом, а на сторону клиента пакеты приходят склеенные пачками, например, за счет буферизации на сетевых устройствах.
- NAT — у нас получилось, что больше 97% пользователей находятся за NAT. Дальше поговорим, почему, что и как.
Рассмотрим перечисленные выше параметры сети на простом примере.
Я у себя из офиса попинговал сайт Новосибирского государственного университета и получил такой странный ping.
Средний jitter в этом примере равен 30 мс, то есть средний интервал между соседними временами ping — порядка 30 мс, а средний ping — 105 мс.
Что важно в звонках, почему будем бороться за p2p?
Очевидно, что, если между нашими пользователями, которые пытаются в Санкт-Петербурге поговорить друг с другом, удалось установить p2p соединение, а не через сервер, который находится в Новосибирске, мы сэкономим порядка 100 мс round-trip и трафик на этот сервис.
Поэтому большая часть статьи посвящена тому, как сделать хороший p2p.
История или legacy
Как я уже говорил, у нас был сервис звонков с 2010 года, а теперь мы его перезапустили.
В 2006 году, когда запускался Skype, Flash купил компанию Amicima, которая делала RTMFP. У Flash уже был протокол RTMP, который в принципе можно использовать для звонков, и он часто используется для стриминга. Позже Flash открыл спецификацию на RTMP. Интересно, зачем им нужен был RTMFP? В 2010 мы использовали именно RTMFP.
Сравним требования к протоколам звонков и протоколам реального стриминга и посмотрим, где эта граница.
Протокол RTMP — это скорее протокол видеостриминга. Он использует TCP, у него есть накапливающаяся задержка. Если у вас хорошее интернет-соединение, звонки на RTMP будут работать.
Протокол RTMFP, несмотря на отличие всего в одну букву, — это протокол UDP. Он лишен проблем буферизации — тех, которые есть на TCP; лишен head-of-line блокировок — это когда у вас пропал один пакет, и TCP не отдает следующие пакеты до тех пор, пора не отправит повторно потерянный. RTMFP умел справляться с NAT и переживал смену IP адреса клиентов. Поэтому мы запускали web на RTMFP в 2010 году.
Потом только в 2011 году появился initial draft WebRTC, еще не совсем работоспособный. В 2012 мы начали поддержку звонков на iOS/Android, потом происходило что-то еще, и в 2016 Chrome прекратил поддержку Flash. Нам пришлось что-то делать.
Мы посмотрели на все протоколы VoIP: как всегда, для того чтобы сделать что-то, мы начинаем с изучения конкурентов.
Конкуренты или с чего начать
Мы выбрали самых популярных конкурентов: Skype, WhatsApp, Google Duo (похож на Hangouts) и ICQ.
Для начала измерили задержку.
Сделать это просто. Выше фотография, на которой:
- Секундомер (см. телефон вверху слева), на котором видно время (03:08).
- Ближний телефон совершает звонок и в качестве видео снимает первый телефон. С того момента, как изображение попало в камеру телефона, и вы его увидели, прошло порядка 100 мс.
- Звонок на другой телефон (белый) и еще одно время. Здесь задержка порядка 310 мс у Google Duo.
Пока не буду раскрывать все карты, но мы делали так, чтобы эти устройства не могли установить p2p соединения. Конечно, измерения проводились в разных сетях, и это просто пример.
Skype все-таки немножко перебивает. У нас получилось, что у Skype, в случае если ему не удастся подключить p2p, задержка составляет 1,1 с.
Тестовая среда у нас была сложная. Мы тестировали в разных условиях (EDGE, 3G, LTE, WiFi), учитывали, что каналы ассиметричные, и я привожу усредненные значения всех измерений.
Для того чтобы оценить расход батареи, нагрузку на процессор и все остальное, мы решили, что можно просто померить температуру телефона пирометром и считать, что это некоторая средняя нагрузка на GPU телефона на процессор, на аккумулятор. В принципе, очень неприятно подносить к уху горячий телефон, да и держать в руках тоже. Пользователю кажется, что сейчас приложение израсходует всю его батарею.
В итоге получилось:
- Самыми медленными по задержке оказались ICQ и Skype, а самым быстрым — Telegram. Это не совсем корректное сравнения, так как у Telegram нет видеозвонков, но зато по аудио у них минимальная latency. Классно работает WhatsApp (порядка 200 мс) и Hangouts — 390 мс.
- По температуре меньше всех ест Telegram без видео, а больше всех Skype.
- С точки зрения времени ответа дольше всех соединение устанавливает Telegram, а самый быстрый WhatsApp и Google Duo.
Отлично, у нас появились какие-то метрики!
Мы протестировали качество видео и голоса в разных сетях, с разными дропами и всем остальным. В результате пришли к выводу, что самое качественное видео на Google Duo, а голос — на Skype, но это в «плохих» сетях, когда уже есть искажения. В целом все работают примерно средненько. У WhatsApp самая замыленная картинка.
Посмотрим, на чем это все реализовано.
У Skype свой проприетарный протокол, а все остальные используют или модификацию WebRTC, или вообще напрямую WebRTC. Hangouts, Google Duo, WhatsApp, Facebook Messenger могут работать с вебом, и у них у всех под капотом WebRTC. Они все такие разные, с разными характеристиками, и у них всех один WebRTC! Значит, нужно уметь его правильно готовить. Плюс есть Telegram, у которого за аудио часть отвечают некоторые части WebRTC, есть ICQ, которая форкнула WebRTC очень давно и пошла развиваться своим путем.
WebRTC. Архитектура
WebRTC подразумевает наличие signaling-сервера, посредника между клиентами, который используется для обмена сообщениями в процессе установки p2p-соединения между ними. После установки прямого соединения клиенты начинают обмениваться медиаданными друг с другом.
WebRTC. Demo
Начнем с простого демо. Есть простые 5 шагов, как установить WebRTC соединение.
1. // Step #1: Getting local video stream and initializing a peer connection with it (both caller and callee)
2.
3. var localStream = null;
4. var localVideo = document.getElementById('localVideo');
5.
6. navigator
7. .mediaDevices
8. .getUserMedia({ audio: true, video: true })
9. .then(stream => {
10. localVideo.srcObject = stream;
11. localStream = stream;
12. });
13.
14. var pc = new RTCPeerConnection({ iceServers: [...] });
15.
16. localStream
17. .getTracks()
18. .forEach(track => pc.addTrack(track, localStream));
19.
20. // Step #2: Creating SDP offer (caller)
21.
22. pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true })
23. .then(offer => signaling.send('offer', offer));
24.
25. // Step #3: Handling SDP offer and sending SDP answer (callee)
26.
27. signaling.on('offer', offer => {
28. pc.setRemoteDescription(offer)
29. .then(() => pc.createAnswer())
30. .then(answer => signaling.send('answer', answer))
31. });
32.
33. // Step #4: Handling SDP answer (calleer)
34.
35. signaling.on('answer', answer => pc.setRemoteDescription(answer));
36.
37. // Step #5: Exchanging ICE candidates
38.
39. pc.onicecandidate = event => signaling.send('candidate', event.candidate);
40.
41. signaling.on('candidate', candidate => pc.addIceCandidate(candidate));
42.
43. // Step #6: Getting remote video stream (both caller and callee)
44.
45. var remoteVideo = document.getElementById('remoteVideo');
46.
47. pc.onaddstream = event => remoteVideo.srcObject = event.streams[0];
В нем написано следующее:
- Взять видео и установить peer connection, передать какие-то iceServers (сразу непонятно, что это).
- Создать SDP offer и отправить его в signaling, и signaling WebRTC за вас никак не имплементирует.
- Потом надо сделать обертку для приходящего из signaling, и это тоже не входит в WebRTC.
- Дальше поменяться какими-то candidates.
- Наконец получить удаленный видеопоток.
Давайте все же разберемся, что там происходит и что нам нужно реализовывать сами.
Смотрим картинку снизу вверх. Есть библиотека WebRTC, которая уже встроена в браузер, поддерживается Chrome, Firefox и др. Вы можете собрать её же под Android/iOS и общаться с ней через API и SDP (Session Description Protocol), в котором описывается сама сессия. Ниже расскажу, что в него входит. Для использования этой библиотеки в своем приложении вы должны установить соединение между абонентами через signaling. Signaling — это тоже ваш сервис, который придется написать самим, WebRTC его не предоставляет.
Далее в статье мы по порядку обсудим сеть, потом видео/аудио, и в конце напишем свой signaling.
WebRTC network или p2p (на самом деле c2s2c)
Кажется, что установить p2p-соединение довольно просто.
У нас есть Алиса и Боб, которые хотят установить p2p соединение. Они берут свои IP адреса, у них есть signaling-сервер, к которому они оба подключены, и через которые могут обменяться этими адресами. Они обмениваются адресами, и ой! Адреса у них одинаковые, что-то пошло не так!
На самом деле оба пользователя сидят, скорее всего, за Wi-Fi роутерами и это их локальные серые IP-адреса. Роутер обеспечивает им такую функцию, как Network Address Translation (NAT). Как она работает?
У вас есть серая подсеть и внешний IP-адрес. Вы отправляете пакет в интернет со своего серого адреса, NAT подменяет ваш серый адрес на белый и запоминает маппинг: то, с какого порта он отправил, какому пользователю и какому порту соответствует. Когда приходит обратный пакет, он по этому маппингу резолвит и отправляет его отправителю. Все просто.
Ниже иллюстрация, как это выглядит у меня дома.
Это мой внутренний IP-адрес и адрес роутера (кстати, тоже серый). Если провести трассировку и посмотреть маршрут, то мы увидим мой Wi-Fi-роутер: пачку серых адресов провайдера и внешний белый IP. Таким образом, на самом деле у меня будет два NAT: один, на котором я на Wi-Fi, и другой еще у провайдера, если я, конечно, не купил себе выделенный внешний IP-адрес.
NAT так популярен, потому что:
- до сих пор у многих IPv4, и адресов не хватает;
- NAT вроде как защищает сеть;
- это стандартная функция роутера: подключаетесь к Wi-Fi, там сразу есть NAT, он работает.
Поэтому только 3% пользователей сидят с внешним IP, а все остальные проходят через NAT.
NAT позволяет совершенно спокойно ходить на любые белые адреса. Но если вы никуда не ходили, то к вам никто не может прийти.
Для установки p2p соединения это проблема. На самом деле Алиса и Боб не могут отправить друг другу пакеты, если они оба находятся за NAT.
В WebRTC для решения этой проблемы есть протокол STUN. Предлагается развернуть STUN-сервер. Тогда Алиса подключается к STUN-серверу, получает свой IP-адрес, через signaling отправляет его Бобу. Боб тоже получает свой IP-адрес и отправляет Алисе. Они отправляют навстречу друг другу пакеты и таким образом пробивают NAT.
Вопрос: у Алисы открыт определенный порт, уже пробит NAT/Firewall на этот порт, и у Боба открыт. Они знают адреса друг друга. Алиса пытается отправить пакет Бобу, он отправляет пакет Алисе. Как вы думаете, они смогут поговорить или нет?
На самом деле, вы правы в любом случае, результат зависит от типа той пары NAT, которая есть у пользователей.
Network Address Translation
Существует 4 типа NAT:
- Full cone NAT;
- Restricted cone NAT;
- Port restricted cone NAT;
- Symmetric NAT.
В базовой версии Алиса отправляет пакет на сервер STUN, у нее открывается какой-то порт. Боб как-то узнает об ее порте и отправляет обратный пакет. Если это Full cone NAT — самый простой, который просто маппит внешний порт на внутренний, то Боб сразу же сможет отправить Алисе пакет, установить соединение, и они поговорят.
Ниже схема взаимодействия: Алиса с какого-то порта отправляет на порт STUN пакет, STUN ей отвечает ее внешним адресом. STUN может ответить с любого адреса, если это Full cone NAT, он все равно пробьет NAT, и Боб может ответить на тот же адрес.
В случае Restricted cone NAT все чуть-чуть сложнее. Он запоминает не просто порт, с которого нужно маппить на внутренний адрес, а еще и внешний адрес, на который вы сходили. То есть если вы установили соединение только к IP STUN-сервера, то никто другой из сети не сможет вам ответить, и тогда пакет Боба не дойдет.
Как решается эта задача? В простой схеме (см. иллюстрацию ниже) так: Алиса отправляет пакет на STUN, он ей отвечает ее IP. STUN может отвечать ей с любого порта, пока это Restricted cone NAT. Боб не может ответить Алисе, потому что у него другой адрес. Алиса ему отвечает пакетом, зная IP адрес Боба. У нее открывается NAT до Боба, Боб ей отвечает. Ура, они поговорили.
Чуть-чуть более сложный вариант — Port restricted cone NAT. Все то же самое, только STUN должен отвечать ровно с того порта, на который к нему обращались. Тоже все будет работать.
Самая вредная штука — это Symmetric NAT.
Вначале работает все точно так же — Алиса отправляет пакет STUN-серверу, он отвечает с того же порта. Боб не может ответить Алисе, но она отправляет пакет Бобу. И тут, несмотря на то что Алиса отправляет пакет на порт 4444, маппинг ей выделяет новый порт. Symmetric NAT отличается тем, что при установке каждого нового соединения, он каждый раз на маршрутизаторе выдает новый порт. Соответственно, Боб бьется в тот порт, с которого Алиса ходила на STUN, и они никак не могут соединиться.
В обратную сторону, если Боб с открытым IP-адресом, Алиса может к нему просто прийти, и они установят соединение.
Все варианты собраны в одной таблице ниже.
В ней видно, что почти все возможно кроме случаев, когда мы пытаемся установить соединения через Symmetric NAT с Port restricted cone NAT или Symmetric NAT на другом конце.
Как мы выяснили, p2p бесценно для нас в плане задержек (latency), но если установить его не удалось, то WebRTC предлагает нам TURN-сервер. Когда мы поняли, что p2p не установится, мы можем просто подключиться к TURN, который будет проксировать весь трафик. Правда при этом вы будете платить за трафик, а у пользователей, возможно, появится некоторые дополнительные задержки.
Практика
Бесплатные STUN-серверы есть у Google. Можно их поставить в библиотеку, будет работать.
У TURN-серверов есть credential (логин и пароль). Скорее всего, вам придется поднять свой, довольно трудно найти бесплатный.
Примеры бесплатных STUN-серверов от Google:
- stun:stun.l.google.com:19302
- stun:stun1.l.google.com:19302
- stun:stun2.l.google.com:19302
- stun:stun3.l.google.com:19302
И бесплатный TURN-сервер с паролями: url: ’turn:192.158.29.39:3478?transport=udp’, credential: ’JZEOEt2V3Qb0y27GRntt2u2PAYA=’, username: ’28224511:1379330808?.
Мы используем coturn.
В результате через p2p соединение у нас проходит 34% траффика, все остальное проксируется через TURN-сервер.
Что еще интересного есть в STUN-протоколе?
STUN позволяет определить тип NAT.
Ссылка на слайде
При отправке пакета можно указать, что вы хотите получить ответ с такого же порта или попросить STUN ответить с другого порта, с другого IP, или вообще с другого IP и порта. Таким образом за 4 запроса к STUN-серверу можно определить тип NAT.
Мы посчитали типы NAT и получили, что почти у всех пользователей или Symmetric NAT, или Port restricted cone NAT. Отсюда и получается, что только треть пользователей могут установить p2p соединение.
Вы можете спросить, зачем я все это рассказываю, если можно было просто взять STUN у Google, засунуть в WebRTC, и вроде как все заработает.
Потому что на самом деле можно определить тип NAT самим.
Это ссылка на Java-приложение, которое ничего хитрого не делает: просто пингует разные порты и разные STUN-серверы, и смотрит, какой порт видит в итоге. Если у вас открытый Full cone NAT, то в ответах STUN сервер будет один и тот же порт. При Restricted cone NAT у вас на каждый запрос к STUN будут приходить разные порты.
При Symmetric NAT у меня в офисе получается вот так. Там абсолютно разные порты.
Но иногда встречается интересная закономерность, что на каждое подключение номер порта увеличивается на единицу.
То есть у многих NAT настроено так, что они увеличивают или уменьшают порт на константу. Эту константу можно найти и таким образом пробить Symmetric NAT.
Таким образом пробиваем NAT — ходим на один STUN-сервер, на другой, смотрим разницу, сравниваем и пробуем еще раз отдать свой порт уже с этим инкрементом или декрементом. То есть Алиса пытается отдать Бобу свой порт, уже скорректированный на константу, зная, что в следующий раз он будет именно такой.
Так нам удалось наварить еще 12% peer-to-peer.
На самом деле, иногда внешние роутеры с одним и тем же IP ведут себя одинаково. Поэтому если пособирать статистику и если Symmetric NAT — это фича провайдера, а не фича Wi-Fi-роутера пользователя, то дельту можно предсказать, сразу же отправить ее пользователю, чтобы он ей воспользовался и не тратил лишнее время на ее определение.
CDN Relay или что делать, если не удалось установить р2р-соединение
Если мы все же используем TURN-сервер и работаем не в p2p, а в real mode, передавая весь трафик через сервер, можно еще добавить CDN. Если, конечно, у вас есть площадка. У нас есть свои CDN-площадки, поэтому для нас это было довольно просто. Но надо было определить, куда лучше отправлять человека: на CDN-площадку или, допустим, на канал до Москвы. Это не очень тривиальная задача, поэтому мы делали так:
- Случайно выдавали некоторым пользователям московские площадки, некоторым — удаленные.
- Собирали статистику по IP пользователя, по серверам и по характеристикам сети.
- По maxMind сгруппировали подсети, посмотрели статистику и смогли по IP понять, какой для пользователя ближайший TURN-сервер для соединения.
В Новосибирске есть CDN. Если у вас все работает через Москву, то 99 перцентиль RTT — 1,3 секунды. Через CDN все работает сильно быстрее (0,4 секунды).
Всегда ли лучше использовать соединение p2p и не использовать сервер? Интересный пример — это два красноярских провайдера Optibyte и Mobra (возможно, имена изменены). Между ними почему-то связь на p2p сильно хуже, чем через MSK. Наверное, они друг с другом не дружат.
Мы проанализировали все такие случаи, случайным образом отправляя пользователей на p2p или через MSK, собрали статистику и построили предсказания. Мы знаем, что статистику нужно обновлять, поэтому некоторым пользователям мы специально устанавливаем разные соединения, чтобы проверить, не изменилось ли что-то в сетях.
Мы померили такие простые характеристики, как round time, packet loss, bandwidth — осталось научиться их правильно сравнивать.
Как понять, что лучше: 2 Mbit/s интернета, 400 мс RTT и 5% packet Loss или 100 Kbit/s, 100 мс задержка и мизерный packet loss?
Точного ответа нет, оценка качества видеозвонка очень субъективна. Поэтому мы после окончания звонка просили пользователей оценить качество в звездочках и по результатам настраивали константы. Получилось, что, например, RTT меньше 300 мс — это уже неважно, дальше важен bitrate.
Выше средние оценки пользователей на Android и iOS. Видно, что пользователи iOS чаще ставят единицу и чаще пятерку. Не знаю почему, наверное, специфика платформы. Но по ним мы подтюнили константы, чтобы у нас было, как нам кажется, хорошо.
Вернемся к нашему плану статьи, мы все еще обсуждаем сеть.
Как выглядит установка соединения?
Передаем в PeerConnection() STUN и TURN-серверы, устанавливается соединение. Алиса узнает свой IP, отправляет его в signaling; Боб узнает об IP Алисы. Алиса получает IP Боба. Они обмениваются пакетами, возможно, пробивают NAT, возможно, устанавливают TURN и общаются.
В 5 шагах установки соединения, которые мы обсуждали ранее, мы разобрались с серверами, поняли, где их взять, и что ICE candidates — это внешние IP-адреса, которыми мы обмениваемся через signaling. Внутренние IP-адреса клиентов, если они находятся в зоне действия одного Wi-Fi, тоже можно пробовать пробить.
Перейдем к части видео.
Видео и аудио
WebRTC поддерживает некоторый набор кодеков видео и аудио, но можно добавить туда свой кодек. Базово поддерживается H.264 и VP8 для видео. VP8 — софтверный кодек, поэтому сильно расходует батарею. H.264 есть не на всех устройствах (обычно он нативный), поэтому приоритет по умолчанию стоит на VP8.
Внутри SDP (Session Description Protocol) существует codec negotiation: когда один клиент отправляет список своих кодеков, другой — своих с приоритетом, и они договариваются, какие кодеки будут использовать для общения. При желании можно поменять приоритет кодеков VP8 и H.264, и за счет этого можно сэкономить батарею на некоторых устройствах, где 264 нативный. Вот пример того, как это можно сделать. Мы так сделали, нам показалось, что пользователи не стали жаловаться на качество, но при этом заряд батареи расходуется сильно меньше.
Для аудио в WebRTC есть OPUS или G711, обычно у всех OPUS всегда работает, ничего с ним делать не надо.
Ниже замеры температуры после 10 минут использования.
Понятно, что мы тестировали разные устройства. Это пример айфона, и на нем приложение OK меньше всех тратит батарею, потому что температура устройства меньше всего.
Вторая штука, которую можно включить, если вы пользуетесь WebRTC — это автоматическое отключение видео при очень плохом коннекте.
Если у вас меньше 40 Кбит/с, видео выключится. Надо просто выставить флажок при создании соединения, пороговое значение можно настроить через интерфейс. Также можно установить минимальный и максимальный стартовый текущий битрейт.
Это очень полезная штука. Если при установке соединения вы заранее знаете, какой битрейт вы ожидаете, можно его передать, звонок начнется с него, и не понадобится адаптация битрейта. Плюс, если вы знаете, что у вас на канале часто бывают packet loss или просадки bandwidth, то максимальное значение тоже можно ограничить.
WhatsApp работает с очень мыльным видео, зато с маленькими задержками, так как агрессивно сверху поджимает битрейт.
Мы собрали статистику и с помощью MaxMind и нанесли на карту.
Это примерное стартовое качество, которое мы используем для звонков в разных регионах России.
Signaling
Эту часть вам, скорее всего, придется написать, если вы захотите сделать звонки. Тут существует много всяких подводных камней. Вспомним, как это выглядит.
Есть приложение с signaling, которое коннектится и обменивается с SDP, и SDP внизу являются интерфейсом к WebRTC.
Так выглядит простой signaling:
Алиса звонит Бобу. Она подключается, например, по web-socket’ному соединению. Боб получает пуш на свой мобильный телефон или в браузер, или в какое-то открытое соединение, подключается по web-socket’у и после этого у него начинает звонить в кармане телефон. Боб снимает трубку, Алиса ему отправляет свои кодеки и другие особенности WebRTC, которые она поддерживает. Боб ей отвечает тем же самым, и после этого они обмениваются candidates, которых они увидели. Ура, звонок!
Все это выглядит довольно долго. Первое, пока вы установите web-socket’ное соединение, пока придет пуш и все остальное, у Боба в кармане не будет звонить телефон. Алиса будет все время ждать, думать, где же Боб, почему он не берет трубку. После подтверждения это все занимает секунды, и даже на хороших соединениях это может быть 3-5 секунд, а на плохих — все 10.
Надо с этим что-то делать! Вы мне скажете, что можно вообще все сделать очень просто.
Если уже есть открытое подключение вашего приложения, можно сразу отправить пуш на установку соединения, подключиться к нужному серверу signaling и сразу начать звонить.
Потом еще одна оптимизация. Даже если телефон все еще звонит в кармане, и вы не сняли трубку, можно, на самом деле, поменяться информацией о поддерживаемых кодеках, внешними IP адресами, начать слать пустые видео пакеты, и вообще у вас все будет прогрето. Как только вы снимите трубку, все будет здорово.
Мы так сделали, и казалось, что все классно. Но нет.
Первая проблема — пользователи часто отменяют вызов. Они нажимают «Позвонить» и тут же делают отмену. Соответственно, пуш уходит на звонок, а пользователь пропадает (у него пропадает интернет или еще что-то). Тем временем у кого-то звонит телефон, он снимает трубку, и его там не ждут. Поэтому наша примитивная оптимизация с тем, чтобы как можно быстрее начать звонить, на самом деле не работает.
При быстрой отмене вызова есть вторая вредная штука. Если вы генерируете ID вашего conversation на сервере, то вам нужно дождаться ответа. То есть вы создаете звонок, получаете ID, и только после этого вы можете делать все, что хотите: отправлять пакеты, обмениваться, в том числе, отменить вызов. Это очень плохая история, потому что получается, что пока у вас response не пришел, вы по факту не можете ничего отменить с клиента. Поэтому лучше всего генерировать какой-то ID на клиенте типа GUID и говорить, что вы начали звонок. Люди еще часто делают так: позвонил, отменил и сразу же позвонил еще раз. Чтобы это все не перепуталось, делайте GUID и отправляйте его.
Вроде бы все ничего, но есть еще одна проблема. Если у Боба два телефона, или еще где-то браузер остался открытым, то вся наша магическая схема с тем, чтобы обменяться пакетами, установить соединение, не работает, если он вдруг ответил с другого устройства.
Что же делать? Вернемся к нашей базовой простой медленной схеме signaling и оптимизируем ее, отправим пуш чуть-чуть пораньше. Пользователь начнет побыстрее коннектиться, но это сэкономит какие-то копейки.
Что же делать с самой долгой частью после того, как он снял трубку и начал обмен?
Можно поступить следующим образом. Понятное дело, что Алиса знает уже все свои кодеки и может их выслать на оба телефона Боба. Она может порезолвить все свои IP адреса и тоже их отправить на signaling, который их придержит у себя в очереди, но не будет отправлять никому из клиентов, чтобы они начали устанавливать с ней соединение раньше времени.
Что может Боб? Получив offer, он может посмотреть, какие там были кодеки, сам сгенерировать свои, написать, что у него есть, и тоже отправить. Но у Боба два телефона, и там разные codec negotiation, поэтому signaling это все придержит на себе и будет держать в очереди до тех пор, пока не узнает, на каком устройстве снимут трубку. Candidates своих тоже сгенерируют оба устройства и отправят на signaling.
Таким образом получается, что у signaling есть одна очередь сообщений от Алисы и несколько очередей сообщений от Боба на разных устройствах. Он это все хранит, и как только на одном из этих устройств снимают трубку, просто перекидывает весь набор уже подготовленных пакетов.
Это работает довольно быстро. У нас получилось таким алгоритмом выйти на характеристики схожие с Google Duo и WhatsApp.
Наверное, можно придумать что-то еще лучше. Например, несколько очередей держать не на signaling, а отправить их на клиент, и потом сказать, какой номер, но, скорее всего, выигрыш будет очень небольшим. Мы на этом решили остановиться.
Какие еще проблемы вас подстерегают?
Бывает такая штука как встречный звонок: один звонит другому, другой звонит в ответ. Классно было бы, если бы они не пытались конкурировать, — на уровне signaling добавить команду, которая скажет, что если кто-то пришел вторым, то надо переключиться в режим, когда ты просто принимаешь вызов, и сразу же снять трубку.
Бывает так, что сеть пропадает, сообщения теряются, поэтому все нужно делать через очереди. То есть у вас должна быть на клиенте очередь отправки. Сообщения, которые вы отправляете с клиента, должны удаляться из очереди исключительно после того, как сервер подтвердил, что он их обработал. У сервера также есть очередь на отправку, и тоже с подтверждением.
Так это все реализовано у нас внутри с учетом того, что у нас сервис 24/7, мы хотим иметь возможность терять дата-центры, перекладывать и обновлять версию нашего ПО.
Ссылка на слайде на видео и ссылка на текстовую версию
Клиенты коннектятся по web-socket на какой-то load balancer, он отправляет на signaling-серверы в разных дата-центрах, разные клиенты могут прийти на разные серверы. На Zookeeper мы делаем Leader Election, который определяет тот сервер signaling, который сейчас управляет этим conversation. Если сервер не является лидером этого conversation, он просто все сообщения прокидывает другому.
Дальше мы используем некоторое распределенное хранилище, у нас это NewSQL поверх Cassandra. На самом деле не важно, что использовать. Можно куда угодно сохранять состояние всех очередей, которые есть на signaling, для того чтобы, если пропадет сервер signaling, выключится электричество или еще что-то случится, сработал Leader Election на Zookeeper, поднялся другой сервер, который станет лидером, из базы восстановит все очереди сообщений и начнет отправлять.
Алгоритм выглядит так:
- Клиент отправляет какое-то сообщение, допустим, свой внешний IP на signaling
- Signaling принимает, записывает в базу.
- После того, как понимает, что все пришло, отвечает, что он это сообщение принял.
- Клиент удаляет это сообщение из своей очереди.
Все пакеты снабжаются уникальными номерами, чтобы не путаться.
С точки зрения базы данных у нас используется надстройка над Cassandra, которая позволяет делать на ней транзакции (видео как раз про это).
Итак, вы узнали:
- что такое iceServers и как их передать;
- что есть Session Description Protocol;
- что его необходимо сгенерировать и отправить на другую сторону;
- что его нужно принять из signaling и передать в WebRTC на другой стороне, обменяться внешними IP адресами;
- и начать отправлять видео!
Мы получили:
- звонки с delay ниже среднего по рынку;
- мы не сильно нагрели телефоны;
- время ответа в нашем приложении на топовом уровне.
Здорово!
Security. Man in the middle attack for WebRTC
Поговорим про man in the middle attack for WebRTC. На самом деле, WebRTC очень трудный протокол в плане того, что он базируется на RTP, который еще 1996 года, а SDP пришел в 1998 из SIP.
Внизу огромный список — это куча RFC и прочих расширений к RTP, которые делают из RTP WebRTC.
Первые в списке два интересных RFC — один из них добавляет в пакеты audio level, а другой говорит, что небезопасно передавать открыто audio level в пакетах, и их шифрует. Соответственно, когда вы обмениваетесь SDP, вам важно знать, какой набор расширений поддерживают клиенты. Там есть даже несколько алгоритмов congestion, несколько алгоритмов восстановления потерянных пакетов и всего-всего.
История WebRTC была сложная. В 2011 году вышел первый драфтовый релиз, в 2013 этот протокол поддержал Firefox, потом он стал собираться на iOS/Android, в 2014 Opera. В общем, много-много лет он развивался, но до сих пор не решает одну интересную задачу.
Когда Алиса и Боб подключаются к signaling, после этого они используют этот канал, устанавливают DTLS Handshake и безопасное соединение. Все здорово, но если это оказался не наш signaling, то в принципе у человека посередине есть возможность «похэншейкаться» и с Алисой, и с Бобом, запроксировать весь трафик и подслушать то, что там происходит.
Если у вас сервис с высоким доверием, то, конечно, обязательно нужно использовать HTTPS, WSS и т.д. Есть еще интересное решение — ZRTP, его использует, например, Telegram.
Многие видели в Telegram эмоджи, когда устанавливается соединение, но мало кто ими пользуется. На самом деле если вы другу скажете, какие у вас эмоджи, он проверит, что у него точно такие, то у вас абсолютно гарантированно безопасное p2p соединение.
Как это работает?
Внутри всех этих протоколов изначально используется обычный алгоритм Диффи — Хеллмана. Алиса генерирует некоторые числа, отправляет Бобу их все, кроме одного. Боб тоже генерирует у себя случайное число и отправляет Алисе. В результате этого обмена у Алисы и у Боба получается некоторое большое число K, про которое человек посередине, который прослушал весь их канал, ничего не знает и не может никак догадаться.
Когда между Алисой и Бобом появляется Дейв, они с ним обмениваются этими же ключами, и у них получаются K1 и K2 соответственно. Отследить наличие этого человека посередине нет возможности. Тогда применяется такой трюк. Эти ключи K1 и K2 у Дейва точно будут разные, так как Алиса и Боб генерируют свои ключи случайно. Мы просто берем некоторый хэш от K1 и K2 и отображаем его в эмоджи: в яблоке, в груше — во всем, чем угодно — и люди голосом просто называют те картинки, которые видят. Так как по голосу можно друг друга идентифицировать, и если эти картинки разные, то кто-то между вами есть и, возможно, он вас слушает.
Результаты
- Мы «намайнили» NAT type и пробили symmetric NAT.
- Статистически оценили, что лучше: р2р или relay, качество, CDN; и повысили качество в звездочках с точки зрения пользователей.
- Поменяли приоритеты кодекам, сэкономили немножко батарею.
- Минимизировали задержку на signaling.
На графике видно, что сначала были старые звонки на RTMFP, потом, когда мы перешли на WebRTC, есть небольшой провал, а потом пик вверх. Не все сразу получилось! В итоге сейчас у нас количество состоявшихся звонков выросло в 4 раза.
Простая инструкция
Если вам все это не надо, есть очень простая инструкция:
- скачать с WebRTC код (https://webrtc.org/native-code/development/ ), собрать его под iOS/Android, во всех браузерах он уже есть;
- развернуть у себя coturn (https://github.com/coturn/coturn);
- написать signaling.
Всё будет звонить, и звонить довольно неплохо.
Уже через неделю Александр Тоболь выступит на HighLoad++ с докладом об архитектуре масштабируемой отказоустойчивой платформы 4К-видеостриминга.
Какие еще темы будут обсуждаться, смотрите в расписании. Хотя и так понятно, в 19 потоках (10 для докладов и 9 для митапов и мастер-классов) найдется всё, что хоть как-то связано с высокими нагрузками. Обязательно приходите, если и у ваших сервисов не сотни, но миллионы пользователей.
Комментарии (14)
Hixon10
31.10.2018 22:02Добрый день. Спасибо за доклад, очень круто!
Таким образом пробиваем NAT — ходим на один STUN-сервер, на другой, смотрим разницу, сравниваем и пробуем еще раз отдать свой порт уже с этим инкрементом или декрементом. То есть Алиса пытается отдать Бобу свой порт, уже скорректированный на константу, зная, что в следующий раз он будет именно такой.
Скажите, пожалуйста, вы решили не вливать эти изменения в публичную реализацию webrtc? Если да, то почему — NDA, или такая оптимизация приминима далеко не всегда, и в общем случае так делать не стоит?alatobol
01.11.2018 01:27+1инкрементить номера портов это скорее хак, чем фича
мы это сделали на уровне приложения, чтобы обновление библиотеки происходило более безболезненно
Но вообще идея хорошая, можем сделать патч для webRTCHixon10
01.11.2018 01:32Да, было бы очень здорово. Я даже боюсь представить, сколько вы сможете сэкономить мирового трафика TURN-серверов (если на ваших данных вышло 12% прироста P2P).
vasilevkirill
31.10.2018 22:43Позанудствую
NAT вроде как защищает сеть
Правильно настроенный NAT не защищает сеть, но неправильно настроенный NAT открывает новые дырки.
Это очень большое заблуждение что NAT способствует безопасности сети.
А за статью спасибо, очень интересно.
alatobol
01.11.2018 02:35и я позанудствую — вы абсолютно правы
но я не хотел в докладе на этом останавливаться, поэтому использовал «вроде как» ;)
arheops
31.10.2018 23:02Допустим, максимальная нагрузка наблюдается 10 часов в сутки.
Также допустим, что распределена она равномерно и средний видеозвонок 30 минут(на самом деле меньше, порядка 10).
1000000/10/3600=27.7 звонков в секунду. 50тысяч одновремнных звонков, причем они все p2p.
Поскольку используется opensource webrtc и p2p, то 4к там или 360 — сказать нельзя, пока сввязь не установится, и размер потока вообще никак не влияет на загрузку(он тупо идет от клиента клиенту).
В принципе, должен справится ОДИН сервер kamailio.
Точно hi-load?alatobol
01.11.2018 02:29только 40% трафика идет через p2p, остальное через turn
на видео доклада говорится что в докладе не будет Highload-а ;)
и доклад про то как работает сервис видеозвонковarheops
01.11.2018 04:07Ну так 10-50тыс звонков через turn это тоже не супер нагрузка для него. Нет, больше одного сервера, но тоже не сильно сложно.
Сложен сам клиент, про который както мало чего рассказано.alatobol
01.11.2018 13:56да, клиент это сложная задача:
нужно скомпилировать webRTC одной командой и прикрутить в ваше iOS или Android приложение, а на вебе 5 шагов pastebin.com/EjsdJx1h (ссылка из статьи)
повторюсь: на видео доклада говорится что в докладе не будет Highload-а ;)
и доклад про то как работает сервис видеозвонков
ReklatsMasters
01.11.2018 02:12Я тут, кстати, в свободное время разрабатываю webrtc datachannel стек на чистом js для nodejs — NodeRTC. Буду рад лайку, ретвиту, совету, комменту.
По поводу статьи замечу, что в одной из спецификаций ясно сказано, что бутстрапинг должен проходить по защищённому каналу. Поэтому, если всё делать по правилам, mitm не возможен.
alatobol
01.11.2018 02:31все так, но как звонящему понять, что канал действительно защищенный?
как раз в этой задаче поможет zRTPReklatsMasters
01.11.2018 03:35В браузере это проверка сертификата. Если SDP сообщение не будет подделано, то и DTLS корректно проверит сертификат и установит защищённый канал.
Или вы имеете в виду гипотетическую ситуацию, когда mitm встраивается между cloudflare и бэком, если между ними канал не защищён?
alatobol
01.11.2018 14:49да в статье идет речь про такой вариант mitm в webRTC:
p2p соединение скомпрометировано и mitm проксирует все сообщения в data канале, т.е. mitm делает два DTLS (по одному с каждым абонентом) и получает доступ к трафику
в этом случае нет возможности валидировать, что вы DTLS делаете именно с вашим абонентном, а не злоумышленником
ianzag
Судя по картинке гусей Боб не очень то хочет в данный момент устанавливать видеозвонок с Алисой…