Go Channel 底层解析:锁、环形缓冲区与等待队列
有一次排查线上并发服务发现 goroutine 数量冲到几万CPU 却不怎么涨。直觉告诉我问题出在 channel一大票 goroutine 堵在ch - v或v : -ch上互相等待活活把并发打成了串行的“等”。当晚我把 Go 运行时里 channel 的实现从头读了一遍读完之后以前那些零散认知——什么时候走缓冲区、什么时候直接做数据拷贝、锁到底保护哪些字段、阻塞的 goroutine 被存放在哪里——全部串了起来。这篇文章就是那次阅读的整理。我按 runtime 中hchan结构的三个核心机制展开锁、环形缓冲区、等待队列。会讲清ch - v与v : -ch背后的完整代码路径也会解释几个非常反直觉的优化。适合已经写过一段时间 Go 并发代码、想往底层再走一步的读者。select 的完整实现不在本文范围那是一个足够单独开一篇的话题。1. hchan 结构体逐字段拆解锁、缓冲、队列如何拼成一台机器1.1 核心源码骨架在 Go 运行时里channel 的运行时表示是hchan。名字很直白heap channel因为它总是在堆上分配的。简化掉和主线无关的字段后核心结构长这样type hchan struct { qcount uint // 当前环形队列中的元素个数 dataqsiz uint // 环形队列容量也就是 make(chan T, n) 里的 n buf unsafe.Pointer // 指向环形队列内存 elemsize uint16 // 单个元素占用的字节数 closed uint32 // 是否已经被 close elemtype *_type // 元素类型拷贝元素时要用 sendx uint // 下一个写入位置 recvx uint // 下一个读取位置 recvq waitq // 阻塞在接收操作的 goroutine 队列 sendq waitq // 阻塞在发送操作的 goroutine 队列 lock mutex // 保护上述所有字段的排他锁 }一眼看过去channel 的“存储”其实是一个带两个下标的循环数组buf是内存sendx是写指针recvx是读指针qcount记录当前积压的元素数量。发送、接收两个方向各有一支等待队列队列节点不是 goroutine 本身而是一个叫sudog的结构。最后整个结构体用一把mutex包住所有字段的读写都在锁内完成。这三个组件各司其职环形缓冲区处理的是“缓冲区还有空间”的正常收发等待队列处理的是“缓冲区满了或空了先睡一会”的阻塞场景锁则保证多个 goroutine 同时操作 channel 时共享状态不会被撕碎。1.2 buf 与索引chan 内部其实是个队列很多人把 channel 理解成“带类型的管道”但从内存布局看它更像一个生产者消费者队列。发送方往队尾放数据接收方从队头取数据sendx和recvx就是队尾和队头的指针。这带来几个直接推论元素的顺序是保序的先发的先被收到FIFO容量不够时新来的发送者会排进sendq等待时也是按顺序出队qcount用来区分“空”和“满”因为当sendx recvx时既可能是空也可能是满单靠两个下标没办法分辨。这个设计比“动态数组 搬迁”简单得多。数组满了不想开新内存直接把写指针绕回开头只要读指针没追上它就行。Go 选择固定容量的环形缓冲区换来的是 O(1) 的入队出队和完全可预测的内存占用代价是容量用完之后只能阻塞。1.3 waitq 与 lock队列本身不是并发安全的recvq和sendq的类型是waitqtype waitq struct { first *sudog last *sudog }它就是一个双向链表的头尾指针。链表里每个节点描述一个“等待中的 goroutine”。这里要注意waitq自己没有锁它只是数据结构链表节点的插入、删除、遍历全部要持hchan.lock。所以 channel 的并发安全并不是来自“队列自带锁”而是来自hchan.lock这个总闸。你读源码时会发现几乎所有函数的第一行都是lock(c.lock)最后一行是unlock(c.lock)就是这个原因。既然锁这么重要就需要搞清楚它到底锁住了什么、为什么能扛住高并发。2. 从 makechan 开始环形缓冲区的分配策略与两个隐藏细节2.1 两种内存布局GC 决定要不要单独分 buf先看出生过程。make(chan T, n)最终会调用makechan。源码里对内存分配做了区分元素类型不含指针和含指针采用两种完全不同的布局。func makechan(t *chantype, size int) *hchan { mem : hchanSize uintptr(size)*t.elem.Size_() var c *hchan if !t.elem.HasPointers() { // 元素不含指针hchan 和 buf 一次性分配紧挨着放 c (*hchan)(mallocgc(mem, nil, flagNoScan)) c.buf add(unsafe.Pointer(c), hchanSize) } else { // 元素含指针hchan 和 buf 分开分配buf 需要 GC 扫描 c new(hchan) c.buf mallocgc(uintptr(size)*t.elem.Size_(), t.elem, flagScan) } c.elemsize uint16(t.elem.Size_()) c.elemtype t.elem c.dataqsiz uint(size) return c }第一次读到这里时我愣了好一会为什么无指针类型要把buf紧跟着hchan分配原因在 GC。如果元素里没有指针GC 扫描这个对象时不需要递归检查每个元素引用于是整块内存可以标记为“无需扫描”分配一次即可。如果元素含指针buf里的内容必须参与 GC 追踪所以要单独分配并且把元素类型传给分配器让它知道怎么扫描。另一个隐藏细节如果元素大小是 0比如make(chan struct{}, 1000000)mem计算出来还是hchanSize分配器也不会为这 100 万个元素真给内存。这对struct{}类型的 channel 来说很划算它本质只承担信号同步作用。2.2 判断空与满为什么必须靠 qcount环形缓冲区的容量判断是最容易被误读的地方。直觉是sendx recvx表示空其实不一定。看一个容量为 4 的 channel操作sendxrecvxqcount状态初始000空连续发送 4 次004满接收 1 次013正常再连续接收 3 次000空可以看到空和满两种状态下sendx都可能等于recvx。区分它们只能看qcountqcount 0是空qcount dataqsiz是满。所以在 channel 源码里你几乎看不到用“两个下标是否相等”来判断状态的代码到处都是对qcount和dataqsiz的比较。这个设计也解释了为什么环形缓冲区容量必须是固定的如果容量可变数据搬迁会非常麻烦而且qcount与容量之间的关系会失去约束力。2.3 下标回绕一个 if 就完成了取模环形缓冲区最常见的写法是用取模(sendx 1) % dataqsiz来绕圈。Go 源码没有用取模用的是更直接的判断func chanbuf(c *hchan, i uint) unsafe.Pointer { return add(c.buf, uintptr(i)*uintptr(c.elemsize)) } // 入队后推进发送下标 c.sendx if c.sendx c.dataqsiz { c.sendx 0 } // 出队后推进接收下标 c.recvx if c.recvx c.dataqsiz { c.recvx 0 }为什么不用取模因为除法在 CPU 里是比较贵的指令而if 清零在绝大多数场景下只需要两次整数比较成本低得多。容量不要求是 2 的幂照样能正确回绕。你要读源码这种“能用if就不用%”的优化思路会一直出现。3. 锁的边界为什么整条收发路径只有一把互斥锁3.1 为什么不能用读写锁channel 的读写操作看起来天然适合“读多写少”能不能用读写锁不能。因为在 channel 里发送方和接收方都会修改同一个共享区域发送方写buf、移动sendx、改qcount接收方读buf、移动recvx、改qcount。两边都是写操作唯一的“读”其实是拷贝元素时短暂读取elemtype、elemsize等元数据。所以这里只能是一把排他锁不分读者和写者。锁保护的范围也很明确hchan的全部字段。具体到一次发送检查c.closed防止向已关闭的 channel 发送检查c.recvq看有没有正在等待的接收者检查c.qcount决定是写入缓冲区还是把自己挂进sendq真正执行memmove拷贝元素推进sendx修改qcount。以上每一步都必须串行否则两个 goroutine 同时入队sendx就会错乱qcount就对不上了。3.2 锁内的临界区到底有多小读源码时你会注意到锁的临界区通常很短没有系统调用、没有 IO、没有用户回调。绝大多数情况下就是几十条指令的内存操作锁持有时间在微秒级甚至纳秒级。这也是为什么 channel 在小规模并发下性能不错高并发下锁竞争虽然存在但临界区短等待者很快能抢到锁。Go 运行时的mutex是自旋与休眠混合实现的。短临界区下竞争线程先自旋一小段时间通常能等到锁释放省去了进入内核态休眠的代价。如果自旋超时还没拿到才把线程休眠。这套机制对 channel 这种“临界区极短”的场景很友好。3.3 一个关键顺序先解锁再唤醒 goroutine最容易被忽略的细节是锁与唤醒的顺序。在send和recv这两个内部函数里我看到的模式是func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { if sg.elem ! nil { sendDirect(c.elemtype, sg, ep) sg.elem nil } gp : sg.g unlockf() // 先释放 hchan.lock goready(gp, skip) // 再唤醒被阻塞的 goroutine }unlockf是对解锁函数的封装传进来的是一个闭包实际就是unlock(c.lock)。这里刻意把goready放在解锁之后非常讲究。goready需要把 goroutine 放进调度器的可运行队列那会拿调度器自己的锁。如果还握着c.lock就跑去唤醒他人可能形成锁顺序依赖A goroutine 持有 channel 锁等待调度器锁另一个 goroutine 持有调度器锁又想在某个 channel 上操作。为了避免这种潜在死锁运行时的惯例是锁内只改状态锁外再唤醒。我在读旧版本代码时见过唤醒在锁内的写法后来 Go 逐步改成锁外唤醒不是没有原因的。你写业务代码时也可以借鉴这个顺序在临界区内完成数据修改然后在临界区外通知别人能显著减少死锁面。3.4 锁竞争的代价单 channel 的吞吐天花板既然锁临界区短是不是就可以随便往一个 channel 里塞数据不是。多核竞争下锁的 cache line bouncing 会让性能变得很难看每次解锁后其他核心要重新拉取这段缓存CPU 时间的相当一部分会耗在缓存同步上。实践中的经验是一个 channel 的吞吐上限通常就是这把锁的上限跟缓冲区大小关系不大。缓冲区只是降低阻塞概率并不会减少锁竞争频率因为每次收发都还是要拿锁。4. sendq、recvq 与 sudog阻塞的 goroutine 是如何被管理的4.1 sudog封装一次阻塞等待当缓冲区满或空goroutine 不能立刻完成收发就要进入等待队列。等待队列的节点是sudogtype sudog struct { g *g // 等待的 goroutine isSelect bool // 是否来自 select elem unsafe.Pointer // 等待中的元素指针 next *sudog prev *sudog releasetime int64 }为什么不让 goroutine 直接排队而要包一层sudog因为一个 goroutine 可能同时等待多个 channel比如在select里。sudog相当于一次“等待请求”的登记表我在等哪个 channel我的数据放在哪个地址我是不是 select 的一部分。同一时刻一个 goroutine 可以被多个sudog引用这为 select 的多个等待分支提供了支持。elem字段也很关键。发送者被阻塞时elem指向待发送变量地址接收者被阻塞时elem指向接收目标地址。后面唤醒后的数据拷贝靠的就是这个指针。4.2 入队出队时机收发路径的四个分支结合sendq和recvqchannel 的每一次收发都遵循一套固定判断顺序。发送ch - v时如果recvq里有等待的接收者直接让对方收走不走缓冲区否则如果缓冲区没满写入缓冲区否则把自己挂进sendqgopark休眠。接收v : -ch时如果sendq里有等待的发送者直接从对方那里拿数据否则如果缓冲区有数据从缓冲区取否则把自己挂进recvqgopark休眠。这套分支顺序贯穿全文后面第 5 节会展开两个反直觉的“快速路径”。4.3 gopark 与 goreadygoroutine 的状态流转阻塞不只是“傻等”它有一套与调度器联动的状态机。调用gopark时当前 goroutine 会从running状态切到waiting状态并让出 CPU去执行别的 goroutine。gopark还会带上一个等待原因比如waitReasonChanSend或waitReasonChanReceive。这就是你在 goroutine dump 里看到chan send (...)、chan receive (...)的来源。等到被唤醒时goready会把 goroutine 从waiting状态放回可运行队列标记为runnable等待调度器再次选中它。所以从挂起到唤醒数据拷贝其实已经完成了唤醒后的 goroutine 只是回到用户代码继续往下走。有一点值得单独说被唤醒并不代表原操作一定成功因为 channel 可能在等待期间被close了。sudog里有个成功标志唤醒后如果发现 channel 已关闭接收者会拿到零值并返回okfalse发送者则会 panic。边界情况在chanrecv和chansend里都有专门处理。5. 收发路径完整走读两个反直觉的快速路径5.1 有缓冲发送recvq 非空时数据根本不放缓冲区很多人以为ch - v一定先把数据放进缓冲区接收方再从缓冲区取。实际上当recvq非空时发送方会绕过缓冲区直接把数据交给等待中的接收者。为什么可以绕过因为这里有一个很重要的不变量如果recvq非空缓冲区一定是空的。想想看如果缓冲区里有数据接收者早就从缓冲区拿到数据了根本不会阻塞到排队。所以发送方遇到等待的接收者时缓冲区必然是空状态直接点对点直传不会破坏 FIFO 语义。对应源码结构lock(c.lock) if sg : c.recvq.dequeue(); sg ! nil { send(c, sg, ep, func() { unlock(c.lock) }, 3) return true }send函数里做的是栈到栈的直接拷贝发送方的数据用sendDirect写到接收方sudog.elem指向的内存。因为接收者正处于等待状态没有执行任何用户代码它的栈是稳定可写的。这个设计省掉了一次“先入 buf再出 buf”的中转而且数据只拷贝一次。5.2 有缓冲接收sendq 非空时会触发“读一个补一个”接收方同样有反直觉的路径。当sendq非空时说明缓冲区一定是满的——缓冲区不满发送者早就入队了不会躺在等待队列里。此时接收方一来正确的做法并不是只从缓冲区拿走一个而是走recv函数func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { if c.dataqsiz 0 { recvDirect(c.elemtype, sg, ep) } else { qp : chanbuf(c, c.recvx) typedmemmove(c.elemtype, ep, qp) // 1. 缓冲区队头元素交给接收方 typedmemmove(c.elemtype, qp, sg.elem) // 2. 等待发送者的数据补进队头位置 c.recvx if c.recvx c.dataqsiz { c.recvx 0 } c.sendx c.recvx c.qcount-- } ... }这段代码非常漂亮。接收方拿走队头旧元素后紧接着把等待发送者的元素直接写入刚刚空出来的位置下一次接收又能立刻拿到它。sendx被设置成与recvx相同因为缓冲区从“满”退回到“满-11”而且新补进去的元素就在对头位置发送下标应该跟随队尾移动。为什么不能简单地从缓冲区取一个然后让等待的发送者醒来慢慢入队因为那样需要多一次唤醒、多一次入队而且发送者在被唤醒后还要重新拿锁执行一次发送流程多走一圈竞态检查。当前实现把“补位”直接塞进了接收者的临界区里一次锁操作完成两件事效率高很多。这段“读一个、补一个”的代码是我觉得整个 channel 实现里最有启发的地方。看懂它才算真正理解了 channnel 的收发不是“进缓冲、出缓冲”的简单两级流水线而是充满直传优化。5.3 无缓冲收发没有中间商纯直传无缓冲 channel 在makechan时dataqsiz 0buf根本没有分配。它的收发只有一条路接收方必须先进入等待队列发送方到来后直接数据拷贝到接收方栈地址然后唤醒。反过来也成立发送方先等接收方到来后从发送方栈上把数据拷走。源码里send与recv对dataqsiz 0的分支都调用了sendDirect/recvDirect这类直传函数本质就是一次typedmemmove。所以无缓冲 channel 的天然语义就是“同步握手”发送完成了接收就完成了接收没准备好发送就永远等着。我踩过的坑是把无缓冲 channel 当成“有缓冲但容量为 0”去理解结果对阻塞时机的预期完全错位。无缓冲 channel 的核心价值在于同步而不是临时存数据。5.4 close 的边界唤醒所有人但发送者等来的是 panicclose的源码路径也是锁内处理等待队列、锁外唤醒。它会遍历recvq和sendq把所有sudog的elem清零收集到本地链表里解锁后逐个goready。关闭后的行为边界设计得很干脆向已关闭的 channel 发送数据panic从已关闭但缓冲区还有数据的 channel 接收先把剩余数据取完缓冲区见底后再接收直接返回零值且ok标志为 false无缓冲 channel 关闭后接收者永远拿到零值。很多人会误以为“关闭就是让接收者不再收到值”其实是“先把存量发完再返回零值”。这个语义在我的排查经历里多次出现过比如一个消费者因为 channel 突然被 close拿到的全是零值又没检查ok标志把零值当真数据处理了。看一遍源码之后这个行为就再也忘不掉了。6. 读完源码后的实战心得选型、调参与避坑清单6.1 缓冲区大小该怎么定依据是什么知道了内部实现再回头选缓冲区大小就有依据了。缓冲区存在的意义是允许生产者和消费者在一段时间内速度不一致消费者临时慢了生产者可以继续往里放几个生产者临时慢了消费者可以继续取出几个。如果双方基本同步缓冲区设成 0 就很合适还强制了每次交互都完成一次握手语义更清晰。如果只有一方偶发波动缓冲区可以设成“波动容忍量”一次突发最多积压多少任务就设成多大。不要拍脑袋设一个很大的数因为过大的缓冲区只会让调度延迟变高消费者拿到的任务不够新鲜还会让内存长时间占用。我在源码里看到qcount与dataqsiz比较满空的逻辑后对“容量只是缓冲队列长度”这个理解更深了一层它不会改变 channel 的语义只改变阻塞出现的时机。6.2 锁竞争明显的场景不要只盯着缓冲区如果你已经排除了其它问题发现压测时大量 goroutine 卡在同一个 channel 的收发上那大概率是hchan.lock本身的竞争饱和了而不是缓冲区不够。此时加缓冲区只会延迟阻塞锁竞争依然存在。缓解手段一般朝两个方向走扇出一个生产者的数据通过 hash 或轮询分到多个 channel多个消费者组分别处理天然降低单把锁的竞争批量把多次发送合并成一次批量发送比如上报日志时攒一批再统一写入 channel减少锁调用次数。我处理过一个类似场景高频埋点数据全部涌向一个 channel把单个 channel 当成消息总线用。改成 4 个 channel 按用户 ID 分片后吞吐提升非常明显。源码的知识有时候不需要真的去改 runtime它帮你判断瓶颈在哪里。6.3 高频踩坑点nil channel、close 与 goroutine 泄漏最后按源码行为列几个高频坑nil channel 收发永久阻塞。源码里chansend遇到c nil直接gopark不再继续。这不是 bug是设计。忘记初始化 channel 就使用会让 goroutine 安静地消失。发送方的 panic 不一定在 close 时立刻发生。如果你向一个已 close 的 channel 发送源码会在获取锁后检查closed并 panic但如果发送方已经阻塞在sendq里close 会唤醒它并直接让它 panic。等待队列不是无限制的。无限地创建 goroutine 去等一个永远不会有东西的 channel最终会拖垮内存。所以 goroutine dump 里如果大量停在chan send或chan receive应该马上排查是否有消费端没起来、或者逻辑上不满足唤醒条件。不要在接收端只依赖返回值不检查 ok 标志。close 后接收会不断返回零值除了“零值是否合法”的问题更重要的是这可能让循环进入忙轮询白白占满 CPU。6.4 我排查问题时常看的三个信号看完源码后我排查 channel 相关问题的步骤也固定了。先抓 goroutine dump数一数停在chan send、chan receive、select上的 goroutine 数量。如果chan send多说明消费者瓶颈或者 buffer 太小如果chan receive多说明生产不足或者任务还没进来如果两者都多且数量相近往往是无缓冲 channel 的同步握手太多锁竞争或唤醒开销占据了主导。抓到信号后再看qcount也就是 channel 当前堆积量。业务代码里如果用了runtime或调试工具能看到 channel 的qcount/dataqsiz这个比值能直接告诉你 buffer 是常满还是基本空闲。常满说明消费者跟不上空闲但 goroutine 阻塞多说明同步模式太密。这套判断方法比我以前“先加 buffer再上日志”要快得多。最后再分享一个小习惯我遇到可疑阻塞问题时先不急着改代码而是先画一条“发送方、channel、接收方”的状态线标出recvq、sendq分别有没有人、缓冲区空不空再对照源码分支判断卡点在哪。多数时候问题不是调度器也不是 Go 版本而是我们对 channel 行为模型的想象出了问题。读完源码以后channel 就不再是黑盒了。

相关新闻

Intel SGX生产实践:EPC内存管理与远程证明避坑指南

Intel SGX生产实践:EPC内存管理与远程证明避坑指南

1. 为什么今天还有人认真研究Intel SGX——不是怀旧,而是现实倒逼“Intel SGX”这四个字母在2024年的技术讨论中,听起来像一句老派的暗号。它不像LLM、RISC-V或存算一体那样高频刷屏,也不在主流招聘JD里明晃晃挂着“精通SGX优先”的标签。但就…

2026/10/15 13:54:39 阅读更多 →
sebastian/global-state 版本演进全解读:从 ChangeLog 看全局状态快照组件的 API 变迁与 PHP 支持策略

sebastian/global-state 版本演进全解读:从 ChangeLog 看全局状态快照组件的 API 变迁与 PHP 支持策略

测试 【免费下载链接】global-state Snapshotting of global state, factored out of PHPUnit into a stand-alone component 项目地址: https://gitcode.com/gh_mirrors/gl/global-state 点击查看 免费下载 sebastian/global-state 是从 PHPUnit 中独立出来的全局…

2026/10/15 13:54:39 阅读更多 →
直流微电网分层控制架构设计与Matlab实现

直流微电网分层控制架构设计与Matlab实现

1. 项目背景与核心价值孤岛式直流微电网是当前分布式能源系统中的研究热点,特别适合偏远地区、岛屿或对供电可靠性要求高的特殊场景。与传统交流微电网相比,直流微电网避免了频率同步问题,更易于接入光伏、储能等直流源荷。但这也带来了新的挑…

2026/10/15 13:54:39 阅读更多 →

最新新闻

OpenShell:面向开发者的Windows桌面增强工具

OpenShell:面向开发者的Windows桌面增强工具

1. OpenShell 是什么?它不是 Shell,更不是“开源外壳” OpenShell 这个名字,乍一听容易让人误以为是某种 Linux 或 macOS 的 shell 替代品——比如像 zsh、fish 那样的命令行解释器,或者某个带图形界面的终端模拟器。但事实恰恰相…

2026/10/15 14:31:41 阅读更多 →
OpenShell:跨平台终端环境协同方案详解

OpenShell:跨平台终端环境协同方案详解

1. OpenShell 是什么:一个被严重误读的“跨平台终端体验重构计划”OpenShell 这个名字在当前技术社区里,正经历一场典型的语义漂移——它既不是某个已发布的开源项目官方名称,也不是微软、苹果或Linux发行版的正式组件代号。但恰恰是这种模糊…

2026/10/15 14:31:41 阅读更多 →
FFmpeg转码核心技术解析与实战应用

FFmpeg转码核心技术解析与实战应用

1. 为什么需要系统掌握FFmpeg转码?第一次接触FFmpeg时,我像大多数人一样只会用简单的命令转换视频格式。直到有次客户要求将4K HDR素材转码为适配老旧机顶盒的MPEG-2格式,才发现参数配置不当会导致画质严重劣化。那次经历让我意识到&#xff…

2026/10/15 14:31:41 阅读更多 →
ShardingJDBC分库分表入门:分片键设计、配置实战与生产避坑指南

ShardingJDBC分库分表入门:分片键设计、配置实战与生产避坑指南

这两年总有同事问我,说订单表已经两千多万行了,到底要不要拆表?我的回答通常是:先别急着拆,先想想你的数据访问规律到底是什么。很多人被性能问题逼到分库分表面前,却没搞清楚中间件到底是怎么工作的&#…

2026/10/15 14:31:41 阅读更多 →
Intel SGX入门:硬件级可信执行环境原理与落地实践

Intel SGX入门:硬件级可信执行环境原理与落地实践

1. 从一个真实困惑说起:为什么需要关注SGX我第一次接触Intel SGX是在做一个跨平台数据协作项目的时候。当时的需求很朴素:多个参与方想联合计算一个统计结果,但谁都不愿意把原始数据交出去。我们试过同态加密,性能损耗大到没法接受…

2026/10/15 14:31:41 阅读更多 →
基于 GPU 算力集群利用率的反向限流:防止私有化推理卡池进入热崩溃

基于 GPU 算力集群利用率的反向限流:防止私有化推理卡池进入热崩溃

大模型私有化部署在各行各业落地过程中,绝大多数架构师在传统微服务中积累的限流经验都会彻底失效。传统的 API 网关限流通常基于固定的 QPS 或并发连接数,比如给某个接口配置 200 QPS 的令牌桶。但这套逻辑放在大模型推理集群上几乎等于自杀。 在 LLM 推…

2026/10/15 14:30:40 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →