読者です 読者をやめる 読者になる 読者になる

Finagleを使った Perl -> Scalaへの移行

Finagleを使った Perl -> Scalaへの移行

どうも、大曲です。

去年あたりから、Scalaをプロダクトで使い始めました。 バッチ処理 -> デーモン処理 -> API(play) -> 配信部分(finagle) 上記の順番で徐々に使う範囲を広げていきました。(約1年くらいかけました。) そのおかげで、ほとんどの機能をScalaで書く様になりました。 (自分のチームのみですが。。) 他のチームではGoやElixirを使う動きはあるみたいですが、 一足先にScalaをがっつり使ったチームになってしまいました。

さて、今回はRPCをがっつり使ったマイクロサービスの話ではないです。 メインの開発言語をPerlからScalaへ変更する際に、 今まで開発したPerlの資源を生かしつつ、移行したいなと思った時に Finagleを使った話です。

下の資料は、プライベートなLT大会で今回の内容を話した時に作った資料です。

Finagleとは

Twitterが開発した RPCシステムです。 https://twitter.github.io/finagle/

RPCのシステムなのでリクストの流れをコードで簡単に実装できそうだったから、採用しました。

Finagleを使って実装した流れ

さて、実際にどんな感じで実装したかのですが。 下の図がFinagleを使った場合の流れです。

img

今回は、サンプルコードの流れを図にしました。

サンプルコード

https://github.com/oomatomo/finagle-perl-scala サンプルで使っているScalaのAPIは、Finagleですが 自分のチームではPlayを使ってます。

Fiangleの全体の実装は、サンプルをみてもらえる分かると思います。 Finagleで利用されているServiceやFilterに関しての説明は、省略します。 以下の内容では、各処理に着目した内容になっています。

Fiangle側でのPerlへリクエストを投げる処理

src/main/scala/com/oomatomo/finaglesample/PerlAuthFilter.scala の内容に関してです。

class PerlAuthFilter[Request, Response](authService: Service[http.Request, http.Response])
  extends SimpleFilter[http.Request, http.Response] {

  override def apply(request: http.Request, service: Service[http.Request, http.Response]): Future[http.Response] = {

    // Perl側へのリクエストを作成
    val authRequest: http.Request = Request(http.Method.Post, Request.queryString("/account/json"))

    // セッションIDを認証のリクエストに付与する
    if (request.cookies.get("plack_session").isDefined) {
      authRequest.addCookie(request.cookies.get("plack_session").get)
    }

    // hostを付けないと starmanがリクエストを受け付けない
    authRequest.host_=("127.0.0.1")

    // flatMapでPerlのリクエストを受け取れる
    authService(authRequest).flatMap { res =>
      ....
    }
  }
}

authServiceがPerlのAPIへリクエストを投げてくれる変数になります。 そしてリクエストの結果をflatMapで受け取れます。

Perl側に送るリクエストは、セッションのCookieの情報と仮のホストのIPだけです。 Perl側では、セッションのCookieを元にログインの時に付与されたセッションがあるか確認します。

Scala側へのリクエストを投げる処理

次に、Perl側からのレスポンスを元にScala側へリクエストを投げる処理になります。

authService(authRequest).flatMap { res =>
  // レスポンスからidを抽出する
  // レスポンスの内容: "{ name: 'hoge', id: 1 }"
  val resString = res.encodeString()
  // レスポンスの内容から、idの中身だけ取り出す正規表現
  val r = """id":([0-9]+)""".r
  (for {
    regex <- r.findFirstMatchIn(resString)
    if regex.groupCount == 1
  } yield {
    val apiRequest = request
    // Scala側に渡す際にアカウントの情報をヘッダーに付与する
    apiRequest.headerMap.set("X-Hoge-Id", regex.group(1))
    service(apiRequest)
  }).getOrElse(Future.value(http.Response(request.version, http.Status.Unauthorized)))
}

authService(authRequest).flatMap Perlのレスポンスの内容が取得できるのでレスポンスの内容から、idだけを取り出します。 (レスポンスの内容は、"{ name: 'hoge', id: 1 }"です)

この時の取り出し方は、正規表現で抽出しています。 抽出したidをヘッダーのX-Hoge-Idに設定します。

service(apiRequest) でScala側のAPIにX-Hoge-Id付きのリクエストを付与しています。

getOrElse(Future.value(http.Response(request.version, http.Status.Unauthorized))) この部分は、もしPerl側のHTTPステータスが200以外の場合に認証エラーとみなして Finagleが返すレスポンスも、401で返す実装になります。

ルーティング

ルーティングは、柔軟ではないので細かく調整する必要があります。 例えば、URLのパスが api/test/hogeapi/test/hoge/1 では、 個別にそれぞれ宣言してあげないといけません。

val router = RoutingService.byPathObject[Request] {
    case Root / "api" / "test" / _ / _ => hogeService // api/test/hoge/1
    case Root / "api" / "test" / _     => hogeService // api/test/hoge
    case _                             => blankService
}

実際に動かす

Perl apiの起動 docker-compose up perl_api Scala apiの起動 activator com.oomatomo.finaglesample.ApiServer rpc server の起動 activator com.oomatomo.finaglesample.RpcServer hostsの編集

127.0.0.1   perl-api
127.0.0.1   scala-api

さて、実際にcurlで叩いてみましょう!!

やる順番は以下の通りです。 1, PerlのAPIでログインする 2, ログインした時のCookieを元にRpcServerへアクセスする

はい、簡単ですね。

PerlのAPIでログインする

# PerlのAPIでログインする
curl --cookie-jar cookie.txt -v "127.0.0.1:5000/account/login"

## cookie.txt にCookieの情報が入ってる
# ログインした時のCookieを元にRpcServerへアクセスする
curl --cookie cookie.txt -v "127.0.0.1:9999/api/test/1"
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 9999 (#0)
> GET /api/test/1 HTTP/1.1
> Host: 127.0.0.1:9999
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: plack_session=0936d1bac7a83477dab036c0e4462d749de44dac
>
< HTTP/1.1 200 OK
< Content-Length: 2
<
* Connection #0 to host 127.0.0.1 left intact
ok%
# okが返ってきたので、成功

まとめ

できれば、RPCのフレームワーク(thriftとか)を使ってみたかったのですが サクッと動くやつが欲しかったので今回の実装になりました。

まぁ、この時使った経験が配信サーバをFinagleで置き換える原点にもなったので 結果オーライかなって思ってます〜〜