【小ネタ】すげー古いLinuxサーバーでGo言語を使うときに注意すること

　この記事は [Go Advent Calendar 2018](https://qiita.com/advent-calendar/2018/go) ７日目の穴埋め記事です。


# シングルバイナリ最高ですよね！

[Go4 Advent Calendar 2018 の２日目の記事](https://qiita.com/Maki-Daisuke/items/c80e7c6755f758a0bd76) で、こんなことを書きました：

> いちいち情シスにおうかがいたてなくても、ホームフォルダにコピるだけ使える！シングルバイナリが作れるGo言語最高ですね！

　ちなみに、[昔書いた別の投稿](https://qiita.com/Maki-Daisuke/items/79146b1c447a36ccf306) でも同じこと書いてました。
　俺、どんだけシングルバイナリ好きなんだよ・・・？


# でも、こんな現象に遭遇したことありませんか？

　ちょうしに乗って色々なサーバにバイナリをコピーして使ってる私みたいな人の中には、こんな目に遭ったことがある人もいるのでは？

```
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](https://golang.org/dl/) からダウンロードできます。
　また、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]。

[^1]: 具体的にはこのコミットです： https://github.com/golang/go/commit/e7a7352e527ca275a2b66cc3cafde09836345a8f

[^2]: といっても、2.6.16 がリリースされたのは2006年のことなので、今は昔というやつですが。

　そして、さきほどのエラーを吐いていた「古いサーバ」は、こんな環境でした：

```
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サーバで、ガンガンにエンドユーザーにさらされてるサーバに２年前まで使われてたんだよ！！！！[^4]

[^3]: ちなみにUbuntuでは同じメジャーバージョンの中でカーネルのバージョンが変わることがあるので一概には言えませんが、6.xくらいまでがあやしいです。

[^4]: なお、自分が担当の時代にAWSに移してOSを最新にしたので、さすがにもう稼働していません。こういうサーバは、日本全国にひっそりとたくさんあるはず。それらの保守をしなければならない全国の情シスの皆さんにお見舞い申し上げます。


# 結局、Goを使うのに必要なLinuxのバージョンっていくつなの？

　はい、それは明確に決められていまして、（現時点での）[Minimum Requirement は kernel 2.6.23 と公式に設定されています](https://github.com/golang/go/wiki/MinimumRequirements)。
　そしてここには、

> 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 でも使えません。

　なので、こんなプログラムを書いてですね、

```golang
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) を提供している](https://access.redhat.com/articles/2901061)のです！！
　そんなわけで、まだ２年近くは全国のレガシーなサーバールームで元気に稼働し続けているはずです。

　この記事が、全国の古いサーバーを日夜支えている情シスの皆さんの一助になれば幸いです。


# 謝辞

　この記事の内容は、Go 1.5 がリリースされた2015年頃、ギョームで作っていたコマンドラインツールが何故か動かなくなり、「なんでだよーヽ(｀Д´#)ﾉ ﾑｷｰ!!」ってなってた時に [#Soozy](https://soozy.fushihara.net) 上の #golang チャンネルで質問したところ、 @mattn さんと @syohex さんに教えていただいたのが元ネタです。
　この場を借りて、あらためて両氏に御礼申し上げます。