背景:入库前审查链路一直整条超时(日志每 ~20s 一条 WARN),异步那层 (含刚装好的 T1)从未成功跑过。 - 新增 endpoint.go:把「连接语义上不可靠」的回环别名规范化成 127.0.0.1 (localhost 大小写不敏感 / 0.0.0.0 / 空主机),只替换 Host,不碰 path / query / fragment / userinfo;显式 [::1] 与真实主机名逐字不动 - Embed 加独立短超时 DefaultRequestTimeout = 5s(远小于审查 20s 预算): 卡住的调用快速失败并走既有「降级到词项余弦」的路径,把预算留给 T1 - .env.example 示例地址 localhost -> 127.0.0.1(未注释行键数 69 -> 69,无增删) - 跨包测试钉住 5s 严格小于审查总预算 诚实边界(子代理实测,推翻了主代理原先的 IPv6 根因推断): - Go 客户端走 localhost 实测约 57ms、curl 约 0.2s ⇒「localhost -> ::1 等满 20s 预算」这条链不成立;更可能是单槽 llama.cpp 的吞吐/争用 (一批 64 条约 2.1~2.9s,审查 201 条约 8~10s,再叠 T1 8s 顶满预算) - 规范化在任何环境下都无害,5s 快速失败对这个真因对症;但真因 (容量/争用)需要另排切片,已记入工作小结
76 lines
3.2 KiB
Go
76 lines
3.2 KiB
Go
package embedding
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// 本文件只管**端点地址的规范化**:把「连接语义上不可靠」的回环主机名别名改写
|
|
// 成明确的 IPv4 回环字面量。它只改主机,不碰路径、查询串、fragment 与用户信息,
|
|
// 也不做任何名字解析或在线可达性探测。
|
|
//
|
|
// 位置口径:规范化发生在 URL 解析之后、真正拨号之前(见 APIEmbedder.Embed),
|
|
// 因此它绝不改变 HTTP 报文里除 Host 之外的任何字节。
|
|
|
|
// loopbackIPv4 是唯一的 IPv4 回环字面量。选它而不是 0.0.0.0 的理由:
|
|
// 0.0.0.0 的语义是「监听所有接口」,**不是**一个可以连接的地址(实测拿它当连接
|
|
// 目标会立即失败);而 127.0.0.1 在 Windows / Linux / macOS / Termux 上行为一致,
|
|
// 且完全不需要名字解析。
|
|
const loopbackIPv4 = "127.0.0.1"
|
|
|
|
// normalizeEndpointURL 返回规范化后的端点 URL。
|
|
//
|
|
// 规范化范围只有三类,且都是**防呆**(把写坏的地址掰回可连接的形态),
|
|
// 而不是「支持拿这些地址去连」:
|
|
// - "localhost":名字解析可能先命中 IPv6 ::1,而本机 embedding 服务通常只绑
|
|
// IPv4 回环(llama.cpp / ollama 默认如此)⇒ 那是一次注定无效的拨号。代价视环境
|
|
// 而定:可能是几十毫秒的无效尝试(2026-10-07 实测本机 Go 客户端约 57ms,
|
|
// curl 约 0.2s),也可能在「::1 被静默丢弃」的防火墙/路由策略下一直等到系统
|
|
// 连接超时,把一个 20s 级的调用预算整段吞掉。写死 IPv4 字面量直接绕开这个
|
|
// 不确定性;
|
|
// - "0.0.0.0":见 loopbackIPv4 的说明,它不是可连接的地址;
|
|
// - 空主机(如 "http:///v1/embeddings"):拼出来的 URL 指向不了任何服务。
|
|
//
|
|
// 不改写的:显式 IPv6 字面量("[::1]" 之类)、真实主机名 / IPv4、非 http(s)
|
|
// scheme —— 使用者怎么写就怎么来。未发生改写时**逐字返回原串**;发生改写时只替换
|
|
// 主机,path / query / fragment / userinfo 由 net/url 原样重放。
|
|
func normalizeEndpointURL(raw string) (string, error) {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return "", fmt.Errorf("embedding: parse api url: %w", err)
|
|
}
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
// 不是本客户端能拨的 scheme:原样返回,让后续的报错如实暴露配置问题,
|
|
// 而不是在这里改主机把它掩盖掉。
|
|
return raw, nil
|
|
}
|
|
host, changed := normalizeLoopbackHost(u.Hostname())
|
|
if !changed {
|
|
return raw, nil
|
|
}
|
|
if port := u.Port(); port != "" {
|
|
u.Host = net.JoinHostPort(host, port)
|
|
} else {
|
|
u.Host = host
|
|
}
|
|
return u.String(), nil
|
|
}
|
|
|
|
// normalizeLoopbackHost 判定主机名是否属于「不可靠的回环别名」,是则返回
|
|
// 127.0.0.1。判定大小写不敏感("LocalHost" 同样改写)。IPv6 字面量不在表内 ——
|
|
// 使用者显式写了 [::1] 就尊重它,不做任何猜测。
|
|
func normalizeLoopbackHost(host string) (string, bool) {
|
|
switch {
|
|
case host == "":
|
|
return loopbackIPv4, true
|
|
case host == "0.0.0.0":
|
|
return loopbackIPv4, true
|
|
case strings.EqualFold(host, "localhost"):
|
|
return loopbackIPv4, true
|
|
default:
|
|
return host, false
|
|
}
|
|
}
|