Optimux 是一个 Go 服务,可即时调整图片和视频大小、重新编码并流式传输——通过一个 URL 参数即可完成。该项目已在内部运行一段时间,现以 AGPL-3.0 协议开源。开源的核心动机是底层 worker 池:决定任意时刻有多少 goroutine 在处理图片的部分,作者认为这是代码库中最有趣的部分,值得从私有仓库中拿出来。
作者从经典的 Job/Dispatcher 模式起步,该模式源自 Marcio Castilho 的《用 Golang 处理每分钟 100 万请求》一文。该模式用有界池替代无界 goroutine 生成,曾将系统从约 100 台 EC2 实例缩减到 4 台 c4.Large,处理接近每分钟百万请求。但那个工作负载是向 S3 上传 JSON——几乎全是 I/O 等待,几乎不占 CPU。换成图片缩放后,模式失效了。
pprof 阻塞分析显示 67.99% 的时间花在 chanrecv1 上——worker 在等任务,dispatcher 在等 worker 归还通道。当单个任务变成数百毫秒的 CPU 密集型图片处理时,通道编排开销不再隐形。目标吞吐 8.6rps,该模式只能到 2.0xrps。作者直接推倒重来。
从同步基线逐变量重建:无 worker 内联处理 2.9rps,已超过 dispatcher 模式;单通道单 worker 2.1rps,比不并发还差;单队列 4 worker 3.15rps,平均延迟 1.37s;双队列(拉取/处理分离)各 4 worker 3.07rps,但延迟分布明显平滑——拉取源图 40-100ms,libvips 处理约 700ms,两阶段服务时间差异巨大,共享队列存在阻抗失配。加权 2 拉取/4 处理反而更差。另尝试了需求驱动型生产者/消费者(类似 Elixir GenStage),协调成本高于收益,未采用。独立于 worker 数量的优化:流式输出编码字节而非整体缓冲,吞吐 3.28rps,延迟降至 1.31s。
核心结论:Job/Dispatcher 模式并非错误,而是适用于外部资源(S3、网络、数据库)受限、自身 CPU 基本空闲的工作负载。libvips 图片处理则相反:受限资源是自身 CPU 和内存,固定 MAX_WORKERS 无法跟踪突发与空闲交替的队列。EC2 上 iostat 显示 CPU 在 80%+ 与 40% 间摆动,%steal 峰值达 8.74%。worker 数量本身必须是活变量。
结构差异:dispatcher 模式用 N+1 个通道(池通道加每 worker 通道)和 dispatcher goroutine 表达可用性;DynamicScaler 所有 worker 直接从一个共享 Queue chan T 拉取,无 dispatcher、无每 worker 通道、无显式"空闲"消息——可用性即"当前阻塞在 Queue 接收上",由 Go 运行时仲裁。另一差异:dispatcher 的 MAX_WORKERS 启动时读一次环境变量,永不变化;DynamicScaler 的每个 WorkerSlot 可标记 Retiring,需有竞态安全的退出路径,因为该工作负载下"当前正确 worker 数"本身就是被争用的资源。
DynamicScaler[T] 是基于队列压力伸缩的泛型 worker 池,类型参数为任务类型,同一 scaler 可独立驱动图片、视频、批处理任务。注册表是单一有序切片,按创建顺序追加,尾部即最新 worker,LIFO 退休策略因此是确定性扫描。活跃数通过遍历计算得出,无独立计数器,不存在状态失同步问题。退休发生在空闲边界:worker 在 select 中阻塞等待取消、退休请求或任务队列,无 default 分支;退休请求在 worker 空闲瞬间到达。默认 2 秒宽限期,超时则取消 context 兜底,但注册表移除只发生在真实退出通知时。伸缩决策有冷却期(默认 30 秒),因瞬时队列长度在突发流量下噪声大;唯一绕过冷却的情况是活跃 worker 低于 MinWorkers 时立即补充,防止崩溃风暴导致池长时间空转。
未测试项:Fiber 的 prefork 模式(SO_REUSEPORT 运行 N 个 OS 进程,内核负载均衡连接)。作者判断不会提升吞吐——所有池形态实验吞吐仅在 2.1 到 3.15rps 间波动,真实成本是每图约 700ms 的 libvips CPU 工作,非调度或通道开销。prefork 改变的是进程数而非核心数。预期收益在尾部延迟和故障隔离:单进程 GC 暂停或 OOM 不会拖垮其他 fork。
后续计划:等待 govips 合并原生流式支持(PR davidbyttow/govips#539),当前每阶段都整体缓冲为 []byte;HTTP/2 跨流优先级调度器——Go 的 x/net/http2.WriteScheduler 已被维护者弃用,nginx 的优先级树私有,fasthttp 解析 PRIORITY 帧后不再使用,真正先例是 H2O(O(1) 但不公平)、nghttp2(WFQ 但 O(n))、Tempesta FW(WFQ + HAProxy ebtree),计划基于 x/net/http2.Framer 从零实现;扩容信号需区分真实突发与 libvips 线程池饱和——govips 的 ConcurrencyLevel 实际只写不读,RuntimeStats 只统计累计操作数,可行方案是原子 in-flight 计数器配合 VipsConcurrency 门控。
仓库:github.com/go-batteries/optimux
#开发者 #工具 #Optimux #Go #Golang #WorkerPool #libvips #图片处理 #视频处理 #AGPL
@DevToolboxHub