跳转到内容

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
  • 检测丢失的测试
  • 通过在分析之前按日期过滤测试来支持对日期范围的分析
  • 改进图表的显示效果
华夏公益教科书