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的资源配额限制来约束任意优先级抢占行为。

最新文章

  1. PyAutoGUI-python版的autoit/AHK
  2. docker push 实现过程
  3. 【python游戏编程之旅】第八篇---pygame游戏开发常用数据结构
  4. 卸载已经安装的rpm包
  5. eclipse常用快捷键及调试方法(虽然现在看不懂,但是感觉以后肯定会用到,先转了)
  6. 剑指offer系列39-----矩阵中的路径
  7. 苏泊尔借助微软CRM提升客户满意度
  8. C#POP3协议实现SSL验证登陆GMAIL
  9. winform访问url传参有返回值
  10. PL/SQL Developer不安装客户端连接远程oracle数据库(转)
  11. JavaScript引擎研究与C、C++与互调用(转)
  12. mysql排序
  13. Ubuntu14.04上修改主机名
  14. Rails中rspec测试xxx_path调用失败的解决
  15. Codeforces 741 D - Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
  16. _ZNote_Qt_QDialog_修改button名称
  17. 【mysql】逗号分割字段的行列转换
  18. JQuery中的Ajax(六)
  19. Koala ===》编译工具 ==》Less和Sass
  20. Sql server中如何将表A和表B的数据合并(乘积方式)

热门文章

  1. Vulnhub之Credit_Card_Scammers靶场渗透
  2. 【爬虫+数据分析+数据可视化】python数据分析全流程《2021胡润百富榜》榜单数据!
  3. Ng-Matero v15 正式发布
  4. asp+vb.net解决调接口返回中文乱码问题
  5. day02-Promise
  6. 快速排序——C++左闭右开区间实现
  7. CSS文字超出宽度---换行总结
  8. Redis - 介绍与使用场景
  9. 基于Docker安装的Stable Diffusion使用CPU进行AI绘画
  10. Random概述和基本使用-Random生成指定范围的随机数