WEBメーラー パフォーマンスチューニング Datadog

Webメーラーの性能改善を、運用データをもとに2段階で進めた話

WEBメーラー パフォーマンスチューニング Datadog

こんにちは。ロリポップ・ムームードメイン事業部エンジニアのカン・サンウンと申します。

ロリポップ・ムームードメイン事業部のメインサービスであるロリポップ、ヘテムル、ムームードメインのWebメーラーは、送受信等の基本操作を支える重要基盤です。動作の遅延や不安定さは即座に問い合わせに直結するため、高い安定性が求められます。

今回はこのWebメーラーのIMAPレイヤーを改善しました。接続プールの導入とメモリ最適化を中心に、IMAPレイヤーのボトルネックを解消しています。

1次改善を行い、デプロイ後には運用データをもとに2次改善を実施しました。この記事で伝えたいのは、コードを整理した先にある「運用で初めて見えるボトルネック」にどう向き合い、改善を続けたかという話です。

背景:なぜIMAPレイヤーを見直したのか

当時のIMAP実装は、機能追加のたびに個別最適を重ねた結果、接続管理やデータ取得の方式が統一されていない状態でした。リクエストのたびに新しいIMAP接続を作成するパスが多く、メール詳細表示や添付ファイルダウンロードでメール原文を必要以上に読み込むフローも残っていました。

普段は持ちこたえられても、メールが多いアカウントでメモリ使用量が急増するケースがあり、安定性の面でリスクを抱えていました。これが改善に着手する直接の契機となりました。

1次改善:接続プールとメモリ最適化

接続再利用のためのconnection poolを導入し、メモリ使用量を減らす方向で構造を作り直しました。

connection poolの導入

以前はリクエスト単位でIMAP接続を新規作成するコストが繰り返し発生していましたが、改善後はユーザーごとの接続を再利用する構造に変えました。

interface PoolConfig {
  maxTotalConnections: number
  maxConnectionsPerUser: number
  idleTimeout: number
  connectionTimeout: number
}

以前の構造ではIMAP接続数に上限がなく、高負荷時にPod間で過負荷が波及するリスクがありました。Pod単位で全体の接続上限とユーザーあたりの同時接続上限を設け、idle connectionの自動整理、上限超過時の即時エラー返却、パスワード変更時の認証情報検証を組み込んでいます。

メモリ最適化

メール全体を読み込んで処理するフローを、必要なデータだけを取得する方式に変えました。特に添付ファイルダウンロードは、メール全体をメモリに乗せるのではなく必要なIMAP partだけをストリームで直接読み込む方式にしています。

async downloadAttachment(
  boxName: string,
  uid: string,
  part: string
): Promise<{ content: NodeJS.ReadableStream } | undefined>

メール一覧もメタデータ中心の軽量な取得に切り替えています。

// Before: メール全文(添付含む)を取得してパース
for await (const message of client.fetch(range, { source: true })) {
  await simpleParser(message.source)  // CPU集約的
}

// After: envelopeとbodyStructureのみ取得
for await (const message of client.fetch(range, {
  envelope: true,        // 差出人、宛先、件名、日付
  bodyStructure: true,   // MIME構造(添付有無の判定用)
  flags: true            // 既読/未読など
})) {
  // 本文パースなしでメタデータのみでリストを構成
}

ただし、この変更はメタデータ取得パスに限定したもので、一覧でプレビューテキストを表示するために本文を取得するパスにはメールソース全量取得が残っていました。この問題はデプロイ後の運用データを見ている中で判明し、2次改善の主な対象となりました。

ローカル検証では改善が見られたものの、機能構造が大きく変わるため、主要なユーザー操作を一通り検証した上でデプロイを進めました。

デプロイ後に見えた課題:構造は良くなったが、運用では別の問題が出てきた

1次改善のデプロイ後、Datadogでメモリ使用量の低下は確認できました。以下はPodごとのメモリ使用量の推移で、デプロイ前後で明確な変化が見られます。

Memory Usage by Pod — 3月5日の1次改善デプロイを境にメモリ使用量のスパイクが大幅に減少している

しかし、レイテンシには目立った変化がありませんでした。同時期にユーザーからの問い合わせも増加しており、コードを整理しただけでは解消されていない問題があることが明らかになりました。

Datadogのメトリクスとユーザー問い合わせの内容を突き合わせていくと、以下の3点が浮かび上がりました。

  • プレビュー取得の非効率 — 一覧プレビューのためにメール原文全体をfetchするパスが残っており、添付ファイルが大きいアカウントでメモリとCPUを圧迫していた
  • 送信フローの誤った失敗判定 — SMTP送信は成功しているのに、後続操作の失敗でユーザーに「送信失敗」と表示されていた
  • エラーの未分類 — 一時的なIMAPエラーと本物の障害がDatadog上で混在し、切り分けができなかった

2次改善:運用で見えた問題に対処する

1次改善のデプロイ後に見えた3つの課題に対して、それぞれ対処しました。

プレビューテキストの部分取得

全体のsource fetchの代わりに BODY.PEEK ベースの部分fetchに切り替えました。本文プレビューに必要なテキストパートだけを短く取得する方式です。

const textPart = findTextPart(bodyStructure)

client.fetch(uids, {
  bodyParts: [`${textPart}.MIME`, { key: textPart, maxLength: 512 }]
}, { uid: true })

同じパート番号を持つUIDをまとめて1回のFETCHで取得し、MIMEヘッダーも同時に読むことでbase64やquoted-printableエンコーディングも正しくデコードするようにしました。この変更で、添付ファイルデータにはアクセスせずに一覧プレビューを維持できるようになりました。

送信フローの耐障害性向上

SMTP送信が成功していれば、その後の送信済みフォルダ保存や一時ファイル整理の失敗がユーザーに「送信失敗」として見えないよう分離しました。ユーザーの視点で成功判定をどこに合わせるかを再定義した形です。

エラーハンドリングと可観測性の整備

IMAPエラーを性質ごとに分類し、存在しないメールボックスは404、timeoutは504、connection errorは502と分けて応答するようにしました。結果として従来は隠れていたエラーが表面化しましたが、これにより問題の切り分けが可能になりました。

ログポリシーの再設計

本番環境で正常リクエストが過剰にinfoログとして残っていた問題を修正し、本物のサーバーエラーだけをerrorとして記録するよう整理しました。Datadogで見るべきシグナルとノイズを分離した形です。

結果

以下は2次改善リリース後4日間のDatadogデータを、リリース前4日間と比較した結果です。構造を整理した1次改善ではなく、運用データに基づいて具体的なボトルネックを潰した2次改善でこれだけの差が出た点に注目してください。

レイテンシ

指標 Before After 改善率
P50 628ms 413ms 34.2%
P75 1,775ms 711ms 59.9%
P90 10,075ms 1,223ms 87.9%
P95 12,136ms 1,775ms 85.4%
P99 24,382ms 3,974ms 83.7%
平均 2,479ms 599ms 75.8%

P90が約10秒から1.2秒に縮まり、平均応答時間も4分の1程度にまで下がりました。Apdexスコアも平日平均で0.669から0.764に改善しています。

ログボリューム

リファクタリング前は日間約180万件のログがDatadogにインジェストされていました。ログポリシー再設計後の安定化時点では日間約4万件にまで減り、約97%の削減を確認できました。運用コスト面でも意味のある改善でした。

おわりに

今回のWebメーラーIMAP改善を通じて得た最も大きな気づきは、リファクタリングは出発点であって到着点ではないということでした。

1次改善でメモリ効率は改善しましたが、P90が10秒を超えるレイテンシはDatadogを見るまで把握できませんでした。プレビューでメール全文をfetchしていた問題も、送信成功なのにユーザーに失敗と表示されていた問題も、コードレビューやローカルテストでは見落とされていました。本当のボトルネックが見えたのはデプロイの後であり、Datadogの数値とユーザー問い合わせを突き合わせて初めて2次改善の方向が定まりました。

このデプロイ→観察→改善のサイクルは今も続いています。connection poolの安定性強化や防御的コーディングの補強といった後続作業を引き続き進めており、今後も運用データを見ながらユーザーが体感できる改善を一つずつ積み上げていくつもりです。お読みいただきありがとうございました。