Java, MySQLをKotlin, PostgreSQLに移行した

7年前にGitHub Rankingというサービスを作り、APIを叩きすぎてGitHubからの風当たりが強くなって*1からはデータの更新を止めていたが、KubernetesGraphQLの時みたいに技術を試す砂場用に惰性で動かし続けていた。

Issueの機能要望対応が段々面倒になってきて、サーバー代節約のために潰すかと考えていたのだけど、毎日1000PVくらいあるので試しにGoogle Adsenseを設置してみたところ1日平均 $1 くらいは入ってて黒字になりそうだったので、ちょっとメンテしやすくしてデータの更新再開するかー、ということで今回いろいろ綺麗にした。

DB: MySQLPostgreSQL

なぜPostgreSQLにしたのか

個人的には多くの用途ではMySQLPostgreSQLどっちでもいいと思っているんだけど、今所属してるチームがメンテしてるサービスのDBの多くがPostgreSQLな割に自分はまだPostgreSQLの経験が浅いので、練習的な意味でPostgreSQLに移行して慣らすことにした。

ConoHaのDBサーバー

ConoHaのDBサーバーは月500円でマネージドMySQLが使える素晴らしいサービスなのだけど、PostgreSQLには対応していないのでこれをやめる必要があった。500円だとSSD 10GBなんだけど、GitHubのDBをまるごとぶち抜いてくる都合上ストレージが足りなくなってきていたので、どの道何かする必要はあった。最初610円のVPS (SSD 30GB) を別途立ててそこに移行したけど、割りとメインのVPSのキャパシティ余裕あるし、SSDも100GBあるので、結局そっちに同居させた。

Embulkでデータ変換

MySQLからPostgreSQLへのデータ転送は皆Embulkでやってるイメージがあったので、EmbulkでMySQLからPostgreSQLへデータ転送という記事を参考に自分もそれでやってみた。DBはusersが1000万レコード、repositoriesが1800万レコードあったが、usersは数分で転送できて速いなと思った。

repositoriesはこの記事に近いエラーになって、記事のようにタイムアウト伸ばすのを試したがそれでは直らなかった。transactionを使うところで落ちていたので、transactionを使わなくて済むようembulk-output-postgresqlのmodeをinsertからinsert_directに変えた。結局同じところで落ちて700万行だけinsertされた状態になったけど、usersさえ全部あればrepositoriesの復旧は大変ではないので、そのまま行くことにした…。

ridgepole → sqldef移行

PostgreSQLスキーマをEmbulkのguessだけに頼って作ったため雑なスキーマになっていたので、UNIQUEやNOT NULLをつけるなど整理する必要があった。もともとRidgepoleというActiveRecordベースの羃等スキーマ管理ツールを使っていて、これを使ってもいいのだが、以前の記事に書いた通りそのActiveRecord非依存版であるsqldefをメンテしているので、この機会にそれに移行した。移行はsqldef --exportを叩くだけでできる。

リリースしてから3年経ち、様々な方、特にZOZO社の方面からいっぱいバグレポートをいただき全て直してきたのでほとんど問題なく動いたが、最近いただいたパッチPostgreSQL 12に対応してなかったので直したり、次に伸べるcitext拡張に対応したりした。ついでにsqldef-railsというのも作った*2。そのうちGradleプラグインも作りたい。

citext拡張

MySQLPostgreSQLの違いは、ORDERなしでSELECTしてidでソートされないとか、AUTO_INCREMENTのかわりにserialとsequenceを使うとか、GET_LOCKのかわりに pg_advisory_xact_lock とか使うなどいろいろあったが、一番苦労したのはMySQLの文字列は普通case insensitiveだけどPostgreSQLに移行した瞬間case sensitiveになってしまうというものだった。GitHubのユーザーやリポジトリの名前はcase insensitiveで引きたいのである。

これは LOWER(foo) にインデックスをかけて常に LOWER(foo) でfooを引いてやっても解決するが、常に LOWER(foo) するというのがいかにも忘れそうである。これを単にMySQLっぽい挙動にしてくれるのが citext (case-insensitive text) という型。psqlRailsからアクセスするとcase-insensitiveにアクセスできたのが、JDBCからアクセスするとなぜかcase-sensitiveになる というとこころにハマった。URLに ?stringtype=unspecified をつける と直るが、まさかクライアントが挙動をコントロールできると思ってなかったので、気付くのに時間がかかった。

Worker: Java → Kotlin

このサイトはRailsで作られていてワーカーも元々Sidekiqで実装していたのだが、高速化のために100スレッドでAPIを叩きActiveRecordを使うということをやったらメモリリークっぽい挙動になって詰んだので、それ以外を使う必要があった。僕が使える言語でこの要件だと多分Goが一番妥当なんだけど、現行のワーカー実装を書いた当時に社のチームが使っていたスタックであるJavaJDBIUndertow *3を練習用に使ったのであった*4

で、大体同じ人がいるチームに今もいるんだけど、JVMでは主にKotlin、jOOQDropwizardを使うように変わったので、普段使っているそれに差し替えることでメンテコストを下げることにした。JDKも8から11に上げた。

Javaと比べたKotlinのいいところ

Kotlinのdata classを使うとJavaより行数が減らせて便利みたいな話をすると、Lombokで十分という反応をしてくる人がいて、僕はLombokは実際使ったことがあるわけではないけどこのスライドとかに出てくる機能だけど見ると、以下の点でKotlinの方が便利かなあと感じる。

  • Named argumentsをキーワード引数的に使える *5
  • 複数行文字列が綺麗に書ける *6
  • String interpolationが便利
  • 標準でもList, Mapの操作が楽
  • LambdaがJavaより書きやすい
  • ?:, let, also みたいな便利な奴が何かと多い
  • 型でNull安全にできる
  • throws書かなくていい
  • extensionが便利、Rubyのrefinementsが気軽に使える感じ
  • Smart castで型操作が楽になることがある
  • coroutineで気軽に軽量なIO多重化ができる

jOOQ

JDBIは生のSQLを毎回書く奴だけど、jOOQSQL風のDSLでクエリをビルドする。JDBIは複雑なクエリを動的に組み立てるみたいなのにはあんまり向いてなくて、実際社で僕がcancancanKotlinに移植した奴はクエリが動的に変わりまくるのでjOOQが活躍している。また、KotlinのNamed argumentsとjOOQを組合せるとオプショナルなキーワード引数でクエリを動的に変える感じにできるので、ActiveRecord風のインターフェースが作りやすい。

普通はテーブルごとにコード生成をして使う奴なんだけど、使い始めた当初Kotlinのコード生成がうまく動いてなかった都合、コード生成なしで使っている。ただ、onDuplicateKeyUpdate とかがコード生成用のAPIを使わないと使えないのは困った*7

Dropwizard

ワーカーなのにUndertowでAPIを生やしていたのはワーカーの操作用APIを生やしていたからだが、メンテコストに対して利益が薄いのでAPIは落とした。なのでDropwizardはここでは使っていないのだが、Dropwizardにはパッケージ構成指針があって、これに従っているとRailsみたいにどこに何があるか予測しやすいのが気にいっていて、このワーカーでもなるべくDropwizardっぽい感じで物を配置することで普段と同じ感じで色々探せるようにした。

Jersey Client

DropwizardはJAX-RSベースなので、opentracing-contrib/java-jaxrsでOpenTracingに対応できるのだが、APIクライアントにもJAX-RSベースのものを使えば同じものでOpenTracingに対応できるので、JAX-RSのクライアントの参照実装であるJersey Clientが弊チームでは使われている。このワーカーでは適当に見つけたgoogle-http-clientを使っていたのだが、今回Jersey Clientに差し替えた。

Jersey Clientでリクエストを投げるとstatus codeに応じた例外を投げることができるんだけど、レスポンスヘッダに入っているRate Limitの残りを見るために一旦 Response を取るとその機能は使えないので自分で投げる必要がある、という仕様にJAX-RSがなってるのがちょっと困った。

Server: Rails

https://gitstar-ranking.com を返してるのはRailsで、RubyのJITや自作の最速テンプレートエンジンであるhamlitが試せるので特に他の何かに移行することなく続投。バージョンアップや使ってないGemの整理だけを行なった。

Ubuntu 20.04

Ubuntu 18.04 で動いていたのでアップグレードした。プロビジョニングはmitamaeで自動化しているものの、ConoHaをポチポチするのが面倒だったのでおもむろに sudo do-release-upgrade をしたが、Rubylibffi.so.6 で No such file or directory になったのでRubyをビルドし直して、それで終わり。

Ruby 3.0

Ruby 2.6だったのをRuby 3.0に上げた。個人的にはendless method definition *8 がお気に入り。逆に、2.7で入ったNumbered parametersは、使いたい状況には度々遭遇するけど、KotlinのitScala_に比べると_1という字面が微妙なので妥協で { |x| x と書くことが多い *9

Rails 6.1

Rails 5.1から6.1に上げた。最近はRailsのアップグレードは bundle updatebin/rails app:update するだけで終了で、あまり困らない感じになってきた。むしろRuby 3に上げる時はHashでキーワード引数渡してたところがおもむろに壊れたので、今回はRubyの方がアップグレード難易度が高かった。Rails 6といえば公式に複数DBやbulk insertに対応したなどがあって、どっちもこのアプリでは使わなかったが、地味にActiveRecordpick は使った。

GraphQL退役

GraphQLでAPIを生やしているところがあったが、ここ以外でgraphql-rubyを一切使ってなくて何も覚えてないのでRails Wayからあまりにも外れたこれをメンテするのがしんどいのと、ほとんどのスキーマは単に実装してみたくて生やしただけだったのでGraphQLは撤去した。JavaScriptから使われていたAPIが一つだけあったが、それのためにjbuilderを依存に足したくなかったので、render json: だけで済ませた。

React, Webpacker退役

毎日使っているTwitterクライアントをReactで自作してそこそこメンテを続けているのでReactの使い方は普通に覚えているのだが、このアプリではユーザー更新ページで部分的に使っているだけだった割りにnpmのライブラリの脆弱性アラートが無限に来るのがコスパ悪いなと思ったので、素のJavaScriptに書き直して、yarnとWebpackerもやめてSprocketsから配布することにした。モダンWebフロントエンドの素振りはどの道Twitterクライアントの方でやり続けるのでこちらではいいかなと。

Sprockets 4移行

長い間betaだったが去年やっと出たSprockets 4にアップグレードした。manifest.js というのを書かないといけなくなるのが面倒でサボっていたが、sprockets/UPGRADING.mdを見たら割りとすぐいけた。

Omniauth 2移行

Omniauth 1の脆弱性アラートが出ていたので上げようとしたのだが、DeviseとかいうGemがOmniauthのバージョン番号をチェックして例外を投げていて、しかもそのチェックを修正したバージョンが未だにリリースされていないのでDeviseはmasterを使う必要があった。それから、Omniauth 2に上げたらコールバックがGETじゃなくてPOST必須になり、そしてPOSTに変えるとCSRF protectionに引っかかるという問題があった。Stackoverflowcookpad/omniauth-rails_csrf_protectionが紹介されていて、メンテナが信頼できそうなのでありがたく導入させていだたいた。

感想

データの更新を再開しつつ壊れた時にメンテしやすいようにしただけで機能追加とかは一切してないので、このサイトのGoogle Adsenseの収益がこれで増える気はしないが、jOOQやJersey周りで仕事中に気になってたけど時間の都合触れてなかった部分をいろいろ試せたりとか、sqldefのPostgreSQL対応の改善に繋ったのでよかった。

*1:この時についでに、サービス名にGitHubいれるのやめろと言われて、Gitstar Rankingという名前になった

*2:元々ActiveRecordが入っているRailsだとRidgepoleを使わない理由はほとんどない気がするけど、同じDBをActiveRecord以外からも読むので素のSQLで管理されてる方が扱いやすい (今回のアプリがそれ) とか、DDLは覚えてるけどActiveRecordDSLは調べないとわからないというようなニッチな用途には便利かもしれない。

*3:正確にはそれを自社でラップした Underwrap https://github.com/treasure-data/underwrap

*4:ちなみに、MySQLのテーブル上に実装したキューにRailsからジョブをエンキューしてJavaのワーカーでデキューして処理するという普通はしない構成を取っていたのだけど、これはTreasure Dataでは頻出のパターンで、TDではRubyJVMが一緒にワイワイしてないサービスを探す方が難しい

*5:メソッドに引数が複数あると、その順序は自明じゃないと思うことが多い。なので順序を覚えてないとろくに呼び出せないメソッドは不親切だなと思っていて、例えばRubyでも引数が2〜3個に達したあたりで2つ目以降か全部の引数をキーワード引数にして呼んだりしていて、これができない言語は結構イライラする。静的型付言語なら引数の型合わなければコンパイルエラーになるし覚えなくていいんじゃない?と思うかもしれないが、同じ型が複数並ぶとその主張は崩壊する。なるべくNamed argumentsで呼び出してやることで、引数のリファクタリングとかも安全感が出る。

*6:JavaにJEP 326 https://openjdk.java.net/jeps/326 入らなかったの残念

*7:コード生成用に用意されてるAPIを自分で呼び出した。まあ、今はコード生成使ったらいいんだけど

*8:この機能見る度にKotlin https://kotlinlang.org/docs/functions.html#single-expression-functions を思い出す

*9:これに関してはCommiter vs the worldか何かの時の投票でそういう立場であると表明しているので、ここでおもむろに意見を出してるわけではない