浅谈 Kubernetes Scheduling-Framework 插件的实现

最近几个月一直在研究 kubernetes 的 scheduling-framework 调度框架,发现还是十分有意思的,我自己也实现了一个基于 scheduling-framework 调度框架的自定义调度器,希望感兴趣的同学一起学习:https://github.com/NJUPT-ISL/Yoda-Scheduler

Scheduling-framework 调度框架

Kubernetes 的 scheduling-framework 调度框架(以下简称调度框架)是针对当前 kubernetes 调度器的增强,它不同于之前的 scheduler-extender,用户可以编写多个插件,这些插件可以在调度的不同阶段作为原有调度器的扩展,并且这些插件会和 kubernetes 原有的调度器源代码会一起编译到调度程序中。

调度框架设计目标

  • 增强 kubernetes 原有调度器的可扩展性。

  • 通过将调度程序的某些功能移至插件,可以简化调度程序核心。

  • 调度框架中可设置多个扩展点。

  • 调度框架通过插件机制来接收插件结果,并根据接收到的结果继续或中止。

  • 提出一种处理错误并将其与插件进行通信的机制。

Proposal

调度框架在 kubernetes 调度器中定义了很多 Go 的接口和 Go API,用于用户设计插件使用。这些用户设计的插件将会被添加到调度程序中,并在编译时包含在内。可以通过配置调度程序的 ComponentConfig 将允许启用、禁用和重新排序插件。自定义调度程序可以“ 在树外 ” 编写其插件并编译包含其自己的插件的调度程序二进制文件。

调度周期和绑定周期

调度器调度一个 Pod 的过程分为两个阶段:调度周期绑定周期

在调度周期中,调度器会为 Pod 选择一个最合适它运行的节点,然后调度过程将进入绑定周期。

在绑定周期中,调度器会检测调度周期中选中的那个“最合适的节点”是不是真的可以让这个 Pod 稳定的运行(比如检测 PV、检测是否有端口冲突等),或者需不需要做一些初始化操作(比如设置这个节点上的 FPGA 板子的状态、设置 GPU 显卡的驱动版本、CUDA 的版本等)。

扩展点

kubernetes 调度框架在调度周期和绑定周期都为我们提供了丰富的扩展点,这些扩展点可以“插上”我们自己设计的调度插件,一个插件可以在多个扩展点注册以执行更复杂或有状态的任务,实现我们想要的调度功能:

下面阐述下各个扩展点可以实现的功能。

Sort 排序

排序扩展点,由于调度器是按照 FIFO 的顺序调度 Pod 的,因此当队列里出现多个等待调度的 Pod 时,可以对这些 Pod 的先后顺序进行排序,把我们想要的 Pod(可能优先级比较高)往出队方向移动,让它可以更快地被调度。

目前的 Sort 扩展点只能启用一个,不可以启用多个 Sort 扩展插件。

我们可以看下 Sort 的接口,代码位于 kubernetes 项目的 /pkg/scheduler/framework/interface.go 中:

type QueueSortPlugin interface {Plugin// Less are used to sort pods in the scheduling queue.Less(*PodInfo, *PodInfo) bool
}

也就是只需要实现 Less 方法即可,比如如下的实现:

func Less(podInfo1, podInfo2 *framework.PodInfo) bool {return GetPodPriority(podInfo1) > GetPodPriority(podInfo2)
}

Pre-filter 预过滤

该扩展点用于预处理有关 Pod 的信息,或检查集群或 Pod 必须满足的某些条件。预过滤器插件应实现 PreFilter 函数,如果 PreFilter 返回错误,则调度周期将中止。注意,在每个调度周期中,只会调用一次 PreFilter。

Pre-filter 插件可以选择实现 PreFilterExtensions 接口,这个接口定义了 AddPodRemovePod 方法以增量方式修改其预处理信息。

type PreFilterPlugin interface {PluginPreFilter(ctx context.Context, state *CycleState, p *v1.Pod) *StatusPreFilterExtensions() PreFilterExtensions
}

这里的 CycleState ,表示调度的上下文,其实是一个 map 的封装,结构体内部通过读写锁实现了并发安全,开发者可以通过 CycleState 来实现多个调度插件直接的数据传递,也就是多个插件可以共享状态或通过此机制进行通信。

 type CycleState struct {mx      sync.RWMutexstorage map[StateKey]StateDatarecordFrameworkMetrics bool// 该值为 true, 则调度框架会记录此次调度周期的数据}

这里的 StateKey 是 string 类型,StateData 是一个接口类型:

 type StateData interface {// Clone is an interface to make a copy of StateData. For performance reasons,// clone should make shallow copies for members (e.g., slices or maps) that are not// impacted by PreFilter's optional AddPod/RemovePod methods.Clone() StateData}

我们可以做一个简单的接口实现,来实现 StateData:

 type Data struct {Value int64}func (s *Data) Clone() framework.StateData {c := &Data{Value: s.Value,}return c}

那么当插件在该扩展点想传递数据时就可以使用如下类似的代码实现数据的传递:

 Max := Data{Value: 0}state.Lock()state.Write(framework.StateKey("Max"), &Max)defer state.Unlock()

Filter 过滤

用于过滤不能满足当前被调度 Pod 运行需求的节点。对于每个节点,调度程序将按配置的顺序调用该类插件。如果有任何过滤器插件将节点标记为不可行,则不会为该节点调用其余插件。可以同时评估节点,并且在同一调度周期中可以多次调用 Filter 插件。这块其实是调度器会启动多个 go 协程以实现对多个节点并发调用 filter,来提高过滤效率。过滤插件其实类似于上一代 Kubernetes 调度器中的预选环节,即 Predicates。

我们看下接口定义:

type FilterPlugin interface {PluginFilter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *schedulernodeinfo.NodeInfo) *Status
}

我们可以对应的实现,比如我这里需要做 GPU 的调度,我需要检查每个节点的 GPU 是否满足 Pod 的运行要求:

func (y *Yoda) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, node *nodeinfo.NodeInfo) *framework.Status {klog.V(3).Infof("filter pod: %v, node: %v", pod.Name, node.Node().Name)// 检查节点 GPU 的健康状态if ok, msg := filter.CheckGPUHealth(node); ok {// 节点的 GPU 是否符合Pod 运行等级if !filter.PodFitsLevel(pod, node) {return framework.NewStatus(framework.Unschedulable, "Node:"+node.Node().Name+" GPU Level Not Fit")}// 节点的 GPU 显存是否符合 Pod 运行if !filter.PodFitsMemory(pod, node) {return framework.NewStatus(framework.Unschedulable, "Node:"+node.Node().Name+" GPU Memory Not Fit")}// 节点的 GPU 数量是否符合 Pod 运行if !filter.PodFitsNumber(pod, node) {return framework.NewStatus(framework.Unschedulable, "Node:"+node.Node().Name+" GPU Number Not Fit")}return framework.NewStatus(framework.Success, "")} else {return framework.NewStatus(framework.Unschedulable, "Node:"+node.Node().Name+msg)}
}

Pre-Score 预打分 (v1alpha1 版本称为 Post-Filter)

注意:Pre-Score 从 v1alpha2 开始可用。

该扩展点将使用通过 Filter 阶段的节点列表来调用插件。插件可以使用此数据来更新内部状态或生成日志、指标。比如可以通过该扩展点收集各个节点中性能指标,所有节点中最大的内存的节点,性能最好的 CPU 节点等。

我们继续来看接口里长什么样子(我这里是v1alpha1):

type PostFilterPlugin interface {PluginPostFilter(ctx context.Context, state *CycleState, pod *v1.Pod, nodes []*v1.Node, filteredNodesStatuses NodeToStatusMap) *Status
}

针对这个扩展点,通过传递的参数可以看出,接口传入了节点的切片,因此开发者可以通过启动多个并发协程来获取数据,并且可以把这些数据存在 CycleState 中,给之后的插件扩展点使用:

func (y *Yoda) PostFilter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node, filteredNodesStatuses framework.NodeToStatusMap) *framework.Status {klog.V(3).Infof("collect info for scheduling  pod: %v", pod.Name)return collection.ParallelCollection(collection.Workers, state, nodes, filteredNodesStatuses)
}

并发这块我们也可以参考 1.13 调度器中经常使用的经典并发模型:

func ParallelCollection(workers int, state *framework.CycleState, nodes []*v1.Node, filteredNodesStatuses framework.NodeToStatusMap) *framework.Status {var (stop <-chan struct{}mx   sync.RWMutexmsg  = "")// 数据存入管道pieces := len(Sum)toProcess := make(chan string, pieces)for _, v := range Sum {toProcess <- v}close(toProcess)// 并发协程数限制if pieces < workers {workers = pieces}wg := sync.WaitGroup{}wg.Add(workers)for i := 0; i < workers; i++ {go func() {// 协程消费管道数据for value := range toProcess {select {case <-stop:returndefault:// state 并发安全,调用的时候可以不用加锁if re := CollectMaxValue(value, state, nodes, filteredNodesStatuses); !re.IsSuccess() {klog.V(3).Infof(re.Message())mx.Lock()// message非并发安全,加锁msg += re.Message()mx.Unlock()}}}wg.Done()}()}wg.Wait()if msg != "" {return framework.NewStatus(framework.Error, msg)}return framework.NewStatus(framework.Success, "")
}

Score 打分

Score 扩展点和上一代的调度器的优选流程很像,它分为两个阶段:

  1. 第一阶段称为 “打分”,用于对已通过过滤阶段的节点进行排名。调度程序将为 Score 每个节点调用每个计分插件。

  2. 第二阶段是 “归一化”,用于在调度程序计算节点的最终排名之前修改分数,可以不实现, 但是需要保证 Score 插件的输出必须是 [MinNodeScore,MaxNodeScore][0-100]) 范围内的整数 。如果不是,则调度器会报错,你需要实现 NormalizeScore 来保证最后的得分范围。如果不实现 NormalizeScore,则 Score 的输出必须在此范围内。调度程序将根据配置的插件权重合并所有插件的节点分数。

看看接口的定义:

type ScorePlugin interface {Plugin// Score is called on each filtered node. It must return success and an integer// indicating the rank of the node. All scoring plugins must return success or// the pod will be rejected.Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)// ScoreExtensions returns a ScoreExtensions interface if it implements one, or nil if does not.ScoreExtensions() ScoreExtensions
}

我们也可以做如下简单的实现:

func (y *Yoda) Score(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) (int64, *framework.Status) {nodeInfo, err := y.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)if err != nil {return 0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))}s, err := score.Score(state, nodeInfo)if err != nil {return 0, framework.NewStatus(framework.Error, fmt.Sprintf("Score Node Error: %v", err))}klog.V(3).Infof("node : %v yoda-score: %v",nodeName,s)return s, framework.NewStatus(framework.Success, "")
}

如果最后的分数不在范围内,我们可能需要实现 NormalizeScore 函数做进一步处理:

func (y *Yoda) NormalizeScore(ctx context.Context, state *framework.CycleState, p *v1.Pod, scores framework.NodeScoreList) *framework.Status {var (highest int64 = 0)// 归一化 for i, nodeScore := range scores {scores[i].Score = nodeScore.Score * framework.MaxNodeScore / highest}return framework.NewStatus(framework.Success, "")
}

Reserve 保留

为给定的 Pod 保留节点上的资源时,维护运行时状态的插件可以应实现此扩展点,以由调度程序通知。这是在调度程序实际将 Pod 绑定到 Node 之前发生的,它的存在是为了防止在调度程序等待绑定成功时发生争用情况。

type ReservePlugin interface {Plugin// Reserve is called by the scheduling framework when the scheduler cache is// updated.Reserve(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
}

这里和上面的 Score 类似,函数并没有提供 nodeInfo 接口,我们可以通过调用 handle.SnapshotSharedLister 来获取节点的信息。

nodeInfo, err := y.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)

那么以上就是调度周期的插件与实现,其实绑定周期的插件实现和上述的方法也都类似,实现相关的函数即可。

插件注册

每个插件必须定义一个构造函数,并将其添加到硬编码的注册表中。

type PluginFactory = func(runtime.Unknown, FrameworkHandle) (Plugin, error)type Registry map[string]PluginFactoryfunc NewRegistry() Registry {return Registry{fooplugin.Name: fooplugin.New,barplugin.Name: barplugin.New,// New plugins are registered here.}
}

那么在编译的时候,编译器会将我们的插件和调度源码一起编译成我们的自定义调度器。

在声明插件的时候也需要实现构造函数和对应的方法:

type Yoda struct {args   *Argshandle framework.FrameworkHandle
}func (y *Yoda) Name() string {return Name
}func New(configuration *runtime.Unknown, f framework.FrameworkHandle) (framework.Plugin, error) {args := &Args{}if err := framework.DecodeInto(configuration, args); err != nil {return nil, err}klog.V(3).Infof("get plugin config args: %+v", args)return &Yoda{args:   args,handle: f,}, nil
}

编译小技巧

由于最终的调度器还是以容器的方式运行的,我们可以写一个 Makefile 来简化编译流程:

all: locallocal:GOOS=linux GOARCH=amd64 go build  -o=my-scheduler ./cmd/schedulerbuild:sudo docker build --no-cache . -t registry.cn-hangzhou.aliyuncs.com/my/schedulerpush:sudo docker push registry.cn-hangzhou.aliyuncs.com/my/schedulerformat:sudo gofmt -l -w .
clean:sudo rm -f my-scheduler

编写调度器的Dockerfile:

FROM debian:stretch-slimWORKDIR /COPY my-scheduler /usr/local/binCMD ["my-scheduler"]

那么编译 -> 构建就可以三步走了:

  • 编译

make local
  • 构建镜像

make build
  • 上传镜像

make push

自定义调度器的配置

首先需要设置一个 ConfigMap ,用于存放调度器的配置文件:

apiVersion: v1
kind: ConfigMap
metadata:name: scheduler-confignamespace: kube-system
data:scheduler-config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1alpha1kind: KubeSchedulerConfigurationschedulerName: yoda-schedulerleaderElection:leaderElect: truelockObjectName: yoda-schedulerlockObjectNamespace: kube-systemplugins:queueSort:enabled:- name: "yoda"filter:enabled:- name: "yoda"score:enabled:- name: "yoda"postFilter:enabled:- name: "yoda"pluginConfig:- name: "yoda"args: {"master": "master", "kubeconfig": "kubeconfig"}

这里主要需要修改的就是 schedulerName 字段的调度器名称和 plugins 字段中各个扩展点的插件名称,enable 才能保证该扩展点运行了你的插件。

接着为调度器创建 RBAC:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: yoda-cr
rules:- apiGroups:- ""resources:- endpoints- eventsverbs:- create- get- update- apiGroups:- ""resourceNames:- yoda-schedulerresources:- endpointsverbs:- delete- get- patch- update- apiGroups:- ""resources:- nodesverbs:- get- list- watch- apiGroups:- ""resources:- podsverbs:- delete- get- list- watch- update- apiGroups:- ""resources:- bindings- pods/bindingverbs:- create- apiGroups:- ""resources:- pods/statusverbs:- patch- update- apiGroups:- ""resources:- replicationcontrollers- servicesverbs:- get- list- watch- apiGroups:- apps- extensionsresources:- replicasetsverbs:- get- list- watch- apiGroups:- appsresources:- statefulsetsverbs:- get- list- watch- apiGroups:- policyresources:- poddisruptionbudgetsverbs:- get- list- watch- apiGroups:- ""resources:- persistentvolumeclaims- persistentvolumesverbs:- get- list- watch- apiGroups:- ""resources:- configmapsverbs:- get- list- watch- apiGroups:- "storage.k8s.io"resources:- storageclasses- csinodesverbs:- watch- list- get- apiGroups:- "coordination.k8s.io"resources:- leasesverbs:- create- get- list- update- apiGroups:- "events.k8s.io"resources:- eventsverbs:- create- patch- update
---
apiVersion: v1
kind: ServiceAccount
metadata:name: yoda-sanamespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: yoda-crbnamespace: kube-system
roleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: yoda-cr
subjects:- kind: ServiceAccountname: yoda-sanamespace: kube-system

最后配置调度器的 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:name: yoda-schedulernamespace: kube-systemlabels:component: yoda-scheduler
spec:replicas: 1selector:matchLabels:component: yoda-schedulertemplate:metadata:labels:component: yoda-schedulerspec:serviceAccount: yoda-sapriorityClassName: system-cluster-criticalvolumes:- name: scheduler-configconfigMap:name: scheduler-configcontainers:- name: yoda-schedulerimage: registry.cn-hangzhou.aliyuncs.com/geekcloud/yoda-schedulerimagePullPolicy: Alwaysargs:- yoda-scheduler- --config=/scheduler/scheduler-config.yaml- --v=3resources:requests:cpu: "50m"volumeMounts:- name: scheduler-configmountPath: /scheduler

随着云计算技术的不断发展,kubernetes scheduler 也在根据各种复杂的需求不断进化,未来也会涌现更多各种各样的丰富的、支持不同功能的调度器在不同的生产环境中发挥着更多强劲的作用,一起期待吧!

作者介绍

李俊江

kubernetes & istio member

南京邮电大学物联网学院研究生,热衷于 Kubernetes 与云原生相关技术。

微信:FUNKY-STARS 欢迎交流!

参考

  • Scheduling Framework

  • enhancements/624

  • scheduler-framework-sample

  • kubernetes 1.13 源码分析

致谢

感谢 Scheduler-SIG Leader HuangWei 大佬在 kubecon 2018 的 Q&A 和指导!

感谢张磊、车漾大佬在 kubecon 2018 的分享和讨论!

直播活动

ServiceMesher 社区联合 MOSN 社区推出的《云原生网络代理 MOSN 多协议机解析》直播,教你如何在 MOSN 中接入新的协议,实现不同 RPC 协议的代理,以方便 Service Mesh 扩展。查看详情:https://mosn.io/zh/blog/news/mosn-channel-1/

点击 阅读原文 查看更多

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311077.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++实现线段树(lazy-tag方法)-区间修改,区间查询

代码如下&#xff1a; #include <iostream> using namespace std; const int N 10010; typedef long long LL; LL input[N];struct node {int l, r;LL sum;LL add; } tree[4 * N];void build(int l, int r, int u) {tree[u].l l;tree[u].r r;if (l r) {tree[u].sum …

.NET Core开发实战(第25课:路由与终结点:如何规划好你的Web API)--学习笔记(上)...

25 | 路由与终结点&#xff1a;如何规划好你的Web API路由系统在 ASP.NET MVC 框架里面就已经存在了&#xff0c;在 ASP.NET Core 框架里面进行了改进路由系统的核心作用是指 URL 和 应用程序 Controller 的对应关系的一种映射这个映射关系实际上有两种作用&#xff1a;1、把 U…

Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)

好吧&#xff0c;这个题目我也想了很久&#xff0c;不知道如何用最简单的几个字来概括这篇文章&#xff0c;原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ASP.NET Identity实现身份认证与授权》&#xff0c;然而如你所见&#xff0c;这样的名字实在是太…

【翻译】.NET 5 Preview 1 发布

.NET 5 Preview 1 发布去年年底,我们发布了.NET Core 3.0和3.1.这些版本添加了桌面应用程序模型Windows Forms(WinForms)和WPF,ASP.NET Blazor用于构建SPA应用程序和用于构建分布式应用和服务的gRPC模板、用于与gRPC对话丰富的客户端代码生成、REST API服务等等.我们很高兴看到…

Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(二)

上文已经介绍了Identity Service的实现过程。今天我们继续&#xff0c;实现一个简单的Weather API和一个基于Ocelot的API网关。回顾《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权&#xff08;一&#xff09;》Weather APIWeather API实现非常简单&#xf…

编程 音量键_盘点市面上那些千元级高逼格的键盘 灯光炫酷 多宏编程

随着科技的变化&#xff0c;以及电竞被更多人熟知&#xff0c;也带动了电竞外设的进步&#xff0c;现在更多的人喜欢选择机械键盘。首先机械键盘可以给我们带来超棒的敲击感&#xff0c;无论是玩游戏还是日常办公打字&#xff0c;都绝对是一等一的好。再者机械键盘在高强度使用…

DotNetCore Web应用程序中的Cookie管理

原文来自互联网&#xff0c;由长沙DotNET技术社区编译。如译文侵犯您的署名权或版权&#xff0c;请联系小编&#xff0c;小编将在24小时内删除。限于译者的能力有限&#xff0c;个别语句翻译略显生硬&#xff0c;还请见谅。作者简介&#xff1a;Jon&#xff08;Jonathan&#x…

逆向so_记一次APP的so层算法逆向(七)

“ 前言&#xff1a;初学逆向 请多多指教 好累 感觉每天这样肝 人有点受不了了...”学习到的内容—1、新学习到IDA的一些分析时候的小技巧2、算法还原代码实现的练习(有个参数没有分析出来&#xff0c;后面知道了会补上的)3、在Frida中使用命令行调试的方便方法分析过程—APP登…

C++实现Huffman树

代码如下&#xff1a; #include <iostream> using namespace std; int s1, s2;typedef struct {int weight;int parent, lch, rch; } HTNode, *HuffmanTree;void Select(HuffmanTree &HT, int n, int &s1, int &s2) {int minv;//定义一个临时变量存储最小值…

.NET Core开发实战(第25课:路由与终结点:如何规划好你的Web API)--学习笔记(下)...

25 | 路由与终结点&#xff1a;如何规划好你的Web API自定义约束实现了路由约束接口&#xff0c;它只有一个 Match 方法&#xff0c;这个方法传入了 Http 当前的 httpContext&#xff0c;route&#xff0c;routeKey这个 routeKey 就是我们要验证的 key 值后面两个参数 RouteVal…

微软 Visual Studio 2019 16.5 发布:.NET 移动开发、生产力

微软最新发布了 Visual Studio 2019 16.5 版本&#xff0c;下面来看看主要更新内容&#xff1a;.NET 移动开发首先要讨论的特性是 XAML Hot Reload for Xamarin.Forms。此功能可加快开发速度&#xff0c;并使开发者可以更轻松地在移动应用的用户界面上进行构建、实验和迭代。且…

chrome主题_谷歌Chrome将很快允许用户创建自定义主题

站长之家(ChinaZ.com) 7月31日 消息:据9to5google报道&#xff0c;虽然用户可以通过Chrome Web Store定制主题&#xff0c;但用户要根据自己的独特喜好定制主题却不是一个简单的事。谷歌正寻求通过在Chrome内置一个自定义主题生成器来解决这个问题。Chrome Web Store中有许多传…

使用Magicodes.IE.Excel完成Excel图片的导入和导出

说明本章教程主要说明如何使用Magicodes.IE.Excel进行图片的导入导出。要点配置DTO进行Excel图片导出配置DTO进行Excel图片导入图片导入导出特性说明ExportImageFieldAttributeHeight&#xff1a;高度(默认15)Width&#xff1a;宽度(默认50)Alt&#xff1a;图片不存在时替换文本…

C++未定义行为-数组越界

我们先来看看下面的代码&#xff1a; #include <iostream> using namespace std; const int N 100010; int a[N]; int main() {for (int i 1;i<N;i) a[i] 2;return 0; }当我们写这段代码的时候&#xff0c;编译器就会发生这样的问题。 这是为什么呢&#xff1f;&a…

SuperBenchmarker一个用.NET编写的压测工具

0x01 前言在这之前想必大家对ab(http)与abs(https)也有一些了解,我们今天不去看ab和abs,SuperBenchmarker(sb.exe)是一个压测工具,他是一个受Apache Benchmark的启发,他会在终端窗口为我们显示最终的结果,同时也会在web界面生成一个动态结果。SuperBenchmarker(sb.exe)可以在Wi…

mysql文献综述_文献综述随笔(二十)

一、基本信息标题&#xff1a;中小型酒店管理系统的设计与实现时间&#xff1a;2013来源&#xff1a;厦门大学关键词&#xff1a;MVC;B/S;JAVA EE;JSP;MySQL;瀑布开发模型二、研究内容1.主要内容&#xff1a;系统业务需求、功能需求、系统架构设计、数据库设计1.1功能模块设计&…

五分钟完成 ABP vNext 通讯录 App 开发

ABP vNext&#xff08;后文简称Abp&#xff09;是 Volo 公司堪称艺术品级的应用开发框架&#xff0c;它基于领域驱动设计&#xff08;DDD&#xff09;的思维&#xff0c;创新地采用了模块化的设计。Abp 目前无疑是 ASP.NET Core 开发框架中最先进和最优雅的存在。笔者认为&…

mysql 5.74安装教程_MySQL安装、基本账户安全(5.0以后版本)

-----------MySQL 5.0以后版本的安装-----------MySQL安装安装包学习的必杀绝技——就是阅读包的安装说明(readme & install)文档。----------# rm /etc/my.cnf (安装前执行一下)----------1.Mysql-5.0.40.tar.gz1.1.Source Installation Overview(lines 74 of …

使用GUI工具Portainer.io管控Docker容器

背景5年前容器技术扑面而来&#xff0c;如今已经成为面向云原生开发的基础架构&#xff0c;基于微服务的设计需要部署大量容器&#xff0c;同时强调了友好快速的管理容器。是时候推荐一个轮子Portainer.io&#xff1a;提供GUI界面的容器管理工具&#xff0c;给开发者的工具箱又…

【项目升级】集成Quartz.Net Job实现(一)

这两天的新闻也是越来越多了&#xff0c;不仅Github接手了NPM&#xff0c;还有.NET 5也要新鲜出炉了&#xff08;11月正式发布&#xff09;&#xff0c;当然还有MVP峰会也正在如火如荼的展开&#xff0c;会有哪些好的东西被碰撞出来&#xff0c;也是很期待的。这些天我也简单的…