マンガメディア開発チームの id:mizdra です。普段はWebアプリケーションエンジニアとして、マンガビューワ「GigaViewer」の開発に携わっています。GigaViewerの提供は2017年に始まり、執筆時点で12の出版社、14のサイトに導入いただいています。
GigaViewerでは、多数のマンガサイトを素早く構築するため、マルチテナントアーキテクチャを採用しています。データベースを始めとしてコードベースに至るまで、多くの部分をサイト間で共通化しています。
マルチテナントアーキテクチャは、プロダクトを多数のプラットフォームに効率よく展開できるメリットがある一方で、アーキテクチャ特有のさまざまな困難もあります。この記事では、マルチテナント環境でSentryを利用したときに発生するグルーピングの問題を解説し、その問題にGigaViewerがどのように対処したのかを紹介します。
なお、GigaViewerにおけるマルチテナントアーキテクチャの採用については、2019年に id:hitode909 が発表した資料がありますので、こちらをご覧ください。
Sentryでエラーを監視する
Sentryは、エラー監視のSaaSです。アプリケーションのさまざまな場所で発生したエラーを自動で収集し、ダッシュボード上で分析することができます。
Sentryにはさまざまな環境向けに公式のSDKが用意されていて、GigaViewerでは主にバックエンドとフロントエンドのエラーの監視に利用しています。
Sentryを導入すると、アプリケーションで日々発生している本当にさまざまなエラーが可視化されます。大きなプロダクトに導入した初期には、エラーの量に圧倒されると思います。とはいえ開発の品質を維持・向上させるには、何とかしてこの大量のエラーに対処していく必要があります。
具体的には、エラーをトリアージして優先度を付け、優先度の順に修正するといった対応を取っていくことになります。ありがたいことに、Sentryにはトリアージを補助してくれる機能が豊富にあります。その1つが、エラーのグルーピングです。
エラーをトリアージできるグルーピング機能
エラーを効率的にトリアージするには、そのエラーの概要を素早くつかめるようになっていることが重要です。例えば、エラーメッセージやエラーの発生件数、ユーザの数、ユーザが利用するブラウザの種類の傾向など、ざっくりとしたデータがあれば、そこからエラーの原因をある程度推測したり、緊急度を判断したりできます。
Sentryでは、似たようなエラーをIssueという単位で自動的にグルーピングするようになっており、エラーメッセージや発生件数といったデータをIssue単位で見ることができます。
同じエラーが分割してグルーピングされる問題
Sentryのグルーピングは、自動でよしなにやってくれて非常に便利なのですが、ときどき予期せぬグルーピングが行われることがあります。
例えばGigaViewerでは、JavaScriptで動的に組み立てたビューワをページにマウントする際に「マウント先の要素が存在しない」というエラーが発生することがあります。このエラーが、なぜか数十件のIssueに分割されてしまっていました。
実はこの「.js-viewer is not found
」だけでなく、他にもいくつかのエラーが数十件に分割されてしまう現象が発生していました。結果として、GigaViewerのエラーダッシュボードは大量のIssueで溢れかえっていました。
Issueが多過ぎるとそれがノイズになり、本当に重要な問題を見逃してしまう可能性が高まります。また、エラーの発生件数、ユーザの数、ユーザが利用するブラウザの種類の傾向などはIssueごとにしか見れないため、Issueが分割されてしまうと、エラーの概要を正確に捉えることも困難になります。
エラーの監視を健全に行うためにも、こうしたエラーを適切にグルーピングしてやることが重要です。
Issueが分割される原因はマルチテナントにあり
問題を解決するため、Sentryのグルーピングの規則をおさらいしましょう。プロジェクトやプログラミング言語にもよりますが、Sentryはデフォルトで以下のようなデータをキーとして、同じキーを持つエラーを同じIssueにグルーピングします(参照)。
- エラー発生したモジュール名
- エラー発生行のソースコード
- エラーの種類
実際にどのようなデータがキーに利用されたかは、Issueの「詳細」タブの一番下にある「EVENT GROUPING INFORMATION」から「Show Datails」をクリックすると確認できます。
表示されるパネルで、青背景の部分がキーとなるデータ*1です。Sentryではこのキーが完全に一致しているエラーが、同じIssueとしてまとめられます。
実際に分割されてしまった複数のIssueでこのパネルを見比べてみると、キーとなっている上位フレームのモジュール名(module行)が微妙に違うことが分かりました。
ところで記事の冒頭でも少し触れましたが、GigaViewerではマルチテナントアーキテクチャを採用しており、データベースやコードベースを複数のサイト間で共通化しています。これはフロントエンドについても例外ではなく、サイト間で共通のフロントエンドのコードベースを利用しています。
具体的には、サイトごとに「src/ts/media/<サイト名>/entrypoint.ts
」という専用のエントリポイントを用意し、ここから共通のロジックを呼び出す構成にすることで、サイト間で大部分のロジックを共通化しつつ、サイトごとの挙動をカスタマイズできるようになっています。
今回のエラーは、共通のロジック部分で発生していたものでした。このロジックは各サイトのエントリポイントから呼び出されるため、結果として上位フレームのモジュール名が変わっていることになり、Issueが分割されてしまっているようでした。
Issueが分割されないようにまとめる方法
Issueが分割されている原因が分かったところで、どのようにして解決するかを考えてみます。これには複数の解決策があります。
A案. 手動でIssueをマージする
Sentryには、Merging Issuesという機能があります。Issueの「Similar Issues」タブから簡単に実行でき、複数のIssueを1つにマージできます。
一見するとこの機能を使い、Issueを1つずつマージしていけばよさそうにも思えます。しかし、今回のケースでは「.js-viewer is not found
」以外にも多数のエラーでIssueが分割されています。Issueが多過ぎて、手動でマージしていくのは現実的ではありません。この方法の採用は見送りました。
B案. Stack Trace Rulesを使う
GigaViewerでは、SentryのStack Trace Rulesという機能を採用しました。これを使うと、スタックトレースのどの部分をグルーピングに利用するかをルールベースでカスタマイズできます。
詳しい仕様は公式のドキュメントに譲りますが、例えば以下のようなルールを書くことで、ルールにマッチするデータをグルーピングのキーから除外できます。
# 自動生成ファイル由来のフレームは、グルーピングのキーから除外 stack.abs_path:**/*.gen.c -group # malloc 関数由来のフレームは、グルーピングのキーから除外 stack.function:malloc -group
GigaViewerでは、サイトごとのエントリポイントをグルーピングのキーから除外するため、以下の設定を適用しました(Webpackを使ってビルドしているため、ちょっと特殊な書き方をしています)。
stack.abs_path:webpack:///./src/ts/media/**/entrypoint.ts -group
グルーピングのルールを適用した結果
Stack Trace Rulesを利用することで、グルーピングのキーからサイトごとのエントリポイントのフレームが除外され、次のようにIssueが1つに結合されました 🎉
「All values」タブに切り替えると、当該フレームが無視されている旨の表示が確認できます。
おわりに
この記事では、マルチテナント環境でSentryを利用したときに発生する問題について、またその問題にGigaViewerではどのように対処したかについて紹介しました。
マルチテナントという特殊な環境の問題を取り扱いましたが、紹介したエラーのグルーピングテクニックはさまざまなプロダクトで参考にできると思います。さらに本記事では紹介しなかったFingerprint Rulesなど、Sentryにはその他のグルーピングテクニックもあります。興味がある方はぜひドキュメントを参照してください。
はてなでは、エラー監視を通して不具合を解決し、より快適にWebやアプリで漫画が読めるようマンガビューアを改善していく仲間を募集しています。
株式会社はてな マンガチーム Webアプリケーションエンジニアの採用情報
*1:Sentry のダッシュボード上では「contributing value」と呼ばれています。
id:mizdra
木戸 章紀(きど・あきのり)。マンガメディア開発チームでWebアプリケーションエンジニアを務める。2020年4月新卒入社時より現職。
Twitter: @mizdra
GitHub: mizdra
Website: mizdra.net