カーソルページネーションによる新着投稿一覧APIの実装について

こんにちは。crispyのエンジニアの馬塲です。こちらの記事では弊社で運営しているHEIMアプリで使用しているカーソルページネーションについて話していきたいと思います。

実装の経緯

HEIMアプリで使用する新着投稿の一覧を返すAPIを実装してリリースしたところ、ページ遷移中に同じ投稿が重複して表示されてしまうという事象が発生しました。

原因を調査したところ、ページ遷移中に別のユーザーが新規投稿を行ったタイミングで重複してしまうことが分かり、ページネーション処理の改修が必要になりました。そして実装したのがカーソルページネーションによるページネーション処理です。

カーソルページネーションとは

カーソルページネーションとはタイムスタンプなど、ユニークな値を基点としてページネーションを行うものです。

一般的に使用されるオフセットページネーションは対象のデータを表示させる数で区切って抽出するのに対し、カーソルページネーションは対象のレコードの前N件、後ろN件のような取得方法になります。

f:id:shun114:20220125180727p:plain
カーソルページネーションとオフセットページネーションの比較

例として、SpotifyのAPIを見てみるとタイムスタンプを使用してカーソルページネーションを使用していることがわかると思います。

f:id:shun114:20220125171609p:plain
Spotify APIでカーソルページネーションを使用している例
developer.spotify.com

webサイトでよく使われるgemは基本的にオフセットページネーションです、Ruby gemで代表的なものだとkaminariやpagyがあります。

github.com

github.com

オフセットページネーションで問題が発生するパターン

基本的にはオフセットページネーションを使用すること多いですが、新着一覧を表示させるとページ遷移した際に重複してデータが表示されてしまうことがあります。

オフセットページネーションの場合、特定の件数ごとに区切ってページ分けをするため、途中でデータが入るとページ分けがずれてしまいます。 図で見た方が分かりやすいと思います。

f:id:shun114:20220125182848p:plain
オフセットページネーションでデータが重複する例

このように既に表示させたデータが新規データ挿入が原因で次のページにも表示されてしまう、といった事象が起きてしまいます。

新着順での表示など、途中でデータが最後尾以外に追加されることがある場合はオフセットページネーションの使用はお勧めできません。

カーソルページネーションのgemの例

カーソルページネーションを利用できるRuby gemも存在します。

activerecord-cursor github.com

pagy-cursor github.com

これらのgemを使用するのが一番手軽なのですが、弊社では別テーブルのカラムを参照してページネーションを行うパターンの対応が必要だったため、自作することにしました。

カーソルページネーション実装

APIは以下のようなパラメータ、レスポンスを用意しました。

パラメータはcursordirectionを用意。

GET /items?cursor=1234567&direction=next

レスポンスはlinks内にprevnextを用意してページ遷移を可能にしました。

{
  data: [
    { id: 1, ... },
    { id: 2, ... },
    { id: 3, ... },
    { id: 4, ... }
  ],
  links: {
    prev: '/items?cursor=1111111&direction=prev',
    next: '/items?cursor=222222&direction=next'
  }
}

処理部分は前述した通り他テーブルのカラムを参照できる必要があったため、基本となる処理を抽象クラスとして実装しました。

f:id:shun114:20220125202050p:plain

抽象クラスを用いることで、cursorを取得、設定する処理をそれぞれのクラスでオーバーライドすることにより、別テーブルのカラムや複数カラムをcursorに設定できるようになりました。

ページネーション処理のロジック部分はactiverecord-cursor gemのソースコードを例に説明したいと思います。

def cursor(options = {})
  @options = default_options.merge!(options).symbolize_keys!
  @options[:direction] =
    if @options.key?(:start) || @options.key?(:stop)
      @options.key?(:start) ? :start : :stop
    end
  @cursor = Params.decode(@options[@options[:direction]]).value # 1
  @records = on_cursor.in_order.limit(@options[:size] + 1) # 2
  set_cursor # 3
  @records
rescue ActiveRecord::StatementInvalid
  raise Cursor::InvalidCursor
end

https://github.com/tsuwatch/activerecord-cursor/blob/dde17f6630cb807716ae66925f52e4b280ee6ef2/lib/activerecord/cursor/model_extension.rb#L9-L21

上のコードがcursorの設定処理と対象のレコードの取得処理を行っている箇所です。activerecord-cursorのcursorの値はキーとなるカラムの値をエンコードした文字列としています。

処理は大きく分けて3つです。

  1. パラメータとして送られてきたcursorをデコードして基点となる値を取得
  2. cursorの値を用いて対象のレコードを取得
  3. レスポンスに設定する次のcursorをセット

実装のポイントは、2の手順で指定の数よりも1件多く取得していることです。

@records = on_cursor.in_order.limit(@options[:size] + 1)

これは3で次のページが存在するかどうかを確認するために行っている処理です。ソースコードを見てみると、set_cursorメソッドの中で次の1件が存在するかどうかを確認する処理が含まれています。

def set_cursor
  @next = nil
  @prev = nil
  if @options[:direction] == :start
    set_cursor_on_start
  elsif @options[:direction] == :stop
    set_cursor_on_stop
  elsif @records.size == @options[:size] + 1
    @records = @records.limit(@options[:size])
    @next = generate_cursor(@records[@records.size - 1])
  end
end

https://github.com/tsuwatch/activerecord-cursor/blob/dde17f6630cb807716ae66925f52e4b280ee6ef2/lib/activerecord/cursor/model_extension.rb#L77-L88

次のレコードがあるかどうかを確認することで、レスポンスにcursorがある場合は次のページが存在し、ない場合はレスポンスデータが最後のページであることが分かります。

以上のような処理を参考にして無事実装が完了しました。カーソルページネーション処理をリリース後、新着一覧でデータが重複する問題が解消されました。

さいごに

カーソルページネーションの自作方法についてまとめてみました。ぜひ参考にしてみてください。

crispyでは、HEIM、3rdmallの開発を一緒に盛り上げてくれるエンジニアを募集しています!興味がある方はぜひ一度お話をしましょう。お待ちしております。

https://www.wantedly.com/companies/crispy-inc/projects

RailsアプリケーションをApp EngineからCloud Runに移行しました

f:id:takamario:20220208155628p:plain

はじめに

CTOの高丸です。Cloud Runの勢いを感じます。

今回は我々のRailsアプリケーションの実行環境の移行に関して、お話ししたいと思います。

経緯

crispyではお買い物支援サービス「HEIM(ハイム)」のサービスを、長らくGoogle Cloud Platform(以下GCP)のApp Engineで稼働させてきましたが、Cloud Runに移行しました。

Railsアプリケーションと言えばHeroku、クラウドならAWSというのがスタートアップ企業ではよくある環境かと思いますが、個人的に使い勝手の良さを感じるところから、GCP でApp Engineを使ってきました。

それまでの経緯は、別記事になりますが、拙著の記事も参考までにどうぞ。

スタートアップ企業であれば、少人数の開発体制でインフラの管理コストを抑えるためにも、サーバーレスの環境にするのが主流かと思います。

時代はさらに、単純なPaaSというよりは、ランタイム環境にある程度の自由度を求めて、コンテナの実行環境へと変わりつつありました。

しかし、Kubernetesのような自由すぎる(設定が多すぎる)環境は、今の我々の開発組織やサービス規模には不相応であることもわかってきました。

そんな中、GCPにCloud Runという絶妙な環境が生まれました。

リリース当初は、「Cloud Functionの単純なコンテナ版かな?」とも思っていましたが、より実践的なアプリケーション使用例やユーザーコミュニティの勢いを感じたため、移行を決めました。

(Cloud Run自体の説明は割愛します)

移行前(2017年〜2021年10月)

移行前のインフラは以下のような感じでした。至ってシンプルなApp Engineの使い方です。

f:id:takamario:20220112123450p:plain
移行前

(図は一部省略してあります)

存在していた問題点

App Engine/Flexibleのデプロイが遅い

GoをApp Engine/Standard環境で動かした場合は爆速で起動しますが、Flexible環境はDockerコンテナをGCP内部で構築するため、ある程度の大きさのRailsアプリケーションになると、20分以上デプロイに時間が掛かるようになってきます。

デプロイ時間が長いと、ロールバックも遅くなるため、サービスの稼働率にも影響してくる重要な項目です。

元々は、このデプロイ時間の高速化のためにCloud Runを検討していました。

オートスケールができない(させてない)

これはあくまでも我々の設定の話なのですが、スタートアップ企業の場合はインフラコストを抑えることも大事なので、オートスケールは行わないようにしていました。

また、オートスケール設定していたとしても、Flexible環境でのRailsサーバーの起動は時間が掛かり、ベストなタイミングでスケールアウトできるとは言えない状態でした。

しかし、今後のサービス成長を考えると、オートスケールは必須であることは明確でした。

カスタムドメインがApp Engineに設定されている

これは移行に関しての問題点ですが、App Engineはカスタムドメインを設定して稼働できる設定があります。

これを使うと、SSL証明書も自動で作成・更新されるため、インフラ管理の手間が減ってとても便利なのですが、逆に言うと、DNSの設定もすべてApp Engineに向いているため、Cloud Runにドメインを移行する際に、DNSを切り替えない限り、Cloud Run側にトラフィックを流すことができず、切り替える際にネットワーク断が発生してしまう可能性がありました。

これに関しては、Cloud Load Balancingを使うことで解決しました。次のセクションで解説します。

移行途中(2021年10月〜2021年12月)

上記のネットワーク断を防ぐため、Cloud Load Balancing(以下LB)を利用することにしました。

LBが、2020年7月にサーバーレスのサービスをバックエンドに設定できるようになった(Serverless NEGs)ため、App Engineの前にLBを置きました。

ただ、今のところ複数のServerless NEGsは設定できないようです。そのため、Cloud Runの前にもLBを置きました。

f:id:takamario:20220112123623p:plain
移行途中

「このままだと、結局DNSをバツっと切り替えないと移行できないんじゃないの?」と思われたかもしれませんが、なんと我々が移行しようとしていたちょうどそのタイミング(2021年10月)で、Cloud DNSの重み付きラウンドロビン(Weghted Roundrobin)がプレビューリリースされていました。

これを使うことにより、DNSを先述の2つのLBに対し割り当てることができ、重み付きでトラフィックを調整することができるようになりました。

この重み付きラウンドロビンは、移行時の負荷を確認するためにも使えました。

いきなり、すべてのトラフィックをCloud Runに流すのではなく、1/3のトラフィックだけをまずCloud Runに流して試しました。数日おきにCloud Run側の割合を増やしていき、最終的に全トラフィックをCloud Runに流していきました。

何か問題があった場合は、すぐにAppEngine側に全てのトラフィックを流し替えることで、ロールバックも可能になりました。

移行後(2021年12月〜)

App Engine側がなくなり、Cloud Runだけになりましたが、LBは、今後移行作業が発生した場合のためにも残してあります。

また、Cloud Armorも同時に試していたので、現状もLBに設定してあります。そのまま攻撃アクセスに対するセキュリティを向上させることができるので、コストに問題なければ導入しておくのが良いかと思います。

f:id:takamario:20220112123710p:plain
移行後

また2021年9月には、Always on CPUという、HTTPアクセスを捌いている時間に限らず常時起動できるオプションも付いたので、バッチ環境としての利用も検討中です。

残っている問題点

移行は無事完了しましたが、いくつか残っている問題点もあります。

まだデプロイが遅い

移行が第一の目標だったので、Dockerコンテナのビルドの仕方がまだキャッシュを活かしきれていない可能性があり、もう少し改善ができそうです。(Asset Pipelineのプリコンパイルなど)

コストが増えてしまった

これは運用上の問題だったのですが、リビジョンタグを付与しているものにはアクセス可能になるため(タグを付与してはじめてアクセス可能になるため必要)、ステージング環境などでは、いろんなfeatureブランチのリビジョンにタグが付いている状態でした。

Cloud Runはアクセスがなくても、リビジョンタグが付いている限りアイドル状態として動いているため、料金が少しずつかかってしまい、これがコスト増加の原因となっていました。デプロイの際に古いリビジョンタグは外すようにお掃除処理をいれることで解決しました。

まとめ

App Engine/FlexibleからCloud Runに移行することで、

  • 自由なランタイム環境(コンテナ)
  • オートスケール
  • コストダウン
  • デプロイ速度向上(志半ば)

加えて、Cloud Load Balancingを使ったおかげで、

  • バックエンド移行作業の簡易化
  • Cloud Armorを使った攻撃対策

が、可能になりました。

はじめにも書きましたが、Cloud Runの勢いをものすごく感じますので、2022年はCloud Runがアツい年になるのではないかと思っています。

エンジニア募集中!

株式会社crispyに興味が出た方は、Wantedlyのページへ!

HEIMアプリの開発で、VSCodeのLive Shareを使ったペアプロを導入してみた

ペアプロをしました

こんにちは!crispyでエンジニアインターンしている村上といいます! エンジニアインターンとしてアプリ開発に携わり、新しいタスクを進めるにあたってペアプロで開発をしたので、気づきや感想など書いていきたいと思います!

ペアプロとは?

ご存知の方も多いと思いますが、ペアプロとはペアプログラングのことです。 僕よりも経験豊富でバリバリ書けるメンターの方と一緒に、ヒントやアドバイスをいただきながらプログラミングしました。 僕の場合はリモートで参加しているので、その場合わせではなくGoogle Meetでビデオチャットを取りながらVisual Studio CodeのLive Shareというツールを用いてペアプロを行いました。

ペアプロで実装したもの

ペアプロでは、HEIMアプリの追加機能を実装しました。 HEIMアプリでは商品について口コミ投稿する機能があり、その時に商品を検索して選択するシーンがあります。 今回実装したものは、商品を検索した時、違う商品が出た場合にお問い合わせをしていただけるためにお問い合わせフォームを用意することです。 f:id:shun114:20210520175223p:plain (↑実装前:左、実装後:右)実装後、画面の下側に「違う商品が表示された場合はこちら」の文章とお問い合わせへのリンクが表示されていることがわかります。

コードでは、このような感じになります。

実装前

f:id:shun114:20210520172211p:plain 今回は、緑色でハイライトされた箇所をメインで変えていきました。 ちなみに、ハイライトしているのは僕ではなく、一緒にペアプロしていただいている方です。 このように相手のカーソルの動きやハイライトされている箇所が、こちら側からもわかりやすく表示されています。

実装後

f:id:shun114:20210520172347p:plain 実装後はこのようになりました! ちなみに、お問い合わせの文章とリンクのUI部分は以下のコードになります。

f:id:shun114:20210520172423p:plain 青いハイライトは自分が選択しているものです!他の方とカーソルやハイライトのカラーを分けてくれているので混乱することもなさそうですね!

ペアプロ楽しい

ペアプロは想像していた以上に学びが多く、貴重な時間になりました。 もちろん自分が知らなかった知識や書き方などが勉強になったのが言うまででもないですが、その他にも自分より実力者の方のコーディングの様子をみれることが勉強になりました。

相手の方のスピード感

まず感じたのは自分より実力者の方のコーディングのスピード感に圧倒されました。 Live Shareを使用しているので、目の前PCの画面に映るわけですからより圧倒されます。 知識やスキルの前に、そのあたりのスピード感から自分にもまだまだ伸び代があることを知れるととても嬉しく思います! また、ツールなどを駆使してより効率よく進められたりなど、開発を進めている上でのテクニックなども参考になるものが多いです。(ペアプロなので、それらの知識をリアルタイムで聞くこともできるメリットもあります。)

相手の雰囲気を感じて、より集中できる

僕はフルリモートで働いているのですが、その中でもペアプロは直接コミュニケーションを取れる機会の1つです。 そのため、普段よりも相手の雰囲気を感じ取ってより緊張感を感じれる状態でコードを書くことができました。

より効率よく学べる

途中過程を一緒にコーディングするため開発の進め方の相談もでき、わからない箇所があればすぐに質問ができるためわからない箇所をそのままにして後々全然理解できない状態であったことも防げるため 他のコードを参考にして学んだり、チャット上でのやりとりより、効率よく学ぶことができました。

少し気になったこと

ペアプロを行って全体を通して良い体験になったのですが、少し気をつけた方が良さそうなこともありました。

コードの解読に集中すると、置いていかれることがある

説明を受けるタイミングと、自分で考えるタイミングを分けておかないと「考えている時に説明されていて、どこを説明されているのかわからなくなってしまった」という問題が起きやすいと思います。 この問題は自分が行いたいことや相手にお願いしたいことを伝える、など相手とコミュニケーションを欠かさずにとることによるすれ違いを起こさない工夫をすることによって解消されるのではないかなと思います。 特に隣り合わせでペアプロをしている場合はお互いの様子がみれることによって察することができますが、リモートだと常に相手の映像を見るわけにもいかず相手の様子を知ることが難しいため、自分から積極的に発言したりするなど、より多くコミュニケーションを取っていく必要があると思います。

ペアプロを通しての気づき

ペアプロは良さがたくさんあり、特に教えていただく側の成長に大きく貢献すると思うのですが 意識次第でより効率的に行えるのではないかなと思いました。

何か目的を持って行う

ここでの目的は「つまずいた箇所を教えてもらう」のような抽象的なものではなく、「ここの行数の役割がわからない」「このUIを実装したいけど、どのように書けばいいのかわからない」といったようなわからないこと、教えてもらいたいことを明確にしたものを考えました。 目的を明確にすることによって、質問も曖昧にならずにアドバイスも的を射たものになりやすくなるのではないでしょうか?? また聞きたいことが明確であれば、事前に自分でも調べることができるため、自分の意見を持った上での相談ができると思います! このようにすると、自分で考えた上での相談の形になるのでより理解度が深まるのではないかと思いました。

学び手がコードを積極的に書いてみる

ペアプロは即座に教えていただけることができる環境なので、どんどんチャレンジできる環境なのではないかなと思いました。 フィードバッグやアドバイスをすぐに頂くことが可能であるため、その場でより良い書き方を教えていただいたり、うまくいかなかった時には原因を理解するスピードが早いなど、いいことが多いのではないでしょうか? また、途中浮かんだ疑問点のように些細な質問も聞きやすく理解度を深めやすくなるのではないでしょうか?

最後に

以上、僕自身がペアプロをしてみた感想や気づきを書いてみました! ペアプロがどんな雰囲気か感じ取っていただけたり、何か参考になるものがあれば幸いです。 ペアプロにはメリットが多いと思いますが、それも忙しい中付き合っていただいている相手がいるお陰であることを忘れずにいれば、より充実した時間になると思います。

ペアプロは手段であり、工夫次第によってさらに良い開発体験になると思います!!

最後まで読んでいただきありがとうございます! crispyでは、HEIMの開発を一緒に盛り上げてくれるエンジニアを募集しています!