Pull to refresh

Comments 52

Сложно представить, конечно, сотню каких-то утилиток, но даже если и так, то в наше-то время дисковое пространство уже давно не проблема.
Кроме того, если размер очень критичен, то почему не скомпилировать через gccgo?
Ещё с большим размером может быть проблема с версионным контролем. Но опять же, в нём совершенно негоже хранить скомпилированные бинарники.
Бинарники при исполнении мапятся в оперативную память, причем разные инстансы одного бинарника в норме все ссылаются на одну и ту же память. Чем бинарник меньше — тем меньше потребление оперативки и меньше сопутствующие накладные расходы, за счет меньшего числа cache miss работать все будет немного быстрее.

Но в целом экономия на спичках конечно.
А нельзя чтоли рантайм вынести в динамически подключаемую библиотеку?
Добро пожаловать в мир golang'а.

И после этого ещё спрашивают, почему go не заменит Си.
А он должет заменить Си?
Были поползновения. Быстро закончилось, но как витающий стереотип — местами замечаю.
Ну как «убийцы» себя позиционируют, по моему, только Rust и Dlang. И то — «Убийцы C++».
А Go если и будет убийцей, то, скорее, всяких PHP/Python/Perl и иже с ними.
Ну, чисто формально, компилируемые языки всё-таки классом выше (ниже?) чем интерпретируемые с точки зрения операционной системы. Потому что ELF это ELF, а скрипты — это просто текстовые файлы. Ни suid'а, ни адекватного использования в роли init/udev/etc.

С другой стороны, потеснить perl/python в качестве «bash++» не получится по той же причине — компилять надо.
Ну компилять первый раз можно и при запуске — не смертельно.

#!/usr/bin/gorun

package main

func main() {
println(«Hello world!»)
}

ufm@msi ~ $ time ./t.go
Hello world!

real 0m0.036s
user 0m0.023s
sys 0m0.015s
ufm@msi ~ $ time ./t.go
Hello world!

real 0m0.005s
user 0m0.000s
sys 0m0.005s

Для сравнения:
ufm@msi ~ $ time python -c 'print «Hello world!»'
Hello world!

real 0m0.008s
user 0m0.004s
sys 0m0.004s

ufm@msi ~ $ time php -r 'echo «Hello world!»;'
Hello world!
real 0m0.022s
user 0m0.015s
sys 0m0.011s
О, а вот в такой форме, да, сможет стать на один уровень.
это было сделано намеренно, чтобы скопировал файл куда угодно и он работает, то есть когда на один сервер задеплоили 5 приложений у них не будет сломанных зависимостей. Хотя намечается тенденция сделать поддержку создания библиотек на go (в основном из-за android и там это как-то даже работает)
Ну и сами библиотеки (пакеты) go поставляются в виде исходников, поэтому плодить библиотеки (so, dll) не имеет смысла
Тажке компилятор go делает очень много inline оптимизаций, из-за этого один и тот же код может встречаться как в inline виде, так и обычном.
Попробуйте сжимать испольняемые бинари upx'ом.
upx работает для разных архитектур linux и работает для «386-х» бинарников для маков и freebsdb:
upx.sourceforge.net/, таблица «Supported executable formats».
Попробовал. Штатно не работает — ругается на неправильный размер файла.

Есть проект, который правит бинарник перед сжатием upx, но с подробным устройством бинаников я не знаком и неизвестно насколько такие правки переносимы и будет ли это дополнительным источником багов когда-то потом, когда я уже привыкну что всё работает.
У меня отлично сжимает бинарники Go программ и на OSX, и под Linux.
Что я делаю не так?
Обычно работаю с go 1.2.1, для проверки скачал последнюю версию. upx — из репозитория ubuntu 14.04

Заголовок спойлера
cd utils
GOROOT=/home/rekby/Downloads/go/ GOTOOLDIR=/home/rekby/Downloads/go/pkg/tool/linux_amd64/ /home/rekby/Downloads/go/bin/go build
upx utils 


                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: utils: EOFException: premature end of file                                

Packed 1 file: 0 ok, 1 error.

bash-3.2$ cat server.go
package main

import (
	"log"
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func (rw http.ResponseWriter, req *http.Request) {
		log.Printf("new request")
		fmt.Fprintf(rw, "Hello %s", "World")
	})
	log.Printf("server starting")
	http.ListenAndServe(":8080", nil)
}


bash-3.2$ go build -o server server.go

bash-3.2$ ls -l
total 11320
-rwxr-xr-x  1 miolini  staff  5790996 Feb 24 15:00 server
-rw-r--r--  1 miolini  wheel      278 Feb 24 15:00 server.go

bash-3.2$ upx --lzma server
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
   5790996 ->   1204224   20.79%   Mach/AMD64    server

Packed 1 file.

bash-3.2$ ls -l
total 2360
-rwxr-xr-x  1 miolini  staff  1204224 Feb 24 15:00 server
-rw-r--r--  1 miolini  wheel      278 Feb 24 15:00 server.go

Проверил подробнее — при компилировании с GOARCH=386 — действительно сжимается и с виду работает — попробую использовать в работе.

При компилировании 64-битного бинарника — ошибка.
Вы говорили, но ссылку не дали. Исправление бинарников работает нормально, к багам не приводит.
Угу, или хранить только сжатые исходники, а компилировать прямо на RAM-диск, в /tmp каталог )))
Причем тут исходники?
Странный вопрос. Потому, что лучше сжимается.
Если уж стали делать «как в busybox» — то почему не использовали их вариант выбора команды до конца?

Иными словами, почему используется ./any --multiex-command=test asdf вместо ./any test asdf?
Это намеренное отступление — для унификации.

параметр --multiex-command включается глобально и работает с любым именем вызова, а не только с неизвестным.

Т.е. например если есть экспортированная функция test, которая принимает параметр run и собственно экспортированная функция run. Что должна выполнить команда ./test run?
Если имя файла совпадает с именем функции — выполняться должна именно функция. Формат с указанием функции первым аргументом — только для исполнимого файла с «основным» именем.

PS представил себе:
cat --busybox-command=bash -c ...

Интересно, если бы кто-нибудь так и сделал, привело бы это к новым необычным уязвимостям?
Тут есть заметное отличие от busybox — busybox он всегда один, а вот такие multiex-ов может быть несколько, поэтому например он и свой модуль не инсталлирует по умолчанию и основного названия файла у него тоже нет — каждый разработчик может скомпилировать его по-своему и разные имена назначать кто символьными ссылками, кто жесткими. Как в этих условиях отличить главный вызов от неглавного (а просто с непредусмотренным именем) не понятно.

При необходимости такого же поведения первого параметра как у busybox можно написать модуль, вроде:

func asbb(){
  os.Args = os.Args[1:]
  multiex.Main()
}

func main(){
multiex.Register(multiex.ExecutorDescribe{Name: "asbb", Function: asbb})
}


Тогда при ./asbb cat 123 будет вызвана программа cat 123.

Про уязвимости — согласен, с sudo могут быть проблемы. Тут не досмотрел (в моём контексте это не актуально). Возможно такой глобальный параметр стоит сделать только опционально включаемым.
подумал, решил что такой параметр для принудительного вызова команд действительно в некоторых сценариях (например sudo) может приводить к уязвимостям, кроме того это менее удобно чем просто первый аргумент, как в busybox.

Переделал.

Заодно еще и русский перевод везде добавил — посмотрим насколько это будет удобно для русскоязычных коллег.
Давно заметил, что Go-бинарники неплохо strip-аются, процентов на 20-25 худеют.
Ага, но не всегда работают потом, особенно reflection ;)
Go перемудрили со своим рантаймом. Почему бы не сделать как в том-же языке Crystal где бинарник линкуентся только лишь с системными библиотеками и не содержит никакого мусора. Как результат простой HTTP сервер на Кристале занимает десятки килобойт а на Go мегабайты.

Зачем Го хранить весть этот мусор в бинарниках?
Что вы подразумеваете под «мусором»?

Дизайн и решения каждого языка — это всегда компромиссы между десятками вещей — памятью и скоростью, тем, что разрешено программисту, а что отдано на откуп компилятору и так далее. Выбор в пользу одного или другого нюанса выбирает с точки зрения реалий, опыта и здравого смысла.
Если 20 лет еще было актуально экономить каждый кило(мега)байт дискового пространства, в ущерб простоте деплоя и кросскомпиляции, то сейчас мир изменился — лишние 5 мб на диске — это буквально ничто, зато профит от упрощения процедур деплоя и кросскомпиляции — колоссальный.

Те, кто в 2015-м требуют бинарников размеров в килобайты любой ценой — что-то важное упускают, имхо.
Например чтобы один и тот же бинарник работал на любом линуксе, независимо от окружающих библиотек — это очень удобно.
1.5 уже умеет линковать динамически. Понимаю, что комментарий устарел, но мало ли)

image
а это вы как так собирали, что размер 9Кб получился?
я разные режимы сборки попробовал — везде около 1Мб минимум. Если использовать пакет net (например net/http) — бинарник действительно получается с динамической линковкой, но размер всё равно большой.

P.S. и заодно может знаете способ принудительно собирать статический бинарник даже при использовании сети? очень уж хорошо в прежних версиях иметь бинарник без зависимостей.
В моём случае нагрузки оч. маленькие и как раз простое получение бинарника без зависимостей, да еще и с простой кросс-компиляцией для меня одно из самых больших преимуществ go перед другими языками.
Надо признать, что про динамическую линковку с net/http только узнал, посмотрел через ldd — действительно O_o.

По поводу того, как собирал:

go install -buildmode=shared std

go build -linkshared hello.go


Для примера, helloworld ниже после компиляции у меня занимает ~15KB, но сервер посложнее с mgo и gin может и мегабайт-два занимать. В теории, если эти библиотеки скомпилировать с -buildmode=shared, то тоже можно прилинковать динамически. На практике пока особо не вникал, в golang только неделю как пришел.

package main

import (
	"io"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, string("Hello world"))
	})

	log.Print("Server started at http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}


да, тоже получилось — я
go install -buildmode=shared std

не делал до этого.

Но есть но:
1. При распространении придется таскать за собой скомпилированный libstd.so, а это в данный момент 37МБ.
2. Снова возвращаемся к зависимостям что для этого бинарника нужна одна версия libstd, для другого — другая и т.п.

Так что меня даже больше интересует какой-то способ принудительно собирать статический бинарник даже при работе с сетью. Чтобы dns-запросы как раньше работали «медленно и неэффективно», в отдельном процессе (не горутине) и т.п., но без внешних зависимостей.
Ну здесь уже палка о двух концах. К примеру на сервере, где крутится сотня-другая микросервисов — имеет смысл вынести libstd. Ну и если go все же получит распространение, и в репозитариях операционок будет достаточное количество приложений с зависимостью от пакета libstd, то он установится только с первой программой.

По поводу второго вопроса, попробуйте сделать так (нагуглил):

go build -ldflags "-linkmode external -extldflags -static" hello.go
К сожалению не знаком с windows
Там же и решение приведено: «Хороший пакетный менеджер».
Сферический и в вакууме :-)

Although these repositories are often huge it is not possible to have every piece of software in them, so dependency hell can still occur. In all cases, dependency hell is still faced by the repository maintainers.


А вот этот кусок мне особенно нравится:

В таком случае удовлетворение долгой цепи зависимостей пакетов даже может привести, например, к запросу другой версии библиотеки glibc, одной из крайне важных, основополагающих системных библиотек. Если это случается пользователю будет предложено удалить тысячи пакетов, что по сути будет равноценно удалению, например, 80% системы, включая графические оболочки и сотни различных программ.
Насколько я помню, сейчас уже ничего не мешает ставить две разные версии glibc в систему.
По поводу stdlib я как раз про то же что и vintage парой коментов выше.
Для каждого бинарника с динамической линковкой потребуется stdlib, собранный из той же версии исходников что и бинарник. Это не зависит от операционки — в Linux будет так же.

Т.е. например включат в дистрибутив stdlib от go 1.5, потом кто-то что-то напишет кто-то что-то на 1.5.1 и нужен уже новый stdlib, т.к. там код поменялся (ошибки поправлены), то же с 1.5.2, потом 1.6 — там уже и реализация runtime скорее всего поменяется и т.д.
Т.е. рассчитывать на системную библиотеку не получится и runtime придётся всё равно надо таскать с собой.
т.е. это может иметь смысл в случае как я описывал — когда сотня мелких утилит по три строчки в каждой, они все вместе компилируются и потом копируются на целевые серверы и тогда с ними можно в нагрузку дать stdlib, правда придется еще учитывать настройки окружения — чтобы подцеплялся именно этот stdlib.so, а не какой-то еще.
Версии библиотек неплохо разруливаются пакетными менеджерами ОС. Если библиотеки обратно совместимы, то достаточно их переодически обновлять. Мне кажется проблема надумана.

Если же компилировать микросервисы в один экзешник, то теряются многие плюсы такой структуры. Например, обновления (или, не дай бог, ошибки) одного микросервиса, будут затрагивать все остальные, чего очень хотелось бы, особенно с проектами в активной фазе разработки.
Насколько я понял docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGhFDelrAP0NqSS_00RgZQ/edit?pli=1 раздел Multiples copies of a Go package — пока что обратной совместимости нет. Она может появиться когда-то потом, но без конкретных планов.

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


Об этом я думал, но не всё так плохо:
1. Работают микросервисы независимо друг от друга, хоть и находятся в одном бинарнике — кодовая база у них раздельна, зависимостей нет. Просто они разделяют общие внешние зависимости. Т.е. если в коде одного из них есть ошибка и он будет падать — на других микросервисах это не отразится, т.к. будут работать другие копии процесса, где ошибочный код не выполняется, т.к. работает другой микросервис/команда.
При запуске каждой внешней команды запускается только одна команда/сервис. С другими она не пересекается. Единственное исключение — код из init-функций выполняется до вызова main в любом случае из всех модулей.

2. Есть проблема что при обновлении бинарника обновляется сразу всё что в него скомпилировано. Но это вполне решается на уровне сборки — т.е. можно разрабатывать микросервисы/команды отдельно, а в общем сборщике для публикации внешним инструментом фиксировать версии, которые туда попадают, независимо от того на каких этапах разработка происходит. Это например легко решается через системы контроля версий: submodules в git и externals на конкретную ревизию в SVN. Обновлять можно так же по одному сервису за раз — как и обычная публикация обычно происходит.
Тогда бинарник включает в себя нужные версии микросервисов — собственно когда мы просто копируем этот набор команд/микросервисов на целевую систему (в бинарниках) — там тоже получается свой набор.
Просто тут он фиксируется на этапе сборки, а не на этапе публикации.
Sign up to leave a comment.

Articles