躬行笔记
type: channel
link: https://www.fuzhoupyy.work/
description: 古人学问无遗力，少壮工夫老始成。 纸上得来终觉浅，绝知此事要躬行。
items: 从手动部署到一键上线：我用 Jenkins + Docker 搭了一套自动化流水线
items: CI/CD，如何优雅地拥抱云计算
items: 从 Docker Compose 到 Kubernetes：我走过的那些坑
items: 线上CPU飙到100%？这套排查方法论建议收藏，关键时刻能救命
items: AWS 出了个桌面端 AI 助手，用了两周之后我觉得它可能会改变我的工作方式
items: 生产环境 Prometheus 告警从一天 500 条到只剩 30 条，我做了这些事
items: Linux 7.1 来了：新 NTFS 驱动、干掉 i486、FRED 默认开启，这次更新有点东西
items: 微软把 ls、grep、cat 搬到了 Windows——这次不是闹着玩的
items: 线上服务突然 fork 不出新进程？聊聊我排查僵尸进程的那些事
items: Docker 代码沙箱防逃逸实战：从 Namespace 到 gVisor，我是怎么一层层锁死容器的

从手动部署到一键上线：我用 Jenkins + Docker 搭了一套自动化流水线
type: item
date: 2026-06-23T14:46:21+00:00
description: 遇到一个项目，代码仓库是 GitLab 托管的，部署方式嘛……让我说得好听点叫"半自动化"——开发在本地跑完测试，打个 tar 包，scp 传到服务器，然后手动重启服务。出过几次事故之后，组里终于下定决心：搞一套像样的 CI/CD。我选了 Jenkins + Docker 的方案。不是因为它最新潮（GitHub Actions、GitLab CI 都更"现代"），而是因为团队里有人用过 Jenkins，而且我们的构建环境比较特殊——有几个依赖只能在特定 OS 版本上编译。Docker 的好处就是可以把这些乱七八糟的环境依赖全打包进镜像，构建环境一劳永逸。环境准备：别小看权限问题Jenkins 装起来很快，官方提供 WAR 文件和 Docker 镜像两种方式。我选的是 Docker 方式跑 Jenkins 本身——是的，用 Docker 跑 Jenkins，然后让 Jenkins 再去操作 Docker 构建应用镜像。这就是所谓的 "Docker in Docker"（DinD），听起来套娃，但实际操作中很常见。不过这里有个坑我必须提一下。Jenkins 容器里的用户默认是 jenkins，它没有权限访问宿主机的 Docker daemon。你会看到类似这样的报错：Got permission denied while trying to connect to the Docker daemon socket解决办法是把 jenkins 用户加入 docker 组：sudo usermod -aG docker jenkins然后重启 Jenkins 和 Docker 服务。如果你是用容器跑的 Jenkins，还需要把宿主机的 /var/run/docker.sock 挂载进去：docker run -d \ -v /var/run/docker.sock:/var/run/docker.sock \ -v jenkins_home:/var/jenkins_home \ -p 8080:8080 \ jenkins/jenkins:lts这样 Jenkins 容器就能和宿主机的 Docker 通信了。说实话，第一次搞这个的时候我折腾了半天，因为忘了重启服务，一直以为是配置写错了……插件：装对了事半功倍Jenkins 的插件生态是它的核心竞争力，也是它被人吐槽"太重"的原因。对于 Docker 集成，有两个必装的：Docker Pipeline Plugin：让你在 Jenkinsfile 里直接用 docker.image() 这种 DSL 来操作容器Pipeline Plugin：这个是基础，没它就没法写声明式 Pipeline装插件的路径是 Manage Jenkins → Manage Plugins → Available，搜索 "Docker Pipeline" 就行。我还额外装了 Blue Ocean（让 Pipeline 可视化变好看）和 Git plugin（连接 GitLab 仓库）。插件不要贪多，装太多会拖慢 Jenkins 启动速度，而且插件之间偶尔还会冲突。Jenkinsfile：Pipeline as Code这是整个方案的核心。我们把 CI/CD 的流程定义写成一个 Jenkinsfile 放在项目根目录，和业务代码一起版本管理。这意味着任何人改了构建流程，都有 Git 记录可以追溯——谁改的、什么时候改的、为什么改的。我们的 Jenkinsfile 长这样（做了脱敏处理）：pipeline { agent { docker { image 'maven:3-alpine' } } environment { DOCKER_REGISTRY = 'registry.company.com' IMAGE_NAME = 'myapp' IMAGE_TAG = "${env.BUILD_NUMBER}" } stages { stage('Build') { steps { sh 'mvn clean install -DskipTests' } } stage('Test') { steps { sh 'mvn test' } post { always { junit '**/target/surefire-reports/*.xml' } } } stage('Build Image') { steps { sh "docker build -t ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} ." sh "docker push ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" } } stage('Deplo…

CI/CD，如何优雅地拥抱云计算
type: item
date: 2026-06-23T14:07:00+00:00
description: 我最近在帮一个初创团队改造他们的发布流程。他们之前用 Jenkins 跑在自己的服务器上，每次部署都得 SSH 登到机器手动检查日志，有时候还会翻车。后来我们逐步把整套系统搬到了 AWS，用 GitHub Actions + CodePipeline + ECS，现在一个 git push 就能自动走完从构建到上线的全过程。这个过程中我学到了很多关于 CI/CD 与云计算结合的妙处。从自建 Jenkins 到云上的自动化说实话，自建 Jenkins 的时代确实很折腾。你需要维护 Jenkins 服务器本身、管理磁盘空间、备份构建历史、处理执行器的故障……这些事情都不直接产生业务价值，但却占用了大量精力。最烦的是，当构建任务突然暴增时，自建的基础设施经常顶不住，你只能去扩容，但扩容又需要时间。云计算改变了这一切。AWS CodePipeline 这样的托管服务，天生就具有弹性。你定义好 pipeline 的各个阶段，剩下的交给云平台处理——它会自动扩展、自动管理，你不用操心基础设施，只需要关注业务流程。我们的做法是这样的：代码推送到 GitHub，GitHub Actions 触发单元测试和代码质量检查（这部分很快，通常 5 分钟内完成）。测试通过后自动构建 Docker 镜像，推送到 ECR（AWS 的容器镜像仓库）。然后 CodePipeline 接手，先部署到测试环境，跑集成测试，通过后再部署到预发布和生产。整个流程大约 20 分钟，全程零人工介入。GitHub Actions vs CodePipeline：我的选择这两个工具有重叠，但适用场景不同，我来说说我的体会。GitHub Actions 更轻，直接在 repository 里配置 workflow，天生和 GitHub 深度整合。如果你的整个工作流都在 GitHub 生态里（代码、issue、PR、release），用 Actions 很顺手。我们用它来做本地验证、单元测试、代码风格检查这些快速反馈。CodePipeline 是 AWS 的托管 CI/CD 服务，优势在于和其他 AWS 服务无缝连接。CodeBuild 可以在 Docker 容器里跑构建，CodeDeploy 可以直接部署到 EC2、ECS、Lambda，CloudFormation 可以自动更新基础设施。如果你的应用需要复杂的多环境部署、跨账户部署、或者需要 Blue/Green 发布策略，CodePipeline 就显得很强大。我们最后的决策是：GitHub Actions 负责快速反馈和初步验证，CodePipeline 负责多环境编排和正式发布。这样既保留了 GitHub 原生工作流的便利，又借助 CodePipeline 的强大编排能力。容器化部署的转变以前我们部署应用，经常遇到"在我的机器上能跑"这个老问题。有时候本地 Python 版本是 3.8，测试环境是 3.9，生产环境又是 3.10，依赖库的版本也不一样……每次上线前都得额外验证。还有一个隐藏的坑：某个环境装了旧版本的依赖包，跑的还是老代码，但没人发现。容器彻底解决了这个问题。我们把应用打成 Docker 镜像，环境就冻结了——本地、测试、生产用的镜像完全一样，只是配置参数不同。这是 DevOps 理想中的"build once, run everywhere"。Artifact 和 Cache：CodePipeline 的隐形优化这里我想细说一个很多人忽视的点。CodePipeline 在各个 stage 之间传递的是 artifact（通常是压缩包），这些文件都存在 S3 里。第一次看到账单时我有点吓到：这些 artifact 占了不少空间，而且每次部署都要从 S3 读写一遍。后来我发现，CodeBuild 有个 Docker layer caching 机制。简单说就是：构建 Docker 镜像时，如果某一层的内容没变，就直接用上次构建缓存的层，不用重新 build。这能把构建时间从十几分钟降到几分钟，成本也下降 50-70%。不过要启用这个功能需要在 buildspec.yml 里配置 cache 模式，而且 CodeBuild 需要有权限访问 S3（存放缓存）。我们的做法是给 CodeBuild project 配置 S3 本地缓存，指定要缓存哪些目录。比如 node_modules 和 pip packages，这两个目录通常最大，缓存效果最明显。这样一来，如果只改了应用代码，依赖没变，构建就能直接用缓存，速度快得多。ECS 的滚动更新和版本一致性现在的流程是这样的：开发提交代码到 GitHubActions 自动构建镜像，跑单元测试（直接在容器里）镜像推送到 ECRCodePipeline 从 ECR 拉镜像，…

从 Docker Compose 到 Kubernetes：我走过的那些坑
type: item
date: 2026-06-21T14:02:52+00:00
description: 我还记得那天下午，当老板问"我们能把线上应该能跑在 K8s 上吧"时，我才意识到我们不能再靠 Docker Compose 撑下去了。当时我们用 Compose 运行了 3 年的微服务：一个 Node.js API、两个 Python 后台任务、一个 Redis 缓存、PostgreSQL 数据库——都在一个 docker-compose.yml 文件里定义，在单台虚拟机上跑。每次发布新版本就是：停容器、拉新镜像、docker-compose up -d。工作是能正常工作，但问题开始浮现了。一个晚上，那台虚拟机的硬盘满了。所有容器崩了，我们直到用户打电话才知道。自动扩容？没有。服务发现？靠写死的 IP。滚动更新？做不了，停一个容器就有几秒的请求失败。当时我就明白，是时候升到 Kubernetes 了。第一个坑：别被"一键转换"骗了我最初的想法很天真——用 Kompose 工具自动转换。# 多么诱人的命令 kompose convert -f docker-compose.yml -o k8s/Kompose 确实能把你的 Compose 文件转成 Kubernetes YAML。我当时用了这个工具，结果生成了一堆 yaml 文件扔进去——然后一切都崩了。问题出在细节上。Kompose 把 depends_on 转成了 initContainer，但它不知道 PostgreSQL 实际上需要 30 秒才能启动。API 容器一启动就连不上数据库，立刻被 crash loop 了。更糟的是，Kompose 没有给任何 pod 设置资源限制（requests/limits），所以 scheduler 没法做合理的资源分配。我的建议是： Kompose 只能当做一个参考。你还是得手动写 YAML，理解每一行在干什么。第二个坑：资源限制这个"可选项"在 Docker Compose 里，我们从没想过资源限制是什么。容器想用多少内存就用多少。移到 Kubernetes 后，我设置了这样的资源定义：apiVersion: apps/v1 kind: Deployment metadata: name: api-server spec: replicas: 3 selector: matchLabels: app: api-server template: metadata: labels: app: api-server spec: containers: - name: api-server image: myregistry/api-server:v1.2.3 ports: - containerPort: 3000 resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "500m"太保守了。一上生产，API 在突发流量下直接被 OOMKill 了。Kubernetes 发现 pod 用了超过 256MB 内存，就杀掉它重启。服务抖动得厉害。反过来，我又把 limits 设得很松（512Mi、1000m），结果一个有内存泄漏的服务逐渐吞掉了整个节点的资源，其他 pod 被无情地驱逐。现在我的做法是：先在本地或测试环境压测一遍，看真实的内存和 CPU 占用，然后 requests 设为实际用量的 1.2 倍左右，limits 设为 1.5-2 倍。这样既给了应用突发的空间，又能保护集群。对了，这里还有个坑：Kubernetes 有三个 QoS class——Guaranteed、Burstable、BestEffort。如果你设置了 requests 和 limits（且相等），pod 会被标记为 Guaranteed，在资源紧张时最后被驱逐。如果只设了 requests，就是 Burstable，被驱逐的优先级更高。理解这个很重要，尤其是你在跑有状态服务时。第三个坑：网络和存储不是"自动的"在 Docker Compose 里，容器之间通过服务名通信，自动有个 bridge network。迁到 K8s 后，我以为 DNS 名字可以一样用——结果踩坑了。Kubernetes 里，service 的 DNS 名字是 service-name.namespace.svc.cluster.local。我的 Node.js API 之前通过 postgres://db:5432 连接数据库（db 是 Compose 里的服务名）。换到 K8s 后，我试了各种办法，最后才意识到应该用 postgres://postgres-service.default.svc.cluster.local:5432。更复杂的是存储。Compose 里，我们用 vo…

线上CPU飙到100%？这套排查方法论建议收藏，关键时刻能救命
type: item
date: 2026-06-17T14:15:21+00:00
description: CPU异常飙升这个问题，说大不大说小不小，有时候就是一个死循环，有时候是一个隐藏很深的GC问题，有时候甚至是一行正则表达式搞的鬼。但不管根因是什么，排查思路其实是有套路的。今天我就把这几年在生产环境中积累的CPU排查经验整理出来，从告警接入到最终定位，全流程走一遍。先搞清楚：CPU到底在忙什么很多人收到CPU告警的第一反应是直接top看一眼哪个进程吃CPU，然后就去翻代码了。这个习惯不能说错，但效率不高。CPU使用率这个指标本身是个很粗粒度的东西，你得先搞清楚是哪部分在消耗CPU。Linux里top命令展示的那几个指标——us、sy、wa、si、hi，每一个都代表不同的含义。us（user）是用户态CPU时间，你的业务代码跑在用户态，如果us高，说明是业务代码本身在烧CPU。sy（system）是内核态CPU时间，如果sy异常高，可能是系统调用太频繁，或者内核处理中断太忙。wa（iowait）是等待IO的时间，严格来说wa高不代表CPU忙，而是CPU闲着没事干在等磁盘。si（softirq）是软中断，网络收发包、定时器这些都会触发软中断。我之前遇到过一个案例，top看CPU使用率80%多，第一反应是业务代码有问题，结果仔细一看sy占了60%多，us才十几。顺着sy高这条线往下查，发现是一个定时器间隔设得太小，500微秒触发一次，内核疯狂处理定时器中断。最后把定时器间隔改成1ms，CPU直接降了一半。所以拿到CPU告警，第一件事不是急着翻代码，而是搞清楚CPU时间花在哪了。排查工具箱：从粗到细的武器库2.1 top / htop —— 快速定位top命令大家都会用，但有几个小技巧值得说一下。# 按CPU排序，只看前20 top -bn1 | head -20 # 看某个具体进程的线程级CPU top -H -p <PID>-H这个参数很关键，它能展开到线程级别。有些Java应用你用top看进程CPU不高，但用top -H一看，某个线程CPU吃满了，这种情况下进程级别的数据是有欺骗性的。2.2 mpstat —— 各核CPU详情mpstat -P ALL 1 3这个命令能看到每个CPU核心的使用率，包括usr、sys、iowait、softirq等细分指标。如果发现某个核CPU特别高而其他核正常，那可能是单线程瓶颈或者中断都绑在一个核上。# 看软中断详情 mpstat -I SCPU 1 5 # 看硬中断详情 mpstat -I CPU 1 5这两个命令在排查网络中断、定时器中断类问题时特别有用。之前那个定时器的案例，就是通过mpstat -I SCPU发现HRTIMER软中断从正常的200次/秒飙升到2000多次/秒，才锁定了问题方向。2.3 pidstat —— 进程级CPU分析# 每秒输出一次，输出5次 pidstat 1 5 # 针对特定进程 pidstat -p <PID> 1 5pidstat比top好的地方在于它能持续输出，方便你观察CPU使用率的波动趋势。而且它还能区分usr和system时间，对判断问题方向很有帮助。2.4 strace —— 系统调用追踪当你发现某个进程sy（内核态）CPU高的时候，strace是直接有效的工具。# 追踪进程的所有系统调用，统计耗时 strace -c -p <PID> # 输出示例： # % time seconds usecs/call calls errors syscall # ------ ----------- ----------- --------- --------- ---------------- # 45.23 0.045123 45 1002 3 futex # 23.15 0.023115 23 998 read # 15.67 0.015670 31 500 write-c参数会统计每个系统调用的次数和耗时，一眼就能看出哪个系统调用在烧CPU。不过要注意，strace本身有性能开销，生产环境慎用，特别是高QPS的服务。2.5 perf —— 性能分析神器perf是Linux内核自带的性能分析工具，可以说是CPU排查的核心武器。# 实时查看CPU热点函数 sudo perf top -p <PID> # 采样30秒 sudo perf record -F 99 -g -p <PID> -- sleep 30 # 查看采样报告 sudo perf report --stdio -g fractal # 指令级分析某个热点函数 sudo perf annotate --symbol=<function_name>perf top适合快速看一下当前CPU在执行什么函数，perf record则是采样后做深度分析。关于采样频率-F 99，为什么不用100？因为…

AWS 出了个桌面端 AI 助手，用了两周之后我觉得它可能会改变我的工作方式
type: item
date: 2026-06-17T14:14:54+00:00
description: 前阵子 AWS 在 2026 年的 What's Next 大会上发布了 Amazon Quick，一个 AI 工作助手。说实话刚看到新闻的时候我没太当回事——又是一个 ChatGPT 套壳？但后来发现它有个桌面端应用 Amazon Quick Desktop，能直接访问本地文件、连接 Slack 和邮箱、还能跑定时任务和浏览器自动化，这就有意思了。我申请了预览版用了两周多，今天把使用体验和一些实际应用场景整理出来。不吹不黑，确实有些地方设计得挺聪明的，也有些地方还比较粗糙。它到底是个什么东西官网：https://quick.aws.com/sn现在凭邮箱注册免费试用plus版本一个月哦**简单说，Amazon Quick Desktop 是一个跑在你电脑上的原生应用（支持 macOS 和 Windows），核心是一个 AI 对话界面，但它不只是聊天。它能读写你的本地文件、连接你的 Slack/Outlook/Gmail、跑后台定时 Agent、操控浏览器、还有一套知识图谱系统。跟网页版 Amazon Quick 最大的区别在于：桌面端采用的是 local-first 架构。AI 后端跑在本地，你的文件不会上传到云端。唯一的网络请求是调 AI 模型的 API 和连接你授权的第三方服务。这一点对企业用户来说很关键——数据不出本机。系统要求不算高：macOS 要 Apple Silicon（M1 及以上），Windows 要 64 位 Win10 以上，内存最低 8GB。磁盘空间安装本体只要 500MB，但如果你要用搜索索引和知识图谱功能，建议预留 10GB 以上。安装和登录下载方式有两种：一种是从 Amazon Quick 网页版的 Extensions 页面下载，另一种直接用链接：macOS：https://desktop.downloads.quick.aws.com/mac/arm64/Amazon-Quick.dmgWindows：https://desktop.downloads.quick.aws.com/windows/x64/Amazon-Quick.exemacOS 装完第一次打开大概率会弹安全提示，去系统设置 → 隐私与安全性里点一下"仍然打开"就行。Windows 上 SmartScreen 也会拦一下，点"更多信息"然后"仍然运行"。登录支持两种模式：个人用户用 Free/Plus 账号登录（支持邮箱、Amazon、Apple、Google、GitHub），企业用户走 Enterprise login 通过公司的 IdP 认证。本地文件访问——这个是真正好用的地方装完之后第一件事就是去 Settings → My Computer → Local Folders 添加你要让 Quick 访问的文件夹。注意是你主动授权的文件夹，不是它想访问哪就访问哪。加完文件夹之后，每个文件夹可以独立配置三种索引模式：索引类型干什么的Keyword search建关键词索引，做全文搜索用Semantic search建语义向量索引，按意思搜而不只是关键词匹配Knowledge graph extraction从文件里提取人名、项目、事件等实体关系这三个可以独立开关。我的建议是常用的项目文档文件夹三个全开，临时文件夹只开 Keyword 就行——语义索引和知识图谱会占不少磁盘空间。索引建好之后，你跟 Quick 聊天的时候直接问"帮我找一下上次那个 Terraform 模块的配置文件在哪"，它就能从你本地文件里搜出来并且给你分析内容。不需要先上传。还有个细节：索引有存储上限和单文件大小上限的设置，硬盘剩余空间低于 8GB 的时候索引会自动暂停。这个设计挺合理的，不会把你磁盘撑爆。连接外部服务Settings → Capabilities → Connections 里可以连各种外部服务。内置支持的挺全的：Slack、Microsoft Outlook、Microsoft Teams、Gmail、Google Calendar、Google Meet、OneDrive、SharePoint、Google Drive、Google Docs/Sheets/Slides、Dropbox、Google Analytics、Airtable、QuickBooks、Zoom、Zapier……连接方式就是点 Sign in 然后走 OAuth 授权流程。连好之后 Quick 就能读取你这些服务里的信息了。我连了 Slack 和 Gmail 之后做了个测试——问它“帮我看看最新的邮件”。这比自己去翻方便太多了。AI 模型和对话聊天界面底部有个模型选择器，四种模式：Fast：最便宜最快，适合简单问题Balanced：平衡模式，日常够用Smar…

生产环境 Prometheus 告警从一天 500 条到只剩 30 条，我做了这些事
type: item
date: 2026-06-17T14:14:25+00:00
description: 上个月我负责的业务线出了一次不大不小的事故，事后复盘发现一个很尴尬的事情：告警其实提前 20 分钟就触发了，但当时值班的同事压根没看到。为什么？因为那天光钉钉群里的告警就有 400 多条，绝大部分是无意义的抖动和重复，大家早就免疫了。这事之后我花了两周把告警体系重新治理了一遍，从每天 500 条干到了 30 条左右。这 30 条里没有一条是废的。整个过程记录一下。动手之前先做了个统计。Alertmanager 有 API，直接用脚本把过去两周的告警 dump 下来，Python 做个分类。结果挺触目惊心的：CPU/内存瞬时抖动触发的：45%同一个故障不同实例重复报的：25%刚恢复又触发来回抖的：15%真正需要人处理的：15%85% 是噪音。这还算好的，我见过有些团队 95% 以上都是噪音。问题集中在几个地方。一个一个说。最容易改也最有效的就是 for 持续时间。很多人写告警规则 for 要么不写，要么就 1m，结果任何毛刺都会触发。原来的 CPU 告警长这样：- alert: HighCPUUsage expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[2m])) * 100) > 80 for: 1m labels: severity: warning annotations: summary: "CPU使用率超过80%"问题是 for: 1m 太短了。线上服务流量高峰 CPU 蹦到 85% 很正常，过几十秒就下来了，这种报警没有意义。改成：- alert: HighCPUUsage expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 5m labels: severity: warning annotations: summary: "{{ $labels.instance }} CPU持续5分钟超过80%"rate 窗口从 2m 拉到 5m，计算结果更平滑。for 从 1m 改 5m，得持续超才报。就这一个改动，CPU 相关告警直接少了 70%。不过不是所有告警都能加长 for。磁盘空间这种变化慢的，for 可以长；5xx 错误率飙升的，for 就不能太长，不然等你收到告警用户早骂了半天了。我大概是这么分的：资源类（CPU/内存/磁盘）5-10m，网络类 3-5m，业务类（5xx/SLA）1-2m，基础设施宕机 30s-1m。然后是 group_by。解决"同一个问题报 N 遍"的问题。Redis 集群挂了，依赖它的 50 个服务实例全报"连接超时"。不做合并就是 50 条基本一样的消息刷屏。route: receiver: 'default' group_by: ['alertname', 'cluster', 'service'] group_wait: 30s group_interval: 5m repeat_interval: 4hgroup_wait: 30s 是收到第一条告警后等 30 秒，把这段时间内同组的合一起发。设太短合并效果不好，设太长收到告警就晚了。repeat_interval 我建议别设太短。之前有同事设了 30 分钟，一个没人处理的告警一天发了 48 次。改成 4h，critical 的 1h，既提醒又不烦。接下来是抑制（Inhibition），这个功能很多人不知道，但真的好用。场景：宿主机挂了，上面 20 个容器全触发"容器不可达"。但根因是宿主机，容器不可达只是衍生问题，不需要单独报。inhibit_rules: - source_match: severity: 'critical' alertname: 'NodeDown' target_match: severity: 'warning' equal: ['instance']当 NodeDown 这个 critical 告警存在时，同 instance 的 warning 全部抑制。还加了一条：整个集群网络不通时，抑制掉集群内所有服务的健康检查失败：inhibit_rules: - source_match: alertname: 'ClusterNetworkPartition' target_match_re: alertname: 'ServiceHealthCheckFailed|PodCrashLooping|EndpointDown' equal: ['cluster']遇到大面积故障的时候告警从上百条压缩到个位数，值班人员可以迅速看到根因在哪。这个在去年双十一前的压测里帮了大忙，模拟了一次…

Linux 7.1 来了：新 NTFS 驱动、干掉 i486、FRED 默认开启，这次更新有点东西
type: item
date: 2026-06-15T14:51:31+00:00
description: 上周末 Linus 把 7.1 推出来了。说实话，一般 .1 版本我不太会单独写文章聊，但这次翻了一下 changelog，发现几个改动还挺有意思的，尤其是搞存储和运维的兄弟可能会比较关注。先说最让我兴奋的——新的 NTFS 驱动终于合进主线了。NTFS 驱动重写：不只是"能写了"这么简单做过双系统或者处理过 Windows 磁盘数据迁移的应该都知道，Linux 下挂 NTFS 一直是个半残废状态。内核自带的老 NTFS 驱动只能读，想写得靠 ntfs-3g。那个 FUSE 方案用过的都懂——每次 I/O 都要在用户态和内核态之间来回切换，大文件拷贝的时候 CPU 占用高得离谱，实际吞吐量也就机械硬盘的水平，就算你底下是 NVMe 也白搭。这次 7.1 合进来的新驱动是 Namjae Jeon 搞了四年的成果。不是在 NTFS3 上打补丁，是基于现代内核基础设施重新实现的。技术架构上有几个关键变化值得展开说：第一，用 iomap 取代 buffer_head。这个变化对熟悉内核文件系统开发的人来说意义很大。buffer_head 是 Linux 内核里非常古老的 I/O 抽象层，每个 block 对应一个 buffer_head 结构体，管理脏页、回写、映射关系。问题是这套东西太重了——一个 4KB 的页面对应一个 buffer_head，一个大文件可能产生成千上万个 buffer_head 链表节点，内存开销和遍历开销都很可观。iomap 是现代替代方案。它不再按 block 粒度管理，而是按 extent（连续区间）来做映射。文件系统只需要告诉 iomap 层"这段文件偏移对应磁盘上哪段连续区域"，后面的 buffered I/O、direct I/O、writeback 全都由 iomap 框架统一处理。XFS 最早用这套，后来 ext4 也在逐步迁移。对于 NTFS 驱动来说，换到 iomap 意味着：大文件顺序读写性能显著提升（减少了元数据管理开销）内存占用更低（不用维护海量 buffer_head 链表）writeback 路径更短、更干净第二，folio 转换。folio 是 5.16 开始引入的页面管理抽象，用来替代 struct page。简单说就是把 compound page（多个物理页组成的大页）当作第一公民来处理，而不是每次都要判断"这个 page 是不是某个 compound page 的尾页"。新 NTFS 驱动原生支持 folio，意味着在大页（huge page）场景下效率更高，pagecache 管理也更现代化。内核文档里已经明确说了：buffer_head is deprecated, new filesystems should use iomap instead。这个新驱动从设计上就走了正确的路。第三，实际测试数据。新驱动通过了 326 个 xfstests 用例，之前的 NTFS3 是 273 个。差的这 53 个用例覆盖了 fallocate、idmapped mounts、权限控制等场景，都是生产环境里真会遇到的东西。# 新驱动的 Kconfig 选项 CONFIG_NTFS_FS=m # NTFS3 暂时还保留着，但内核文档已经指向新驱动 # 未来 NTFS3 大概率会被标记 deprecated # 配套的用户态工具 # ntfsprogs-plus 提供了 fsck.ntfs 等 # 之前 ntfs-3g 那套工具依赖 FUSE，新方案不需要还有个小插曲——Linus 在合并的时候因为 Git 树的结构问题把代码 un-pull 了一次。具体原因是 pull request 的 commit 组织方式不对，Linus 要求重新 rebase 后才接受。他给的评价是 "ntfs resurrection"，复活。这个词用得确实到位。对运维的实际影响：如果你的工作涉及 Windows/Linux 混合存储环境——比如跨平台的 NAS、双系统数据盘、或者从 Windows 服务器迁移数据——升级到 7.1 之后可以直接 mount NTFS 分区读写，不需要再装 ntfs-3g，性能和稳定性都有本质提升。Intel FRED：四十年来最大的中断处理改革FRED 全称 Flexible Return and Event Delivery。7.1 之前需要手动加 fred=on 启动参数，7.1 开始默认开启。先说这东西解决什么问题。x86 的中断处理机制依赖一个叫 IDT（Interrupt Descriptor Table）的结构，这玩意儿是 80286 时代设计的，距今 40 多年了。整个流程大概是这样：CPU 收到中断/异常 → 查 IDT 找到对应的 handler 地址 …

微软把 ls、grep、cat 搬到了 Windows——这次不是闹着玩的
type: item
date: 2026-06-10T15:51:08+00:00
description: 上周刷技术新闻，看到一条消息差点以为是恶搞：微软在 Build 2026 上正式发布了 Coreutils for Windows，把 75+ 个 Linux/Unix 命令原生搬到了 Windows 的 CMD 和 PowerShell 里。ls、grep、cat、cp、mv、rm、find、head、tail、sort……全都有了。注意——这不是 WSL，不是 Cygwin，不是 Git Bash，是原生跑在 Windows 上的。一个 4.6MB 的 Rust 编译二进制，winget install Microsoft.Coreutils 就装好了。Satya Nadella 在 Build 的主题演讲上亲自演示了一把 grep："Grep in full glory is now available for full Windows access." 那场面，Steve Ballmer 看了怕不是棺材板都按不住。为什么我说这事比看起来大如果你觉得这只是"方便了一下偶尔用 Windows 的 Linux 用户"——那格局小了。先说个背景。微软那个 "Linux is a cancer" 的名言大家都还记得吧？那是 2001 年 Ballmer 说的。后来 WSL 出来的时候我们就已经感叹"时代变了"，再后来 Azure 上 Linux 跑得比 Windows 还多也没人惊讶了。但这次不一样——这次微软是把 Linux 的命令行基础设施直接焊死在 Windows 内部了。过去 Windows 开发者想用 Linux 命令有多痛苦呢？我列一下选项：装个 Git Bash，但只有一部分命令开个 WSL，但你的文件系统访问跨了两个世界搞个 Cygwin/MSYS2，但维护负担重，版本经常滞后用 PowerShell 的等价命令，但语法完全不一样每次写个 README 里的命令示例，你都得加一句"Windows 用户请用 xxx 替代"。每次跨平台脚本，都得维护两套逻辑。这些年我在团队里见过无数次：明明是一个简单的 grep -r "ERROR" /var/log/ 的事，Windows 上就得折腾半天。现在呢？一条命令装完，ls 就是 ls，grep 就是 grep，cat 就是 cat。flag 一样，行为一样，输出一样。底层是 Rust 重写的 uutils微软没有直接搬 GNU Coreutils——那玩意儿是 GPL 授权的，微软碰 GPL 就像碰了高压线。他们选了 uutils/coreutils 项目，这是一个社区用 Rust 从零重写 GNU Coreutils 的开源项目，MIT 授权。用 Rust 重写有几个好处：内存安全。Coreutils 这种底层工具如果有内存漏洞那影响面太大了，Rust 天然规避了这类问题。跨平台设计。uutils 从一开始就是为了在 Windows/macOS/Linux 上都能跑而写的。MIT 协议。微软可以放心用，不用担心 GPL 传染的问题。当然也有坑。Windows 和 Linux 的文件系统语义差异不是 Rust 能抹平的——盘符、路径分隔符、ACL 权限模型、大小写不敏感、符号链接行为，这些都是真实的差异。所以有些命令是缺席的：chmod、chown、chroot 这些跟 POSIX 权限模型绑死的命令没有，因为 Windows 用 ACLkill、timeout 没有，因为 Windows 没有 POSIX 信号机制dd、shred、sync 暂时也没有dir、date、whoami 这些跟 Windows 内置命令冲突的，需要注意 PATH 优先级实际用下来，日常 80% 的场景是覆盖的。我前两天在一台 Windows 开发机上试了下，ls -la、grep -rn "pattern" .、cat file | sort | uniq -c，这些管道操作跑得很顺，速度甚至比 Cygwin 快不少（毕竟是 Rust 原生编译的）。真正的野心：AI Agent 时代的基础设施好了，到这里如果你觉得微软只是在"讨好开发者"，那还是格局小了。Build 2026 上微软同时宣布了另一个东西：OpenClaw for Windows。OpenClaw 是今年爆火的开源 AI Agent 框架，能让自主 Agent 在本地机器上执行复杂任务。这些 Agent 的执行逻辑是什么？跑 shell 命令、写脚本、操作文件系统、搜索日志——全是命令行操作。之前 OpenClaw 用户疯狂买 Mac Mini 来跑，因为 macOS 天然有完整的 Unix 命令行。Windows 用户想跑？得装 WSL、配环境、折腾半天。现在 Coreutils 一装，Windows 直接具备了 Agen…

线上服务突然 fork 不出新进程？聊聊我排查僵尸进程的那些事
type: item
date: 2026-06-09T13:48:30+00:00
description: 前阵子半夜被告警叫醒，一台线上机器的 worker 进程起不来了。打开监控一看，CPU 不高、内存也还行，但服务就是 fork 失败。第一反应是 ulimit 的 nproc 打满了？跑了一下 ps aux | wc -l，进程数确实很高，再仔细一看——一堆 <defunct> 的进程静静地躺在那。好家伙，僵尸进程爆了。僵尸进程到底是什么鬼说白了，僵尸进程就是一个已经执行完了的子进程，但它的父进程没有调用 wait() 来回收它的退出状态。这个子进程已经"死了"——不占 CPU，不占内存，但它仍然在内核的进程表里占着一个 PID 的坑位。你可以把它想象成快递柜里一个没人取的包裹。包裹送到了（子进程执行完了），快递员也走了（进程代码已经退出），但取件码还在占着格子（进程表里还有条目），你不去签收（父进程不 wait），这个格子就一直被占着。一个两个僵尸进程无所谓，但如果大量堆积起来就麻烦了。Linux 64 位系统默认 PID 空间最大是 4194304，听着很大对吧？但实际生产环境里 pid_max 经常被调小，而且 nproc 的 ulimit 限制通常更紧。一旦进程表被僵尸填满，任何需要新进程的操作全部歇菜——新连接进不来、worker 起不了、cron job 跑不了。之前看到一个案例挺典型的：某 K8s 集群里一个 DNS pod 因为 Golang 的 channel 泄漏，单个 node 上堆积了超过 26000 个僵尸进程，直接把整个集群的 DNS 解析搞挂了。和孤儿进程搞清楚区别很多人容易搞混僵尸进程和孤儿进程，但这俩完全不是一回事：僵尸进程（Zombie）：子进程已经退出了，但父进程没回收。子进程是"死的"，状态是 Z。孤儿进程（Orphan）：父进程先挂了，子进程还活着。子进程会被 init（PID 1）领养，还在正常运行。孤儿进程其实不算问题，因为 init/systemd 会自动接管并在合适的时候回收它们。真正头疼的是僵尸——它已经死了，你 kill -9 都杀不掉它，因为它本来就不在运行。实战排查流程回到那天晚上的故事。发现一堆 defunct 之后，我的排查流程大概是这样的：第一步：确认僵尸进程数量ps aux | grep -w Z | grep -v grep | wc -l或者直接 top 看头部信息：top -bn1 | grep zombie那天一看，300 多个。心一凉。第二步：找到它们的父进程僵尸进程你杀不掉，关键是找到谁生了它们又不管。ps -eo pid,ppid,stat,comm | grep -w Z输出大概长这样： 3457 3425 Z my-worker 3533 3425 Z my-worker 3612 3425 Z my-worker ...好嘛，PPID 全是 3425。看看 3425 是啥：ps -p 3425 -o pid,comm,args发现是我们的一个任务调度进程，它 fork 子进程去执行任务，但执行完之后没有正确 wait。第三步：用 pstree 看清楚父子关系pstree -p -s 3457这个命令会从 init 一路画到目标进程，树状结构很清晰：systemd(1)───supervisord(1205)───task-scheduler(3425)───[my-worker](3457)一目了然。第四步：尝试通知父进程回收先温柔地来：kill -SIGCHLD 3425发一个 SIGCHLD 给父进程，意思是"嘿，你的子进程退出了，快 wait 一下"。如果父进程代码写得还行——比如注册了 SIGCHLD handler——那它就会老老实实调 waitpid 把僵尸收了。等了几秒再看，嗯，僵尸数没变。说明这个父进程压根没处理 SIGCHLD。第五步：重启父进程没办法了，直接重启：kill 3425父进程一死，它下面的僵尸就会被 init/systemd 接管，然后立刻被回收。ps aux | grep -w Z | wc -l # 0清爽了。然后再把调度进程拉起来，盯了一会儿确认不会再产生新的僵尸。当然，实际生产中你不能总这么暴力，因为杀父进程意味着它正在处理的所有子任务也全部中断。如果是重要服务，需要先评估影响。根因分析和修复排查完之后就该看代码了。翻了一下那个 task-scheduler 的源码，问题一目了然——它 fork 出子进程之后，用了一个 epoll 循环等事件，但 SIGCHLD 信号被忽略了，也没有任何地方调用 waitpid。修复方案有几种，看场景选：方案一：直接忽略子进程退出状态如果你不关心子进程的退出码，最简单的方式：signal(SIGCHLD, SIG_IGN);设了这个之后，子进程退出时内核会自动清理，根本…

Docker 代码沙箱防逃逸实战：从 Namespace 到 gVisor，我是怎么一层层锁死容器的
type: item
date: 2026-06-02T15:45:49+00:00
description: 前阵子有个做在线编程平台的朋友找到我，说他们平台要支持用户在线提交代码并执行，问我用 Docker 跑用户代码安不安全。我当时就笑了——这事儿我太熟了，之前在做一个类似 LeetCode 的 OJ 系统的时候，就踩过不少坑。说白了，Docker 容器不是什么铜墙铁壁，它本质上就是跑在宿主机内核上的进程，只不过套了几层"隔离马甲"。你要是觉得 docker run 一下就万事大吉了，那迟早要出事。今天我就把自己在生产环境中是怎么一步步加固 Docker 代码沙箱的经验，原原本本讲出来。先搞清楚一个问题：Docker 容器到底隔离了什么？很多人对 Docker 有个误解，觉得容器就是轻量级虚拟机。这个想法很危险。虚拟机有自己独立的内核，而容器是共享宿主机内核的，区别大了去了。Docker 的隔离主要靠两样东西：Namespace 和 Cgroup。Namespace 做的是资源视角的隔离，让容器里的进程觉得自己是系统唯一的主人。Linux 目前提供了 6 个主要的 Namespace：PID Namespace：容器里的进程看不到宿主机和其他容器的进程Mount Namespace：容器有自己独立的文件系统挂载点Network Namespace：容器有独立的网络栈、IP地址、端口IPC Namespace：进程间通信隔离UTS Namespace：主机名隔离User Namespace：用户和用户组隔离Cgroup 做的是资源使用的限制，防止一个容器把宿主机的 CPU、内存、磁盘 IO 全吃光。听着挺完善的对吧？但问题在于，这些隔离都是"软隔离"，不是"硬隔离"。共享内核意味着，一旦内核有漏洞，或者你的配置有疏漏，容器里的恶意代码就有可能突破这些限制，逃到宿主机上。我之前看过一个案例，某在线编程平台因为配置不当，用户通过 Python 的 os.system() 直接在宿主机上执行了命令，把整台服务器搞崩了。所以，做代码沙箱，安全这根弦必须绷紧。第一道锁：资源限制，别让恶意代码把机器吃干抹净做代码沙箱，最基本的就是限制资源。你想想，要是用户提交一个死循环，或者疯狂 fork 进程的 fork 炸弹，不限制的话宿主机直接就废了。我一般会在 docker run 的时候加上这些参数：docker run \ --memory="512m" \ --memory-swap="512m" \ --cpus="1" \ --pids-limit="50" \ --cpu-shares=512 \ sandbox-runner逐个解释下：--memory="512m"：限制容器最多使用 512MB 内存，超过就直接 OOM Kill，不给任何商量余地--memory-swap="512m"：这个和 memory 设成一样的值，意思就是禁用 swap。别小看这个，有些恶意代码会利用 swap 来绕过内存限制--cpus="1"：最多使用 1 个 CPU 核心的算力--pids-limit="50"：限制容器内最多 50 个进程。这个是防 fork 炸弹的关键，:(){ :|:& };: 这种东西直接就废了--cpu-shares=512：CPU 时间的权重分配，多个容器竞争 CPU 时按这个比例来还有一点容易忽略的，磁盘 IO 也得限制：# 创建限速的 cgroup mkdir /sys/fs/cgroup/blkio/sandbox echo "8:0 1048576" > /sys/fs/cgroup/blkio/sandbox/blkio.throttle.read_bps_device不过说实话，磁盘 IO 限制在实际操作中比较麻烦，我更倾向于直接把容器的文件系统设成只读，后面会讲。第二道锁：Capability 裁剪，别给容器不需要的权限Linux 把 root 的权限拆分成了几十个 Capability，Docker 默认只保留了容器运行所需的一小部分。但即便如此，默认配置下容器还是有一些不必要的权限。你可以用这个命令看看默认情况下容器有哪些 Capability：docker run --rm alpine capsh --print我这边输出大概有这些：CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER, CAP_MKNOD, CAP_NET_RAW, CAP_SETGID, CAP_SETUID, CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE。对于代码沙箱来说，这里面很多都是多余的。我的做法是把所有 Capability 都去掉，只加回必须的…