「站点慢」几乎总会落到数据库上。而如果按正确的顺序去查,只需几分钟:先看正在慢的时候才看得到的,再看已经积攒了好几周的。

要在问题正在发生的时候看

基本原则是:最有价值的信息恰恰在一切正慢的时候才拿得到。MySQL 的第一条命令:

sudo mysqladmin processlist
mysql -e 'SHOW FULL PROCESSLIST'

PostgreSQL 的:

sudo -u postgres psql -c "SELECT pid, state, wait_event, now()-query_start AS dur, query
FROM pg_stat_activity WHERE state != 'idle' ORDER BY dur DESC;"

你会看到此刻什么在跑、跑了多久。而答案通常立刻就有:某一条重查询把其余的全挡住了,或者一百条一样的查询在等一把锁被放开。

连接数

mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'"
mysql -e "SHOW GLOBAL STATUS LIKE 'Max_used_connections'"
mysql -e "SHOW VARIABLES LIKE 'max_connections'"

如果 Max_used_connections 正在逼近 max_connections,那么应用时不时会收到「too many connections」的拒绝。而抬高上限不该是第一件事:每个连接都占内存,而在一台内存本就紧张的服务器上抬高它只会加快走向 swap 的进程。请先弄清连接为什么没被释放:通常是某条长查询,或者应用侧没有连接池。

另外也请从另一面看:连接数的骤增可能并非来自人气上涨,而是来自某些机器人在猛敲一个很重的页面——某个搜索、列表过滤器、报表生成器。

慢查询

没有慢查询日志就无从下手。MySQL:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

一天之后:

sudo mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log

PostgreSQL,在 postgresql.conf 里:

log_min_duration_statement = 1000

然后是 pg_stat_statements 扩展,它直接给出每条查询的累计耗时:

SELECT calls, round(total_exec_time) AS total_ms, query
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

请注意:要紧的不是最长的那条查询,而是累计耗时最大的那条。一条 50 毫秒、每分钟跑一千次的查询,比一天跑一次的十秒报表危害更大。

最常被忘掉的那项设置

对 MySQL 和 MariaDB 来说,那就是 InnoDB 缓冲池的大小。默认值是 128 兆,几十年没变过。而在一台数据库占了好几个 G 的服务器上,这意味着不断地从磁盘读而不是从内存读。

mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'"

对一台专用的数据库服务器来说,合理的标尺是内存的一半;而对一台同时跑着 Web 服务器和 PHP 的服务器来说是四分之一,并要留意别掉进 swap。而单单这一项设置,通常比花一周优化查询给的还多。

两样悄悄吃掉磁盘的东西

MySQL 的二进制日志。它们用于复制和按时间点恢复,会不断增长,而在默认设置下有时根本不会被删除。检查方法:

sudo du -sh /var/lib/mysql/*bin.*
mysql -e "SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'"

如果你没有复制,也不使用按时间点恢复,那就把保留期设成合理的几天。

带着被遗忘的复制槽的 PostgreSQL WAL 文件。一种较少为人所知也更危险的情形:如果存在一个复制槽而它的消费者已经不再连上来,那么 PostgreSQL 就有义务保留从断开那一刻起的每一份日志——而且会一直保留到磁盘用尽为止。

sudo -u postgres psql -c "SELECT slot_name, active, restart_lsn FROM pg_replication_slots;"

这样一个谁也不需要的非活动槽应当被删掉。而这是数据库确确实实会把磁盘清到零的少数情形之一。

体积与增长

SELECT table_schema, round(sum(data_length+index_length)/1024/1024) AS mb
FROM information_schema.tables GROUP BY table_schema ORDER BY mb DESC;

而有用的不是当前的数字,而是它的变化速度。一个在流量没有相应增长的情况下一个月翻倍的数据库,通常意味着某张记录表在被不断写入而从不清理——会话、任务历史、某个插件的事件日志。

两行字的安全检查

既然数据库控制台已经开着,那就有两件事值得查一下。第一,数据库从外部够不够得着(ss -tulpn | grep 3306):它几乎永远不该朝外。第二,那些可以从任意地址连接的账号:

mysql -e "SELECT user, host FROM mysql.user"

一条 host = '%' 的记录意味着「从任何地方都能连」。而如果端口也开着,那么数据库和互联网之间就只剩一个密码。

备份

最后一点。有备份文件本身什么也不保证;能保证的只有经过验证的恢复。一份从未恢复过的副本,被截断、带着锁错误取得、或者里面装的是别的库,这几种可能性一样大。而这必须提前在另一台机器上验证,而不是在需要用到它的那一天。

对日常观察来说三个数字就够了:连接数、每天的慢查询数量,以及数据库的体积。三者的变化都缓慢且可预测,而其中任何一个的急剧动作都值得瞥一眼。它们在一个页面上是什么样子,见下面的演示。