enter image description here


Привет. Меня зовут Марко (я системный программист в 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)


  1. rustler2000
    28.02.2017 18:25
    +5

    foo@kopoba~/go/src/github.com/fattyproject$ go build; ls -la fattyproject 
    -rwxrwxr-x 1 foo foo 13409025 Feb 28 17:18 fattyproject
    
    
    foo@kopoba~/go/src/github.com/fattyproject$ go build -ldflags "-s -w"; ls -la fattyproject 
    -rwxrwxr-x 1 foo foo 8939968 Feb 28 17:18 fattyproject
    


    1. mkevac
      28.02.2017 18:43
      +3

      Спасибо за дополнение. Жалко только вы не откомментировали его никак.

      $ go tool link -h 2>&1 | egrep "(-w|-s)"
      -s disable symbol table
      -w disable DWARF generation

      Эти два флага просят линкер не включать debug информацию в бинарник.


      1. rustler2000
        28.02.2017 20:27
        +8

        Честно говоря дар речи обронил.
        Это как скомпайлить С++ с бустом, ужаснутся Х*10Мб бинарю, и вместо ```strip``` начать выпиливать буст.
        Хотя если принять во внимание что наверное половина девов с рельсов на голанг ушла (гдето видал такие оценки), то оно более понятно.

        Да и флаги эти гуляют то там то сям в топиках про релиз.


        1. mkevac
          28.02.2017 20:34
          +3

          Автор, как мне кажется, делает упор не на то, чтобы рассказать как уменьшить размер бинаря, а на то, чтобы показать как можно увидеть сколько зависимости прибавляют. Дать людям инструмент решить самим хотят ли они эту зависимость или нет.


          1. rustler2000
            28.02.2017 20:59
            +4

            За автора додумывать не стоит, но поскольку оговорок нету, то скорее всего и мыслей нету.
            А тем временем,.а файлы это полные сорцы модулей со всемы символами — а сколько реально места после линковки они займут, зависит от того, что из них используется.
            Тоесть это вообще сильно бесполезный инструмент, если не контрпродуктивный.


  1. Ivan_83
    28.02.2017 22:05
    -22

    Лучше бы сишечку учил, чем хернёй страдать.


    1. 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-уно, то просто никто не будет на нем писать. Естественный отбор.


      1. Ivan_83
        02.03.2017 00:07
        -4

        Язык явно не к месту.
        И дальше понеслось: не было у бабы хлопот, купила баба парася.

        16160 T crypto/sha512.block — у меня на венде 15 лет назад прога с гуем(!) и md5+sha1 + hmac занимала 15 кб — просто я выкинул сишный рантайм и секции помержил. Притом я точно знаю что никаких огромных таблиц в sha512 нет.

        Такими темпамы мы скоро придём к тому, что унылое приложение хелловорлд будет включать в свой бинарник ещё и ядро какого нибудь линукса, а запускать его нужно будет в специально виртуалке.
        Тогда то уже все адепты говнокодинга будут довольны — ведь теперь их поделия наконец то смогут не только у них в компе но и где то ещё.

        Само приложение автора прекрасно проиллюстрировано картинкой:image


        1. vvzvlad
          02.03.2017 17:36
          +2

          Бессмысленно мериться «у кого программа меньше» без сравнения стоимости разработки и поддержки. Быструю и медленную программу можно написать на чистом асме. Ну и что, много вы видели разработок на нем? Даже для МК их мало, просто потому что проще заплатить памятью/скоростью за удобство разработки и возможность что-то поменять в этом коде через пять лет другим разработчиком.
          GO — следующий шаг. Еще удобнее, еще больше накладных расходов. Ну и что? Если есть ресурсы, почему бы и нет.

          Мне казалось, это настолько простая вещь, что можно принимать такой подход, можно не принимать и писать на асме дальше, но понимать-то должны все.


          1. Ivan_83
            03.03.2017 04:26
            -6

            Что мне до твоей стоимости разработки и поддержки когда из за таких пограмистов приходится тысячам и миллионам людей заменять вполне работающую технику на новую чтобы оно меньше тормозило.
            А в мире носимого барахла ещё и довольно остро стоит проблема потребления энергии, это дома никто не смотрит и не видит что тормозящая прога переводит деньги в тепло, а когда оно в кармане тёплое и разряжается быстро это сразу видно.

            И опять же встаёт вопрос: для кого вы такой дерьмовый софт пишете? Неужели для себя?

            Сходите куда нибудь в длинк, тплинк, лыжу, гнусмас и расскажите им что нужно всё писать на го, а они вам счётами в мозг объяснят что тратить больше денег на железо из за горепограмистов они не будут и что лучше наймут специалистов.
            А в мобильных подразделениях ваше рац предложение могут не понять и спустить с лестницы сразу.

            При этом мне всё равно на чём что то там накорябано (покуда не приходится собирать ещё и компелятор или интерпретатор для этого) и насколько оно плохо работает если:
            — оно занимает пренебрежимо мало места в постоянной памяти
            — оно занимает пренебрежимо мало места в оперативной памяти либо занимает не много и не на долго
            — оно не висит в top~е сколь нибудь заметное время
            Те даже скрипты вполне терпимы покуда запустились-отработали-вышли или висят в слипе и памяти меньше 1% жрут.

            И не надо передёргивать в асм, сишчка вполне удобна, есть практически везде, и от асма не сильно далека в плане производительности а иногда и трансляции.

            Го это не следующий шаг, я бы сказал что таких языков которые всё барахло носят с собой уже были: тот же вижал бейск, и особенно дельфи с их мегатоннами компонентов. Го до них как до луны как в количестве библиотек так в гуе и IDE.
            Сишечка тоже умеет статически линковаться. И либ у неё больше всех.

            Касательно 5 лет — это сильно оптимистичный прогноз для языка которому нет пяти лет.
            Завтра гугелю это надоест и они его забросят, а через 5 лет будет полторы калеки на нём писать для музея.


            1. vvzvlad
              04.03.2017 01:49
              +1

              Боже, какой ад. Даже комментировать дальше не хочется.


              1. Ivan_83
                05.03.2017 01:20
                -2

                Так и не нужно было лезть со своим псевдо манагерским подходом в технический топик.
                Остальные адепты го трусливо засунули язык подальше и проминусовали — сказать то нечего, ибо автор полез с ГО туда куда не надо :)

                С чисто эгоистической точки зрения: чем больше людей будут го/раст/1с/пхп/жава/луа/перл/питон программистами — тем больше мне будут платить и больше выбора что делать у меня будет, ибо потребность в си — она постоянная, от неё не возможно уйти, а на этих модных языках (го, раст) ещё ничего нужного и большого не написано.


                1. vvzvlad
                  05.03.2017 01:30
                  +3

                  Сказать нечего, потому что вы аргументы уже сказанные не воспринимаете, говорить смысла мало.


                  1. Ivan_83
                    09.03.2017 05:29

                    За стоимость разработки и ресурсов я вам и ответил, а вы сдулись.
                    Остальное — дешёвые ораторские приёмы.


                    1. vvzvlad
                      09.03.2017 18:40

                      Подскажите, что из той адовой простыни, что вы мне написали, можно считать ответом «за стоимость разработки»?


  1. Begetan
    03.03.2017 20:09

    Я бы хотел узнать больше про этого IoT агента на Go.

    На Гихабе, код — пустышка, увы!