ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Loki 依赖包解读:modern-go/concurrent 的并发 Map 与 Executor 生命周期管理

Loki 依赖包解读:modern-go/concurrent 的并发 Map 与 Executor 生命周期管理 Loki 依赖包解读modern-go/concurrent 的并发 Map 与 Executor 生命周期管理【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki本篇基于 Loki 仓库中 vendored 的github.com/modern-go/concurrent包的文档与源码展开讲清它的两个核心组件为 Go 1.9 之前版本 backport 的concurrent.Map以及具备显式所有权、可取消、panic 安全的concurrent.Executor。读完你可以理解这个包在 Loki 依赖链中的位置一个// indirect的传递依赖并掌握如何用 Executor 模式管理长生命周期 goroutine 的启动、取消与优雅退出。这个包在 Loki 仓库中的位置modern-go/concurrent并不是 Loki 业务代码直接使用的模块它在 go.mod 中被标记为间接依赖github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect同样的版本号也出现在 operator/go.mod 与 operator/api/loki/go.mod 中说明它随 Loki 主模块与 Operator 模块一起被锁定在同一个 2018 年的提交上。从源码结构看Loki 自身代码并不 import 该包它是由 JSON 序列化生态库同仓库中同样间接存在的modern-go/reflect2、modern-go/encoding家族引入的传递依赖。仓库通过vendor/目录固化了它的完整实现本文对实现细节的引用均以下列真实文件为准文件作用vendor/github.com/modern-go/concurrent/README.md包文档本文核心依据vendor/github.com/modern-go/concurrent/executor.goExecutor接口定义vendor/github.com/modern-go/concurrent/unbounded_executor.goUnboundedExecutor完整实现vendor/github.com/modern-go/concurrent/go_above_19.goGo 1.9 下的 Map 实现vendor/github.com/modern-go/concurrent/go_below_19.goGo 1.9 之前的 Map 实现vendor/github.com/modern-go/concurrent/log.gopanic 日志与等待日志的输出口concurrent.Mapsync.Map 的可移植版本README 开篇给出的动机很直接sync.Map直到 Go 1.9 才进入标准库为了让代码在更老的 Go 版本上也能编译concurrent.Map提供了一个行为对齐sync.Map的包装。README 中的官方用法示例m : concurrent.NewMap() m.Store(hello, world) elem, found : m.Load(hello) // elem will be world // found will be true这个包装是如何做到跨版本可移植的答案在两个带 build tag 的文件中Go 1.9 路径go_above_19.go文件头声明//build go1.9此时Map直接内嵌标准库的sync.Map// Map is a wrapper for sync.Map introduced in go1.9 type Map struct { sync.Map }内嵌之后Load/Store等方法自动从sync.Map上提升不需要任何额外代码。Go 1.9 之前的路径go_below_19.gobuild tag 为!go1.9Map退化为读写锁 原生 map的经典写法NewMap()初始容量固定为 32type Map struct { lock sync.RWMutex data map[interface{}]interface{} } func (m *Map) Load(key interface{}) (elem interface{}, found bool) { m.lock.RLock() elem, found m.data[key] m.lock.RUnlock() return } func (m *Map) Store(key interface{}, elem interface{}) { m.lock.Lock() m.data[key] elem m.lock.Unlock() }需要注意的实现差异低版本实现只提供了Load与Store两个方法源码注释写作 Load is same as sync.Map Load并没有覆盖sync.Map的完整方法面如LoadOrStore、Range等。也就是说这份 backport 只保证了最基本的线程安全的存取契约而不是sync.Map的全量 API。此外低版本实现内部使用interface{}作为键值类型与高版本路径基于具体类型的sync.Map在类型安全上并不等价——从源码结构看这个包的兼容层是为老版本能跑起来服务的而非性能优化。对今天使用 Loki 的读者而言Loki 要求的 Go 版本远高于 1.9实际编译走的是go_above_19.go这条内嵌sync.Map的路径这个包的意义更多是体现其上游依赖JSON 序列化库对老版本 Go 的兼容策略。concurrent.Executor带所有权的 goroutine 管理器README 给出的第二个组件是concurrent.Executor官方示例展示了一个每秒打印一次、可被取消的 ticker goroutineexecutor : concurrent.NewUnboundedExecutor() executor.Go(func(ctx context.Context) { everyMillisecond : time.NewTicker(time.Millisecond) for { select { case -ctx.Done(): fmt.Println(goroutine exited) return case -everyMillisecond.C: // do something } } }) time.Sleep(time.Second) executor.StopAndWaitForever() fmt.Println(executor stopped)README 对它的价值归纳为两点通过Stop/StopAndWait/StopAndWaitForever停止 executor 时连带取消它名下所有 goroutine提供 panic 回调goroutine 里的 panic 默认不再让整个应用崩溃而是被 recover 并记录日志。Executor 接口用所有权替代裸goexecutor.go 中接口的注释把设计意图说得很清楚Executor用来替代go关键字启动 goroutine由 executor 启动的 goroutine 属于 该 executor调用方只需要停止 executor 本身就能取消它名下所有 goroutine。接口本身刻意不包含Stop方法——注释明确指出启动并拥有 executor 的一方应持有具体类型而非接口type Executor interface { // Go starts a new goroutine controlled by the context Go(handler func(ctx context.Context)) }这是一个值得留意的 API 设计点能力被拆成了使用方看到的启动接口和持有方看到的生命周期接口两层避免把停止权限意外暴露给不拥有 executor 生命周期的代码。UnboundedExecutor 的实现细节核心实现在 unbounded_executor.goUnboundedExecutor不限制存活 goroutine 数量Unbounded 的由来它内部只跟踪自己启动过的 goroutinetype UnboundedExecutor struct { ctx context.Context cancel context.CancelFunc activeGoroutinesMutex *sync.Mutex activeGoroutines map[string]int HandlePanic func(recovered interface{}, funcName string) }结合源码可以提炼出四个关键机制1. 以 context 为取消信号。NewUnboundedExecutor()通过context.WithCancel(context.TODO())创建内部 context见 unbounded_executor.go 第 38-46 行。Go()把executor.ctx传给 handler所以被托管的 goroutine 只需要像 README 示例那样监听ctx.Done()executor 的Stop()就会统一触发取消。2. 以启动位置为键的活跃度追踪。Go()第 50-77 行用反射拿到 handler 函数的运行时地址与源码位置pc : reflect.ValueOf(handler).Pointer() f : runtime.FuncForPC(pc) funcName : f.Name() file, line : f.FileLine(pc) startFrom : fmt.Sprintf(%s:%d, file, line) executor.activeGoroutines[startFrom] 1同一处代码启动的多个 goroutine 计数累加goroutine 退出无论正常返回还是 panic时在defer中减一。这个计数正是StopAndWait判断是否全部退出的依据。3. panic 被默认 recover 并打日志。全局默认回调HandlePanic第 13-17 行把 panic 值与完整debug.Stack()堆栈写到ErrorLogger实例字段HandlePanic可覆盖全局行为第 64-69 行优先调用实例级回调。ErrorLogger默认输出到 stderr定义在 log.go。另外源码注释明确提示如果想让 goroutine 退出时不触发 panic 回调应使用runtime.Goexit()退出而不是 panic 一个特殊值。4. 等待退出采用轮询而非同步原语。StopAndWait(ctx)第 90-105 行先cancel()然后每隔 100ms 检查一次activeGoroutines计数是否全部归零期间如果传入的 ctx 被取消则放弃等待直接返回。StopAndWaitForever()是它的便捷封装使用context.Background()即等到最后一个 goroutine 退出为止。checkNoActiveGoroutines()在仍有存活 goroutine 时会通过InfoLogger打印它们的启动位置与数量——默认InfoLogger写入ioutil.Discard即静默需要诊断时把它指向日志器即可看到哪些 goroutine 还没退。还有一个全局单例GlobalUnboundedExecutor第 29-33 行注释说明它与程序同生命周期希望在 main 退出前被关停的 goroutine 可以从它启动但它无法自动感知 main 退出需要 main 函数显式调用 stop。适用边界与工程启示结合上述源码可以总结这个包的两条边界Map 的兼容价值随 Go 版本推移而淡化。// indirect标记与 2018 年的锁定版本v0.0.0-20180306012644-bacd9c7ef1dd说明它属于历史依赖链的一部分对使用现代 Go 版本编译 Loki 的场景实际生效的是内嵌sync.Map的go_above_19.go路径。Executor 的价值在于把goroutine 生命周期变成可管理的对象。它的等待机制是 100ms 轮询计数StopAndWait内每轮新建一个time.NewTimer而非事件驱动因此全部退出的确认最多有约 100ms 的滞后且追踪粒度是启动位置级别的计数不能定位到单个 goroutine。对于 Loki 这类长服务进程更常见的做法是通过服务框架自带的依赖注入与优雅关停机制管理 goroutine这个包更多是上游序列化库的实现细节。如果要深入阅读建议顺序是先读 README.md 建立 API 认知再看 executor.go 的接口注释理解所有权设计最后通读 unbounded_executor.go 中Go、StopAndWait、checkNoActiveGoroutines三个方法即可完整掌握这套启动位置计数 context 取消 panic 兜底的 goroutine 托管模型。【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表