Перевод статьи разработчика компании 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 свободной оперативной памяти при этом поддерживая загрузку видео в облако.

Комментарии (12)


  1. youROCK
    30.08.2017 15:04
    +3

    В программах на Go на самом деле очень хорошо видно, что они не заточены под низкое потребление памяти. Оно, конечно, обычно не такое, как в Java, но все равно примерно на полпорядка больше, чем аналогичные программы на C/C++. Я предполагаю, что это связано с тем, что Google в основном разрабатывает у себя stateless-сервисы, и им много памяти в любом случае не требуется. Для разработки в условиях относительного небольшого доступного количества памяти го подходит очень плохо. Альтернативы, конечно, тоже не огонь (в основном это C/C++, может быть Rust), но работать с ними в таких условиях должно быть сильно проще.


    1. divan0 Автор
      30.08.2017 15:21

      Ну да, Go очень явно дизайнился с расчетом на сервера, и многие компромисы были приняты в сторону большего использования памяти. Фреймворки вроде https://gobot.io и https://github.com/kidoman/embd, по идее, на Go не имели шанса появиться. Тот факт, что Go активно бегает на embedded и устройствах с маленьким количеством ресурсов это комплимент Go


      1. kireevco
        30.08.2017 19:40

        Какое отношение имеет gobot.io к запуску go на embeded? Такое же что и artoo.io cylonjs.com, или я ошибаюсь?


  1. 0r4anin
    30.08.2017 16:54
    -1

    Мне одному кажется что писать на Go для одноплатных систем с урезанными ресурсами это все равно что шуроповертом гвозди закалачивать?


  1. Siemargl
    30.08.2017 23:13
    -8

    Вот же блин, у моего Зикселя 64Мб памяти.

    Какое го- го- горе, Го туда не помещается…

    P.S. Скажу проще. Охренели Го-ре разработчики, забыли про единицу измерения в Килобайт


    1. wing_pin
      31.08.2017 11:33
      +4

      Вас забыли спросить, ага.


  1. KosToZyB
    31.08.2017 07:58
    -2

    Чего только не придумают, лишь бы не городить парк технологий. Но в любом случае интересно, если бы Go не был open source (не смогли бы чаще память отдавать память системе), то вряд ли бы они его оставили. Больше интересует почему изначально для телеметрии был выбран Go?


    1. pfihr
      31.08.2017 09:04

      Видимо, из-за встроенной библиотеки работы с сетью.


  1. remzalp
    31.08.2017 10:19

    Flussonic успешно пишется на Erlang, творит чудеса с потоковым видео в реалтайме. Не смотрели в ту сторону?


    1. divan0 Автор
      01.09.2017 00:07
      -1

      Это, наверное, к автору статьи вопрос :)


  1. monah_tuk
    31.08.2017 10:34
    +3

    Ребята работают с Go на MIPS с 32Мб памяти на борту (WiFi точка доступа под свои задачи). Результирующий бинарь 16Мб, потребление памяти — 4Мб. Сборка с вырезанием всего лишнего (символы, дебаг и пр.) и ручное управление GC. Функционал:


    • Http server
    • DHCP
    • DNS cache
    • Подсистема руления правилами iptables ipset tc
    • Netflow коллектор
    • Подсистема руления кишками AP

    Ну и логика работы с облаком (своё, часть проекта).


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


    1. monah_tuk
      01.09.2017 00:35

      Меня поправили, бинарь на MIPS — 3.5Мб.