Java, MySQLをKotlin, PostgreSQLに移行した
7年前にGitHub Rankingというサービスを作り、APIを叩きすぎてGitHubからの風当たりが強くなって*1からはデータの更新を止めていたが、KubernetesやGraphQLの時みたいに技術を試す砂場用に惰性で動かし続けていた。
Issueの機能要望対応が段々面倒になってきて、サーバー代節約のために潰すかと考えていたのだけど、毎日1000PVくらいあるので試しにGoogle Adsenseを設置してみたところ1日平均 $1 くらいは入ってて黒字になりそうだったので、ちょっとメンテしやすくしてデータの更新再開するかー、ということで今回いろいろ綺麗にした。
DB: MySQL → PostgreSQL
なぜPostgreSQLにしたのか
個人的には多くの用途ではMySQLとPostgreSQLどっちでもいいと思っているんだけど、今所属してるチームがメンテしてるサービスの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拡張
MySQLとPostgreSQLの違いは、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) という型。psqlやRailsからアクセスするとcase-insensitiveにアクセスできたのが、JDBCからアクセスするとなぜかcase-sensitiveになる というとこころにハマった。URLに ?stringtype=unspecified
をつける と直るが、まさかクライアントが挙動をコントロールできると思ってなかったので、気付くのに時間がかかった。
Worker: Java → Kotlin
このサイトはRailsで作られていてワーカーも元々Sidekiqで実装していたのだが、高速化のために100スレッドでAPIを叩きActiveRecordを使うということをやったらメモリリークっぽい挙動になって詰んだので、それ以外を使う必要があった。僕が使える言語でこの要件だと多分Goが一番妥当なんだけど、現行のワーカー実装を書いた当時に社のチームが使っていたスタックであるJava、JDBI、Undertow *3を練習用に使ったのであった*4。
で、大体同じ人がいるチームに今もいるんだけど、JVMでは主にKotlin、jOOQ、Dropwizardを使うように変わったので、普段使っているそれに差し替えることでメンテコストを下げることにした。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を毎回書く奴だけど、jOOQはSQL風の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
をしたが、Rubyが libffi.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のit
やScalaの_
に比べると_1
という字面が微妙なので妥協で { |x| x
と書くことが多い *9 。
Rails 6.1
Rails 5.1から6.1に上げた。最近はRailsのアップグレードは bundle update
と bin/rails app:update
するだけで終了で、あまり困らない感じになってきた。むしろRuby 3に上げる時はHashでキーワード引数渡してたところがおもむろに壊れたので、今回はRubyの方がアップグレード難易度が高かった。Rails 6といえば公式に複数DBやbulk insertに対応したなどがあって、どっちもこのアプリでは使わなかったが、地味にActiveRecordの pick
は使った。
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に引っかかるという問題があった。Stackoverflowでcookpad/omniauth-rails_csrf_protectionが紹介されていて、メンテナが信頼できそうなのでありがたく導入させていだたいた。
感想
データの更新を再開しつつ壊れた時にメンテしやすいようにしただけで機能追加とかは一切してないので、このサイトのGoogle Adsenseの収益がこれで増える気はしないが、jOOQやJersey周りで仕事中に気になってたけど時間の都合触れてなかった部分をいろいろ試せたりとか、sqldefのPostgreSQL対応の改善に繋ったのでよかった。
*1:この時についでに、サービス名にGitHubいれるのやめろと言われて、Gitstar Rankingという名前になった
*2:元々ActiveRecordが入っているRailsだとRidgepoleを使わない理由はほとんどない気がするけど、同じDBをActiveRecord以外からも読むので素のSQLで管理されてる方が扱いやすい (今回のアプリがそれ) とか、DDLは覚えてるけどActiveRecordのDSLは調べないとわからないというようなニッチな用途には便利かもしれない。
*3:正確にはそれを自社でラップした Underwrap https://github.com/treasure-data/underwrap
*4:ちなみに、MySQLのテーブル上に実装したキューにRailsからジョブをエンキューしてJavaのワーカーでデキューして処理するという普通はしない構成を取っていたのだけど、これはTreasure Dataでは頻出のパターンで、TDではRubyとJVMが一緒にワイワイしてないサービスを探す方が難しい
*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か何かの時の投票でそういう立場であると表明しているので、ここでおもむろに意見を出してるわけではない