缓冲池设成70MiB,为什么变成80MiB?用小实例看懂Chunk和在线调整
你已经知道innodb_buffer_pool_size控制InnoDB缓冲池容量,也会写SET GLOBAL。现在试着把一个小实例从64MiB调到70MiB,命令没有失败,读回参数却是80MiB。
是把单位写错了吗?还是MySQL没有采用刚才的设置?
先不把缓冲池所有数据结构重新讲一遍。我们只看这次调整:给它多一点空间时,它到底按什么大小加进去?
先把三个实际值读出来
以下都是MySQL 8.0.46的专用临时实例,不改已有数据库。先读取当前有效值:
SELECT @@version,
@@innodb_buffer_pool_size DIV 1048576 AS pool_MiB,
@@innodb_buffer_pool_chunk_size DIV 1048576 AS chunk_MiB,
@@innodb_buffer_pool_instances AS instances;
第一个实验实例启动时明确指定:缓冲池64MiB、Chunk16MiB、实例数1。读回也是这三个值。
这里MiB是1024×1024字节。后文所有70、80、96都按这个单位算,不把十进制MB混进来。
当前总容量64MiB,实例数1
[16] [16] [16] [16]
每一格表示一个Chunk
Chunk不是数据页,也不是一条记录。可以先把它理解为调整缓冲池容量时成组增加、移除的一块空间;这里每块16MiB。我们关心的是容量变化,不把示意格子当作真实内存地址布局。
请求70MiB时,为什么不能只加6MiB?
在这个实验实例上执行:
SET GLOBAL innodb_buffer_pool_size = 70 * 1024 * 1024;
原来64MiB,目标70MiB,表面上只差6MiB。但是以16MiB为调整单位,当前4块不够,再加一块就到80MiB:
64MiB: [16][16][16][16]
想再加6
|
不能只补6MiB
v
80MiB: [16][16][16][16][16]
实际读回的innodb_buffer_pool_size是83886080字节,也就是80MiB。本次不是参数被忽略,而是目标值按调整单位进行了取整。
再从80MiB出发请求81MiB,结果是96MiB;请求48MiB,完成后是48MiB。这个例子中,每次目标都会落在16的倍数上。
| Chunk | 请求容量 | 本次完成后的容量 |
|---|---|---|
| 16MiB | 70MiB | 80MiB |
| 16MiB | 80MiB | 80MiB |
| 16MiB | 81MiB | 96MiB |
| 16MiB | 48MiB | 48MiB |
请求的目标已经是当前80MiB时,不需要真的再搬一次空间。运行器把这个分支也留下,避免把每次SET都当成发生了一次调整。
同样请求70MiB,换成32MiB的Chunk呢?
我们结束第一个实例,再用新的临时数据目录启动第二个。初始容量仍是64MiB、实例数仍是1,只把启动时的Chunk改成32MiB。
开始: [32][32] = 64MiB
请求70MiB:两块不够
增加后:[32][32][32] = 96MiB
结果确实变成96MiB。同样的目标值,调整单位不同,最终容量也会不同。因此读参数时不能只截取buffer_pool_size这一行,把Chunk和实例数一起读出来,才能解释变化。
MySQL 8.0要求总容量对齐到Chunk大小 × 缓冲池实例数的倍数。我们这次实测的实例数为1;如果用2个实例、每块16MiB作纸上演算,总调整单位就是32MiB。这个多实例例子是计算说明,不是本次另起了一个多实例服务。MySQL 8.0容量配置说明
检查较小学习实例时尤其应读取实际instances,不要从别人的大内存配置复制一个数就假设它已经采用了。
配置里写的Chunk,也一定是实际Chunk吗?
再用第三个专用实例,只给64MiB总容量,启动参数却写Chunk128MiB。
读回的实际Chunk是64MiB,并不是128MiB。这个小实例连一块128MiB都放不下,初始化时调整了有效值。随后请求70MiB,按实际64MiB的单位完成到128MiB。
启动请求:总64,Chunk128
|
读取有效值
v
实际: 总64,Chunk64
再请求总70 -> 需要两块64
-> 最后总128
这一步专门提醒一个容易遗漏的问题:拿配置文件里的数字去推导之前,先确认它是不是服务实际采用的值。不能把“默认Chunk是某个大小”当作所有运行实例的答案。
SET返回了,怎么知道后台已经调完?
容量调整由后台过程完成。只看SET没报错,还不足以说明全部工作结束。这个8.0.46实例可以这样查看状态:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_resize_status%';
其中Innodb_buffer_pool_resize_status_code为0表示当前没有调整操作在进行;其他状态码用于区分相应阶段。这个数值状态从8.0.31起提供,更老版本不要直接照搬字段名。
但“看到0”也要结合这次请求看:它可能是尚未发生新调整时的空闲状态。自动运行器会同时确认:容量已经到达预期有效值、状态回到0,并且在确实改变容量的轮次出现新的完成日志。它还读取缓冲池实际管理的页数,核对扩容时增加、缩容时减少。
记录旧容量、旧完成日志位置
|
提交调整请求
|
新容量 + 空闲状态 + 新完成日志
|
再核对页数方向
这是在确认“这次动作确实做完”,不是在测容量越大性能越好。缓冲池大小也不等于MySQL整个进程的内存占用,其他分配和管理结构仍然存在。
能在线调容量,是否也能顺手调Chunk?
本次分别尝试在线修改innodb_buffer_pool_chunk_size与innodb_buffer_pool_instances,都得到1238错误,说明它们是只读变量;这两个配置要在启动时安排,不能从“size能在线改”推断出三个都能在线改。
在线调整容量也不等于业务访问永远不受影响:调整中存在需要协调缓冲池访问的阶段。实际服务还要结合事务、负载、可用内存与变更窗口判断。这里使用小型专用实例,是为了先把有效值、调整单位和完成状态看清楚,不是推荐一套生产容量参数。
以后遇到“我明明设置了,怎么结果不一样”,可以先读三个有效值,算清调整单位,再看这次任务是否真的完成。这样比反复修改配置、重启、猜测缓存是否生效,更容易定位问题。
附:完整实验程序
程序使用Python3和解包的MySQL 8.0二进制,普通用户运行。BLOG_MYSQL_ROOT指定解包根目录,需要能找到usr/sbin/mysqld、usr/bin/mysql及相应动态库:
BLOG_MYSQL_ROOT=/path/to/mysql-package-root python3 verify_resize.py
每个实例独立建临时数据目录,只开Unix socket,不开放TCP端口,不连接现有数据库。三轮启动条件依次是16、32、128MiB请求Chunk,初始总容量均64MiB;一共执行10次容量请求,其中两次本来就等于当前容量。退出时关闭自建进程,保留日志与results.json用于核对。
这个实验把实际值与观察结果保存下来,不仅用纸上公式计算一个数字。前面的70→80与70→96可以直接对照各轮日志和状态记录;过大Chunk的初始化修正也有独立对照。
import getpass
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
base=Path(__file__).resolve().parent
package=Path(os.environ.get('BLOG_MYSQL_ROOT',
'/mnt/d/blog-ops/experiments/2026-10-03-candidates/mysql-packages/root'))
env=dict(os.environ,LD_LIBRARY_PATH=str(package/'usr/lib/x86_64-linux-gnu'))
MiB=1024*1024
record={'passed':False,'runs':[]}
def experiment(chunk, initial, requests):
run=Path(tempfile.mkdtemp(prefix='resize-',dir=base));(run/'data').mkdir()
sock=Path('/dev/shm')/(run.name+'.sock')
common=[str(package/'usr/sbin/mysqld'),'--no-defaults',
'--basedir='+str(package/'usr'),'--datadir='+str(run/'data'),
'--lc-messages-dir='+str(package/'usr/share/mysql'),'--user='+getpass.getuser(),
'--secure-file-priv=NULL','--innodb-redo-log-capacity=16777216',
'--innodb-buffer-pool-size='+str(initial*MiB),
'--innodb-buffer-pool-chunk-size='+str(chunk*MiB),
'--innodb-buffer-pool-instances=1','--log-error-verbosity=3',
'--log-error='+str(run/'server.log')]
subprocess.run(common+['--initialize-insecure'],env=env,check=True,capture_output=True,timeout=90)
client=[str(package/'usr/bin/mysql'),'--no-defaults','--protocol=SOCKET',
'--socket='+str(sock),'-u','root','--batch','--skip-column-names','--connect-timeout=2']
server=subprocess.Popen(common+['--skip-networking','--mysqlx=0','--socket='+str(sock),
'--pid-file='+str(run/'mysql.pid'),'--tmpdir='+str(run)],env=env,
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
result={'run':str(run),'requestedChunkMiB':chunk,'requestedStartupMiB':initial,'resizes':[]}
def sql(text,success=True):
r=subprocess.run(client,input=text,text=True,env=env,capture_output=True,timeout=8)
if success:
assert r.returncode==0,(r.stdout,r.stderr)
return r.stdout.strip().splitlines()
assert r.returncode!=0 and 'read only variable' in r.stderr,r.stderr
return r.stderr.strip()
def snapshot():
values=sql('SELECT @@innodb_buffer_pool_size,@@innodb_buffer_pool_chunk_size,@@innodb_buffer_pool_instances;')[0]
status=sql("SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_resize_status%';")
pages=sql('SELECT SUM(POOL_SIZE) FROM INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS;')[0]
return {'values':list(map(int,values.split('\t'))),'status':status,'poolPages':int(pages)}
try:
for _ in range(200):
assert server.poll() is None,(run/'server.log').read_text()
try:
result['version']=sql('SELECT VERSION();')[0];break
except AssertionError:time.sleep(.1)
else:raise TimeoutError('MySQL readiness')
assert result['version'].startswith('8.0.46')
result['initial']=snapshot()
actual_chunk=result['initial']['values'][1]
assert result['initial']['values'][2]==1
for requested in requests:
previous=snapshot();before_log=(run/'server.log').read_text()
before_completions=before_log.lower().count('completed to resize buffer pool')
expected=((requested*MiB+actual_chunk-1)//actual_chunk)*actual_chunk
sql('SET GLOBAL innodb_buffer_pool_size=%d;'%(requested*MiB))
deadline=time.monotonic()+20
while True:
now=snapshot();status=dict(line.split('\t',1) for line in now['status'])
completed=(run/'server.log').read_text().lower().count('completed to resize buffer pool')
changed=previous['values'][0]!=expected
if now['values'][0]==expected and status['Innodb_buffer_pool_resize_status_code']=='0' and (not changed or completed>before_completions):
break
assert time.monotonic()<deadline,(requested,now,(run/'server.log').read_text()[-2000:])
time.sleep(.05)
assert now['values'][1]==actual_chunk
if changed:
assert (now['poolPages']>previous['poolPages'])==(expected>previous['values'][0])
result['resizes'].append({'requestedMiB':requested,'expectedMiB':expected//MiB,
'before':previous,'after':now,'completionLogObserved':changed})
result['readonlyErrors']=[sql('SET GLOBAL innodb_buffer_pool_chunk_size=8388608;',False),
sql('SET GLOBAL innodb_buffer_pool_instances=2;',False)]
result['passed']=True
finally:
server.terminate()
try:server.wait(timeout=30)
except subprocess.TimeoutExpired:server.kill();server.wait()
result['serverStopped']=server.poll() is not None
result['socketRemoved']=not sock.exists()
return result
try:
record['runs'].append(experiment(16,64,[70,80,81,48,64]))
record['runs'].append(experiment(32,64,[70,96,64]))
record['runs'].append(experiment(128,64,[70,64]))
assert record['runs'][2]['initial']['values'][1]==64*MiB
record['passed']=True
finally:
(base/'results.json').write_text(json.dumps(record,indent=2))
print('RESULTS='+str(base/'results.json'))
本文通过MySQL 8.0.46小实例实验,深入解析InnoDB缓冲池在线调整时的Chunk对齐机制。当请求70MiB容量时,因Chunk大小(如16MiB或32MiB)导致实际分配为80MiB或96MiB;实验证明实际Chunk可能被初始化修正,且innodb_buffer_pool_chunk_size与innodb_buffer_pool_size均为只读变量,不可在线修改。需结合innodb_buffer_pool_resize_status确认调整完成。
260

被折叠的 条评论
为什么被折叠?



