Beads `bd serve` 运维手册深度解读:从部署、认证到楔入检测与连接预算的完整运营指南
2026/9/13 1:39:50 网站建设 项目流程

Beadsbd serve运维手册深度解读:从部署、认证到楔入检测与连接预算的完整运营指南

【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads

本技术指南以 Beads 开源仓库中的 SERVE_RUNBOOK.md 为骨架,结合bd serve的源码实现(cmd/bd/serve.go、internal/httpapi/server.go、internal/httpapi/auth.go、internal/httpapi/events_watch.go)逐项展开,系统讲解如何把 Beads 的 v0 HTTP 服务面(bd serve)可靠地部署到生产环境。你将掌握:显式端口与互斥模型的正确姿势、基于 token 文件的免重启轮换认证、liveness/readiness 探针的正确接法、数据库楔入(wedge)的三种可告警信号、每个进程约 22 条连接的预算算法,以及优雅关停下的歧义 claim 恢复策略——全部以当前仓库实际编译进二进制的常量和行为为准。

bd serve向自动化客户端与编排器暴露的是 CLI 同一个工作面,只不过以 HTTP 呈现,免去了自动化程序"每次调用 fork 一个 bd 子进程"的启动开销与 stdout 文本解析成本(详见 design/bd-serve-v0.md)。本手册的每个数字(除非另行说明)都是 internal/httpapi/server.go 中的编译期常量——它们目前还不是命令行参数,但即便未来成为参数,线上的 wire 契约也不会变。

一、部署:端口、互斥与进程形态

1.1 显式端口:TCP bind 就是唯一的互斥机制

默认地址是--addr 127.0.0.1:0,即绑定一个临时端口。这适合临时和测试场景——调用方从 stdout 上读一行就能拿到已绑定地址。但要注意,临时端口不携带任何互斥

  • 两个bd serve进程指向同一个 workspace、都用:0,会各自绑定到不同的临时端口、并行运行,没有任何枚举手段,也没有任何失败发生;
  • 固定端口则相反:第二个进程会在 bind 时直接失败(操作系统自带的 address-in-use 错误),这正是设计意图,也是该命令唯一的互斥机制。
bd serve --addr 127.0.0.1:7777

无论哪种方式,并发 serve 都是数据安全的——claim 的仲裁发生在 SQL server 里,而不是 HTTP 进程里。失去固定端口真正失去的是:无法知道当前跑着几个 server,以及客户端无法找到"你本来想让它连的那一个"。这一点在 internal/httpapi/server.go 的Listen注释中明确记载:没有锁文件、没有 pid 文件、没有发现文件bd serve是运维显式调用的,固定端口上的 TCP bind 即互斥,客户端是"配置地址"而不是"发现地址"。

另外,--addr的主机部分必须是数字 IP 字面量,DNS 名称会被拒绝。对应实现见ValidateBindAddr(internal/httpapi/server.go):net.ParseIP(host)解析失败即报错host must be a numeric IP literal, not a name — use 127.0.0.1 rather than localhost,且 Unix socket 完全不支持。

1.2 标准流:stdout 一行,其余全走 stderr

stdout 在绑定时刻只输出一行,别无他物:

bd serve: listening on http://127.0.0.1:7777

这就是申请临时端口的调用方发现端口的方式——读一行就停。包括请求日志在内的一切其余输出都走 stderr,因此两者可以分别重定向。对应实现见 internal/httpapi/server.go:fmt.Fprintf(s.stdout, "bd serve: listening on http://%s\n", s.Addr()),日志则用log.New(cfg.Stderr, "bd serve: ", ...)创建(同文件 server.go)。

1.3 放在 supervisor 下运行

bd serve在前台运行,并在SIGHUP、SIGINT、SIGTERM 三者下都能优雅关停。SIGHUP 被特意纳入这一集合:关闭一个前台bd serve的终端就能把它停掉,而不是留下一个孤儿进程继续占着端口和数据库连接池。因此,一个 double-fork 并期望子进程脱离控制终端的 supervisor,必须自己把进程从终端脱离,或者把它留在 supervisor 下的前台。这对应根命令的信号上下文设计(cmd/bd/serve.goserveListen调用srv.Serve(rootCtx),见 cmd/bd/serve.go)。

二、认证:token 文件的免重启轮换

2.1 默认关闭,关闭即回环姿态

默认无认证,而"无认证"本身就是回环(loopback)姿态:不带任何 auth 标志的bd serve与它一直以来的样子逐字节相同。回环的信任模型就是回环边界本身——与它背后数据库已经依赖的边界是同一个。

bd serve --addr 127.0.0.1:7777 --auth-token-file /run/secrets/bd-tokens

开启后,除GET /healthz外的每个操作都要求Authorization: Bearer <token>GET /v0/beads/context并不豁免——它会报告仓库根、beads 目录和数据库名,属于敏感信息。/healthz 是唯一豁免,因为 kubelet 探针不携带凭据,一个对 liveness 返回 401 的端点意味着 pod 无限重启。

2.2 文件格式与轮换机制

文件每行一个 token,所有非空行均被接受。刻意没有--auth-token标志:命令行参数对本地所有用户都可通过ps读到。BEADS_SERVE_TOKEN_FILE是环境变量兜底,且仅在未传 flag 时生效——该变量在 cmd/bd/serve.go 定义为常量serveTokenFileEnv = "BEADS_SERVE_TOKEN_FILE",并在 resolveServeConfig 中读取;internal/httpapi从不读取环境变量,因此库、测试和嵌入调用方无法被某个人的 shell 环境变量重配置。

轮换就是一次文件重写,无需重启:新 token 与旧 token 并存写入,滚动客户端,再删除旧行——增删两者都在约一秒钟内生效。服务器运行期间持续重读该文件,全进程 gate 在最多每秒一次stat(2),且接受路径与不匹配路径都会触发检查——这正是撤销(revocation)能工作而不仅是轮换能工作的原因(见 internal/httpapi/auth.go 的Verify:freshness 在比较之前检查,且在每次调用时检查,不只在不匹配时检查——否则被删除的 token 永远匹配缓存集合、永不触发重载、永久有效)。

写入必须原子化(临时文件 + rename;Kubernetes secret mount 天然如此)。重读失败或读到空文件时保留最后一份有效集合并记录event=auth_reload_error——这样"先 truncate 再写"的写作者不会把所有客户端锁在门外。对应实现见 auth.go 的reload:失败或解析出零 token 时保留 last-good 集合并返回错误,stat 缓存只在成功时更新,因此下一次越过 gate 的尝试会自动重读并自行恢复。

2.3 token 的存储与比较:SHA-256 摘要 + 常量时间

token 以SHA-256 摘要形式持有和比较(常量时间),因此进程不持有任何明文凭据,堆转储也不会泄露。实现见 internal/httpapi/auth.go:accepts对每个缓存摘要做subtle.ConstantTimeCompare无提前退出——耗时不透露哪个 token 最接近。启动时文件就会被打散成摘要集合(NewTokenFileAuth是 eager 且严格的:不可读、超大、无 token 的文件在启动时即报错,而不是让服务器照样绑定)。

文件大小上限为 1 MiB(maxTokenFileBytes = 1 << 20,auth.go),超大文件被判定为"路径指错了,而不是 token 文件"。

2.4 401 的语义与可观测性

拒绝是401+code: unauthenticated+WWW-Authenticate: Bearerdetail是固定字符串,从不回显所出示的内容;缺失 header、错误 scheme、未知 token 三种情况刻意统一成一个 code。每种情况分别记录event=auth_refused且带reason=missingreason=malformedreason=unknown_token——这正是运维区分"配置错误的客户端"与"过期 token"的依据。

认证检查运行在数据库信号量之前(internal/httpapi/server.go 的route中,凭据检查先于 semaphore 获取),因此一场拒绝风暴每次只花费一次 SHA-256,永远不可能占据已认证客户端在等待的槽位。

2.5 token 不是身份

token 是授予整个表面的共享秘密。它不是身份、不携带任何 scope,因此永远不会让actor变成已认证主体——actor始终是审计线索的调用方自述来源(caller-asserted provenance),任何被 token 准入的客户端都可以冒充任意名字。这一点在 CLI 上同理:任何本地进程都能传任意--actor。claim 的 compare-and-set 因此是并发正确性的栅栏,而不是授权边界:它保证两个竞争 claimer 不能同时获胜,但不保证获胜者究竟是谁。

部署后应从启动行确认姿态,而不是从"没看到 flag"推断:event=startup携带auth=noneauth=bearer (<path>)(server.go 的logStartup)。

三、绑定到回环之外:allow-non-loopback 与 Host 白名单

3.1 非回环绑定强制认证

--allow-non-loopback要求--auth-token-file。回环之外,可达地址本身就是全部授权——每个能触达该地址的对端都获得完整的读与 claim 权限。

bd serve --addr 0.0.0.0:7777 --auth-token-file /run/secrets/bd-tokens \ --allowed-host bd.internal.example

--insecure-no-auth是显式、可审计的"我就是要无凭据绑定非回环"方式。它只在--allow-non-loopback旁边生效(回环上没什么可豁免的),且与--auth-token-file互斥。仅应在"你已信任的网络边界在做这件事"时使用。

两种非回环绑定都会在启动时向 stderr 打印警告,且是两种不同句式——一个点名缺失的凭据,一个点名缺失的 TLS:

bd serve: WARNING: --insecure-no-auth binds 0.0.0.0:7777 beyond loopback with no authentication. Any peer that can reach it can read every issue and claim work as any actor. bd serve: WARNING: 0.0.0.0:7777 is bound beyond loopback with bearer authentication but NO TLS. Tokens and issue data travel in plaintext; deploy it inside a trusted network boundary.

实现见 cmd/bd/serve.go 的warnServePosture:仅在AllowNonLoopback时打印,回环默认什么都不打印。而姿态校验ValidateAuthPosture(internal/httpapi/auth.go)在 CLI(resolveServeConfig)与库层(Listen各执行一次,确保第二个调用方无法组装出一个"把整个表面无凭据地暴露给网络"的 Config。

3.2 仍然没有 TLS

即便有 token,凭据和每个 issue 正文都明文传输,所以回环之外的部署必须自己提供机密性——service mesh 或可信网络边界。前置一个带认证的反向代理仍是合理形态;token 文件的作用是:代理被绕过时,源端(origin)不至于裸奔。

3.3 Host 白名单:服务化部署最容易踩的坑

Host allowlist 是服务化部署最先绊倒的地方。DNS-rebinding 检查只应答回环拼写和绑定地址本身,因此 dialing 一个服务 DNS 名的客户端会在每一个请求上得到 400。用--allowed-host枚举它 dial 的名字(可重复,精确匹配,无通配符)。event=startup行会打印生效的 allowlist。

实现细节见 internal/httpapi/server.go 的newHostPolicy:回环拼写(127.0.0.1、::1、localhost)永远被允许,绑定地址自身也被加入(包括 127.0.0.2 这类替代回环绑定);通配符绑定(0.0.0.0、::)没有单一配置地址,因此允许任意数字 IP 字面量,但仍然拒绝外来 DNS 名——被 rebind 的页面无法产生 IP 字面量的Host(浏览器发送的是攻击者 URL 里的主机名)。匹配基于解析后的地址(net.IP.Equal),所以一个允许地址的每种拼写都被允许。

3.4 行为差异:limit=0 被拒

--allow-non-loopback还带来一个行为差异:limit=0(无限)在两个 list 操作上被 400invalid_argument/reason: "invalid_value"拒绝,无论是否配置了 token。无限读取会把整个活动集合及其 JSON 编码缓冲进一个共享进程,绝不能允许任意网络对端触达。客户端必须分页。

四、探针:liveness 与 readiness 的正确接法

探针路由"绿"的含义
LivenessGET /healthz进程在运行。
ReadinessGET /v0/beads/ready?limit=1进程在运行数据库应答了。

/healthz从进程回答,从不触碰数据库。数据库不可达、卡死或 idle 停止时它依然保持绿色——这正是它作为正确 liveness 探针、无用 readiness 探针的原因。不要把它接到负载均衡器的 readiness 检查上。

GET /v0/beads/ready?limit=1是带一行上界的真实查询。200 表示就绪;503 表示"活着但未就绪",其code说明是哪一种:db_unavailable(连接失败)或busy(争用或槽位饱和)。两者都携带Retry-After;探针应把任一情况都视为"未就绪、重试",而不是硬失败。

建议的探针设置:readiness 探针最坏情况继承服务器的 60s 整请求截止时间,因此给它远低于此的超时(2–5 秒),让探针自身的失败阈值来做平滑。

在带--auth-token-file启动的服务器上,readiness 探针需要 token,liveness 不需要。/healthz是唯一 auth 豁免路由;GET /v0/beads/ready不是——未认证的 readiness 探针得到401(永远不会是 503),只看状态类的负载均衡器会把健康服务器记为永久未就绪。给 readiness 探针加Authorization: Bearerheader,并对event=auth_refusedop=listReadyWork告警——这正是"你忘了"的信号。

五、检测楔入(wedge):进程健康而数据库不回答

需要预案的失败模式是:进程保持健康,而数据库停止应答/healthz看不见它。三个信号可以:

  • event=semaphore_saturated—— 请求等待数据库槽位达到 1 秒以上。这是楔入检测信号:没有流量就没有饱和事件,因此一连串饱和事件把"楔住了"和"安静的"区分开来。outcome=acquired表示最终拿到了槽位;outcome=abandoned表示客户端在排队期间挂断或请求截止时间到期——这是从"活得不够久、没来得及被抛弃"的请求看到的同一个楔入。实现见 internal/httpapi/server.go 的acquire/noteSaturationsaturationWarn = time.Secondsemaphore_saturated事件仅在等待超阈值时记录。
  • event=semaphore_timeout—— 请求等待满 10 秒(semAcquireTimeout = 10 * time.Second)后被作为 503busy抛弃(server.go)。
  • event=conn_cap_saturated—— 已接受连接达到 64 条连接上限。每次跨越该上限记录一次(edge-triggered,见 server.go 的connStateconnCapWarnedCompareAndSwap保证只记一次,连接数回落后才复位)。

对 readiness 探针告警,并用这三个事件区分"楔住的数据库"与"慢客户端"。

六、连接预算:为共享 dolt sql-server 精确配容

在把若干个bd serve进程指向一个共享的外部dolt sql-server之前,先算好容量。每个bd serve进程在稳态下占用的连接:

消费者连接数
Handler 池,最大打开数(maxInflight + 420
根命令的DoltStore,server / external-server / shared-server 模式1–2 空闲
每进程最坏情况约 22

数字背后的形态:

  • maxInflight = 16约束触碰数据库的 handler。每个工作单元(unit of work)钉住一条 SQL 连接,因此这也是负载下的稳态连接数。
  • 池尺寸为maxInflight + 4 = 20打开、16 空闲(servePoolLimits,见 server.go),因为信号量约束的是handler而非连接:被失败 ROLLBACK 毒化的连接会被替换、提交事务的每次重试都会在一条全新钉住的连接上打开新工作单元、任何豁免信号量的 handler 之后触碰数据库时完全逃逸信号量——四个备用槽位吸收这些。
  • 在 server、external-server、shared-server 模式下,根命令已经打开了一个bd serve永远不会使用DoltStore。它在进程生命周期内保持打开,对着这个即将再池化二十条的服务器持有 1–2 条空闲连接(见 cmd/bd/serve.go 的注释)。打开与关闭它是根命令的事务——但它不是免费的,必须计入算术。

因此共享服务器上的max_connections必须覆盖大约22 × (bd serve 进程数),再加上指向同一服务器的每个其他 bd 进程各自占用的份额。空闲连接 5 分钟后回收(ConnMaxIdleTime: 5 * time.Minute)、一小时后复用(ConnMaxLifetime: time.Hour),所以突发性部署会回落到最坏情况以下——但请按最坏情况配容

池限制仅在 unit-of-work provider 暴露该旋钮时应用。若不暴露,bd serve在启动时用event=pool_limits_unavailable明说,并以无界池运行,而不是悄悄假装(见 server.go)。信任上述算术之前先找这一行。

这套算术对已注册后端(registered backend)完全不适用——它从根命令打开的 store 提供,而不是从 unit-of-work provider 提供(启动行上表现为db=roles,而每个 Dolt 拓扑都是db=provider)。池属于那个后端,bd serve既不拥有也无法触及,且不会发出pool_limits_unavailable行。在那个后端配置处为它的池配容。

6.1 连接上限、流上限与 TCP keepalive

maxConns = 64约束的是已接受 TCP 连接——这是客户端侧限制,不占数据库预算。它存在是因为信号量不约束连接:Go 每个连接起一个 goroutine,一个停在满信号量上的连接仍持有 goroutine、文件描述符和缓冲区。超出的连接在内核 accept backlog里等待,而不是占着 Go 内存。

maxWatchStreams = 48约束GET /v0/beads/events:watch——唯一一个请求可持续数小时的操作——并且刻意比maxConns低 16 条连接LimitListener只在连接关闭时归还连接槽位——在 handler 返回之后,因此也在流计数器已经降下来之后——所以流上限若等于连接上限将永远不可达:本应赢得503的那次连接永远不会被接受,被指示回退的 poll 也永远不被接受。这个余量让拒绝可以送达,也让流槽位全占时 poll、mutation 和新连接的/healthz仍然可应答(理由详见 internal/httpapi/events_watch.go)。

看仪表而不是看悬崖:event=events_watch_admitted在每次连接时携带streams=N max_streams=48,所以累积在第一次拒绝之前就可见;event=events_watch_saturated才是拒绝本身。

maxConns处的连接饱和是更糟的悬崖,而且仍然可达——64 个普通客户端,或 48 条流加 16 条其他连接。它天然沉默LimitListener只是停止调用Accept,后续连接在内核 backlog 里等待,stderr 上什么都没有,/healthz也拿不到新连接。conn_cap_saturated是唯一的警告,且是边沿触发。

流没有读截止时间(SSE 客户端合法地数小时不发送任何东西,读截止时间会杀死健康流),这引出问题:对没有 FIN/RST 就消失的消费者(拔线、VM 被杀)靠什么回收?TCP keepalive 已经做了,且默认开启:Go 的 listener 在每个已接受连接上启用SO_KEEPALIVE,用自己的 15s 覆盖系统空闲和间隔,probe 次数沿用系统值。本仓库实测(2026-08-09):SO_KEEPALIVE=1TCP_KEEPIDLE=15TCP_KEEPINTVL=15TCP_KEEPCNT=9——对空闲连接约150 秒回收,而裸系统默认(7200/75/9)要约 2 小时 11 分。对端消失时仍在积极写入的流改由重传回收,基于tcp_retries2(此处为 15,约15 分钟)——因为数据未确认时 keepalive 不运行。因此被消失对端占用的流槽位最坏情况是分钟级而非小时级——流上限正是按"分钟级"设计的。

流在两次读取之间不消耗数据库预算:handler 围绕每次一秒的 poll 拿一个信号量槽位并立刻归还(events_watch.go 的readWatchBatch),所以 48 条打开的流不占据 16 个 handler 槽位。它们也因同样的原因豁免 60s 整请求截止时间,每次读取由自己独立的 15s 截止时间约束(eventsWatchReadDeadline = 15 * time.Second)——这个短值还约束了流能多久不发心跳、能多久无视关停。要累积的消费者改用 pollGET /v0/beads/events,它永不被容量拒绝。

七、关停与歧义 claim:恢复即重新 claim

SIGINT、SIGTERM 或 SIGHUP 时,服务器停止接受连接并最多排水 20 秒drainTimeout = 20 * time.Second,见 server.go 的Serve)。排水刻意不取消进行中的 handler 上下文:预算覆盖"一个 claim 在其序列化重试预算内 + 其提交",因为过早杀死这样的连接会让客户端无法判断自己的写入是否落地。

Journal 流是例外,且必须如此:一个一直打开的响应会在整个预算期间占位然后照样被杀死。排水开始时它们收到显式关闭信号,在一个 poll 间隔内自行结束——或者,若处于读取中或积压中,在一个 15s 读取内结束——因此打开的流不会把干净停止变成 20 秒的停止(s.http.RegisterOnShutdown(s.closeStreams),server.go)。它们的客户端重连并从已达的 id 续读。

有一个情况仍会强制关闭,且是被接受而非修复的:客户端停止读取的流阻塞在写操作上,而解除它的写停顿截止时间是 30 秒(writeStallTimeout = 30 * time.Second),大于 20 秒排水。这样的流会被强制关闭,shutdown_forced行是正确的——那条连接本来就已歧义。无损失:流不携带写入,其客户端从自己的最后 id 续读。

若超出排水预算,剩余连接被关闭并记录event=shutdown_forced及计数。这就是歧义情况,且对客户端可见:一个死在提交中途的 claim 可能已落地也可能未落地,客户端看到的是连接断开而不是响应。

恢复就是重新 claim,且构造上安全。claim 是 compare-and-set,同一 actor 的重新 claim 是幂等的:若第一次尝试已落地,第二次会发现 actor 已持有它、返回 200、不写任何提交;若第一次未落地,第二次正常 claim;若中间有别的 actor 获胜,第二次得到带该 actor 的assignee的类型化 409already_claimed——那是真实答案,不是需要重试的错误。

所以歧义关停的恢复是:用同一 actor 重新发出 claim 并读取结果。绝不从断开的连接推断结果,绝不退回去读 issue 猜测。

同理覆盖客户端超时、挂断、claim 中途重启。客户端挂断不算服务器故障:event=request记录code=client_closed且不发出request_error——急躁的调用方不会推高运维告警的信号(server.go 的failErrerrors.Is(err, context.Canceled)时仅改记 code,不触发 request_error)。

八、请求日志:结构化的逐请求诊断

每个请求在stderr上输出一行结构化日志,前缀bd serve:加标准 logger 的 UTC 时间戳:

bd serve: 2026/08/01 05:17:42 event=request request_id=8f3a1c07-000042 op=listReadyWork method=GET path=/v0/beads/ready status=200 code="" duration_ms=12.480 sem_wait_ms=0.031 uow_ms=11.902 conns=1 remote_addr=127.0.0.1:54321

字段按序:

字段含义
request_id关联 id,<每进程随机前缀>-<序号>。任何 problem body 的request_id成员都会回显它。
opspec 的operationId
methodpathstatus按发送与应答原样记录。
codeproblemcode,成功时为""
duration_ms整请求,毫秒三位小数。
sem_wait_ms等待数据库槽位的时间。
uow_ms持有 unit of work 的时间。
conns当前存活的已接受连接数——注意它爬向 64。
remote_addr哪个客户端;回环上端口标识本地进程。
refused仅当请求因某个具体值被拒时出现:那个违规值。

每进程随机前缀(newIDPrefix,4 字节随机 hex,server.go)保证两个服务器或同一服务器的两次运行产生的 id 在共享日志中永不碰撞。

值包含空格、"=或任意控制字符时加引号,空值渲染为""logValue,server.go)。这不是装饰:没有它,调用方提供的值——Hostheader、被拒参数、错误消息——就能伪造字段甚至整行;未加引号的 C1 控制字符如 U+009B 是 CSI 引导符,会驱动运维的终端。

同一流上的其他事件:

事件何时出现
startup绑定地址、模式、workspace、数据库、host allowlist、能力,以及auth——nonebearer (<token 文件路径>)。部署后检查auth,而不是从进程列表推断。
auth_refused一次401。携带request_idopremote_addrreasonmissingmalformedunknown_token)。来自同一对端的unknown_token突发 = 客户端还留在已轮换掉的 token 上;missing= 从未配置过 token 的客户端。
auth_reload_errortoken 文件无法重读。不是拒绝——last-good 集合仍然生效——但运维正在轮换的文件不可读,且没有别的东西会说出来。
limits本构建编译进的运行包络(operating envelope)。记录它,然后与本手册对比。
request_error伴随 ≥500,携带 body 刻意隐藏的真实错误。用request_id关联。客户端挂断时不发出。
pool_limits_unavailableunit-of-work provider 不暴露池旋钮,因此下述限制未被应用、池无界。值得告警;它改变连接预算。
panichandler panic:值、栈,以及同一个request_id
semaphore_saturatedsemaphore_timeoutconn_cap_saturated见"检测楔入"。
shutdown_startshutdown_completeshutdown_forced排水过程。
events_watch_admitted打开了一条 journal 流,携带streams=N max_streams=48。每条流一行,而非每条记录一行:这是显示触顶前累积的仪表。
events_watch_saturated因本进程已持有其上限而拒绝了一条 journal 流。携带存活数与上限。流在消费者离开时结束,所以一串此类事件意味着消费者在累积,而非服务器慢。
events_watch_failed一条打开的 journal 流以无法报告给客户端(因 200 已发出)的失败结束:读取失败(携带已达的 checkpoint),或存储的 payload 无法编码(携带其seq)。客户端自行重连;重复的读取失败点名数据库问题,重复的seq点名一条将终止每条到达它的流的不可编码行。
events_watch_unflushable因 response writer 无法 flush 而拒绝了一条 journal 流。无法通过bd serve自己的服务器触达;意味着有什么东西在包装这个 handler。

5xx body 刻意携带固定的静态 detail,因此request_id是客户端拿到"唯一包含真实错误的那一行"的唯一把手。用户报告 500 时,索取request_id并 grep——request行给出形状,request_error行给出原因。

九、启动时预期会遇到的拒绝

消息原因
bd serve requires a Dolt SQL server; this workspace uses embedded Dolt永久。嵌入式后端在独立连接上、SQL 事务之外提交,因此该服务器声称的逐请求原子性在那里会是谎言。由serveDatabaseSource拒绝,且是唯一拒绝它的地方(见 cmd/bd/serve.go 与errServeEmbeddedcmd/bd/serve.go,以及 design/bd-serve-v0.md 的 "Workspace modes" 一节)。
bd serve is unavailable under strict readonly--readonly,或 config 中的readonly。该命令绑定的每个服务器都发布 issue-claim 操作,而广告的能力集是构建的属性、不是启动它的进程上标志的属性——所以替代方案要么是一个广告着它永远失败的 claim 的服务器,要么是一个悄悄什么都没买到的--readonly。去掉该标志才能 serve(errServeReadonly,cmd/bd/serve.go)。
--addr "localhost:7777": host must be a numeric IP literal, not a name — use 127.0.0.1 rather than localhost--addr给了 DNS 名称。
--addr "0.0.0.0:7777" binds beyond loopback, which requires --allow-non-loopback (and, with it, --auth-token-file)非回环--addr没有该标志。
--allow-non-loopback requires --auth-token-file (or the explicit --insecure-no-auth): every peer that can reach the address gets full read and claim access无凭据且无豁免地绑定回环之外。
--insecure-no-auth applies only to a bind beyond loopback; on loopback there is nothing to waive, so pass --allow-non-loopback or drop the flag豁免出现在它所豁免的绑定之前。
--insecure-no-auth contradicts --auth-token-file; pass one or the other同一个 auth 决定的两种写法同时出现。
--auth-token-file: token file <path>: open <path>: no such file or directorytoken 文件不可读。还有is a directorycontains no tokensis larger than 1048576 bytes; that is a mis-pointed path, not a token file。四种都是启动时拒绝,绝不会出现"照样绑定"的服务器。
address already in use固定端口互斥按预期工作:已有另一个服务器在该端口上。

上表中五个 auth/bind 拒绝由resolveServeConfig在 serve 打开数据库源或 listener之前抛出,与 workspace 无关,因此在每个 workspace 模式下读起来一样。(根命令的PersistentPreRunE仍然先解析 workspace,所以没有 beads 数据库的目录会在上述任何一条之前回答no beads database found。)此外httpapi.Listen重查同样的规则(internal/httpapi/server.go),所以包的第二个调用方无法组装一个把整个表面无凭据暴露给网络的 Config。

十、结语:把 runbook 的每个数字变成可验证的检查项

bd serve的运维面被刻意设计成"数字即常量、行为即代码":运行包络全部集中在 internal/httpapi/server.go 的常量块,认证规则集中在 internal/httpapi/auth.go,流行为集中在 internal/httpapi/events_watch.go,命令装配与姿态校验集中在 cmd/bd/serve.go。运维实践的检查清单可以浓缩为:

  1. 显式端口上线,用address already in use做互斥;stdout 只读一行,其余全看 stderr;
  2. 部署后event=startupauthhost_allowlist,而非推断;
  3. /healthz只接 liveness,readiness 用GET /v0/beads/ready?limit=1且带 token,超时 2–5s;
  4. semaphore_saturatedsemaphore_timeoutconn_cap_saturated三个事件告警,以区分"楔住的数据库"与"慢客户端";
  5. 共享 dolt sql-server 按22 × 进程数 + 其他 bd 进程份额max_connections,并确认启动时没有pool_limits_unavailable
  6. 歧义关停后重新 claim 并读结果,绝不从断开的连接推断;
  7. 用户报 500 时索取request_id并 greprequest_error

在此基础上,若需要了解该 HTTP 表面所提供的操作全集、错误码词汇、游标契约与 loopback 姿态的设计决策,可直接阅读 design/bd-serve-v0.md;线上契约的单一事实来源则是internal/httpapi/spec/openapi.v0.yaml与 internal/httpapi/routes.go,二者由TestSpecRouteParity以双向集合相等焊死——操作面永远以 spec 文档、路由表或一台运行中的服务器为准。

【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询