验证器匹配可以省去正文传输,但不变的文件元数据也可能掩盖已经变化的字节。

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

验证器让接收方询问:自己保存的表示,是否仍与当前选中的表示相符。条件 GET 验证成功时,可以返回没有正文的 304,避免重新传输内容。新鲜度是另一个决定,它控制已存响应何时能直接复用而不发送这次请求。ETag 本身不指定缓存寿命,304 也不是一份替代正文;客户端需要保留原响应,才能利用成功验证的结果。 坏掉的样子 把所有 ETag 都叫作正文哈希,会掩盖 nginx 的一个重要实现细节。在这里测试的 1.28.0 静态文件实现中,生成的标签组合了修改时间和内容长度,并不计算文件内容哈希。如果替换字节时保留这两个输入,就可能保留同一个标签。于是,持有不同旧字节的客户端仍收到看似成功的验证,错误不能仅靠“响应里有 ETag”来排除。 强弱验证器适用于不同比较。对于需要字节身份的用途,强验证器必须能区分表示数据变化;弱验证器则可以把字节不同、但被认为语义等价的表示归在一起。W/ 并不是“接受字节等价变化”的标志,也不意味着这个标签适用于所有前提条件或字节范围续传。使用哪种比较,需要依据具体请求条件,而不是凭标签外观猜测。 条件请求不保证省掉应用计算。服务器可能先生成表示,之后才判断验证器是否匹配。这个静态 nginx 示例根本没有应用渲染器。较小的响应在这里证明节省了正文传输,却不能证明数据库查询、渲染成本或回源请求数必然减少。评估优化效果时,应把实际减少的数据传输和实际减少的后台工作分别测量。 快速验证 把脚本保存为 check-validators.py,先取得固定摘要的 nginx 镜像,再用 Python 和 Docker 运行。它通过宿主回环提供一个可丢弃的四字节静态文件,实测 nginx 版本为 1.28.0。实验只控制容器内文件的时间属性,不修改宿主时间,也不修改共享 Docker 虚拟机时钟: 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' files={'asset.txt':'AAAA','noetag.txt':'AAAA'} with lab(config('location = /noetag.txt { etag off; }'),files) as (proxy,request): def timestamp(value):docker('exec',proxy,'touch','-m','-d','@'+str(value),'/srv/asset.txt') timestamp(1577836800) status,headers,body=request('/asset.txt');assert status==200 and body==b'AAAA' tag=headers['etag'];modified=headers['last-modified'];results={'etag':tag,'last_modified':modified} cases={ 'matching_tag':({'If-None-Match':tag},304), 'weak_comparison':({'If-None-Match':'W/'+tag},304), 'matching_date':({'If-Modified-Since':modified},304), 'tag_takes_precedence':({'If-None-Match':'"different"','If-Modified-Since':modified},200), 'weak_if_match_rejected':({'If-Match':'W/'+tag},412), } for name,(conditions,expected) in cases.items(): status,_,body=request('/asset.txt',headers=conditions);assert status==expected,(name,status) if status==304:assert body==b'' results[name]=status docker('exec',proxy,'sh','-c','printf BBBB > /srv/asset.txt');timestamp(1577836800) status,current,body=request('/asset.txt');assert status==200 and body==b'BBBB' and current['etag']==tag status,_,body=request('/asset.txt',headers={'If-None-Match':tag});assert status==304 and body==b'' results['changed_bytes_preserved_metadata']=status timestamp(1577836802) status,current,body=request('/asset.txt',headers={'If-None-Match':tag}) assert status==200 and body==b'BBBB' and current['etag']!=tag and current['last-modified']!=modified results['changed_metadata']={'status':status,'etag':current['etag']} status,current,_=request('/noetag.txt');assert status==200 and 'etag' not in current print(json.dumps(results,sort_keys=True)) 最初文件内容是 AAAA,修改时间固定为 2020-01-01,产生 ETag "5e0be100-4"。GET 在 If-None-Match 中携带这个标签时,得到正文为空的 304。把请求标签加上 W/ 仍返回 304,因为该条件采用弱比较。在本次配置中,携带相等 If-Modified-Since 日期,也会得到 304。这些结果展示标签与日期两条验证路径。 ...

二月 17, 2028 · 2 分钟 · XB Field Notes