为什么需要kubernetes?

  • 大规模多节点容器调度
  • 快速扩缩容
  • 故障自愈
  • 弹性伸缩
  • 技术趋势
  • 一致性、不锁定
    早期型多的一些服务都属于单体服务、单节点、单进程的一种单体服务架构,后续随着技术的发展衍生出了容器技术。容器技术其实也不能满足我们的多节点、分布式的应用架构体系,从而衍生出了kubernetes容器编排引擎。

那么我们来看一下早期单体容器架构

其实对于容器化技术带来了那些优势呢?

  1. 其实我觉得容器化带来的最大的优势就是交付和部署的优势
容器单点架构流程图
容器单点架构流程图

那么随之而来带来的问题是:

那么由于Docker的容器镜像可以在A、B、C任意一台机器上运行,那么是否可以当A机器所运行的镜像挂掉以后自动的帮我在B机器上进行重启呢?

okey 带着这个问题 一起往下进行。

kubernes组件

先看一张官方给出的kubernetes的架构图
图中列出了kubernetes的组成以及相对应的组件

  • ControlPlane: 控制平面节点
  • Node: 工作节点
  • Kubelet: 用于控制staticPod,其主要就是用来控制静态Pod,因为静态Pod不受ApiServer的影响。

Oh 不插一句嘴 学到了一个新的命令

1
2
# jq命令是一个用于处理json的命令
kubectl get deploy wecho-canary -o json | jq .spec

okey 继续…
我们时长谈起到的control-plane实际上并不是一台机器他只是一个抽象出来的概念,实际上我们是在说所谓的control-plane层面的组件。也就是说这些组件可以运行在控制面的机器上同时也可以运行在Node机器上

Pasted image 20231108213644.png

kubernetes核心概念

  • ResourceObject: 是我认为相对而言kubernetes集群当中比较核心的资源对象,其实也就是我们所说的PodDeploymentDaemonset等kubernetes的资源类型
    对于一个Pod而言,kubernetes对其定义的键值无非以下的几种
    1
    2
    3
    4
    5
    6
    7
    8
    [root@Online-Beijing-master1 ~]# kubectl get deploy wecho-canary -o json | jq keys
    [
    "apiVersion",
    "kind",
    "metadata",
    "spec", // spec描述的是Pod预期的状态
    "status"
    ]
    你可以通过kubectl api-resource来获取kubernetes相对应的资源类型。

kubernets的资源提交

我们平时使用的kubectl run nginx-$RANDOM --image="nginx:alpine"究竟是执行了什么样的内容?

正常来说Api-Server本身就是服务,那么当我把请求发送给Api-Server的时候,我是以什么样的请求内容进行了提交?那Api-Server接收了我的请求内容又对我的请求内容做出了什么样的处理呢?

首先我们来看实际作为客户端,也就是client端提交的请求

1
2
# 可以通过--dry-run=client来模拟客户端提交的请求内容
kubectl run nginx-$RANDOM --image="nginx:alpine" --dry-run=client -ojson -v6

正常的返回响应应该如下,这是一个我们正常通过kubelet创建一个Pod所发送的请求体内容,但作为clinet只会在我们本地进行处理,所以你也可以看到返回的结构内容中带有I1108 21:54:49.922270 1219589 loader.go:374] Config loaded from file: /root/.kube/config,也就证明了它并没有像Api-Server发送任何请求,只是读取了相关的配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"name": "nginx-27147",
"creationTimestamp": null,
"labels": {
"run": "nginx-27147"
}
},
"spec": {
"containers": [
{
"name": "nginx-27147",
"image": "nginx:alpine",
"resources": {}
}
],
"restartPolicy": "Always",
"dnsPolicy": "ClusterFirst"
},
"status": {}
}

很好上面我们模拟了一个client端所生产的内容,那么下面我们看看当实际发送给Api-Server的时候产生了哪些内容

1
kubectl run nginx-$RANDOM --image="nginx:alpine" --dry-run=server -ojson -v6

我们可以清晰地看到日志的输出

  1. 首先第一步加载了kubernetes相关的配置文件信息。
  2. 发送了相关请求(盲猜应该是验证api-server是否正常)
  3. https://resk8s.api.beijing.io:8443/api/v1/namespaces/default/pods?dryRun=All&fieldManager=kubectl-run发送了POST请求用于创建Pod
    1
    2
    3
    I1108 21:58:14.866131 1221713 loader.go:374] Config loaded from file:  /root/.kube/config
    I1108 21:58:14.884329 1221713 round_trippers.go:553] GET https://resk8s.api.beijing.io:8443/openapi/v2?timeout=32s 200 OK in 15 milliseconds
    I1108 21:58:14.946553 1221713 round_trippers.go:553] POST https://resk8s.api.beijing.io:8443/api/v1/namespaces/default/pods?dryRun=All&fieldManager=kubectl-run 201 Created in 15 milliseconds
    事实上我们由此可见发送到Api-Server的请求内容多了很多东西
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    {
    "kind": "Pod",
    "apiVersion": "v1",
    "metadata": {
    "name": "nginx-14483",
    "namespace": "default",
    "uid": "d27e1836-8bac-40d4-805b-420f4bca4ee1",
    "creationTimestamp": "2023-11-08T14:05:36Z",
    "labels": {
    "run": "nginx-14483"
    },
    "annotations": {
    "kubernetes.customized/fabric-networks": "default"
    }
    },
    "spec": {
    "volumes": [
    {
    "name": "kube-api-access-vwtvp",
    "projected": {
    "sources": [
    {
    "serviceAccountToken": {
    "expirationSeconds": 3607,
    "path": "token"
    }
    },
    {
    "configMap": {
    "name": "kube-root-ca.crt",
    "items": [
    {
    "key": "ca.crt",
    "path": "ca.crt"
    }
    ]
    }
    },
    {
    "downwardAPI": {
    "items": [
    {
    "path": "namespace",
    "fieldRef": {
    "apiVersion": "v1",
    "fieldPath": "metadata.namespace"
    }
    }
    ]
    }
    }
    ],
    "defaultMode": 420
    }
    }
    ],
    "containers": [
    {
    "name": "nginx-14483",
    "image": "nginx:alpine",
    "resources": {},
    "volumeMounts": [
    {
    "name": "kube-api-access-vwtvp",
    "readOnly": true,
    "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
    }
    ],
    "terminationMessagePath": "/dev/termination-log",
    "terminationMessagePolicy": "File",
    "imagePullPolicy": "IfNotPresent"
    }
    ],
    "restartPolicy": "Always",
    "terminationGracePeriodSeconds": 30,
    "dnsPolicy": "ClusterFirst",
    "serviceAccountName": "default",
    "serviceAccount": "default",
    "securityContext": {},
    "schedulerName": "default-scheduler",
    "tolerations": [
    {
    "key": "node.kubernetes.io/not-ready",
    "operator": "Exists",
    "effect": "NoExecute",
    "tolerationSeconds": 300
    },
    {
    "key": "node.kubernetes.io/unreachable",
    "operator": "Exists",
    "effect": "NoExecute",
    "tolerationSeconds": 300
    }
    ],
    "priority": 0,
    "dnsConfig": {
    "options": [
    {
    "name": "single-request-reopen"
    }
    ]
    },
    "enableServiceLinks": true,
    "preemptionPolicy": "PreemptLowerPriority"
    },
    "status": {
    "phase": "Pending", // 可以看到当前处于Pending阶段
    "qosClass": "BestEffort"
    }
    }

    另外: 当你需要创建资源类型的时候,我并不建议你从头开始去编写相关文件,可以灵活的应用kubectl run来进行填充相关的字段信息。

kubernetes设计理念

  • 声明式:典型就是在资源文件中进行声明
  • 无侵入性
  • 可移植: 所有符合kubernetes标准的kubernetes平台都可以进行迁移
  • 显示接口:所有的操作都是开放性的,不会存在私有接口,无论是Api-Server或者Clinet-go所操作的接口都是一模一样的。

创建资源的工作流程

  1. 首先当用户的请求进入到Api-Server后会进入到Authorization认证授权的处理接口实际上就是加载我们的config配置文件,其具体代码在loader.go:372进行实现
创建资源的工作流程
创建资源的工作流程

服务发现原理和应用

kubernetes中Pod的通信

  • 每个Pod都有自己的IP分配
  • Pod间的可以通过IP进行通信
  • Pod的IP是可变的
  • Pod的IP通常不能被提前获取,一般都是网络插件进行分配

kubernetes的Service通信

它是一种抽象,帮助你将 Pod 集合在网络上公开出去。 每个 Service 对象定义端点的一个逻辑集合(通常这些端点就是 Pod)以及如何访问到这些 Pod 的策略。
我们通常可以使用以下命令来简单的暴露一个Service

1
kubectl expose deploy/nginx --port=80

通常来说我们访问某个服务都是访问服务的IP地址,当然了,在kubernetes中访问Service对应的地址也可以访问到服务

1
2
3
4
[root@Online-Beijing-master1 ~]# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
wecho-canary ClusterIP 10.10.166.79 <none> 80/TCP,9113/TCP 69d
kubernetes ClusterIP 10.10.0.1 <none> 443/TCP 279d

实际上也就是说当我需要访问wecho-canary这个服务的时候,我无需关心他后端end如何进行变化,我只需要记住访问wecho-canaryService所给出的ClusterIP即可进行访问到相应的服务。

另外的一种方式就是通过我们的Service名称来进行访问,下面说一下具体是如何通过Service名称进行访问的。

我们假设使用上面的wecho-canary名称进行一次访问

1
curl -v wecho-canary # 假设访问正常

很简单,大家都知道当我们访问一个域名的时候背后肯定又DNS服务器来解析其所对应的IP地址,那么其实在kubernetes当中也有一个内部的DNS服务器,名字叫做kube-dns
我们可以通过kubectl get svc -A进行查看,其中所指定的10.10.0.10就是我们kube-dns的IP地址.

1
kube-system    kube-dns                        ClusterIP   10.10.0.10      <none>        53/UDP,53/TCP,9153/TCP                         279d

看到我们kube-dns有以下端点

1
2
3
[root@Online-Beijing-master1 ~]# kubectl get ep -n kube-system | grep kube-dns
kube-dns 10.10.151.159:53,10.10.4.95:53,10.10.151.159:53 + 3 more... 279d
kube-dns-upstream 10.10.151.159:53,10.10.4.95:53,10.10.151.159:53 + 1 more... 81d

其次可以看到我们容器内所对应的resolv.conf所写内容的配置文件

1
2
3
search jiashicang.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.254.20.10
options ndots:5 single-request-reopen

由于我使用了NodeLocalDns所以我这个地方的nameserver不太一样,关于NodeLocalDns我们后续再说。
总结以下几点:

  1. 当我们访问wecho-canary的时候实际上是访问了wecho-canary.default.cluster.local.svc,即ServiceName.Namespace.cluster.local.svc
  2. 所有的解析域名请求都会请求到kube-dns当中,当kube-dns无法完成解析的时候,我们会将请求forward到本地的解析文件当中,如果本地解析文件也无法解析则认为失败。典型的就是超时、无法解析等问题。
  3. Service通过监视API server的数据变化来感知后端Pod的变化,并及时更新负载均衡规则。具体来说,Kubernetes ApiServer负责管理集群状态,它会记录每一个对象(包括Pod)的Spec和Status。Service对象使用ApiServer的watch接口,监视后端关联Pod对象的变化事件。比如Pod实例加入或销毁时,ApiServer会主动通知Service。一旦检测到Pod变化,Service会立即使用新的Pod列表,重新计算并更新自己负载均衡的端口转发规则。例如使用iptables规则或IPVS表更新后端目标地址。这样,无论Pod是动态伸缩还是故障转移,Service都能即时感知,保持负载均衡入口地址的高可靠性。这就是Service如何实现动态更新负载均衡规则的原理。

Service类型

  • ClusterIP: 通过集群的内部IP公开Service,选择该值时Service只能够在集群内部访问。这也是你没有为服务显式指定type时使用的默认值。
  • NodePort: 通过每个节点上的IP和静态端口NodePort公开 Service。为了让Service可通过节点端口访问,Kubernetes 会为 Service配置集群IP地址, 相当于你请求了type:ClusterIP的服务。
  • LoadBalancer:使用云平台的负载均衡器向外部公开Service,一般来说都用在云厂商才会使用LoadBalancer
  • ExternalName:将服务映射到externalName字段的内容 例如,映射到主机名:api.foo.bar.example,该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的CNAME记录。

常用的暴露Service的方法

1
kubectl expose deploy/nginx --port=80 --type=NodePort

哦,对了,推荐一个开源的LB插件

服务发现和流量路由

总的来说我们分为一下几点吧

  1. Pod之间的流量通信:
    1. IP直连的方式: 不推荐这种方式,因为Pod的IP严格意义上来说对于服务版本更新生命周期相对较短。
    2. ClusterIP: 相对于比较推荐通过指定ServiceName的方式来绑定活着监听某种服务
  2. 外部访问Pod的通信
    1. NodePort: 通过暴露外部端口的方式来进行Pod的访问
    2. LoadBalancer: 通过对Service绑定LB的方式进行Pod的访问
  3. Pod访问外部通信:
    1. ExternalName: 将 Service 映射到 DNS 名称
    2. HeadlessService: 当你不需要负载均衡,也不需要单独的 ServiceIP,你可以使用HeadlessService
      1
      2
      3
      4
      5
      6
      7
      8
      9
      # 下面是ExternalName的简单实例
      apiVersion: v1
      kind: Service
      metadata:
      name: my-service
      namespace: prod
      spec:
      type: ExternalName
      externalName: my.database.example.com

一般来说Kubernetes的内部DNS记录有两种规范

1
2
3
PodIP.Namespace.pod.cluster-domain.example
---
ServiceName.Namespace.svc.cluster-domain.example