跳轉到內容

XQuery/正常執行時間監控

來自華夏公益教科書,開放書籍,開放世界

您希望監控多個網站或網路服務的服務可用性。您希望使用 XQuery 完成所有這些操作並將結果儲存在 XML 檔案中。您還希望看到正常執行時間的“儀表板”圖形顯示。

有幾種商業服務(PingdomHost-tracker),它們會根據正常執行時間和響應時間監控您的網站的效能。

儘管可靠服務的生產需要一個伺服器網路,但基本功能可以使用 XQuery 在幾個指令碼中執行。

這種方法側重於網頁的正常執行時間和響應時間。核心方法是使用 eXist 作業排程程式定期執行 XQuery 指令碼。此指令碼對 URI 執行 HTTP GET 並將網站的 statusCode 記錄到 XML 資料檔案中。

操作的計時是為了從經過的時間(在使用率低的伺服器上有效)收集響應時間,並存儲測試結果。然後可以從測試結果執行報告,並在觀察到網站關閉時傳送警報。

即使是一個原型,對細粒度資料的訪問已經揭示了大學中某個網站的一些響應時間問題。

觀察列表

概念模型

[編輯 | 編輯原始碼]

此 ER 模型是在 QSEE 中建立的,QSEE 還可以生成 SQL 或 XSD。

在此表示法中,條形表示 Test 是一個弱實體,存在依賴於 Watch。

將 ER 模型對映到模式

[編輯 | 編輯原始碼]

觀察-測試關係

[編輯 | 編輯原始碼]

由於 Test 依賴於 Watch,因此 Watch-Test 關係可以作為組合來實現,多個 Test 元素包含在 Log 元素中,Log 元素本身是 Watch 元素的子元素。測試按時間順序儲存。

觀察組合

[編輯 | 編輯原始碼]

兩種可能的方法

  • 將 Log 作為元素新增到 Watch 的基本資料中
  Watch
     uri
     name
     Log
        Test
  • 構造一個 Watch 元素,該元素包含 Watch 基本資料作為 WatchSpec 和 Log
  Watch
     WatchSpec (the Watch entity )
        uri
        name
     Log

第二種方法保留原始 Watch 實體作為節點,並且也適合使用 XForms,允許將整個 WatchSpec 節點包含在表單中。但是,它引入了一個難以命名的中間元素,並導致如下路徑:

  $watch/WatchSpec/uri 

何時

  $watch/uri would be more natural.

這裡我們選擇第一種方法,理由是,為了更簡單地實現特定介面,不希望引入中間元素。

觀察實體

[編輯 | 編輯原始碼]

觀察實體可以作為檔案或集合中的元素來實現。這裡我們選擇將 Watch 作為文件中 Monitor 容器中的元素來實現。但是,這是一個艱難的決定,XQuery 程式碼應該儘可能地隱藏此決定。

屬性實現

[編輯 | 編輯原始碼]

觀察屬性對映到元素。測試屬性對映到屬性。

模型生成
[編輯 | 編輯原始碼]

QSEE 將生成一個XML 模式。在此對映中,所有關係都使用外部索引鍵實現,使用 key 和 keyref 來描述關係。在這種情況下,需要編輯模式以透過組合來實現 Watch-Test 關係。

透過推理
[編輯 | 編輯原始碼]

此模式是透過 Trang(在 Oxygen 中)從示例文件生成的,示例文件是在系統執行時建立的。

  • 緊湊型 Relax NG
    element Monitor {
        element Watch {
            element uri { xsd:anyURI },
            element name { text },
            element Log {
                element Test {
                    attribute at { xsd:dateTime },
                    attribute responseTime { xsd:integer },
                    attribute statusCode { xsd:integer }
                }+
            }
        }+
    }

  • XML 模式

XML 模式

設計的模式

[編輯 | 編輯原始碼]

編輯 QSEE 生成的模式會導致包含對 statusCodes 的限制的模式。

XML 模式


測試資料

[編輯 | 編輯原始碼]

XQuery 指令碼將 XML 模式(或其子集)轉換為符合該模式的文件的隨機例項。

隨機文件

測試按屬性 at 升序排列的約束在此模式中沒有定義。生成器需要透過有關字串長度和列舉值、迭代和可選元素的機率分佈的附加資訊來幫助生成有用的測試資料。

等效 SQL 實現

[編輯 | 編輯原始碼]
CREATE TABLE Watch(
	uri	VARCHAR(8) NOT NULL,
	name	VARCHAR(8) NOT NULL,
	CONSTRAINT	pk_Watch PRIMARY KEY (uri)
) ;

CREATE TABLE Test(
	at	TIMESTAMP NOT NULL,
	responseTime	INTEGER NOT NULL,
	statusCode	INTEGER NOT NULL,
	uri	VARCHAR(8) NOT NULL,
	CONSTRAINT	pk_Test PRIMARY KEY (at,uri)
) ;

ALTER TABLE Test
  ADD INDEX (uri), 
  ADD CONSTRAINT fk1_Test_to_Watch FOREIGN KEY(uri) 
                 REFERENCES Watch(uri) 
                 ON DELETE RESTRICT
                 ON UPDATE RESTRICT;

在關係型實現中,Watch 的主鍵 **uri** 是 Test 的外部索引鍵。將系統生成的 ID 新增到此有意義的 URI 中以代替它將有利於消除冗餘並減小外部索引鍵的大小。但是,需要一種機制來分配唯一的 ID。

實現

[edit | edit source]

依賴

[edit | edit source]

eXistdb 模組

[edit | edit source]
  • xmldb 用於資料庫更新和登入
  • datetime 用於日期格式化
  • util - 用於 system-time 函式
  • httpclient - 用於 HTTP GET
  • scheduler - 用於排程監控任務
  • validation - 用於資料庫驗證

其他

[edit | edit source]
  • Google 圖表


函式

[edit | edit source]

單個 XQuery 模組中的函式。

module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor";

資料庫訪問

[edit | edit source]

訪問監控資料庫,該資料庫可能是本地資料庫文件或遠端文件。

declare function monitor:get-watch-list($base as xs:string) as element(Watch)* {
   doc($base)/Monitor/Watch
};

特定的 Watch 實體由其 URI 標識

  let $wl:= monitor:get-watch-list("/db/Wiki/Monitor3/monitor.xml")

對 Watch 的進一步引用是透過引用進行的。例如

declare function monitor:get-watch-by-uri($base as xs:string, $uri as xs:string) as element(Watch)* {
   monitor:get-watch-list($base)[uri=$uri]
};

執行測試

[edit | edit source]

測試對 uri 進行 HTTP GET。GET 由對 util:system-time() 的呼叫括起來,以計算以毫秒為單位的經過的掛鐘時間。測試報告包含 statusCode。

declare function monitor:run-test($watch as element(Watch)) as element(Test) { 
   let $uri := $watch/uri
   let $start := util:system-time()
   let $response :=  httpclient:get(xs:anyURI($uri),false(),())
   let $end := util:system-time()
   let $runtimems := (($end - $start) div xs:dayTimeDuration('PT1S'))  * 1000  
   let $statusCode := string($response/@statusCode)
   return
       <Test  at="{current-dateTime()}" responseTime="{$runtimems}" statusCode="{$statusCode}"/>
};


生成的測試將追加到日誌的末尾

declare function monitor:put-test($watch as element(Watch), $test as element(Test)) {
    update insert $test into $watch/Log
};

要執行測試,指令碼將登入,遍歷 Watch 實體,併為每個實體執行測試並存儲結果


import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";

let $login := xmldb:login("/db/","user","password")
let $base := "/db/Wiki/Monitor3/Monitor.xml"
for $watch in monitor:get-watch-list($base)
let $test := monitor:run-test($watch)
let $update :=monitor:put-test($watch,$test) 
return $update

作業排程

[edit | edit source]

計劃每 5 分鐘執行此指令碼的作業。

let $login := xmldb:login("/db","user","password")
return scheduler:schedule-xquery-cron-job("/db/Wiki/Monitor/runTests.xq" , "0 0/5 * * * ?")

索引頁面

[edit | edit source]

索引頁面基於提供的 Monitor 文件,預設情況下為生產資料庫。

import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";

declare option exist:serialize  "method=xhtml media-type=text/html";
declare variable $heading := "Monitor Index";
declare  variable $base := request:get-parameter("base","/db/Wiki/Monitor3/Monitor.xml");

<html>
   <head>
        <title>{$heading}</title>
    </head>
    <body>
       <h1>{$heading}</h1>
       <ul>
          {for $watch in  monitor:get-watch-list($base)
          return 
               <li>{string($watch/name)}&#160;  &#160; 
                        <a href="report.xq?base={encode-for-uri($base)}&amp;uri={encode-for-uri($watch/uri)}">Report</a>  
               </li>
          }
      </ul>
    </body>
</html>

在此實現中,監控文件的 URI 透過 URI 傳遞給依賴指令碼。另一種方法是透過會話變數傳遞此資料。

檢視

報告

[edit | edit source]

報告借鑑了 Watch 的 Tests 日誌

declare function monitor:get-tests($watch as element(Watch)) as element(Test)* {
    $watch/Log/Test
};

概述報告

[edit | edit source]

基本報告顯示有關受監控 URI 的摘要資料以及響應時間隨時間變化的嵌入圖表。正常執行時間是狀態程式碼為 200 的測試與測試總數的比率。

import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";

declare option exist:serialize  "method=xhtml media-type=text/html";

let $base := request:get-parameter("base",())
let $uri:= request:get-parameter("uri",())
let $watch :=monitor:get-watch-by-uri($base,$uri)

let $tests := monitor:get-tests($watch)
let $countAll := count($tests)
let $uptests := $tests[@statusCode="200"]
let $last24hrs := $tests[position() >($countAll - 24 * 12)]
let $heading := concat("Performance results for ", string($watch/name))
return 
<html>
    <head>
        <title>{$heading}</title>
    </head>
    <body>
       <h3>
            <a href="index.xq">Index</a>
        </h3>
        <h1>{$heading}</h1>
        <h2><a href="{$watch/uri}">{string($watch/uri)}</a></h2>
        {if (empty($tests)) 
         then ()
         else
   <div>      
        <table border="1">
              <tr>
                <th>Monitoring started</th>
                <td> {datetime:format-dateTime($tests[1]/@at,"EE dd/MM HH:mm")}</td>
            </tr>
            <tr>
                <th>Latest test</th>
                <td> {datetime:format-dateTime($tests[last()]/@at,"EE dd/MM HH:mm")}</td>
            </tr>
            <tr>
                <th>Minimum response time </th>
                <td> {min($tests/@responseTime)} ms </td>
            </tr>
            <tr>
                <th>Average response time</th>
                <td> { round(sum($tests/@responseTime) div count($tests))} ms</td>
            </tr>
            <tr>
                <th>Maximum response time </th>
                <td> {max($tests/@responseTime)} ms</td>
            </tr>
            <tr>
                <th>Uptime</th>
                <td>{round(count($uptests) div count($tests)  * 100) } %</td>
            </tr>
            <tr>
                <th>Raw Data </th>
                <td>
                    <a href="testData.xq?base={encode-for-uri($base)}&amp;uri={encode-for-uri($uri)}">View</a>
                </td>
            </tr>
            <tr>
                <th>Response Distribution </th>
                <td>
                    <a href="responseDistribution.xq?base={encode-for-uri($base)}&amp;uri={encode-for-uri($uri)}">View</a>
                </td>
            </tr>
        </table>
        <h2>Last 24 hours </h2>
            {monitor:responseTime-chart($last24hrs)}    
         <h2>1 hour averages </h2>
            {monitor:responseTime-chart(monitor:average($tests,12))}    

       </div>
       }
    </body>
</html>

檢視

響應時間圖

[edit | edit source]

該圖表使用 Google 圖表 API 生成。從 0 到 100 的預設垂直刻度適合典型的響應時間。在這個簡單的例子中,圖表是未修飾的或未解釋的。

declare function monitor:responseTime-chart($test as element(Test)* ) as element(img) {
   let $points := 
       string-join($test/@responseTime,",")
   let  $chartType := "lc"
   let $chartSize :=  "300x200"
   let $uri := concat("http://chart.apis.google.com/chart?",
                                       "cht=",$chartType,"&amp;chs=",$chartSize,"&amp;chd=t:",$points)
   return 
        <img src="{$uri}"/>  
};

響應時間頻率分佈

[edit | edit source]

響應時間的頻率分佈總結了響應時間。首先,分佈本身被計算為一系列組。間隔計算很粗略,使用 11 個組來適應 Google 圖表。

declare function monitor:response-distribution($test as element(Test)* ) as element(Distribution) {
  let $times := $test/@responseTime
  let $min := min($times)
  let $max := max($times)
  let $range := $max - $min
  let $step := round( $range div 10)
  return
 <Distribution>
 {
      for $i in (0 to 10)
      let $low := $min + $i * $step
      let $high :=$low + $step
      return
           <Group i="{$i}" mid="{round(($low + $high ) div 2)}" count="{ count($times[. >= $low] [. < $high]) }"/>
 }
</Distribution>
};

然後可以將此分組分佈繪製為條形圖。在這種情況下需要縮放。

declare function monitor:distribution-chart($distribution as element(Distribution)) as element(img) {
   let $maxcount := max($distribution/Group/@count)
   let $scale :=100 div $maxcount 
   let $points := 
       string-join( $distribution/Group/xs:string($scale * @count),",")
   let  $chartType := "bvs"
   let $chartSize :=  "300x200"
   let $uri := concat("http://chart.apis.google.com/chart?",
                                       "cht=",$chartType,"&amp;chs=",$chartSize,"&amp;chd=t:",$points)
   return 
        <img src="{$uri}"/>  
};

最後是用於建立頁面的指令碼

import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";

declare option exist:serialize  "method=xhtml media-type=text/html";

let $base := request:get-parameter("base",())
let $uri:= request:get-parameter("uri",())
let $watch := monitor:get-watch($base,$uri)
let $tests := monitor:get-tests($watch)
let $heading := concat("Distribution for ", string($watch/name))
let $distribution := monitor:response-distribution($tests)
return 

<html>
    <head>
        <title>{$heading}</title>
    </head>
    <body>
        <h1>{$heading}</h1> {monitor:distribution-chart($distribution)} <br/>
        <table border="1">
            <tr>
                <th>I </th>
                <th>Mid</th>
                <th>Count</th>
            </tr> {for $group in $distribution/Group return <tr>
                <td>{string($group/@i)}</td>
                <td>{string($group/@mid)}</td>
                <td>{string($group/@count)}</td>
            </tr> } </table>
    </body>
</html>

驗證

[edit | edit source]

eXist 模組提供了用於根據模式驗證文件的函式。Monitor 文件連結到模式


let $doc := "/db/Wiki/Monitor3/Monitor.xml"

return 
<report>
 <document>{$doc}</document>
  {validation:validate-report(doc($doc))}
</report>

執行

或者,可以根據任何模式驗證文件

let $schema := "http://www.cems.uwe.ac.uk/xmlwiki/Monitor3/trangmonitor.xsd"
let $doc := "/db/Wiki/Monitor3/Monitor.xml"

return 
<report>
 <document>{$doc}</document>
  <schema>{$schema}</schema>
  {validation:validate-report(doc($doc),xs:anyURI($schema))}
</report>

執行

這用於檢查隨機生成的例項是否有效

let $schema := request:get-parameter("schema",())
let $file := doc(concat("http://www.cems.uwe.ac.uk/xmlwiki/XMLSchema/schema2instance.xq?file=",$schema))
return 
 <result>
   <schema>{$schema}</schema>
   {validation:validate-report($file,xs:anyURI($schema))}
   {$file}
</result>

執行

停機警報

[edit | edit source]

監控的目的是向負責網站的人員發出故障警報。此類警報可能是透過簡訊、電子郵件或其他一些渠道傳送的。Watch 實體需要用配置引數進行增強。

檢查是否失敗

[edit | edit source]

首先,有必要計算網站是否已關閉。如果過去 $watch/fail-minutes 中的所有測試都沒有返回狀態程式碼 200,則 monitor:failing() 返回 true()。

 declare function monitor:failing($watch as element(Watch)) as xs:boolean  {
   let $now := current-dateTime()
   let $lastTestTime :=  $now - $watch/failMinutes * xs:dayTimeDuration("PT1M")
   let $recentTests := $watch/Log/Test[@at > $lastTestTime] 
   return
        every $t in $recentTests satisfies 
              not($t/statusCode = "200")    
   };

檢查是否已傳送警報

[edit | edit source]

如果此測試由計劃的作業重複執行,則可以在適當的通道上生成警報訊息。但是,警報訊息將在條件為真的每次都發送。最好減少傳送警報的頻率。一種方法是將警報元素新增到日誌中,與測試交織在一起。這不會影響訪問測試的程式碼,但允許我們在最近傳送過警報時抑制警報。如果在過去 $watch/alert-minutes 中傳送過警報,則 alert-sent() 將為 true。

declare function monitor:alert-sent($watch as element(Watch) as xs:boolean )  {
   let $now := current-dateTime()
   let $lastAlertTime := $now - $watch/alertMinutes * xs:dayTimeDuration("PT1M")
   let $recentAlerts := $watch/Log/Alert[@at > $lastAlertTime] 
   return
       exists($recentAlerts)                                 
};

更改通知任務

[edit | edit source]

用於檢查監控日誌的任務將遍歷 Watches,併為每個 Watch 檢查它是否失敗,但在此期間沒有傳送警報。如果是,則將構建一條訊息並將警報元素新增到日誌中。使用日誌來記錄警報事件意味著不需要保留其他狀態,並且此任務執行的頻率與警報週期無關。

import module namespace monitor = "http://www.cems.uwe.ac.uk/xmlwiki/monitor" at "monitor.xqm";
 
let $login := xmldb:login("/db/","user","password")
let $base := "/db/Wiki/Monitor3/Monitor.xml"
for $watch in monitor:get-watch-list($base)
return 
   if (monitor:failing($watch) and not(monitor:alert-sent($watch)))
   then 
       let $update := update insert <Alert at="{current-dateTime()}"/> into $watch/Log
       let $alert := monitor:send-alert($watch,$message)
       return true()
   else false()

討論

[edit | edit source]

警報事件可以新增到單獨的 AlertLog 中,但新增一類新事件可能比為每個事件建立單獨的序列更容易。還可能存在測試和事件之間的順序關係有用的情況。


[重新設計的架構]

待辦事項

[edit | edit source]
  • 新增建立/編輯 Watch
  • 檢測丟失的測試
  • 透過在分析之前按日期過濾測試來支援對日期範圍的分析
  • 改進圖表的顯示效果
華夏公益教科書