Привет. Меня зовут Марко (я системный программист в Badoo). И я представляю вашему вниманию перевод поста по Go, который мне показался интересным. Go действительно ругают за толстые бинарники, но при этом хвалят за статическую линковку и за удобство выкладки единственного файла. Если на современных серверах толстые бинарники – не проблема, то на встраиваемых системах – еще как. Автор описывает свою историю борьбы с ними в Go. Маленький размер файлов важен для приложений, работающих в условиях очень ограниченных ресурсов. В этой статье мы рассмотрим создание программы-агента, которая должна работать на разных маломощных устройствах. Ресурсы памяти и процессора у них будут невелики, и я даже не могу предсказать, насколько. Бинарники Go отличаются маленьким размером и самодостаточностью: создав программу на Go, вы получаете единственный двоичный файл, в котором находится всё необходимое. Сравните с такими платформами, как Java, Node.js, Ruby и Python, где ваш код занимает лишь небольшую часть приложения, а всё остальное — куча зависимостей, которые тоже приходится упаковывать, если хочется получить самодостаточный пакет. Несмотря на такое важное удобство, как возможность создавать самодостаточные бинарники, в Go нет встроенного инструментария, помогающего оценить размеры зависимостей, чтобы разработчики могли принимать взвешенные решения о том, включать эти зависимости в файл или нет.  Инструмент gofat поможет разобраться с размерами зависимостей в вашем Go-проекте.  Я немного расскажу о том, как мы продумывали и создавали один из наших сервисов — IoT-агент, который будет развёртываться на маломощных устройствах по всему миру. И рассмотрим его архитектуру с операционной точки зрения.  Пример кода можно скачать отсюда: https://github.com/jondot/fattyproject Во-первых, нам нужна хорошая CLI-эргономика, поэтому воспользуемся kingpin — это POSIX-совместимая библиотека CLI-флагов и опций (мне настолько нравится эта библиотека, что я использовал её во многих своих проектах). Но на самом деле я воспользуюсь своим проектом go-cli-starter, включающим в себя эту библиотеку: Раз наша программа – это агент, то она должна работать постоянно. В качестве примера для этого мы воспользуемся циклом, который бесконечно выполняет ерундовую операцию.  Во время длительной работы в памяти накапливается всякий хлам — небольшие утечки памяти, забытые дескрипторы открытых файлов. Но даже крохотная утечка может превратиться в гигантскую, если приложение работает безостановочно годами. К счастью, в Go есть встроенные метрики и средство контроля за состоянием системы – expvars. Это очень поможет при анализе внутренней кухни агента: поскольку он должен длительное время работать без остановки, время от времени мы будем анализировать его состояние — потребление процессора, циклы сбора мусора и так далее. Всё это будут для нас делать expvars и весьма удобный для решения подобных задач инструмент expvarmon.  Для использования expvars нам понадобится волшебный импорт. Волшебный – потому что в ходе импорта будет добавлен хэндлер к имеющемуся HTTP-серверу. Для этого нам нужен работающий HTTP-сервер из net/http. Раз наша программа превращается в сложный сервис, можем добавить ещё и библиотеку логирования с поддержкой уровней, чтобы получать информацию об ошибках и предупреждениях, а также понимать, когда программа работает штатно. Для этого воспользуемся zap (от компании Uber). Сервис, безостановочно работающий на удалённом устройстве, который вы не контролируете и, вероятнее всего, не сможете обновлять, должен быть крайне устойчивым. Так что целесообразно заложить в него гибкость. Например, чтобы он мог исполнять кастомные команды и скрипты, то есть обеспечить механизм изменения поведения сервиса без его переразвёртывания или перезапуска. Добавим средство запуска произвольного удалённого скрипта. Хотя это и выглядит подозрительно, но если это ваш агент или сервис, то вы можете подготовить встроенную runtime-песочницу для запуска кода. Чаще всего встраивают runtime-среды на JavaScript и Lua. Мы воспользуемся встраиваемым JS-движком otto. Если предположить что контент, передающийся в Run, мы получаем извне, мы получили сложный и самообновляемый IoT-агент! Итак, к чему мы пришли. Будем считать, что нам нужны все добавленные зависимости, но в результате размер двоичного файла подбирается к 12 мегабайтам. Хотя это немного по сравнению с другими языками и платформами, однако с учётом скромных возможностей IoT-оборудования целесообразно будет уменьшить размер файла и затраты вычислительных ресурсов. Давайте выясним, как добавляются зависимости в наш двоичный файл. Для начала разберёмся с хорошо известным бинарником. GraphicsMagick — современная вариация популярной системы обработки изображений ImageMagick. Вероятно, она у вас уже установлена. Если нет, то под OS X это можно сделать с помощью brew install graphicsmagick. otool – альтернатива инструменту ldd, только под OS X. С его помощью мы можем проанализировать двоичный файл и узнать, с какими библиотеками он слинкован. Из списка можно вычленить и размер каждой зависимости: Можем ли мы таким образом получить достаточно полное представление о любом двоичном файле? Очевидно, что ответ — «нет». По умолчанию Go линкует зависимости статично. Благодаря этому мы получаем единственный самодостаточный двоичный файл. Но это также означает, что otool, как и любой другой подобный инструмент, будет бесполезен. Если всё же пытаться разобрать двоичный файл Go на его зависимости, то нам придётся воспользоваться инструментом, который понимает формат этих двоичных файлов. Давайте поищем что-то подходящее. Для получения списка доступных инструментов воспользуемся go tool: Можем сразу обратиться к исходным кодам этих инструментов. Возьмём, к примеру, nm, и посмотрим его документацию пакета. Я умышленно упомянул этот инструмент. Как оказалось, возможности nm очень близки к тому, что нам нужно, но этого всё-таки недостаточно. Он умеет выводить список символов и размеров объектов, но всё это бесполезно, если мы пытаемся составить общее представление о зависимостях двоичного файла. Хотя применительно к самим зависимостям указанные размеры (вторая колонка) могут быть точны, но в целом мы не можем просто взять и сложить эти значения. Остался последний трюк, который должен сработать. Когда вы компилируете свой двоичный файл, Go генерирует промежуточные файлы для каждой зависимости, прежде чем статически слинковать их в единый файл. Представляю вашему вниманию gofat — shell-скрипт, который является комбинацией кода на Go и некоторых Unix-инструментов. Он анализирует размеры зависимостей в двоичных файлах Go: Если торопитесь, то просто скопируйте или скачайте этот скрипт и сделайте его исполняемым (chmod +x). Потом запустите скрипт без каких-либо аргументов в директории своего проекта, чтобы получить информацию о его зависимостях.  Давайте разберёмся с этой командой: eval go build -work -a 2>&1 Флаг -a говорит Go, чтобы он игнорировал кэш и собирал проект с нуля. В этом случае все зависимости будут пересобраны принудительно. Флаг –work выводит рабочую директорию, так что мы можем её проанализировать (спасибо разработчикам Go!). Затем мы с помощью инструмента find находим все файлы *.a, представляющие собой наши скомпилированные зависимости. Затем передаём все строки (месторасположения файлов) в xargs. Эта утилита позволяет применять команды к каждой передаваемой строке — в нашем случае в du, который получает размер файла. Наконец, воспользуемся gsort (GNU-версия sort) для выполнения сортировки размеров файлов в обратном порядке. Убираем отовсюду префикс папки WORK и выводим на экран очищенную строку с данными по зависимости. Переходим к самому интересному: что же занимает 12 Мб в нашем двоичном файле? В первый раз запускаем gofat применительно к нашему игрушечному проекту с IoT-агентом. Получаем такие данные: Если поэкспериментируете, то заметите, что с gofat время сборки значительно увеличивается. Дело в том, что мы запускаем сборку в режиме -a, при котором всё пересобирается заново. Теперь мы знаем, сколько места занимает каждая зависимость. Закатаем рукава, проанализируем и предпримем действия. Всё, что связано с обработкой HTTP, тянет на 1,8 Мб. Пожалуй, можно это выкинуть. Откажемся от expvar, вместо этого будем периодически сбрасывать в лог-файл критически важные параметры и информацию о состоянии программы. Если это делать часто, то всё будет хорошо. Обновление: С выходом Go 1.8 net/http стал весить 2,2 Мб. А это большой сюрприз: около 1 Мб занимает весьма удобная POSIX-фича для парсинга флагов. Можно от неё отказаться и использовать пакет из стандартной библиотеки, или даже вообще покончить с флагами и считывать конфигурацию из переменных окружения (а это тоже займёт какой-то объём). Newrelic добавляет ещё 1,3 Мб, так что его тоже можно отбросить: `Zap тоже выкинем. Воспользуемся стандартным пакетом для логирования: 392K go.uber.org/zap/zapcore.a Otto, будучи встраиваемым JS-движком, весит немало: В то же время logrus занимает мало места для такой многофункциональной библиотеки журналирования: Можно оставить. Мы нашли способ вычислить размеры зависимостей в Go и сэкономили около 7 Мб. И решили, что не будем использовать определённые зависимости, а вместо них возьмем аналоги из стандартной библиотеки Go.  Более того, скажу, что, если сильно постараться и поэкспериментировать с набором зависимостей, то мы можем ужать наш двоичный файл с изначальных 12 Мб до 1,2 Мб. Заниматься этим не обязательно, потому что зависимости в Go и так невелики по сравнению с другими платформами. Но вам обязательно нужно иметь под рукой инструменты, которые помогут лучше понимать то, что вы создаёте. И если вы разрабатываете ПО для окружений с весьма ограниченными доступными ресурсами, то одним из таких инструментов может быть gofat. P.S.: если хотите поэкспериментировать еще, вот референсный репозиторий: https://github.com/jondot/fattyproject. Системный программист