今後のAdventar

メリークリスマス!Adventarを支える技術 Advent Calendar 2019 の25日目、最終日です。

最終日は今後の Adventar をどうしていきたいかについて技術編と機能編に分けて書こうと思います。

技術編

細かく直したいところはいっぱいありますが、大きめトピックだけいくつか書きます。

SSR のキャッシュを入れたい

SSR のキャッシュについては19日目の記事に詳しく書きました。今は毎回 Lambda を呼び出してレンダリングしているので、キャッシュすることでパフォーマンスをあげたいと思っていますが、コスト(お金)がかかる問題をどうにかしないと...。

gRPC-Web の Node.js 対応

5日目の記事に書きましたが、今 Node.js では gRPC-Web が使えないので SSR の際に仕方なく JSON API を使っていますが、完全に無駄なので gRPC-Web を Node.js でも使えるようにして同一コードでサーバー/クライアント両方動くようにしたいですね。

そのためには gRPC-Web の ptoroc プラグインに手をいれないといけなくて、C++ を書く必要がありそうです。できるのかな。

gRPC サーバーを Lambda で動かしてみる

gRPC サーバーを Lambda と API Gateway で動かすのは、たぶんやればできるんじゃないかなと思ってますが、確証はないです。これができると ECS などのサーバーが不要になり、真のサーバーレスにできます。真のサーバーレスになると圧倒的にコスト削減できるはずなので、取り組んで見る価値はあると思います。

GitHub Actions で CI/CD したい

今はテストは手元で実行するだけになっているし、デプロイも手元からやっているという、プロとして恥ずべき状態です。CircleCI とかでもいいんですが、GitHub Actions ちゃんと使ったことないのでやってみたい。

まあこのぐらいですね。どれも実際にやるかどうかはわかりません。

機能編

サービスをシンプルに保ちたいので、そんなにたくさん機能を追加するつもりはないのですが、現状でいくつかやりたいことはあります。

探しやすくする

今カレンダーを探すのはテキスト検索ぐらいしかないので、もう少しカレンダーを探しやすくするような機能をいれたいです。具体的にはカテゴライズ、スターを導入して人気順、募集中のものだけ検索、などを考えています。

ちなみにカテゴライズについては過去のカレンダーを手動でカテゴライズしてみたのですが、ジャンルが幅広すぎて、あってもなくてもあんまり変わらないのでは、という気になったので一回置きました。機械学習でいい感じにカテゴライズできたりするかな。

アイコン更新できない問題

地味な問題ではあるんですが、今年一番お問い合わせが多かった問題です。詳しくは8日目の記事に書きましたが、Firebase Authentication の仕様の問題で、Twitter などのアイコンを更新した際に Adventar 側のアイコンが更新されるタイミングが難しい感じになっています。

地味なんですけど、アイコン大事なのでどうにかしたいですね。

iOS/Android のアプリ

どちらかというと技術的な興味によるものです。Web よりもいい体験が提供できるんだろうか。ちょっとやってみないとわかりません。

ちなみに技術的には、Swift や Kotlin を学ぶために、それらの言語で作ってもいいし、React Native で作って、Web を React Native for Web で作り変えてみるなんていうのも技術的には面白そうではあります。

以上です。もし機能要望などがあれば TwitterGitHub にお気軽に書いてください。GitHub の Issue は日本語でも大丈夫です。

まとめ

書ききりました。がんばった。全部の記事を読んでくれた方、一部の記事を読んでくれた方、Adventar を使ってくれた方、ありがとうございました。

今年はもう疲れたので何もしませんが、来年以降またがんばろうと思いますので、今後も Adventar をよろしくお願いします!

それでは良いお年を!

Adventar の技術変革の歴史

Adventarを支える技術 Advent Calendar 2019 の24日目です。

今日はこれまで Adventar が利用してきた技術がどのように変わってきたのかを書こうと思います。

2012年

リリースした年です。最初は RubyRails を勉強したいと思い、何かいいサービスの題材はないかなあと思っていて、当時主に ATND で行われていた Advent Calendar が使い勝手が悪すぎて、Advent Calendar 専用のサービスを作ったら使いやすいんじゃないか、というので作りはじめました。(これは ATND が悪いわけではなくて、そもそも ATND はそういう目的のサービスではなかったというだけです)

なので初期実装は Rails でした。サーバーは、たしか YAPC Asia か何かで、当時ペパボに勤めていた刺身さんから、Sqale というペパボのホスティングサービス(Heroku みたいなやつ)のクーポンをもらったのがきっかけで Sqale を使ってホスティングしていました。

値段も安かったし簡単だったんですごくよかったんですけど、今はもう終了しちゃってます。

ちなみに Internet Archive で見つけてきた当時の様子はこんな感じでした。やる気がなさすぎるw

f:id:hokaccha:20191204230504p:plain

2013年

ほぼ一人でやっていたところに、june29ayumikoという強力な仲間を得て、飛躍的に完成度が高まった年です。

june29 さんには僕の初心者 Rails コードをバシバシレビューしてもらい、ayumiko さんにはクソダサかった見た目を見違えるようなデザインにしてもらいました。

また、この年からサーバーは Heroku に移りました。明確な理由はよく覚えてないけど、june29 さんが Heroku に慣れていた、Redis とか memcached などのミドルウェアが Sqale で利用できなかった(ような気がする)あたりが理由だった気がします。

それとフロントエンドは Backbone.js を利用するようになったようです。当時流行ってたんですよ。

当時のデザインはこんな感じだったみたいです。

f:id:hokaccha:20191204230524p:plain

たしか12月になったらカレンダー一覧でなく記事一覧をトップに出したりしてました。背景の画像がなんかかっこいいですね。

2014年

この年は軽微な変更で、特に大きい変更や技術的チャレンジはないみたいでした。あんまり覚えてない。

2015年

Qiita にエントリを書いてました。

https://qiita.com/hokaccha/items/c5cd96c2ec002e27ff4b

サーバーは相変わらず Heroku だけど、フロントエンドを React で書き換えて、react-rails を使った Server Side Rendering を導入しました。

当時 React がちょうど流行りはじめぐらいで、試してみたいなーと思っていたので投入してみた。当時まだ hypernova とかもなくて、Server Side Rendering の知見はほぼない状態だったので色々と苦戦した覚えがあります。

react-rails は Turbolinks とまあまあ相性がよくて、Turbolinks の遷移時のイベントで React Component を Mount/Unmount できるし、初期描画のときだけ SSR、Turbolinks 遷移のときは CSR というふうにできて、ルーティングをクライアント側でやらずに Rails に乗っかれるし、アーキテクチャとしては今でも悪くない気がしてます。流行りはしないと思うけど。詳しくは前に発表したときの資料がありました。

https://speakerdeck.com/hokaccha/react-rails-1

2016年

軽微な変更のみで大きな変更はなかったみたいです。

2017年

この年は ECS にしてみました。

当時仕事でも ECS を使い始めていて、基盤チームが色々整備してくれていたのだけど、自分では何もわからなかったので一回 ECS でインフラを作ってみたかったというのが動機です。

とにかく難しくて大変だった記憶があります。まあもちろん今年のほうが大変だけど。

2018年

ECS にして毎月1万円ぐらいかかることが判明して、ECS だいたい理解したし、できるだけ安価に運用したいということで雑な VPS に移して月1000円ぐらいで運用できるようにしました。

itamaeで構成管理したり、 Mackerel を使い始めたりしました。あとなぜかタイムスリップして古きよき Capistrano によるデプロイになりました。

Heroku に戻らなかった理由は覚えてないけど、なんでだっけな...。単純に VPS のほうが安かったからかな。

2019年

とにかくモダンな構成にしたいと思ってがんばって作り直しました。

来年には Heroku に戻ってるかも知れないです。

まとめ

Adventar ができてからの歴史を振り返ってみました。2013年に基本的なかたちができてか大きい機能や見た目の変更はなく、個人サービスなので技術的なチャレンジを色々とやってみる実験台みたいになってます。

今後も大幅な機能変更とかの予定はないけど、できるだけ使いやすくはしたいし、長くサービスを継続できるようにがんばりたいし、技術的チャレンジももっとやっていきたいと思います。明日はいよいよ最終日です。今後のAdventarの技術的チャレンジや方向性について書こうと思います。

サービスをオープンソースにする

Adventarを支える技術 Advent Calendar 2019 の23日目です。

今年から Adventar はオープンソースにしました。

ツールやライブラリ、言語などのソフトウェアであれば今の時代オープンソースというのは山程ありますが、サービスがオープンソースというのはそんなに多くないと思うので今回はそうした理由や、いい点、悪い点などについて書こうと思います。

オープンソースにする理由

特にクローズドである意味もないので、オープンソースにしたいとは前々から思っていて、昔のコードはオープンにしづらい履歴もあるし、システムリニューアルのタイミングでちょうどいいので、このタイミングでオープンにしました。

オープンソースにして誰かの役に立てば嬉しいし、誰かが勝手にバグを直したり機能改善をしてくれるかもしれないし、Fastly や AWS などはオープンソース支援で料金を補助してくれたりするし(試しに申請してみる予定)、いいことばっかりです。

ちなみにすでに実際に何件か Pull Request を頂いて、オープンにしてよかったと思えました。ありがとうございます。

https://github.com/adventar/adventar/pulls?q=is%3Apr+is%3Aclosed

(自分は master 直 push マンなのがバレる)

また、オープンにすることであまり雑にできない、という緊張が生まれるのは良いですね。実際コードを読んでる人はほとんどいないでしょうが、見られる可能性がある、誰かの参考にされる可能性がある、というだけで、ちゃんと書かないと、という意識になります。と、書いてて思ったけどこれってサービスに限らず普通の OSS でも同じですね。

ちなみに緊張感があるといいつつ、実際時間がなくて(いいわけ) Go のコードとかはけっこうひどい感じではあります。

懸念点

セキュリティ

少し大変なのは、オープンになることでセキュリティリスクが高まることかなと思います。ただ、世の中のはオープンソースのソフトウェアで溢れていて、それに対して脆弱性もばんばんでているわけで、アプリケーションのコードだけ隠しても劇的にセキュリティが強固になるとは思いません。もちろんオープンよりはクローズドのほうがセキュリティリスクは減ると思いますが。

また、クローズドだと雑にリポジトリ内に入れられるような情報を、オープンだと入れられない(ので環境変数などにする)、みたいなのは多少ありますが、より健全になるだけなのでこれについてはオープンのほうがいいですね。

ただ、terraform のコードをオープンにするかはけっこう迷いました。ネットワークまわりの設定を間違えていたりすると危険だし、あまり外に出したくない情報ではあります。

が、そういう理由もあり terraform でのインフラ構成はどは特に外には出さないので、こういうものこそ誰かの参考になれば、という気持ちでオープンにしました。危険な設定を見つけたらこっそり教えてください。

サービスを真似される危険

オープンソースにしたときに聞かれたことがあったので一応書いておきます。

営利目的だと話は違いますが、Adventar を真似て作られたところで痛くないし、むしろ Adventar より使われるようなサービスになった喜んで Adventar を閉じます。他には雑なスパムみたいなコピーサービスが増えるみたい可能性もありますが、それは別にクローズドでも見た目を真似ることはできるし、結局コンテンツがないと意味がないので心配してません。

ちなみに、サービスのコードをオープンにしているところはいくつかあって、有名どころだと dev.to や gitlab などがそうです。

これらのサイトはオープンですが特にそれを使って類似サービスがでたりはしてないし、心配するだけ無駄だと思っています。

まとめ

今日はサービスをオープンソースにして開発する意味や懸念点について書きました。明日は Adventar の歴史について書こうと思います。

細かすぎて伝わらない UI の工夫

Adventarを支える技術 Advent Calendar 2019 の22日目です。

さすがにネタ切れ気味なので、UI 系の細かいネタを投下します。

ユーザーアイコンが404のときにフォールバック

Adventar では Twitter などのソーシャルログインを使っていて、ユーザーのアイコンは各プロバイダから取得できる画像の URL をそのまま使っています。その URL がずっと使えるならいいのですが、ユーザーがプロバイダのほうでアイコンを更新した場合などに古いアイコンの URL がリンク切れになってしまいます。

そうなると、ひどいと以下のようなってしまいます。

f:id:hokaccha:20191219223950p:plain

これはひどい。せめてデフォルトアイコンを設定するようにしたいところです。しかし、アイコンの URL はアクセスしてみないと取得できるのかどうかわかりません。そこで img 要素の onerror イベントをトリガーしてデフォルト画像にフォールバックしています。

<img src={{user.icon}} onerror="this.src = '/default.png'">

実際は Vue.js でやっていてもう少し複雑ですが、こんな感じのイメージです。実際のコードは以下です。

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/frontend/components/UserIcon.vue

これでさっきの画面は以下のようになります。

f:id:hokaccha:20191219224003p:plain

だいぶマシですね。

部分的にローディング

API の呼び出し待ちなどにローディングを表示するのはユーザーに状況を伝えるのに重要なアクションですが、ローディングを表示するのはできるだけ小さい範囲に留めるようにしています。

例えばトップ検索画面のカレンダー一覧ですが、カレンダーの一覧取得には API の結果を待たないといけないので、返ってくるまでしばらくラグが発生します。そのとき全画面ローディングにしてもいいのですが、以下のように、必要な部分だけをローディングにすることで、可能な限りユーザーに速く画面を見せます。

f:id:hokaccha:20191219224035p:plain:w300

また、8日目の記事に書いたのですが、最初のログイン処理にやや時間がかかってしまうため、その間ローディングを出したいのですが、これもログイン情報が必要なところは多くないので、必要なところだけローディングにして体験を損ねないようにしています。

f:id:hokaccha:20191205123054p:plain:w300

これでだいぶ初期表示が速くなり、体験がよくなります。

吹き出しの位置のこだわり

これは本当に細かいのですが、カレンダーで登録情報を編集するポップアップがあるのですが、この吹き出しの位置に微妙なこだわりがあって、端の登録を押したとき、スマホなどの画面幅が狭い場合は以下のようになります。

f:id:hokaccha:20191219224120g:plain

可能な限り選択した吹き出しに近い位置に出して、吹き出しの三角は選択したセルを指します。

また、PC などの画面幅が広いデバイスで見た場合は左右に余白ができるので、端のセルでも中央にポップアップを表示したいところです。

f:id:hokaccha:20191219224138g:plain

書いてみると当たり前の動きな動きすぎてなんでこれを紹介しているのか自分でもよくわからなくなってきましたが、この動作をあらゆる画面幅で動作するように実装するのが思いの他大変だったので紹介したくなっただけです。

実装を見返してみると色々ハードコーディングしてあったりして泥臭くてだいぶひどい感じですが、がんばった後が見られますね...。

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/frontend/components/CalendarTable.vue#L188-L212

まとめ

今日はネタ切れ気味で細かい UI の話を書きました。本当に細かすぎてすいません。明日は Adventar をオープンソースで公開した話を書こうと思います。

View のモバイル対応

Adventarを支える技術 Advent Calendar 2019 の21日目です。

このご時世には信じがたい話ですが、去年まで Adventar はスマホで見ると PC View を縮小するだけで、スマホでは非常に使いづらいサービスでした。今年のシステムリニューアルでは機能追加したりや見た目を変える余裕はなかったのですが、さすがに恥ずかしい、アクセスも iOS が一番多いという言い訳のできない証拠があったので、モバイル対応だけはやることにしました。

レスポンシブと出し分け

モバイル対応(というかマルチデバイス対応)は、大きく分けて2つの手法があって、HTML は同一で、画面の大きさによって CSS を変更することで対応するレスポンシブ(Webデザイン)と言われる手法、ユーザーエージェントなどの情報を元に、スマホ用や PC 用などの HTML を出し分ける方法があります。

機能やレイアウトがガラっと変わるのであれば出し分けのほうがよいですが、スタイルの変更だけで要件がまかなえるのであればレスポンシブのほうが楽な場合が多いです。

個人的には、スマホで見たときと PC で見たときにコンテンツの位置が全然違ったり、PC にあった機能がスマホだとなくなっているという体験が嫌いなので、今回はレスポンシブで対応しました。

ブレイクポイントを決める

ブレイクポイントは感覚がよくわからないので Twitter Bootstrap を参考にしました。

https://getbootstrap.com/docs/4.3/layout/overview/#responsive-breakpoints

// Extra small devices (portrait phones, less than 576px)
// No media query for `xs` since this is the default in Bootstrap

// Small devices (landscape phones, 576px and up)
@media (min-width: 576px) { ... }

// Medium devices (tablets, 768px and up)
@media (min-width: 768px) { ... }

// Large devices (desktops, 992px and up)
@media (min-width: 992px) { ... }

// Extra large devices (large desktops, 1200px and up)
@media (min-width: 1200px) { ... }

もともと Adventar の PC View は最大幅が 1000px で、992px, 1200px あたりは不要だったので、576px と 768px だけ採用し、以下の3パターンに絞りました。

サイズ
Smallwidth < 576px
Medium576px <= width < 768px
Large768px <= width

Small

f:id:hokaccha:20191219211632p:plain:w300

f:id:hokaccha:20191219211824p:plain:w300

Medium

f:id:hokaccha:20191219211854p:plain:w420

f:id:hokaccha:20191219211909p:plain:w420

Large

f:id:hokaccha:20191219211928p:plain:w600

f:id:hokaccha:20191219211941p:plain:w600

モバイルファーストで作る

PC 用のスタイルを画面の小さいスマホ向けに変更していくのはけっこう難しいのと、基本的に PC で開発するのでモバイルの確認が漏れがち、などの理由があり、モバイル向けの画面をプライマリで作り、そこから幅を広げた場合の画面を作っていくことにしました。

実装としては、メディアクエリに max-width でなく min-width を使うのがポイントです。例えば480pxをブレイクポイントにしてスタイルを変更するケースを考えてみます。max-width を使うとこうです。

// PC 用
.btn {
  font-size: 20px;
}

// スマホ用
@media (max-width: 480px) {
  .btn {
    font-size: 14px;
  }
}

min-width の場合はこう。

// スマホ用
.btn {
  font-size: 14px;
}

// PC 用
@media (min-width: 481px) {
  .btn {
    font-size: 20px;
  }
}

基本的には media query で元スタイルを上書きしていくほうがやりやすいので、min-width を使うと自然と画面が小さいサイズ向けのスタイルがプライマリになります。

まとめ

UI をモバイル対応した話を書きました。明日は細かすぎて伝わらない UI の工夫について書こうと思います(ネタ切れです)。

Bugsnagを利用したエラートラッキング

Adventarを支える技術 Advent Calendar 2019 の20日目です。

今日はエラートラッキングについて書きます。

Bugsnag

エラートラッキングのサービスは色々あって、有名なのは SentryAirbrake あたりでしょうか。今回は Bugsnag というサービスを利用しました。これは Rails 時代から使っていて、無料で使える範囲が一番大きそう、という基準で選びました。

Bugsnag だと 250 events/day は無料枠で使えるので、Adventar ぐらいの規模であればスパイクしなければ余裕です。Sentry も今見たら無料で 5000 events/month なのでこっちでもいけそうな気がします(昔からこうだっけな)。

今回は Go の gRPC サーバー、Nuxt.js で SSR しているところに Bugsnag を使っています。フロントエンドの JS でも動くのですが、経験上フロントエンドでのエラートラッキングはノイズが多く、無料枠を食いつぶしてしまう可能性がありそうだったので今回は入れていません。もしかしたらそんなことはなくて、意外とさくっといける可能性はあります。

Rails では何もはまらずに使えていたのですが、Go と Node.js に有効にするのにけっこう苦労したので、それについて書いておきます。

Go/gRPCでの利用

Bugsnag の Go SDK はいくつかのフレームークに対応してますが、gRPC はありませんでした。

https://docs.bugsnag.com/platforms/go/

なのでOther Go appsを見て自力でどうにかする必要がありそうです。

Go のサーバーでエラーをトラッキングしたいのは主に

  • 予期せぬエラーになった場合
  • panic で死んだ場合

の2つで、この場合に

bugsnag.Notify(err, ctx)

を呼べばよさそうです。最終的には以下のようなコードを Interceptor に刺しこみました。

func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = grpc.Errorf(codes.Internal, "Internal Server Error")
            fmt.Printf("%s\n", r)
            if bugsnagAPIKey != "" {
                bugsnag.Notify(fmt.Errorf("%s", r), ctx)
            }
        }
    }()
    resp, err := handler(ctx, req)
    s, _ := status.FromError(err)
    if s.Code() == codes.Unknown {
        stacktrace := fmt.Sprintf("%+v\n", err)
        fmt.Print(stacktrace)
        if bugsnagAPIKey != "" {
            bugsnag.Notify(err, ctx, bugsnag.MetaData{"info": {"stacktrace": stacktrace}})
        }
        err = grpc.Errorf(codes.Internal, "Internal Server Error")
    }
    return resp, err
},

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/api-server/grpc-server/service/service.go#L75-L96

今回エラーハンドリングには xerrors を使っているのですが、xerrors で wrap したエラーを投げると、bugsnag 上でのエラーが全部 *xerrers.Wrap になるの困っています。どうにかしたいのですが、エラーの量もそこまで多くなくて困らないので放置しています。

f:id:hokaccha:20191218223657p:plain

また、スタックトレースの情報がわかりづらく、例えば以下のエラーは

f:id:hokaccha:20191218223713p:plain

以下の箇所で発生していますが、entry.go:118スタックトレースに表示されません。

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/api-server/grpc-server/service/entry.go#L118

この問題はメタデータとして、fmt.Sprintf("%+v\n", err)を送信することで一時しのぎしています。これは以下の表示されます。

f:id:hokaccha:20191218223734p:plain

わかりやすい...。もう少しちゃんとしたやり方があると思うので直したいところです。

Node.js/AWS Lambdaでの利用

SSR している Lambda では express を利用しているので、ドキュメントに沿って導入してみたが動きませんでした。どうやら bugsnag へのエラー送信が終わる前に Lambda が終了してしまうことが原因みたいだったようです。Issueもありました。

https://github.com/bugsnag/bugsnag-js/issues/495

エラーを送信する処理(bugsnagClient.notify)を自分で実行して、その終了を待てばよさそうなのですが、必要な情報を自分で詰めないといけないのでややめんどうでした。例えば express plugin を使えばこのあたりでやってくれます。

https://github.com/bugsnag/bugsnag-js/blob/96238c360d1f021af9d006fead5d10f827cf0079/packages/plugin-express/src/express.js#L64-L76

とりあえず最低限の情報だけ詰めて対応しました。

app.use((err, req, _, next) => {
  const opt = {
    request: {
      headers: req.headers,
      httpMethod: req.method,
      url: req.url
    }
  };

 // このコールバックでエラー送信の終了を待つ
  bugsnagClient.notify(err, opt, () => {
    next(err);
  });
});

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/frontend/server.ts#L60-L69

また、もう一つの問題は source maps です。

https://docs.bugsnag.com/platforms/javascript/source-maps/

ブラウザの JS であれば、sourceMappingURL に書いてある URL に取りに行ってくれるので便利ですが、Node.js の場合は source maps のファイルを別途 Bugsnag にアップロードする必要があります。がんばればできそうですが、今回はめんどうなので諦めました。いつかやるかもしれません。

まとめ

今回は Bugsnag を導入するのにはまったことなどを書きました。実際 Bugsnag はかなり役に立っていて、Bugsnag のおかげでいくつかのエラーを特定して潰すことができました。いくつか中途半端になっているところがあるので今後改善していきたいと思います。

明日は UI のスマホ対応について書きます。

SSR の CDN によるキャッシュ戦略

Adventarを支える技術 Advent Calendar 2019 の19日目です。

今日は Server Side Redering (以下 SSR) した結果を CDN でキャッシュする戦略について書きます。SSR の概要については以下にも概要を書いたので一読しておくとよりわかりやすいと思います。

上記に書いてありますが、今回は SSR を Lambda で行っていて、Lambda の実行はそんなに速くないし、実行回数での課金になるので、前段の CDN でキャッシュすることで、パフォーマンスとコストを最適化することができます。

その設計について書こうと思いますが、、先に書いておくと今回は SSR の結果をキャッシュせずに毎回リクエストのたびに Lambda を実行しています。そのあたりの理由も含めて書いていきます。

キャッシュのパージ

キャッシュはやれば速くなるのは確実ですが、コンテンツが更新されたタイミングで適切にパージしないと、新しいコンテンツがユーザーに届きません。パージの戦略は提供するコンテンツやキャッシュする範囲によっても様々です。

例えばブログサービスのようなサイトでページキャッシュするケースを考えてみます。単純に考えると、ブログ記事が更新されたときに対象のブログページだけパージすればよさそうですが、キャッシュする範囲によってはその限りではありません。

例えばブログにコメントや Like のような機能があって、それらも含めてキャッシュしているならコメントや Like が更新されたタイミングでも必要かもしれません。また、トップページや最新記事一覧ページのようなところに記事のコンテンツが表示されていて、それらのページもキャッシュしているなら、すべての記事作成、更新でそれらのページのパージが必要です。

さらに、はてなブログのようなログイン機能があったらどうでしょう。ログインしている状態でキャッシュをつくると最初にキャッシュしたときにログインしているユーザー情報が全員に表示されてひどいことになります。そんなことないだろ、と思うかもしれませんが気をつけていてもミスによってそういった状態になってしまう例をいくつも見てきました。最近ではメルカリのニュースが記憶に新しいです。

なのでキャッシュは狭い範囲で必要最小限に留めるべきです。例えば上記のようなブログサービスの例であれば、コメントや Like、ユーザー情報の表示はクライアント側(JS)でレンダリングする、キャッシュするのは記事ページのみに留める、などです。

今回の Adventar でも、キャッシュはカレンダーページ( https://adventar.org/calendars/3860 のようなページ)だけに留め、ログイン情報も SSR では扱わず、クライアント側で認証情報を付け足す、という設計にしました。認証については以下に書きました。

これで、キャッシュする範囲は最低限ですが、それでもパージするタイミングは少なくありません。

  • カレンダーのタイトル、概要の更新
  • エントリの登録・編集・削除
  • ユーザー情報(名前、アイコン)の変更
  • デプロイ

などです。このような操作が行われたときに、どうやってパージするかを見ていきましょう。

Fastly によるパージ

まず最初にこの設計を考えたときに一番手にあがったのは Fastly です。Fastly には Instant Purge という機能があり、パージリクエストをして 150ms 以内にキャッシュをパージしてくれます。

https://www.fastly.com/products/web-and-mobile-performance/caching-and-purging

パージの速さはユーザー体験につながるので、ぜひこれを使いたいところです。Fastly で具体的にどのようにパージするかを説明します。

  • カレンダーのタイトル、概要の更新
  • エントリの登録・編集・削除

これらは単純で、該当カレンダーページのキャッシュだけをパージすればいいだけです。Fastly のパージは非常にシンプルで、該当の URL に PURGE メソッドでリクエストするだけです。

https://docs.fastly.com/api/purge

$ curl -X PURGE https://adventar.org/calendars/1

だけで済みます。特別なクライアントもいらず、非常に簡単です。

  • ユーザー情報(名前、アイコン)の変更

はもう少し複雑で、そのユーザーが登録しているカレンダーページをすべてパージする必要があります。一つ一つ丁寧に上記のPURGEメソッドでパージすることもできますが、数が多くなると負荷も大きくなりますし、fastly の API Limit もあります。こういうときに使えるのが Surrogate-Key という機能です。

https://docs.fastly.com/en/guides/getting-started-with-surrogate-keys

この機能を使うと、キャッシュをタグのようなものでグルーピングして一括パージすることができます。リクエスト時にこのヘッダに複数の値を指定します。例えばSurrogate-Keyの値にu1 u2 u3のように、そのカレンダーに登録しているユーザー ID のをもたせておきます。そして user_id: 1 のユーザーがプロフィールを更新したら、u1 を指定してキャッシュをパージすることができます。

  • デプロイ

デプロイ時にはすべてのキャッシュを消す必要がありますが、それは purge_all という API があるのでこれを使います。

https://docs.fastly.com/api/purge#purge_bee5ed1a0cfd541e8b9f970a44718546

このように、やりたいことは完璧に実現できそうだったので Fastly を使いところではあったのですが、Fastly の料金は最低料金が $50/month ということで、コスト面が折り合わず今回は断念しました。

一方 CloudFront は、最低利用料金がなく、転送量だけであれば先日の記事にも書いたように、最もアクセスが多い12月でも$30〜$40ぐらいで落ち着きそうで、1月〜10月はほぼアクセスがないのでこれの 1/10 ぐらいに収まると思っています。個人サービスの上に1円も稼いでいないサービスなので $600/year はけっこうきついので、今回は CloudFront を採用しました。

なお、Fastly にはオープンソース向けの無償アカウントがあるので、これを申請してみようかと思っています。

https://docs.fastly.com/en/guides/accounts-and-pricing-plans#free-open-source-developer-accounts

CloudFront によるパージ

まず、CloudFront のパージは Fastly ほど高機能ではありません。パージの速度も Fastly ほど速くありません(公式のドキュメントが見つけられませんでしたが、少なくとも ms のオーダーではない)し、Surrogate-Keyのような機能もありません。

それだけであれば許容できると思ったのですが、調べてみると思ったよりパージに料金がかかることがわかりました。

https://aws.amazon.com/cloudfront/pricing/

月間で無効をリクエストしたパスの最初の 1,000 パスまでは追加料金なし。それ以降は、無効をリクエストしたパスごとに 0.005 USD かかります。

どのぐらいパージ処理が走りそうかを概算してみます。

  • カレンダーのタイトル、概要の更新
  • ユーザー情報(名前、アイコン)の変更
  • デプロイ

については、回数も多くないし一旦無視します。問題は

  • エントリの登録・編集・削除

です。去年ベースで考えるとエントリの数は13767、そのうちコメントが更新されているもの12063、URL が更新されているものが11893でした。Adventar ではこれらの更新処理はタイミング的に別々に行わられるので、単純に足し算になります。さらに削除や、コメントやURLを複数回変更する場合もあります。ここではざっくり全体の10%ぐらいで計算してみます。

  • 13767 + (12063 * 1.1) + (11893 * 1.1) = 40118.6

これらはほぼすべて11月、12月で呼ばれるので、その2ヶ月でどのぐらいかかるかを出してみます。また、無料枠が 1000req/month あるので、それも加味します。

  • (40118.6 - 2000) * 0.005 = $190.593

だいたい $100/month くらいといったところです。ざっくり計算ですが、カレンダーや登録数は年々線形に伸びているし、もっと多くなる可能性もありそうです。

11月と12月以外は無料枠に収まると思うので、トータルで見ると Fastly よりは安く済みそうですが、ピーク時のコストを10,000円前後ぐらいで考えていたので少し厳しい金額です。

なので、今回は CDN によるキャッシュを諦め、毎回 Lambda を実行することにしました。ちなみにこれによって Lambda の実行回数は多くなりますが、昨日の記事にも書いたように Lambda のコストは $0.4 ぐらいになりそうです。爆安。

まとめ

SSR の結果を CDN でキャッシュする戦略と、コスト面でその戦略を諦めた話を書きました。キャッシュによるパフォーマンスの最適化はユーザー体験にかなり寄与すると思っているので、Fastly のオープンソース向けの無償アカウントなどでコスト面の折り合いがつけばまたチャレンジしたいと思っています。

明日は Bugsnag によるエラートラッキングについて書きます。