前言

当api-server处理完一个pod的创建请求后,此时可以通过kubectl把pod get出来,但是pod的状态是Pending。在这个Pod能运行在节点上之前,它还需要经过scheduler的调度,为这个pod选择合适的节点运行。调度的整理流程如下图所示

本篇阅读源码版本1.19

调度的流程始于Scheduler的scheduleOne方法,它在Scheduler的Run方法里被定时调用

代码位于/pkg/scheduler/scheduler.go

  1. func (sched *Scheduler) Run(ctx context.Context) {
  2. sched.SchedulingQueue.Run()
  3. wait.UntilWithContext(ctx, sched.scheduleOne, 0)
  4. sched.SchedulingQueue.Close()
  5. }
  6. func (sched *Scheduler) scheduleOne(ctx context.Context) {
  7. //获取需要调度的pod
  8. podInfo := sched.NextPod()
  9. ...
  10. //执行节点预选,节点优选,节点选择这些操作
  11. scheduleResult, err := sched.Algorithm.Schedule(...)
  12. //
  13. //异步执行节点绑定操作
  14. go fun(){
  15. err := sched.bind(bindingCycleCtx, prof, assumedPod, scheduleResult.SuggestedHost, state)
  16. }()
  17. }

进入Scheduler.scheduleOne从sched.NextPod里拿到需要调度的pod,sched.NextPod的来源是pod的Informer。以一个Controller的模式收到api pod的变更后放入Queue中,sched.NextPod充当消费者将Pod从Queue取出执行调度流程

预选优化

预选优化实际是节点预算的一部分,位于执行预算优化的算法之前,优化操作为了解决集群规模过大时,过多地执行预算算法而耗费性能。优化操作就是计算出筛选出预选节点与集群总结点数达到一个比例值时,就停止执行预选算法,将这批节点拿去执行节点优选。以这种方式减少算法执行次数,这个比例默认是50%,如果设置成100则视为不启动预选优化

代码的调用链如下

  1. sched.scheduleOne
  2. |--sched.Algorithm.Schedule
  3. |==genericScheduler.Schedule /pkg/schduler/core/generic_scheduler.go
  4. |--g.findNodesThatFitPod
  5. |--g.findNodesThatPassFilters
  6. |--g.numFeasibleNodesToFind ##预选优化

预选优化的方法,代码位于 /pkg/schduler/core/generic_scheduler.go

  1. func (g *genericScheduler) numFeasibleNodesToFind(numAllNodes int32) (numNodes int32) {
  2. if numAllNodes < minFeasibleNodesToFind || g.percentageOfNodesToScore >= 100 {
  3. return numAllNodes
  4. }
  5. adaptivePercentage := g.percentageOfNodesToScore
  6. if adaptivePercentage <= 0 {
  7. basePercentageOfNodesToScore := int32(50)
  8. adaptivePercentage = basePercentageOfNodesToScore - numAllNodes/125
  9. if adaptivePercentage < minFeasibleNodesPercentageToFind { //minFeasibleNodesPercentageToFind是常量,值为5
  10. adaptivePercentage = minFeasibleNodesPercentageToFind
  11. }
  12. }
  13. numNodes = numAllNodes * adaptivePercentage / 100
  14. if numNodes < minFeasibleNodesToFind { //minFeasibleNodesToFind是常量,值为100
  15. return minFeasibleNodesToFind
  16. }
  17. return numNodes
  18. }

由上述代码可得当集群规模小于100个节点时不进行预选优化,预选优化的最小值就是100。当percentageOfNodesToScore设置成非正数时,会通过公式50-numAllNodes/125 算出,得出的值如果小于5则强制提升成5,即比例的最小值是5。

节点预选

当执行完预选优化后就会执行节点预选,节点预选主要是执行一个函数,判定节点是否符合条件,不符合的节点就会被筛选掉,只有符合的节点才会留下来作下一步的节点优选

代码位于 /pkg/schduler/core/generic_scheduler.go

  1. func (g *genericScheduler) findNodesThatPassFilters(...) ([]*v1.Node, error) {
  2. //获取节点
  3. allNodes, err := g.nodeInfoSnapshot.NodeInfos().List()
  4. //预选优化
  5. numNodesToFind := g.numFeasibleNodesToFind(int32(len(allNodes)))
  6. checkNode := func(i int) {
  7. //在此进去执行节点预选
  8. fits, status, err := PodPassesFiltersOnNode(ctx, prof.PreemptHandle(), state, pod, nodeInfo)
  9. if fits {
  10. length := atomic.AddInt32(&feasibleNodesLen, 1)
  11. if length > numNodesToFind {
  12. cancel()
  13. atomic.AddInt32(&feasibleNodesLen, -1)
  14. }
  15. }
  16. }
  17. parallelize.Until(ctx, len(allNodes), checkNode)
  18. }

调用g.nodeInfoSnapshot.NodeInfos().List()就是获取从node Informer中获取到的集群节点信息,执行节点预选的函数在一个局部函数checkNode中被调用,该函数是被并发执行,当筛选出的节点数达到预选优化获取的值时,就会取消并发操作。

PodPassesFiltersOnNode函数的定义如下

  1. func PodPassesFiltersOnNode(...){
  2. for i := 0; i < 2; i++ {
  3. if i == 0 {
  4. var err error
  5. podsAdded, stateToUse, nodeInfoToUse, err = addNominatedPods(ctx, ph, pod, state, info)
  6. if err != nil {
  7. return false, nil, err
  8. }
  9. } else if !podsAdded || !status.IsSuccess() {
  10. break
  11. }
  12. statusMap := ph.RunFilterPlugins(ctx, stateToUse, pod, nodeInfoToUse)
  13. status = statusMap.Merge()
  14. if !status.IsSuccess() && !status.IsUnschedulable() {
  15. return false, status, status.AsError()
  16. }
  17. }
  18. }

这个预选算法会有可能执行两次,这个跟Preempt抢占机制有关系,第一次执行是尝试加上NominatedPod执行节点预选,NominatedPod指的是那些优先级比当前pod高且实际上还没调度到本节点上的pod,执行这个主要是为了考虑pod的亲和性与反亲和性这种场景的高级调度,第二次则不加NominatedPod,两次都能通过的才算是通过了节点预选。当然当前节点没有NominatedPod,就执行一次算法就够了。

从PodPassesFiltersOnNode函数到遍历执行各个预选算法仍需要多层调用,以下是调用链

  1. PodPassesFiltersOnNode
  2. |--ph.RunFilterPlugins
  3. |==frameworkImpl.RunFilterPlugins /pkg/scheduler/runtime/framework.go
  4. |--frameworkImpl.runFilterPlugin
  5. |--pl.Filter ##节点预选

节点预选算法

节点预选算法有以下几种

算法名称 功能
GeneralPredicates 包含3项基本检查: 节点、端口和规则
NoDiskConflict 检查Node是否可以满足Pod对硬盘的需求
NoVolumeZoneConflict 单集群跨AZ部署时,检查node所在的zone是否能满足Pod对硬盘的需求
MaxEBSVolumeCount 部署在AWS时,检查node是否挂载了太多EBS卷
MaxGCEPDVolumeCount 部署在GCE时,检查node是否挂载了太多PD卷
PodToleratesNodeTaints 检查Pod是否能够容忍node上所有的taints
CheckNodeMemoryPressure 当Pod QoS为besteffort时,检查node剩余内存量, 排除内存压力过大的node
MatchInterPodAffinity 检查node是否满足pod的亲和性、反亲和性需求

节点优选

节点优选是从节点预选筛选后的节点执行优选算法算分,汇聚出来的总分供后续“节点选定”时选择。

调用链如下

  1. sched.scheduleOne
  2. |--sched.Algorithm.Schedule
  3. |==genericScheduler.Schedule /pkg/schduler/core/generic_scheduler.go
  4. |--g.prioritizeNodes
  5. | |--prof.RunScorePlugins
  6. | |==frameworkImpl.RunScorePlugins /pkg/scheduler/runtime/framework.go
  7. | | |--f.runScorePlugin
  8. | | | |--pl.Score ##节点优选
  9. | | |--nodeScoreList[i].Score = nodeScore.Score * int64(weight)
  10. | |--result[i].Score += scoresMap[j][i].Score

关键函数定义如下,代码位于/pkg/scheduler/runtime/framework.go

  1. func (f *frameworkImpl) RunScorePlugins(...) (ps framework.PluginToNodeScores, status *framework.Status) {
  2. //初始化存放分数的map
  3. pluginToNodeScores := make(framework.PluginToNodeScores, len(f.scorePlugins))
  4. for _, pl := range f.scorePlugins {
  5. pluginToNodeScores[pl.Name()] = make(framework.NodeScoreList, len(nodes))
  6. }
  7. //按预选结果的node并行执行优选算法,得出每个节点分别在各个优选算法下的分数
  8. // Run Score method for each node in parallel.
  9. parallelize.Until(ctx, len(nodes), func(index int) {
  10. for _, pl := range f.scorePlugins {
  11. nodeName := nodes[index].Name
  12. //
  13. s, status := f.runScorePlugin(ctx, pl, state, pod, nodeName)
  14. pluginToNodeScores[pl.Name()][index] = framework.NodeScore{
  15. Name: nodeName,
  16. Score: s,
  17. }
  18. }
  19. })
  20. //并行计算每个节点每个插件加权后的分数
  21. parallelize.Until(ctx, len(f.scorePlugins), func(index int) {
  22. pl := f.scorePlugins[index]
  23. // Score plugins' weight has been checked when they are initialized.
  24. weight := f.pluginNameToWeightMap[pl.Name()]
  25. nodeScoreList := pluginToNodeScores[pl.Name()]
  26. for i, nodeScore := range nodeScoreList {
  27. nodeScoreList[i].Score = nodeScore.Score * int64(weight)
  28. }
  29. })
  30. }

节点优选的临时结果是存放在一个map[优选算法名]map[节点名]分数这样的二重map中,并行计算时是每一个节点顺序执行所有优选插件,然后存放在临时map中。优选计算完毕后再并行计算各个分数加权后的值,所有分数的汇总在RunScorePlugins的调用者genericScheduler.prioritizeNodes处执行

代码位于/pkg/schduler/core/generic_scheduler.go

  1. func (g *genericScheduler) prioritizeNodes(...){
  2. result := make(framework.NodeScoreList, 0, len(nodes))
  3. for i := range nodes {
  4. result = append(result, framework.NodeScore{Name: nodes[i].Name, Score: 0})
  5. for j := range scoresMap {
  6. result[i].Score += scoresMap[j][i].Score
  7. }
  8. }
  9. }

优选算法

节点优选算法有如下几种

算法名称 功能
LeastRequestedPriority 按node计算资源(CPU/MEM)剩余量排序,挑选最空闲的node
BalancedResourceAllocation 补充LeastRequestedPriority,在cpu和mem的剩余量取平衡
SelectorSpreadPriority 同一个Service/RC下的Pod尽可能的分散在集群中。 Node上运行的同个Service/RC下的Pod数目越少,分数越高。
NodeAffinityPriority 按soft(preferred) NodeAffinity规则匹配情况排序,规则命中越多,分数越高
TaintTolerationPriority 按pod tolerations与node taints的匹配情况排序,越多的taints不匹配,分数越低
InterPodAffinityPriority 按soft(preferred) Pod Affinity/Anti-Affinity规则匹配情况排序,规则命中越多,分数越高/低

节点选定

节点选定是根据节点优选的结果求出总分最大值节点,当遇到分数相同的时候则通过随机方式选出一个节点

代码位于/pkg/schduler/core/generic_scheduler.go

  1. func (g *genericScheduler) selectHost(nodeScoreList framework.NodeScoreList) (string, error) {
  2. if len(nodeScoreList) == 0 {
  3. return "", fmt.Errorf("empty priorityList")
  4. }
  5. maxScore := nodeScoreList[0].Score
  6. selected := nodeScoreList[0].Name
  7. cntOfMaxScore := 1
  8. for _, ns := range nodeScoreList[1:] {
  9. if ns.Score > maxScore {
  10. maxScore = ns.Score
  11. selected = ns.Name
  12. cntOfMaxScore = 1
  13. } else if ns.Score == maxScore {
  14. cntOfMaxScore++
  15. if rand.Intn(cntOfMaxScore) == 0 {
  16. // Replace the candidate with probability of 1/cntOfMaxScore
  17. selected = ns.Name
  18. }
  19. }
  20. }
  21. return selected, nil
  22. }

Preempt抢占

Preempt抢占是发生在调用sched.Algorithm.Schedule失败,错误是core.FitError时

  1. func (sched *Scheduler) scheduleOne(ctx context.Context) {
  2. scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, prof, state, pod)
  3. if err != nil {
  4. nominatedNode := ""
  5. if fitError, ok := err.(*core.FitError); ok {
  6. if !prof.HasPostFilterPlugins() {
  7. } else {
  8. // Run PostFilter plugins to try to make the pod schedulable in a future scheduling cycle.
  9. result, status := prof.RunPostFilterPlugins(ctx, state, pod, fitError.FilteredNodesStatuses)
  10. if status.IsSuccess() && result != nil {
  11. nominatedNode = result.NominatedNodeName
  12. }
  13. }
  14. sched.recordSchedulingFailure(prof, podInfo, err, v1.PodReasonUnschedulable, nominatedNode)
  15. return
  16. }

从代码看出,即便抢占成功了,pod也不会马上调度到对应节点,而是重新入队,祈求下次调度时能调度成功

调度的关键方法是DefaultPreemption.preempt,从prof.RunPostFilterPlugins经过多层调用到达,调用链如下

  1. |--prof.RunPostFilterPlugins
  2. |==frameworkImpl.RunPostFilterPlugins /pkg/scheduler/runtime/framework.go
  3. | |--f.runPostFilterPlugin
  4. | |--pl.PostFilter
  5. | |==DefaultPreemption.PostFilter /pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go
  6. | |--pl.preempt ##preempt抢占

抢占流程包含以下几点

  1. 拿最新版本的pod,刷新lister的缓存
  2. 确保抢占者有资格抢占其他Pod
  3. 寻找抢占候选者
  4. 与注册扩展器进行交互,以便在需要时筛选出某些候选者。
  5. 选出最佳的候选者
  6. 在提名选定的候选人之前,先进行准备工作。

    方法简略如下,代码位于/pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go
  1. func (pl *DefaultPreemption) preempt(...) (string, error) {
  2. cs := pl.fh.ClientSet()
  3. ph := pl.fh.PreemptHandle()
  4. nodeLister := pl.fh.SnapshotSharedLister().NodeInfos()
  5. //1.拿最新版本的pod,刷新lister的缓存
  6. pod, err := pl.podLister.Pods(pod.Namespace).Get(pod.Name)
  7. //2.确保抢占者有资格抢占其他Pod
  8. //看pod是否已有历史的抢占记录pod.Status.NominatedNodeName,
  9. // 无则直接通过
  10. // 有则需要检查是否该node上有优先级比当前小且正在被删除的pod,估计遇到这个不用抢下次有机会能调度到此节点上
  11. if !PodEligibleToPreemptOthers(pod, nodeLister, m[pod.Status.NominatedNodeName]) {
  12. }
  13. //3.寻找抢占候选者
  14. // 候选者的结构是nodeName+(牺牲的pod+PBD数量)数组
  15. // 利用预选算法模拟计算各个节点优先级低的pod是否影响调度从而判定能否牺牲,及牺牲中PBD的数量
  16. // 候选者的结构是nodeName+牺牲的pod数组
  17. candidates, err := FindCandidates(ctx, cs, state, pod, m, ph, nodeLister, pl.pdbLister)
  18. //4.与注册扩展器进行交互,以便在需要时筛选出某些候选者。
  19. candidates, err = CallExtenders(ph.Extenders(), pod, nodeLister, candidates)
  20. //5.选出最佳的候选者
  21. //选择标准如下
  22. //1.选择一个PBD违规数量最少的
  23. //2.选择一个包含最高优先级牺牲者最小的
  24. //3.所有牺牲者的优先级联系被打破
  25. //4.联系仍存在,最少牺牲者的
  26. //5.联系仍存在,拥有所有最高优先级的牺牲者最迟才启动的
  27. //6.联系仍存在,经排序或随机后,第一个节点
  28. bestCandidate := SelectCandidate(candidates)
  29. //6.在提名选定的候选人之前,先进行准备工作。
  30. //驱逐(实际上是删掉)牺牲pod并拒绝他们再调到本节点上
  31. //把比本pod优先级低的Nominated也清掉,更新这些pod的status信息
  32. if err := PrepareCandidate(bestCandidate, pl.fh, cs, pod); err != nil {
  33. }
  34. return bestCandidate.Name(), nil
  35. }

由于篇幅限制,这部分的逻辑暂不细说,对应函数包含的操作可参考上面的注释

Bind绑定

最终选出节点后,需要client-go往api更新pod资源,为其填上节点的值,这个操作就是Bind,而且不是单纯调用client-go的update方法,而是client-go针对pod特有的Bind方法

代码的调用链如下

  1. sched.scheduleOne /pkg/scheduler/scheduler.go
  2. |--sched.bind
  3. |--prof.RunBindPlugins
  4. |==frameworkImpl.RunBindPlugins /pkg/scheduler/runtime/framework.go
  5. |--f.runBindPlugin
  6. |--bp.Bind
  7. |==DefaultBinder.Bind /pkg/scheduler/framework/plugins/defaultbinder/default_binder.go

最终执行Bind操作的是在DefaultBinder.Bind方法。在方法中可看到声明了一个Binding的资源,执行Pod的Bind方法将Pod资源以一种特殊的方式更新,代码位于/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go

  1. func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) *framework.Status {
  2. klog.V(3).Infof("Attempting to bind %v/%v to %v", p.Namespace, p.Name, nodeName)
  3. binding := &v1.Binding{
  4. ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID},
  5. Target: v1.ObjectReference{Kind: "Node", Name: nodeName},
  6. }
  7. err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})
  8. if err != nil {
  9. return framework.AsStatus(err)
  10. }
  11. return nil
  12. }

小结

本篇讲述了scheduler执行调度Pod的流程,主要概括成以下几点

1 预选优化:防止节点多导致预选性能下降,默认开启并设置默认为50%,预选到此比例数量节点后就开始优选

2 节点预选:基于一系列的预选规则对每个节点进行检查,将那些不符合条件的节点过滤,从而完成节点的预选。有可能执行两次算法,原因Preempt资源抢占及pod的亲缘性有关(这个结论要再看代码)

3 节点优选:对预选出的节点进行优先级排序,以便选出最合适运行Pod对象的节点

4 节点选定:从优先级排序结果中挑选出优先级最高的节点运行Pod,当这类节点多于1个时,则进行随机选择

5 Preempt抢占机制:从预选中选潜在节点,找出被抢节点,列出被驱逐Pod与低优先级NominatedPod,驱逐并清除

6 Bind绑定:创建bind资源,调用client-go pod的Bind方法

整个调用链如下所示

  1. Run /cmd/kube-scheduler/app/server.go
  2. |--sched.Run /pkg/scheduler/scheduler.go
  3. |--sched.scheduleOne
  4. |--sched.Algorithm.Schedule
  5. |==genericScheduler.Schedule /pkg/schduler/core/generic_scheduler.go
  6. | |--g.findNodesThatFitPod
  7. | | |--g.nodeInfoSnapshot.NodeInfos().List()
  8. | | |--g.findNodesThatPassFilters
  9. | | |--g.numFeasibleNodesToFind ##预选优化
  10. | | |--PodPassesFiltersOnNode
  11. | | |--ph.RunFilterPlugins
  12. | | |==frameworkImpl.RunFilterPlugins /pkg/scheduler/runtime/framework.go
  13. | | |--frameworkImpl.runFilterPlugin
  14. | | |--pl.Filter ##节点预选
  15. | |--g.prioritizeNodes
  16. | | |--prof.RunScorePlugins
  17. | | |==frameworkImpl.RunScorePlugins /pkg/scheduler/runtime/framework.go
  18. | | | |--f.runScorePlugin
  19. | | | | |--pl.Score ##节点优选
  20. | | | |--nodeScoreList[i].Score = nodeScore.Score * int64(weight)
  21. | | |--result[i].Score += scoresMap[j][i].Score
  22. | |--g.selectHost
  23. |--prof.RunPostFilterPlugins
  24. |==frameworkImpl.RunPostFilterPlugins /pkg/scheduler/runtime/framework.go
  25. | |--f.runPostFilterPlugin
  26. | |--pl.PostFilter
  27. | |==DefaultPreemption.PostFilter /pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go
  28. | |--pl.preempt ##preempt抢占
  29. |--sched.recordSchedulingFailure ##记录抢占结果,结束本轮调度
  30. |--sched.bind
  31. |--prof.RunBindPlugins
  32. |==frameworkImpl.RunBindPlugins /pkg/scheduler/runtime/framework.go
  33. |--f.runBindPlugin
  34. |--bp.Bind
  35. |==DefaultBinder.Bind /pkg/scheduler/framework/plugins/defaultbinder/default_binder.go

scheduler调度完毕后,到kubelet将pod启起来,如有兴趣可阅读鄙人先前发的拙作《kubelet源码分析——启动Pod

如有兴趣可阅读鄙人“k8s源码之旅”系列的其他文章

kubelet源码分析——kubelet简介与启动

kubelet源码分析——启动Pod

kubelet源码分析——关闭Pod

scheduler源码分析——调度流程的更多相关文章

  1. apiserver源码分析——启动流程

    前言 apiserver是k8s控制面的一个组件,在众多组件中唯一一个对接etcd,对外暴露http服务的形式为k8s中各种资源提供增删改查等服务.它是RESTful风格,每个资源的URI都会形如 / ...

  2. scheduler源码分析——preempt抢占

    前言 之前探讨scheduler的调度流程时,提及过preempt抢占机制,它发生在预选调度失败的时候,当时由于篇幅限制就没有展开细说. 回顾一下抢占流程的主要逻辑在DefaultPreemption ...

  3. 鸿蒙内核源码分析(调度故事篇) | 用故事说内核调度 | 百篇博客分析OpenHarmony源码 | v9.07

    百篇博客系列篇.本篇为: v09.xx 鸿蒙内核源码分析(调度故事篇) | 用故事说内核调度过程 | 51.c.h .o 前因后果相关篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 ...

  4. 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 百篇博客分析OpenHarmony源码 | v7.07

    百篇博客系列篇.本篇为: v07.xx 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...

  5. 分布式调度平台XXL-JOB源码分析-调度中心

    架构图 上图是我们要进行源码分析的2.1版本的整体架构图.其分为两大块,调度中心和执行器,本文先分析调度中心,也就是xxl-job-admin这个包的代码. 关键bean 在application.p ...

  6. scrapy-redis(调度器Scheduler源码分析)

    settings里面的配置:'''当下面配置了这个(scrapy-redis)时候,下面的调度器已经配置在scrapy-redis里面了'''##########连接配置######## REDIS_ ...

  7. Hadoop学习之--Capaycity Scheduler源码分析

    Capacity Scheduler调度策略当一个新的job是否允许添加到队列中进行初始化,判断当前队列和用户是否已经达到了初始化数目的上限,下面就从代码层面详细介绍整个的判断逻辑.Capaycity ...

  8. struts2源码分析-初始化流程

    这一篇文章主要是记录struts.xml的初始化,还原struts2.xml的初始化流程.源码依据struts2-2.3.16.3版本. struts2初始化入口,位于web.xml中: <fi ...

  9. django源码分析 请求流程

    一.从浏览器发出一个请求,到返回响应内容,这个过程是怎么样的? 1. 浏览器解析输入的url 2. 查找url对应的ip地址 3. 通过ip地址访问我们的服务器 1.  请求进入wsgi服务器(我在这 ...

随机推荐

  1. MongoDB学习笔记一(MongoDB介绍 + 基本指令 + 查询语句)

    什么是MongoDB MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统. 在高负载的情况下,添加更多的节点,可以保证服务器性能. MongoDB 旨在为WEB应用提供可扩 ...

  2. 深入Pulsar Consumer的使用方式&源码分析

    原文链接 1.使用前准备 引入依赖: <dependency> <groupId>org.apache.pulsar</groupId> <artifactI ...

  3. 刷题-力扣-230. 二叉搜索树中第K小的元素

    230. 二叉搜索树中第K小的元素 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/kth-smallest-element-in-a ...

  4. kubebuilder实战之七:webhook

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  5. C#多线程开发-线程基础 01

    最近由于工作的需要,一直在使用C#的多线程进行开发,其中也遇到了很多问题,但也都解决了.后来发觉自己对于线程的知识和运用不是很熟悉,所以将利用几篇文章来系统性的学习汇总下C#中的多线程开发. 线程基础 ...

  6. 约瑟夫环问题详解 (c++)

    问题描述: 已知n个人(以编号0,2,3...n-1分别表示)围坐在一起.从编号为0的人开始报数,数到k的那个人出列:他的下一个人又从1开始报数,数到k的那个人又出列:依此规律重复下去,直到圆桌周围的 ...

  7. 将数据保存到excel文件(纯前端实现)

    // 导出excel文件 /** * 依赖: import XLSX from 'xlsx' */ let obj = { '学生信息表': [ ['姓名', '性别', '年龄', '分数'], [ ...

  8. MySQL实战45讲(10--15)-笔记

    11 | 怎么给字符串字段加索引? 维护一个支持邮箱登录的系统,用户表是这么定义的: mysql> create table SUser( ID bigint unsigned primary ...

  9. 20210720 noip21

    又是原题,写下题解吧 Median 首先时限有 2s(学校评测机太烂,加到 4s 了),可以放心地筛 \(1e7\) 个质数并算出 \(s_2\),然后问题变为类似滑动求中位数.发现 \(s_2\) ...

  10. k8s garbage collector分析(1)-启动分析

    k8s garbage collector分析(1)-启动分析 garbage collector介绍 Kubernetes garbage collector即垃圾收集器,存在于kube-contr ...