將 Azure 監視器中的記錄查詢最佳化

Azure 監視器記錄會使用 Azure 資料總管來儲存記錄資料,並執行查詢來分析該資料。 這會為您建立、管理和維護 Azure Data Explorer 叢集,並針對記錄分析工作負載將其最佳化。 當您執行查詢時,會對查詢進行最佳化,並將其路由傳送至可儲存工作區資料的適當 Azure Data Explorer 叢集。

Azure 監視器記錄和 Azure Data Explorer 會使用許多自動查詢最佳化機制。 自動最佳化可提供大幅提升,但在某些情況下也可以大幅改善查詢效能。 本文說明效能考量和數種修正技術。

大部分的技術都適用於直接在 Azure Data Explorer 和 Azure 監視器記錄上執行的查詢。 我們也將會探討數個獨特的 Azure 監視器記錄考量。 如需更多 Azure Data Explorer 最佳化秘訣,請參閱查詢最佳做法

最佳化查詢將會:

  • 執行更快速,並減少執行查詢的整體持續時間。
  • 有少許的機會可以進行節流或拒絕。

請特別注意用於週期性和模擬使用量的查詢,例如儀表板、警示、Azure Logic Apps 和 Power BI。 在這些情況下,無效查詢的影響很大。

以下是關於查詢最佳化的詳細影片逐步解說。

查詢詳細資料窗格

在 Log Analytics 中執行查詢之後,請選取畫面右下角的 [查詢詳細資料],以開啟 [查詢詳細資料] 窗格。 此窗格顯示查詢的幾個效能指標的結果。 下節將說明這些效能指標。

Screenshot that shows the Query Details pane in Azure Monitor Log Analytics.

查詢效能指標

下列查詢效能指標適用於每個執行的查詢:

  • CPU 總計:用來處理跨所有計算節點的查詢的整體計算。 其代表用於計算、剖析和資料擷取的時間。
  • 用於已處理查詢的資料:已存取以處理查詢的整體資料。 受到目標資料表大小、使用的時間範圍、套用的篩選條件,以及所參考資料行數目的影響。
  • 已處理查詢的時間範圍:已存取以處理查詢的最新資料與最舊資料之間的差距。 受到指定給查詢的明確時間範圍的影響。
  • 已處理資料的存留期:已存取以處理查詢的現在資料與最舊資料之間的差距。 其高度影響資料擷取的效率。
  • 工作區數目:根據隱含或明確選取在查詢處理期間存取的工作區數目。
  • 區域數目:根據含或明確選取工作區在查詢處理期間存取的區域數目。 多區域查詢的效率較低,而效能指標會呈現部分涵蓋範圍。
  • 平行處理原則:指出系統能夠在多個節點上執行此查詢的程度。 僅與具有高 CPU 耗用量的查詢相關。 受到使用特定函數和運算子的影響。

CPU 總計

投資在所有查詢處理節點間以處理此查詢的實際計算 CPU。 由於大部分查詢都是在大量節點上執行,因此這通常會大幅高於執行查詢所花費的持續時間。

使用超過 100 秒 CPU 的查詢會被視為耗用過多資源的查詢。 使用超過 1,000 秒 CPU 的查詢會被視為濫用查詢,而且可能會對其進行節流。

查詢處理時間花費在:

  • 資料擷取:擷取舊資料所耗用的時間將會多於擷取最近資料。
  • 資料處理:資料的邏輯和評估。

除了查詢處理節點所花費的時間之外,Azure 監視器記錄也會在這些項目花費時間:

  • 驗證使用者並確認他們有權存取此資料。
  • 尋找資料存放區。
  • 剖析查詢。
  • 配置查詢處理節點。

查詢總 CPU 時間不包括此時間。

在使用高 CPU 函式之前,先初步篩選記錄

有些查詢命令和函數需要耗用極大的 CPU。 此案利特別適用於剖析 JSON 和 XML 或是擷取複雜規則運算式的命令。 這類剖析可以透過 parse_json()parse_xml() 函式明確地進行,或在其參照動態資料行時隱含地進行。

這些函式耗用的 CPU 與處理的資料列數目成比例。 最有效率的最佳化是在查詢初期新增 where 條件。 如此一來,就可以在執行 CPU 密集函式之前儘量篩選出記錄。

例如,下列查詢將產生完全相同的結果。 不過,第二個查詢最有效率,因為剖析前的 where 條件會排除許多記錄:

//less efficient
SecurityEvent
| extend Details = parse_xml(EventData)
| extend FilePath = tostring(Details.UserData.RuleAndFileData.FilePath)
| extend FileHash = tostring(Details.UserData.RuleAndFileData.FileHash)
| where FileHash != "" and FilePath !startswith "%SYSTEM32"  // Problem: irrelevant results are filtered after all processing and parsing is done
| summarize count() by FileHash, FilePath
//more efficient
SecurityEvent
| where EventID == 8002 //Only this event have FileHash
| where EventData !has "%SYSTEM32" //Early removal of unwanted records
| extend Details = parse_xml(EventData)
| extend FilePath = tostring(Details.UserData.RuleAndFileData.FilePath)
| extend FileHash = tostring(Details.UserData.RuleAndFileData.FileHash)
| where FileHash != "" and FilePath !startswith "%SYSTEM32"  // exact removal of results. Early filter is not accurate enough
| summarize count() by FileHash, FilePath
| where FileHash != "" // No need to filter out %SYSTEM32 here as it was removed before

避免使用評估的 where 子句

包含評估資料行 (而非資料集中實際存在的資料行) 上 where 子句的查詢都會遺失效率。 針對評估的資料行進行篩選,可防止在處理大型資料集時執行一些系統最佳化。

例如,下列查詢將產生完全相同的結果。 不過,第二個查詢較有效率,因為 where 條件會參照內建資料行:

//less efficient
Syslog
| extend Msg = strcat("Syslog: ",SyslogMessage)
| where  Msg  has "Error"
| count 
//more efficient
Syslog
| where  SyslogMessage  has "Error"
| count 

在某些情況下,查詢處理引擎會隱含地建立評估的資料行,因為篩選不只是針對欄位:

//less efficient
SecurityEvent
| where tolower(Process) == "conhost.exe"
| count 
//more efficient
SecurityEvent
| where Process =~ "conhost.exe"
| count 

在 summarize 和 join 中使用有效的彙總命令和維度

某些彙總命令,例如 max()sum()count()avg() 由於其背後運作邏輯,對 CPU 影響較低。 其他命令比較複雜並包含啟發學習法和預估,以便有效率地執行。 例如,dcount() 使用 HyperLogLog 演算法,在不實際計算每個值的情況下提供大型資料集相異計數的貼近真實情況的預估。

百分位數函式會使用最接近的排名百分位數演算法,來執行類似的近似值。 數個命令包括可減少其影響的選用參數。 例如,makeset() 函數具有選用參數可定義集合大小上限,而這會大幅影響 CPU 和記憶體。

joinsummarize 命令在處理大量資料時可能會造成高 CPU 使用率。 其複雜度與資料行的可能值數目 (稱為「基數」) 直接相關,而資料行在 summarize 中用作 by,或是用作 join 屬性。 如需 joinsummarize 的說明和最佳化,請參閱其文件文章和最佳化秘訣。

例如,下列查詢會產生完全相同的結果,因為 CounterPath 一律會一對一地對應至 CounterNameObjectName。 第二個更有效率,因為彙總維度較小:

//less efficient
Perf
| summarize avg(CounterValue) 
by CounterName, CounterPath, ObjectName
//make the group expression more compact improve the performance
Perf
| summarize avg(CounterValue), any(CounterName), any(ObjectName) 
by CounterPath

CPU 耗用量也可能受到需要大量計算的 where 條件或擴充資料行的影響。 所有簡單的字串比較 (例如 equal ==startswith) 對 CPU 的影響大致相同。 進階文字比對的影響較大。 具體而言,has 運算子比 contains 運算子更有效率。 由於字串處理技術的緣故,尋找長度超過四個字元的字串比短字串更有效率。

例如,根據 Computer 命名原則,下列查詢會產生類似的結果。 不過,第二個查詢會更有效率:

//less efficient – due to filter based on contains
Heartbeat
| where Computer contains "Production" 
| summarize count() by ComputerIP 
//less efficient – due to filter based on extend
Heartbeat
| extend MyComputer = Computer
| where MyComputer startswith "Production" 
| summarize count() by ComputerIP 
//more efficient
Heartbeat
| where Computer startswith "Production" 
| summarize count() by ComputerIP 

注意

此指標只會呈現來自立即叢集的 CPU。 在多區域查詢中,這只代表其中一個區域。 在多工作區查詢中,這可能未包括所有工作區。

避免在字串剖析運作時進行完整 XML 和 JSON 剖析

XML 或 JSON 物件的完整剖析可能會耗用高 CPU 和記憶體資源。 在許多情況下,如果只需要一兩個參數且 XML 或 JSON 物件很簡單,則將其剖析為字串會更輕鬆。 使用剖析運算子或其他文字剖析技術。 效能提升將會隨著 XML 或 JSON 物件中的記錄數目增加而更為明顯。 記錄數目達到數千萬時,此項就非常重要。

例如,下列查詢不執行完整的 XML 剖析,但傳回的結果與上述查詢完全相同。 請注意,此查詢對 XML 檔案結構做一些假設,例如 FilePath 元素在 FileHash 後面,而且兩者都沒有屬性:

//even more efficient
SecurityEvent
| where EventID == 8002 //Only this event have FileHash
| where EventData !has "%SYSTEM32" //Early removal of unwanted records
| parse EventData with * "<FilePath>" FilePath "</FilePath>" * "<FileHash>" FileHash "</FileHash>" *
| summarize count() by FileHash, FilePath
| where FileHash != "" // No need to filter out %SYSTEM32 here as it was removed before

用於已處理查詢的資料

查詢處理的重要因素是掃描和用於查詢處理的資料量。 相較於其他資料平台,Azure Data Explorer 會使用積極最佳化以大幅減少資料量。 不過,查詢中有些重要因素可能會影響所使用的資料量。

處理超過 2,000 KB 資料的查詢會被視為耗用過多資源的查詢。 處理超過 20,000 KB 資料的查詢會被視為濫用查詢,而且可能會對其進行節流。

在 Azure 監視器記錄中,TimeGenerated 資料行將用作編制資料索引的方法。 盡量縮小限制 TimeGenerated 值的範圍,將可改善查詢效能。 縮小範圍會大幅限制必須處理的資料量。

避免使用不必要的 search 和 union 運算子

另一個增加所處理資料的因素是使用大量的資料表。 此案例通常是在使用 search *union * 命令時發生。 這些命令會強制系統評估和掃描工作區中所有資料表的資料。 在某些情況下,工作區中可能有數百個數據表。 請避免使用 search * 或任何不將範圍限定在特定資料表中的搜尋。

例如,下列查詢會產生完全相同的結果,但最後一個查詢最有效率:

// This version scans all tables though only Perf has this kind of data
search "Processor Time" 
| summarize count(), avg(CounterValue)  by Computer
// This version scans all strings in Perf tables – much more efficient
Perf
| search "Processor Time" 
| summarize count(), avg(CounterValue)  by Computer
// This is the most efficient version 
Perf 
| where CounterName == "% Processor Time"  
| summarize count(), avg(CounterValue)  by Computer

將早期篩選新增至查詢

另一個減少資料量的方法是在查詢早期出現 where 條件。 Azure Data Explorer 平台包括一個快取,可讓查詢知道哪些分割區包括特定 where 條件的相關資料。 例如,如果查詢包含 where EventID == 4624,則只會將查詢散發至節點,而節點可處理具有相符事件的分割區。

下列範例查詢會產生完全相同的結果,但第二個查詢更有效率:

//less efficient
SecurityEvent
| summarize LoginSessions = dcount(LogonGuid) by Account
//more efficient
SecurityEvent
| where EventID == 4624 //Logon GUID is relevant only for logon event
| summarize LoginSessions = dcount(LogonGuid) by Account

避免使用條件式彙總函式和 materialize 函式多次掃描相同的來源資料

當查詢有幾個子查詢透過 join 或 union 運算子合併時,每個子查詢都會個別掃描整個來源。 然後其會合併結果。 此動作會讓資料掃描次數加倍,這是大型資料集的重要因素。

避免這種情況的技巧,就是使用條件式彙總函式。 summarize 運算子中使用的大部分彙總函式都有條件式版本,可讓您使用單一 summarize 運算子搭配多個條件。

例如,下列查詢會顯示每個帳戶的登入事件數目和程序執行事件數目。 其會傳回相同的結果,但第一個查詢會掃描資料兩次。 第二個查詢只會掃描一次:

//Scans the SecurityEvent table twice and perform expensive join
SecurityEvent
| where EventID == 4624 //Login event
| summarize LoginCount = count() by Account
| join 
(
    SecurityEvent
    | where EventID == 4688 //Process execution event
    | summarize ExecutionCount = count(), ExecutedProcesses = make_set(Process) by Account
) on Account
//Scan only once with no join
SecurityEvent
| where EventID == 4624 or EventID == 4688 //early filter
| summarize LoginCount = countif(EventID == 4624), ExecutionCount = countif(EventID == 4688), ExecutedProcesses = make_set_if(Process,EventID == 4688)  by Account

另一個不需要子查詢的情況是預先篩選 parse 運算子,以確保只處理符合特定模式的記錄。 這並非必要動作,因為 parse 運算子和其他類似的運算子會在模式不相符時傳回空白結果。 下列兩個查詢會傳回完全相同的結果,而第二個查詢只會掃描資料一次。 在第二個查詢中,只有其事件的每個剖析命令才會相關。 extend 運算子之後會顯示如何參照空白資料情況:

//Scan SecurityEvent table twice
union(
SecurityEvent
| where EventID == 8002 
| parse EventData with * "<FilePath>" FilePath "</FilePath>" * "<FileHash>" FileHash "</FileHash>" *
| distinct FilePath
),(
SecurityEvent
| where EventID == 4799
| parse EventData with * "CallerProcessName\">" CallerProcessName1 "</Data>" * 
| distinct CallerProcessName1
)
//Single scan of the SecurityEvent table
SecurityEvent
| where EventID == 8002 or EventID == 4799
| parse EventData with * "<FilePath>" FilePath "</FilePath>" * "<FileHash>" FileHash "</FileHash>" * //Relevant only for event 8002
| parse EventData with * "CallerProcessName\">" CallerProcessName1 "</Data>" *  //Relevant only for event 4799
| extend FilePath = iif(isempty(CallerProcessName1),FilePath,"")
| distinct FilePath, CallerProcessName1

當上述查詢不可避免一定要使用子查詢時,另一個技巧是使用 materialize() 函式來向查詢引擎提示,每個子查詢中只使用單一來源資料。 來源資料來自查詢內多次使用的函式時,此技巧十分有用。 子查詢的輸出遠小於輸入時,Materialize 就十分有效。 查詢引擎將會快取並重複使用所有出現項目中的輸出。

減少所擷取的資料行數目

由於 Azure Data Explorer 是單欄式資料存放區,因此每個資料行的擷取與彼此無關。 所擷取的資料行數目會直接影響整體資料量。 您應該只透過總結結果或投影特定資料行,以在輸出中包括所需的資料行。

Azure Data Explorer 有數項最佳化,可減少所擷取資料行數目。 如果判斷不需要資料行 (例如,如果未在 summarize 命令中參考資料行),則不會擷取資料行。

例如,第二個查詢可能會處理三倍以上的資料,因為不是需要擷取一個資料行,而是擷取三個資料行:

//Less columns --> Less data
SecurityEvent
| summarize count() by Computer  
//More columns --> More data
SecurityEvent
| summarize count(), dcount(EventID), avg(Level) by Computer  

已處理查詢的時間範圍

Azure 監視器記錄中的所有記錄都會根據 TimeGenerated 資料行進行分割。 所存取的分割區數目與時間範圍直接相關。 減少時間範圍是確保提示查詢執行的最有效率方式。

時間範圍超過 15 天的查詢會被視為耗用過多資源的查詢。 時間範圍超過 90 天的查詢會被視為濫用查詢,而且可能會對其進行節流。

您可以使用 Log Analytics 畫面中的時間範圍選取器來設定時間範圍,如 Azure 監視器 Log Analytics 中的記錄查詢範圍和時間範圍中所述。 這是建議的方法,因為選取的時間範圍是使用查詢中繼資料傳遞至後端。

替代方法是在查詢的 TimeGenerated 上明確包括 where 條件。 請使用此方法,因為這可確保時間範圍固定,即使從不同的介面使用查詢也一樣。

請確定查詢的所有部分都有 TimeGenerated 篩選條件。 當查詢有子查詢從各種資料表或相同資料表擷取資料時,每個子查詢都必須包含自己的 where 條件。

請確定所有子查詢都有 TimeGenerated 篩選條件

例如,在下列查詢中,Perf 資料表只會掃描最後一天。 Heartbeat 資料表會掃描其所有歷程記錄,最多兩年:

Perf
| where TimeGenerated > ago(1d)
| summarize avg(CounterValue) by Computer, CounterName
| join kind=leftouter (
    Heartbeat
    //No time span filter in this part of the query
    | summarize IPs = makeset(ComputerIP, 10) by  Computer
) on Computer

發生這類錯誤的常見案例是使用 arg_max() 來尋找最近的發生次數時。 例如:

Perf
| where TimeGenerated > ago(1d)
| summarize avg(CounterValue) by Computer, CounterName
| join kind=leftouter (
    Heartbeat
    //No time span filter in this part of the query
    | summarize arg_max(TimeGenerated, *), min(TimeGenerated)   
by Computer
) on Computer

在內部查詢中新增時間篩選條件,您即可輕鬆修正此情況:

Perf
| where TimeGenerated > ago(1d)
| summarize avg(CounterValue) by Computer, CounterName
| join kind=leftouter (
    Heartbeat
    | where TimeGenerated > ago(1d) //filter for this part
    | summarize arg_max(TimeGenerated, *), min(TimeGenerated)   
by Computer
) on Computer

這個錯誤的另一個範例是在對數個資料表執行 union 之後,立即執行時間範圍篩選時。 您在執行聯合時,應該限定每個子查詢的範圍。 您可以使用 let 陳述式來確保一致性範圍。

例如,下列查詢將會掃描 HeartbeatPerf 資料表中的所有資料,而不只是最後一天:

Heartbeat 
| summarize arg_min(TimeGenerated,*) by Computer
| union (
    Perf 
    | summarize arg_min(TimeGenerated,*) by Computer) 
| where TimeGenerated > ago(1d)
| summarize min(TimeGenerated) by Computer

若要修正查詢:

let MinTime = ago(1d);
Heartbeat 
| where TimeGenerated > MinTime
| summarize arg_min(TimeGenerated,*) by Computer
| union (
    Perf 
    | where TimeGenerated > MinTime
    | summarize arg_min(TimeGenerated,*) by Computer) 
| summarize min(TimeGenerated) by Computer

時間範圍度量限制

度量一律會大於指定的實際時間。 例如,如果查詢上的篩選是 7 天,則系統可能會掃描 7.5 或 8.1 天。 此變異數是因為系統將資料分割成可變大小的區塊。 為了確保掃描所有相關記錄,系統會掃描整個分割區。 此流程可能會涵蓋數小時甚至超過一天。

在一些情況下,系統無法準確測量時間範圍。 在查詢範圍少於一天或在多工作區查詢的大部分情況下,就會發生這種情況。

重要

此指標只會顯示立即叢集中所處理的資料。 在多區域查詢中,這只代表其中一個區域。 在多工作區查詢中,這可能未包括所有工作區。

已處理資料的存留期

Azure Data Explorer 使用數個儲存層:記憶體內部、本機 SSD 磁碟和較慢的 Azure Blob。 資料越新,就越有可能儲存在較高效能且延遲較短的層,從而降低查詢持續時間和 CPU。 除了資料本身之外,系統也有中繼資料快取。 資料越舊,其中繼資料在快取中的機會就越少。

如果查詢所處理的資料超過 14 天,則視為耗用過多資源。

雖然有些查詢需要使用舊資料,但某些情況下,則是誤用到舊資料。 如果執行查詢但未在其中繼資料內提供時間範圍,而且並非所有資料表參考都包括對 TimeGenerated 資料行的篩選,則會發生這種情況。 在這些情況下,系統將會掃描該資料表中所儲存的所有資料。 當資料保留期很長時,其可以涵蓋很長的時間範圍。 因此,系統會掃描與資料保留期間一樣舊的資料。

例如,這類情況可以是:

  • 對於沒有受限的子查詢,未在 Log Analytics 中設定時間範圍。 請查看上述範例。
  • 使用沒有時間範圍選用參數的 API。
  • 使用不會強制時間範圍的用戶端,例如 Power BI 連接器。

請參閱上一節的範例和注意事項,因為這些也與此案例有關。

區域數目

在一些情況下,可能會跨不同的區域執行單一查詢。 例如:

  • 明確列出幾個位於不同區域的工作區時。
  • 如果資源範圍查詢正在擷取資料,而且資料儲存至位於不同區域的多個工作區。

跨區域查詢執行需要系統在後端大型中繼資料區塊中序列化和傳輸,而這些區塊通常會略大於查詢最終結果。 這也會限制系統執行最佳化、啟發學習法和利用快取的能力。

如果沒有理由來掃描所有這些區域,則應該將範圍調整為涵蓋較少區域。 如果將資源範圍最小化,但仍使用許多區域,則可能是設定錯誤所造成。 例如,稽核記錄和診斷設定可能會傳送至不同區域中的不同工作區,或可能有多個診斷設定組態。

跨越超過 3 個區域的查詢會被視為耗用過多資源的查詢。 跨越超過 6 個區域的查詢會被視為濫用查詢,而且可能會對其進行節流。

重要

當查詢跨數個區域執行時,CPU 和資料量值不會精確,而且只會代表其中一個區域的量值。

工作區數目

工作區是用來隔離和管理記錄資料的邏輯容器。 後端會最佳化所選取區域內實體叢集上的工作區位置。

執行個體在以下情況會使用多個工作區:

  • 已明確列出數個工作區。
  • 資源範圍查詢正在擷取資料,而該資料存放於多個工作區中。

跨區域和跨叢集執行查詢需要系統在後端大型中繼資料區塊中序列化和傳輸,而這些區塊通常會略大於查詢最終結果。 這也會限制系統執行最佳化、啟發學習法和利用快取的能力。

跨越超過 5 個工作區的查詢會被視為耗用過多資源的查詢。 查詢不能跨越 100 個以上的工作區。

重要

  • 在某些多工作區案例中,CPU 和資料量值將不會精確,而且只代表幾個工作區的量值。
  • 具有明確標識碼的跨工作區查詢:工作區識別碼 (或工作區 Azure 資源識別碼) 耗用較少資源且效能更高。

平行處理原則

Azure 監視器記錄會使用 Azure Data Explorer 的大型叢集來執行查詢。 這些叢集會隨著規模而有所不同,而且可能會達到數十個計算節點。 系統會根據工作區放置邏輯和容量來自動調整叢集。

為了有效率地執行查詢,系統會根據處理查詢所需的資料,將查詢分割並散發至計算節點。 在某些情況下,系統無法有效率地執行此步驟,這可能會導致查詢的持續時間較長。

可減少平行處理原則的查詢行為包括:

  • 使用序列化和視窗函式,例如 serialize 運算子next()prev()row 函式。 時間序列和使用者分析函數可以用於其中一些案例。 如果未在查詢結尾使用下列運算子,則也可能會發生無效率的序列化:rangesortordertoptop-hittersgetschema
  • 使用 dcount() 彙總函式可強制系統具有相異值的中央複本。 資料規模很高時,請考慮使用 dcount 函式選用參數來降低精確度。
  • 在許多情況下,join 運算子會降低整體平行處理原則。 效能發生問題時,請檢查 shuffle join 以作為替代方案。
  • 在資源範圍的查詢中,如果有很大量的 Azure 角色指派,則可能會停留在執行前的 Kubernetes 角色型存取控制 (RBAC) 或 Azure RBAC 檢查。 此情況可能會導致檢查時間較長,進而造成較低的平行處理原則。 例如,查詢可能會在訂用帳戶上執行,其中有數千個資源,而且每個資源的資源層級中都有許多角色指派,而不是在訂用帳戶或資源群組上。
  • 如果查詢處理少量的資料區塊,則平行性會很低,因為系統不會將查詢分散到許多計算節點。

下一步

Kusto 查詢語言的參考文件