Go Runtime
Go 运行时 Part 1 Runtime基础 01 Runtime是什么 Go runtime 是 Go 程序运行时依赖的一组基础设施。它不是 JVM 那种执行字节码的虚拟机,也不是一个需要单独安装和启动的外部进程。普通 Go 程序在编译链接之后,最终生成的是机器码可执行文件;runtime 会作为程序的一部分被链接进去,在程序启动、并发调度、内存分配、垃圾回收、栈管理、阻塞唤醒、网络轮询、定时器、panic/recover 等方面提供支持。 可以先把 Go 程序理解成两部分: TEXT用户代码 main.main 业务函数 package init 普通 goroutine 运行时 程序启动 goroutine 调度 栈增长 内存分配 GC channel/select timer netpoll syscall/signal panic/recover 这两部分最终在同一个进程里执行。用户代码看起来像是在直接调用函数、创建 goroutine、分配对象、读写 channel,但很多操作背后都会落到 runtime 的实现上。 例如下面这段代码: GOpackage main func main() { ch := make(chan int) go func() { ch <- 1 }() println(<-ch) } 表面上只有 make(chan int)、go func()、发送、接收几个语法动作;底层则至少涉及: ...
编译原理
编译原理:从源代码到可执行程序 高级语言中的变量、函数、结构体、接口、模块等概念更接近抽象语义;机器实际执行的是指令、寄存器操作、内存访问和跳转。编译器的核心任务是将高级语言程序转换为机器可执行的目标形式,并在这个过程中完成语法约束、类型约束、控制流约束、内存布局和目标平台约束的处理。 典型编译流程可以抽象为: TEXT源代码 -> 词法分析:字符流 -> Token 流 -> 语法分析:Token 流 -> AST -> 语义分析:AST -> 带类型、作用域、绑定关系的 AST -> 中间代码生成:AST -> IR -> 代码优化:IR -> 优化后的 IR -> 目标代码生成:IR -> 汇编 / 机器指令 -> 汇编与链接:目标文件 -> 可执行文件 这条流水线的关键不是“把文本翻译成另一段文本”,而是不断把程序转换为更适合下一阶段处理的结构化表示。Token 适合语法分析,AST 适合表达源代码结构,IR 适合数据流和控制流优化,机器指令适合在具体硬件上执行。 编译过程概览 以一段简单的 Go 代码为例: GOpackage main func add(a, b int) int { return a + b } 在编译器内部,它会经历类似下面的结构转换: ...
Serialization and Deserialization
Java 序列化与反序列化:原理、Jackson 实战与避坑指南 一、序列化与反序列化基础 1.1 定义 序列化(Serialization):将内存中的对象转换为可存储或可传输的数据格式(字节流、JSON、XML 等)的过程。 反序列化(Deserialization):将存储或传输的数据格式还原为内存对象的逆过程。 TEXT 序列化 Object ──────► byte[] / JSON / XML / Protobuf ... 反序列化 Object ◄────── byte[] / JSON / XML / Protobuf ... 1.2 核心目的 场景 说明 持久化 将对象状态保存到磁盘、数据库 网络传输 RPC、HTTP API、消息队列中传递对象 进程间通信 跨 JVM 数据交换 深拷贝 通过序列化/反序列化实现对象深复制 缓存 Redis、Memcached 等存储 Java 对象 1.3 常见序列化方案对比 方案 格式 可读性 性能 跨语言 典型场景 JDK Serializable 二进制 ✗ 低 ✗ 遗留系统 JSON (Jackson/Gson) 文本 ✓ 中 ✓ REST API、配置 Protobuf 二进制 ✗ 高 ✓ gRPC、高性能 RPC Kryo 二进制 ✗ 高 ✗ Spark、Flink 内部 Hessian 二进制 ✗ 中 ✓ Dubbo Avro 二进制 ✗ 高 ✓ Kafka、大数据 1.4 序列化的本质问题 序列化不仅仅是"转格式",需要解决以下核心问题: ...
Regex Guide
Regex 1. 字符类 (Character Classes) 语法 说明 等价于 . 匹配换行符外的任意单字符 [^\n\r] \d 匹配数字 [0-9] \D 匹配非数字 [^0-9] \w 匹配字母、数字或下划线 [A-Za-z0-9_] \W 匹配非单词字符 [^A-Za-z0-9_] \s 匹配空白符 [ \t\n\r\f\v] \S 匹配非空白符 [^ \t\n\r\f\v] [ABC] 匹配括号内任意单字符 - [^ABC] 匹配括号内字符外的任意单字符 - 2. 量词 (Quantifiers) 语法 说明 * 匹配 0 次或多次 + 匹配 1 次或多次 ? 匹配 0 次或 1 次 {n} 匹配确切 n 次 {n,} 匹配至少 n 次 {n,m} 匹配 n 到 m 次 注:默认贪婪匹配(匹配最大长度)。在量词后追加 ?(如 *?、+?)切换为懒惰匹配(匹配最小长度)。 ...
微服务稳定性兜底:深入解析 go-kratos/aegis 的核心设计
微服务稳定性兜底:深入解析 go-kratos/aegis 的核心设计 在高并发微服务体系中,单点故障、流量洪峰、缓存热点都可能引发雪崩。aegis 是 Kratos 框架中的稳定性组件库,集成了多种常见的保护机制,以较低开销提供多层防护。本文围绕其核心模块展开,分析背后的算法原理与工程权衡。 一、项目全景 TEXTgo-kratos/aegis ├── ratelimit/bbr # 自适应限流(BBR 算法) ├── circuitbreaker/sre # 熔断器(Google SRE 自适应节流) ├── topk/heavykeeper # Top-K 热点 key 检测(HeavyKeeper 算法) ├── hotkey # 热点 key 自动本地缓存 ├── subset # 一致性哈希子集路由 └── internal ├── window # 滑动窗口(环形数组) ├── consistent # 一致性哈希 ├── minheap # 最小堆 └── cpu # CPU 使用率采样 核心哲学:用概率和统计代替精确计数,以极低的内存和计算开销换取足够准确的系统保护。 二、BBR 自适应限流 2.1 问题背景 传统限流通常是硬编码一个 QPS 上限(如"最多 1000 RPS")。问题在于: ...
复制滞后与多端同步
前言 主从复制可以显著提升系统的读能力,但复制滞后会直接破坏多端同步体验。 本文聚焦三个最常见的一致性问题:读自己写、单调读、前缀一致读,以及在即时通讯场景中的对应治理手段。 读自己的写 读自己写失效:客户端提交了写入,但随后的读取请求被路由到了尚未完成同步的从节点,导致客户端读不到自己刚刚写入的数据。 用户读自己写数据,强制走主节点。如果大部分数据都修改,会给主库造成巨大压力 用户记录最近更新时间戳,可以是逻辑时间和系统时钟(时钟不可靠)。多设备不适用,并不知道记录的时间戳。 单调读 单调读失效:用户的多次读取请求打到了同步进度不一致的多个副本上,导致用户先看到了较新的数据,随后又看到了较旧的数据,出现了“时光倒流”。 始终从同一副本读取,例如基于用户id哈希。但是如果副本失效,必须重新路由到另一个副本 前缀一致读 前缀/因果一致性失效:具有因果关系(先后顺序)的写入,由于底层分布式组件的处理速率不一致,导致在第三方的视角中,事件发生的顺序被颠倒。 将具有因果顺序的都交由一个分区处理。效率低 即时通讯场景下的主从滞后问题 读自己写失效(Read-Your-Writes Anomaly) 多端同步丢失: 用户在手机端发了一条消息,成功写入服务端主库。用户立刻打开电脑端(PC 版)查看,PC 端的拉取请求恰好打到了一个由于网络抖动而延迟了 500 毫秒的 MySQL 从库。结果 PC 端界面上一片空白,用户以为消息没发出去。 解决方案:纯内存 Push 模型跑赢物理复制 放弃让其他在线端去“查”数据库的传统做法。 手机端消息先进入 Kafka,由消息处理服务按会话维度分配 Seq 并完成落库。 后端的 msg_transfer 服务消费 Kafka,定位到 PC 端的 WebSocket 长连接,直接将消息从内存推(Push)过去。 核心逻辑: 内存与网络的流转速度远快于磁盘 IO 和数据库 Binlog 复制。客户端直接利用 Push 过来的数据渲染上屏,从物理架构上彻底绕开了从库延迟的陷阱。 单调读失效(Monotonic Reads Anomaly) 漫游消息凭空消失: 用户断网重连,触发历史消息拉取(Pull)。第一次拉取,网关路由到了延迟极低的从库 A,用户看到了最新的 10 条消息。用户立刻下拉刷新,第二次请求被路由到了卡顿的从库 B,从库 B 还没这 10 条消息。于是,用户屏幕上刚刚还在的 10 条最新聊天记录突然集体消失。 解决方案:客户端主导的严格 Seq 游标 在 OpenIM 中,拉取漫游消息的“游标控制权”在客户端手里,而不是服务端盲查。 客户端本地 SQLite 记录着自己当前看到的最后一条连续消息的 MaxLocalSeq(如 Seq=100)。 发起 Pull 请求时,客户端携带极其明确的条件:“只拉取 Seq > 100 的增量消息”。 核心逻辑: 服务端接收到游标后,会和 Redis 中维护的 ServerMaxSeq 对比。如果查到的从库最新数据只有 Seq=95,服务端立刻判定该副本滞后,可以选择等待、报错或强制回源主库。这保证了客户端拉取的数据永远是向前递增的,彻底封杀时光倒流。 前缀/因果一致性失效(Causal Consistency Anomaly) 旁观者视角的逻辑错乱: 在百人群聊中,用户 A 问:“去不去吃饭?”,用户 B 看到后秒回:“去”。(A 绝对发生在 B 之前)。然而,A 和 B 的消息并发打入服务端,处理 B 的线程极快,处理 A 的线程卡顿,导致 B 的消息先同步到了部分从库。此时,旁观者 C 刷新群聊,竟然先看到了 B 说“去”,过了几秒才看到 A 问“去不去吃饭”。因果逻辑彻底崩塌。 解决方案:Kafka 分区串行化 + 会话内 Seq 发号 步骤 1(先入队): 消息以 ConversationID 为 Hash Key 投递到 Kafka,确保同一会话的消息进入同一个 Partition。 步骤 2(再发号): 消息处理服务按 Partition 顺序消费,并为该会话分配严格递增的 Seq,然后落库。 步骤 3(客户端连续性校验): 就算网络抖动导致 Seq=11 的消息先推给旁观者 C,客户端发现本地缺少 Seq=10 时,会先放入重排缓冲区,待 Pull 补齐后再按序展示。 核心逻辑: 先利用 Kafka 分区保证同会话处理顺序,再用会话内递增 Seq 做最终顺序锚点,最后由客户端状态机兜底连续性。 分布式架构设计:不与底层的物理不确定性(网络抖动、磁盘延迟)死磕,而是通过高层的应用逻辑(Seq、Push/Pull、分区 Hash)来建立确定的秩序 ...
LSM-Tree 原理与消息存储选型
1. 概述 LSM-Tree(Log-Structured Merge-Tree)是一种典型的写优化存储结构。它的核心思路是:不追求每次写入都直接落到最终有序位置,而是将写入拆成**“前台快速落地 + 后台异步整理”**两个阶段。 设计目标可以归纳为: 顺序化写入:前台写入以追加为主,避免随机写。 后台整理:异步 Compaction 控制查询路径复杂度。 可调节的权衡:在写吞吐、读延迟和空间占用之间提供调优空间。 2. 整体架构与核心组件 LSM-Tree 的运行依赖以下组件协同工作: 组件 位置 职责 WAL(Write-Ahead Log) 磁盘 写前日志,保证崩溃恢复 MemTable 内存 有序表,接收实时写入 Immutable MemTable 内存 写满后冻结,等待刷盘 SSTable 磁盘 有序、不可变的持久化文件 Compaction 后台任务 合并 SSTable,版本收敛与空间回收 数据在组件间的流转路径如下: flowchart LR W[Write Request] --> L[Append WAL] L --> M[Insert MemTable] M -->|Threshold reached| IM[Immutable MemTable] IM --> F[Flush to SSTable] F --> C[Compaction] C --> S[(Merged SSTables)] 3. 写路径 写入的关键原则是 “先确认可恢复,再确认可查询”。具体步骤: 追加 WAL——保证持久化,崩溃后可重放。 更新 MemTable——对外可查。 冻结 MemTable——达到阈值后转为 Immutable。 Flush 为 SSTable——后台将 Immutable MemTable 写入磁盘。 这条路径带来的关键特性: ...
Agent_practice
1. 客服agent 客户问题种类: 事实类问题、诊断类问题、模糊类问题、其他问题等 诊断类问题 解释问题 VS 解决问题 React Agent 从Agent的最开始,LLM先思考(Thought),然后触发动作(Action)和输入(Action Input),之后执行并观察工具执行结果(Observation),如果观察的效果不满足需求,会重回到思考阶段,最后生成最终回答(Final Answer) 选择API、反问、提取入参、执行API的准确度 “问题识别” -> “查询SOP工具” -> “反问客户、获取信息” -> “根据信息查询工具” -> “查询到工具执行结果” -> “根据执行结果来回复客户” -> “客户继续沟通” -> … -> “解决问题” TEXT用户提问 -> 判断意图 -> 模糊 -> 追问 -> 清楚 - 需要进入agent? -> api检索 -> api选择 -> 参数判断 -> 参数组装 -> 动作执行 -> 观察结果 -> 生成答案 -> 不足 -> 追问 问题与优化 在实际业务落地的情况下,需要考虑几个因素:执行效果、整体耗时、大模型生成成本、API调用成本等等诸多因素,如果效果好,但是耗时太久,或者大模型的生成成本(token、qps)、API的调用成本(qps)等都太高,那么也未必有好的用户体验。 多步调用耗时,将api做到开箱即用,支持多种传参方式,减少对用户的询问。 慢api作异步处理,前端显示卡片展示进度。 构造高质量的训练数据集来对模型进行Finetune训练,让大模型在选择API、反问、提取入参、执行API的准确度上都尽可能的高 2. why agent? agent: 让大模型“代理/模拟”「人」的行为,使用某些“工具/功能”来完成某些“任务”的能力。 ...
分布式 IM 网关路由架构:从集中式 Redis 到连接即路由
分布式 IM 网关路由架构分析与设计 单机 IM 网关阶段,用户与 WebSocket 连接关系维护在本地内存即可,例如使用 sync.Map 或 map[int64]*websocket.Conn + RWMutex。该模式实现简单、链路短、时延低。 当在线规模持续增长后,单机的 TCP 连接数、CPU 核数与网络带宽会触达上限,系统必须演进为多实例网关集群。此时核心问题变为: 用户分散在不同网关实例后,A 给 B 发消息时,如何在毫秒级准确定位 B 所在节点? 本文围绕该问题展开,重点覆盖接入层方案、路由演进路径、集中式 Redis 架构在高规模下的瓶颈,以及“连接即路由”的去中心化方向。 一、接入层:客户端先连到谁 在内部路由之前,需要先确定客户端第一跳接入方式。主流做法通常分为两类。 1. 前置负载均衡接入(Nginx/LVS/SLB) 在网关集群前加反向代理,客户端连接统一入口,再由代理层分发到后端网关。 特点:接入结构直观,客户端侧配置简单。 问题:代理层同样承担长连接状态,在高并发七层代理场景下会带来明显内存与成本压力。 2. Dispatch 服务引流(服务发现 + 直连) 客户端先请求无状态的 HTTP Dispatch 接口。Dispatch 根据网关健康度、负载指标或一致性 Hash 返回目标地址,例如 192.168.1.100:8080,然后客户端直接连目标网关。 优势:去掉代理层代持长连接成本,网络拓扑更扁平。 代价:对调度策略、健康探测和故障转移能力要求更高。 二、路由策略:A 如何找到 B 接入路径确定后,核心问题回到路由:A 发给 B,A 所在网关如何将消息准确投递到 B 所在网关。 演进一:全局广播(Broadcast) A 所在网关把消息广播给所有网关实例。每个实例检查本地连接表,命中就投递,未命中就丢弃。 优点:实现成本低,网关可保持近似无状态。 缺点:单聊被放大为 $N$ 份网络与计算开销($N$ 为网关节点数),规模增大后资源浪费显著。 演进二:Redis 集中式路由(Centralized Routing) 为避免广播风暴,可将 Redis 作为全局路由目录:用户登录时写入“用户-网关”绑定,发消息前查询 Redis 后再定向投递。 ...
用基础数学理解 nanoGPT 的语言建模
把语言建模问题写成条件概率:估计 $P(x_{t+1}\mid x_{\le t})$。 这篇文章更偏“看源码时能对得上号”的解释:以 nanoGPT(GPT-2 风格 Transformer)为参照(推荐对照这份带注释的实现:https://github.com/buvidk1234/nanoGPT ),只用必要的基础数学,把它为什么能做下一个 token 预测、训练时到底在优化什么,讲清楚。 约定一个更贴近实现的术语:模型直接处理的不是“单词”,而是 token id(通常由 BPE 等子词分词得到的整数序列)。训练后我们有时会观察到 token embedding 的几何相似性,但它不是先验语义规则,而是被目标函数与数据共同塑形的结果。 从最底层开始:如何在数值空间里表示 token,并让这种表示能够被后续计算反复使用? 如何让计算机表示 token? 可以借助一种数学工具——向量。 两个向量的夹角余弦可以衡量它们的相似度: $$\cos(\theta) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|}$$$\cos(\theta)$ 越接近 1,两个向量越相似;越接近 -1,越相反。 因此,我们可以用向量来表示 token;余弦相似度常被用作“观察/分析”这种表示的一个指标(例如看哪些 token 的向量更接近)。但需要强调: 余弦相似度不是“语义”的定义,只是向量空间中的一个几何量。 向量会在训练目标(下一个 token 预测)驱动下被塑形,最终是否呈现出你直觉里的“近义/反义”取决于数据、模型容量与训练过程。 这种表示方式称为 Token Embedding:每个 token 对应一个 $d$ 维向量 $\mathbf{e} \in \mathbb{R}^d$。 如何让计算机表示序列? 同一个 token 在不同上下文里可能承担不同功能。仅靠 Token Embedding 得到的是“上下文无关”的初始表示,还需要一种方式把位置信息与上下文交互纳入计算。 为此引入位置向量(Positional Embedding):假设序列长度为 $m$,为每个位置分配一个 $d$ 维向量 $\mathbf{p}_j$。在 nanoGPT / GPT-2 的常见实现里,这个位置向量通常是一个可学习的查表(learned positional embedding)。将两者相加得到模型输入: ...