Help us understand the problem. What is going on with this article?

Rubyで作るfinagle-thriftクライント

More than 1 year has passed since last update.

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の構成

PORTフロントエンド含む構成図

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ファイルはこちらです。

hello_service.thrift
namespace java com.laysakura
namespace rb Laysakura.Idl

service HelloService {
  string sayHelloTo(1: string name)
}

sayHelloTo() APIがひとつ定義されただけのシンプルなものです。

Thriftファイルを以下のコマンドでコンパイルすると、 gen-rb ディレクトリ以下にRubyのファイルが生成されます。

Thriftファイルのコンパイル
$ 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を:

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を生やしたサーバが立っていること前提
run.rb
$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層で下記のように実現しています。

lib/folio/rack_middleware/store_request_local_vars.rb
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
config/initializers/logger.rb
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による記事です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした