- API認証サービスを提供するUnkeyは、Cloudflare WorkersベースのサーバーレスアーキテクチャからGoベースのステートフルサーバーへ移行し、性能とアーキテクチャの複雑性の問題を解決
- 新しい構成では、応答速度が6倍向上するとともに、複雑なキャッシュ回避策やデータパイプラインのオーバーヘッドを排除
- サーバーレス環境では、関数呼び出し間で永続メモリが保証されないため、すべてのキャッシュ読み取りにネットワークリクエストが必要で、p99基準で30msを超えるキャッシュレイテンシが発生
- 分散システムからシンプルなアプリケーションアーキテクチャへ移行することで、セルフホスティングが可能になり、プラットフォーム独立性も確保、開発者体験も大幅に改善
- サーバーレスは断続的なワークロードや単純なリクエスト/レスポンスパターンには適しているが、一貫した低レイテンシが必要な場合や永続的な状態管理が必要な場合は、ステートフルサーバーの方が効果的
サーバーレスの限界と性能ボトルネック
- UnkeyではAPI認証がリクエストパスの中核にあり、数ミリ秒単位の遅延がユーザー体験に直接影響する構造だった
- Cloudflare Workersの世界中へのエッジ配備と自動スケーリングは魅力的だったが、キャッシュ永続性の欠如とネットワークリクエスト遅延が問題として表面化
-
キャッシュの問題
- サーバーレス関数には呼び出し間で維持されるメモリがないため、キャッシュ参照のたびに外部ネットワークリクエストが必要
- Cloudflareのキャッシュ参照ではp99レイテンシが30ms以上と測定
- 複数層のキャッシュ(SWR、Redisなど)を積み重ねても、根本的に**「ネットワークリクエスト0回」より速くはなれない**
- 結果として、目標だった10ms未満の応答を達成できなかった
-
SaaS結合の問題
- サーバーレスはインフラ管理の負担を減らしてくれるが、実際には追加のSaaSツールが必須になる
- キャッシュ → Redis、バッチ処理 → Queue、ログ → Durable Objects など
- 各サービスは遅延・コスト・障害点を追加する
- Cloudflare Durable Objects、Queues、Workflowsなどを混在利用しても、実質的な複雑性はむしろ増加
-
データパイプラインの問題
- サーバーレスは揮発的な環境のため、すべての呼び出しごとにデータを即座にフラッシュしなければならない
- イベント・ログ・メトリクスを処理するため、複雑なバッファリングおよび中継サービスを自前で構築する必要があった
- 例:
chproxyを通じてClickHouseへログをバッチ送信
- Axiomへログ送信時のレートリミットを回避するため、別途バッファリングサーバーを構築
- 結果として、単純な分析機能ですら分散イベント処理システム級の複雑さを招いた
ステートフルサーバーへの移行
- Goベースのステートフルサーバー(v2)を導入したことで、メモリ内バッチ処理と定期的なフラッシュが可能に
- 追加サービスや複雑なパイプラインは不要
- 保守性・デバッグ・ローカルテストが大幅に簡素化
-
性能結果
/v1/keys.verifyKeyと/v2/keys.verifyKeyの比較結果では、レイテンシが6分の1に減少
- Cloudflareの世界300 POPより少ないインフラでも、ユーザー体感の性能は大きく向上
性能以外の利点
-
セルフホスティング(Self-Hosting)
- Cloudflareランタイムに依存していたため、顧客はUnkeyをセルフホスティングできなかった
- Workersランタイムは技術的にはオープンソースだが、ローカル実行(開発モードでも)は極めて困難
- 標準的なGoサーバーへ移行したことでセルフホスティングが簡単に
docker run -p 8080:8080 unkey/api コマンドだけで実行可能
- 特殊なランタイムや複雑な設定は不要
- 開発者は今や数秒でUnkeyスタック全体をローカル実行でき、デバッグとテストがはるかに容易に
- ローカル開発とデバッグの速度も飛躍的に向上
-
開発者体験の改善
- サーバーレスで直面していた限界:
- 関数実行の制約を考慮する必要がある
- 呼び出し間で状態を維持しにくい
- 分散ログのデバッグが複雑
- ローカルテスト環境が不便
- ステートフルサーバーへ移行することで、こうした**複雑性税(Complexity Tax)**を解消
-
プラットフォーム独立性の確保
- Cloudflareエコシステムにこれ以上縛られない
- どこへでもデプロイ可能
- どんなデータベースでも利用可能
- ランタイム互換性を気にせずサードパーティサービスを統合可能
マイグレーション戦略と教訓
- マイグレーションを、時間とともに蓄積したAPI設計上の問題を修正する機会として活用
- 新しいv2 APIは既存のv1と並行稼働し、顧客はサポート終了期間中に両バージョンを利用可能
- サーバーレスを維持していた利点の1つは、利用量がゼロまで減ってもv1 APIを実行するコストがあまりかからず、無償の移行期間を確保できたこと
-
維持したもの
- サーバーレス的アプローチのすべてを捨てたわけではない
- グローバルエッジ配備: AWS Global Acceleratorを使って世界的に低レイテンシを維持
- 自動スケーリング: Fargateがサーバーレスの制約なしにスケーリングを処理
-
レートリミッターの性能向上
- サーバーレスモデルでは、速度・正確性・コストの間で大きなトレードオフが必要で、分散特性のため3つすべてを同時に満たすのはほぼ不可能だった
- ステートフルサーバーとインメモリ状態により、より高速で正確、かつ運用コストも低いレートリミッターを構築
サーバーレスが適している場合(そして適していない場合)
-
サーバーレスが適している場合
- 断続的なワークロード: 常時稼働しない場合、ゼロスケールの経済性が非常に高い
- 単純なリクエスト/レスポンスパターン: 永続的な状態や複雑なデータパイプラインが不要な場合
- イベント駆動アーキテクチャ: インフラ管理なしでイベントに応答するのに優れる
-
サーバーレスが不向きな場合
- 一貫した低レイテンシが必要なとき: 外部ネットワーク依存が性能を低下させる
- 永続的な状態が必要なとき: ステートレス性を回避する作業が複雑性を増す
- 高頻度ワークロード: 呼び出しごとの課金モデルが非経済的
- 細かな制御が必要なとき: プラットフォームの抽象化が制約になりうる
-
複雑性コスト
- このマイグレーションでの最大の教訓は、プラットフォーム制約を回避する作業の複雑性コストを理解すること
-
サーバーレスで構築していたもの
- ステートレス性を回避するための高度なキャッシュライブラリ
- データのバッチ処理のための複数の補助サービス
- メトリクス収集のための複雑なログパイプライン
- ローカル開発のための高度な回避策
-
ステートフルサーバーでは
- 上記のすべてが消えた
- 多数の可動部を持つ分散システムから、シンプルなアプリケーションアーキテクチャへ移行
- ときには制約を回避するよりも、別の土台を選ぶことが最善の解決策となる
次のステップ
- 現在はGlobal Acceleratorの背後にあるAWS Fargate上で稼働中だが、これは暫定対応
- 来年、顧客(そしてUnkey自身)が望む場所ならどこでもUnkeyを実行できるようにする**独自デプロイプラットフォーム「Unkey Deploy」**を公開予定
- ステートフルなGoサーバーへの移行は、Unkeyを真に移植可能かつセルフホスト可能にする第一歩
- 実装の詳細はGitHubリポジトリでオープンソースとして公開
3件のコメント
私もかつては、ほとんどサーバーレスの信奉者のようにサーバーレスアーキテクチャをあちこちに採用していましたが、最近は
ec21台にrds1つで構成された構造のほうを好んでいます。そして必要なものを少しずつ切り出していきます。サーバーレスの導入はかなり悩んだうえで行うようになりました。理由はいろいろありますが、チームにサーバーレスの知識がない人が1人いるだけでも、コミュニケーションや保守のコストがかなり増えるんですよね。そして改めてサーバーを回してみると、サーバーレスは思ったほど安くもなく、思ったほど楽でもなかったのだとあらためて感じました。
Hacker Newsのコメント
数年間 serverless 環境(主に Amazon Lambda、ほかにもいくつか)を使ってきた立場として、著者の意見に強く同意する。
serverless はある部分では手間を減らしてくれるが、同時に「人為的に生まれる問題」を解決するために別の領域で仕事が増える。
自分の例のひとつはアップロード容量制限の問題だった。
既存アプリケーションを serverless に移行したとき、大容量の顧客データ import 用に API エンドポイントを作り、バックグラウンドワーカーを付ければ十分だろうと思っていた。
しかし "api gateway"(コードを呼び出すプロキシ)では 100MB を超えるアップロードができず、制限変更が可能か問い合わせたところ、単に顧客にもっと小さいファイルに分割してアップロードしてもらうよう案内された。
技術的にはもっともらしく聞こえるかもしれないが、実際の顧客がアップロード方法を変えてくれるわけがないというのが現実だ。
「真空中ではうまく動く」に近い感覚で、理論上はかっこよく見えても、実際にやってみると serverless への移行で節約した時間とコストを、結局は serverless 特有の問題を解決するために再投入することになる。
この問題を解決するには presigned S3 URL を提供する必要がある。
ユーザーが S3 に直接アップロードしたあとでアップロード結果を送ってもらうか、ファイル名を request id で区別する方法などで連携できる。
AWS 環境を長く使ってきた立場からするとこうした点は煩わしいが、任意ファイルのアップロード API を開けておくと DoS 攻撃のリスクが大きいので、100MB 制限自体は理解できる。
ただ、最近のインターネット速度を考えると 100MB という基準はやや時代遅れに感じる。
それでも、いずれかの制限は必要だと思う。
うちの会社は一時期、AWS の SSL 証明書部門を代表する顧客だった。
Vanity URL(カスタムドメイン)をサポートしようとすると、ドメインごとに SSL 証明書が必要で、何千個も必要になった。
AWS の証明書管理ツールは数百件規模までしか快適ではなかったため、この問題が解決するまでに 3 か月ほどかかった。
AWS の一部サービスが、ごく少数の顧客ニーズに対しては素早く対応できないという点に驚いた。
最初は Lambda がとても可能性のあるものに見えて導入したが、結局すべての Lambda プロジェクトを捨て、必要に応じてコンテナ環境へ移した。
Lambda でも Node ランタイムを 1〜2 年ごとにアップグレードしなければならず、コンテナならその周期を自分で決められるので、より柔軟だ。
コンピューターサイエンスで最も難しい問題は、あるコンピューターから別のコンピューターへファイルをコピーすることだ。
将来の読者のために参考資料を残しておく。
"tus" uploader と endpoint を使えば、アップロード時の分割や再開などの機能が提供されるので、こうした制限を乗り越えるのに向いている。
https://tus.io/
記事で説明されているように、serverless には間違いなくそれなりの用途がある。
だが、ほとんどのアプリケーションには適していないと思う。
特別な状況でなければ、serverless を中核インフラとして使うつもりはない。
実際には、むしろインフラ管理のほうが面倒になる印象だ。
プラットフォームごとに要件が違い、テストや開発のやり方もバラバラなので、毎回あいまいでローカルテストも難しい。
抽象化レイヤーにも各プラットフォームごとの落とし穴があり、本当の標準は存在しない。
Docker イメージで実行ファイルをパッケージングするほうが楽で、環境設定もちょうどよく抽象化できるので、自分にはそのほうが自然な開発環境だ。
Linux 環境とファイルシステムレベルの最小限の抽象化が最も効率的だと感じる。
必要ならそのイメージをサーバーとして立ち上げて、オンデマンドまたは常時待機状態で運用することもできる。
この 10 年で流行したさまざまな技術トレンドを見ると、大企業が自分たちの規模でしか実質的に存在しない問題を解決するために使っていた技術が人気を集めることがよくあった。
例として GraphQL、react、Tailwind、NextJS などが当てはまる。
どんなツールもすべての問題に万能ではなく、自分の状況や課題に対する経験と理解に基づいて選ぶことが重要だ。
今自分がどれだけ「楽しく」 Amazon Lambda アプリをローカルで動かそうとしているか言いたいくらいだ。
デプロイ前にテストしようとすると、それ自体がチャレンジになる。
Knative(Kubernetes 向け Serverless)はコンテナをそのまま受け付ける。
標準的なパッケージング形式なので、さまざまなプラットフォームへ簡単に移せる。
そのチームは実際にはアプリケーションではなく、状態を持つサーバーアプリケーションに統合されるライブラリを開発していた。
パフォーマンス上の利点も、authentication を顧客環境の近くで処理したおかげであって、serverless そのもののおかげではない。
実際のところ、すべての "serverless" プラットフォームは Docker イメージを受け付けるのでは?
Cloud Run は対応しているはずだ。
実のところ懸念しているのは、"serverless function" があまりに多くのものを抽象化しすぎていることだ。
ClickHouse は小さな insert を何千回も受けるのを嫌うので、うちは chproxy という Go サービスでイベントを集めて大きな batch にして送っている。
各 Cloudflare Worker が chproxy に analytic イベントを送り、chproxy がまとめて ClickHouse に大量送信する。
ClickHouse データに限るなら、わざわざ別サービスを作らなくても非同期 insert 機能があったのに、なぜそれを使わなかったのか気になる。
https://clickhouse.com/docs/optimize/asynchronous-inserts
開発者は「簡単にしてくれる」ツールに埋もれている感じがする。
実際には、ほとんどの問題は最も基本的なツール(コンパイラ、bash スクリプト、ライブラリ)でも簡単に解ける。
このようにツールに執着すると、企業にも開発者にもかえって害になることがある。
自分の考えでは、2013 年の Docker こそが最後に本当に普遍的で前向きな変化をもたらしたツールだった。
それ以降は、ある会社には役立っても、すべての会社に当てはめようとして逆に生産性が壊れたり、システムが崩れたりする例が多い。
最近 Cloudflare のようなところでは v8 isolates を「次世代の Docker」のように売り込んでいるが、一部のワークロードには刺さっても、どこにでも合うものではない。
「Docker イメージを受け取ってインターネット上で動かす」というパターンはあまりに強力で、2040 年になってもなお最強の方式だと思う。
基礎をわかっている人がますます少なくなっているのも問題だ。
スタックの大半をサードパーティーサービスに外注する形で仕事が進み、自分たちが実際に作ったのは中核アプリケーションの一部だけ、ということになっている。
しかもそれすら AI が書くようになるかもしれない。
業界全体が「学習された無力感」を育てている最中だ。
思っている以上に多くの人がコンパイラ、bash スクリプト、ライブラリをよく知らない。
AWS Lambda は安すぎるくらいだ。
知名度向上には役立つ!
bash スクリプトを書いて Staff 等級まで上がった人は見たことがない。
"vercel security checkpoint" に聞きたい。
iPhone で Proton VPN と Firefox Focus の組み合わせを使い、カリフォルニアやカナダの exit ノードで接続すると、code 99 "Failed to verify your browser" エラーが繰り返し発生する。
何が問題なんだろう?
このスレッドで自分の考えが確信に変わった。
serverless という用語自体の定義があまりに曖昧で、名前そのものがナンセンスに感じる。
結局サーバーは依然として存在する。
それは「電気がない」と言いながら実際には電気を使っているようなものだ。
名前だけが変わって、実際に何が起きているのかを語っていない感じがする。
「serverless が悪い」で終わる話ではないと思う。
より重要な教訓は、サービスに依存関係があるなら、サービスをクライアントの近くへ移しても、その依存関係まで一緒に移さなければ end-to-end の体験は意外と遅くなる、という点だ。
必然的に依存関係の近くでビルドするほうがよく、不十分ならすべての依存関係をクライアント近くへ移すか同期すべきだが、実際にはその過程があまりにも複雑になることが多い。
依存関係をどう使うか、どれだけ頻繁に使うかによって違うし、DB ならサーバーの近くに置くべきか、クライアントの近くに置くべきかは用途次第だ。
ある利用シナリオでは速い応答が必要で、別のものでは遅くても問題ない。
サーバー側やクライアント側で何をキャッシュできるかによって分割の仕方も変わる。
そこまで二分法で考える必要はないと思う。
最高の価格対性能比を求めるなら、自分でインスタンスをセットアップして必要なことを自分で処理するのが正解だ。
クラウド事業者を過剰請求の魔術師にしておく必要はない。
現時点で私たちが到達している「局所最適(local maximum)」は、Docker コンテナを標準の環境・配布物として使い、必要に応じて秘密情報だけ注入する形だ。
こうすればローカルテストが容易で、インフラ自動化や再現性などの主要な利点はほとんど活かせる。
serverless は大半のアプリにはやりすぎだが、一部のアプリにはよく合う。
特に、自前インフラが不要な単純なユーティリティやオンデマンドサービス、大規模な stateless アプリには serverless のほうが向いていることがある。
serverless が必ず単純用途にしか使われないわけではなく、「従来型の Web アプリ」モデルと serverless プラットフォームの間には根本的な矛盾があると思う。
そろそろ misterio を受け入れる準備ができた気がする。
https://github.com/daitangio/misterio
シンプルな stateless Docker クラスターラッパーだ。
自分の home lab から始まったが、反響が広がった。
Docker は microservices に少し似ている。
一部のアプリには適しているが、業界標準のように過大評価されてきた傾向がある。
Docker を乱用するとセキュリティパッチや運用負荷が生まれ、何でもかんでも使うとリスク管理に失敗する。
依存関係の問題も、今ではグローバルインストールをしないパターンで大半の開発者が解決している。
以前ほど Docker が必須ではないが、それでも依然として主流だ。
ホスティング事業者の立場では、Docker 導入で利益率が最低 10% は増えるだろうと予想する。
誰かが下で指摘していた「ツール執着」には Docker も含まれると感じる。
要点は serverless が通用しないということではなく、著者たちが自分たちの構築している基盤をよく理解していなかったということだ。
レイテンシに敏感な API を stateless edge runtime に載せるのは初心者レベルのミスで、その結果として味わった苦痛も十分予測可能なものだった。
自分の経験では、クラウドサービスの問題の大半はアーキテクチャの誤用や誤解に由来する。
もう少し慎重に設計していれば避けられた人的な問題だ。
ただ、問題はたいていクラウドベンダーがもっともらしいマーケティングで製品を宣伝し、実際の性能値を隠していることだ。
本当に Lambdas が自分のワークロードに対して十分速いのか、AWS RDS の外部レプリケーションが適しているのかは、試してみるまでわからない。
AWS の実際の性能は自分でベンチマークしなければならないと、経験を通じて学んだ。
著者たちが理解していなかったことがポイントなのではなく、誰かが共有してくれた情報そのものに価値がある。
これを単なる「初心者のミス」とだけ見ることはできないと思う。
実際、エンジニアはあるやり方が実務に向いていないことをわかっていても、マネージャーが「今どきのやり方だから」といった調子で簡単に押し通すことがよくある。
あるいは時間やコストの制約のために、「より悪い選択」をせざるを得ないこともある。
もちろん本当に元の実装チームがよくわかっていなかった可能性もあるが、いずれにせよ、こうした話を共有することはコミュニティ全体の発展に大いに役立つと思う。
自分の経験では、何か確実なものが必要なら(高速な auto scaling、低レイテンシ、CPU、ディスク、ネットワーク速度など)、自分で EC2 インスタンスを管理するのがいちばん確実だ。
制御権を手放して性能向上を期待すると、修正不能なボトルネックにぶつかることになる。
結局、著者たちは「今日の 10,000 人に 1 人」だったと認めることになる。
https://xkcd.com/1053/
個人的には、こうした情報や失敗を共有してくれたことに感謝している。
エンジニアリングは常にコストとの戦いだ
初期には、プロトタイピングやビジネス構築にかかる時間を減らすために使い、
後には最適化しながらコストを下げるべきだ
こういう文章そのものが、自分がどれだけエンジニアではないかを証明している
こんなふうに