Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。現在本ブログは更新されておりません。新ブログ: https://medium.com/zeals-tech-blog

gRPC(protobuf)をモノリシックなRailsアプリケーションに導入する

f:id:zeals-engineer:20191112233421p:plain

こんにちは!
Railsエンジニアをやっているtakakudaです。

今回のエントリーでは、Zeals(旧fanp)サービスへのgRPCを導入を紹介できればと思います。
目次は以下のようになります。

そもそもgRPCとは

gRPCはRPCを実現するためにGoogleが開発したプロトコルの1つで、インターフェイス定義言語のもとになるメッセージ交換形式としてProtocol Buffersを利用できます。gRPC上のアプリケーションでは、別マシン上にあるアプリケーションのメソッドをローカルオブジェクトのように直接呼び出すことができ、分散アプリケーションおよびサービスの作成を簡単にできます。

f:id:takakudakei:20191108193526p:plain

https://www.grpc.io/docs/guides/

導入経緯

簡単にZealsの機能紹介をすると、サービスは大きく分けて以下の3つから構成されています。

[1] チャットボットの管理用Webアプリケーション

[2] チャットボットへのメッセージ送受信サービス

[3] 1と2から共通処理を切り出したマイクロサービス群

もっと詳しく知りたい方は、以前公開しました以下の記事をご覧ください。

tech.zeals.co.jp

今回は、上記の構成の中でも[3]にあたる、「チャットボット管理用のWebアプリケーション」と「チャットボットへのメッセージ送受信サービス」で共通して発生する処理をAPIとして切り出すために利用したgRPCをテーマとしています。

2つのサービスで共通する処理をサービスごとにRuby, Pythonと異なる開発言語で実装してるため、

  • 機能追加や修正の際に、それぞれのアプリに同じ実装を行わなければならない
  • 挙動に差異があった場合、その差異がどちらの実装のバグによるものかを特定するのに時間がかかる

といった問題が発生しており、サービス全体見た場合のメンテナンスコストが高くなってきたため、共通処理部分をGoのAPIとして切り出すことを決定しました。

導入

以下では、実際にZealsで使用している「メッセージの配信対象となるユーザーをフィルタリングする処理」を例に解説していきます。

またprotobufによって生成されたコードについてですが、今回はRailsに導入する部分について紹介したいのでサーバー側の実装については触れず、クライアント側のコードについて紹介させていただきます。
導入手順としては以下のようになります。

  1. proto fileにデータ構造を定義する
  2. protocコマンドを用いてRubyコードを生成する
  3. 生成されたRubyコードをlib配下に置き、config/initializers配下でStubを作成する
  4. 共通化したいメソッド呼び出し部分をprotobufで生成されたメソッドに書き換える

proto fileにデータ構造を定義する

まずは.protoという拡張子のファイルで、

  • Service:RPCサービスのインターフェイスを定義することができる 
  • Message:メッセージのフォーマットを定義することができる

をそれぞれ書いていきます。

/proto/cosmos.proto

syntax = "proto3";


import "entities/common.proto";
import "entities/entities.proto";
import "entities/message.proto";

// CosmosService is microservices composed of a suite of small and lightweight services.
service CosmosService {
  // Filter filters a list of end users related with a given chatbot ID by using a given filter ID.
  // End users is filtered by the following conditions.
  // 1. Whether the attribute ID of the end user matches filter's one.
  // 2. Whether inflow date and time of the end user matches filter's one.
  // 3. Whether the chatbot has permission to send a message to the end user.
  rpc Filter(FilterRequest) returns (stream e.EndUser) {}
}

// FilterRequest
message FilterRequest {
  // The ID of the chatbot that will be used to collect end users related with it. And then the set of end users will be filtered.
  int64 chatbot_id = 1;
  // The ID of the filter to be used to filter a set of end users.
  int64 filter_id = 2;
}

syntaxは2019年11月現時点で主流versionである3を指定します。

簡単に説明するとCosmosServiceはFilterRequestという型を受け取ると、FilterRequestの条件によってフィルタリングされたユーザーをresponseとして返すということを定義しています。

developers.google.com

protocコマンドを用いてRubyコードを生成する

gem install grpc-tools

Protocol Buffersの定義から自動でコードを生成するのに必要なツールをinstallします。

https://rubygems.org/gems/grpc-tools/versions/1.25.0

grpc_tools_ruby_protoc -I /proto \--ruby_out=lib --grpc_out=lib ../proto/cosmos.proto

コマンドを実行することでProtocol Buffersの定義をもとにRubyコードが生成されます。

https://grpc.io/docs/tutorials/basic/ruby/

生成されたRubyコードをlib配下に置き、config/initializers配下でStubを作成する

自動生成されたコードはlib配下に置きます。

Protocol Buffersから生成されたコードを実行するためにはStubを作成する必要があるので、 config/initializer にStubを作成する設定fileを作成します。

stub = CosmosService::Service.rpc_stub_class.new(localhost:38080, :this_channel_is_insecure)

共通化したいメソッド呼び出し部分をprotobufで生成されたメソッドに書き換える

生成されたコードを、Rubyで実装しているチャットボット管理画面のコードへ組み込みます。

# requestを用意して
req = FilterRequest.new(chatbot_id: chatbot_id, filter_id: filter_id)
# Stubを使ってサーバー側のメソッドを呼び出します
end_users = stub.filter(req)
# ストリーミングで返ってくるので1つずつ取り出し、end_userのidの配列として返します
end_users.map(&:id)

Railsへ導入時にハマったこと

Rails側での実装を終え、いざステージング環境にて動作確認します。 動作確認の際に、gRPCのdependencyであるgoogle-protobufをビルド済み共有ライブラリとしてインストールすると、正常に動作しないという問題にぶつかりました。

その際の環境は、以下の通りです。

  • ruby:2.5.5-alpine
  • grpc (1.23.0)

ここが僕が大きくハマったポイントです…。
Zealsではkubernetesを本番環境で運用しており、その際に利用するDockerのimageとしてAlpine Linuxを使用していました。

この問題を調査すると、どうやらalpineのimageでprotobufを利用しようとすると発生している問題らしく、本家のprotocolbuffers/protobufでもissueが上がっていました。Alpine LinuxだとRubyライブラリに不足があり、ld-linux-x86-64.soがないというエラーです。

github.com

対応としては上記のissueで議論されている内容を参考にしました。

具体的には、gRPCのdependencyであるgoogle-protobuf を BUNDLE_FORCE_RUBY_PLATFORM=1 でソースからビルドするworkaroundを取るように、以下のようにDockerfileを編集しました。

mv Gemfile Gemfile.orig && \
sed -e /grpc/d Gemfile.orig > Gemfile && \
bundle install && \
cp Gemfile.orig Gemfile && \
BUNDLE_FORCE_RUBY_PLATFORM=1 bundle install

やっていることは、最初のbundle installではgRPCを省いた状態で実行し、2回目のbundle install時にgRPCのみで実行できるようにしています。 一見二度手間のようなことをやっていますが、目的はbundle installでかかる時間を少しでも短くするためです。

gRPCを導入してみて

  • 機能追加や修正の際に、それぞれのアプリに同じ実装を行わなければならない
  • 挙動に差異があった場合、その差異がどちらの実装のバグによるものかを特定するのに時間がかかる

この問題が解決し、今では修正もデバックも容易になりました。

現在は上記のイチ機能しかまだ切り出せていないのですが、これを機に巨大なモノリシックになりつつあるRailsアプリを切り出していき、メンテナンス性や拡張性をマネジメントしやすいプロダクトに成長させていきます。

最後に

Zealsでは、発見した課題をワクワクしながら技術で解決するエンジニアを求めています!

いわゆる「1->10」で確実に伸びているフェーズだからこそ経験できることも多いはずです。 ご興味がある方は、ぜひ下記募集からご応募ください!

hrmos.co