太いバイナリの物語

ここに画像の説明を入力してください







こんにちは 私の名前はマルコです(私はBadooのシステムプログラマーです)。 そして、私が興味深いと思った囲onの投稿の翻訳をあなたの注意に提示します。 濃いバイナリは本当にscられますが、同時に静的リンクと単一ファイルのレイアウトの便利さを称賛されました。 シックバイナリが最新のサーバーで問題にならなくても、組み込みシステムでは問題になります。 著者は、囲themでそれらを扱う彼の物語を説明します。







非常に限られたリソースで実行されるアプリケーションにとって、小さなファイルサイズは重要です。 この記事では、さまざまな低電力デバイスで動作するエージェントプログラムの作成を検討します。 それらのメモリとプロセッサのリソースは小さくなり、どれだけ予測することもできません。







Goバイナリは小さく、自己完結型です。Goでプログラムを作成すると、必要なものがすべて含まれた単一のバイナリファイルが作成されます。 コードがアプリケーションのごく一部を占めるJava、Node.js、Ruby、Pythonなどのプラットフォームと比較してください。他のすべては依存関係の束であり、自己完結型のパッケージを取得するにはパッケージ化する必要があります。







自己完結型のバイナリを作成する機能などの重要な利便性にもかかわらず、Goには依存関係のサイズを見積もる組み込みツールがないため、開発者はこれらの依存関係をファイルに含めるかどうかについて十分な情報に基づいた決定を行うことができます。







gofat



ツールは、Goプロジェクトの依存関係のサイズを把握するのに役立ちます。







IoT Agentの作成



世界中の低電力デバイスに展開されるIoTエージェントであるサービスの1つを考えて作成した方法について少しお話しします。 また、運用上の観点からそのアーキテクチャを検討してください。







サンプルコードは、 https//github.com/jondot/fattyprojectからダウンロードできます







まず、適切なCLI人間工学が必要なのでkingpin



使用しkingpin



これはCLIフラグとオプションのPOSIX互換ライブラリです(このライブラリが大好きなので、多くのプロジェクトで使用しました)。 しかし実際には、このライブラリを含む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



は、このような問題を解決するのに非常に便利なexpvars



ます。







expvars



を使用するには、マジックインポートが必要です。 マジック-インポート中に、ハンドラーが既存のHTTPサーバーに追加されるため。 これを行うには、 net/http



から動作する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))
      
      





制御しておらず、更新できない可能性が高いリモートデバイスでノンストップで実行されるサービスは、非常に安定している必要があります。 そのため、柔軟性を持たせることをお勧めします。 たとえば、カスタムコマンドとスクリプトを実行できるように、つまり、サービスを再デプロイまたは再起動せずにサービスの動作を変更するメカニズムを提供します。







任意のリモートスクリプトを実行するツールを追加します。 これは疑わしいように見えますが、エージェントまたはサービスの場合は、コードを実行するための組み込みランタイムサンドボックスを準備できます。 ほとんどの場合、ランタイム環境にはJavaScriptとLuaが組み込まれています。







Otto埋め込みJSエンジンを使用します。







 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



を使用してbrew install graphicsmagick



できます。







otool



は、OS Xでのみlddツールの代替です。これを使用して、バイナリファイルを分析し、リンクされているライブラリを見つけることができます。







 $ 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









 $ 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
      
      





依存関係自体に関しては、示されているディメンション(2番目の列)は正確である可能性がありますが、一般にこれらの値を取得して追加することはできません。







ゴファット



動作する最後のトリックが1つありました。 バイナリをコンパイルすると、Goは各依存関係の中間ファイルを生成してから、それらを単一のファイルに静的にリンクします。







gofat



-GoコードといくつかのUnixツールを組み合わせたシェルスクリプトをgofat



ます。 Goバイナリの依存関係のサイズを分析します。







 #!/bin/sh eval `go build -work -a 2>&1` && find $WORK -type f -name "*.a" | xargs -I{} du -hxs "{}" | gsort -rh | sed -es:${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 -es:${WORK}/::g
      
      





すべての場所からWORKフォルダープレフィックスを削除し、依存関係に関するデータを含むクリアされた文字列を表示します。







最も興味深いのは、バイナリファイルの12 MBとは何かということです。







重量を失う



初めて、IoTエージェントを使用したトイプロジェクトに関連してgofat



を起動します。 次のデータを取得します。







 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



するとビルド時間が大幅に増加することがgofat



ます。 実際には、 -a



モードでアセンブリを開始します。このモードでは、すべてが再構築されます。







これで、各依存症がどれだけのスペースを占めるかがわかりました。 袖をまくり上げて分析し、行動を起こしてください。







 1.8M net/http.a
      
      





HTTP処理に関連するものはすべて1.8 MBを消費します。 おそらくあなたはそれを捨てることができます。 expvar



を拒否しexpvar



、代わりに、プログラムの状態に関する非常に重要なパラメーターと情報をログファイルに定期的にダンプします。 これを頻繁に行うと、すべてがうまくいきます。







更新: Go 1.8 netのリリースに伴い、httpの重量が2.2 MBになりました。







 788K gopkg.in/alecthomas/kingpin.v2.a 388K github.com/alecthomas/template.a
      
      





そして、これは大きな驚きです。約1 MBがフラグ解析のための非常に便利なPOSIX機能によって占有されています。 それを拒否して標準ライブラリのパッケージを使用するか、フラグを廃止して環境変数から構成を読み取ることもできます(これにはある程度の時間がかかります)。







Newrelic



はさらに1.3 MBを追加するため、ドロップすることもできます。







 668K github.com/newrelic/go-agent.a 624K github.com/newrelic/go-agent/internal.a
      
      





`ザップも投げる。 ロギングには標準パッケージを使用します。







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 MB節約しました。 そして、特定の依存関係を使用しないことに決めましたが、代わりに標準のGoライブラリから類似物を取得します。







さらに、一生懸命に依存関係のセットを試してみると、元の12 MBから1.2 MBにバイナリを縮小できると言います。







Goの依存関係は他のプラットフォームに比べて既に小さいため、これを行う必要はありません。 ただし、作成しているものをよりよく理解するためのツールを手元に用意する必要があります。 また、利用可能なリソースが非常に限られている環境向けのソフトウェアを開発している場合、そのようなツールの1つはgofat



です。







PS:さらに実験したい場合は、参照リポジトリhttps://github.com/jondot/fattyprojectをご覧ください








All Articles