一、场景与问题引入

我们团队最近在把所有微服务都迁移到容器化环境里跑,用K8s管容器,Nacos做服务注册和发现,本来是想让不同节点的服务实例按权重分配流量——比如把跑在性能好的节点上的实例权重设高,让它承接更多请求,结果发现设置了权重之后根本没生效,流量还是平分给所有实例;还碰到过DNS解析的时候,明明实例都在,却连不上,查了半天才搞清楚是SRV记录和K8s Service的映射出了问题。这俩问题其实都是容器化场景下,Nacos动态DNS和K8s适配没做好导致的,今天就把这些问题拆明白。

二、核心原理铺垫

2.1 Nacos动态DNS的工作逻辑

你可以把Nacos想象成一个全公司的服务花名册,每个微服务的实例都会主动注册到这个花名册里,Nacos再把这些实例的信息整理成DNS记录。动态DNS的意思就是,只要K8s里的实例变了,Nacos会自动更新这个花名册,不用手动改。而SRV记录是DNS里的“服务专属记录”,它不仅会告诉你要访问哪个IP,还会说明要连的端口,以及这个实例的权重——应用拿到SRV记录后,会根据权重按比例分配请求,比如权重1和权重2,就是1:2的流量。

2.2 K8s Service与Nacos的映射关系

K8s里的Service是服务的“统一入口”,相当于把多个容器实例包成一个对外的服务。要让Nacos自动同步K8s里的实例,得用到Nacos专门给K8s做的对接工具,这个工具会把K8s里的Service和实例的IP、端口、属性(比如权重)同步到Nacos,这样应用既可以用K8s Service的地址访问,也可以用Nacos的服务名通过DNS访问。但很多时候,这个对接工具没配置对,就会出问题。

三、具体问题拆解

3.1 权重路由失效的表现

举个实际的例子:我们部署了用户服务user-service,有2个实例,一个在性能好的节点A,给它设权重1;另一个在节点B,设权重2,按道理应该是1:2的流量,但实际抓包发现,两个实例的请求数几乎各占50%,相当于权重没生效。后来查Nacos控制台,发现两个实例的权重显示的都是默认值1,完全没同步到我们设的2。

3.2 SRV记录解析不匹配的问题

还是用用户服务举例,我们用nslookup查Nacos的SRV记录,结果返回的端口是K8s Service的ClusterIP对应的端口,而不是容器实例自己的端口;更离谱的是,有时候返回的IP是K8s Service的ClusterIP,而不是实际容器的IP,导致应用连的时候,其实连的是Service的入口,没法直接到指定的实例,流量分配就乱了。

四、完整示例

技术栈:Kubernetes 1.27 + Nacos 2.2.3

# 1. Nacos对接K8s的配置文件,确保开启K8s服务发现
apiVersion: v1
kind: ConfigMap
metadata:
  name: nacos-k8s-cm
  namespace: nacos
data:
  application.properties: |
    spring.nacos.discovery.k8s.enabled=true # 开启K8s同步功能
    spring.nacos.discovery.k8s.namespace=default # 只同步default命名空间的服务
    spring.nacos.discovery.k8s.port-name=http # 只同步端口名是http的容器端口
---
# 2. K8s里的user-service部署配置,给不同实例设权重
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
      # 给Nacos的实例设置权重,必须加这个注解
      annotations:
        nacos.io/weight: "{{ if eq .metadata.name \"user-service-7f4d9c5b8-2xqkz\" }}1{{ else }}2{{ end }}"
    spec:
      containers:
      - name: user-service
        image: your-registry/user-service:v1
        ports:
        - name: http # 端口名必须和Nacos配置里的port-name一致
          containerPort: 8080
---
# 3. K8s里的user-service Service配置
apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: default
spec:
  selector:
    app: user-service
  ports:
  - name: http
    port: 80
    targetPort: http # 对应容器里的port-name
# 验证Nacos里的实例和权重,在K8s的Nacos容器里执行
# 先进入Nacos容器
kubectl exec -it nacos-0 -n nacos bash
# 查user-service的实例列表,看权重是否正确
curl "http://localhost:8848/nacos/v1/ns/instance/list?serviceName=user-service&namespaceId=public"
# 退回到宿主机,查SRV记录解析
exit
nslookup -type=srv _user-service._tcp.nacos

五、原因分析

5.1 权重路由失效的根因

Nacos的权重路由是靠实例的weight属性来实现的,但K8s和Nacos的对接工具,默认不会自动把K8s里的自定义权重同步到Nacos的实例属性里——很多人以为在K8s的Deployment里加环境变量就能同步,但其实得用Nacos推荐的注解nacos.io/weight,而且注解里的值必须是动态生成的,对应每个pod的名字,不然所有实例的权重都会是一样的。刚才的示例里,如果注解写错了,比如所有实例都设成1,那权重肯定不对。

5.2 SRV映射不兼容的细节

SRV记录的生成逻辑里,Nacos会优先用K8s Service的端口名和容器的IP,但如果K8s Service有多个端口,或者容器的port-name和Nacos配置里的不一致,Nacos会随便选一个,就像你让快递员送件,只写了“门牌号”没写“单元号”,快递员会随便送一个单元。还有,K8s Service的类型如果是ClusterIP,Nacos默认同步的是ClusterIP,而不是容器的真实IP,这时候SRV记录返回的IP是ClusterIP,应用连的时候其实走的是Service的负载均衡,根本到不了指定实例,自然没法按权重分配流量。

六、解决方法与注意事项

6.1 解决权重路由失效的步骤

  1. 必须在K8s pod的模板里加nacos.io/weight注解,并且给每个pod设置不同的值——比如第一个pod权重1,第二个设2,这样Nacos同步的时候会把这个值存到实例的weight属性里。
  2. 验证的时候,用刚才的示例命令查Nacos的实例列表,确保每个实例的weight和你设置的一致,比如返回的JSON里weight字段分别是1和2。
  3. 检查Nacos的DNS配置,确保spring.nacos.discovery.k8s.enabled是true,并且spring.nacos.discovery.k8s.port-name和K8s里的port-name完全一致。

6.2 解决SRV映射不兼容的步骤

  1. 确保K8s Service的每个端口的name属性和Nacos配置里的spring.nacos.discovery.k8s.port-name完全一样,比如都是http,不能有拼写错误。
  2. 如果K8s Service有多个端口,你可以在Nacos配置里指定多个port-name,用逗号分隔,比如spring.nacos.discovery.k8s.port-name=http,grpc,这样Nacos会同步所有端口。
  3. nslookup查SRV记录,看返回的port是否是容器的真实端口(比如8080),target是否是容器的IP,而不是Service的ClusterIP——如果返回的是ClusterIP,那是K8s的Service类型不对,应该换成Headless Service,这样SRV记录返回的是容器的真实IP。

6.3 注意事项

  • 权重的取值范围是0到10000,不能设超过这个范围的数,不然Nacos会自动截断,导致权重异常。
  • 不要混合用Nacos的静态注册和K8s自动同步,要么所有实例都通过K8s同步到Nacos,要么全部手动注册,不然会出现重复实例,流量分配混乱。
  • K8s的pod重启后,Nacos会自动更新实例的IP和权重,但最好把Nacos的同步刷新时间设成30秒(默认值),不用改太短,不然会增加Nacos的负载。

七、总结

容器化场景下,Nacos和K8s的服务发现适配问题,本质是两个系统的元数据(比如权重、端口、IP)映射没做好。权重路由失效,是因为没给pod加正确的Nacos权重注解;SRV映射不兼容,是因为端口名和服务类型不对。只要按照刚才的步骤配置,确保K8s和Nacos的对接工具参数正确,每个pod的权重和端口都匹配,就能解决这两个问题。简单来说,就是把“服务的身份信息”在两个系统里统一好,别让两者的信息对不上。