LoginSignup
35
10

More than 3 years have passed since last update.

Alpine Linux 上で gRPC を使ってはまった話

Posted at

はじめに

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 で落ちることでしょう。

どうするか?

二通りの方法があります。

  1. Alpine を諦める
  2. 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 でイメージを起動しておきます。

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
Gemfile
# 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 の番です。引き続き宜しくお願いいたします。

35
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
10