最近尝试对博客进行转型,继续发硬核技术帖的同时,会增加一些贴地气的内容。如果说以前使用Hexo、Hugo本质上是为了解决穷的问题,那么现在换回了WordPress是为了解决数据运营的闭环问题。纯静态博客有很多的好处,但是内容和数据成了一个个孤岛,整合起来需要做一定的开发工作,从投入产出来看,换用WordPress是一个比较具有性价比的选择,虽然它很重,有些地方又很ugly,响应速度会慢一些,但是它的生态提供的扩展能力和业务连续性,目前找不到替代品。

所以,这篇文章,记录一下安装WordPress的过程。这篇文章从写完到发布,间隔了大概一个多月的时间,就是希望实际上线测试一段时间看看,再公开教程,避免遗留一些坑给大家踩,截止到发文时,整个系统应该是已经趋于稳定了,满足需求的同时,每年成本在500元左右。

当你看到这句话的时候,我已经从WordPress回滚到Hugo了,不为什么,只能说WordPress还是太难用了。虽然我已经重新回到了Hugo,但是不妨碍这篇文章给想要装WordPress的朋友参考,所以这篇文章我也不下掉了,就放在这里供大家有需要的人参考。

写在开始

建议使用Ubuntu 24.04.1 LTS系统,不再推荐使用Rocky Linux、Alma Linux这些系统,原因可以查看后文。由于我一开始就是使用Rocky Linux安装的,因此接下来的文章就按照这个系统写的命令,对于新开始的朋友,建议使用Ubuntu系统,参考下文的命令改动一下,大多数都能直接用。

建议先一口气看完文章,再进行操作,因为后文有很多纠正了前文的操作,因此中间有些操作可能是不必要的。这也是我一边操作一边写文章的遗留问题,没有办法一步到位,只能做了哪些记录哪些。

存储选型

一开始我选择使用阿里云的NFS存储来放数据库、WordPress的文件,Docker镜像也在其中,后来发现博客打开速度非常慢,一个页面差不多要等3-5秒左右才能打开,排查发现是CPU的iowait非常多,NFS成瓶颈了,因此这里不建议使用NFS。我在系统盘之外专门开了一块ESSD(PL0)的云盘存数据库、WordPress的文件,Docker镜像就直接放在系统盘中了。

其实这里也没太多必要分两个云盘,合在一起是完全可以的,只是我的云主机内存比较小,如果swap和数据库文件在一个盘上,经验来看内存满的时候会把磁盘负载打满,系统盘和数据盘切割开来一定程度上有助于控制爆炸半径,提供一个上机排错的机会。

配置docker

安装docker后,禁用掉docker自带的bridge网络和iptables规则。这里我并不推荐使用docker的-p参数暴露端口,这样改动iptables规则后重启服务,就会把docker加的规则给清掉了,导致容器网络出现问题,所以一开始就禁掉。

cat > /etc/docker/daemon.json << EOF
{
    "iptables": false,
    "bridge": "none",
    "log-driver": "local",
    "log-opts": {
        "max-size": "1m"
    }
}
EOF

然后重启docker服务以生效。

systemctl restart docker.service

创建一个docker网络,不用默认的bridge。

docker network create 
  --driver=bridge 
  --subnet=192.168.1.0/24 
  --gateway=192.168.1.1 
  xuegao

安装mysql

由于WordPress的表都是InnoDB类型,因此这里着重优化一下InnoDB,然后把不用的功能关了省点内存。

[mysqld]
# 禁用性能统计,反正部署上了就不会管了,关了这个功能省点内存
performance_schema = off
# WordPress都是用的InnoDB类型,这里着重优化InnoDB
# 默认128M,这里减小pool大小,注意控制在docker的内存限额内
# 不要256M,实测会OOM。
innodb_buffer_pool_size = 64M
# 默认64M,但是WP没有这么大的事务,给16M就行了,超过的刷磁盘去
innodb_log_buffer_size = 16M
max_connections = 500

character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

bind_address = 0.0.0.0
mysqlx=off

# 不需要binlog
skip-log-bin

# default
host-cache-size=0
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql

pid-file=/var/run/mysqld/mysqld.pid
[client]
socket=/var/run/mysqld/mysqld.sock

!includedir /etc/mysql/conf.d/

初始化mysql容器,让mysql生成相关的文件。

docker run --restart=unless-stopped --name mysql \
    -v /data/mysql/data:/var/lib/mysql \
    -v /data/mysql/config:/etc/mysql \
    -e MYSQL_ROOT_PASSWORD="<填root密码>" \
    --network=host \
    -d mysql:8.4.2

然后停止并重新创建mysql容器,就可开始使用了,这样可以避免把root密码留在环境变量中。

docker run --restart=unless-stopped --name mysql 
    -v /data/mysql/data:/var/lib/mysql 
    -v /data/mysql/config:/etc/mysql 
    --memory 512m 
    -e TZ="Asia/Shanghai" 
    --network xuegao 
    --ip 192.168.1.2 
    -d mysql:8.4.2

为了方便管理,再创建一个phpmyadmin的容器,不过不用的时候最好停了,省点内存。

docker run --restart=unless-stopped --name pma 
    -e PMA_HOST=192.168.1.2 
    -e TZ="Asia/Shanghai" 
    --memory 512m 
    --network=xuegao 
    --ip 192.168.1.3 
    -d phpmyadmin:5.2.1

安装WordPress

修改上传文件的大小限制和执行时长限制,注意记下这两个数值,后边在nginx和CDN中都需要配置。

cat > /data/wordpress/config/php/uploads.ini << EOF
file_uploads = On
memory_limit = 32M
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
EOF

启动WordPress容器。

docker run --restart=unless-stopped --name wordpress 
    -v /data/wordpress/data:/var/www/html 
    -v /data/wordpress/config/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini 
    --memory 1024m 
    -e WORDPRESS_DB_HOST="192.168.1.2" 
    -e WORDPRESS_DB_USER="数据库用户名" 
    -e WORDPRESS_DB_PASSWORD="数据库密码" 
    -e WORDPRESS_DB_NAME="数据库名" 
    -e TZ="Asia/Shanghai" 
    --network xuegao 
    --ip 192.168.1.11 
    -d wordpress:6.6.2-apache

配置防火墙

首先在阿里云的防火墙上只放开ICMP和TCP 80端口,我这里有堡垒机,就走内网SSH管理了,公网只需要开一个80口。

安装防火墙相关的服务,以便开机能自启动。

dnf remove firewalld
dnf install ipset-service iptables-service

然后添加CDN的所有IP。

ipset create permit80 hash:net family inet hashsize 1024 maxelem 1000000

# 添加所有的允许的来源IP地址。
ipset add permit80 X.X.X.X/24

# 保存
ipset save > /etc/sysconfig/ipset

配置防火墙,把除了CDN的来源IP全部拦掉。直接把下边的内容贴到/etc/sysconfig/iptables文件中即可,filter表中的规则是拦非CDN来源的IP的,nat表的是docker容器出网规则。这里的filter中的规则主要是拦掉不可信来源的流量,不会拦SSH流量,如果需要开SSH,需要去阿里云的安全组放开。

# Completed on Mon Oct 14 20:58:07 2024
# Generated by iptables-save v1.8.10 (nf_tables) on Mon Oct 14 20:58:07 2024
*filter
:INPUT ACCEPT [25:1732]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [17:2408]
-A INPUT -i eth0 -p tcp -m tcp --dport 80 -m set ! --match-set permit80 src -j DROP
COMMIT
# Completed on Mon Oct 14 20:58:07 2024
# Generated by iptables-save v1.8.10 (nf_tables) on Mon Oct 14 20:58:07 2024
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -d 100.64.0.0/10 -o eth0 -j SNAT --to-source 100.64.11.101
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT

重启两个服务,并设置开机自启动。

systemctl restart ipset.service 
systemctl restart iptables.service

systemctl enable --now ipset.service
systemctl enable --now iptables.service

配置反向代理

这里我加了一层nginx做反代,这样更新时可以先启动一个新容器,确保更新成功后再删除老容器,让nginx自动切换上游即可,这样不会影响访问,其实就是K8S的滚动更新。其次,因为配置了virtual host,所以可以支持多个网站同时跑。

源站和CDN都是IDC的,中间不太可能会被运营商劫持,因此我也懒得配置HTTPS了,CDN分发的时候再打开HTTPS就行。

直接dnf安装nginx。

dnf install -y nginx

打开日志滚动,免得把存储撑爆了。

systemctl enable --now logrotate.service

修改nginx的配置。

# cat /etc/nginx/nginx.conf

user nginx;
# 设置为2个线程(和CPU统一)
worker_processes 2;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    # 加大连接数
    worker_connections 2048;      
}                                  

http {
    # 注意我在这里加了个 $request_time ,以便记录下来PHP的执行时间,后续慢的话可以有东西拿来分析
    log_format  main  '$remote_addr - $remote_user [$time_local] $request_time "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    # 删掉其他没用的配置,只保留下边这一段。
    server {
        listen       80;
        listen       [::]:80;
        server_name  _;

        # 没有匹配到virtual host的,全部都返回404
        location / {
            return 404;
        }
    }
}

然后加WordPress的反代。192.168.1.12:80的上游,仅在更新容器时才需要启用,默认情况下不建议启用,不然感觉响应延迟会增加。

# cat /etc/nginx/conf.d/wordpress.conf 
upstream wordpress_backend {
    server 192.168.1.11:80;
#    server 192.168.1.12:80;
}

server {
    listen 80;
    server_name blog.xuegaogg.com;

    client_max_body_size 100M;

    location / {
        proxy_pass http://wordpress_backend;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 10s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

测试一下没问题。

# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

然后就可以启动nginx了。

systemctl enable --now nginx.service

不保存历史命令

为了防止命令里泄露一些凭据,使用如下命令可以不记录历史命令到文件中。

> ~/.bash_history && chattr +i ~/.bash_history

配置WordPress计划任务

WordPress插件的自动备份等一些功能需要依赖wp-cron.php,但是在容器中这个脚本似乎不会被定期执行,需要我们手工调起。

在主机上添加cronjob任务,定期触发容器中的计划任务执行。

# crontab -l
* * * * * docker exec -it wordpress curl http://127.0.0.1/wp-cron.php?doing_wp_cron

安装netdata监控

单机监控,注意netdata开源版的没有登录认证的,不要暴露到公网上。我是绑定在了127.0.0.1上,需要查问题的时候,通过SSH端口转发访问。

curl https://get.netdata.cloud/kickstart.sh > /tmp/netdata-kickstart.sh && sh /tmp/netdata-kickstart.sh --no-updates --stable-channel --disable-telemetry

有了这个东西之后,服务出现问题就有数据可以回溯了,不然都不知道啥原因挂了。

WordPress配置

修改WordPress下的wp-config.php文件,在stop editing这行注释前添加配置。

// 强制HTTPS,以避免配合CDN一起用时提示重定向的次数过多
// 只有在开启了CDN和HTTPS才需要配置
$_SERVER['HTTPS'] = 'on';
define('FORCE_SSL_LOGIN', true);
define('FORCE_SSL_ADMIN', true);

// 关闭自动更新
define('WP_AUTO_UPDATE_CORE', false);

/* That's all, stop editing! Happy publishing. */

WordPress推荐插件

  • Block Specific Plugin Updates 禁用指定插件的更新。

  • Code Block Pro 代码高亮。

  • Disable & Remove Google Fonts 禁用并移除Google字体。有一些字体会使用Google字体的CDN,这个地址在大陆打开非常慢,会导致网页一直卡在渲染阶段,所以不如直接禁用了。

  • Disable User Gravatar 禁用用户的Gravatar头像。设置里现在可以关了,不用这个插件了,而且这个插件现在已经不维护了。

  • Rank Math SEO SEO插件。有很多高级SEO功能,我到现在还没摸完,强推。其中有一个重定向功能,可以把迁移前的链接添加进来,这样就可以保障迁移后搜索引擎不会检索丢了。

  • Smush 图片优化和懒加载插件,可以自动优化图片大小,效果非常显著。

  • Wordfence 安全 可以配置一些安全配置,并且支持漏洞扫描、登录时二步验证,强烈推荐。

  • WP Mail SMTP 支持SMTP更多参数的外发邮件。企业微信的邮箱用这个插件就可以使用了。

  • WP Statistics 统计插件。老牌统计插件,我从2015年就开始用WordPress了,那时候这个插件非常干净,现在搞了一堆收费项目牛皮藓,功能是比较完善,但是在找替代品了。找了几个月,没找到替代品,它DataPlus支持Google Analytics那样的行为分析,是一个很好的运营工具,除了贵其他都挺好。

  • UpdraftPlus - Backup/Restore 备份插件。每天往OSS备份数据,以防万一。

  • WP微信 支持微信注册和登录。有点难用,已经卸载。

  • 微信公众号引流 基于网上找的一个插件修改而来,支持关注公众号获取验证码再查看内容。做博客10年了,学生时代都是靠着饭钱买的服务器,现在成本日趋上升,本身对内容有信心,是时候该做点转变了。

  • 简易目录 自动生成目录的插件。

排查性能问题

部署完成之后,安装插件发现非常卡,排查发现CPU iowait非常高,看来是NFS的性能差,换成本地盘就好多了。

再排查一些接口,仍然需要半秒左右才能打开,抓包看发现是数据库请求太多了。

中间截掉了很多内容,主要关注查数据库的时间就好了,发起了N多次数据库操作,耗费了0.3s。

一个请求竟然有这么多查询?看了一下差不多有80多个,这性能也太难好到哪去了,查了一下StackOverflow,这个是WordPress的常规操作,只能加层缓存缓解一下。

那就安装个redis好了,先打配置。

cat > /data/redis/config/redis.conf << EOF
bind 0.0.0.0
tcp-backlog 4096
timeout 0
tcp-keepalive 30
supervised no
loglevel notice
logfile ""
maxclients 10000
# 256MB,超过之后就报错
maxmemory 268435456
io-threads 4
EOF

然后启动容器。

docker run --restart=unless-stopped --name redis 
    -v /data/redis/data:/data 
    -v /data/redis/config:/config 
    --memory 512m 
    -e TZ="Asia/Shanghai" 
    --network xuegao 
    --ip 192.168.1.4 
    -d redis:7.4.1 
    redis-server /config/redis.conf

由于WordPress的官方镜像默认不带redis的PHP模块,这里需要手工打进去,先创建一个Docker构建脚本。

cat > Dockerfile.wordpress << EOF
FROM wordpress:6.6.2-apache

RUN pecl install redis && 
    docker-php-ext-enable redis
EOF

然后开始构建。

docker build -f Dockerfile.wordpress -t wordpress:6.6.2-apache-redis --network host .

然后使用这个新的镜像重启WordPress容器,就可以生效了。

为了减少查询次数,我安装了一个Redis Object Cache Pro的插件,SQL的查询量直接砍半,查询速度确实有一定的提升,但是不是特别显著。

经过几天的观察,发现这玩意效果实在是不显著,还得多维护一个redis,因此干掉。看来WordPress想快只有生成静态页面这一条路走了。

服务器卡死问题

刚装机没几天,就挂了。晚上回来发现SSH连接不上了,看了一下阿里云的监控,发现磁盘打满了,CPU也拉满了,从经验来看,大概率是什么应用把内存用满了,导致了内核在不停地驱逐页,但是又有服务在运行,就又一直装载页,就把磁盘load爆了。

但是,MySQL我已经关掉很多特性了,是啥玩意会把内存搞满?看了一下日志,原来是dnf-makecache.timer搞的,印象里dnf一直内存用量很高,这次直接把系统搞挂了,自此之后我应该不会再开这玩意了。

systemctl disable --now dnf-makecache.timer

这个问题在GitHub和Bugzilla都有收录,都可以追溯到2020年了。

https://github.com/rpm-software-management/dnf/issues/2010

连夜把我所有的系统禁用掉这个功能。

解决PHP内存泄露问题

只关注1点半这会就行,后边数据乱了是因为我重启了服务。只要有持续的请求进来,机器的内存消耗就一直会增长,1点半这里有一个小的凸起,就是因为我又进了控制台执行了一些操作,可以看到Active(file)Active(anon)两个都是增长的,Active(file)增长不必担心,因为文件背景的page能够在触发内核水位线的时候被驱逐,Active(anon)增长就需要特备注意,因为是匿名的page,没有办法驱逐到文件中,如果有swap的话可以临时换到swap中,但是如果匿名page一直增加的话,迟早会把内存打爆的。

然后看一下内存排名,果然是httpd用的最多,不出意外的话就是mod_php泄露了。再确认一下,httpd在1点半左右内存使用确实有增加。

这个问题很多年前就遇到过了,能确定的就是PHP内存泄露了,暂时没有更好的解决办法,让httpd定期回收进程是一个比较可靠的解决办法,只是没想到这个问题这么多年了还么解决。

切换httpd的并发处理为prefork模式,不要用prefork之外的模式,不然还是会泄露。这里我用的镜像里的httpd默认就已经是在使用mpm_prefork,因此我只需要改一下参数即可。

下边的命令中创建了一个新的mpm_prefork.conf文件,后边将会挂进WordPress容器中,以便控制内存使用。这个文件相对WordPress容器中的文件,缩减了httpd的并发数,改动了MaxConnectionsPerChild,从0设置为了100,即每个prefork的进程只要处理过100个请求后,就必须销毁并开启新的进程,那么即便内存有泄露,进程销毁之后也就不复存在了,泄露问题自然就解决了。

特别注意,下面的参数还是有点大,仍然会导致OOM,因此实际操作时这一步可以跳过,看下边的文章有新的配置。

cat > /data/wordpress/config/apache2/mpm_prefork.conf << EOF
# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves

StartServers            5
MinSpareServers         5
MaxSpareServers         10
MaxRequestWorkers       50
MaxConnectionsPerChild  100
EOF

然后把这个配置文件挂进WordPress容器中。

docker run --restart=unless-stopped --name wordpress 
    -v /data/wordpress/data:/var/www/html 
    -v /data/wordpress/config/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini 
    -v /data/wordpress/config/apache2/mpm_prefork.conf:/etc/apache2/mods-available/mpm_prefork.conf 
    --memory 1024m 
    -e WORDPRESS_DB_HOST="192.168.1.2" 
    -e WORDPRESS_DB_USER="数据库用户名" 
    -e WORDPRESS_DB_PASSWORD="数据库密码" 
    -e WORDPRESS_DB_NAME="数据库名" 
    -e TZ="Asia/Shanghai" 
    --network xuegao 
    --ip 192.168.1.11 
    -d wordpress:6.6.2-apache

可以看到,在这样改完配置之后,再持续使用WordPress,内存用量就不会持续增长了,Active(anon)开始出现了下降,整体就维持在了一个比较正常的水平。

内核task hung住

正准备夸奖PHP现在比5年前我用它时稳定了一些,它就出问题了。

问题表现为运行WordPress的容器完全无法kill掉,但是httpd的进程已经全部没有了,容器网络也已经不通了,内核日志里刷出来了task hung的内容,我心头一凉,难道迁移要失败了?

[280523.782810]  </TASK>
[280523.782814] INFO: task apache2:1167474 blocked for more than 245 seconds.
[280523.783241]       Tainted: G               X  ------- --- 5.14.0-427.37.1.el9_4.x86_64 #1
[280523.783775] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[280523.784254] task:apache2         state:D stack:0     pid:1167474 ppid:399804 flags:0x00000002
[280523.784257] Call Trace:
[280523.784258]  <TASK>
[280523.784259]  __schedule+0x21b/0x550
[280523.784261]  schedule+0x2d/0x70
[280523.784263]  d_alloc_parallel+0x385/0x3f0
[280523.784265]  ? __pfx_default_wake_function+0x10/0x10
[280523.784269]  lookup_open.isra.0+0x285/0x570
[280523.784272]  open_last_lookups+0x176/0x3d0
[280523.784274]  ? path_init+0x2c5/0x3f0
[280523.784277]  path_openat+0x89/0x280
[280523.784280]  do_filp_open+0xb2/0x160
[280523.784284]  ? __check_object_size.part.0+0x47/0xd0
[280523.784287]  do_sys_openat2+0x96/0xd0
[280523.784289]  __x64_sys_openat+0x53/0xa0
[280523.784292]  do_syscall_64+0x5c/0x90
[280523.784294]  ? exit_to_user_mode_prepare+0xec/0x100
[280523.784298]  entry_SYSCALL_64_after_hwframe+0x72/0xdc
# 省略了一些内容
[280523.784312]  </TASK>

看主机上的进程,容器里的httpd进程已经没有了。

# ps -ef | grep httpd
root     1174253 1170046  0 18:44 pts/0    00:00:00 grep --color=auto httpd

docker看容器还在运行中,但是不论如何停止都停不掉了。

docker停止、kill容器都失败了。

# docker restart wordpress 
Error response from daemon: Cannot restart container wordpress: tried to kill container, but did not receive an exit event

nginx的反向代理日志直接报502错误。

125.X.X.X - - [21/Oct/2024:18:46:17 +0800] 0.000 "GET /wp-admin/admin.php HTTP/1.1" 502 559 "https://blog.xuegaogg.com/wp-admin/admin.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" "X.X.X.X"

docker的日志直接报kill容器失败。

# journalctl -u docker.service --follow 
Oct 21 18:45:43 xuegao-blog dockerd[850]: time="2024-10-21T18:45:43.627858872+08:00" level=error msg="Handler for POST /v1.47/containers/wordpress/restart returned error: Cannot restart container wordpress: tried to kill container, but did not receive an exit event" spanID=xxxxxxx traceID=xxxxxxxx

这些日志看不出来问题,只能再从netdata找数据看看了。首先缩小一些时间窗口,发生这个事件的事件窗口在18点30分到18点40分,就直接选择这个时间窗口的数据。

可以看到,在18点33分前后,system消耗了大量的CPU,紧接着iowait就把整机打满了,这可是固态啊,能这么容易打满,绝对不是应用能够导致的了。

这时候需要找一下iowait主要发生在哪块硬盘了,因为有一块是系统盘,一块是数据盘。由于netdata中似乎没有区分磁盘,我从zabbix找到了数据,vda打满了,也就是系统盘打满了,不出意外又是swap。

可以看到,18点33前后,刚好是swap置换高峰的时候,swap用量也在上升,所以确定是swap导致的问题没得跑了。

但是,为啥这次的问题这么温和?以往每逢swap把CPU打满,一定会导致SSH都登录不上,这次的不仅能登录上,甚至监控数据都还能看到,只是httpd也确实挂了没有办法恢复了,只能通过重启解决,好歹留了个现场我,难道这就是容器的好处?

进一步去看磁盘的IOPS,18点33分这里,vda的IOPS直接被打到了1900,直接把云盘拉满了。

再看CPU硬件中断,可以看到,在18点33分16秒这个高峰这里,CAL、TLB分别有19K、10K的中断次数。

netdata官方给出了这些指标的解读,CAL为内核的函数调用中断,TLB为CPU的VM地址查物理地址中断。

再看软中断,18点33分16秒这个高峰这里,BLOCK、SCHED分别有1.9K、0.7K的中断次数。

netdata官方给出了这些指标的解读,BLOCK为IO设备的阻塞,SCHED为调度更高优先级活动。

再看18点33分16秒的高峰这里不同程序的CPU用量,发现system的已经比httpd还要多了,整体看起来内核内部的问题严重性已经比httpd的要高了。

这也太奇怪了,18点33分16秒,httpd的内存访问造成的page fault已经有59K/s了,一旦page fault就要涉及查TLB,所以产生了一大堆的TLB中断。但是,在这个时间点之后httpd在ps命令中已经看不到了,然而swap还在使用,没有回收。

再看高峰时间点,脏页也非常多,有可能是文件背景的内存消耗比较多,触发了内核的水位线置换了更多的匿名页面。

在内存的图上,看不活动的匿名页确实是上升了,说明httpd2的匿名页被swap置换到了磁盘上,在进程没了之后又没有回收。最重要的是,在18点33分14秒左右,活动的文件继续增多,这时候内核不得不驱逐了活动的匿名页,swap到了磁盘中更多的页。

那么总结一下,这看起来又是一个swap引发的惨案,无非就是因为现在服务是在容器里,并且在docker层面加了内存限制,所以没有导致整个系统hung住,但是容器的进程hung住了,而且无法重启,这和系统hung住也没什么区别了,无非给我留了点数据可以看罢了。细节来看,又是一个page驱逐的问题,频繁又要使用结果换到swap里了,用的时候引发page fault又拉高了TLB中断,从磁盘频繁读写swap进一步拉高了iowait,成了一个死循环。

看来mpm的参数调的还是有点大了,还是再保守一些,然后把swap关了,cgroup配合swap一起用,看起来还是会引发一些问题,还是不要开了,docker加了限制,只要内存用超了直接kill了,就别再置换了,免得又搞成死循环。

关了swap后,再执行dnf update,然后内存满了又死机了。自从CentOS被RedHat搞没了之后,新生派的Rocky Linux和Alma Linux看起来还是不太行,问题频发,是时候换到Ubuntu了,希望这背后的商业公司能比RedHat晚点收割用户。

为了解决这个问题,我把系统更换为了Ubuntu 24.04.1 LTS,按照上边的配置步骤,我简单列举了一些命令步骤。

安装iptables、ipset的持久化服务,这样可以把规则保存起来,开机自启动。

apt install iptables-persistent ipset-persistent

迁移上边Rocky Linux中的配置到Ubuntu中,如果是新装的就不用迁移了,直接配置规则即可。

cat sysconfig/ipset > /etc/iptables/ipsets 
cat sysconfig/iptables > /etc/iptables/rules.v4 
cat sysconfig/ip6tables > /etc/iptables/rules.v6

重启服务使这些规则生效。

systemctl restart netfilter-persistent.service

为了避免swap再换出问题,这次索性不开swap了,并且WordPress的容器相关参数换成了如下。

主要是进一步调低了httpd的mpm的工作进程数量,同时调小了容器内存限制数值,增加了容器CPU限制参数,以便给系统留一点余量,避免影响到主机上的监控和报警插件。

cat > /data/wordpress/config/apache2/mpm_prefork.conf << EOF
# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves

StartServers            2
MinSpareServers         2
MaxSpareServers         4
MaxRequestWorkers       10
MaxConnectionsPerChild  20
EOF
docker run --restart=unless-stopped --name wordpress 
    -v /data/wordpress/data:/var/www/html 
    -v /data/wordpress/config/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini 
    -v /data/wordpress/config/apache2/mpm_prefork.conf:/etc/apache2/mods-available/mpm_prefork.conf 
    --memory 768m 
    --cpus="1.5" 
    -e WORDPRESS_DB_HOST="192.168.1.2" 
    -e WORDPRESS_DB_USER="数据库用户名" 
    -e WORDPRESS_DB_PASSWORD="数据库密码" 
    -e WORDPRESS_DB_NAME="数据库名" 
    -e TZ="Asia/Shanghai" 
    --network xuegao 
    --ip 192.168.1.11 
    -d wordpress:6.6.2-apache

自从我更换系统为Ubuntu并且改了一些配置之后,内存的用量就正常多了,持续观察了几天,没有再出问题了。

又观察了一个月,已经没有再出过问题了。