11. K8S资源限制,多账户管理及网络实现

1. Kubernetes pod、container与namespace资源限制

image-20211101230348104

image-20211101230425800
  • CPU 以核心(毫核,1核=1000毫核,500m=0.5核)为单位。

  • memory 以字节为单位。

  • requests 为kubernetes scheduler执行pod调度时node节点至少需要拥有的资源。

  • limits 为pod运行成功后最多可以使用的资源上限。

1.1 对单个容器的CPU及memory实现资源限制

https://kubernetes.io/zh/docs/tasks/configure-pod-container/assign-memory-resource/

#apiVersion: extensions/v1beta1
apiVersion: apps/v1
kind: Deployment
metadata:
  name: limit-test-deployment
  namespace: test
spec:
  replicas: 1
  selector:
    matchLabels: #rs or deployment
      app: limit-test-pod
#    matchExpressions:
#      - {key: app, operator: In, values: [ng-deploy-80,ng-rs-81]}
  template:
    metadata:
      labels:
        app: limit-test-pod
    spec:
      containers:
      - name: limit-test-container
        image: lorel/docker-stress-ng
        resources:      #不能被直接更新,即之前没有指定资源限制,则后期直接添加该字段后,容器会重建才生效。
          limits:
            memory: "200Mi"
            cpu: 200m
          requests:
            memory: "100Mi"
        #command: ["stress"]
        args: ["--vm", "2", "--vm-bytes", "256M"]
      #nodeSelector:
      #  env: group1
root@deploy:~# kubectl top pod -n test
W1101 23:21:00.710519   23384 top_pod.go:140] Using json format to get metrics. Next release will switch to protocol-buffers, switch early by passing --use-protocol-buffers flag
NAME                                          CPU(cores)   MEMORY(bytes)   
deploy-devops-redis-5f874fd856-65z8f          0m           0Mi             
limit-test-deployment-7545f64fcc-9fb4q        200m         168Mi        

推荐限制:

Nginx: 2c2g

微服务:2c2g/2c3g

Mysql/ES: 4c6g/4c/8g (略小于虚拟机)

1.2 对单个pod的CPU及memory实现资源限制

  • Kubernetes 集群上的容器运行使用的计算资源没有限制。
  • Kubernetes中可以以命名空间为单位,限制其资源的使用与创建。
  • 为避免一个 Pod 或 Container独占可用资源,使用Limit Range资源对象。
  • Limit Range是对具体某个Pod或容器的资源使用进行限制
  • https://kubernetes.io/zh/docs/concepts/policy/limit-range/

Limit Range的实现形式:

  • 限制namespace中每个Pod或容器的最小与最大计算资源

  • 限制namespace中每个Pod或容器计算资源request、limit之间的比例

  • 限制namespace中每个存储卷声明(PersistentVolumeClaim)可使用的最小与最大存储空间

  • 设置namespace中容器默认计算资源的request、limit,并在运行时自动注入到容器中

apiVersion: v1
kind: LimitRange
metadata:
  name: limitrange-test
  namespace: test
spec:
  limits:
  - type: Container       #限制的资源类型
    max:
      cpu: "2"            #限制单个容器的最大CPU
      memory: "2Gi"       #限制单个容器的最大内存
    min:
      cpu: "500m"         #限制单个容器的最小CPU
      memory: "512Mi"     #限制单个容器的最小内存
    default:
      cpu: "500m"         #默认单个容器的CPU限制
      memory: "512Mi"     #默认单个容器的内存限制
    defaultRequest:
      cpu: "500m"         #默认单个容器的CPU创建请求
      memory: "512Mi"     #默认单个容器的内存创建请求
    maxLimitRequestRatio:
      cpu: 2              #限制CPU limit/request比值最大为2  
      memory: 1.5         #限制内存limit/request比值最大为1.5
  - type: Pod
    max:
      cpu: "4"            #限制单个Pod的最大CPU
      memory: "4Gi"       #限制单个Pod最大内存
  - type: PersistentVolumeClaim
    max:
      storage: 50Gi        #限制PVC最大的requests.storage
    min:
      storage: 30Gi        #限制PVC最小的requests.storage
root@deploy:~# kubectl describe limitranges limitrange-test -n test
Name:                  limitrange-test
Namespace:             test
Type                   Resource  Min    Max   Default Request  Default Limit  Max Limit/Request Ratio
----                   --------  ---    ---   ---------------  -------------  -----------------------
Container              memory    512Mi  2Gi   512Mi            512Mi          2
Container              cpu       500m   2     500m             500m           2
Pod                    cpu       -      4     -                -              -
Pod                    memory    -      4Gi   -                -              -
PersistentVolumeClaim  storage   30Gi   50Gi  -                -              -
# 若yaml创建的资源不符合limitrange的要求,pod调度失败,pod会一直无法get到,但并没有明显报错,需要查看deployment详情才可以。
kubectl  get  deployments test-wordpress-deployment  -n test  -o json

1.2.1 案例:CPU与内存 RequestRatio比例限制

形如上面的limitrange-test,执行以下deployment,注意resources中的limit与request 资源的比例,可以看到容器test-wordpress-php-container的cpu limit/request 是 4,大于limitrange(单容器Max Limit/Request Ratio pe=2)的限制。容器test-wordpress-nginx-container的limit memory也超限了(单容器最大memory为2G)。

kind: Deployment
apiVersion: apps/v1
metadata:
  labels:
    app: test-wordpress-deployment-label
  name: test-wordpress-deployment
  namespace: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-wordpress-selector
  template:
    metadata:
      labels:
        app: test-wordpress-selector
    spec:
      containers:
      - name: test-wordpress-nginx-container
        image: nginx:1.16.1
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          protocol: TCP
          name: http
        env:
        - name: "password"
          value: "123456"
        - name: "age"
          value: "18"
        resources:
          limits:
            cpu: 2
            memory: 3Gi
          requests:
            cpu: 2
            memory: 1.5Gi

      - name: test-wordpress-php-container
        image: php:5.6-fpm-alpine 
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          protocol: TCP
          name: http
        env:
        - name: "password"
          value: "123456"
        - name: "age"
          value: "18"
        resources:
          limits:
            #cpu: 1
            cpu: 2
            memory: 1Gi
          requests:
            cpu: 500m
            memory: 512Mi


---
kind: Service
apiVersion: v1
metadata:
  labels:
    app: test-wordpress-service-label
  name: test-wordpress-service
  namespace: test
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
    nodePort: 30063
  selector:
    app: test-wordpress-selector

执行apply后,pod调度失败,get deployment 得到的报错如下。执行以下deployment,注意,

image-20211102102223092

1.2.2 案例:CPU与内存或超分限制

仍然采用形如上面的limitrange-test,test-wordpress-php-container中limits-cpu为2.8 超过了容器最大cpu为2核的限制,报错如下。

kind: Deployment
apiVersion: apps/v1
metadata:
  labels:
    app: test-wordpress-deployment-label
  name: test-wordpress-deployment
  namespace: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-wordpress-selector
  template:
    metadata:
      labels:
        app: test-wordpress-selector
    spec:
      containers:
      - name: test-wordpress-nginx-container
        image: nginx:1.16.1
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          protocol: TCP
          name: http
        env:
        - name: "password"
          value: "123456"
        - name: "age"
          value: "18"
        resources:
          limits:
            cpu: 1
            memory: 1Gi
          requests:
            cpu: 500m
            memory: 512Mi

      - name: test-wordpress-php-container
        image: php:5.6-fpm-alpine 
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          protocol: TCP
          name: http
        env:
        - name: "password"
          value: "123456"
        - name: "age"
          value: "18"
        resources:
          limits:
            cpu: 2.8
            #cpu: 2
            memory: 1Gi
          requests:
            cpu: 1.5
            memory: 512Mi


---
kind: Service
apiVersion: v1
metadata:
  labels:
    app: test-wordpress-service-label
  name: test-wordpress-service
  namespace: test
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
    nodePort: 30033
  selector:
    app: test-wordpress-selector

image-20211102105306828

1.3 对整个namespace的CPU及memory实现资源限制

https://kubernetes.io/zh/docs/concepts/policy/resource-quotas/

当多个用户或团队共享具有固定节点数目的集群时,通过资源配额来解决资源分配使用的问题。资源配额,通过 ResourceQuota 对象来定义,对每个命名空间的资源消耗总量提供限制。

实现形式:

  • 限定某个对象类型(如Pod、service)可创建对象的总数;
  • 限定某个对象类型可消耗的计算资源(CPU、内存)与存储资源(存储卷声明)总数。
apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-test
  namespace: test
spec:
  hard:
    requests.cpu: "8"
    limits.cpu: "8"
    requests.memory: 4Gi
    limits.memory: 4Gi
    requests.nvidia.com/gpu: 4
    pods: "2"
    services: "6"
root@deploy:~# kubectl get resourcequota -n test
NAME         AGE   REQUEST                                                                                                        LIMIT
quota-test   38s   pods: 13/2, requests.cpu: 2100m/8, requests.memory: 2660Mi/4Gi, requests.nvidia.com/gpu: 0/4, services: 14/6   limits.cpu: 3/8, limits.memory: 1536Mi/4Gi

root@deploy:~# kubectl describe resourcequota quota-test -n test
Name:                    quota-test
Namespace:               test
Resource                 Used    Hard
--------                 ----    ----
limits.cpu               3       8
limits.memory            1536Mi  4Gi
pods                     13      2
requests.cpu             2100m   8
requests.memory          2660Mi  4Gi
requests.nvidia.com/gpu  0       4
services                 14      6

2. Kubernetes准入、鉴权、RBAC简介

2.1 RBAC鉴权

  • 使用RBAC鉴权:https://kubernetes.io/zh/docs/reference/access-authn-authz/rbac/

基于角色(Role)的访问控制(RBAC)是一种基于组织中用户的角色来调节控制对 计算机或网络资源的访问的方法。

RBAC 鉴权机制使用 rbac.authorization.k8s.io API 组 来驱动鉴权决定,允许你通过 Kubernetes API 动态配置策略。

RBAC API 声明了四种 Kubernetes 对象:RoleClusterRoleRoleBindingClusterRoleBinding。你可以像使用其他 Kubernetes 对象一样, 通过类似 kubectl 这类工具 描述对象, 或修补对象。

  • Role 和 ClusterRole

Role 总是用来在某个名字空间 内设置访问权限;在你创建 Role 时,你必须指定该 Role 所属的名字空间。

ClusterRole 则是一个集群作用域的资源。这两种资源的名字不同(Role 和 ClusterRole)是因为 Kubernetes 对象要么是名字空间作用域的,要么是集群作用域的, 不可两者兼具。

ClusterRole 有若干用法。你可以用它来:

  1. 定义对某名字空间域对象的访问权限,并将在各个名字空间内完成授权;

  2. 为名字空间作用域的对象设置访问权限,并跨所有名字空间执行授权;

  3. 为集群作用域的资源定义访问权限。

  • RoleBinding 和 ClusterRoleBinding

如果你希望在名字空间内定义角色,应该使用 Role; 如果你希望定义集群范围的角色,应该使用 ClusterRole。

角色绑定(Role Binding)是将角色中定义的权限赋予一个或者一组用户。 它包含若干 主体(用户、组或服务账户)的列表和对这些主体所获得的角色的引用。 RoleBinding 在指定的名字空间中执行授权,而 ClusterRoleBinding 在集群范围执行授权。

一个 RoleBinding 可以引用同一的名字空间中的任何 Role。 或者,一个 RoleBinding 可以引用某 ClusterRole 并将该 ClusterRole 绑定到 RoleBinding 所在的名字空间。 如果你希望将某 ClusterRole 绑定到集群中所有名字空间,你要使用 ClusterRoleBinding。

2.2 鉴权概述

RBAC是基于角色的访问控制(Role-Based Access Control)

鉴权概述:https://kubernetes.io/zh/docs/reference/access-authn-authz/authorization/

Kubernetes 使用 API 服务器对 API 请求进行鉴权。 它根据所有策略评估所有请求属性来决定允许或拒绝请求。 一个 API 请求的所有部分都必须被某些策略允许才能继续。 这意味着默认情况下拒绝权限。

Kubernetes 仅审查以下 API 请求属性:

  • 用户 - 身份验证期间提供的 user 字符串。
  • - 经过身份验证的用户所属的组名列表。
  • 额外信息 - 由身份验证层提供的任意字符串键到字符串值的映射。
  • API - 指示请求是否针对 API 资源。
  • 请求路径 - 各种非资源端点的路径,如 /api/healthz
  • API 请求动词 - API 动词 getlistcreateupdatepatchwatchproxyredirectdeletedeletecollection 用于资源请求。 要确定资源 API 端点的请求动词,请参阅 确定请求动词
  • HTTP 请求动词 - HTTP 动词 getpostputdelete 用于非资源请求。
  • Resource - 正在访问的资源的 ID 或名称(仅限资源请求)- 对于使用 getupdatepatchdelete 动词的资源请求,你必须提供资源名称。
  • 子资源 - 正在访问的子资源(仅限资源请求)。
  • 名字空间 - 正在访问的对象的名称空间(仅适用于名字空间资源请求)。
  • API 组 - 正在访问的 API 组 (仅限资源请求)。空字符串表示核心 API 组

3. Kubernetes 多账户实现案例

3.1 在指定的namespace创建账号

kubectl create serviceaccount user1 -n  test

3.2 创建role规则

#cat test-role.yaml 
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: test
  name: user1-role
rules:
- apiGroups: ["*"]
  resources: ["pods/exec"] #进入容器执行操作的权限
  #verbs: ["*"]
  ##RO-Role
  verbs: ["get", "list", "watch", "create"]

- apiGroups: ["*"]
  resources: ["pods"]
  #verbs: ["*"]
  ##RO-Role
  verbs: ["get", "list", "watch"]  

- apiGroups: ["apps/v1"]
  resources: ["deployments"]
  #verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  ##RO-Role
  verbs: ["get", "watch", "list"]
kubectl apply  -f test-role.yaml

root@deploy:~# kubectl get role -n test
NAME         CREATED AT
user1-role   2021-11-02T08:27:43Z

root@deploy:~# kubectl describe role user1-role -n test
Name:         user1-role
Labels:       <none>
Annotations:  <none>
PolicyRule:
  Resources            Non-Resource URLs  Resource Names  Verbs
  ---------            -----------------  --------------  -----
  pods.*/exec          []                 []              [get list watch create]
  pods.*               []                 []              [get list watch]
  deployments.apps/v1  []                 []              [get watch list]

3.3 将规则与账户进行绑定

# cat user1-role-bind.yaml 
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: role-bind-user1
  namespace: test
subjects:
- kind: ServiceAccount
  name: user1
  namespace: test
roleRef:
  kind: Role
  name: user1-role
  apiGroup: rbac.authorization.k8s.io
kubectl apply  -f user1-role-bind.yaml

root@deploy:~# kubectl get rolebinding -n test
NAME              ROLE              AGE
role-bind-user1   Role/user1-role   15m

3.4 获取token名称

kubectl get secret -n  test | grep user1
user1-token-bltgx           kubernetes.io/service-account-token   3      60s

3.5 使用base64加密

kubectl get secret user1-token-bltgx -o jsonpath={.data.token} -n test |base64 -d && echo ""
eyJhbGciOiJSUzI1NiIsImtpZCI6IllQRVg2c1N2QWxZVmtUcXBYLVRJelcwUWZMQ2J5bTRVWVdFZnBYSmg3UkEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJ0ZXN0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InVzZXIxLXRva2VuLWJsdGd4Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6InVzZXIxIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiN2RiYmZiYmItZjg3Yy00N2ZjLTgxMjAtZjcxYTczMGYxODZjIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OnRlc3Q6dXNlcjEifQ.hOwF7_0dae9F9foJOVCr51o1CST0fuONn9vEnVRDo5lJHfMscydPk85leCnpUI53grn3GksBIW2tajF1UnXLpTTGT9gywdrCLwe32x7JkCtT4Le01FVII63AyMzid15uaiy8vW8kPKS1XQXXJyrD6XrdvmFvKY6DajjtilUvaK9q04UYmgL_1F-Wdg-n7j18__goA1TMJ5tapo7POUUuEIOUoqRZtgr4NN0tgcM9DkRfzAagVriK7qRMA5i40IgoFfyjqsfzYw6AEWdm9mIYpw0rzjIADTXMoyBfqlb5kSumLbV95Xi_iyzTEiocaY54_DUAqD3swqQm5kk-fdER2g

3.6 使用user1的token登录dashboard测试

image-20211102165404712

image-20211102165427328

image-20211102165624533

3.7 基于kube-config文件登录

3.7.1 创建csr文件

# cat user1-csr.json 
{
  "CN": "China",
  "hosts": [],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "CN",
      "ST": "BeiJing",
      "L": "BeiJing",
      "O": "k8s",
      "OU": "System"
    }
  ]
}

3.7.2 签发证书

ln -sv /etc/kubeasz/bin/cfssl* /usr/bin/
cfssl gencert -ca=/etc/kubeasz/clusters/k8s-01/ssl/ca.pem  -ca-key=/etc/kubeasz/clusters/k8s-01/ssl/ca-key.pem -config=/etc/kubeasz/clusters/k8s-01/ssl/ca-config.json -profile=kubernetes user1-csr.json | cfssljson -bare  user1
-rw-r--r-- 1 root root  218 Nov  2 17:04 user1-csr.json
-rw------- 1 root root 1679 Nov  2 17:06 user1-key.pem
-rw-r--r-- 1 root root  993 Nov  2 17:06 user1.csr
-rw-r--r-- 1 root root 1383 Nov  2 17:06 user1.pem

3.7.3 生成普通用户kubeconfig文件

kubectl config set-cluster cluster1 --certificate-authority=/etc/kubeasz/clusters/k8s-01/ssl/ca.pem --embed-certs=true --server=https://172.16.244.202:6443 --kubeconfig=user1.kubeconfig
#--embed-certs=true为嵌入证书信息
#--certificate-authority=/etc/kubernetes/ssl/ca.pem 在master节点上就写这个路径,在deploy节点上,就写kubeasz配置集群时生成的证书文件路径。

3.7.4 设置客户端认证参数

cp *.pem /etc/kubeasz/clusters/k8s-01/ssl/  #在master节点上就写/etc/kubernetes/ssl/ 这个路径,目的是保存账户的key

kubectl config set-credentials user1 \
--client-certificate=/etc/kubeasz/clusters/k8s-01/ssl/user1.pem \
--client-key=/etc/kubeasz/clusters/k8s-01/ssl/user1-key.pem \
--embed-certs=true \
--kubeconfig=user1.kubeconfig

3.7.5 设置上下文参数(多集群使用上下文区分)

https://kubernetes.io/zh/docs/concepts/configuration/organize-cluster-access-kubeconfig/

kubectl config set-context context-cluster1 \
--cluster=cluster1 \
--user=user1 \
--namespace=test \
--kubeconfig=user1.kubeconfig

3.7.6 设置默认上下文

kubectl config use-context context-cluster1 --kubeconfig=user1.kubeconfig

3.7.7 获取token

kubectl  get secrets  -n test | grep user1
kubectl  describe secrets user1-token-bltgx  -n  test 
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6IllQRVg2c1N2QWxZVmtUcXBYLVRJelcwUWZMQ2J5bTRVWVdFZnBYSmg3UkEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJ0ZXN0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InVzZXIxLXRva2VuLWJsdGd4Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6InVzZXIxIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiN2RiYmZiYmItZjg3Yy00N2ZjLTgxMjAtZjcxYTczMGYxODZjIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OnRlc3Q6dXNlcjEifQ.hOwF7_0dae9F9foJOVCr51o1CST0fuONn9vEnVRDo5lJHfMscydPk85leCnpUI53grn3GksBIW2tajF1UnXLpTTGT9gywdrCLwe32x7JkCtT4Le01FVII63AyMzid15uaiy8vW8kPKS1XQXXJyrD6XrdvmFvKY6DajjtilUvaK9q04UYmgL_1F-Wdg-n7j18__goA1TMJ5tapo7POUUuEIOUoqRZtgr4NN0tgcM9DkRfzAagVriK7qRMA5i40IgoFfyjqsfzYw6AEWdm9mIYpw0rzjIADTXMoyBfqlb5kSumLbV95Xi_iyzTEiocaY54_DUAqD3swqQm5kk-fdER2g

3.7.8 将token写入用户kube-config文件

image-20211102184203825

3.7.9 dashboard登录测试

image-20211102184253988

4.kubernetes中的网络实现方式

https://kubernetes.io/zh/docs/concepts/cluster-administration/networking/

容器网络的目的:

  1. 实现同一个pod中的不同容器通信(LNMP)

  2. 实现pod与pod同主机与跨主机的容器通信

  3. pod和服务之间的通信(nginx通过调用tomcat)

  4. pod与k8s之外的网络通信

    外部到pod(客户端的请求)

    pod到外部(响应报文)

image-20211103140220068

4.1 网络通信方式

4.1.1 二层网络

基于目标mac地址通信,不可夸局域网通信,通常是由交换机实现报文转发。

  • private mode:
    private 模式下,同一父接口下的子接口之间彼此隔离,不能通信,从外部也无法访问。

  • vepa(Virtual Ethernet Port Aggregator,虚拟以太网端口聚合器) mode:
    vepa 模式下,子接口之间的通信流量需要导到外部支持 802.1Qbg/VPEA 功能的交换机上(可以是物理的或者虚拟的), 经由外部交换机转发,再绕回来。

  • bridge mode:
    bridge 模式下,模拟的是 Linux bridge 的功能,但比 bridge 要好的一点是每个接口的 MAC 地址是已知的,不用学习,所以这种模式下,子接口之间就是直接可以通信的。

  • passthru mode(直通模式):
    passthru 模式,只允许单个子接口连接父接口。

  • source mode:
    这种模式,只接收源 mac 为指定的 mac 地址的报文。

image-20211103143127277

4.1.3 三层网络

基于目标 IP通信,也叫做IP交换技术,解决了夸局域网、跨网络通信,通常是由路由器、 防火墙、三层交换机等。

image-20211103151349032

4.1.4 网桥

安装完 docker 之后会默认生成一个 docker 的网桥设备,网桥设备通过 mac 地址转发报文到各个容器,如果容器要访问当前宿主机意外的容器或者网络,则会使用宿主机的路由功能进行源地址转换。

image-20211103151558825

4.1.5 Vlan

VLAN(Virtual Local Area Network)即虚拟局域网,是将一个物理(交换机)的网络在逻辑上划 分成多个广播域的通信技术,VLAN 内的主机间可以直接通信,而 VLAN 网络外的主机需要 通过三层网络设备转发才可以通信,因此一个 vlan 可以将服务器的广播报文限制在一个 VLAN 内,从而降低单个网络环境中的广播报文,vlan 采用 12 位标识 vlan ID,即一个交换机 设备最多为 2^12=4096 个 vlan。

image-20211103151741081

4.2 Overlay

4.2.1 Vxlan

  • 本质上是一种 overlay 的隧道封装技术
  • 将 L2 的以太帧封装到 UDP报文(即 L2 over L4)中,在 L3 网络中传输
  • 使用 MAC in UDP 的方法对报文进行重新封装
  • vxlan 采用24 位标识 vlan ID 号,因此可以支持 2^24=16777216 个 vlan
image-20211103152727059
4.2.1.1 vtep
  • VTEP 是 VXLAN 网络的边缘设备,是 VXLAN 隧道的起点和终点。
  • VXLAN 对用户原始数据帧的封装和解封装均在 VTEP 上进行,用于VXLAN 报文的封装和解封装。
  • VTEP 与物理网络相连,分配的地址为物理网 IP 地址。
  • VXLAN 报文中源 IP 地址为本节点的 VTEP 地址,VXLAN 报文中目的 IP 地址为对端节点的 VTEP 地址。
  • 一对 VTEP 地址就对应着一个 VXLAN 隧道,服务器上的虚拟交换机(隧道 flannel.1就是 VTEP)。
4.2.1.2 VNI

VNI(VXLAN Network Identifier)

  • Vxlan的网络标识,类似于vlanID
  • 不同vxlan段的虚拟机不能直接通信
  • 一个VNI对应一个租户

image-20211103155539695

4.2.2 NVGRE

Network Virtualization using Generic Routing Encapsulation

  • 主要支持者是 Microsoft。
  • NVGRE 没有采用标准传输协议(TCP/UDP),而是借助通用路由封装协议(GRE)。
  • NVGRE 使用 GRE 头部的低 24 位作为租户网络标识符(TNI), 与 VXLAN 一样可以支持 1777216 个 vlan。

4.3 Underlay

  • 容器网络中的 Underlay 网络是指借助驱动程序将宿主机的底层网络接口直接暴露给容器使用的一种网络构建技术,较为常见的解决方案有 MAC VLAN、IP VLAN 和直接路由 等。
  • Underlay 依赖于网络网络进行跨主机通信。

4.3.1 bridge和macvlan模式

  • bridge: 桥接模式。
  • macvlan:支持在同一个以太网接口上虚拟出多个网络接口(子接口),每个虚拟接口都有唯一的mac地址并可配置网卡子接口的IP。

image-20211103160607879

image-20211103160634784

4.3.2 IP Vlan

  • 创建新的虚拟网络接口并为每个接口分配唯一的 IP 地址
  • 每个虚拟接口将共享使用物理接口的 MAC 地址,从而不再违反防止 MAC 欺 骗的交换机的安全策略,且不要求在物理接口上启用混杂模式。
  • IP VLAN 有 L2 和 L3 两种模型,
    • IP VLAN L2 的工作模式类似于 MAC VLAN 被用作网桥或交换机
    • IP VLAN L3 模式中,子接口地址不一样,但是公用宿主机的 MAC 地址。 虽然支持多种网络模型,但 MAC VLAN 和 IP VLAN 不能同时在同一物理接口上使用。
    • 一般使用 MACVLAN。
    • Linux 内核自 4.2 版本后才支持 IP VLAN。

4.4 Vxlan的通信过程

https://support.huawei.com/enterprise/zh/doc/EDOC1100087027

https://www.pianshen.com/article/1890293327/

4.4.1 vxlan的通信流程

  1. VMA发送L2 帧与VM请求与VMB通信。
  2. 源宿主机 VTEP 添加或者封装 VXLAN、UDP 及 IP 头部报文。
  3. 网络层设备将封装后的报文通过标准的报文在三层网络进行转发到目标主机。
  4. 目标宿主机 VTEP 删除或者解封装 VXLAN、UDP 及 IP 头部。
  5. 将原始 L2 帧发送给目标 VM。

image-20211103165548093

4.4.2 报文格式

image-20211103165700122

4.5 Docker的网络支持

https://docs.docker.com/network/

root@node2:~# docker info
Client:
 Debug Mode: false

Server:
 Containers: 21
  Running: 6
  Paused: 0
  Stopped: 15
 Images: 13
 Server Version: 19.03.15
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Native Overlay Diff: true
 Logging Driver: json-file
 Cgroup Driver: systemd
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 ...

Docker 的网络子系统是可插拔的,使用驱动程序。默认情况下存在多个驱动程序,并提供核心网络功能:

  • bridge:默认的网络驱动程序。如果您没有指定驱动程序,这就是您正在创建的网络类型。当您的应用程序在需要通信的独立容器中运行时,通常会使用桥接网络。请参阅 桥接网络。

  • host:对于独立容器,去掉容器与Docker主机的网络隔离,直接使用主机的网络。请参阅 使用主机网络。

  • overlay:覆盖网络将多个 Docker 守护进程连接在一起,并使 swarm 服务能够相互通信。您还可以使用覆盖网络来促进 swarm 服务和独立容器之间的通信,或者不同 Docker 守护进程上的两个独立容器之间的通信。这种策略消除了在这些容器之间进行操作系统级路由的需要。请参阅覆盖网络。

  • macvlan:Macvlan 网络允许您为容器分配 MAC 地址,使其在您的网络上显示为物理设备。Docker 守护进程通过容器的 MAC 地址将流量路由到容器。macvlan 在处理期望直接连接到物理网络而不是通过 Docker 主机的网络堆栈路由的遗留应用程序时,使用驱动程序有时是最佳选择。请参阅 Macvlan 网络。

  • none:对于这个容器,禁用所有网络。通常与自定义网络驱动程序结合使用。none不适用于群服务。请参阅 禁用容器网络。

  • 网络插件:您可以通过 Docker 安装和使用第三方网络插件。这些插件可从 Docker Hub 或第三方供应商处获得。有关安装和使用给定网络插件的信息,请参阅供应商的文档。

4.6 Docker的通信方案

  • Bridge 网络,docker0 默认是桥接网络

  • Docker 网络驱动:

    • Overlay:基于 VXLAN、NVGRE 等封装技术实现 overlay 叠加网络。
    • Macvlan:基于 Docker 宿主机物理网卡的不同子接口实现多个虚拟 vlan,一个子接口就 是一个虚拟 vlan,容器通过宿主机的路由功能和外网保持通信。
  • 常见的通信方案:

    • Calico—BGP+IPIP:已被 github 等公司用于生产环境
    • Flannel: VXLAN(内核 3.12 或以上)/UDP
    • OpenvSwitch:VXLAN(内核 3.12 或以上)/UDP
    • Canal:合并 flannel 和 calico 的功能
    • Weave:VXLAN(内核 3.12 或以上)和 UDP

4.6.1 Underlay-macvlan

4.6.1.1 Private(私有)模式

在Private模式下,同一个宿主机下的容器不能通信,即使通过交换机把数据转发回来也不行。

image-20211104154807995
4.6.1.1.1 创建
docker network create -d macvlan --subnet=172.16.244.0/24 --gateway=172.16.244.2 -o parent=eth0 -o macvlan_mode=private macvlan_private

docker run -it --rm --net=macvlan_private --name=test_private1 --ip=172.16.244.231 alpine:latest sh

docker run -it --rm --net=macvlan_private --name=test_private2 --ip=172.16.244.232 alpine:latest sh
4.6.1.1.2 验证
/ # ping 172.16.244.232
PING 172.16.244.232 (172.16.244.232): 56 data bytes
root@docker3:~# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
8dc1e90d20e1        bridge              bridge              local
9ce3dbbfda0a        host                host                local
88a146720df0        macvlan_private     macvlan             local
5d6bf526316e        none                null                local
root@docker3:~# docker network inspect macvlan_private
[
    {
        "Name": "macvlan_private",
        "Id": "88a146720df051df1f36a52fd5235b1392fa622f07840888e169870ba4171ba5",
        "Created": "2021-11-04T15:38:05.176636032+08:00",
        "Scope": "local",
        "Driver": "macvlan",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.16.244.0/24",
                    "Gateway": "172.16.244.2"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "e0dacda0431265dca039490ee526f8c34e8b780bf5e618327fcb6824b94d8787": {
                "Name": "test_private1",
                "EndpointID": "13fcac6bf83385ae80f7b6f2fe11207a0dad7ec862933bccfa87748af52686b1",
                "MacAddress": "02:42:ac:10:f4:e7",
                "IPv4Address": "172.16.244.231/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "macvlan_mode": "private",
            "parent": "eth0"
        },
        "Labels": {}
    }
]
4.6.1.2 VEPA模式

虚拟以太端口汇聚器(Virtual Ethernet Port Aggregator,简称 VEPA),在这种模式下, macvlan 内的容器不能直接接收在同一个物理网卡的容器的请求数据包,但是可以经过交换机的(端口回流)再转发回来可以实现通信。此模式下macvlan之间是隔离的,需要交换机支持hairpin功能才能通信。

  • 对端交换机接口IP 通
  • 同物理网卡的 macvlan 不通
  • 物理网卡 不通
image-20211104154856356
4.6.1.2.1 创建
docker network create -d macvlan --subnet=172.16.244.0/24 --gateway=172.16.244.2 -o parent=eth0 -o macvlan_mode=vepa macvlan_vepa

docker run -it --rm --net=macvlan_vepa --name=test_private1 --ip=172.16.244.231 alpine:latest sh

docker run -it --rm --net=macvlan_vepa --name=test_private2 --ip=172.16.244.232 alpine:latest sh
4.6.1.2.2 验证
root@docker3:~# docker network inspect macvlan_vepa
[
    {
        "Name": "macvlan_vepa",
        "Id": "1aeed2b3eb07ce27ce809d4bcd79b1aa8c2a9ad7c1c1a1a37d60b24a2762b659",
        "Created": "2021-11-04T15:58:04.875327507+08:00",
        "Scope": "local",
        "Driver": "macvlan",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.16.244.0/24",
                    "Gateway": "172.16.244.2"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "457be5b531e2f01a7ebccbbeb143a91472e8f9e2a2a2c100100a552dce8ead7f": {
                "Name": "test_private2",
                "EndpointID": "1d2eea9e9e1d763485b3ab811445346f333088fb73795501245d10cd6a01d704",
                "MacAddress": "02:42:ac:10:f4:e8",
                "IPv4Address": "172.16.244.232/24",
                "IPv6Address": ""
            },
            "6bb3d06e79303a029990aa939977a65305aad7ac84bb85607c5532aa3bda6b3a": {
                "Name": "test_private1",
                "EndpointID": "b49abc1ef3a9fd0c6e0e15e8b8a570fe6d53873cb014829878588b6c42fd9ccc",
                "MacAddress": "02:42:ac:10:f4:e7",
                "IPv4Address": "172.16.244.231/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "macvlan_mode": "vepa",
            "parent": "eth0"
        },
        "Labels": {}
    }
]
4.6.1.3 Passthru模式

Passthru 模式下该 macvlan 只能创建一个容器,当运行一个容器后再创建其他容器则会报错。

docker network create -d macvlan --subnet=172.16.244.0/24 --gateway=172.16.244.2 -o parent=eth0 -o macvlan_mode=passthru macvlan_passthru
docker run -it --rm --net=macvlan_passthru --name=test_private1 --ip=172.16.244.231 alpine:latest sh

image-20211104161441535

4.6.1.4 Bridge

在 bridge 这种模式下,使用同一个宿主机网络的 macvlan 容器可以直接通信

docker network create -d macvlan --subnet=172.16.244.0/24 --gateway=172.16.244.2 -o parent=eth0 -o macvlan_mode=bridge macvlan_bridge
  • Node1:
docker run -it --rm --net=macvlan_bridge --name=test_private1 --ip=172.16.244.231 alpine:latest sh

docker run -it --rm --net=macvlan_bridge --name=test_private2 --ip=172.16.244.232 alpine:latest sh
  • Node2:
docker network create -d macvlan --subnet=172.16.244.0/24 --gateway=172.16.244.2 -o parent=eth0 -o macvlan_mode=bridge macvlan_bridge
docker run -it --rm --net=macvlan_bridge --name=test_private3 --ip=172.16.244.233 alpine:latest sh
  • 同主机的容器通信:

image-20211104162846881

  • 跨主机通信:

image-20211104162914744

4.6.1.5 多macvlan配置

注意:对端交换机端口需要设置为trunk模式(如果是access模式)

4.6.1.5.1 添加网卡子接口
ifconfig eth0:1 172.16.244.210 netmask 255.255.255.0
4.6.1.5.2 添加新的macvlan
docker network create -d macvlan --subnet=172.16.244.0/24 --gateway=172.16.244.2 -o parent=eth0.1 -o macvlan_mode=bridge macvlan1_bridge

4.6.2 Overlay-Calico-BGP+IPIP

已被 github 等公司用于生产环境

两种模式:

  • 未开启IPIP
image-20211104100824130
  • 开启 IPIP
image-20211104100938741

通过tunnel隧道将各个Node的路由连接起来的模式。即将一个IP数据包套在另一个IP包里,使用Linux的隧道技术,将两个根本不通的网络点对点的连接起来。

Node内的Pod访问不会用到ipip隧道封装。

Pod的ip都是由calico-node设置的IP地址池进行分配的,docker0对kubernetes设置的PodIP地址将不再起作用。

官网:https://www.tigera.io/project-calico/

calico 是一个纯三层的解决方案,为容器提供多node间的访问通信,calico将每一个node节点都当作为一个路由,各节点通过BGP(border gateway protocal)边界网关协议学习并在node节点生成路由规则,从而将不同node节点上的pod连接起来进行通信。

4.6.2.1 简介

网络通过3层路由技术(如静态路由或BGP路由分配)或第2层地址学习来感知工作负载IP地址。因此,它们可以将未封装的流量路由到作为最终目的地的端点的正确主机。但是,并非所有网络都能够路由工作负载IP地址。例如公有云环境,跨VPC子网边界的AWS,以及无法通过BGP、calico对应到underlay网络,或无法轻松配置静态路由的其它场景,这就是为什么Calico支持封装,因此您可以在工作负载之间发送流量,而无需底层网络知道工作负载IP地址。

4.6.2.2 封装类型

calico支持两种类型的封装:vxlan和IP-in-IP,vxlan没有IP-in-IP在某些环境中受支持(例如Azure)。vxlan的每个数据包开销高,因为包头大。但除非您运行的是网络密集型工作负载,否则您通常不会注意到这种差异。这两种封装之间的另一个小差异是calico的vxlan实现不使用BGP,calico的IP-in-IP是在calico节点之间使用BGP协议实现跨子网通信。

4.6.2.3 BGP

BGP是一个去中心化的协议,它通过自动学习和维护路由表实现网络的可用性,但并不是所有的网络都支持BGP,另外为了跨网络实现更大规模的网络管理,calico还支持IP-in-IP的叠加模型,简称IPIP,IPIP可以实现不同网段建立路由通信,但是会存在安全性问题,其在内核内置,可以通过calico的配置文件,设置是否开开启IPIP,在公司内部如果k8s的node节点没有跨网段,建议关闭IPIP。

IPIP是一种将各node的路由之间做一个tunnel,再把两个网络连接起来的模式,启用IPIP模式时,calico将在各node上创建一个名为tunl0的虚拟网络接口。

BGP模式则直接使用物理机作为虚拟路由(vrouter),不再创建额外的tunnel。

calico+IPIP(130-140QPS左右,5%左右的损耗,如果要提升QPS,可以将入口服务开多副本)

image-20211114161140339

calico+关闭IPIP(150-160QPS左右)

image-20211114160754437

4.6.2.4 Calico的核心组件

Felix:calico的agent,运行在每一台node节点上,其主要时维护路由规则、汇报当前节点状态以确保pod的跨主机通信。

BGP client:每台node都运行,其主要负责监听ndoe节点上有felix生成的路由信息,然后通过BGP协议广播至其它剩余的node节点,从而互相学习路由实现pod通信。

Route Relector:集中式路由反射器,calico v3.3 开始支持,当calico BGP客户端将路由从FIB(forward information database,转发信息库)通告到route relector时,route relector会将这些路由信息通告给集群中的其它节点,router relector专门用于管理BGP网络的路由规则,不会产生pod数据通信。

注:calico默认的工作模式时BGP的node-to-node mesh。如果要使用route reflector需要进行相关配置。

https://docs.projectcalico.org/networking/bgp

Calico nodes can be configured to act as route reflectors. To do this, each node that you want to act as a route reflector must have a cluster ID - typically an unused IPv4 address.

To configure a node to be a route reflector with cluster ID 244.0.0.1, run the following command.

calicoctl patch node my-node -p '{"spec": {"bgp": {"routeReflectorClusterID": "244.0.0.1"}}}'

Typically, you will want to label this node to indicate that it is a route reflector, allowing it to be easily selected by a BGPPeer resource. You can do this with kubectl. For example:

kubectl label node my-node route-reflector=true

Now it is easy to configure route reflector nodes to peer with each other and other non-route-reflector nodes using label selectors. For example:

kind: BGPPeer
apiVersion: projectcalico.org/v3
metadata:
  name: peer-with-route-reflectors
spec:
  nodeSelector: all()
  peerSelector: route-reflector == 'true'
image-20211114162414887
4.6.2.5 部署

部署:https://docs.projectcalico.org/getting-started/kubernetes/

模式选择:https://docs.projectcalico.org/networking/determine-best-networking

4.6.2.6 验证当前路由表
root@node1:~# calicoctl node status
Calico process is running.

IPv4 BGP status
+----------------+-------------------+-------+----------+-------------+
|  PEER ADDRESS  |     PEER TYPE     | STATE |  SINCE   |    INFO     |
+----------------+-------------------+-------+----------+-------------+
| 172.16.244.101 | node-to-node mesh | up    | 07:39:06 | Established |
| 172.16.244.102 | node-to-node mesh | up    | 07:39:06 | Established |
| 172.16.244.103 | node-to-node mesh | up    | 07:39:06 | Established |
| 172.16.244.112 | node-to-node mesh | up    | 07:39:10 | Established |
| 172.16.244.113 | node-to-node mesh | up    | 07:39:20 | Established |
+----------------+-------------------+-------+----------+-------------+

IPv6 BGP status
No IPv6 peers found.
4.6.2.7 Node上的路由表
  • 打开IPIP模式:
root@node1:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.16.244.2    0.0.0.0         UG    0      0        0 eth0
10.200.67.0     172.16.244.102  255.255.255.192 UG    0      0        0 tunl0
10.200.99.64    172.16.244.112  255.255.255.192 UG    0      0        0 tunl0
10.200.147.192  172.16.244.113  255.255.255.192 UG    0      0        0 tunl0
10.200.187.192  172.16.244.103  255.255.255.192 UG    0      0        0 tunl0
10.200.213.128  172.16.244.101  255.255.255.192 UG    0      0        0 tunl0
10.200.255.128  0.0.0.0         255.255.255.192 U     0      0        0 *
10.200.255.150  0.0.0.0         255.255.255.255 UH    0      0        0 calif8fa4face0c
10.200.255.151  0.0.0.0         255.255.255.255 UH    0      0        0 cali3236cc47f9c
10.200.255.153  0.0.0.0         255.255.255.255 UH    0      0        0 cali696cf7e75d8
10.200.255.154  0.0.0.0         255.255.255.255 UH    0      0        0 cali9f0d961fc03
10.200.255.157  0.0.0.0         255.255.255.255 UH    0      0        0 calie738407d56e
10.200.255.160  0.0.0.0         255.255.255.255 UH    0      0        0 calif262c63a722
172.16.244.0    0.0.0.0         255.255.255.0   U     0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
  • 关闭IPIP(不能跨子网)

image-20211114164027346

4.6.3 Overlay-flannel

文档:https://docs.openshift.com/container-platform/3.4/architecture/additional_concepts/flannel.html

GitHub: https://github.com/flannel-io/flannel

由CoreOS开源的针对k8s的网络服务,主要解决集群内各主机上的pod互通问题,其借助etcd维护网络IP的分配,并为每个node服务器分配一个不同的IP地址段。flannel只支持网络通信,不支持网络策略。

4.6.3.1 Flannel的网络模型
  • UDP:早期版本,使用udp封装报文,实现跨主机转发,安全性和性能略差
  • Vxlan:使用Vxlan实现基于3层网络的虚拟2层网络,目前flannel已经是基于vxlan网络,推荐使用vxlan作为网络模型。vxlan有隧道模式和directrouting模式,directrouting模式与Host-gw模式相似,一样无法跨网段访问,跨主机间通信时,容器内流量直接通过物理网卡的网关路由转发,不通过flannel叠加,从而提高vxlan性能,但该方式使用较少,一般这种场景下可以直接使用Host-gw后端模型,性能更好。
  • Host-gw:通过在node节点上创建到达各目标容器地址的路由表而完成报文转发,因此这种方式要求各node节点本身必须处于同一局域网(二层网络中),因此不适用于网络频繁变动或比较大型的网络环境,但其性能较好。
4.6.3.2 Flannel组件解释
  • cni0: 网桥设备,每创建一个pod都会创建一对veth pair,其中一端是pod中的eth0,另一端是cni0网校中的端口(网卡),pod中从网卡eth0发出的流量,都会发送到cni0网桥设备的端口(网卡)上,cni0设备获得的IP地址是该节点分配到的网段的第一个地址。
  • flannel.1:overlay网络的设备,用来进行vxlan报文的处理(封包和解包),不同node之间的pod数据流量都从overlay设备以隧道的形式发送到对端。
image-20211104180323081 image-20211114160849093
4.6.3.3 Flannel的系统文件及目录

/run/flannel

/usr/bin/flannel 或 /usr/local/bin/flannel

/var/lib/cni/flannel #目录下有当前节点上的每个容器的网络信息

/var/run/flannel/subnet.env #当前节点获得的地址段

4.6.3.4 Flannel的跨节点通信
  • vxlan overlay模式:
image-20211104223104100
  1. pod中产生数据,根据pod的路由信息,将数据发送到cni0
  2. cni0根据节点的路由表,将数据发送到隧道设备flannel.1
  3. flannel.1查看数据包的目的IP,从flanneld获得对端隧道设备的必要信息,封装数据包。
  4. Flannel.1将数据包发送到对端设备,对端节点的网卡接收到数据包。
  5. 对端节点发现数据包为overlay数据包,解开外层封装,并发送到本机的flannel.1设备。
  6. cni0匹配路由表,发送数据给网桥上对应的端口(pod)。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值