同一个符合条件的 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
来源描述的 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
客户端分别分支到 QUIC 与 UDP 上的 h3 或 TCP 上的 h2;路径不要求先建立 h2 连接。

HTTP3 走 UDP,HTTP2 仍走 TCP 兜底

HTTP/3 运行在 QUIC 与 UDP 之上,常用端口是 443,但协议不要求只能使用这个端口。HTTP/2 各流共用 TCP 的有序字节流,缺失片段可能阻塞无关请求的数据交付。QUIC 按流提供有序交付,某条流丢失数据不必阻塞另一条流;连接级拥塞控制与 HTTP 层依赖仍可能影响多个流,不能说每条流都有完全独立的拥塞控制或丢包恢复状态。 仅启用服务器模块还不够。客户端需要 HTTP/3 支持、可代表该源的端点、可验证的 TLS 身份,以及实际可用的 UDP 路径。发现机制和回退策略决定何时尝试它;不能把此前必须成功访问一次 TCP 服务当成所有客户端都要满足的协议前提。 两种传输,一个端口号 UDP:443 与 TCP:443 使用不同套接字,只是端口数字相同;并不是一个套接字同时监听两种传输。nginx 的 server 块可以并列配置 listen 443 quic reuseport; 与 listen 443 ssl; http2 on;。受支持系统上的 reuseport 用于每个 worker 的监听套接字,不是修复重复 server 声明或其他进程占用端口的万能选项。应先核对完整配置、实际错误与套接字归属,再判断是否需要修改。 ...

二月 12, 2028 · 3 分钟 · XB Field Notes
在没有 early data 的完整证书型 HTTPS 握手中,ClientHello 的 SNI 选择 TLS 配置与证书;稍后的 HTTP Host 参与请求路由。

SNI 让一个 IP 承载多个 HTTPS 站点

Server Name Indication(简称 SNI)在 TLS 握手中把目标主机名发给服务器,使同一个 IP 地址能够承载多张证书、多个站点。没有 SNI 时,分配不同地址是一种办法,却不是唯一办法:一张包含多个主体备用名称(SAN)的证书,或覆盖相应名字的通配符证书,也可以供多个站点共用。问题在于先后顺序:在不使用 early data 的完整证书型 HTTPS 握手中,服务器选证书发生在收到标识应用站点的加密 HTTP 请求之前,不能等读到 Host 再为这次握手换证书。 本文关注 nginx 的配置边界:哪个 server 块提供证书,名字没有匹配时采取什么策略,以及诊断命令究竟证明了什么。选中证书、客户端验证身份、把请求送到应用,是三个需要分别检查的决定,不能把其中一项成功当作另外两项也成功。 名字最先报出 客户端在 ClientHello 中携带 SNI,nginx 据此选择 TLS 服务配置。这个扩展使用已有的握手报文,不需要额外发送一次请求来询问站点。不过,把全过程说成“发生在加密之前”并不准确:TLS 1.3 在 ServerHello 之后加密证书报文。这里重要的是名字在证书选择时已经可用,而不是证书或整个握手始终明文传输。 客户端必须发送 SNI,服务器才能在这个阶段按名字选择站点。不带 SNI 时,nginx 从对应监听地址与端口的缺省服务配置开始处理;该配置可能提供证书,也可能拒绝握手。若收到证书,客户端仍需检查信任链以及自己预期的身份。收到一张证书不等于接受它,也不等于已经知道后续请求会进入哪个应用。 用 server_name 匹配 nginx 在相关监听地址和端口的 server_name 条目中匹配 SNI:精确名优先,然后是最长的前置星号通配名、最长的后置星号通配名,最后是配置顺序中第一个匹配的正则表达式。证书和私钥配置可以继承,也可以由多个站点共用;并不存在每个名字必须单独持有一张证书的规则。 ...

二月 7, 2028 · 2 分钟 · XB Field Notes
HTTP/1.1 与 HTTP/2 两条并行轨道都连接 gzip,区分传输选择与响应正文编码。

gzip、HTTP/2 与 Brotli 需要分别验证

HTTP/2 改变 HTTP 传输分帧和多路复用方式;gzip 与 Brotli 编码的是表示数据。启用 HTTP/2 不会自动压缩响应正文,启用 gzip 也不能证明客户端协商出了 HTTP/2。HTTP/2 的头部压缩又是另一种机制。可靠检查应分别记录协商协议、响应 Content-Encoding、传输正文字节数,以及实际解码后得到的内容,不能用其中一项代替其他结果。 坏掉的样子 发送 Accept-Encoding: gzip, br,然后把结果保存成 .br,并不能证明使用了 Brotli。服务器可能选择 gzip,也可能发送仍被请求接受的原始表示。真实编码由响应头说明。根据请求列表或输出文件名猜测,会导致使用错误解码器,也可能让报告声称获得了实际上根本没有发生的压缩结果。文件名只是本地命名,不是协议证据。 Brotli 支持还属于部署能力,并不是安装 nginx 后自然具备的功能。Brotli 过滤和预压缩文件处理由独立模块提供,需要针对实际二进制与配置确认可用性、构建兼容性。添加不受支持的指令可能让配置验证失败,而客户端声明接受 Brotli 不会替服务器安装功能。检查模块时,应以当前程序行为为准,不能从另一台机器的配置推断。 客户端自动解码会制造另一种测量陷阱。curl --compressed 收到较小编码正文后,可以保存成较大的解码文件。把这个文件大小与原始压缩输出比较,实际比较的是不同对象。反过来,只使用 --http2 却不检查协商结果,也可能把允许的协议回落误当成 HTTP/2 已经成功验证。命令选项表达请求意图,结果字段才说明本次发生了什么。 分别核对实际协议、编码与解码一致性。字节数来自历史文章报告,当前集成仍待验证。 快速验证 下文结果归属于历史文章报告,本轮没有重跑;保留的验证文件中未找到原始集成运行日志。运行需备好本地固定镜像并启用 Python 断言;清理仅尽力执行,不能据此保证所有资源已删除。 ...

一月 28, 2028 · 3 分钟 · XB Field Notes
客户端向 /old 发送 POST,收到 308 与 Location,再按原文夹具显式向 /destination 发送第二次 POST。

先决定让 nginx 跳转客户端,还是在内部重写

nginx 跳转与内部重写改变请求流程的不同部分。跳转返回状态和 Location,由客户端决定是否发起新请求;内部重写则在处理现有请求时改变 nginx 当前 URI。两者的选择决定客户端是否看到新地址、location 是否重新搜索,以及最终处理器收到怎样的路径和查询。不能因为都能让某个 URL 得到内容,就把它们当成同一种操作。 坏掉的样子 本来需要内部映射,却改成跳转,会增加客户端往返并暴露新 URL;本来需要公开永久迁移,却只做内部重写,客户端仍会继续使用旧地址。return 和 rewrite 也不能被当成总是默认相同跳转行为的通用缩写。明确状态码与重写标志,才能让预期转换可审阅,避免一行看似简单的配置隐藏行为差异。 状态选择影响方法语义和缓存。307、308 在自动跳转时保留请求方法,301、302 则允许历史上的 POST 改为 GET。永久不意味着每个客户端立刻更新所有已保存链接,临时也不意味着响应绝对不能缓存。缓存指令、请求上下文和客户端行为仍然重要,不能用状态名称替代明确的复用策略。 内部重写还可能选择错误处理器,或反复匹配自己。last 会为改变后的 URI 开始新 location 搜索;break 结束当前重写序列后,继续在现有 location 处理。查询参数可以按替换语法追加或丢弃。把这些区别视为可以互换,可能导致读取错误文件、重复参数,或进入返回 HTTP 500 的循环。 客户端新请求,或内部转换。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 下文集成实验的结果保留自历史运行记录;本次修订没有重新执行该实验,不能将这些记录当成本次环境的新实测。 ...

一月 21, 2028 · 2 分钟 · XB Field Notes
精确 /api 结束选择;普通最长前缀可被匹配正则接管,选中的 ^~ 前缀跳过正则。没有正则匹配时,使用记住的前缀。

nginx 前缀在匹配结束前只是候选配置

nginx location 在已选中的 server 内为请求选择配置。对于这里验证的平面前缀与正则结构,精确匹配会结束搜索;否则 nginx 先记住最长匹配前缀,再可能选择第一个匹配的正则。如果没有适用正则胜出,就使用先前记住的前缀。这是一个完整选择过程,不是简单执行配置文件里最先遇到的块,也不是只比较路径看起来有多具体。 坏掉的样子 前缀是字符串前缀,不会自动要求完整路径段。location /api 也能匹配 /apix。如果真正需要的是端点 /api 和 /api/ 下的后代,应明确表达这两个边界。依赖未声明的路径段边界,可能让只是共享开头字母的路径,进入错误处理器、缓存策略或访问规则。配置应反映真实 URL 集合,而不是依靠读者自行补充含义。 最长普通前缀不一定是最终选择。匹配 .php 的正则,可以接管一个也匹配了更详细 API 前缀的请求。把前缀移到正则之前,不会改变基本优先规则。竞争正则之间的声明顺序有意义,普通前缀则主要比较匹配长度,而不是出现位置。因此,单纯整理文本顺序不一定修复实际路由问题。 ^~ 在该前缀成为相关层级的最长匹配前缀时,抑制正则搜索。它不是正则运算符,也不是对所有以某个较短标记前缀开头的路径提供无条件保护。嵌套 location 和内部跳转需要额外推理;下面故意使用平面 server 结构,让每次选择都能明确对应一个规则,不把复杂嵌套情况隐藏在简化结论里。 前缀仍可能被正则接管。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 下文集成实验的结果保留自历史运行记录;本次修订没有重新执行该实验,不能将这些记录当成本次环境的新实测。 ...

一月 17, 2028 · 2 分钟 · XB Field Notes
两组六请求条用通过箭头、暂停线及拒绝叉,均接受三格、拒绝三格;延迟模式把其中两格标为等待,nodelay 让全部获准格直接通过。

nginx 突发容量决定哪些请求等待、哪些被拒绝

nginx 请求限流使用漏桶方法,依据配置速率计算超额请求。burst 决定可以接受多少超额,nodelay 和 delay 则影响已经被接受的超额请求何时继续处理。这里涉及准入与调度,并不是所有超过名义速率的请求都会失败,也不是直接测量应用里有多少并发工作。理解这几个不同维度,才能解释一次突发里为何同时出现成功、等待和拒绝。 坏掉的样子 把 nodelay 解释成“立即丢弃超额”,正好说反了它的作用。在突发额度以内,它允许超额请求不经过通常的延迟,直接继续处理;超过额度才拒绝。已经接受的突发仍消耗需要随时间恢复的容量。后端很快完成,并不会让下一批突发自动获得全新额度,因此不能把 nodelay 理解成只管单次批次、不累计速率债务。 被延迟的请求也不会自动变成一串 503。突发范围内获准等待的请求,可以在等待之后得到成功应用响应;超过配置额度才会按所选状态拒绝。客户端自己的截止时间可能在等待中到期,所以服务器能接受的排队,不一定满足客户端延迟契约。讨论结果时应区分服务端拒绝、获准等待和客户端主动超时。 配置语法同样需要验证。在 nginx 1.28.0 中,显式写 burst=0 会被拒绝,尽管省略 burst 的默认超额额度就是零。无效配置不是一种更严格但正常工作的限流策略。应同时做语法检查和真实请求验证,并确认受保护 location 确实进入限流阶段,而不是在更早的 rewrite 阶段通过 return 提前结束。 六客户端计数假定在额度恢复前到达。请求速率决定不限制长时间并发占用;旧标签链接保留,显示名称已为“漏桶”。 快速验证 下文结果归属于历史文章报告,本轮没有重跑;保留的验证文件中未找到原始集成运行日志。运行需备好本地固定镜像并启用 Python 断言;清理仅尽力执行,不能据此保证所有资源已删除。 将脚本保存为 check-burst.py,准备 Python、Docker 和固定镜像后执行。记录环境使用 nginx 1.28.0 和真实 HTTP 后端。每个案例采用独立的公开测试 key、每秒一个请求的速率,并同时放行六个客户端。nginx 在响应头中暴露本次限流决定,入口只发布到宿主回环,结束后尝试清理自己的实验资源。预期计数假定请求在下一次额度恢复前到达;调度延迟可能使这个时间前提失效: ...

一月 5, 2028 · 3 分钟 · XB Field Notes