2026-08-19 21:56:59
昨晚 10 点,一个网站的负载飙高,触发了 uptime 的宕机报警。从 top 命令结果里看,redis 进程 cpu 飙高,但是我起初以为是被恶意攻击了,把 cloudflare 防护规则严格了一些,临时负载降下来了。由于太困,明天还有事情,就没有继续观察。
早上 6 点起来,又看了,负载还是高。但是并不影响 golang 相关的服务,只有 php 的旧站点受到了影响。
哎,PHP 这些破玩意真糟心。牛马还是要上班,就只能寄希望于晚上回来解决了。
晚上捣鼓了两个小时,算是解决了。
redis 放在 docker 里,所以登录 redis 使用:
docker compose exec redis redis-cli
查看运行状态:
> INFO stats
total_connections_received:168608
total_commands_processed:11273966
instantaneous_ops_per_sec:326
total_net_input_bytes:203350837911
total_net_output_bytes:26614034188264
total_net_repl_input_bytes:0
total_net_repl_output_bytes:0
instantaneous_input_kbps:31.01
instantaneous_output_kbps:453887.44
instantaneous_input_repl_kbps:0.00
instantaneous_output_repl_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:189148
expired_stale_perc:0.36
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:22144
evicted_keys:0
evicted_clients:0
total_eviction_exceeded_time:0
current_eviction_exceeded_time:0
keyspace_hits:7151109
keyspace_misses:2783287
pubsub_channels:0
pubsub_patterns:0
pubsubshard_channels:0
latest_fork_usec:11041
total_forks:245
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
total_active_defrag_time:0
current_active_defrag_time:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
total_error_replies:3546
dump_payload_sanitizations:0
total_reads_processed:11920639
total_writes_processed:18558028
io_threaded_reads_processed:0
io_threaded_writes_processed:0
reply_buffer_shrinks:123859
reply_buffer_expands:375511
结论:输入和输出的差距高达 1.4万倍。这说明客户端只发了一个很小的请求(比如 HGETALL 或 SMEMBERS),但Redis返回了一个极其庞大的数据给客户端。
CLIENT LIST
输出:
id=112411 addr=[2400:8901::7]:41764 laddr=[2400:8901::4]:6379 fd=21 name= age=28381 idle=0 flags=N db=2 sub=0 psub=0 ssub=0 multi=-1 qbuf=0 qbuf-free=20474 argv-mem=0 multi-mem=0 rbs=4096 rbp=4096 obl=0 oll=1 omem=20971544 tot-mem=20996888 events=rw cmd=get user=default redir=-1 resp=2
id=112430 addr=[2400:8901::7]:52090 laddr=[2400:8901::4]:6379 fd=26 name= age=28366 idle=0 flags=N db=2 sub=0 psub=0 ssub=0 multi=-1 qbuf=0 qbuf-free=20474 argv-mem=0 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=1 omem=20971544 tot-mem=21009176 events=rw cmd=get user=default redir=-1 resp=2
有 3个客户端连接 正在拉取 超大Key,每个连接的输出缓冲区(omem)都达到了 20,971,544 字节(约 20 MB).这三个连接都在执行 cmd=get,且都在 db=2(数据库2),说明它们都在 反复读取同一个超大字符串类型的Key(或者内容完全相同的大Key)。每个响应包约 20MB,三个连接同时输出,瞬时输出带宽自然飙到 450MB/s。
连 redis db 2:
执行完后,它会告诉你哪个数据类型的Key最大(比如最大的Hash有多大)。
> docker compose exec redis redis-cli -n 2 --bigkeys -i 0.1
Sampled 4658 keys in the keyspace!
Total key length in bytes is 297586 (avg len 63.89)
Biggest string found '"9c6a2e0bcbb03f32fb0382d38fa00bbe-cache-com_modules-5cdbee63315b6b81941ea5c4844ffd8c"' has 26627066 bytes
-i 0.1 表示每扫描100个Key就休息0.1秒,防止在高负载时加剧CPU抖动。
docker compose exec redis redis-cli -n 2 DEL "9c6a2e0bcbb03f32fb0382d38fa00bbe-cache-com_modules-5cdbee63315b6b81941ea5c4844ffd8c"
cpu 就正常了。但是只正常了半个小时,然后这个 key 又变大到了 15M。。。
删了几次,还是反复。我无语了。。。
改问 gemini 之后,找到了根源:
joomla 的 redis 缓存中,这个 key : cache-com_modules 一直暴涨,有 20 多M,这是为什么
在 Joomla 中,cache-com_modules 这个 key 专门用于存储网站所有模块(Modules)的缓存数据(即模块渲染后的 HTML 代码及相关参数)。
这个 key 的体积暴涨到 20 多兆(在 Redis 这种内存数据库中,对于单一文本 key 来说已经非常庞大),通常是由以下几个核心原因导致的:
开启了“渐进式缓存”(Progressive Caching)—— 最常见原因
Joomla 的全局缓存分为两种模式:保守式(Conservative)和渐进式(Progressive)。
如果你的网站开启了“渐进式缓存”,当有大量的搜索引擎爬虫(蜘蛛)、恶意扫描机器人或大量真实散客访问时,Joomla 会为每一个全新的 Session 生成一份完全独立的模块缓存。这些缓存全部堆积在 com_modules 下,导致该 key 的体积呈指数级暴涨。
切换回“保守式缓存”. 这是最立竿见影的解决方法:
# docker compose exec redis redis-cli -n 2 TTL "9c6a2e0bcbb03f32fb0382d38fa00bbe-cache-com_jmap-9667e9aa5d8b93f1cb1835bd4315b7c9"
(integer) 3456
返回结果 (integer) 3456 表示该 Key 的剩余存活时间为 3456 秒,换算一下大约是 57.6 分钟(接近 1 小时)。
看来不需要我加定时任务自动清理了。
2026-08-18 19:45:11
这周写了个软件开发需求文档,用 Markdown 格式写的,主要是为了方便编辑,也方便 AI 去补写。
但是转 word 居然变成了难题。试了几种方案:
我第一次用的时候,不但让 workbuddy 把 markdown 转换成 word 格式,同时修改一下文法,改成汇报的文风。
workbuddy 的处理逻辑是,先把 markdown 转换成 HTML,然后再基于 HTML 转换成 docx。但是,在二级标题的处理上,加了不兼容的样式,非常影响美观度。整个过程还采用了 Python 脚本,不太专业,之前提过 软著自动生成 Skill 最好的方案是
.NET SDK 8.0+:用于启用更完整的 DOCX OpenXML 生成和校验能力。Python 方案只能作为备用
Python 生成 word 的样式还是不太完美。整个过程异常耗时,耗 token。毫无意义。
飞书文档支持直接 ctrl v 黏贴进去 markdown,然后导出 word。
我也是用的这个方案,但是太朴素了,我自己用其实没啥问题。但是领导嫌弃太简陋。确实有点。。。
我又用 office word 和 WPS 分别试了一下美化,哎,都是鸡肋,连个 AI 美化都做不好。
我觉得,有两个方案可以试试:
虽然已经进入了离开了 AI 就没法办公的时代,但是这种常见其实浪费 token 毫无意义,还不如自己写的工具靠谱。
2026-08-16 16:55:26
很多年没有给领导写过项目开发汇报文件了,毕竟之前都是直接开干,也不需要程序员去评估。在写了一版之后,发现遗漏了不少关键点,例如:服务器成本、认证成本、每个功能的备选方案、工期能否砍一半等。为了以后能更高效地输出汇报文件,我决定生成一个 AI Skill,帮我去自动排查汇报中是否遗漏。
虽然是给领导看的,但是我觉得最重要的是这个文档对自己有价值。把冠冕堂皇的内容放在前面,应付完之后。后面还是需要记录对自己开发有价值的内容,例如,用户角色分析、用户故事、需要提前申请的资源等。
至于设计稿,我感觉意思一下就行了,要完整出所有界面,也挺耗费精力的。也耗费 token 😅。
时间控制在一个小时之内,不用花费太多时间在上面。毕竟领导可能看不了1分钟。同时,不要给子标题添加标号,因为随时会调整顺序。
首先我问了 DeepSeek 一个问题:
给领导写软件项目研发计划汇报,需要突出哪些要素
我觉得回答非常精炼:
如果时间有限,只能讲3分钟,请死磕这3点:
话糙理不糙。我决定就用这三个作为二级标题了,放在文档最前面。
下面是 AI 给出的所有内容。值得借鉴:
给领导汇报软件项目研发计划,最核心的一点是:不要把自己当作“程序员”去汇报技术细节,而要把自己当作“CEO”去汇报如何达成商业目标。
领导(尤其是高层)最关心的是投资回报率、风险控制和节奏感。以下是需要重点突出的六大核心要素,以及具体的表达话术:
不要一上来就讲“我们要用React还是Vue”,要先讲业务价值。
领导最怕“遥遥无期”。不要只给一个最终截止日期,要设置明确的“关键里程碑”。
这是领导签字的核心依据。要明确说出缺口和边界。
这是展现你成熟度的地方。只提进度不提风险,在领导看来等于“立军令状却不设退路”。
向领导表明你已建立有序的管控体系,让他放心。
这是很多汇报中容易忽略的。明确“不做”,能有效避免领导在中途随意追加需求,保护项目不延期。
最后记得: 汇报结尾一定要给出明确的“决策请求”——比如“以上计划请领导审批,如无异议我将按此启动。”——这样能直接推动项目进入执行阶段。祝汇报顺利!
2026-08-12 20:39:49
例如在 Cloudflare 的页面缓存规则中,设置了 3 天,但是返回的网页 HTTP 头是:
pragma: no-cache 代表这是一个 HTTP/1.0 协议的遗留指令,其作用是要求缓存服务器在返回内容前,必须向源服务器进行验证(即强制回源验证)。
但在当前的场景中,它基本不起作用,可以忽略。因为现代浏览器和 Cloudflare 优先遵循 HTTP/1.1 的 Cache-Control 头。
现代标准以 Cache-Control 为准。如果同时存在,客户端会忽略 Pragma。
cache-control: max-age=7200(源站要求缓存 2 小时)
age: 73132(当前资源已在 Cloudflare 缓存中存了 73132 秒 ≈ 20.3 小时)
age 远大于 max-age,这证明了 Cloudflare 的“边缘缓存 TTL”(设置的 3 天/259200 秒)成功覆盖(覆盖/忽略)了源站的 max-age=7200。缓存规则已经完全生效,Cloudflare 强制将这个页面保留了将近一天,并且还会继续保留到 3 天期满。
pragma: no-cache 常由 Joomla 的系统缓存设置产生。关闭它可能从源头解决问题。
如果没有权限修改 Joomla 后台配置,可以在服务器上直接修改源码:
libraries/vendor/joomla/application/src/AbstractWebApplication.php
搜索 no-cache 并注释掉:
if (!$this->allowCache()) {
// Expires in the past.
$this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true);
// Always modified.
$this->setHeader('Last-Modified', \gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
// HTTP 1.0
$this->setHeader('Pragma', 'no-cache');
}
2026-08-11 21:49:20
又中招了,GTranslate 服务说网站请求量过大,停止了服务。查看首页,发现一个首页,加载了 1000 多个 css 文件。
发现是 Joomla SP Page Builder 的漏洞,会通过 POST 上传一堆文件。
SP Page Builder在2026年被曝出多个严重安全漏洞(如CVE-2026-48908、CVE-2026-66494等),允许未授权用户上传恶意文件甚至执行代码
然后影响首页加载很多 css 文件。好在,之前添加了 nginx 规则,会阻止 media 路径下的所有代码执行。所以,只会看到加载了一千多个 css 文件,但是没有其他影响。
晚上,清理了这些配置,并且增加了 cloudflare 规则,阻止了 SP Page Builder 相关的 post 请求。
POST /index.php?option=com_sppagebuilder&task=asset.uploadCustomIcon 200
GET /media/com_sppagebuilder/assets/iconfont/icorlycgq/fonts/fawvfmj.PHP?t=ixbszjbawvencrtq&c=echo+SPPB-RCE-%24%28%287%2A6%29%29 HTTP/1.1 403
攻击 IP 来自新加坡的 194.233.86.244。上传完成后,就开始尝试执行恶意 PHP 代码,好在我阻止了 media 下的 PHP 代码执行。
当请求是 POST 时,并且
http.request.uri.query contains "option=com_sppagebuilder"
就直接拦截。不用 Nginx 拦截的原因是,我不知道怎么设置 nginx 判断这种多条件的情况。而且用 cloudflare 也更节省服务器资源。
确认是否禁止了 com_sppagebuilder 相关的 POST 请求。
curl -X POST "http://your-domain.com/index.php?option=com_sppagebuilder&task=asset.uploadCustomIcon" -I
如果返回 HTTP/2 403,说明拦截成功了。
即便清空了服务器
需要到数据库的 xxx_sppagebuilder_assets 数据表,清理掉里面的恶意文件配置。
2026-08-10 19:59:07
枯燥的 MES 重构工作,感觉没有尽头,调教 SKILL 反而成了唯一的乐趣。
Spring Boot 功能代码开发完成后,可以让 AI 自动编译,通过编译日志来排查是否有问题,然后自动修复。
但是,对于 Mybatis XML 文件中的 SQL 代码,在编译时通常排查不出问题,只有执行时才能发现数据库报错。例如:
Error querying database. Cause: java.sql.SQLSyntaxErrorException: ORA-01722: 无效数字
或者
Error querying database. Cause: java.sql.SQLDataException: ORA-01861: 文字与格式字符串不匹配
这些都是 AI 在实现代码时,经常会制造的一些 bug。虽然根源是没有把数据库表结构喂给 AI。
但是,我依然想实现一个 Skill 能自动发现 Mybatis XML 文件中的 SQL 代码的问题,然后自动修复,省去了我来回在界面里点击,再把错误信息复制给 AI,如此反复的过程。
这个 Skill 可以对指定功能的 mybatis xml 文件中的 SQL 做单元测试。
如果要模拟数据库操作,是使用 H2 数据库(内存数据库,支持各种数据库的 SQL)还是 Oracle 数据库呢?
因为 SQL 强依赖 Oracle 专有语法(DECODE/TO_DATE)和真实表数据,只有连真实库才能真正验证。最后还是决定使用测试环境的 Oracle 数据库。
这个 skill 接收功能名称,判断指定功能是否已经有代码实现了,如已实现,则自动创建对应的 mybatis xml 相关的 SQL 测试案例。如果已经有了单元测试,则按照该 skill 的最新标准修正单元测试。
需要通过 python (oracledb 库)脚本连接测试数据库(jdbc:oracle:thin:@//x.x.x.x:1521/testdb),来判断相关表的表结构、字段类型。并插入测试数据,注意,只能使用测试数据库执行。绝对不可以连接正式生产数据库来执行上面的操作。
同时把对应的表结构整理到 docs\database\tables.md 中, 使用 markdown 格式,二级标题是表名及对应的中文名,内容是表结构或者建表语句的 code block。目的是方便后续开发,或者调试 bug 时可以参考。
写入的测试数据,不需要清理,因为测试数据库是独立的,不会影响生产环境。
然后执行测试,如果测试通过,则提示测试通过,否则提示测试失败,并输出失败的测试案例名称。
测试需要覆盖到各种前端查询条件的场景,即,各种前端查询条件的场景,需要写一个测试案例,测试案例名称是功能名称 + 前端查询条件的名称。
AI 在编写 Skill 时,实际运行试点测试时,测试基础设施精确复现了你之前报告的 ORA-01861 错误——测试库有数据时 sn.created_at >= TO_DATE(?, ‘yyyy-MM-dd hh24:mi:ss’)(created_at 是 VARCHAR2)抛”文字与格式字符串不匹配”。这正验证了本 skill 的核心价值:真实测试库 + 有数据的表才能暴露空表单测发现不了的 SQL 问题。测试失败案例名为 queryByTimeRange。根因与修复已在 CODEBUDDY.md 记录(去掉 TO_DATE 改字符串比较)。
由于 plan 约束”不得改动业务源码”,我没有擅自修改 XML。需要我修复这个业务 XML 的 TO_DATE bug 吗? 修复后时间条件测试即可通过,年周号自检记录 在真实库的时间查询功能也将恢复正常。
修改 skill 的规则,如果发现测试失败,则自动修复,允许改动业务源码。
mvn -pl ruoyi-admin test "-Dtest=SomeFunctionMapperTest" "-Dskip.tests=false"
mvn -pl ruoyi-admin test "-Dtest=com.sunzhongwei.somefunction.*" "-Dskip.tests=false"
mvn -pl ruoyi-admin test "-Dtest=*MapperTest" "-Dskip.tests=false"
https://mybatis.org/spring-boot-starter/mybatis-spring-boot-test-autoconfigure/zh_CN/index.html
开发环境的版本: mybatis-plus-boot-starter 3.5.1
当前 RuoYi 项目生产配置用的是 dynamic-datasource-spring-boot-starter(@DS 注解切数据源)。@MybatisTest 的自动配置期望的是单一 spring.datasource.* 数据源 + 默认的 SqlSessionFactory。而 dynamic-datasource 会自定义 DataSource 代理和 SqlSessionFactory,两者容易互相干扰——这也是为什么之前手工方案里特意用 DriverManagerDataSource 绕开 Druid/dynamic-datasource。
项目用的是 MyBatis-Plus(mybatis-plus-boot-starter),而 mybatis-spring-boot-starter-test 的 @MybatisTest 自动配置是基于原生 MyBatis 的 SqlSessionFactory 来探测和加载的。官方 starter 对 MP 的兼容不是开箱即用,需要额外配置(如指定 MybatisSqlSessionFactoryBean、全局 MybatisConfiguration 等)。当前方案用的 MybatisSqlSessionFactoryBean + setTypeAliasesPackage 正是为了兼容 MP。
@MybatisTest 默认不扫描 MP 的 typeAliasesPackage,之前我们踩过 SysConfig 别名解析失败的坑,手工配置里通过 setTypeAliasesPackage("com.sunzhongwei.domain") 明确解决了。