はじめに
Zeals Advent Calendar 2019 の16日目の記事です。
Zeals でデータエンジニアをやっている鍵本です。本日はデータエンジニアリングの話ではなく、これまでインフラエンジニアなどの経験で培った知識を生かして、弊社バックエンドエンジニアと闘った標題に関するお話とその後日談を紹介します。
背景
弊社のサービス Zeals (旧fanp) は Alpine Linux の Docker イメージにアプリケーションをデプロイして GKE 上に展開していました。 Alpine イメージを採用した理由は、イメージのサイズを小さくしてCIやデプロイを早くすることにあります。その後、機能追加等でアプリケーションが肥大化し、責務の切り分けをした方がいいだろうということで、マイクロサービス化に取り組むことになりました。詳細は gRPC(protobuf)をモノリシックなRailsアプリケーションに導入する を参照してください。
環境
- Docker Image: ruby:2.5.5-alpine
- grpc: 1.24.0
- google-protobuf: 3.9.1
- Rails: 5.2.3
gRPC を Rails アプリに導入してはまった話
対処法の結論
- Alpine イメージを諦めて Debian イメージにする
- grpc および google-protobuf gem をソースビルドする
- Alpine 用の gem を作ってローカル gem サーバに配置する
何が問題か?
ruby から C で書かれた関数群を呼ぶような gem パッケージには以下に示すそれぞれの環境用の共有ライブラリが用意されています。
- x86_64-linux
- x86-linux (32bit Linux)
- universal-darwin (MacOS)
- x86-mingw32 (32bit Windows OS)
- x64-mingw32 (64bit Windows OS)
今日のテーマである grpc や google-protobuf も然りです。単に bundle install
すると、Alpine Linux では x86_64-linux 用がインストールされます。
Debian, Ubuntu, CentOS などの Linux 環境では ld-linux-x86-64.so.2 が実行可能プログラムから最初に参照されます。しかし Alpine Linux ではこれがありません。そこで以下のようなワークアラウンドが報告されております。
cd /lib
ln -s ld-musl-x86_64.so.1 ld-linux-x86-64.so.2
しかしこれでも動かないプログラムは存在します。それは Alpine Linux が GNU C Library を使っていないからです。
gem の中に内包されている x86_64-linux 用の共有オブジェクトには、これに強く依存したものが存在し、google-protobuf もその一つに数えられます。そのため、x86_64-linux 用に既にビルドされたものを Alpine Linux では使うことができないというわけです。実際に使ってみればわかりますが、セグメンテーション違反で異常終了します。Rails で使用した場合には、デプロイ時の bundle exec rails assets:precompile
で落ちることでしょう。
どうするか?
二通りの方法があります。
- Alpine を諦める
- grpc や google-protobuf を Alpine Linux 環境用としてソースビルドする
Alpine を諦める
これは簡単ですね。DockerHub に登録されているイメージを見ると ruby が予めインストールされた Debian のイメージが利用可能です。また -slim
が付いているものを選択すればイメージの容量を小さく保つことができます。
この場合の注意点は、Alpine のイメージと既にインストールされているパッケージに差異があるということです。弊社では nc
コマンドや日本語ロケールがなくて、確認作業中に「あれ?」ってなりました。
ソースビルドする
bundle コマンドには環境変数 BUNDLE_FORCE_RUBY_PLATFORM
が存在し、この値を 1 にすることでソースビルドになります。この時の問題点は、ソースビルドが可能なパッケージ全てがその対象となってしまうということです。Rails アプリでは多数の gem パッケージを用いているため、場合によっては bundle install
に時間がかかりすぎるかもしれません。
弊社では gRPC(protobuf)をモノリシックなRailsアプリケーションに導入する でも記載しましたように、トリッキーな方法で grpc と google-protobuf だけソースビルドするようにしました。この方法は bundler が二回動くのでそのオーバーヘッドが大きくなります。
そこで bundle install を一回にする次の方法も試してみました。
bundle install
gem uninstall grpc google-protobuf --force
gem install grpc:`grep '^ \{4\}grpc (' Gemfile.lock | sed -e 's/.*(\([^)]\+\))/\1/'` \
google-protobuf:`grep '^ \{4\}google-protobuf (' Gemfile.lock | sed -e 's/.*(\([^)]\+\))/\1/'` --platform ruby -i /usr/local/bundle
その他の方法
正直なところ毎回 gem のソースビルド+インストールは面倒です。自社で gem サーバを運用しているのであれば、Alpine Linux 用の gem を予め作成してローカル gem サーバに登録しておけば、bundle install だけで済みます。弊社にはまだなかったので、このためだけに gem サーバを立てるのはコスト高と判断してこの方法は諦めました。
後日談
どうしてセグメンテーション違反になるのか気になったので少し調べてみました。特に結論というほどのものはありませんが、興味があれば読んでいただけると幸いです。
テスト環境の準備
Rails アプリでテストするのは大変なので、mogulla3 さんの gRPC × Rubyのチュートリアルをカスタムしてやってみた のプログラムをお借りして実施することにします。
環境構築
以下の Dockerfile でイメージを起動しておきます。
FROM ruby:2.5.5-alpine
ENV BUNDLER_VERSION=2.0.2
RUN apk -U --no-cache add build-base bash gdb && \
gem install bundler
WORKDIR /tmp
COPY Gemfile Gemfile.lock server.rb /tmp/
COPY lib /tmp/lib
COPY proto /tmp/proto
RUN bundle install
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "google-protobuf", "3.9.1"
gem "grpc"
docker build -t ruby:grpc-test .
docker run -it --cap-add=SYS_PTRACE --security-opt="seccomp=unconfined" ruby:grpc-test /bin/bash
GNU debugger (gdb) をコンテナ内で利用しますので、@kutsurogi194 さんの Dockerコンテナにgdbによるデバッグ環境の構築 を参考にコンテナを立ち上げています。
このままでは共有ライブラリがないのでシンボリックリンクを作っておきます。
cd /lib/
ln -s ld-musl-x86_64.so.1 ld-linux-x86-64.so.2
セグメンテーション違反の発生箇所の確認
では server.rb を起動してみましょう。
cd /tmp
bundle install --path vendor/bundle
bundle exec ruby server.rb
/tmp/lib/proto/card_pb.rb:27: [BUG] Segmentation fault at 0x0000000000019296
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-linux-musl]
-- Control frame information -----------------------------------------------
c:0007 p:---- s:0027 e:000026 CFUNC :msgclass
c:0006 p:0048 s:0023 E:001c80 TOP /tmp/lib/proto/card_pb.rb:27 [FINISH]
c:0005 p:---- s:0020 e:000019 CFUNC :require
c:0004 p:0013 s:0015 e:000014 TOP /tmp/lib/proto/card_services_pb.rb:7 [FINISH]
c:0003 p:---- s:0012 e:000011 CFUNC :require
c:0002 p:0024 s:0007 E:001438 EVAL server.rb:4 [FINISH]
c:0001 p:0000 s:0003 E:000040 (none) [FINISH]
-- Ruby level backtrace information ----------------------------------------
server.rb:4:in `<main>'
server.rb:4:in `require'
/tmp/lib/proto/card_services_pb.rb:7:in `<top (required)>'
/tmp/lib/proto/card_services_pb.rb:7:in `require'
/tmp/lib/proto/card_pb.rb:27:in `<top (required)>'
/tmp/lib/proto/card_pb.rb:27:in `msgclass'
-- Machine register context ------------------------------------------------
RIP: 0x0000000000019296 RBP: 0x00007ffcf0ce6cb0 RSP: 0x00007ffcf0ce6c68
RAX: 0x0000000000000000 RBX: 0x000055c4cebf7e60 RCX: 0x0000000000000008
RDX: 0x0000000000000001 RDI: 0x00007ffcf0ce6c70 RSI: 0x00007ffcf0ce6c7e
R8: 0x00007ffcf0ce6c7d R9: 0x0000000000000005 R10: 0x0000000000000000
R11: 0x0000000000000297 R12: 0x0000000000000000 R13: 0x0000000000000000
R14: 0x00007ffcf0ce6d18 R15: 0x000055c4cebf7e60 EFL: 0x0000000000010246
セグメンテーション違反で異常終了しましたね。もう少し情報が欲しいので gdb で調べてみましょう。
bundle exec gdb ruby
(gdb) run server.rb
bt
コマンドで最初の 10 フレーム分だけ掲載すると以下のようになります。
Starting program: /usr/local/bin/ruby server.rb
[New LWP 128]
Thread 1 "ruby" received signal SIGSEGV, Segmentation fault.
0x0000000000019296 in ?? ()
(gdb) bt
#0 0x0000000000019296 in ?? ()
#1 0x00007ffff70371fc in putchecktag () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#2 0x00007ffff70374df in compile_methods () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#3 0x00007ffff70381e3 in mgroup_new () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#4 0x00007ffff70383e5 in upb_pbcodecache_getdecodermethod () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#5 0x00007ffff7038469 in upb_pbdecodermethod_new () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#6 0x00007ffff7018acc in new_fillmsg_decodermethod () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#7 0x00007ffff7013bbd in build_class_from_descriptor () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#8 0x00007ffff70094b8 in Descriptor_msgclass () from /usr/local/bundle/gems/google-protobuf-3.9.1-x86_64-linux/lib/google/2.5/protobuf_c.so
#9 0x00007ffff7e52e89 in vm_call_cfunc_with_frame (ci=0x555556036710, cc=<optimized out>, calling=<optimized out>, reg_cfp=0x7ffff7bebf00, ec=0x5555555582e8) at vm_insnhelper.c:1918
フレーム 0 のプログラムカウンター ($pc の値) が 0x19296 と、いかにも怪しい値になっています。直前の putchecktag
で止めてみましょう。
(gdb) b putchecktag
(gdb) run
最初の24命令だけ見ると、この関数から upb_fielddef_number
と __memcpy_chk
にジャンプしているようです。
(gdb) x/24i $pc
=> 0x7ffff70371a4 <putchecktag+4>: push %r13
0x7ffff70371a6 <putchecktag+6>: push %r12
0x7ffff70371a8 <putchecktag+8>: push %rbx
0x7ffff70371a9 <putchecktag+9>: mov %rdi,%rbx
0x7ffff70371ac <putchecktag+12>: mov %rsi,%rdi
0x7ffff70371af <putchecktag+15>: mov %edx,%r13d
0x7ffff70371b2 <putchecktag+18>: mov %ecx,%r12d
0x7ffff70371b5 <putchecktag+21>: sub $0x28,%rsp
0x7ffff70371b9 <putchecktag+25>: mov %fs:0x28,%rax
0x7ffff70371c2 <putchecktag+34>: mov %rax,-0x28(%rbp)
0x7ffff70371c6 <putchecktag+38>: xor %eax,%eax
0x7ffff70371c8 <putchecktag+40>: callq 0x7ffff7006530 <upb_fielddef_number@plt>
0x7ffff70371cd <putchecktag+45>: shl $0x3,%eax
0x7ffff70371d0 <putchecktag+48>: or %r13d,%eax
0x7ffff70371d3 <putchecktag+51>: jne 0x7ffff7037280 <putchecktag+224>
0x7ffff70371d9 <putchecktag+57>: lea -0x32(%rbp),%rsi
0x7ffff70371dd <putchecktag+61>: movb $0x0,-0x32(%rbp)
0x7ffff70371e1 <putchecktag+65>: mov $0x1,%edx
0x7ffff70371e6 <putchecktag+70>: lea -0x40(%rbp),%rdi
0x7ffff70371ea <putchecktag+74>: mov $0x8,%ecx
0x7ffff70371ef <putchecktag+79>: movq $0x0,-0x40(%rbp)
0x7ffff70371f7 <putchecktag+87>: callq 0x7ffff7007290 <__memcpy_chk@plt>
0x7ffff70371fc <putchecktag+92>: mov -0x40(%rbp),%rcx
0x7ffff7037200 <putchecktag+96>: bsr %rcx,%rdx
結論を言うと、__memcpy_chk
にジャンプしたあとで例外が発生しています。そのあたりを見てみましょう。
(gdb) b __memcpy_chk
(gdb) cont
(gdb) x/5i $pc
=> 0x7ffff7007290 <__memcpy_chk@plt>: jmpq *0x2545da(%rip) # 0x7ffff725b870 <__memcpy_chk@got.plt>
0x7ffff7007296 <__memcpy_chk@plt+6>: pushq $0x10b
0x7ffff700729b <__memcpy_chk@plt+11>: jmpq 0x7ffff70061d0
0x7ffff70072a0 <upb_enumdef_numvals@plt>: jmpq *0x2545d2(%rip) # 0x7ffff725b878 <upb_enumdef_numvals@got.plt>
0x7ffff70072a6 <upb_enumdef_numvals@plt+6>: pushq $0x10c
最初にジャンプ命令が実行されています。
(gdb) p *($rip+0x2545da)
$8 = {void ()} 0x7ffff725b86a <printer_sethandlers_any@got.plt+2>
ジャンプ先は printer_sethandlers_any
のようです。
(gdb) x/5i $rip+0x2545da
0x7ffff725b86a <printer_sethandlers_any@got.plt+2>: add %edi,%esi
0x7ffff725b86c <printer_sethandlers_any@got.plt+4>: (bad)
0x7ffff725b86d <printer_sethandlers_any@got.plt+5>: jg 0x7ffff725b86f <printer_sethandlers_any@got.plt+7>
0x7ffff725b86f <printer_sethandlers_any@got.plt+7>: add %dl,0x192(%rsi)
0x7ffff725b875 <__memcpy_chk@got.plt+5>: add %al,(%rax)
あれ、(bad)
ってなんぞ?このあたりでマシン語がおかしくなっているんだと思います。私の技術力ではここまでしかわかりませんでした。
おまけ
grpc, google-protobuf をソースビルドすれば大丈夫と言いましたが、実は google-protobuf のバージョンを本記事執筆時点での最新バージョンである 3.11.1 にすると動かなくなりました。上記 Gemfile から 3.9.1 とバージョン指定している部分を削除して以下を実行します。
BUNDLE_FORCE_RUBY_PLATFORM=1 bundle update google-protobuf
bundle exec ruby server.rb
Traceback (most recent call last):
8: from server.rb:4:in `<main>'
7: from server.rb:4:in `require'
6: from /tmp/lib/proto/card_services_pb.rb:7:in `<top (required)>'
5: from /tmp/lib/proto/card_services_pb.rb:7:in `require'
4: from /tmp/lib/proto/card_pb.rb:4:in `<top (required)>'
3: from /tmp/lib/proto/card_pb.rb:4:in `require'
2: from /usr/local/bundle/gems/google-protobuf-3.11.1/lib/google/protobuf.rb:48:in `<top (required)>'
1: from /usr/local/bundle/gems/google-protobuf-3.11.1/lib/google/protobuf.rb:51:in `rescue in <top (required)>'
/usr/local/bundle/gems/google-protobuf-3.11.1/lib/google/protobuf.rb:51:in `require': Error relocating /usr/local/bundle/gems/google-protobuf-3.11.1/lib/google/protobuf_c.so: __va_copy: symbol not found - /usr/local/bundle/gems/google-protobuf-3.11.1/lib/google/protobuf_c.so (LoadError)
hyt さんの Alpine Linux に glibc を入れる備忘録 を参考にして glibc を入れてもこれについては解決しませんでした。そのため最新版を使うのであれば、Debian イメージで gRPC を利用する
一択になってしまうのかもしれません。
おわりに
最後はとりとめもない終わり方になってしまいましたが、長い文章を最後まで読んでいただきありがとうございました。
明日は17日目の @WWK563388548 の番です。引き続き宜しくお願いいたします。