使用服務器計時測量性能
已發表: 2022-03-10在進行任何類型的性能優化工作時,我們學到的第一件事就是,在您提高性能之前,您必須首先對其進行測量。 如果無法衡量某項工作的速度,我們就無法判斷所做的更改是否正在提高性能、沒有效果,或者甚至使事情變得更糟。
我們中的許多人都熟悉在某種程度上處理性能問題。 這可能就像試圖弄清楚為什麼頁面上的 JavaScript 沒有足夠快地啟動,或者為什麼圖像在糟糕的酒店 wifi 上顯示時間過長一樣簡單。 這類問題的答案通常可以在一個非常熟悉的地方找到:瀏覽器的開發人員工具。
多年來,開發人員工具已得到改進,可幫助我們解決應用程序前端的此類性能問題。 瀏覽器現在甚至內置了性能審計。這可以幫助追踪前端問題,但是這些審計可以顯示另一個我們無法在瀏覽器中修復的緩慢來源。 這個問題是服務器響應時間慢。
“第一個字節的時間”
幾乎沒有瀏覽器優化可以改善在服務器上構建速度很慢的頁面。 該成本是在瀏覽器發出文件請求和接收響應之間產生的。 在開發人員工具中研究您的網絡瀑布圖將在“等待(TTFB)”類別下顯示此延遲。 這是瀏覽器在發出請求和接收響應之間等待的時間。
在性能方面,這被稱為Time to First Byte - 服務器開始發送瀏覽器可以開始使用的內容之前所花費的時間。 包含在等待時間中的是服務器構建頁面所需的一切。 對於典型的站點,這可能涉及將請求路由到應用程序的正確部分、驗證請求、多次調用後端系統(如數據庫等)。 它可能涉及通過模板系統運行內容,向第三方服務發出 API 調用,甚至可能涉及發送電子郵件或調整圖像大小等事情。 服務器為完成請求所做的任何工作都被壓縮到用戶在瀏覽器中體驗的 TTFB 等待中。
那麼我們如何減少時間並開始更快地將頁面交付給用戶呢? 嗯,這是一個大問題,答案取決於您的應用程序。 那就是性能優化本身的工作。 我們首先需要做的是衡量性能,以便可以判斷任何更改的好處。
服務器計時標頭
服務器計時的工作不是幫助您對服務器上的活動進行實際計時。 您需要使用後端平台提供給您的任何工具集自己進行計時。 相反,服務器計時的目的是指定如何將這些測量值傳達給瀏覽器。
這樣做的方式非常簡單,對用戶透明,並且對您的頁面重量的影響最小。 該信息作為一組簡單的 HTTP 響應標頭髮送。
Server-Timing: db;dur=123, tmpl;dur=56
此示例傳達名為db
和tmpl
的兩個不同時間點。 這些不是規範的一部分——這些是我們選擇的名稱,在這種情況下分別代表一些數據庫和模板時間。
dur
屬性表示操作完成所需的毫秒數。 如果我們查看開發人員工具的網絡部分中的請求,我們可以看到時間已添加到圖表中。
Server-Timing
標頭可以採用逗號分隔的多個指標:
Server-Timing: metric, metric, metric
每個指標可以指定三個可能的屬性
- 指標的短名稱(例如我們示例中的
db
) - 以毫秒為單位的持續時間(表示為
dur=123
) - 描述(表示為
desc="My Description"
)
每個屬性都用分號作為分隔符分隔。 我們可以像這樣向我們的示例添加描述:
Server-Timing: db;dur=123;desc="Database", tmpl;dur=56;desc="Template processing"
唯一需要的屬性是name
。 dur
和desc
都是可選的,可以在需要的地方隨意使用。 例如,如果您需要調試發生在一台服務器或數據中心而不是另一台服務器或數據中心上的計時問題,那麼在沒有相關計時的情況下將該信息添加到響應中可能會很有用。
Server-Timing: datacenter;desc="East coast data center", db;dur=123;desc="Database", tmpl;dur=56;desc="Template processing”
然後,這將與時間一起顯示。
您可能會注意到的一件事是時間條不會以瀑布模式顯示。 這僅僅是因為 Server Timing 不會嘗試傳達時序序列,而只是傳達原始指標本身。
實現服務器計時
您自己的應用程序中的確切實現將取決於您的具體情況,但原則是相同的。 步驟總是:
- 時間一些操作
- 收集計時結果
- 輸出 HTTP 標頭
在偽代碼中,響應的生成可能如下所示:
startTimer('db') getInfoFromDatabase() stopTimer('db') startTimer('geo') geolocatePostalAddressWithAPI('10 Downing Street, London, UK') endTimer('geo') outputHeader('Server-Timing', getTimerOutput())
在任何語言中,按照這些思路實現某些東西的基礎都應該是直截了當的。 一個非常簡單的 PHP 實現可以使用microtime()
函數進行計時操作,並且可能看起來類似於以下內容。
class Timers { private $timers = []; public function startTimer($name, $description = null) { $this->timers[$name] = [ 'start' => microtime(true), 'desc' => $description, ]; } public function endTimer($name) { $this->timers[$name]['end'] = microtime(true); } public function getTimers() { $metrics = []; if (count($this->timers)) { foreach($this->timers as $name => $timer) { $timeTaken = ($timer['end'] - $timer['start']) * 1000; $output = sprintf('%s;dur=%f', $name, $timeTaken); if ($timer['desc'] != null) { $output .= sprintf(';desc="%s"', addslashes($timer['desc'])); } $metrics[] = $output; } } return implode($metrics, ', '); } }
測試腳本將使用它如下,這裡使用usleep()
函數人為地在腳本運行中創建延遲,以模擬需要時間才能完成的過程。
$Timers = new Timers(); $Timers->startTimer('db'); usleep('200000'); $Timers->endTimer('db'); $Timers->startTimer('tpl', 'Templating'); usleep('300000'); $Timers->endTimer('tpl'); $Timers->startTimer('geo', 'Geocoding'); usleep('400000'); $Timers->endTimer('geo'); header('Server-Timing: '.$Timers->getTimers());
運行此代碼會生成一個如下所示的標頭:
Server-Timing: db;dur=201.098919, tpl;dur=301.271915;desc="Templating", geo;dur=404.520988;desc="Geocoding"
現有實現
考慮到 Server Timing 的方便程度,我能找到的實現相對較少。 server-timing NPM 包提供了一種在 Node 項目中使用 Server Timing 的便捷方式。
如果您使用基於中間件的 PHP 框架 tuupola/server-timing-middleware 也提供了一個方便的選項。 幾個月來我一直在 Notist 的生產環境中使用它,如果你想在野外看到一個例子,我總是會啟用一些基本的時間安排。
對於瀏覽器支持,我見過的最好的是 Chrome DevTools,這就是我在本文的屏幕截圖中使用的。
注意事項
Server Timing 本身為通過網絡發送回的 HTTP 響應增加了非常小的開銷。 標頭非常小,通常可以安全地發送,而不必擔心僅針對內部用戶。 即便如此,保持名稱和描述簡短還是值得的,這樣您就不會增加不必要的開銷。
更令人擔憂的是您可能在服務器上為您的頁面或應用程序計時所做的額外工作。 添加額外的計時和日誌記錄本身會對性能產生影響,因此值得實現一種在需要時打開和關閉它的方法。
使用服務器計時標頭是確保應用程序前端和後端的所有計時信息都可在一個位置訪問的好方法。 如果您的應用程序不太複雜,那麼它可以很容易實現,並且您可以在很短的時間內啟動並運行。
如果您想了解更多關於服務器時序的信息,您可以嘗試以下方法:
- W3C 服務器計時規範
- Server Timing 上的 MDN 頁麵包含瀏覽器支持的示例和最新詳細信息
- 來自 BBC iPlayer 團隊的一篇關於他們使用服務器計時的有趣文章。