メッセージングアプリの永続化サービス・ミドルウェアの選定について

色々有識者の方に相談に乗ってもらったのでシェア。

要件的にはまず完成を目指して、チューニングはちょっとあとからということで進んでいたのだけど、 せっかく作るのだから少ない労力である程度の規模までは戦えるようにしたいよねーくらいなレベルで、少し考えて永続化するミドルウェア・サービスを選定してみることにした。 *1

RDBMS

まずこれは一番扱いやすいしすでに資産としてある。 技術者も多いので真っ先に思い浮かぶんだけど、有識者と相談して、

  • 「メッセージングで利用するのであれば、すぐに限界来るだろうし...」
  • トランザクションが不要な要件ではもったいない気がする...」
  • 「とりあえずちょっと考える時間あるし、他あたってダメそうなら…」

というまぁ当たり前の結論になって次の選択肢を検討した。

Redis

これも候補として上がってきた。 Pairy何かが最初期はRedisでチャットのメッセージを保存していたようだ。(後にDynamoDBに移行している。)

techblog.timers-inc.com

techblog.timers-inc.com

あとは、これは組み合わせで利用する用途だけど、まずアプリケーションはRedisにメッセージを書き込んで、 一定時間毎にRDBMSなんかにBulkInsertするような仕組みもいいね、という話にもなった。

ただデータの永続化が難しいこと、AzureのRedisCacheでクラスターをシュッと立てるにしてもかなりお高い値段なのと、管理が大変そうなので、現実的ではない... なので、「Redisだけ」という選択肢はすぐに外れた。 また、SidekiqあたりでRDBMSへの書き込みを非同期に行うケースも結構ありなのなとは思った。

HBase

正直、キャッチアップしている余裕もないしスモールスタートできなそうということで候補からはあっさり外した。

DynamoDB

プライシングでもスモールスタートできるし、 データベース構造でもメッセージングに最適だと思っていて、初めて登場してからしばらく立っていて実装も枯れている。*2 RubySDKもあり、今回の案件で自分がが求めているものに一番近いように感じた。

aws.amazon.com

ただ、社内的な制約があって弊社ではメインのクラウドサービスにAzureを利用していて、 できればAzure内のサービスで完結させておきたいという気持ちがある。そこで候補に上がったのがCosmosDBだった。

CosmosDB

AzureでのDynamoDBの代替サービスだけど... 自分は100%そうじゃないと思っていて、理由としてはCosmosDBが複数のAPI(接続方法)に対応しているからである。 その中でサポートしているものから、候補を模索していくことにした。

docs.microsoft.com

MongoDB API

結構最終候補くらいまで残っていた。

docs.microsoft.com

理由はRDBMSよりもスケールアウトしやすいのと、そこまでの整合性が必要ではなかったこと。 Mongoはメインで利用している言語(Ruby)のサポートもそれなりにあって、結構知見があったことから。 ただ、CosmosDBでもいくつかの接続APIがあり、それが異なることによって、スループットの差が「多少」生じるとのことだった。 Mongoはスキーマレスだけど、大量Write,Readのあるようなチャットに向くソリューションかどうかわからないので、一旦他の方法を検討してみることに。

Cassandra API

MongoDB互換だけでなく、Cassandra互換も用意されていた。

docs.microsoft.com

Cassandraの構造が一番メッセージングに適している、かつマネージドなので非常に良い選択肢として模索していた。 が、Cassandraの構造からメッセージングサービスのデータ構造を設計するに妙に難しさを感じていて、 心が折れてしまったので、ほかを当たってみることにした。

あとは扱えない技術者が少ないのがネックかも。

Firebase Firestore

Firebase RealTime Databaseのクエリ周りをいい感じにしたプロダクト。

firebase.google.com

RealTime Databaseよりもクエリ周りが便利なのと、WebフロントエンドやiOS, AndroidSDKからも書き込みが直接可能で、 Realtime Database共々、チャットアプリを作成するのによく利用されているイメージがある。

更に永続化だけではなく、 フロントエンドから読み込みを行うことでリアルタイムで追加・更新・削除処理が簡単に行えるので、 Websocketあたりのミドルウェアを自前で用意する必要もなくなった。 永続化のサービス・ミドルウェアの選定というところから少しスコープが外れているかもしれないが、これは本当に大きかった。

ただ、永続化サービスという意味ではちょっと違うかもしれない…?

いろいろ考え、有識者に相談した結果

RDBMSとFirebase(Firestore)の併用案にひとまず落ち着いた。 基本的にはメッセージの参照・読み込みをFirestoreに、それ以外のリソースに関してはAPIを経由して、RDBMSを参照するようにしている。 メッセージの書き込みをAPIを経由して書き込み、APIサーバから非同期でFirestoreにWriteを行っている。

やはり、チャットの大部分を占める読み込みをFirestoreに任せることで、 Websocketの実装や管理が不要だったのが一番大きいかなと。

別にFirestoreだけでもいいじゃんということなんだけど、Firestoreだけでの運用は、

  • やっぱりベンダーロックインが怖いこと。
  • フロントエンドから色々書き込むのは処理が煩雑になりがちになってしまうこと。
  • 既存のアプリケーションの1機能として入れるので、サーバーサイドを通したほうが実装と管理がラク。
    • チャットに投稿できるかどうかをサーバーサイドで判定してから、Firestoreに流すとかやりたい。
  • RealtimeDatabaseに比べてクエリ周りで便利になったとは言え、検索周りでAPIあったほうが今後便利では

ということになりこの案を採用して現在は動いている。*3

そんな感じで、現在はメッセージングアプリケーションを作っている最中だ。 サーバーサイドの観点では結構手抜き構成にしてあったのが幸いして、 苦労しがちなフロントエンド周りに時間を掛けることができて非常に助かっている。

*1:いきなりチャ社の新アーキテクチャのような設計は難しいし、そんな時間がそもそもない。用途的にも2〜3年で1億メッセージも行かないでしょうと踏んでいる。

*2:ソースとしてWikipediaが良いかはわからんが、2012年に出来たとのこと。Amazon DynamoDB - Wikipedia

*3:有識者・僕ともに、サーバーサイドエンジニア寄りだったというのと、既存の資産があったので。

Railsでのお手軽なキャッシュ戦略

キャッシュの主な動機は、

  • パフォーマンスの向上
  • より多くのトラフィックを捌けるように
  • サーバー・リソースの節約

が主だとおもう。 今回、Webメディアの運用が決まったあたりで、サーバーサイドのキャッシュを真面目に考えてみたので記録を簡単に残しておく。

キャッシュで気をつけるべき点

キャッシュで気をつけるべき点はもうキャッシュコントロールの一言に尽きる。 これを気をつけていい感じに取ってパフォーマンスを向上させるのが今回のお仕事。

主にRailsでのキャッシュ戦略について

  1. Railsでフラグメントキャッシュを取る
  2. Railsでページキャッシュを取る
  3. Nginxでキャッシュをとる

あまり調査 -> 実装 -> リリースまでの時間がなかったことから、お手軽に対応できそうな方法を中心に検討してみることにしました。

今回、CDNでのキャッシュ戦略は取っていないが、有効な手段だと思う。 後日検証を行って追加する予定である。順を追って簡単に説明する。

フラグメントキャッシュ

キャッシュコントロール

キャッシュ効果 ◯

Railsで一番ポピュラーに取られている(と思われる)キャッシュ手法である。

ただ、どうしてもWebアプリケーション(というかUnicornやPuma)で処理を行う以上、 前段のNginxで処理を完了させるキャッシュの方式よりも効果は少ない。 とはいえDBへのアクセスは減らせるのと、キャッシュコントロールの難易度が他に比べるとかなり楽にできるので、 ログイン処理のような動的コンテンツがあるようなWebページでも導入が容易だと思う。

その他のやりやすさ

他にもサーバーサイド側でデバイス判定して、 HTMLの出し分けだったりでどうしても動的な処理がサーバーサイド側で発生する場合でもそこまで心配せずに導入することが可能。 また、HTMLの一部分だけをキャッシュするという手法も取れるので他の手法に比べて考える事、 キャッシュを取ることによってできなくなることはほとんど無いように感じる。

ページキャッシュ

キャッシュコントロール

RailsRuby)側でのファイル操作が可能なので、記事を更新したときに更新した記事だけの、ページキャッシュを削除したりということがひとまず可能。 後述するNginxのキャッシュよりもキャッシュのコントロールが幾分ラク。 ただどうしても静的ファイルとして出力するので、色々柔軟性には欠ける。

キャッシュ効果 ◎

静的ファイルを直接nginxから呼び出せば効果はばつぐんだ。

その他のやりやすさ

サーバーサイドでデバイス判定して、ページ構成を変更しているようなページを処理するときには注意が必要。 それぞれで異なるページキャッシュを作らないと行けないし、PC用のページキャッシュは必ずPCに、SP用のページキャッシュを必ずSPに返すために工夫が必要だし、 ドキュメントだけを眺めてもそのやり方は書いていないので、自前で実装と言うか工夫が必要になる。 後述するnginxのキャッシュの方がオフィシャルにデバイスによって取得するキャッシュキーを変更できる機能を持っている分、nginxのキャッシュの方が良いのではないか?と思っている。

nginxのキャッシュ

キャッシュ効果 ◎

当然ながらキャッシュの効果は絶大。 Railsのページキャッシュとのスピード面の計測はしていないけど、まぁ変わらないのではないかと思っている。

キャッシュコントロール ×

ただキャッシュのコントロールについては良くないと思っており、 どこかを更新した際には、nginxのすべてのキャッシュファイルを削除するしか無いと思っている。(どのファイルがどの記事のキャッシュかを判別する情報がない) ただ、NginxでキャッシュをとりつつRailsでSessionのブラウザCookieベースで管理する場合、 色々工夫が必要で大変なので、CookieベースのSession管理を取りやめRedisやDBでSessionをStoreする方が良いかもしれない。

結局どの戦略を取ったか

動的に便利にやりたい部分があったというのと、実装までに期間が余り取れなかったという理由もあって、ひとまずフラグメントキャッシュで様子を見てみようということにした。 動作として特に問題が発生していないのと、結構潤沢にサーバーを使える環境というのもあったので、この方法で対応した。

そして、1000万以上PVくらいのWebメディアを半年くらい運用していたのだけどとりたてて問題は発生しなかった。 とりあえずDBアクセスをなくすだけでも効果は絶大なんだな、ということを実感しました。

予算等の見直しや要件に変更が入った場合は2や3への変更を視野に入れる必要もあったのかな、と思っている。

まとめ

  • キャッシュは色々便利だけど、最初はなるべくキャッシュは利用しない方向でできるだけ頑張る
  • 最初は簡単なのでフラグメントキャッシュから始めるといいです。
    • nginxのページキャッシュやRailsのページキャッシュの導入はその後検討でいいと思う。

PAY.JPアンチパターンという内容でLTした

PAY.JPのプラットフォーム向けサービスである、 「PAY.JP Platform」のリリース記念ミートアップで、「PAY.JP アンチパターン」という内容でLTをさせていただきました。

内容

ざっくり見てもらえればわかるのですが、 PAY.JPを使っていく際のアンチパターンを3つほどまとめたものになります。 基本的には定額課金周りのアンチパターンが多めな感じです。

雑感

  • はじめてのLTだったので、それなりに緊張したが、なんとかやりおおした。
  • 発表のしかたがあまり良くない気もしたが、LTの内容は自分のが一番テックな内容だと思っていて、その点はよかった。
  • 今後もLT参加したい。
  • 今後PAY.JPのアンチパターンのスライドの完全版を作りたい。
    • いくつかのパターンを追加するのと、図解や補足説明を入れるようにしたい。
  • ちなみにPAY.JP Platformはうまく使えばかなり便利なのと、競合他社製品に比べると安さ・シンプルさがウリなので、ぜひつかって見てほしい。

また、LTにお誘いいただいた、 PAYの皆様、ありがとうございました。そしてお疲れ様でした。

いちばんやさしい新しいSEOの教本を読んだ

個人的興味や、知識の補完のためにSEO入門するに当たり、みんながおすすめしているように見えたので、この本を買って読んで見ることにした。

前提として、SEO対策についての知識は簡単にWebで調べれば出るものくらいは知っている感じ。

内容について

目次は公開情報なので、下記に記載してみる。

◆Chapter 1 SEOの目的と考え方を身に付けよう

◆Chapter 2 Webサイトの目的と訪問者の目的を考えよう

◆Chapter 3 有効な検索キーワードを調査しよう

◆Chapter 4 業種別に最適なサイト構成を考えよう

◆Chapter 5 適切な内部対策でSEOの効果を高めよう

◆Chapter 6 質の高い外部対策でWebサイトの価値を高めよう

◆Chapter 7 ソーシャルメディアからWebサイトに集客しよう

◆Chapter 8 技術的な問題を解決して優れたWebサイトを目指そう

◆Chapter 9 SEOの効果を分析してさらなる改善を進めよう

・付録 1 業種・ジャンル別キーワード一覧表

・付録 2 サイト全体のテンプレート一覧管理表

・付録 3 今すぐ使えるSEORFPフォーマット

・用語集

Chapter1はともかく、 Chapter2,3,4はサイトを作る前の準備段階の話に3Chapter分の枠を割いていて、 本書を読み進めるごとに実感するのは、SEO対策はとにかく調査と事前準備・設計が大事だということだった。

当たり前の事しか書いていない

少し話は変わるのだけど、 ちょっと前に、メディアサイトのリニューアルにエンジニアとして携わった事があった。

そのときにSEOの面倒を見てくれた方がいて、SEO対策をエンジニアの立場から色々やったりしたのですが、 一般的に言われている「ちゃんとmeta titleつけよう」「H1タグはページ内に1つだけにしようね」とか、「記事のリンク先変わったらちゃんと301リダイレクトしよう」そういう「当たり前の事の徹底」の泥臭い事だったように記憶している。

しかし、そういう当たり前な、簡単な施策をやっておくだけだったのだけど、 リニューアルから数ヶ月後で検索結果に対してもしっかりと効果が現れているそうだ。

本書でも、ごくごく当たり前の事を当たり前にやっていく。 そんな当たり前なことだからこそ、本書紹介されているページ設計やテクニックは今に通ずるものがほとんどだと思っており、 数年しか効果がないような雑なものはないのだろう。

どうしてこの設計にするべきなのかなどの、施策に対しての理由についても結構しっかりめに説明されており、 前述のメディアリニューアル案件で「そういえばこんなページ構成だったな…」「回遊性を上げる理由はそこにあったのかー」とか思い出して、かなり納得感を持って読み進めることができた。

タイトルに「新しい」と入れたのは失敗

流石に最初に出版されたのは5年前で、新しくないのは当たり前。 日進月歩進化していっているWeb界隈や、SEO関連で近年あったような大型アップデート*1については当然触れられてはいない。 いい意味でタイトルに「新しい」と入れたのは失敗だと思っており、SEO入門の古典的な本だと思ってる。

Webエンジニア目線から見て

本書はWebエンジニア目線ではなく、Web担当者やPM向けの印象を受ける。*2

なので、技術的にどうすればいいかみたいなところの章にページは余り割かれておらず*3、 キーワード設計、サイトのページ設計などの事前作業や設計についての大事さがやっぱりメインな感じ。

エンジニアからみたSEO観が変わった

  • SEO対策は当たり前の連続だけど、とくに最初のページ・キーワード設計周りなどがめっちゃ大変。できれば専業でお願いしたほうが良い。
  • エンジニアリングだけでSEOを良くすることはできない。必ずそこにはSEO上の設計が必要。
  • SEO対策の仕事は、サイトを作ったときに見てもらえないという、「検索上のもったいない」をなくす仕事。

おすすめしたい人

  • とにかくSEOの基礎知識を身に着けたい人。
    • 個人的観測だと、基礎はほとんど、全体の60 - 70%はカバーできるのでは。
    • Webに載せるまでもない情報が貴重なので、この本でなくてもいいので、1冊SEO入門書を読んでおいたほうが学習効率は高いと思う。
  • メディア案件でSEO意識して!なんて言われてみたものの、SEOよくわからない。
  • ディレクターや担当者からの301リダイレクトの依頼を面倒で断っちゃうめんどくさいエンジニア

とにかくSEOに入門するに当たり、この本はおすすめ。 ぜひ読んでみてください。

最後に

  • SEO対策をプロジェクトのメインの仕事にするのは正直やりたくないなと思いました。
    • キーワード設計したり、ホント泥臭い仕事。
  • 興味を持って読んでみてすごく理解したし納得感あったが、自分でやりたいなと思えなかった。
    • 技術的に新しみがないし、面白味もぶっちゃけあんまりない。
  • この記事はSEOを意識しないでパパっと書きました。

追記

第二版が出ているらしいです。 ちょっとこれもさらっと読んでみるかなー

*1:いわゆるWELQアップデートとか

*2:本書の中でも結構「エンジニアに協力を…」なんて事を言っているので。

*3:そもそも余り書くことが無い

SendGridで1つのメールを最大1000件のメールアドレスにまとめて送信する方法

これはよく見るとドキュメントに載っているのだけど、 結構見落としがちだったので、記載しておく。

ドキュメントの内容

v3 Mail Send API概要 - ドキュメント | SendGrid

SendGridのMailSendAPIを使えばOKな感じです。

Rubyのコード

これで終了だとあまりにもつまらないので、 SendGrid公式でRubyのGem(SDK)が提供されているので、それを使った際のサンプルを載せておく。 Rubyのコードにするとざっくりこんな感じだろう。

def mail_send
  sendgrid = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
  sendgrid.client.mail._("send").post(request_body: params)
end

def params
  param = {
    "custom_args" => {
      "test_mode" => true
    },
    "from" => {
      "email" => 'example@example.com',
      "name" => 'サイト名'
    },
    "content" => [
      {
        "type" => 'text/html',
        "value" => 'テスト'
      }
    ],
    "mail_settings" => {
      "sandbox_mode" =>  {
        "enable" => false
      }
    }
  }
  
  param["personalizations"] = User.all.find_each do |user|
    {
      "to" => [
        {
          "email" => user.email,
          "name" => user.name
        }
      ],
      "substitutions" => {
        "[USER_NAME]" => user.name,
        "[USER_EMAIL]" => user.email,
      },
      "subject" => 'メールタイトル',
      "custom_args" => {
        "site_user_id" => user.id.to_s,
      }
    }
  end
  param
end

mail_send

この方法で運用すれば1000つの宛先へのメール送信をまとめることができる。 配信メールアドレスが30000件なら、30回のリクエストに分割してメール配信リクエストをSidekiqやDelayedJob等でWorkerの数を調整して処理してやれば送信リクエストはサクッと完了するだろう。 ただ、流石に30,000件とかのユーザーへの一斉送信はMarketing Campaigns API Overview - ドキュメント | SendGrid を使うべきでは…? と思ってしまっています。*1

テンプレートに変数をアサイ

他にもMailAPIでは、テンプレートを変数を定義しておき、送信時に変数を展開することができる。(コードのsubstitutionsあたりを参照してください。) それぞれのユーザーでメールアドレスだけでなく、ユーザー名や宛名なんかも異なるだろうし、メールの文章中にそういった動的な文言も含めることができる。

まさにSendGridさまさまである。

*1:今回はAzureからの利用だったため、Marketing Campaigns APIは使えなかったのです。