前言
根据以往的项目构造,业务层数据库基本使用长连接形式进行批量操作。大部分周期有执行的链接基本正常。再长期的内测中也没有发生CLOSE_WAIT的现象。 上线后采用的数据库使用了新的版本,发现产生CLOSE_WAIY。根据开发经验和网上搜索,发现网上也有相关的开发人员询问ORACLE。但是没有直观答案,全是检查网络的。 也让网络侧的进行了相关查阅配置。根据协议开发经验,我断定是会话层引入了链路保活机制,工作年限十年以内,没有和最初的项目建设人员共事过。不知道以往有没有这个现象。
验证环境
- linux服务器
- oracle 数据-该位DBA设定,我查阅了一边sql.ora 没有什么特殊的配置,应当是默认值
- PRO*C程序
- tcpdump 抓包软件
- wireshark分析
程序构造
多线程,链路长连接。其中有一个专用线程只有产生数据才会入库。 伪代码
void* threadToDBoperator()
{
int ret=0;
while(1){
if(hasdata)
{
ret=DataTooperator(DB);
if(ret==ok)
{
}else
{
release(DB);
connect(DB);
}
}else{sleep(1);}
}
}
可以发现,一旦长期没有数据,那么程序是不会执行数据库操作,因此包括oracle自身的数据库基础链路保活协议都不会执行的。
虽然TCP层也有链路保活机制,但是TCP的机制在缓冲区满的情况是无法及时到达的,因此应用层的感知需要应用层有探测功能。
通过长期无数据的程序中,根据对各个线程增加线程周期执行时间定点日志,确定oralce的保活机制在确认长期无交互的情况回在服务器端主动掐断链路。因此客户端会出现CLOSE_WAIT。
定位方式
线程在执行数据库时进行时间日志打印,用该时间进行定位。我本来想用session的客户端端口定位的,但是找了半天,没有找到客户端提取客户端端口的办法,只能加调测信息,利用数据库的最高权限进行定位,并提取客户端端口
-- 根据最近执行接收的SQL时间断定程序线程的执行时间
-- 先查到程序清单
select TO_CHAR( PREV_EXEC_START,'yyyymmddhh24miss'),port,program from v$session;
-- 例如程序是wechat@hhapp
select TO_CHAR( PREV_EXEC_START,'yyyymmddhh24miss'),port,program from v$session where program like 'wechat@hhapp %' ;
date -d @1678188352 '+%Y-%m-%d %T %z'
已经通过客户端netstat 检测到接收缓冲区有数据残留在CLOSE_WAIT状态下。 也通过抓包看到TNS包结构。 通过sql端口过滤出来的,猜测以下为服务端的链路探测包
程序方面如何应对解决
根据这个现象,目前我认为有两种解决方案, 1.直接长连接,变成短连接接。缺点:一旦需要频繁操作,性能耗在链路重连这边。 2.操作数据前进行保活探测,先执行一下无关的命令,命令失败再进行重连。
SELECT TO_CHAR(SYSDATE,'yyyymmddhh24miss') into :CHECK_TIME FROM DUAL;
附录
数据库的一些操作参数应当是和项目相关性极强的。程序的架构设计也是和项目特征相关性极强。 数据库TCP层阻塞操作。和数据库内部处理无关的。大部分数据库在tcp层设置的都是阻塞读写操作。 与之比较的mysql是在8.0后的版本后面才引入noblock操作。这种必须和项目的重要性严重捆绑。 与平时设计的流量太多选择丢弃数据还是长期阻塞抉择是类似的。
https://docs.oracle.com/cd/E11882_01/network.112/e10835/sqlnet.htm#NETRF227
sqlnet.ora
设置连入数据库后必须在多长时间内完成认证(如:输入用户名/密码),超过此时间没有完成的话,数据库会断开此连接,并将客户端的IP地址和ORA-12170: TNS:Connect timeout occurred错误信息记录到sqlnet.log,而且客户端会收到ORA-12547: TNS:lost contact或ORA-12637: Packet receive failed错误信息。这个设置主要是为了防止denial-of-service攻击
在10.2.0.1.0版本中sqlnet.inbound_connect_timeout参数默认为60秒,即如果连接时间超过60秒则提示超时。
而在其他10G版本中这两个参数默认为0,即无限制。
SQLNET.INBOUND_CONNECT_TIMEOUT
CONNECT_TIME
Specify the total elapsed time limit for a session, expressed in minutes.
IDLE_TIME
Specify the permitted periods of continuous inactive time during a session, expressed in minutes. Long-running queries and other operations are not subject to this limit.
检测超时样例 分钟级别 提示服务端 探测终止连接或客户端终端异常需要断开
SQLNET.EXPIRE_TIME=10
SQLNET.RECV_TIMEOUT=3
SQLNET.SEND_TIMEOUT=3