ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Go语言构建个人微服务技术栈:网关、认证与部署实践

基于Go语言构建个人微服务技术栈:网关、认证与部署实践 去年年底清点服务器发现手头七八个小服务各自为政登录逻辑写了好几套日志格式每个仓库都不一样任务调度有的在 crontab 里有的在代码里轮询还有的靠人肉跑脚本。改一个用户密码的字段得挨个项目去动代码部署的时候又得记着每台机器上的环境变量。那阵子我一直在想能不能把反复出现的这些基础能力抽出来做成一套统一的东西。于是就有了 gstack。gstack 不是一个开源框架也不是什么重磅级中间件它是我给自己的一套“个人服务技术栈”。用 Go 语言做底座把网关、认证、配置、日志、任务队列、部署这几块固定能力拼装成一套可复用的骨架。核心目标很朴素新开一个业务项目时不重新写登录、不重新配日志直接按 gstack 的方式把一个服务拉起来接上统一入口然后专注业务本身。这套东西适合谁如果你是 Go 开发者或者自己维护几个小服务、小应用正在头疼“为什么每个项目都要重复做一遍基建”那这篇记录应该能给你一些思路。文里我会讲清楚每个部分为什么这么选、怎么落地、踩过哪些坑以及遇到问题该怎么查。1. gstack 是什么从名字说起拆解我的选型思路1.1 名字由来与核心定位“gstack”这个名字是我自己起的。g 有三重含义第一重是 Go因为整套栈以 Go 为编程底座第二重是 Gateway因为所有流量的统一入口是网关第三重是 General它是一套通用的个人服务骨架而不是某一个专项工具。需要注意gstack 不是把一堆开源软件装在一起就叫技术栈它更接近一套固定的目录约定、一组共享的基础库、几个常驻服务的组合方式。我的划分是gs-gateway 负责流量入口gs-auth 负责身份认证gs-config 负责配置下发gs-task 负责异步任务gs-log 负责日志聚合再加上最外层的 Docker Compose 编排和 Makefile 脚本。业务服务挂在网关后面按固定格式接入就完成了一次接入。这个定位很重要。一旦你明确“技术栈是固定的”做选择的标准就变了。你不追求某个组件在所有维度上最强你追求的是整条链路的一致性接入方式一致、日志一致、部署一致、排查路径一致。很多个人项目最后变成一团乱麻不是因为某个单点做错了是因为每个点都是各写各的。1.2 为什么选 Go 而不是 Node 或 Python先给结论选 Go 是因为它在资源占用、部署形态、并发能力这三个维度上最匹配个人服务器和小型团队的现状。个人服务器普遍内存不大2G 或者 4G 是常事。Go 编译出来是单个二进制不依赖运行时环境一个服务基础内存占用在几十 MB 级别。对比 Node 要带运行时和 node_modulesPython 要处理虚拟环境和依赖版本部署摩擦一下子小了很多。并发模型也是现实需求。网关要同时维持大量 HTTP 连接任务消费要处理持续的队列读取这两个场景下 goroutine 的性价比极高。我用 Node 写并发不是不行但回调、事件循环这些心智负担长期维护下来并不轻松。Python 就更麻烦真要压并发还得靠 asyncio学习成本和排错成本都不低。还有生态的克制性。Go 标准库覆盖了 HTTP 服务器、JSON、模板、TLS很多基础功能不用引第三方包版本升级也不会像 Node 生态那样频繁破坏 API。对个人项目来说这意味着每个季度只需要做一次小版本升级而不是整天处理依赖问题。附一张当时的对比表现在看依然有效维度GoNode.jsPython部署形态单二进制拷贝即可运行需 runtime node_modules需解释器 虚拟环境内存占用低基础服务几十 MB中等较高并发模型goroutine语言级原生事件循环asyncio 或进程模型生态稳定性标准库强第三方少而精丰富但依赖迭代快丰富但环境问题多心智负担较低容易维护中等中等当然这绝不是“Go 天下第一”的意思。你的项目如果大量依赖机器学习生态那 Python 仍然是对的选择如果你做前端全栈Node 可以让前后端语言统一。我是做通用业务服务居多后端接口、定时任务、消息消费、部署脚本都要照顾Go 是权衡之后最省心的选择。2. 整体设计与服务拆分gstack 到底由哪几块组成2.1 核心组件清单gstack 的组件清单我控制在六个原则是“每个组件只干一类事边界清晰”。第一是 gs-gateway它是所有 HTTP 请求的唯一入口。统一做身份校验、限流、路由转发。我用 Go 标准库 net/http 加反向代理实现没有额外引入 NGINX。理由很直接少一个外部依赖就少一个出问题的点而且网关里要接认证、要接限流、要打链路日志用 Go 自己写反而灵活。第二是 gs-auth身份认证中心。负责注册、登录、发 JWT、刷新 token、会话管理。它只对内提供接口外部流量不直接访问。第三是 gs-task基于 Redis 的轻量任务队列。业务服务需要做异步处理时往 Redis 列表里推一条任务worker 消费处理。没有引入 Kafka 或 RabbitMQ原因后面细说。第四是 gs-config配置下发中心。它维护一份集中的 YAML 配置各服务启动时拉一次运行中每隔一段时间检查更新更新后通过 atomic.Value 热加载。第五是 gs-log日志聚合服务。它不是重型的 ELK而是一个日志接收端点业务服务把结构化日志统一投递过来按天落盘配合 grep 和简单脚本排查。第六是 Docker Compose 编排与 Makefile 脚本。这一层不写代码但它是整个栈的“操作界面”build、up、down、logs 全由它统一管理。每个组件的选型清单如下组件职责技术选型对外暴露gs-gateway路由、鉴权、限流net/http ReverseProxy8080gs-auth登录、JWT 签发Go PostgreSQL内网 gRPCgs-task异步任务队列Redis List Worker内网gs-config配置中心YAML HTTP 拉取内网gs-log日志聚合JSON Lines 落盘内网deploy编排部署Docker Compose Makefile服务器这个拆法可能被老手说“简单得不够看”但对我来说刚刚好。个人项目最怕过度设计微服务拆到十几个每个服务就一个接口治理成本比业务开发还高。gstack 保持六块每块能独立演进又足够精简到一个人维护得过来。2.2 服务间通信为什么选 gRPC 而不是 REST内部服务之间我统一走 gRPC对外接口则全部收敛到网关转成 HTTP。这个决策经历了两次返工。第一次返工前内部服务之间也是 REST。当时的问题很典型服务间调用的接口没有强约束A 服务调 B 服务的接口字段名靠文档和默契维护B 改一个字段A 可能过好久才发现。更要命的是序列化格式不统一有的地方返回 JSON有的地方带多余包装排错时人脑要来回翻译。换到 gRPC 之后最直观的改善是接口定义变成了 .proto 文件字段、类型、版本全在里面。改接口时 protoc 会重新生成代码编译不通过就说明有地方没适配。配合 gRPC 自带的 deadline、错误码、metadata 透传链路排查也顺了很多。一个简单的 proto 示例长这样syntax proto3; package gs.auth.v1; message LoginRequest { string username 1; string password 2; } message LoginResponse { string access_token 1; string refresh_token 2; } service AuthService { rpc Login(LoginRequest) returns (LoginResponse); }生成代码时我用 buf 管理依赖和编译命令大致是buf generate或者直接用 protoc 手动编译protoc --go_out. --go-grpc_out. \ --go_optpathssource_relative \ --go-grpc_optpathssource_relative \ api/auth/v1/auth.proto有一点要强调gRPC 不适合直接暴露给浏览器和公网。它基于 HTTP/2调试工具链不如 HTTP/1.1 成熟反向代理配置也麻烦一些。所以我的策略是“内网 gRPC外网 HTTP”网关负责把外部请求转成内部 gRPC 调用。这样两边各取所长外部用户拿到的是熟悉的 REST 接口内部服务之间拿到的是强类型契约。3. 核心细节解析与实操要点让你少踩一半坑的细节3.1 配置管理环境变量不够用之后怎么办配置这件事我一开始特别天真直接用环境变量每个服务启动前在 systemd 文件里写一堆 export。后来服务多了环境变量管理变成了灾难哪些变量是新的哪些配置已经废弃改动一个公共配置要重启多少个服务于是我把配置收敛成 gs-config 中心化管理。统一路径是这样的所有服务启动时先向 gs-config 注册携带服务名和环境名拿到自己的 YAML 配置。gs-config 本身维护一份带版本号的配置表服务端支持热更新。配置下发有个关键细节服务必须保留本地缓存。gs-config 挂了服务不能跟着挂要用启动时或最近一次拉到的配置继续运行。我在代码里用 atomic.Value 存整个配置对象后台 goroutine 定时拉取有新版本就替换指针请求处理时统一从 atomic.Value 读配置。下面是一段简化版的热更新逻辑type Config struct { Version string App AppConfig } func loadConfigLoop(ctx context.Context, cfgPtr *atomic.Value) { ticker : time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case -ctx.Done(): return case -ticker.C: cfg, err : fetchRemoteConfig() if err ! nil { log.Warn(fetch remote config failed, err, err) continue } cfgPtr.Store(cfg) } } }配置文件里支持占位符比如${DB_DSN}这样数据库密码这类敏感信息仍然放在环境变量或部署密钥里不进配置文件本身。整体原则是非敏感配置集中管理敏感配置留到最后一步注入。实操中最容易踩坑的是“以为改了配置就生效”。如果你的服务没有实现热更新逻辑那改配置就必须重启。gstack 的统一约定是配置中心只负责“下发新版本”服务端是否热更新由各服务自己决定。我会把所有 gs- 前缀服务都实现热更新业务服务则看情况有时直接重启更省事。3.2 日志与可观测性统一 format、traceId 贯穿日志是个人项目里最容易被忽视、又最要命的环节。我在 gstack 里定了两条硬性规矩第一所有服务必须输出结构化 JSON 日志第二每次请求从网关入口生成一个 trace_id贯穿整条链路。为什么必须是 JSON因为 JSON 字段可以被 jq、grep 直接解析调试时按 service、level、trace_id 过滤非常方便。Go 1.21 之后标准库的 log/slog 就支持 JSON Handler我全栈统一用它。一个典型的日志行长这样{time:2024-06-15T10:30:00.123Z,level:INFO,service:gs-gateway,trace_id:abc123def,path:/api/v1/order,method:GET,status:200,duration_ms:23}trace_id 的透传是链路排查的关键。外部请求到达网关时如果没有 X-Trace-ID网关就生成一个如果有就透传下去。网关调用内部 gRPC 时把 trace_id 塞进 gRPC 的 metadata下游服务从 metadata 里取出来继续传给自己的日志。中间件代码大致是这样func TraceMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { traceID : r.Header.Get(X-Trace-ID) if traceID { traceID generateTraceID() } ctx : context.WithValue(r.Context(), TraceKey{}, traceID) w.Header().Set(X-Trace-ID, traceID) next.ServeHTTP(w, r.WithContext(ctx)) }) }转发到 gRPC 时把 trace_id 放进 metadatamd : metadata.Pairs(x-trace-id, traceID) ctx : metadata.NewOutgoingContext(ctx, md) resp, err : client.Call(ctx, req)链路追踪还有指标部分。我不引完整 APM只在每个服务暴露一个 /metrics 端点用 Prometheus 拉取。常用的指标是 QPS、P99 延迟、队列长度、goroutine 数量。启动参数里加一个 pprof 端点排查性能问题时直接抓火焰图比在日志里瞎猜快得多。有一个让我记忆很深的教训有段时间服务偶发变慢日志里看不到任何错误。后来加上 pprof 才发现热点落在 JSON 序列化上一个循环里反复 Marshal 了同一个对象。这类问题不做性能观测靠人肉 review 要浪费很长时间。3.3 信号处理与优雅退出不能让服务“死”得很难看另一个高频问题是没有正确处理退出信号。比如 docker stop 的时候默认发 SIGTERM如果你的程序直接默认退出正在处理的请求会被打断Redis 里的任务也可能会处理一半就丢。gstack 的统一做法是用 signal.NotifyContext 接管 SIGTERM 和 SIGINT然后给每一类组件提供优雅退出方法HTTP 服务器用 ShutdowngRPC 服务器用 GracefulStop队列消费者则是退出消费循环前先把当前消息处理完。主函数的骨架长这样func main() { ctx, stop : signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() srv : http.Server{ Addr: :8080, Handler: buildHandler(), } go func() { if err : srv.ListenAndServe(); err ! nil err ! http.ErrServerClosed { log.Error(http server error, err, err) } }() -ctx.Done() log.Info(shutting down ...) shutdownCtx, cancel : context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err : srv.Shutdown(shutdownCtx); err ! nil { log.Error(forced shutdown, err, err) } }队列消费者在收到退出信号后要先停止从 Redis 拉新任务同时把当前正在处理的任务跑完再退出。这里有一个约定任务处理方法不该无限阻塞最好给每个任务设置一个超时避免优雅退出也等不到它结束。docker-compose 的 stop_grace_period 也要配好我一般设 30s给服务留足处理时间容器自己倒计时到了才强制杀。4. 实操过程从零把 gstack 跑起来4.1 目录结构与代码组织gstack 采用 monorepo 结构所有服务放在同一个仓库里。这样做的最大好处是proto 文件和公共库只在仓库里维护一份改动之后一次编译就能发现所有受影响的服务。目录结构如下gstack/ api/ # 所有 proto 文件 auth/v1/auth.proto task/v1/task.proto common/v1/common.proto cmd/ # 服务入口 gs-gateway/main.go gs-auth/main.go gs-task/main.go gs-config/main.go gs-log/main.go internal/ # 各服务内部实现 auth/ gateway/ task/ config/ logger/ pkg/ # 对外共享库 httpx/ # HTTP 工具、中间件 logx/ # slog 封装 tracex/ # trace_id 透传 safeconfig/ # 配置热更新封装 deploy/ # 编排与脚本 docker-compose.yml configs/ Makefilemonorepo 的缺点也存在一次推送会触发所有服务的镜像构建。我的解决方式是用 Makefile 指定目标比如 make build-service SERVICEgs-gateway只构建指定的服务避免全量重复。4.2 网关接入认证的完整流程网关最核心的职责之一就是统一鉴权不能让每个业务服务自己解析 JWT。具体流程是用户向网关发起登录请求路径为 /api/v1/auth/login网关把请求转给 gs-authgs-auth 校验用户名密码签发 access_token 和 refresh_token后续所有业务请求都带 Authorization: Bearer网关在中间件里校验 JWT 签名和有效期通过后再路由到业务服务校验 JWT 的公钥由 gs-auth 下发网关会定期拉取并缓存避免每个请求都去认证中心查一次。JWT 用的是 RS256 算法私钥只保存在 gs-auth网关拿公钥验证这样做的好处是即使网关被攻破也没法伪造 token。关键的校验中间件是这样func JWTMiddleware(publicKey *rsa.PublicKey, allowedIssuer string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path /api/v1/auth/login || r.URL.Path /api/v1/auth/refresh { next.ServeHTTP(w, r) return } authHeader : r.Header.Get(Authorization) tokenStr : strings.TrimPrefix(authHeader, Bearer ) if tokenStr authHeader { http.Error(w, missing token, http.StatusUnauthorized) return } token, err : jwt.ParseWithClaims(tokenStr, Claims{}, func(t *jwt.Token) (interface{}, error) { return publicKey, nil }) if err ! nil || !token.Valid { http.Error(w, invalid token, http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } }完整跑一遍服务的命令是curl -X POST http://your-server:8080/api/v1/auth/login \ -H Content-Type: application/json \ -d {username:demo,password:secret123}返回结果里带上 access_token然后curl http://your-server:8080/api/v1/order \ -H Authorization: Bearer $TOKEN网关拿到 token 后也不是直接放行它会把 token 里的 user_id 放进请求头 X-User-Id业务服务从这个头取用户身份不用再解析一次 JWT。这里有个坑业务服务内部的 gRPC 调用也把 user_id 放到 metadata 里继续传递保证整条调用链都能知道操作者是谁。4.3 任务队列的可靠投递与幂等消费异步任务这块我一开始差点引 Kafka。仔细盘算了一下个人服务的消息量级一天也就几十万条封顶Kafka 带来的分区运维、磁盘规划、客户端学习成本对项目没有任何收益。Redis List 配合 BRPOPLPUSH 已经能覆盖绝大部分场景而且 Redis 本来就要部署不需要新增任何依赖。核心思路是使用两条队列pending 和 processing。生产者 LPUSH 任务到 pending 列表消费者用 BRPOPLPUSH 把任务原子地搬进 processing处理完成后再从 processing 列表 LREM 删除。这样消费者一旦在处理中途宕机任务会留在 processing 里不会直接丢失另一个恢复机制会按超时把任务重新搬回 pending。这里是我的消费循环伪代码func consume(ctx context.Context, redisClient *redis.Client, pendingKey, processingKey string) { for { select { case -ctx.Done(): log.Info(worker stopped) return default: } payload, err : redisClient.BRPopLPush(ctx, pendingKey, processingKey, 5*time.Second).Result() if err redis.Nil { continue } if err ! nil { log.Error(pop task error, err, err) continue } err handleTask(ctx, payload) if err ! nil { log.Error(handle task error, payload, payload, err, err) } redisClient.LRem(ctx, processingKey, 1, payload) } }可靠投递还有一个更关键的要求任务的幂等性。Redis 方案不能保证只处理一次只能保证不无故丢失。处理逻辑里我会为每个任务生成一个唯一任务 ID业务处理前先检查当前 ID 是否已经处理过处理结果写入 Redis Set 或数据库唯一索引重复消息直接丢弃。比如发邮件任务任务体里带 task_id处理前先 SETNX task_id 到 Redis值设为 processing处理完再设成 done。如果 SETNX 返回 0说明已经处理过直接跳过。这个成本极低但能避免很多“重复提醒用户”的线上问题。队列长度是重要的健康指标。我会在 gs-task 上暴露指标比如 pending 队列长度、processing 队列长度、单任务处理耗时。一旦发现 pending 持续上涨而 processing 没有波动赶紧看 worker 数量多半是消费能力不够或者某个任务卡住了。5. 部署与容器化上服务器的最后一步5.1 Docker Compose 编排与资源限制部署层我选择了 docker compose 而非 Kubernetes。原因很简单个人服务器的节点规模一到三个K8s 的复杂度完全是负担。compose 单文件能把整个栈拉起能配资源限制、健康检查、日志驱动够用了。compose 里几个关键点要注意。第一是资源限制不能省特别是 Redis 和网关。我不给 PostgreSQL 配完整内存限制但会给每个服务设置 memory 上限避免某个服务 OOM 把整台机器拖垮。一个简化版的 compose 文件片段services: redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 3s retries: 5 gs-config: build: ../cmd/gs-config environment: - GS_ENVproduction depends_on: redis: condition: service_healthy restart: unless-stopped gs-gateway: build: ../cmd/gs-gateway ports: - 8080:8080 depends_on: gs-config: condition: service_healthy gs-auth: condition: service_healthy restart: unless-stopped健康检查这块必须做不能依赖 depends_on 默认行为。默认的 depends_on 只是等待容器启动不是等待服务就绪。假如 gs-auth 的端口没真正监听网关启动成功去连它连接会失败。healthcheck 加 condition: service_healthy 之后compose 会等服务真正健康了才启动下游。资源限制配置我推荐这样分档网关 512m认证服务 384m任务 worker 512m配置中心和日志服务 128m 就够。不要每个服务无脑设 1G小机器总共就 2G 内存六个服务一下子全拉起就直接 OOM 了。5.2 版本管理与平滑发布版本管理是所有小项目最容易乱的地方。gstack 的做法很统一代码用 git tag 标版本镜像 tag 用 git commit 的短哈希这样每个镜像都能追溯到具体代码。构建时把版本号注入二进制看日志就知道线上跑的到底是哪个版本VERSION : $(shell git describe --tags --always) LDFLAGS : -X main.version$(VERSION) build: go build -ldflags $(LDFLAGS) -o bin/gs-gateway ./cmd/gs-gateway发布流程我用 Makefile 封装成一条命令make deploy SERVICEgs-auth背后的脚本大致是构建镜像 - 推送到私有 registry - SSH 到服务器 - 在 deploy 目录里 pull 指定服务 - docker compose up -d SERVICE。平滑发布的一个简化做法是“逐服务替换”。先把业务服务的新版本部署上去观察日志五分钟确认没有报错再更新网关和其他基础服务。如果出问题回滚只针对单个服务不用全栈都动。数据备份是最容易被忽略的。Redis 开了 AOF 之后我每天凌晨用 redis-cli BGSAVE 做一次快照备份保留最近 7 天PostgreSQL 用 pg_dump 按库备份。备份脚本也放进 compose 的 cron 容器里执行不要靠人肉记。这里有一个每次发布都会踩的坑compose 里的配置文件和代码仓库分离。我建议把 deploy/configs 目录和代码放一起但把数据库密钥、registry 登录信息这类敏感内容放在服务器本地的 .env 文件里不要提交进仓库。compose 通过 env_file 读它既保证迁移方便又不泄露敏感信息。6. 常见问题与排查技巧实录6.1 高频问题速查表把 gstack 跑起来的半年里我收集了一大堆自己踩过的问题。这里整理成速查表遇到相似情况可以照着排查。现象可能原因排查方式网关返回 401JWT 公钥缓存过期或未拉取检查 gs-gateway 日志看公钥拉取请求是否成功网关返回 503下游服务未就绪或健康检查失败docker compose ps 查看状态docker logs 看具体错误gRPC 调用报 Unavailable服务注册地址错误或服务未启动确认 gRPC target 地址和端口grpcurl 测试连通性Redis 内存持续增长pending 队列堆积消费速率跟不上redis-cli LLEN 查队列长度检查 worker 日志有没有卡点容器反复重启healthcheck 失败服务未真正就绪docker inspect -f {{json .State.Health}} 查看健康日志端口被占用上一版容器未完全释放ss -lntp 查看端口占用配合容器名处理日志里没有 trace_id网关透传失效或用 curl 测试时没带请求头检查中间件顺序确认 trace 中间件在最外层服务内存缓慢增长goroutine 泄漏或缓存无限增长pprof 抓 goroutine 和 heap 结果定位泄漏点值得注意的是“容器反复重启”这个问题。compose 默认 restart: unless-stopped如果进程启动后立刻崩容器会无限重启。每次重启会清掉 stdout但日志要单独拿到宿主机的目录再分析。所以我给每个服务都配了 logging 驱动日志写到挂载目录方便容器挂了之后还能捞到最后一段输出。6.2 排查心法把“猜”变成“查”最后分享一套我屡试不爽的排查思路。遇到任何“不知道为什么”的线上问题先走这条流程而不是马上改代码或者重启服务第一步看日志。用仓库统一的结构化日志直接按级别过滤grep level:ERROR再按 trace_id 把整条调用链拉出来。很多时候真相就藏在一条 error 和一条 warn 之间。第二步看指标。如果日志没有明显报错去 Prometheus 看这个服务过去一小时的 QPS、P99、错误率。先确认是“所有请求都慢”还是“个别请求慢”。前者多半是资源耗尽或死锁后者多半是外部依赖抖动或单条数据问题。第三步现场捞证据。如果还不能定位给服务开 debug 日志或者直接用 pprof 抓实时样本。pprof 接入很简单只要在 main 里引入import _ net/http/pprof然后访问 /debug/pprof/goroutine?debug1 就能看到所有 goroutine 的堆栈。我以前有个任务队列消费慢的怪现象凭直觉怀疑是 Redis 慢结果 pprof 显示协程都卡在一个外部 API 调用的等待上根本不是 Redis 的问题。第四步修复后补监控。任何线上问题的修复最后都要加一条对应的指标或日志。比如外呼 API 增加了超时重试就加一个“外呼耗时”的 histogram下次再变慢一眼就能看到。有个小技巧值得单独说排查问题前把版本先记下来。docker ps 里每个容器的镜像 tag 是什么git log 对应当前哪个 commit这些都是关键上下文。我见过不少人查了半天最后发现线上根本不是他以为的那个版本所有推理都建立在错误前提上。最后再分享一个小技巧gstack 这一路迭代下来如果问我哪个决定性价比最高我会说是从第一天就统一了 trace_id。虽然最开始多写几个中间件多花了点时间但后面每一次线上排查都是靠 trace_id 把网关、认证、任务和业务服务串成一条线节省的时间远超当时投入。如果你也想搭一套类似的个人服务栈我的建议是从日志和 trace_id 开始不要一上来就铺开六个组件。先把一个网关、一个认证、一份统一日志跑通形成固定接入的方式再逐步加任务队列和配置中心。技术债会少很多踩坑也会少很多。对我来说gstack 就是一台服务器和一份 Makefile它不复杂但足够让我在开新项目时不用再从零开始。希望这篇记录能让你少走一些我走过的弯路。
返回列表