用 mirrord 把本地进程接入 K8s 集群:从 Demo 到真实调试实践
目录
调试 Kubernetes 里的服务有多烦?本地改一行代码,要走完 mvn package → docker build → kind load → kubectl rollout restart 这一套,少则一两分钟,多则好几分钟。而且这还只是单服务,如果服务依赖同命名空间里的 MySQL、Redis 或者其他微服务,kubectl port-forward 也救不了你——端口转发不能同时暴露多个服务的网络环境,更没法让你的本地进程「假装」是 Pod 本身。
mirrord 解决的正是这个问题。这篇文章通过一个完整的 Demo 项目,带你从零走完「本地进程通过 mirrord 接入 k8s 集群调试」的完整流程。
问题:开发者与 K8s 之间的摩擦#
常见的几种调试方案,各有明显局限:
| 方案 | 痛点 |
|---|---|
kubectl port-forward |
只能转发单个端口,进程本身感知不到集群的服务发现和环境变量 |
| Skaffold / Tilt 热重载 | 仍然需要 build → load → rollout,只是自动化了步骤,没有消除延迟 |
| 在 Pod 内直接 exec | 调试体验差,IDE 断点无法接入 |
| 远程 JVM debug + port-forward | 可以断点,但进程还是跑在集群里,不是本地环境 |
理想状态是:本地用 IDE 正常启动应用,但进程的网络、环境变量、文件系统全都来自集群。 这就是 mirrord 的设计目标。
mirrord 是什么?它怎么工作?#
mirrord 是 MetalBear 开源的一个开发者工具(GitHub),核心思路是:
在本地进程旁边注入一个 agent,把进程的系统调用重定向到集群 Pod 上运行的 mirrord-agent 容器。
具体来说,mirrord 会拦截本地进程的:
- 网络出站调用:进程发出的 TCP/UDP 连接透过 agent 走到集群内网,所以本地代码可以直接用
mysql.mirrord-demo.svc.cluster.local:3306这类集群 DNS。 - 网络入站流量:集群 Pod 收到的请求可以被镜像(mirror)或劫持(steal)到本地进程。
- 环境变量:注入目标 Pod 的环境变量,本地进程和 Pod 看到的配置完全一致。
- 文件系统(可选):本地读取某些路径时,实际读的是 Pod 里的文件。
整个过程不需要修改应用代码,也不需要重新打镜像。集群里的 Pod 继续运行,只是 mirrord 在它旁边悄悄塞了一个 agent sidecar(用后自动清理)。
本地 Java 进程
│ (系统调用被 mirrord 拦截)
│
mirrord-agent (本地侧)
│ (加密隧道)
▼
mirrord-agent (Pod 旁的 sidecar)
│
├──► 集群内网 DNS / MySQL / 其他服务
└──► Pod 的入站流量
Demo 项目架构#
演示项目在 meirongdev/mirrord-demo,组成如下:
本地开发机
┌────────────────────────────────────────────────────┐
│ │
│ make run-local │
│ ┌──────────────────────────────┐ │
│ │ 本地 Spring Boot :8080 │◄── curl :8080 │
│ │ │ │
│ │ mirrord agent │ │
│ └──────────┬───────────────────┘ │
│ │ 拦截 DB 连接 + steal 指定请求 │
└─────────────┼──────────────────────────────────────┘
│
kind 集群 │
┌─────────────▼──────────────────────────────────────┐
│ namespace: mirrord-demo │
│ │
│ ┌──────────────────┐ ┌────────────────────┐ │
│ │ mirrord-demo Pod │───►│ MySQL Pod :3306 │ │
│ │ NodePort 30080 │ └────────────────────┘ │
│ └─────────┬────────┘ │
└────────────┼───────────────────────────────────────┘
│ kind port mapping
curl :18080 ▼
127.0.0.1:18080
应用本身非常简单:一个 /api/messages/current 的 GET/POST 接口,消息存在 MySQL 里,响应里带一个 handledBy 字段,用来区分请求是被集群 Pod 还是本地进程处理的。
// GET /api/messages/current
{
"message": "hello from cluster",
"handledBy": "cluster" // 或 "local"
}
快速上手:五个命令跑通全流程#
前置依赖:Docker、kind、kubectl、Java 21、Maven、mirrord
# 安装 mirrord(macOS)
brew install metalbear-co/mirrord/mirrord
# 克隆项目
git clone https://github.com/meirongdev/mirrord-demo.git
cd mirrord-demo
# 一键部署:建 kind 集群 → 编译 → 打镜像 → 部署 MySQL + App
make deploy
# 启动本地进程,接入集群(新终端窗口)
make run-local
make run-local 启动后,你会在日志里看到 Spring Boot 正常启动,而且它连上了集群里的 MySQL,不需要本地安装任何数据库。
集群侧需要准备什么?#
这个 Demo 看起来像“本地连集群”,但集群侧其实不需要做很多魔改。最小前提是:
- 能访问目标集群:本地
kubectl config current-context指向目标集群,并且当前身份有权限访问mirrord-demonamespace。 - 目标工作负载已经存在:这里的 target 是
deployment/mirrord-demo。 - 依赖服务已经在集群里可用:这里是
mysql.mirrord-demo.svc.cluster.local:3306。 - 请求要走真实集群网络路径:这个 Demo 用 kind 的宿主机端口映射
18080 -> 30080,再配合应用 Service 的NodePort 30080,让流量按正常路径进入 Pod。 - 允许 mirrord 创建 agent:执行
mirrord exec时,mirrord 会利用当前 kubeconfig 在目标 Pod 一侧拉起mirrord-agent。所以至少需要允许它访问 Kubernetes API,并在目标 namespace 完成这一步。
也就是说,这个项目不需要你先在应用里接 SDK,也不需要额外改 Spring Boot 代码。只要集群里已经有目标 Deployment 和依赖服务,mirrord 就能把本地进程接进来。
make run-local 背后到底执行了什么?#
表面上你执行的是:
make run-local
实际展开后,脚本运行的是:
mirrord exec -f .mirrord/mirrord.json -- \
java \
-Ddemo.app-instance=local \
-Dspring.datasource.url='jdbc:mysql://mysql.mirrord-demo.svc.cluster.local:3306/mirrord_demo?createDatabaseIfNotExist=true&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true' \
-Dspring.datasource.username=demo \
-Dspring.datasource.password=demo \
-Dspring.sql.init.mode=never \
-jar target/mirrord-demo-0.0.1-SNAPSHOT.jar
这几点很关键:
mirrord exec不是去远端启动 JVM,而是在本地启动你的 Java 进程。-f .mirrord/mirrord.json指定目标 workload 和流量规则。--后面仍然是原始本地命令,所以 IDE、断点、热重启这些本地体验都保留着。
核心配置解析:.mirrord/mirrord.json#
{
"target": {
"namespace": "mirrord-demo",
"path": "deployment/mirrord-demo"
},
"feature": {
"network": {
"incoming": {
"mode": "steal",
"http_filter": {
"header_filter": "^x-mirrord-mode: steal$"
}
}
}
}
}
逐字段解释:
target#
"target": {
"namespace": "mirrord-demo",
"path": "deployment/mirrord-demo"
}
告诉 mirrord 要「附着」在哪个工作负载上。mirrord 会在这个 Deployment 的 Pod 旁边注入 agent,本地进程的出站网络流量和入站流量都经过这个 Pod。
path 支持 deployment/<name>、pod/<name>、rollout/<name> 等格式。
feature.network.incoming.mode#
mirrord 有两种处理入站流量的模式:
| 模式 | 行为 |
|---|---|
mirror |
复制一份流量发给本地,Pod 同时处理原始请求(不影响线上) |
steal |
把匹配的流量完全劫持到本地,Pod 不再处理这些请求 |
Demo 使用 steal 模式,但配合了 http_filter 只劫持特定请求,其他流量继续由 Pod 处理。这是生产环境友好型配置的雏形。
http_filter.header_filter#
"header_filter": "^x-mirrord-mode: steal$"
正则表达式,匹配 HTTP 头。只有带 x-mirrord-mode: steal 这个 header 的请求才会被劫持到本地。普通请求继续走 Pod。
为什么用 header filter?
在多人共享集群或预发布环境中,header filter 让你可以在不影响其他人的情况下调试特定请求。流量通过 header 路由,而不是全量接管。
.mirrord/mirrord.json 要不要提交到 Git?#
这个 Demo 里,答案是 要。
原因很简单:mirrord.json 不是某个人的本机临时状态,而是这个项目的共享运行配置,它定义了:
- target 是哪个 Deployment
- 入站流量是
steal还是mirror - 哪些请求会被 header filter 路由到本地
如果不提交,别人拉下仓库后就无法直接执行文档里的:
mirrord exec -f .mirrord/mirrord.json -- ...
更合理的做法是:
- 提交:像
/.mirrord/mirrord.json这种团队共享配置 - 忽略:本机私有、临时、可能带凭据或环境差异的其他 mirrord 文件
steal vs mirror:怎么选?#
mirror 模式
请求 ──────────► Pod (原始处理)
└─────► 本地进程 (副本,不影响响应)
steal 模式(全量)
请求 ──────────► 本地进程 (Pod 不处理)
steal + http_filter 模式
带 header 请求 ──► 本地进程
普通请求 ──────► Pod
mirror 适合:只想观察请求 / 响应,不需要本地进程的返回值影响调用方。比如新功能上线前在 staging 上用镜像流量跑本地代码做压测对比。
steal + filter 适合:需要让本地进程实际处理请求,比如开发新接口逻辑、调试请求处理 bug。加上 filter 可以做到精准路由,不打扰其他请求。
完整调试演练#
假设已经 make deploy 完成,再开一个终端 make run-local。
1. 确认本地进程在接管请求#
curl http://127.0.0.1:8080/api/messages/current
# {"message":"hello from cluster","handledBy":"local"}
handledBy: local 说明这个请求被本地进程处理了。数据库里读出来的内容(hello from cluster)是集群 Pod 之前写进去的——因为本地进程用的是同一个 MySQL。
2. 本地写入,集群可见#
# 通过本地进程写入
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"message":"written by local dev"}' \
http://127.0.0.1:8080/api/messages/current
# 通过集群 NodePort 读取(绕过 mirrord,直接打集群 Pod)
curl http://127.0.0.1:18080/api/messages/current
# {"message":"written by local dev","handledBy":"cluster"}
本地进程写的数据,集群 Pod 立刻能读到。因为 mirrord 让本地进程的 DB 连接打到了集群内的 MySQL,共享同一份数据,没有任何数据同步问题。
3. steal header 请求测试#
# 走集群 NodePort,但带 steal header
curl -H 'x-mirrord-mode: steal' http://127.0.0.1:18080/api/messages/current
# {"message":"written by local dev","handledBy":"local"}
请求从 18080(集群 NodePort)进来,经过 mirrord 的 http_filter 匹配到 header,被劫持到本地进程处理。handledBy: local 证明了流量劫持成功。
kind 的网络特殊性:为什么不用 port-forward?#
这是 Demo 里一个容易踩的坑,值得单独说清楚。
kubectl port-forward 的工作机制:它通过 Kubernetes API Server 开一条隧道,直接连接到 Pod 端口,完全绕过了 Service → iptables → Pod 这条网络路径。
mirrord 的流量拦截点:mirrord 的入站流量拦截是在 Pod 的网络命名空间层面工作的,它 hook 的是从 Service 转发过来的真实流量。port-forward 的流量不经过这条路径,所以 mirrord 的 steal/mirror 对它不起作用。
Demo 的解法:使用 kind 的 extraPortMappings + NodePort Service:
# kind-config.yaml
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 18080
protocol: TCP
# k8s/app-deployment.yaml (Service 部分)
spec:
type: NodePort
ports:
- port: 8080
nodePort: 30080
18080 → kind 容器 30080 → NodePort → iptables → Pod。这条路径上的流量,mirrord 可以正常拦截。
官方文档和这个 Demo 的对应关系#
如果你想把“这个 Demo 为什么能工作”彻底串起来,建议对照看这三篇官方文档:
对应关系如下:
-
Introduction
官方定义了 mirrord 的核心价值:让本地进程运行在云环境上下文里。在这个 Demo 里,这体现在本地 JVM 可以直接访问集群内 MySQL、沿用 Pod 的网络环境。 -
Architecture
这篇解释了mirrord-cli、mirrord-layer、mirrord-agent的分工:CLI 负责启动,layer 注入本地进程并 hook 底层系统调用,agent 在集群侧代理目标 Pod 的上下文。也正因为这样,mirrord exec -- java ...启动的依然是本地进程,但出站网络已经像 Pod 一样工作。 -
Network Traffic
这篇解释了mirror、steal、http_filter.header_filter。本 Demo 用的就是steal + header filter:只有带x-mirrord-mode: steal的请求才会被导向本地进程,普通请求继续由集群 Pod 处理。
这三篇文档连起来看,再回头看 kind + NodePort + header filter 的配置,基本就能完整理解这个 Demo 的工作机制。
应用到真实项目#
从 Demo 扩展到真实项目,mirrord 的配置需要做哪些调整?
1. target 精确到容器
如果 Pod 有多个容器,可以指定:
"target": {
"namespace": "my-namespace",
"path": "deployment/my-service",
"container": "app"
}
2. 环境变量注入
默认 mirrord 会注入 Pod 的环境变量。如果想覆盖某些值(比如把日志级别改成 DEBUG),在本地启动时加 JVM 参数即可,mirrord 不会覆盖显式设置的值。
3. 出站网络:调用其他微服务
不需要额外配置。本地进程发出的 TCP 连接会经过 mirrord 隧道走集群内网,所以 http://order-service.production.svc.cluster.local 这类服务发现 URL 可以直接用。
4. 多人共享集群
使用 steal + http_filter,每个开发者用不同的 header 值:
"http_filter": {
"header_filter": "^x-dev: alice$"
}
调用方在请求头里加 x-dev: alice,流量就只会路由到 Alice 的本地进程。
5. IDE 集成
mirrord 有 IntelliJ IDEA 和 VS Code 插件,可以在 IDE 里直接启动带 mirrord 的运行配置,不需要命令行。IntelliJ 插件支持选择目标 Pod、配置 feature,和 Run Configuration 无缝集成,断点调试体验和本地开发完全一致。
总结#
| 传统方案(build → deploy) | mirrord | |
|---|---|---|
| 验证一次代码改动 | 2–5 分钟 | 秒级(本地 JVM 热重启) |
| 访问集群内服务 | port-forward(有限) | 集群内网完全可用 |
| 本地 IDE 断点 | 需要 remote debug + port-forward | 直接 Run/Debug |
| 对集群影响 | 重新部署(影响其他人) | agent sidecar(可选 filter 隔离) |
| 需要修改应用代码 | 否 | 否 |
mirrord 最大的价值是缩短了代码改动到验证的循环。本地开发环境该有的工具(IDE、断点、热重启)全部可用,但应用运行时的上下文(网络、配置、数据)来自集群。
对于微服务密集、依赖多、集群是唯一完整运行环境的项目,mirrord 是目前体验最接近「本地开发但访问真实上下文」的方案。
Demo 项目地址:github.com/meirongdev/mirrord-demo
跑一遍 make deploy && make validate,整个流程端到端跑通,看完日志基本就能理解 mirrord 的工作方式。