Submariner 是一个完全开源的项目,可以帮助我们在不同的 Kubernetes 集群之间(无论是在本地还是云端)实现网络通信。Submariner 有以下功能:
Submariner 由几个主要部分组成:
Submariner 中跨集群的 DNS 服务发现由以下两个组件基于 Kubernetes Multi-Cluster Service APIs 的规范来实现:
clusterset.local
域名的解析请求发往 Lighthouse DNS server。MCS API 是 Kubernetes 社区定义的用于跨集群服务发现的规范,主要包含了 ServiceExport 和 ServiceImport 两个 CRD。
apiVersion: multicluster.k8s.io/v1alpha1
kind: ServiceExport
metadata:
name: my-svc
namespace: my-ns
apiVersion: multicluster.k8s.io/v1alpha1
kind: ServiceImport
metadata:
name: my-svc
namespace: my-ns
spec:
ips:
- 42.42.42.42 # 跨集群访问的 IP 地址
type: "ClusterSetIP"
ports:
- name: http
protocol: TCP
port: 80
sessionAffinity: None
Gateway Engine 部署在每个集群中,负责建立到其他集群的隧道。隧道可以由以下方式实现:
可以在使用 subctl join
命令加入集群的时候使用 --cable-driver
参数设置隧道的类型。
Gateway Engine 部署为 DaemonSet,只在有 submariner.io/gateway=true
Label 的 Node 上运行,当我们使用 subctl join
命令加入集群的时候,如果没有 Node 有该 Label,会提示我们选择一个 Node 作为 Gateway Node。
Submariner 也支持 active/passive 高可用模式的 Gateway Engine,我们可以在多个节点上部署 Gateway Engine。在同一时间内,只能有一个 Gateway Engine 处于 active 状态来处理跨集的流量,Gateway Engine 通过领导者选举的方式确定 active 的实例,其他实例在 passive 模式下等待,准备在 active 实例发生故障时接管。
Submariner 的一大亮点是支持在不同集群间存在 CIDR 重叠的情况,这有助于减少网络重构的成本。例如,在部署过程中,某些集群可能使用了默认的网段,导致了 CIDR 重叠。在这种情况下,如果后续需要更改集群网段,可能会对集群的运行产生影响。
为了支持集群间重叠 CIDR 的情况,Submariner 通过一个 GlobalCIDR 网段(默认是 242.0.0.0/8)在流量进出集群时进行 NAT 转换,所有的地址转换都发生在 active Gateway Node 上。在 subctl deploy
部署 Broker 的时候可以通过 --globalnet-cidr-range
参数指定所有集群的全局 GlobalCIDR。在 subctl join
加入集群的时候还可以通过 --globalnet-cidr
参数指定该集群的 GlobalCIDR。
导出的 ClusterIP 类型的 Service 会从 GlobalCIDR 分配一个 Global IP 用于入向流量,对于 Headless 类型的 Service,会为每个关联的 Pod 分配一个 Global IP 用于入向和出向流量。
在本次实验中,我们使用一台运行 Ubuntu 20.04 的虚拟机,并通过 Kind 启动多个 Kubernetes 集群来进行测试。
root@seven-demo:~# seven-demo
Static hostname: seven-demo
Icon name: computer-vm
Chassis: vm
Machine ID: f780bfec3c409135b11d1ceac73e2293
Boot ID: e83e9a883800480f86d37189bdb09628
Virtualization: kvm
Operating System: Ubuntu 20.04.5 LTS
Kernel: Linux 5.15.0-1030-gcp
Architecture: x86-64
安装相关软件和命令行工具。
# 安装 Docker,根据操作系统安装 https://docs.docker.com/engine/install/
sudo apt-get update
sudo apt-get install -y \
ca-certificates \
curl \
gnupg
sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 安装 Kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.18.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# 安装 kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin
apt install -y bash-completion
echo 'source <(kubectl completion bash)' >>~/.bashrc
# 安装 subctl
curl -Lo subctl-release-0.14-linux-amd64.tar.xz https://github.com/submariner-io/subctl/releases/download/subctl-release-0.14/subctl-release-0.14-linux-amd64.tar.xz
tar -xf subctl-release-0.14-linux-amd64.tar.xz
mv subctl-release-0.14/subctl-release-0.14-linux-amd64 /usr/local/bin/subctl
chmod +x /usr/local/bin/subctl
使用 Kind 创建一个 3 节点的集群,这里读者需要将 SERVER_IP
替换成自己服务器的 IP。默认情况下,Kind 将 Kubernetes API 服务器 IP:Port 设置为本地环回地址 ( 127.0.0.1
): 随机端口,这对于从本机与集群交互来说很好,但是在本实验中多个 Kind 集群之间需要通信,因此我们需要把 Kind 的 apiServerAddress 改成本机 IP。
# 替换成服务器 IP
export SERVER_IP="10.138.0.11"
kind create cluster --config - <kind: Cluster
name: broker
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
networking:
apiServerAddress: $SERVER_IP
podSubnet: "10.7.0.0/16"
serviceSubnet: "10.77.0.0/16"
EOF
kind create cluster --config - <kind: Cluster
name: c1
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
networking:
apiServerAddress: $SERVER_IP
podSubnet: "10.8.0.0/16"
serviceSubnet: "10.88.0.0/16"
EOF
kind create cluster --config - <kind: Cluster
name: c2
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
networking:
apiServerAddress: $SERVER_IP
podSubnet: "10.9.0.0/16"
serviceSubnet: "10.99.0.0/16"
EOF
在本次实验中,我们专门将一个集群配置为 Broker 集群。Broker 集群可以是专用集群,也可以是连接的集群之一。执行 subctl deploy-broker
命令部署 Broker,Broker 只包含了一组 CRD,并没有部署 Pod 或者 Service。
subctl --context kind-broker deploy-broker
部署完成后,会生成 broker-info.subm
文件,文件以 Base64 加密,其中包含了连接 Broker 集群 API Server 的地址以及证书信息,还有 IPsec 的密钥信息。
{
"brokerURL": "https://10.138.0.11:45681",
"ClientToken": {
"metadata": {
"name": "submariner-k8s-broker-admin-token-f7b62",
"namespace": "submariner-k8s-broker",
"uid": "3f949d19-4f42-43d6-af1c-382b53f83d8a",
"resourceVersion": "688",
"creationTimestamp": "2023-04-05T02:50:02Z",
"annotations": {
"kubernetes.io/created-by": "subctl",
"kubernetes.io/service-account.name": "submariner-k8s-broker-admin",
"kubernetes.io/service-account.uid": "da6eeba1-b707-4d30-8e1e-e414e9eae817"
},
"managedFields": [
{
"manager": "kube-controller-manager",
"operation": "Update",
"apiVersion": "v1",
"time": "2023-04-05T02:50:02Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:ca.crt": {},
"f:namespace": {},
"f:token": {}
},
"f:metadata": {
"f:annotations": {
"f:kubernetes.io/service-account.uid": {}
}
}
}
},
{
"manager": "subctl",
"operation": "Update",
"apiVersion": "v1",
"time": "2023-04-05T02:50:02Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:annotations": {
".": {},
"f:kubernetes.io/created-by": {},
"f:kubernetes.io/service-account.name": {}
}
},
"f:type": {}
}
}
]
},
"data": {
"ca.crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJek1EUXdOVEF5TkRVMU1Wb1hEVE16TURRd01qQXlORFUxTVZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBSytXCmIzb0h1ZEJlbU5tSWFBNXQrWmI3TFhKNXRLWDB6QVc5a0tudjQzaGpoTE84NHlSaEpyY3ZSK29QVnNaUUJIclkKc01tRmx3aVltbU5ORzA4c2NLMTlyLzV0VkdFR2hCckdML3VKcTIybXZtYi80aHdwdmRTQjN0UDlkU2RzYUFyRwpYYllwOE4vUmlheUJvbTBJVy9aQjNvZ0MwK0tNcWM0NE1MYnBkZXViWnNSckErN2pwTElYczE3OGgxb25kdGNrClIrYlRnNGpjeS92NTkrbGJjamZSeTczbUllMm9DbVFIbE1XUFpSTkMveDhaTktGekl6UHc4SmZSOERjWk5Xc1YKa1NBUVNVUkpnTEhBbjY5MlhDSEsybmJuN21pcjYvYVZzVVpyTGdVNC9zcWg3QVFBdDFGQkk3NDRpcithTjVxSwpJRnRJenkxU3p2ZEpwMThza3EwQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZFQUhFbndHditwTXNVcHVQRXNqbkQwTEgvSFpNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQTFGckk1cGR1VTFsQzluVldNNwowYlc2VFRXdzYwUTlFVWdsRzc4bkRFZkNKb3ovY2xWclFNWGZrc2Zjc1VvcHZsaE5yWFlpbmd0UEE4aEMrTnRJCmdPZElDZDJGaWFOTjRCYkt3a1NmRkQvbmhjWDU1WmQ0UzN1SzZqb2JWVHIzaXVJRVhIdHg0WVIyS1ZuZitTMDUKQTFtbXdzSG1ZbkhtWEllOUEyL3hKdVhtSnNybWljWTlhMXhtSXVyYzhNalBsa1pZWVU1OFBvZHJFNi9XcnBaawpBbW9qcERIWWIrbnZxa0FuaG9hYUV3b2FEVGxYRjY0M3lVLy9MZE4wTmw5MWkvSHNwQ2tZdVFrQjJmQXNkSGNaCkMrdzQ4WVhYT21pSzZXcmJGYVJnaEVKdjB6UjdsZk50UEVZVWJHWEFxV0ZlSnFTdnM5aUYwbFV1NzZDNkt3YWIKbmdnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"namespace": "c3VibWFyaW5lci1rOHMtYnJva2Vy",
"token": "ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklqaHZWVnBuZVVoZk1uVTFjSEJxU1hOdE1UTk1NbUY0TFRaSlIyVlZVRGd4VjI1dmMyNXBNMjFYZFhjaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUp6ZFdKdFlYSnBibVZ5TFdzNGN5MWljbTlyWlhJaUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sWTNKbGRDNXVZVzFsSWpvaWMzVmliV0Z5YVc1bGNpMXJPSE10WW5KdmEyVnlMV0ZrYldsdUxYUnZhMlZ1TFdZM1lqWXlJaXdpYTNWaVpYSnVaWFJsY3k1cGJ5OXpaWEoyYVdObFlXTmpiM1Z1ZEM5elpYSjJhV05sTFdGalkyOTFiblF1Ym1GdFpTSTZJbk4xWW0xaGNtbHVaWEl0YXpoekxXSnliMnRsY2kxaFpHMXBiaUlzSW10MVltVnlibVYwWlhNdWFXOHZjMlZ5ZG1salpXRmpZMjkxYm5RdmMyVnlkbWxqWlMxaFkyTnZkVzUwTG5WcFpDSTZJbVJoTm1WbFltRXhMV0kzTURjdE5HUXpNQzA0WlRGbExXVTBNVFJsT1dWaFpUZ3hOeUlzSW5OMVlpSTZJbk41YzNSbGJUcHpaWEoyYVdObFlXTmpiM1Z1ZERwemRXSnRZWEpwYm1WeUxXczRjeTFpY205clpYSTZjM1ZpYldGeWFXNWxjaTFyT0hNdFluSnZhMlZ5TFdGa2JXbHVJbjAub1JHM2d6Wno4MGVYQXk5YlZ5b1V2NmoyTERvdFJiNlJyOTF4d0ZiTDMwdFNJY3dnS3FYd3NZbVV1THhtcFdBb2M5LWRSMldHY0ZLYklORlZmUUttdVJMY2JsenlTUFFVMlB3WVVwN1oyNnlxYXFOMG1UQ3ZNWWxSeHp6cWY3LXlXUm8yNE9pWS1nMnNmNmNrRzRPMkdwa2MwTlNoOWRTUGY4dXJTbjZSVGJwbjFtcFZjTy1IQjJWeU5hTE9EdmtWS3RLVFJfVS1ZRGc1NzVtczM0OXM0X2xMZjljZjlvcjFaQXVvXzcyN0E5U0VvZ0JkN3BaSndwb0FEUHZRb1NGR0VLQWZYYTFXXzJWVE5PYXE4cUQxOENVbXVFRUFxMmtoNElBN0d5LVRGdUV2Q0JYUVlzRHYzUFJQTjZpOGlKSFBLVUN1WVNONS1NT3lGX19aNS1WdlhR"
},
"type": "kubernetes.io/service-account-token"
},
"IPSecPSK": {
"metadata": {
"name": "submariner-ipsec-psk",
"creationTimestamp": null
},
"data": {
"psk": "NL7dUK+RagDKPQZZj+7Q7wComj0/wLFbfvnHe12hHxR8+d/FnkEqXfmh8JMzLo6h"
}
},
"ServiceDiscovery": true,
"Components": [
"service-discovery",
"connectivity"
],
"CustomDomains": null
}
执行 subctl join
命令将 c1 和 c2 两个集群加入 Broker 集群。使用 --clusterid
参数指定集群 ID,每个集群 ID 需要唯一。提供上一步生成的 broker-info.subm
文件用于集群注册。
subctl --context kind-c1 join broker-info.subm --clusterid c1
subctl --context kind-c2 join broker-info.subm --clusterid c2
会提示我们选择一个节点作为 Gateway Node,c1 集群选择 c1-worker 节点作为 Gateway,c2 集群选择 c2-worker 节点作为 Gateway。
两个 Gateway Node 的 IP 地址如下,之后会分别使用这两个地址在两个集群间建立隧道连接。
等待 c1 和 c2 集群中 Submariner 的相关组件都运行成功后,执行以下命令查看集群间连接情况。
subctl show connections --context kind-c1
subctl show connections --context kind-c2
可以看到 c1 和 c2 集群分别和对方建立的连接。
查看 c1 gateway 日志,可以看到成功与 c2 集群建立了 IPsec 隧道。
至此,我们已经成功在 c1 和 c2 集群间建立了跨集群的连接,接下来我们将创建服务并演示如何将其导出给其他集群进行访问。
在下面的示例中,我们在 sample Namespace 中创建相关资源。请注意,必须在两个集群中都创建 sample 命名空间,服务发现才能正常工作。
kubectl --context kind-c2 create namespace sample
# 需要确保 c1 集群上也有 sample Namespace,否则 Lighthouse agent 创建 Endpointslices 会失败
kubectl --context kind-c1 create namespace sample
首先测试 ClusterIP 类型的 Service。执行以下命令在 c2 集群创建服务。本实验中 whereami 是一个用 Golang 编写的 HTTP Server,它通过 Downward API 将 Kubernetes 的相关信息(Pod 名字,Pod 所在的 Namespace,Node)注入到容器的环境变量中,当接收到请求时进行输出。另外 whereami 还会打印请求方的 IP 地址和端口信息。
kubectl --context kind-c2 apply -f - <apiVersion: apps/v1
kind: Deployment
metadata:
name: whereami
namespace: sample
spec:
replicas: 3
selector:
matchLabels:
app: whereami
template:
metadata:
labels:
app: whereami
spec:
containers:
- name: whereami
image: cr7258/whereami:v1
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: whereami-cs
namespace: sample
spec:
selector:
app: whereami
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
在 c2 集群查看服务。
root@seven-demo:~# kubectl --context kind-c2 get pod -n sample -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
whereami-754776cdc9-28kgd 1/1 Running 0 19h 10.9.1.18 c2-control-plane <none> <none>
whereami-754776cdc9-8ccmc 1/1 Running 0 19h 10.9.1.17 c2-control-plane <none> <none>
whereami-754776cdc9-dlp55 1/1 Running 0 19h 10.9.1.16 c2-control-plane <none> <none>
root@seven-demo:~# kubectl --context kind-c2 get svc -n sample -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
whereami-cs ClusterIP 10.99.2.201 <none> 80/TCP 19h app=whereami
在 c2 集群中使用 subctl export
命令将服务导出。
subctl --context kind-c2 export service --namespace sample whereami-cs
该命令会在创建一个和 Service 相同名字和 Namespace 的 ServiceExport 资源。
root@seven-demo:~# kubectl get serviceexports --context kind-c2 -n sample whereami-cs -o yaml
apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ServiceExport
metadata:
creationTimestamp: "2023-04-06T13:04:15Z"
generation: 1
name: whereami-cs
namespace: sample
resourceVersion: "327707"
uid: d1da8953-3fa5-4635-a8bb-6de4cd3c45a9
status:
conditions:
- lastTransitionTime: "2023-04-06T13:04:15Z"
message: ""
reason: ""
status: "True"
type: Valid
- lastTransitionTime: "2023-04-06T13:04:15Z"
message: ServiceImport was successfully synced to the broker
reason: ""
status: "True"
type: Synced
ServiceImport 资源会由 Submariner 自动在 c1,c2 集群中创建,IP 地址是 Service 的 ClusterIP 地址。
kubectl --context kind-c1 get -n submariner-operator serviceimport
kubectl --context kind-c2 get -n submariner-operator serviceimport
在 c1 集群创建一个 client Pod 来访问 c2 集群的 whereami 服务。
kubectl --context kind-c1 run client --image=cr7258/nettool:v1
kubectl --context kind-c1 exec -it client -- bash
先尝试下 DNS 解析,ClusterIP Service 类型的 Service 可以通过以下格式进行访问
。
nslookup whereami-cs.sample.svc.clusterset.local
返回的 IP 是在 c2 集群 Service 的 ClusterIP 的地址。
我们查看一下 CoreDNS 的配置文件,这个 Configmap 会被 Submariner Operator 修改,将 clusterset.local
用于跨集群通信的域名交给 Lighthouse DNS 来解析。
root@seven-demo:~# kubectl get cm -n kube-system coredns -oyaml
apiVersion: v1
data:
Corefile: |+
#lighthouse-start AUTO-GENERATED SECTION. DO NOT EDIT
clusterset.local:53 {
forward . 10.88.78.89
}
#lighthouse-end
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
creationTimestamp: "2023-04-05T02:47:34Z"
name: coredns
namespace: kube-system
resourceVersion: "1211"
uid: 698f20a5-83ea-4a3e-8a1e-8b9438a6b3f8
Submariner 遵循以下逻辑来进行跨集群集的服务发现:
cluster-id
前缀来访问特定集群的服务,...svc.clusterset.local
。通过 curl 命令发起 HTTP 请求。
curl whereami-cs.sample.svc.clusterset.local
返回结果如下,我们根据输出的 node_name 字段可以确认该 Pod 是在 c2 集群。
这里结合下图对流量进行简单的说明:流量从 c1 集群的 client Pod 发出,首先经过 veth-pair 到达 Node 的 Root Network Namespace,然后经过 Submariner Route Agent 设置的 vx-submariner 这个 VXLAN 隧道将流量发往 Gateway Node 上(c1-worker)。接着经过连接 c1 和 c2 集群的 IPsec 隧道到达对端,c2 集群的 Gateway Node(c2-worker)接收到流量后将,经过 iptables 的反向代理规则(在这过程中根据 ClusterIP 进行了 DNAT)最终发送到后端的 whereami Pod 上。
接下来我们在 c1 集群也创建相同的服务。
kubectl --context kind-c1 apply -f - <apiVersion: apps/v1
kind: Deployment
metadata:
name: whereami
namespace: sample
spec:
replicas: 3
selector:
matchLabels:
app: whereami
template:
metadata:
labels:
app: whereami
spec:
containers:
- name: whereami
image: cr7258/whereami:v1
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: whereami-cs
namespace: sample
spec:
selector:
app: whereami
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
在 c1 集群上查看服务。
root@seven-demo:~# kubectl --context kind-c1 get pod -n sample -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
whereami-754776cdc9-hq4m2 1/1 Running 0 45s 10.8.1.25 c1-worker <none> <none>
whereami-754776cdc9-rt84w 1/1 Running 0 45s 10.8.1.23 c1-worker <none> <none>
whereami-754776cdc9-v5zrk 1/1 Running 0 45s 10.8.1.24 c1-worker <none> <none>
root@seven-demo:~# kubectl --context kind-c1 get svc -n sample
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whereami-cs ClusterIP 10.88.132.102 <none> 80/TCP 50s
在 c1 集群导出服务。
subctl --context kind-c1 export service --namespace sample whereami-cs
查看 ServiceImport。
kubectl --context kind-c1 get -n submariner-operator serviceimport
kubectl --context kind-c2 get -n submariner-operator serviceimport
由于在 c1 集群本地也有相同的服务,因此这次请求将会发给 c1 集群的服务。
kubectl --context kind-c1 exec -it client -- bash
nslookup whereami-cs.sample.svc.clusterset.local
curl whereami-cs.sample.svc.clusterset.local
我们也可以通过
来指定访问访问特定集群的 ClusterIP Service。例如我们指定访问 c2 集群的 Service。
curl c2.whereami-cs.sample.svc.clusterset.local
Submariner 还支持带有 StatefulSets 的 Headless Services,从而可以通过稳定的 DNS 名称访问各个 Pod。在单个集群中,Kubernetes 通过引入稳定的 Pod ID 来支持这一点,在单个集群中可以通过
格式来解析域名。跨集群场景下,Submariner 通过
的格式来解析域名。
在 c2 集群创建 Headless Service 和 StatefulSet。
kubectl --context kind-c2 apply -f - <apiVersion: v1
kind: Service
metadata:
name: whereami-ss
namespace: sample
labels:
app: whereami-ss
spec:
ports:
- port: 80
name: whereami
clusterIP: None
selector:
app: whereami-ss
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: whereami
namespace: sample
spec:
serviceName: "whereami-ss"
replicas: 3
selector:
matchLabels:
app: whereami-ss
template:
metadata:
labels:
app: whereami-ss
spec:
containers:
- name: whereami-ss
image: cr7258/whereami:v1
ports:
- containerPort: 80
name: whereami
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
EOF
在 c2 集群查看服务。
root@seven-demo:~# kubectl get pod -n sample --context kind-c2 -o wide -l app=whereami-ss
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
whereami-0 1/1 Running 0 38s 10.9.1.20 c2-control-plane <none> <none>
whereami-1 1/1 Running 0 36s 10.9.1.21 c2-control-plane <none> <none>
whereami-2 1/1 Running 0 31s 10.9.1.22 c2-control-plane <none> <none>
root@seven-demo:~# kubectl get svc -n sample --context kind-c2 -l app=whereami-ss
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whereami-ss ClusterIP None <none> 80/TCP 4m58s
在 c2 集群导出服务。
subctl --context kind-c2 export service whereami-ss --namespace sample
解析 Headless Service 的域名可以得到所有 Pod 的 IP。
kubectl --context kind-c1 exec -it client -- bash
nslookup whereami-ss.sample.svc.clusterset.local
也可以指定单个 Pod 进行解析。
nslookup whereami-0.c2.whereami-ss.sample.svc.clusterset.local
通过域名访问指定的 Pod。
curl whereami-0.c2.whereami-ss.sample.svc.clusterset.local
查看 ServiceImport,在 IP 地址的一栏是空的,因为导出的服务类型是 Headless。
kubectl --context kind-c1 get -n submariner-operator serviceimport
kubectl --context kind-c2 get -n submariner-operator serviceimport
对于 Headless Service,Pod IP 是根据 Endpointslice 来解析的。
kubectl --context kind-c1 get endpointslices -n sample
kubectl --context kind-c2 get endpointslices -n sample
接下来将演示如何通过 Submariner 的 Globalnet 功能来解决多集群间 CIDR 重叠的问题,在本实验中,我们将会创建 3 个集群,并且将每个集群的 Service 和 Pod CIDR 都设置成相同的。
# 替换成服务器 IP
export SERVER_IP="10.138.0.11"
kind create cluster --config - <kind: Cluster
name: broker-globalnet
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
networking:
apiServerAddress: $SERVER_IP
podSubnet: "10.7.0.0/16"
serviceSubnet: "10.77.0.0/16"
EOF
kind create cluster --config - <kind: Cluster
name: g1
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
networking:
apiServerAddress: $SERVER_IP
podSubnet: "10.7.0.0/16"
serviceSubnet: "10.77.0.0/16"
EOF
kind create cluster --config - <kind: Cluster
name: g2
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
networking:
apiServerAddress: $SERVER_IP
podSubnet: "10.7.0.0/16"
serviceSubnet: "10.77.0.0/16"
EOF
使用 --globalnet=true
参数启用 Globalnet 功能,使用 --globalnet-cidr-range
参数指定所有集群的全局 GlobalCIDR(默认 242.0.0.0/8)。
subctl --context kind-broker-globalnet deploy-broker --globalnet=true --globalnet-cidr-range 120.0.0.0/8
使用 --globalnet-cidr
参数指定本集群的 GlobalCIDR。
subctl --context kind-g1 join broker-info.subm --clusterid g1 --globalnet-cidr 120.1.0.0/24
subctl --context kind-g2 join broker-info.subm --clusterid g2 --globalnet-cidr 120.2.0.0/24
subctl show connections --context kind-g1
subctl show connections --context kind-g2
在两个集群中都创建 sample 命名空间。
kubectl --context kind-g2 create namespace sample
kubectl --context kind-g1 create namespace sample
在 g2 集群创建服务。
kubectl --context kind-g2 apply -f - <apiVersion: apps/v1
kind: Deployment
metadata:
name: whereami
namespace: sample
spec:
replicas: 3
selector:
matchLabels:
app: whereami
template:
metadata:
labels:
app: whereami
spec:
containers:
- name: whereami
image: cr7258/whereami:v1
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: whereami-cs
namespace: sample
spec:
selector:
app: whereami
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
在 g2 集群查看服务。
root@seven-demo:~/globalnet# kubectl --context kind-g2 get pod -n sample -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
whereami-754776cdc9-72qd4 1/1 Running 0 19s 10.7.1.8 g2-control-plane <none> <none>
whereami-754776cdc9-jsnhk 1/1 Running 0 20s 10.7.1.7 g2-control-plane <none> <none>
whereami-754776cdc9-n4mm6 1/1 Running 0 19s 10.7.1.9 g2-control-plane <none> <none>
root@seven-demo:~/globalnet# kubectl --context kind-g2 get svc -n sample -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
whereami-cs ClusterIP 10.77.153.172 <none> 80/TCP 26s app=whereami
在 g2 集群导出服务。
subctl --context kind-g2 export service --namespace sample whereami-cs
导出服务后,我们再查看一下 g2 集群的 Service,会发现 Submariner 自动在与导出的服务相同的命名空间中创建了一个额外的服务,并且设置 externalIPs
为分配给相应服务的 Global IP。
kubectl --context kind-g2 get svc -n sample
在g1 集群访问 g2 集群的 whereami 服务。
kubectl --context kind-g1 run client --image=cr7258/nettool:v1
kubectl --context kind-g1 exec -it client -- bash
DNS 将会解析到分配给 c2 集群 whereami 服务的 Global IP 地址,而不是服务的 ClusterIP IP 地址。
nslookup whereami-cs.sample.svc.clusterset.local
用 curl 命令发起 HTTP 请求,从输出的结果可以发现,在 g2 集群的 whereami 看来,请求的源 IP 是 120.1.0.5,也就是说当流量从 g1 发往 g2 集群时,在 g1 集群的 Gateway Node 上对流量进行了 SNAT 源地址转换。
curl whereami-cs.sample.svc.clusterset.local
这里结合下图对流量进行简单的说明:流量从 c1 集群的 client Pod 发出,经过 DNS 解析后应该请求 IP 120.2.0.253。首先经过 veth-pair 到达 Node 的 Root Network Namespace,然后经过 Submariner Route Agent 设置的 vx-submariner 这个 VXLAN 隧道将流量发往 Gateway Node 上(c1-worker)。在 Gateway Node 上将源 IP 10.7.1.7 转换成了 120.1.0.5, 然后通过 c1 和 c2 集群的 IPsec 隧道发送到对端,c2 集群的 Gateway Node(c2-worker)接收到流量后,经过 iptables 的反向代理规则(在这过程中根据 Global IP 进行了 DNAT)最终发送到后端的 whereami Pod 上。
我们可以分别查看 g1 和 g2 集群上 Gateway Node 的 Iptables 来验证 NAT 规则,首先执行 docker exec -it g1-worker bash
和 docker exec -it g2-worker bash
进入这两个节点,然后执行 iptables-save
命令可以看到 iptables 配置,以下我筛选了相关的 iptables 配置。
g1-worker 节点:
# 在出访的时候将源 IP 转换为 120.1.0.1-120.1.0.8 中的一个
-A SM-GN-EGRESS-CLUSTER -s 10.7.0.0/16 -m mark --mark 0xc0000/0xc0000 -j SNAT --to-source 120.1.0.1-120.1.0.8
g2-worker 节点:
# 访问 120.2.0.253:80 的流量跳转到 KUBE-EXT-ZTP7SBVPSRVMWSUN 链
-A KUBE-SERVICES -d 120.2.0.253/32 -p tcp -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr external IP" -m tcp --dport 80 -j KUBE-EXT-ZTP7SBVPSRVMWSUN
# 跳转到 KUBE-SVC-ZTP7SBVPSRVMWSUN 链
-A KUBE-EXT-ZTP7SBVPSRVMWSUN -j KUBE-SVC-ZTP7SBVPSRVMWSUN
# 随机选择 whereami 后端的一个 Pod
-A KUBE-SVC-ZTP7SBVPSRVMWSUN -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr -> 10.7.1.7:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-BB74OZOLBDYS7GHU
-A KUBE-SVC-ZTP7SBVPSRVMWSUN -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr -> 10.7.1.8:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-MTZHPN36KRSHGEO6
-A KUBE-SVC-ZTP7SBVPSRVMWSUN -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr -> 10.7.1.9:80" -j KUBE-SEP-UYVYXWJKZN2VHFJW
# DNAT 地址转换
-A KUBE-SEP-BB74OZOLBDYS7GHU -p tcp -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr" -m tcp -j DNAT --to-destination 10.7.1.7:80
-A KUBE-SEP-MTZHPN36KRSHGEO6 -p tcp -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr" -m tcp -j DNAT --to-destination 10.7.1.8:80
-A KUBE-SEP-UYVYXWJKZN2VHFJW -p tcp -m comment --comment "sample/submariner-fzpkhsc5wssywpk5x3par6ceb6b2jinr" -m tcp -j DNAT --to-destination 10.7.1.9:80
接下来测试 Globalnet 在 Headless Service + StatefulSet 场景下的应用。在 g2 集群创建服务。
kubectl --context kind-g2 apply -f - <apiVersion: v1
kind: Service
metadata:
name: whereami-ss
namespace: sample
labels:
app: whereami-ss
spec:
ports:
- port: 80
name: whereami
clusterIP: None
selector:
app: whereami-ss
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: whereami
namespace: sample
spec:
serviceName: "whereami-ss"
replicas: 3
selector:
matchLabels:
app: whereami-ss
template:
metadata:
labels:
app: whereami-ss
spec:
containers:
- name: whereami-ss
image: cr7258/whereami:v1
ports:
- containerPort: 80
name: whereami
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
EOF
在 g2 集群查看服务。
root@seven-demo:~# kubectl get pod -n sample --context kind-g2 -o wide -l app=whereami-ss
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
whereami-0 1/1 Running 0 62s 10.7.1.10 g2-worker <none> <none>
whereami-1 1/1 Running 0 56s 10.7.1.11 g2-worker <none> <none>
whereami-2 1/1 Running 0 51s 10.7.1.12 g2-worker <none> <none>
root@seven-demo:~# kubectl get svc -n sample --context kind-c2 -l app=whereami-ss
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whereami-ss ClusterIP None <none> 80/TCP 42h
在 g2 集群导出服务。
subctl --context kind-g2 export service whereami-ss --namespace sample
在 g1 集群访问 g2 集群服务,Globalnet 会为每一个 Headless Service 关联的 Pod 分配一个 Global IP,用于出向和入向的流量。
kubectl --context kind-g1 exec -it client -- bash
nslookup whereami-ss.sample.svc.clusterset.local
指定解析某个 Pod。
nslookup whereami-0.g2.whereami-ss.sample.svc.clusterset.local
指定访问某个 Pod。
curl whereami-0.g2.whereami-ss.sample.svc.clusterset.local
查看 ServiceImport,在 IP 地址的一栏是空的,因为导出的服务类型是 Headless。
kubectl --context kind-g1 get -n submariner-operator serviceimport
kubectl --context kind-g2 get -n submariner-operator serviceimport
对于 Headless Service,Pod IP 是根据 Endpointslice 来解析的。
kubectl --context kind-g1 get endpointslices -n sample
kubectl --context kind-g2 get endpointslices -n sample
执行 docker exec -it g2-worker bash
命令进入 g2-worker 节点,然后执行 iptables-save
命令寻找相关的 Iptables 规则。
# SNAT
-A SM-GN-EGRESS-HDLS-PODS -s 10.7.1.12/32 -m mark --mark 0xc0000/0xc0000 -j SNAT --to-source 120.2.0.252
-A SM-GN-EGRESS-HDLS-PODS -s 10.7.1.11/32 -m mark --mark 0xc0000/0xc0000 -j SNAT --to-source 120.2.0.251
-A SM-GN-EGRESS-HDLS-PODS -s 10.7.1.10/32 -m mark --mark 0xc0000/0xc0000 -j SNAT --to-source 120.2.0.250
# DNAT
-A SUBMARINER-GN-INGRESS -d 120.2.0.252/32 -j DNAT --to-destination 10.7.1.12
-A SUBMARINER-GN-INGRESS -d 120.2.0.251/32 -j DNAT --to-destination 10.7.1.11
-A SUBMARINER-GN-INGRESS -d 120.2.0.250/32 -j DNAT --to-destination 10.7.1.10
执行以下命令删除本次实验创建的 Kind 集群。
kind delete clusters broker c1 c2 g1 g2
本文首先介绍了 Submariner 的架构,包括 Broker、Gateway Engine、Route Agent、Service Discovery、Globalnet 和 Submariner Operator。接着,通过实验向读者展示了 Submariner 在跨集群场景中如何处理 ClusterIP 和 Headless 类型的流量。最后,演示了 Submariner 的 Globalnet 是如何通过 GlobalCIDR 支持不同集群间存在 CIDR 重叠的情况。