- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- Other occupations (19)
- Development
- Business
こんにちは、Arch Squad の @igsr5 です。
この記事では2022年現在 Wantedly サービス上でどのように画像を扱っているか、画像配信をどのように最適化しているかをお話しします。
話すこと
- 従来の画像に関する課題
- それらを解決する新マイクロサービス imagesの導入
- images導入後の世界
- images導入までにやったこと
はじめに
Wantedly Visit において画像はサービスのコア体験を支える重要な要素です。
旧来の求人媒体における転職・就活では、条件面でのマッチングに重きが置かれていたため文字情報を中心として情報が提供されていました。しかし Wantedly Visit では、採用選考の前にカジュアルに話を聞きにいく体験をコアにしており、そのために会社のビジョン・カルチャーによるマッチングを重視しています。そして我々はこのコア体験を支えるために、会社の雰囲気を非言語情報で伝えることのできる「画像」に重きを置いています。
しかしそんな Wantedly Visit ですが、プロダクト上での画像表示が十分に高速でない、という問題を長らく抱えていました。それには単純に画像加工が不十分であるという表面的な問題や、そもそも画像サーバの設計的な問題から配信高速化などの施策が打ちにくいという根本的な問題が含まれていました。
そこで我々はこれらの問題を解決すべく、2022年上半期に images という新たな画像サーバを開発・導入しました。
images とは
images は特定ドメインによらない画像の配信・加工、アップロードを行うマイクロサービスです。images 以前はサービス開始当時からある巨大なRails サーバが画像URL取得・アップロードを、専用の nginx サーバが配信・加工を行なっていました。
導入背景
images の導入背景は前述した通り、従来の画像サーバが抱える表面的・根本的な課題をまとめて解決することです。この章ではもう少し深掘りしてそれらの課題をお話しします。
前述した問題は大別すると次の2つに分けることができます。
- 開発者体験の問題 (根本的)
- ユーザ体験の問題 (表面的)
それぞれについて説明します。
開発者体験の問題
従来の仕組みでは、巨大なRailsサーバが画像のURL取得・アップロードを行なっていました。この仕組みは機能面では大変よく出来ており Rails の Polymorphic Associations を用いて各方面からの利用が容易にできるようになっていました。しかし新規に画像体験に関する施策を打つにはいくつか複雑(根本的)な問題がありました。具体的には下記のような問題が挙げられます。
- 画像の存在確認・URL取得に毎回専用のDB tableを見る必要がある
- 加工パラメタの組み立てが難しすぎる
画像の存在確認・URL取得に毎回専用のDB tableを見る必要がある
これは前述のPolymorphic Associations を利用していることによる弊害です。画像を持つ各モデルは専用のtableから画像URLを取得しているので画像URLを確認・取得する際には毎回別tableへの参照が必要でした。
これはどのような問題かというと例えば、「いいねした人のなかからアバター画像のある人を優先して5人表示する」コードがすんなり書けないという問題がありました。
加工パラメタの組み立てが難しすぎる
従来の仕組みでは、画像の加工にngx_small_light を利用していました。このngx_small_lightには複雑な画像加工が可能というメリットはありつつ、加工パラメタの組み立てが非常に複雑というデメリットがあります。
例えば「width=200, height=300, format=webp 」に加工する時には
http://localhost:8000/small_light(dw=200,dh=300,of=webp)/img/filename.jpg
# `small_light(dw=200,dh=300,of=webp)` をURLパスに含む
というURL指定になります。このように非常に加工パラメタの指定が難しいので、画像体験に関する施策が行うのが難しくなっていました。(default resize, formt, etc...)
ユーザ体験の問題
前述の通り、従来の仕組みには複雑な開発者体験の問題がありました。しかしこれらの問題は開発者体験のみに収まらず、下記のようなユーザ体験の悪化へも繋がっていました。
- 画像体験に関する施策が難しくて行えない
- 一部キャッシュが使いにくいURLが存在する
画像体験に関する施策が難しくて行えない
具体的には画像体験に関する施策を行うのが困難であることで、キャッシュや画像加工に関する施策(default resize) などがほとんど出来ない、などが挙げられます。
一部キャッシュが使いにくいURLが存在する
また複雑な画像URL組み立てを隠蔽するために GET: /users/:id/avatar
のように特定Railsサーバが画像サーバへのリダイレクトを返すケースもありました。これらのケースでは素早いURL構築こそできましたが、ネットワーク的に配信が遅くなり画像更新でもURLが変わらないためキャッシュが使いにくい状況でした。
これらの問題が長く解決されなかった結果、 画像が主要なページではLCP(Largest Contentful Paint) 等が悪化していました。
これらの問題を解決すべく images が生まれた
繰り返しになりますが、従来の画像サーバの仕組みには「サービス上で扱われる画像の開発者体験が悪い(ので、ユーザー体験も良くならない)」という課題が存在しました。
そこで我々は新規に画像に関するマイクロサービス・仕組みを導入して「サービスで扱う画像ドメインの分離」を行い、そこでこれらの課題の根本的な解決することにしました。
アーキテクチャ・技術スペック
この章では images のアーキテクチャ・技術スペックを軽く紹介します。
上図は images 周りのアーキテクチャを示したものになります。要所となる点は下の通りです。
- 画像配信・加工は HTTP Server が、画像のアップロード・削除は gRPC Serverがそれぞれ行う
- 画像配信はキャッシングのためアプリケーション前段にCloudFrontを挟む
- 画像加工は画像URLに専用のクエリパラメタを付与することで行う
- 一部特定の非同期処理を行うために Cloud Pub/Sub Subscriberも立っている
- 画像はRDB, S3に保存される
- RDB: 画像のメタデータ(id, width, height, s3 object path, ...)
- S3: 画像本体のデータ
- 画像をもつモデルは自身のカラムで画像URLを保存する
- e.g. ユーザのアバター画像、 profiles tableのavatar_urlカラム
- 画像を更新するとこのカラムの値が更新される
また技術スぺックとしては
- 言語は Golang
- 画像加工にh2non/bimg (libvips/libvips)
を採用しています。
Wantedlyサービスの画像体験がいかに素晴らしくなったか
この章では images 導入前後でWantedlyサービス上の画像体験がどのように良くなったかを開発者体験・ユーザ体験の2つの観点でお話しします。
開発者体験の向上
この観点ではimages導入によって、前章で触れた「開発者体験の問題」が全て解決されました。
- 画像の存在確認・URL取得
- 従来: 専用のDB tableを毎回参照する
- images導入後: 画像を持つモデルが自身で画像URLを持つ
- 画像加工のパラメタ指定
- 従来: 複雑
- images導入後: 簡単
また、上にはないトピックとして画像に関する実装が全体的にシンプルになりました。これには古くからある巨大なRailsサーバからマイクロサービスとして切り出されたことや、そもそも従来の仕組みが過度に複雑であったことが影響しています(従来の画像サーバ実装は10年前から存在しており、古き良きRailsといった雰囲気でした)。この改善が次節で説明する「ユーザ体験の向上」にも繋がっています。
ユーザ体験の向上
前章ではimagesの導入によって開発者体験が向上したことをお話しました。ではユーザ体験にはどのような効果があったのでしょうか。
この記事の前半で私は従来の仕組みをこのように述べました。
(開発者体験が悪く) 画像体験に関する施策を行うのが困難であることで、キャッシュや画像加工に関する施策(default resize) などがほとんど出来ない状況でした
開発者体験が悪く施策が困難なことでユーザ体験も良くならない、といったお話です。imagesでは開発者体験の向上によってこれらの問題も解決され、実際に画像体験に関する施策が多く行われています (次章で詳しく紹介)。
そして現在、すでにそれらの施策で
- 画像の表示が従来より速く
- 重たい画像を安定して返せる
ようになっています。これには従来の画像サーバでは困難であった施策(実装)がimagesの導入により可能になったことが影響しています。
次章では具体的に行なった画像体験に関する施策を紹介します。
images はこうやって画像配信を最適化する
images では開発者体験のみならず、プロダクト的な価値を提供するための画像配信の高速化に関するいくつかの施策を行なっています。すでに一定の効果が得られているので、この章では行なった施策を具体的に紹介します。
「一定の効果」(前章より)
画像の表示が従来より速く
重たい画像を安定して返せる
現時点で行なった施策は下記の通りです。
- キャッシュ効率化
- 画像アップロード時に事前にリサイズ画像を生成しておく
- Accept ヘッダを見て最適な画像フォーマットに変換
- デフォルトリサイズ
それぞれ説明していきます。
キャッシュ効率化
images はできる限り低コストに高速に画像を配信するためにCDNをアプリケーション前段に挟み、コンテンツをキャッシュしています。従来の画像サーバも同様にCDNを利用していましたが、そのURL設計により一部キャッシュを利用できない箇所がありました (「導入背景」参照)
この章でお話しする施策はこのキャッシュを最大限効率的に利用するための施策です。言い方を変えると「CDN、クライアントのキャッシュヒット率を上げるための施策」になります。以下が具体的に行なったことです。
- 画像更新で確実に画像URLが変わるようにする
- キャッシュ期間を非常に長く設定する
この記事の前半部の「アーキテクチャ・技術スペック」で少しだけ触れましたが、imagesの画像URLは画像更新を行うと必ずURLも変わるような設計になっています。この設計によりキャッシュが画像更新に取り残される心配はなく、Cache-Control: max-age=OO
を非常に長く設定することが出来ています。
画像アップロード時に事前にリサイズ画像を生成しておく
imagesでは画像アップロード時によく利用される加工パターンのファイルをアップロード時に事前生成しています。
e.g. w=320&format=webp
, w=2560&format=webp
この施策は重くなりがちな画像加工を高速化するために行っています(ただしキャッシュの存在しない初回の画像表示の話です)。この施策を行うことで
- リクエストされた加工パターンが既に生成されていればそれをそのまま返す
- リクエストに完全に一致しなくても近いサイズのファイルから加工を行う
といった機能を実現しており、キャッシュが効いていない時も高速に画像表示ができるようになっています。
また技術スペックとしては、Cloud Pub/Sub を利用して非同期に事前加工を行っています。
Accept ヘッダを見て最適な画像フォーマットに変換
前提として現在世の中にある全てのブラウザはHTTPリクエストヘッダに適切な Accept ヘッダをつけます。このヘッダはクライアントが理解できるコンテンツを MIME タイプ で伝えてくれます。
# chrome の例
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
images はこの Accept ヘッダをいい感じに利用します。
具体的にはAcceptヘッダの値に指定されたMIMEタイプのうち、最も優先度が高いかつimagesが対応するものを選び、デフォルトでフォーマット変換します。
# chrome の例
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
# 配信画像はデフォルトで image/webp になる
この施策によりimagesの配信する画像は特に意識(パラメタ指定)しなくても常に最適なフォーマットが返ります。さらには配信画像のファイルサイズが最適化されることでより高速な画像配信を実現しています。
余談ですがnext/imageでもここで紹介したアプローチが採られています。(next/imageはVercel が提供するNext.js v10.0.0~内包の標準パッケージです)
デフォルトリサイズ
Wantedlyサービスにアップロードされる画像には時に20000pxを超えるような超巨大なものが含まれます。冒頭でもお話ししましたが、Wantedlyサービスではその特性から「画像」に重きを置いています。例に漏れず、20000pxを超えるような超巨大な画像でも我々のサービスでは他画像同様に高速に表示されることが望まれます(またアップロードサイズに制限をかけることもできますが同様の理由でそれらの制限はかけていません)。
しかし実際にはそれらの巨大すぎる画像をリサイズせずに配信するとサイズが大きすぎてタイムアウトしていました。リサイズを適切に指定すれば回避できる問題ですが、現実的にはどうしてもリサイズ指定が漏れてしまっている箇所があります。
そこでこの章でお話しする施策です。
imagesでは一定以上サイズ(px)の大きい画像はデフォルトでリサイズを行うようにしています。
e.g. 10240x5000px
→ デフォルトリサイズ後 2560x1250px
具体的には width/height どちらか大きい方が一定の閾値(現在は2560px)を超えないようにpx数を調整しています。
この施策によりimages は通常配信できないような巨大画像を配信しています。
それだけではなく
ここまで紹介したように images では導入直後にも関わらず、すでに多くの画像体験に関する施策を行っています。しかしimagesの本質的な価値はそれらの1つ1つの施策に留まりません。
というのも、これらの施策を実際に images 上で実現するのはとても容易でした。それにはimagesのシンプルな設計であったり、Wantedlyサービス全体で見たときに画像に関する責務が適切に分離されていることが要因として考えられると思います。
つまり我々は images の導入によって「開発者体験の悪化がユーザ体験の悪化に繋がっていた世界」を「開発者体験の向上がユーザ体験の向上に繋がる世界」へ変えることに成功したのです。
images は単に画像体験向上の施策を詰め込んだマイクロサービスではありません。今後もWantedlyサービスにとって必要な画像体験の施策を行い続けるための基盤としてimagesは最大の価値を発揮します。
images 導入までの取り組みを紹介
最後にこの章ではimagesの誕生から導入までの取り組みを軽く紹介します。
- 内部実装期
- 導入検証期
- パフォーマンスチューニング期
- 本番導入期 (画像表示)
- 社内浸透期
内部実装期
この時期には images の内部実装を行っていました。具体的には
- HTTP Server, gRPC Server の実装
- 画像加工処理の実装
- アクセスログ、エラー通知などの監視系の設定
などです。実際にサービス内で動かしてみないと分からないことも多かったので、ここでは作り込みすぎずにある程度の段階で次の「導入検証期」へ移行しました。
導入検証期
この時期にはWantedlyサービスの検証用環境 (Sandbox, QA) へimages を導入検証しました。
- 現時点で imagesはどのくらいのパフォーマンスを出せるか
- 初回 Latency
- Request Per Second
- 致命的な不具合、設計ミスがないか
あたりを実際に検証用のサービス環境で動かしながら確かめて、一部再実装を行なっていました。この期間でパフォーマンス(特にlatency)に課題が見つかったため次の「パフォーマンスチューニング期」へ移行します。
パフォーマンスチューニング期
この時期は「導入検証期」で見つかったパフォーマンス上の課題を解決する期間に当てました。
具体的にやったことは
- pprof, New Relic, benchmark等でメトリクスを取集
- ボトルネックを特定
- ボトルネックを解消
のサイクルを回しました。
当時のパフォーマンス改善で書いたブログ: GoのS3 ダウンロード処理で知っておくと良いこと - バックエンドパフォーマンス改善
この時期にある程度、画像配信は高速化しましたが、これ以降もimagesでは随時高速化のための施策を行っていました(前章「images はこうやって画像配信を最適化する」参照)。
本番導入期(画像表示)
この時期ではいよいよimagesを本番環境で動かすための作業を行いました。
この期間ではまず、いきなり表示頻度の高い画像から移行するのではなく、比較的負荷も軽いであろう画像からimagesに移行していました。そしてそれらの画像をいくつか移行した後にWantedlyサービス上でトップレベルに表示箇所の多いユーザのアバター画像表示をimagesに移行しました。
また実際にユーザアバター画像をimagesに移行する際は、その利用箇所の多さからimagesへのリクエスト数の急増が予想されたので、 Canary Release による段階リリースを用いたキャパシティプラニングやKubernetes Autoscalerの調整などを行いました。
そしてユーザのアバター画像がimagesに移行され、作業は「社内浸透期」に移ります。
社内浸透期
いよいよラストです。
この時期では「社内のエンジニアがプロダクト上で新しい画像サーバを使いたいと思ったときに使えるようにする」を目標にimagesの存在を社内に周知する活動を行いました。
具体的には
- ドキュメント作成
- 社内LTでimages紹介
を行いました。
また周知とは少し違った方向で「社内のエンジニアがプロダクト上で...」を達成するためにimagesのクライアント向けの仕組みもこの時期に実装しました。
詳細は省きますが、Ruby で独自DSLを実装しimagesを簡単に導入・利用するための仕組みを作ることで、社内のエンジニアがimagesを利用するハードルを下げました。
おわりに
この記事では、Wantedlyサービスにおける画像体験の裏側を
- 従来の課題
- それらを解決する新マイクロサービス imagesの導入
- images導入後の世界
を中心にお話ししました。
画像体験に関する取り組みはもちろん、このような「長期的に効きそうな課題解決」を行なった1つの例として伝わっていれば幸いです。
この記事は以上になります。ここまで読んでいただいてありがとうございました!