FOLIOのアドベントカレンダー9日目です。
前日は@ken5scalによるAzureADでYubiKeyを管理しつつ認証(TOTP)する記事でした。
手元の下書きに「前日分の感想を書く」って書いてありましたが、感想が書きづらいタイプのhow-to記事でした。
本日は Rubyで作るfinagle-thriftクライント と銘打ち、小ネタを書きます。感想が書きづらいタイプのhow-to記事です。
TL;DR
- FOLIOではPORTフロントエンドと呼ばれる管理画面的なものを作ってる
- バックエンドは大体
finagle-thrift
を喋る - PORTフロントエンドも
finagle-thrift
喋れるようにして分散トレーシングできるようにした - Rubyで
finagle-thrift
やってるドキュメントどこにもなくて苦労したので本稿で供養
FOLIOの構成
FOLIO ( https://folio-sec.com ) は、モバイルアプリ, Webサービスで利用可能なオンライン証券サービスを作っています。
バックエンドはほぼScalaによるマイクロサービスでできていて、RPCには finagle-thrift
を利用しています(詳細後述)。
BtoCサービスによくあることですが、裏側にはいわゆる「管理画面」が存在しています。
FOLIOでは、証券サービスを支える裏方部分を PORT と呼んでいます。
(参考: 株式会社FOLIOの次世代証券システムをひもとく )
PORTのフロントエンドをRailsで作成しています。
サービスリリース当初からPORTフロントエンドはバックエンドシステムに対しRead/WriteのThrift通信を行っており、
- 商品として配信するテーマの入稿
- 口座開設の審査
などの機能を提供していました。
しかしPORTフロントエンドは分散トレーシングのための仕組みを導入していなかったので、PORTフロントエンドの利用者がトラブルを報告してきても、どのバックエンドマイクロサービスによる事象かを調査する際、日時によるログの絞り込みを行う必要がありました(低精度)。
流石につらくなってきたので、BtoCのサービス側と同じく、分散トレーシングに対応するためにRailsでも finagle-thrift
ライブラリを使うようにしました。
その際、自分の調べた範囲ではRubyで finagle-thrift
を導入する方法が見つからなかったので、色々と苦労しました。
もう二度とこんな苦労をする人が現れないように、記録を残しておくのが本稿の趣旨です。
finagle-thrift の解説
finagle-thrift
の前にFinagleの解説をします。
FinagleはTwitter社が中心に作成しているOSSのRPCフレームワークです。
特にサーバ側のパフォーマンスを追求しており、多くのリクエストを並行に捌くことができるように設計されています。
Finagleでは通信プロトコルとして、HTTP, Thrift, MySQLが使えます。
本稿ではこのうちThriftを通信プロトコルとして使うためのライブラリ、 finagle-thrift
に関して記載します。
Thriftは、始めはFacebook社によって開発された、RPCライブラリです。
今はApache ThriftとしてOSSで公開されています。
Twitter社はThriftを拡張し、自社のService-Oriented Architechtureで利用するために、Zipkinによる分散トレーシングのための情報をパケットに載せられるようにしました。
この拡張プロトコルをTTwitterと呼びます。
以下、Rubyのクライアントから、 finagle-thrift
ライブラリを使ってTTwitterプロトコルで通信するための方法を記載します。
Rubyで finagle-thrift
を使って通信する最小コード
このセクションで解説するコードは https://github.com/laysakura/ruby-finagle-thrift-client-example に置いてあります。
実行方法も書いているので、そちらも参考にしてください。
まず、サーバサイドと共有するThriftファイルはこちらです。
namespace java com.laysakura
namespace rb Laysakura.Idl
service HelloService {
string sayHelloTo(1: string name)
}
sayHelloTo()
APIがひとつ定義されただけのシンプルなものです。
Thriftファイルを以下のコマンドでコンパイルすると、 gen-rb
ディレクトリ以下にRubyのファイルが生成されます。
$ brew install thrift
$ thrift --version
Thrift version 0.11.0
$ thrift --gen rb hello_service.thrift
$ ls gen-rb
hello_service.rb hello_service_constants.rb hello_service_types.rb
これでThriftのAPI定義に合わせてRPCする準備はOKです。
あとは下記のGemfileを:
source 'https://rubygems.org'
gem 'finagle-thrift'
gem 'thrift_client'
使ってライブラリをインストールし、下記の run.sh
を実行すれば localhost:9999
に立っているTTwitterを喋るサーバへのRPCコールができます。
$ bundle install --path=vendor/bundle
$ bundle exec ruby run.sh # localhost:9999 に sayHelloTo() APIを生やしたサーバが立っていること前提
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'gen-rb'))
require 'thrift_client'
require 'finagle-thrift'
require 'hello_service'
SERVER = 'localhost:9999'
CLIENT_NAME = 'ruby-finagle-thrift-client-example'
# HelloServiceのクライアント作成
client = ::ThriftClient.new(::Laysakura::Idl::HelloService::Client, SERVER)
# Tracingの有効化
#
## https://zipkin.io/pages/instrumenting.html の "Endpoint" を設定。
##
## このように設定しないと、 https://github.com/twitter/finagle/blob/version-6.40.0/finagle-thrift/src/main/ruby/lib/finagle-thrift/trace.rb#L145 のように
## gethostname(3) の結果を名前解決しようとするが、名前解決できない名前が取得されるとThrift API callが例外で失敗する。
::Trace.default_endpoint = ::Trace::Endpoint.new(::Trace::Endpoint.host_to_i32('127.0.0.1'), 0, CLIENT_NAME)
::FinagleThrift.enable_tracing!(client, ::FinagleThrift::ClientId.new(name: CLIENT_NAME))
# HelloServiceのサーバ側の sayHelloTo() APIを叩く
begin
resp = client.sayHelloTo('John')
ensure
client.disconnect!
end
puts "Response: '#{resp}'"
puts "TraceId: #{::Trace.id.trace_id.to_s}"
::Trace.default_endpoint = ...
の行に注意してください。
自分の試している限りでは、この行がなければ ::FinagleThrift.enable_tracing!(...)
が失敗し、Zipkin用のトレース情報を出すことができませんでした。
RailsでTraceIdをロギングする
おまけ的な扱いです。
先に上げたPORTのフロントエンド(管理画面)はRailsで作っています。
PORTフロントエンドのRailsで生成したTraceIdをバックエンドのマイクロサービスに伝達しつつ、PORTフロントエンドのログにもバックエンドのログにもTraceIdを出力するようにしています。
ログはKibanaに転送されるので、Kibana上で traceId:9549be5ecbfacc56
のように検索すると、PORTフロントエンドへのリクエストを完遂させるために叩いたバックエンドマイクロサービスのログも一気通貫して見ることができます。
Rails.logger.info()
などのロギングメソッドを叩くときに、「このRailsへのリクエストで生成したTraceId」を取得することが必要でしたが、調べても分からなかった、試行錯誤の結果request_store Gemを使い、rack_middleware層で下記のように実現しています。
module Folio
module RackMiddleware
# loggerなどで使うため、Rackのrequestごとに固有の値を ::RequestStore に格納する
class StoreRequestLocalVars
def initialize(app)
@app = app
end
def call(env)
# finagle-thrift の traceId
#
# https://github.com/twitter/finagle/blob/version-6.40.0/finagle-thrift/src/main/ruby/lib/finagle-thrift/trace.rb#L151-L159
# にあるように、Thread local variableを使っているので、
# ここで取得した値が別のリクエストのtraceIdではないことが保証される。
# (1つの thread local variable に複数のリクエストが同時に書き込むことはないという想定)
::RequestStore.store[:trace_id] = ::Trace.id.trace_id.to_s
@app.call(env)
end
end
end
end
class JsonLogFormatter
include ActiveSupport::TaggedLogging::Formatter
def initialize
@fixed_fields = {
appname: Settings.app_name,
appVersion: Settings.app_version,
hostname: Socket.gethostname
}
end
def call(severity, time, progname, msg)
fields = {
'@timestamp': time,
level: severity,
message: msg
}
# 以下、リクエストごとに変わる値。
# loggerを呼び出しているのとは異なるリクエストの値を取らないよう、 ::RequestStore から取得すべき。
trace_id_fields = { traceId: ::RequestStore.store[:trace_id] }
fields.merge(
trace_id_fields.merge(
@fixed_fields
)
).to_json.concat("\n")
end
end
おわりに
PORTフロントエンドでも分散トレーシングできるようにするため、 finagle-thrift
をRubyから使う方法を開拓しました。
苦労を供養するため、このドキュメントを残しました。
誰も書いていなかったということは誰もRubyで finagle-thrift
なんてやってないような気はするのですが... 誰かのお役に立てれば嬉しいです。
明日はFOLIOアドベントカレンダー10日目、@takanoripeによる記事です。