網路效能突發問題說來就來,遇上了往往只能透過事件檢視器或 IIS Log 還原現場,設法拼湊真相推敲出事發原因。

既然跟效能有關,了解不同時間點的請求執行時間變化就顯得格外重要,用耗時變化還原出時間軸常是破案的關鍵,而正確理解及利用 IIS Log 時間與耗時欄位是重要的第一步。

IIS 預設採用 W3C Log 格式

W3C 格式每筆記錄可包含以下欄位(* 表 IIS 預設啟用):參考資料

FieldAppears AsDescription
Date*date活動發生日期
Time*time活動發生時間,使用 UTC 時間
Service Name and Instance Numbers-sitename用戶端運行的服務名稱和實例編號
Server Names-computername伺服器名稱
Server IP Address*s-ip伺服器IP地址
Method*cs-method請求動詞,例如GET/POST
URI Stem*cs-uri-stem動詞目標,例如 index.html
URI Query*cs-uri-query用戶端嘗試執行的查詢
Server Port*s-port伺服器埠號
User Name*cs-username訪問伺服器的已驗證用戶名,匿名用戶以連字號表示
Client IP Address*c-ip發出請求的用戶端 IP 地址
Protocol Versioncs-version用戶端使用的 HTTP 協議版本
User Agent*cs(User-Agent)用戶端使用的瀏覽器類型
Cookiecs(Cookie)發送或接收的 Cookie 內容(如果有)
Referrer*cs(Referrer)使用者最後訪問的網站,使用者由該網站提供的連結連過來
Hostcs-host主機標頭名稱(如果有)
HTTP Status*sc-statusHTTP 狀態碼
Protocol Substatus*sc-substatus子狀態錯誤碼
Win32 Status*sc-win32-statusWindows 狀態碼
Bytes Sentsc-bytes伺服器發送的位元組數
Bytes Receivedcs-bytes伺服器接收並處理的位元組數
Time Taken*time-taken執行操作所花費的時間,單位為毫秒
Stream IDstreamidStream ID

分析效能問題時,我們最關心的欄位通常是 time(時間)、cs-uri-stem(呼叫哪一頁)、sc-status(200/500 成功或失敗)、time-taken(花了多少時間)。

而新手解讀網站 Log 常犯的一個錯誤是搞不清楚 time 的意義,誤把它當成請求發送時間進行推理,搞錯狀況下錯判斷。(我小時候就因此鬼打牆過...)

用一個簡單實驗來驗證。以下的 ASP.NET 網頁會先發出一個 ?m=lock 請求使用 C# lock 機制阻擋其他 ?m=ping 請求五秒鐘,接著用 setTimeout() 每隔一秒發一個 ?m=ping 共四次,這四次請求會被 ?m=lock 卡住,最後再一起傳回應:

<%@Page Language="C#"%>
<script runat="server">
    static object syncLock = new Object();
    void Page_Load(object sender, EventArgs e)
    {
        var mode = Request["m"];
        if (!string.IsNullOrEmpty(mode))
        {
            var start = DateTime.Now;
            switch (mode)
            {
                case "lock":
                    lock (syncLock)
                    {
                        System.Threading.Thread.Sleep(5000);
                    }
                    break;
                case "ping":
                    lock (syncLock) 
                    {
                        System.Threading.Thread.Sleep(1);
                    }
                    break;
                    
            }
            Response.Write(string.Format("{0:mm:ss.fff} ~ {1:mm:ss.fff} {2}", start, DateTime.Now, Request.Url.Query));
            Response.End();
        }
    }
</script>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <style>
            html,body {
                font-family: 'Courier New', Courier, monospace;
            }
        </style>
    </head>
    <body>
        <ul id="msgs">

        </ul>
        <script>
            function addMsg(msg) {
                var ul = document.getElementById("msgs");
                var li = document.createElement("li");
                li.innerText = msg;
                ul.appendChild(li);
            }
            function ajax(url) {
                fetch(url).then(res => res.text()).then(text => addMsg(text));
            }
            ajax("?m=lock");
            for (var i = 0; i < 4; i++) {
                const delay = i * 1000 + 1000;
                const url = "?m=ping&n=" + i;
                setTimeout(() => ajax(url), delay);
            }
        </script>
    </body>
</html>

使用瀏覽器 F12 網頁分析工具觀察 fetch 請求發送結果如下,由最右方的時間軸可以看出 ?m=lock 及四個 ?m=ping 各自間隔一秒先後發出,最後一起獲得回應同時結束。

用以上結果對照 IIS Log,可清楚地看出「time 是結束時間」的事實,五個 /aspnet/iislogtime 請求的 time 都是 35'11",表示 IIS 在同一秒執行完五個請求寫入 Log。而最後一欄的 time-taken 則是每個請求的執行時間,嚴格來說是 IIS 從收到第一個 Byte 到送完回應為止花費的時間,在 IIS 7.0+ 它還會包含網路傳輸時間,延伸客戶端回覆收到(Acknowlege)或重設網路連線為止 參考)。

換言之,如果你想觀察某個時間起處理請求的時間是否愈來愈慢,用 time - time-taken 推算接收到請求的時間(我喜歡叫它 cs-time),用 cs-time 排序再做分析較能看出趨勢。

要做到這點不難,若 Log 檔不大,用幾行 C# 程式即可實現,如以下範例:

using System.Globalization;

if (args.Length == 0)
{
    Console.WriteLine("syntax: calc-cstime <IIS log path>");
    return;
}
string logPath = args[0];
if (!File.Exists(logPath))
{
    Console.WriteLine($"File not found: {logPath}");
    return;
}
foreach (var line in process(logPath).OrderBy(o => o)) Console.WriteLine(line);
static IEnumerable<string> process(string logPath)
{
    using var reader = new StreamReader(logPath);
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        if (line.StartsWith("#Fields: date time "))
        {
            var fldNames = line.Split(' ');
            if (fldNames[fldNames.Length - 1] != "time-taken")
            {
                Console.WriteLine("Error: 'time-taken' must be the last field");
                break;
            }
            line = line.Replace("#Fields: date time ", "#Fields: date cs-time time ");
        }
        else if (!line.StartsWith("#"))
        {
            var p = line.Split(' ');
            var timeTaken = int.Parse(p[p.Length - 1]);
            var csTime = DateTime.ParseExact($"{p[0]} {p[1]}", "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).AddMilliseconds(-timeTaken);
            var newArray = new[] { p[0], csTime.ToString("HH:mm:ss.fff") }
                .Concat(p[1..])
                .ToArray();
            line = string.Join(' ', newArray);
        }
        yield return line;
    }
}

這段程式會讀入 Log 檔,插入 cs-time 欄位並依 cs-time 重新排序(程式未完整防呆及對考慮處理超大 Log 檔的效能優化),轉換後的 Log 更容易依據請求先後順序推敲因果關係並觀察執行時間的變化。

Understanding IIS logs is crucial for diagnosing performance issues. Key fields include time, cs-uri-stem, sc-status, and time-taken. The time field indicates when a request finishes, not when it starts. Analyzing cs-time (calculated as time - time-taken) helps trace request processing trends.


Comments

# by ChrisTorng

可惜原始紀錄的時間只精確到秒,沒有小數位,因此轉換後時間誤差最大近一秒,排序後還是有亂序的極大可能。

# by yoyo

應該導入現代化的Log分析工具 常見的有ELK Stack, Grafana 系列

Post a comment