[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-strapi-article:zh:http-client-proxy-retry-duplicate-submit-test":3,"blog-article:\u002Fblog\u002Fhttp-client-proxy-retry-duplicate-submit-test":1013,"blog-related:\u002Fblog\u002Fhttp-client-proxy-retry-duplicate-submit-test":1014,"blog-translations:\u002Fblog\u002Fhttp-client-proxy-retry-duplicate-submit-test":1015},{"available":4,"article":5,"relatedArticles":199,"translations":884},true,{"source":6,"id":7,"documentId":8,"translationKey":8,"locale":9,"slug":10,"path":11,"_path":11,"title":12,"description":13,"excerpt":13,"image":14,"publishedAt":15,"updatedAt":16,"author":17,"tags":18,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":23,"bodyHtml":198},"strapi",3896,"a6t066253yf747tqsxn0ts4x","zh","http-client-proxy-retry-duplicate-submit-test","\u002Fblog\u002Fhttp-client-proxy-retry-duplicate-submit-test","HTTP 客户端升级后，如何检查代理重试是否导致重复提交","HTTP 客户端升级后，如何检查重试是否导致重复提交？本文用请求计数区分自动重试、重定向重放和连接中断，并说明幂等验证要点。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_HTTP_cover_6e441939e4.webp","2026-11-03T03:00:50.977Z","2026-11-03T03:00:50.965Z","Global Proxies Team",[],false,0,2,"blocks",[24,30,35,39,56,60,64,68,72,90,94,106,110,114,118,132,136,140,144,148,152,163,167,174,178,184,188,194],{"type":25,"children":26},"paragraph",[27],{"text":28,"type":29},"重复提交往往不是业务代码写错，而是客户端在代理 IP 链路上自动重试了非幂等请求。要验证这一点，得让“请求被执行了几次”变成可观测的数字，不靠肉眼核对结果。下面用一个计数接口做靶子，配合注入失败来暴露重试路径。","text",{"type":31,"level":21,"children":32},"heading",[33],{"text":34,"type":29},"怎么判断请求被重复处理了？",{"type":25,"children":36},[37],{"text":38,"type":29},"业务代码调用一次后，客户端可能发送多次网络请求；这不等于业务副作用也执行多次。对照三类记录：客户端发送尝试、服务端接收次数、同一业务键对应的实际副作用次数。再检查幂等键是否一致。只看最终结果不够，因为重复写入可能互相覆盖而不显错。常见重放入口如下：",{"type":40,"format":41,"children":42},"list","unordered",[43,48,52],{"type":44,"children":45},"list-item",[46],{"text":47,"type":29},"重试机制：状态码重试 ｜ 触发条件：503、502、连接超时 ｜ 注入方式：让服务端对部分请求返回 503",{"type":44,"children":49},[50],{"text":51,"type":29},"重试机制：重定向重放 ｜ 触发条件：307、308 通常要求保留方法与请求体 ｜ 注入方式：测试网关让 A 重定向到 B，仅跳转一次；循环作为单独负例并限制跳数",{"type":44,"children":53},[54],{"text":55,"type":29},"重试机制：连接重置重试 ｜ 触发条件：客户端认为请求未发出 ｜ 注入方式：代理侧主动断开连接",{"type":31,"level":21,"children":57},[58],{"text":59,"type":29},"怎么搭建一个可观察请求次数的测试接口？",{"type":25,"children":61},[62],{"text":63,"type":29},"测试接口按业务键统计接收次数和副作用次数，并返回请求序号。日志只保留脱敏关联键、必要状态和计数，过滤认证头、Cookie、URL 凭据与业务正文。测试请求应经过被测客户端和代理路径，以便对照客户端发送、服务端接收及业务处理记录。",{"type":31,"level":21,"children":65},[66],{"text":67,"type":29},"客户端会在什么情况下自动重试？",{"type":25,"children":69},[70],{"text":71,"type":29},"客户端的重试策略由具体库、版本、状态码和请求配置决定，不能只按 HTTP 方法推断。下表列的是需要核验的风险：",{"type":40,"format":41,"children":73},[74,78,82,86],{"type":44,"children":75},[76],{"text":77,"type":29},"方法：GET \u002F HEAD ｜ 方法语义：幂等 ｜ 待核验的风险：通常可安全重试，仍需核对客户端策略与服务端实现",{"type":44,"children":79},[80],{"text":81,"type":29},"方法：PUT \u002F DELETE ｜ 方法语义：幂等 ｜ 待核验的风险：客户端可能重试，需确认服务端确实做过幂等实现",{"type":44,"children":83},[84],{"text":85,"type":29},"方法：POST ｜ 方法语义：非幂等 ｜ 待核验的风险：重试即可能重复创建，需核对客户端策略、幂等键与服务端处理",{"type":44,"children":87},[88],{"text":89,"type":29},"方法：PATCH ｜ 方法语义：非幂等 ｜ 待核验的风险：部分客户端仍会重试，需按具体版本核对",{"type":25,"children":91},[92],{"text":93,"type":29},"升级后要特别确认：新版本是否把 POST 也纳入了可重试范围。这正是重复提交最常见的来源。",{"type":25,"children":95},[96,98,104],{"text":97,"type":29},"需要继续核对投递、签名与重试行为，可参考",{"text":99,"type":29,"marks":100},"SaaS Webhook 地区测试怎么做？到达、签名与重试",[101],{"href":102,"type":103},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fsaas-webhook-delivery-retry-region-testing","link",{"text":105,"type":29},"。",{"type":31,"level":21,"children":107},[108],{"text":109,"type":29},"307 和 308 为什么可能重复发送请求？",{"type":25,"children":111},[112],{"text":113,"type":29},"客户端若自动跟随 307\u002F308，通常会尝试保留方法和请求体重新发送，但请求体是否可重放、原请求是否已被处理、服务端是否有副作用，都取决于客户端实现与应用设计。",{"type":25,"children":115},[116],{"text":117,"type":29},"测试时检查客户端是否自动跟随、请求体能否重放，以及 A、B 两端分别收到几次请求：",{"type":40,"format":41,"children":119},[120,124,128],{"type":44,"children":121},[122],{"text":123,"type":29},"是否携带同一个幂等键；",{"type":44,"children":125},[126],{"text":127,"type":29},"是否重复执行了创建逻辑；",{"type":44,"children":129},[130],{"text":131,"type":29},"是否存在重定向循环。",{"type":25,"children":133},[134],{"text":135,"type":29},"仅在隔离的测试环境中，可将 `Location` 指向计数接口，观察客户端是否再次发送请求。请求次数增加说明发生了重放；还需检查服务端是否重复执行了业务操作。若方法或请求体发生变化，应记录具体客户端行为，并对照其版本文档。",{"type":31,"level":21,"children":137},[138],{"text":139,"type":29},"代理连接中断时，写请求会不会被重新发送？",{"type":25,"children":141},[142],{"text":143,"type":29},"断开位置决定服务端是否处理过请求。可在受控环境中分别模拟发送前失败、服务端接收后失败和业务提交后丢弃响应，再对照客户端日志、接收次数与副作用记录。连接重置本身无法说明请求是否已执行。",{"type":31,"level":21,"children":145},[146],{"text":147,"type":29},"怎么模拟失败并确认修复有效？",{"type":25,"children":149},[150],{"text":151,"type":29},"在隔离环境中逐项注入故障：发送前断开、业务提交后丢弃响应、返回 503、A 到 B 的一次 307，以及受控超时。重定向循环另作负例并限制跳数。每轮只改一个条件，记录客户端发送、服务端接收和业务副作用次数。修复后重跑相同用例；只有业务记录没有重复副作用且各层计数符合预期，才算通过。",{"type":25,"children":153},[154,156,161],{"text":155,"type":29},"307\u002F308 的 HTTP 语义见 ",{"text":157,"type":29,"marks":158},"RFC 9110",[159],{"href":160,"type":103},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9110.html",{"text":162,"type":29},"；客户端是否自动跟随及请求体能否重放，应按具体版本验证。",{"type":31,"level":21,"children":164},[165],{"text":166,"type":29},"FAQ",{"type":25,"children":168},[169],{"text":170,"type":29,"marks":171},"Q：幂等键能避免所有重复写入吗？",[172],{"type":173},"bold",{"type":25,"children":175},[176],{"text":177,"type":29},"A：不能，需确认服务端确实支持幂等键，并验证键的作用范围和保留时间。",{"type":25,"children":179},[180],{"text":181,"type":29,"marks":182},"Q：读请求和写请求可以使用相同重试策略吗？",[183],{"type":173},{"type":25,"children":185},[186],{"text":187,"type":29},"A：不应默认相同；写请求需评估服务端处理状态未知时的重复执行风险。",{"type":25,"children":189},[190],{"text":191,"type":29,"marks":192},"Q：客户端没有收到响应，能否判断服务端未处理请求？",[193],{"type":173},{"type":25,"children":195},[196],{"text":197,"type":29},"A：不能。连接中断时处理状态可能未知，应通过服务端记录或业务状态核对。","",[200,389,616],{"source":6,"id":201,"documentId":202,"translationKey":202,"locale":9,"slug":203,"path":204,"_path":204,"title":205,"description":206,"excerpt":206,"image":207,"publishedAt":208,"updatedAt":209,"author":17,"tags":210,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":211,"bodyHtml":198},3897,"y568ae3jjjs4yuaunluzwsqs","ai-api-egress-region-network-policy-diagnosis","\u002Fblog\u002Fai-api-egress-region-network-policy-diagnosis","测试 AI API 地区切换时，如何判断变化来自网络还是服务策略","切换代理 IP 出口地区后结果变化，如何区分网络与服务策略？本文用配对测试对照链路耗时、出口身份和服务端响应，避免把地区差异当成故障。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_AI_API_cover_0df5f0b27f.webp","2026-11-03T03:01:09.178Z","2026-11-03T03:01:09.154Z",[],[212,216,220,224,238,242,246,250,254,258,262,266,270,274,284,288,292,302,306,310,328,332,346,356,359,365,369,375,379,385],{"type":25,"children":213},[214],{"text":215,"type":29},"换地区之后结果变了，可能是链路、出口身份或服务策略，三者要分开取证。做法是固定请求只改地区，用分阶段耗时、IP 画像和响应证据各查一层。换地区这一步通常由代理 IP 完成，所以“换了地区”和“换了 IP”要当成两个变量记录。测试限制在自有账号和测试额度内，不通过反复请求真实付费接口制造样本。",{"type":31,"level":21,"children":217},[218],{"text":219,"type":29},"地区切换后，哪些因素可能导致结果变化？",{"type":25,"children":221},[222],{"text":223,"type":29},"地区切换可能同时改变链路、出口身份和服务策略相关条件；测试时分别记录这三类证据。",{"type":40,"format":41,"children":225},[226,230,234],{"type":44,"children":227},[228],{"text":229,"type":29},"链路质量：RTT、丢包、TLS 握手、首字节时间。",{"type":44,"children":231},[232],{"text":233,"type":29},"出口身份：IP 归属、ASN、IP 类型与信誉。",{"type":44,"children":235},[236],{"text":237,"type":29},"服务策略：限流、地区可用性、账号权限、模型可用列表。",{"type":25,"children":239},[240],{"text":241,"type":29},"如果同时换地区、出口和请求条件，结果无法区分来自哪项变化，因此对照时每轮只改变一个目标变量。",{"type":31,"level":21,"children":243},[244],{"text":245,"type":29},"怎么设计只改变出口地区的对照测试？",{"type":25,"children":247},[248],{"text":249,"type":29},"固定模型、prompt、客户端、超时和重试；按测试目的只改变地区或出口，并记录实际命中的版本和时间。",{"type":25,"children":251},[252],{"text":253,"type":29},"请求次数与观察时段应按接口波动和测试预算设定；尽量在相近时间重复对照，并避免用单次结果判断地区策略。",{"type":25,"children":255},[256],{"text":257,"type":29},"对照组至少三组：原地区、目标地区、以及目标地区的第二个出口 IP。第二组用来区分“地区本身”和“这个 IP”。",{"type":31,"level":21,"children":259},[260],{"text":261,"type":29},"怎么从 DNS、连接和响应耗时判断网络问题？",{"type":25,"children":263},[264],{"text":265,"type":29},"用 curl 对照时，必须显式使用与被测 API 相同的代理、绕过和凭据配置；凭据应从受限环境注入，不写进命令历史。下列字段是从请求开始累计的时间里程碑，不是独立 TCP\u002FTLS 耗时。代理 CONNECT、代理侧 DNS、连接复用和重定向都会影响解释；跳过的阶段标记为未观测。",{"type":25,"children":267},[268],{"text":269,"type":29},"```bash\ncurl -o \u002Fdev\u002Fnull -s -x \"$PROXY_URL\" -w 'namelookup_cumulative=%{time_namelookup} connect_cumulative=%{time_connect} appconnect_cumulative=%{time_appconnect} ttfb_cumulative=%{time_starttransfer}\\n' \\\nhttps:\u002F\u002Fapi.example.com\u002Fv1\u002Fmodels\n```",{"type":25,"children":271},[272],{"text":273,"type":29},"固定客户端、目标接口和时间窗口建立自身基线，再比较阶段分布；样本不足时只记录现象。`time_connect` 与 `time_appconnect` 不能直接当作到源站的独立 TCP\u002FTLS 时长。首字节慢也可能来自上游处理。",{"type":25,"children":275},[276,278,283],{"text":277,"type":29},"若要建立可比较的地区延迟基线，可参考",{"text":279,"type":29,"marks":280},"SaaS API 地区延迟基线怎么建？请求口径与分位数",[281],{"href":282,"type":103},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fsaas-api-region-latency-baseline",{"text":105,"type":29},{"type":31,"level":21,"children":285},[286],{"text":287,"type":29},"怎么确认出口 IP 的地区和类型？",{"type":25,"children":289},[290],{"text":291,"type":29},"对照多个 IP 数据库与目标服务看到的源 IP\u002F地区标签，并记录来源和查询时间。ASN、组织、城市和 IP 类型标签只能提供线索，不能单独证明出口的物理所在地或住宅\u002F机房属性；平台地区识别、购买地区与物理位置应分开记录。",{"type":25,"children":293},[294,296,301],{"text":295,"type":29},"同一地区用第二个出口复测。结果变化只说明出口或路径相关因素可能参与；固定账号、请求和时间窗口重复对照，再结合链路与服务端证据判断。来源冲突时标记为不确定。相关归因思路可参考",{"text":297,"type":29,"marks":298},"多地区测试发现问题，如何判断是网络故障还是产品缺陷？",[299],{"href":300,"type":103},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fregional-network-vs-product-defect",{"text":105,"type":29},{"type":31,"level":21,"children":303},[304],{"text":305,"type":29},"哪些响应信息能提示账号或地区策略限制？",{"type":25,"children":307},[308],{"text":309,"type":29},"结合错误体、限流头、账号\u002F模型可用范围和传输记录判断；即使链路不稳定，有明确来源的服务端错误仍可作为证据，但不应单独结束排查。下表列出可继续核验的线索：",{"type":40,"format":41,"children":311},[312,316,320,324],{"type":44,"children":313},[314],{"text":315,"type":29},"证据：429 与 Retry-After ｜ 候选方向：限流或配额 ｜ 下一步核对：查限流头、控制台用量及重复样本",{"type":44,"children":317},[318],{"text":319,"type":29},"证据：403 与地区或模型提示 ｜ 候选方向：可用范围 ｜ 下一步核对：核对服务方文档、账号和模型配置",{"type":44,"children":321},[322],{"text":323,"type":29},"证据：request ID 与限流 header ｜ 候选方向：服务端处理 ｜ 下一步核对：按服务方定义与网关日志核验",{"type":44,"children":325},[326],{"text":327,"type":29},"证据：链路指标稳定但持续失败 ｜ 候选方向：服务策略候选 ｜ 下一步核对：重复请求并核对错误体和账号范围",{"type":31,"level":21,"children":329},[330],{"text":331,"type":29},"确认原因后，下一步分别怎么处理？",{"type":40,"format":41,"children":333},[334,338,342],{"type":44,"children":335},[336],{"text":337,"type":29},"候选方向：链路异常 ｜ 对应证据：阶段耗时反复偏离自身基线 ｜ 下一步动作：换路径或时段复测，再看服务端响应",{"type":44,"children":339},[340],{"text":341,"type":29},"候选方向：出口相关 ｜ 对应证据：固定其余条件后结果随出口变化 ｜ 下一步动作：增加配对样本，核对路径与出口来源",{"type":44,"children":343},[344],{"text":345,"type":29},"候选方向：服务策略候选 ｜ 对应证据：错误体、账号\u002F模型范围和重复样本相互吻合 ｜ 下一步动作：核对配置或向服务方确认",{"type":25,"children":347},[348,350,355],{"text":349,"type":29},"证据不一致时，记录缺少的对照条件并保持未决。curl 参数和时间字段的含义可查",{"text":351,"type":29,"marks":352},"curl 手册",[353],{"href":354,"type":103},"https:\u002F\u002Fcurl.se\u002Fdocs\u002Fmanpage.html",{"text":105,"type":29},{"type":31,"level":21,"children":357},[358],{"text":166,"type":29},{"type":25,"children":360},[361],{"text":362,"type":29,"marks":363},"Q：测试时 API 返回内容不稳定，会影响地区归因吗？",[364],{"type":173},{"type":25,"children":366},[367],{"text":368,"type":29},"A：会。应固定模型、输入和采样窗口；无法固定时，将内容波动作为单独限制记录。",{"type":25,"children":370},[371],{"text":372,"type":29,"marks":373},"Q：同一地区的两个出口结果不同，能直接认定是 IP 信誉吗？",[374],{"type":173},{"type":25,"children":376},[377],{"text":378,"type":29},"A：不能，先排除网络路径、账号状态和上游时间差，再把结论标为待验证。",{"type":25,"children":380},[381],{"text":382,"type":29,"marks":383},"Q：地区策略由服务方临时调整时，旧测试结论还有效吗？",[384],{"type":173},{"type":25,"children":386},[387],{"text":388,"type":29},"A：只对当时的账号、地区和时间窗口有效，策略变化后应重新验证。",{"source":6,"id":390,"documentId":391,"translationKey":391,"locale":9,"slug":392,"path":393,"_path":393,"title":394,"description":395,"excerpt":395,"image":396,"publishedAt":397,"updatedAt":398,"author":17,"tags":399,"featured":19,"sortOrder":20,"readingTime":400,"bodyType":22,"bodyBlocks":401,"bodyHtml":198},3895,"tf37dm2ee9mh0u8xbco8p5ch","go-transport-proxy-connection-regression","\u002Fblog\u002Fgo-transport-proxy-connection-regression","Go 版本或 Transport 配置变更后，如何重新检查代理连接","Go 版本或 Transport 配置变更后，如何检查代理连接？本文核对代理配置来源，并对照连接复用、TLS、HTTP\u002F2 和分阶段耗时。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_Go_Transport_cover_4e51fe43de.webp","2026-11-03T03:00:34.422Z","2026-11-03T03:00:34.401Z",[],4,[402,406,410,414,428,432,436,440,444,448,452,456,460,464,468,472,482,486,490,504,508,512,516,530,534,538,542,546,550,572,576,583,586,592,596,602,606,612],{"type":25,"children":403},[404],{"text":405,"type":29},"Go 的代理 IP 行为由版本、环境变量和 `Transport` 配置共同决定，任何一项变化都可能改变实际走向。回归的关键是把“代理从哪来”和“请求走到哪一步”都变成可观测的。下面用标准库的 `httptrace` 与会话复用指标做示范，测试在自有服务与测试出口上进行。",{"type":31,"level":21,"children":407},[408],{"text":409,"type":29},"升级后先确认 Go 版本和哪些配置变了",{"type":25,"children":411},[412],{"text":413,"type":29},"先记录 Go 版本、构建选项和代理环境配置，作为升级前后对照基线：",{"type":40,"format":41,"children":415},[416,420,424],{"type":44,"children":417},[418],{"text":419,"type":29},"Go 版本：`go version`，大版本升级可能改变默认 TLS 与 HTTP\u002F2 行为；",{"type":44,"children":421},[422],{"text":423,"type":29},"GOFLAGS 与构建标签：是否启用或关闭了某些默认行为；",{"type":44,"children":425},[426],{"text":427,"type":29},"环境变量：`HTTP_PROXY`、`HTTPS_PROXY`、`NO_PROXY` 是否被设置，以及大小写写法。",{"type":25,"children":429},[430],{"text":431,"type":29},"`HTTP_PROXY` 与 `http_proxy` 的处理及 `NO_PROXY` 匹配规则应按 Go 版本核实。回归前记录变量是否设置、脱敏后的主机\u002F端口与绕过规则，再和升级前记录对比。不要输出含 URL userinfo、口令或完整凭据的代理环境变量。",{"type":31,"level":21,"children":433},[434],{"text":435,"type":29},"Go 请求最终使用了哪种代理配置？",{"type":25,"children":437},[438],{"text":439,"type":29},"最终用哪套配置，以当前请求实际使用的 `Transport` 和绕过规则为准。除了 `Proxy`，还要检查自定义 `DialContext`、`NO_PROXY`、重定向和连接池；环境变量是否被读取还取决于读取时机，不能只看代码或环境变量就下判断。",{"type":25,"children":441},[442],{"text":443,"type":29},"```go\ntr := &http.Transport{\nProxy: http.ProxyFromEnvironment, \u002F\u002F 读环境变量\n\u002F\u002F Proxy: http.ProxyURL(proxyURL), \u002F\u002F 显式指定\nTLSHandshakeTimeout: 5 * time.Second,\n}\nclient := &http.Client{Transport: tr, Timeout: 30 * time.Second}\nreq, _ := http.NewRequest(\"GET\", \"https:\u002F\u002Fapi.example.com\u002Fv1\u002Fmodels\", nil)\nreq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"API_KEY\"))\nresp, err := client.Do(req)\n```",{"type":25,"children":445},[446],{"text":447,"type":29},"若 `Transport.Proxy` 显式设置了代理函数或地址，请求将按该配置处理，不会再由另一套环境变量配置替代。使用标准库 `ProxyFromEnvironment` 时，代理环境配置存在进程级缓存行为；按目标 Go 版本核对实现。修改环境变量后，应在设置好环境的新进程中测试，只新建 Transport 不保证刷新缓存。需要运行时动态选择代理时，用明确管理的 `Proxy` 函数或 `ProxyURL`，并单独管理连接池。",{"type":25,"children":449},[450],{"text":451,"type":29},"在隔离测试环境中，可将代理地址指向不可用端口作为负向测试。请求仍成功只说明该请求没有依赖这个代理配置；还应排查 `NO_PROXY`、其他代理设置、连接复用等因素，并核对最终出口。",{"type":25,"children":453},[454],{"text":455,"type":29},"`NO_PROXY` 命中时请求会绕过代理；应把匹配结果与实际出口一起核对。",{"type":31,"level":21,"children":457},[458],{"text":459,"type":29},"怎么用 `httptrace` 定位连接耗时？",{"type":25,"children":461},[462],{"text":463,"type":29},"`httptrace` 提供的是阶段事件，不是现成的耗时报表；需要在事件回调里记时间戳、算差值，再和总耗时对照。",{"type":25,"children":465},[466],{"text":467,"type":29},"```go\nstart := time.Now()\nvar dnsStart, connStart, tlsStart time.Time\ntrace := &httptrace.ClientTrace{\nDNSStart:          func(httptrace.DNSStartInfo) { dnsStart = time.Now() },\nDNSDone:           func(httptrace.DNSDoneInfo) { log.Printf(\"dns %v\", time.Since(dnsStart)) },\nConnectStart:      func(network, addr string) { connStart = time.Now(); log.Printf(\"connect start %s %s\", network, addr) },\nConnectDone:       func(network, addr string, err error) { log.Printf(\"connect done %s %v %v\", addr, time.Since(connStart), err) },\nTLSHandshakeStart: func() { tlsStart = time.Now() },\nTLSHandshakeDone: func(cs tls.ConnectionState, err error) {\nlog.Printf(\"tls %v proto=%s\", time.Since(tlsStart), cs.NegotiatedProtocol)\n},\nGotFirstResponseByte: func() { log.Printf(\"ttfb %v\", time.Since(start)) },\n}\nreq = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))\n```",{"type":25,"children":469},[470],{"text":471,"type":29},"开始事件与完成事件要成对记录，才能计算 DNS、拨号、TLS 等阶段时长；只注册起点不会得到 duration。示例中的单一时间变量只适用于串行、单次尝试；Happy Eyeballs 等情况下可能并发或多次触发回调，应按连接尝试关联时间戳，并用互斥锁或并发安全结构保护。若连接复用，可能没有新的拨号事件，缺事件不等于零耗时。代理下 `ConnectStart` 的目标通常是代理地址；经 HTTP 代理时 DNS 也可能由代理完成。",{"type":25,"children":473},[474,476,481],{"text":475,"type":29},"移动应用场景的 DNS、连接和 TLS 分阶段定位方法，可参考",{"text":477,"type":29,"marks":478},"移动 App 网络兼容测试：五个失败阶段定位法",[479],{"href":480,"type":103},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fmobile-app-network-egress-compatibility-testing",{"text":105,"type":29},{"type":31,"level":21,"children":483},[484],{"text":485,"type":29},"怎么确认连接复用和超时设置仍然生效？",{"type":25,"children":487},[488],{"text":489,"type":29},"检查连接池配置与 `GotConn` 事件，比较同一负载下的复用结果：",{"type":40,"format":41,"children":491},[492,496,500],{"type":44,"children":493},[494],{"text":495,"type":29},"`DisableKeepAlives: true` 会让每个请求重新建连；",{"type":44,"children":497},[498],{"text":499,"type":29},"`TLSHandshakeTimeout` 过短会在跨境链路上频繁失败；",{"type":44,"children":501},[502],{"text":503,"type":29},"`MaxIdleConnsPerHost` 过低会导致高并发时连接被丢弃重建。",{"type":25,"children":505},[506],{"text":507,"type":29},"观测方式：看 `httptrace.GotConn` 回调里 `info.Reused` 的取值，统计连续请求中的复用比例。复用比例受请求模式、连接池和服务端行为影响，不设统一阈值；将升级前后相同负载下的结果对照，异常时再检查配置和默认值。",{"type":31,"level":21,"children":509},[510],{"text":511,"type":29},"升级后 HTTP\u002F2 或 TLS 协商有什么变化？",{"type":25,"children":513},[514],{"text":515,"type":29},"Go 版本升级可能改变 HTTP\u002F2 启用条件和 TLS 默认值，判断时以 Go 客户端自己的结果为准。",{"type":40,"format":41,"children":517},[518,522,526],{"type":44,"children":519},[520],{"text":521,"type":29},"自定义 `TLSClientConfig` 可能关闭 HTTP\u002F2；",{"type":44,"children":523},[524],{"text":525,"type":29},"`ForceAttemptHTTP2` 的设置会影响协商结果；",{"type":44,"children":527},[528],{"text":529,"type":29},"最低 TLS 版本提升后，旧代理或中间设备可能握手失败。",{"type":25,"children":531},[532],{"text":533,"type":29},"以 Go 的 `resp.Proto`、`resp.TLS.NegotiatedProtocol`（TLS 响应时）及客户端日志作为 Go 实际协商结果。curl 只能作显式相同代理、目标和路径下的独立对照，不能代表 Go 客户端结果：",{"type":25,"children":535},[536],{"text":537,"type":29},"```bash\ncurl -sI -o \u002Fdev\u002Fnull -w '%{http_version}\\n' https:\u002F\u002Fapi.example.com\u002F\n```",{"type":25,"children":539},[540],{"text":541,"type":29},"该命令只显示 curl 自己的协商结果；Go 的结果以 `resp.Proto` 与 TLS 状态为准。两者不一致时，先确认代理、目标和请求路径相同，再检查 Go 版本、`tls.Config` 与代理隧道能力。",{"type":31,"level":21,"children":543},[544],{"text":545,"type":29},"怎么判断代理连接回归通过？",{"type":25,"children":547},[548],{"text":549,"type":29},"可按下表对照代理来源、分阶段耗时、连接复用、协议协商及超时重试。",{"type":40,"format":41,"children":551},[552,556,560,564,568],{"type":44,"children":553},[554],{"text":555,"type":29},"检查项：代理来源 ｜ 观测点：检查 `Transport.Proxy`、绕过规则及实际出口 ｜ 正常表现：与预期代理配置和出口一致",{"type":44,"children":557},[558],{"text":559,"type":29},"检查项：分阶段耗时 ｜ 观测点：`httptrace` 成对事件算出的各段时长 ｜ 正常表现：各段与升级前同一量级",{"type":44,"children":561},[562],{"text":563,"type":29},"检查项：连接复用 ｜ 观测点：`GotConn` 的 `Reused` ｜ 正常表现：与升级前相同负载下的基线对照",{"type":44,"children":565},[566],{"text":567,"type":29},"检查项：Go 协商协议 ｜ 观测点：`resp.Proto` 与 TLS 状态 ｜ 正常表现：按目标服务、代理路径和升级前基线核对",{"type":44,"children":569},[570],{"text":571,"type":29},"检查项：超时与重试 ｜ 观测点：三类超时的触发日志 ｜ 正常表现：各按预期触发，次数不变",{"type":25,"children":573},[574],{"text":575,"type":29},"如果实际出口与配置不符，先检查 `Transport.Proxy` 和绕过规则；若某阶段耗时变化，检查对应的连接与超时配置；HTTP\u002F2 协商结果变化时，再核对 Go 版本、TLS 配置和代理支持情况。",{"type":25,"children":577},[578],{"text":579,"type":29,"marks":580},"Go net\u002Fhttp 官方文档",[581],{"href":582,"type":103},"https:\u002F\u002Fpkg.go.dev\u002Fnet\u002Fhttp",{"type":31,"level":21,"children":584},[585],{"text":166,"type":29},{"type":25,"children":587},[588],{"text":589,"type":29,"marks":590},"Q：环境变量代理设置会自动覆盖自定义 Transport 吗？",[591],{"type":173},{"type":25,"children":593},[594],{"text":595,"type":29},"A：取决于 `Transport.Proxy` 是否显式设置及 Go 版本行为，应以实际配置和版本文档为准。",{"type":25,"children":597},[598],{"text":599,"type":29,"marks":600},"Q：测试关闭连接复用会代表线上表现吗？",[601],{"type":173},{"type":25,"children":603},[604],{"text":605,"type":29},"A：不会。它适合隔离连接问题，结果应与默认复用配置分开记录。",{"type":25,"children":607},[608],{"text":609,"type":29,"marks":610},"Q：只在一个 Go 版本通过是否足够？",[611],{"type":173},{"type":25,"children":613},[614],{"text":615,"type":29},"A：若生产使用多个版本，应覆盖实际部署版本及升级目标版本。",{"source":6,"id":617,"documentId":618,"translationKey":618,"locale":9,"slug":619,"path":620,"_path":620,"title":621,"description":622,"excerpt":622,"image":623,"publishedAt":624,"updatedAt":625,"author":17,"tags":626,"featured":19,"sortOrder":20,"readingTime":627,"bodyType":22,"bodyBlocks":628,"bodyHtml":198},3894,"vkahpc5jjd98znyiua0xhimo","git-clone-slow-proxy-repository-protocol-diagnosis","\u002Fblog\u002Fgit-clone-slow-proxy-repository-protocol-diagnosis","Git Clone 速度变慢时，如何判断是代理、仓库还是协议的问题","Git Clone 变慢时，如何判断问题来自代理链路、仓库还是协议？本文用分阶段计时和控制变量的对照缩小排查范围，不靠主观感受判断。","https:\u002F\u002Fcms.quanqiudaili.com\u002Fuploads\u002Flarge_Git_Clone_cover_72566b8c95.webp","2026-11-03T03:00:15.196Z","2026-11-03T03:00:15.176Z",[],3,[629,633,637,641,659,663,667,671,675,679,693,697,715,719,723,737,741,745,749,767,771,775,785,789,793,803,807,811,815,841,851,854,860,864,870,874,880],{"type":25,"children":630},[631],{"text":632,"type":29},"Git 变慢往往不是“网速不够”，而是卡在某一阶段。正确做法是固定仓库、客户端、协议、缓存和时间，用 Git 自带的跟踪开关分段计时，再对照代理 IP 链路、仓库限速和协议版本三种原因。测试使用公开仓库与自有环境，不对目标仓库发起高频重复克隆。",{"type":31,"level":21,"children":634},[635],{"text":636,"type":29},"Clone 变慢时，先排查哪些因素？",{"type":25,"children":638},[639],{"text":640,"type":29},"先固定影响耗时的条件，后续对照才有意义：",{"type":40,"format":41,"children":642},[643,647,651,655],{"type":44,"children":644},[645],{"text":646,"type":29},"仓库：同一 URL、同一分支、同一提交历史；",{"type":44,"children":648},[649],{"text":650,"type":29},"客户端：Git 版本、配置、是否启用稀疏检出；",{"type":44,"children":652},[653],{"text":654,"type":29},"协议：HTTP(S) 还是 SSH，协议版本 v0\u002Fv1\u002Fv2；",{"type":44,"children":656},[657],{"text":658,"type":29},"缓存：本机对象库、DNS 缓存、代理缓存。",{"type":25,"children":660},[661],{"text":662,"type":29},"第一次克隆和后续操作的时间可能受本地或服务端缓存影响，不能直接对比。已有工作区执行 fetch、使用 `--reference`\u002Falternates、共享对象库或特定本地 clone 方式时，Git 才可能复用已有对象。对远端 URL 在独立空目录执行普通 clone，不会仅因其他目录有同仓库就自动借用其对象；记录命令、目标目录与 reference 配置，并区分客户端、服务端和代理缓存。",{"type":31,"level":21,"children":664},[665],{"text":666,"type":29},"怎么用 Git 日志找出耗时阶段？",{"type":25,"children":668},[669],{"text":670,"type":29},"用 Git 跟踪日志观察各阶段，并给输出加时间戳：",{"type":25,"children":672},[673],{"text":674,"type":29},"```bash\nGIT_TRACE=1 GIT_TRACE_PERFORMANCE=1 GIT_TRACE_CURL=1 GIT_TRACE_CURL_NO_DATA=1 \\\ngit clone \u003Curl> 2>&1 | while IFS= read -r line; do printf '%s %s\\n' \"$(date +%H:%M:%S)\" \"$line\"; done\n```",{"type":25,"children":676},[677],{"text":678,"type":29},"默认的 `GIT_TRACE_CURL` 可能记录收发数据及请求\u002F响应头；`GIT_TRACE_CURL_NO_DATA=1` 可避免记录传输正文，但仍可能暴露认证信息、Cookie、私有仓库 URL、查询参数和内部主机名。只在测试凭据与可分享仓库上使用；限制日志访问，分享前脱敏。发生凭据泄露时按流程轮换。该 trace 不提供 TCP 重传证据。",{"type":40,"format":41,"children":680},[681,685,689],{"type":44,"children":682},[683],{"text":684,"type":29},"`GIT_TRACE=1` 显示执行的子命令与阶段；",{"type":44,"children":686},[687],{"text":688,"type":29},"`GIT_TRACE_PERFORMANCE=1` 输出各阶段耗时；",{"type":44,"children":690},[691],{"text":692,"type":29},"`GIT_TRACE_CURL=1` 提供 Git 使用的 HTTP 层诊断信息；它不等于完整网络抓包，也不能据此测量 TCP 重传。",{"type":25,"children":694},[695],{"text":696,"type":29},"读日志时依次看建连\u002FTLS、引用协商和 packfile 接收阶段，不要只比较总时长。",{"type":40,"format":41,"children":698},[699,703,707,711],{"type":44,"children":700},[701],{"text":702,"type":29},"阶段：建连与 TLS ｜ 在输出里怎么认：出现 `Connected to`、TLS 握手相关行 ｜ 变慢时先查什么：代理网络路径、出口质量、DNS",{"type":44,"children":704},[705],{"text":706,"type":29},"阶段：引用协商 ｜ 在输出里怎么认：`advertise refs`、`ls-remote` 相关行 ｜ 变慢时先查什么：引用数量、协议版本、服务端处理",{"type":44,"children":708},[709],{"text":710,"type":29},"阶段：接收对象 ｜ 在输出里怎么认：`Receiving objects` 进度 ｜ 变慢时先查什么：packfile 接收阶段的字节数、速率与耗时",{"type":44,"children":712},[713],{"text":714,"type":29},"阶段：解包与检出 ｜ 在输出里怎么认：`Resolving deltas`、`Checking out files` ｜ 变慢时先查什么：本机 CPU、磁盘或文件数量；与网络接收分开计时",{"type":31,"level":21,"children":716},[717],{"text":718,"type":29},"怎么判断时间花在对象协商还是文件传输？",{"type":25,"children":720},[721],{"text":722,"type":29},"协商耗时受引用、协议和服务端处理影响；先看阶段数据，再判断是否与传输带宽有关。",{"type":40,"format":41,"children":724},[725,729,733],{"type":44,"children":726},[727],{"text":728,"type":29},"引用数量极多：`ls-remote` 与 advertise refs 阶段变慢；",{"type":44,"children":730},[731],{"text":732,"type":29},"历史极深：需要传输的 packfile 更大；",{"type":44,"children":734},[735],{"text":736,"type":29},"大文件多：单个对象大，分块与重传影响明显。",{"type":25,"children":738},[739],{"text":740,"type":29},"如果建连快而协商耗时长，先结合实际协议、过滤能力、协商请求与服务端处理复测；若接收阶段速率稳定但慢，再比较链路、平台限速和字节量。`git ls-remote \u003Curl> | wc -l` 只能粗略估计引用规模，不能用“上万行”作为通用阈值，也不能据此断言升带宽无效。每次只改一个变量，记录协商耗时、响应字节和客户端\u002F服务端情况。",{"type":31,"level":21,"children":742},[743],{"text":744,"type":29},"怎么区分代理链路、仓库限速和协议影响？",{"type":25,"children":746},[747],{"text":748,"type":29},"上一节用于判断时间落在协商还是对象传输；下表把观测现象对应到下一项对照。",{"type":40,"format":41,"children":750},[751,755,759,763],{"type":44,"children":752},[753],{"text":754,"type":29},"现象（只是线索）：建连或 TLS 阶段就慢 ｜ 待验证的假设：代理网络路径 ｜ 验证方式：换出口复测",{"type":44,"children":756},[757],{"text":758,"type":29},"现象（只是线索）：传输阶段速度稳定在某个上限 ｜ 待验证的假设：仓库或平台限速 ｜ 验证方式：同仓库不同时段、不同出口对比",{"type":44,"children":760},[761],{"text":762,"type":29},"现象（只是线索）：协商阶段请求次数异常多 ｜ 待验证的假设：协议版本或配置 ｜ 验证方式：切换 v1\u002Fv2、加 `-c protocol.version=2`",{"type":44,"children":764},[765],{"text":766,"type":29},"现象（只是线索）：起初快、中途频繁停顿 ｜ 待验证的假设：链路抖动、服务端等待或本地处理 ｜ 验证方式：对照 Git 阶段耗时、接收速率及本机 CPU\u002F磁盘；重传需独立网络证据",{"type":25,"children":768},[769],{"text":770,"type":29},"这些现象只是线索，不是结论。同一仓库、同一提交、同一客户端、同一缓存状态下多轮测试，记录阶段耗时、传输字节量、速率分布和响应头，再决定往哪个方向归因。",{"type":25,"children":772},[773],{"text":774,"type":29},"传输速率长期稳定在某一水平时，可检查仓库侧整形、带宽上限或并发限制；速率波动时则同时核对链路、服务端等待与本地解包。单次曲线不能区分这些原因，应在同一仓库和客户端条件下跨时段或出口重复测试，并记录对应阶段数据。不同出口的耗时差异只能说明结果与出口\u002F路径相关，仍需分段证据定位。",{"type":25,"children":776},[777,779,784],{"text":778,"type":29},"若表现为地区依赖或链路不可用，可参考",{"text":780,"type":29,"marks":781},"某地区依赖服务不可用时，如何验证 SaaS 产品的降级与恢复？",[782],{"href":783,"type":103},"https:\u002F\u002Fwww.quanqiudaili.com\u002Fblog\u002Fsaas-regional-fallback-testing",{"text":105,"type":29},{"type":31,"level":21,"children":786},[787],{"text":788,"type":29},"怎么用 HTTPS、SSH 和浅克隆做对照？",{"type":25,"children":790},[791],{"text":792,"type":29},"协议、深度和过滤是三种独立对照，每次只改变其中一项：",{"type":40,"format":41,"children":794},[795,799],{"type":44,"children":796},[797],{"text":798,"type":29},"协议对照：同一仓库分别用 HTTPS 与 SSH 克隆。如果 HTTPS 走代理而 SSH 直连，两者耗时差异可能来自代理路径，也可能来自协议、认证或服务端处理差异，需结合分阶段记录判断。",{"type":44,"children":800},[801],{"text":802,"type":29},"深度对照：加 `--depth 1`。浅克隆明显变快，说明减少传输对象可能改善耗时；这不能单独排除链路因素。",{"type":25,"children":804},[805],{"text":806,"type":29},"再加一个 `--filter=blob:none` 的部分克隆对照，可进一步区分对象数量与对象体积的影响。三种对照按“协议 → 深度 → 过滤”的顺序做，每做一次只改一个变量。",{"type":31,"level":21,"children":808},[809],{"text":810,"type":29},"根据对照结果，下一步先检查什么？",{"type":25,"children":812},[813],{"text":814,"type":29},"结合前面各项对照结果，选择下一步核查：",{"type":40,"format":41,"children":816},[817,821,825,829,833,837],{"type":44,"children":818},[819],{"text":820,"type":29},"观察结果：建连或 TLS 慢 ｜ 优先排查项：代理网络路径、出口质量、DNS ｜ 下一条对照：同仓库换出口、换时段各跑一轮",{"type":44,"children":822},[823],{"text":824,"type":29},"观察结果：协商慢 ｜ 优先排查项：仓库规模、协议版本或服务端处理 ｜ 下一条对照：对照协议、协商耗时、响应字节与服务端资料",{"type":44,"children":826},[827],{"text":828,"type":29},"观察结果：packfile 慢、速率平稳 ｜ 优先排查项：平台限速或带宽上限 ｜ 下一条对照：同仓库不同时段、不同出口对比速率曲线",{"type":44,"children":830},[831],{"text":832,"type":29},"观察结果：起初快、中途停顿 ｜ 优先排查项：链路抖动、服务端等待或本地解包 ｜ 下一条对照：对照传输速率、Git 阶段与本机资源；重传需独立网络证据",{"type":44,"children":834},[835],{"text":836,"type":29},"观察结果：加 `--depth 1` 明显变快 ｜ 优先排查项：传输对象量 ｜ 下一条对照：再加 `--filter=blob:none` 对照",{"type":44,"children":838},[839],{"text":840,"type":29},"观察结果：三种对照都无差异 ｜ 优先排查项：本地磁盘、杀毒扫描、Git 并发 ｜ 下一条对照：结合本机资源监控确认",{"type":25,"children":842},[843,845,850],{"text":844,"type":29},"浅克隆后耗时下降，先确认传输对象量、仓库状态和本地缓存条件一致，再判断减少的数据量是否解释了差异。Git 命令和配置细节可查",{"text":846,"type":29,"marks":847},"Git 官方文档",[848],{"href":849,"type":103},"https:\u002F\u002Fgit-scm.com\u002Fdocs\u002Fgit",{"text":105,"type":29},{"type":31,"level":21,"children":852},[853],{"text":166,"type":29},{"type":25,"children":855},[856],{"text":857,"type":29,"marks":858},"Q：浅克隆更快，能证明代理链路有问题吗？",[859],{"type":173},{"type":25,"children":861},[862],{"text":863,"type":29},"A：不能，它改变了传输数据量；只能说明负载规模影响结果，需结合分阶段计时判断。",{"type":25,"children":865},[866],{"text":867,"type":29,"marks":868},"Q：SSH 和 HTTPS 速度不同，哪种结果可信？",[869],{"type":173},{"type":25,"children":871},[872],{"text":873,"type":29},"A：两者路径和认证机制不同，应分别记录，不把单次差异视为代理性能结论。",{"type":25,"children":875},[876],{"text":877,"type":29,"marks":878},"Q：公开仓库和私有仓库能放在同一组对照里吗？",[879],{"type":173},{"type":25,"children":881},[882],{"text":883,"type":29},"A：不建议，权限、服务端策略和仓库规模都会改变传输条件。",[885],{"source":6,"id":7,"documentId":8,"translationKey":8,"locale":9,"slug":10,"path":11,"_path":11,"title":12,"description":13,"excerpt":13,"image":14,"publishedAt":15,"updatedAt":16,"author":17,"tags":886,"featured":19,"sortOrder":20,"readingTime":21,"bodyType":22,"bodyBlocks":887,"bodyHtml":198},[],[888,891,894,897,908,911,914,917,920,934,937,944,947,950,953,964,967,970,973,976,979,986,989,994,997,1002,1005,1010],{"type":25,"children":889},[890],{"text":28,"type":29},{"type":31,"level":21,"children":892},[893],{"text":34,"type":29},{"type":25,"children":895},[896],{"text":38,"type":29},{"type":40,"format":41,"children":898},[899,902,905],{"type":44,"children":900},[901],{"text":47,"type":29},{"type":44,"children":903},[904],{"text":51,"type":29},{"type":44,"children":906},[907],{"text":55,"type":29},{"type":31,"level":21,"children":909},[910],{"text":59,"type":29},{"type":25,"children":912},[913],{"text":63,"type":29},{"type":31,"level":21,"children":915},[916],{"text":67,"type":29},{"type":25,"children":918},[919],{"text":71,"type":29},{"type":40,"format":41,"children":921},[922,925,928,931],{"type":44,"children":923},[924],{"text":77,"type":29},{"type":44,"children":926},[927],{"text":81,"type":29},{"type":44,"children":929},[930],{"text":85,"type":29},{"type":44,"children":932},[933],{"text":89,"type":29},{"type":25,"children":935},[936],{"text":93,"type":29},{"type":25,"children":938},[939,940,943],{"text":97,"type":29},{"text":99,"type":29,"marks":941},[942],{"href":102,"type":103},{"text":105,"type":29},{"type":31,"level":21,"children":945},[946],{"text":109,"type":29},{"type":25,"children":948},[949],{"text":113,"type":29},{"type":25,"children":951},[952],{"text":117,"type":29},{"type":40,"format":41,"children":954},[955,958,961],{"type":44,"children":956},[957],{"text":123,"type":29},{"type":44,"children":959},[960],{"text":127,"type":29},{"type":44,"children":962},[963],{"text":131,"type":29},{"type":25,"children":965},[966],{"text":135,"type":29},{"type":31,"level":21,"children":968},[969],{"text":139,"type":29},{"type":25,"children":971},[972],{"text":143,"type":29},{"type":31,"level":21,"children":974},[975],{"text":147,"type":29},{"type":25,"children":977},[978],{"text":151,"type":29},{"type":25,"children":980},[981,982,985],{"text":155,"type":29},{"text":157,"type":29,"marks":983},[984],{"href":160,"type":103},{"text":162,"type":29},{"type":31,"level":21,"children":987},[988],{"text":166,"type":29},{"type":25,"children":990},[991],{"text":170,"type":29,"marks":992},[993],{"type":173},{"type":25,"children":995},[996],{"text":177,"type":29},{"type":25,"children":998},[999],{"text":181,"type":29,"marks":1000},[1001],{"type":173},{"type":25,"children":1003},[1004],{"text":187,"type":29},{"type":25,"children":1006},[1007],{"text":191,"type":29,"marks":1008},[1009],{"type":173},{"type":25,"children":1011},[1012],{"text":197,"type":29},null,[],[]]