2026年一月月 发布的文章

像构建 Claude Code 一样构建应用:揭秘 Agent-native 架构的 5 大核心原则

本文永久链接 – https://tonybai.com/2026/01/13/agent-native-architecture

大家好,我是Tony Bai。

软件智能体(Software Agents)现在已经能够可靠地工作了。Claude Code 证明了,只要赋予一个大语言模型(LLM)访问 Bash 和文件系统的权限,并让它在一个循环中运行直到达成目标,它就能自主完成复杂的多步骤任务。

而这里有一个令人惊讶的发现:一个优秀的编程 Agent,本质上就是一个优秀的通用 Agent。

支撑 Claude Code 重构代码库的同一套架构,同样可以用来整理你的文件、管理阅读列表,或自动化你的工作流。通过 Claude Code SDK、Google adk-go,这种能力变得触手可及。你可以构建一种全新的应用:其功能不再是你写死的代码,而是你描述的结果(Outcome)——由一个装备了工具的 Agent,在一个循环中自主实现。

这开启了一个全新的领域:软件开始像 Claude Code 一样工作,但应用场景远超编程。这就是 AnthropicDan Shipper 联合发布的最新架构理念 —— Agent-native Architecture(智能体原生架构)

本文将深入拆解这份文档中的核心原则与架构模式,带你领略这一前沿范式。


核心原则

要构建 Agent-native 应用,我们需要遵循以下 5 大核心原则:

平权 (Parity)

原则: 用户通过 UI 能做的任何事,Agent 必须能通过工具(Tools)完成。

这是基石。如果没有平权,其他一切都无从谈起。你必须确保 Agent 拥有一套完整的工具集,能够覆盖 UI 的所有能力。

随机挑选一个 UI 动作。Agent 能完成吗?这是验证这一原则的测试标准!

颗粒度 (Granularity)

原则: 工具应该是原子级(Atomic)的原语。功能特性(Features)是由 Agent 在循环中通过组合工具达成的结果。

工具是基本能力。功能特性是 Prompt 描述的一个结果,由 Agent 动态组合工具来实现。

要改变软件的行为,你是通过修改 Prompt,还是重构代码?如果是前者,说明颗粒度对了。

可组合性 (Composability)

原则: 拥有了原子工具和平权,你可以仅通过编写新 Prompt 来创造新功能。

比如:想要一个“每周回顾”功能?这只是一个 Prompt:

“检查本周修改过的文件。总结关键变更。基于未完成项和截止日期,建议下周的三个优先级事项。”

Agent 会自动调用 list_files、read_file 并结合自身判断力。你描述结果,Agent 负责循环执行。

涌现能力 (Emergent Capability)

原则: Agent 可以完成你从未显式设计过的任务。

这是 Agent-native 的飞轮效应:

  1. 你构建原子工具和平权能力。
  2. 用户提出了你未预料到的需求。
  3. Agent 组合工具完成了任务(或失败,揭示了工具缺口)。
  4. 你观察模式,添加领域工具或专用 Prompt 来优化。
  5. 重复上述过程。

然后,通过观察你的应用能处理领域内的开放式请求,来验证Agent是否具备这种涌现能力。

随时间进化 (Improvement over time)

原则: Agent-native 应用通过积累上下文和 Prompt 优化而变得更好,而无需重新发版。

与传统软件不同,Agent-native的应用可以通过以下方式自我进化:

  • 积累上下文: 状态通过上下文文件(Context Files)跨会话持久化。
  • 开发者级优化: 你推送更新的 Prompt,所有用户受益。
  • 用户级定制: 用户修改 Prompt 以适应自己的工作流。

实践中的原则

有了原则,我们要如何落地?以下是具体的工程实践指南。

解决“平权”问题

想象一个笔记 App。用户说:“总结我关于这次会议的笔记,并标记为紧急。”

如果 UI 能做,但 Agent 做不到,Agent 就会卡住。

修正方案: 建立一张能力映射表(Capability Map)。

每当添加一个 UI 功能时,都要问:Agent 能达成这个结果吗?如果不能,添加相应的原语工具。将这作为一条工程纪律遵守和贯彻!

解决“颗粒度”问题:原子化 vs 捆绑逻辑

要知道:Agent 追求的是通过判断力(Judgment)达成结果,而不是执行死板的序列。

  • ❌ 错误做法(捆绑逻辑):
    • 工具:classify_and_organize_files(files)
    • 问题:决策逻辑是你写死的代码(充斥着if、else等)。要改变行为,必须重构代码。灵活性差。
  • ✅ 正确做法(原子化):
    • 工具:read_file, write_file, move_file, bash
    • Prompt:“整理下载文件夹…”
    • 优势:Agent 负责决策并组合工具完成。要改变行为,只需修改 Prompt。Agent 拥有了更强地灵活性。

演进路线:从原语到领域工具

一开始,只提供纯粹的原语(Bash, 文件操作)。这能证明架构可行,并揭示 Agent 真正需要什么。

当模式涌现后,有意识地添加领域工具(Domain Tools)

  1. 词汇表 (Vocabulary): create_note 工具教会了 Agent 你的系统中“笔记”是什么。
  2. 护栏 (Guardrails): 某些操作需要验证,不应完全交给 Agent 判断。
  3. 效率 (Efficiency): 将常用操作打包,提升速度和降低成本。

领域工具的规则: 它们应该代表用户视角的一个概念性动作。它们可以包含机械验证,但不要包含“做什么”或“是否做”的判断——这属于 Prompt。同时,默认保持原语可用,不要为了这种封装而关闭底层权限,除非有安全理由。

升级成为代码 (Graduating to Code)

有些操作因为性能或可靠性原因,需要从“Agent 编排”升级成为“优化代码”。

  1. 阶段 1: Agent 在循环中使用原语(灵活,验证概念)。
  2. 阶段 2: 添加领域工具(更快,但仍由 Agent 编排)。
  3. 阶段 3: 针对热点路径,用优化代码实现(极快,确定性)。

注意: 即使升级为代码,操作时,Agent 仍应保留直接触发该代码的能力,并保留回退到原语的能力以处理边缘情况。


架构模式:文件即通用接口

为什么 Claude Code 如此依赖文件系统?因为 Bash + Filesystem 是最经受考验的 Agent 接口。

  • 已知的: Agent 天生懂 cat, grep, mv。
  • 可检查的: 用户能直接看到、编辑、移动文件。没有黑盒。
  • 可移植的: 导出和备份极其简单。
  • 自文档化: /projects/acme/notes/ 这种路径本身就携带了语义,比 SELECT * FROM notes WHERE id=123 更容易让 Agent 理解。

实体作用域目录 (Entity-scoped directories)

这是一种推荐的文件结构模式:

{entity_type}/{entity_id}/
├── primary content  (主内容)
├── metadata         (元数据)
└── related materials (相关素材)

例如:Research/books/{bookId}/ 包含全文、笔记、来源和 Agent 日志。

context.md 模式

Agent 需要知道它在处理什么。系统 Prompt 应该注入以下内容:

  • 可用资源:
    “`markdown
    ## Available Data

    • 12 notes in /notes
    • 3 active projects in /projects
    • Preferences at /preferences.md
      “`
  • 能力 (Capabilities): 描述它能做什么(创建、搜索、整理)。
  • 最近活动 (Recent Activity): 用户刚刚做了什么,Agent 上一步做了什么。

文件 vs 数据库

  • 用文件: 用户需要读写的内容、配置、Agent 生成的内容、大文本。
  • 用数据库: 高频结构化数据、复杂查询、临时状态(Session/Cache)、强关系数据。

冲突处理: 建议采用 Shared Workspace 模式。Agent 和用户在同一个数据空间工作,不搞沙盒。这需要处理并发写入(如 Last-write-wins 或文件锁)。


成功标准与反模式

在构建 Agent-native 应用时,我们经常会不自觉地滑回传统的软件工程思维。以下是具体的反模式对照表

Agent 作为路由器 (Agent as Router)

  • 反模式: Agent 的唯一工作就是分析用户意图,然后调用一个写死的 run_workflow() 函数。
  • 问题: 你把 Agent 的智能降级成了 if/else。如果用户需求稍微偏离你的预设(例如:“这次运行工作流但跳过最后一步”),系统就会崩溃。
  • 正确姿势: Agent 应该负责执行,而不仅仅是路由。它应该拥有组成该工作流的原子工具,并根据情况决定调用顺序。

“请求/响应”思维 (Request/Response Thinking)

  • 反模式: 认为 Agent 就像一个 API:给一个输入,它吐出一个输出。
  • 问题: 这完全丢失了 Master Loop(Agent主循环) 的价值。真正的 Agent 是追求结果(Outcome)的。它可能会尝试、失败、分析错误、再尝试,直到结果达成。
  • 正确姿势: 给 Agent 一个目标,让它在一个循环中运行,并在过程中处理意外情况。

防御性工具设计 (Defensive Tool Design)

  • 反模式: 你因为习惯了防御性编程,所以把工具的输入限制得很死(例如:严格的 Enums,层层校验)。
  • 问题: 这虽然安全,但也扼杀了涌现能力。Agent 无法用你没想到的方式使用工具。
  • 正确姿势: 默认保持工具开放。除非有明确的安全风险(如删库),否则允许 Agent 传入任何它认为合理的参数。

工作流形状的工具 (Workflow-shaped Tools)

  • 反模式: 创建一个名为 analyze_and_organize_files() 的工具。
  • 问题: 你把“判断逻辑”捆绑进了工具里。如果要改变组织方式,必须重写代码。
  • 正确姿势: 拆解为 read、analyze、move。让 Agent 在运行时决定如何组织。

“幸福路径”思维 (Happy Path in Code)

  • 反模式: 在代码里写死了所有边缘情况的处理逻辑(if error_code == 500: retry)。
  • 问题: 如果代码处理了所有边缘情况,Agent 就只是一个无脑调用者。
  • 正确姿势: Agent-native 架构允许 Agent 用判断力处理边缘情况。如果遇到 500 错误,Agent 可以决定是重试、还是检查网络、还是通知用户。

成功的终极测试 (The Ultimate Test)

描述一个你的应用领域内的结果,但针对一个你从未专门开发过的功能。

看看Agent 能否通过在一个循环中运行,自主搞定它?如果能,你构建的就是一个真正的 Agent-native 应用。

小结:把判断力还给 Agent

当我们谈论 Agent-native 时,我们到底在谈论什么?

其实,这是一场关于“控制权移交”的变革。在传统的软件工程中,程序员试图用代码穷尽所有的逻辑分支,我们追求的是确定性。但在 Agent-native 的世界里,我们学会了放手

我们不再编写死板的“步骤”,而是提供灵活的“原语”。我们把“如何做(How)”的判断力,从代码中剥离出来,归还给了 Agent。

  • 平权 让 Agent 拥有了和人一样的行动力。
  • 颗粒度 赋予了 Agent 自由组合的能力。
  • 涌现 让我们看到了软件在设计之外的可能性。

这并不意味着代码不重要了,而是代码的角色变了——从“指挥官”变成了“军火库”。你负责提供精良的武器(工具),而 Agent 负责在前线根据战况(上下文)灵活作战。

这,才是 AI 时代软件架构的终极形态。

资料链接:https://every.to/guides/agent-native


你的 Agent 构想

Agent-native 架构为我们打开了一扇通往无限可能的大门。如果让你用这种架构重新设计你最熟悉的一个应用(比如待办清单、邮件客户端),你会赋予它哪些以前做不到的“涌现能力”?

欢迎在评论区分享你的脑洞! 让我们一起畅想软件的未来形态。

如果这篇文章颠覆了你对软件架构的认知,别忘了点个【赞】和【在看】,并转发给你的产品经理和架构师朋友!


体验最成功的Agent-native应用

Agent-native 不仅仅是一种架构,更是一种全新的开发体验。而目前市面上最成熟、最顶级的 Agent-native 实践,就是 Claude Code 本身。

想要真正理解什么是“原子化工具”?什么是“循环中的判断力”?最好的方式不是空谈理论,而是亲自去体验一个优秀的 Agent 是如何工作的。

欢迎关注我的极客时间专栏《AI 原生开发工作流实战》。我们将深入 Claude Code 的实战场景,带你亲眼见证它如何利用这些架构原则,把一个模糊的需求变成完美运行的代码。

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

从入门到极致:VictoriaMetrics 教你写出最高效的 Go 代码

本文永久链接 – https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code

大家好,我是Tony Bai。

InfluxDB 转Rust 之后,VictoriaMetrics 迅速崛起,成为了 Go 生态中无可争议的第一时序数据库。凭借其惊人的写入性能、极低的内存占用以及对 Prometheus 生态的完美兼容,它赢得了大量Go开发者以及大厂的青睐。除了核心数据库,其家族还拥有 VictoriaLogsVictoriaTraces 等明星产品,它们共同构成了一个高性能的可观测性平台。

很多 Gopher 都好奇:为什么用同样的语言,VictoriaMetrics 能跑得这么快、省这么多内存?

答案就藏在它的源码里。VictoriaMetrics 的代码库堪称一本活着的“Go 高性能编程教科书”。从基础的工程规范,到极致的内存复用,再到对并发模型的精细控制,每一行代码都是对性能的极致追求。

今天,我们就来完整拆解 VictoriaMetrics 的核心编程模式,带你体验一场从入门到极致的 Go 性能进阶之旅。


入门——务实的工程基石

在追求极致性能之前,首先要保证代码是稳健且可维护的。VictoriaMetrics 在基础工程实践上,展现了极其实用主义的智慧。

日志系统的“自我保护” (Rate Limiting)

很多系统挂掉,不是因为 bug,而是因为错误引发的“日志风暴”耗尽了磁盘 I/O。VictoriaMetrics 教我们的第一课是:日志也需要限流

它不仅支持INFO/WARN/ERROR/FATAL/PANIC五级日志,以及默认支持 JSON 格式输出,便于结构化日志采集:

// lib/logger/logger.go
var (
    loggerLevel    = flag.String("loggerLevel", "INFO", "Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC")
    loggerFormat   = flag.String("loggerFormat", "default", "Format for logs. Possible values: default, json")
)

更引入了关键的限流参数,防止日志风暴导致磁盘 IO 过载:

// lib/logger/logger.go
var (
    // 启动参数控制日志级别和限流阈值
    errorsPerSecondLimit = flag.Int("loggerErrorsPerSecondLimit", 0, "Per-second limit on the number of ERROR messages...")
    warnsPerSecondLimit  = flag.Int("loggerWarnsPerSecondLimit", 0, Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit)
)

在输出日志时,根据日志限流配置,对ERROR和WARN级别日志进行限制:

func logMessage(level, msg string, skipframes int) {
    ... ...
    // rate limit ERROR and WARN log messages with given limit.
    if level == "ERROR" || level == "WARN" {
        limit := uint64(*errorsPerSecondLimit)
        if level == "WARN" {
            limit = uint64(*warnsPerSecondLimit)
        }
        ok, suppressMessage := logLimiter.needSuppress(location, limit)
        if ok {
            return
        }
        if len(suppressMessage) > 0 {
            msg = suppressMessage + msg
        }
    }
    ... ...

在你的高并发服务中,给 Error 日志加上限流开关。虽然可能丢失部分细节,但它能保护你的系统不被日志拖垮。

配置管理:Flag 的艺术

VictoriaMetrics 并未使用第三方的flag包,而是大量使用标准库 flag 包,但用得非常智能。它为每个配置项提供了清晰文档和合理默认值,并支持通过 lib/envflag 内部包从环境变量覆盖配置。这种设计既简单又符合云原生部署需求:

// lib/envflag/envflag.go
var (
    // -envflag.enable: 启用从环境变量读取标志
    enable = flag.Bool("envflag.enable", false, "Whether to enable reading flags from environment variables in addition to the command line. "+
        "Command line flag values have priority over values from environment vars. "+
        "Flags are read only from the command line if this flag isn't set. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#environment-variables for more details")
    // -envflag.prefix: 环境变量前缀
    prefix = flag.String("envflag.prefix", "", "Prefix for environment variables if -envflag.enable is set")
)

// Parse parses environment vars and command-line flags.
//
// Flags set via command-line override flags set via environment vars.
//
// This function must be called instead of flag.Parse() before using any flags in the program.
func Parse() {
    ParseFlagSet(flag.CommandLine, os.Args[1:])
    applySecretFlags()
}

模块化与克制的抽象

打开源码目录,你会发现

VictoriaMetrics 将功能拆分为独立的 lib 包,每个包职责单一:

  • lib/storage: 核心存储引擎
  • lib/mergeset: 合并索引
  • lib/encoding: 数据编码
  • lib/bytesutil: 字节工具函数
  • lib/workingsetcache: 工作集缓存

在VictoriaMetrics代码中,你很少能看到层层嵌套的接口或复杂的依赖注入框架。 这种结构既保持了模块化,又避免了过度抽象带来的性能损耗,

对于 CPU 密集型应用,函数调用的层级越少越好。简单、直接的代码不仅易于阅读,对编译器优化(如内联)也更友好。


进阶——内存管理的艺术

对于数据库而言,内存就是生命线。VictoriaMetrics 在内存管理上的造诣,是其高性能的核心秘诀之一。

sync.Pool 的高效对象复用模式

Go 的 GC 在处理海量小对象时会面临巨大压力。VictoriaMetrics 的策略是:能复用,绝不分配。 VictoriaMetrics 大量使用 sync.Pool 来复用对象,减少 GC 压力。它不仅复用简单的结构体,甚至复用复杂的切片对象,比如下面这段复用切片对象的代码。

// lib/encoding/int.go
var uint64sPool sync.Pool

// Uint64s holds an uint64 slice
type Uint64s struct {
    A []uint64
}

// GetUint64s returns an uint64 slice with the given size.
// The slice contents isn't initialized - it may contain garbage.
func GetUint64s(size int) *Uint64s {
    v := uint64sPool.Get()
    if v == nil {
        return &Uint64s{
            A: make([]uint64, size),
        }
    }
    is := v.(*Uint64s)
    // 关键技巧:复用底层数组,仅调整切片长度
    // 避免了重新 make([]uint64) 的开销
    is.A = slicesutil.SetLength(is.A, size)
    return is
}

// PutUint64s returns is to the pool.
func PutUint64s(is *Uint64s) {
    uint64sPool.Put(is)
}

这里用到了 slicesutil.SetLength,通过切片操作复用底层数组,避免了重新分配内存:

// lib/slicesutil/slicesutil.go

// SetLength sets len(a) to newLen and returns the result.
//
// It may allocate new slice if cap(a) is smaller than newLen.
func SetLength[T any](a []T, newLen int) []T {
    if n := newLen - cap(a); n > 0 {
        a = append(a[:cap(a)], make([]T, n)...)
    }
    return a[:newLen]
}

突破 sync.Pool 的限制:Channel 对象池

sync.Pool 虽好,但它有两个缺点:它是 per-CPU 的,且在 GC 时会被清空。对于极大的对象(如超过 64KB 的缓冲区),这可能导致内存使用量的不可控膨胀。

VictoriaMetrics 教你一招:用 Channel 当对象池,比 sync.Pool 更可控。

// lib/storage/inmemory_part.go

// inmemoryPart represents in-memory partition.
type inmemoryPart struct {
    ph partHeader

    timestampsData chunkedbuffer.Buffer
    valuesData     chunkedbuffer.Buffer
    indexData      chunkedbuffer.Buffer
    metaindexData  chunkedbuffer.Buffer

    creationTime uint64
}

// 容量严格限制为 CPU 核数,防止内存无限膨胀
// Use chan instead of sync.Pool in order to reduce memory usage on systems with big number of CPU cores,
// since sync.Pool maintains per-CPU pool of inmemoryPart objects.
//
// The inmemoryPart object size can exceed 64KB, so it is better to use chan instead of sync.Pool for reducing memory usage.
var mpPool = make(chan *inmemoryPart, cgroup.AvailableCPUs())

func getInmemoryPart() *inmemoryPart {
    select {
    case mp := <-mpPool: // 尝试从池中获取
        return mp
    default:
        return &inmemoryPart{} // 池空了,才新建
    }
}

func putInmemoryPart(mp *inmemoryPart) {
    mp.Reset()
    select {
    case mpPool <- mp: // 尝试归还
    default:
        // Drop mp in order to reduce memory usage.
        // 池满了,直接丢弃,等待 GC 回收
    }
}

VictoriaMetrics认为:当你需要严格控制大对象的总数量时,带缓冲的 Channel 是比 sync.Pool 更安全的选择。

切片复用的极致:[:0] 技巧

在处理数据流时,VictoriaMetrics 几乎从不通过 make 创建新切片,而是疯狂复用缓冲区。最常用的模式就是 buf = buf[:0],该模式清空切片但保留和重用底层数组,避免重新分配新切片(包括底层数组):

// lib/mergeset/encoding.go

func (ib *inmemoryBlock) updateCommonPrefixSorted() {
    items := ib.items
    if len(items) <= 1 {
        // There is no sense in duplicating a single item or zero items into commonPrefix,
        // since this only can increase blockHeader size without any benefits.
        ib.commonPrefix = ib.commonPrefix[:0]   // 重置切片长度为 0,但保留底层容量 (capacity)
        return
    }

    data := ib.data
    cp := items[0].Bytes(data)
    cpLen := commonPrefixLen(cp, items[len(items)-1].Bytes(data))
    cp = cp[:cpLen]
    ib.commonPrefix = append(ib.commonPrefix[:0], cp...) // append 操作会直接利用底层数组,无内存分配
}

智能的缓冲区分配策略

并不总是越大越好。VictoriaMetrics 实现了三种精细的缓冲区调整策略 (lib/bytesutil):

  1. ResizeWithCopyMayOverallocate:按 2 的幂次增长(减少未来扩容次数,空间换时间)。
  2. ResizeWithCopyNoOverallocate:精确分配(节省内存,时间换空间)。
  3. ResizeNoCopy…:扩容但不拷贝旧数据(用于完全覆盖写入场景,最快)。

过度分配可节省 CPU 但浪费内存;精确分配节省内存但可能频繁扩容,究竟使用哪种调整策略,需要根据实际情况权衡。


高级——并发与锁的智慧

面对高并发,如何让多核 CPU 跑满而不互相打架?

分片锁 (Sharding):化整为零

这是解决锁竞争的“银弹”。VictoriaMetrics 将大的数据结构拆分为多个分片,每个分片有独立的锁。

// lib/storage/partition.go

// The number of shards for rawRow entries per partition.
//
// Higher number of shards reduces CPU contention and increases the max bandwidth on multi-core systems.
// 1. 根据 CPU 核数决定分片数量
var rawRowsShardsPerPartition = cgroup.AvailableCPUs()

type rawRowsShards struct {
    flushDeadlineMs atomic.Int64

    shardIdx atomic.Uint32

    // Shards reduce lock contention when adding rows on multi-CPU systems.
    // 2. 创建一组分片,每个分片有独立的锁
    shards []rawRowsShard

    rowssToFlushLock sync.Mutex
    rowssToFlush     [][]rawRow
}

func (rrss *rawRowsShards) addRows(pt *partition, rows []rawRow) {
    shards := rrss.shards
    shardsLen := uint32(len(shards))
    for len(rows) > 0 {
        n := rrss.shardIdx.Add(1)
        idx := n % shardsLen
        tailRows, rowsToFlush := shards[idx].addRows(rows) // 在分片中添加row
        rrss.addRowsToFlush(pt, rowsToFlush)
        rows = tailRows
    }
}

func (rrs *rawRowsShard) addRows(rows []rawRow) ([]rawRow, []rawRow) {
    var rowsToFlush []rawRow

    rrs.mu.Lock() // 只锁定这一个分片,其他分片仍可并发写入
    if cap(rrs.rows) == 0 {
        rrs.rows = newRawRows()
    }
    if len(rrs.rows) == 0 {
        rrs.updateFlushDeadline()
    }
    n := copy(rrs.rows[len(rrs.rows):cap(rrs.rows)], rows)
    rrs.rows = rrs.rows[:len(rrs.rows)+n]
    rows = rows[n:]
    if len(rows) > 0 {
        rowsToFlush = rrs.rows
        rrs.rows = newRawRows()
        rrs.updateFlushDeadline()
        n = copy(rrs.rows[:cap(rrs.rows)], rows)
        rrs.rows = rrs.rows[:n]
        rows = rows[n:]
    }
    rrs.mu.Unlock() // 解除分片锁

    return rows, rowsToFlush
}

原子操作:无锁编程

对于简单的计数器和状态标志操作这种简单逻辑,VictoriaMetrics 大量使用 atomic 包替代 Mutex。在 Bloom Filter (lib/bloomfilter/filter.go) 中,它更是使用 atomic.LoadUint64 和 atomic.CompareAndSwapUint64 (CAS) 来实现无锁并发位设置,性能比互斥锁快 10-100 倍。

// lib/bloomfilter/filter.go
func (f *filter) Has(h uint64) bool {
    bits := f.bits
    maxBits := uint64(len(bits)) * 64
    bp := (*[8]byte)(unsafe.Pointer(&h))
    b := bp[:]
    for i := 0; i < hashesCount; i++ {
        hi := xxhash.Sum64(b)
        h++
        idx := hi % maxBits
        i := idx / 64
        j := idx % 64
        mask := uint64(1) << j
        w := atomic.LoadUint64(&bits[i])
        if (w & mask) == 0 {
            return false
        }
    }
    return true
}

func (f *filter) Add(h uint64) bool {
    bits := f.bits
    maxBits := uint64(len(bits)) * 64
    bp := (*[8]byte)(unsafe.Pointer(&h))
    b := bp[:]
    isNew := false
    for i := 0; i < hashesCount; i++ {
        hi := xxhash.Sum64(b)
        h++
        idx := hi % maxBits
        i := idx / 64
        j := idx % 64
        mask := uint64(1) << j
        w := atomic.LoadUint64(&bits[i])
        for (w & mask) == 0 {
            wNew := w | mask
            // The wNew != w most of the time, so there is no need in using atomic.LoadUint64
            // in front of atomic.CompareAndSwapUint64 in order to try avoiding slow inter-CPU synchronization.
            if atomic.CompareAndSwapUint64(&bits[i], w, wNew) {
                isNew = true
                break
            }
            w = atomic.LoadUint64(&bits[i])
        }
    }
    return isNew
}

本地化 Worker Pool:消除 CPU 间通信

通用的 Worker Pool 有一个全局任务队列,这会导致多个 CPU 核心竞争同一个锁,且任务在不同核心间切换会带来缓存失效。

VictoriaMetrics 实现了一种本地化优先的 Worker Pool:每个 Worker 优先处理分配给自己的任务(通过独立的 Channel),只有在空闲时才去“帮助”其他 Worker。这种设计极大提升了多核系统的可扩展性。

// app/vmselect/netstorage/netstorage.go

// MaxWorkers returns the maximum number of concurrent goroutines, which can be used by RunParallel()
func MaxWorkers() int {
    n := *maxWorkersPerQuery
    if n <= 0 {
        return defaultMaxWorkersPerQuery
    }
    if n > gomaxprocs {
        // There is no sense in running more than gomaxprocs CPU-bound concurrent workers,
        // since this may worsen the query performance.
        n = gomaxprocs
    }
    return n
}

var gomaxprocs = cgroup.AvailableCPUs()

// 根据 CPU 核数动态决定 worker 数量(最多 32 个)
var defaultMaxWorkersPerQuery = func() int {
    // maxWorkersLimit is the maximum number of CPU cores, which can be used in parallel
    // for processing an average query, without significant impact on inter-CPU communications.
    const maxWorkersLimit = 32

    n := min(gomaxprocs, maxWorkersLimit)
    return n
}()

func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, workerID uint) error) (int, error) {
    tswsLen := len(rss.packedTimeseries)
    if tswsLen == 0 {
        // Nothing to process
        return 0, nil
    }

    var mustStop atomic.Bool
    initTimeseriesWork := func(tsw *timeseriesWork, pts *packedTimeseries) {
        tsw.rss = rss
        tsw.pts = pts
        tsw.f = f
        tsw.mustStop = &mustStop
    }
    maxWorkers := MaxWorkers()
    if maxWorkers == 1 || tswsLen == 1 {
        // It is faster to process time series in the current goroutine.
        var tsw timeseriesWork
        tmpResult := getTmpResult()
        rowsProcessedTotal := 0
        var err error
        for i := range rss.packedTimeseries {
            initTimeseriesWork(&tsw, &rss.packedTimeseries[i])
            err = tsw.do(&tmpResult.rs, 0)
            rowsReadPerSeries.Update(float64(tsw.rowsProcessed))
            rowsProcessedTotal += tsw.rowsProcessed
            if err != nil {
                break
            }
        }
        putTmpResult(tmpResult)

        return rowsProcessedTotal, err
    }

    // Slow path - spin up multiple local workers for parallel data processing.
    // Do not use global workers pool, since it increases inter-CPU memory ping-pong,
    // which reduces the scalability on systems with many CPU cores.

    // Prepare the work for workers.
    tsws := make([]timeseriesWork, len(rss.packedTimeseries))
    for i := range rss.packedTimeseries {
        initTimeseriesWork(&tsws[i], &rss.packedTimeseries[i])
    }

    // Prepare worker channels.
    workers := min(len(tsws), maxWorkers)
    itemsPerWorker := (len(tsws) + workers - 1) / workers
    // 为每个 Worker 创建独立的 Channel
    workChs := make([]chan *timeseriesWork, workers)
    for i := range workChs {
        workChs[i] = make(chan *timeseriesWork, itemsPerWorker)
    }

    // Spread work among workers.
    for i := range tsws {
        idx := i % len(workChs)
        workChs[idx] <- &tsws[i]
    }
    // Mark worker channels as closed.
    for _, workCh := range workChs {
        close(workCh)
    }

    // Start workers and wait until they finish the work.
    var wg sync.WaitGroup
    for i := range workChs {
        wg.Add(1)
        qtChild := qt.NewChild("worker #%d", i)
        go func(workerID uint) {
            // Worker 优先处理自己 Channel 中的任务
            timeseriesWorker(qtChild, workChs, workerID)
            qtChild.Done()
            wg.Done()
        }(uint(i))
    }
    wg.Wait()

    // Collect results.
    var firstErr error
    rowsProcessedTotal := 0
    for i := range tsws {
        tsw := &tsws[i]
        if tsw.err != nil && firstErr == nil {
            // Return just the first error, since other errors are likely duplicate the first error.
            firstErr = tsw.err
        }
        rowsReadPerSeries.Update(float64(tsw.rowsProcessed))
        rowsProcessedTotal += tsw.rowsProcessed
    }
    return rowsProcessedTotal, firstErr
}

并发度控制:Channel 作为信号量进行限流

为了防止内存溢出,必须严格限制并发处理的数据块数量。VictoriaMetrics 使用带缓冲 Channel 作为信号量来实现限流。

// lib/mergeset/table.go

// Table represents mergeset table.
type Table struct {
    ... ...
    // inmemoryPartsLimitCh limits the number of inmemory parts to maxInmemoryParts
    // in order to prevent from data ingestion slowdown as described at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5212
    inmemoryPartsLimitCh chan struct{}
    ... ...
}

func (tb *Table) addToInmemoryParts(pw *partWrapper, isFinal bool) {
    // Wait until the number of in-memory parts goes below maxInmemoryParts.
    // This prevents from excess CPU usage during search in tb under high ingestion rate to tb.
    // See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5212
    select {
    case tb.inmemoryPartsLimitCh <- struct{}{}:
    default:
        tb.inmemoryPartsLimitReachedCount.Add(1)
        select {
        case tb.inmemoryPartsLimitCh <- struct{}{}: // 满则阻塞等待
        case <-tb.stopCh:
        }
    }
    ... ...
}

专家——黑魔法与算法优化

当常规手段用尽,VictoriaMetrics 开始使用一些“非常规”武器。

Unsafe 的零拷贝技巧

Go 的 string 和 []byte 转换通常涉及内存拷贝。在热点路径上,VictoriaMetrics 使用 unsafe 绕过。

// lib/bytesutil/bytesutil.go
// 零拷贝:[]byte -> string
func ToUnsafeString(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}

// 零拷贝:string -> []byte
func ToUnsafeBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

此外,它还使用 unsafe.Add 进行直接指针运算来获取子切片,以及直接将 uint64 转为字节数组指针进行哈希计算,这些都可以在热路径上减少了边界检查和内存分配。

警告:这是一把双刃剑。你必须确保原始数据在生命周期内有效且不可变,否则会导致严重的逻辑错误甚至 Panic。

汇编优化与算法选择

VictoriaMetrics 本身并不手写汇编,但它极其善于利用经过汇编优化的第三方库(如 xxhash, zstd)。

更重要的是,它针对时序数据特点,发明了 Nearest Delta 编码(最近邻 Delta 编码)。它不仅存储数值的“差值(delta)”,还通过位运算移除不必要的精度和末尾的零。

它还支持策略自适应,会智能判断数据类型(Gauge vs Counter),选择不同编码。甚至在压缩效果不佳时自动回退到存储原始数据,确保在 CPU 和存储空间之间取得最佳平衡。

内存布局优化:公共前缀提取

在索引存储中,有序数据的 Key 往往有很长的公共前缀。VictoriaMetrics 会自动提取首尾元素的公共前缀,只存储差异部分。这不仅减少了内存占用,更提高了 CPU 缓存的命中率。


小结:Gopher 的修行之路

通过完整剖析 VictoriaMetrics 的源码,我们看到了一条清晰的性能进阶之路:

  1. 入门:编写简单、直接、模块化的代码,利用 Flag 和日志限流构建稳健系统。
  2. 进阶:精通内存复用,灵活运用 sync.Pool 和 Channel 对象池,将 GC 压力降至最低。
  3. 高级:深刻理解并发,利用分片锁、原子操作和本地化队列,压榨多核 CPU 的极限。
  4. 极致:在热点路径上,敢于使用 unsafe 和自定义算法,通过对数据特征的深刻理解换取最后的性能提升。

性能优化没有黑魔法,只有对原理的深刻理解和对细节的极致打磨。 希望 VictoriaMetrics 的这些实战技巧,能帮助你在 Go 语言的修行之路上,更上一层楼。


你的性能优化“必杀技”

VictoriaMetrics 的代码确实让人叹为观止。在你的 Go 开发生涯中,有没有哪一个性能优化技巧(比如 sync.Pool 或 unsafe)让你印象最深刻,或者真的帮了大忙?

欢迎在评论区分享你的“优化故事”! 让我们一起挖掘更多 Go 语言的性能宝藏。

如果这篇文章让你对 Go 高性能编程有了新的领悟,别忘了点个【赞】和【在看】,并转发给你的团队,好代码值得被更多人看到!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com
这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats