做 .Net 微服务做多了你会发现,服务注册发现和网关这两块,才是架构里最容易乱的地方。服务少的时候,A 服务调 B 服务直接写死地址就行,服务一多,拆成十几个二十几个,谁在哪个端口、哪个节点挂了、流量怎么进,全成了事故现场。Consul + Nginx 这套组合,是我这几年在 .Net 项目里用得最顺手的一套方案,注册中心用 Consul,网关和负载均衡交给 Nginx,今天就把完整的落地过程、配置细节和踩过的坑都写出来。文章不聊空泛的架构图,直接给你能抄作业的配置和代码,适合正在做 .Net 微服务改造、或者被服务间调用搞到头大的团队参考。
这套方案的适用场景很明确:你的服务是 .NET 写的一堆 Web API,内部需要互相调用,对外又希望只暴露一个统一入口,同时你还不想引入太重的基础组件。Consul 负责服务的注册、发现、健康检查,Nginx 负责把外部请求按路径转发到具体服务,再做一层负载均衡。相比直接用 Ocelot 这种全功能网关,Consul + Nginx 的性能开销更低,运维链路更简单,出了问题也好排查。
1. 为什么是 Consul + Nginx
1.1 微服务网关到底解决什么问题
微服务拆开之后,第一个要解决的问题是服务发现。以前单体应用不管怎么样,调用就是本地方法调用,拆成服务之后变成远程调用,你就得知道对方服务在哪台机器、哪个端口。硬编码 IP 和端口是最省事的做法,但也是最早爆炸的做法。服务实例扩容、缩容、迁移、宕机,只要有变化,硬编码的调用方就会拿到失效地址。所以我们需要一个注册中心,每个服务启动的时候把自己的地址报上去,挂了或者停止的时候自动摘除,调用方动态获取可用实例列表。
第二个要解决的问题是网关。服务多了,每个服务都有自己的端口和路由规则,你不可能让前端或者外部调用方去挨个记住这些地址。网关就是一个统一入口,外部请求只打到网关,网关根据路径把请求转发到内部对应的服务。网关还能顺带做负载均衡、限流、超时控制、日志记录这些事情。某种意义上,网关就是微服务对外的门面,内部再乱,门面要整洁可预测。
Consul 在这套方案里承担的是服务注册中心和健康检查的角色。它有 HTTP API、DNS 接口,还有一套简单可靠的健康检查机制,服务挂了能自动从可用列表里踢掉。Nginx 承担的是反向代理和负载均衡的角色,外部流量进到 Nginx,它按规则转发到后面的服务实例。所以这两个组件没有功能冲突,一个管服务发现,一个管流量转发,配合起来非常默契。
1.2 方案选型:为什么不用 Ocelot 或 Service Fabric
很多人会问,.Net 生态里现成的 Ocelot、Service Fabric 不是也能干这事吗,为什么还要自己搭 Consul + Nginx。这个问题我在实际项目里对比过,说下我的判断。
Ocelot 确实能和 Consul 集成,也能做路由、限流、负载均衡,但它本质上是一个跑在 .NET 进程里的网关,所有请求都要先进入一个 .NET 应用再做转发。这意味着网关本身的性能和稳定性,取决于你部署 Ocelot 的机器和 .NET 运行时的表现。流量大的时候,Ocelot 的 CPU 和内存开销不小,而且它本身也是需要维护的服务,版本升级、配置热更新都还得处理。关键它不是独立于应用的,网关挂了会影响所有服务。
Nginx 是 C 写的,事件驱动模型,并发能力极强,一台普通机器轻松扛几万 QPS。它的配置语法简单,热重载方便,社区资料也多。用 Nginx 做网关层,性能上几乎不用操心。Consul 管注册发现,Nginx 管流量转发,两个都是轻量级组件,职责单一,出问题容易定位。Service Fabric 是微软的重量级方案,功能很全,但部署和运维复杂度高,如果你的团队规模不大,只是想把服务拆开管理好,用 Service Fabric 有点杀鸡用牛刀。
当然,Ocelot 也不是一无是处,它做聚合网关的时候很灵活,可以在网关层做服务聚合、改请求响应结构。如果你有这类强需求,可以考虑 Ocelot 和 Nginx 结合:Nginx 在最外层挡流量,Ocelot 在内层做聚合和协议转换。但绝大多数场景下,Consul + Nginx 就足够了,少一层 .NET 进程就少一份开销和故障点。
2. Consul:服务注册中心的部署与核心机制
2.1 Consul 的下载、安装和集群配置
Consul 官方提供的是单文件二进制,下载后直接运行,不需要安装依赖,这一点相当方便。选择一个和你服务器架构匹配的版本,放到 /usr/local/bin 或者 Windows 的某个 PATH 目录里,运行 consul version 能输出版本信息就算安装完成。
开发环境直接单节点跑即可:
consul agent -server -bootstrap -data-dir=/data/consul -ui -bind=0.0.0.0 -client=0.0.0.0生产环境建议至少三个节点组成集群,避免单点故障。节点之间需要能互相通信,每个 agent 以 server 模式启动,再通过 retry_join 指定其他节点地址:
consul agent -server -bootstrap-expect=3 -data-dir=/data/consul \ -node=consul-1 -bind=<本机IP> -client=0.0.0.0 -retry-join=<节点2IP> -retry-join=<节点3IP> -ui启动之后,浏览器访问 http://<服务器IP>:8500/ui 就能看到 Consul 的控制台。8500 是 HTTP API 端口,8600 是 DNS 端口,服务注册、发现、健康检查都走 8500,DNS 方式的服务发现走 8600。记得在安全组或防火墙里只放开你需要访问的 IP 段,不要把 8500 端口裸奔到公网,否则任何人都能通过 API 看到你服务的内部地址,也能随意注册和注销服务。
单节点评估的话,加 -bootstrap 参数让这个节点自己选举为 leader,即可完成初始化。多节点集群需要注意 -bootstrap-expect 的配置,它表示集群需要几个 server 节点才能形成 quorum,一般设为 3 或 5。
2.2 .Net 服务如何注册到 Consul
.Net 服务注册到 Consul 不需要引入特别重的框架,最简单的就是在服务启动的时候,调用 Consul 的 HTTP API 完成注册。当然,更规范的做法是引入 Consul 官方客户端库,选举健康检查、服务信息更新都更方便。下面这段代码展示了在 ASP.NET Core 服务启动时通过官方客户端注册服务,并注册一个 HTTP 健康检查端点。
// NuGet: Consul public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConsulClient>(sp => new ConsulClient(c => { c.Address = new Uri(Configuration["Consul:Address"]); })); } public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime) { var client = app.ApplicationServices.GetRequiredService<IConsulClient>(); var serviceId = $"{Configuration["Service:Name"]}-{Environment.MachineName}-{Configuration["Service:Port"]}"; var registration = new AgentServiceRegistration { ID = serviceId, Name = Configuration["Service:Name"], // 服务名,例如 order-service Address = GetLocalIpAddress(), // 注册给 Consul 的地址,必须是其他服务能访问到的地址 Port = int.Parse(Configuration["Service:Port"]), Tags = new[] { "v1", "order" }, // 标签,可用作版本或环境标识 Check = new AgentServiceCheck { HTTP = $"http://{GetLocalIpAddress()}:{Configuration["Service:Port"]}/health", Interval = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(5), DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1) } }; client.Agent.ServiceRegister(registration).GetAwaiter().GetResult(); lifetime.ApplicationStopping.Register(() => { client.Agent.ServiceDeregister(serviceId).GetAwaiter().GetResult(); }); }这段代码里几个关键点值得展开说。
第一,服务 ID 必须全局唯一,同一台机器上同一个服务可能起多个实例,所以 ID 里拼上了机器名和端口。如果不注意 ID 冲突,后注册的实例会把先注册的实例信息覆盖掉,造成调用方拿到错误地址。
第二,注册的 Address 要特别小心。很多人习惯拿本机回环地址 127.0.0.1 去注册,单机跑没问题,但跨机器调用的时候,别的服务拿着 127.0.0.1 来访问你,必然会失败。所以注册时要用局域网内可路由的 IP,一般通过路由表或者配置中心指定的方式获取。
第三,健康检查的地址也要保证从 Consul 服务器能访问到。如果你在 Docker 容器里跑服务,容器内的 /health 地址从宿主机访问不通,健康检查就会一直失败。这时候要么用容器网络模式,要么在健康检查地址里配置成宿主机映射后的地址。
DeregisterCriticalServiceAfter 是一个容易忽略但是很重要的参数。它表示如果服务健康检查失败超过指定时间,Consul 就自动把这个服务实例从注册中心移除。没有这个参数,挂了的服务会一直留在服务列表里,虽然调用方在下一次拉取时可能拿不到它,但控制台和 API 里会一直显示,影响后续排查。我建议设置成 1 分钟或 90 秒,这个时长既能容忍短暂抖动,又能及时清理死服务。
2.3 服务发现:调用方怎么拿到可用实例
服务注册上去之后,调用方有两种方式来发现服务:HTTP API 和 DNS。HTTP API 直接请求 /v1/health/service/服务名 就能拿到该服务的健康实例列表:
curl http://127.0.0.1:8500/v1/health/service/order-service?passing加 passing 参数表示只返回健康检查通过的服务实例,否则会把检查失败但尚未摘除的实例也返回给你,调用方用了就会出问题。
返回结果是一个 JSON 数组,每个元素包含 Service 和 Checks 两个部分。实际做服务发现的时候,就是把 Service 里的 Address 和 Port 拼起来形成一个实例地址列表。
DNS 方式更直观。Consul 内置 DNS 服务,你可以在调用方配置 /etc/resolv.conf 里把 nameserver 指向 Consul 节点,然后直接用服务名做 DNS 解析:
dig @127.0.0.1 -p 8600 order-service.service.consulDNS 方式的好处是零代码,很多语言和组件天然支持通过域名访问,解析的时候 Consul 会自动负载均衡返回实例地址。坏处是通常情况下 DNS 结果有 TTL 缓存,服务实例变化后不会立刻生效。HTTP API 方式实时性更好,适合在代码里动态获取服务地址。
在 .Net 里我一般用 HTTP API 做服务发现,做一个简单的负载均衡客户端,每次调用前拉取可用实例列表,再用轮询或者随机策略选一个地址出来。这个做法的好处是直观、可控制,坏处是每个服务都要自己实现一套良逻辑。如果服务间调用非常频繁,建议在网关层做服务发现和负载均衡,内部服务之间直接用内网域名访问,避免重复造轮子。
3. Nginx:网关层的实现与细节
3.1 网关的职责划分
Consul 解决的是服务发现,那 Nginx 就要解决流量入口的问题。在我这套方案里,Nginx 的职责有几个:统一接收外部请求,按路径映射到不同微服务;对同一个服务的多个实例做负载均衡;实现反向代理,隐藏内部服务真实地址;记录访问日志,方便排查问题。
最直接的做法是在 Nginx 的 http 块里用 upstream 定义一组服务实例,然后在 server 块里用 location 匹配路径,proxy_pass 转发到对应的 upstream。Nginx 配置长这样:
upstream order_service { server 192.168.1.10:8001 weight=3; server 192.168.1.11:8001 weight=2; server 192.168.1.12:8001 weight=1; } server { listen 80; server_name gateway.example.com; location /api/order/ { proxy_pass http://order_service/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }location /api/order/ 的斜杠结尾和 proxy_pass 地址的斜杠结尾是很多人会搞错的地方。location 后面带斜杠,表示只匹配这个前缀;proxy_pass 后面带斜杠,表示转发时会把 location 匹配到的前缀去掉。例如请求 /api/order/123,转发到上游的 URI 是 /123。如果你不想去掉前缀,proxy_pass 地址后面就不要带斜杠,比如 proxy_pass http://order_service;。这个细微差别直接决定你的服务端接收到的路由是不是你预期的样子。
3.2 静态 upstream 的局限和动态化改造
如果服务实例数不变,用上面这种静态 upstream 配置是没问题的。但微服务最核心的能力就是动态扩缩容,你今天加了两个实例,明天坏了一个,不可能每次都手动维护 Nginx 配置再去 reload。所以要让 Nginx 和 Consul 联动起来,Consul 里服务列表变了,Nginx 的 upstream 配置也要跟着变。
我有两种动态化方案。
第一种是使用 Consul Template 生成 Nginx 配置。Consul Template 是一个守护进程,它会监听 Consul 中的 key/value 和服务列表变化,一旦有变化就根据模板生成新的 Nginx 配置文件,然后执行 Nginx reload。这是一套非常成熟的方案。模板文件长这样:
upstream dynamic_order_service { {{- range service "order-service" }} server {{ .Address }}:{{ .Port }}; {{- end }} }Consul Template 的配置里,指定要监听的服务,检测到变化后重新渲染模板并 reload Nginx。这种方法改动小,稳定,不侵入 Nginx 本身,是我最常用的方案。
第二种是使用 Nginx 的 njs 模块或者第三方模块直接调用 Consul API 动态更新 upstream。这种方案在开源版 Nginx 上实现门槛较高,配置复杂,而且第三方模块和 Nginx 版本兼容性是个坑。我一般建议先用 Consul Template 过渡,除非你的变更有秒级到达的需求,否则没必要引入更复杂的机制。
无论用哪种方案,都要注意 Nginx reload 不是无代价的。reload 会重新加载配置,虽然 Nginx 的 reload 设计成不中断现有连接,但在高并发下频繁 reload 还是会造成短暂的 worker 进程重建,可能出现极短时间的请求抖动。所以动态更细的间隔要设置合理,一般服务列表变化后 5-10 秒内同步即可,不需要刻意追求毫秒级。
3.3 多个服务的路由与版本管理
网关的服务多了,路由规则就成了一个需要重点管理的对象。我见过有人把所有 location 堆在一个 server 块里,写了一百多行,看着就头晕。更合理的做法是按功能模块分文件,用 include 的方式聚合。
比如把每个服务的路由配置单独放到 /etc/nginx/conf.d/routes/ 目录下,每个服务一个文件:
# /etc/nginx/conf.d/routes/order.conf location /api/order/ { proxy_pass http://dynamic_order_service/; }然后在主配置里统一 include:
# 主配置 include /etc/nginx/conf.d/routes/*.conf;这样每个服务自治各自的匹配规则,互不影响,新增一个服务只需要丢一个文件进来,再 reload 一次。团队协作的时候,A 服务负责人改 A 服务自己的文件,不会因为改同一份配置产生冲突。
另外,网关层可以做简单的版本管理。比如 /api/order/v1/ 转发到 order-service 的 v1 实例,/api/order/v2/ 转发到 v2 实例。常见的做法是给服务实例在 Consul 注册时打上版本 tag,查询服务的时候通过 tag 过滤。
在 Consul API 里可以这样查询指定 tag 的服务实例:
curl 'http://127.0.0.1:8500/v1/health/service/order-service?passing&tag=v2'Consul Template 的模板里也可以用过滤条件,只选出特定 tag 的实例,然后生成对应的 Nginx upstream。这样同一个服务可以同时存在多个版本,网关根据请求路径分发到不同版本,为灰度发布和蓝绿发布提供了基础技术能力。
3.4 HTTPS、限流、日志这些网关层的高级操作
网关不只是转发请求,还应该承担一些边界职责。我强烈建议在 Nginx 层把 HTTPS 证书配置好,对外只暴露 443 端口,内部服务之间走 HTTP 即可。配置 HTTPS 后,记得设置 HTTP 强制跳转 HTTPS。
server { listen 80; server_name gateway.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name gateway.example.com; ssl_certificate /etc/nginx/ssl/gateway.crt; ssl_certificate_key /etc/nginx/ssl/gateway.key; ssl_protocols TLSv1.2 TLSv1.3; }限流是网关层很实用的功能,不需要在每个服务里实现。Nginx 的 limit_req 模块可以按 IP 对某个接口做请求速率限制,比如限制每个 IP 每秒最多 5 个请求:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s; server { location /api/order/ { limit_req zone=api_limit burst=10 nodelay; proxy_pass http://dynamic_order_service/; } }burst=10 表示允许短暂的突发超出,nodelay 表示超出的请求不排队直接返回 503。这个配置对防止恶意刷接口很有效。
日志方面,Nginx 默认的 access_log 记录的内容已经很详细,建议加上 upstream 的响应时间和上游地址,方便定位是网关层慢还是服务本身慢:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'upstream_addr=$upstream_addr ' 'request_time=$request_time ' 'upstream_response_time=$upstream_response_time';4. 常见问题与排查技巧实录
4.1 Consul 相关的坑
Consul 最常见的问题是服务注册了但健康检查一直失败,导致服务显示为 critical,调用方拿不到实例。定位这个问题的第一步是去 Consul 控制台,找到对应服务,点开健康检查详情,里面会显示健康检查的最终错误信息。绝大多数情况下都是健康检查 URL 在 Consul 服务器那边访问不通,要么是 IP 写错了,要么是端口没放开,要么是服务本身没有实现 /health 端点。
我建议每个写着 ASP.NET Core 服务都实现一个轻量的健康检查中间件,不需要引入三方库,自己在管道里加一个端点即可:
app.MapGet("/health", () => Results.Ok(new { status = "healthy", time = DateTime.Now }));健康检查里可以加上依赖组件的状态,比如数据库连接池、Redis 连通性。但注意检查逻辑不要太重,如果健康检查本身因为依赖组件超时而拖到几秒,会拖垮 Consul 的检查效率,可能导致误杀。
另一个常见问题是服务停掉之后没有调用注销接口,Consul 里残留一堆不存在的服务实例。虽然 DeregisterCriticalServiceAfter 能在健康检查失败后自动清理,但建议在进程退出时主动调用 ServiceDeregister,这个我在前面已经给出过代码。注意进程被 kill -9 强制杀掉时,ApplicationStopping 可能会来不及执行,所以生产环境里建议配合超时参数,在进程收到终止信号后留出几秒给清理逻辑。
还有一个问题是 Consul 集群的 leader 选举。常见是一些节点失联后重新加入,raft 协议会花较长时间选主,期间写入注册信息会失败,新启动的服务注册时报 500。排查方向是检查节点间的网络连通性,以及各节点的系统时间是否一致,时间偏移太大会破坏 raft 的一致性,这对于网络服务和时钟同步敏感的应用来说是个经典坑。集群里所有机器务必开启 NTP 时间同步。
4.2 Nginx 相关的坑
Nginx 最常见的问题是 502 Bad Gateway,几乎每个做网关的人都会遇到。502 表示 Nginx 连接不上上游服务。先用 curl 直接访问上游服务的地址,看服务是不是通的。如果服务本身有响应,那要看 Nginx 和上游服务之间的网络是否通,是不是防火墙拦了。
如果 Nginx 是在 Docker 里跑,上游服务在宿主机上,那 upstream 地址不能写 127.0.0.1,因为 Docker 容器里的 127.0.0.1 指容器自己。要写宿主机在 docker0 网桥上的 IP,一般是 172.17.0.1。这个坑尤其常见,我见过有人被这个问题卡了一下午。
配置改动后要记得测试配置,再 reload:
nginx -t nginx -s reloadnginx -t 会检查所有配置文件语法,有问题会明确报出哪个文件的哪一行。建议在 CI/CD 的流水线里也加一步配置检查,避免线上提交一份语法有误的配置导致 Nginx 直接拒绝加载。
还有 Nginx 转发到 .Net 服务时的 Host 头问题。ASP.NET Core 默认情况下会根据请求的 Host 头生成一些重定向地址,如果你在 Nginx 里没有把 Host 头传给上游,或者传的是内网地址,某些场景下服务重定向出来的地址就是不对的。所以网关层我始终加上:
proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;4.3 微服务间调用超时与重试
服务注册发现没问题,网关注册没问题,服务间调用还是偶发超时,这种情况最让人头疼。我有次排查一个偶发的 500 问题,发现是某个服务在调用另一个服务时,使用了自己实现的 HTTP 客户端,默认超时时间是 100 秒,加上没有重试和熔断,下游服务一个慢 SQL,引起的告警直接蔓延到整个调用链。
在 .Net 里做服务间调用,我非常推荐使用 Polly 库做超时和重试策略:
var retryPolicy = Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => (int)r.StatusCode >= 500) .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(5), TimeoutStrategy.Pessimistic); var circuitBreakerPolicy = Policy .Handle<HttpRequestException>() .CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));超时时间一定要设置,而且一定要小于网关层的超时时间。比如 Nginx 的 proxy_read_timeout 设了 10 秒,那服务内部的 HTTP 超时就应该设成 3-5 秒,这样在下游真正出问题的时候,服务能比网关更快地返回失败,而不是让网关一直挂住等。重试要谨慎,幂等接口可以重试,非幂等接口重试可能导致重复下单、重复扣款,所以重试策略要按业务接口区分。
4.4 Consul 与 Nginx 联调时的小技巧
联调阶段,我建议先把一个最小闭环跑通:启动 Consul,注册一个测试服务,Nginx 静态转发到该服务,验证最外层的请求能打到服务;然后再引入 Consul Template,把 upstream 改造成动态的。
有一个非常实用的排查技巧,就是利用 Nginx 的 upstream_addr 日志字段确认实际转发到了哪个实例。如果同时起了多个实例,通过访问日志里的 upstream_addr 可以直接看到每次请求落在哪个 IP 上,判断负载均衡是否生效。如果请求总是落在同一个实例上,要么是 upstream 配置里其他实例的权重设成了 0,要么是其他实例健康检查有问题导致 Nginx 把它剔除了。
还有一个很重要但容易忽略的点:Consul Template 生成的配置文件,一定要用清晰的前缀和分隔符,避免多个模板写同一个 upstream 名称互相覆盖。我在项目里遇到过 Consul Template 模板更新后,某个服务的 upstream 配置突然空了,后来发现是模板里服务名写错,查询不到实例,生成出来的 upstream 块就是空的,Nginx reload 后该服务的请求全部 502。这个场景在无监控数据的情况下很难察觉,所以我一般在 Consul Template 生成配置后写一个脚本,检查生成的配置文件里 server 行数量是否为 0,为 0 就不 reload,直接告警。
5. 从单机到集群的演进建议
如果是小团队或者刚起步的微服务项目,建议先把 Consul 和 Nginx 部署在同一台或者两台机器上,跑通整套流程。这个时候不用考虑高可用,先把服务注册、健康检查、网关转发这套机制跑稳定,让团队养成通过 Consul 管理服务地址的习惯。
当服务数量变多、流量变大之后,就要开始拆分部署了。Consul 最好是独立集群,不要和业务服务混布,否则业务的高负载会影响到 Consul 的稳定性。Nginx 网关层可以部署多台,前面再加一层负载均衡。这时候需要考虑会话保持、HTTPS 证书统一管理等,但核心配置思路和单机一样,变得是部署拓扑和运维流程。
服务发现从 HTTP API 切换到 DNS 也是一种演化方向。Consul 自带 DNS 接口,配合 CoreDNS 等工具,可以做到服务名即域名,调用方直接用域名访问。这样服务实例的变化就能对调用方透明,代码里不需要自己实现拉取和负载均衡逻辑。不过 DNS 有缓存,服务变更的生效时间会延后,需要结合业务需求权衡。
6. 最后再分享一个实用技巧
如果你正在使用 Consul + Nginx 这套方案,建议把 Consul 的注册生命周期和容器化部署一并考虑进来。现在很多 .Net 微服务已经跑在 Docker 或者 Kubernetes 里,服务实例的 IP 是动态分配的,注册到 Consul 的地址如果用容器 IP,其他服务也可以访问,前提是网络互通。在 Kubernetes 环境下,我建议直接用 Pod IP 注册,并配合 readinessProbe 做好健康检查,避免服务还在启动过程中就被注册进去,导致调用方请求到未就绪的实例。
在实际操作中,我自己的体会是,这套方案最大的价值不是某个单一组件多厉害,而是它把服务注册发现、健康检查、流量接入这些微服务的基础能力,拆成了清晰可见、可独立排查的几层。出了问题,你能很快判断是 Consul 的服务列表不对,还是 Nginx 的路由没配好,或者是后端服务自己挂了。这种可排查性,在微服务架构里比任何炫酷的特性都重要。希望这篇文章能帮你少踩几个坑,把这套组合顺顺利利地用起来。