Pull to refresh

Go на устройствах с маленькой памятью

Reading time4 min
Views7.5K
Original author: Maxwell Dergosits

Перевод статьи разработчика компании Samsara об опыте использования Go на автомобильном роутере с 170MB оперативки.


В компании Samsara мы разрабатываем автомобильные роутеры, которые предоставляют в реальном времени телеметрию двигателя через CAN шину, данные с беспроводных термосенсоров через Bluetooth Low Energy и соединение по Wi-Fi. Эти роутера очень ограничены по ресурсам. В отличие от серверов с 16 GB оперативной памяти, у наших роутеров есть всего 170MB и всего одно ядро.



Наша новая камера CM11, закреплённая в кабине.


Чуть ранее в этом году мы выпустили видео камеру, которая крепится внутри кабины, чтобы улучшить безопасность машин для наших клиентов. Эта камера, по сути, является периферийным устройством для нашего роутера, которое генерирует много данных. Она записывает 1080p H.264 видео с частотой 30 кадров в секунду.


Наша первая реализация сервиса работы с камерой, который был отдельным процессом на роутера, потреблял 60MB, то есть почти половина всей доступной памяти, но мы знали, что можем добиться лучших результатов. Мы буферизировали только 3 секунды видео потока на 5Mpbs, и 60MB было достаточно, чтобы удерживать в памяти целых 90 секунд видео, так что мы решили посмотреть, где мы можем уменьшить использование памяти.


Реализация сервиса работы с камерой


Сервис работы с камерой устанавливает параметры записи в камере, затем получает и сохраняет видео. Сохранённое H.264 видео затем конвертируется в mp4 и загружается в облако, но это происходит чуть позже в фоне.


Мы решили написать сервис полностью на Go, чтобы легко интегрироваться с остальными компонентами нашей системы. Это позволило быстро и легко написать первую реализацию сервиса, но он потреблял половину доступной на устройстве памяти и мы начали получать паники ядра из-за нехватки памяти. Наши задачи были следующими:


  • поддерживать Resident Set Size (RSS) процесса в пределах 15MB или меньше, чтобы другим сервисам также оставалось памяти под их задачи
  • оставить хотя бы 20% от общей памяти свободными, чтобы позволить периодические пики в использовании памяти

Тюнинг размера буфера


Нашей первой попыткой уменьшить использование памяти было просто уменьшить количество буферизации в память. Так как мы изначально буферизировали 3 секунды, то мы попробовали не буферизировать вообще и сохранять на диск один кадр за раз. Такой подход не сработал, потому что оверхед записи 20KB (средний размер кадра) при частоте 30 кадров в секунду уменьшал пропускную способность и увеличивал время отклика до цифр, при которых мы просто не справлялись со входящим потоком.



Слева — оригинальная архитектура буферизации: мы буферизировали примерно 90 кадров видео перед записью на диск. Справа — подход без буферизации: каждый кадр пишется сразу на диск


Затем мы попробовали буферизировать фиксированное количество байт. Мы использовались преимуществами пакета io из стандартной библиотеки Go и использовали bufio.Writer, который предоставлял буферизированую запись в любой io.Writer тип, даже если низлежащая структура не поддерживает буферизацию. Это позволило нам легко указать, сколько байт мы хотим буферизировать.


Следующим вызовом было определить оптимальный компромис между размером буфера и временем ожидания операций ввода-вывода. Слишком большой буфер и мы можем потерять слишком много памяти, но, с другой стороны, слишком много времени на чтение/запись и мы перестанем справляться с входящим видео с камеры. Мы провели простой бенчмарк, который менял размер буфера от 1KB до 1MB и замерили время необходимое для записи 3 секунд (или около 1.8MB) видео на диск.



Размер буфера vs Время записи


Исходя из графика, хорошо видно переломный момент около 64KB — хороший выбор, который не использует слишком много памяти и при этом достаточно быстр, чтобы не терять кадры. (Эта заметна разница во времени обьясняется реализацией флеш-памяти). Это изменение в буферные уменьшило использование памяти на порядок мегабайт, но всё ещё не ниже предела, к которому мы стремились.



Финальная архитектура: мы всегда буферизируем 64KB перед записью


Следующим шагом было профилирование использования памяти сервисом с помощью встроенного в Go профайлера pprof. Мы выяснили, что на самом деле, процесс расходовал очень мало времени, но что-то подозрительно происходило со сборщиком мусора.


Тюнинг сборщика мусора


Сборщик мусора в Go делает ставку на низкое время отклика и простоту. У него есть единственный параметр для тюнинга, GOGC — процент, контролирующие отношение общего размера кучи к доступному процессу размеру. Мы поигрались с этим параметром, но особо эффекта не было, поскольку память, освобождаемая после сборки мусора не сразу возвращалась операционной системе.


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


Таймаут для возвращения памяти операционной системе нельзя изменить, но в Go есть для этого функция debug.FreeOSMemory, котора запускает сборку мусора и возвращает принудительно освобожденную память операционной системе. Это было удобно. Мы изменили наш сервис работы с камерой таким образом, что он вызывал эту функцию каждые 5 секунд и увидели, что RSS параметр уменьшился почти в 5 раз до приемлемых 10-15MB! Уменьшение потребления памяти не даётся бесплатно, само собой, но в нашем случае это подходило, так как у нас не было гарантий реального времени и мы могли чуть пожертвовать временем отклика из-за более частых пауз от сборки мусора.


Если вам интересно, почему это помогло: мы загружаем видео в облако периодически, и это приводит к пикам потребления памяти около 15MB. Мы можем спокойно позволить такие пики если они держатся несколько секунд, но чуть дольше уже нет. Пик в 30MB и значение GOGC=200% означает, что сборщик мусора может выделить до 60MB. После пика, Go не возвращает память в течении 5 минут, но вызывая debug.FreeOSMemory мы уменьшили этот период до 5 секунд.


Заключение


Добавление новых периферийных устройств, с которыми работал наш роутер привели к серьезному удару по ограничениям памяти. Мы поэкспериментировали с различными стратегиями буферизации для уменьшения использования памяти, но что, в итоге, помогло это конфигурация сборщика мусора Go под другое поведение. Это было немного сюрпризом для нас — обычно при разработке в Go вы не думаете про выделение памяти и сборку мусора, но в наших условиях нам пришлось этим заняться. Мы смогли уменьшить потребление памяти в приятные 5 раз и гарантировать, что роутер всегда имеет 50MB свободной оперативной памяти при этом поддерживая загрузку видео в облако.

Tags:
Hubs:
+6
Comments14

Articles