为什么我在本地微服务联调中放弃了 K8s,回归了 Docker Compose?

Lirous.
4/17/2026
9 min read
一次关于微服务本地容器化编排的踩坑复盘。从陷入 K8s 的“云原生焦虑”,到采用手工 Make 脚本配合 Docker Compose,最终回归 KISS 原则,在业务体量与基础设施复杂度之间寻找平衡。

云原生焦虑:从 Docker Compose 到 K8s 的折腾

随着 live-interact-engine 项目的演进,系统终于被按照职责拆分为了 api-servicegift-serviceuser-servicedanmaku-serviceroom-service 等 5 个核心基础服务,底层还依赖了 PostgreSQL、Redis 和 RabbitMQ,并且串联了 Jaeger 作为请求追踪体系。

其实最开始,我就采用了 Docker Compose 来统筹编排本地环境。它就像一个听话的大管家,一键启停各司其职,并没有像传统原始单机跑服务那样“需要开七八个终端窗口手动按顺序启动、并时刻担心连不上基础设施导致 Go 服务 Panic 退出”的痛苦。这套极简流的体验让我能安稳地专注于业务代码开发。

然而,看着各大技术社区铺天盖地的云原生进阶教程,我内心渐渐生出了一种“云原生焦虑”——既然都已经把单体拆成微服务了,连大厂都在向 K8s 全面拥抱,我是不是也必须得上 Kubernetes 才能证明项目的架构“含金量”?

初探 K8s:强大的编排能力与联调阵痛

Kubernetes 毫无疑问是当今云原生微服务编排的事实标准。为了让项目直接对齐工业级架构,我果断用 minikube 在本地拉起了一个集群,开始将服务全面 K8s 化。

在这个过程中,我深刻体会到了 K8s 设计的严谨性。为了让一个简单的 Go 服务连上 PostgreSQL,不再是简单敲几行 Docker 端口映射,而是需要清晰地声明 Deployment、Service,挂载 PVC,以及梳理 NodePort 和 ClusterIP 等网络拓扑机制。这些在生产环境中极具价值的隔离与容错设计,在本地高频开发时,却带来了一条极其繁琐的联调链路。每次修改哪怕一行 Go 代码,常规操作下我都必须:

  1. 重新编译二进制文件。
  2. 重新打包 Docker 镜像。
  3. 将镜像推送到 minikube 的本地 registry(或者手动 load 进去)。
  4. 执行 kubectl rollout restart 或是删除旧 Pod 让它重启。

惊艳的 Tilt:试图抹平云原生的门槛

为了解决这令人抓狂的开发循环,我引入了云原生本地协同神器 Tilt

不得不说,Tilt 的能力是极其惊艳的。只需编写一份简单的 Tiltfile,它就能智能监控本地代码变更,自动拦截、快速重塑容器并无缝更新到 K8s 集群中,实现了非常顺滑的热重载(Hot Reload)。当看到代码一保存,K8s 集群中的 Pod 就全自动完成了热替换,那一刻,我真切地感受到了顶级 DevEx(开发者体验)工具链的强大威力,它在努力用优雅的生态抹平基础设施底层庞大的复杂性。

务实的觉醒:KISS 原则与沉没成本

新鲜感褪去后,我不得不面对一个很扎心的现实:对于我目前的状态而言,这套云原生方案实在太重了。

为了跑起这几个拆分出来的微服务及中间件依赖,单是一个空白的 minikube 就要占用我电脑庞大的内存和计算资源;每次想新增一个简单的环境变量用于测试,我都要去翻阅冗长的 YAML 规范并重新执行部署流程。我的绝大部分精力,就这样从“写好每一行代码”本身,深陷入了复杂繁琐的运维泥潭中。

我开始反思:作为一个由单人研发维护、流量和负载目前也远没有达到单节点计算极限的微服务实践项目,我真的需要在本地开发环境强行吃下 K8s 动态扩缩容、资源限制、滚动更新和极致自愈能力这些“屠龙技”吗?

答案是否定的。

有个十分经典的编程哲学叫 KISS (Keep It Simple, Stupid)。最好的架构设计,永远不是盲目甚至虚荣地去堆砌最顶级的技术栈,而是它刚好最匹配你当前的开发节奏与运维心智

返璞归真:Docker Compose + Makefile 的极简流

最终,我选择将那些长篇大论、堆积成山的 K8s YAML 文件全部封存(现在它们正静静地躺在我项目中的 infra/development/k8s/ 目录下吃灰),全面回归并拥抱了极简的 Docker Compose,并配以最为硬核且直白的 Makefile 脚本进行手动构建。

我抛弃了所有花里胡哨的“热重载(Hot Reload)”魔法,因为我发现:魔法越多,出 Bug 时越难排查到底是谁的问题。

回看这精简却直中痛点、维护性极强的配置片段:

# docker-compose.yml (gift-service 配置片段)
services:
  gift-service:
    build:
      context: .
      dockerfile: Dockerfile-gift
    container_name: gift-service
    ports:
      - "9096:9096" # gRPC API 端口
    environment:
      - GRPC_ADDR=:9096
      - DATABASE_DSN=postgres://postgres:password@postgres:5432/gift_service?sslmode=disable
      - REDIS_ADDR=redis:6379
      - JAEGER_ENDPOINT=jaeger:4318
      - ENT_MODE=dev
    depends_on:
      jaeger:
        condition: service_started
      postgres:
        condition: service_started
      redis:
        condition: service_started
      rabbitmq:
        condition: service_started
    networks:
      - live-interact

配合根目录下的 Makefile,我的开发流变得极其踏实可控:

# Makefile
.PHONY: run restart-gift

# 一键拉起所有基础设施和微服务
run:
	docker-compose up -d

# 修改代码后,手动重新构建并重启指定服务
restart-gift:
	docker-compose build gift-service
	docker-compose rm -fs gift-service
	docker-compose up -d gift-service

我的日常开发流程又回到了那个让人心生愉悦的轻盈状态:

  1. 每天早上来到工位,一行 make run 一键拉起整个局域网链路的基础设施应用大盘。
  2. 代码改动后,并不需要随时依赖框架自动 Reload 导致偶尔的无响应假死。而是在我认为功能阶段性闭环时,在终端敲击一句 make restart-gift,静待几秒钟,容器就会完成重新构建并崭新如初。
  3. 如果只是单纯打断点调试某几个方法函数,甚至可以利用宿主机直接连接那几个暴露了端口的中间件容器网段,完全跳出容器束缚去 Debug。
  4. 清爽,踏实,没有黑盒魔法心智负担,并且 完全足够用

结语:架构其实就是关于边界的权衡 (Trade-off)

这次的“离家出走再回来”折腾并非只是一场毫无意义的闹剧。它切实让我对云原生底层的网络体系、资源调度以及服务发现有了最深刻的实战认知,不再去畏惧“K8s 是一门玄学”这样虚无缥缈的神话。

更重要的是,它切切实实地给我上了一堂极其生动而且昂贵的架构课:千万不要让运行基础设施的复杂度,强留在系统本身解决业务核心时的复杂度之上。

等到未来哪一天,当 live-interact-engine 的并发压力真正冲破了单机的上限瓶颈、被要求部署高可用流控以满足跨地域的场景时。到了那个时候,顺理成章地唤醒硬盘深处那些 Kubernetes YAML 配置文件,也不过是一行命令的事。在此之前,只管专注于代码本身——写好每一行业务逻辑。

K8s 会一直在那儿,等你需要的时候。

评论区