ブログに戻る

PHP から Go へ移行すべき理由

PHP はリクエストごとにワーカーを丸ごと 1 つ占有しますが、Go は 1 つのプロセスで数千の接続を同時に保持できます。この並行処理の扱い方の違いこそがチームが移行する本当の理由です。しかも、すべてを書き直すのではなく、サービスを 1 つずつ移行できます。

PHP から Go へ移行すべき理由

PHP が大規模な環境で苦戦するとき、言語そのものの速度が原因であることはまれです。問題は同時リクエストの扱い方にあります。PHP-FPM では、リクエストごとに 1 つのワーカープロセスが処理の最初から最後まで占有されます。そのため、遅いクエリ、外部サービスの呼び出し、開いたままの接続などで待ち時間が長いリクエストは、本来ならほかのユーザーに応えられたはずのワーカーを塞いでしまいます。ライブダッシュボード、ストリーミングレスポンス、ファンアウトの大きい API といったリアルタイム機能は、すぐにこの上限に達します。ワーカーやマシンを増やせば上限は押し上げられますが、上限自体は残ります。

Go の並行処理モデルにはこの上限がありません。それが、チームが PHP から Go へ移行する主な理由です。この記事では、このモデルがなぜ重要なのか、デプロイとコストにどう効くのか、移行したチームが報告した成果、そしてプロダクトの命運を書き直しに賭けるのではなくサービスを 1 つずつ移行する方法を扱います。

要約

  • 並行処理モデルのために移行しましょう。 PHP のリクエストごとに 1 プロセスという設計では、リクエストが続く間ずっとワーカーが丸ごと 1 つ占有されます。Go の goroutine なら、1 つのプロセスで数千の同時接続をさばけます。リアルタイム機能や接続の多い機能を追加するほど、この差は広がります。
  • メモリの差は大きいです。 フレームワークを読み込んだ PHP-FPM ワーカーは、およそ 30〜60 MB を消費します。一方、goroutine は約 8 KB から始まります。50 本の同時ストリームで FPM プールが飽和することもありますが、Go のプロセス 1 つならほとんど影響を受けません。
  • 移行したチームは大きな成果を報告しています。 Cloudflare とフィンテック企業 Curve のエンジニアは、あるサービスが Go への移行後に毎秒約 10 リクエストから数千リクエストまで処理できるようになり、新しいエンジニアも 1〜2 週間で戦力になったと語っています(Go Time #316)。
  • デプロイがシンプルになります。 Go は単一の静的バイナリにコンパイルされます。ランタイムのインストールも、Composer の実行も、FPM のチューニングも要らないので、マイクロサービスや Kubernetes とよく合います。
  • すべてを書き直す必要はありません。 ストラングラーパターンを使いましょう。問題を抱えているサービスを 1 つだけ移行して PHP と並べて運用し、効果が確かめられてから範囲を広げていきます。

目次

移行する本当の理由は並行処理

従来の PHP は、シェアードナッシングかつリクエストごとに 1 プロセスという方式で動きます。リクエストが届くと PHP-FPM がそれをワーカーに渡し、ワーカーはまっさらな状態からレスポンスを返すまでコードを実行し、その後リセットされます。シンプルで分離性も高い設計です。その代わりにメモリを消費します。

フレームワークを読み込んだ PHP-FPM ワーカー 1 つは、通常 30〜60 MB の常駐メモリを使います。50 個のリクエストを同時に処理するには 50 個のワーカーが必要で、約 1.5〜3 GB の RAM を費やすことになります。しかもその大半は待ち時間に使われるだけです。

では、その 50 個のリクエストが、それぞれ数分間開いたままになる Server-Sent Events のストリームや WebSocket 接続だったらどうでしょうか。長時間続く 50 本の接続で FPM プール全体が飽和し、51 人目のユーザーは順番待ちになります。同時接続 1 本ごとにプロセスが丸ごと 1 つ必要になるので、ハードウェアを足して対処するとすぐに高くつきます。

Go の仕組みはその逆です。Go の並行処理の単位である goroutine は約 8 KB のスタックから始まり、ランタイムは数千の goroutine を少数の OS スレッドに多重化します。goroutine が I/O でブロックすると、スケジューラはそれを待機させ、同じスレッドで別の goroutine を実行します。Go のプロセス 1 つで数万の接続を開いたまま保持でき、使うメモリは同等の FPM 群が必要とする量のごく一部で済みます。接続ハンドラはごく普通の関数として書けます。

example.gogo
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
	// Each connection is one cheap goroutine. Blocking here parks
	// this goroutine and frees the thread for others, no pool to exhaust.
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "streaming unsupported", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/event-stream")

	for event := range s.events(r.Context()) {
		fmt.Fprintf(w, "data: %s\n\n", event)
		flusher.Flush()
	}
}

net/http のサーバーは各リクエストを自動的にそれぞれの goroutine で実行するため、このコードは重いワーカーを確保することなくライブのストリーミング接続を保持します。プロダクトがリアルタイム機能、ファンアウトの大きい API ゲートウェイ、多数の同時接続を保持するサービスへと向かっているなら、この違いが大きく効いてきます。PHP のモデルはこうした負荷に向いておらず、Go は特別な工夫なしにこれを処理できます。goroutine や channel に馴染みがない場合は、Go Concurrency Fundamentals コースでこのモデルを手を動かしながら学べます。

移行後にチームが得られるもの

実際の PHP から Go への移行について、当事者の話として最も参考になるのは Go Time ポッドキャストの第 316 回です。Cloudflare とフィンテック企業 Curve のエンジニアが、本番環境の PHP システムを Go に移行した経緯を語っています(Go Time #316, Changelog)。実務者が自分たちのシステムについて語っているので、ベンダーの導入事例や、外部から数字を推測したブログ記事よりも役に立ちます。

あるサービスは、旧構成では毎秒約 10 リクエストだった処理能力が、移行後には数千リクエストになったと報告されています。ランタイムについては「出来の悪い Go でさえ驚くほど高速だ」とまとめていました。つまり、最適化が必要になるずっと手前の段階から高いスループットが得られるということです。新しいエンジニアは数か月ではなく 1〜2 週間で戦力になりました。これは Go の言語仕様が小さいおかげです。

Curve の Go の系譜は、同じく Go を全面的に採用したフィンテック企業 Monzo にさかのぼります。このパターンがフィンテック業界に広がったのも、こうしたつながりによるところがあります。

Go の本番運用事例はほかにもあります。HelloFresh は API ゲートウェイの Janus を Go で構築し、オープンソースとして公開しました(GitHub の Janus)。ゲートウェイはファンアウトの大きいルーティング層で、goroutine が最も役立つ種類の負荷です。また、PHP から Go に書き直したバッチジョブ 1 つが、わずかなリソースではるかに速く動くようになったと報告する個々のエンジニアも数多くいます。

Go vs PHP の技術的な優位点

バックエンドサービスにとって重要な点で、両者を比較すると次のようになります。

観点PHPGo
並行処理リクエストごとに 1 プロセス、Fibers で並行性は加わるが並列性はなしM:N スケジューラ上の goroutine による、複数コアにまたがる真の並列処理
型付け動的型付け、段階的な型ヒント静的型付け、コンパイル時にチェック
デプロイコード + Composer の依存関係 + FPM + Web サーバー静的バイナリ 1 つ、ランタイムのインストール不要
実行方式インタプリタ、JIT による補助ネイティブのマシンコードにコンパイル

最も重要なのは並行処理です。PHP 8.1 では Fibers が追加されました。これは確かな改善ですが、何ができるのかは正確に理解しておく価値があります。Fibers が提供するのは、非同期 I/O を構造化するための協調的な並行処理です。1 つのワーカーの中で待機中のタスクを交互に進められるようにする仕組みです。Go のスケジューラのように、1 つの PHP プロセスで 16 コアを CPU 処理で使い切れるようにするものではありません。

Go は多数の goroutine を多数の OS スレッドに割り当てて並列に実行し、ブロックしたときには低コストで待機させます。同じプリミティブで、低コストな並行処理と本物の並列処理の両方が得られます。ワーカープールがよい例です。Go では頻繁に使いますが、従来の PHP には単一プロセスでこれをきれいに実現する手段がありません。

example.gogo
func process(jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done()
	for job := range jobs {
		results <- job.Run() // each worker pulls the next job when free
	}
}

func main() {
	jobs := make(chan Job, 100)
	results := make(chan Result, 100)
	var wg sync.WaitGroup

	// Fan out to 8 workers sharing one queue, all in one process.
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go process(jobs, results, &wg)
	}

	go func() {
		for _, j := range loadJobs() { // pull the batch from your work source
			jobs <- j
		}
		close(jobs)
	}()

	go func() { wg.Wait(); close(results) }()
	for r := range results {
		record(r)
	}
}

デプロイは、もっと注目されてよい点です。Go をビルドすると単一の静的バイナリができ、そのファイルをサーバーや scratch コンテナにコピーするだけでデプロイが済むこともあります。PHP ランタイムのインストールも、Composer の実行も、FPM のチューニングも、前段に置く Web サーバーの設定も要りません。Kubernetes でマイクロサービスを運用しているなら、デプロイのたびにこの小さく自己完結した成果物を扱えます。

静的型付けも役に立ちます。PHP では実行時にしか表面化しない種類のエラーを、コンパイラがビルド時に検出します。サービスが成長し、関わる人が増えるほど、これは重要になっていきます。

次に「最近の PHP は速い」という反論についてです。PHP は、PHP 8 の JIT や、リクエストをまたいでアプリをメモリ上に起動したまま保持する FrankenPHP や RoadRunner といったワーカーランタイムによって、純粋な速度の差をかなり縮めました。しかし、FrankenPHP と RoadRunner はどちらも Go で書かれています。PHP のエコシステムは、高い並行性を持つアプリケーションサーバーが必要になったとき、それを Go のランタイム上に構築しました。並行処理が課題なら、PHP の前段に Go 製のランタイムを置くこともできますし、接続の多いサービスを直接 Go で書いて中間層を省くこともできます。

ベンチマークの実態と本当のメリット

Go は PHP より 10〜30 倍速いという見出しの数字を目にすることがあるでしょう。これらは TechEmpower ベンチマークのような合成ベンチマークによるもので、CPU とフレームワークがボトルネックになる理想化された条件でランタイムを試した結果です(TechEmpower Framework Benchmarks)。数字自体は本物ですが、本番環境のレイテンシを予測するものではありません。その倍率を期待して移行するのはやめましょう。

本番環境では、リクエストにかかる時間の大半はデータベースとネットワークに費やされ、Go も PHP もそこでは同じように待つしかありません。そのため、倍率はすぐに縮みます。移行後に誠実に計測したチームの多くは、エンドツーエンドの平均レイテンシの改善を 1 桁台から 10% 台前半程度と報告しています。

この平均値の陰に、Go が勝っている部分が隠れています。それはテールレイテンシの安定性とリクエストあたりのコストです。Go では空いている FPM ワーカーを待つリクエストがないため、負荷がかかっても p99 や p99.9 のレイテンシが横ばいに保たれます。また、プールでは吸収しきれない並行性を 1 つのプロセスで吸収できるので、同じトラフィックをはるかに少ないマシンでさばけます。本番環境と請求書で実感できるのはこうした効果です。そしてこの効果は、合成ベンチマークが誰にも気にされなくなった後も続きます。

採用もリリースも速くなる

採用とオンボーディングの面でも Go は有利です。言語仕様は小さく、キーワードは 25 個、フォーマッタは強制される 1 つだけです。そのため、PHP やほかの言語から来た有能なエンジニアなら 1〜2 週間で戦力になります。これは Cloudflare と Curve のエンジニアが語っていたオンボーディング期間とも一致します。

つまり、すでに Go を知っている人を採用する必要はありません。Go は短期間で身につきますし、強制されるフォーマットと静的型付けのおかげで、書かれるコードは初日から一貫したものになります。

市場調査では Go の給与が高めに出る傾向もあり、需要が供給を上回っていることがうかがえます。Glassdoor などの情報源から引用される具体的な数字は、地域や職種によって異なります。ビジネスケースの 1 項目ではなく、採用における追い風として捉えてください。Go のエコシステムが最も強いのは、ネットワーク、インフラ、CLI、高スループットのサービスで、これはまさに移行の対象になる仕事です。goroutine はそうした仕事のために作られ、周辺のライブラリとツールも成熟しています。

会社の命運を賭けずに移行する方法

すべてを一度に書き直してはいけません。全面的な書き直しは、プロダクトを停滞させる最も確実な方法の 1 つです。動いているコードには長年のバグ修正やエッジケースへの対応が詰まっていて、書き直せばそれを捨てることになるからです(Joel on Software)。Cloudflare と Curve のチームも、ビッグバン方式の書き直しはしていません。ストラングラーパターンを使っており、これは真似する価値があります。

まずは、実際に問題を抱えているサービスを 1 つ選びます。プールを飽和させている SSE ストリーム、ワーカーを塞いでいるファンアウト API、十分な数の接続を保持できないゲートウェイなどです。そのサービスだけを Go で作り直し、同じルーティング層の後ろに配置して、PHP と並べて運用します。

トラフィックの一部をそこに流し、メモリとテールレイテンシを監視してから、流す量を増やしていきます。その間も PHP はほかのすべてを処理し続けるので、一斉切り替えの日はなく、1 回の切り替えにすべてが懸かることもありません。最初のサービスで効果が実証されれば(たいていは実証されます)、実際の数字と、次のサービスに使えるテンプレートが手に入ります。

そのため、移行はサービス単位の判断になります。「Go 一色の会社」になる必要はありません。CMS、コンテンツページ、チームが素早く開発できている Laravel の管理画面など、PHP が合っているところでは PHP を使い続けましょう。そして、接続が多く高スループットのサービスを Go に移しましょう。

よくある質問

Go は PHP より速いですか?

CPU バウンドの合成ベンチマークでは、はい。多くの場合、大きな差がつきます(TechEmpower)。本番環境では、リクエストにかかる時間の大半がデータベースとネットワークに費やされ、どちらの言語もそこでは同じように待つため、平均レイテンシの差は通常もっと小さくなります。高負荷時の並行処理とテールレイテンシでは、Go の優位が続きます。Go は 1 つのプロセスで数千の接続を保持し、FPM プールならワーカーが足りなくなる状況でも p99 レイテンシを横ばいに保ちます。そのため、同じトラフィックに必要なマシンが少なくて済みます。

PHP アプリ全体を Go で書き直すべきですか?

いいえ。段階的に移行しましょう。全面的な書き直しは、プロダクトを停滞させる最も確実な方法の 1 つです(Joel on Software)。ストラングラーパターンを使い、並行性が高く問題を抱えているサービスを 1 つ Go に移して PHP と並べて運用し、トラフィックを徐々に切り替え、効果が実証されてから範囲を広げます。リスクの高い一斉切り替えをせずに、その恩恵を得られます。

最初に移行すべきなのはどんなサービスですか?

接続が多いサービスや、ファンアウトの大きいサービスから始めましょう。WebSocket や SSE のエンドポイント、リアルタイム機能、API ゲートウェイ、ストリーミングレスポンス、そして多くのリクエストが I/O 待ちで開いたままになるあらゆるサービスが該当します。PHP のリクエストごとに 1 プロセスというモデルはこれらで最初に限界を迎え、goroutine はすぐに効果を発揮します。そのため、移行が成果を上げていることを最も早く確かめられます。

PHP で WebSocket やリアルタイム機能は扱えますか?

はい、補助があれば扱えます。従来の PHP-FPM は接続ごとにワーカーを 1 つ占有するため、長時間続く接続が多いとプールが枯渇します。Swoole や FrankenPHP のようなランタイムは、ワーカーを常駐させてイベント駆動の並行処理を加えることで、これを動くようにしています。Go は同じ負荷をネイティブに処理でき、接続 1 本につき低コストな goroutine が 1 つで済みます。接続の多いサービスは、チームが移行する理由として最もよく挙がるものです。

Go は PHP より習得が難しいですか?

構文は難しくありません。Go のキーワードは 25 個しかなく、フォーマットのスタイルも強制される 1 つだけで、ほとんどのエンジニアは 1〜2 週間で戦力になります。PHP 開発者にとって新しいのは、並行処理モデル(goroutine と channel)と、エラーを例外ではなく戻り値として扱うことです。これらにはある程度の練習が必要ですが、移行したサービスで最もよく使うスキルでもあります。

本番環境で使える Go を書く準備はできましたか?

Go を評価する最善の方法は、実際に書いてみることです。LevelUpGo では、すべてのレッスンがブラウザ上で解く実践的な演習になっています。コードは本物の Go ツールチェーンで実行され、演習のテストで検証されます。Go について読むだけでなく、動く Go のコードに対してすぐにフィードバックが得られます。

PHP 開発者向けの学習パスは次のとおりです。

  • Go Fundamentals では、型、struct、エラー処理など、PHP から来ると違いを感じる部分を扱います。無料で始められ、クレジットカードは不要です。
  • Composite Types では、slice、map、struct という、サービス内でやり取りするデータの日常的な構成要素を学びます。
  • Go Concurrency Fundamentals では、goroutine、channel、select を学びます。PHP-FPM のプール全体でもできないことを、1 つの Go プロセスで実現するための機能です。
  • ロードマップ では、最初のプログラムから並行処理を使った総仕上げのプロジェクトまでの道筋を示しています。

ほかの比較記事として、Python から Go への移行、2026 年の Go vs Rust、Go vs Node.js とサプライチェーンの問題もあわせてご覧ください。

出典

この記事で引用した出典です。実務者による一次情報を先頭に記載しています。

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成