В связи с узурпацией должности управдома нашего МКД, и необходимости «причесать» общественный беспорядок, потихоньку конструирую систему видеонаблюдения.
Разумеется, финансирование минимальное, планы — грандиозные, поэтому собирается всё из подножного корма.
Подробности чуть позже, а вот один из интересных багов, который заставил перебраьт много чего.
Итак, началось всё с малого — простенькая купольная камера, бралась самая паршивая по принципу "только б IP и купол". После пошел процесс перебора софта (тема отдельного поста, будет, опять же, позже)… В конце зафиксировался на Macroscop в один канал. Стояла пару месяцев, есть-пить не просила, наркоманов гонять помогала.
И вот работает вроде всё, но как-то подлагивает периодически. Грешил на всё: на проц, на сеть, на софт, на погоду на марсе… Саппорт по логам говорит, что-де камера периодически отваливается.
Да были закуплены еще камеры, качеством и ценою значительно выше. Поборовши лень в три прекрасных дня в разные недели было это всё смонтировано на 1м этаже, заведено в комп, подключено. И пошла вторая итерация перебора софта, так чтоб на камер побольше, ценою гуманней, интерфейс удобней… После второго раунда перебора, пока стоит AxxonNext. Да вот только лаги вышли на новый уровень: камеры отваливаются чуть ли не синхронно каждые 5-10 минут если включить TCP; а если держать UDP, то артефакты лезут как не знаю что.
Пожаловался я в жуйке на эту тему, а maxlapshin возьми да и скажи, что это стабильный баг китайских камер, где прошивка создана их собственными силами: если клиент не успевает выгребать, то в поток лезет мусор. Так как продаван на ali прикормленный, было решено попытаться решить проблему капитально. Несколько раз разными словами пытался баг описать, но понял, что английский у нас обоих оставляет желать лучшего, поэтому надо просто «show code».
Первый этап раскопок: найти причину
Итак, сперва берём tcpdump, и ждём ситуацию обрыва. Ждать недолго, за 5 минут поймалось аж 3 штуки. Как понять что произошло? Поток в полтора миллиона пакетов… Для начала фильтруем, оставляя только одну камеру. Затем Ctrl+F => tcp.flags.syn==1 => находим начало реконнекта, откуда листаем вверх, чтобы понять что случилось…
Наблюдаем, что коннект был закрыт со стороны компа… Вот только смущает что перед этим Win=11460, Win=10200, Win=8940 — то есть, похоже, клиент и правда не успевает. Стоп-стоп-стоп. Как это не успевает? AMD Phenom II X6 1100T? Не, странно. IO? Так запись на отдельный диск, и не упёрлось в полку. Да и тогда была бы зависимость от просмотра, например — а её нет… В общем, листаем еще выше чуть-чуть:
Так буквально секунду назад вообще в ZeroWindow уходили. Точно не успевает. Но почему обрыв-то?! Листаем ниже, смотрим другой обрыв…
Так, наблюдаем ZeroWindow в течение аж 2.5 секунд — совсем ни в какие ворота не лезет. Листаем ниже, и, кажется, начинаем понимать:
Смотрим — совсем нехорошо себя повел TCP стек (камеры? вероятно...), после чего RTP поток сбился — в один прекрасный момент вместо DynamicRTP-Type-96 пошли RTSP Continuation.
Итак, делаем вывод: всё что нужно для симуляции, это запросить RTP поток, немного вытянуть, а потом сделать sleep(), и смотреть сломается ли поток.
Поиск частей для франкенштейна
Как поступить, когда надо быстро накидать тестовый кусок? Взять скриптовый язык, набор готовых библиотек, слепить всё это вместе, радоваться. Лезем в гугль. Python+ONVIF. Тухло. Ruby+ONVIF. О, есть ruby-onvif-client. ОК, берём. URL стрима поймали. Отлично, а если :protocol покрутить?.. UDP, HTTP, TCP… итог один — отлично, по onvif ловить URL научился, теперь его бы слить.
Curl? Не жуёт. Ладно, Ruby + RTSP… Ура, есть либа. Скушиваем ему урл, и облом. Пытается авторизоваться исключительно через Basic. Еще немного гугля. Облом. Тогда остаётся один метод — напильник. Впаиваем калёным железом, по пути матерясь на объёмы магии руби (кто мне скажет, как правильно сделать «честную» рекурсию, чтобы работал и return и yield? правильно, а как догадаться по описанию функции, что оно может потребоваться? правильно… проще избавиться от неё — и чище код, и шелковистее волосы).
И снова ликуем, rtsp_client работает. Открываем во втором окне tcpdump… Блин! Я же в ONVIF запрашивал :protocol=>«TCP». Что за черт, почему UDP?
Паяльник в руки… Ха! UDP прибит гвоздями в lib/rtsp/client.rb@request_transport. Так, впаиваем туда теми же гвоздями /TCP. Запускаем — падает. Почему? Куда? Ага, он требует наличия client_port… Какой client_port, если это TCP? Хардкодим rtp_port если его нет в 554. Так, IP надо сервера — хардкодим. Опа, не может, говорит, при'bind()'иться на 554 порт не рутом. Логично. Так, а зачем? Ну-ка… Это в RTP::Receiver… Смотрим на init_socket в режиме :TCP и удивляемся — а зачем ему TCPServer для Receiver? Что-то не то. Явно, вот явно на TCP никто не отлаживал.
После пары минут попыток понять логику, до Зоркого Глаза дошло, что стенки-то нет:
transport: RTP/AVP/TCP;unicast;destination=172.28.1.199;source=172.28.1.95;interleaved=0-1
Ну-ка ну-ка… что за interleaved? Google: rtp interleaved => Wikipedia => Find on page «interleaved» => Ага! Так RTP и RTSP валятся в этом же самом коннекте!
Выкидываем rtsp либу, так как для моей задачи править либо откровенно не хочется — тут надо явно по-хорошему уже переделывать архитектуру.
Выращиваем амёбу
Итак, мы снова в нуле: у нас есть rtsp:// урл камеры, есть необходимость его слить и поиграться во время слива с коннектом… Стоп! Сначала. Есть урл. Есть камера. Есть задача воспроизвести на ней баг. Зачем мне ONVIF? Зачем RTSP либы? Надо просто запросить и качать ответ.
Сказано — сделано. DESCRIBE? А зачем он нам… Нам надо только сперва SETUP, откуда забрать Session: затем PLAY с ней.
Первый же опыт показал, что ему даже авторизация тут не требуется.
Отлично! Первый же блин выстрелил сразу: простнький скрипт прекрасно воспроизводил баг — после sleep()'а поток ломался на ура.
Но чтобы поиграться с TCP Window надо бы иметь возможность задать TCP_WINDOW_CLAMP. А для этого надо сделать setsockopt ДО connect но после создания сокета.
А как это в руби сделать? Эм… Заглядываем в гугль… Пусто. Заглядываем в исходник — init_inetsock_internal… фиг там! сперва создаём rsock_socket(), а затем сразу rsock_connect(). Блин.
Ладно, я всё равно руби не очень люблю. За 2 минуты переписываем на питон, добавляем setsockopt. Добавляем анализ номера RTP пакета.
Итог анализа: от размера начального окна меняется объём инфромации, сколько успевает камера передать нормального, прежде чем сломается. Впрочем, неважно, закоммитил итог, и написал багрепорт продавцу, дабы они передали девелоперам прошивки.
Так бы и успокоился, но maxlapshin спрашивал можно ли как-то жить с этим багом. И это потребовало дополнительных раскопок.
В принципе, как с этим жить?
1) Можно отслеживать сбой и пересоединяться самостоятельно
2) Можно отслеживать собственный лаг (не знаю как, но можно) и пересоедениться сразу, не дожидаясь поломки потока
3) Можно попытаться восстановить синхронизацию с потоком.
Итак, втыкаем вместо вызова raise — вызов reconnect, и анализируем потери по разнице номеров RTP пакетов. Если действуем по пункту 1, то потери составляют около 1500-1800 RTP пакетов (примерно 600 пакетов на секунду sleep()).
Втыкаем reconnect сразу после sleep(). Итог в точности такой же.
Втыкаем ресинхронизацию методом поиска "$x00" — работает отвратно. Втыкаем ресинхронизацию методом поиска "$x00[LEN]{len bytes}$x00" — работает стабильно, потеря составляет в полтора раза меньше, чем при reconnect. Но самое главное — TCP соединение при этом не падает, а значит, алгоритм адаптации TCP Window и буферов приема продолжают работать. Вследствие чего, спустя 1-2 сбоя в начале соединения, поток просто перестаёт ломаться — sleep() продолжают регулярно усыплять клиента, а поток не падает.
Полученный тест-скрипт качеством кода не блещет, но прекрасно выполняет функцию proof-of-concept.
И теперь, наконец-то, мы пришли к вопросу, заданному в заголовке.
Баг ли это, или фича?
Моё личное мнение — это реально лучше, чем рвать соединение целиком: номера RTP пакетов в коннекте есть, так что без проблем потерянный объём можно замерить.
Если канал тоньше, чем пропускная способность сети — то потери будут расти, таким образом, обнаружив потери больше чем на 5 сек — можно спокойно жаловаться на толщину канала (реконнектиться с меньшим качеством, попросить поменять кабель, взорвать АЭС, или любой другой способ реакции на эту проблему); если же проблема в том, что приёмник просто не успевает по какой-то причине выгребать — resync поведение = наше щасте. Получаем объединение плюсов одновременно UDP и TCP подходов: если совсем что-то плохо — кусочек потеряли; в остальных случаях — ретрансмиты автоматические и проблем не доставляют.
Ну и на закуску, поговорим о том, что за баг в прошивке… А баг прост: send(somedata, somelen) возвращает <0 в случае ошибки, число байт, если отработало. И любмая ошибка всех начинающих сетевых разработчиков: send(somedata, somelen) может вернуть что-то МЕНЬШЕ, чем somelen — в буфер не влезло.
Если это не обрабатывать, хвост от somedata просто теряется — его и не отправили, и выбросили.
Как правильно починить?
Починить так: надо запомнить недоотправленный кусок, и прекратить посылать что бы то ни было, пока send() этого остатка не отправит всё целиком. После этого надо начать посылать _следующие_ пакеты (выбросив те, которые были всё время, что буфер был занят). Тогда мы получим то самое поведение помеси TCP и UDP, но без необходимости клиенту заниматься магической ресинхронизацией с границами пакетов.
Надеюсь, производители поправят этот баг, и через некоторое время, все китайские прошивки для китайских камер будут радовать корректной работой без единого разрыва&tm;
Автор: datacompboy