缓冲池设成70MiB,为什么变成80MiB?用小实例看懂Chunk和在线调整

本文通过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确认调整完成。

缓冲池设成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请求容量本次完成后的容量
16MiB70MiB80MiB
16MiB80MiB80MiB
16MiB81MiB96MiB
16MiB48MiB48MiB

请求的目标已经是当前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'))
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值