一、概述
在复杂多样的云数据库使用背景下,上游应用框架百花齐放,连接数据库的方式越来越多,这导致遇到的问题也是千奇百怪;因此相较于传统dba来说,云上dba在深耕技术深度的同时还需要横向拓展多种数据库技术栈、以及简单代码能力来剖析疑难,攻克一个又一个问题。
作为一个有着长期云上数据库运维“老鸟”,在这里分享一个php代码框架中埋的巨坑!近期也是遇到两个大客户反馈,并且长时间影响核心业务,希望大家对php框架默认行为有所了解,在对客户有php应用的时候,能提醒到php中埋的大雷。
二、案例分享
Case 1:某公司2024-02-10反馈核心业务不可用
分析
step 1 看监控:只看监控来看业务量是有变化的,数据库不可用可能跟业务量有关。
现象:2024-02-10 9:30左右业务连接开始上涨,每秒新建连接数超过1000+,cpu波幅不大
step 2:查会话:客户既然说数据库慢,那么慢查、会话管理(processlist)是一定需要看的。
这里查看会话现象就能看到非常关键的两个会话状态:opening tables,removing tmp table,会话执行的sql都是show columns from <table_name>;
step 3:pstack看堆栈:
看到堆栈热点主要是内部临时表相关,新建连接都是在执行show columns创建内存临时表,和客户的业务行为是正匹配的。
create_tmp_table -> instantiate_tmp_table ,主要是临时表过程中出现的问题
解问题:show columns在php程序上关闭了这个行为,后续没有再复现。
总结
问题开始是有业务突增的情况,但是实例cpu、iops等负载没有明显打高,新建连接创建内存临时表导致的,大量堆积会话在做“show columns”,扁鹊中能看到创建内存临时表热点;最后和用户确认到业务代码用到thinkphp框架,默认每次查询操作都会执行show columns操作;“show columns”操作过多导致实例在处理tmp table过程出现了性能瓶颈;
Case 2:某公司切换到polar-M之后出现性能问题
分析
step 1 看监控:业务量是有变化的,但是连接数是成倍的堆积上升。
现象:2024-06-5 12:49左右业务连接开始上涨,活跃链接从10+堆积到50+,cpu增幅20%+
step 2:查洞察:当时客户是开了sql洞察,大量会话执行慢的会话都是在show columns ,执行时间都是在几秒+
有了之前有处理过show columns问题经验,怀疑客户用的php框架(和客户确认后果然是老版本php框架)
查扁鹊:
看到热点仍然在instantiate_tmp_table创建,是和客户thinkphp框架show columns行为匹配的。
热点仍然在instantiate_tmp_table创建
结论
与case是一致的,thinkphp框架的默认show columns行为的导致,在业务量稍有变化会导致“雪崩”,实例夯死。
深入思考
mysql 源码分析
一句话共性分析:两个case都是mysql内核,在业务量稍微变化的前提下,show columns 语句引发的数据库雪崩问题;
Q1:为什么show columns 语句会导致数据库雪崩?
Some query conditions prevent the use of an in-memory temporary table, in which case the server uses an on-disk table instead:
- The SHOW COLUMNS and DESCRIBE statements use
BLOB
as the type for some columns, thus the temporary table used for the results is an on-disk table.
5.7官方文档中说明show columns语句生成是blob字段,所以内部临时表是会使用到物理磁盘临时表。
从扁鹊堆栈调用来看,热点调用堆栈show columns语句会调用instantiate_tmp_table函数生成内部临时表,大量物理磁盘临时表创建导致堵塞,最终引发性能问题。
Q2:既然是内部表创建导致性能问题,是否能通过调参进行优化?
- 默认internal_tmp_disk_storage_engine 设置为innodb,调整参数能有缓解?
- case1中 内部临时表为innodb,case 2 中polardb 参数是 myisam;show columns常见的就是磁盘临时表,并且所以是没有办法通过调整内部临时表的存储引擎来解决;
- 是否能通过调整tmp_table_size参数来优化?
- 当然也是不行的,因为tmp_table_size是限制的内存临时表大小,show columns还是语句创建的磁盘临时表是无法限制的。
The maximum size of internal in-memory temporary tables. This variable does not apply to user-created MEMORY
tables.
所以这个问题就是一个典型的客户行为问题,调优数据库参数是治标不治本,还是要去协调客户去优化php的行为。
Thinkphp源码分析
thinkphp getFields函数会执行"'SHOW COLUMNS FROM",具体调用路径是autoCheckField-->_checkTableInfo-->flush()-->getFields()
#getFields /** * 取得数据表的字段信息 * @access public */ public function getFields($tableName) { $this->initConnect(true); list($tableName) = explode(' ', $tableName); if (strpos($tableName, '.')) { list($dbName, $tableName) = explode('.', $tableName); $sql = 'SHOW COLUMNS FROM `' . $dbName . '`.`' . $tableName . '`'; } else { $sql = 'SHOW COLUMNS FROM `' . $tableName . '`'; } #_checkTableInfo() /** * 自动检测数据表信息 * @access protected * @return void */ protected function _checkTableInfo() { // 如果不是Model类 自动记录数据表信息 // 只在第一次执行记录 if (empty($this->fields)) { // 如果数据表字段没有定义则自动获取 if (C('DB_FIELDS_CACHE')) { $db = $this->dbName ?: C('DB_NAME'); $fields = F('_fields/' . strtolower($db . '.' . $this->tablePrefix . $this->name)); if ($fields) { $this->fields = $fields; if (!empty($fields['_pk'])) { $this->pk = $fields['_pk']; } return; } } // 每次都会读取数据表信息 $this->flush(); } } # flush() /** * 获取字段信息并缓存 * @access public * @return void */ public function flush() { // 缓存不存在则查询数据表信息 $this->db->setModel($this->name); $fields = $this->db->getFields($this->getTableName()); if (!$fields) { // 无法获取字段信息 return false; } $this->fields = array_keys($fields); unset($this->fields['_pk']); foreach ($fields as $key => $val) { // 记录字段类型 $type[$key] = $val['type']; if ($val['primary']) { // 增加复合主键支持 if (isset($this->fields['_pk']) && null != $this->fields['_pk']) { if (is_string($this->fields['_pk'])) { $this->pk = array($this->fields['_pk']); $this->fields['_pk'] = $this->pk; } $this->pk[] = $key; $this->fields['_pk'][] = $key; } else { $this->pk = $key; $this->fields['_pk'] = $key; } if ($val['autoinc']) { $this->autoinc = true; } } } // 记录字段类型信息 $this->fields['_type'] = $type; // 2008-3-7 增加缓存开关控制 if (C('DB_FIELDS_CACHE')) { // 永久缓存数据表信息 $db = $this->dbName ?: C('DB_NAME'); F('_fields/' . strtolower($db . '.' . $this->tablePrefix . $this->name), $this->fields); } }
Q1:为什么Thinkphp会执行show columns?什么情况会执行?
从源码来看,thinkphp是在每次执行DML语句的时候,都会执行show columns的语句;好家伙,php本来就是短连接模型,没有连接池,也就是每个请求在执行sql前都会show columns,也就能理解稍微业务有业务上涨,就扛不住了?
Q2:如果关闭 or 减少 show columns 语句执行?
调用路径:autoCheckField-->_checkTableInfo-->flush()-->getFields()
也就是每次会检查表字段是否有缓存,只需要提前生成字段缓存就好了;
thinkphp源码分析结论:
- 生成字段缓存是thinkphp默认行为,本身无法关闭
- 调试模式下会每次重新加载缓存(show columns),会导致执行频率大大增加,所以得关闭调试模式。
- 提前手动加载字段缓存,runtime/schema 目录下面按照数据表生成字段缓存文件
建议与总结
该问题是一个典型thinkphp框架使用mysql风险隐患,在这里引出分享一些mysql内部临时表相关知识点,大家在深入客户业务架构同时,检查风险or快速解决。
步骤1:关闭调试模式 #app.php 'app_debug' => false, database.php 'debug' => false, 步骤2:手动加载缓存文件,可以在runtime/schema检查问阿金是否生产。 php think optimize:schema