不少团队把 Confluence 当知识库来用,页面用着用着就越来越慢,打开要转圈圈,保存也卡,整个人都麻了。这种慢问题其实有规律可循,我们不需要一上来就拍脑袋,可以照着下面这套思路慢慢捋。
一、先搞清楚“慢”到底慢在哪
1.1 慢是“感觉”还是“数据”
遇到慢问题,先别急着动服务器。问自己几个问题:是所有页面都慢,还是只有某个页面慢?是上午高峰慢,还是半夜也慢?是第一次打开慢,还是刷新也没用?这些问题能把排查范围缩小很多。
比如所有页面都慢,优先怀疑数据库或应用本身;只有某个带报表的页面慢,那可能那个页面查询太重;每天早上慢,可能定时任务刚好在那个时候跑。我有个朋友的公司,上午十点全员反馈 Confluence 几乎打不开,后来发现是定时备份任务和早晨高峰期撞在一起,数据库被长时间锁住。你看,不先看场景直接调内存,问题大概率还会回来。
1.2 三个最可疑的地方
Confluence 是 Java 写的,跑在 Tomcat 上,数据存数据库,附件存文件目录。页面要渲染出来,需要经过“应用线程干活 + 数据库查数据 + 内存分配对象”这几步。所以慢的原因基本集中在三处:
- 数据库响应慢:比如某条 SQL 没走索引,数据量大时全表扫描,一等就是几秒。
- Java 堆内存不够:对象太多,垃圾回收频繁,CPU 忙个不停,页面就卡。
- 应用线程被卡住:比如在等数据库连接、等锁,很多请求堆在一起,看起来就像死机。
这三处分别对应三样东西:慢查询日志、GC 日志、线程堆栈。下面我们一个个看怎么从日志里找出问题。
二、从日志里找线索
2.1 访问日志告诉你哪个页面慢
Confluence 的访问日志记录每个请求的处理时间。打开日志,找到那些耗时超过两秒的请求,看看路径是什么,就能知道用户平时抱怨的“慢”是不是集中在某个功能上。
日志路径一般在 Confluence 的 home 目录下的 logs 文件夹里,文件名类似 access.log,每一行最后的数值就是处理耗时。我们可以用 Python 把耗时排名前十的请求找出来。这里需要注意:同一个 URL 可能被请求很多次,光看单次耗时还不够,要按平均耗时排序。下面是一个简单脚本。
技术栈:Python 3.x
# 技术栈:Python 3.x
import re
from collections import defaultdict
# access.log 模板示例:
# 192.168.1.10 - - [10/May/2024:10:00:01 +0800] "GET /display/MyPage HTTP/1.1" 200 12345 0.456
pattern = re.compile(
r'"(GET|POST) (\S+) HTTP/[\d.]+" \d+ \d+ (\d+\.\d+)'
)
def rank_slow_requests(access_log, top_n=10):
# 用字典统计每个 URL 的总耗时和请求次数
records = defaultdict(list)
with open(access_log, 'r', encoding='utf-8') as f:
for line in f:
match = pattern.search(line)
if match:
url = match.group(2)
cost = float(match.group(3))
records[url].append(cost)
avg_costs = []
for url, costs in records.items():
avg_cost = sum(costs) / len(costs) # 平均耗时
avg_costs.append((url, avg_cost, len(costs)))
# 按平均耗时从高到低排序
avg_costs.sort(key=lambda x: x[1], reverse=True)
for url, avg_cost, count in avg_costs[:top_n]:
print(f'URL: {url}, 平均耗时: {avg_cost:.3f} 秒, 次数: {count}')
rank_slow_requests('access.log')
这个脚本输出的结果,能帮我们快速定位:是首页慢、搜索慢,还是某个历史版本页面慢。如果慢的请求都打在同一个 URL 上,就可以顺着这个 URL 去查后台对应的查询逻辑。
2.2 GC 日志告诉你内存是不是不够
Java 应用的内存,如果频繁“腾地方”,就会表现成页面时不时卡一下,因为腾地方的时候所有线程都要暂停一下。这个“腾地方”就是垃圾回收,也就是 GC。
GC 日志在 Confluence 的 home 目录 logs 文件夹里,文件名以 atlassian-confluence-gc 开头,里面记录每次垃圾回收的停顿时间。我们用 Python 统计一下停顿的频率和平均耗时,看看内存压力到底大不大。
技术栈:Python 3.x
# 技术栈:Python 3.x
import glob
import re
# 日志里常见的一行:
# 2024-05-10T10:00:01.123+0800: 123.456: [GC (Allocation Failure) 1024M->512M(2048M), 0.2345678 secs]
# 我们只提取最后面的 "xxx secs" 中的秒数
pattern = re.compile(r',\s+(\d+\.\d+)\s+secs\]')
def parse_gc_logs():
total_pauses = []
# 匹配 home 目录下所有 GC 日志文件
for log_path in glob.glob('atlassian-confluence-gc-*.log'):
with open(log_path, 'r', encoding='utf-8') as f:
for line in f:
if 'GC' not in line:
continue
match = pattern.search(line)
if match:
total_pauses.append(float(match.group(1)))
if not total_pauses:
print('没有找到 GC 停顿记录,请确认日志路径是否正确')
return
avg_pause = sum(total_pauses) / len(total_pauses)
max_pause = max(total_pauses)
print(f'共统计到 {len(total_pauses)} 次 GC 停顿')
print(f'平均停顿 {avg_pause:.3f} 秒')
print(f'最长停顿 {max_pause:.3f} 秒')
# 如果平均停顿超过 0.1 秒,说明内存分配压力已经比较高了
if avg_pause > 0.1:
print('平均停顿超过 0.1 秒,建议重点排查堆内存设置')
parse_gc_logs()
这里要注意,GC 日志的格式在不同 Java 版本上会有些差别,但思路都一样:看停顿次数和时长。如果 GC 频繁且停顿时间长,可以尝试调大堆内存参数,或者检查是不是有内存泄漏,比如很大的页面一次加载太多数据,长期不释放。
2.3 线程堆栈告诉你线程为什么等
线程卡住的时候,页面就像在“等一个永远不会回来的人”。比如数据库连接池被占满,新请求拿不到连接,线程全部变成等待状态。
我们可以用 Java 自带的 jstack 命令把线程堆栈导出到文件,再用 Python 统计一下线程状态。导出文件的名字可以自己定,比如 thread_stack.txt。
技术栈:Python 3.x
# 技术栈:Python 3.x
from collections import Counter
def count_thread_states(stack_file):
states = Counter()
with open(stack_file, 'r', encoding='utf-8') as f:
for line in f:
# 线程状态这一行类似:
# java.lang.Thread.State: WAITING (parking)
if 'java.lang.Thread.State:' in line:
# 取冒号后面的部分,再去掉可能存在的括号说明
state = line.split(':', 1)[1].strip()
if '(' in state:
state = state.split('(')[0].strip()
states[state] += 1
return states
states = count_thread_states('thread_stack.txt')
for state, count in states.most_common():
print(f'{state}: {count}')
如果 WAITING 或 BLOCKED 状态特别多,说明线程挤在一起等资源。这时候再结合堆栈里的类名和行号,能看到具体是等数据库连接,还是等某个锁。如果没有线索,就继续看是哪些类出现的频率高。
三、把三个信息合并成一个简易“体检单”
上面三个脚本是分开看的,其实可以把访问日志、GC 日志、线程堆栈的统计结果汇总起来,形成一个“体检单”。比如访问日志发现某个 URL 慢,GC 日志发现内存压力大,线程堆栈发现一堆线程在等连接池,三个信息拼在一起,基本就能断定是数据库连接池不够用,还是 SQL 本身慢。
下面用一个完整的 Python 示例,把“访问日志分析”和“GC 日志分析”整合成一个函数,方便我们一次跑完。
技术栈:Python 3.x
# 技术栈:Python 3.x
# 这个示例把“访问日志排名”和“GC停顿统计”整合成一个函数
import re
from collections import defaultdict
def analyze_confluence_slow(access_log_path, gc_log_path):
# ---------- 1. 访问日志分析 ----------
access_pattern = re.compile(
r'"(GET|POST) (\S+) HTTP/[\d.]+" \d+ \d+ (\d+\.\d+)'
)
url_times = defaultdict(list)
with open(access_log_path, 'r', encoding='utf-8') as f:
for line in f:
m = access_pattern.search(line)
if m:
url_times[m.group(2)].append(float(m.group(3)))
print('=== 访问日志 TOP 3 慢请求 ===')
sorted_urls = sorted(
url_times.items(),
key=lambda item: sum(item[1]) / len(item[1]),
reverse=True
)
for url, times in sorted_urls[:3]:
avg = sum(times) / len(times)
print(f'{url} 平均耗时 {avg:.2f} 秒')
# ---------- 2. GC 日志分析 ----------
gc_pattern = re.compile(r',\s+(\d+\.\d+)\s+secs\]')
pauses = []
with open(gc_log_path, 'r', encoding='utf-8') as f:
for line in f:
m = gc_pattern.search(line)
if m:
pauses.append(float(m.group(1)))
if pauses:
avg_pause = sum(pauses) / len(pauses)
print('=== GC 停顿统计 ===')
print(f'共 {len(pauses)} 次,平均 {avg_pause:.3f} 秒')
else:
avg_pause = 0
# 这里还可以继续加入线程堆栈分析,思路类似
print('=== 提示 ===')
if sorted_urls and avg_pause > 0.1:
print('既存在慢请求,又存在频繁 GC,建议先调内存再看数据库')
elif sorted_urls:
print('主要问题可能集中在应用逻辑或 SQL 上,可以继续看线程堆栈')
# 调用示例
# analyze_confluence_slow('access.log', 'atlassian-confluence-gc-2024-05-10.log')
这个示例虽然简化了很多,但把排查的框架搭出来了。实际使用中,我们会根据日志格式调整正则,但排查路径是一样的。
四、常见优化手段和注意事项
4.1 从慢查询日志下手
访问日志能看出哪个页面慢,但数据库层也可能藏着大问题。MySQL 的慢查询日志能记录执行时间超过阈值的 SQL。我们用 Python 统计一下哪些 SQL 出现次数最多,这样就能知道到底是谁在拖后腿。
技术栈:Python 3.x
# 技术栈:Python 3.x
# 统计 MySQL 慢查询日志中,出现次数最多的 SQL
from collections import Counter
def top_slow_sql(slow_log_path, top_n=5):
counter = Counter()
current_sql = []
with open(slow_log_path, 'r', encoding='utf-8') as f:
for line in f:
# 跳过注释行
if line.startswith('#'):
continue
# 去掉首尾空白,并累积到当前 SQL
current_sql.append(line.strip())
# 一条 SQL 以分号结束
if line.rstrip().endswith(';'):
sql = ' '.join(current_sql).replace('\n', ' ')
# 截取前 200 个字符,防止统计时被长 SQL 刷屏
counter[sql[:200]] += 1
current_sql = []
for sql, count in counter.most_common(top_n):
print(f'{count} 次 -> {sql}')
top_slow_sql('mysql-slow.log')
拿到慢 SQL 之后,可以打开执行计划看看有没有走索引。常见做法是给 WHERE 条件涉及的字段加索引,但要注意不要加太多,否则写入会变慢。索引不是越多越好,适合就好。
4.2 JVM 内存怎么调
前面 GC 停顿多,最直接的调整是修改 Confluence 的 JVM 堆大小。配置文件在 Confluence 安装目录的 bin 文件夹下,名字叫 setenv.sh,里面有一段 CATALINA_OPTS。想从 2G 调到 4G,可以用文本替换工具。但提醒一句,修改前先备份。
下面用 Python 演示自动替换配置的过程,重点在说明思路,实际操作时可以直接编辑文件。
技术栈:Python 3.x
# 技术栈:Python 3.x
# 演示如何修改 setenv.sh 中的 -Xmx 内存参数
import shutil
config_path = 'setenv.sh'
# 先备份,避免改错了回不去
shutil.copyfile(config_path, config_path + '.bak')
with open(config_path, 'r', encoding='utf-8') as f:
content = f.read()
old_param = '-Xmx2048m'
new_param = '-Xmx4096m'
if old_param in content:
content = content.replace(old_param, new_param)
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f'已将 {old_param} 替换为 {new_param}')
print('请重启 Confluence 后观察 GC 日志')
else:
print('未找到目标参数,请检查实际配置')
需要注意的是,-Xmx 不是越大越好。如果服务器总共 16G 内存,Confluence 占 8G,数据库和操作系统就可能不够用了,反而把整个机器拖垮。一般来说,堆内存不要超过系统物理内存的一半,具体多少还要看其他服务的占用情况。
4.3 首页和页面内容别太“胖”
很多慢问题不是基础设施不行,而是页面内容太重。首页塞了几十个宏、十几张大图,每次有人打开都要生成一遍,服务器再快也扛不住。建议把首页改成静态内容,把动态列表改成定时生成的快照。
另外,历史页面里的附件版本过多也会拖慢浏览,可以定期清理无用的旧附件。这些优化效果往往比调内存更立竿见影,而且操作起来没有风险。
4.4 注意事项
- 配置改动前一定备份,尤其是数据库和安装目录。
- 观察日志时,要选高峰时段的数据,别拿深夜的数据代表整体情况。
- 如果使用了自签名证书或代理服务器,还要检查 SSL 握手和反向代理的超时设置。
- 不要同时改多个参数。一次只改一个,否则出了新问题都不知道是哪一步造成的。
五、总结
Confluence 页面慢,通常不是单个原因,而是几个原因叠加。先通过访问日志找到慢页面,再用 GC 日志判断内存压力,最后用线程堆栈看有没有线程在等待。三个信息放在一起,慢在哪一层就清楚了。优化时优先做成本低、见效快的动作:加索引、调堆内存、精简首页。改完一个变量,再回到日志里去看效果,反复几次,页面自然就快了。
排查慢问题最怕乱试,记住“量化优先、小步调整、持续观察”这十二个字,慢问题总能被一点一点磨掉。
评论
围绕“Confluence页面加载缓慢的性能瓶颈排查与优化思路”参与讨论