go GC

基础知识 Tip go的垃圾回收是没有分代,不整理,并发的三色标记清扫算法 go1.3标记清除 从gc root出发,标记所有可达对象。最后扫描整个head,将没有标记的对象(不可达对象)清除。但缺点是STW、需要扫描整个heap、清除后会产生大量碎片。 为了缓解STW来带的停止长时间用户程序执行,标记之后马上停止STW,清除阶段与用户程序并行执行。 ...

九月 25, 2024 · by NOSAE

golang GMP调度器

Note 本文基于go1.21.2,不同版本的go可能会有差异。文中部分代码会由于不是知识点强相关而省略,但被忽略的每行代码或多或少都有它们实际的用处,甚至可能不宜删除,欢迎指出 Tip 分析底层的相关的代码时,往往会由于平台架构而带来代码上的差异,比如我机器是darwin/arm64,使用vscode查看代码,而且我希望基于linux/amd64来看代码,可以在.vscode/settings.json中设置GOOS和GOARCH: ...

九月 24, 2024 · by NOSAE

epoll中的LT和ET

本来我在看的是golang的gmp调度器,然后看到注释和代码里面有提到netpoll这个东西,不知不觉又去翻看了下linux网络编程相关的知识,上网找了下博客,找到了ants开源库作者关于go netpoll的博客,而后又因为我不了解其中epoll的LT/ET是什么东西,又赶紧补了一下这些知识,唉,本该早点就了解的知识,一直没能沉下心来看,虽然平时开发业务大概率用不到,但是看稍微底层一点的知识就会涉及到这些东西呢… LT和ET的区别 epoll(linux)/kqueue(unix)是现代操作系统用来做I/O多路复用的著名技术,通常有两种不同的工作模式LT(level- triggered,水平触发)和ET(edge-triggered,边缘触发),默认情况下epoll是工作在LT模式下的。 之前摸过一段时间的verilog,很熟悉edge-triggered这个玩意。电平见过吧,1是高0是低,那么一段连续变化的电平,大概可以表示成这个样子: 电平是一段时间内的值,比如横线在上代表这段时间内电平值是1,下面横线代表0,竖线代表由0变1(下降沿,如图中绿色竖线)或者1变0(上升沿,如图中红色竖线)。 这个跟我们的要说的LT和ET有什么关系呢?我们将1代表有fd可读写(对应电平1),0代表没有fd可读写(对应电平0),如果epoll工作在LT模式下,只要fd可读写,那么就epoll_wait一定返回有值,否则阻塞,相当于你去询问现在电平是多少,他就返回当前电平的值给你。但如果epoll工作在ET模式下,只有当fd是从不可读写变为可读写的时候(有新事件到来),epoll_wait才会告诉你可读写,相当于存储了一个上升沿状态,你epoll_wait调用相当于去消费这个状态,消费完后,在下一次边缘到来之前,epoll_wait就没有数据返回了,因为这个边缘已经在上一次epoll_wait被消费掉了。 举个例子,当前注册fd1和fd2的可读事件到epoll上,初始状态为[0,0]: 工作模式为LT,调用epoll_wait fd1可读事件到达 epoll_wait返回fd1可读,状态变成[1,0] 再次调用epoll_wait(注意此时状态依然是[1,0]),立刻返回fd1可读 ET模式: 工作模式为ET,调用epoll_wait fd1可读事件到达,状态变成[1,0] epoll_wait返回fd1可读 再次调用epoll_wait(注意此时状态依然是[1,0]),阻塞 结论就是,在LT工作模式下,可以通过不断询问epoll_wait来决定是否要读写fd,因为他总能告诉你最真实的是否可读写的状态。在ET工作模式下,你只会被通知一次fd可以读写,然后你最好就一次性读写完成,相当于将fd的当前状态清除掉,然后才去进行下一次epoll_wait,这里说的“一次性读写”并不是说只调用一次read/write,而是反复调用直到没有数据可以读出/写入,注意到因为需要反复调用,如果fd是阻塞模型的话,很可能最后一次就阻塞住了,导致你的I/O从多路复用模式直接变成了阻塞模式,甚至这个fd以后可能不再可读写,那你这个程序就再也跑不下去了,所以ET一般和fd设置成非阻塞一起使用。而LT模式下,fd设成阻塞或非阻塞都可以,因为epoll_wait会告诉你是否可读写,如果可以读写,read/write一定是成功的,因此不会阻塞。 当然,ET还有一些坑需要了解,详情看参考链接 参考链接 https://strikefreedom.top/archives/linux-epoll-with-level-triggering-and-edge-triggering https://strikefreedom.top/archives/go-netpoll-io-multiplexing-reacto https://zhuanlan.zhihu.com/p/40572954

九月 16, 2024 · by NOSAE

golang Context源码阅读

前言 本文基于go1.21,不同版本的Context内部实现可能会有细微差别 使用场景 为什么需要Context,首先思考一个场景:客户端去请求某个服务,这个服务依赖于多个可并行执行的下游服务,为了提高这个服务的响应速度,自然会为每个下游服务开启一个协程去执行。 倘若在执行的过程中,客户端取消了对这个服务的请求,下游服务也应该被停掉。但是go不提供直接关闭协程的方法,我们可以使用chan来协作式地关闭这些子协程:在服务代码中创建一个no buffer的chan并传递给这些子协程,通过子协程read chan+父亲协程close chan来达到通知子协程关闭的效果。 我们将这个场景扩展一下: 在这个场景中,child svc1又依赖于两个下游服务,并且某次请求中child svc1因为某些原因会取消对下游服务的请求(不影响其他服务节点的正常运行),老样子,在child svc1中创建一个chan并通过这个chan去关闭下游协程就好了。child svc1的代码可能会这么写: func childSvc1(upstream <-chan) { downstream := make(chan struct{}, 0) defer close(downstream) go childSvc1_1(downstream) go childSvr1_2(downstream) for { select { case <-upstream: close(downstream) return default: // 处理业务... // 因为某些原因需要取消下游服务 close(downstream) } } } 可以看到这段小snippet还能勉强阅读,但是从使用语义上来说,chan本身是用于传递数据的,这种read+close来关闭协程的方式就是某种hack手段,对于有一定规模的项目来说,这样的使用方式可读性将会非常差并且容易出错。因此我们需要一个带有“取消下游”语义的库来完成这种任务。 Context则很好地充当了这样的角色,它不但能控制下游的生命的生命周期,还自带超时控制、取消传递、并发安全、数据传递等功能: 取消传递:父Context的取消会自动传递到所有子孙Context,使得子孙Context也自动取消 超时控制:通过 context.WithTimeout 和 context.WithDeadline,提供自动超时取消机制 数据传递:可以携带请求范围内的数据(比如请求ID、path、begin timestamp等),这个特性在分布式链路追踪框架中被广泛使用 上面第二个场景中的代码换成Context来实现: import "context" func childSvc1(ctx context.Context) { cancel, ctx := context.WithCancel(ctx) // 创建子Context defer cancel() go childSvc1_1(ctx) go childSvr1_2(ctx) for { select { case ctx.Done(): return default: // 处理业务... // 因为某些原因需要取消下游服务 cancel() } } } 带超时控制的Context: ...

九月 14, 2024 · by NOSAE

golang底层知识汇总

Note 本文基于go1.21.2,不同版本的go可能会有差异。文中部分代码会由于不是知识点强相关而省略,但被忽略的每行代码或多或少都有它们实际的用处,甚至可能不宜删除,欢迎指出 Tip 分析底层的相关的代码时,往往会由于平台架构而带来代码上的差异,比如我机器是darwin/arm64,使用vscode查看代码,而且我希望基于linux/amd64来看代码,可以在.vscode/settings.json中设置GOOS和GOARCH: ...

九月 3, 2024 · by NOSAE