ARTICLE DETAIL

资讯详情

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

微服务服务发现与Consul实战:注册中心原理到Spring Cloud集成

微服务服务发现与Consul实战:注册中心原理到Spring Cloud集成 做微服务这段时间被问得最多的问题之一就是服务之间到底是怎么找到彼此的IP 写死不就行了嘛短期可以服务一多、实例一变、一扩缩容马上就会乱成一锅粥。这也是为什么现在只要聊到微服务架构服务发现绝对是绕不开的一环。我这边用的是 Consul 来做的服务注册与发现从集群搭建到 Spring Cloud 集成完整跑了一遍期间踩了不少坑也把原理层面的东西摸了个七七八八。这篇就把我的实操过程和经验整理出来给正准备做服务发现、或者正在 Eureka、Nacos、Consul 之间纠结的团队一个参考。这篇文章会覆盖这么几个部分为什么需要服务发现、Consul 的核心原理与数据模型、单机和集群怎么搭、服务注册和健康检查怎么做、Spring Cloud 怎么集成最后还有我实际运维中遇到的高频问题和排查思路。无论你是刚拆微服务的新手还是已经在维护注册中心的开发这篇文章里应该都有能直接拿去用的东西。1. 为什么微服务离不开服务发现1.1 没有注册中心时服务调用有多痛苦先回到最原始的场景。假设你有一个订单服务和一个用户服务订单服务要调用户服务的接口。最笨的办法就是配置里写死用户服务的 IP 和端口比如http://192.168.1.10:8080。刚开始实例少确实够用。但一旦用户服务部署了 3 个节点或者某台机器挂了要缩容麻烦就来了你得手动改配置、改 Nginx 上游、重新 reload而且挂掉的那个节点 Nginx 并不知道照样往里转发流量线上就开始零星报错。我见过不少团队在这个阶段用 Nginx 做反向代理把服务地址统一收敛到 Nginx然后业务代码只调 Nginx。这比写死 IP 好一些但本质上是把手动改配置从业务代码转移到了 Nginx 配置实例变动的通知依然靠人肉。一旦微服务规模上了两位数每次发布、扩容、故障转移都要改一遍 Nginx运维压力非常大而且极易出错。这不是工具不好用而是思路错了。Nginx 适合做流量入口的负载均衡不适合做服务间动态调用的注册表。服务之间的调用关系是动态的实例随时在变必须有一个组件能实时记录当前有哪些服务、各自在哪个地址、是否健康并且把这个信息自动同步给所有调用方。1.2 服务发现帮我们解决了哪三件事服务发现解决的核心问题其实就三件事。第一件是服务注册。服务启动时自动把自己的 IP、端口、服务名、元数据信息上报给注册中心。第二件是健康检查。注册中心定期探测服务的存活状态发现实例不健康就自动标记、摘除不再把流量分给它。第三件是服务发现与负载均衡。调用方在发起请求前先向注册中心拿一份可用实例列表然后按照负载均衡策略挑一个发起调用。可以用一个生活化的类比来理解。你去一家热门餐厅吃饭门口有个等位取号系统。你到了之后先取号这就是注册系统会不断喊号没人应答的就跳过这就是健康检查轮到你的号时服务员带你去空桌这就是发现与分配。如果没有这个取号系统你就得挨个桌子问有没有空位效率极低而且很多桌子已经坐满了人你却不知道。现在主流的注册中心方案有 Consul、Nacos、Eureka、ZooKeeper 等。Eureka 2.x 已经停止维护ZooKeeper 更偏向分布式协调场景。Nacos 在国内用得多功能也很全自带配置中心和注册中心。我选择 Consul 主要看中它的多数据中心支持、一致性协议更成熟、以及和 Spring Cloud 的集成度很高后面我会详细讲。2. Consul 服务发现的核心原理2.1 先认识 Consul 里的角色与端口Consul 是 HashiCorp 家的产品核心由 Agent 组成。Agent 有两种运行模式Server 模式和 Client 模式。Server 节点负责维护集群状态、处理查询和写入请求、参与 Raft 一致性协议选举是 Consul 集群的大脑。生产环境一般部署 3 个或 5 个 Server 节点必须是奇数因为 Raft 协议要求多数派才能提交数据。Client 模式则是一个轻量代理部署在每台业务机器上负责转发请求给 Server、执行健康检查、维护本机的服务注册信息。业务进程不直接和 Server 集群通信而是先找本机 Client再由 Client 转发这是一个很典型的分层设计。Consul 用到了几个端口我用一张表整理了一下方便排查问题的时候对照端口协议用途8500HTTP提供 REST API 和 Web UI服务注册、查询都走这里8600TCP/UDPDNS 接口可以通过域名解析服务地址8300TCPServer 节点之间的 RPC 通信8301TCP/UDP同数据中心内 Agent 间 gossip 通信LAN8302TCP/UDP跨数据中心 Agent 间 gossip 通信WAN我刚开始部署的时候没注意端口问题结果集群起来之后成员之间一直互相看不到排查了半天才发现是防火墙把 8301 端口给拦了。如果你也遇到 Agent 日志里反复出现 join 失败优先检查这几个端口是否放通。2.2 服务注册与查询的数据模型Consul 里最核心的数据模型是 Service。一个服务实例用下面几个关键字段描述ID实例的唯一标识同一个服务下不能重复Name服务名逻辑上的服务名称Tags标签可以用来区分版本、环境等Address 和 Port实例的访问地址和端口Check健康检查配置这里有个容易混淆的点Consul 的服务查询接口有两套/v1/catalog/service/{name}和/v1/health/service/{name}。前者返回的是注册表里的原始数据不管实例是否健康都会返回后者只返回通过健康检查的实例。实际调用的时候一定要用/v1/health/service/{name}否则你把流量打到一个已经挂掉的实例上故障排查会非常痛苦。我自己在项目里就遇到过这样的问题服务调用的下游实例已经宕机了但调用方还是能拿到它的地址。查了半天发现代码里用的是 catalog 接口改成 health 接口之后挂掉的实例被自动过滤掉问题立刻消失。这个细节在 Consul 官方文档里写得不算醒目但生产环境非常重要。2.3 三种健康检查方式的选择逻辑Consul 的健康检查有三种模式适用场景完全不同很多人一开始容易搞混。第一种是 HTTP 检查。Consul 定期请求你指定的 HTTP 接口比如/actuator/health根据返回的 HTTP 状态码判断是否健康。只要接口返回 200就认为实例存活。这是我用得最多的一种因为 Spring Boot 的 Actuator 天然提供了健康检查端点可以直接对接。第二种是 TCP 检查。Consul 定期尝试和实例的 IP:Port 建立 TCP 连接连得上就认为健康。适合没有 HTTP 接口的服务比如数据库连接、自定义 RPC 服务。第三种是 TTL 检查。服务实例自己定期主动上报心跳告诉 Consul 我还活着。如果超过指定时间没有上报就判定为不健康。这种模式下 Consul 不会主动探测适合那些不方便提供 HTTP 端点、或者内部有复杂存活判断逻辑的服务。选择逻辑其实很简单能用 HTTP 检查就用 HTTP 检查因为它最直接地反映了服务的真实可用状态服务本身没有 HTTP 接口就用 TCP需要服务自己决定是否存活、或者不想让注册中心主动探测的场景选 TTL。但要注意TTL 模式依赖业务代码主动上报心跳一旦业务线程卡死心跳可能还在发实际服务已经不能处理请求了这会造成误判所以能用 HTTP 检查的地方我一般不会用 TTL。2.4 Consul 的一致性保证与多数据中心Consul 的 Server 节点采用 Raft 协议保证数据一致性。Raft 是一种分布式一致性算法核心思想是选举一个 Leader 节点负责处理写入请求其他节点同步数据。写入操作必须得到多数派节点确认才算成功所以集群里挂掉的节点不能超过半数否则整个集群会变成只读状态服务注册和更新都会失败。这个机制保证了数据不会丢但也带来一个运维常识Consul 集群的 Server 节点数最好是 3 或 5不要因为节省成本只部署 2 个因为 2 个节点挂 1 个就凑不齐多数派了连一台都不挂反而不如单点稳定。我后面会详细演示 3 节点集群怎么搭。Consul 还支持多数据中心每个数据中心有独立的 Server 集群数据中心之间通过 WAN gossip 协议交换服务目录信息。这一点在做异地多活或跨机房容灾时很有价值应用层不需要感知物理机房的差异直接通过服务名就能拿到对端机房的可用实例。如果你的公司暂时没有多机房需求这个功能可以先了解但选型时多一个加分项总是好的。3. 从零搭建 Consul 集群并完成服务注册3.1 单机快速体验开发模式先从最简单的单机模式开始跑通了再上集群。Consul 的安装很简单直接从官网下载二进制包解压后把可执行文件放到 PATH 里就行。启动开发模式consul agent -dev-dev模式会启动一个单节点的 Consul所有功能默认开启非常适合本地调试。启动成功后打开浏览器访问http://127.0.0.1:8500就能看到 Consul 的 Web UI。界面上有 Services、Nodes、ACL 等菜单服务注册进来后在 Services 页面就能看到实例列表和健康状态。开发模式下如果提示端口被占用可以用-http-port指定其他端口。我做本地实验时常用consul agent -dev -http-port18500避开可能被占用的 8500 端口。3.2 3 节点集群搭建实操生产环境我不会用单节点至少搭 3 个 Server 节点的集群。这里演示在 3 台 Linux 服务器上搭建假设三台机器的 IP 分别是 10.0.0.11、10.0.0.12、10.0.0.13。每台机器上先准备一个配置文件consul.hcl内容大致如下data_dir /opt/consul/data log_level INFO server true bootstrap_expect 3 ui true bind_addr 0.0.0.0 client_addr 0.0.0.0 retry_join [10.0.0.11, 10.0.0.12, 10.0.0.13]然后依次在三台机器上执行consul agent -config-dir/etc/consul.d第一台启动的时候因为bootstrap_expect 3Consul 会等待 3 个 Server 节点都加入后才开始选举 Leader。这个参数的意思是期望的 Server 节点数用于避免过早选举产生脑裂。等三台机器全部启动后在任意一台执行consul members应该能看到三个节点都是alive状态。再执行consul operator raft list-peers可以看到有一个节点是 leader其他节点是 followerRaft 集群正常工作了。这里有个经验生产环境的 Server 节点最好是奇数3 个或 5 个。原因在 Raft 协议里说过了要凑多数派。另外如果集群规模很大或者请求量很高还可以给 Server 节点前加一层负载均衡但一般的微服务规模用不到业务请求会优先打到本机的 Client Agent再转发给 Server压力可控。3.3 通过 REST API 注册、查询、注销服务Consul 提供了完整的 REST API我先用 curl 演示最基础的服务注册流程。注册一个名为user-service的服务实例到 Consulcurl -X PUT http://127.0.0.1:8500/v1/agent/service/register \ -H Content-Type: application/json \ -d { ID: user-service-1, Name: user-service, Tags: [primary], Address: 192.168.1.100, Port: 8080, Check: { HTTP: http://192.168.1.100:8080/actuator/health, Interval: 10s } }这里注意两个细节。第一注册接口是/v1/agent/service/register走的是本机 Agent。第二Check 里的 HTTP 地址要填业务实例的地址不是 Consul 的地址Consul 会主动去探测这个接口。注册成功后在浏览器 UI 里能看到这个服务。查询可用实例用 health 接口curl http://127.0.0.1:8500/v1/health/service/user-service返回结果里每个实例会带一个Checks数组里面Status为passing的才是健康实例。服务下线时要调用注销接口curl -X PUT http://127.0.0.1:8500/v1/agent/service/deregister/user-service-1这个接口是 Agent 级别的只注销本机 Agent 上注册的这个实例。搞清楚 agent 和 catalog 两套 API 的区别能避免很多误操作。4. Spring Cloud 集成 Consul服务注册与调用4.1 服务提供者注册到 Consul手动用 curl 注册服务只是为了理解原理真实项目里不会这么干都是让框架自动完成。Spring Cloud 对 Consul 的集成非常成熟我在 Spring Boot 2.x Spring Cloud 2021.0.x 环境下测试过步骤很简洁。在服务提供者项目里引入依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-consul-discovery/artifactId /dependency然后在application.yml里配置 Consul 地址和注册信息spring: application: name: user-service cloud: consul: host: 127.0.0.1 port: 8500 discovery: instance-id: ${spring.application.name}-${spring.cloud.client.ip-address}-${server.port} prefer-ip-address: true health-check-path: /actuator/health health-check-interval: 10s主类上加上EnableDiscoveryClient注解SpringBootApplication EnableDiscoveryClient public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }启动应用后服务会自动注册到 Consul。这里instance-id的配置非常关键如果一台机器上同一个服务部署了多个实例端口不同那么 ID 里带上 IP 和端口就能保证唯一否则会出现后面的实例把前面的实例覆盖掉的情况这是我在多实例部署时踩过的坑。prefer-ip-address: true会让服务注册时优先使用 IP 而不是主机名。如果不开这个配置在容器环境或内网 DNS 不完善的环境下注册到 Consul 的地址可能是主机名其他服务解析不了调用就会失败。4.2 服务消费者通过 Consul 找到并调用服务服务消费者的配置和服务提供者几乎一样只是不注册自身的情况更多。如果某个服务只是调用别人不需要被别人调用可以在配置里关闭注册spring: cloud: consul: discovery: register: false调用方式有两种主流方案。一种是 RestTemplate 加LoadBalanced注解Configuration public class RestTemplateConfig { Bean LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }然后直接通过服务名调用String result restTemplate.getForObject(http://user-service/api/user/1, String.class);另一种是用 OpenFeign声明式调用更符合微服务的风格FeignClient(name user-service) public interface UserClient { GetMapping(/api/user/{id}) String getUser(PathVariable(id) Long id); }这两套方式底层的原理是一样的拦截到服务名后向 Consul 查询可用实例列表再用负载均衡策略选一个实例发起请求。Spring Cloud LoadBalancer 默认的负载均衡策略是轮询你也可以根据自己的需求替换成随机、最少连接数等策略。我第一次用 RestTemplate 调服务名时报了UnknownHostException原因就是忘加LoadBalanced注解。这个注解的作用是给 RestTemplate 注入一个拦截器让它能识别http://user-service这种服务名格式并走服务发现逻辑。没有这个注解RestTemplate 只会把它当普通域名去 DNS 解析自然就失败了。4.3 健康检查、优雅下线与自动摘除Spring Cloud Consul 默认的健康检查路径就是/actuator/health但前提是项目里引入了 Spring Boot Actuator。如果没引入健康检查请求会返回 404Consul 会把实例标记为不健康。所以一定要在服务提供者项目里加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependencyhealth-check-interval: 10s表示每 10 秒检查一次。这个值不要设得太短否则频率太高会浪费不必要的资源也不要设得太长否则实例挂了之后下游最长要等一个周期才能感知到。10 秒是我觉得比较平衡的配置如果对时效性要求高可以压到 5 秒。还有一个很实用的配置是deregister-critical-service-after。当健康检查连续失败实例进入 critical 状态后如果超过这个时间还没有恢复Consul 会自动把实例从注册表里删除spring: cloud: consul: discovery: deregister-critical-service-after: 2m这个配置我强烈建议加上否则一个实例挂了之后它的记录会一直躺在 Consul 服务列表里UI 上看着红叉一片数据也不干净。加了这个配置后Consul 会在 2 分钟后自动清理。优雅下线方面Spring Cloud 在应用关闭时会自动从 Consul 注销服务实例不需要额外写代码。但如果你用的是容器编排系统比如 Kubernetes 或 Docker Compose要注意关闭顺序。先让 Consul 把实例标记为不健康并停止接收新流量再真正销毁容器这样才能做到滚动发布无感知。单纯依赖进程退出时的注销逻辑在容器被强杀时往往来不及执行。5. 常见问题与排查技巧实录5.1 服务列表里看不到服务这个是最常见的问题排查思路从简单到复杂排开先看 Consul UI 的 Services 页面确认服务有没有注册成功再看服务提供者的启动日志有没有报错然后用curl http://127.0.0.1:8500/v1/agent/services查看本机 Agent 上注册的服务列表。一个容易被忽略的原因是配置的spring.cloud.consul.host指向了错误的机器或者端口不是 8500。另外检查服务提供者和 Consul 之间的网络连通性在服务提供者所在机器上直接执行telnet {consul_host} 8500如果端口不通说明网络层面有问题。踩过的一个隐蔽坑是服务注册请求确实发出去了但注册到 Consul 的地址是内网 Docker 网段的 IP比如172.17.0.2其他机器访问不了。这就是没有配prefer-ip-address: true或者容器网络配置不当造成的。解决方法是配置spring: cloud: consul: discovery: prefer-ip-address: true ip-address: 宿主机对外IP # 可选手动指定注册IP5.2 控制台健康状态红叉但服务本身正常服务进程明明还在跑接口也能通但 Consul UI 里显示健康检查失败。这时候先看 Consul 配置的健康检查路径是什么再手动在 Consul 所在机器上 curl 一下这个地址。如果是/actuator/health返回 404说明服务提供者没引入 Actuator或者 context-path 配置导致路径不对。Spring Boot 如果设置了server.servlet.context-path/api那么健康检查端点也会跟着变化Consul 配置里的health-check-path也要改成/api/actuator/health。还有一种情况是健康检查返回了 200但检查频率太高把服务打挂了表现就是服务偶尔可用偶尔不可用。我有一次把 interval 配成了 1 秒结果 Consul 集群对每个实例每秒发一个请求业务高峰期把服务拖得很慢。后来调整为 10 秒一切正常。健康检查的频率不是越高越好还是一个平衡问题。5.3 服务实例被自动摘除后反复重连如果实例处于不太健康的状态Consul 会标记为 critical然后deregister-critical-service-after时间一到就删除注册信息。但服务端的 Spring Cloud Consul 组件有自动重连机制会尝试重新注册于是在 UI 上看到的现象就是服务一直在注册、删除、注册、删除之间反复横跳。这种情况下核心问题是实例本身不稳定可能是内存溢出、数据库连接池耗尽、或者磁盘满了。先去查服务日志和健康检查端点的返回内容Actuator 的/actuator/health返回体里会带上各组件的健康状态比如 MySQL 连接、Redis、磁盘空间等能直接指出是哪一个组件出了问题。5.4 集群出现脑裂或不可写Consul 集群的 Server 节点网络发生分区时Raft 协议会触发重新选举。如果某个分区的节点数凑不齐多数派这个分区就不可写服务注册和更新都会失败。这不是 Consul 的 bug而是 Raft 为防止脑裂写的固有机制。排查方法是登录 Server 节点执行consul operator raft list-peers查看 Raft 状态。如果 leader 一直在切换或者没有 leader说明节点间网络不稳定检查 8300 端口连通性和机房之间的专线质量。另一个常见原因是服务器时钟偏差太大Raft 对时钟一致性有要求生产环境务必配置好 NTP 时间同步。5.5 服务下线时没有及时清空进程被 kill 之后服务实例在 Consul 里还会存在一段时间直到健康检查连续失败后才被标记为 critical再等到deregister-critical-service-after触发才被清理。这是正常现象但如果是主动发布最好在发布脚本里先调用注销接口把实例从 Consul 里摘掉再停进程。写一个简单的下线脚本#!/bin/bash SERVICE_IDuser-service-192.168.1.100-8080 curl -X PUT http://127.0.0.1:8500/v1/agent/service/deregister/${SERVICE_ID} kill -TERM $(pgrep -f user-service)这里调的还是本机 Agent 的接口所以脚本在服务提供者机器上执行即可。结合 CI/CD 流水线在停止容器前先执行这个下线步骤可以让发布期间下游调用方始终只访问存活实例真正实现无感发布。6. 一点扩展ACL 安全与配置中心玩法6.1 别忽略 ACL 安全Consul 老版本曝出过一些安全漏洞大多和 ACL 权限校验绕过有关。如果只是在内网跑很多人会忽略安全配置但微服务架构里注册中心掌握着所有服务的地址一旦被入侵整个系统的调用拓扑就暴露了风险非常高。Consul 支持完整的 ACL 系统可以为不同的服务、Key 配置细粒度的读写权限。简单做法是启用 ACLacl { enabled true default_policy deny tokens { master your-bootstrap-token } }开启后所有 API 请求都需要带 Token 头。Spring Cloud 的 Consul 集成也支持配置 Tokenspring: cloud: consul: config: acl-token: your-token discovery: acl-token: your-token我的建议是即使内网环境也把 ACL 开启至少做个基础防护。同时尽量使用较新版本的 Consul官方修复安全漏洞后在 release note 里都有记录及时升级比什么防护都管用。6.2 Consul 还能当轻量配置中心用Consul 的内置 KV 存储除了支撑服务发现也可以直接用做配置中心。虽然没有 Nacos 的命名空间、分组、灰度这些丰富功能但对中小团队来说够用。Spring Cloud Consul Config 的接入方式和 Nacos 类似。引入依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-consul-config/artifactId /dependency配置里指定 KV 路径spring: cloud: consul: config: enabled: true prefixes: config default-context: application然后在 Consul 的 KV 里创建config/user-service/data这样的路径存放配置文件内容。配合spring-cloud-starter-bus可以实现配置变更后的自动刷新。不过要提醒一句Consul 的 KV 功能适合存一些简单的、变更频率不高的配置。如果配置项特别多、需要分环境分团队管理、需要灰度发布还是用 Nacos 或者 Apollo 这类专业配置中心更合适。选型要看团队体量没有银弹。我在实际使用中最深的一个体会是服务发现这块选哪个注册中心不是最难的难的是把健康检查、优雅上下线、负载均衡这些细节真正落实到生产环境里。很多人项目跑起来看着一切正常等到发布日才发现流量打到了正在关停的实例上或者服务扩容后新实例迟迟没有被下游感知到。这些坑大多不是注册中心本身的问题而是配置和使用姿势的问题。希望这篇文章能让你在搭服务发现的时候少走些弯路。最后再分享一个小习惯无论用 Consul 还是其他注册中心上线前一定要把“实例下线 - 健康检查失败 - 自动摘除”这条链路完整演练一遍确认每个环节的时间都符合预期。这个流程顺畅了线上发布和故障处理会省去很多麻烦。
返回列表