mise: 按目录切换工具版本

解决什么问题 同一台机器上,不同项目要的工具版本常常不一样:一个项目锁 Node 24,另一个已经升到 Node 26;CI 用 Go 1.27,本机系统包里是别的版本。 常见的几种做法各有问题: 做法 问题 系统包管理器(pacman、Homebrew) 一个工具只有一个版本,全局生效;要 root 或会影响全系统;版本跟着发行版走,不跟项目走 每种语言一个版本管理器(nvm / fnm、pyenv、gvm、SDKMAN) 每个都要单独安装、单独配置 shell,切版本的方式各不相同 手动下载解压,改 PATH 换项目时要记得手动切,很容易用错版本 mise(读作 “meez”,原名 rtx)是一个统一的工具版本管理器,用 Rust 写的。它的核心功能是:在项目目录里写一个 mise.toml 声明要哪些工具、哪个版本,进入这个目录时自动用这些版本,离开就恢复。 一个工具管所有语言和 CLI。 它最早是 asdf 的替代品,兼容 asdf 的 .tool-versions 文件,但更快,也不需要为每个工具单独装插件。 三个功能 mise 现在做三件事,都写在同一个 mise.toml 里: [tools] go = "1.27.0" node = "24.21.0" task = "latest" golangci-lint = "2.14.0" [env] GOPROXY = "https://goproxy.cn,direct" [tasks.test] run = "go test ./..." [tools]:工具版本,这是主要功能。 [env]:进入目录时设置的环境变量,类似 direnv。 [tasks]:项目命令,mise run test 执行,类似 make 或 Task。 后两个是可选的,只用 [tools] 也完全可以。 ...

Created 20206-10-20 · Updated 21216-10-21 · 2 min · 376 words · -

ntfy: 用 HTTP 请求给手机发推送

ntfy 是什么 ntfy(读作 notify)是一个开源的发布/订阅推送服务。脚本用一个普通的 HTTP PUT/POST 把消息发到某个 topic,订阅了这个 topic 的手机 App、浏览器或其他脚本就会收到通知。 不需要注册账号,topic 第一次被用到时自动创建 服务端是一个 Go 程序,可以用官方的 ntfy.sh,也可以自己部署 客户端:Android(Google Play / F-Droid)、iOS(App Store)、网页(Web Push),都是开源的 典型用途是让无人值守的脚本在出结果时通知人:备份完成、CI 失败、定时任务出错、AI agent 跑完一个任务。 最简单的用法 手机装好 App,订阅一个 topic,比如 wiloon-enx-agent-7f3k9q,然后: # Plain message curl -d "Backup finished" ntfy.sh/wiloon-enx-agent-7f3k9q # Title, priority, tags (emoji shortcodes) and a link opened on tap curl \ -H "Title: enx-agent: #51 merged" \ -H "Priority: default" \ -H "Tags: white_check_mark" \ -H "Click: https://github.com/wiloon/enx/pull/203" \ -d "PR #203 merged, issue #51 closed" \ ntfy.sh/wiloon-enx-agent-7f3k9q 常用的请求头: ...

Created 17176-10-17 · Updated 17176-10-17 · 2 min · 406 words · -

Rate Limiting, Circuit Breaker and Degradation 限流、熔断与降级

限流、熔断与降级 限流 Rate Limiting 限流是从系统的流量入口考虑,从进入的流量上进行限制,达到保护系统的作用 是指需要限制并发/请求量的场景 (如秒杀等) ,通过限流保护服务免受雪崩之灾。 计数器 多少次每秒/每分/每天,根据作用范围的不同,计算器可以分为单机计数器即Local Counter (每个机器都有自己的计数器) 和全局计数器也就是分布式计数器即Global Counter (所有机器公有一个计数器阈值) 令牌桶 令牌桶算法是网络流量整形 (Traffic Shaping) 和速率限制 (Rate Limiting) 中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。 令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 令牌桶的两个参数、和其他限流算法的比较、适用场景和 Go 示例,见 Token Bucket: 令牌桶限流。 漏桶 漏桶算法思路很简单,水 (请求) 先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。 方法 优点 缺点 计数器 简单方便 存在一下子被刷完计数器的情况 令牌桶 可以处理瞬间突发的流量 ~ 漏桶 限制请求的平均速率 不能处理瞬间流量的情况,当流量把漏桶装满后,大部分的请求都会被丢弃掉 熔断 Circuit Breaker 主调熔断 主调熔断是熔断的主要场景,在微服务中是结合服务注册和发现组件来使用的。主调通过服务发现组件来获取被调的实例,如果主调的失败达到了一个阈值,服务发现组件会熔断被调的实例。 被调熔断 被调熔断,被调实例获取当前的实例负载情况,如果cpu/内存/io 等超过了一定的阈值,那么就会触发熔断,将抛弃进来的请求。此时熔断和限流比较像,一个是基于负载情况,一个是根据配置的阈值情况。 降级 降级,是从系统内部的平级服务或者业务的维度考虑,流量大了,可以干掉一些,保护其他功能正常使用。 降级也就是服务降级,当我们的服务器压力剧增为了保证核心功能的可用性 ,而选择性的降低一些功能的可用性,或者直接关闭该功能。这就是典型的丢车保帅了。当服务暂时不可用或者影响到核心流程时,需要待高峰或者问题解决后再打开。通过降级实现部分可用、有损服务。主流的降级策略有基于RT (响应时间) 、异常比率和异常数的降级。 就比如贴吧类型的网站,当服务器吃不消的时候,可以选择把发帖功能关闭,注册功能关闭,改密码,改头像这些都关了,为了确保登录和浏览帖子这种核心的功能。 降级的手段大致有这么三种 强一致变为最后一致,延迟服务 干掉一些次要功能 简化流程 每个服务都需要制定自己的降级策略,根据服务不同的优先级来设定降级方案。另外降级策略最好是由开关系统或者配置系统统一控制。一般而言都会建立一个独立的降级系统,可以灵活且批量的配置服务器的降级功能。当然也有用代码自动降级的,例如接口超时降级、失败重试多次降级等。具体失败几次,超时设置多久,由你们的业务等其他因素决定。开个小会,定个值,扔线上去看看情况。根据情况再调优。 熔断和限流都可以认为是降级的一种方式 https://xie.infoq.cn/article/2593d1a3b9e1e06cac6502c4f?utm_source=rss&utm_medium=article 维护记录 时间 修改内容 原因 2026-10-16 文件重命名为 rate-limiting-circuit-breaker-degradation.md;补 url、英文标题、lastmod、分类和标签;令牌桶一节链到新文章 Token Bucket;修正表格分隔行 文件名含中文;新增令牌桶专文

Created 16166-10-16 · Updated 16166-10-16 · 1 min · 79 words · -

分布式事务 2PC, 3PC, TCC, Saga

数据库事务 在讲述分布式事务的概念之前,我们先来回顾下事务相关的一些概念。 事务的基本概念: 就是一个程序执行单元,里面的操作要么全部执行成功,要么全部执行失败,不允许只成功一半另外一半执行失败的事情发生。例如一段事务代码做了两次数据库更新操作,那么这两次数据库操作要么全部执行成功,要么全部回滚。 事务的基本特性: 我们知道事务有4个非常重要的特性,即我们常说的 (ACID) 。 Atomicity (原子性) 事务是一个不可分割的整体,所有操作要么全做,要么全不做。一旦其中某个操作出错,整个事务回滚到开始前的状态,已执行的操作全部撤销。比如 A 向 B 转账,扣款和入账这两步必须同时成功或同时失败,不能只扣了 A 的钱而 B 没收到。 Consistency (一致性) 是说事务执行前后,数据必须满足预定义的约束和规则,从一个合法状态转换到另一个合法状态。比如 A 向 B 转账,无论事务是否成功,A 和 B 的账户总金额必须保持不变——这个"总额守恒"就是一致性约束。注意:A 扣了钱 B 没收到,描述的是原子性被破坏;一致性关注的是业务规则(约束)是否始终成立。 Isolation (隔离性) 多个并发事务之间相互隔离,不能互相干扰。并发事务操作同一份数据时,可能出现三类读异常: 脏读:事务 A 读取了事务 B 尚未提交的数据,若 B 随后回滚,A 读到的就是无效数据。解决方法:隔离级别提升到"读已提交"。 不可重复读:事务 A 在同一事务内两次读取同一行,期间事务 B 修改并提交了该行,导致两次结果不同。解决方法:隔离级别提升到"可重复读"。 幻读:事务 A 在同一事务内两次执行相同的范围查询,期间事务 B 插入或删除了符合条件的行并提交,导致两次结果集的行数不同——凭空多出或少了几行,如同"幻象"。注意:幻读的前提是 A 本身处于事务中,且处于"可重复读"隔离级别(该级别能保证已有行不变,但挡不住新插入的行)。解决方法:隔离级别提升到"串行化",或使用间隙锁(Gap Lock)。 四个隔离级别(读未提交、读已提交、可重复读、串行化)依次能解决脏读、不可重复读、幻读,但隔离级别越高并发性能越低。 Durability (持久性) 事务一旦提交,其对数据库的修改就会被永久保存,即使系统崩溃或重启也不会丢失。数据库通过 WAL(Write-Ahead Log,预写日志)等机制来保证持久性。 关于数据库事务的基本概念大家可以去网上搜一下,这里只是给大家回顾下事务的基本概念及特性,诸如事务并发问题、事务隔离级别等大家如有遗忘可以去回顾下 (tips: 面试经常会问到的问题哦) 。 分布式事务 以上内容我们回顾了下事务的基本概念,那么分布式事务又是个什么概念呢?它与数据库事务之间又有什么区别呢? 其实分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就需要满足事务的基本特性 (ACID) ,只是分布式事务相对于本地事务而言其表现形式有很大的不同。举个例子,在一个JVM进程中如果需要同时操作数据库的多条记录,而这些操作需要在一个事务中,那么我们可以通过数据库提供的事务机制 (一般是数据库锁) 来实现。 ...

Created 7076-09-07 · Updated 10106-10-10 · 3 min · 506 words · -

Git Hosting Platforms: GitHub, GitLab, Gitea, Forgejo, Codeberg 对比

先分清两层:Git 和平台 GitHub、Gitea、Forgejo、Codeberg 底层用的都是标准 Git。它们之间的区别不在版本控制,而在包在 Git 外面的那层平台: 版本控制层:Git 负责提交历史、分支、合并,服务器上存的是普通的 Git bare 仓库。本地的 git clone、git push、git pull 在哪个平台上都一样。 平台层:提供 Web 界面、用户和权限、Issue、Pull Request、Wiki、Release、CI 集成等。这些 Git 本身没有,是各平台自己实现的。 另外,这几个名字不是同一类东西: 名字 类型 说明 GitHub 托管服务(闭源) 微软旗下,2018 年被收购 GitLab 平台软件 + 托管服务 有开源社区版,可自建;也有 gitlab.com Gogs 平台软件(开源) Go 编写的轻量 Git 服务,Gitea 的前身 Gitea 平台软件(开源) 2016 年从 Gogs 分叉,可自建 Forgejo 平台软件(开源) 2022 年从 Gitea 分叉,可自建 Codeberg 托管服务(非营利) 运行 Forgejo 的公共实例,codeberg.org 分叉关系:Gogs → Gitea → Forgejo Gogs:早期的 Go 语言自建 Git 服务,单个二进制就能跑,资源占用小。但基本由一位维护者主导,合并社区贡献慢。 Gitea:2016 年社区从 Gogs 分叉出来,采用社区治理,功能迭代快,后来成了自建 Git 服务的主流选择之一。 Forgejo:2022 年 Gitea 的域名和商标被转到新成立的营利公司 Gitea Ltd,部分社区成员担心项目被商业公司控制,于是分叉出 Forgejo。Forgejo 由非营利组织 Codeberg e.V. 托管,一开始是 Gitea 的“软分叉”(持续同步上游),2024 年起变成硬分叉,不再跟随 Gitea 代码。 所以 Gitea 和 Forgejo 目前的界面和用法仍然很像,但功能细节已经开始分化。Forgejo 的一个重点方向是联邦化(ForgeFed 协议),目标是不同实例之间能互相关注、提 Issue、提 PR。 ...

Created 27276-09-27 · Updated 27276-09-27 · 2 min · 267 words · -

System Libraries: 跨平台底层库速查

包管理器升级时,经常会看到一些不认识的底层库(harfbuzz、giflib、little-cms2 等)。它们通常不是自己主动装的,而是被某个上层软件依赖,顺带装上。 这篇文章记录这些底层库是什么、做什么、各平台叫什么包名。库本身是跨平台的,macOS(Homebrew)、Linux(apt / pacman / dnf)上都有,因此不按平台拆分。 遇到不认识的包时,怎么在 macOS 上查依赖关系,见 Homebrew (brew)。 索引 库 功能 常见使用方 harfbuzz OpenType 文本整形(text shaping) Pango、Qt、Firefox、Chromium、LibreOffice、OpenJDK freetype 字体解析与字形光栅化 Linux 桌面、Android、OpenJDK、各类游戏引擎 giflib GIF 图片编解码 OpenJDK、ImageMagick libpng PNG 图片编解码 几乎所有图形软件 jpeg-turbo JPEG 编解码(libjpeg 的 SIMD 加速实现) OpenJDK、浏览器、ImageMagick little-cms2 色彩管理(ICC profile) OpenJDK、GIMP、Ghostscript 包名对照 各平台的包名不完全一致,Linux 发行版通常把运行时库和开发头文件拆成两个包,编译其他软件时需要装带 dev / devel 的那个。 库 Homebrew Debian / Ubuntu(开发包) Arch Fedora / RHEL(开发包) harfbuzz harfbuzz libharfbuzz-dev harfbuzz harfbuzz-devel freetype freetype libfreetype-dev freetype2 freetype-devel giflib giflib libgif-dev giflib giflib-devel libpng libpng libpng-dev libpng libpng-devel jpeg-turbo jpeg-turbo libjpeg-dev libjpeg-turbo libjpeg-turbo-devel little-cms2 little-cms2 liblcms2-dev lcms2 lcms2-devel Arch 和 Homebrew 没有 dev / devel 拆分,头文件和库在同一个包里。 ...

Created 22226-09-22 · Updated 22226-09-22 · 2 min · 251 words · -

MAU: Monthly Active Users 月活用户

MAU 是什么 MAU = Monthly Active Users,中文叫「月活跃用户数」,口语里就说「月活」。MAU、月活、月活跃用户是同一个指标的三种叫法,没有区别。 它衡量的是:在一个月的时间窗口里,有多少个不同的用户对产品做过一次「算数的」操作。 两个变量决定了这个数字: 「活跃」怎么算 —— 打开 App?登录?完成一次核心操作(发消息、查一个词、下一单)?每个产品自己定,口径不同数字差很多。 「一个月」是哪一个月 —— 见下面「统计口径」。 同一个用户这个月里操作 100 次,也只算 1 个 MAU(按用户去重)。 统计口径:滚动 30 天 vs 自然月 自然月:2 月 1 日 00:00 到 2 月最后一天,落在这个区间里活跃过的去重用户数。一个月出一个数。 滚动 30 天(trailing 30 days):从今天往前数 30 天,这个窗口里的去重活跃用户。每天都能出一个数,曲线更平滑,也不受「这个月有 28 天还是 31 天」影响。 分析工具(GA4、Mixpanel、Amplitude)和产品仪表盘多数用滚动窗口;财报、投资人材料多数用自然月。看到一个 MAU 数字,先问清楚是哪一种。 DAU / WAU / MAU 同一套逻辑,换时间窗口: 指标 窗口 主要用途 DAU 日活跃用户 看日常波动、发版影响、周末效应 WAU 周活跃用户 平滑掉「工作日 / 周末」噪声 MAU 月活跃用户 看整体规模、增长趋势、对外汇报 DAU ≤ WAU ≤ MAU:窗口越大,去重后能框进来的人越多。 ...

Created 4046-09-04 · Updated 4046-09-04 · 1 min · 186 words · -

Compiled vs Interpreted: 编译型、解释型与脚本语言

背景 教材里常把编程语言分成 编译型(compiled) 和 解释型(interpreted)。二者是一组相对概念,但现代语言大多落在光谱中间,不宜一刀切。 脚本语言(scripting language) 在日常说法里,往往就是解释型一侧的别名,或更窄一点:强调「缩短 edit-compile-link-run、以源码直接跑、做自动化 / 嵌入」的那一类解释型语言。下文先讲编译 / 解释,再单独说脚本。 两种执行路径 类型 典型流程 运行时依赖 编译型 源码 → 编译器 → 机器码 / 本地二进制 → 直接由 OS 加载执行 一般不需要语言自带的解释器;可内嵌 runtime(如 GC) 解释型 源码(或中间表示)由 解释器 边读边执行 必须安装对应解释器 / 运行时 对照: 编译型(AOT): 源码 ──compile──► 本地二进制 ──► 运行 解释型: 源码 ──► 解释器逐句执行 字节码 + VM: 源码 ──compile──► 字节码 ──► 解释 / JIT ──► 机器码 「预编译 / ahead-of-time(AOT)」说的是:运行前就编成可执行形态;日常口语里常与「编译型」混用。注意 C 里的「预编译」有时指 #include / #define 的预处理,那是编译流水线的一步,不是语言分类。 ...

Created 1016-08-01 · Updated 1016-08-01 · 2 min · 395 words · -

GitHub CODEOWNERS:自动分配 PR Reviewer

概述 CODEOWNERS 是 GitHub 的一个约定文件:声明仓库里哪些路径由哪些人/团队"负责"。当 PR 改动了某个路径下的文件,GitHub 会自动把对应的 owner 加进 PR 的 Reviewers 列表。配合分支保护规则里的 “Require review from Code Owners”,还可以强制要求这些 owner 必须 approve,PR 才能合并。 它解决的问题:团队变大之后,“这段代码改了应该找谁 review” 不再是靠口头约定或者人肉记忆,而是写进仓库、可版本管理、对所有协作者可见的规则。 文件位置 GitHub 按以下顺序查找,只会使用第一个找到的: 仓库根目录:CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS 文件名区分大小写,没有扩展名,也不支持在多个位置同时生效。 基本语法 语法风格类似 .gitignore:一行一个路径 pattern,后面跟一个或多个 owner。 # 默认 owner,匹配仓库里所有文件 * @wiloon # 某个目录下的文件由指定团队负责 /infra/ @starlabrys/engineering # 具体文件可以指定多个 owner,任意一个 approve 都算数 /docs/architecture.md @wiloon @some-teammate # 用邮箱也可以(必须是该用户在 GitHub 账号上验证过的邮箱) *.sql db-owner@example.com Owner 可以是: @username:单个 GitHub 用户 @org/team-slug:某个组织下的团队 email@example.com:用户在 GitHub 上验证过的邮箱 匹配规则:后面的 pattern 优先级更高 和 .gitignore 不同,.gitignore 靠 ! 取消忽略;CODEOWNERS 没有取消符号,而是文件里越靠后的匹配行优先级越高——如果一个文件同时匹配多条 pattern,以最后一条为准。 ...

Created 17176-07-17 · Updated 17176-07-17 · 2 min · 285 words · -

Scheduler Design Patterns, 调度器设计:抢占与协作

为什么会发生调度?因为 CPU 是有限的,而操作系统上的进程很多,所以操作系统需要平衡各个进程的运行时间。比如说有的进程运行时间已经很长了,已经占用了 CPU 很长时间了,这个时候操作系统要公平,就会换下一个需要运行的进程。 抢占 vs 协作 从调度机制上来讲,调度可以分为抢占式和协作式。 非抢占,协作 非抢占方式是指一旦将调度资源(如 CPU)分配给某任务后,便让该任务一直执行,直到该任务完成或阻塞或主动让出 CPU 控制权,非抢占调度又称为协作式调度,它实现简单,并且对共享资源的访问也更安全(如允许使用不可重入函数)。非抢占算法常见的如 FIFO、STCF(Short Time to Complete First)等。 抢占 抢占方式则允许调度程序根据某种规则,剥夺当前进程的调度资源,将其分配给其它进程。常见的抢占策略有: 基于时间片 给每个进程分配时间片,当时间片用完后,则停止该进程重新调度下一个进程,这种均分 CPU 的算法,又叫轮转调度算法。 基于优先级 给每个进程分配一个优先级,一旦出现一个优先级更高的进程,则停止当前进程并切换到该高优先级进程,这种调度算法又叫优先级抢占。轮转调度和优先级抢占结合,即相同优先级的进程使用轮转调度,如果遇到更高优先级的进程,则可抢占 CPU,现代 OS 如 Linux 通常都使用这种混合调度策略(Linux 具体的调度器体系见 Linux 进程调度)。 基于优先级抢占容易出现优先级反转的问题:优先级低的任务持有一个被优先级高的任务所需要的共享资源,这种情况下,优先级低的任务有资源而得不到 CPU,优先级高的任务有 CPU 而得不到资源,从而阻塞(导致其它中优先级的任务获得执行)或者忙等(可能永远无法获得资源)。 解决优先级反转的方案: 给临界区一个高优先级,所有进入该临界区的任务将获得该高优先级,避免其被随意抢占; 当高优先级任务在等待低优先级进程持有资源时,低优先级进程将暂时获得高优先级进程的优先级,VxWorks 采用的方式; 禁止中断,也就是在临界区不可被抢占,Linux 采用的就是这种方式,在 thread_info.preempt_count 记录每个进程当前持锁计数; 另外,调度器是不能在进程指令流的任意一点执行打断的,因为进程可能此时正在做任何事情,如系统调用、死循环、锁操作等,要实现任意状态的可抢占性代价是很大的,需要 OS 和 App 的通力配合,特别是在涉及内核态的时候,目前所有 OS 都不能在进程执行的任意点进行抢占,只是说不断让抢占区更大、抢占点尽可能地密集。 实时 vs 非实时 从系统需求或用户角度而言,调度系统可以分为实时系统和非实时系统。 在实时操作系统中,系统必须在特定的时间内完成指定的应用。实时通常分为软实时(soft real-time)和硬实时(hard real-time),硬实时是指系统要有确定的最坏情况下的服务时间,即对于事件的响应时间截止期限无论如何都必须得到满足,通常应用在军工航天领域;而软实时只提供统计意义上的实时,比如应用要求在 95% 的情况下都会确保在规定的时间内执行某个任务,而不一定要求 100%。实时系统通常采用抢占调度,实现一个硬实时系统的代价是很高的,要做到进程可以在任意时刻被抢占,在现代 OS 上来讲基本是不可能的,因为 OS 的很多系统调用都是不可被打断的,并且很多操作具备时间的随机性,比如 CPU Cache Miss、Page Fault、CPU 分支预测器等等。 ...

Created 27276-04-27 · Updated 14146-07-14 · 2 min · 387 words · -

GitHub Personal Access Token:Classic vs Fine-grained

GitHub 的 Personal Access Token(PAT)有两套互相独立、并列的机制:Classic 和 Fine-grained。两者各自有独立的设置页面和独立的 token 值,互不共享权限、互不影响——改了一个的 scope,对另一个没有任何作用。 对比 Classic token (ghp_...) Fine-grained token (github_pat_...) 权限粒度 粗粒度,按 scope 打勾(repo、admin:org、workflow …),一个 scope 覆盖你有权限的所有仓库/组织 细粒度,创建时指定 Resource owner(个人账号或某个具体组织),再选具体仓库(全部/指定几个),每个仓库单独授予 Contents / Administration / Issues 等权限 作用范围 只要本人对某组织/仓库有权限,token 默认就能操作(除非组织手动 Restrict) 天生绑死在创建时选的 Resource owner,只能代表操作那一个账号或组织下的资源 安全性 权限范围大,一旦泄露风险高 权限可以缩得很小,泄露影响面小,GitHub 官方推荐新场景一律用这个 过期时间 早期可选"永不过期" 设计上引导必须设置过期时间 组织侧控制 只有"全允许 / 全禁止"的粗暴开关(组织 Settings → Personal access tokens → Tokens (classic)) 组织可以看到每一个申请访问本组织资源的 fine-grained token,逐个 Approve / Deny / 撤销,可审计 Fine-grained 要解决的问题 Classic token 加 scope 解决不了几个结构性问题,这也是 GitHub 推出 fine-grained token 的原因: ...

Created 13136-07-13 · Updated 13136-07-13 · 1 min · 186 words · -

Make Illegal States Unrepresentable:让非法状态不可表达

“Make illegal states unrepresentable”(让非法状态不可表达)是一句常被引用的设计口号,出自 Yaron Minsky(Jane Street)2011 年的博客文章 Effective ML Revisited,文中专门有一节就叫「Make illegal states unrepresentable」,用一个 OCaml 的 connection_state 类型做 before/after 对比。核心主张是:与其允许构造出错误的状态、再靠运行时检查去堵漏洞,不如设计数据结构本身,让错误状态压根编译不过、或者根本没有对应的值可以构造。 它要解决的问题 一个常见的反模式:用一个共享结构 + 若干标志位/可空字段,去表达"实际上互斥的几种状态"。 // 反例:一个类硬塞下多种状态,靠字段组合和约定表达"当前是哪种情况" public class Order { private String status; // "CREATED" / "PAID" / "SHIPPED" / "CANCELLED" private String paymentId; // 只有 status = PAID 之后才该有值 private String trackingNo; // 只有 status = SHIPPED 之后才该有值 private String cancelReason; // 只有 status = CANCELLED 才该有值 } 这段代码里,“没付款却有 trackingNo““状态是 CANCELLED 但 paymentId 和 cancelReason 同时非空"这些本不该存在的组合,在类型系统眼里都是合法值——编译器完全放行,只能靠人读代码、写注释、加运行时校验去维持"约定”。约定总有被忘记、被破坏的一天:少写一个 if、漏掉一次校验,非法组合就悄悄溜进了生产数据。 ...

Created 1016-07-01 · Updated 1016-07-01 · 2 min · 239 words · -

Fault Tolerance: 容错

Fault tolerance(容错)指的是:系统在部分组件出现故障时,仍能继续提供服务(哪怕是降级的服务),而不是直接整体瘫痪。它的出发点是承认「故障一定会发生」——硬件会坏、网络会断、进程会崩——所以与其指望不出错,不如让系统在出错时还能扛住。 容错与 fail-fast 的关系 这两个概念常被放在一起,但取向不同,而且并不矛盾: fail-fast(快速失败):一检测到不该出现的状态就立即、显眼地失败,把错误暴露在离源头最近的地方。 fault tolerance(容错):出错时尽量不让整个系统垮掉,通过冗余、隔离、降级等手段继续提供服务。 实际系统往往两者同时用、用在不同层面:单个组件内部对非法状态 fail-fast(尽早抛错、不带病运行),而系统整体对组件级故障容错(一个实例挂了还有别的顶上)。换句话说,「让局部快速失败」恰恰是「让整体可容错」的前提——故障被快速隔离出来,才不会扩散成全局问题。 常见的容错手段 容错不是单一技术,而是一组配合使用的做法: 冗余 / 副本(redundancy / replication):关键组件多部署几份,单点坏了还有备份。是容错的基础。 失效转移(failover):主组件故障时自动切到备用组件,对外尽量无感。 优雅降级(graceful degradation):非核心功能出问题时,关掉或简化它,保住核心链路,而不是整体报错。 重试与超时(retry / timeout):对偶发、可恢复的失败(网络抖动等)做有限重试,并用超时避免无限等待。 熔断与隔离(circuit breaker / bulkhead):下游故障时快速失败、暂时切断调用,防止故障沿调用链扩散、拖垮上游;用隔离把故障限制在局部。 拜占庭容错(BFT):更强的一类容错,能在部分节点发送错误甚至恶意信息时仍达成一致,多见于分布式共识、区块链。 微服务语境下的「服务容错」(熔断、隔离、限流、降级、超时)可参考 微服务 一文;Actor 模型的「任其崩溃(let it crash)」则是另一种思路——不在每处做防御式校验,而是让监督者在崩溃后重建。 容错 ≠ 高可用,但服务于它 容错是手段,高可用(high availability)是目标之一。通过容错让单点故障不影响整体服务,正是为了达成更高的可用性(更短的中断、更小的影响范围)。衡量容错切换效果常用的 RTO / RPO,见 failover。 边界:容错不是无限兜底 容错有成本:冗余要多花机器,复制要付出一致性 / 性能代价(参见 CAP 里 C 与 A 的取舍)。不是所有系统都需要、也不是所有故障都值得容错。 别把错误吞掉:容错是「继续提供服务」,不是「假装没出错」。被容错处理掉的故障仍要记录、告警、可观测,否则就成了「静默失败」——问题被盖住、没人知道,反而更糟。 区分该容忍的和该暴露的:对可预期、可恢复的外部故障(网络、下游不可用)做容错;对「程序员的错误 / 不该发生的内部状态」则应 fail-fast,越早暴露越好。 一句话:fault tolerance 关心「出错时系统还能不能用」,靠的是冗余、隔离、降级这些手段把故障的影响范围压下去;它和 fail-fast「尽早暴露错误」在不同层面配合,共同支撑系统的可用性。

Created 30306-06-30 · Updated 30306-06-30 · 1 min · 63 words · -

Failover / Failfast / Failback / Failsafe: 四个故障处理术语

这四个词长得像、也容易混,但讲的是「系统出故障时怎么应对」的不同侧面。下面把它们放在一起做个简单区分。 Failover:失效转移 / 故障转移 当正在工作的主组件出现异常时,把它的职责切换到备用组件,让对外服务尽量不受影响。要点是有主有备,主故障时备能顶上、并被提升为新的主。 典型例子是数据库主从:正在使用的主库挂了,就把备库切为主库继续提供服务。理想情况下用户完全感知不到这次切换。衡量切换效果常用两个指标: RTO(Recovery Time Objective):从故障到服务恢复需要多长时间。这段时间里服务通常是不可用的,用户感知到的是中断而不只是变慢,所以越接近 0 越好。 RPO(Recovery Point Objective):能恢复到故障前哪一刻,也就是最多会丢多少数据。它不是一个固定的量级,取决于数据保护方式:同步复制可做到 RPO ≈ 0(不丢数据,但成本高);异步复制一般是秒级到分钟级;定期快照 / 备份则是分钟级到小时级(每天备一次,最坏丢近一天)。换句话说,秒级和小时级都可能,取决于你用哪种复制 / 备份策略以及业务能容忍丢多少。 Failfast:快速失败 一旦检测到不该出现的状态,就立即、显眼地失败,把错误暴露在离源头最近的地方,而不是带着错误继续运行、之后在别处以奇怪的方式崩掉。它和 fault-tolerant(容错)是相对的取向。 一个常见例子是 Java 集合的 fail-fast 迭代器:当一个线程在遍历集合时,集合内容被其他线程改动,遍历线程会立刻抛出 ConcurrentModificationException,而不是返回不可预期的结果。 fail-fast 作为一条通用的软件工程原则(不止集合迭代器),单独展开在 Fail-Fast 快速失败 一文。 Failback:失效自动恢复 failover 之后的「回切」。当原来的主组件修复、重新可用后,把网络资源和服务从备用系统切回原主系统的过程。常见于需要临时维护主机、把流量先转到备用系统、维护完再切回的场景。 Failsafe:失效安全 即使发生故障,也尽量不造成伤害、或把伤害降到最低。一个形象的例子是红绿灯的冲突监测模块:当检测到错误或冲突的信号时,让整个路口进入闪烁警示模式,而不是把所有方向都显示为绿灯。 一句话区分 failover:主挂了,备顶上,继续服务。 failback:主修好了,再切回去。 failfast:出错就立刻停下报错,别硬撑。 failsafe:出错也要保证安全、把伤害降到最小。 failover / failback 关注的是「服务不中断」(可用性、容错),failfast / failsafe 关注的是「出错时的行为」——一个偏向尽早暴露,一个偏向兜底保护。 参考 失效转移、快速失败等概念(CSDN) https://blog.csdn.net/u013699827/article/details/73251649 维护记录 时间 修改内容 原因 2026-06-30 从 inbox/ 移到 pattern/;改写为自有表述、重排四个术语并加「一句话区分」;分类由 inbox 改为 development,标签更新为 fault-tolerance/high-availability/Pattern/remix/AI-assisted;新增到 fail-fast 的内链 原在未分类 inbox、内容为转载片段,整理归类并提升可读性 2026-06-30 RTO 说明改为强调「服务中断」而非延迟;RPO 补充其量级取决于复制 / 备份策略(同步 ≈0、异步秒/分钟级、备份分钟/小时级),不是固定小时级 澄清 RTO/RPO 含义,纠正「RPO 固定某量级」的误解

Created 6066-07-06 · Updated 30306-06-30 · 1 min · 83 words · -

Fail-Fast 快速失败:尽早暴露错误

Fail-fast(快速失败)是一种设计原则:当系统检测到一个不该发生的状态时,应当立即、显眼地失败,而不是带着错误继续运行下去。它的核心主张是——把问题暴露在离源头最近的地方,让错误尽早被发现,而不是拖到后面变成难以排查的怪现象,或者悄无声息地产出错误结果。 Jim Shore 在 Martin Fowler 主持的 IEEE Software「Design」专栏上写过一篇就叫《Fail Fast》的短文(2004 年 9/10 月号),把这个原则讲得很直接:带着错误「慢慢失败」(failing slowly)的程序会在后面以奇怪的方式出问题,而 fail-fast 的系统则在问题发生的当下就立即、显眼地失败(fail immediately and visibly)——看似更脆弱,实则更健壮,因为缺陷更容易被发现和修复。原文可读 Fowler 站点上的 Fail Fast (PDF),作者本人也在 jamesshore.com 上做了简介。 它要解决的问题 很多 bug 难查,不是因为错误本身复杂,而是因为错误发生的位置和错误暴露的位置离得很远。 设想一个函数收到了一个非法参数(比如本不该为 null 的值),它没有检查,而是默默接受、继续往下传。这个坏值可能流过好几层调用、被写进缓存、存进数据库,直到很久以后在某个完全不相干的地方抛出一个莫名其妙的异常。这时你拿到的堆栈和真正的出错点已经隔了十万八千里。 fail-fast 的思路是反过来:在边界上就把关,一发现状态不对就立刻停下来报错。错误在源头炸掉,定位成本最低。 几个常见的例子 fail-fast 不是某一种具体技术,而是一类做法,散落在很多地方: 参数 / 前置条件校验:方法一进来就检查参数,非法立刻抛异常,而不是带病运行。 断言(assertions):用断言把「不该发生」的假设显式写出来,一旦被打破马上崩。 构造即校验:对象在构造时就保证自己处于合法状态,构造不出来就抛错,而不是造出一个半残对象。 Java 集合的 fail-fast 迭代器:遍历过程中集合被并发修改,立刻抛 ConcurrentModificationException,而不是返回不可预期的结果(这一面在 failover / failfast / failback / failsafe 一文里也有提到)。 编译器:源码有语法错误就停止编译并报错,绝不会「跳过有问题的函数、把剩下的编译出来」。 构建工具 / 静态站点生成器:比如 Hugo,只要有一篇文档的 front matter 解析失败,就会中止整站构建——这正是我这个博客踩过的坑,记录在 一次构建失败导致的「假性停更」。 下面是一个最朴素的 fail-fast 校验: // fail fast: reject the bad input right at the boundary public void transfer(Account from, Account to, long amount) { if (from == null || to == null) { throw new IllegalArgumentException("account must not be null"); } if (amount <= 0) { throw new IllegalArgumentException("amount must be positive: " + amount); } // ... safe to proceed; state is known-good from here on } 为什么这么设计 fail-fast 背后的判断是:带着错误状态继续运行,比直接失败更危险。 ...

Created 30306-06-30 · Updated 30306-06-30 · 1 min · 189 words · -

YAGNI: You Aren't Gonna Need It 你不需要它

YAGNI 是 “You Aren’t Gonna Need It” 的缩写,意思是「你(现在)不会需要它」。它是极限编程(XP)提出的一条实践原则,由 Kent Beck、Ron Jeffries 等人总结,核心主张是:只在真正需要某个功能时才去实现它,不要因为「以后可能用得上」就提前把它做出来。 它要解决的问题 开发中有一种很常见的冲动:在写当前功能时,顺手把「将来或许会用到」的扩展点、配置项、抽象层一起加上。这类提前实现往往带来几种代价: 做了用不上的功能:付出了开发和测试成本,最终却从未被使用。 增加复杂度:每一个为「未来」准备的抽象、参数、分支,都会让代码更难读、更难改。 猜错方向:未来的真实需求常常和当初设想的不一样,提前写好的代码反而成了包袱,删也不是、改也不是。 维护负担:哪怕没用上,这些代码也要被阅读、被编译、被测试、跟着一起重构。 Martin Fowler 把这种「为了未必到来的需求而提前增加的灵活性」称为 speculative generality(投机性通用化),并将其列为一种代码坏味道。YAGNI 正是针对它的解药。 一个例子 假设要做一个把数据导出为 CSV 的功能。 违反 YAGNI 的做法:考虑到「以后可能还要导出 JSON、Excel、PDF」,于是先设计一套 Exporter 接口、工厂、注册表、插件机制,再让 CSV 成为其中一个实现。结果当下只用到 CSV,那一整套扩展架构全是空转的成本。 遵循 YAGNI 的做法:现在只需要 CSV,那就先写一个直接、清晰的 CSV 导出函数。等真的要支持第二种格式时,再基于那时已经明确的两个真实场景去抽象接口——这时的抽象建立在事实之上,而不是猜测之上。 与 KISS、DRY 的关系 这几条原则常被一起提及,但侧重点不同: YAGNI:不要做现在不需要的功能(针对「范围」)。 KISS(Keep It Simple, Stupid):用尽量简单的方式解决问题(针对「实现」)。 DRY(Don’t Repeat Yourself):消除重复的知识(针对「重复」)。 三者方向一致,都指向「不要引入不必要的复杂度」。值得注意的是 YAGNI 和 DRY 偶尔会拉扯:为了「将来复用」而过早抽取公共逻辑,可能正是一种 YAGNI 想避免的投机性抽象。经验法则是先容忍少量重复,等重复出现第三次、模式真正清晰时再抽象。 边界:YAGNI 不是什么 YAGNI 容易被误读为「不要做任何设计」或「怎么糙怎么来」,这是误解。它有明确的适用边界: 它针对的是「功能/特性」,不是「质量」。该有的测试、错误处理、清晰命名、合理结构都不能省——这些是当前就需要的,不是「未来」。 它偏好可逆决策。YAGNI 之所以成立,前提是「以后再加也不难」。对于难以回退的决策(如数据库选型、对外 API 契约、安全模型、数据格式),仍需要提前认真设计,因为它们不满足「需要时再做」的低成本假设。 它不反对抽象,只反对过早抽象。抽象应当由重复的、已经发生的真实需求来驱动,而不是由想象中的需求来驱动。 实践建议 写代码前问一句:这个能力是当前这个需求现在就要的吗?如果不是,先不做。 把「以后可能要做」的想法记进 backlog 或 TODO,而不是写进代码。 优先保证当前实现简单、清晰、可改;保留把改动控制在小范围内的能力,比预先把扩展点全部铺好更有价值。 对不可逆的决策慎用 YAGNI,对可逆的决策大胆用。 YAGNI 的本质是一种关于「时机」的纪律:把实现推迟到信息最充分的那一刻。需求在「需要时」往往比「设想时」清晰得多,那时再动手,既省去了无用功,也避免了基于错误猜测的设计。 ...

Created 28286-06-28 · Updated 28286-06-28 · 1 min · 83 words · -

XHS Push Test: 小红书推送测试页

说明 这是 wiloon.com 上的测试页面,用于验证「博客文章 → 小红书」推送流程。线上地址:https://wiloon.com/xhs-push-test/。 推送脚本或人工联调时,可用本页校验: 能否正确抓取标题与 description 正文 Markdown 转笔记文案是否正常 部署后 URL 是否可公网访问 固定测试元数据 字段 值 站点 wiloon.com 路径 /xhs-push-test 创建日期 2026-06-24 用途 小红书推送联调 正文摘录(用于 diff) 下面这段文案故意写死,方便对比抓取结果是否与源站一致: 后端工程师的日常笔记站。技术栈:Java、Go、Kubernetes、Ansible。本段为 XHS 推送测试专用固定文本,请勿当作正式内容发布。 可选检查项 标题含英文:XHS Push Test 分类:development 标签:hugo 联调通过后,可删除本页或改为 draft: true 避免出现在站点列表中。

Created 24246-06-24 · Updated 24246-06-24 · 1 min · 42 words · -

Hugo Static Site Search: Fuse.js 与 Pagefind

背景 本站使用 Hugo + PaperMod,通过 Containerfile 构建为 Nginx 静态站点镜像。文章约 2800+ 篇(Pagefind 实测索引 2779 页)。本文记录站内搜索方案调研与当前 Pagefind 接入方式。 PaperMod 内置搜索(Fuse.js,已弃用) PaperMod 主题自带基于 Fuse.js 的客户端搜索:Hugo 构建时生成 index.json,搜索页在浏览器里加载全文索引并做模糊匹配。零后端依赖,但本站实测 index.json 约 13MB,需一次下载全部正文,首访慢,中文分词也一般。现已移除 Fuse 相关配置,改用 Pagefind。 Hugo 静态站搜索方案对比 方案 原理 适合规模 优点 缺点 PaperMod Fuse.js 浏览器加载完整 JSON 小站(约 500 篇以内) 零配置、纯静态 大站索引体积大、首访慢;中文分词弱 Pagefind 构建时生成索引,搜索时按查询加载分片 中大型站 首访不必下完全部正文;extended 支持 CJK 需加构建步骤;索引目录总体积未必更小 Lunr.js 类似 Fuse,浏览器建倒排索引 小中型 多主题支持 与 Fuse 同类局限 Algolia DocSearch 构建时推送到 Algolia 云端 文档站 / 开源项目 搜索体验好 需申请、依赖第三方 Meilisearch / Typesense 自托管搜索引擎 大型站 功能强 需跑服务,个人博客偏重 本站实现:Pagefind 采用 Pagefind extended(v1.5.2,支持中文分词)。搜索 UI 使用 Default UI(pagefind-ui.js / PagefindUI),不是 1.5 新推荐的 Component UI。 ...

Created 24246-06-24 · Updated 24246-06-24 · 2 min · 279 words · -

Markdown to PDF: Pandoc + XeLaTeX 简历导出实践

投递职位需要 PDF 版简历,但内容仍希望用 Markdown 维护。这次在 Arch Linux 上搭了一套 Pandoc + XeLaTeX 流程,把中英文简历稳定导出为 PDF,并沉淀了定制 LaTeX 模板与一键脚本。本文记录选型、依赖安装、流水线、排版分工,以及模板优化上踩过的坑。 背景 源文件是 Markdown:多级标题、表格、列表、加粗混排。需要: 中英文各一份 PDF,字体分别可调 改 Markdown 后能一条命令重新导出 排版达到简历水准(页眉、列表间距、技能表行间线等) 早期试过 cmark + Chromium headless(Markdown → HTML → 打印 PDF),能跑通,但 CSS 调版费时;最终选用 Pandoc 生成 LaTeX,再用 XeLaTeX 编译 PDF——排版可控,适合反复迭代。 工具链概览 组件 作用 LaTeX 中间排版语言(.tex);详见 LaTeX 简介 Pandoc Markdown → LaTeX(--standalone);详见 Pandoc 介绍 XeLaTeX 编译 .tex 为 PDF;详见 XeLaTeX 介绍 resume.latex 定制 Pandoc LaTeX 模板(基于默认模板,统一入口) resume-style.tex 标题层级、列表间距、页眉样式、技能表行间线 pandoc-pdf.sh 串联上述步骤的一键脚本 整体流水线: Resume.md → pandoc(--template resume.latex + resume-style.tex + 字体变量) → input.tex → xelatex × 2 → Resume.pdf 相比早期实现,已去掉 Python 后处理:表格行间 \midrule 与标题样式一并写在 LaTeX 模板层(resume-style.tex),由 Pandoc 在生成 .tex 时注入导言区。 ...

Created 22226-06-22 · Updated 23236-06-23 · 5 min · 871 words · -

LaTeX: 标记语言与排版系统简介

LaTeX 是一套基于 TeX 的文档排版系统:用纯文本写 .tex 源文件,交给编译器生成 PDF。学术论文、书籍、简历等需要稳定版式的场景里很常见。若你用 Pandoc 把 Markdown 转成 PDF,中间产物通常就是 LaTeX,再由 XeLaTeX 等引擎编译。 TeX 与 LaTeX 名称 是什么 TeX Donald Knuth 在 1970 年代设计的底层排版程序与语言 LaTeX Leslie Lamport 在 1980 年代于 TeX 之上构建的宏集,提供章节、列表、表格、交叉引用等高层命令 日常说的「写 LaTeX」「装 TeX Live」,通常指:用 LaTeX 语法写文档,用 TeX 发行版里的引擎编译。 人物与历史 谁创造了什么 LaTeX 与 Linux 常被一起提起(学术写作、Arch 上装 TeX Live 等),但作者之间没有直接关系: 项目 主要作者 年代 做什么 TeX Donald Knuth 1970 年代起 底层排版程序与语言 LaTeX Leslie Lamport 1984 年起 建立在 TeX 上的文档宏集 Linux Linus Torvalds 1991 年起 操作系统内核 LaTeX 后来的标准与维护由 LaTeX Project 社区延续(如 Frank Mittelbach 等),并非 Knuth 个人在管 LaTeX。生态里还有大量宏包作者(如 beamer、TikZ 的 Till Tantau)。 ...

Created 22226-06-22 · Updated 22226-06-22 · 2 min · 424 words · -