CakePHP
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
この記事はBASEアドベントカレンダー 2025 の 7 日目の記事です。 エンジニアの右京です。BASE では今年、表示速度の改善を目標にすべてのショップへ Cloudflare を導入しました。これは、その過程や技術面の簡単な解説です。 記事は前後半になっており、この記事は後半で、Cloudflare Workers を利用したコンテンツのキャッシングの話題となります。 前半はこちら: Cloudflare でショップページをちょっとだけ速くしてみた - 導入/SSL for SaaS 編 ショップページのレスポンス速度を改善したい レスポンス速度を改善するにあたって、Cloudflare Workers を利用して前段でコンテンツをキャッシュするアプローチが有効なことは事前にイメージがついていました。この方針について特に以下の記事が参考になりました: zenn.dev しかし、現状のショップページが前段でキャッシュされることを想定して作られているわけではありません。例えば在庫数は、アクセス毎にサーバーサイドで都度計算を行い HTML として書き出しているため、長くキャッシュを持ってしまうと「商品ページは在庫がある表示なのにカートに商品が入らない」といったことが起こってしまいます。他には、時間経過によって販売状態が変化する商品であったり、抽選販売への応募期間といった時刻が関係するものも、現状の実装では長くキャッシュすることができません。 一方で、実際の購入フローでは必ずカートでの購入操作がある上、厳密な在庫や時刻に関する処理はカートで行われるので、ショップページに在庫数などがリアルタイムに反映され続ける必要というのは実はそこまでありません。そこで、数秒であればこのズレは納得できる範囲だろうと判断し、まずは小さく始めることができ、かつ大きな効果が期待できる「商品ページを数秒間マイクロキャッシングする」を実装することにしました。 最終的にはほとんど静的な作りにし、長くキャッシュを持つことで高速なレスポンスにするという目標はありつつも、まずはアクセススパイク時のインフラ面への負荷を抑えることを主軸としていきます。 Cloudflare Workers の設計と実装 まず、Cloudflare を利用する上での大前提として「Cloudflare ありきの設計にはせず、何かあった場合は外せるようにする」ということを設定しました。これには Workers 単体での障害程度であれば Workers を外すことでサービスを維持できるように、最悪 Cloudflare をやめることになってもサービスを維持できるように、という想いがあります。 ストレージの選択 当初想定していた Workers KV ではなく Cache API を採用することにしました。Cache API は前半でも登場した Cloudflare の Cache (あいまいさ回避のため以後 Cf Cache と呼ぶ)を Workers から操作できる API で、同一 DC 内であれば高速な書き込みと読み出しが行えます。 developers.cloudflare.com Cf Cache を Workers から扱うもう一つの方法として fetch を行う際に独自のフィールドを持った Request を利用する、というものがあります。 developers.cloudflare.com Cache API と fetch を比較した場合、性能だけを見ると fetch の方が次の 2 点で優秀です: Cache API では Tiered Caching が働かない fetch は同一のリクエストと判定できる場合リクエストをまとめてくれる(Request Collapsing) その上で今回 Cache API を選択したのは、その柔軟さにあります。Cache API は Cf Cache に乗せる API と Origin(BASE の Web アプリケーション本体) へのリクエストが分かれているので、キャッシュする前に Header を加工したいようなケースで有効になります。 また、これは自分の調査検証不足もあると思うのですが、 Request Collapsing を利用するにはレスポンスがキャッシュできる前提が必要であるような挙動をします。シークレット EC 機能を実現する際に、Request Collapsing を利用するとどうしてもどこかがキャッシュされてしまったため、Cache API を利用することにしました。 次に Workers KV を見送った理由ですが、書き込み制限と反映の遅延にあります。 KV は同一キーには 1 秒間に一度しか書き込みできず、かつその仕組み上反映が最大で 60 秒遅延します。 developers.cloudflare.com この特性から、今回の「数秒のマイクロキャッシング」には適していない判断としました。ただし KV を完全に利用していないわけではなく、後述する X-Webapp-Version のストアには KV を利用しています。コンテンツの更新頻度が頻繁ではないデータを、長期間に渡って信頼できるソースとして扱うことに向いているようです。 そして、KV ではなく Cf Cache を使う最大の利点が「 Cache-Control を元々うまく扱える」というところで、コアとなる仕組みはこれを活用した設計になっています。 Cache-Control を利用したキャッシング よく max-age=86400 などが指定されている Response Header で、コンテンツをキャッシュする際の挙動を指示するためのものです。このディレクティブにはいくつか CDN 向けのものがあります。 www.cloudflare.com CDN 向けのものに s-maxage というディレクティブがあり、これが今回のコアとなっています。 s-maxage は端的に言えば CDN 用の max-age で、例えば s-maxage=2 であれば 2 秒間 CDN にキャッシュできる、ということを示しています。 Cf Cache はこれを扱えるので、Header に Cache-Control: s-maxage=2 を持つ Response を Cache API から put することで、 2 秒間生存するキャッシュを作ることができます。作られたキャッシュは match で取り出せるので、これらを合わせると次のようなコードで Workers でのキャッシュを実現できます: export default { async fetch ( request , env , ctx ) { const cache = caches . default ; const cacheKey = new Request (( new URL ( request . url )) . toString () , request ) ; const cachedResponse = await cache . match ( cacheKey ) ; if ( cachedResponse ) { return cachedResponse ; } const newResponse = await fetch ( request ) ; // Cache-Control: s-maxage=2 ctx . waitUntil ( cache . put ( cacheKey , newResponse . clone ())) ; return newResponse ; } } Origin がキャッシュしたいページで Cache-Control: s-maxage=2 を返すと、Workers でこのコードを通って 2 秒間コンテンツがキャッシュされます。この s-maxage と合わせて 3 種類の Cache-Control を Origin が返すことでキャッシュをコントロールしています: s-maxage=N - コンテンツを N 秒間キャッシュする private - CDN にキャッシュできないことを示すディレクティブ、公開だがキャッシュしたくないページ、未対応のページで使用 no-store - CDN にもローカルにもキャッシュしない、シークレット EC で使用 上記のコードは一見すべてのレスポンスをキャッシュするように見えますが、 Cache API は Cache-Control がキャッシュできないことを指示していたり、 Set-Cookie が含まれている場合にはそのコンテンツをキャッシュしません。実際のコードでは put する条件として s-maxage を含んでいることを条件にしてはいますが、このままでも Origin がキャッシュ可能なレスポンスを返さない限りは何もしないようになっているので、安心して利用することができます。 この実装をコアとして、Origin との整合性を担保するための仕組みと、 Cache Stampede を緩和するための機能を加えています。 キャッシュキーの設計 ショップページでは一つの URL からユーザーの環境や設定に合わせた複数のレスポンスが返されるため、 Request Header や Cookie からキャッシュに利用するキーを計算することで、URL に対して複数のキャッシュを紐づけています。このキーにはキャッシュの世代管理のための値も含まれていて、Origin がデプロイされた場合にキャッシュのパージを行うのではなく、利用するキャッシュの参照を切り替える方式を取っています。 ざっくりと次のようなキーになっています: shop . example . com / items / 1234 ? webapp_version = xxxx - yyyy - zzzz & accept_language = ja , en & i18n_language = ja & i18n_currency = JPY webapp_version はキャッシュの世代管理のための値で Origin から取得します。Origin にはデプロイ毎に一意の値が割り振られており、ショップページのすべてのレスポンスと専用の API に X-Webapp-Version という独自の Response Header を含んでいます。 X-Webapp-Version: d8643bc9-02ae-49db-b37f-81c26e77cb39 Workers から定期的に API をコールして最新の値を取得していて、Workers ではこの値に一致するキャッシュのみを有効なものとして扱っています。また、各ページのレスポンスにもこれを含んでおくことで、早い段階でのデプロイの検知や、Blue / Green デプロイ中で Origin が新旧を混合で返す状態でも古いキャッシュを作成しないようにしたり、ということに役立てています。 accept_language には BASE でサポートしている日本語と英語にあわせて、Request Header の Accept-Language を ja,en もしくは en,ja に丸めた値が入ります。 Accept-Language をそのまま使用してもよいですが、種類が増えキャッシュヒットレートが下がってしまうので Workers で丸めています。 i18n_language と i18n_currency はユーザーが選択した言語と通貨の情報で、Cookie に入っています。 Origin ではこの Cookie の値が accept_language よりも優先され、指定された言語と通貨で HTML をレンダリングするため、キャッシュを細かく分ける必要があります。 基本的には静的な作りにしていく方針ですが、言語通貨のようにどうしてもユーザーによってレスポンス内容を変えたい場合はキャッシュキーを拡張して対応します。 X-Fresh-For stale-while-revalidate な動作を実現するための仕組みで、 s-maxage と組み合わせて使用します。コンテンツが新鮮な時間を表す Response Header で、任意の値が Origin から返されます。 Cache-Control: s-maxage=10 X-Fresh-For: 2 キャッシュから取得した Response の Age とこの値を比較し、指定されていた分の時間が過ぎていたら、ユーザーにはキャッシュが古いことを表す STALE 状態でキャッシュを返し、バックグラウンドでキャッシュの更新をします。上記であれば、最大 10 秒間キャッシュし 2 秒を超えた時点でアクセスがあれば更新を行う、という動作になります。 キャッシュが切れた際にオリジンへのアクセスが再度集中してしまう、 Cache Stampede を緩和する仕組みとして導入しました。キャッシュ時間を s-maxage のみの場合よりも遥かに長くすることができ、 STALE している間に次のキャッシュを作ることで、キャッシュが完全にない状態を減らすことが目的です。 概念としては次のようなコードで実装されています(このコードは動作しません)。さっきは Cache API と fetch からの Cf Cache 利用を比較していましたが、ここではこの 2 つを組み合わせていることがポイントです。 STALE 状態のキャッシュは「キャッシュできるコンテンツである」という前提があることになるので、安全にリクエストをまとめることができます。 const cachedResponse = cache . match ( cacheKey ) ; if ( isStale ( cachedResponse )) { ctx . waitUntil (() => { const revalidateKey = cacheKey + `&revalidate= ${ cacheResponse . header . get ( 'X-Cache-Id' )} ` ; const newResponse = fetch ( request , { cf : { cacheKey : revalidateKey , cacheTtl : 1 } }) ; newResponse . headers . set ( 'X-Cache-Id' , uuid ()) ; cache . put ( cacheKey , newResponse ) ; }) ; } return cachedResponse ; キャッシュされてから時間が経ったものを検知すると、アクセスに対しては古いレスポンスを返しつつ、裏で更新を行っています。この時に再検証用のキャッシュキーを別途作り、それを使って Request Collapsing の利用を目的とした fetch を呼び出し、その結果を Cache API で実際に使用するキャッシュとして改めて put します。このような実装にすることで、複数の再検証リクエストが一つにまとまり、Origin へ到達するリクエストを削減することが可能になりました。 2025/12/15 追記: この利用方法の場合、 Request Collapsing は fetch のレスポンスが Cf-Cache-Status: MISS の場合に動作するようです。 MISS ではなく Cf-Cache-Status: EXPIRED となる場合、リクエストがまとめられていないことがあります。実際に動作しているものは Response にユニークな Id を割り振ったものをキャッシュし、更新リクエストに含めています。上記のコードも修正済みです。 Cache-Control には stale-while-revalidate ディレクティブがあり、これを利用したいと考えていたのですが、Cache API ではこれを利用できないという制約があり独自に実装するような形になりました。例えば s-maxage=2, stale-while-revalidate=10 の場合、キャッシュとしては STALE になりつつも 12 秒間生存してほしいのですが、Cache API の場合は 2 秒でキャッシュが蒸発してしまいます。キャッシュ時間自体を伸ばすためには s-maxage を伸ばす必要があり、このような形になりました。 developers.cloudflare.com Origin 側の変更点 Workers だけではキャッシュが動作しないようになっているので、ここまでに解説してきた各種 Response Header を Origin が返すように改修を行いました。CDN でのキャッシュを禁止する Cache-Control: private をすべてのページで返すことを基本としつつ、キャッシュしたいページでは次のように返します: Cache-Control: s-maxage=6 X-Webapp-Version: d8643bc9-02ae-49db-b37f-81c26e77cb39 X-Fresh-For: 2 キャッシュ動作に関して Origin は Response Header を追加しただけで、これによって動作がなにか変わることはありません。Workers がなくなっても動き続ける設計を達成できたように思います。 また、商品の特性によってキャッシュ時間をコントロールすることも可能なので、例えば販売前→販売開始のようにステータスが遷移する時刻をまたぐ場合直前には s-maxage を短く設定するようにしています: Cache-Control: s-maxage=1 X-Webapp-Version: d8643bc9-02ae-49db-b37f-81c26e77cb39 ただし、さすがに既存機能のコードをまったく変更せずに、とはいかなかったので事前にいくつか以下のような調整を行っていました: 言語通貨設定が CakePHP の Session 機能に依存していたため、 Plain な Cookie での実装へ変更し、Cloudflare Workers でも読み出せるように 商品の閲覧履歴を CakePHP の Session 機能から localStorage を用いたものに変更 一部ログイン状態によってラベルやメニューが変更される箇所の改修、元々非ログイン状態の表示に統一したい認識があったため、これにあわせて変更 query として付与させる referrer 情報を事前にサーバー側で処理するコードがあったため、クライアント側で処理ができるように調整 効果 まず、レスポンス速度についてです。キャッシュの導入以前の商品ページは、利用している拡張機能やアクセスの状況にもよりますが、Chrome DevTools で確認する限りでは大体 600ms ~ 2s 程度のサーバー応答待ち時間(Waiting for server response の値)がありました。 キャッシュが有効な場合はこの値が大きく改善され、100ms ~ 150ms 程度で安定するようになります。平均的に 1 秒を超えてくるようなショップだと、1/10 程度になったことになります。ただしあくまでキャッシュが存在する前提なので、すべてのアクセスでこの恩恵を受けられるわけではありません。 では、キャッシュがどの程度働いているかをとある日のアクセススパイクを含む 30 分で見てみます。縦軸がキャッシュヒット率(%)、横軸が時刻、赤が全体のキャッシュヒットレート、緑が HIT 、青が STALE でそれぞれ返した割合です。 12:00 頃にアクセスが集中し、キャッシュヒット率が 80-90% 付近まで跳ね上がっています。具体的な数字で言えば、商品ページ毎にざっくり 50,000 程度のアクセスがあり、そのうち 40,000 を HIT 、5,000 を STALE で返しているようなイメージ感で、高いものだと 90% のリクエストキャッシュから返しています。このログには Bot も含まれているのですべてが人間に向けて返されたものではありませんが、Origin への到達を 90% キャッシュで捌けていると考えるとそれなりに効果があるように思えます。 このタイミングで商品ページにアクセスすると 100ms 程度でレスポンスが返ってくるので、全体でみるとちょっとだけショップページが速くなったことになります、なりませんか? おわりに ということでショップページがちょっとだけ速くなった話でした。ショップページは改良の余地が多くがあり、Cloudflare の活用もまだまだこれからです。こういった領域に興味が湧きましたら採用情報もぜひご覧ください。 binc.jp 明日は @takashima です、お楽しみに!
はじめに こんにちは、BASE株式会社 上級執行役員 SVP of Development の藤川です。 今年、生成AIの活用は経営課題の一つとして大きな注目を集めています。 開発担当役員という立場としても、この変化を肌で感じる必要があると考え、何年ぶりかにソースコードと向き合い、実際にプルリクエストを出してみることにしました。 ソースコードから離れていた10年間 最後にBASEのソースコードを書いていたのは、2016年頃まで。 上場に向けて、採用活動や組織拡大がマネジメント課題として本格化し、マネージャ育成やエンジニア採用、IT内部統制、情報システム整備といった役割が増えていく中で、自然と現場のコードから離れていきました。 その後、システムは大きく進化していきました。 開発環境のDocker化、本番環境のコンテナ化、テスト導入、React/Vue.js採用、モジュラモノリス化、CakePHP依存度の低下、PHP5から7〜8への移行…。 自分が作った開発チームの人たちの手で、気付けば、今のコードやサーバ構成はすっかり「浦島太郎」状態になっていました。 CursorでBASEのコードをキャッチアップ 最初の壁は、この10年分の変化をどう取り戻すか。 以前からのBASEのシステム構造は頭に入っていたので、その知識をベースにAIを使いながらキャッチアップしていきました。 使ったのはAIエージェントアプリの「Cursor」です。 Dockerfileやドキュメントを一つひとつ読むには時間がかかりますが、これらの情報やソースコードを下地としてCursorに質問すると、必要な情報を整理してくれます。 合間の時間を活用しながら進め、ほぼ誰にも質問せずに1週間で開発環境を再現できました。 どうしてもわからない部分だけ、CTOにヒントをもらう程度で済みました。 特に驚いたのは、ソースコードだけを元に「BASE Apps」という概念をCursorが理解してくれたことです。 BASE Appsとは、抽選販売機能やTikTok Shop連携などの機能を、後からショップにインストールできる仕組みです。 使い始めのBASEはシンプルなまま、必要に応じて高い機能を追加できるようにすることで、お店の成長に合わせた柔軟な情報アーキテクチャを実現しています。 この構造をAIが説明してくれたとき、正直ちょっと感動しました。 実際にPRを出してみた 環境構築が終わり、手元のDocker環境でBASEの開発環境が動くようになったので、新入社員やインターンの人に割り当てられるオンボーディング用のチケットプールから、ちょっとした不具合修正のチケットを割り当ててもらいました。 チケットに書いてある要件をプロンプトとしてCursorに渡すと、修正案を生成してくれます。チケットに書いてある要件が適切かつ具体的であればあるほど、修正案の生成は精度が高くなります。 それを元にコードを書き換えてプルリクエストを出すことに成功しました。 コード自体は当社のエンジニアの仕事場ですから適当なコードは出せません。できるだけ丁寧に内容のチェックをしました。ここまではほぼほぼ短時間での作業なのですが気の使い所です。 エンジニアからは丁寧なレビューが返ってきましたが、振り返ると「必要以上に大きな修正だったかもしれない」と感じています。 理由は、フロントエンドとバックエンドが別リポジトリで管理されており、Cursorがそれぞれの中で最適解を導いた結果、全体としては少し大げさな修正になっていたためです。 今のCursorの管理単位はリポジトリ単位になるため、リポジトリ間の関係性については人間が俯瞰して補う必要があり、そこは甘かったなと痛感しました。また、その解像度でコードレビューをしてくれた当社エンジニアはマジですごいなと改めて思いました。 レビューを通じて感じたこと レビューの指摘自体は正しく、Webサービスの変更において最小限の修正が望まれるという考え方には納得です。 今回のケースではCursorが生成したコードは「動作的には正しい」ものでしたが、それが「チームとして最適なコード」であるとは限りません。 AIが導いた“技術的に正しい解”と、プロダクトとして“望ましい解”は必ずしも一致しない 。 このギャップは今回の大きな学びでした。 一方で、AIが生成したコードを人間が精緻にレビューするのは工数がかかります。 過剰にレビューするのは非効率にもつながり、今後は「どこまで人力で見極めるか」の基準づくりが重要になると感じました。 なお、今回のレビューは、上級執行役員からのプルリクエストということもあり、普段より丁寧に対応してくれた可能性があります(もしかすると警戒もあったかもしれません 笑)。普段のメンバー同士であれば、もう少し軽いレビューで済んだかもしれません。 なので、逆にこれほどの時間をつかってもらって申し訳ないなと思ったのが正直な感想で、アウトプットをCTOや開発チームに委ねていて、自分自身で責任を持ちきれない今の役割においては、気軽にプルリクは出せないなとも思いました。 生産性と責任のバランス AI活用で避けて通れないのが、「どこまで人間が最終責任を持つべきか」という課題です。 AIが出したコードが正しく動いているなら、そのまま通すべきか? それとも品質を優先してさらに精緻化するべきか? この答えは一つではなく、チームやプロジェクトによって変わります。 だからこそ、開発チーム全体でレビュー方針を共有し、最適なバランスを探ることが大切です。 そしてもう一つ、AI活用は Biz・PdM・エンジニアといった役割間の業務の「のりしろ」を増やす可能性 を秘めています。 MCPなどを活用してソースコードに直接アクセスし、コードを元データとして新規開発のプロトタイプを作ったり、企画検討や初期見積もりを高精度に進めたりする未来は、もう遠くありません。 役割を超えて協業するための「のりしろ」を増やすことが、AI時代の開発組織に求められる視点 だと感じています。 この「のりしろ」があることで、Biz・PdM・エンジニアが同じデータを共有し、より速く高精度な意思決定ができる未来が見えてきます。 将来的にはAIの処理性能が向上し、複数プロジェクトや複数Webサービスを横断してカバーできるようになるでしょう。 その時に備え、現時点での最適解を常に更新し続けるチームでありたいと考えています。 おわりに〜生成AIの可能性 今回、久しぶりにコードへ戻りPRを出す中で感じた一番の収穫は、現場を離れていたベテランエンジニアでも、AIを活用すれば短期間でキャッチアップできると実感できたことです。 採用やマネジメントに注力していたエンジニアリングマネージャにとっても、生成AIは「現場感を取り戻すための強力な手段」になり得ます。 小さな修正でも構いません。AIを足がかりにコードへ再び触れることで、意思決定の精度やチーム理解は大きく変わります。 ただ正直、今回は「PRを出すとレビューしてもらうのが申し訳なく、遠慮してしまう」気持ちもありました。 だからこそ、AIに任せきりにするのではなく、自分なりの意見を持つためにも、まずはコードに触れてみることが大切だと思います。 これはマネージャだけの話ではありません。 現場のメンバーも「今のやり方がベスト」と思い込まず、AIを取り入れることで新しい速度感や発想を手に入れられます。 AI活用への温度差は、やがて成果や成長速度の差に直結します。 AIは、現場とマネージャの距離を縮め、チーム全体を底上げするツールです。 重要なのは「使うかどうか」ではなく、 「どう使い、どう組織に取り込むか」 です。 半年後、一年後にチームとしてどこまでスピードと精度を高められるかは、いまどれだけ実践と学びを積み重ねられるかにかかっています。 AIをチームの戦力に変えるために、今のうちから試行錯誤を重ね、未来の開発組織の在り方を自分たちで形作っていくことが大切です。
始めに こんにちは。株式会社ペライチの開発部長の佐藤です。 ペライチでは、長年の機能開発を経てアプリケーション基盤がモノリシックに育ってきました。 また、 CakePHP, Backbone.js など、時を経て利用者が少なくなってきた FW をベースに開発をされていました。 サービスの拡大に向けて、このままのアーキテクチャで開発を続けることは、長期的に生産性低下につながるリスクがあると考え、マイクロサービス化、技術要素の刷新、統一を進めています。 その中でも今回は、とあるバックエンドサービスを CakePHP から Ruby on Rails へマイグレーションしたときの工夫につい
動画
該当するコンテンツが見つかりませんでした
書籍
該当するコンテンツが見つかりませんでした








