K8s集群调度
k8s 调度器
Scheduler 是 kubernetes 的调度器,主要的任务是把定义的 pod 分配到集群的节点上。听起来非常简单,但有很多要考虑的问题:
- 公平:如何保证每个节点都能被分配资源
- 资源高效利用:集群所有资源最大化被使用
- 效率:调度的性能要好,能够尽快地对大批量的 pod 完成调度工作
- 灵活:允许用户根据自己的需求控制调度的逻辑
Sheduler 是作为单独的程序运行的,启动之后会一直监听 API Server,获取 PodSpec.NodeName 为空的 pod,对每个 pod 都会创建一个 binding,表明该 pod 应该放到哪个节点上
K8s调度策略
deployment:全自动调度
Deployment的主要功能之一就是自动部署一个容器应用的多份副本,以及持续监控副本的数量,在集群内始终维持用户指定的副本数量。从调度策略上来说,Pod由系统全自动完成调度。它们各自最终运行在哪个节点上,完全由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。除了使用系统自动调度算法完成一组Pod的部署,Kubernetes也提供了多种丰富的调度策略,用户只需在Pod的定义中使用NodeSelector、NodeAffinity、PodAffinity、Pod驱逐等更加细粒度的调度策略设置,就能完成对Pod的精准调度。
NodeSelector:定向调度
Kubernetes Master上的Scheduler服务(kube-scheduler进程)负责实现Pod的调度,整个调度过程通过执行一系列复杂的算法,最终为每个Pod都计算出一个最佳的目标节点,这一过程是自动完成的,通常我们无法知道Pod最终会被调度到哪个节点上。在实际情况下,也可能需要将Pod调度到指定的一些Node上,可以通过Node的标签(Label)和Pod的nodeSelector属性相匹配,也可以直接在Pod.spec.NodeName直接指定node名称来达到上述目的。
首先查看node的标签
[root@master ~]# kubectl get node --show-labels NAME STATUS ROLES AGE VERSION LABELS master Ready control-plane,master 46h v1.21.0 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers= node1 Ready <none> 46h v1.21.0 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1,kubernetes.io/os=linux node2 Ready <none> 46h v1.21.0 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux
给node2打上一个标签
[root@master ~]# kubectl label node node2 app=nginx node/node2 labeled [root@master ~]# kubectl get node --show-labels NAME STATUS ROLES AGE VERSION LABELS master Ready control-plane,master 46h v1.21.0 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers= node1 Ready <none> 46h v1.21.0 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1,kubernetes.io/os=linux node2 Ready <none> 46h v1.21.0 app=nginx,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux
创建一个yaml,定义selector让5个pod都运行在标签为app=nginx的node2之上
[root@master ~]# cat deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-nginx namespace: default spec: replicas: 5 selector: matchLabels: app: nginx template: metadata: name: mypod namespace: default labels: app: nginx spec: nodeSelector: #node标签选择 app: nginx containers: - name: nginx image: nginx ports: - name: http containerPort: 80
执行yaml,查看pod发现,5个pod都运行在node2节点上,这是因为只有node2有app=nginx的标签,所以pod都创建到node2上面了。
[root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES deployment-nginx-6c5ccc4cb9-c2htc 1/1 Running 0 26s 10.244.104.13 node2 <none> <none> deployment-nginx-6c5ccc4cb9-f8hln 1/1 Running 0 26s 10.244.104.11 node2 <none> <none> deployment-nginx-6c5ccc4cb9-jdh5z 1/1 Running 0 26s 10.244.104.15 node2 <none> <none> deployment-nginx-6c5ccc4cb9-qwlmx 1/1 Running 0 26s 10.244.104.12 node2 <none> <none> deployment-nginx-6c5ccc4cb9-xnqhx 1/1 Running 0 26s 10.244.104.14 node2 <none> <none>
也可以直接指定NodeName来完成定向调度,比如通过指定Node Name我们定向调度到node1上
[root@master ~]# cat deployment2.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-nginx namespace: default spec: replicas: 5 selector: matchLabels: app: nginx template: metadata: name: mypod namespace: default labels: app: nginx spec: nodeName: node1 containers: - name: nginx image: nginx
执行yaml,并查看pod
[root@master ~]# kubectl apply -f deployment2.yaml deployment.apps/deployment-nginx created [root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES deployment-nginx-8c459867c-4fw9q 0/1 ContainerCreating 0 5s <none> node1 <none> <none> deployment-nginx-8c459867c-ccbkz 0/1 ContainerCreating 0 5s <none> node1 <none> <none> deployment-nginx-8c459867c-cksqd 0/1 ContainerCreating 0 5s <none> node1 <none> <none> deployment-nginx-8c459867c-mrdz4 0/1 ContainerCreating 0 5s <none> node1 <none> <none> deployment-nginx-8c459867c-zt8n8 0/1 ContainerCreating 0 5s <none> node1 <none> <none>
NodeAffinity:Node亲和性调度
NodeAffinity意为Node亲和性的调度策略,是用于替换NodeSelector的全新调度策略。目前有两种节点亲和性表达。
- RequiredDuringSchedulingIgnoredDuringExecution:必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于硬限制。
- PreferredDuringSchedulingIgnoredDuringExecution:强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制。多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序。
我们定义一个affinity.yaml文件,里面定义一个5个pod,定义requiredDuringSchedulingIgnoredDuringExecution让pod调度至node2上
[root@master ~]# cat affinity.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-affinity namespace: default spec: replicas: 5 selector: matchLabels: app: nginx template: metadata: name: mypod namespace: default labels: app: nginx spec: containers: - name: nginx image: nginx affinity: #调度策略 nodeAffinity: #node亲和性调度 requiredDuringSchedulingIgnoredDuringExecution: #硬限制,必须满足条件 nodeSelectorTerms: - matchExpressions: - key: app #node标签的键名 operator: In #In表示,必须在含有定义的标签的node上创建,NotIn表示不能在含有定义标签的node上创建pod values: - nginx #node标签键值
执行yaml,查看pod
[root@master ~]# kubectl apply -f affinity.yaml deployment.apps/deployment-affinity created [root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES deployment-affinity-5787c89c6b-crpwk 1/1 Running 0 23s 10.244.104.18 node2 <none> <none> deployment-affinity-5787c89c6b-dpq45 1/1 Running 0 23s 10.244.104.20 node2 <none> <none> deployment-affinity-5787c89c6b-g9zvg 1/1 Running 0 23s 10.244.104.16 node2 <none> <none> deployment-affinity-5787c89c6b-kbjsh 1/1 Running 0 23s 10.244.104.17 node2 <none> <none> deployment-affinity-5787c89c6b-n4d92 1/1 Running 0 23s 10.244.104.19 node2 <none> <none>
可以查看到pod都创建在node2上面,如果将yaml文件中的operator改为NoteIN,则不会在node2上面创建,再定义一下软限制,软限制会有权重值,pod创建时会优先满足软限制,当软限制不满足的时候也能被创建,是非必须满足项。软限制可以和硬限制配合使用,首先要满足应策略,在尽可能满足软策略。
[root@master ~]# cat affinity1.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-affinity namespace: default spec: replicas: 5 selector: matchLabels: app: nginx template: metadata: name: mypod namespace: default labels: app: nginx spec: containers: - name: nginx image: nginx affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: app operator: NotIn values: - nginx
执行yaml,查看pod
[root@master ~]# kubectl apply -f affinity1.yaml deployment.apps/deployment-affinity created [root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES deployment-affinity-d495775f4-8sft6 1/1 Running 0 20s 10.244.166.144 node1 <none> <none> deployment-affinity-d495775f4-dkt8z 1/1 Running 0 20s 10.244.166.143 node1 <none> <none> deployment-affinity-d495775f4-lfppv 1/1 Running 0 20s 10.244.166.145 node1 <none> <none> deployment-affinity-d495775f4-nmgz5 1/1 Running 0 20s 10.244.104.21 node2 <none> <none> deployment-affinity-d495775f4-pzf2p 1/1 Running 0 20s 10.244.166.146 node1 <none> <none>
因为是非必须满足条件,所以也会在node2上创建pod。
yaml中的operator值运算关系有以下几种:
- In:必须满足这个标签
- NotIn:必须不满足这个标签
- Exists:标签必须存在
- DoesNotExist:标签必须不存在
- Gt:标签的值大于定义的值,比如定义的值为4,那么必须在标签大于4的node上创建pod
- Lt:标签的值小于定义的值,比如定义的值为4,那么必须在标签小于4的node上创建pod
NodeAffinity规则设置的注意事:
- 如果同时定义了nodeSelector和nodeAffinity,那么必须两个条件都得到满足,Pod才能最终运行在指定的Node上。
- 如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能够匹配成功即可。
- 如果在nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。
PodAffinity:pod亲和性和反亲和性
pod亲和性根据在node上正在运行的Pod的标签而不是node的标签进行判断和调度,要求对node和Pod两个条件进行匹配。这种规则可以描述为:如果在具有标签X的Node上运行了一个或者多个符合条件Y的Pod,那么Pod应该运行在这个Node上。(如果是互斥的情况,那么就变成拒绝)
和节点亲和相同,Pod亲和与互斥的条件设置也是
- requiredDuringSchedulingIgnoredDuringExecution:硬限制
- preferredDuringSchedulingIgnoredDuringExecution:软限制
首先创建一个pod,给pod打上标签,version=V1,app=nginx
[root@master ~]# cat pod1.yaml apiVersion: v1 kind: Pod metadata: labels: version: V1 app: nginx name: nginx-pod1 spec: containers: - image: nginx name: nginx restartPolicy: Always
创建pod并查看pod以及labels
[root@master ~]# vim pod1.yaml [root@master ~]# kubectl apply -f pod1.yaml pod/nginx-pod1 created [root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-pod1 1/1 Running 0 13s 10.244.166.147 node1 <none> <none> [root@master ~]# kubectl get pod --show-labels NAME READY STATUS RESTARTS AGE LABELS nginx-pod1 1/1 Running 0 9m7s app=nginx,version=V1
下面创建第2个Pod来说明Pod的亲和性调度,这里定义的亲和标签是version=V1,对应上面的nginx-pod1,topologyKey的值被设置为“kubernetes.io/hostname” (node1的标签值)
[root@master ~]# cat pod2.yaml apiVersion: v1 kind: Pod metadata: name: nginx-pod2 spec: containers: - image: nginx name: nginx restartPolicy: Always affinity: #调度策略 podAffinity: #pod亲和性调度 requiredDuringSchedulingIgnoredDuringExecution: #硬限制,必须满足条件 - labelSelector: #pod标签 matchExpressions: - key: app #pod标签的键名 operator: In #In表示,必须跟已创建pod相同标签的node上创建 values: - nginx #node标签键值 topologyKey: kubernetes.io/hostname #node标签的键名
执行yaml,查看pod
[root@master ~]# kubectl apply -f pod2.yaml pod/nginx-pod2 created [root@master ~]# kubectl get pod -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-pod1 1/1 Running 0 29m 10.244.166.147 node1 <none> <none> nginx-pod2 1/1 Running 0 75s 10.244.166.148 node1 <none> <none>
此时nginx-pod2也被创建到node1上面,下面创建nginx-pod3,演示反亲和调度。
[root@master ~]# cat pod3.yaml apiVersion: v1 kind: Pod metadata: name: nginx-pod3 spec: containers: - image: nginx name: nginx restartPolicy: Always affinity: podAntiAffinity: #pod非亲和性调度 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: version operator: In values: - V1 topologyKey: kubernetes.io/hostname
执行yaml,查看pod,发现nginx-pod3被创建到了node2上。
[root@master ~]# kubectl delete -f pod3.yaml pod "nginx-pod3" deleted [root@master ~]# kubectl get pod -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-pod1 1/1 Running 0 56m 10.244.166.147 node1 <none> <none> nginx-pod2 1/1 Running 0 27m 10.244.166.148 node1 <none> <none> nginx-pod3 1/1 Running 0 6s 10.244.104.22 node2 <none> <none>
Taints和Tolerations:污点和容忍
前面介绍了NodeAffinity节点亲和性,是在pod上定义的一种属性,使得Pod能够被调度到某些Node节点上运行(优先选择或强制要求)Taint则正好相反,它让Node拒绝Pod运行。
Taint要与Toleration配合使用,让Pod避开那些不适合的Node,在Node上设置一个或者多个Taint之后,除非Pod明确生命能够容忍这些污点,否则无法在这写Node节点上运行,Tolerations是Pod属性,让Pod能够(注意,只是能够,而非必须)运行标注了Taint的Node上
使用kubectl taint命令可以给某个node打上污点,node被打上污点后与pod之间存在一种相斥的关系,可以让node拒绝pod的调度执行,甚至将node上已存在的pod驱逐出去。
污点的组成如下:
key=value:effect
每个污点有一个key和value作为污点的标签,其中value可以为空,effect描述 污点的作用。当前Taints的effect支持如下三个选项:
- NoSchedule:表示K8s不会将pod调度到具有该污点的node上
- PreferNoSchedule:表示K8s尽量避免将pod调度到具有该污点的node上
- NoExecute:表示K8s不会将pod调度到具有该污点的node上,同时会将node上已经存在的pod驱逐出去
我们在node2上面打上污点
[root@master ~]# kubectl taint node node2 node=nginx:NoSchedule
node/node2 tainted
我们可以通过describe查看node2的污点信息
[root@master ~]# kubectl describe node node2 | grep Taints Taints: node=nginx:NoSchedule
当我们再创建pod时,将不会在node2上面创建了
[root@master ~]# cat deployment1.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-nginx namespace: default spec: replicas: 5 selector: matchLabels: app: nginx template: metadata: name: mypod namespace: default labels: app: nginx spec: containers: - name: nginx image: nginx
执行yaml创建pod看一下
[root@master ~]# kubectl apply -f deployment1.yaml deployment.apps/deployment-nginx created [root@master ~]# kubectl get pod -o wide deployment-nginx-5b45c89ccd-gmw9h 1/1 Running 0 29s 10.244.166.149 node1 <none> <none> deployment-nginx-5b45c89ccd-gs6f2 1/1 Running 0 29s 10.244.166.153 node1 <none> <none> deployment-nginx-5b45c89ccd-nqvnh 1/1 Running 0 29s 10.244.166.151 node1 <none> <none> deployment-nginx-5b45c89ccd-nwss7 1/1 Running 0 29s 10.244.166.150 node1 <none> <none> deployment-nginx-5b45c89ccd-p8wc2 1/1 Running 0 29s 10.244.166.152 node1 <none> <none>
此时我们看到pod全部创建在node1上,我们再将容忍Tolerations加到yaml里看一下
[root@master ~]# cat deployment1.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deployment-nginx namespace: default spec: replicas: 5 selector: matchLabels: app: nginx template: metadata: name: mypod namespace: default labels: app: nginx spec: containers: - name: nginx image: nginx tolerations: #容忍 - key: "node" #匹配的污点键名,必须与node设置的保持一致 operator: "Equal" #容忍的关系,Equal表示必须与Taine的value保持一致,Exists可以不指定value。 value: "nginx" #匹配的污点键值,必须与node设置保持一致 effect: "NoSchedule" #匹配的污点属性,必须与node设置保持一致
说明:
当operator不设置时默认值为Equal,当operator设置为Exists时,空的key能匹配所有的键和值表示容忍所有的污点key值,空的effect能匹配所有的规则,表示容忍所有的污点规则。
执行yaml查看pod
[root@master ~]# kubectl apply -f deployment1.yaml deployment.apps/deployment-nginx created [root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES deployment-nginx-5db6cb544c-49fvf 1/1 Running 0 27s 10.244.166.154 node1 <none> <none> deployment-nginx-5db6cb544c-4z82r 1/1 Running 0 27s 10.244.166.156 node1 <none> <none> deployment-nginx-5db6cb544c-m57w8 1/1 Running 0 27s 10.244.104.24 node2 <none> <none> deployment-nginx-5db6cb544c-mswfk 1/1 Running 0 27s 10.244.104.23 node2 <none> <none> deployment-nginx-5db6cb544c-xfsnm 1/1 Running 0 27s 10.244.166.155 node1 <none> <none>
查看pod发现仍然会有pod调度到node2节点,是因为虽然node2上有污点,但是我们设置了容忍,允许有pod创建到有污点的node2上面。
当运行的K8s集群中需要维护单独的node时,我们可以将这个node打上一个NoExecute污点,这样没有设置容忍的pod就会被驱离,便于我们的维护。
以上面的yaml为例,我们在node1上面打一个NoExecute污点:
[root@master ~]# kubectl taint node node1 pod=status:NoExecute node/node1 tainted [root@master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES deployment-nginx-5db6cb544c-jxsqc 0/1 ContainerCreating 0 18s <none> node2 <none> <none> deployment-nginx-5db6cb544c-m57w8 1/1 Running 0 24m 10.244.104.24 node2 <none> <none> deployment-nginx-5db6cb544c-m9rm9 0/1 ContainerCreating 0 18s <none> node2 <none> <none> deployment-nginx-5db6cb544c-mswfk 1/1 Running 0 24m 10.244.104.23 node2 <none> <none> deployment-nginx-5db6cb544c-ztl2h 0/1 ContainerCreating 0 18s <none> node2 <none> <none>
当我们在node1打上NoExecute污点后,可以看到之前在node1的pod全部被驱离,由于pod类型时deployment,所以又在node2节点创建了新的pod,保证了副本数的完整性。
如果要删除taint,只需要在后面加上“-”符号就可以了
[root@master ~]# kubectl taint node node1 pod=status:NoExecute-
node/node1 untainted
Pod Priority Preemption:Pod优先级调度
对于运行各种负载(如Service、Job)的中等规模或者大规模的集群来说,出于各种原因,我们需要尽可能提高集群的资源利用率。而提高资源利用率的常规做法是采用优先级方案,即不同类型的负载对应不同的优先级,同时允许集群中的所有负载所需的资源总量超过集群可提供的资源,在这种情况下,当发生资源不足的情况时,系统可以选择释放一些不重要的负载(优先级最低的),保障最重要的负载能够获取足够的资源稳定运行。
优先级抢占调度策略的核心行为分别是驱逐(Eviction)与抢占(Preemption),这两种行为的使用场景不同,效果相同。Eviction是kubelet进程的行为,即当一个Node发生资源不足(under resource pressure)的情况时,该节点上的kubelet进程会执行驱逐动作,此时Kubelet会综合考虑Pod的优先级、资源申请量与实际使用量等信息来计算哪些Pod需要被驱逐;当同样优先级的Pod需要被驱逐时,实际使用的资源量超过申请量最大倍数的高耗能Pod会被首先驱逐。对于QoS等级为“Best Effort”的Pod来说,由于没有定义资源申请(CPU/Memory Request),所以它们实际使用的资源可能非常大。Preemption则是Scheduler执行的行为,当一个新的Pod因为资源无法满足而不能被调度时,Scheduler可能(有权决定)选择驱逐部分低优先级的Pod实例来满足此Pod的调度目标,这就是Preemption机制。
Pod优先级调度示例如下。
首先,由集群管理员创建PriorityClasses,PriorityClass不属于任何命名空间:
apiVersion: scheduling.k8s.io/v1beta1 kind: PriorityClass metadata: name: high-priority value: 100000 globaDefault: false description: "This priority class should be used for XYZ service pods only"
上述YAML文件定义了一个名为high-priority的优先级类别,优先级为100000,数字越大,优先级越高,超过一亿的数字被系统保留,用于指派给系统组件。
我们可以在任意Pod中引用上述Pod优先级类别:
apiVersion: v1 kind: Pod metadata: name: nginx lables: env: test spec: containers: - name: nginx image: nginx imagePullPolicy: IfNotPresent priorityClasses: high-priority
如果发生了需要抢占的调度,高优先级Pod就可能抢占节点N,并将其低优先级Pod驱逐出节点N,高优先级Pod的status信息中的nominatedNodeName字段会记录目标节点N的名称。需要注意,高优先级Pod仍然无法保证最终被调度到节点N上,在节点N上低优先级Pod被驱逐的过程中,如果有新的节点满足高优先级Pod的需求,就会把它调度到新的Node上。而如果在等待低优先级的Pod退出的过程中,又出现了优先级更高的Pod,调度器将会调度这个更高优先级的Pod到节点N上,并重新调度之前等待的高优先级Pod。
最后要指出一点:使用优先级抢占的调度策略可能会导致某些Pod永远无法被成功调度。因此优先级调度不但增加了系统的复杂性,还可能带来额外不稳定的因素。因此,一旦发生资源紧张的局面,首先要考虑的是集群扩容,如果无法扩容,则再考虑有监管的优先级调度特性,比如结合基于Namespace的资源配额限制来约束任意优先级抢占行为。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
2021-08-06 Ansible的hoc命令行以及常用模块