连接数据库异常,看报错信息 2ms 之前该连接还正常可用,所以不存在连接空闲时间过长被服务端关闭的问题。程序基于 spring boot,连接数据库组件为 harika,默认 IDLE_TIMEOUT
= 10分钟,默认 MAX_LIFETIME
=30分钟。
程序架构如下:
其中 ProxySQL 版本为 2.3.2,MySQL 版本为 8.0.27。
经分析,当时该报错 sql 按对应的 proxysql 路由规则走从库,查看从库的监控如下: 发现 1:13 左右,数据库的连接数从 200 急剧下降到 100 以下。
查看 proxysql 的监控如下: proxysql 和后端数据库的连接数大幅减少,但客户端连接 proxysql 的连接数几乎没有变化。
注:以上客户端连接大幅超过后端连接,一是因为客户端连接是所有的 host group,二是 proxysql 默认采用了连接复用(• Connection Multiplexing),即允许多个客户端连接重复使用同一个后端连接。
查看 proxysql 的日志如下:
Shunning Server,节点临时被剔除,可能因后端 too many connections error,或者超过了可容忍延迟阀值 max_replication_lag。结合错误日志 with replication lag of 709 second 以及 detected a lagging server during query,可以确定是数据库主从延迟过大。
查看当时的从库延迟可确认该情况。
继续查看 ProxySQL 配置的最大主从延迟时间为 500s,因此超过时,ProxySQL 把从节点临时下线。经查,凌晨 1 点左右有个长事务执行(历史数据归档),大概需要 10 分钟左右。
ProxySQL 设置从库的状态为 SHUNNED,此时 ProxySQL 和从节点的连接失效,按路由规则,新的请求(新的客户端连接)应该要走主库。
正常请求发送时,客户端与 ProxySQL 建立连接,ProxySQL 与数据库建立连接,关联关系如下:
当节点 SHUNNED 时,客户端的连接还正常,会继续复用连接发送请求,之后找到对应的 ProxySQL 与从库的连接,但此时由于从库节点状态为 SHUNNED,会直接返回报错连接不可用,同时销毁 ProxySQL 和从库之间的这条连接。
查看源码验证下,以下为相关代码:
ini
/**
* 判断 rc 是否等于 -1,如果是,则执行方法进行错误处理
* handler_ProcessingQueryError_CheckBackendConnectionStatus
*/
int MySQL_Session::handler() {
...
handler_again:
switch (status) {
case PROCESSING_STMT_PREPARE:
case PROCESSING_STMT_EXECUTE:
case PROCESSING_QUERY:
int rc;
rc = RunQuery(myds, myconn);
if (rc==-1) {
int rc1 = handler_ProcessingQueryError_CheckBackendConnectionStatus(myds);
if (rc1 == -1) {
handler_ret = -1;
return handler_ret;
}
}
break;
}
}
int MySQL_Session::RunQuery(MySQL_Data_Stream *myds, MySQL_Connection *myconn) {
switch (status) {
case PROCESSING_QUERY:
rc=myconn->async_query(myds->revents, myds->mysql_real_query.QueryPtr,myds->mysql_real_query.QuerySize);
break;
}
return rc;
}
/**
* 当节点离线时,返回 -1
*/
int MySQL_Connection::async_query(short event, char *stmt, unsigned long length, MYSQL_STMT **_stmt, stmt_execute_metadata_t *stmt_meta) {
if (IsServerOffline())
return -1;
...
}
/**
* 当节点处于 MYSQL_SERVER_STATUS_SHUNNED_REPLICATION_LAG 状态时,返回 true
* 即认为该节点已离线
*/
bool MySQL_Connection::IsServerOffline() {
bool ret=false;
if (parent==NULL)
return ret;
server_status=parent->status; // we copy it here to avoid race condition. The caller will see this
if (
(server_status==MYSQL_SERVER_STATUS_OFFLINE_HARD) // the server is OFFLINE as specific by the user
||
(server_status==MYSQL_SERVER_STATUS_SHUNNED && parent->shunned_automatic==true && parent->shunned_and_kill_all_connections==true) // the server is SHUNNED due to a serious issue
||
(server_status==MYSQL_SERVER_STATUS_SHUNNED_REPLICATION_LAG) // slave is lagging! see #774
) {
ret=true;
}
return ret;
}
继续查看 handler_ProcessingQueryError_CheckBackendConnectionStatus 方法:
rust
// this function returns:
// 0 : no action
// -1 : the calling function will return
// 1 : call to NEXT_IMMEDIATE
int MySQL_Session::handler_ProcessingQueryError_CheckBackendConnectionStatus(MySQL_Data_Stream *myds) {
MySQL_Connection *myconn = myds->myconn;
// the query failed
if (
// due to #774 , we now read myconn->server_status instead of myconn->parent->status
(myconn->server_status==MYSQL_SERVER_STATUS_OFFLINE_HARD) // the query failed because the server is offline hard
||
(myconn->server_status==MYSQL_SERVER_STATUS_SHUNNED && myconn->parent->shunned_automatic==true && myconn->parent->shunned_and_kill_all_connections==true) // the query failed because the server is shunned due to a serious failure
||
(myconn->server_status==MYSQL_SERVER_STATUS_SHUNNED_REPLICATION_LAG) // slave is lagging! see #774
) {
if (mysql_thread___connect_timeout_server_max) {
myds->max_connect_time=thread->curtime+mysql_thread___connect_timeout_server_max*1000;
}
bool retry_conn=false;
/**
* 当节点状态为 MYSQL_SERVER_STATUS_SHUNNED_REPLICATION_LAG 时,打印错误日志
* 和 proxysql 错误日志匹配上了
*/
if (myconn->server_status==MYSQL_SERVER_STATUS_SHUNNED_REPLICATION_LAG) {
thread->status_variables.stvar[st_var_backend_lagging_during_query]++;
proxy_error("Detected a lagging server during query: %s, %d\n", myconn->parent->address, myconn->parent->port);
MyHGM->p_update_mysql_error_counter(p_mysql_error_type::proxysql, myconn->parent->myhgc->hid, myconn->parent->address, myconn->parent->port, ER_PROXYSQL_LAGGING_SRV);
}
// 销毁和 MySQL 的连接
myds->destroy_MySQL_Connection_From_Pool(false);
return -1;
}
return 0;
}