ASP.NET 实时 Oracle 数据库监控仪表板





5.00/5 (4投票s)
Oracle Performance Dashboard (OPD) 是一个小型 ASP.NET 网站,可近乎实时地向您展示一个或多个 Oracle 实例的性能和问题。它使用动态性能视图 (DPV) 并运行一些流行的 DBA 脚本,以从服务器中提取有意义、易于理解的信息。
引言
Oracle Performance Dashboard (OPD) 是一个小型 ASP.NET 网站,可近乎实时地向您展示一个或多个 Oracle 实例的性能和问题。它使用动态性能视图 (DPV) 并运行一些流行的 DBA 脚本,以从服务器中提取有意义、易于理解的信息。您可以使用它快速发现阻塞查询、谁在阻塞谁、消耗高 CPU 或磁盘的昂贵查询、查看是否存在异常锁、极高的磁盘活动等。
演示
您可以在这里观看实时演示
http://odp.omaralzabir.com
获取代码
二进制文件在此处,您可以将其解压到 IIS 文件夹,在 web.config 文件中**放置连接字符串**,即可准备就绪。无需在服务器上安装任何 Oracle 客户端软件。
GitHub 项目二进制文件
您可以从 GitHub 项目网站获取源代码
https://github.com/oazabir/OraclePerformanceDashboard
为什么不使用 OEM
OEM 确实很棒,毫无疑问。但我要的是一个能够运行复杂的 DBA 脚本、处理输出并精确告诉我哪里出了问题的工具。在标准的监控工具如 OEM 中,您可以看到关键性能指标和一些通用的警报,但要深入调查服务器问题,更强大的方法是运行那些在 Oracle Performance Tuning 书籍和博客中找到的 DBA 脚本,这些脚本真正触及问题的核心,并向您展示问题出在哪里。有很多强大的脚本在书籍、论坛、博客中都有,可以方便地检测服务器上的问题,收集关于数据库健康状况的相关有用信息。这个工具试图为那些不像我一样的专业 Oracle DBA 提供一个漂亮的界面和易于发现线索的途径。
功能演示
OPD 在 V1 版本中提供以下功能
- 所有实例的摘要,集中在一屏显示,展示每个实例的重要指标。快速检查所有数据库是否正常。
- 实例仪表板,显示实例的详细信息
- OS 上的 CPU 使用率。
- 每个会话消耗的 CPU
- 重要的系统统计信息,如 Buffer Gets、Parse to execute ratio,这些可以指示服务器上的一些常见问题。
- 会话及其正在执行的操作,消耗的资源量,是否占用大量磁盘。
- 等待、阻塞、锁、死锁,这些都会导致数据库性能下降。
- 数据库的历史分析,展示一些非常有用的信息
- **最昂贵的查询**(就 CPU 和 Buffer Get 而言),这对于开发人员来说是需要立即解决的问题。
- 数据文件的 IO 使用情况。您可以查看某个数据文件是否负载过高,是否受到大量物理 IO 的冲击。
- 表空间使用情况。当表空间使用率超过 85% 时会发出警报。
- **过时的统计信息**(关于表)。您应该始终保持这部分内容的清洁。
- **糟糕的索引**,这些索引实际上会拖垮您的系统并混淆 Oracle 查询优化器。您需要移除这些索引,并重写那些您认为会利用这些索引以获得更好性能的查询。它们不会。它们会毁了您的数据库。
摘要屏幕
启动 OPD 时,您会看到所有实例的快速摘要以及指示任何实例异常的指标。然后您可以点击实例名称,深入了解每个实例的更多详细信息。
此摘要是通过运行一个长查询生成的,该查询从服务器收集一些重要指标,进行一些计算,以确定是否有异常情况。
SELECT 'Block' as Name, (select
nvl(sum(seconds_in_wait),0)
from
v$session
where
blocking_session is not NULL) as Value from Dual
union all
select 'Locks' as Name, (select count(1)
from v$session sn,
v$lock m
where
((sn.SID = m.SID and m.REQUEST != 0)
or (sn.SID = m.SID and m.REQUEST = 0 and LMODE != 4 and (ID1, ID2) in
(select s.ID1, s.ID2
from v$lock S
where REQUEST != 0
and s.ID1 = m.ID1
and s.ID2 = m.ID2)))) as Value FROM Dual
union all
select 'Waits' as Name, (select count(1) FROM v$session_wait w, v$session s, dba_objects o WHERE s.sid = w.sid AND w.p2 = o.object_id) as Value from Dual
union all
select 'Long op (sec rem)' as Name, (SELECT
nvl(sum(time_remaining),0)
FROM v$session_longops sl
INNER JOIN v$session s ON sl.SID = s.SID AND sl.SERIAL# = s.SERIAL#
WHERE time_remaining > 0) as Value FROM Dual
union all
select 'CPU' as Name, (
with AASSTAT as (
select
decode(n.wait_class,'User I/O','User I/O',
'Commit','Commit',
'Wait') CLASS,
sum(round(m.time_waited/m.INTSIZE_CSEC,3)) AAS,
BEGIN_TIME ,
END_TIME
from v$waitclassmetric m,
v$system_wait_class n
where m.wait_class_id=n.wait_class_id
and n.wait_class != 'Idle'
group by decode(n.wait_class,'User I/O','User I/O', 'Commit','Commit', 'Wait'), BEGIN_TIME, END_TIME
union
select 'CPU_ORA_CONSUMED' CLASS,
round(value/100,3) AAS,
BEGIN_TIME ,
END_TIME
from v$sysmetric
where metric_name='CPU Usage Per Sec'
and group_id=2
union
select 'CPU_OS' CLASS ,
round((prcnt.busy*parameter.cpu_count)/100,3) AAS,
BEGIN_TIME ,
END_TIME
from
( select value busy, BEGIN_TIME,END_TIME from v$sysmetric where metric_name='Host CPU Utilization (%)' and group_id=2 ) prcnt,
( select value cpu_count from v$parameter where name='cpu_count' ) parameter
union
select
'CPU_ORA_DEMAND' CLASS,
nvl(round( sum(decode(session_state,'ON CPU',1,0))/60,2),0) AAS,
cast(min(SAMPLE_TIME) as date) BEGIN_TIME ,
cast(max(SAMPLE_TIME) as date) END_TIME
from v$active_session_history ash
where SAMPLE_TIME >= (select BEGIN_TIME from v$sysmetric where metric_name='CPU Usage Per Sec' and group_id=2 )
and SAMPLE_TIME < (select END_TIME from v$sysmetric where metric_name='CPU Usage Per Sec' and group_id=2 )
)
select
round(
decode(sign(CPU_OS-CPU_ORA_CONSUMED), -1, 0, (CPU_OS - CPU_ORA_CONSUMED )) +
CPU_ORA_CONSUMED +
decode(sign(CPU_ORA_DEMAND-CPU_ORA_CONSUMED), -1, 0, (CPU_ORA_DEMAND - CPU_ORA_CONSUMED )) +
COMMIT +
READIO +
WAIT
, 2)
CPU_TOTAL
from (
select
min(BEGIN_TIME) BEGIN_TIME,
max(END_TIME) END_TIME,
sum(decode(CLASS,'CPU_ORA_CONSUMED',AAS,0)) CPU_ORA_CONSUMED,
sum(decode(CLASS,'CPU_ORA_DEMAND' ,AAS,0)) CPU_ORA_DEMAND,
sum(decode(CLASS,'CPU_OS' ,AAS,0)) CPU_OS,
sum(decode(CLASS,'Commit' ,AAS,0)) COMMIT,
sum(decode(CLASS,'User I/O' ,AAS,0)) READIO,
sum(decode(CLASS,'Wait' ,AAS,0)) WAIT
from AASSTAT)
)
as Value FROM Dual
UNION ALL
select 'Space' as Name, (
select count(1) FROM (
SELECT df.tablespace_name,
df.file_name,
df.size_mb,
f.free_mb,
df.max_size_mb,
f.free_mb + (df.max_size_mb - df.size_mb) AS max_free_mb,
ROUND((df.max_size_mb-(f.free_mb + (df.max_size_mb - df.size_mb)))/max_size_mb,0) AS used_pct
FROM (SELECT file_id,
file_name,
tablespace_name,
TRUNC(bytes/1024/1024) AS size_mb,
TRUNC(GREATEST(bytes,maxbytes)/1024/1024) AS max_size_mb
FROM dba_data_files) df,
(SELECT TRUNC(SUM(bytes)/1024/1024) AS free_mb,
file_id
FROM dba_free_space
GROUP BY file_id) f
WHERE df.file_id = f.file_id (+)
ORDER BY df.tablespace_name,
df.file_name
) where USED_PCT > 85) as Value from Dual
UNION ALL
select 'Invalid Objects' as Name, (SELECT count(1)
FROM dba_objects
WHERE status = 'INVALID') as Value FROM Dual
实例仪表板
当您深入了解一个实例时,您将看到该实例的仪表板。
仪表板显示实例的资源使用情况和一些关键系统统计信息。这是用于收集服务器各种 CPU 计数的查询,该查询执行两次,间隔一秒,然后计算差值以得出图表的值。
SELECT systimestamp as DATETIME,
(SELECT value
FROM v$osstat
WHERE stat_name = 'NUM_CPUS') CPU,
sum(decode(stat_name,'IDLE_TIME', value, NULL)) as idle_time,
sum(decode(stat_name,'USER_TIME', value, NULL)) as user_time,
sum(decode(stat_name,'SYS_TIME', value, NULL)) as sys_time,
sum(decode(stat_name,'IOWAIT_TIME', value, NULL)) as iowait_time,
sum(decode(stat_name,'NICE_TIME', value, NULL)) as nice_time
FROM v$osstat
WHERE stat_name in ('IDLE_TIME','USER_TIME','SYS_TIME','IOWAIT_TIME','NICE_TIME')
GROUP BY systimestamp
资源图
左侧显示 OS 上的 CPU 使用情况,以及用户、系统、IO 所使用的 CPU 量。通常当存在 IO 瓶颈时,您会看到 IO 线会上涨。
右侧显示每个会话消耗的 CPU。如果某个会话的 CPU 使用量异常高,您就知道该会话是罪魁祸首。
底部有 4 个关键指标。如果它们不符合最优值或至少不符合最佳实践指南,它们将显示为红色。例如,Buffer Cache hit Ratio 应该高于 95%。您可以看到图表中有时它会变绿,因为有时它会超过 95%。
会话详情
这显示了活动会话的详细信息。您可以查看是否有任何会话消耗大量 CPU、大量物理读取、硬解析等。当出现异常时,黄色的警告指示器会亮起。
这是用于收集此数据的查询
SELECT
SID,
v$session.STATUS,
USERNAME || ' (' || OSUSER || ')' "USER",
MACHINE,
Logon_time,
/*SYS.AUDIT_ACTIONS.NAME || ' ' || OBJECT_NAME "Command",*/
SYS.AUDIT_ACTIONS.NAME "Command",
(case
when v$session.STATUS = 'ACTIVE' and v$session.ROW_WAIT_OBJ# > 0 then (select OBJECT_NAME FROM dba_objects WHERE v$session.ROW_WAIT_OBJ# = dba_objects.object_ID)
else ''
end) "Wait Object",
sql_text,
(SELECT ROUND (VALUE / 1024 / 1024, 2)
FROM v$sesstat
WHERE STATISTIC# = 25 AND v$sesstat.SID = v$session.sid)
AS "PGA Memory, in MB",
(SELECT VALUE
FROM v$sesstat
WHERE STATISTIC# = 12 AND v$sesstat.SID = v$session.sid)
AS "CPU, used by session",
ROUND ( (SELECT VALUE
FROM v$sesstat
WHERE STATISTIC# = 339 AND v$sesstat.SID = v$session.sid)
/ (SELECT DECODE (VALUE, 0, 1, VALUE)
FROM v$sesstat
WHERE STATISTIC# = 338 AND v$sesstat.SID = v$session.sid),
2)
AS "Hard Parse, %",
(SELECT ROUND (VALUE / 1024 / 1024, 2)
FROM v$sesstat
WHERE STATISTIC# = 58 AND v$sesstat.SID = v$session.sid)
AS "Physical read bytes, in MB",
(SELECT ROUND (VALUE / 1024 / 1024, 2)
FROM v$sesstat
WHERE STATISTIC# = 66 AND v$sesstat.SID = v$session.sid)
AS "Physical write bytes, in MB",
(SELECT ROUND (VALUE / 1024 / 1024, 2)
FROM v$sesstat
WHERE STATISTIC# = 139 AND v$sesstat.SID = v$session.sid)
AS "Redo size, in MB",
(SELECT ROUND (VALUE / 1024 / 1024, 2)
FROM v$sesstat
WHERE STATISTIC# = 344 AND v$sesstat.SID = v$session.sid)
AS "Received from client, in MB",
(SELECT ROUND (VALUE / 1024 / 1024, 2)
FROM v$sesstat
WHERE STATISTIC# = 343 AND v$sesstat.SID = v$session.sid)
AS "Sent to client, in MB",
SERIAL#,
PROGRAM,
OWNERID,
PROCESS
FROM v$session
/*LEFT OUTER JOIN
DBA_OBJECTS
ON v$session.ROW_WAIT_OBJ# = dba_objects.object_ID*/
LEFT OUTER JOIN
v$sqlarea
ON
v$session.sql_hash_value = v$sqlarea.hash_value
AND v$session.sql_address = v$sqlarea.address
LEFT OUTER JOIN SYS.AUDIT_ACTIONS ON v$session.command = SYS.AUDIT_ACTIONS.action
WHERE v$session.LOGON_TIME BETWEEN TRUNC (SYSDATE) AND SYSDATE
AND
v$session.sid != userenv('SESSIONID')
AND
rownum < 10
ORDER BY 2, 10 DESC
此查询的输出绑定到 GridView,然后 GridView 查看每一行并决定是否显示任何警告。
<asp:GridView CssClass="table table-striped" ID="GridView1" runat="server" DataSourceID="sqlDataSource" EnableModelValidation="True">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<%#HandleDbNull(Eval("Hard Parse, %"))>0 ? "<span class='label label-warning'>Hard Parse</span>" : ""%>
<%#HandleDbNull(Eval("PGA Memory, in MB"))>10 ? "<span class='label label-warning'>PGA</span>" : ""%>
<%#HandleDbNull(Eval("CPU, used by session"))>3000 ? "<span class='label label-warning'>CPU</span>" : ""%>
<%#HandleDbNull(Eval("Physical read bytes, in MB"))>100 ? "<span class='label label-warning'>Physical Read</span>" : ""%>
<%#HandleDbNull(Eval("Physical write bytes, in MB"))>100 ? "<span class='label label-warning'>Physical Write</span>" : ""%>
<%#HandleDbNull(Eval("Redo size, in MB"))>10 ? "<span class='label label-warning'>Redo</span>" : ""%>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
坏索引
这对开发人员非常有用,因为您可以快速看到哪些索引设计得很糟糕,因为索引中的列选择性非常低。例如,在屏幕截图中,您可以看到一些糟糕的索引。尝试使用这些索引实际上会拖垮您的服务器。您可以看到第一个例子,列 PRODUCT_ID 只有 7 个唯一值,但有超过 500 万行。因此,每个 PRODUCT_ID 可能对应超过 100 万行。
有些开发人员认为有一个查询使用了 WHERE PRODUCT_ID=X,因此在 PRODUCT_ID 上创建了索引。但在这种情况下,它不起作用。
有时在开发和测试期间,我们没有来自生产数据库的代表性数据,因此我们没有发现这些问题。只有当我们上线后,在接下来的一个小时内出现问题时,我们才意识到自己做错了什么。这个“坏索引”视图将有助于识别这些错误的索引设计。
这是查找这些坏索引的查询
select i.table_name,i.index_name,
(select rtrim (xmlagg (xmlelement (e, column_name || ', ')).extract ('//text()'), ',') column_names from USER_IND_COLUMNS where index_name=i.index_name) column_names,
TO_CHAR(ROUND((i.distinct_keys/(i.num_rows+1))*100, 3), '999.000') as Uniqueness,
case
when i.distinct_keys/(i.num_rows+1) = 0 then 'IGNORE'
when i.distinct_keys/(i.num_rows+1) < 0.1 AND num_rows > 1000 then 'KILLER!'
when i.distinct_keys/(i.num_rows+1) < 0.1 AND num_rows < 1000 then 'IGNORE'
when i.distinct_keys/(i.num_rows+1) < 0.2 AND num_rows > 1000 then 'SERIOUSLY POOR'
when i.distinct_keys/(i.num_rows+1) < 0.5 AND num_rows > 1000 then 'POOR'
when i.distinct_keys/(i.num_rows+1) < 0.8 then 'OK'
when i.distinct_keys/(i.num_rows+1) < 0.9 then 'GOOD'
else 'VERY GOOD'
end Quality,
i.distinct_keys,i.num_rows,i.blevel,i.leaf_blocks,i.avg_leaf_blocks_per_key,i.avg_data_blocks_per_key from user_indexes i
where i.table_name in (select table_name from user_tables)
and i.num_rows > 0
group by table_name,i.index_name,i.distinct_keys,i.num_rows,i.blevel,i.leaf_blocks,i.avg_leaf_blocks_per_key,i.avg_data_blocks_per_key
order by Uniqueness, num_rows desc
锁、等待
锁和等待是导致服务器性能下降的常见原因。您可以从这个工具中快速看到哪些锁正在被持有,以及哪些是昂贵的等待。
它将向您展示持有锁并导致争用甚至死锁的确切查询。在这里您可以看到会话 ID1 和 ID2 正在获取排他锁并陷入死锁。
工作原理
看呐,没有 AJAX!
您会注意到面板在定期刷新。您可能认为我正在使用 AJAX 调用某个服务器端 Web 服务以获取 JSON/XML 响应,然后使用某个 jQuery 模板来渲染 HTML 输出。不。我使用的是我们的祖先几代人一直在愉快使用的方法。天然、原生的 IFRAME 解决方案,没有副作用。每个面板的 HTML 输出来自单独的 ASP.NET 页面,通过 IFRAME 注入到主 Dashboard 页面的 DIV 中。
这种方法有几个好处
- 这些小部件是独立的页面,用户可以直接以全浏览器视图浏览。
- 每个小部件都是一个纯粹的 ASP.NET 页面。无需构建 Web 服务来以 JSON/XML 格式返回数据。也无需使用通常用于序列化为 JSON/XML 的任何实体类。
- HTML 内容是服务器端使用常规 ASP.NET 生成的。因此,无需使用任何基于 JavaScript 的 HTML 模板库。
- 由于不需要 AJAX 或 HTML 模板,因此无需担心 jQuery 或其插件在新版本中出现兼容性问题,也无需定期更新 JavaScript 库。
让我们看看如何做到这一点。首先是绘制面板的 HTML 标记
<div class="row">
<div class="panel panel-success">
<div class="panel-heading"><a href="WhoIsActive.aspx?c=<%= ConnectionString %>">What's going on</a></div>
<div class="panel-body panel-body-height" id="WhoIsActive">
<div class="progress">
<div class="progress-bar progress-bar-striped" style="width: 60%"><span class="sr-only">100% Complete</span></div>
</div>
</div>
<iframe class="content_loader" onload="setContent(this, 'WhoIsActive')" src="WhoIsActive.aspx?c=<%= ConnectionString %>" style="width: 100%; height: 100%; border: none; display: none" frameborder="0"></iframe>
</div>
</div>
这是取自 Twitter Bootstrap theme 的标记。
您会注意到那里有一个不可见的 IFRAME。当 IFRAME 加载时,它会调用 `setContent` 函数。该函数获取 IFRAME 的全部内容并将其注入到 panel-body div 中。
function setContent(iframe, id) {
...
$('#' + id)
.html($(iframe).contents().find("form").html())
.dblclick(function () {
iframe.contentWindow.location.reload();
})
...
}
就这样,干净的类 AJAX 解决方案,没有任何 AJAX:没有 XMLHTTP,没有 JSON 管道,没有 HTML 模板,没有服务器端 Web 服务。
但是,这对于在 IFRAME 中挂载的任何事件处理程序都不起作用。那么,点击查询如何显示带有完整查询的弹出窗口?另外,如果它是一个 IFRAME,弹出窗口不应该出现在 IFRAME 内部吗?
点击功能是在主 Dashboard 页面上完成的。在将内容注入 DIV 后,它会挂载 click 事件处理程序,在主页面上显示弹出窗口。
function setContent(iframe, id) {
$('#' + id)
.find('td.large-cell').off('click');
if ($('#' + id).scrollLeft() == 0) {
$('#' + id)
.html($(iframe).contents().find("form").html())
.dblclick(function () {
iframe.contentWindow.location.reload();
})
.find('td.large-cell').find('div').click(function () {
$('#content_text').text($(this).html());
$('#basic-modal-content').modal();
});
}
这里它会查找任何具有 class large-cell 的 `
绘制图表
该图表使用 jQuery 插件 Flot 将一些性能计数器渲染为运行图。
有一个 SysStats.aspx 文件负责渲染显示性能计数器的表格。它选择一些重要的计数器,并将它们标记为出现在图表中。首先,它遍历表格,查找计数器,并将计数器的标签标记为 x 轴,计数器的值标记为 y 轴。
var plot = ["Buffer cache hit ratio (95+)", "Parse CPU to total CPU ratio (<30)", "Shared pool free (~0)", "Parse to execute ratio (<0.3)"];
$('td').each(function (i, e) {
td = $(e);
if (td.text().trim().length > 0) {
for (var i = 0; i < plot.length; i ++) {
if (plot[i] == td.text().trim()) {
td.addClass("x-axis");
td.next().addClass("y-axis");
}
}
}
})
现在这个页面托管在 Dashboard 页面内的 IFRAME 中。因此,Dashboard 页面会扫描 IFRAME 内容,查找这些标签,提取它们的值,然后传递给 Flot 图表插件。
$(iframe).contents().find("form").find(".x-axis").each(function (i, e) {
var x = $(e);
var y = x.next('.y-axis');
var xname = x.text();
var yvalue = parseInt(y.text());
if (datasets[xname]) {
var data = datasets[xname].data;
data.pop();
data.splice(0, 0, yvalue);
}
});
更新 Flot 图表的其余工作由常规的 Flot 代码完成。
function updatePlot() {
var index = 0;
$.each(datasets, function (key, val) {
var items = [];
for (var i = 0; i < val.data.length; i++)
items.push([i, val.data[i]]);
var data = { color: val.color, data: items };
if (plots[index] != null) {
plot = plots[index];
plot.setData([data]);
plot.draw();
}
else {
plot = $.plot("#placeholder" + (index + 1), [data], {
series: {
//shadowSize: 0 // Drawing is faster without shadows
},
lines: { show: true, fill: true },
grid: {
hoverable: true,
clickable: true
},
yaxis: {
min: 0,
max: val.ymax
},
xaxis: {
show: false
}
});
就是这样!同样,没有 AJAX,没有 Web 服务,没有 HTML 模板,没有 JSON 管道。纯粹原生的 IFRAME 和 HTML。
结论
OPD 旨在帮助我们轻松监控 Oracle 实例的健康状况。它从 Oracle 内置视图产生的冗长数据中提取有意义的信息。它准确地向您展示问题所在。您可以按需自定义代码,添加自己的警告、自己的阈值,并根据您的特定需求进行调整。