Привет. Меня зовут Марко (я системный программист в Badoo). И я представляю вашему вниманию перевод поста по Go, который мне показался интересным. Go действительно ругают за толстые бинарники, но при этом хвалят за статическую линковку и за удобство выкладки единственного файла. Если на современных серверах толстые бинарники – не проблема, то на встраиваемых системах – еще как. Автор описывает свою историю борьбы с ними в Go.
Маленький размер файлов важен для приложений, работающих в условиях очень ограниченных ресурсов. В этой статье мы рассмотрим создание программы-агента, которая должна работать на разных маломощных устройствах. Ресурсы памяти и процессора у них будут невелики, и я даже не могу предсказать, насколько.
Бинарники Go отличаются маленьким размером и самодостаточностью: создав программу на Go, вы получаете единственный двоичный файл, в котором находится всё необходимое. Сравните с такими платформами, как Java, Node.js, Ruby и Python, где ваш код занимает лишь небольшую часть приложения, а всё остальное — куча зависимостей, которые тоже приходится упаковывать, если хочется получить самодостаточный пакет.
Несмотря на такое важное удобство, как возможность создавать самодостаточные бинарники, в Go нет встроенного инструментария, помогающего оценить размеры зависимостей, чтобы разработчики могли принимать взвешенные решения о том, включать эти зависимости в файл или нет.
Инструмент gofat
поможет разобраться с размерами зависимостей в вашем Go-проекте.
Создание IoT-агента
Я немного расскажу о том, как мы продумывали и создавали один из наших сервисов — IoT-агент, который будет развёртываться на маломощных устройствах по всему миру. И рассмотрим его архитектуру с операционной точки зрения.
Пример кода можно скачать отсюда: https://github.com/jondot/fattyproject
Во-первых, нам нужна хорошая CLI-эргономика, поэтому воспользуемся kingpin
— это POSIX-совместимая библиотека CLI-флагов и опций (мне настолько нравится эта библиотека, что я использовал её во многих своих проектах). Но на самом деле я воспользуюсь своим проектом go-cli-starter
, включающим в себя эту библиотеку:
$ git clone https://github.com/jondot/go-cli-starter fattyproject
Cloning into 'fattyproject'...
remote: Counting objects: 55, done.
remote: Total 55 (delta 0), reused 0 (delta 0), pack-reused 55
Unpacking objects: 100% (55/55), done.
Раз наша программа – это агент, то она должна работать постоянно. В качестве примера для этого мы воспользуемся циклом, который бесконечно выполняет ерундовую операцию.
for {
f := NewFarble(&Counter{})
f.Bumple()
time.Sleep(time.Second * 1)
}
Во время длительной работы в памяти накапливается всякий хлам — небольшие утечки памяти, забытые дескрипторы открытых файлов. Но даже крохотная утечка может превратиться в гигантскую, если приложение работает безостановочно годами. К счастью, в Go есть встроенные метрики и средство контроля за состоянием системы – expvars
. Это очень поможет при анализе внутренней кухни агента: поскольку он должен длительное время работать без остановки, время от времени мы будем анализировать его состояние — потребление процессора, циклы сбора мусора и так далее. Всё это будут для нас делать expvars
и весьма удобный для решения подобных задач инструмент expvarmon
.
Для использования expvars
нам понадобится волшебный импорт. Волшебный – потому что в ходе импорта будет добавлен хэндлер к имеющемуся HTTP-серверу. Для этого нам нужен работающий HTTP-сервер из net/http
.
import (
_ "expvar"
"net/http"
:
:
go func() {
http.ListenAndServe(":5160", nil)
}()
Раз наша программа превращается в сложный сервис, можем добавить ещё и библиотеку логирования с поддержкой уровней, чтобы получать информацию об ошибках и предупреждениях, а также понимать, когда программа работает штатно. Для этого воспользуемся zap (от компании Uber).
import(
:
"go.uber.org/zap"
:
logger, _ := zap.NewProduction()
logger.Info("OK", zap.Int("ip", *ip))
Сервис, безостановочно работающий на удалённом устройстве, который вы не контролируете и, вероятнее всего, не сможете обновлять, должен быть крайне устойчивым. Так что целесообразно заложить в него гибкость. Например, чтобы он мог исполнять кастомные команды и скрипты, то есть обеспечить механизм изменения поведения сервиса без его переразвёртывания или перезапуска.
Добавим средство запуска произвольного удалённого скрипта. Хотя это и выглядит подозрительно, но если это ваш агент или сервис, то вы можете подготовить встроенную runtime-песочницу для запуска кода. Чаще всего встраивают runtime-среды на JavaScript и Lua.
Мы воспользуемся встраиваемым JS-движком otto.
import(
:
"github.com/robertkrimen/otto"
:
for {
:
vm.Run(`
abc = 2 + 2;
console.log("\nThe value of abc is " + abc); // 4
`)
:
}
Если предположить что контент, передающийся в Run
, мы получаем извне, мы получили сложный и самообновляемый IoT-агент!
Разбираемся с зависимостями двоичного файла Go
Итак, к чему мы пришли.
$ ls -lha fattyproject
... 13M ... fattyproject*
Будем считать, что нам нужны все добавленные зависимости, но в результате размер двоичного файла подбирается к 12 мегабайтам. Хотя это немного по сравнению с другими языками и платформами, однако с учётом скромных возможностей IoT-оборудования целесообразно будет уменьшить размер файла и затраты вычислительных ресурсов.
Давайте выясним, как добавляются зависимости в наш двоичный файл.
Для начала разберёмся с хорошо известным бинарником. GraphicsMagick — современная вариация популярной системы обработки изображений ImageMagick
. Вероятно, она у вас уже установлена. Если нет, то под OS X это можно сделать с помощью brew install graphicsmagick
.
otool
– альтернатива инструменту ldd, только под OS X. С его помощью мы можем проанализировать двоичный файл и узнать, с какими библиотеками он слинкован.
$ otool -L `which convert`
/usr/local/bin/convert:
/usr/local/Cellar/imagemagick/6.9.3-0_2/lib/libMagickCore-6.Q16.2.dylib (compatibility version 3.0.0, current version 3.0.0)
/usr/local/Cellar/imagemagick/6.9.3-0_2/lib/libMagickWand-6.Q16.2.dylib (compatibility version 3.0.0, current version 3.0.0)
/usr/local/opt/freetype/lib/libfreetype.6.dylib (compatibility version 19.0.0, current version 19.3.0)
/usr/local/opt/xz/lib/liblzma.5.dylib (compatibility version 8.0.0, current version 8.2.0)
/usr/lib/libbz2.1.0.dylib (compatibility version 1.0.0, current version 1.0.5)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
/usr/local/opt/libtool/lib/libltdl.7.dylib (compatibility version 11.0.0, current version 11.1.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)
Из списка можно вычленить и размер каждой зависимости:
$ ls -lha /usr/l/.../-0_2/lib/libMagickCore-6.Q16.2.dylib
... 1.7M ... /usr/.../libMagickCore-6.Q16.2.dylib
Можем ли мы таким образом получить достаточно полное представление о любом двоичном файле? Очевидно, что ответ — «нет».
По умолчанию Go линкует зависимости статично. Благодаря этому мы получаем единственный самодостаточный двоичный файл. Но это также означает, что otool
, как и любой другой подобный инструмент, будет бесполезен.
$ cat main.go
package main
func main() {
print("hello")
}
$ go build && otool -L main
main:
Если всё же пытаться разобрать двоичный файл Go на его зависимости, то нам придётся воспользоваться инструментом, который понимает формат этих двоичных файлов. Давайте поищем что-то подходящее.
Для получения списка доступных инструментов воспользуемся go tool
:
$ go tool
addr2line
api
asm
cgo
compile
cover
dist
doc
fix
link
nm
objdump
pack
pprof
trace
vet
yacc
Можем сразу обратиться к исходным кодам этих инструментов. Возьмём, к примеру, nm, и посмотрим его документацию пакета. Я умышленно упомянул этот инструмент. Как оказалось, возможности nm
очень близки к тому, что нам нужно, но этого всё-таки недостаточно. Он умеет выводить список символов и размеров объектов, но всё это бесполезно, если мы пытаемся составить общее представление о зависимостях двоичного файла.
$ go tool nm -sort size -size fattyproject | head -n 20
5ee8a0 1960408 R runtime.eitablink
5ee8a0 1960408 R runtime.symtab
5ee8a0 1960408 R runtime.pclntab
5ee8a0 1960408 R runtime.esymtab
4421e0 1011800 R type.*
4421e0 1011800 R runtime.types
4421e0 1011800 R runtime.rodata
551a80 543204 R go.func.*
551a80 543204 R go.string.hdr.*
12d160 246512 T github.com/robertkrimen/otto._newContext
539238 100424 R go.string.*
804760 65712 B runtime.trace
cd1e0 23072 T net/http.init
5e3b80 21766 R runtime.findfunctab
1ae1a0 18720 T go.uber.org/zap.Any
301510 18208 T unicode.init
5e9088 17924 R runtime.typelink
3b7fe0 16160 T crypto/sha512.block
8008a0 16064 B runtime.semtable
3f6d60 14640 T crypto/sha256.block
Хотя применительно к самим зависимостям указанные размеры (вторая колонка) могут быть точны, но в целом мы не можем просто взять и сложить эти значения.
Gofat
Остался последний трюк, который должен сработать. Когда вы компилируете свой двоичный файл, Go генерирует промежуточные файлы для каждой зависимости, прежде чем статически слинковать их в единый файл.
Представляю вашему вниманию gofat
—?shell-скрипт, который является комбинацией кода на Go и некоторых Unix-инструментов. Он анализирует размеры зависимостей в двоичных файлах Go:
#!/bin/sh
eval `go build -work -a 2>&1` && find $WORK -type f -name "*.a" | xargs -I{} du -hxs "{}" | gsort -rh | sed -e s:${WORK}/::g
Если торопитесь, то просто скопируйте или скачайте этот скрипт и сделайте его исполняемым (chmod +x
). Потом запустите скрипт без каких-либо аргументов в директории своего проекта, чтобы получить информацию о его зависимостях.
Давайте разберёмся с этой командой:
eval go build -work -a 2>&1
Флаг -a говорит Go, чтобы он игнорировал кэш и собирал проект с нуля. В этом случае все зависимости будут пересобраны принудительно. Флаг –work выводит рабочую директорию, так что мы можем её проанализировать (спасибо разработчикам Go!).
find $WORK -type f -name "*.a" | xargs -I{} du -hxs "{}" | gsort -rh
Затем мы с помощью инструмента find
находим все файлы *.a
, представляющие собой наши скомпилированные зависимости. Затем передаём все строки (месторасположения файлов) в xargs
. Эта утилита позволяет применять команды к каждой передаваемой строке — в нашем случае в du
, который получает размер файла.
Наконец, воспользуемся gsort
(GNU-версия sort) для выполнения сортировки размеров файлов в обратном порядке.
sed -e s:${WORK}/::g
Убираем отовсюду префикс папки WORK и выводим на экран очищенную строку с данными по зависимости.
Переходим к самому интересному: что же занимает 12 Мб в нашем двоичном файле?
Сбрасываем вес
В первый раз запускаем gofat
применительно к нашему игрушечному проекту с IoT-агентом. Получаем такие данные:
2.2M github.com/robertkrimen/otto.a
1.8M net/http.a
1.4M runtime.a
960K net.a
820K reflect.a
788K gopkg.in/alecthomas/kingpin.v2.a
668K github.com/newrelic/go-agent.a
624K github.com/newrelic/go-agent/internal.a
532K crypto/tls.a
464K encoding/gob.a
412K math/big.a
392K text/template.a
392K go.uber.org/zap/zapcore.a
388K github.com/alecthomas/template.a
352K crypto/x509.a
344K go/ast.a
340K syscall.a
328K encoding/json.a
320K text/template/parse.a
312K github.com/robertkrimen/otto/parser.a
312K github.com/alecthomas/template/parse.a
288K go.uber.org/zap.a
232K time.a
224K regexp/syntax.a
224K regexp.a
224K go/doc.a
216K fmt.a
196K unicode.a
192K compress/flate.a
172K github.com/robertkrimen/otto/ast.a
172K crypto/elliptic.a
156K encoding/asn1.a
152K os.a
136K strconv.a
128K os/exec.a
128K github.com/Sirupsen/logrus.a
128K flag.a
112K vendor/golang_org/x/net/http2/hpack.a
104K strings.a
104K net/textproto.a
104K mime/multipart.a
Если поэкспериментируете, то заметите, что с gofat
время сборки значительно увеличивается. Дело в том, что мы запускаем сборку в режиме -a
, при котором всё пересобирается заново.
Теперь мы знаем, сколько места занимает каждая зависимость. Закатаем рукава, проанализируем и предпримем действия.
1.8M net/http.a
Всё, что связано с обработкой HTTP, тянет на 1,8 Мб. Пожалуй, можно это выкинуть. Откажемся от expvar
, вместо этого будем периодически сбрасывать в лог-файл критически важные параметры и информацию о состоянии программы. Если это делать часто, то всё будет хорошо.
Обновление: С выходом Go 1.8 net/http стал весить 2,2 Мб.
788K gopkg.in/alecthomas/kingpin.v2.a
388K github.com/alecthomas/template.a
А это большой сюрприз: около 1 Мб занимает весьма удобная POSIX-фича для парсинга флагов. Можно от неё отказаться и использовать пакет из стандартной библиотеки, или даже вообще покончить с флагами и считывать конфигурацию из переменных окружения (а это тоже займёт какой-то объём).
Newrelic
добавляет ещё 1,3 Мб, так что его тоже можно отбросить:
668K github.com/newrelic/go-agent.a
624K github.com/newrelic/go-agent/internal.a
`Zap тоже выкинем. Воспользуемся стандартным пакетом для логирования:
392K go.uber.org/zap/zapcore.a
Otto
, будучи встраиваемым JS-движком, весит немало:
2.2M github.com/robertkrimen/otto.a
312K github.com/robertkrimen/otto/parser.a
172K github.com/robertkrimen/otto/ast.a
В то же время logrus
занимает мало места для такой многофункциональной библиотеки журналирования:
128K github.com/Sirupsen/logrus.a
Можно оставить.
Заключение
Мы нашли способ вычислить размеры зависимостей в Go и сэкономили около 7 Мб. И решили, что не будем использовать определённые зависимости, а вместо них возьмем аналоги из стандартной библиотеки Go.
Более того, скажу, что, если сильно постараться и поэкспериментировать с набором зависимостей, то мы можем ужать наш двоичный файл с изначальных 12 Мб до 1,2 Мб.
Заниматься этим не обязательно, потому что зависимости в Go и так невелики по сравнению с другими платформами. Но вам обязательно нужно иметь под рукой инструменты, которые помогут лучше понимать то, что вы создаёте. И если вы разрабатываете ПО для окружений с весьма ограниченными доступными ресурсами, то одним из таких инструментов может быть gofat
.
P.S.: если хотите поэкспериментировать еще, вот референсный репозиторий: https://github.com/jondot/fattyproject.
Комментарии (16)
Ivan_83
28.02.2017 22:05-22Лучше бы сишечку учил, чем хернёй страдать.
blackstrip
01.03.2017 14:21-9гугл, вероятно, забашлял блогеру из Badoo за рекламу Go, а вам не нравится)
Это странный код, как будто для обладателей гуманитарного склада ума, куча пробелов и минимум слов, где две строки превращаются в монстра:
import( : "github.com/robertkrimen/otto" : for { : vm.Run(` abc = 2 + 2; console.log("\nThe value of abc is " + abc); // 4 `) : }
И статья — одна вода непонятная. Сделали стрёмный язык, а теперь одни костыли подпирают другими. Если на выходе получается толстое go-уно, то просто никто не будет на нем писать. Естественный отбор.Ivan_83
02.03.2017 00:07-4Язык явно не к месту.
И дальше понеслось: не было у бабы хлопот, купила баба парася.
16160 T crypto/sha512.block — у меня на венде 15 лет назад прога с гуем(!) и md5+sha1 + hmac занимала 15 кб — просто я выкинул сишный рантайм и секции помержил. Притом я точно знаю что никаких огромных таблиц в sha512 нет.
Такими темпамы мы скоро придём к тому, что унылое приложение хелловорлд будет включать в свой бинарник ещё и ядро какого нибудь линукса, а запускать его нужно будет в специально виртуалке.
Тогда то уже все адепты говнокодинга будут довольны — ведь теперь их поделия наконец то смогут не только у них в компе но и где то ещё.
Само приложение автора прекрасно проиллюстрировано картинкой:vvzvlad
02.03.2017 17:36+2Бессмысленно мериться «у кого программа меньше» без сравнения стоимости разработки и поддержки. Быструю и медленную программу можно написать на чистом асме. Ну и что, много вы видели разработок на нем? Даже для МК их мало, просто потому что проще заплатить памятью/скоростью за удобство разработки и возможность что-то поменять в этом коде через пять лет другим разработчиком.
GO — следующий шаг. Еще удобнее, еще больше накладных расходов. Ну и что? Если есть ресурсы, почему бы и нет.
Мне казалось, это настолько простая вещь, что можно принимать такой подход, можно не принимать и писать на асме дальше, но понимать-то должны все.Ivan_83
03.03.2017 04:26-6Что мне до твоей стоимости разработки и поддержки когда из за таких пограмистов приходится тысячам и миллионам людей заменять вполне работающую технику на новую чтобы оно меньше тормозило.
А в мире носимого барахла ещё и довольно остро стоит проблема потребления энергии, это дома никто не смотрит и не видит что тормозящая прога переводит деньги в тепло, а когда оно в кармане тёплое и разряжается быстро это сразу видно.
И опять же встаёт вопрос: для кого вы такой дерьмовый софт пишете? Неужели для себя?
Сходите куда нибудь в длинк, тплинк, лыжу, гнусмас и расскажите им что нужно всё писать на го, а они вам счётами в мозг объяснят что тратить больше денег на железо из за горепограмистов они не будут и что лучше наймут специалистов.
А в мобильных подразделениях ваше рац предложение могут не понять и спустить с лестницы сразу.
При этом мне всё равно на чём что то там накорябано (покуда не приходится собирать ещё и компелятор или интерпретатор для этого) и насколько оно плохо работает если:
— оно занимает пренебрежимо мало места в постоянной памяти
— оно занимает пренебрежимо мало места в оперативной памяти либо занимает не много и не на долго
— оно не висит в top~е сколь нибудь заметное время
Те даже скрипты вполне терпимы покуда запустились-отработали-вышли или висят в слипе и памяти меньше 1% жрут.
И не надо передёргивать в асм, сишчка вполне удобна, есть практически везде, и от асма не сильно далека в плане производительности а иногда и трансляции.
Го это не следующий шаг, я бы сказал что таких языков которые всё барахло носят с собой уже были: тот же вижал бейск, и особенно дельфи с их мегатоннами компонентов. Го до них как до луны как в количестве библиотек так в гуе и IDE.
Сишечка тоже умеет статически линковаться. И либ у неё больше всех.
Касательно 5 лет — это сильно оптимистичный прогноз для языка которому нет пяти лет.
Завтра гугелю это надоест и они его забросят, а через 5 лет будет полторы калеки на нём писать для музея.vvzvlad
04.03.2017 01:49+1Боже, какой ад. Даже комментировать дальше не хочется.
Ivan_83
05.03.2017 01:20-2Так и не нужно было лезть со своим псевдо манагерским подходом в технический топик.
Остальные адепты го трусливо засунули язык подальше и проминусовали — сказать то нечего, ибо автор полез с ГО туда куда не надо :)
С чисто эгоистической точки зрения: чем больше людей будут го/раст/1с/пхп/жава/луа/перл/питон программистами — тем больше мне будут платить и больше выбора что делать у меня будет, ибо потребность в си — она постоянная, от неё не возможно уйти, а на этих модных языках (го, раст) ещё ничего нужного и большого не написано.vvzvlad
05.03.2017 01:30+3Сказать нечего, потому что вы аргументы уже сказанные не воспринимаете, говорить смысла мало.
Begetan
03.03.2017 20:09Я бы хотел узнать больше про этого IoT агента на Go.
На Гихабе, код — пустышка, увы!
rustler2000
mkevac
Спасибо за дополнение. Жалко только вы не откомментировали его никак.
$ go tool link -h 2>&1 | egrep "(-w|-s)"
-s disable symbol table
-w disable DWARF generation
Эти два флага просят линкер не включать debug информацию в бинарник.
rustler2000
Честно говоря дар речи обронил.
Это как скомпайлить С++ с бустом, ужаснутся Х*10Мб бинарю, и вместо ```strip``` начать выпиливать буст.
Хотя если принять во внимание что наверное половина девов с рельсов на голанг ушла (гдето видал такие оценки), то оно более понятно.
Да и флаги эти гуляют то там то сям в топиках про релиз.
mkevac
Автор, как мне кажется, делает упор не на то, чтобы рассказать как уменьшить размер бинаря, а на то, чтобы показать как можно увидеть сколько зависимости прибавляют. Дать людям инструмент решить самим хотят ли они эту зависимость или нет.
rustler2000
За автора додумывать не стоит, но поскольку оговорок нету, то скорее всего и мыслей нету.
А тем временем,.а файлы это полные сорцы модулей со всемы символами — а сколько реально места после линковки они займут, зависит от того, что из них используется.
Тоесть это вообще сильно бесполезный инструмент, если не контрпродуктивный.