当前位置: 首页 > news >正文

K8s安全管理与持久化存储实战指南

目录

一、k8s安全管理:认证、授权、准入控制概述

1.1 认证

1.2 授权

二、ServiceAccount介绍

三、RBAC认证授权策略

3.1 Role:角色

3.2 ClusterRole:集群角色

3.3 RoleBinding:角色绑定、ClusterRolebinding:集群角色绑定

四、资源的引用方式

五、常见角色示例

六、常见的角色绑定示例

七、对Service Account的授权管理

八、使用kubectl命令行工具创建资源对象

九、限制不同的用户操作k8s集群(重点)

1、k8s持久化存储:emptyDir

2、k8s持久化存储:hostPath

3、k8s持久化存储:nfs

4、k8s持久化存储: PVC

4.1.1  k8s PV是什么?

4.1.2  k8s PVC是什么?

4.1.3  k8s PVC和PV工作原理

4.1.4  创建pod,使用pvc作为持久化存储卷

5、 k8s存储类:storageclass

5.1 概述

StorageClass运行原理

5.2 安装nfs provisioner,用于配合存储类动态生成pv

5.3 创建storageclass,动态供给pv

5.4  创建pvc,通过storageclass动态生成pv

5.5  创建pod,挂载storageclass动态生成的pvc:storage-pvc


一、k8s安全管理:认证、授权、准入控制概述

k8s对我们整个系统的认证,授权,访问控制做了精密的设置;对于k8s集群来说,apiserver是整个集群访问控制的唯一入口,我们在k8s集群之上部署应用程序的时候,也可以通过宿主机的NodePort暴露的端口访问里面的程序,用户访问kubernetes集群需要经历如下认证过程:

认证->授权->准入控制(admination controller)

1.认证(Authenticating)是对客户端的认证,通俗点就是用户名密码验证

2.授权(Authorization)是对资源的授权,k8s中的资源无非是容器,最终其实就是容器的计算,网络,存储资源,当一个请求经过认证后,需要访问某一个资源(比如创建一个pod),授权检查会根据授权规则判定该资源(比如某namespace下的pod)是否是该客户可访问的。

3.准入(Admission Control)机制:

准入控制器(Admission Controller)位于 API Server中,在对象被持久化之前,准入控制器拦截对 API Server 的请求,一般用来做身份验证和授权。其中包含两个特殊的控制器:

Mutating Admission Webhook 和 Validating Admission Webhook。分别作为配置的变更和验证准入控制 webhook。

变更(Mutating)准入控制:修改请求的对象

验证(Validating)准入控制:验证请求的对象

准入控制器是在 API Server 的启动参数配置的。一个准入控制器可能属于以上两者中的一种,也可能两者都属于。当请求到达 API Server 的时候首先执行变更准入控制,然后再执行验证准入控制。

我们在部署 Kubernetes 集群的时候都会默认开启一系列准入控制器,如果没有设置这些准入控制器的话可以说你的 Kubernetes 集群就是在裸奔,应该只有集群管理员可以修改集群的准入控制器。

例如:会默认开启如下的准入控制器。

--admission-control=ServiceAccount,NamespaceLifecycle,NamespaceExists,LimitRanger,ResourceQuota,MutatingAdmissionWebhook,ValidatingAdmissionWebhook

k8s的整体架构也是一个微服务的架构,所有的请求都是通过一个GateWay,也就是kube-apiserver这个组件(对外提供REST服务),k8s中客户端有两类,一种是普通用户,一种是集群内的Pod,这两种客户端的认证机制略有不同,但无论是哪一种,都需要依次经过认证,授权,准入这三个机制。

1.1 认证

1、认证支持多种插件

(1)令牌(token)认证:

双方有一个共享密钥,服务器上来先创建一个密码下来,客户端登陆的时候拿这个密码登陆即可,这个就是对称密钥认证方式;k8s提供了一个restful风格的接口,它的所有服务都是通过http协议提供的,因此认证信息只能经由http协议的认证首部进行传递,这种认证首部进行传递通常叫做令牌;

(2)ssl认证:

对于k8s访问来讲,ssl认证能让客户端确认服务器的认证身份,我们在跟服务器通信的时候,需要服务器发过来一个证书,我们需要确认这个证书是不是ca签署的,如果是我们认可的ca签署的,里面的subj信息与我们访问的目标主机信息保持一致,没有问题,那么我们就认为服务器的身份得到认证了,k8s中最重要的是服务器还需要认证客户端的信息,kubectl也应该有一个证书,这个证书也是server所认可的ca签署的证书,双方需要互相认证,实现加密通信,这就是ssl认证。

2、kubernetes上的账号

客户端对apiserver发起请求,apiserver要识别这个用户是否有请求的权限,要识别用户本身能否通过apiserver执行相应的操作,那么需要哪些信息才能识别用户信息来完成对用户的相关的访问控制呢?

kubectl explain pods.spec可以看到有一个字段serviceAccountName(服务账号名称),这个就是我们pod连接apiserver时使用的账号,因此整个kubernetes集群中的账号有两类,ServiceAccount(服务账号),User account(用户账号)

User account:实实在在现实中的人,人可以登陆的账号,客户端想要对apiserver发起请求,apiserver要识别这个客户端是否有请求的权限,那么不同的用户就会有不同的权限,靠用户账号表示,叫做username

ServiceAccount:方便Pod里面的进程调用Kubernetes API或其他外部服务而设计的,是kubernetes中的一种资源

sa账号:登陆dashboard使用的账号

user account:这个是登陆k8s物理机器的用户(系统用户)

1.ServiceAccount

Service account是为了方便Pod里面的进程调用Kubernetes API或其他外部服务而设计的。它与User account不同,User account是为人设计的,而service account则是为Pod中的进程调用Kubernetes API而设计;User account是跨namespace的,而service account则是仅局限它所在的namespace;每个namespace都会自动创建一个default service account;

开启ServiceAccount Admission Controller后

1)每个Pod在创建后都会自动设置spec.serviceAccount为default(除非指定了其他ServiceAccout)

2)验证Pod引用的service account已经存在,否则拒绝创建;

当创建 pod 的时候,如果没有指定一个 serviceaccount,系统会自动在与该pod 相同的 namespace 下为其指派一个default service account。这是pod和apiserver之间进行通信的账号,如下:

[root@k8s-master01 ~]# kubectl get pods
NAME                READY  STATUS    RESTARTS  AGE
mysql-pod-volume          1/1   Running   0      43m
nfs-provisioner-cd5589cfc-c8vc5   1/1   Running   0      13h
pod-secret              1/1   Running   0      18m
web-0                1/1   Running   0      13h
web-1                1/1   Running   0      13h[root@k8s-master01 ~]# kubectl get pods web-0 -o yaml | grep "serviceAccountName"serviceAccountName: default[root@k8s-master01 ~]# kubectl get sa
NAME        SECRETS  AGE
default      0       12d

默认的service account 仅仅只能获取当前Pod自身的相关属性,无法观察到其他名称空间Pod的相关属性信息。

创建一个serviceaccount:

[root@k8s-master ~]# kubectl create serviceaccount test
[root@k8s-master ~]# kubectl describe sa test  #查看test这个账号的详细信息 
[root@k8s-master ~]# kubectl describe sa test
Name:                test
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   <none>
Tokens:              <none>
Events:              <none>

2、kubeconfig文件

在K8S集群当中,每一个用户对资源的访问都是需要通过apiserver进行通信认证才能进行访问的,那么在此机制当中,对资源的访问可以是token,也可以是通过配置文件的方式进行保存和使用认证信息,可以通过kubectl config进行查看配置,如下:

[root@k8s-master ~]#  kubectl config view 
apiVersion: v1
clusters:
- cluster:certificate-authority-data: DATA+OMITTEDserver: https://192.168.40.199:6443   #apiserver的地址name: kubernetes             #集群的名字
contexts:
- context:cluster: kubernetes           user: kubernetes-admin  name: kubernetes-admin@kubernetes   #上下文的名字
current-context: kubernetes-admin@kubernetes    #当前上下文的名字
kind: Config
preferences: {}
users:
- name: kubernetes-adminuser:client-certificate-data: REDACTEDclient-key-data: REDACTED

查看当前管理集群的账户:

kubectl auth whoami

在上面的配置文件当中,定义了集群、上下文以及用户。其中Config也是K8S的标准资源之一,在该配置文件当中定义了一个集群列表,指定的集群可以有多个;用户列表也可以有多个,指明集群中的用户;而在上下文列表当中,是进行定义可以使用哪个用户对哪个集群进行访问,以及当前使用的上下文是什么。

1.2 授权

如果用户通过认证,什么权限都没有,需要一些后续的授权操作,如对资源的增删该查等,kubernetes1.6之后开始有RBAC(基于角色的访问控制机制)授权检查机制。(role basic access control)

Kubernetes的授权是基于插件形成的,其常用的授权插件有以下几种:

  1. Node(节点认证)

  2. ABAC(基于属性的访问控制)

  3. RBAC(基于角色的访问控制)(Role Basic Access Control)

  4. Webhook(基于http回调机制的访问控制)

什么是RBAC(基于角色的访问控制)?

让一个用户(Users)扮演一个角色(Role),角色拥有权限,从而让用户拥有这样的权限,随后在授权机制当中,只需要将权限授予某个角色,此时用户将获取对应角色的权限,从而实现角色的访问控制。如图:

在k8s的授权机制当中,采用RBAC的方式进行授权,其工作逻辑是,把对对象的操作权限定义到一个角色当中,再将用户绑定到该角色,从而使用户得到对应角色的权限。

如果通过rolebinding绑定role,只能对rolebingding所在的名称空间的资源有权限,上图user1这个用户绑定到role1上,只对role1这个名称空间的资源有权限,对其他名称空间资源没有权限,属于名称空间级别的;

另外,k8s为此还有一种集群级别的授权机制,就是定义一个集群角色(ClusterRole),对集群内的所有资源都有可操作的权限,从而将User2通过ClusterRoleBinding到ClusterRole,从而使User2拥有集群的操作权限。

Role、RoleBinding、ClusterRole和ClusterRoleBinding的关系如下图:

通过上图可以看到,可以通过rolebinding绑定role,rolebinding绑定clusterrole,clusterrolebinding绑定clusterrole。

上面我们说了两个角色绑定:

(1)用户通过rolebinding绑定role

(2)用户通过clusterrolebinding绑定clusterrole

还有一种:rolebinding绑定clusterrole

rolebinding绑定clusterrole的好处:

假如有6个名称空间,每个名称空间的用户都需要对自己的名称空间有管理员权限,那么需要定义6个role和rolebinding,然后依次绑定,如果名称空间更多,我们需要定义更多的role,这个是很麻烦的,所以我们引入clusterrole,定义一个clusterrole,对clusterrole授予所有权限,然后用户通过rolebinding绑定到clusterrole,就会拥有自己名称空间的管理员权限了

注:RoleBinding仅仅对当前名称空间有对应的权限。

二、ServiceAccount介绍

kubernetes中账户区分为:User Accounts(用户账户) 和 Service Accounts(服务账户) 两种:

UserAccount是给kubernetes集群外部用户使用的,例如运维或者集群管理人员,,kubeadm安装的k8s,默认用户账号是kubernetes-admin;

 k8s客户端(一般用:kubectl) ------>API Server

  APIServer需要对客户端做认证,使用kubeadm安装的K8s,会在用户家目录下创建一个认证配置文件 .kube/config 这里面保存了客户端访问API Server的密钥相关信息,这样当用kubectl访问k8s时,它就会自动读取该配置文件,向API Server发起认证,然后完成操作请求。

用户名称可以在kubeconfig中查看

[root@k8s-master01~]# cd ~/.kube/
[root@k8s-master01 .kube]# ls
cache config http-cache
[root@k8s-master01 .kube]# cat config
...
users:
name: kubernetes-admin
...

ServiceAccount是Pod使用的账号,Pod容器的进程需要访问API Server时用的就是ServiceAccount账户;ServiceAccount仅局限它所在的namespace,每个namespace创建时都会自动创建一个default service account;创建Pod时,如果没有指定Service Account,Pod则会使用default Service Account。

三、RBAC认证授权策略

RBAC介绍

在Kubernetes中,所有资源对象都是通过API进行操作,他们保存在etcd里。而对etcd的操作我们需要通过访问 kube-apiserver 来实现,上面的Service Account其实就是APIServer的认证过程,而授权的机制是通过RBAC:基于角色的访问控制实现。

RBAC有四个资源对象,分别是Role、ClusterRole、RoleBinding、ClusterRoleBinding

3.1 Role:角色

一组权限的集合,在一个命名空间中,可以用其来定义一个角色,只能对命名空间内的资源进行授权。如果是集群级别的资源,则需要使用ClusterRole。例如:定义一个角色用来读取Pod的权限

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:namespace: rbacname: pod-read
rules:
- apiGroups: [""]resources: ["pods"]resourceNames: []
verbs: ["get","watch","list"]

rules中的参数说明:

  • apiGroups:支持的API组列表,例如:"apiVersion: batch/v1"等

  • resources:支持的资源对象列表,例如pods、deplayments、jobs等

  • resourceNames: 指定resource的名称

  • verbs:对资源对象的操作方法列表。

3.2 ClusterRole:集群角色

具有和角色一致的命名空间资源的管理能力,还可用于以下特殊元素的授权

  • 集群范围的资源,例如Node

  • 非资源型的路径,例如:/healthz

  • 包含全部命名空间的资源,例如Pods

例如:定义一个集群角色可让用户访问任意secrets

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:name: secrets-clusterrole
rules:
- apiGroups: [""]resources: ["secrets"]verbs: ["get","watch","list"]

3.3 RoleBinding:角色绑定、ClusterRolebinding:集群角色绑定

角色绑定和集群角色绑定用于把一个角色绑定在一个目标上,可以是User,Group,Service Account,使用RoleBinding为某个命名空间授权,使用ClusterRoleBinding为集群范围内授权。

例如:将在rbac命名空间中把pod-read角色授予用户es

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:name: pod-read-bindnamespace: rbac
subjects:
- kind: Username: esapiGroup: rbac.authorization.k8s.io
roleRef:kind: Rolename: pod-readapiGroup: rbac.authorization.k8s.io

RoleBinding也可以引用ClusterRole,对属于同一命名空间内的ClusterRole定义的资源主体进行授权, 例如:es能获取到集群中所有的资源信息

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:name: es-allresourcenamespace: rbac
subjects:
- kind: Username: esapiGroup: rbac.authorization.k8s.io
roleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: cluster-admin

集群角色绑定的角色只能是集群角色,用于进行集群级别或对所有命名空间都生效的授权

例如:允许manager组的用户读取所有namaspace的secrets

apiVersion: rabc.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:name: read-secret-global
subjects:
- kind: Groupname: managerapiGroup: rabc.authorization.k8s.io
roleRef:kind: ClusterRolename: secret-readapiGroup: rabc.authorization.k8s.io

四、资源的引用方式

多数资源可以用其名称的字符串表示,也就是Endpoint中的URL相对路径,例如pod中的日志是GET /api/v1/namaspaces/{namespace}/pods/{podname}/log

如果需要在一个RBAC对象中体现上下级资源,就需要使用“/”分割资源和下级资源。

例如:若想授权让某个主体同时能够读取Pod和Pod log,则可以配置 resources为一个数组

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:name: logs-readernamespace: default
rules:
- apiGroups: [""]resources: ["pods","pods/log"]verbs: ["get","list"]

资源还可以通过名称(ResourceName)进行引用,在指定ResourceName后,使用get、delete、update、patch请求,就会被限制在这个资源实例范围内。

例如,下面的声明让一个主体只能对名为my-configmap的Configmap进行get和update操作:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:namespace: defaultname: configmap-update
rules:
- apiGroups: [""]resources: ["configmap"]resourceNames: ["my-configmap"]verbs: ["get","update"]

查看资源信息的命令:

[root@k8s-master01 ~]# kubectl api-resources 

五、常见角色示例

(1)允许读取核心API组的Pod资源

rules:
- apiGroups: [""]resources: ["pods"]verbs: ["get","list","watch"]

(2)允许读写extensions和apps两个API组中的deployment资源

rules:
- apiGroups: ["extensions","apps"]resources: ["deployments"]verbs: ["get","list","watch","create","update","patch","delete"]

(3)允许读取Pod以及读写job信息

rules:
- apiGroups: [""]resources: ["pods"]verbs: ["get","list","watch"]、
- apiGroups: ["batch","extensions"]resources: ["jobs"]verbs: ["get","list","watch","create","update","patch","delete"]

(4)允许读取一个名为my-config的ConfigMap(必须绑定到一个RoleBinding来限制到一个Namespace下的ConfigMap):

rules:apiGroups: [""]resources: ["configmap"]resourceNames: ["my-configmap"]verbs: ["get"]

(5)读取核心组的Node资源(Node属于集群级的资源,所以必须存在于ClusterRole中,并使用ClusterRoleBinding进行绑定):

rules:apiGroups: [""]resources: ["nodes"]verbs: ["get","list","watch"]

(6)允许对非资源端点“/healthz”及其所有子路径进行GET和POST操作(必须使用ClusterRole和ClusterRoleBinding):

rules:nonResourceURLs: ["/healthz","/healthz/*"]verbs: ["get","post"]

六、常见的角色绑定示例

(1)用户名alice

subjects:
- kind: Username: aliceapiGroup: rbac.authorization.k8s.io 

(2)组名alice

subjects:
- kind: Groupname: aliceapiGroup: rbac.authorization.k8s.io

(3)kube-system命名空间中默认Service Account

subjects:
kind: ServiceAccountname: defaultnamespace: kube-system

(4)qa命名空间中的所有Service Account:

subjects:
kind: Groupname: system:serviceaccounts:qaapiGroup: rbac.authorization.k8s.io

(5)所有Service Account

subjects:
kind: Groupname: system:serviceaccountsapiGroup: rbac.authorization.k8s.io

(6)所有认证用户

subjects:
kind: Groupname: system:authenticatedapiGroup: rbac.authorization.k8s.io

(7)所有未认证用户

subjects:
kind: Groupname: system:unauthenticatedapiGroup: rbac.authorization.k8s.io  

(8)全部用户

subjects:
kind: Groupname: system:authenticatedapiGroup: rbac.authorization.k8s.io
kind: Groupname: system:unauthenticatedapiGroup: rbac.authorization.k8s.io

七、对Service Account的授权管理

pod内可获取rbac命名空间的所有Pod资源,pod-reader-sc的Service Account是绑定了名为pod-read的Role。

apiVersion: v1
kind: Pod
metadata:name: nginxnamespace: rbac
spec:serviceAccountName: pod-reader-sccontainers:name: nginximage: nginximagePullPolicy: IfNotPresentports:containerPort: 80

默认的RBAC策略为控制平台组件、节点和控制器授予有限范围的权限,但是除kube-system外的Service Account是没有任何权限的。

(1)为一个应用专属的Service Account赋权

此应用需要在Pod的spec中指定一个serviceAccountName,用于API、Application Manifest、kubectl create serviceaccount等创建Service Account的命令。

例如为my-namespace中的my-sa Service Account授予只读权限

kubectl create rolebinding my-sa-view --clusterrole=view --serviceaccount=my-namespace:my-sa --namespace=my-namespace

(2)为一个命名空间中名为default的Service Account授权

如果一个应用没有指定 serviceAccountName,则会使用名为default的Service Account。注意,赋予Service Account “default”的权限会让所有没有指定serviceAccountName的Pod都具有这些权限

例如,在my-namespace命名空间中为Service Account“default”授予只读权限:

kubectl create rolebinding default-view --clusterrole=view --serviceaccount=my-namespace:default --namespace=my-namespace

另外,许多系统级Add-Ons都需要在kube-system命名空间中运行,要让这些Add-Ons能够使用超级用户权限,则可以把cluster-admin权限赋予kube-system命名空间中名为default的Service Account,这一操作意味着kube-system命名空间包含了通向API超级用户的捷径。

kubectl create clusterrolebinding add-ons-add-admin --clusterrole=cluster-admin --serviceaccount=kube-system:default 

(3)为命名空间中所有Service Account都授予一个角色

如果希望在一个命名空间中,任何Service Account应用都具有一个角色,则可以为这一命名空间的Service Account群组进行授权

kubectl create rolebinding serviceaccounts-view --clusterrole=view --group=system:serviceaccounts:my-namespace --namespace=my-namespace

(4)为集群范围内所有Service Account都授予一个低权限角色

如果不想为每个命名空间管理授权,则可以把一个集群级别的角色赋给所有Service Account。

kubectl create clusterrolebinding serviceaccounts-view --clusterrole=view --group=system:serviceaccounts

(5)为所有Service Account授予超级用户权限

kubectl create clusterrolebinding serviceaccounts-view --clusterrole=cluster-admin --group=system:serviceaccounts

八、使用kubectl命令行工具创建资源对象

(1)在命名空间rbac中为用户es授权admin ClusterRole:

kubectl create rolebinding bob-admin-binding --clusterrole=admin --user=es --namespace=rbac

(2)在命名空间rbac中为名为myapp的Service Account授予view ClusterRole:

kubctl create rolebinding myapp-role-binding --clusterrole=view --serviceaccount=rbac:myapp --namespace=rbac 

(3)在全集群范围内为用户root授予cluster-admin ClusterRole:

kubectl create clusterrolebinding cluster-binding --clusterrole=cluster-admin --user=root 

(4)在全集群范围内为名为myapp的Service Account授予view ClusterRole:

kubectl create clusterrolebinding service-account-binding --clusterrole=view --serviceaccount=myapp

yaml文件进行rbac授权:使用 RBAC 鉴权 | Kubernetes

九、限制不同的用户操作k8s集群(重点)

ssl认证

生成一个证书

(1)生成一个私钥

cd /etc/kubernetes/pki/
umask 077; openssl genrsa -out lucky.key 2048

(2)生成一个证书请求

openssl req -new -key lucky.key -out lucky.csr -subj "/CN=lucky"

(3)生成一个证书

openssl x509 -req -in lucky.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out lucky.crt -days 3650

在kubeconfig下新增加一个lucky这个用户

(1)把lucky这个用户添加到kubernetes集群中,可以用来认证apiserver的连接

[root@xianchaomaster1 pki]# kubectl config set-credentials lucky --client-certificate=./lucky.crt --client-key=./lucky.key --embed-certs=true  

(2)在kubeconfig下新增加一个lucky这个账号

kubectl config set-context lucky@kubernetes --cluster=kubernetes --user=lucky

(3)切换账号到lucky,默认没有任何权限

[root@k8s-master01 pki]# kubectl config use-context lucky@kubernetes
[root@k8s-master01 pki]# kubectl get pod
Error from server (Forbidden): pods is forbidden: User "lucky" cannot list resource "pods" in API group "" in the namespace "default"
###切换到有权限的管理账户
[root@k8s-master01 pki]# kubectl config use-context kubernetes-admin@kubernetes 

这个是集群用户,有任何权限;把lucky这个用户通过rolebinding绑定到clusterrole上,授予权限,权限只是在lucky这个名称空间有效

(1)把lucky这个用户通过rolebinding绑定到clusterrole上

[root@k8s-master01 pki]# kubectl create ns lucky
[root@k8s-master01 pki]# kubectl create rolebinding lucky -n lucky --clusterrole=cluster-admin --user=lucky

(2)切换到lucky这个用户

[root@k8s-master01 pki]# kubectl config use-context lucky@kubernetes

(3)测试是否有权限

kubectl get pods -n lucky
[root@k8s-master01 pki]# kubectl get pod -n lucky
No resources found in lucky namespace.
[root@k8s-master01 pki]# kubectl get sa -n lucky 
NAME      SECRETS   AGE
default   0         2m10s
#有权限操作这个名称空间
kubectl get pods[root@k8s-master01 pki]# kubectl get pod
Error from server (Forbidden): pods is forbidden: User "lucky" cannot list resource "pods" in API group "" in the namespace "default"#没有权限操作其他名称空间 

添加一个lucky的普通用户

useradd lucky
cp -ar /root/.kube/ /home/lucky/
chown -R lucky.lucky /home/lucky/
su - lucky
vim .kube/config
.......server: https://192.168.158.15:6443name: kubernetes
contexts:
- context:cluster: kubernetesuser: luckyname: lucky@kubernetes
current-context: lucky@kubernetes
....
su - root 
chattr +i /home/lucky/.kube/config
exit
kubectl get pods -n lucky

验证创建pod:

[lucky@k8s-master ~]$ cat nginx.txt 
apiVersion: v1
kind: Pod
metadata:name: nginx-pod
spec:containers:- name: nginximage: nginximagePullPolicy: IfNotPresent
[lucky@k8s-master ~]$ kubectl apply -f nginx.txt 
##发现报错
Error from server (Forbidden): error when retrieving current configuration of:
Resource: "/v1, Resource=pods", GroupVersionKind: "/v1, Kind=Pod"
Name: "nginx-pod", Namespace: "default"
from server for: "nginx.txt": pods "nginx-pod" is forbidden: User "lucky" cannot get resource "pods" in API group "" in the namespace "default"[lucky@k8s-master ~]$ cat nginx.txt 
apiVersion: v1
kind: Pod
metadata:name: nginx-podnamespace: lucky
spec:containers:- name: nginximage: nginximagePullPolicy: IfNotPresent
[lucky@k8s-master ~]$ kubectl apply -f nginx.txt 
##发现成功创建
pod/nginx-pod created
[lucky@k8s-master ~]$ kubectl -n lucky get po
NAME        READY   STATUS    RESTARTS   AGE
nginx-pod   1/1     Running   0          28s

在k8s中为什么要做持久化存储?

在k8s中部署的应用都是以pod容器的形式运行的,假如我们部署MySQL、Redis等数据库,需要对这些数据库产生的数据做备份。因为Pod是有生命周期的,如果pod不挂载数据卷,那pod被删除或重启后这些数据会随之消失,如果想要长久的保留这些数据就要用到pod数据持久化存储。

1、k8s持久化存储:emptyDir

查看k8s支持哪些存储

[root@k8s-master](mailto:root@k8s-master) ~]# kubectl explain pods.spec.volumes
FIELDS:awsElasticBlockStore  <Object>azureDisk <Object>azureFile <Object>cephfs   <Object>cinder    <Object>configMap<Object>csi   <Object>downwardAPI   <Object>emptyDir <Object>ephemeral <Object>fc    <Object>flexVolume    <Object>flocker   <Object>gcePersistentDisk <Object>gitRepo   <Object>glusterfs<Object>hostPath <Object>iscsi <Object>name  <string> -required-nfs  <Object>persistentVolumeClaim<Object>photonPersistentDisk  <Object>portworxVolume    <Object>projected <Object>quobyte   <Object>rbd   <Object>scaleIO   <Object>secret   <Object>storageos <Object>vsphereVolume <Object>

常用的如下

emptyDir
hostPath
nfs
persistentVolumeClaim
glusterfs
cephfs
configMap
secret

我们想要使用存储卷,需要经历如下步骤

1、定义pod的volume,这个volume指明它要关联到哪个存储上的

2、在容器中要使用volumemounts挂载对应的存储

经过以上两步才能正确的使用存储卷

emptyDir类型的Volume是在Pod分配到Node上时被创建,Kubernetes会在Node上自动分配一个目录,因此无需指定宿主机Node上对应的目录文件。 这个目录的初始内容为空,当Pod从Node上移除时,emptyDir中的数据会被永久删除。emptyDir Volume主要用于某些应用程序无需永久保存的临时目录,多个容器的共享目录等。

创建一个pod,挂载临时目录emptyDir

Emptydir的官方网址:

Volumes | Kubernetes

[[root@k8s-master](mailto:root@k8s-master) ~]# cat emptydir.yaml
​
apiVersion: v1
kind: Pod
metadata:name: pod-empty
spec:containers:- name: container-emptyimage: nginximagePullPolicy: IfNotPresentvolumeMounts:- mountPath: /cachename: cache-volume  ##与volumes中的name保持一致volumes:- emptyDir: {}name: cache-volume

更新资源清单文件

[[root@k8s-master](mailto:root@k8s-master) ~]# kubectl apply -f emptydir.yaml
​
pod/pod-empty created

查看本机临时目录存在的位置,可用如下方法:

查看pod调度到哪个节点

[root@k8s-master ~]# kubectl get  pod pod-empty -o wide
NAME        READY   STATUS    RESTARTS   AGE   IP              NODE           NOMINATED NODE   READINESS GATES
pod-empty   1/1     Running   0          27s   172.16.69.230   k8s-node2   <none>           <none>

查看pod的uid

[root@k8s-master ~]# kubectl get  pod pod-empty -o yaml | grep uiduid: 45a10614-495b-4745-be0f-c7492b90e2b7

登录到k8s-node2上

[root@k8s-node2 ~]# tree /var/lib/kubelet/pods/45a10614-495b-4745-be0f-c7492b90e2b7/
/var/lib/kubelet/pods/45a10614-495b-4745-be0f-c7492b90e2b7/
├── containers
│   └── container-empty
│       └── 43f2b9b9
├── etc-hosts
├── plugins
│   └── kubernetes.io~empty-dir
│       ├── cache-volume
│       │   └── ready
│       └── wrapped_kube-api-access-njjrv
│           └── ready
└── volumes├── kubernetes.io~empty-dir│   └── cache-volume└── kubernetes.io~projected└── kube-api-access-njjrv├── ca.crt -> ..data/ca.crt├── namespace -> ..data/namespace└── token -> ..data/token
​
11 directories, 7 files

测试

###模拟产生数据测试
root@pod-empty:/cache# touch file{1..10}
root@pod-empty:/cache# ls
10}file{1.  aaa  file1  file10  file2  file3  file4  file5  file6  file7  file8  file9
root@pod-empty:/cache# exit
exit
​
###node查看
[root@k8s-node2 ~]# tree /var/lib/kubelet/pods/45a10614-495b-4745-be0f-c7492b90e2b7/
/var/lib/kubelet/pods/45a10614-495b-4745-be0f-c7492b90e2b7/
├── containers
│   └── container-empty
│       └── 43f2b9b9
├── etc-hosts
├── plugins
│   └── kubernetes.io~empty-dir
│       ├── cache-volume
│       │   └── ready
│       └── wrapped_kube-api-access-njjrv
│           └── ready
└── volumes├── kubernetes.io~empty-dir│   └── cache-volume│       ├── 10}file{1.│       ├── aaa│       ├── file1│       ├── file10│       ├── file2│       ├── file3│       ├── file4│       ├── file5│       ├── file6│       ├── file7│       ├── file8│       └── file9└── kubernetes.io~projected└── kube-api-access-njjrv├── ca.crt -> ..data/ca.crt├── namespace -> ..data/namespace└── token -> ..data/token
​
12 directories, 18 files
###模拟删除pod测试
​
[root@k8s-master ~]# kubectl delete pod pod-empty 
pod "pod-empty" deleted
[root@k8s-node2 ~]# tree /var/lib/kubelet/pods/45a10614-495b-4745-be0f-c7492b90e2b7/
/var/lib/kubelet/pods/45a10614-495b-4745-be0f-c7492b90e2b7/ [error opening dir]
​
0 directories, 0 files

2、k8s持久化存储:hostPath

hostPath Volume是指Pod挂载宿主机上的目录或文件。 hostPath Volume使得容器可以使用宿主机的文件系统进行存储,hostpath(宿主机路径):节点级别的存储卷,在pod被删除,这个存储卷还是存在的,不会被删除,所以只要同一个pod被调度到同一个节点上来,在pod被删除重新被调度到这个节点之后,对应的数据依然是存在的。

查看hostPath存储卷的用法

[[root@k8s-master](mailto:root@k8s-master) ~]# kubectl explain pods.spec.volumes.hostPath
KIND:     Pod
VERSION:  v1
RESOURCE: hostPath <Object>
DESCRIPTION:HostPath represents a pre-existing file or directory on the host machinethat is directly exposed to the container. This is generally used forsystem agents or other privileged things that are allowed to see the hostmachine. Most containers will NOT need this. More info:[https://kubernetes.io/docs/concepts/storage/volumes#hostpath](https://kubernetes.io/docs/concepts/storage/volumes#hostpath)Represents a host path mapped into a pod. Host path volumes do not supportownership management or SELinux relabeling.
FIELDS:path <string> -required-type <string> 

创建一个pod,挂载hostPath存储卷

[root@k8s-master~]# cat hostpath.yaml
​
apiVersion: v1
kind: Pod
metadata:name: test-hostpath
spec:containers:- image: nginximagePullPolicy: IfNotPresentname: test-nginxvolumeMounts:- mountPath: /usr/share/nginx/htmlname: test-volumevolumes:- name: test-volumehostPath:path: /datatype: DirectoryOrCreate

注意:

DirectoryOrCreate表示本地有/data目录,就用本地的,本地没有就会在pod调度到的节点自动创建一个

更新资源清单文件,并查看pod调度到了哪个物理节点

[root@k8s-master ~]# kubectl apply -f  test-hostpath.yaml 
pod/test-hostpath created
​
[root@k8s-master ~]# kubectl get pod test-hostpath -o wide 
NAME            READY   STATUS    RESTARTS   AGE   IP              NODE           NOMINATED NODE   READINESS GATES
test-hostpath   1/1     Running   0          80s   172.16.69.248   k8s-node2   <none>           <none>

测试

###没有首页
[root@k8s-master ~]# curl 172.16.69.248
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.25.3</center>
</body>
</html>
###生成首页
[root@k8s-node2 ~]# cd /data/
[root@k8s-node2 data]# ls
[root@k8s-node2 data]# echo 1111 > index.html
​
[root@k8s-master ~]# curl 172.16.69.248
1111

hostpath存储卷缺点

单节点,pod删除之后重新创建必须调度到同一个node节点,数据才不会丢失

如何调度到同一个nodeName呢 ? 需要我们再yaml文件中进行指定就可以

apiVersion: v1
kind: Pod
metadata:name: test-hostpath
spec:nodeName: k8s-node2containers:- image: nginxname: test-nginxvolumeMounts:- mountPath: /usr/share/nginx/htmlname: test-volumevolumes:- name: test-volumehostPath:path: /datatype: DirectoryOrCreate

测试

[root@k8s-master ~]# kubectl apply -f  test-hostpath.yaml 
pod/test-hostpath configured
[root@k8s-master ~]# kubectl delete pod test-hostpath 
pod "test-hostpath" deleted
[root@k8s-master ~]# kubectl apply -f  test-hostpath.yaml 
pod/test-hostpath created
[root@k8s-master ~]# kubectl get pod test-hostpath -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP              NODE           NOMINATED NODE   READINESS GATES
test-hostpath   1/1     Running   0          9s    172.16.69.219   k8s-node2   <none>           <none>
[root@k8s-master ~]# curl 172.16.69.219
1111

3、k8s持久化存储:nfs

上节说的hostPath存储,存在单点故障,pod挂载hostPath时,只有调度到同一个节点,数据才不会丢失。那可以使用nfs作为持久化存储。

搭建nfs服务

以k8s的控制节点作为NFS服务端

[root@k8s-master ~]# yum install -y nfs-utils

在宿主机创建NFS需要的共享目录

[root@k8s-master ~]# mkdir /data -pv
mkdir: 已创建目录 "/data"
mkdir: 已创建目录 "/data"

配置nfs共享服务器上的/data目录

[root@k8s-master] ~]# systemctl enable --now nfs
[root@k8s-master] ~]# vim /etc/exports
/data 192.168.166.0/24(rw,sync,no_root_squash,no_subtree_check)

使NFS配置生效

[root@k8s-master ~]# exportfs -avr
exporting 192.168.166.0/24:/data

所有的worker节点安装nfs-utils

yum install nfs-utils -y
systemctl enable --now nfs
#在k8s-node1和k8s-node2上手动挂载试试:
[root@k8s-node1 ~]# mount 192.168.166.3:/data /mnt
[root@k8s-node1 ~]# df -Th | grep nfs
192.168.166.3:/data nfs4      116G  6.7G  109G    6% /mnt
#nfs可以被正常挂载
#手动卸载:
[root@k8s-node1 ~]# umount /mnt

创建Pod,挂载NFS共享出来的目录

Pod挂载nfs的官方地址:卷 | Kubernetes

[root@k8s-master ~]# cat nfs.yamlapiVersion: v1
kind: Pod
metadata:name: test-nfs
spec:containers:- name: test-nfsimage: nginximagePullPolicy: IfNotPresentports:- containerPort: 80protocol: TCPvolumeMounts:- name: nfs-volumesmountPath: /usr/share/nginx/htmlvolumes:- name: nfs-volumesnfs:path: /data  #共享目录server: 192.168.166.3  ##nfs服务器地址

更新资源清单文件

[root@k8s-master ~]# kubectl apply -f  test-nfs.yaml 
pod/test-nfs created

查看pod是否创建成功

[root@k8s-master ~]# kubectl get pods -o wide | grep nfs
test-nfs                  1/1     Running   0              55s     172.16.79.68    k8s-node1   <none>           <none>

测试

[root@k8s-master ~]# curl 172.16.79.68
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.25.3</center>
</body>
</html>
#登录到nfs服务器,在共享目录创建一个index.html
[root@k8s-master ~]# cd /data/
[root@k8s-master volumes]# echo nfs > index.html
[root@k8s-master ~]# curl 172.16.79.68
nfs

上面说明挂载nfs存储卷成功了,nfs支持多个客户端挂载,可以创建多个pod,挂载同一个nfs服务器共享出来的目录;但是nfs如果宕机了,数据也就丢失了,所以需要使用分布式存储,常见的分布式存储有glusterfs和cephfs

4、k8s持久化存储: PVC

参考官网:

Persistent Volumes | Kubernetes

4.1.1  k8s PV是什么?

PersistentVolume(PV)是群集中的一块存储,由管理员配置或使用存储类动态配置。 它是集群中的资源,就像pod是k8s集群资源一样。 PV是容量插件,如Volumes,其生命周期独立于使用PV的任何单个pod。

4.1.2  k8s PVC是什么?

PersistentVolumeClaim(PVC)是用户使用存储的请求。 它类似于pod。Pod消耗节点资源,PVC消耗存储资源。 pod可以请求特定级别的资源(CPU和内存)。 pvc在申请pv的时候也可以请求特定的大小和访问模式(例如,可以一次读写或多次只读)。

4.1.3  k8s PVC和PV工作原理

PV是群集中的资源。 PVC是对这些资源的请求。

PV和PVC之间的相互作用遵循以下生命周期:

(1)pv的供应方式

可以通过两种方式配置PV:静态或动态。

静态的:

集群管理员创建了许多PV。它们包含可供群集用户使用的实际存储的详细信息。它们存在于Kubernetes API中,可供使用。

动态的:

当管理员创建的静态PV都不匹配用户的PersistentVolumeClaim时,群集可能会尝试为PVC专门动态配置卷。此配置基于StorageClasses,PVC必须请求存储类,管理员必须创建并配置该类,以便进行动态配置。

(2)绑定

用户创建pvc并指定需要的资源和访问模式。在找到可用pv之前,pvc会保持未绑定状态

(3)使用

  • 需要找一个存储服务器,把它划分成多个存储空间;

  • k8s管理员可以把这些存储空间定义成多个pv;

  • 在pod中使用pvc类型的存储卷之前需要先创建pvc,通过定义需要使用的pv的大小和对应的访问模式,找到合适的pv;

  • pvc被创建之后,就可以当成存储卷来使用了,我们在定义pod时就可以使用这个pvc的存储卷;

  • pvc和pv它们是一一对应的关系,pv如果被pvc绑定了,就不能被其他pvc使用了;

  • 我们在创建pvc的时候,应该确保和底下的pv能绑定,如果没有合适的pv,那么pvc就会处于pending状态。

(4)回收策略

当我们创建pod时如果使用pvc做为存储卷,那么它会和pv绑定,当删除pod,pvc和pv绑定就会解除,解除之后和pvc绑定的pv卷里的数据需要怎么处理,目前,卷可以保留、回收或删除:

1、Retain
当删除pvc的时候,pv仍然存在,处于released状态,但是它不能被其他pvc绑定使用,里面的数据还是存在的,当我们下次再使用的时候,数据还是存在的,这个是默认的回收策略。
2、Delete
删除pvc时即会从Kubernetes中移除PV,也会从相关的外部设施中删除存储资产。

4.1.4  创建pod,使用pvc作为持久化存储卷

1、创建nfs共享目录

#在宿主机创建NFS需要的共享目录[root@k8s-master] ~]# mkdir /data/v{1..10} -p#配置nfs共享宿主机上的/data/v1..v10目录
[root@k8s-master ~]# cat /etc/exports
/data 192.168.166.0/24(rw,no_root_squash)
/data/v1 192.168.166.0/24(rw,no_root_squash)
/data/v2 192.168.166.0/24(rw,no_root_squash)
/data/v3 192.168.166.0/24(rw,no_root_squash)
/data/v4 192.168.166.0/24(rw,no_root_squash)
/data/v5 192.168.166.0/24(rw,no_root_squash)
/data/v6 192.168.166.0/24(rw,no_root_squash)
/data/v7 192.168.166.0/24(rw,no_root_squash)
/data/v8 192.168.166.0/24(rw,no_root_squash)
/data/v9 192.168.166.0/24(rw,no_root_squash)
/data/v10 192.168.166.0/24(rw,no_root_squash)
#重新加载配置,使配置成效
[root@k8s-master ~]# exportfs -arv

2、如何编写pv的资源清单文件

#查看定义pv需要的字段
[root@k8s-master ~]# kubectl explain pv
KIND:     PersistentVolume
VERSION:  v1
DESCRIPTION:PersistentVolume (PV) is a storage resource provisioned by anadministrator. It is analogous to a node. More info:[https://kubernetes.io/docs/concepts/storage/persistent-volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes)
FIELDS:apiVersion  <string>skind<string>metadata<Object>spec<Object>
#查看定义nfs类型的pv需要的字段
[root@k8s-master] ~]# kubectl explain pv.spec.nfs
KIND:     PersistentVolume
VERSION:  v1
RESOURCE: nfs <Object>
FIELDS:path<string> -required-readOnly<boolean>server  <string> -required-

3、创建pv

[root@k8s-master ~]# cat pv.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:name: v1
spec:capacity:storage: 1Gi #pv的存储空间容量accessModes: ["ReadWriteOnce"]nfs:path: /data/v1 #把nfs的存储空间创建成pvserver: 192.168.166.3 #nfs服务器的地址
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v2
spec:capacity:storage: 2GiaccessModes: ["ReadWriteMany"]nfs:path: /data/v2server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v3
spec:capacity:storage: 3GiaccessModes: ["ReadOnlyMany"]nfs:path: /data/v3server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v4
spec:capacity:storage: 4GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v4server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v5
spec:capacity:storage: 5GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v5server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v6
spec:capacity:storage: 6GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v6server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v7
spec:capacity:storage: 7GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v7server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v8
spec:capacity:storage: 8GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v8server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v9
spec:capacity:storage: 9GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v9server: 192.168.166.3
---
apiVersion: v1
kind: PersistentVolume
metadata:name: v10
spec:capacity:storage: 10GiaccessModes: ["ReadWriteOnce","ReadWriteMany"]nfs:path: /data/v10server: 192.168.166.3

更新资源清单文件

[root@k8s-master ~]# kubectl apply -f pv.yaml 
persistentvolume/v1 created
persistentvolume/v2 created
persistentvolume/v3 created
persistentvolume/v4 created
persistentvolume/v5 created
persistentvolume/v6 created
persistentvolume/v7 created
persistentvolume/v8 created
persistentvolume/v9 created
persistentvolume/v10 created

查看pv资源

[root@k8s-master ~]# kubectl get pvNAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
v1     1Gi        RWO            Retain           Available                                   88s
v10    10Gi       RWO,RWX        Retain           Available                                   88s
v2     2Gi        RWX            Retain           Available                                   88s
v3     3Gi        ROX            Retain           Available                                   88s
v4     4Gi        RWO,RWX        Retain           Available                                   88s
v5     5Gi        RWO,RWX        Retain           Available                                   88s
v6     6Gi        RWO,RWX        Retain           Available                                   88s
v7     7Gi        RWO,RWX        Retain           Available                                   88s
v8     8Gi        RWO,RWX        Retain           Available                                   88s
v9     9Gi        RWO,RWX        Retain           Available                                   88s#STATUS是Available,表示pv是可用的
NAME(名称)
通常用于标识某个特定的对象,在这里可能是指存储相关的实体(比如存储卷等)的具体名字,方便对其进行区分和引用。
CAPACITY(容量)
代表该对象所具备的存储容量大小,一般会以特定的存储单位(如字节、KB、MB、GB、TB 等)来衡量,表示其能够容纳的数据量多少。
ACCESS MODES(访问模式)
指的是允许对相应存储资源进行访问的方式,例如可以是只读、读写、可追加等不同模式,决定了用户或系统对其存储内容操作的权限范围。
RECLAIM POLICY(回收策略)
关乎当存储资源不再被使用或者释放时,对其所占用空间等资源的处理方式,常见的策略比如立即回收、延迟回收、按特定条件回收等,确保资源能合理地被再次利用。
STATUS(状态)
描述该存储相关对象当前所处的情况,例如可能是可用、不可用、正在初始化、已损坏等不同状态,便于了解其是否能正常工作。
CLAIM(声明)
在存储语境中,往往涉及对存储资源的一种请求或者占用声明,表明某个主体对相应存储资源有着相关权益或者正在使用它等情况。
STORAGECLASS(存储类别)
用于区分不同特性的存储分类,比如可以根据存储的性能(高速、低速)、存储介质(磁盘、磁带等)、存储成本(昂贵、廉价)等因素划分出不同的存储类,以满足不同场景的需求。
VOLUMEATTRIBUTESCLASS(卷属性类别)
主要是针对存储卷这一特定存储对象而言,涵盖了该卷在诸如容量属性、性能属性、安全属性等多方面的类别划分,体现其具备的各类特性。
REASON(原因)
如果存储对象处于某种特定状态(比如异常状态等),此处用于说明造成该状态的具体缘由,方便排查问题、分析情况。
AGE(时长)
一般是指从该存储对象创建开始到当前所经历的时间长度,可用于衡量其存在的时间阶段,辅助判断其使用情况等。

4、创建pvc,和符合条件的pv绑定

[root@k8s-master ~]# cat pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: my-pvc
spec:accessModes: ["ReadWriteMany"]resources:requests:storage: 2Gi

更新资源清单文件

[root@k8s-master ~]# kubectl apply -f my-pvc.yamlpersistentvolumeclaim/my-pvc created

查看pv和pvc,STATUS是Bound,表示这个pv已经被my-pvc绑定了

[root@k8s-master ~]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM             STORAGECLASS   REASON   AGE
v1     1Gi        RWO            Retain           Available                                             17m
v10    10Gi       RWO,RWX        Retain           Available                                             17m
v2     2Gi        RWX            Retain           Bound       default/my-pvc                            17m
v3     3Gi        ROX            Retain           Available                                             17m
v4     4Gi        RWO,RWX        Retain           Bound       default/my-pvc1                           17m
v5     5Gi        RWO,RWX        Retain           Available                                             17m
v6     6Gi        RWO,RWX        Retain           Available                                             17m
v7     7Gi        RWO,RWX        Retain           Available                                             17m
v8     8Gi        RWO,RWX        Retain           Available                                             17m
v9     9Gi        RWO,RWX        Retain           Available                                             17m
[root@k8s-master ~]# kubectl get pvc
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pvc    Bound    v2       2Gi        RWX                           12m
my-pvc1   Bound    v4       4Gi        RWO,RWX                       4m11s

5、创建pod,挂载pvc

[root@hd1 volume]# cat pod_pvc.yamlapiVersion: v1
kind: Pod
metadata:name: pod-pvc
spec:containers:- name: nginximage: nginximagePullPolicy: IfNotPresentvolumeMounts:- name: nginx-htmlmountPath: /usr/share/nginx/htmlvolumes:- name: nginx-htmlpersistentVolumeClaim:claimName: my-pvc

更新资源清单文件

[root@k8s-master ~]# kubectl apply -f pod-pvc.yaml
pod/pod-pvc created

查看pod状态

[root@k8s-master ~]# kubectl get pod -o wide | grep pvc
pod-pvc                   1/1     Running   0              16s     172.16.79.127   k8s-node1   <none>           <none
#通过上面可以看到pod处于running状态,正常运行

测试

[root@k8s-master ~]# curl 172.16.79.127
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.25.3</center>
</body>
</html>####在nfs服务器端写入index.html
[root@k8s-master ~]# cd /data/v2
[root@k8s-master v2]# echo pvc > index.html
[root@k8s-master ~]# curl 172.16.79.127
pvc
###创建另一个pod测试

删除pod-pvc  这个pod,发现pvc还是存在的

[root@k8s-master ~]# kubectl delete pod pod-pvc
[root@k8s-master ~]# kubectl get pvc
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pvc    Bound    v2       2Gi        RWX                           36s
my-pvc1   Bound    v4       4Gi        RWO,RWX                       20m[root@k8s-master ~]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM             STORAGECLASS   REASON   AGE
v1     1Gi        RWO            Retain           Available                                             54s
v10    10Gi       RWO,RWX        Retain           Available                                             33m
v2     2Gi        RWX            Retain           Bound       default/my-pvc                            54s
v3     3Gi        ROX            Retain           Available                                             33m
v4     4Gi        RWO,RWX        Retain           Bound       default/my-pvc1                           33m
v5     5Gi        RWO,RWX        Retain           Available                                             33m
v6     6Gi        RWO,RWX        Retain           Available                                             33m
v7     7Gi        RWO,RWX        Retain           Available                                             33m
v8     8Gi        RWO,RWX        Retain           Available                                             33m
v9     9Gi        RWO,RWX        Retain           Available                                             33m

删除pvc

[root@k8s-master ~]# kubectl delete pvc my-pvc
persistentvolumeclaim "my-pvc" deleted
[root@k8s-master ~]# kubectl get pvc
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pvc1   Bound    v4       4Gi        RWO,RWX                       20m
#发现pv的状态发生了变化
[root@k8s-master ~]# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM             STORAGECLASS   REASON   AGE
v1     1Gi        RWO            Retain           Available                                             2m9s
v10    10Gi       RWO,RWX        Retain           Available                                             34m
v2     2Gi        RWX            Retain           Released    default/my-pvc                            2m9s
v3     3Gi        ROX            Retain           Available                                             34m
v4     4Gi        RWO,RWX        Retain           Bound       default/my-pvc1                           34m
v5     5Gi        RWO,RWX        Retain           Available                                             34m
v6     6Gi        RWO,RWX        Retain           Available                                             34m
v7     7Gi        RWO,RWX        Retain           Available                                             34m
v8     8Gi        RWO,RWX        Retain           Available                                             34m
v9     9Gi        RWO,RWX        Retain           Available                                             34m

此时我们进入到nfs的共享目录,发现我们建立的index.html还是存在的

[root@k8s-master v2]# ls
index.html

注:使用pvc和pv的注意事项

1、我们每次创建pvc的时候,需要事先有划分好的pv,这样可能不方便,那么可以在创建pvc的时候直接动态创建一个pv这个存储类,pv事先是不存在的

2、pvc和pv绑定,如果使用默认的回收策略retain,那么删除pvc之后,pv会处于released状态,我们想要继续使用这个pv,需要手动删除pv,kubectl delete pv pv_name,删除pv,不会删除pv里的数据,当我们重新创建pvc时还会和这个最匹配的pv绑定,数据还是原来数据,不会丢失。

5、 k8s存储类:storageclass

5.1 概述

上面介绍的PV和PVC模式都是需要先创建好PV,然后定义好PVC和pv进行一对一的Bond,但是如果PVC请求成千上万,那么就需要创建成千上万的PV,对于运维人员来说维护成本很高,Kubernetes提供一种自动创建PV的机制,叫StorageClass,它的作用就是创建PV的模板。k8s集群管理员通过创建storageclass可以动态生成一个存储卷pv供k8s pvc使用。

每个StorageClass都包含字段provisioner,parameters和reclaimPolicy。

具体来说,StorageClass会定义以下两部分:

1、PV的属性 ,比如存储的大小、类型等;

2、创建这种PV需要使用到的存储插件,比如Ceph、NFS等

有了这两部分信息,Kubernetes就能够根据用户提交的PVC,找到对应的StorageClass,然后Kubernetes就会调用 StorageClass声明的存储插件,创建出需要的PV。

StorageClass运行原理

  • volumeClaimTemplates实现了pvc的自动化,StorageClass实现了pv的自动化

  • 每个 StorageClass 都包含 provisioner、parameters 和 reclaimPolicy 字段, 这些字段会在 StorageClass 需要动态分配 PersistentVolume 时会使用到。

  • StorageClass 对象的命名很重要,用户使用这个命名来请求生成一个特定的类。 当创建 StorageClass 对象时,管理员设置 StorageClass 对象的命名和其他参数,一旦创建了对象就不能再对其更新。

  • 管理员可以为没有申请绑定到特定 StorageClass 的 PVC 指定一个默认的存储类

要使用 StorageClass,我们就得安装对应的自动配置程序,比如我们这里存储后端使用的是 nfs,那么我们就需要使用到一个 nfs-client 的自动配置程序,我们也叫它 Provisioner(制备器),这个程序使我们已经配置好的 nfs 服务器,来自动创建持久卷,也就是自动帮我们创建 PV。

搭建StorageClass+NFS,大致有以下几个步骤:

1.创建一个可用的NFS Serve(实际存储文件的空间)
2.创建Service Account 这是用来管控NFS provisioner在k8s集群中运行的权限
3.创建StorageClass 负责建立PVC并调用NFS provisioner进行预定的工作,并让PV与PVC建立管理
4.创建NFS provisioner(存储制备器)1)每个 StorageClass 都有一个制备器(Provisioner)用来决定使用哪个卷插件制备 PV。 该字段必须指定。2)主要有两个功能一个是在NFS共享目录下创建挂载点(volume)另一个则是建了PV并将PV与NFS的挂载点建立关联

storageclass字段

#查看定义的storageclass需要的字段
[root@k8s-master~]# kubectl explain storageclass
KIND:     StorageClass
VERSION:  storage.k8s.io/v1
DESCRIPTION:StorageClass describes the parameters for a class of storage for whichPersistentVolumes can be dynamically provisioned.StorageClasses are non-namespaced; the name of the storage class accordingto etcd is in ObjectMeta.Name.
FIELDS:allowVolumeExpansion    <boolean>allowedTopologies   <[]Object>apiVersion  <string>kind<string>metadata<Object>mountOptions    <[]string>parameters  <map[string]string>provisioner<string> -required-reclaimPolicy   <string>volumeBindingMode   <string>

provisioner:供应商(也称作制备器),storageclass需要有一个供应者,用来确定我们使用什么样的存储来创建pv

常见的provisioner

provisioner既可以由内部供应商提供,也可以由外部供应商提供,如果是外部供应商可以参考https://github.com/kubernetes-incubator/external-storage/下提供的方法创建。

https://github.com/kubernetes-sigs/sig-storage-lib-external-provisioner

以NFS为例,要想使用NFS,我们需要一个nfs-client的自动装载程序,称之为provisioner,这个程序会使我们已经配置好的NFS服务器自动创建持久卷,也就是自动帮我们创建PV。

allowVolumeExpansion:允许卷扩展,PersistentVolume 可以配置成可扩展。将此功能设置为true时,允许用户通过编辑相应的 PVC 对象来调整卷大小。当基础存储类的allowVolumeExpansion字段设置为 true 时,以下类型的卷支持卷扩展。

注意:此功能仅用于扩容卷,不能用于缩小卷。

5.2 安装nfs provisioner,用于配合存储类动态生成pv

1、创建运行nfs-provisioner需要的sa账号

[root@k8s-master newnfs]# cat sa.yaml 
apiVersion: v1
kind: Namespace
metadata:name: newnfs
---
apiVersion: v1
kind: ServiceAccount
metadata:name: nfs-client-provisionernamespace: newnfs
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: nfs-client-provisioner-runner
rules:- apiGroups: [""]resources: ["persistentvolumes"]verbs: ["get", "list", "watch", "create", "delete"]- apiGroups: [""]resources: ["persistentvolumeclaims"]verbs: ["get", "list", "watch", "update"]- apiGroups: ["storage.k8s.io"]resources: ["storageclasses"]verbs: ["get", "list", "watch"]- apiGroups: [""]resources: ["events"]verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: run-nfs-client-provisioner
subjects:- kind: ServiceAccountname: nfs-client-provisionernamespace: newnfs
roleRef:kind: ClusterRolename: nfs-client-provisioner-runnerapiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: leader-locking-nfs-client-provisionernamespace: newnfs
rules:- apiGroups: [""]resources: ["endpoints"]verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: leader-locking-nfs-client-provisionernamespace: newnfs
subjects:- kind: ServiceAccountname: nfs-client-provisionernamespace: newnfs
roleRef:kind: Rolename: leader-locking-nfs-client-provisionerapiGroup: rbac.authorization.k8s.io######################################
[root@k8s-master newnfs]# kubectl apply -f sa.yaml
namespace/newnfs created
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created

2、安装nfs-provisioner程序

[root@k8s-master ~]# mkdir /data -p#把/data变成nfs共享的目录[root@k8s-master ~]# cat /etc/exports
/data 192.168.166.0/24(rw,no_root_squash)
/data/v1 192.168.166.0/24(rw,no_root_squash)
/data/v2 192.168.166.0/24(rw,no_root_squash)
/data/v3 192.168.166.0/24(rw,no_root_squash)
/data/v4 192.168.166.0/24(rw,no_root_squash)
/data/v5 192.168.166.0/24(rw,no_root_squash)
/data/v6 192.168.166.0/24(rw,no_root_squash)
/data/v7 192.168.166.0/24(rw,no_root_squash)
/data/v8 192.168.166.0/24(rw,no_root_squash)
/data/v9 192.168.166.0/24(rw,no_root_squash)
/data/v10 192.168.166.0/24(rw,no_root_squash)###############################
[root@k8s-master newnfs]# cat nfs.yaml 
kind: Deployment
apiVersion: apps/v1
metadata:name: nfs-client-provisionernamespace: newnfs
spec:replicas: 1selector:matchLabels:app: nfs-client-provisionerstrategy:type: Recreate        #设置升级策略为删除再创建(默认为滚动更新)template:metadata:labels:app: nfs-client-provisionerspec:serviceAccountName: nfs-client-provisioner  #上一步创建的ServiceAccount名称containers:- name: nfs-client-provisionerimage: registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner:v4.0.0imagePullPolicy: IfNotPresentvolumeMounts:- name: nfs-client-rootmountPath: /persistentvolumesenv:- name: PROVISIONER_NAME  # Provisioner的名称,以后设置的storageclass要和这个保持一致value: storage-nfs- name: NFS_SERVER        # NFS服务器地址,需和valumes参数中配置的保持一致value: 192.168.166.3- name: NFS_PATH          # NFS服务器数据存储目录,需和volumes参数中配置的保持一致value: /data- name: ENABLE_LEADER_ELECTIONvalue: "true"volumes:- name: nfs-client-rootnfs:server: 192.168.166.3       # NFS服务器地址path: /data        # NFS共享目录

更新资源清单文件

[root@k8s-master ~]# kubectl apply -f nfs.yamldeployment.apps/nfs-client-provisioner created

查看nfs-provisioner是否正常运行

[root@k8s-master newnfs]# kubectl -n newnfs get pods | grep nfs
nfs-client-provisioner-5486f75d5-qjjnh   1/1     Running   0          8m1s

5.3 创建storageclass,动态供给pv

[root@k8s-master newnfs]# cat sc.yaml 
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:namespace: newnfsname: nfs-storageannotations:storageclass.kubernetes.io/is-default-class: "false"  ## 是否设置为默认的storageclass
provisioner: storage-nfs                                   ## 动态卷分配者名称,必须和上面创建的deploy中环境变量“PROVISIONER_NAME”变量值一致
parameters:archiveOnDelete: "true"                                 ## 设置为"false"时删除PVC不会保留数据,"true"则保留数据
mountOptions: - hard                                                  ## 指定为硬挂载方式- nfsvers=4                                             ## 指定NFS版本,这个需要根据NFS Server版本号设置nfs[root@k8s-master ~]# kubectl apply -f sc.yaml
storageclass.storage.k8s.io/nfs-storage created

查看storageclass是否创建成功

[root@k8s-master nfs]# kubectl -n newnfs get sc
NAME          PROVISIONER   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-storage   storage-nfs   Delete          Immediate           false                  8m30s     

显示内容如上,说明storageclass创建成功了

注意:provisioner处写的example.com/nfs应该跟安装nfs provisioner时候的env下的PROVISIONER_NAME的value值保持一致,如下:

env:name: PROVISIONER_NAMEvalue: storage-nfs

5.4  创建pvc,通过storageclass动态生成pv

[root@k8s-master newnfs]# cat pvc.yaml 
kind: PersistentVolumeClaim
apiVersion: v1
metadata:name: storage-pvcnamespace: newnfs
spec:storageClassName: nfs-storage    ## 需要与上面创建的storageclass的名称一致accessModes:- ReadWriteOnceresources:requests:storage: 1Mi[root@k8s-master newnfs]# kubectl apply  -f pvc.yaml 
persistentvolumeclaim/storage-pvc created

查看是否动态生成了pv,pvc是否创建成功,并和pv绑定

[root@k8s-master newnfs]# kubectl -n newnfs get pvc
NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
storage-pvc   Bound    pvc-a56945be-6150-4591-b3e0-5e4a698b8e3a   1Mi        RWO            nfs-storage    8m8s
[root@k8s-master newnfs]# kubectl -n newnfs get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
pvc-a56945be-6150-4591-b3e0-5e4a698b8e3a   1Mi        RWO            Delete           Bound    newnfs/storage-pvc   nfs-storage             26m

通过上面可以看到test-claim1的pvc已经成功创建了,绑定的pv是pvc-da737fb7-3ffb-43c4-a86a-2bdfa7f201e2,这个pv是由storageclass调用nfs provisioner自动生成的。

步骤总结:

1、供应商:创建一个nfs provisioner

2、创建storageclass,storageclass指定刚才创建的供应商

3、创建pvc,这个pvc指定storageclass

报错修复

[root@k8s-master manifests]# kubectl -n newnfs get pvc
NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
storage-pvc   pending
[root@k8s-master manifests]# kubectl -n newnfs logs nfs-client-provisioner-5486f75d5-qjjnh
E0131 09:17:58.845719       1 controller.go:766] Unexpected error getting claim reference to claim "default/test-claim1": selfLink was empty, can't make reference
[root@k8s-master manifests]# cd /etc/kubernetes/manifests
[root@k8s-master manifests]# cat kube-apiserver.yaml 
apiVersion: v1
kind: Pod
metadata:annotations:kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 192.168.166.161:6443creationTimestamp: nulllabels:component: kube-apiservertier: control-planename: kube-apiservernamespace: kube-system
spec:containers:- command:- kube-apiserver- --advertise-address=192.168.166.161- --allow-privileged=true- --authorization-mode=Node,RBAC- --client-ca-file=/etc/kubernetes/pki/ca.crt- --enable-admission-plugins=NodeRestriction- --enable-bootstrap-token-auth=true- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key- --etcd-servers=https://127.0.0.1:2379- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key- --requestheader-allowed-names=front-proxy-client- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt- --requestheader-extra-headers-prefix=X-Remote-Extra-- --requestheader-group-headers=X-Remote-Group- --requestheader-username-headers=X-Remote-User- --secure-port=6443- --service-account-issuer=https://kubernetes.default.svc.cluster.local- --service-account-key-file=/etc/kubernetes/pki/sa.pub- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key- --service-cluster-ip-range=10.10.0.0/16- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key- --feature-gates=RemoveSelfLink=false    ###增加该行,1.20版本以后image: registry.aliyuncs.com/google_containers/kube-apiserver:v1.24.0imagePullPolicy: IfNotPresentlivenessProbe:failureThreshold: 8httpGet:host: 192.168.166.161path: /livezport: 6443scheme: HTTPSinitialDelaySeconds: 10periodSeconds: 10timeoutSeconds: 15name: kube-apiserverreadinessProbe:failureThreshold: 3httpGet:host: 192.168.166.161path: /readyzport: 6443scheme: HTTPSperiodSeconds: 1timeoutSeconds: 15resources:requests:cpu: 250mstartupProbe:failureThreshold: 24httpGet:host: 192.168.166.161path: /livezport: 6443scheme: HTTPSinitialDelaySeconds: 10periodSeconds: 10timeoutSeconds: 15volumeMounts:- mountPath: /etc/ssl/certsname: ca-certsreadOnly: true- mountPath: /etc/pkiname: etc-pkireadOnly: true- mountPath: /etc/kubernetes/pkiname: k8s-certsreadOnly: truehostNetwork: truepriorityClassName: system-node-criticalsecurityContext:seccompProfile:type: RuntimeDefaultvolumes:- hostPath:path: /etc/ssl/certstype: DirectoryOrCreatename: ca-certs- hostPath:path: /etc/pkitype: DirectoryOrCreatename: etc-pki- hostPath:path: /etc/kubernetes/pkitype: DirectoryOrCreatename: k8s-certs
status: {}

5.5  创建pod,挂载storageclass动态生成的pvc:storage-pvc

[root@k8s-master nfs]# cat read-pod.yamlkind: Pod
apiVersion: v1
metadata:name: read-podnamespace: newnfs
spec:containers:- name: read-podimage: nginximagePullPolicy: IfNotPresentvolumeMounts:- name: nfs-pvcmountPath: /usr/share/nginx/htmlrestartPolicy: "Never"volumes:- name: nfs-pvcpersistentVolumeClaim:claimName: storage-pvc
#更新资源清单文件[root@k8s-master newnfs]# kubectl apply -f test-pod.yaml 
pod/read-pod created#查看pod是否创建成功[root@k8s-master nfs]# kubectl -n newnfs get pods | grep readread-pod                             1/1     Running     0  
##在NFS服务端写入首页
[root@k8s-master ~]# cd /data/newnfs-storage-pvc-pvc-a56945be-6150-4591-b3e0-5e4a698b8e3a/
[root@k8s-master newnfs-storage-pvc-pvc-a56945be-6150-4591-b3e0-5e4a698b8e3a]# echo nfs > index.html
##测试访问
[root@k8s-master newnfs]# curl 172.16.79.96
nfs

查看index.html的物理位置

删除read-pod ,删除pvc 会自动删除pv ,所以默认的回收策略是delete是可以验证出来的

[root@k8s-master newnfs]# kubectl -n newnfs delete pod read-pod
pod "read-pod" deleted
###pv没有变化
[root@k8s-master newnfs]# kubectl -n newnfs delete pvc storage-pvc 
persistentvolumeclaim "storage-pvc" deleted
###pv发生变化,没有了
##在NFS服务端发生如下变化,会将原来的nfspv进行更改名称归档
[root@k8s-master volumes]# ls
archived-pvc-a56945be-6150-4591-b3e0-5e4a698b8e3a

重新更新pvc文件,会发现物理nfs中多了一个目录。

[root@k8s-master newnfs]# kubectl apply -f pvc.yaml 
persistentvolumeclaim/storage-pvc created
####
[root@k8s-master volumes]# ls
archived-pvc-a56945be-6150-4591-b3e0-5e4a698b8e3a  newnfs-storage-pvc-pvc-397dfa84-4b11-4f96-8378-801f034806d6
http://www.dtcms.com/a/343182.html

相关文章:

  • Seaborn数据可视化实战:Seaborn入门-环境搭建与基础操作
  • Seaborn数据可视化实战
  • AI对口型唱演:科技赋能,开启虚拟歌者新篇章
  • 刷机维修进阶教程-----如何清除云账号 修复wifi 指南针 相机 指纹等刷机故障
  • 自然处理语言NLP:One-Hot编码、TF-IDF、词向量、NLP特征输入、EmbeddingLayer实现、word2vec
  • Linux 802.11协议栈深度分析与实践指南
  • 车机两分屏运行Unity制作的效果
  • OpenAI重新开源!gpt-oss-20b适配昇腾并上线魔乐社区
  • WebSocket连接的例子
  • 链游开发新篇章:融合区块链技术的游戏创新与探索
  • 什么是撮合引擎
  • 模型的量化-nf4和pf4
  • 基于STM32F103单片机智能门禁热释人体感应报警设计
  • C#串口单例 + 端口复用
  • LCD DMA day59
  • 为何vivo做了头显,小米却选择AI眼镜
  • 【GNSS基带算法】Chapter.2 相干积分与非相干积分
  • 基于 .NET Core Web API 请求 Nacos 配置中心的最佳实践
  • 微服务01-微服务架构:Java中的最佳实践
  • 业务扩展字段系统设计理念与流程图
  • LeetCode_动态规划
  • 【NLP(01)】NLP(自然语言处理)基础
  • nginx-自制证书实现
  • Python学习 -- MySQL数据库的查询及案例
  • 自然语言处理——03 RNN及其变体
  • C++ 命名规范示意表
  • iOS 应用上架瓶颈与解决方案 从开发到审核的全流程实战
  • 机器学习中的聚类与集成算法:从基础到应用
  • word参考文献对齐
  • week3-[循环嵌套]好数