云原生焦虑:从 Docker Compose 到 K8s 的折腾
随着 live-interact-engine 项目的演进,系统终于被按照职责拆分为了
api-service、gift-service、user-service、danmaku-service、room-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 代码,常规操作下我都必须:
- 重新编译二进制文件。
- 重新打包 Docker 镜像。
- 将镜像推送到 minikube 的本地 registry(或者手动 load 进去)。
- 执行
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
我的日常开发流程又回到了那个让人心生愉悦的轻盈状态:
- 每天早上来到工位,一行
make run一键拉起整个局域网链路的基础设施应用大盘。 - 代码改动后,并不需要随时依赖框架自动 Reload 导致偶尔的无响应假死。而是在我认为功能阶段性闭环时,在终端敲击一句
make restart-gift,静待几秒钟,容器就会完成重新构建并崭新如初。 - 如果只是单纯打断点调试某几个方法函数,甚至可以利用宿主机直接连接那几个暴露了端口的中间件容器网段,完全跳出容器束缚去 Debug。
- 清爽,踏实,没有黑盒魔法心智负担,并且 完全足够用。
结语:架构其实就是关于边界的权衡 (Trade-off)
这次的“离家出走再回来”折腾并非只是一场毫无意义的闹剧。它切实让我对云原生底层的网络体系、资源调度以及服务发现有了最深刻的实战认知,不再去畏惧“K8s 是一门玄学”这样虚无缥缈的神话。
更重要的是,它切切实实地给我上了一堂极其生动而且昂贵的架构课:千万不要让运行基础设施的复杂度,强留在系统本身解决业务核心时的复杂度之上。
等到未来哪一天,当 live-interact-engine
的并发压力真正冲破了单机的上限瓶颈、被要求部署高可用流控以满足跨地域的场景时。到了那个时候,顺理成章地唤醒硬盘深处那些 Kubernetes
YAML 配置文件,也不过是一行命令的事。在此之前,只管专注于代码本身——写好每一行业务逻辑。
K8s 会一直在那儿,等你需要的时候。