Kubernetes 微服务

目录

  1. [微服务是什么 & 为什么需要 Service](#微服务是什么 & 为什么需要 Service)
  2. [Service 的四种类型总览](#Service 的四种类型总览)
  3. 前置环境说明
  4. [实验一:理解标签与 Service 中的 Endpoints](#实验一:理解标签与 Service 中的 Endpoints)
  5. [实验二:用 Service 暴露 Deployment 控制器](#实验二:用 Service 暴露 Deployment 控制器)
  6. [kube-proxy:iptables vs ipvs 模式](#kube-proxy:iptables vs ipvs 模式)
  7. [实验三:ClusterIP(含 Headless 无头服务)](#实验三:ClusterIP(含 Headless 无头服务))
  8. 实验四:NodePort(含端口范围破限)
  9. [实验五:LoadBalancer + MetalLB 裸金属环境](#实验五:LoadBalancer + MetalLB 裸金属环境)
  10. ExternalName:给集群内访问打个外部域名别名
  11. [实验六:Ingress 七层代理(部署 + 路径 + 域名 + TLS + 认证 + 重写)](#实验六:Ingress 七层代理(部署 + 路径 + 域名 + TLS + 认证 + 重写))
  12. 实验七:金丝雀(灰度)发布

一、微服务是什么 & 为什么需要 Service

上一节我们学了控制器(Deployment / ReplicaSet ...),它能保证有 N 个 Pod 在跑。但是 Pod 有一个天生的问题:IP 不固定。一个 Pod 被删除重建,IP 就会变;Deployment 扩缩容,新 Pod 又是新 IP。客户端如果直接连 Pod IP,服务端每次重建就要改客户端配置,根本没法用。

Service(微服务 / 服务)就是来解决这个问题的

  • 它是一组提供相同服务的 Pod 对外暴露的统一入口;
  • 背后通过**标签选择器(label selector)**自动把符合条件的 Pod IP:Port 收集到一份 Endpoints 列表里;
  • 客户端只需要记 Service 的名字(DNS)或者 VIP(ClusterIP),不用关心后端 Pod 怎么变;
  • 默认只做 四层(TCP/UDP)负载均衡,要 HTTP 域名 / URL 路径分发要靠 Ingress(下一节)。

一句话总结:

  1. Service 通过 selector 选中一批 Pod,把它们的 IP:Port 自动写进同名的 Endpoints 对象;
  2. 访问方只需要记 Service 的名字(DNS)或 ClusterIP,Pod 重建 / 扩缩容后 Endpoints 自动更新;
  3. Service 默认只做四层(TCP/UDP)负载均衡,七层(HTTP 域名 / 路径)能力要交给 Ingress。

二、Service 的四种类型总览

spec.type 决定 Service 的暴露方式,一共有 4 种外加 1 个特殊变体。

类型 作用 典型访问方式
ClusterIP(默认) 集群内分配一个虚拟 IP(VIP),只能在集群内部访问;提供健康检测与自动发现 ClusterIP:Port服务名.命名空间.svc.cluster.local
NodePort 在 ClusterIP 基础上,在每个节点的同一个端口上开洞,外部通过任意 NodeIP:nodePort 访问 NodeIP:30000-32767
LoadBalancer 在 NodePort 基础上,由云厂商(或裸机 MetalLB)分配一个外部 VIP 并做负载均衡 云厂商给的 EXTERNAL-IP
ExternalName 不给 ClusterIP,而是用 DNS CNAME 把集群内请求转发到指定外部域名 服务名 → 外部域名

以上四种都是四层;七层(基于域名 / URL 路径 / TLS / 认证)由 Ingress 补齐(见第十一节)。


三、前置环境说明

为了方便对照命令,本文所有示例使用:

  • 集群节点:k8s-master (172.25.254.100)、k8s-node1k8s-node2ceph 节点;
  • Pod 网段:10.244.0.0/16(flannel);
  • Service 网段:10.96.0.0/12(默认);
  • 测试镜像:myapp:v1Hello MyApp | Version: v1)、myapp:v2Hello MyApp | Version: v2)、busyboxplus
  • 私有镜像仓库:reg.timinglee.org(本系列实验统一使用)。

如果你的环境网段 / 仓库域名不同,把命令里的 IP 与镜像名换掉即可,整体流程不变


四、实验一:理解标签与 Service 中的 Endpoints

目标 :亲眼看到 Service 是怎么靠标签认亲的------标签对不上 Endpoints 是空的,打上正确标签 Endpoints 立刻出现。

1. 创建一个 Service

bash 复制代码
# kubectl create service clusterip --tcp 80:80 <name> 自动生成 yaml 模板
[root@k8s-master services]# kubectl create service clusterip --tcp 80:80 testservice \
  --dry-run=client -o yaml > testservice.yaml
[root@k8s-master services]# vim testservice.yaml

生成的 yaml 默认 selector: { app: webserver }(这是模板自带的,先不改它,留着当反面教材):

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  labels: { app: testservice }
  name: testservice
spec:
  ports:
  - name: webport
    port: 80
    protocol: TCP
    targetPort: 80
  selector: { app: webserver }      # ← 注意:选择器是 app=webserver
  type: ClusterIP

应用:

bash 复制代码
[root@k8s-master services]# kubectl apply -f testservice.yaml
service/testservice created

2. 起一个"标签对不上"的 Pod

bash 复制代码
[root@k8s-master services]# kubectl run webserver --image myapp:v1 \
  --dry-run=client -o yaml > webserver.yml
[root@k8s-master services]# cat webserver.yml
yaml 复制代码
kind: Pod
metadata:
  labels: { run: webserver }         # ← 注意:Pod 的标签是 run=webserver
  name: webserver
spec:
  containers:
  - image: myapp:v1
    name: webserver
bash 复制代码
[root@k8s-master services]# kubectl apply -f webserver.yml
pod/webserver created

3. 监控:Endpoints 是空的

bash 复制代码
[root@k8s-master ~]# watch -n 1 \
  "kubectl describe svc testservice ; kubectl get pods --show-labels"
text 复制代码
Name:                     testservice
Namespace:                default
Selector:                 app=webserver
Type:                     ClusterIP
IP:                       10.100.62.227
Port:                     webport  80/TCP
TargetPort:               80/TCP
Endpoints:                <none>          # ← 关键:没有后端
Session Affinity:         None

NAME        READY   STATUS    RESTARTS   AGE   LABELS
webserver   1/1     Running   0          77s   run=webserver

Endpoints: <none> 说明:Pod 在跑,但 Service 选不中它 ------因为 Service 要 app=webserver,Pod 给的是 run=webserver

4. 给 Pod 补上正确标签

bash 复制代码
[root@k8s-master services]# kubectl label pods webserver app=webserver
pod/webserver labeled

再回到监控窗口,立刻看到:

text 复制代码
Endpoints:                10.244.2.7:80      # ← 自动加进来了

NAME        READY   STATUS    RESTARTS   AGE     LABELS
webserver   1/1     Running   0          3m10s   app=webserver,run=webserver

直接 curl Service 的 VIP 也通了:

bash 复制代码
[root@k8s-master services]# curl 10.100.62.227
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

经验法则 :Service 的 selector 与 Pod 的 label 是匹配关系,少一个字段都算不匹配 。所以生产中约定一个统一的 app=xxx 标签,Service 就盯这一个标签去找 Pod 即可。


五、实验二:用 Service 暴露 Deployment 控制器

上一个实验只暴露了单个 Pod。生产中没人会这么干,因为 Pod 挂了就没了。我们要的是:Deployment 维护 N 个 Pod 副本,Service 自动把所有 Pod 加到 Endpoints,副本被删重建后 Service 自动跟上。

1. 一键生成 Deployment + Service 组合 yaml

bash 复制代码
[root@k8s-master services]# kubectl create service clusterip --tcp 80:80 webservice \
  --dry-run=client -o yaml >> webservice.yaml
[root@k8s-master services]# kubectl create deployment webcluster --image myapp:v1 \
  --dry-run=client -o yaml >> webservice.yaml
[root@k8s-master services]# vim webservice.yaml

selector: app=webcluster 与 Deployment labels: app=webcluster 对齐,让 Service 选得到 Pod:

yaml 复制代码
---
apiVersion: v1
kind: Service
metadata:
  labels: { app: webservice }
  name: webservice
spec:
  ports:
  - name: webport
    port: 80
    protocol: TCP
    targetPort: 80
  selector: { app: webcluster }      # ← 对齐 Deployment
  type: ClusterIP

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels: { app: webcluster }
  name: webcluster
spec:
  replicas: 2
  selector:
    matchLabels: { app: webcluster }
  template:
    metadata:
      labels: { app: webcluster }
    spec:
      containers:
      - image: myapp:v1
        name: myapp

2. 应用 & 验证

bash 复制代码
[root@k8s-master services]# kubectl apply -f webservice.yaml
service/webservice created
deployment.apps/webcluster created
bash 复制代码
# 持续观察(另一终端跑)
[root@k8s-master ~]# watch -n 1 \
  "kubectl describe svc webservice ; \
   kubectl get deploy --show-labels; \
   kubectl get pods -o wide --show-labels"
text 复制代码
Name:             webservice
Selector:         app=webcluster
Type:             ClusterIP
IP:               10.98.31.55
Port:             webport  80/TCP
TargetPort:       80/TCP
Endpoints:        10.244.2.8:80,10.244.5.95:80     # ← 2 个 Pod 都进来了

NAME         READY   UP-TO-DATE   AVAILABLE   AGE   LABELS
webcluster   2/2     2            2           3m41s app=webcluster

NAME                          READY   STATUS    IP            NODE
webcluster-77c87d9946-pz5nq   1/1     Running   10.244.5.95   k8s-node2
webcluster-77c87d9946-tlzxj   1/1     Running   10.244.2.8    ceph

3. 访问验证(负载均衡)

myapp 镜像有个 /hostname.html 接口,会返回当前 Pod 的主机名。多 curl 几次,看到两个 Pod 轮询被命中

bash 复制代码
[root@k8s-master services]# curl 10.98.31.55/hostname.html
webcluster-77c87d9946-tlzxj
[root@k8s-master services]# curl 10.98.31.55/hostname.html
webcluster-77c87d9946-pz5nq
[root@k8s-master services]# curl 10.98.31.55/hostname.html
webcluster-77c87d9946-pz5nq

六、kube-proxy:iptables vs ipvs 模式

Service 本身只是一段配置 ,真正负责"把请求转发到后端 Pod"的是每个节点上的 kube-proxy 组件。kube-proxy 一直在监听 apiserver,发现 Service / Endpoints 变化就刷新本机的转发规则。它有两种实现模式:

模式 实现方式 性能 适用场景
iptables(老版本默认) 在宿主机上写大量 iptables 规则(KUBE-SERVICES 链),顺序匹配 规则多时 O(n),CPU 消耗高 小规模集群(< 1000 Service)
ipvs(推荐) 内核态哈希表 + 虚拟网卡 kube-ipvs0 承载所有 Service IP,O(1) 查找 万级 Pod 也能撑住 生产环境必选

切换 ipvs 的命令很简单,但改完配置必须重建 kube-proxy Pod 才会生效(它是 DaemonSet,会自动重建一个干净的):

bash 复制代码
# 所有节点装工具,用于查看 ipvs 规则
[root@k8s-master ~]# for name in 100 10 20 200; do
>   ssh -l root 172.25.254.$name "dnf install ipvsadm -y"
> done

# 改 ConfigMap
[root@k8s-master ~]# kubectl -n kube-system edit cm kube-proxy
# 把 mode: "" 改成 mode: "ipvs"
[root@k8s-master ~]# kubectl -n kube-system get pods | awk '/kube-proxy/{system("kubectl -n kube-system delete pods "$1)}'

切换成功后,每台节点都会多出一张虚拟网卡 kube-ipvs0,上面挂着所有 Service 的 VIP:

bash 复制代码
[root@k8s-master ~]# ip a | tail
8: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether fe:9f:c8:5d:a6:c8 brd ff:ff:ff:ff:ff:ff
    inet 10.96.0.10/32 scope global kube-ipvs0
    inet 10.96.0.1/32  scope global kube-ipvs0
    inet 10.97.59.25/32 scope global kube-ipvs0    # ← 我们的 Service VIP

ipvsadm -Ln 能看到所有 Service 的转发规则和调度算法

bash 复制代码
[root@k8s-master ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.96.0.1:443 rr
  -> 172.25.254.100:6443          Masq    1      0          0
TCP  10.96.0.10:53 rr                                    # kube-dns
  -> 10.244.1.2:53                Masq    1      0          0
  -> 10.244.1.3:53                Masq    1      0          0
TCP  10.98.31.55:80 rr                                   # 我们的 webservice
  -> 10.244.2.8:80                Masq    1      0          0
  -> 10.244.5.95:80               Masq    1      0          0

ipvs 常用的调度算法:

缩写 全称 用途
rr Round Robin 轮询(默认)
lc Least Connection 最少连接
dh Destination Hashing 目标地址哈希(搭配 Ingress 持久化)
sh Source Hashing 源地址哈希(会话保持
sed Shortest Expected Delay 最短期望延迟
nq Never Queue 永不排队

七、实验三:ClusterIP(含 Headless 无头服务)

1. 普通 ClusterIP

最常见的用法,前面实验二其实就是 ClusterIP。再次确认一下基础 yaml:

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: webservice
spec:
  selector: { app: webcluster }
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: ClusterIP        # 默认就是 ClusterIP,不写也行
bash 复制代码
[root@k8s-master services]# kubectl apply -f webservice.yaml
[root@k8s-master services]# kubectl describe svc webservice
text 复制代码
Name:        webservice
Type:        ClusterIP
IP:          10.109.112.7        # ← vip,访问这个就能被调度到后端 Pod
Endpoints:   10.244.5.98:80,10.244.2.14:80

2. 集群内部用 DNS 解析

K8s 集群内置 coredns(Service 名是 kube-dns)做服务发现。ServiceName.<namespace>.svc.cluster.local 就是完整的 FQDN 域名:

bash 复制代码
[root@k8s-master services]# kubectl -n kube-system get svc
NAME       TYPE        CLUSTER-IP   PORT(S)
kube-dns   ClusterIP   10.96.0.10   53/UDP,53/TCP,9153/TCP
bash 复制代码
# dig <servicename>.<namespace>.<sourcetype>.cluster.local @<coredns>
[root@k8s-master services]# dig webservice.default.svc.cluster.local @10.96.0.10
text 复制代码
;; ANSWER SECTION:
webservice.default.svc.cluster.local. 30 IN A   10.109.112.7

;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)

经验 :集群内部通信用服务名而不是 IP,因为 IP 会变,名字不会。这也是微服务架构"服务发现"的基础。

3. Headless(无头)服务

Headless Service 是一种特殊的 ClusterIP:设置 clusterIP: None 之后:

  • Service 不分配 VIP
  • kube-proxy 不处理它(也就没有负载均衡);
  • DNS 解析直接返回所有后端 Pod 的真实 IP(多条 A 记录),由客户端自己挑一个连。

适用场景:有状态服务 (MySQL 主从、Redis Cluster、Elasticsearch 等)需要客户端直连指定实例时,Headless 配合 StatefulSet 是标准方案。

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: webservice
spec:
  selector: { app: webcluster }
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: ClusterIP
  clusterIP: None          # ← 关键:没有 VIP
bash 复制代码
[root@k8s-master services]# kubectl apply -f webservice.yaml
[root@k8s-master services]# kubectl describe svc webservice
text 复制代码
Name:        webservice
Type:        ClusterIP
IP:          None                 # ← 没有 vip
IPs:         None
Port:        webport  80/TCP
Endpoints:   10.244.2.15:80,10.244.5.99:80
bash 复制代码
[root@k8s-master services]# dig webservice.default.svc.cluster.local @10.96.0.10
text 复制代码
;; ANSWER SECTION:
webservice.default.svc.cluster.local. 30 IN A   10.244.5.99
webservice.default.svc.cluster.local. 30 IN A   10.244.2.15

直接得到两个 Pod IP,没有负载均衡,客户端自己选。

4. 在 Pod 里验证 DNS 解析

bash 复制代码
[root@k8s-master ~]# kubectl run test --image busyboxplus -it
/ # nslookup webservice
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name:     webservice
Address 1: 10.244.5.99
Address 2: 10.244.2.15
/ # curl webservice
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

八、实验四:NodePort(含端口范围破限)

ClusterIP 只能在集群内部访问。如果想让集群外的用户 (开发、测试、对端服务)也能访问,最简单的办法是 NodePort ------在每台节点上同一个端口开洞,外部通过 任意节点IP:nodePort 都能访问到。

1. 创建 NodePort

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: webservice
spec:
  ports:
  - name: webport
    port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30951           # ← 手动指定;不写就在 30000-32767 随机一个
  selector: { app: webcluster }
  type: NodePort
bash 复制代码
[root@k8s-master ~]# kubectl apply -f nodeport.yaml

2. 验证转发规则

bash 复制代码
[root@k8s-master services]# ipvsadm -Ln
TCP  172.17.0.1:30951 rr              # docker 网桥 IP + nodePort
  -> 10.244.2.16:80  Masq  1  0  0
  -> 10.244.5.100:80 Masq  1  0  0
TCP  172.25.254.100:30951 rr          # 节点真实网卡 IP + nodePort
  -> 10.244.2.16:80  Masq  1  0  0
  -> 10.244.5.100:80 Masq  1  0  0

3. 外部访问

bash 复制代码
# 在集群外任意一台机器(这里是被机主机的 Windows 终端)
[Administrator.DESKTOP-VJ307M3] ➤ curl 172.25.254.100:30951/hostname.html
webcluster-77c87d9946-ks6cd
[Administrator.DESKTOP-VJ307M3] ➤ curl 172.25.254.100:30951/hostname.html
webcluster-77c87d9946-rn6rh

即便请求打到的是 k8s-master 节点,流量也会自动转发到真正运行 Pod 的节点(这里是 k8s-node2 / ceph),不需要 Pod 在哪台机器上就让用户连哪台。

4. 端口破限:30000-32767 不够用怎么办

yaml 复制代码
- name: webport
  port: 80
  targetPort: 80
  nodePort: 44444        # ← 超出范围,会报错
bash 复制代码
[root@k8s-master services]# kubectl apply -f nodeport.yaml
The Service "webservice" is invalid:
spec.ports[0].nodePort: Invalid value: 44444:
provided port is not in the valid range. The range of valid ports is 30000-32767

破限方法:修改 apiserver 的启动参数(apiserver 是 static pod,改完会自动重建):

bash 复制代码
[root@k8s-master services]# vim /etc/kubernetes/manifests/kube-apiserver.yaml
42     - --service-node-port-range=30000-50000

保存即生效(不需要手动重启 apiserver)。等 apiserver 重新 Ready 之后再 apply:

bash 复制代码
[root@k8s-master services]# kubectl apply -f nodeport.yaml
service/webservice created
[root@k8s-master services]# kubectl describe svc webservice
NodePort:    webport  44444/TCP

九、实验五:LoadBalancer + MetalLB 裸金属环境

NodePort 已经能让"外部访问"了,但每次让用户记"哪台节点 IP + 端口"很不优雅。LoadBalancer 的目标是给你一个单一稳定的外部 VIP,背后是云厂商的负载均衡器(AWS ALB / 阿里 SLB 等)。

1. 裸金属环境的痛点

type 改成 LoadBalancer 试试:

yaml 复制代码
spec:
  type: LoadBalancer
bash 复制代码
[root@k8s-master services]# kubectl apply -f loadbalancer.yml
[root@k8s-master services]# kubectl get svc
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP      10.96.0.1      <none>        443/TCP        4d5h
webservice   LoadBalancer   10.111.5.103   <pending>     80:31426/TCP   12s
#                                                                 ^
#                                                  EXTERNAL-IP 一直是 pending

因为我们不是云厂商,没有 LB 资源 可分配。裸金属环境要自己解决 LB 问题,靠 MetalLB

2. MetalLB 是什么

MetalLB 是为裸金属 / 自建机房 K8s 集群实现 LoadBalancer 类型的开源项目。它能做两件事:

  1. 从地址池分配 VIPIPAddressPool,支持二层 ARP / 三层 BGP 两种模式);
  2. 用 ARP/NDP 让 VIP 在局域网内可达(speaker 是 DaemonSet,每个节点一个)。

3. 部署 MetalLB

Step 1:所有节点装 ipvsadm,并让 kube-proxy 支持 strictARP

bash 复制代码
# 所有节点装 ipvsadm
[root@k8s-master ~]# for name in 100 10 20 200; do
>   ssh -l root 172.25.254.$name "dnf install ipvsadm -y"
> done
bash 复制代码
[root@k8s-master mateLLB]# kubectl edit configmap -n kube-system kube-proxy
yaml 复制代码
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  strictARP: true                    # ← 必须开,speaker 才能用 ARP 宣告 VIP
bash 复制代码
# 删掉所有 kube-proxy Pod 让它们重新加载配置
[root@k8s-master mateLLB]# kubectl -n kube-system delete pods kube-proxy-xxxx

Step 2:上传镜像到私有仓库

bash 复制代码
# 离线环境:先 load 镜像包
[root@k8s-master metaLLB]# docker load -i metallb-v0.16.1.tar
[root@k8s-master metaLLB]# docker tag quay.io/metallb/controller:v0.16.1  reg.timinglee.org/metallb/controller:v0.16.1
[root@k8s-master metaLLB]# docker tag quay.io/metallb/speaker:v0.16.1     reg.timinglee.org/metallb/speaker:v0.16.1
[root@k8s-master metaLLB]# docker push reg.timinglee.org/metallb/controller:v0.16.1
[root@k8s-master metaLLB]# docker push reg.timinglee.org/metallb/speaker:v0.16.1

Step 3:改 deploy.yaml 镜像地址 + 应用

bash 复制代码
# 把 controller / speaker 镜像地址改成自己仓库
[root@k8s-master metaLLB]# vim 1-metallb-native.yaml
2136:        image: reg.timinglee.org/metallb/controller:v0.16.1
2233:        image: reg.timinglee.org/metallb/speaker:v0.16.1

[root@k8s-master metaLLB]# kubectl apply -f 1-metallb-native.yaml

Step 4:配置地址池 + L2 通告

bash 复制代码
[root@k8s-master metaLLB]# vim 2-metallb-confmap.yml
yaml 复制代码
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 172.25.254.50-172.25.254.99      # ← 改成你本机空闲网段
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool
bash 复制代码
[root@k8s-master metaLLB]# kubectl apply -f 2-metallb-confmap.yml

4. 验证

bash 复制代码
[root@k8s-master metaLLB]# kubectl -n metallb-system get pods
NAME                         READY   STATUS    RESTARTS   AGE
controller-b7cdf5787-2hf62   1/1     Running   0          17m
speaker-4spld                1/1     Running   0          17m
speaker-h84jr                1/1     Running   0          17m
speaker-tsqns                1/1     Running   0          17m
speaker-xnb7g                1/1     Running   0          17m

重新创建一次 Service 让它从 MetalLB 拿 VIP:

bash 复制代码
[root@k8s-master services]# kubectl delete -f loadbalancer.yml
[root@k8s-master services]# kubectl apply -f loadbalancer.yml
[root@k8s-master services]# kubectl get svc
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)
kubernetes   ClusterIP      10.96.0.1      <none>          443/TCP
webservice   LoadBalancer   10.111.5.103   172.25.254.50   80:31426/TCP
#                                                              ^^^^^^^^^^^
#                                                     拿到 MetalLB 分配的 VIP 了

集群外直接访问这个 VIP:

bash 复制代码
[Administrator.DESKTOP-VJ307M3] ➤ curl 172.25.254.50
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

⚠️ 地址池网段必须和宿主机网段一致 ,且这段 IP 不能再被其他机器占用,否则 ARP 会冲突。L2 模式简单但单点(一个 speaker 节点负责响应 ARP),需要高可用就上 BGP 模式。


十、ExternalName:给集群内访问打个外部域名别名

ExternalName 是一种特殊的 Service:

  • 不分配 ClusterIP;
  • kube-proxy 不处理它;
  • 集群内访问这个 Service 名时,CoreDNS 把它解析成 CNAME 记录 ,指向 spec.externalName 指定的外部域名。
yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: externalname-demo
spec:
  selector:                  # 没有 selector,因为外部没有 Pod
    app: externalname
  type: ExternalName
  externalName: www.timinglee.org
bash 复制代码
[root@k8s-master ~]# kubectl apply -f extrnalname.yaml
[root@k8s-master services]# kubectl get svc externalname-demo
NAME                TYPE           CLUSTER-IP   EXTERNAL-IP         PORT(S)   AGE
externalname-demo   ExternalName   <none>       www.timinglee.org   <none>    5s

典型用途

  1. 业务迁移过渡 :外部老系统要被迁进 K8s,但短期内还需要让老调用方继续用原来的域名。先把外部域名映射成 Service 名,等 Pod 都进 K8s 了再切换到 ClusterIP,业务代码不用改。
  2. 跨集群访问:把外部服务的真实域名封装成一个 K8s 内部 Service 名,配置统一。

十一、实验六:Ingress 七层代理(部署 + 路径 + 域名 + TLS + 认证 + 重写)

Service + ClusterIP / NodePort / LoadBalancer 都是 四层(TCP/UDP)代理。如果想要:

  • 一个 VIP 背后根据域名分发给不同业务;
  • 根据 URL 路径 分发(/v1 给 A,/v2 给 B);
  • HTTPS 终止(TLS 证书卸载);
  • Basic Auth 用户认证;
  • URL 重写 / 灰度发布;

就得靠 Ingress。Ingress 由两部分组成:

  1. Ingress Controller :实际干活的代理(nginx / envoy / traefik),本文用官方 ingress-nginx
  2. Ingress 资源对象 :你写的 yaml 规则(host + path → backend service)。

1. 部署 ingress-nginx

Step 1:拉镜像 / 离线加载镜像

bash 复制代码
[root@k8s-master ingress-v1.5.1]# docker load -i ingress-v1.15.1.tar
[root@k8s-master ingress-v1.5.1]# docker tag \
  registry.k8s.io/ingress-nginx/controller:v1.15.1 \
  reg.timinglee.org/ingress-nginx/controller:v1.15.1
[root@k8s-master ingress-v1.5.1]# docker tag \
  registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.6.9 \
  reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
[root@k8s-master ingress-v1.5.1]# docker push reg.timinglee.org/ingress-nginx/controller:v1.15.1
[root@k8s-master ingress-v1.5.1]# docker push reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9

Step 2:下载 deploy.yaml 并改镜像

bash 复制代码
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/baremetal/deploy.yaml
bash 复制代码
[root@k8s-master ingress-v1.5.1]# vim deploy.yaml
# 改三处镜像地址为自己的私有仓库
444:        image: reg.timinglee.org/ingress-nginx/controller:v1.15.1
547:        image: reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
603:        image: reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
# 把 service type 改成 LoadBalancer(裸金属环境走 MetalLB 拿 VIP)
365:        type: LoadBalancer

Step 3:部署并验证

bash 复制代码
[root@k8s-master ingress-v1.5.1]# kubectl apply -f deploy.yaml
[root@k8s-master ingress-v1.5.1]# kubectl -n ingress-nginx get svc
NAME                                 TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)
ingress-nginx-controller             LoadBalancer   10.103.86.192    172.25.254.50   80:42476/TCP,443:31023/TCP
ingress-nginx-controller-admission   ClusterIP      10.103.198.161   <none>          443/TCP

NAME                                       READY   UP-TO-DATE   AVAILABLE
deployment.apps/ingress-nginx-controller   1/1     1            1

EXTERNAL-IP: 172.25.254.50 正是上一节 MetalLB 分配的地址------ingress-nginx-controller 自己也声明了一个 LoadBalancer 类型 Service,从 MetalLB 拿到了 IP。

2. 基于路径的访问

业务准备:建两个版本的 Deployment + Service。

bash 复制代码
# 业务 1:myapp:v1
[root@k8s-master services]# kubectl create deployment myappv1 --image myapp:v1 \
  --replicas 2 --dry-run=client -o yaml > myappv1.yaml
[root@k8s-master services]# kubectl create service clusterip servicev1 --tcp 80:80 \
  --dry-run=client -o yaml >> myappv1.yaml
# vim 把 spec.selector 改成 app: myappv1
[root@k8s-master services]# kubectl apply -f myappv1.yaml

# 业务 2:myapp:v2(用上面 yaml 当模板改一份即可)
# 关键差异:镜像 myapp:v2、label app=myappv2、service 名 servicev2
bash 复制代码
[root@k8s-master services]# curl 10.97.35.20
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[root@k8s-master services]# curl 10.100.116.247
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>

Ingress 规则

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /      # 把 /v1、/v1/xxx 都重写为 / 再转发
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /v1
        pathType: Prefix                                  # 前缀匹配
      - backend:
          service: { name: servicev2, port: { number: 80 } }
        path: /v2
        pathType: Prefix
bash 复制代码
[root@k8s-master services]# kubectl apply -f 1-ingress.yml

测试

bash 复制代码
# /etc/hosts 加解析
[Administrator.DESKTOP-VJ307M3] ➤ vim /etc/hosts
172.25.254.50 www.timinglee.org

[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v1
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v2
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
# rewrite-target 让 /v2/aaaa 也仍然能命中 servicev2
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v2/aaaa
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>

pathType 的三种取值(决定 URL 怎么匹配):

含义
Exact 完全匹配,多一个字符都不行
Prefix 前缀匹配,/v1 能匹配 /v1/v1/abc(按 / 分段,不会 匹配 /v1abc
ImplementationSpecific 由 Controller 自己决定;配 nginx.ingress.kubernetes.io/use-regex: "true" 可写正则

3. 基于域名的访问

一个 VIP 想给多个域名用,那就一个 host 一条 rule:

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
spec:
  ingressClassName: nginx
  rules:
  - host: myapp1.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /
        pathType: Prefix
  - host: myapp2.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev2, port: { number: 80 } }
        path: /
        pathType: Prefix
bash 复制代码
# /etc/hosts 把这两个域名都指向 VIP
172.25.254.50 www.timinglee.org myapp1.timinglee.org myapp2.timinglee.org

[Administrator.DESKTOP-VJ307M3] ➤ curl myapp1.timinglee.org
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[Administrator.DESKTOP-VJ307M3] ➤ curl myapp2.timinglee.org
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>

4. 动静分离 / 正则路径

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /index.html
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /php
        pathType: ImplementationSpecific
      - backend:
          service: { name: servicev2, port: { number: 80 } }
        path: /static
        pathType: ImplementationSpecific
bash 复制代码
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/php/
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/static/
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>

5. TLS 加密访问

Step 1:生成自签证书

bash 复制代码
[root@k8s-master ~]# openssl req -newkey rsa:2048 -nodes -keyout tls.key \
  -x509 -days 365 -subj "/CN=nginxsvc/O=nginxsvc" -out tls.crt

Step 2:把证书塞进 K8s Secret

bash 复制代码
[root@k8s-master ~]# kubectl create secret tls web-tls-secret --key tls.key --cert tls.crt
secret/web-tls-secret created

Step 3:在 Ingress 里引用

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"     # 访问 HTTP 强制跳 HTTPS
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  tls:
  - hosts:
    - myapp-tls.timinglee.org
    secretName: web-tls-secret
  ingressClassName: nginx
  rules:
  - host: myapp-tls.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /
        pathType: Prefix
bash 复制代码
[Administrator.DESKTOP-VJ307M3] ➤ vim /etc/hosts
172.25.254.50 ... myapp-tls.timinglee.org
[Administrator.DESKTOP-VJ307M3] ➤ curl -k https://myapp-tls.timinglee.org
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

生产中证书用 Let's Encrypt / cert-manager 自动签发,不要用自签。Secret 存的是 K8s 内部一种"配置注入"机制,并不是加密手段。

6. Basic Auth 用户认证

Step 1:生成认证文件

bash 复制代码
[root@k8s-master ~]# dnf install httpd-tools -y
[root@k8s-master ~]# htpasswd -cm auth admin
# 输入两次密码
[root@k8s-master ~]# cat auth
admin:$apr1$k8PiN9hR$TWLIGse6Y9oGgGFZr96P00

Step 2:注入为 K8s Secret

bash 复制代码
[root@k8s-master ~]# kubectl create secret generic web-auth --from-file auth

Step 3:Ingress 加认证注解

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
  annotations:
    nginx.ingress.kubernetes.io/auth-type: basic
    nginx.ingress.kubernetes.io/auth-secret: web-auth
    nginx.ingress.kubernetes.io/auth-realm: "Please input username and password"
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /v1
        pathType: Prefix

验证

bash 复制代码
# 不带用户名密码 → 401
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v1
<html><head><title>401 Authorization Required</title></head>
<body><center><h1>401 Authorization Required</h1></center></body></html>

# 带用户名密码 → 正常返回
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v1 -uadmin:lee
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

7. URL 重写与默认文件

场景 A:访问 / 直接跳到某个具体文件

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
  annotations:
    nginx.ingress.kubernetes.io/app-root: /hostname.html    # 默认入口
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /
        pathType: Prefix
bash 复制代码
[root@k8s-master services]# curl -I www.timinglee.org
HTTP/1.1 302 Moved Temporarily
Location: http://www.timinglee.org/hostname.html

[root@k8s-master services]# curl -L www.timinglee.org
webcluster-77c87d9946-xxxx         # 跟到 hostname.html 去了

场景 B:正则重写,路径变着花样都能匹配

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2          # 第二个捕获组作为新路径
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /
        pathType: Prefix
      - backend:
          service: { name: servicev2, port: { number: 80 } }
        path: /lee(/.*|$)(.*)            # 匹配 /lee, /lee/abc, /lee/a/b...
        pathType: ImplementationSpecific
bash 复制代码
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org
Hello MyApp | Version: v1
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org/lee
Hello MyApp | Version: v2
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org/lee/a
Hello MyApp | Version: v2
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org/lee/a/a
Hello MyApp | Version: v2

rewrite-target: /$2 意思是把正则里第 2 个括号捕获到的内容作为新路径 转发。/lee(/.*|$)(.*) 两个括号:第 1 个是 /xxx 部分的中间串,第 2 个是剩余内容 ------这样 /lee 后面接啥都能稳稳命中 v2。


十二、实验七:金丝雀(灰度)发布

金丝雀发布 (Canary / 灰度发布):新版本先放一小部分流量 上去验证,没问题再逐步放大,直到全量替换。本质是"在保证老版本可用 + Pod 总量不低于期望值的前提下先加后减"。

我们继续用 ingress-nginxcanary 注解来做,两种方式

1. 基于请求头(精准控制)

适用场景:想让"特定测试用户 / 内部账号"先去新版本。

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress1                # 正式:v1
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev1, port: { number: 80 } }
        path: /
        pathType: Prefix
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress2                # 灰度:v2
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "version"   # 自定义请求头名
    nginx.ingress.kubernetes.io/canary-by-header-value: "2"   # 该头等于 2 时走 v2
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev2, port: { number: 80 } }
        path: /
        pathType: Prefix
bash 复制代码
# 普通请求 → 走 v1
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org
Hello MyApp | Version: v1

# 带 version: 2 请求头 → 走 v2
[Administrator.DESKTOP-VJ307M3] ➤ curl -H "version:2" www.timinglee.org
Hello MyApp | Version: v2

2. 基于权重(按比例分流)

适用场景:想让"随机 10% 的真实用户"先去新版本。

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress2
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"        # 10% 流量到 v2
    nginx.ingress.kubernetes.io/canary-weight-total: "100" # 总权重 100
spec:
  ingressClassName: nginx
  rules:
  - host: www.timinglee.org
    http:
      paths:
      - backend:
          service: { name: servicev2, port: { number: 80 } }
        path: /
        pathType: Prefix

写个脚本跑 100 次统计比例

bash 复制代码
[root@k8s-master services]# vim check_cannary.sh
#!/bin/bash
v1=0
v2=0
for (( i=0; i<100; i++ ))
do
    response=$(curl -s www.timinglee.org | grep -c v1)
    v1=$(expr $v1 + $response)
    v2=$(expr $v2 + 1 - $response)
done
echo "v1:$v1, v2:$v2"
bash 复制代码
[root@k8s-master services]# sh check_cannary.sh
v1:95, v2:5    # 接近 10%,但因为是概率,结果会有小波动

灰度逐步放量流程:先 5% → 20% → 50% → 100%,每次观察监控 / 错误率,没问题再放量。一旦监控到异常,立刻把 canary-weight 改成 0,相当于快速回滚。

相关推荐
源代码•宸2 小时前
前置准备:定时微服务背景和现状
开发语言·经验分享·后端·微服务·云原生·架构·golang
想要成为老金高手3 小时前
Kubernetes 调度器详解:从 nodeName 到污点容忍
java·容器·kubernetes
名字还没想好☜4 小时前
Docker buildx 多架构镜像实战:一次构建 amd64+arm64,告别 exec format error
运维·docker·eureka·架构·kubernetes
gs801405 小时前
告别 CI/CD 误伤与红条:Docker 镜像智能清理与优雅防冲突实战
ci/cd·docker·容器
wjcroom5 小时前
FileBrowser的docker运行了改变密码长度的做法
运维·docker·容器
码云数智-园园7 小时前
我为什么从微服务退回单体架构
微服务·云原生·架构
DevOps老兵7 小时前
AI Infra实战05:用Helm在K8s中部署vLLM,从安装到压测全流程
人工智能·kubernetes·helm·vllm·大模型推理·ai infra
Asum1ta7 小时前
生产环境 Kubernetes 高可用集群部署实战:外部 etcd + Keepalived + HAProxy + 多 Master 架构
架构·kubernetes·etcd
dazhong20128 小时前
Docker 进阶篇(一) CentOS 7 离线升级 Docker 完整实战
docker·容器·centos