一.为什么升级总是出问题?
云南一市级人民医院是我们在云南目前最重要的客户之一,以前去过两次,留下的映象也比较好。此次HIS升级到10.26,云南公司的同事也做了不少的准备,可还是出问题了。问题流转到我这里之前,同事们已经处理了大部分的应用的问题。但目前性能还是比较差,已经影响了了医院的正常使用。主要表现在几个方面:

1. 其中一个节点的cpu长期都在90%以上;一些关键的业务出现了严重的性能问题。
2. $ORACLE_BASE所在的盘,由于产生大的system state dump文件,时常被充满,导致实例停止响应,感觉象down机一样。
今天是周末,接到了云南公司同事要求远程支持的电话,远程连上去取了一份awr报告,简单做了一下分析:

a. 首先感觉到的就是dbtime过高,对于一个只有4 cpu的机器来说,60分钟内就有288.67分钟的db time.
Snap Id Snap Time Sessions Cursors/Session
Begin Snap: 18140 04-6月 -10 10:00:36 281 78.7
End Snap: 18141 04-6月 -10 11:00:06 262 78.9
Elapsed: 59.49 (mins)
DB Time: 288.67 (mins)

b. Soft Parse %不足90%, 而Library Hit %只有91%,这在RAC环境是很糟糕的比率,会引发非常严重的闩锁争用。
Instance Efficiency Percentages (Target 100%)
Buffer Nowait %: 100.00 Redo NoWait %: 100.00
Buffer Hit %: 99.68 In-memory Sort %: 100.00
Library Hit %: 91.17 Soft Parse %: 89.66
Execute to Parse %: 54.12 Latch Hit %: 99.81
Parse CPU to Parse Elapsd %: 11.51 % Non-Parse CPU: 95.87

c. Top5中出现了cursor: pin S wait on X等待事件,这在我们的产品用户中还是比较少见的。
Top 5 Timed Events
Event Waits Time(s) Avg Wait(ms) % Total Call Time Wait Class
CPU time 8,754 50.5
db file sequential read 590,176 1,801 3 10.4 User I/O
cursor: pin S wait on X 138,673 1,350 10 7.8 Concurrency
db file scattered read 661,274 1,276 2 7.4 User I/O
row cache lock 463,151 295 1 1.7 Concurrency
这个时候从报告来看,共享池空间的足够的:
Shared Pool Statistics
Begin End
Memory Usage %: 28.79 29.96
% SQL with executions>1: 92.40 78.45
% Memory for SQL w/exec>1: 96.62 85.59

使用的共享池空间还不到30%,还有近70%的空闲空间,为什么会出现这么严重的共享池问题呢?遇到这类问题,第一件事情就是上metalink。metalink上有个文档描述了这个现象.文档编号:ID 731233.1。大意是cursor: pin S wait on X如果与row cache lock、Library Cache Load Locks大量伴随出现一般与SGA自动管理的resize操作有关,
我还沉浸在发现问题的兴奋中,突然老陈打电话过来了,问我这边能不能现场。医院因为这次升级,一些部门已经停用了大半天,意见已经很大了;明天又是星期一,如果明天不能解决问题,麻烦就大了,由于医院是当地最大的医院,甚至可能酿成社会问题;话说到这个份上,我也只能跑一趟了。跟老婆讲的时候,老婆嘟哝了一句“周末怎么老有事情?”。

二.愉快的旅途!

定了下午3:00点的飞机,简单收拾了下走奔机场;由于考虑到晚上肯定要加班,一上飞机就开始睡觉。快到昆明时,被飞机上提示开始下降的播音吵醒了,往窗外看了看,真是碧空×××,地面的景色一览无余,云南的空气质量真是不错,每次来都感觉神清气爽。我对云南的映象一直不错,看来这次也不会例外了。
刚出机场,就见到了昆明公司老李,简单寒暄了几句,老李就引我走向了他的私人座驾,一辆漂亮的“小奔”;这确实是礼遇了,感觉受宠若惊,前几次都是自己做坐大巴车去现场的。这是我第一次坐“小奔”感觉有点兴奋,老李开车的技术不是盖的,不一会就出了昆明市区上了高速,直奔玉溪。在高速上,我们讨论了一些开车的趣闻,老李也介绍了这次升级的经过和出现的一些问题。这让我对这次工作的内容有了大致的了解。
一个小时车程就到了玉溪,由于已经到了吃饭的时间,医院的人已经订好了餐厅,就先去了餐厅。一下车医院的Y主任就迎了过来。Y主任是医院的信息部门负责人,几年前见过几次,彼此都留下了比较好的映象,也就少了许多客套;在饭桌上Y主任直奔主题:“老贺,我们的问题,能在今天晚上解决么?”。这个时候,用户最想得到是肯定的回答, “尽力而为吧!根据以往的经验,保证明天的主要业务应该能够做到”我答到。这个时候客户最需要的是信心!由于还有活要干,饭就吃得比较快,Y主任还特意点了些柠檬汁,说是对长时间加班的人补充维生素很有好处….公司的几个人也就拼命的多喝了几口;呵呵,看来大家对身体还挺在意的嘛。。。

三.又是一个艰苦的晚上
到了医院,配好网络、连上服务器已经是晚上8:00过了。与老李、老陈等人商量了一下,一致应该先解决性能问题,至少撑过明天的业务高峰。晚上主要做了下面的一些调整:

1. SGA自动管理的调整
继续下午的分析,从Time Model来看sql execute elapsed time只占到了81.50,而parse time elapsed占到了db time的25%。由于ZLHIS在绑定变量上相对还是比较注意的,正常情况下,不会出现这严重的解析问题。这比率明显偏高,应该是共享池管理方面出了问题。

Statistic Name Time (s) % of DB Time
sql execute elapsed time 14,115.38 81.50
DB CPU 8,753.75 50.54
parse time elapsed 4,464.48 25.78
hard parse elapsed time 3,600.01 20.78
 
配合meatlink上文档 731233.1对诊断事件的描述,查询了v$sga_resize_ops视图,查询的SQL如下:
col component for a25
col final_size for 999,999,999,999 head "Final"
col started for a20
SELECT component, oper_type, final_size, to_char(start_time ,'mm/dd/yyyy hh24:mi:ss') started
FROM v$sga_resize_ops
WHERE status ='COMPLETE'
ORDER BY started desc, component;
发现共享池的Resize操作很频繁,这就基本确定了来之前的猜测。Oracle从10g开始引入了SGA自动管理,会自动根据系统的负载情况和统计信息,动态增大或收缩SGA中各个组件的大小,如共享池与高速缓存的大小;这种调整就就是Resize操作,如果Resize的频率过高,就会对性能造成比较严重的影响;这种现象也被称为共享池的“颠簸”。
解决共享池的“颠簸”,一般有两种方案:
a. 对shared_pool_size或db_cache_size设定一个值,这是一个最低值;这在一定程度上可以减少resize操作,可始终保证共享池与高速缓存有相对充足的物理空间。
b. 将sga_target设置为0,彻底取消SGA手动管理,采用手动指定各个sga组件的大小。
我决定采用后一种方案,最后确定的参数如下:

db_cache_size=3670016000
java_pool_size=117440512
large_pool_size=117440512
log_buffer=31457280
sga_max_size=8589934592
shared_pool_reserved_size=367001600
shared_pool_size=3670016000
streams_pool_size=104857600

2. 序列的调整
ZLHIS中的主键基本上都是通过序列来顺序生成的,这种设计在性能方面有两个主要的问题:
a. 序列是顺序增长的,在普通的B*tree索引时,索引“最右边”的块容易成为热点块,如果是在RAC环境 ,还会引起索引“最右边”的块在实例间频繁传递,形成全局热点块。
以玉溪的情况为例,从awr来看到Buffer busy等待的对象基本上都是索引:

Segments by Buffer Busy Waits
? % of Capture shows % of Buffer Busy Waits for each top segment compared
? with total Buffer Busy Waits for all segments captured by the Snapshot
Owner Tablespace Name Object Name Subobject Name Obj. Type Buffer Busy Waits % of Capture
xxHIS ZL9INDEXCIS 电子病历内容_PK INDEX 8 57.14
xxHIS ZL9INDEXHIS 病人费用记录_IX_登记时间 INDEX 1 7.14
xxHIS ZL9INDEXHIS 病人费用记录_PK INDEX 1 7.14
xxHIS ZL9INDEXCIS 病人医嘱记录_PK INDEX 1 7.14
xxHIS ZL9INDEXCIS 病人医嘱状态_IX_操作时间 INDEX 1 7.14

这个问题的解决方案是:
1).对索引做hash或范围分区,尽量把索引块在实例间分散;
2).使用反转索引,所谓反转索引就是利用reverse函数把索引值进行“反转”,再存入索引中。比如有两个连续的键值1234和1235,反转后的值就为4321和5321在索引存储时就可能存储在不同的数据块中,也就达到了打散热点块的目的。实施这两个方案的代价太大,需要重建索引,不可能在短时间能够做完,只有放到下一次的调整中了。
b.序列默认的cache为20,也就是一次取20个数字出来放到缓存里;用完了再取,这样的取值操作会带来一定的内部闩锁,如果频率使用的序列上就会发生频繁的这种以取值操作。 在RAC环境这种争用的代价更大;一般建议加大cache并使用noorder选项。Metalink上有个专门的文档介绍这个知识,文档编号为:RAC and sequences [ID 853652.1]。医院目前在这个问题上已经比较严重了,在awr中也有体现:

Cache Get Requests Pct Miss Scan Reqs Pct Miss Mod Reqs Final Usage
dc_awr_control 64 46.88 0 1 1
dc_global_oids 22,645 15.95 0 0 41
dc_histogram_data 5,387,173 6.68 0 0 4,051
dc_histogram_defs 1,568,861 12.08 0 0 2,392
dc_object_grants 163,781 11.38 0 0 293
dc_object_ids 6,170,715 0.84 0 31 786
dc_objects 1,162,120 13.71 0 82 1,905
dc_profiles 209 33.01 0 0 1
dc_rollback_segments 16,479 0.00 0 0 191
dc_segments 2,287,396 2.42 0 2 794
dc_sequences 2,415 56.15 0 2,415 5
dc_tablespace_quotas 2 100.00 0 2 0
dc_tablespaces 3,636,645 0.02

从这里我们可以看到dc_sequences的丢失率达到了56%。
我们将rac的cache调整为10000,使用的脚本如下:
select 'alter sequence ZLHIS.'||SEQUENCE_NAME||' cache 12000 ; '
from dba_sequences
where sequence_owner='ZLHIS'

3. 高事务表itl的调整

Itl是事务槽,在一个表或索引中通过initrans参数来指定初始化的itl数。itl的数量决定数据块所能同时执行的最大事务数,一个itl需要占用24字节的空间,如果inittrans不足使用,Oracle会动态调整itl的数量。基于上述原因,如果pctfree设置0或很低的值,可能导致itl无法扩展,从而造成严重的tx等待。
在RAC环境,对于高并发事务的表和索引设置较高的initrans值,并保证足够的块空间用于扩展itl。对于RAC环境下itl设置,Oracle的建议如下:
INITRANS=CPU内核数量*节点数量*0.75
医院目前的为二节点的rac,每个节点配置了4颗双核的安腾CPU,则需要设置8*2*0.75=12。 可以通过awr中Segments by ITL Waits列表中的对象来确定对那些对象做调整:

Segments by ITL Waits
? % of Capture shows % of ITL waits for each top segment compared
? with total ITL waits for all segments captured by the Snapshot
Owner Tablespace Name Object Name Subobject Name Obj. Type ITL Waits % of Capture
ZLHIS ZL9INDEXHIS 病人费用记录_PK INDEX 2 40.00
ZLHIS ZL9INDEXHIS 票据使用明细_IX_打印ID INDEX 1 20.00
ZLHIS ZL9INDEXHIS 票据使用明细_IX_使用时间 INDEX 1 20.00
ZLHIS ZL9INDEXHIS 票据使用明细_PK INDEX 1 20.00

需要注意的是调整initrans参数只对新分配的数据块起作用,如果需要调整已有的数据块,需要move表,索引则需要重建。如果是表的数据一直增长,update较少的话,调整已有数据块的作用不大,如医嘱记录,费用记录都是这种情况。如果是以udpate为主,就必须move表和做索引重建了,典型的就是号码控制表。

4. 号码控制表的处理

据现场的同事反映"号码控制表"的tx锁定比较严重,dump出号码控制表的数据块,发现itl已经自动扩展到了14:

Block header dump: 0x020090bc
Object id on Block? Y
seg/obj: 0xee429 csc: 0x00.784f2a73 itc: 20 flg: E typ: 1 - DATA
brn: 0 bdba: 0x20090b9 ver: 0x01 opc: 0
inc: 0 exflg: 0

Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0007.026.0004df05 0x00800657.aef8.2f C--- 0 scn 0x0000.784f29f3
0x02 0x0008.01a.0004e42d 0x008008fe.ae4a.09 C--- 0 scn 0x0000.784f2a70
0x03 0x0002.010.0004da61 0x00800571.ac2b.36 --U- 1 fsc 0x0000.784f2a74
0x04 0x0002.001.0004da5a 0x00800571.ac2b.34 C--- 0 scn 0x0000.784f2a68
0x05 0x0003.00a.0004e787 0x00801320.af7f.0a C--- 0 scn 0x0000.784f2a6a
0x06 0x0009.014.0004edc7 0x008012d1.b7f1.28 C--- 0 scn 0x0000.784f2a6c
0x07 0x0007.01a.0004deef 0x00800657.aef8.31 C--- 0 scn 0x0000.784f2a72
0x08 0x0014.02b.0004d82d 0x01417737.7a75.0d C--- 0 scn 0x0000.784f29be
0x09 0x000d.000.0004cac4 0x01441929.62da.03 --U- 1 fsc 0x0000.784f2ad3
0x0a 0x0001.021.0003a1c0 0x00800aec.89f9.2e C--- 0 scn 0x0000.784f29eb
0x0b 0x0008.029.0004e432 0x008008fe.ae4a.05 C--- 0 scn 0x0000.784f29ef
0x0c 0x0006.02f.0003a578 0x00800b78.89a6.2f C--- 0 scn 0x0000.784f2a53
号码控制表是HIS中访问和事务最频繁的表了,所有的单据处理都要操作这张表。这张表的数据量很小,几十行数据全部存在一个数据块里,这样容易造成严重的性能问题:
a. 由itl等原因引起的tx行锁
b. 在实例间频率传递块,包括传递PI(past p_w_picpath)、CR(一致性读)块的传递。形成HIS系统中“最热”的全局热点块。
对于第一个问题,前面已经有所叙述,直接使用了下面的脚本进行调整:
alter table 号码控制表 initrans 24 ;
alter table 号码控制表 move zl9baseitem;
alter index 号码控制表_UQ_项目名称 rebuild online initrans 24 ;
alter index 号码控制表_UQ_PK rebuild online initrans 24 ;
第二个问题,就是想办法把热点打散,让一个块存储更少的行,手段一般来讲有:
a.使用很高的pctfree值
b.使用hash或范围分区
c.使用MINIMIZE RECORDS_PER_BLOCK参数
我决定采用第三种方案, MINIMIZE RECORDS_PER_BLOCK参数可以限制一个块存多少行数据,实施的简要步骤如下:
1). 导出表的数据,或者ctas也可以。
2). Truncate这张表
3). insert期望的行数,比如你希望一个块存储两行数据,就只插入2行数据
4). 修改表的minimize records_per_block参数
Alter table 号码控制表 minimize records_per_block;
5). 删除第3步中插入的数据
6). 导入这张表,或者从备份表insert 过来。
经过上面的分析和处理已经是凌晨2:00了,这时候老陈提到说升级后因为收集统计信息时间太长,没等系统收集完,就强行中断了。看来需要重新收集一下HIS相关表的统计信息了。现在是凌晨2:00,时间应该来得及,于是写好了收集统计信息的脚本,放到unix后台来跑。
处理完了这些问题以后,医院Y主任建议我们再出去吃点东西,其实熬夜太晚,不建议吃太多东西,对健康不怎么好,但看到大家都纷纷响应,怎能拒绝呢?君子有成人之美的道理我还是懂的! 大家驱车来到一个餐馆,在饭桌上向主任简单汇报了一下今天晚上的处理情况。大家都希望明天能有一个好的结果。。。。
吃完了饭,我才想起我今晚还没找地方住呢,这时老陈说房间已经订好了,就在医院旁边的玉溪宾馆。我对老陈说:“这宾馆的名头挺响,会不会超标呀?”,老陈笑答到“上次研发部门封闭开发的时候就住那儿,你也应该去感受一下”。
到了宾馆以后,才知道其实就是一个政府的老招待所,设备看起来都是上世纪的东西。不过意外的是房间居然还有小冰箱,这也看出了宾馆曾经的奢华。。。。哈哈,不小心当了一把没落的贵族。。

四.功夫不负有心人!

早上不到7点就被外面的歌舞声吵醒了,玉溪是国歌曲作者聂耳的故乡,这里人可能有爱好音乐的传统吧,但扰人清梦就不算什么美德了!我找开窗户恶狠狠的丢了句“狗日的”,看到一群老头老太正在扭着我看不懂的舞蹈。到医院前,在医院对面的小巷子里,同事老金请吃了一碗鸡汤面,还整了几个烧豆腐,这东西我只在云南吃到过,味道还不错。
医院是8点上班,一般9:00~11:00点是业务高峰。在两个节点打开unix的top工具,开始监控系统性能。一直到10过,一号节点cpu都在60%以下,但一个奇怪的现象是2号节点虽然相比调整前cpu有所下降,但还是在80%左右,两个节点存在明显的差距。这是为什么呢?看来活还没有干完,不过能够撑过周一上午的高峰已经算成功了一半。由于调整取得了一定的效果,午饭的时候,大家还是比较愉快的,就在医院的职工食堂将就了一顿。。。..

四.万恶的接口,万恶的轮询!
二号节点为什么比一号节点的cpu一直要高呢?难倒有什么特殊的业务在二号节点跑?于是观察了中午两个节点的情况,中午一般没什么业务,但2号节点的cpu还是在20%左右,这是不正常。于是单独取了二号节点的awr报告,发现一条SQL语句:
select * from T_led where 诊室='眼科一诊室1' or 诊室='眼科二诊室' or 诊室='眼科诊断室' or 诊室='眼科四诊室' or 诊室='斜弱视专科诊室' or 诊室='口腔科诊室1' or 诊室='皮肤科专家诊室' or 诊室='皮肤科普通诊室'
这条SQL的执行的很频繁,向老陈咨询,得到这是一个叫号程序的SQL,指定连接到2号节点;ZLHIS通过触发器将药品处方的信息写入到一个临时表,对方的叫号程序每秒两次取一次临时表,叫号程序使用完以后,再把临时表的记录delete掉。这条sql执行频率太高,真实业务上并不需要这么高的频率;同时这段sql没有使用绑定变量。老陈立即与叫号程序的提供商联系,要求修改调用频率和使用绑定变量。郁闷的是这名开发人员,居然不知道绑定变量为何物,还需要我们在网上找了个delphi使用绑定变量的例子给他。
考虑到叫号程序查询的临时表,只是临时使用,使用完了就立即delete掉,这样频繁的insert/update的表,如果使用nologging选项,能减少redo产生的量,对性能调整有一定的效果:
alter table zlhis.t_led nologging ;
alter table zlhis.t_医技叫号 nologging ;
反复折腾沟通了几次,叫号程序的开发人员那边已经改好了程序,老陈他们去科室换上了新的叫号程序,两个节点的cpu占用终于差不多了。

五.烦人的Bug
来之前就已经知道,Oracle的安装目录/opt所在盘,时常被充满,导致Oracle停止响应。由于一直忙于处理性能问题,把这个问题放到了最后。之前的考虑是性能问题解决了,可能这个问题就自动解决了;确实有些时候Oracle的一些bug在性能差的时候,更容易被触发。自从调整性能后,这个问题出现的频率的降低了一些,但还是在出;正在思考的时候,发现/opt的使用率又已经到90%了,检查发现在安装目录的bdump目录又新生成了一个4g左右的核心转储文件。
正是这个核心转储文件,占用了大量的磁盘空间。检查alertSID.log文件发现如下的内容:
Sun Jun 6 18:51:35 2010
Global Enqueue Services Deadlock detected. More info in file
/opt/oracle/product/admin/orcl/udump/orcl1_ora_26649.trc.
Sun Jun 6 19:14:05 2010
System State dumped to trace file /opt/oracle/product/admin/orcl/bdump/orcl1_diag_4783.trc

对应的trace文件有以下内容:
*** SERVICE NAME:(orcl) 2010-06-05 08:46:55.989
*** SESSION ID:(1879.63644) 2010-06-05 08:46:55.988
Single resource deadlock: blocked by granted enqueue, f 0
Granted global enqueue 0xc0000001f871fa28
----------enqueue 0xc0000001f871fa28------------------------
lock version : 1255997
Owner node : 1
grant_level : KJUSERPR
req_level : KJUSERPR
bast_level : KJUSERNL
notify_func : 0x0000000000000000
resp : 0xc000000119aa9a08
procp : 0xc0000001fac43308
pid : 1812
proc version : 495
oprocp : 0x0000000000000000
opid : 0
group lock owner : 0xc0000001fb82f4a0
possible pid : 1812
xid : 2005-0055-000008D5
dd_time : 0.0 secs
dd_count : 0
timeout : 0.0 secs
On_timer_q? : N
On_dd_q? : N
lock_state : GRANTED
Open Options : KJUSERDEADLOCK
Convert options :
History : 0x14951495
Msg_Seq : 0x0
res_seq : 9476
valblk : 0x00000000000000000000000000000000 .
Requesting global enqueue 0xc0000001f8725980
----------enqueue 0xc0000001f8725980------------------------
日志显示是由于Oracle检查到了一个ges的死锁,于是做了系统级的dump操作,这种系统级的dump出来的内容很多,导致了dump文件特别大。
感觉象是遇到了bug,在metalink找到了现象类似的bug:6145177,bug中描述的现象与医院有些相似,但不完全一样,但打了补丁后,没再出现这个问题。这个问题在10.2.0.4中已经解决了,目前医院使用的是10.2.0.3,Oracle已经提供了单独的Patch,为了避免大的版本升级,就应用了单独的patch,整个过程比较顺利。打了这个patch,一直用到第二天上午,再也没有出现这个错误 ,看来patch已经起了作用。

六.功德还是没法圆满
问题处理的完了,我已经是来玉溪的快一周了。这天中午Y主任过来说道,他跟领导汇报了我们处理的情况,领导相对还是比较满意,晚上宴请我们的人表示感谢。
晚上到了饭店,发现起码有30个人围着一张特别大的桌子,我的第一感觉就是这是不是太夸张了 。后来再得知,这次是财务的人不光请了我们,还请了医保、药品等部门的人,我们只是客人之一。桌子上的最高领导就是医院的财务总监 ,老李介绍说这是Y主任的直接领导。席间医院的人轮番对公司表示感谢云云。。。。我们也只能说我们工作还没做好,对医院的支持表示感谢云云。。。。这种场合喝酒是免不了的,不管你怎么推迟,我还是被半推半就的喝了不少白的。。。
晚上在网上定了第二天中午1点的飞机,由于第二天就是周末了,老李和老陈都要回昆明,我就又一次感受了老李的车技和乘座“小奔”的殊荣。到了昆明11点过了,三个人在机场附近找了一家还算有名的餐厅吃饭,郁闷的是居然在一个素菜里吃到了一支虫子…....这算是对我们几天来工作的小奖励吧!素菜的价格,荤菜的内容。。。功德什么时候才能做圆满呀! …..