dnsmasq 内,转发缓存随 TTL 老化,host-record 则来自配置。SIGHUP 清转发缓存,配置记录仍保留,即使本地 TTL 为零。

dnsmasq 区分转发缓存与本地配置名称

dnsmasq 可以向上游转发 DNS 查询,在有界缓存中保留支持的回答,也可以提供本地配置的名称。这些是不同的数据来源。重复得到相同回答,可能来自缓存命中,也可能来自本地配置。主机名与权威数据不一致、或者清缓存后仍然解析到原地址时,先确认来源十分重要;否则容易把有效配置误认为没有清干净的缓存。 先识别回答来源。转发缓存与本地配置名称具有不同寿命:本地 TTL 零不等于删除配置。SIGHUP 清缓存并重载特定数据来源,通常不重读主配置。次数与地址复述文章历史示例,本次没有重跑。 坏掉的样子 启动 dnsmasq,不会自动让所有应用使用它。客户端必须把 DNS 查询交给预期监听,有的应用还会使用其他解析服务。同样,本地监听正常,也可能转发到错误上游或者读取了意外 hosts 配置。在增加缓存容量或重启服务前,应先确认真实查询路径。如果失败查询根本没到达这个进程,对它反复操作不会改变调用方看到的结果。 本地名称可以有意覆盖上游数据。清理转发缓存不会删除这个配置名称,因此反复清空也不能让上游地址获胜。应把本地记录当作有负责人、有用途的配置管理。即使回答的本地 TTL 为零,dnsmasq 仍可持续返回该值,因为它来自配置生成,不是靠保留某份转发回答维持。零 TTL 与删除本地配置并不是同一件事。 缓存容量和新鲜度解决不同问题。更大缓存可以保留更多合适条目,却不会延长条目的有效 TTL,也不会让上游故障期间所有名称都能查询成功。新名称与过期条目仍需要合适的解析路径。缓存大小也不直接代表可同时发送多少上游查询,更不能单独保证应用可用。应分别观察容量、寿命、并发与上游健康,而不是用一个参数代替所有约束。 快速验证 将脚本保存为 check-dnsmasq.py,准备 Docker 后执行 python3 check-dnsmasq.py。固定环境使用 dnsmasq 2.90 和 BIND 9.18.49。实验转发器忽略环境中的 resolv 和 hosts 文件,明确绑定回环监听,并只转发到自己的临时权威服务器。它不发布宿主端口,没有 DHCP 配置,结束后清理自己的实例,不影响现有服务: ...

四月 15, 2028 · 2 分钟 · XB Field Notes
同一个符合条件的 200 缓存键从 MISS 到配置有效期为 2 秒的新鲜 HIT,再到 EXPIRED;另一行展示缓存锁把六个受控冷填充客户端合并为一次回源。

微缓存短暂复用新鲜条目,并显式合并并发未命中

微缓存让可复用响应保持很短的新鲜度寿命,常见尺度是几秒。在这个窗口里,请求同一选中表示的后续访问可以复用新鲜条目。响应可能落后于源站刚发生的更新,但不能仅因为它来自缓存,就称它在 HTTP 意义上已经过期。何时过期、是否允许提供过期内容、如何协调并发填充,是三个不同策略,需要分别定义。 坏掉的样子 TTL 短并不意味着共享响应对所有用户都安全。如果个性化请求读取或填充通用缓存条目,一个用户的表示就可能在另一个上下文里被交付。proxy_cache_bypass 根据条件禁止从缓存读取;proxy_no_cache 根据条件禁止保存响应。只控制其中一侧,可能让另一条路径不符合预期隐私边界。读取已有公开条目和保存新私有响应都需要考虑。 缓存键决定哪些请求共享响应。忽略有意义的查询参数或主机维度,可能合并不同表示;把任意变化数据全部放进键,又可能让复用失效。下面实验只有一个后端 authority,并保留包含参数的完整请求 URI。真实服务如果按租户、语言或其他输入改变内容,就必须确定自己的表示身份,不能把示例键当成适用于所有应用的通用公式。 仅仅启用缓存,也不保证冷启动突发只产生一次回源。在任何响应保存完成前,多个请求可能同时未命中同一条目。proxy_cache_lock 负责协调新条目的填充,并有有限的超时和年龄控制。这个锁不是 TTL,缩短新鲜度寿命也不会隐式开启锁。新鲜条目复用与并发未命中合并,需要通过不同对照才能验证。 新鲜度与填充是两项策略。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 下文集成实验的结果保留自历史运行记录;本次修订没有重新执行该实验,不能将这些记录当成本次环境的新实测。 将脚本保存为 check-microcache.py,准备 Python、Docker 和固定镜像后执行。记录环境为 Linux arm64 上的 nginx 1.28.0。可丢弃后端报告每条路径的请求计数,nginx 使用两秒有效期,并显式设置 Cookie 与 Authorization 绕过条件。监听仅发布到宿主回环,使用的会话和授权值都是公开测试标记,不连接真实账户: from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory import http.client,json,socket,subprocess,time,uuid NGINX='nginx@sha256:552e7481ca93ffccd046aa658dbbed22caefbc09c66fa7cd247cbb90b8a5c609' PYTHON='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def wait(predicate): deadline=time.monotonic()+15 while time.monotonic()<deadline: try: if predicate():return except OSError:pass time.sleep(.1) raise AssertionError('fixture did not become ready') @contextmanager def lab(configuration,files=None,backend=None): name='xbcatm-nginx-'+uuid.uuid4().hex[:10];tags=[name+':proxy',name+':backend'];containers=[name+'-proxy',name+'-backend'] try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory);(root/'files').mkdir();(root/'nginx.conf').write_text(configuration) for path,content in (files or {}).items(): target=root/'files'/path;target.parent.mkdir(parents=True,exist_ok=True) target.write_bytes(content if isinstance(content,bytes) else content.encode()) docker('network','create',name) if backend is not None: (root/'backend.py').write_text(backend) (root/'backend.Dockerfile').write_text(f'FROM {PYTHON}\nCOPY backend.py /backend.py\nCMD ["python", "/backend.py"]\n') docker('build','--network','none','-f',str(root/'backend.Dockerfile'),'-t',tags[1],str(root)) docker('run','-d','--name',containers[1],'--network',name,'--network-alias','backend',tags[1]) wait(lambda:docker('exec',containers[1],'python','-c','import socket;s=socket.create_connection(("127.0.0.1",8081),timeout=.5);s.close()',check=False).returncode==0) (root/'Dockerfile').write_text(f'FROM {NGINX}\nCOPY nginx.conf /etc/nginx/nginx.conf\nCOPY files /srv\n') docker('build','--network','none','-t',tags[0],str(root)) docker('run','--rm','--network',name,tags[0],'nginx','-t') docker('run','-d','--name',containers[0],'--network',name,'-p','127.0.0.1::8080',tags[0]) details=json.loads(docker('inspect',containers[0]).stdout)[0] binding=details['NetworkSettings']['Ports']['8080/tcp'][0];assert binding['HostIp']=='127.0.0.1';port=int(binding['HostPort']) def ready(): connection=socket.create_connection(('127.0.0.1',port),timeout=.5);connection.close();return True wait(ready) def request(path,method='GET',headers=None,body=None): connection=http.client.HTTPConnection('127.0.0.1',port,timeout=5) try: connection.request(method,path,body=body,headers=headers or {}) response=connection.getresponse();return response.status,{k.lower():v for k,v in response.getheaders()},response.read() finally:connection.close() request.port=port yield containers[0],request finally: docker('rm','-f',*containers,check=False);docker('image','rm',*tags,check=False);docker('network','rm',name,check=False) def config(locations,extra=''): return 'worker_processes 1;\nerror_log /tmp/error.log notice;\nevents {}\nhttp {\naccess_log off;\n'+extra+'\nserver { listen 8080; root /srv;\n'+locations+'\n}\n}\n' from concurrent.futures import ThreadPoolExecutor import threading backend=r''' from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer from urllib.parse import urlsplit,parse_qs import json,threading lock=threading.Lock();counts={};gates={'/lock':threading.Event(),'/nolock':threading.Event()} class Handler(BaseHTTPRequestHandler): protocol_version='HTTP/1.1' def log_message(self,*args):pass def do_GET(self): parsed=urlsplit(self.path) if parsed.path=='/control/state': with lock:data=dict(counts) elif parsed.path=='/control/release': gates[parse_qs(parsed.query)['path'][0]].set();data={'released':True} else: with lock:counts[self.path]=counts.get(self.path,0)+1;count=counts[self.path] if parsed.path in gates:assert gates[parsed.path].wait(15) data={'path':self.path,'count':count,'cookie':self.headers.get('Cookie'),'authorization_present':bool(self.headers.get('Authorization'))} body=json.dumps(data,sort_keys=True).encode();self.send_response(200) if parsed.path=='/cookie':self.send_header('Set-Cookie','fixture=public-example; Path=/') if parsed.path=='/private':self.send_header('Cache-Control','private') if parsed.path=='/no-store':self.send_header('Cache-Control','no-store') self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(body)));self.end_headers();self.wfile.write(body) ThreadingHTTPServer(('0.0.0.0',8081),Handler).serve_forever() ''' extra=''' proxy_cache_path /tmp/cache levels=1:2 keys_zone=pages:1m inactive=30s max_size=10m; map $http_cookie $skip_cookie { default 1; "" 0; } map $http_authorization $skip_auth { default 1; "" 0; } proxy_cache pages; proxy_cache_key "$scheme|$proxy_host|$request_uri"; proxy_cache_valid 200 2s; proxy_cache_bypass $skip_cookie $skip_auth; proxy_no_cache $skip_cookie $skip_auth; proxy_cache_lock on; proxy_cache_lock_timeout 5s; proxy_cache_lock_age 5s; add_header X-Cache $upstream_cache_status always; ''' locations=''' location /control/ { proxy_cache off; proxy_pass http://backend:8081; } location = /nolock { proxy_cache_lock off; proxy_pass http://backend:8081; } location / { proxy_pass http://backend:8081; } ''' with lab(config(locations,extra),backend=backend) as (_,request): def get(path,headers=None): status,h,b=request(path,headers=headers);assert status==200;return h.get('x-cache'),json.loads(b) results={} first=get('/item?x=1');second=get('/item?x=1');assert first[0]=='MISS' and second==('HIT',first[1]) other=get('/item?x=2');assert other[0]=='MISS' and other[1]['path']!=first[1]['path'] results['fresh']=[first[0],second[0]];results['different_query']=other[0] for name,headers in [('cookie',{'Cookie':'session=public-fixture'}),('authorization',{'Authorization':'Bearer public-fixture'})]: private=get('/item?x=1',headers);assert private[0]=='BYPASS' and private[1]!=first[1] assert get('/item?x=1')==('HIT',first[1]) new='/new-'+name;assert get(new,headers)[0]=='BYPASS' public=get(new);assert public[0]=='MISS' and public[1]['cookie'] is None and not public[1]['authorization_present'] results[name]={'warm_key':'BYPASS','public_after_private_miss':public[0]} for path in ['/cookie','private','no-store']: path='/'+path.lstrip('/');a=get(path);b=get(path) assert a[0]==b[0]=='MISS' and a[1]['count']==1 and b[1]['count']==2 results[path]=[a[0],b[0]] deadline=time.monotonic()+6 while time.monotonic()<deadline: value=get('/item?x=2') if value[0]=='EXPIRED':break assert value[0]=='HIT';time.sleep(.15) else:raise AssertionError('TTL did not expire') assert value[1]['count']==2;results['after_ttl']={'status':value[0],'count':value[1]['count']} for path,number,origin_count in [('/lock',6,1),('/nolock',4,4)]: barrier=threading.Barrier(number+1) def client():barrier.wait();return get(path) with ThreadPoolExecutor(max_workers=number) as pool: futures=[pool.submit(client) for _ in range(number)];barrier.wait() wait(lambda:get('/control/state')[1].get(path)==origin_count) time.sleep(.3);before=get('/control/state')[1][path] assert before==origin_count and not any(f.done() for f in futures) get('/control/release?path='+path);responses=[f.result(timeout=10) for f in futures] assert get('/control/state')[1][path]==origin_count statuses=sorted(r[0] for r in responses) if path=='/lock':assert statuses==['HIT']*5+['MISS'] and all(r[1]==responses[0][1] for r in responses) else:assert statuses==['MISS']*4 results[path]={'origin_requests_while_held':before,'cache_statuses':statuses} print(json.dumps(results,sort_keys=True)) 首次公开请求 /item?x=1 报告 MISS,下一次报告 HIT,并保留相同后端结果。/item?x=2 有自己的 MISS,响应中的路径也不同。持续检查第二个条目,最终观察到 EXPIRED,后端计数变为二。这依据服务器时钟观察实际过期,不假定精确到亚秒的切换时刻;测试关注状态变化及重新计算,而不是某个固定睡眠值。 ...

三月 12, 2028 · 3 分钟 · XB Field Notes
文中三次查询相隔 1.2 秒:TTL 1 时回源三次,TTL 20 时回源一次。到期本身不会安排查询。

DNS TTL 在重复回源查询与保留旧回答之间取舍

DNS TTL 是附着于缓存数据的寿命,不是每秒执行的刷新任务。只要条目仍被保留且有效,缓存可以复用它;过期后的下一次请求通常需要重新解析,除非启用了其他明确行为。无人查询的记录,并不一定在 TTL 归零时主动产生上游请求。因此讨论成本时,要同时看请求到达方式与缓存状态,不能把 TTL 直接当成定时查询频率。 文中受控示例将三次查询间隔设为 1.2 秒:TTL 1 回源三次,TTL 20 回源一次。下游收到剩余寿命,而非重置的 TTL。此图复述来源所述行为,本次制图没有运行解析器实验。 坏掉的样子 持续查询相同数据时,缩短 TTL 可能增加上游工作,却不会让所有权威流量按同一个固定倍数增长。查询频率、解析器共享程度、缓存容量、预取和名称多样性都会影响结果。一个热门名称使用短 TTL,与大量只问一次的名称流,行为不同。没有这些负载条件,就无法仅凭 TTL 数字预测真实查询量或容量需求。 长 TTL 允许在权威数据修改后继续使用先前回答。该缓存仍可能符合最初收到的寿命要求,尽管内容已经不同于最新区域。把所有不一致都叫成过期或损坏,会混淆实际契约。TTL 表示允许保留多久,不承诺持续与权威同步。修改 TTL 同样不能追溯改变已经保存的更长寿命,需要等那些旧副本自然退出普通有效期。 多层转发通常不会让每一跳重新获得原始完整 TTL。缓存返回剩余寿命,下一层再继续计时。按层数累加完整 TTL,会虚构普通合规转发并不产生的传播延迟。本地 TTL 覆盖、提供陈旧回答等是另外的配置行为,存在时应明确识别。不能先假设所有解析器都遵守同一种特殊策略,再用这个假设解释实际观测。 ...

三月 1, 2028 · 2 分钟 · XB Field Notes
输入匹配时复用已记录的 RUN 结果。原文简单依赖链中,改变文件权限或内容会使 COPY 及其依赖的 RUN 失效;只改 mtime 不会。

Docker 构建缓存复用已记录的工作,也可能复用过期结果

Docker 构建缓存保存构建操作的结果,让后续构建在相关输入仍匹配时复用已有工作。缓存命中不表示命令重新运行并生成相同输出,而是构建器接受了一份先前结果。这个区别直接影响软件包安装、生成时间戳、远程下载,以及输出可能变化却没有相应声明输入变化的操作。理解缓存时,首先要区分“再次执行”和“取出已经执行过的结果”。 依据原文历史样例说明机制。当前审核仍为已修订、执行受阻。本次绘图未执行 Docker 命令,未增加运行、页面、集成或发布验证。 坏掉的样子 没有变化的 RUN apt-get update 可以直接复用昨天的结果,完全不访问软件仓库。构建器通常不会为了判断这条 RUN 是否仍然新鲜,先检查远端仓库有没有更新。因此,缓存构建成功不能独立证明软件包已经收到最新安全修复。刷新策略必须明确,并与项目要求的可复现性一致,不能依赖某次构建恰好缓存失效。 COPY 的输入不同于普通命令字符串。文件内容和相关元数据参与缓存计算,但源文件修改时间本身不参与。部署脚本如果只是 touch 文件,依赖它的 RUN 仍可能继续命中缓存。反过来,即使文本完全相同,改变可执行权限也可能使 COPY 失效,从而重新执行后续工作。排查时需要比较实际输入,不能只看文件大小和肉眼可见的文字。 基础镜像、步骤实际使用的构建参数和前置依赖也会影响结果。缓存失效沿依赖传播,不能把复杂多阶段构建一律解释成从某行开始全部重做的直线。下面使用简单的单阶段链:复制文件发生变化,就使其后的时间戳 RUN 重新执行,而固定基础镜像的文件系统层仍可继续复用。这种受控结构让实验结论容易对应到真实原因。 快速验证 使用宿主 Python 运行脚本,并连接一个可用于实验的 Docker 构建器。如果本地没有指定摘要,先拉取它。实测环境是 Docker 29.3.1、Buildx 0.32.1,以及 Linux arm64 的 Python 3.11.16 基础镜像。构建 RUN 指令和运行容器关闭外部网络,但镜像解析仍可能访问仓库。脚本删除自己的临时标签,不清理其他镜像,也不对全局构建缓存执行 prune: ...

二月 20, 2028 · 1 分钟 · XB Field Notes
来源描述的 nginx 1.28.0 实验:AAAA 改成 BBBB,修改时间和四字节长度不变,ETag 相同,旧标签仍得到 304。

nginx 验证器是否可靠,取决于表示身份是否准确

验证器让接收方询问:自己保存的表示,是否仍与当前选中的表示相符。条件 GET 验证成功时,可以返回没有正文的 304,避免重新传输内容。新鲜度是另一个决定,它控制已存响应何时能直接复用而不发送这次请求。ETag 本身不指定缓存寿命,304 也不是一份替代正文;客户端需要保留原响应,才能利用成功验证的结果。 文章描述 nginx 1.28.0 的固定版本反例:修改时间与长度相同,可能让不同字节共享 ETag。本次核验未重新运行该实验。 坏掉的样子 把所有 ETag 都叫作正文哈希,会掩盖 nginx 的一个重要实现细节。在固定版本 1.28.0 的静态文件实现中,生成的标签组合了修改时间和内容长度,并不计算文件内容哈希。如果替换字节时保留这两个输入,就可能保留同一个标签。于是,持有不同旧字节的客户端仍收到看似成功的验证,错误不能仅靠“响应里有 ETag”来排除。 强弱验证器适用于不同比较。对于需要字节身份的用途,强验证器必须能区分表示数据变化;弱验证器则可以把字节不同、但被认为语义等价的表示归在一起。W/ 并不是“接受字节等价变化”的标志,也不意味着这个标签适用于所有前提条件或字节范围续传。使用哪种比较,需要依据具体请求条件,而不是凭标签外观猜测。 条件请求不保证省掉应用计算。服务器可能先生成表示,之后才判断验证器是否匹配。这个静态 nginx 示例根本没有应用渲染器。较小的响应在这里证明节省了正文传输,却不能证明数据库查询、渲染成本或回源请求数必然减少。评估优化效果时,应把实际减少的数据传输和实际减少的后台工作分别测量。 快速验证 下文结果保留自来源中的实验报告,并非本次新运行的 Docker 或 nginx 结果。把脚本保存为 check-validators.py,先取得固定摘要的 nginx 镜像,再用 Python 和 Docker 运行。它通过宿主回环提供一个可丢弃的四字节静态文件,实测 nginx 版本为 1.28.0。实验只控制容器内文件的时间属性,不修改宿主时间,也不修改共享 Docker 虚拟机时钟: ...

二月 17, 2028 · 2 分钟 · XB Field Notes
PostgreSQL 共享缓冲位于 OS 页缓存上方;OS 命中从内存返回数据,避开下方存储路径。

PostgreSQL 使用两层数据缓存

普通缓冲式关系文件读写中,PostgreSQL 的共享缓冲位于操作系统文件缓存之上。命中共享缓冲时,不需要再次通过下面的文件读取路径;没有命中时,也可能由操作系统直接从内存提供数据。同一文件内容可以在两层各有副本,所以这里并不承诺数据库与内核共用同一份物理内存,更不能把两层缓存理解为完全没有重复占用。mmap 共享内存配置针对共享区域,不能据此推断普通表文件被直接内存映射。 坏掉的样子 执行计划里的共享读取计数,表示块被读入 PostgreSQL 缓冲池,不证明存储设备实际执行了一次物理读取。如果把每次共享读取都算成磁盘未命中,已经温热的操作系统缓存也会被误认为冰冷,从而引出错误的内存配置判断。同样,第二次查询更快,也不能独自说明究竟是哪一层缓存发生了变化,或是否只是其他执行条件改变。 缓存驻留是临时状态。其他查询可能挤走页面,工作集超过容量时,也不能指望所有页永久留在内存里;关系扫描采用的缓冲策略还可能影响复用方式。因此应使用已知大小的工作集、检查实际访问路径,并把耗时结论与真正观察到的缓存层事实分开。没有设备层测量,就不能仅凭一个计划字段宣布磁盘速度发生了变化。 Postgres 有两层缓存:机制图。 快速验证 使用 PostgreSQL 18 可丢弃实例和超级用户,确保附带的缓冲观察与预热扩展可安装、且尚未安装。保留的较早运行使用 PostgreSQL 18.6 单用户后端及一百二十八兆字节共享缓冲。本次来源复核没有重跑;psql 传输与并发活动仍未验证。下面的数据可以轻松放下。通过一个全新的 psql -X -v ON_ERROR_STOP=1 会话按顺序执行。关系驱逐函数用于开发测试,这里只针对隔离实验创建的对象操作: CREATE SCHEMA cache_layer_demo; CREATE EXTENSION pg_buffercache WITH SCHEMA cache_layer_demo; CREATE EXTENSION pg_prewarm WITH SCHEMA cache_layer_demo; CREATE TABLE cache_layer_demo.items (id integer PRIMARY KEY,payload text) WITH (autovacuum_enabled=false); INSERT INTO cache_layer_demo.items SELECT g,repeat(md5(g::text),10) FROM generate_series(1,12000) AS g; VACUUM (ANALYZE) cache_layer_demo.items; CREATE VIEW cache_layer_demo.residency AS SELECT count(*) AS heap_buffers FROM cache_layer_demo.pg_buffercache WHERE reldatabase=(SELECT oid FROM pg_database WHERE datname=current_database()) AND relfilenode=pg_relation_filenode('cache_layer_demo.items') AND relforknumber=0; SET max_parallel_workers_per_gather=0; SELECT pg_relation_size('cache_layer_demo.items')/current_setting('block_size')::int AS heap_blocks; SELECT heap_buffers>0 AS initially_buffered FROM cache_layer_demo.residency; 本次构建的主堆有五百四十六个块,初始缓存比较返回真。观察视图限制数据库、文件标识与主分支,并假定这个新建实验只使用默认表空间。通用监控还必须区分表空间,因为文件编号不是数据库内跨表空间唯一的物理身份。每个缓冲条目的观察是一致的,但整个视图并不是同一瞬间的原子缓存快照。接着,仅驱逐这个关系的共享缓冲: ...

一月 27, 2028 · 1 分钟 · XB Field Notes
时钟与满容量容器表示两种不同未命中原因;正 TTL 不能保护符合候选条件的键免于容量逐出。

Redis 的过期是截止时间,逐出是容量策略

过期与逐出都可能让一次成功查找变成未命中,但它们回答的是不同问题。过期服从键的截止时间,逐出则在内存压力下移除符合条件的键。延长 TTL 不能阻止容量驱动的逐出,扩大内存也不能纠正应用写错的截止时间。必须先确定实际发生了哪种变化,才能判断应该检查写入协议、调整容量,还是重新设计缓存用途。 时间与容量是两个独立维度 带有过期设置的键在 Redis 中称为 volatile。Redis 会在访问和主动清理过程中检查过期键,但这不承诺键一定存活到截止时间。显式删除、替换以及适用的逐出策略,都可能提前改变结果。因此,不管最初设定的期限还有多久,缓存客户端都必须能处理未命中,不能把较长 TTL 当作某条数据不会丢失的保证。 maxmemory 是 Redis 进行内存管理决策时使用的统计上限,不是操作系统 RSS 的硬上限。复制与追加日志缓冲区可能包含不计入逐出判断的内存,进程开销、分配器行为和持久化任务也需要容量余量。大型命令还可能短暂超过配置值。规划时既要考虑缓存预算,也要考虑运行整个进程的环境,不能把两者设成同一个数字就认为足够。 先确定候选范围,再讨论排序 allkeys 类策略不管键有没有 TTL,都会把它纳入候选;volatile 类策略则只考虑带过期设置的键。在各自候选范围内,LRU 偏向最久未访问的键,LFU 使用访问频率信息,random 类策略随机选择。Redis 的 LRU、LFU 是近似算法,不保证全局精确的受害键顺序。volatile-ttl 偏向剩余寿命较短的条目,也不是只清除已经到期的数据。 noeviction 在无法满足内存限制时拒绝增长内存的命令,而不是为了腾空间丢键;已有数据仍可能正常读取。但它不是持久性保证,普通过期、显式删除、进程故障和持久化配置仍然分别起作用。应检查当前实例实际配置,不能根据某种部署形式或集群名称,推断所有环境都使用相同默认策略。 volatile 策略如果找不到带 TTL 的候选键,在这种场景下就表现得像 noeviction。选了 volatile-lru,不会自动把持久键变成可丢弃数据。反过来,把会话或权威记录与可重建缓存混放在 allkeys 策略之下,它们也会成为候选。键名前缀只是命名约定,不会给逐出算法建立独立的数据保护边界。 坏掉的样子 未命中突然增加,可能来自过期、逐出、主动失效或键构造变化。需要把 expired_keys、evicted_keys 的增量,与同期请求负载和写命令结果一起看。这些是累计计数,应比较有限时间区间,并考虑重启或重置;仅看到逐出数上升,还不能知道哪个应用功能失去了哪些数据。 ...

十二月 26, 2027 · 2 分钟 · XB Field Notes
Fetch no-store 取得版本二,原有缓存版本一仍保留;reload 取得并存入版本三。响应 Cache-Control 策略与 Fetch 缓存模式应分开。

Cache-Control 是缓存与源站之间的约定

缓存策略需要行为测试,而不只是检查头部是否存在。两个响应即使都带 max-age,也可能因为其中一个要求验证、另一个禁止存储而表现不同。请求本身同样影响结果:浏览器可以绕过已有副本,却不修改源站的响应策略,也不删除更早保存的缓存条目。要知道用户真正拿到什么,必须观察完整的重复请求过程。 Fetch no-store 取得版本二,原有缓存版本一仍保留;reload 取得并存入版本三。响应 Cache-Control 策略与 Fetch 缓存模式应分开。 仅为来源示意;本任务未执行示例。 双方都要读同一份约定 Cache-Control 指令具有方向性。请求指令不会自动复制到响应里,RFC 9111 还明确将请求指令描述为建议性的要求;响应指令则约束遵守协议的缓存。中间层即使自己不实现缓存,也必须在转发中保留相应缓存指令。部署配置可能另有覆盖规则,但“代理按自己的配置做了”不等于已经证明源站声明的策略得到执行。 应拆开三个问题:这条响应能否存储,当前年龄是否仍在适用的新鲜度寿命内,这次请求是否允许不经验证直接复用。这里的 no-store 禁止 HTTP 缓存存储;不带字段名限定的 no-cache 允许存储,但复用前必须成功验证。示例不包含能够在特定条件下覆盖 no-store 的 must-understand 组合,不能据此推导所有指令组合的行为。max-age 设定新鲜度,must-revalidate 则禁止响应过期后未经验证就复用。响应仍然新鲜时,must-revalidate 本身并不要求每次回源,不能把它误解成 no-cache。 ...

十二月 20, 2027 · 2 分钟 · XB Field Notes