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を使った場合の流れです。
今回は、サンプルコードの流れを図にしました。
サンプルコード
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/hoge
と api/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で置き換える原点にもなったので 結果オーライかなって思ってます〜〜