小Cの已经记不起来的博客

服务发现 的十种打开方式:工程实践笔记

上周线上调用 502,排查到最后发现实例早就被缩掉了,调用方本地缓存还拿着旧地址往死胡同里打。看了一眼我们那个所谓的“服务注册中心”,其实就是一张 Redis 表,心跳超时之后也没有真正踢掉死实例。这玩意儿是真的坑,一开始都觉得有注册中心就万事大吉,结果网络分区、容器重启、发布缩容、健康检查误判,全都能把它打回原形。

下面这些是我实际用过、或者在故障里被迫读明白的十种打开方式。不一定优雅,也不一定适合你的规模,但基本都属于“知道怎么把它跑起来,也知道它什么时候会咬人”。

1. 静态配置 + 环境变量:小系统别搞花活

如果你的系统只有三五个服务,真的别一上来就折腾注册中心、mesh、配置中心全家桶。最土的方式反而最容易排查。

# docker-compose.yml
services:
  order:
    image: internal/order:latest
    environment:
      USER_SERVICE: http://10.0.3.12:8080
      PAYMENT_SERVICE: http://10.0.3.13:9090

代码里直接读环境变量,拼个 URL,完事。

坑也很明显:IP 变了要改配置,服务重启会漂地址,灰度发布全靠人工搬砖。但它的好处是,出问题时你一眼就能看出来到底是网络、进程、配置,还是调用方自己没缓存更新。别小看这一点,线上救火的时候,简单就是命。

2. DNS SRV:让 DNS 替你打工

很多老系统其实支持服务名解析,只是大家习惯用 A 记录,忘了 SRV。

dig +short SRV _api._tcp.example.com
10 5 8080 api-01.example.com.
10 5 8080 api-02.example.com.
20 2 8080 api-03.example.com.

拿到 SRV 后,按优先级、权重去选实例。听起来很标准,实际用起来你会发现很多语言库对 SRV 支持一般,或者客户端自己缓存了 DNS 结果。

这个方式适合做兜底,或者配合内部 DNS 使用。问题是 TTL 不好控制,太短 DNS 压力大,太长实例挂了调用方还蒙在鼓里。可能是我瞎没找到吧,有些客户端文档写得跟没写一样。

3. 数据库当注册中心:老系统活化石

在一些更老的项目里,服务发现其实就是数据库表。

SELECT addr, port, last_heartbeat
FROM service_instance
WHERE service_name = 'user-service'
  AND status = 'UP'
  AND last_heartbeat > NOW() - INTERVAL 10 SECOND;

然后调用方每隔几秒查一次,本地放一会儿缓存。

这种方式最大的优点是“它真的能跑”,而且对基础设施要求极低。最大的问题是数据库表会热死。服务多、实例多、心跳频繁,数据库就变成一个奇怪的中心化瓶颈。时钟还不一定能信,机器时间一飘,实例就被当成死了。

4. ZooKeeper:CP,适合强一致,但别天天扩容

ZooKeeper 做服务发现很经典,尤其是一些 Java 老项目、Dubbo 早期用法,经常能看到它。

$ zkCli.sh
[zk: localhost:2181] create -e /services/user-service/instance-01 '{"addr":"10.0.3.12:8080","status":"UP"}'
[zk: localhost:2181] ls /services/user-service
[instance-01]

临时节点很香,客户端 session 断了,节点自动消失,看起来天生适合服务存活探测。

但坑也不少。ZK 是 CP,网络抖一下,leader 选举来了,watch 风暴来了,客户端重连风暴也来了。服务实例特别多的时候,watch 注册和事件推送压力很大。强一致听着可靠,可一旦 ZK 集群出点毛病,调用方可能宁可拿到过期数据也别全挂。

5. etcd:账本很清楚,但 watch 也得养着

etcd 在云原生里太常见了,Kubernetes 后面就是它。

LEASE=$(etcdctl lease grant 15 | awk '{print $2}')
etcdctl put /services/user-service/10.0.3.12:8080 '{"status":"UP"}' --lease=$LEASE

再开一个进程 watch:

etcdctl watch --prefix /services/user-service/

这种方式比 ZK 轻一些,lease 机制也清楚,TTL 一到没续租,key 就没了。实际生产里要注意版本兼容,还有 watch 断线重连时的 gap。别以为 watch 连上就永远活着,网络一抽,事件丢没丢,得自己处理。

还有一个很烦的点,lease 时间设置太小,服务 GC 一卡,实例就被踢了;设置太大,实例真死了还要等半天。这玩意儿是真的要调。

6. Consul:healthcheck 很方便,别把它当银弹

Consul 比 etcd 更像“注册中心完整套餐”,自带 health check、DNS 接口、KV、多数据中心。

{
  "service": {
    "name": "user-service",
    "address": "10.0.3.12",
    "port": 8080,
    "check": {
      "http": "http://10.0.3.12:8080/health",
      "interval": "5s",
      "timeout": "2s"
    }
  }
}

放到 /etc/consul.d/user-service.json 后重载:

consul reload

调用方可以去查:

curl http://consul:8500/v1/health/service/user-service

Consul 的好处是 health check 做得比较像话,DNS 也能用。坑在于 agent 多了以后,gossip、WAN 联邦、ACL、证书轮转这些都会出来凑热闹。很多时候不是注册中心不行,是运维它的人开始头大。

7. Eureka:宁可错发,不可漏调

Eureka 是 AP 思路,它不保证你拿到的一定是最完美的实例列表,但它尽量不让你啥都拿不到。

注册:

curl -X POST http://eureka:8761/eureka/apps/USER-SERVICE \
  -H "Content-Type: application/json" \
  -d '{
    "instance": {
      "ipAddr": "10.0.3.12",
      "status": "UP",
      "port": {
        "$": 8080,
        "@enabled": "true"
      }
    }
  }'

查询:

curl -s http://eureka:8761/eureka/apps/USER-SERVICE | xml

Eureka 有个自我保护模式,实例续约失败率太高时,它不一定立刻把实例踢掉。有人觉得这是 bug,其实很多时候这是救命的。网络抖一下,如果把所有实例都摘了,调用方就直接 503 了。宁可拿到几个过期实例,也比拿不到实例强。

但过期实例也可能被调用方反复打死,所以客户端的重试、熔断、超时还是得自己做。

8. Nacos:方便是真方便,互相影响也是真互相影响

Nacos 把配置中心和服务发现放一起,很多团队会很喜欢,因为它确实省事。

curl -X POST \
  'http://nacos:8848/nacos/v1/ns/instance' \
  -d 'serviceName=order-service' \
  -d 'ip=10.0.3.12' \
  -d 'port=8080' \
  -d 'enabled=true' \
  -d 'healthy=true'

查实例:

curl 'http://nacos:8848/nacos/v1/ns/instance/list?serviceName=order-service'

问题是配置和发现混在一个控制台、一套网络链路、一堆客户端心跳里。哪天长轮询、命名空间、权限、网络抖动凑一起,配置拿不到和服务实例拿不到可能会一起发生。排查的时候你会分不清到底是配置问题,还是注册问题。

另外 Nacos 的 namespace/group 很容易用混,本地、测试、预发、线上要是命名不统一,事故会来得特别朴素。

9. Kubernetes Service:集群内默认答案

如果已经在 K8s 里,服务发现其实默认就有,不一定要额外塞一套注册中心。

apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: prod
spec:
  selector:
    app: user-service
  ports:
    - name: http
      port: 8080
      targetPort: 8080

Pod 里直接访问:

curl http://user-service.prod.svc.cluster.local:8080/health

背后其实是 kube-proxy、CoreDNS、EndpointsSlice 在配合干活。看实例状态可以用:

kubectl get endpointslices -l kubernetes.io/service-name=user-service

这个方式很适合单集群。坑是跨集群、多集群、混合云、灰度发布、细粒度流量控制这些,K8s Service 本身不一定够。还有 CoreDNS 缓存、conntrack、无头 Service 行为差异,偶尔会让人怀疑人生。

10. Sidecar / Envoy / Mesh:把发现挪到代理里

如果你需要重试、熔断、限流、mTLS、流量镜像,那 sidecar 或 mesh 会进入视野。发现这件事不再只是“拿到一个实例列表”,而是变成代理路由表的一部分。

Istio 里可以这样定义路由和负载均衡:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: user-service
  namespace: prod
spec:
  host: user-service.prod.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    connectionPool:
      http:
        h2UpgradePolicy: UPGRADE

Envoy 自己也可以通过 CDS/EDS 拿集群和端点。好处很多,控制粒度细,流量策略统一。问题是链路变长了。注册中心、控制面、数据面、证书、配置下发、sidecar 注入,哪一环都能慢一点,也可能抖一下。

很多团队一开始 mesh,最后发现线上最烦的不是服务发现不准,而是配置没下发、证书过期、sidecar 没 ready、mTLS 模式没对齐。这玩意儿确实高级,但它也会把简单问题变复杂。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放