本文简单梳理一下一套快速搭建后台服务的框架,包含rpc、go、k8s、docker、微服务等技术,以及数据存储、服务组织、入口路由、镜像构建、镜像编排和配置下发等工作要点。对于个人开发者与小型项目而言,应该够用了。
总览
在本文中,我们将一起从服务骨架到发布流水线,了解一套基于 Go 的后台技术栈。整条链路的所有组件要么开源,要么可以直接通过 docker 命令拉起,因此任何人都能照着搭出一套能用的后台基础设施。本文只讨论架构骨架,不涉及任何业务。
整条链路为:
(tRPC-Go + Gin)
上面两条线是本文的全部内容,其中第一条线是运行时链路,第二条线是交付链路。各组件的技术选型与开源情况如下:
| 层 | 选型 | 开源 / 部署方式 |
|---|---|---|
| 语言 | Go 1.22+ | 开源 |
| 服务框架 | tRPC-Go + Gin | 开源(trpc-group/trpc-go,Apache 2.0) |
| 数据存储 | MySQL 8 / Redis 7 | docker run mysql:8 / docker run redis:7 |
| 入口网关 | Nginx / ingress-nginx | 开源 |
| 容器化 | Docker | Docker 社区版,镜像仓库用 docker.io 或自建 Harbor |
| 编排平台 | Kubernetes + Helm | minikube / k3s / kind 单机拉起集群,Helm 开源 |
| 配置中心 | Nacos | docker run nacos/nacos-server |
服务骨架:Go + tRPC-Go + Gin 中间件
我们先从整条链路的中心,即服务本身说起。Go + tRPC-Go + Gin 中间件是相对流行的微服务框架,其中:
- Go 是编程语言(说了等于没说),其优势在于:编译出的是单一静态二进制,运行镜像只需要二进制加配置文件;每个 goroutine 初始栈仅几 KB,其高并发性能适合后台服务的网络 I/O 密集特性;吞吐性能强,并且在事实上,我们后续要说的 Docker、Kubernetes、etcd、Prometheus 全是 Go 实现的。
- tRPC-Go 是服务容器,负责进程生命周期、优雅退出(收到退出信号后排空存量连接再关闭)、统一配置加载和结构化日志;
- Gin 是 HTTP 引擎,负责路由匹配、参数绑定和灵活的中间件链,一般来说,中间件链只放横切能力,也就是 Recovery、Logger、Auth 这类与业务无关的通用处理,业务逻辑不允许进中间件。
两者的组合方式很简单,我们将 Gin 引擎作为 HTTP Service Handler 挂载进 tRPC-Go 服务容器。服务的代码组织采用四层结构,依赖方向为从外向内,禁止反向依赖,给出一个代码结构示例:
1 | cmd/ # 入口层:main.go 只引导启动,不含业务逻辑 |
分层里最重要的一条约定是接口与实现分离,譬如,在 logic 目录下 demo.go 定义接口,demo_impl.go 提供实现,router 层只依赖接口。这样每个服务都能独立测试,替换实现不影响上层。
回到组合方式,把 Gin 引擎挂进 tRPC-Go 容器的完整代码如下:
1 | func main() { // 程序入口 |
tRPC-Go 由腾讯开源,仓库 github.com/trpc-group/trpc-go,官方文档在 trpc.group。
数据层:MySQL & Redis
数据层是整条链路里最不能出错的环节,分工原则也很简单,即MySQL 存事实,Redis 存热数据。事实指业务实体的最终状态,比如账户余额、订单状态,这类数据必须满足 ACID,靠 MySQL 的事务保证;热数据指被高频读取、可以随时重建的内容,比如缓存、会话、计数,放 Redis 用 TTL 管理生命周期。
MySQL 侧有三个机制是标配:连接池、事务、悲观锁。连接池的初始化和参数决定并发上限;事务保证多步写操作的原子性;悲观锁(SELECT FOR UPDATE)用于转账、转让这类并发写场景,在事务内锁住目标行,防止竞态:
1 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 连接 MySQL,dsn 含地址、账号、密码 |
Redis 侧有两个机制:TTL 精确控制和 fail-open 降级。一个典型的例子是对象存储的预签名 URL:签名一次 24 小时有效,把它缓存进 Redis 时 TTL 设成 23 小时,留 1 小时缓冲,避免客户端拿到一个即将过期的地址。这里有一个必须遵守的原则:缓存是性能优化,不是核心链路。Redis 挂了要能退化为每次实时签名或直接查库,功能不中断,只是多一次调用开销,绝不能让缓存故障变成服务故障。
数据层用一条 docker 命令就能拉起:
1 | docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=xxx mysql:8 # 拉起 MySQL 8:-d 后台运行,-p 映射端口,-e 设置 root 密码 |
同时,业务上线后,如果面临多 Pod 部署,那么数据的竞态问题需要额外考虑,必须谨慎对待。
入口层:Nginx
服务跑起来之后,对外只需要暴露一个域名和一个端口,多个服务按路径前缀分发到各自容器,这一部分工作由Nginx完成。
最小的 Nginx 配置只需要 server 和 location 两层,location 的前缀规则与服务路由前缀一一对应:
1 | http { # 全局 http 配置块 |
本地或单机形态下,Nginx 以容器或系统包方式运行,而如果使用 Kubernetes,入口层的职责由 ingress-nginx 接管,nginx.conf 里的 location 前缀规则原样迁移成 Ingress 资源的 path 规则,一一对应。
构建层:Docker 两段式镜像
镜像的构建策略决定了交付物的大小和构建速度。这里采用两段式构建:
- 先维护一个 builder 基础镜像,预装 Go 工具链并预缓存依赖,每个服务镜像从它编译出二进制。
- 再把二进制和配置文件装进一个精简运行镜像。
构建依赖大而运行依赖小,分离之后运行镜像小、传输快、攻击面小。
给出一个示例dockerfile配置:
1 | # 编译阶段:产出可执行二进制 |
我们可以将镜像构建和推送统一收敛到 build.sh 脚本,直接通过一条命令完成:
1 | ./docker/build.sh -s demosvr --push --tag v1.3.0 # 构建 demosvr 镜像并推送,镜像 tag 为 v1.3.0 |
镜像仓库用 docker.io 或自建 Harbor,两者都是公开可用的方案。
平台层:Kubernetes 与 Helm
Kubernetes 是容器编排平台,管理容器集群的调度、自愈、扩缩容和服务发现。它把一组容器打包成最小调度单元 Pod,围绕 Pod 提供四类核心能力:
- 调度(决定 Pod 跑在哪台机器上)
- 自愈(Pod 挂了自动重建)
- 扩缩(按副本数增减 Pod)
- 服务发现(Service 给一组 Pod 提供稳定的访问入口)。
我们后面要讲的滚动更新也来自它,即按策略逐个替换 Pod,全程不中断服务。
Helm 是 Kubernetes 的包管理工具。K8s 里每个组件都对应众多 yaml 清单(Deployment、Service、Ingress、ConfigMap、Secret),直接维护的话,环境之间的差异无从处理。Helm 把一组清单打包成一个 Chart,清单里的差异部分用模板变量占位,由 values 文件在不同环境注入不同的值。
一个服务在集群里需要的最小资源集是六个,透明各自的职责如下表所示:
| 资源 | 职责 |
|---|---|
| Deployment | 无状态工作负载,管理副本和滚动更新 |
| Service | 集群内稳定的访问入口(ClusterIP) |
| Ingress | 对外入口,与第三章的 Nginx 职责对应 |
| ConfigMap | 非敏感配置 |
| Secret | 敏感配置(密码、密钥) |
| HPA | 按指标自动扩缩(可选) |
Helm 的价值在于 values 分层,默认配置写在 values.yaml,各环境用独立文件覆盖,发布时局部参数用 --set 覆盖,三者的优先级从低到高,下面是一些常见的哦诶之文件:
1 | helm/ # Helm Chart 根目录 |
滚动更新是这套栈里"更新不中断"的机制来源,配置在 Deployment 模板里:
1 | strategy: # 更新策略 |
新 Pod 先起来,readinessProbe 通过后再把流量切过去,旧 Pod 等存量请求排空后再销毁,这就是零停机更新的全部机制。环境隔离也是平台层的职责,各环境在配置来源、镜像 tag、副本数三个维度上分层:
| 环境 | 配置来源 | 镜像 tag | 副本 |
|---|---|---|---|
| local | 本地 .env | 本地编译 | 1 |
| dev | docker-compose | test | 1 |
| test | ConfigMap + Secret | test | 1-2 |
| staging | ConfigMap + Secret | rc-x.x.x | 2 |
| prod | ConfigMap + Secret | v-x.x.x | 2-5 |
配置层:三层配置体系与 Nacos
服务运行需要的参数由配置层提供与管理。这里采用三层结构,每层的变更成本不同:
| 层 | 内容 | 载体 | 变更成本 |
|---|---|---|---|
| 框架级 | 端口、日志、超时 | 镜像内 trpc_go.yaml | 重新构建镜像 |
| 业务级 | DB 地址密码、API Key | .env / K8s Secret | 更新 Secret 并重启 |
| 运行时动态 | 开关、阈值、模型参数 | Nacos | 热更新,不重启 |
Nacos 是阿里巴巴开源的动态服务发现与配置管理平台,名字取自 Naming 和 Configuration 两个词。它提供两大能力:服务注册与发现、配置管理。在这套架构里,服务发现已经由 Kubernetes 的 Service 和 Ingress 承担,所以这里我们只使用它的配置管理能力。当然,如果不需要热更新的配置项,也可以直接使用 Kubernetes 自带 ConfigMap
Nacos提供三个核心能力:命名空间隔离(dev、prod 各一套配置互不干扰)、配置热更新(应用监听变更立即生效)、可视化控制台。拉起它只需要一条命令:
1 | docker run -d -p 8848:8848 -e MODE=standalone nacos/nacos-server # 单机模式拉起 Nacos,控制台与 API 端口 8848 |
需要说明 tRPC-Go 配置插件生态的现状:官方文档的插件矩阵里,Config 数据源 tested 的是 etcd(trpc-ecosystem/go-config-etcd),Naming 是 Polaris;Nacos 在 tRPC 生态里主要覆盖 Java 端。Go 端接入 Nacos 有两条路径:一是用社区实现,二是基于 tRPC-Go 的配置 Provider 接口自研适配,该接口只要求加载与监听两个能力,实现成本很低。如果不想做任何适配,直接用官方 tested 的 etcd 也能获得同样的热更新能力,代价是少了可视化控制台。
配置的变更路径由此分成两类:
- 改运行时参数使用 Nacos 热更新
- 改框架级配置使用 ConfigMap 更新随后进行
kubectl rollout restart
拉起全流程:从代码到服务的闭环
了解每一个层次的技术选型之后,我们可以尝试将它们串成一条完整的链路,首次拉起一共七步,而每一步的产物恰好是下一步的输入:
| 步骤 | 动作 | 产物 |
|---|---|---|
| 1 | 本地写好 Go 代码 | 源码 |
| 2 | build.sh 构建并推送镜像 | registry 里的新 tag 镜像 |
| 3 | Nacos 写好业务配置 | 配置中心里的配置 |
| 4 | helm upgrade 挂载 Deployment | 新 Pod |
| 5 | Pod 启动读 trpc_go.yaml,从 Nacos 拉配置 | 服务就绪 |
| 6 | Ingress 路由生效 | 对外可访问 |
| 7 | 健康检查确认 | 服务上线 |
日常更新则只有两条路径,修改业务代码后提交新镜像,并滚动更新,而修改参数可以直接通过 Nacos 热更新。
需要额外注意的是,每次发布推新 tag 时,绝不可以覆盖旧 tag,原因在于 Kubernetes 的回滚机制依赖镜像引用,执行回滚时平台去仓库拉取旧 tag,如果它已经被新代码覆盖,拉到的还是新代码,回滚名存实亡。因此,我们可以配合代码的 git 仓库,每次修改业务代码后,push到仓库,使用该次 commit 号作为tag,更有利于管理。
说到git仓库,对于一个后台系统的代码开发,可以给每个微服务拉去一个基于主分支的业务分支,在业务分支上开发,测试与验收后合并入主分支,合并前,记得先合并主分支远端代码到业务分支并处理冲突。
从零到上线的完整命令序列的示例如下:
1 | # 首次拉起 |
往这套骨架里加一个新服务,需要动的地方只有七处,全部是机械操作:
cmd/<newsvr>/main.go:入口internal/app/<newsvr>/:应用层(Register + router + logic)configs/<newsvr>/:trpc_go.yaml 与 .env 模板docker/Dockerfile.<newsvr>:构建镜像docker/build.sh:注册服务名helm/values.yaml:添加服务配置docker/nginx.conf或 Ingress:添加路由规则
构建发布流水线
到这一章,所有环节还是手工命令,为了方便与规范化,我们可以使用流水线将一系列动作压缩为一次触发,阶段划分如下:
打 tag
新 tag
(有 schema 变更时)
触发滚动
流水线成立有两个前提,都是前文已经铺垫过的规则,一是tag 不可变,流水线强制每次发布使用新 tag,从机制上禁止覆盖;二是服务无状态,数据全部外置到 MySQL 和 Redis,Pod 只是无状态执行体,滚动更新才不会丢状态。
更新镜像本身不会修改数据库,MySQL 和 Redis 是独立服务,数据原样保留。真正有风险的是 Schema 变更,如果新代码改了表结构,而迁移在应用启动时执行,滚动窗口内新旧代码并存,旧代码读写新结构就会出错。因此,我们可以采用 Expand-Contract模式,即先发布只做加法的迁移(加列带默认值、加索引),新旧代码都能读写;全量流量切到新版本并稳定一个周期后,再删旧列和旧索引。迁移本身要独立于应用启动,作为流水线里的单独一步,避免多副本同时启动时并发执行迁移。
滚动窗口内还有两个瞬态问题需要注意,长事务在 Pod 收到退出信号时会被中断回滚,这由 tRPC-Go 的优雅退出处理,它先排空存量连接再关闭进程;定时任务可能在新旧副本之间重叠执行,任务本身需要幂等或加锁兜底。
落地形态从脚本级开始。给 build.sh 加一个 deploy 参数,把"构建、推送、更新"这三步绑定为一条命令:
1 | ./docker/build.sh -s demosvr --push --deploy --tag v1.2.0 --env prod # 构建、推送镜像并触发部署,目标环境 prod |
再进一步是 CI 级,提交 tag 即触发整条流水线。一个最小的 GitHub Actions 定义如下,核心只有两步:
1 | name: deploy # 工作流名称 |
GitLab CI、Jenkins 的写法同理,都是同一个阶段序列。同时,流水线末尾必须预留回滚入口,采用helm rollback 或切回旧 tag,一条命令即可完成回滚,不需要重新构建、重新推送镜像(因为旧镜像已经在仓库里了)。
如果希望连"打 tag"这一步都省掉,让平台监听镜像仓库、发现新 tag 自动滚动,可以引入 Flux 的 image automation 或 Argo CD Image Updater,这里我们就不详细叙述了。
参考文章
[1] tRPC-Go 官方文档[EB/OL]. https://trpc.group/docs/languages/go/.
[2] trpc-group/trpc-go[EB/OL]. https://github.com/trpc-group/trpc-go.
[3] Gin Web Framework[EB/OL]. https://gin-gonic.com/.
[4] GORM[EB/OL]. https://gorm.io/.
[5] MySQL Docker Official Image[EB/OL]. https://hub.docker.com/_/mysql.
[6] Redis Docker Official Image[EB/OL]. https://hub.docker.com/_/redis.
[7] Nginx[EB/OL]. https://nginx.org/.
[8] ingress-nginx[EB/OL]. https://github.com/kubernetes/ingress-nginx.
[9] Docker[EB/OL]. https://www.docker.com/.
[10] Kubernetes[EB/OL]. https://kubernetes.io/.
[11] Helm[EB/OL]. https://helm.sh/.
[12] Nacos[EB/OL]. https://nacos.io/.
[13] minikube[EB/OL]. https://minikube.sigs.k8s.io/.
[14] trpc-ecosystem/go-config-etcd[EB/OL]. https://github.com/trpc-ecosystem/go-config-etcd.
[15] Flux Image Automation[EB/OL]. https://fluxcd.io/flux/guides/image-update/.
[16] Argo CD Image Updater[EB/OL]. https://argocd-image-updater.readthedocs.io/.