相等的 8 GiB 范围示意 2 MiB 比 4 KiB 映射更少;分块数量不按比例,也没有测量 TLB 缺失。

巨型页在分配灵活性与翻译覆盖之间取舍

巨型页可以增加每条地址翻译缓存能够覆盖的内存范围,在合适负载下减少翻译开销。但效果取决于访问模式和硬件,不能仅因为申请了很大一块内存,就断定程序频繁发生 TLB 缺失。描述工作集需要的页数减少,也不表示所有翻译能够同时装入处理器的 TLB;页数算术、硬件容量与真实性能是三个不同问题。 页大小是一项输入 x86 常见基准页为 4 KiB,但 Linux 的页大小依赖架构与配置,应读取运行系统而不是把四千多字节当作普遍常量。支持的巨型页尺寸同样会变化;2 MiB 与 1 GiB 是常见例子,不是所有处理器的完整列表。跨机器比较时,应先确认单位和实际尺寸,再比较统计数字。 对理想化的连续 8 GiB 范围,4 KiB 粒度需要 2,097,152 个页面,2 MiB 粒度只需 4,096 个。这只是映射覆盖率计算,没有测量硬件 TLB 容量、缺失率、页表遍历延迟,或同时驻留的翻译数量。不同页面大小对应的翻译缓存结构还可能具有不同容量,因此不能把两种页数之差直接称为实际节省的 TLB 项。 显式池与透明分配 Hugetlb 提供显式申请的大页,并由内核维护相应池。持久池中的页面不能同时用于普通分配,也不能在内存压力下换出。池可以在启动时准备,也能在运行中调整,还可能允许额外的 surplus 页面。启动较早时通常更容易获得连续内存,但这并不表示大页只能一次性在开机前决定数量,之后永远无法改变。 透明巨型页采用另一套机制。依内核支持和政策,较大的匿名内存分配既可能发生在缺页时,也可能由后续合并产生。THP 不只是后台线程,也不要求管理员先准备 hugetlb 池。现代内核还可以支持多种 THP 尺寸,不能把每次透明分配都等同于一个两兆字节映射,更不能用 hugetlb 总数代替 THP 使用量。 ...

十二月 21, 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
EXPLAIN ANALYZE 会真正执行。回滚到保存点可以恢复表变更,但已经消耗的序列值不会归还。图示取自文章示例,本次未重跑。

EXPLAIN ANALYZE 把查询真的跑一遍

EXPLAIN 展示 PostgreSQL 的执行计划,EXPLAIN ANALYZE 还会真正执行语句并测量工作量。最有用的比较,是同一个节点的估计行数与实际行数。两者都不能单独当成结论:估计来自模型,实测则只描述特定数据、缓存、并发和插桩条件下的一次执行。不能把前者称为谎言,也不能把后者当成脱离环境的永恒事实。 ANALYZE 会真正执行。文章示例区分可恢复的表变更与已消耗的序列值;本图没有重新运行实验。 先比较同一种量 计划中的 cost 是相对成本单位,不是毫秒。两个成本值分别描述启动成本与完成成本,带 LIMIT 的查询可能更重视尽早得到前几行,因此选择不同计划。actual time 才使用毫秒。即使成本和耗时恰好都打印成一百,也不能把它们直接相减或求比例;它们本来就不表示同一件事。 一个节点被重复执行时,actual rows 和节点耗时是每次循环的平均值,需要结合 loops 理解总工作量。但不能把所有节点耗时简单相加,因为父节点已经包含子节点的工作,重复相加会重复计算。并行执行时更不能用这种方式猜总延迟。先沿树看输入输出、循环次数、过滤条件和行数流向,再决定哪个位置值得调查。 BUFFERS 区分 shared、local 和临时块活动。shared hit 表示 PostgreSQL 在共享缓冲区里找到了块;shared read 只表示需要读入,操作系统仍可能从页缓存满足它,不能直接断言每次都访问了物理磁盘。临时表使用本地缓冲区,排序或哈希溢出则可能读写临时文件。父节点计数包含子节点,逐行把所有计数加起来同样会夸大 I/O。 ...

十二月 16, 2027 · 2 分钟 · XB Field Notes
两个常量条件汇入同一个参数化结构,再分别形成总时间与平均时间摘要,要求身份与采集窗口相同。

pg_stat_statements 在有效采集窗口内比较查询代价

pg_stat_statements 把语句执行归组并累计代价。它既能找出调用很频繁、单次很短但总耗时很大的查询,也能帮助定位单次特别慢的查询。这是两个不同问题,任何一种排名都不能单独证明某项修改会改善业务延迟。执行时间也不等于处理器消耗、存储等待或完整请求时长。先明确要优化的现象,再选择统计列,才能避免把排行榜本身当成优化结论。 归组有用,但有明确边界 PostgreSQL 18 通常把代表性查询文本中的常量替换为参数符号,并按分析后的结构归组。条目身份包含用户、数据库、查询标识,以及是否为顶层执行;查询文本并不是完整身份。对象解析不同,可能让相同文本进入不同条目;相关分组范围内,也存在哈希碰撞把无关结构合并的微小可能。因此监控应保留足够身份字段,而不是只截取一段 SQL 当作永远可靠的主键。 代表性文本用于诊断,不是每条原始请求的审计流水。有些工具语句或归一化失效场景仍会保留字面量,应限制访问并避免把秘密嵌进 SQL。查询标识不承诺跨大版本或任意目录变化保持稳定。PostgreSQL 18 还会对某些长度不同的常量列表归一化;换到另一个版本时,需要查对应手册,不能把当前显示形式当成所有历史版本的统一行为。 根据问题选择排名 总执行时间适合寻找累计数据库工作量大的对象。平均值、最大值与离散程度描述单次调用的分布特点,但它们不是分位数,也不能单独指出原因。波动可能来自参数、缓存状态、竞争或计划变化。看到标准差较大时,应继续检查计划和等待,不能直接宣布发生了锁竞争;平均较高也不自动意味着规划器选择错误。 并发会话的执行区间会重叠,所以累计执行时长可以超过统计窗口的墙钟时间。这不证明服务器重启过,也不证明数据无效。启用计划统计时,规划耗时另有字段。共享块读取表示数据库缓冲管理器发起的读取,仍可能由操作系统缓存满足。乘以服务器块大小后得到的是请求读取量,不是物理设备实际流量;开启输入输出计时增加耗时测量,基础块计数并不依赖它。 坏掉的样子 模块需要在服务器启动时通过 shared_preload_libraries 加载,检查所用数据库也需要创建 SQL 扩展。查询标识计算必须可用,通常由 compute_query_id 的 auto 或 on 提供,也可以来自外部模块。缺少配置、权限限制和关闭跟踪是不同失败,应读取实际错误,而不是把任何缺失条目都解释成没有负载发生。监控查询能够运行,也不代表它有权看见所有其他用户的文本。 可靠增量要求条目身份与采集窗口都可比较。PostgreSQL 18 提供每条记录的 stats_since、最小最大值的独立起点,以及全局重置和淘汰信息。定向重置或淘汰可以只破坏一个条目的连续性,而不重置整个视图。差值非负也不足以证明窗口连续:重置之后积累足够工作,可能把异常掩盖。新条目和窗口变化应重新建立基线,不能跨越它们直接做减法。 归一化受条目身份和采集连续性约束。修订夹具绑定四项身份字段;执行仍有保留,未声明新的 SQL 或耗时结果。 快速验证 使用全新且没有其他负载的 PostgreSQL 18 可丢弃集群和数据库,预先加载模块、启用内置查询标识与顶层语句跟踪,并让测试角色能够看见和使用或安装扩展。保持角色、搜索路径与对象不变,实验期间不得有并发负载、重置或条目淘汰。在一个全新会话中执行下面 SQL,任何错误都应作为失败处理。成功时删除测试表和临时视图;错误可能留下这些对象,新安装的扩展也会保留。它不重置任何已有统计,因此不会为了演示排名而破坏监控窗口。 CREATE EXTENSION IF NOT EXISTS pg_stat_statements; CREATE TABLE pgss_demo (id integer); INSERT INTO pgss_demo SELECT generate_series(1,1000); CREATE TEMP TABLE pgss_key ( userid oid NOT NULL, dbid oid NOT NULL, toplevel boolean NOT NULL, queryid bigint NOT NULL CHECK (queryid <> 0), PRIMARY KEY (userid,dbid,toplevel,queryid) ); DO $$ DECLARE first_plan json; second_plan json; first_id bigint; second_id bigint; BEGIN IF current_setting('compute_query_id') NOT IN ('auto','on') OR current_setting('pg_stat_statements.track') <> 'top' THEN RAISE EXCEPTION 'requires built-in query identifiers and top-level tracking'; END IF; EXECUTE 'EXPLAIN (VERBOSE, FORMAT JSON) SELECT sum(id) FROM pgss_demo WHERE id < 100' INTO STRICT first_plan; EXECUTE 'EXPLAIN (VERBOSE, FORMAT JSON) SELECT sum(id) FROM pgss_demo WHERE id < 500' INTO STRICT second_plan; first_id := (first_plan->0->>'Query Identifier')::bigint; second_id := (second_plan->0->>'Query Identifier')::bigint; IF first_id IS NULL OR first_id = 0 OR second_id IS DISTINCT FROM first_id THEN RAISE EXCEPTION 'expected matching nonzero query identifiers'; END IF; INSERT INTO pgss_key SELECT (SELECT oid FROM pg_roles WHERE rolname=current_user), (SELECT oid FROM pg_database WHERE datname=current_database()), true,first_id; END $$; CREATE TEMP VIEW pgss_target AS SELECT s.* FROM pg_stat_statements AS s JOIN pgss_key AS k USING (userid,dbid,toplevel,queryid); CREATE TEMP TABLE pgss_window AS SELECT dealloc,stats_reset FROM pg_stat_statements_info; SELECT sum(id) FROM pgss_demo WHERE id < 100; CREATE TEMP TABLE pgss_first AS SELECT userid,dbid,toplevel,queryid,calls,stats_since FROM pgss_target; SELECT sum(id) FROM pgss_demo WHERE id < 500; SELECT userid,dbid,toplevel,queryid,query,calls,rows, round(total_exec_time::numeric,3) AS total_ms, round(mean_exec_time::numeric,3) AS mean_ms,stats_since FROM pgss_target; DO $$ BEGIN IF (SELECT count(*) FROM pgss_first) <> 1 OR (SELECT count(*) FROM pgss_target) <> 1 OR (SELECT count(*) FROM pgss_window) <> 1 THEN RAISE EXCEPTION 'expected one identity-bound entry and collection baseline'; END IF; IF NOT EXISTS ( SELECT 1 FROM pgss_first AS a JOIN pgss_target AS b USING (userid,dbid,toplevel,queryid) CROSS JOIN pgss_window AS w CROSS JOIN pg_stat_statements_info AS i WHERE a.calls=1 AND b.calls=2 AND b.calls-a.calls=1 AND b.stats_since=a.stats_since AND i.dealloc=w.dealloc AND i.stats_reset=w.stats_reset) THEN RAISE EXCEPTION 'expected calls 1 then 2 in one unchanged collection window'; END IF; END $$; SELECT userid,dbid,toplevel,queryid,calls, round(total_exec_time::numeric,3) AS total_ms, round(mean_exec_time::numeric,3) AS mean_ms FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10; SELECT userid,dbid,toplevel,queryid,calls, round(mean_exec_time::numeric,3) AS mean_ms FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10; SELECT dealloc,stats_reset FROM pg_stat_statements_info; SELECT round((sum(shared_blks_read)::numeric * current_setting('block_size')::numeric)/1073741824,6) AS requested_read_gib FROM pg_stat_statements; DROP VIEW pgss_target; DROP TABLE pgss_first,pgss_window,pgss_key; DROP TABLE pgss_demo; SELECT 'pgss fixture completed' AS completion; 两个求和结果应分别是四千九百五十和十二万四千七百五十。不带 ANALYZE 的 EXPLAIN 在不执行求和的情况下取得两个查询标识,要求二者相同且非零。目标视图使用该标识、当前用户、数据库和顶层标志做完整身份连接,不用代表性文本匹配。两份快照要求先一次、后两次调用,并核对 stats_since 与全局重置、淘汰元数据未变。这些检查依赖无其他负载的前提,不保证哈希永不碰撞,也不是并发监控方案。两个通用排名完全可能相同,它们展示的是排序依据的区别,不制造必然不一致的承诺。对双精度耗时先转换为 numeric,才可以使用指定小数位数的两参数 round。 ...

十二月 12, 2027 · 3 分钟 · XB Field Notes
客户端提供移位1,服务器提供移位2。建立连接后,服务器通告字段53,213乘以4,得到212,852字节接收窗口。SYN 与 SYN-ACK 的窗口字段不缩放。

TCP 窗口缩放分别扩展两个方向的接收窗口编码

TCP 头部的接收窗口字段只有十六位。窗口缩放让已建立连接依据握手期间确定的移位解释这个字段,从而允许接收端通告超过65535字节的空间。两个方向可以采用不同移位值。它扩展的是接收窗口信息的编码,不会直接扩大每个缓冲区,也不保证发送端始终让那么多数据处于未确认状态。 缩放因子属于通告接收窗口的端点,并在建立连接时固定。更大的编码范围可解除一种限制;拥塞、应用与实际路径仍需测量。 坏掉的样子 高带宽且 RTT 较长的路径,可能需要远多于未缩放窗口所能描述的在途数据。例如每秒一百兆比特、往返八十毫秒,对应大约一百万字节在途数据;65535字节窗口会形成明显更低的理想速率上限。带宽时延积用于容量估算,不是吞吐量实测,也不保证链路不存在其他约束,不能用一个乘法直接宣布应用应达到某个速度。 发送端还受到拥塞窗口限制,并且必须有应用数据可发。扩大接收窗口编码,无法修复过小的拥塞预算、停止读取的应用、丢包或被阻断的路由。零窗口经过缩放仍然是零。应判断究竟哪个资源限制进展,不能仅因握手里存在缩放选项,就断言连接理应跑满链路,也不能把所有低吞吐问题都归因于缓冲配置。 对协商的误解还会导致抓包解释错误。双方都必须提供选项,缩放才能生效;每个因子用于解释提供它的端点所通告的窗口。SYN 与 SYN-ACK 自身的窗口字段从不缩放。使用反方向的移位值,或把 SYN 窗口也乘上因子,即使数据包字节完全正确,也会算出错误容量。报文值需要结合方向和连接阶段解释。 快速验证 将脚本保存为 check-window-scaling.py,准备 Docker 后运行 python3 check-window-scaling.py。一次性环境使用 Python 3.11.16、iproute2 6.1.0 和 tcpdump 4.99.3,在 Linux 6.12.76 上验证。NET_ADMIN 与 SYS_ADMIN 支持嵌套命名空间及临时 proc 挂载。端点通过新建 veth 相连,所有设置仅作用于实验范围: 以下环境与数值结果来自原有文章报告,本次没有独立重现。容器镜像固定并不固定宿主内核,也不保证所列软件包版本仍然可取得。 from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid recipe='FROM python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84\nRUN apt-get update && apt-get install -y --no-install-recommends iproute2=6.1.0-3 iputils-ping=3:20221126-1+deb12u1 nftables=1.0.6-2+deb12u2 tcpdump=4.99.3-1 traceroute=1:2.1.2-1 procps=2:4.0.2-3 && rm -rf /var/lib/apt/lists/*\nCMD ["python3"]\n' program=r""" from contextlib import ExitStack,contextmanager from pathlib import Path from tempfile import TemporaryDirectory import json,os,select,signal,subprocess,time def run(*args,check=True,input=None,timeout=8): result=subprocess.run(args,input=input,capture_output=True,text=True,timeout=timeout) if check:assert result.returncode==0,(args,result.returncode,result.stdout,result.stderr) return result def ns(name,*args,**kwargs):return run('ip','netns','exec',name,*args,**kwargs) def ip(name,*args,**kwargs):return ns(name,'ip',*args,**kwargs) def stop(process): if process.poll() is None: process.terminate() try:process.wait(timeout=2) except subprocess.TimeoutExpired:process.kill();process.wait(timeout=2) def line(process,timeout=3): assert select.select([process.stdout],[],[],timeout)[0],'child response deadline exceeded' value=process.stdout.readline();assert value,('child exited without a response',process.stderr.read()) return value def spawn(stack,command): process=subprocess.Popen(command,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) stack.callback(stop,process);return process @contextmanager def lab(names): with TemporaryDirectory(prefix='net-path-') as directory,ExitStack() as stack: root=Path(directory);proc=root/'proc';proc.mkdir();run('mount','-t','proc','proc',str(proc)) stack.callback(run,'umount',str(proc)) for name in names: run('ip','netns','add',name);stack.callback(run,'ip','netns','delete',name,check=False) ip(name,'link','set','lo','up') def setting(name,key,value): assert name in names path=proc/'sys'/key.replace('.','/') return ns(name,'python3','-c',f'from pathlib import Path; p=Path({str(path)!r}); p.write_text({str(value)!r}); print(p.read_text().strip())').stdout.strip() yield root,stack,setting def wire(left,left_if,right,right_if,left_addresses,right_addresses): run('ip','link','add',left_if,'type','veth','peer','name',right_if) for name,interface,addresses in [(left,left_if,left_addresses),(right,right_if,right_addresses)]: run('ip','link','set',interface,'netns',name) for address in addresses:ip(name,'address','add',address,'dev',interface,*(['nodad'] if ':' in address else [])) ip(name,'link','set',interface,'up') def nft(name,script):return ns(name,'nft','-f','-',input=script) def counters(name): data=json.loads(ns(name,'nft','-j','list','ruleset').stdout);found={} for entry in data['nftables']: rule=entry.get('rule',{}) if 'comment' in rule: for expression in rule['expr']: if 'counter' in expression:found[rule['comment']]=expression['counter']['packets'] return found def capture(stack,root,name,interface,expression,label): path=root/(label+'.pcap') process=subprocess.Popen(['ip','netns','exec',name,'tcpdump','--immediate-mode','-Z','root','-U','-n','-i',interface,'-w',str(path),expression], stdout=subprocess.DEVNULL,stderr=subprocess.PIPE,text=True) stack.callback(stop,process) deadline=time.monotonic()+3 while time.monotonic()<deadline: assert process.poll() is None,'tcpdump exited before listening' if select.select([process.stderr],[],[],.1)[0] and 'listening on' in process.stderr.readline():return process,path raise AssertionError('tcpdump did not become ready') def packets(process,path): import struct,socket process.send_signal(signal.SIGINT);_,diagnostic=process.communicate(timeout=3) assert process.returncode==0,diagnostic raw=path.read_bytes();assert raw[:4]==bytes.fromhex('d4c3b2a1') and struct.unpack_from('<I',raw,20)[0]==1 offset=24;result=[] while offset<len(raw): seconds,micros,size,original=struct.unpack_from('<IIII',raw,offset);offset+=16 frame=raw[offset:offset+size];offset+=size if frame[12:14]!=b'\x08\x00' or frame[23]!=6:continue ihl=(frame[14]&15)*4;length=struct.unpack_from('!H',frame,16)[0];tcp=14+ihl sport,dport,seq,ack=struct.unpack_from('!HHII',frame,tcp);header=(frame[tcp+12]>>4)*4 opts={};cursor=tcp+20 while cursor<tcp+header: kind=frame[cursor] if kind==0:break if kind==1:cursor+=1;continue option_length=frame[cursor+1];data=frame[cursor+2:cursor+option_length];assert option_length>=2 if kind==2:opts['mss']=struct.unpack('!H',data)[0] elif kind==3:opts['wscale']=data[0] elif kind==4:opts['sack_ok']=True elif kind==5:opts['sack']=[list(pair) for pair in struct.iter_unpack('!II',data)] cursor+=option_length body=frame[tcp+header:14+length] result.append({'t':seconds+micros/1e6,'src':socket.inet_ntoa(frame[26:30]),'dst':socket.inet_ntoa(frame[30:34]), 'sport':sport,'dport':dport,'seq':seq,'ack':ack,'flags':frame[tcp+13],'window':struct.unpack_from('!H',frame,tcp+14)[0], 'length':len(body),'options':opts,'preview':body[:24].hex()}) return result with lab(['client','target']) as (root,stack,setting): wire('client','c0','target','t0',['192.0.2.1/30'],['192.0.2.2/30']) server_code=r''' import socket,json def read_exact(stream,length): data=b'' while len(data)<length: chunk=stream.recv(length-len(data));assert chunk,'premature EOF';data+=chunk return data listener=socket.socket();listener.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) listener.setsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF,262144) listener.bind(('192.0.2.2',18080));listener.listen();print('listening',flush=True) with listener.accept()[0] as connection: connection.settimeout(4);print(json.dumps({'rcvbuf':connection.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)}),flush=True) assert read_exact(connection,4)==b'ping';connection.sendall(b'pong') assert connection.recv(1)==b'' ''' client_code=r''' import socket,json,sys def read_exact(stream,length): data=b'' while len(data)<length: chunk=stream.recv(length-len(data));assert chunk,'premature EOF';data+=chunk return data s=socket.socket();s.settimeout(4);s.setsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF,65536) s.bind(('192.0.2.1',40400+int(sys.argv[1])));s.connect(('192.0.2.2',18080));s.sendall(b'ping') assert read_exact(s,4)==b'pong' print(json.dumps({'rcvbuf':s.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF),'reply':'pong'}),flush=True) assert sys.stdin.readline().strip()=='close';s.close() ''' outcomes={} for enabled in [1,0]: setting('client','net.ipv4.tcp_window_scaling',enabled);setting('target','net.ipv4.tcp_window_scaling',enabled) service=spawn(stack,['ip','netns','exec','target','python3','-u','-c',server_code]);assert line(service).strip()=='listening' tap,path=capture(stack,root,'client','c0','tcp port 18080','window'+str(enabled)) client=spawn(stack,['ip','netns','exec','client','python3','-u','-c',client_code,str(enabled)]) client_result=json.loads(line(client));server_result=json.loads(line(service)) diagnostics=ns('client','ss','-tin','state','established').stdout client.stdin.write('close\n');client.stdin.flush();client.wait(timeout=2);service.wait(timeout=2) assert client.returncode==service.returncode==0 observed=packets(tap,path);syn=next(p for p in observed if p['flags']&0x12==0x02) synack=next(p for p in observed if p['flags']&0x12==0x12) advertised=next(p for p in observed if p['sport']==18080 and p['length']==4) if enabled: assert 'wscale' in syn['options'] and 'wscale' in synack['options'] effective=advertised['window']<<synack['options']['wscale'];assert effective>65535 else: assert 'wscale' not in syn['options'] and 'wscale' not in synack['options'];effective=advertised['window'];assert effective<=65535 outcomes[enabled]={'client':client_result,'server':server_result,'syn':syn,'synack':synack, 'server_post_handshake_window_field':advertised['window'],'effective_receive_window':effective,'ss':diagnostics} print(json.dumps(outcomes)) """ name='xbcatm-tcpflow-'+uuid.uuid4().hex[:10] def docker(*args,**kwargs):return subprocess.run(['docker',*args],capture_output=True,text=True,timeout=300,**kwargs) try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory);(root/'Dockerfile').write_text(recipe) build=docker('build','-t',name,str(root));assert build.returncode==0,build.stderr result=docker('run','--rm','-i','--name',name,'--network','none','--cap-add','NET_ADMIN','--cap-add','SYS_ADMIN', '--memory','256m','--pids-limit','128',name,'python3','-',input=program) assert result.returncode==0,result.stderr print(json.dumps(json.loads(result.stdout),sort_keys=True)) finally: docker('rm','-f',name);docker('image','rm',name) 连接建立之前,客户端请求65536字节接收缓冲,服务器监听套接字请求262144字节。脚本读取最终套接字选项值,不假定请求值会原样使用。Linux 的缓冲记账和上限会影响结果,本次客户端报告131072,服务器接受后的套接字报告425984。二者均不能直接等同于线上通告窗口。 ...

十二月 2, 2027 · 3 分钟 · XB Field Notes
文章受控样例中,同一键的十二个重叠调用,不合并时执行十二次加载,一个共享 Group 执行一次,两个独立 Group 执行两次。这轮结束后,下一轮仍会重新加载。

缓存集中失效会放大回源工作

缓存雪崩或击穿所造成的一个关键风险,是并发未命中对同一依赖发起大量重复工作。同时到期只是触发条件之一:容量淘汰、刚部署的空缓存、缓存服务不可用,都可能造成类似突发。即使只有一把热门键,也足以压垮后端。判断风险时应数清楚:一份结果尚未计算完的这段时间内,到底有多少次重建真正抵达数据源。 文章的重叠调用样例在明确的 Group 边界内统计工作量。一个 Group 合并一轮在途工作,两个独立 Group 各自加载,下一轮仍重新执行。完成结果的存储仍由应用负责。 在真实边界内合并工作 Go 的 singleflight Group 对同一键,在这个 Group 内最多保持一次在途函数执行;重复调用者共享结果,也共享错误。不同 Group 之间不会协调,即使它们位于同一个进程;不同服务实例通常也各有自己的 Group。因此它既不是分布式锁,也不是保存已完成结果的缓存。把 Group 放在每个请求内部重新创建,无法实现跨请求合并。 实际使用时,应让请求共享同一个 Group,并使键准确表示等价工作。不同租户、权限、币种或响应表示如果错误地使用同一个键,可能拿到不属于自己的结果。对于缓存回填,在共享函数内部应再次检查缓存,确实仍缺失才访问数据源,并在函数返回前完成成功结果的缓存写入。这些存储操作都不是 singleflight 自动提供的能力。 错开时间不能改变新鲜度承诺 随机抖动可以打散不同条目或独立副本的到期时间,却不能阻止访问同一过期热键的人一起未命中。如果最大 TTL 是业务规定的新鲜度上限,在有界范围内随机缩短寿命,不会偷偷越过这个上限;任意增加正向抖动则改变了原有承诺。不能脱离请求量、计算成本和允许陈旧程度,宣称几个百分点或几秒钟就普遍足够。 ...

十一月 22, 2027 · 2 分钟 · XB Field Notes
原有抓包顺序分两行:启用 Nagle 为 a、覆盖 a 的 ACK、b;TCP_NODELAY 为 a、b、首个数据 ACK。两次试验均收到 ab 并回复 OK。

Nagle 与延迟 ACK 可能让小请求的第二部分等待

Nagle 算法减少 TCP 小报文:当前面数据尚未确认时,它会暂缓额外的小块数据,除非已经积累到足以构成完整段。延迟确认则是接收端的另一种行为,可以短暂推迟 ACK。当应用把一个请求拆成多个很小的写入,而对端必须取得后续字节才能生成响应时,两者的相互作用就会影响延迟。需要明确是哪一部分数据已经发出、哪一部分仍在等待。 原有两次试验都提交 a,刻意停顿 2 毫秒,再提交 b;服务器等到 ab 后回复 OK。启用 Nagle 时抓包顺序为 a、覆盖 a 的 ACK、b;启用 TCP_NODELAY 时为 a、b、首个数据 ACK。约 41 毫秒和 2 毫秒的段间隔来自原有受控运行,不是统一常数。 仅为原有实验示意,本轮配图未重跑。 坏掉的样子 单独一个小请求,不会自动被 Nagle 延迟。如果没有更早的未确认数据,第一个小段可以直接发送。问题场景是第一部分已在途中,第二个短小部分排在后面等待。如果接收端延迟确认,同时应用又在等完整请求,进展可能暂停,直到确认计时器到期。不能把所有“小请求慢”都归到这个机制,也不能省略前面存在未确认数据这一条件。 ...

十月 31, 2027 · 3 分钟 · XB Field Notes
示意图对照直接遍历表行,以及沿非覆盖索引相邻条目前进并回表获取负载。绘制五行仅为示意,不是实验行数或耗时结果。

小表上全表扫描可能更快

索引用存储和写入成本,换取减少读取工作的机会。表很小,或者谓词选中大部分数据时,全表扫描可能更快,但行数与选择性都不能单独决定胜负。查询需要哪些列、排序要求、缓存状态、行宽和可用访问路径,都会改变比较结果。首先明确实际查询必须返回什么,再判断哪种访问方法做了更多有用工作。 看清两条路径实际做什么 B 树范围扫描并不是每匹配一个索引条目,都重新从根节点下降。定位范围起点后,可以沿相邻条目继续前进。非覆盖索引还可能需要回表取得缺少的列;宽谓词下,这部分工作可能成为主要成本。覆盖索引能够省去这些读取,因此,即使查询命中多数行,索引也仍有可能更划算,不能把宽谓词与索引无用直接画等号。 页很重要,但不是唯一成本。PostgreSQL 默认的 seq_page_cost 与 random_page_cost 是优化器使用的相对估计,不是毫秒,也不是每返回一行就机械收取一次的费用。缓存、数据相关性、CPU 工作、位图扫描与仅索引扫描,都影响计划选择。SQLite 普通表把行放在按 rowid 组织的 B 树中,与 PostgreSQL 堆表存储不同,不能直接把一种引擎测得的性能比例套到另一种引擎。 SQLite 的 PRAGMA page_count 返回整个数据库文件的页数,包括索引和其他对象,不能当作某个指定表的独立页数。逻辑页遍历也不保证磁盘上的连续物理读取。比较时应保持统计对象范围一致,记录页大小、模式、负载宽度和缓存条件,不能只报告行数,再把其他成本差异全部归因于扫描或索引。 保留历史测量的适用范围 此前笔记记录了 SQLite 3.53、二兆字节页缓存下的结果。宽谓词的扫描与索引时间,在一千行时为 0.04 与 0.05 毫秒,在五万行时为 1.79 与 6.61 毫秒,在百万行时为 44.37 与 531.18 毫秒。同一百万行表的高选择性查询,索引为 0.61 毫秒,扫描为 17.88 毫秒。相同行数的窄、宽负载样本还分别记录了十二与一千零十页,以及 0.02 与 1.30 毫秒。 ...

九月 16, 2027 · 2 分钟 · XB Field Notes