一次升级,系统大量报错,交易返回全部超时。
我接手的某汽车融资项目,由于某银行网络方面的要求,互联网架构改为了专线模式。这次改动较小,在测试环境验证了常规的接口功能之后就和对方车厂约定了上线时间,结果上线之后大量异常,各种socat超时错误。生产系统崩溃,业务面临瘫痪。
原因初现:对方接口发送频率从30s一次修改为了1s200次。
看前置转发机日志发现,对方在某一时刻发了大量请求进来,项目只有在前几个有寥寥返回,后面的请求都报超时错误。联系对方确认了对方一次性将200多条有业务需求的接口发送了过来,但是项目服务器资源以及并发处理能力有限,哪怕是调大线程数也收效甚微。看原有的互联网架构前置机日志发现对方发送的频率为30-40s一次,由于对方无法短时改动发送频率以及特定的接口,无奈将专线暂时恢复到了互联网版本。
继续排查,一次交易竟然需要40s一次返回?
继续等待了两天,对方发送频率可以进行手动控制了,先按照原有生产环境的频率发送,我们这边的项目可以正常处理。但是有时业务量比较大,虽然业务时效性不要求那么高,可以按照40s一次的频率发,考虑到未来业务的增长或者月末业务增加,准备将响应速度提升。联系对方按照30s一次、20s一次的频率发,忽然发现接收几个交易之后后面的交易返回越来越慢。进一步排查发现,从接收到对方发送的请求开始,直到给对方响应回去,单笔并发的时间都需要40s左右。调整了发送频率之后,上一个请求还没有响应回去新的请求就又进来,直到同时处理了很多个。
问题定位,为何上传下载文件需要7s之久。
进一步排查发现,某额度查询交易一次上传下载文件时需要的时间竟然需要7s,但是文件本身非常小,只有几十几百k。
某001交易上传文件后文件服务器会返回校验码,这一步需要6s左右,该001交易返回报文之后,还会有一个下载响应文件的操作,这里也需要6-7s左右。该交易有两次和文件服务器关于文件相关的交易,再加上另外一个007交易,上传下载总共三次,就耗时了20s左右。 起初怀疑是网路原因,使用ping -t 命令持续和文件服务器测试了延迟,发现延迟非常低,那问题还是出现了自己项目这块。进一步看代码发现文件上传和下载的地方都是配置的同一路径,找到该路径下发现从2019年项目上线之初到20年末的文件都在该地址下面,将改地址改为一个新的路径,这个问题就解决了。这里的问题是我读取本地文件时由于历史积压的文件过多导致文件读取时效率过慢。
更新操作缓慢,如何具体定位问题?
看上图应该发现了,该交易有两次更新交易流水表的操作,每次都需要5-8s之久,直接影响了交易的响应速度。进一步排查交易流水表的数据,发现以下现象:
该交易流水表的数据总量大约400w条,每年都会新增50w条左右,我搜索了Oracle数据库单表性能相关的数据,发现400w条好像也不多。于是写了一个sql进行测试,查询某个2019年的数据,并更新该记录命中的数据。发现只需要千分之几秒。就开始怀疑是项目本身的问题,从事务和数据库连接池等多个角度进行了思考,冥思苦想搜集资料,最后无可奈何实在是找不到为什么项目更新数据库的时候这么慢,但是数据库更新却这么慢。最后悟到了为什么更新其他表就这么快,可是更新流水表就这么慢?问题还是出现在了交易流水这张表上,继续进行排查。由于plsql查询一次只会出来部分数据,并且按照默认是顺序排列,所以先排列的是2019年的数据。按照交易日期字段倒序排列了一下,找到一条2024年的数据进行测试,发现按照交易流水号查询和更新都需要6s!这里终于复现了项目中出现的问题。
索引优化,更新操作效率提升 99.975% ,几乎接近于极限优化!
既然定位到了是交易流水表中的问题,那接下来就好办了,查询该张表的结构,发现交易流水号既是主键又是索引。接下来查询了该表的B-tree深度blevel为2,索引层级并不深,数据量也并不算特别大,一般来说大于3才需要优化。但是leaf_blocks却达到了17000左右,说明虽然该表的层级不深,可是叶子节点却比较多,索引的存储开销也比较大。
使用以下命令进行优化
索引重建
alter index 具体索引 rebuild online;
功能:此命令会重建指定的索引,通常用于优化索引的存储结构。重建索引会删除原来的索引并重新创建一个新的索引,从而消除索引中的碎片。
影响:
减少碎片:随着数据的插入、更新或删除,索引可能会产生碎片,导致索引叶子节点和内部节点的存储分布不均匀。重建索引会重新组织索引结构,压缩并优化叶子节点的分布,可能导致叶子节点数量减少。
索引压缩:索引重建有时会进行压缩,尤其是当索引的键值有重复数据时。通过压缩,重复的键值会被合并,导致叶子节点的数量减少。
优化存储:索引重建还会使得索引条目更加紧凑,从而减少需要的叶子节点数量。
状态收集:
exec dbms_stats.gather_schema_stats(ownname=> '数据库名',options=> 'GATHER AUTO',estimate_percent => dbms_stats.auto_sample_size,method_opt => 'for all indexed columns' ,degree=> 15,cascade=> true);
功能:这个命令会收集并更新表、索引以及相关对象的统计信息。cascade => TRUE 会导致不仅收集表的统计信息,还会更新所有相关的索引统计信息。
影响:
优化查询计划:虽然统计信息收集本身不会直接改变索引的结构,但它有助于查询优化器生成更有效的查询计划。如果索引的使用变得更加合理,查询优化器可能会选择更合适的索引,从而减少无效的索引条目或冗余的索引数据。
索引的选择性调整:收集新的统计信息可能导致索引的选择性评估发生变化。如果某些索引变得更有效,可能会减少对一些旧的、碎片化的索引块的引用,从而减少叶子节点的数量。
索引整理
alter index 具体索引 coalesce;
功能:此命令用于整理索引,减少索引中的空闲空间并消除碎片。COALESCE 会合并索引块,删除空闲的索引块,但不会像 REBUILD 那样完全重建索引。
影响:
合并空闲空间:索引中的空闲空间会导致额外的叶子节点和内部节点。当索引有很多空闲空间时,这些节点不会被及时回收或合并。COALESCE 会将这些空闲的空间合并,导致叶子节点数减少。
减少碎片:通过整理,索引结构变得更加紧凑,某些分散的条目会被重新组织到更少的叶子节点中,从而减少叶子节点的数量。
优化后:
再次看更新时间已经变为了千分之几秒,索引优化完成。
又出问题,为何速度继续下降?
经过索引优化,该交易的响应速度从30-40s,提升到了2s以内,对方按照3s一次的频率发完全可以满足业务需要。可是第二天忽然发现交易又是大量超时,发现更新交易流水表的操作又重新回到了5-6s。
通过查询V$SQL_PLAN看是否走索引:
可以发现options为full,代表全表扫描,索引失效。
进一步排查更新的sql发现:
由于jylsh字段长度为20,但是项目中实际生成的长度为16,就导致第一次插入进去的jylsh有空格,但是更新时使用trim函数去除了空格导致索引失效。去除trim函数索引优化也解决。
日志影响效率,磁盘io难题如何解决?
又过了一天,交易还是报错大量超时,排查发现从某一时刻开始,某个单独的交易变慢,用了6s左右返回,可是这次不是索引失效,而是平均的慢在了交易的所有环节。
考虑到是日志打印较多,具体的机制还需要结合logback和WinServer 操作系统深入研究,将日志更改为了error级别暂时解决了该问题。
最后使用异步日志的配置,将控制台输出和文件输出都采用了异步配置,经过测试,配置为异步日志之后性能良好,至此,优化全部结束。
使用异步日志时,发现日志打印出来不会显示代码行数,显示的是问号,可以加入堆栈信息解决。
<includeCallerData>true</includeCallerData> <!-- 开启调用栈解析 -->
最后总结,都有哪些注意事项。
经过这次问题解决,发现有一些需要注意的地方。
1.项目上线之后,文件需要定期优化,可以写脚本定时备份清理。
2.数据库索引可能随着数据量的增大导致B-tree层级变深或者叶子节点变多,也需要定期优化。
3.在排查交易流水表的记录时,最开始使用2019年的数据进行测试,以后这种情况应该随机命中多一些记录进行测试。
4.避免索引失效的一些方式:
方案 1:使用 VARCHAR2 代替 CHAR VARCHAR2 是可变长度的,不会自动填充空格,因此推荐在列定义时使用 VARCHAR2。
CREATE TABLE employees (
emp_id NUMBER,
emp_name VARCHAR2(20)
);
方案 2:保持查询和存储一致 查询时包括填充的空格,避免使用函数(如 TRIM),这样可以利用原始索引:
SELECT * FROM employees WHERE emp_name = 'John '; -- 包含空格
方案 3:创建函数索引 如果必须用 TRIM,可以创建基于 TRIM 的函数索引:
CREATE INDEX idx_emp_trim_name ON employees(TRIM(emp_name));
SELECT * FROM employees WHERE TRIM(emp_name) = 'John';
小插曲
我感觉这次优化值得记录一下,于是让ChatGPT给我推荐一些文章标题。
后面的标题果然很标题党,不过我认为搞技术需要务实一点,最后还是选了一个常规的标题。
声明:本文索引优化的影响以及避免索引失效的方式由ChatGPT生成。