<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Apache on Tarragon</title><link>https://tarrragon.github.io/blog/tags/apache/</link><description>Recent content in Apache on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 05 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/apache/index.xml" rel="self" type="application/rss+xml"/><item><title>mod_php</title><link>https://tarrragon.github.io/blog/programming/php/knowledge-cards/mod-php/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/php/knowledge-cards/mod-php/</guid><description>&lt;p>mod_php 是編譯成 Apache 模組的 PHP，Apache 啟動時用 &lt;code>LoadModule&lt;/code> 載入，之後 Apache 的每個子程序裡都有一份 PHP 直譯器，收到 &lt;code>.php&lt;/code> 請求就在自己的程序裡執行。它的 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/sapi/" data-link-title="SAPI（Server API）" data-link-desc="在 PHP_SAPI 或 phpinfo 看到 cli、fpm-fcgi、apache2handler、frankenphp 這類值，要知道它代表 PHP 以哪種方式被啟動、請求怎麼交進來時">SAPI&lt;/a> 名稱是 &lt;code>apache2handler&lt;/code>。可先對照 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/php-fpm/" data-link-title="PHP-FPM（FastCGI Process Manager）" data-link-desc="在 Nginx 或容器環境看到 php-fpm、9000 埠、pool 設定或 502 Bad Gateway，要知道 PHP-FPM 負責什麼、哪些設定決定並發與記憶體時">PHP-FPM&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/apache-mpm/" data-link-title="Apache MPM（Multi-Processing Module）" data-link-desc="在 Apache 設定或 httpd -V 看到 prefork、worker、event，或要判斷 mod_php 為什麼讓 Apache 只能用 prefork、換成 PHP-FPM 之後能改用哪一種時">Apache MPM&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>mod_php 把直譯器放進 web 伺服器的程序裡：省掉 CGI 每請求啟動程序的成本，代價是 PHP 的壽命、執行帳號與設定都綁在 Apache 上。沒有以 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/zts/" data-link-title="ZTS（Zend Thread Safety）" data-link-desc="在 php -i 看到 Thread Safety 是 enabled 或 disabled，或要判斷某個 PHP 能不能跑在多執行緒的伺服器（Apache worker/event MPM、FrankenPHP）裡時">ZTS&lt;/a> 編譯的 mod_php 只能搭配 prefork MPM。PHP 官方文件對新部署的建議是改用 PHP-FPM 搭配 Apache 的 &lt;code>mod_proxy_fcgi&lt;/code>。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;code>apache2ctl -M&lt;/code> 列出 &lt;code>php_module&lt;/code> 與 &lt;code>mpm_prefork_module&lt;/code>&lt;/strong>：多數發行版的 mod_php 沒有開 ZTS，&lt;code>php -i&lt;/code> 顯示 &lt;code>Thread Safety =&amp;gt; disabled&lt;/code>。&lt;/li>
&lt;li>&lt;strong>送靜態檔的子程序也映射著 &lt;code>libphp&lt;/code>&lt;/strong>：每個子程序都帶著 PHP，不論它在處理什麼請求。&lt;/li>
&lt;li>&lt;strong>所有站台以 Apache 的帳號（如 &lt;code>www-data&lt;/code>）執行&lt;/strong>：PHP 產生的檔案屬於這個帳號。&lt;/li>
&lt;li>&lt;strong>&lt;code>.htaccess&lt;/code> 的 &lt;code>php_value&lt;/code>&lt;/strong>：mod_php 註冊了這個指令；沒載入 mod_php 的 Apache 讀到它會回 500（&lt;code>Invalid command 'php_value'&lt;/code>），而且整個目錄都壞掉。&lt;/li>
&lt;/ul>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>從 mod_php 搬到 PHP-FPM 時，&lt;code>.htaccess&lt;/code> 裡的 &lt;code>php_value&lt;/code> / &lt;code>php_flag&lt;/code> 要移到 &lt;code>.user.ini&lt;/code>（見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/php-ini/" data-link-title="php.ini / .user.ini" data-link-desc="PHP 的執行期設定檔，控制記憶體上限、上傳大小、錯誤報告等 runtime 行為">php.ini / .user.ini&lt;/a>）。完整的行為與實測見 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/mod-php/" data-link-title="mod_php：PHP 直譯器嵌進 Apache 程序的執行模型" data-link-desc="說明 mod_php 怎麼把 PHP 直譯器載入 Apache 的每個子程序：為什麼它要搭配 prefork MPM、每個子程序都帶著 PHP 對記憶體與並發的影響、所有站台共用 Apache 的執行帳號、php.ini 與 .htaccess 的設定方式，以及從 mod_php 搬到 PHP-FPM 時 .htaccess 的 php_value 與 .user.ini 的差異">mod_php&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>mod_php 是編譯成 Apache 模組的 PHP，Apache 啟動時用 <code>LoadModule</code> 載入，之後 Apache 的每個子程序裡都有一份 PHP 直譯器，收到 <code>.php</code> 請求就在自己的程序裡執行。它的 <a href="/blog/programming/php/knowledge-cards/sapi/" data-link-title="SAPI（Server API）" data-link-desc="在 PHP_SAPI 或 phpinfo 看到 cli、fpm-fcgi、apache2handler、frankenphp 這類值，要知道它代表 PHP 以哪種方式被啟動、請求怎麼交進來時">SAPI</a> 名稱是 <code>apache2handler</code>。可先對照 <a href="/blog/programming/php/knowledge-cards/php-fpm/" data-link-title="PHP-FPM（FastCGI Process Manager）" data-link-desc="在 Nginx 或容器環境看到 php-fpm、9000 埠、pool 設定或 502 Bad Gateway，要知道 PHP-FPM 負責什麼、哪些設定決定並發與記憶體時">PHP-FPM</a> 與 <a href="/blog/devops/infra/knowledge-cards/apache-mpm/" data-link-title="Apache MPM（Multi-Processing Module）" data-link-desc="在 Apache 設定或 httpd -V 看到 prefork、worker、event，或要判斷 mod_php 為什麼讓 Apache 只能用 prefork、換成 PHP-FPM 之後能改用哪一種時">Apache MPM</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>mod_php 把直譯器放進 web 伺服器的程序裡：省掉 CGI 每請求啟動程序的成本，代價是 PHP 的壽命、執行帳號與設定都綁在 Apache 上。沒有以 <a href="/blog/programming/php/knowledge-cards/zts/" data-link-title="ZTS（Zend Thread Safety）" data-link-desc="在 php -i 看到 Thread Safety 是 enabled 或 disabled，或要判斷某個 PHP 能不能跑在多執行緒的伺服器（Apache worker/event MPM、FrankenPHP）裡時">ZTS</a> 編譯的 mod_php 只能搭配 prefork MPM。PHP 官方文件對新部署的建議是改用 PHP-FPM 搭配 Apache 的 <code>mod_proxy_fcgi</code>。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<ul>
<li><strong><code>apache2ctl -M</code> 列出 <code>php_module</code> 與 <code>mpm_prefork_module</code></strong>：多數發行版的 mod_php 沒有開 ZTS，<code>php -i</code> 顯示 <code>Thread Safety =&gt; disabled</code>。</li>
<li><strong>送靜態檔的子程序也映射著 <code>libphp</code></strong>：每個子程序都帶著 PHP，不論它在處理什麼請求。</li>
<li><strong>所有站台以 Apache 的帳號（如 <code>www-data</code>）執行</strong>：PHP 產生的檔案屬於這個帳號。</li>
<li><strong><code>.htaccess</code> 的 <code>php_value</code></strong>：mod_php 註冊了這個指令；沒載入 mod_php 的 Apache 讀到它會回 500（<code>Invalid command 'php_value'</code>），而且整個目錄都壞掉。</li>
</ul>
<h2 id="設計責任">設計責任</h2>
<p>從 mod_php 搬到 PHP-FPM 時，<code>.htaccess</code> 裡的 <code>php_value</code> / <code>php_flag</code> 要移到 <code>.user.ini</code>（見 <a href="/blog/devops/infra/knowledge-cards/php-ini/" data-link-title="php.ini / .user.ini" data-link-desc="PHP 的執行期設定檔，控制記憶體上限、上傳大小、錯誤報告等 runtime 行為">php.ini / .user.ini</a>）。完整的行為與實測見 <a href="/blog/programming/php/01-server-runtime/mod-php/" data-link-title="mod_php：PHP 直譯器嵌進 Apache 程序的執行模型" data-link-desc="說明 mod_php 怎麼把 PHP 直譯器載入 Apache 的每個子程序：為什麼它要搭配 prefork MPM、每個子程序都帶著 PHP 對記憶體與並發的影響、所有站台共用 Apache 的執行帳號、php.ini 與 .htaccess 的設定方式，以及從 mod_php 搬到 PHP-FPM 時 .htaccess 的 php_value 與 .user.ini 的差異">mod_php</a>。</p>
]]></content:encoded></item><item><title>mod_php：PHP 直譯器嵌進 Apache 程序的執行模型</title><link>https://tarrragon.github.io/blog/programming/php/01-server-runtime/mod-php/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/php/01-server-runtime/mod-php/</guid><description>&lt;p>這篇說明 mod_php 這種執行模型：PHP 以 Apache 模組的形式載入，直譯器住在 Apache 的子程序裡。用 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/request-lifecycle-and-sapi/" data-link-title="PHP 的請求生命週期與 SAPI：直譯器的位置、程序壽命與狀態壽命" data-link-desc="說明一個 HTTP 請求怎麼變成一次 PHP 執行：PHP 在程序啟動與每個請求開始、結束時各做什麼，SAPI 這一層介面怎麼讓同一個直譯器接上 CGI、Apache、FastCGI 與常駐伺服器，以及用直譯器住在哪裡、執行它的程序活多久、應用程式的狀態活多久這組問題比較各種執行模型">PHP 的請求生命週期與 SAPI&lt;/a> 的那組問題來看，它的答案是：直譯器住在 web 伺服器的程序裡，程序活過很多請求，應用程式狀態仍然只活一個請求。文中的行為取自官方的 &lt;code>php:8.5-apache&lt;/code> 映像檔。&lt;/p>
&lt;h2 id="php-以-apache-模組的形式載入">PHP 以 Apache 模組的形式載入&lt;/h2>
&lt;p>mod_php 是編譯成 Apache 模組的 PHP（Debian 系的套件裡檔名是 &lt;code>libphp.so&lt;/code> 一類），Apache 啟動時用 &lt;code>LoadModule&lt;/code> 把它載入，它的 SAPI 名稱是 &lt;code>apache2handler&lt;/code>（見 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/mod-php/" data-link-title="mod_php" data-link-desc="接手 Apache 加 mod_php 的舊站、評估要不要換成 PHP-FPM，或遇到 .htaccess 的 php_value 在新環境回 500 時">mod_php&lt;/a>）。載入之後，Apache 的每個子程序裡都有一份完整的 PHP 直譯器，收到 &lt;code>.php&lt;/code> 的請求時在自己的程序裡直接執行，不經過任何程序之間的溝通。&lt;/p>
&lt;p>這個做法省掉了 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/cgi/" data-link-title="CGI：每個請求啟動一個 PHP 程序的執行模型" data-link-desc="說明 CGI 怎麼把一個 HTTP 請求交給外部程式：web 伺服器為每個請求啟動一個程序，用環境變數與標準輸入輸出傳遞請求和回應；PHP 以 php-cgi 執行時每個請求付出的啟動成本、OPcache 為什麼沒有作用、以每個站台的使用者身分執行帶來的隔離，以及請求資料與程式呼叫參數之間的邊界問題">CGI&lt;/a> 每個請求都要啟動一個新程序的成本：子程序處理完一個請求之後留下來，接著處理下一個。在 &lt;code>php:8.5-apache&lt;/code> 上對同一支計數腳本送 10 個請求，請求平均落在 5 個子程序上，每個處理兩次：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl"> 2 pid=16 static=1 global=1 sapi=apache2handler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 2 pid=17 static=1 global=1 sapi=apache2handler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> 2 pid=18 static=1 global=1 sapi=apache2handler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> 2 pid=19 static=1 global=1 sapi=apache2handler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> 2 pid=20 static=1 global=1 sapi=apache2handler&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>子程序被重複使用，&lt;code>static&lt;/code> 與全域變數仍然每次從 1 開始：程序活下來了，PHP 依舊在每個請求結束時清空應用程式的狀態。&lt;/p>
&lt;h2 id="mod_php-要搭配-prefork-mpm">mod_php 要搭配 prefork MPM&lt;/h2>
&lt;p>Apache 處理並發的方式由 MPM（Multi-Processing Module）決定，常見的有三種（見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/apache-mpm/" data-link-title="Apache MPM（Multi-Processing Module）" data-link-desc="在 Apache 設定或 httpd -V 看到 prefork、worker、event，或要判斷 mod_php 為什麼讓 Apache 只能用 prefork、換成 PHP-FPM 之後能改用哪一種時">Apache MPM&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>MPM&lt;/th>
 &lt;th>並發的單位&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>prefork&lt;/td>
 &lt;td>每個連線佔用一個子程序，子程序裡沒有多執行緒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>worker&lt;/td>
 &lt;td>每個子程序有多個執行緒，每個連線佔用一個執行緒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>event&lt;/td>
 &lt;td>跟 worker 一樣用執行緒，另外把閒置的 keep-alive 連線交給專門的執行緒看管，不佔住處理請求的執行緒&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>PHP 官方的安裝文件寫明：除非 PHP 以執行緒安全（&lt;code>--enable-zts&lt;/code>）編譯，mod_php 只能搭配 prefork MPM，而這「significantly limits concurrency」。原因在 PHP 的實作：沒有開啟 ZTS（Zend Thread Safety）的 PHP 把每個請求的狀態放在全域變數裡，兩個執行緒同時執行 PHP 會互相覆寫；而 PHP 的擴充與它們依賴的 C 函式庫也不保證能在多執行緒下使用（見 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/zts/" data-link-title="ZTS（Zend Thread Safety）" data-link-desc="在 php -i 看到 Thread Safety 是 enabled 或 disabled，或要判斷某個 PHP 能不能跑在多執行緒的伺服器（Apache worker/event MPM、FrankenPHP）裡時">ZTS&lt;/a>）。多數發行版的 mod_php 套件沒有開 ZTS，&lt;code>php:8.5-apache&lt;/code> 也一樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">$ apache2ctl -M | grep -E &amp;#34;mpm|php&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> mpm_prefork_module (shared)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> php_module (shared)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">$ php -i | grep &amp;#34;Thread Safety&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">Thread Safety =&amp;gt; disabled&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apache 2.4 自己的預設 MPM 是 event（官方 &lt;code>httpd:2.4&lt;/code> 映像檔回報 &lt;code>Server MPM: event&lt;/code>）。裝上 mod_php，Apache 就要退回 prefork。&lt;/p></description><content:encoded><![CDATA[<p>這篇說明 mod_php 這種執行模型：PHP 以 Apache 模組的形式載入，直譯器住在 Apache 的子程序裡。用 <a href="/blog/programming/php/01-server-runtime/request-lifecycle-and-sapi/" data-link-title="PHP 的請求生命週期與 SAPI：直譯器的位置、程序壽命與狀態壽命" data-link-desc="說明一個 HTTP 請求怎麼變成一次 PHP 執行：PHP 在程序啟動與每個請求開始、結束時各做什麼，SAPI 這一層介面怎麼讓同一個直譯器接上 CGI、Apache、FastCGI 與常駐伺服器，以及用直譯器住在哪裡、執行它的程序活多久、應用程式的狀態活多久這組問題比較各種執行模型">PHP 的請求生命週期與 SAPI</a> 的那組問題來看，它的答案是：直譯器住在 web 伺服器的程序裡，程序活過很多請求，應用程式狀態仍然只活一個請求。文中的行為取自官方的 <code>php:8.5-apache</code> 映像檔。</p>
<h2 id="php-以-apache-模組的形式載入">PHP 以 Apache 模組的形式載入</h2>
<p>mod_php 是編譯成 Apache 模組的 PHP（Debian 系的套件裡檔名是 <code>libphp.so</code> 一類），Apache 啟動時用 <code>LoadModule</code> 把它載入，它的 SAPI 名稱是 <code>apache2handler</code>（見 <a href="/blog/programming/php/knowledge-cards/mod-php/" data-link-title="mod_php" data-link-desc="接手 Apache 加 mod_php 的舊站、評估要不要換成 PHP-FPM，或遇到 .htaccess 的 php_value 在新環境回 500 時">mod_php</a>）。載入之後，Apache 的每個子程序裡都有一份完整的 PHP 直譯器，收到 <code>.php</code> 的請求時在自己的程序裡直接執行，不經過任何程序之間的溝通。</p>
<p>這個做法省掉了 <a href="/blog/programming/php/01-server-runtime/cgi/" data-link-title="CGI：每個請求啟動一個 PHP 程序的執行模型" data-link-desc="說明 CGI 怎麼把一個 HTTP 請求交給外部程式：web 伺服器為每個請求啟動一個程序，用環境變數與標準輸入輸出傳遞請求和回應；PHP 以 php-cgi 執行時每個請求付出的啟動成本、OPcache 為什麼沒有作用、以每個站台的使用者身分執行帶來的隔離，以及請求資料與程式呼叫參數之間的邊界問題">CGI</a> 每個請求都要啟動一個新程序的成本：子程序處理完一個請求之後留下來，接著處理下一個。在 <code>php:8.5-apache</code> 上對同一支計數腳本送 10 個請求，請求平均落在 5 個子程序上，每個處理兩次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">   2 pid=16 static=1 global=1 sapi=apache2handler
</span></span><span class="line"><span class="ln">2</span><span class="cl">   2 pid=17 static=1 global=1 sapi=apache2handler
</span></span><span class="line"><span class="ln">3</span><span class="cl">   2 pid=18 static=1 global=1 sapi=apache2handler
</span></span><span class="line"><span class="ln">4</span><span class="cl">   2 pid=19 static=1 global=1 sapi=apache2handler
</span></span><span class="line"><span class="ln">5</span><span class="cl">   2 pid=20 static=1 global=1 sapi=apache2handler</span></span></code></pre></div><p>子程序被重複使用，<code>static</code> 與全域變數仍然每次從 1 開始：程序活下來了，PHP 依舊在每個請求結束時清空應用程式的狀態。</p>
<h2 id="mod_php-要搭配-prefork-mpm">mod_php 要搭配 prefork MPM</h2>
<p>Apache 處理並發的方式由 MPM（Multi-Processing Module）決定，常見的有三種（見 <a href="/blog/devops/infra/knowledge-cards/apache-mpm/" data-link-title="Apache MPM（Multi-Processing Module）" data-link-desc="在 Apache 設定或 httpd -V 看到 prefork、worker、event，或要判斷 mod_php 為什麼讓 Apache 只能用 prefork、換成 PHP-FPM 之後能改用哪一種時">Apache MPM</a>）：</p>
<table>
  <thead>
      <tr>
          <th>MPM</th>
          <th>並發的單位</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>prefork</td>
          <td>每個連線佔用一個子程序，子程序裡沒有多執行緒</td>
      </tr>
      <tr>
          <td>worker</td>
          <td>每個子程序有多個執行緒，每個連線佔用一個執行緒</td>
      </tr>
      <tr>
          <td>event</td>
          <td>跟 worker 一樣用執行緒，另外把閒置的 keep-alive 連線交給專門的執行緒看管，不佔住處理請求的執行緒</td>
      </tr>
  </tbody>
</table>
<p>PHP 官方的安裝文件寫明：除非 PHP 以執行緒安全（<code>--enable-zts</code>）編譯，mod_php 只能搭配 prefork MPM，而這「significantly limits concurrency」。原因在 PHP 的實作：沒有開啟 ZTS（Zend Thread Safety）的 PHP 把每個請求的狀態放在全域變數裡，兩個執行緒同時執行 PHP 會互相覆寫；而 PHP 的擴充與它們依賴的 C 函式庫也不保證能在多執行緒下使用（見 <a href="/blog/programming/php/knowledge-cards/zts/" data-link-title="ZTS（Zend Thread Safety）" data-link-desc="在 php -i 看到 Thread Safety 是 enabled 或 disabled，或要判斷某個 PHP 能不能跑在多執行緒的伺服器（Apache worker/event MPM、FrankenPHP）裡時">ZTS</a>）。多數發行版的 mod_php 套件沒有開 ZTS，<code>php:8.5-apache</code> 也一樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">$ apache2ctl -M | grep -E &#34;mpm|php&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl"> mpm_prefork_module (shared)
</span></span><span class="line"><span class="ln">3</span><span class="cl"> php_module (shared)
</span></span><span class="line"><span class="ln">4</span><span class="cl">$ php -i | grep &#34;Thread Safety&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">Thread Safety =&gt; disabled</span></span></code></pre></div><p>Apache 2.4 自己的預設 MPM 是 event（官方 <code>httpd:2.4</code> 映像檔回報 <code>Server MPM: event</code>）。裝上 mod_php，Apache 就要退回 prefork。</p>
<h2 id="每個子程序都帶著-php">每個子程序都帶著 PHP</h2>
<p>prefork 加 mod_php 的組合，讓 PHP 直譯器出現在每一個子程序裡，不論這個子程序正在處理什麼請求。在容器裡列出 Apache 的程序與它們映射的 <code>libphp</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">pid=1  user=root     rss_kb=29112 libphp_mapped=6
</span></span><span class="line"><span class="ln">2</span><span class="cl">pid=16 user=www-data rss_kb=9928  libphp_mapped=6
</span></span><span class="line"><span class="ln">3</span><span class="cl">pid=17 user=www-data rss_kb=10640 libphp_mapped=6
</span></span><span class="line"><span class="ln">4</span><span class="cl">pid=18 user=www-data rss_kb=9928  libphp_mapped=6</span></span></code></pre></div><p>（讀其他程序的 <code>/proc/&lt;pid&gt;/maps</code> 需要 <code>SYS_PTRACE</code> 權限，在 Docker 裡要用 <code>--cap-add SYS_PTRACE</code> 啟動容器才讀得到。）</p>
<p>剛處理過一個靜態檔 <code>hello.txt</code> 請求的子程序，也映射著 <code>libphp</code>。這帶出 mod_php 的三個代價，它們的根源相同：</p>
<p><strong>送靜態檔的程序也背著 PHP。</strong> 一個頁面載入一支 PHP 與數十個圖片、CSS、JS，每個請求都佔用一個帶著 PHP 的子程序。能同時存在的子程序數量受主機記憶體約束，而每個子程序的記憶體裡都有一份 PHP，所以同樣的記憶體能開的子程序比純送靜態檔的 web 伺服器少。</p>
<p><strong>並發數等於子程序數。</strong> prefork 一個連線佔一個子程序，同時能處理的連線數上限就是子程序的上限（<code>MaxRequestWorkers</code>）。網路慢的客戶端、或是在 keep-alive 時間內沒有送下一個請求的連線，都會佔住一個帶著 PHP 的子程序，而它在這段時間什麼都沒做。</p>
<p><strong>所有站台以同一個帳號執行。</strong> 子程序的執行帳號是 Apache 的帳號（上面的 <code>www-data</code>）。同一台伺服器上的每個站台，PHP 都以這個帳號執行，A 站的 PHP 讀得到 B 站的設定檔；PHP 產生的檔案屬於 <code>www-data</code>，站台擁有者用自己的 FTP 帳號刪不掉。共享主機因此長期用 <a href="/blog/programming/php/01-server-runtime/cgi/#cgi-%e5%9c%a8%e5%85%b1%e4%ba%ab%e4%b8%bb%e6%a9%9f%e4%b8%8a%e7%95%99%e4%b8%8b%e4%be%86%e7%9a%84%e7%90%86%e7%94%b1" data-link-title="CGI：每個請求啟動一個 PHP 程序的執行模型" data-link-desc="說明 CGI 怎麼把一個 HTTP 請求交給外部程式：web 伺服器為每個請求啟動一個程序，用環境變數與標準輸入輸出傳遞請求和回應；PHP 以 php-cgi 執行時每個請求付出的啟動成本、OPcache 為什麼沒有作用、以每個站台的使用者身分執行帶來的隔離，以及請求資料與程式呼叫參數之間的邊界問題">CGI</a> 加 suEXEC，而不用 mod_php。例外是第三方的 MPM <code>mpm-itk</code>：它讓每個虛擬主機以自己的帳號執行 mod_php，使用的人較少，Apache 官方發行的 prefork、worker、event 都沒有這個能力。</p>
<h2 id="設定的位置與生效時機">設定的位置與生效時機</h2>
<p>mod_php 的 PHP 跟 Apache 是同一個程序，所以 PHP 的設定跟 Apache 綁在一起：</p>
<ul>
<li><strong><code>php.ini</code> 改了要重新載入 Apache</strong>：設定在子程序啟動時讀入，改了檔案要 <code>apachectl graceful</code> 讓 Apache 換掉子程序才生效。重啟 PHP 就是重啟 Apache。</li>
<li><strong><code>.htaccess</code> 可以改 PHP 設定</strong>：mod_php 在 Apache 裡註冊了 <code>php_value</code>、<code>php_flag</code> 這些指令，站台在自己目錄的 <a href="/blog/devops/infra/knowledge-cards/htaccess/" data-link-title=".htaccess" data-link-desc="Apache Web Server 的目錄層級設定檔，控制 URL rewrite、存取權限、PHP 設定覆寫與安全標頭">.htaccess</a> 裡寫 <code>php_value upload_max_filesize 64M</code> 就能覆寫設定，不必有伺服器的管理權限。這是共享主機時代最常用的調整方式。</li>
</ul>
<h2 id="從-mod_php-換到-php-fpm-時-htaccess-的變化">從 mod_php 換到 PHP-FPM 時 .htaccess 的變化</h2>
<p>PHP 官方文件現在對 Apache 的建議是：新的部署用 PHP-FPM 搭配 Apache 的 <code>mod_proxy_fcgi</code>，這樣 Apache 可以用 event MPM，PHP 也能獨立重啟（見 <a href="/blog/programming/php/01-server-runtime/php-fpm/" data-link-title="PHP-FPM：常駐 PHP worker 的程序管理與主要的調校旋鈕" data-link-desc="說明 PHP-FPM 怎麼管理一群常駐的 PHP worker：pool 怎麼做到每個站台不同執行帳號、pm 的三種開程序策略、max_children 如何決定並發上限與請求排隊、max_requests 定期換掉 worker 控制記憶體、request_terminate_timeout 與 slowlog 怎麼處理跑太久的請求、status 頁怎麼讀，以及每一項對壓測數字的影響">PHP-FPM</a>）。換過去時，<code>.htaccess</code> 裡的 PHP 設定是最常撞到的問題。</p>
<p><code>php_value</code> 這個指令是 mod_php 註冊的，Apache 沒載入 mod_php 時不認得它。在官方的 <code>httpd:2.4</code> 上放一個只有一行 <code>php_value upload_max_filesize 64M</code> 的 <code>.htaccess</code>，連同目錄下的靜態檔 <code>index.html</code> 都回 500，錯誤 log 是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[core:alert] ... /usr/local/apache2/htdocs/.htaccess: Invalid command &#39;php_value&#39;, perhaps misspelled or defined by a module not included in the server configuration</span></span></code></pre></div><p><code>.htaccess</code> 一旦有 Apache 不認得的指令，整份檔案都不能用，所以壞掉的是整個目錄，不只是 PHP 的請求。</p>
<p>PHP-FPM（以及 CGI）下對應的機制是 <code>.user.ini</code>：放在站台目錄裡、語法跟 <code>php.ini</code> 相同的檔案，PHP 官方文件寫明它只由 CGI/FastCGI SAPI 處理（見 <a href="/blog/devops/infra/knowledge-cards/php-ini/" data-link-title="php.ini / .user.ini" data-link-desc="PHP 的執行期設定檔，控制記憶體上限、上傳大小、錯誤報告等 runtime 行為">php.ini / .user.ini</a>）。所以搬家時要把 <code>.htaccess</code> 裡的 <code>php_value</code> 與 <code>php_flag</code> 移到 <code>.user.ini</code>，並刪掉 <code>.htaccess</code> 裡的那幾行。<code>.user.ini</code> 不是每個請求都重讀：PHP 依 <code>user_ini.cache_ttl</code>（預設 300 秒）快取它，改了之後最多要等這段時間才生效。</p>
<h2 id="mod_php-適合的場合">mod_php 適合的場合</h2>
<p>mod_php 的優點是簡單：一個程序、一份設定，Apache 與 PHP 之間沒有任何協定要設定，也沒有第二個服務要監看。單一站台、流量不大、靜態檔由 CDN 或另一台伺服器處理時，它的代價不明顯。</p>
<p>它的代價集中在兩件事上：每個子程序都帶著 PHP，以及 PHP 的壽命與身分綁在 Apache 上。把 PHP 從 web 伺服器的程序裡搬出去、同時保留常駐程序的好處，需要一個讓 web 伺服器與 PHP 程序溝通的協定，那就是 <a href="/blog/programming/php/01-server-runtime/fastcgi/" data-link-title="FastCGI：讓 web 伺服器與常駐的 PHP 程序溝通的協定" data-link-desc="說明 FastCGI 協定怎麼在 CGI 的基礎上去掉每個請求重啟程序這一點：常駐的應用程式程序、web 伺服器與它之間的連線、一個請求在協定裡的樣子，以及 SCRIPT_FILENAME 由 web 伺服器決定會帶來的安全邊界（cgi.fix_pathinfo 與 try_files）">FastCGI</a>。</p>
]]></content:encoded></item><item><title>Apache MPM（Multi-Processing Module）</title><link>https://tarrragon.github.io/blog/devops/infra/knowledge-cards/apache-mpm/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/knowledge-cards/apache-mpm/</guid><description>&lt;p>Apache MPM 決定 Apache 用程序還是執行緒處理並發的連線：prefork 每個連線佔一個子程序、子程序裡沒有多執行緒；worker 每個子程序有多個執行緒，每個連線佔一個執行緒；event 跟 worker 一樣用執行緒，另外把閒置的 keep-alive 連線交給專門的執行緒看管。Apache 2.4 的官方映像檔預設是 event。可先對照 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/nginx/" data-link-title="nginx" data-link-desc="高效能 Web Server 與 Reverse Proxy，以集中設定檔取代 Apache 的 .htaccess 分散設定">nginx&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/cgi/" data-link-title="CGI（Common Gateway Interface）" data-link-desc="接手 cgi-bin 目錄或以 php-cgi 執行的舊站，或在 $_SERVER 看到 REQUEST_METHOD、QUERY_STRING 這類大寫鍵名，要知道它們從哪裡來時">CGI&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>MPM 是 Apache 的並發模型，跟 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/nginx/" data-link-title="nginx" data-link-desc="高效能 Web Server 與 Reverse Proxy，以集中設定檔取代 Apache 的 .htaccess 分散設定">nginx&lt;/a> 的事件驅動模型是同一個問題的不同答案；它也跟 PHP 的執行方式互相限制：&lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/mod-php/" data-link-title="mod_php" data-link-desc="接手 Apache 加 mod_php 的舊站、評估要不要換成 PHP-FPM，或遇到 .htaccess 的 php_value 在新環境回 500 時">mod_php&lt;/a> 把 PHP 放進 Apache 的程序裡，沒有以 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/zts/" data-link-title="ZTS（Zend Thread Safety）" data-link-desc="在 php -i 看到 Thread Safety 是 enabled 或 disabled，或要判斷某個 PHP 能不能跑在多執行緒的伺服器（Apache worker/event MPM、FrankenPHP）裡時">ZTS&lt;/a> 編譯時只能搭配 prefork；PHP 改由 &lt;a href="https://tarrragon.github.io/blog/programming/php/knowledge-cards/php-fpm/" data-link-title="PHP-FPM（FastCGI Process Manager）" data-link-desc="在 Nginx 或容器環境看到 php-fpm、9000 埠、pool 設定或 502 Bad Gateway，要知道 PHP-FPM 負責什麼、哪些設定決定並發與記憶體時">PHP-FPM&lt;/a> 在外面執行（Apache 用 &lt;code>mod_proxy_fcgi&lt;/code> 轉送）時，Apache 就能用 event。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;code>httpd -V&lt;/code> 的 &lt;code>Server MPM&lt;/code>&lt;/strong>：官方 &lt;code>httpd:2.4&lt;/code> 是 &lt;code>event&lt;/code>，&lt;code>php:8.5-apache&lt;/code> 是 &lt;code>prefork&lt;/code>（裝了 mod_php）。&lt;/li>
&lt;li>&lt;strong>&lt;code>apache2ctl -M&lt;/code> 列出 &lt;code>mpm_prefork_module&lt;/code>&lt;/strong>：同時列出 &lt;code>php_module&lt;/code> 時，就是 mod_php 加 prefork 的組合。&lt;/li>
&lt;li>&lt;strong>並發上限等於子程序上限&lt;/strong>：prefork 下 &lt;code>MaxRequestWorkers&lt;/code> 就是能同時處理的連線數，慢客戶端與閒置的 keep-alive 連線都佔著一個子程序。&lt;/li>
&lt;/ul>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>從 mod_php 換到 PHP-FPM 時，可以順便把 MPM 換成 event，讓 Apache 用較少的資源處理大量連線。詳見 &lt;a href="https://tarrragon.github.io/blog/programming/php/01-server-runtime/mod-php/" data-link-title="mod_php：PHP 直譯器嵌進 Apache 程序的執行模型" data-link-desc="說明 mod_php 怎麼把 PHP 直譯器載入 Apache 的每個子程序：為什麼它要搭配 prefork MPM、每個子程序都帶著 PHP 對記憶體與並發的影響、所有站台共用 Apache 的執行帳號、php.ini 與 .htaccess 的設定方式，以及從 mod_php 搬到 PHP-FPM 時 .htaccess 的 php_value 與 .user.ini 的差異">mod_php&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Apache MPM 決定 Apache 用程序還是執行緒處理並發的連線：prefork 每個連線佔一個子程序、子程序裡沒有多執行緒；worker 每個子程序有多個執行緒，每個連線佔一個執行緒；event 跟 worker 一樣用執行緒，另外把閒置的 keep-alive 連線交給專門的執行緒看管。Apache 2.4 的官方映像檔預設是 event。可先對照 <a href="/blog/devops/infra/knowledge-cards/nginx/" data-link-title="nginx" data-link-desc="高效能 Web Server 與 Reverse Proxy，以集中設定檔取代 Apache 的 .htaccess 分散設定">nginx</a> 與 <a href="/blog/devops/infra/knowledge-cards/cgi/" data-link-title="CGI（Common Gateway Interface）" data-link-desc="接手 cgi-bin 目錄或以 php-cgi 執行的舊站，或在 $_SERVER 看到 REQUEST_METHOD、QUERY_STRING 這類大寫鍵名，要知道它們從哪裡來時">CGI</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>MPM 是 Apache 的並發模型，跟 <a href="/blog/devops/infra/knowledge-cards/nginx/" data-link-title="nginx" data-link-desc="高效能 Web Server 與 Reverse Proxy，以集中設定檔取代 Apache 的 .htaccess 分散設定">nginx</a> 的事件驅動模型是同一個問題的不同答案；它也跟 PHP 的執行方式互相限制：<a href="/blog/programming/php/knowledge-cards/mod-php/" data-link-title="mod_php" data-link-desc="接手 Apache 加 mod_php 的舊站、評估要不要換成 PHP-FPM，或遇到 .htaccess 的 php_value 在新環境回 500 時">mod_php</a> 把 PHP 放進 Apache 的程序裡，沒有以 <a href="/blog/programming/php/knowledge-cards/zts/" data-link-title="ZTS（Zend Thread Safety）" data-link-desc="在 php -i 看到 Thread Safety 是 enabled 或 disabled，或要判斷某個 PHP 能不能跑在多執行緒的伺服器（Apache worker/event MPM、FrankenPHP）裡時">ZTS</a> 編譯時只能搭配 prefork；PHP 改由 <a href="/blog/programming/php/knowledge-cards/php-fpm/" data-link-title="PHP-FPM（FastCGI Process Manager）" data-link-desc="在 Nginx 或容器環境看到 php-fpm、9000 埠、pool 設定或 502 Bad Gateway，要知道 PHP-FPM 負責什麼、哪些設定決定並發與記憶體時">PHP-FPM</a> 在外面執行（Apache 用 <code>mod_proxy_fcgi</code> 轉送）時，Apache 就能用 event。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<ul>
<li><strong><code>httpd -V</code> 的 <code>Server MPM</code></strong>：官方 <code>httpd:2.4</code> 是 <code>event</code>，<code>php:8.5-apache</code> 是 <code>prefork</code>（裝了 mod_php）。</li>
<li><strong><code>apache2ctl -M</code> 列出 <code>mpm_prefork_module</code></strong>：同時列出 <code>php_module</code> 時，就是 mod_php 加 prefork 的組合。</li>
<li><strong>並發上限等於子程序上限</strong>：prefork 下 <code>MaxRequestWorkers</code> 就是能同時處理的連線數，慢客戶端與閒置的 keep-alive 連線都佔著一個子程序。</li>
</ul>
<h2 id="設計責任">設計責任</h2>
<p>從 mod_php 換到 PHP-FPM 時，可以順便把 MPM 換成 event，讓 Apache 用較少的資源處理大量連線。詳見 <a href="/blog/programming/php/01-server-runtime/mod-php/" data-link-title="mod_php：PHP 直譯器嵌進 Apache 程序的執行模型" data-link-desc="說明 mod_php 怎麼把 PHP 直譯器載入 Apache 的每個子程序：為什麼它要搭配 prefork MPM、每個子程序都帶著 PHP 對記憶體與並發的影響、所有站台共用 Apache 的執行帳號、php.ini 與 .htaccess 的設定方式，以及從 mod_php 搬到 PHP-FPM 時 .htaccess 的 php_value 與 .user.ini 的差異">mod_php</a>。</p>
]]></content:encoded></item></channel></rss>