Fastapi框架(22)kubernets 部署之K8S初步 -纯属个人梳理Pod、Service基础知识点(4)

976 阅读34分钟

一、Pod(豌豆荚)篇


1.一些概念上的说明

  • Pod是k8s最小调度的单元
  • 一个Pod是集群中有运行的一个进程
  • Pod是对一个容器或一组容器的抽象封装,表示一组一个或多个应用程序容器
  • Pod是对容器的一种组织管理方式
  • 一个Pod可以只有一个容器也可以有多个容器(几个需要紧密耦合互相协作的容器,它们之间共享资源)组成
  • 一个Pod中所有的容器共享同一个网络(Pause根容器),多个容器共享Pause容器的IP,共享Pause容器挂载的Volume,这样可以简化容器之间的通信问题,也可以解决容器之间的文件共享问题。
  • 是K8s最小的调度单元
  • 运行再Node工作节点上,如果一个node节点如果宕机,则分配到了这个node上的pod,在会经过设定的超时时间后会被重新调度到其他node节点上。
  • Pod被运行在工作节点的上的Kubelet进程进行管理
  • 每个Pod都有属于自己的Ip地址,它们之间可以通过进程间通信,可以通过localhost互相发现
  • 不同Pod的容器具有不同的IP地址,不能直接的通过IPC进行通信

2.Pod分类

2.2 从生命周期分类

  • 单独的创建资源类的Pod 单独的创建出来的Pod的话,通常会随着Node故障而终止运行,且不会自愈。如果Node故障或调度器本身或调度过程异常,有可能Pod就会被删除,且不会重新生成,也就是不会重新生成新的Pod(不会重启新的Pod来取代)
  • 使用控制器来创建的Pod 通过控制器来管理启动的Pod,可以进行Node节点故障的自动调度和自愈等。如果Pod异常的,还会进行自动的重启。通过控制器启动的Pod提供了集群级别的副本管理、滚动升级和自愈等的能力。

2.3 从控制器类型分类

  • ReplicationController和ReplicaSet
  • Deployment
  • DaemonSet
  • StateFulSet
  • Job/ConJob
  • Horizontal Pod Autoscaling

1;RS 跟 RC 没有本质的不同,只是名字不不同,且RS 支持集合式的 selector

2: 通过 RS 与 Deployment 的关联一起使用

3.Pod创建流程图示

图来自网络(侵删):

image.png

Kubernetes通过watch的机制进行每个组件的协作,每个组件之间的设计实现了解耦。

  • 1)用户使用create yaml定制我们的pod相关规格,或使用kubectl或其他API客户端请求给apiseerver

  • 2)apiserver 把Pod相关属性信息(metadata)写入etcd,等待写入操作完成后,apiserver把结果反馈客户端

  • 3)apiserver触发watch机制准备创建pod,信息转发给kube-schedule调度器,kube-schedule调度器根据调度算法选择node工作节点,且调度器将node工作节点信息信息反馈给apiserver,

  • 4)调度完成后,apiserver_将绑定的node工作节点信息写入etcd

  • 5)apiserver又通过watch机制,监听Pod对象的调度结果,通知Node节点上的kubelet根据指定的pod信息 触发docker run命令创建容器,和启动容器

  • 6)容器启动和创建完成之后反馈给kubelet, kubelet又将pod的状态信息给apiserver,apiserver又将pod的状态信息写入etcd。

  • 7)其中kubectl get pods命令调用的时etcd_的信息。

4.Pod(Podphase)几个运行状态

Pod的status字段是一个PodStatus对象,PodStatus中有一个phase字段

参考资料来源: debian.cn/articles/69…

  • Pending(挂起): Pod已提交创建到K8s,可能当前还处于准备中,如容器下载等,暂时调度不成功

  • Running(运行中): Pod已经绑定到一个Node节点上,且已创建了所有的容器,至少有一个容器正在运行中,或正在启动或重新启动

  • Succeeded(成功): pod中的所有的容器已经正常的自行退出,并且k8s永远不会自动重启这些容器

  • Faile(失败) :Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止。

  • Unkonwn(未知) :出于某种原因,无法获得Pod的状态,通常是由于与Pod主机通信时出错

  • terminating(退出中): Pod正在进行退出中

  • Completed :当作业完成时,不再创建pod,但不会创建pod 也删除了。保留它们可以让您仍然查看日志 用于检查错误、警告或其他诊断信息的已完成pod的数目 可以查看其状态。由用户在完成后删除旧作业 注意到他们的身份

5.Pod详细阶段运行状态

  • CrashLoopBackOff: 容器退出,kubelet正在将它重启
  • InvalidImageName: 无法解析镜像名称
  • ImageInspectError: 无法校验镜像
  • ErrImageNeverPull: 策略禁止拉取镜像
  • ImagePullBackOff: 正在重试拉取
  • RegistryUnavailable: 连接不到镜像中心
  • ErrImagePull: 通用的拉取镜像出错
  • CreateContainerConfigError: 不能创建kubelet使用的容器配置
  • CreateContainerError: 创建容器失败
  • m.internalLifecycle.PreStartContainer  执行hook报错
  • RunContainerError: 启动容器失败
  • PostStartHookError: 执行hook报错
  • ContainersNotInitialized: 容器没有初始化完毕
  • ContainersNotReady: 容器没有准备完毕
  • ContainerCreating:容器创建中
  • PodInitializing:pod 初始化中
  • DockerDaemonNotReady:docker还没有完全启动
  • NetworkPluginNotReady: 网络插件还没有完全启动

6.Pod 资源清单配置信息

apiVersion: v1  #版本号
kind: Pod       #资源对象类型,还有其他可选的类型
metadata:       #元数据
  name: string  #pod的名称
  namespace: string  #pod所属的命名空间
  labels:  #自定义标签列表
    - name: string
  annotations:  #自定义注解列表
    - name: string
spec:  #pod基于那个的容器来创建的详细定义
  containers:  
  - name: string   #容器的名称
    image: string  #容器的镜像
    imagePullPolicy: [Always|Never|IfNotPresent]  #容器镜像拉取策略
    command: [string]   #容器的启动命令列表
    args: [string]      #启动命令参数
    workingDir: string  #容器的工作目录
    volumeMounts:       #挂载到容器内部的存储卷配置
    - name: string
      mountPath: string #挂载的目录
      readOnly: boolean #是否只读挂载
    ports:  #容器暴露的端口列表
    - name: string  #端口名称
      containerPort: int  #容器监听的端口
      hostPort: int  #容器所在主机需要监听的端口
      protocol: string  #端口协议
    env: #容器中的环境变量
    - name: string 
      value: string
    resources: #资源限制设置
      limits: #最大使用资源
        cpu: string
        memory: string
      requests: #请求时资源设置
        cpu: string
        memory: string
    livenessProbe:  #对容器的健康检查
      exec:  #通过命令的返回值
        command: [string]
      httpGet: #通过访问容器的端口的返回的状态码
        path: string
        port: number
        host: string
        scheme: string
        httpHeaders:
        - name: string
          value: string
      tcpSocket:  #通过tcpSocket
        port: number
      initialDelaySeconds: 0 #首次健康检查时间
      timeoutSeconds: 0  #健康检查的超时时间
      periodSeconds: 0  #每次健康检查的时间间隔
      successThreshold: 0 
      failureThreshold: 0
    securityContext:
      privileged: false
  restartPolicy: [Always|Never|OnFailure] #pod的重启策略
  nodeSelector: object  #运行节点的选择器,给节点设置标签后可以指定的运行pod的node标签
  imagePullSecrets:  
  - name: string
  hostNetwork: false  #是否使用主机网络模式
  volumes:  #该pod上定义的共享存储卷列表
  - name: string
    emptyDir: {}  #临时挂载
    hostPath:  #挂载宿主机目录
      path: string
    secret: #类型为secret存储卷
      secretName: string
      items:
      - key: string
        path: string
    configMap: #类型为configMap的存储卷
      name: string
      items:
      - key: string
        path: string

7.Pod 一个简单的配置示例:

顶一个pod的yaml文件信息 mypodtest.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: pod-xiaozhong-test
  namespace: default
  labels:
    app: mybusybox
spec:
  containers:
  - name: busybox
    image: busybox:latest
    command:
    - "/bin/sh"
    - "-c"
    - "sleep 10"

然后应用:


[root@k81-master01 k8s-install]# kubectl apply -f mypodtest.yaml
pod/pod-xiaozhong-test created
[root@k81-master01 k8s-install]#

然后查看pod:


[root@k81-master01 k8s-install]# kubectl get pods
NAME                                READY   STATUS      RESTARTS   AGE
chaoge-nginx-586ddcf5c4-bg8f4       1/1     Running     0          2d20h
nginx-deployment-574b87c764-47vpw   1/1     Running     0          2d19h
nginx-deployment-574b87c764-745gt   1/1     Running     0          6d16h
nginx-deployment-574b87c764-cz548   1/1     Running     0          2d19h
nginx-deployment-574b87c764-j9bps   1/1     Running     0          2d19h
nginx-deployment-574b87c764-ldtj6   1/1     Running     0          2d19h
pod-xiaozhong-test                  0/1     Completed   1          51s
zyx-nginx-5c559d5697-dhz2z          1/1     Running     0          8d
zyx-nginx-5c559d5697-qvkjl          1/1     Running     0          2d19h
[root@k81-master01 k8s-install]#

image.png

8.Pod 指定节点调度创建

  • nodeName : 把Pod调度到指定的Node工作节点名称上(跳过调度器直接分配)
  • nodeSelector:通过给节点打标签后,然后把Pod调度到匹配Label的Node工作节点上

如使用nodeName调度示例:

先删除上一个简单的示例:

[root@k81-master01 k8s-install]# kubectl delete pod pod-xiaozhong-test
pod "pod-xiaozhong-test" deleted
[root@k81-master01 k8s-install]#

在创建重新配置我们的yaml文件并添加指定的节点192.168.219.140(node3节点):

apiVersion: v1
kind: Pod
metadata:
  name: pod-xiaozhong-test
  namespace: default
  labels:
    app: mybusybox
spec:
  nodeName: 192.168.219.140
  containers:
  - name: busybox
    image: busybox:latest
    command:
    - "/bin/sh"
    - "-c"
    - "sleep 60"

然后再应用:


[root@k81-master01 k8s-install]# kubectl apply -f mypodtest.yaml
pod/pod-xiaozhong-test created
[root@k81-master01 k8s-install]#

查看pod信息:

image.png

修改为使用节点的名称,而不是使用ip:

apiVersion: v1
kind: Pod
metadata:
  name: pod-xiaozhong-test
  namespace: default
  labels:
    app: mybusybox
spec:
  nodeName: k81-node03
  containers:
  - name: busybox
    image: busybox:latest
    command:
    - "/bin/sh"
    - "-c"
    - "sleep 60"

再查看应用:

image.png

image.png

查看pod执行的日志明细:


[root@k81-master01 k8s-install]# kubectl  describe pod pod-xiaozhong-test
Name:         pod-xiaozhong-test
Namespace:    default
Priority:     0
Node:         k81-node03/192.168.219.140
Start Time:   Fri, 10 Sep 2021 11:08:40 +0800
Labels:       app=mybusybox
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"mybusybox"},"name":"pod-xiaozhong-test","namespace":"default...
Status:       Running
IP:           10.244.2.17
IPs:
  IP:  10.244.2.17
Containers:
  busybox:
    Container ID:  docker://c9b0c40d8f6315dd062a89119c0c14803891af17fc71ba9a265bdf30eec65136
    Image:         busybox:latest
    Image ID:      docker-pullable://busybox@sha256:b37dd066f59a4961024cf4bed74cae5e68ac26b48807292bd12198afa3ecb778
    Port:          <none>
    Host Port:     <none>
    Command:
      /bin/sh
      -c
      sleep 60
    State:          Running
      Started:      Fri, 10 Sep 2021 11:12:54 +0800
    Last State:     Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Fri, 10 Sep 2021 11:11:22 +0800
      Finished:     Fri, 10 Sep 2021 11:12:22 +0800
    Ready:          True
    Restart Count:  3
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-m4qkm (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-m4qkm:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-m4qkm
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason   Age                  From                 Message
  ----     ------   ----                 ----                 -------
  Warning  BackOff  74s (x3 over 2m46s)  kubelet, k81-node03  Back-off restarting failed container
  Normal   Pulling  62s (x4 over 5m6s)   kubelet, k81-node03  Pulling image "busybox:latest"
  Normal   Pulled   56s (x4 over 4m52s)  kubelet, k81-node03  Successfully pulled image "busybox:latest"
  Normal   Created  56s (x4 over 4m52s)  kubelet, k81-node03  Created container busybox
  Normal   Started  56s (x4 over 4m52s)  kubelet, k81-node03  Started container busybox
[root@k81-master01 k8s-install]#

还可以查看日志记录:

还可以([root@k81-master01 k8s-install]# kubectl logs pod-xiaozhong-test

9.Pod钩子

  • Pod 钩子是由Kubelet发起的。

容器钩子两类触发点:

  • PostStart:容器创建后

  • PreStop:容器终止前

PostStart

  • 这个钩子在容器创建后立即执行。
  • 但是,并不能保证钩子将在容器ENTRYPOINT之前运行。
  • 容器ENTRYPOINT和钩子执行是异步操作。如果钩子花费太长时间以至于容器不能运行或者挂起, 容器将不能达到running状态

PreStop

  • 这个钩子在容器终止之前立即被调用。它是阻塞的,意味着它是同步的, 所以它必须在删除容器的调用发出之前完成

  • 如果钩子在执行期间挂起, Pod阶段将停留在running状态并且永不会达到failed状态。

  • 如果PostStart或者PreStop钩子失败, 容器将会被kill。用户应该使他们的钩子处理程序尽可能的轻量。

9.1 PostStart钩子简单示例:

定义文件mypodpoststart.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: pod-xiaozhong-poststart
  namespace: default
  labels:
    app: mybusybox
spec:
  containers:
  - name: busybox
    image: busybox:latest
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c"]
    command:
    - "/bin/sh"
    - "-c"

执行并查看结果:

# 应用执行
[root@k81-master01 k8s-install]# kubectl apply -f mypodpoststart.yaml
pod/pod-xiaozhong-poststart created
# 日志查看
[root@k81-master01 k8s-install]# kubectl logs pod-xiaozhong-
pod-xiaozhong-poststart  pod-xiaozhong-test
[root@k81-master01 k8s-install]# kubectl logs pod-xiaozhong-
pod-xiaozhong-poststart  pod-xiaozhong-test
[root@k81-master01 k8s-install]# kubectl logs pod-xiaozhong-poststart
/bin/sh: -c requires an argument
# 明细查看
[root@k81-master01 k8s-install]# kubectl describe pod pod-xiaozhong-poststart
Name:         pod-xiaozhong-poststart
Namespace:    default
Priority:     0
Node:         k81-node02/192.168.219.139
Start Time:   Fri, 10 Sep 2021 11:28:29 +0800
Labels:       app=mybusybox
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"mybusybox"},"name":"pod-xiaozhong-poststart","namespace":"de...
Status:       Running
IP:           10.244.1.33
IPs:
  IP:  10.244.1.33
Containers:
  busybox:
    Container ID:  docker://f638f100e976ab2641b3f98f478e332c7cbf40bc2553518451ff4aef20c4e626
    Image:         busybox:latest
    Image ID:      docker-pullable://busybox@sha256:b37dd066f59a4961024cf4bed74cae5e68ac26b48807292bd12198afa3ecb778
    Port:          <none>
    Host Port:     <none>
    Command:
      /bin/sh
      -c
    State:          Waiting
      Reason:       PostStartHookError: rpc error: code = Unknown desc = container not running (f638f100e976ab2641b3f98f478e332c7cbf40bc2553518451ff4aef20c4e626)
    Last State:     Terminated
      Reason:       Error
      Exit Code:    2
      Started:      Fri, 10 Sep 2021 11:29:09 +0800
      Finished:     Fri, 10 Sep 2021 11:29:09 +0800
    Ready:          False
    Restart Count:  2
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-m4qkm (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             False
  ContainersReady   False
  PodScheduled      True
Volumes:
  default-token-m4qkm:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-m4qkm
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason               Age                From                 Message
  ----     ------               ----               ----                 -------
  Normal   Scheduled            <unknown>          default-scheduler    Successfully assigned default/pod-xiaozhong-poststart to k81-node02
  Normal   Pulling              15s (x3 over 48s)  kubelet, k81-node02  Pulling image "busybox:latest"
  Normal   Pulled               10s (x3 over 43s)  kubelet, k81-node02  Successfully pulled image "busybox:latest"
  Normal   Created              10s (x3 over 43s)  kubelet, k81-node02  Created container busybox
  Normal   Started              9s (x3 over 43s)   kubelet, k81-node02  Started container busybox
  Warning  FailedPostStartHook  9s (x3 over 43s)   kubelet, k81-node02  Exec lifecycle hook ([/bin/sh -c echo 1 > /tmp/message]) for Container "busybox" in Pod "pod-xiaozhong-poststart_default(e25ea0a3-49fc-4407-a4ef-c35bccc4d875)" failed - error: rpc error: code = Unknown desc = container not running (busybox), message: ""
  Normal   Killing              9s (x3 over 43s)   kubelet, k81-node02  FailedPostStartHook
  Warning  BackOff              9s (x3 over 30s)   kubelet, k81-node02  Back-off restarting failed container
[root@k81-master01 k8s-install]#

查看容器状态:显示容器已异常退出:

image.png

10.Pod探针(存活探测机制)

  • 探针是由每个Node节点的kubelet进行检测

  • 探针是kubelet对容器定期诊断机制

10.1 容器内诊断执行Handle类型(readinessProbe检测方案)

10.1.1 三种类型

- ExecAction:容器内执行指定命令。如果命令执行结束退出时返回码为0则认为诊断成功。

- TCPSocketAction:对指定端口上的容器的IP地址进行TCP健康检查。如果端口打开,则诊断被认为是成功的。

- HTTPGetAction:对指定的端口和路径上的容器的IP地址执行HTTPGet请求。如果响应的状态码大于等于200且小于400,则诊断被认为是成功。

10.1.2 诊断结果

- 成功:容器通过了诊断。 
- 失败:容器未通过诊断。 
- 未知:诊断失败,不会采取任何行动

10.2 探针的方式:

10.2.1 startupProbe探针 (启动检查):

-   用于判断**容器内应用程序是否已经启动**,如果配置了startuprobe,就会先禁用其他的探测,直到它成功为止,成功后将不再进行探测,这个是对一个Pod启动情况的检查机制。

10.2.2 livenessProbe探针(存活检查):

-   探测的是**容器是否运行**
-   根据用户自定义规则来判定pod对象健康状态,如果livenessProbe探针探测到容器不健康,则kubelet会根据启动的时候Pod重启策略来决定当前的不健康的Pod是否重启
-   如果一个容器不包含livenessProbe探针,则kubelet会认为容器的livenessProbe探针的返回值永远成功。

10.2.3 ReadinessProbe探针 (就绪检查):

-   探测的是的**容器内的程序健康**情况,如果它的返回值为success,那么就代表这个容器已经完成启动,且程序已处于可以接受流量的状态.
-   根据用户自定义规则来判定pod对象健康状态,如果探测失败,控制器会将此pod从对应service的endpoint列表中移除,从此不再将任何请求分发调度到这个Pod上,直至下次探测成功为止。

10.2.4 一些探针参数值说明

initialDelaySeconds:默认值是0,最小值是0,容器启动后等待多久,liveness or readiness开始第1次执行
periodSeconds:多久执行一次探针,默认值是10,最小值是1
timeoutSeconds:探针执行超时时间,默认值是1,最小值是1
successThreshold:失败后执行多少次探针成功才算成功,默认值是1,最小值是1,对于liveness来说必须是1
failureThreshold:探针连续失败多少次后才算失败,默认是3,最小值是1

11.Pod的restartPolicy重启策略机制

一个pod运行过程中,当一个容器执行完成后退出时,不管是成功还是失败,都会触发重启策略,pod会根据restartPolicy的设置参数值来决定要不要重启该容器,restartPolicy有以下3种取值:

Always:是默认取值,不管是成功退出还是失败退出,都尝试重启该容器,Pod的阶段会保持为Running OnFailure:限于失败的时候重启,Pod的阶段保持为Running,成功的时候不重启,Pod的阶段转为Succeeded Never:不管失败还是成功都不重启,Pod的阶段都转为Failed或者Succeeded 当一个Pod退出后要重启时,有一段指数级递增的延迟(10s,20s,40s…最大达到5分钟),当一个容器重启并运行10分钟不出问题后,这个延迟会重置。

12.Pod的删除和回收

12.1 删除

  • Pod代表着一组运行在node上的进程,所以允许这些进程在不再需要的时候能优雅地退出很重要(而不是被一个KILL信号强制停止,没有机会进行资源清理)

  • 当对一个Pod发起一个删除请求时,集群在强制删掉这个Pod前,会记录和追踪一段grace period(terminationGracePeriodSeconds, 默认是30s),在这段时间内,kubelet会尝试优雅地(graceful)关闭Pod。(如果存在preStop钩子需要执行耗时处理任务的时候,一般建议把terminationGracePeriodSeconds调大)

  • 当grace period开始时,Pod就会被从可用的服务列表中移除,服务请求不会再被转发到这个Pod上。

  • 当所有容器(包括pause)都停掉以后,kubelet会触发API Server把这个Pod删除掉。

来源参考资料:kubernetes.io/docs/concep…

12.2 回收

  • 对于已完成的Pod(包括Failed和Succeeded的Pod),它们的API对象会保留在系统中。当系统中创建的Pod数量超出了指定阈值(kube-controller-manager中的terminated-pod-gc-threshold)时,控制面板会清理这些已完成的Pod(包括Failed和Succeeded的Pod)。

来源参考资料:kubernetes.io/docs/concep…


二 、Service篇

一、服务一些要点归纳笔记

1、服务的一些关键点梳理总结

主要作用:

  • Pod启动和销毁都是导致自身的IP重新创建,也就是Pod 的IP会漂移
  • 使用Service来关联一组Pod,不关系Pod的IP的变化
  • 解耦Pod之间的关联性
  • 实现Pod服务发现和负载均衡
  • 服务通过标签关联相关的Pod对象实例
  • 作用于Proxy之后访问服务再转发对应的Pod里面 补充其他说明:
  • Service本身是一种虚拟IP地址,在service对象创建后就会保持不变,集群内POD资源都可以进行访问。
  • Service 可以理解是一种四层代理,主要工作层位于:TCP/IP层。
  • Service和Pod的连接,通常是通过Service所关联的endpoints对象来进行连接。
  • 创建service对象时,它所关联的endpoints对象也会被自动的创建。
  • 一个service对象对应是工作节点上的一些iptables或ipvs,这些iptables或ipvs主要是把到达service对象的IP地址的流量进行转发到绑定的它所关联的endpoints对象的IP地址和端口上。
  • service资源对象通常结合deployment来完成应用的创建和对外发布,所以一般部署一个的服务的时候可以两个创建的过程写到同一个文件里面去
  • 用来管理Pod的逻辑分组,外部访问service即可以访问Pod的策略,pod和service。 通过Label Selector连接

同一个文件里面创建一组相关的服务如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myNginx
  labels:
    app: myNginx
spec:
  replicas: 3
  template:
    metadata:
      name: myNginx
      labels:
        app: myNginx
    spec:
      containers:
        - name: myNginx
          image: icepear/myNginx:v1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
      restartPolicy: Always
  selector:
    matchLabels:
      app: myNginx
---
apiVersion: v1
kind: Service
metadata:
  name: nginxService
spec:
  selector:
    app: myNginx
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort


2、服务对象分类(个人理解梳理)

  1. ClusterIP类型(仅用于集群内部通信)
  2. NodePort类型(接入集群外部请求,如集群需外部访问,但仅限于访问节点)
  3. LoadBalancer类型(k8S 工作在云环境中的时候直接调用云环境所创建负载均衡器)
  4. ExternalName类型(将集群外部的服务引入到当前K8S集群内部,方便在集群内部使用)

二、服务几种方式类型

1、 默认的类型-创建 ClusterIp service服务

1.1 定义ClusterIp类型服务

默认的情况我们的yaml不知道的创建的service类型的时候就是默认的ClusterIp,ClusterIp其实可以理解为就一个VIP(Virtual IP Address,虚拟IP地址)

apiVersion: v1
kind: Service
metadata:
  name: xiaozhong-nginx-clusterip
  namespace: default
  labels:
    app: nginx
spec:
  ports: # 指定端口
    - name: socket
      port: 8828 # 暴露给服务的端口
      targetPort: 8828 #目标端口,容器的端口
      protocol: TCP
  selector: # 标签选择器,知道选择要关联的Pod资源标签
    app: nginx
  type: ClusterIP #指定服务类型为ClusterIP,默认不写就是这样类型



1.2 查看资源的明细:


[root@k81-master01 k8s-install]# kubectl describe svc xiaozhong-nginx-clusterip
Name:              xiaozhong-nginx-clusterip
Namespace:         default
Labels:            app=nginx
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"nginx"},"name":"xiaozhong-nginx-clusterip","namespace":"...
Selector:          app=nginx
Type:              ClusterIP
IP:                10.99.170.220 # 这个地方就是服务的虚拟IP
Port:              socket  8828/TCP
TargetPort:        8828/TCP
Endpoints:         10.244.1.19:8828,10.244.1.20:8828,10.244.2.10:8828 + 4 more... # 后端管理的Pod相关的IP和端口
Session Affinity:  None
Events:            <none>
[root@k81-master01 k8s-install]#

1.3 关于Session Affinity,Pod的会话保持

从上面查看我们的服务资源明细中有一个项是关于Session Affinity,它的主要作用就是对于同一个客户端的请求的处理的会话保持的机制。

会话保持就是:当我们的一个请求依赖于一些客户端的身份信息等来请求知道的某个Pod的时候,固定的调度某个Pod的时候,责可以启动这个粘性会话机制。

  • 它仅支持使用“ None” 和“ ClientIP” 两种属性值

  • 默认是None(随机调度) - 意思是:取消session

  • ClientIP: 表示开启session保持,同一ip访问同一个pod

1.4 数据转发流程理解

1:创建服务类型clusterIP之后, clusterIP在每个 node 节点使用 iptables,将发向 clusterIP 对应端口的数据,转发到 kube-proxy 中。

2:然后 基于kube-proxy 自己内部实现负载均衡的方法,查询到对应service 下对应 pod 的地址和端口,进而把数据转发给对应的 pod 的地址和端口

2、基于ClusterIP 基础上-NodePort类型访问

基于 ClusterIP 基础上为 Service 在每台机器上绑定一个端口, 这样就可以通过<NodeIP>:NodePort 来访问该SVC后端的80的pod组服务!

NodePort类型(接入集群外部请求,如集群需外部访问,但仅限于访问节点)

PS:此服务类型再安装K8S的使用已经预留一个端口范围用于NodePort,默认的端口范围是:30000-32767之间

2.1 定义NodePort类型服务

xiaozhong-nginx-nodeport.yaml文件:


apiVersion: v1
kind: Service
metadata:
  name: xiaozhong-nginx-nodeprt
  labels:
    app: nginx
spec:
  selector:
    app: nginx

  ports:
    - name: http
      port: 8000
      protocol: TCP
      targetPort: 80
  type: NodePort

2.2 创建服务和查看


[root@k81-master01 k8s-install]# kubectl apply -f xiaozhong-nginx-nodeport.yaml
service/xiaozhong-nginx-nodeprt created
[root@k81-master01 k8s-install]# kubectl get svc -o wide
NAME                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE    SELECTOR
kubernetes                ClusterIP   10.96.0.1       <none>        443/TCP          7d2h   <none>
xiaozhong-nginx-nodeprt   NodePort    10.102.138.39   <none>        8000:30595/TCP   102s   app=nginx
[root@k81-master01 k8s-install]#

2.3 外网访问验证

image.png

每个节点都去访问:

image.png

image.png

image.png

好像Nodeport也可以启动类似负载均衡器类型的作用啊!有点奇怪!估计我的理解还是有点偏差!后续继续再补充!

简图的图示:

image.png

2.4 数据转发流程理解:

1: 创建nodePort服务类型后, 则在 node 上开启一个随机(或指定的)端口,

2:访问Nodeip:nodePort的时候将向该端口的流量导入到 kube-proxy

3:然后由 kube-proxy 进一步(与接口层交互)到给对应的 pod

3、LoadBalancer:基于NodePort 的基础上的-LoadBalancer类型的Service服务部署创建的示例回顾

这种类型其实可以理解为:在 NodePort 的基础上,借助 cloud provider 创建一个外部负载均衡器,并将请求转发到<NodeIP>:NodePort

loadBalancer和nodePort其实是同一种方式。主要区别点在于 :loadBalancer比nodePort多了一步,就是可以调用cloud provider去创建LB(阿里云SLB)来向节点导流

从最初开始部署一个Nginx应用示例的时候,我们就开始其实开启了一个默认的LoadBalancer类型的Service,现在重新回顾一下当时的具体流程。

1、应用和服务创建流程

  • 第1步:使用Yaml创建部署一个deployment

编写一个deployment的yaml部署文件xiaozhong-nginx-dev.yaml:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: xiaozhong-nginx
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

  • 第2步:开始执行部署

[root@k81-master01 k8s-install]# kubectl apply -f xiaozhong-nginx-dev.yaml
deployment.apps/xiaozhong-nginx created
[root@k81-master01 k8s-install]#

  • 第3步:查看部署之后的Pod运行情况
[root@k81-master01 k8s-install]# kubectl get pods -o wide |grep xiaozhong
xiaozhong-nginx-5c559d5697-cfcxx    1/1     Running   0          4s      10.244.1.30   k81-node02   <none>           <none>
xiaozhong-nginx-5c559d5697-jtfbs    1/1     Running   0          4s      10.244.1.31   k81-node02   <none>           <none>
[root@k81-master01 k8s-install]#

两个Pod都运行再了Node2节点上!!

  • 第4步:直接访问Pod的id验证

image.png

全部节点访问验证: 在master和在Node2和Node3节点上访问:

PS:目前仅限于当前集群上内网内的访问,外部是无法访问

[root@k81-master01 k8s-install]# curl 10.244.1.31
[root@k81-master01 k8s-install]# curl 10.244.1.30
[root@k81-node02 ~]#  curl 10.244.1.30
[root@k81-node02 ~]#  curl 10.244.1.31
[root@k81-node03 ~]#  curl 10.244.1.31
[root@k81-node03 ~]#  curl 10.244.1.30

响应的结果:


结果都返回:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
[root@k81-master01 k8s-install]#

  • 第5步:(master节点)查看当前的服务资源明细
[root@k81-master01 k8s-install]# kubectl get svc -o wide
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE    SELECTOR
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   7d1h   <none>
[root@k81-master01 k8s-install]#

  • 第6步:(master节点)使用命令方式创建一个LoadBalancer负载均衡器服务

[root@k81-master01 k8s-install]#  kubectl expose deployment xiaozhong-nginx --port=80 --type=LoadBalancer
service/xiaozhong-nginx exposed
[root@k81-master01 k8s-install]# kubectl get svc -o wide
NAME              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE    SELECTOR
kubernetes        ClusterIP      10.96.0.1       <none>        443/TCP        7d1h   <none>
xiaozhong-nginx   LoadBalancer   10.98.134.172   <pending>     80:32610/TCP   5s     app=nginx
[root@k81-master01 k8s-install]#

查看我们的SVC明细:

image.png

  • 首先我们的这个服务svc的标签是: app: nginx,和我们的之前启动的应用绑定一起!
  • 此时我们的80是当前集群内分配给LoadBalancer服务对象的端口,我们可以通过集群内访问这个端口,就可以分发到集群内的Pod里面。 如:我们只需要访问当前,都可以分发到集群内的Pod

[root@k81-master01 k8s-install]# curl  10.98.134.172
[root@k81-node02 ~]# curl  10.98.134.172
[root@k81-node03 ~]# curl  10.98.134.172

  • 外网的访问,则是需要通过节点IP:32610端口访问

image.png

如访问如下任意节点访问都可以访问成功:


[root@k81-master01 k8s-install]# curl  192.168.219.138:32610
[root@k81-node02 ~]# curl  192.168.219.138:32610
[root@k81-node03 ~]# curl  192.168.219.138:32610
[root@k81-node03 ~]# curl  192.168.219.139:32610
[root@k81-node03 ~]# curl  192.168.219.140:32610

image.png

image.png

image.png

以上是一个简单的创建负载均衡器示例。

2、查看服务时候一些字段说明


[root@k81-master01 k8s-install]# kubectl get svc -o wide
NAME              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE    SELECTOR
kubernetes        ClusterIP      10.96.0.1       <none>        443/TCP        7d2h   <none>
xiaozhong-nginx   LoadBalancer   10.98.134.172   <pending>     80:32610/TCP   18m    app=nginx

  • NAME 服务名称
  • TYPE 服务类型
  • CLUSTER-IP kubernets给服务分配的IP地址
  • EXTERNAL-IP 外部IP地址,如果没指定的则是none
  • ports 是为服务的服务端口
  • AGE 启动多久了
  • SELECTOR 这个服务是属于哪个表情选择器的

3、服务更多详细明细

[root@k81-master01 k8s-install]# kubectl describe svc xiaozhong-nginx
Name:                     curl 
Namespace:                default
Labels:                   app=nginx
Annotations:              <none>
Selector:                 app=nginx
Type:                     LoadBalancer
IP:                       10.98.134.172
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  32610/TCP
Endpoints:                10.244.1.19:80,10.244.1.20:80,10.244.1.30:80 + 6 more...
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
[root@k81-master01 k8s-install]#

4、服务代理的Pod信息


[root@k81-master01 k8s-install]# kubectl get endpoints xiaozhong-nginx
NAME              ENDPOINTS                                                  AGE
xiaozhong-nginx   10.244.1.19:80,10.244.1.20:80,10.244.1.30:80 + 6 more...   24m
[root@k81-master01 k8s-install]#

5、删除服务

  • 通过文件删除


[root@k81-master01 k8s-install]# kubectl delete -f xiaozhong-nginx-dev.yaml
deployment.apps "xiaozhong-nginx" deleted
[root@k81-master01 k8s-install]#
[root@k81-master01 k8s-install]# kubectl delete -f xiaozhong-nginx-dev.yaml
Error from server (NotFound): error when deleting "xiaozhong-nginx-dev.yaml": deployments.apps "xiaozhong-nginx" not found
[root@k81-master01 k8s-install]# ^C
[root@k81-master01 k8s-install]#

这种方式实践好像删除不了!

再查看服务信息的时候没删除!情况未知!


[root@k81-master01 k8s-install]# kubectl get svc
NAME              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes        ClusterIP      10.96.0.1       <none>        443/TCP        7d2h
xiaozhong-nginx   LoadBalancer   10.98.134.172   <pending>     80:32610/TCP   38m
[root@k81-master01 k8s-install]#


  • 通过命令行(可以删除成功,且各节点外部无法再访问)
[root@k81-master01 k8s-install]# kubectl delete svc xiaozhong-nginx
service "xiaozhong-nginx" deleted
[root@k81-master01 k8s-install]#

[root@k81-master01 k8s-install]# kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   7d2h
[root@k81-master01 k8s-install]#

上面的示例还只是一个简单的IP端看示例!

4、 创建ExternalName service

PS:此主机名需要被DNS服务解析成CNAME 类型的记录,且此类似的服务对象其无clusterIP 和 Noport,也没有标签选择器,因此没endpoints ,其是一个域名,是CNAME结构,用于集群内部的域名

4.1 定义ExternalName类型服务

xiaozhong-nginx-ext.yaml文件:

apiVersion: v1
kind: Service
metadata:
  name: xiaozhong-nginx-externa
  labels:
    app: nginx
spec:
  selector:
    app: nginx
  ports:
    - name: http
      port: 8001
      protocol: TCP
      targetPort: 80
  type: ExternalName
  externalName: zyx.xiaozhong.cn

4.2 创建服务和查看


[root@k81-master01 k8s-install]# kubectl apply -f xiaozhong-nginx-ext.yaml
service/xiaozhong-nginx-externa created
[root@k81-master01 k8s-install]# ^C
[root@k81-master01 k8s-install]# kubectl get svc -o wide
NAME                      TYPE           CLUSTER-IP      EXTERNAL-IP        PORT(S)          AGE    SELECTOR
kubernetes                ClusterIP      10.96.0.1       <none>             443/TCP          7d3h   <none>
xiaozhong-nginx-externa   ExternalName   <none>          zyx.xiaozhong.cn   8001/TCP         65s    app=nginx
xiaozhong-nginx-nodeprt   NodePort       10.102.138.39   <none>             8000:30595/TCP   20m    app=nginx
[root@k81-master01 k8s-install]#

注意看这地方的外部的IP信息:

image.png

4.3 访问验证(因为我的域名是随便写的)

在集群内访问这个域名的时候提示没权限信息


[root@k81-master01 k8s-install]# curl zyx.xiaozhong.cn
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
 Sorry for the inconvenience.<br/>
Please report this message and include the following information to us.<br/>
Thank you very much!</p>
<table>
<tr>
<td>URL:</td>
<td>http://zyx.xiaozhong.cn/</td>
</tr>
<tr>
<td>Server:</td>
<td>izj6c7rml031xf17m0lnliz</td>
</tr>
<tr>
<td>Date:</td>
<td>2021/09/08 16:36:44</td>
</tr>
</table>
<hr/>Powered by Tengine/2.3.2<hr><center>tengine</center>
</body>
</html>
[root@k81-master01 k8s-install]#

5、 创建 Headless Service资源类型(无头服务)

无头服务类型的理解:

如果客户端需要直接的访问service资源后端的所有POD资源,此时则需要向客户端暴露每个Pod对象的IP地址,而不是使用中间层service对象类型中的的ClusterIP的IP地址。 这种的服务可以定义是一个无头的服务类型!

特点:

  • 无头服务没有ClusterIP
  • 没有负载均衡和代理问题

这里暂时理解的还不是很透!!!以后再深入理解一下!!!

5.1 定义Headless Service类型服务

apiVersion: v1
kind: Service
metadata:
    name: demo-service  # service名称
spec:
    clusterIP: None  #指定其类型为Headless Service资源
    selector:  # 用于匹配后面POD对象
        app: service
    ports:
        - protocol: TCP  # 使用的协议
          port: 80  #定义暴露的端口
          targetPort: 80  # 后端POD端口号
          name: httpport  # 定义名字

三、Service 三种代理模式

  • userspace
    • 用户空间 在Kubernetes 1.1版本之前,userspace是默认的代理模型。
  • iptables
    • iptables代理模式由Kubernetes 1.1版本引入,自1.2版本开始成为默认类型。
  • IPVS
    • Kubernetes自1.9-alpha版本引入了ipvs代理模式,自1.11版本开始成为默认设置。

四、了解Service-中的Ingress(关键重要)

首先我们的再之前的NodePort类型服务中我们的了解到,我们这种是在每个Node工作节点的上暴露我们的NodePort节点的方式来提供访问Nodeip:NodePort来访问我们的Pod的服务。

那如果我们的需要统一访问的路口处,使用域名的方式来规划我们的访问呐?这时候该如何的处理呢?

引入Ingress,Ingress也是Kubernetes中为了使外部的应用能够访问集群内的服务,在提供了以下一种方案:

  1. NodePort(上面已说)
  2. LoadBalancer(上面一说)
  3. Ingress(本节重点关注的)

以下下主要大部分文字来源主要是参考:blog.csdn.net/zhangjunli/…

4.1 Ingress 的组成

  • ingress对象 :将Nginx的配置抽象成一个Ingress对象,每添加一个新的服务只需写一个新的Ingress的yaml文件即可,它一般也是可以裂解为是k8s一个api对象,一般使用yaml进行配置,它的作用主要是定义请求转发到service规则,可以理解为是一个规则配置模板。

  • ingress controller :将新加入的Ingress对象的模板规则进行解析并转化成Nginx的配置文件并使之生效,来实现根据规则进行反向代理和负载均衡请求转发。

PS: ingress controller 可以有多种的实现,不一定是使用Nginx

4.2 Ingress 工作原理

ingress通过http或https暴露集群内部service,给service提供外部URL、负载均衡、SSL/TLS能力以及基于host的方向代理,ingress要依靠ingress-controller来具体实现以上功能。

1.ingress controller,的形式其实就是都是一个pod,里面跑着daemon程序和反向代理程序,它通过和kubernetes api交互,动态的去感知集群中ingress规则变化。

2.然后读取它,按照自定义的规则,规则就是写明了哪个域名对应哪个service,生成一段nginx配置

3.再写到nginx-ingress-control的pod里,这个Ingress controller的pod里运行着一个Nginx服务,控制器会把生成的nginx配置写入/etc/nginx.conf文件中

4.然后reload一下使配置生效。以此达到域名分配置和动态更新的问题。

4.3 Ingress 解决什么问题

ingress可以简单理解为service的service,它通过独立的ingress对象来制定请求转发的规则,把请求路由到一个或多个service中。

  • 1.动态配置服务

  如果按照传统方式, 当新增加一个服务时, 我们可能需要在流量入口加一个反向代理指向我们新的k8s服务. 而如果用了Ingress, 只需要配置好这个服务, 当服务启动时, 会自动注册到Ingress的中, 不需要而外的操作. 这种处理方式可以吧服务和请求进行解耦,可以从业务维护统一角度考虑业务暴露,不需要再为每个service单独考虑。

  • 2.减少不必要的端口暴露   配置过k8s的都清楚, 第一步是要关闭防火墙的, 主要原因是k8s的很多服务会以NodePort方式映射出去, 这样就相当于给宿主机打了很多孔, 既不安全也不优雅. 而Ingress可以避免这个问题, 除了Ingress自身服务可能需要映射出去, 其他服务都不要用NodePort方式

4.5 Ingress 的几个优点

  • 解决我们通过Service发现Pod进行管理,且提供基于域名的访问机制
  • 通过ingress-control 实现Pod负载均衡(基于策略)
  • 支持TCP/UDP4层的负载均衡和HTTP 7层负载均衡
  • 可以根据不同域名、不同path转发请求到不同的service,并且支持https/http

PS: 1:Ingress通常不会公开任意端口和协议!

PS: 2:向 Internet 公开 HTTP 和 HTTPS 以外的服务通常Service.Type类型是:NodePort

4.4 Ingress 可选控制器类型

  • nginx
  • istio(网格服务)
  • Traefix
  • haproxy
  • apisix

4.5 Ingress 流程图解

前提理解要点:

  • 我们的ingress-control其实也是一个Pod对象,且它的是一种DaemonSet类型的资源(前提基于DaemonSet+HostNetwork+nodeSelector部署模式下)

PS:关于DaemonSet类型的资源后续会展开,这里先做一个简单的介绍:

1:DaemonSet类型它也是一种Pod控制器。

2:它的特点是:会在每一个Node节点上都会生成并且只能生成一个Pod资源。

3:使用场景:如果必须将Pod运行在固定的某个或某几个节点,且要优先于其他Pod的启动。

4:通常情况下,默认每一个节点都会运行,并且只能运行一个Pod。这种情况推荐使用DaemonSet资源对象。比如我们的普罗米修斯的采集信息,都需要在每个节点进行采集。就合适使用这种类型Pod.

  • 如果使用的是(nginx-ingress),那么ingress-control它会在每个工作节点上启动一个NGING的Pod

问题点思考:

image.png

上述的问题点是:我们的外部访问依赖使用Nodeip:NodePort(service暴露的端口)的方式,那我们的能不能统一的使用一个域名的方式来做统一的访问入口呐?

image.png

4.4 Ingress 安装部署示例验证说明

blog.csdn.net/yujia_666/a…

来自官网的安装部署步骤: kubernetes.github.io/ingress-ngi…

4.4.1 Ingress 常见的部署和暴露方式:

PS:以下的文字来自blog.csdn.net/yujia_666/a…

Deployment+LoadBalancer模式的Service

如果要把ingress部署在公有云,那用这种方式比较合适。用Deployment部署ingress-controller,创建一个type为LoadBalancer的service关联这组pod。大部分公有云,都会为LoadBalancer的service自动创建一个负载均衡器,通常还绑定了公网地址。只要把域名解析指向该地址,就实现了集群服务的对外暴露。

Deployment+NodePort模式的Service

同样用deployment模式部署ingress-controller,并创建对应的服务,但是type为NodePort。这样,ingress就会暴露在集群节点ip的特定端口上。由于nodeport暴露的端口是随机端口,一般会在前面再搭建一套负载均衡器来转发请求。该方式一般用于宿主机是相对固定的环境ip地址不变的场景。
NodePort方式暴露ingress虽然简单方便,但是NodePort多了一层NAT,在请求量级很大时可能对性能会有一定影响。

DaemonSet+HostNetwork+nodeSelector

用DaemonSet结合nodeselector来部署ingress-controller到特定的node上,然后使用HostNetwork直接把该pod与宿主机node的网络打通,直接使用宿主机的80/433端口就能访问服务。这时,ingress-controller所在的node机器就很类似传统架构的边缘节点,比如机房入口的nginx服务器。该方式整个请求链路最简单,性能相对NodePort模式更好。缺点是由于直接利用宿主机节点的网络和端口,一个node只能部署一个ingress-controller pod。比较适合大并发的生产环境使用。


以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!

结尾

END

简书:www.jianshu.com/u/d6960089b…

掘金:juejin.cn/user/296393…

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822