この記事は Go Advent Calendar 2018 7日目の穴埋め記事です。
シングルバイナリ最高ですよね!
Go4 Advent Calendar 2018 の2日目の記事 で、こんなことを書きました:
いちいち情シスにおうかがいたてなくても、ホームフォルダにコピるだけ使える!シングルバイナリが作れるGo言語最高ですね!
ちなみに、昔書いた別の投稿 でも同じこと書いてました。
俺、どんだけシングルバイナリ好きなんだよ・・・?
でも、こんな現象に遭遇したことありませんか?
ちょうしに乗って色々なサーバにバイナリをコピーして使ってる私みたいな人の中には、こんな目に遭ったことがある人もいるのでは?
myMacbook> GOOS=linux GOARCH=arm64 go build
myMacbook> scp ./kaito centos4.8:/home/myhome
myMacbook> ssh centos4.8
centos4.8> ./kaito -v
futexwakeup addr=0x6d02a0 returned -38
fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1006 pc=0x4277fb]
...(中略)...
goroutine 1 [runnable, locked to thread]:
syscall.Syscall6(0x10b, 0xffffffffffffff9c, 0x2a980140a0, 0x2a98058000, 0x80, 0x0, 0x0, 0xffffffffffffffff, 0x0, 0x26)
/Users/maki/.brew/Cellar/go/1.11.2/libexec/src/syscall/asm_linux_amd64.s:44 +0x5
syscall.readlinkat(0xffffffffffffff9c, 0x59955a, 0xe, 0x2a98058000, 0x80, 0x80, 0x55ab80, 0x56f301, 0x2a98058000)
/Users/maki/.brew/Cellar/go/1.11.2/libexec/src/syscall/zsyscall_linux_amd64.go:90 +0xbd
...(略)
なんか盛大にSEGVしてますね。
Goで作ったバイナリを、あまりに古いLinuxサーバで使うとこういうpanicに見舞われることがあります。こんなときどうすればいいのでしょうか?
答え: Go 1.4でビルドしましょう
いきなり答えですが、Go 1.4でビルドすることでだいたい解決します。
古いバージョンの Goツールは、こちらの Archived versions からダウンロードできます。
また、Mac なら Homebrew で古いバージョンの Go をインストールできます。今回はこちらの方法を使いましょう。
brew
コマンドを使ってインストールします。その際、忘れずにクロスコンパイラのオプションを指定しましょう:
myMacbook> brew install go@1.4 --with-cc-all
でも、go@1.4は keg-only のため、デフォルトではPATHが通ったところには置かれません。
なので、今回はフルパスを明示して実行します:
myMacbook> GOOS=linux GOARCH=amd64 ~/.brew/Cellar/go@1.4/1.4.3-20170922/bin/go build
さて、今度はどうでしょうか?
myMacbook> scp ./kaito centos4.8:/home/myhome
myMacbook> ssh centos4.8
centos4.8> ./kaito -h
Usage:
kaito [OPTIONS]
Application Options:
-G, --disable-gzip Disable Gzip decompression and pass through raw input.
-B, --disable-bzip2 Disable Bzip2 decompression and pass through raw input.
-X, --disable-xz Disable Xz decompression and pass through raw input.
-n, --force-native Force to use Go-native implentation of decompression algorithm.
-c, --stdout Write the decompressed data to standard output instead of a file. This implies --keep.
-k, --keep Don't delete the input files.
-d, --decompress Nop. Just for tar command.
Help Options:
-h, --help Show this help message
動きましたね。
なんで Go 1.4 なの?
上のスタックトレースを見てみると syscall.readlinkat
と書かれていることから、どうやら readlinkat
システムコールを呼び出そうとして失敗したことがわかります。
実は Go 1.5 から、Linuxのシステムコール呼び出しが変わっていまして、openat
とか readlinkat
等の「○○at」系のシステムコールを使うように標準ライブラリの syscall パッケージが軒並み書き換えられました1。
そして、この「○○at」系のシステムコールは、kernel 2.6.16 で導入された比較的新しいシステムコールなんですね2。
そして、さきほどのエラーを吐いていた「古いサーバ」は、こんな環境でした:
centos4.8> cat /etc/redhat-release
CentOS release 4.8 (Final)
centos4.8> uname --kernel-release
2.6.9-89.EL
カーネルが2.6.16以前なので、readlinkat
システムコールがまだ実装されていなかったというわけです。
CentOS と RedHat Enterprise Linux (RHEL)だと、この4.x系までが「○○at」系をサポートする以前のカーネルを使っています3。
まぁ、4.x系なんて2011年にサポートが終わっているので、さすがにもう使ってる人なんていねーだろ、と思ったそこのアナタ!・・・・いたんだよ!某社のWebサーバで、ガンガンにエンドユーザーにさらされてるサーバに2年前まで使われてたんだよ!!!!4
結局、Goを使うのに必要なLinuxのバージョンっていくつなの?
はい、それは明確に決められていまして、(現時点での)Minimum Requirement は kernel 2.6.23 と公式に設定されています。
そしてここには、
We don't support CentOS 5. The kernel is too old (2.6.18).
って明記されちゃってたりするんですね。
じゃあ、実際に CentOS 5系でなにか問題があるのか?
実は、「○○at」系システムコール以外にも、新しめのシステムコールとして epoll_pwait
っていうのがあります。
kernel 2.6.19 からなので、CentOS 4.xはもちろん、kernel 2.6.18 を使っている CentOS 5.x でも使えません。
なので、こんなプログラムを書いてですね、
package main
import (
"io"
"os"
)
func main() {
r, w, _ := os.Pipe()
go func() {
w.Write(([]byte)("Hello, world"))
w.Close()
}()
io.Copy(os.Stdout, r)
}
これをCentOS 5のサーバーにコピーして実行すると、
myMacbook> GOOS=linux GOARCH=amd64 go build -a
myMacbook> scp ./pipe centos5.11:/home/myhome
myMacbook> ssh centos5.11
centos5.11> ./pipe
runtime: epollwait on fd 5 failed with 38
fatal error: runtime: netpoll failed
runtime stack:
runtime.throw(0x47c243, 0x17)
/Users/maki/.brew/Cellar/go/1.11.2/libexec/src/runtime/panic.go:608 +0x72
runtime.netpoll(0xc00001e000, 0x0)
/Users/maki/.brew/Cellar/go/1.11.2/libexec/src/runtime/netpoll_epoll.go:75 +0x215
...(略)
怒られます。
しかし、Go 1.4 でビルドして実行すると、
myMacbook> GOOS=linux GOARCH=amd64 ~/.brew/Cellar/go@1.4/1.4.3-20170922/bin/go build -a
myMacbook> scp ./pipe centos5.11:/home/myhome
myMacbook> ssh centos5.11
centos5.11> ./pipe
Hello, world
ちゃんと動きましたね。
実は、Go 1.10 からポーリングの実装が epoll_wait
システムコールから epoll_pwait
システムコールを使うように変わりました。
なので、CentOS 5系をお使いの方は Go 1.9 までが使えますよ。良かったですね!
CentOS 5.xだってもう使ってる人なんていないでしょ?と思ったそこのアナタ!
実はこの5.x世代は、RedHat Enterprise Linux が 2020年11月まで Extended Life Cycle Support (ELS) を提供しているのです!!
そんなわけで、まだ2年近くは全国のレガシーなサーバールームで元気に稼働し続けているはずです。
この記事が、全国の古いサーバーを日夜支えている情シスの皆さんの一助になれば幸いです。
謝辞
この記事の内容は、Go 1.5 がリリースされた2015年頃、ギョームで作っていたコマンドラインツールが何故か動かなくなり、「なんでだよーヽ(`Д´#)ノ ムキー!!」ってなってた時に #Soozy 上の #golang チャンネルで質問したところ、 @mattn さんと @syohex さんに教えていただいたのが元ネタです。
この場を借りて、あらためて両氏に御礼申し上げます。
-
具体的にはこのコミットです: https://github.com/golang/go/commit/e7a7352e527ca275a2b66cc3cafde09836345a8f ↩
-
といっても、2.6.16 がリリースされたのは2006年のことなので、今は昔というやつですが。 ↩
-
ちなみにUbuntuでは同じメジャーバージョンの中でカーネルのバージョンが変わることがあるので一概には言えませんが、6.xくらいまでがあやしいです。 ↩
-
なお、自分が担当の時代にAWSに移してOSを最新にしたので、さすがにもう稼働していません。こういうサーバは、日本全国にひっそりとたくさんあるはず。それらの保守をしなければならない全国の情シスの皆さんにお見舞い申し上げます。 ↩