Volcano Scheduler调度器源码解析

Volcano Scheduler调度器源码解析

本文从源码的角度分析Volcano Scheduler相关功能的实现。

本篇Volcano版本为v1.8.0。

Volcano项目地址: https://github.com/volcano-sh/volcano

controller命令main入口: cmd/scheduler/main.go

controller相关代码目录: pkg/scheduler
关联文章:

  • 《Volcano Controller控制器源码解析》

更多文章访问: https://www.cyisme.top

看过我之前文章《从源码解析KubeScheduler调度过程》的朋友应该已经大致了解了k8s原生调度器的运行逻辑了。

k8s scheduler大致运行流程:

listewatch
schedulOne
Informer
SchedulingQueue
schedulingCycle
bindingCycle

k8s scheduler的调度大概是从infomer监听事件到之后,就会对这个事件进行调度,期间会运行framework插件,最终绑定到目标node上。

volcano scheduler的调度和k8s scheduler的流程略有不同, 但是思想差不多。

listewatch
resources snapshot
open ssn
Informer
Cache
Session
Plugin
bind

volcano scheduler调度不是监听到事件后就执行调度, 而是周期性的(open ssn动作是周期性执行的)。 每过一段时间(默认1s)就会执行一次调度,期间会运行plugin插件,最终绑定到目标node上。

在这里插入图片描述

Cache

cache组件会list/watch, 维护最新的资源信息。 注册informer事件的代码这里不做展示。

type SchedulerCache struct {// ...// 一些informer...podInformer                infov1.PodInformernodeInformer               infov1.NodeInformerpodGroupInformerV1beta1    vcinformerv1.PodGroupInformerqueueInformerV1beta1       vcinformerv1.QueueInformerpvInformer                 infov1.PersistentVolumeInformerpvcInformer                infov1.PersistentVolumeClaimInformerscInformer                 storagev1.StorageClassInformerpcInformer                 schedv1.PriorityClassInformerquotaInformer              infov1.ResourceQuotaInformercsiNodeInformer            storagev1.CSINodeInformercsiDriverInformer          storagev1.CSIDriverInformercsiStorageCapacityInformer storagev1beta1.CSIStorageCapacityInformercpuInformer                cpuinformerv1.NumatopologyInformer// 用于绑定nodeBinder         Binder// 存储list/watch到的资源Jobs                 map[schedulingapi.JobID]*schedulingapi.JobInfoNodes                map[string]*schedulingapi.NodeInfoQueues               map[schedulingapi.QueueID]*schedulingapi.QueueInfoPriorityClasses      map[string]*schedulingv1.PriorityClassNodeList             []stringdefaultPriorityClass *schedulingv1.PriorityClassdefaultPriority      int32CSINodesStatus       map[string]*schedulingapi.CSINodeStatusInfo// ...
}

cache启动时,我们关注三个goroutine的工作职责。

func (sc *SchedulerCache) Run(stopCh <-chan struct{}) {sc.informerFactory.Start(stopCh)sc.vcInformerFactory.Start(stopCh)// 处理错误的任务go wait.Until(sc.processResyncTask, 0, stopCh)// 清理jobgo wait.Until(sc.processCleanupJob, 0, stopCh)// 执行绑定操作go wait.Until(sc.processBindTask, time.Millisecond*20, stopCh)// ...
}

processResyncTask会从errTasks队列中, 取出task, 并重新从apiserver中获取资源信息更新它。

func (sc *SchedulerCache) processResyncTask() {obj, shutdown := sc.errTasks.Get()// 省略一些代码// syncTask 会用从apiserver中获取到资源信息更新taskif err := sc.syncTask(task); err != nil {sc.resyncTask(task)}
}

processCleanupJob会从DeletedJobs队列中, 取出job, 如果job已经完成,则删除cache中的job信息。

func (sc *SchedulerCache) processCleanupJob() {obj, shutdown := sc.DeletedJobs.Get()if shutdown {return}// 省略一些代码// // 判断job是否已经完成。如果pg和task都为空,则认为job已经完成if schedulingapi.JobTerminated(job) {// 从cache中删除jobdelete(sc.Jobs, job.UID)} else {// Retrysc.deleteJob(job)}
}

processBindTask会从BindFlowChannelchannel中, 取出数个task, 执行绑定操作。

func (sc *SchedulerCache) processBindTask() {for {select {case taskInfo, ok := <-sc.BindFlowChannel:if !ok {return}sc.bindCache = append(sc.bindCache, taskInfo)// 如果绑定任务达到batchNum,则执行绑定操作// batchNum默认为1if len(sc.bindCache) == sc.batchNum {sc.BindTask()}default:}if len(sc.BindFlowChannel) == 0 {break}}if len(sc.bindCache) == 0 {return}sc.BindTask()
}
func (sc *SchedulerCache) BindTask() {// 拷贝一份绑定任务var tmpBindCache []*schedulingapi.TaskInfo = make([]*schedulingapi.TaskInfo, len(sc.bindCache))copy(tmpBindCache, sc.bindCache)// 异步执行绑定操作go func(tasks []*schedulingapi.TaskInfo) {successfulTasks := make([]*schedulingapi.TaskInfo, 0)for _, task := range tasks {// 检查voluem是否已经准备好, 过滤出可以绑定的taskif err := sc.VolumeBinder.BindVolumes(task, task.PodVolumes); err != nil {sc.VolumeBinder.RevertVolumes(task, task.PodVolumes)// 如果volume没有准备好,放到errTasks队列中,等待更新信息sc.resyncTask(task)} else {successfulTasks = append(successfulTasks, task)}}bindTasks := make([]*schedulingapi.TaskInfo, len(successfulTasks))copy(bindTasks, successfulTasks)// 执行绑定操作, 调用k8s apiif err := sc.Bind(bindTasks); err != nil {return}}(tmpBindCache)// 清空列表sc.bindCache = sc.bindCache[0:0]
}

Framework

framework中的主要对象其实是session, 一个调度周期会开启一个session, 调度工作将会在这个session中进行。

如果参照k8s schedulersession其实对应的就是k8s framework

session是插件的载体与管理者, 由Action触发。

session会存储当前的资源信息, 以及插件的注册信息。

type Session struct {// 省略一些代码// 用于存储cache中的资源信息, 这些信息是深拷贝的Jobs           map[api.JobID]*api.JobInfoNodes          map[string]*api.NodeInfoCSINodesStatus map[string]*api.CSINodeStatusInfoRevocableNodes map[string]*api.NodeInfoQueues         map[api.QueueID]*api.QueueInfoNamespaceInfo  map[api.NamespaceName]*api.NamespaceInfo// 存储启用的插件名称Tiers          []conf.Tier// 存储插件对象plugins           map[string]Plugin// 存储事件处理函数eventHandlers     []*EventHandler// 这些func用于判断资源需要调用哪些插件// map[插件名称]具体方法// 插件中会根据自己的需求, 将自己的名称以及对应的方法注册到session中// 这些方法将会在不同的Action中调用jobOrderFns       map[string]api.CompareFnclusterOrderFns   map[string]api.CompareFnjobStarvingFns    map[string]api.ValidateFn// 省略了一些*Fns // ...
}

session的生命周期

OpenSission方法会返回一个新的session对象, 对应一个新的调度周期。

func OpenSession(cache cache.Cache, tiers []conf.Tier, configurations []conf.Configuration) *Session {ssn := openSession(cache)// 省略一些代码// for _, tier := range tiers {for _, plugin := range tier.Plugins {// scheduler启动时会将每个插件的名称以及对应的构建方法注册到一个pluginBuilders map中// 这里会根据这个map, 获取到插件的构建方法, 并执行构建方法, 返回一个插件对象if pb, found := GetPluginBuilder(plugin.Name); !found {klog.Errorf("Failed to get plugin %s.", plugin.Name)} else {plugin := pb(plugin.Arguments)ssn.plugins[plugin.Name()] = pluginonSessionOpenStart := time.Now()// OnSessionOpen 中会注册上述的 *Fns plugin.OnSessionOpen(ssn)}}}return ssn
}
func openSession(cache cache.Cache) *Session {ssn := &Session{// 省略一些代码}// 从cache中获取资源信息, 并深拷贝到session中snapshot := cache.Snapshot()ssn.Jobs = snapshot.Jobsfor _, job := range ssn.Jobs {// only conditions will be updated periodicallyif job.PodGroup != nil && job.PodGroup.Status.Conditions != nil {ssn.podGroupStatus[job.UID] = *job.PodGroup.Status.DeepCopy()}// 检查job是否可以调度, jobValid目前只有gang插件注册了if vjr := ssn.JobValid(job); vjr != nil {if !vjr.Pass {jc := &scheduling.PodGroupCondition{Type:               scheduling.PodGroupUnschedulableType,Status:             v1.ConditionTrue,LastTransitionTime: metav1.Now(),TransitionID:       string(ssn.UID),Reason:             vjr.Reason,Message:            vjr.Message,}if err := ssn.UpdatePodGroupCondition(job, jc); err != nil {klog.Errorf("Failed to update job condition: %v", err)}}delete(ssn.Jobs, job.UID)}}// 深拷贝其他资源信息ssn.Nodes = snapshot.Nodes// 省略...return ssn
}

对应的CloseSession方法会在调度周期结束时调用, 执行收尾工作,并清理session中数据。

func CloseSession(ssn *Session) {for _, plugin := range ssn.plugins {onSessionCloseStart := time.Now()// 调用插件的OnSessionClose方法, 方法中会有如设置pg状态等收尾工作plugin.OnSessionClose(ssn)metrics.UpdatePluginDuration(plugin.Name(), metrics.OnSessionClose, metrics.Duration(onSessionCloseStart))}closeSession(ssn)
}
func closeSession(ssn *Session) {// 更新cache中的数据状态ju := newJobUpdater(ssn)ju.UpdateAll()updateQueueStatus(ssn)// 清理session中的数据ssn.Jobs = nil// 省略...
}

statement

statement用于存储这一次“打包”调度的信息, 最终统一提交或取消。

// 存储task的操作信息
type operation struct {// 操作名称, Evict/Pipeline/Allocatename   Operation// 操作的tasktask   *api.TaskInfo// 操作的原因reason string
}
type Statement struct {operations []operationssn        *Session
}

对应三个操作名称, 有三个方法,供plugin调用。以Evict举例:

func (s *Statement) Evict(reclaimee *api.TaskInfo, reason string) error {// 更新session中的job状态if job, found := s.ssn.Jobs[reclaimee.Job]; found {if err := job.UpdateTaskStatus(reclaimee, api.Releasing); err != nil {}} else {}// 更新node中task的状态if node, found := s.ssn.Nodes[reclaimee.NodeName]; found {err := node.UpdateTask(reclaimee)if err != nil {return err}}// 触发session中的事件处理函数for _, eh := range s.ssn.eventHandlers {if eh.DeallocateFunc != nil {eh.DeallocateFunc(&Event{Task: reclaimee,})}}// 加入到待处理列表s.operations = append(s.operations, operation{name:   Evict,task:   reclaimee,reason: reason,})return nil
}
// evict用于提交操作, cache组件会调用api更新。
func (s *Statement) evict(reclaimee *api.TaskInfo, reason string) error {if err := s.ssn.cache.Evict(reclaimee, reason); err != nil {if e := s.unevict(reclaimee); e != nil {klog.Errorf("Faled to unevict task <%v/%v>: %v.", reclaimee.Namespace, reclaimee.Name, e)}return err}return nil
}
// unevict用于撤销操作恢复task状态, 是上面的逆操作
func (s *Statement) unevict(reclaimee *api.TaskInfo) error {// Update status in sessionjob, found := s.ssn.Jobs[reclaimee.Job]if found {if err := job.UpdateTaskStatus(reclaimee, api.Running); err != nil {}} else {}// Update task in node.if node, found := s.ssn.Nodes[reclaimee.NodeName]; found {err := node.UpdateTask(reclaimee)if err != nil {return err}}for _, eh := range s.ssn.eventHandlers {if eh.AllocateFunc != nil {eh.AllocateFunc(&Event{Task: reclaimee,})}}return nil
}

plugin会在最后根据情况判断是否提交或者取消。

// 撤销
func (s *Statement) Discard() {for i := len(s.operations) - 1; i >= 0; i-- {op := s.operations[i]op.task.GenerateLastTxContext()switch op.name {case Evict:err := s.unevict(op.task)case Pipeline:err := s.unpipeline(op.task)case Allocate:err := s.unallocate(op.task)}}
}
// 提交
func (s *Statement) Commit() {klog.V(3).Info("Committing operations ...")for _, op := range s.operations {op.task.ClearLastTxContext()switch op.name {case Evict:err := s.evict(op.task, op.reason)case Pipeline:s.pipeline(op.task)case Allocate:err := s.allocate(op.task)}}
}

Actions

action是触发执行plugin的动作。

如果参照k8s scheduleraction相当于preFilter、Fileter、PostFilter、PreBind、Bind、PostBind等这些动作。

action有6种:

  • Enqueue 调度器的准备阶段, 判断资源是否满足调度条件。一般作为调度的前置条件。 Enqueue 能够防止集群下有大量不能调度的pod,提高了调度器的性能。
  • Allocate 执行调度操作(分配node), 是必不可缺的一步
  • Preempt 抢占资源, 用于处理高优先级调度问题。 可以在同queue或同job中抢占资源。
  • Backfill 回填步骤,处理待调度Pod列表中没有指明资源申请量的Pod调度。 Backfill能够提高集群吞吐量,提高资源利用率。
  • Reclaim 根据队列权重回收队列的资源。
  • Shuffle 根据资源状况重新分配节点

可以在volcano的配置中,指定使用哪些action以及plugin

# kubectl get configmap volcano-scheduler-configmap -nvolcano-system -oyaml
apiVersion: v1
data:
volcano-scheduler.conf: |
actions: "enqueue, allocate, backfill"
tiers:
- plugins:- name: priority- name: gang- name: conformance
- plugins:- name: drf- name: predicates- name: proportion- name: nodeorder- name: binpack
kind: ConfigMap
metadata:
annotations:
creationTimestamp: "2020-08-15T04:01:02Z"
name: volcano-scheduler-configmap
namespace: volcano-system

配置文件中声明的actions顺序即为实际执行的顺序, 在上面的配置文件的声明中,将依次执行enqueue, allocate, backfill三个action

volcano并不会检查action顺序的合理性,action可以任意顺序。

action需要实现framework中定义的Action interface

type Action interface {// 唯一名称Name() string// 初始化动作Initialize()// 执行动作Execute(ssn *Session)// 取消初始化UnInitialize()
}

Execute中会调用session中由plugins注册的方法, 以enqueue动作为例:

func (enqueue *Action) Execute(ssn *framework.Session) {for _, job := range ssn.Jobs {//...}for {// ...// ssn.JobEnqueueable调用plugin方法if job.PodGroup.Spec.MinResources == nil || ssn.JobEnqueueable(job) {ssn.JobEnqueued(job)job.PodGroup.Status.Phase = scheduling.PodGroupInqueuessn.Jobs[job.UID] = job}// Added Queue back until no job in Queue.queues.Push(queue)}
}
// ssn.JobEnqueueable
func (ssn *Session) JobEnqueueable(obj interface{}) bool {var hasFound boolfor _, tier := range ssn.Tiers {for _, plugin := range tier.Plugins {// 未启用enqueue动作则跳过if !isEnabled(plugin.EnabledJobEnqueued) {continue}// 未注册方法则跳过fn, found := ssn.jobEnqueueableFns[plugin.Name]if !found {continue}// 执行res := fn(obj)if res < 0 {return false}if res > 0 {hasFound = true}}// 如果存在enqueue插件, 则代表允许排队, 这个函数中,不再执行下一Tiers的插件if hasFound {return true}}return true
}

Plugins

pluginsvolcano实现调度逻辑的核心, 也是volcano的核心竞争力。

pluginsinit方法中注册自己的初始化函数到pluginBuilders中, 供sessionOpenSession时调用。

func init() {framework.RegisterPluginBuilder(drf.PluginName, drf.New)framework.RegisterPluginBuilder(gang.PluginName, gang.New)// 省略...
}

plugin需要实现framework中定义的Plugin interface

type Plugin interface {// 插件唯一名称Name() string// 在OpenSession时调用OnSessionOpen(ssn *Session)// 在CloseSession时调用OnSessionClose(ssn *Session)
}

OpenSession时会调用pluginOnSessionOpen方法, plugin会在这时向session注册自己的方法。

gang插件中会注册数个方法:

func (gp *gangPlugin) OnSessionOpen(ssn *framework.Session) {validJobFn := func(obj interface{}) *api.ValidateResult {//...}ssn.AddJobValidFn(gp.Name(), validJobFn)preemptableFn := func(preemptor *api.TaskInfo, preemptees []*api.TaskInfo) ([]*api.TaskInfo, int){//...}ssn.AddReclaimableFn(gp.Name(), preemptableFn)ssn.AddPreemptableFn(gp.Name(), preemptableFn)//....
}

CloseSession时会调用pluginOnSessionClose方法, plugin会在这时会执行清理、检查等收尾工作。

func (gp *gangPlugin) OnSessionClose(ssn *framework.Session) {var unreadyTaskCount int32var unScheduleJobCount intfor _, job := range ssn.Jobs {if !job.Ready() {// ...// 检查未就绪的task数量unreadyTaskCount = job.MinAvailable - schedulableTaskNum()// 更新pg状态jc := &scheduling.PodGroupCondition{Type:               scheduling.PodGroupUnschedulableType,Status:             v1.ConditionTrue,LastTransitionTime: metav1.Now(),TransitionID:       string(ssn.UID),Reason:             v1beta1.NotEnoughResourcesReason,Message:            msg,}if err := ssn.UpdatePodGroupCondition(job, jc); err != nil {klog.Errorf("Failed to update job <%s/%s> condition: %v",job.Namespace, job.Name, err)}// 省略...}
}

运行流程

scheduler.Run会启动整个调度器。

func (pc *Scheduler) Run(stopCh <-chan struct{}) {// 监听配置文件变化pc.loadSchedulerConf()go pc.watchSchedulerConf(stopCh)// 启动cachepc.cache.SetMetricsConf(pc.metricsConf)pc.cache.Run(stopCh)pc.cache.WaitForCacheSync(stopCh)// 启动调度, 每隔一段时间执行一次go wait.Until(pc.runOnce, pc.schedulePeriod, stopCh)if options.ServerOpts.EnableCacheDumper {pc.dumper.ListenForSignal(stopCh)}
}

runOnce会每隔一段时间执行一次, 创建一个新的session执行动作。

func (pc *Scheduler) runOnce() {// 周期性调度pc.mutex.Lock()actions := pc.actionsplugins := pc.pluginsconfigurations := pc.configurationspc.mutex.Unlock()// 动态watch配置, 所以每次调度都会重新加载配置conf.EnabledActionMap = make(map[string]bool)for _, action := range actions {conf.EnabledActionMap[action.Name()] = true}// 打开一个sessionssn := framework.OpenSession(pc.cache, plugins, configurations)defer func() {// 关闭sessionframework.CloseSession(ssn)metrics.UpdateE2eDuration(metrics.Duration(scheduleStartTime))}()// 执行actionsfor _, action := range actions {actionStartTime := time.Now()// 执行pluginsaction.Execute(ssn)metrics.UpdateActionDuration(action.Name(), metrics.Duration(actionStartTime))}
}

会按照配置文件中的顺序执行action, action.Execute会调用plugin注册的方法, 对task、job进行处理, 最终会由backfillalloc或者preempt动作中调用对应的方法添加到cacheBindFlowChannel中, 等待绑定。
在这里插入图片描述

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

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

相关文章

Vue3.x+Echarts (可视化界面)

Vue3.0Echarts &#xff08;可视化界面&#xff09; 1. 简介1.1 技术选型1.2 ECharts支持的数据格式1.3 ECharts使用步骤 2. ECharts图形2.1 通用配置2.2 柱状图2.3 折线图2.4 散点图2.5 直角坐标系常用配置2.6 饼图2.7 地图2.8 雷达图2.9 仪表盘2.10 小结 3. Vue3.2ECharts5数…

RecombiMAb anti-mouse VEGFR-2

DC101-CP132单克隆抗体是原始DC101单克隆的重组嵌合型抗体。可变结构域序列与原始DC101相同&#xff0c;但是恒定区序列已经从大鼠IgG1变为小鼠IgG2a。DC101-CP132单克隆抗体像原始大鼠IgG1抗体一样&#xff0c;不包含Fc突变。 DC101-CP132单克隆抗体能与小鼠VEGFR-2(血管内皮生…

ZGC垃圾收集器介绍

ZGC&#xff08;The Z Garbage Collector&#xff09;是JDK 11中推出的一款低延迟垃圾回收器&#xff0c;它的设计目标包括&#xff1a; 停顿时间不超过10ms&#xff1b;停顿时间不会随着堆的大小&#xff0c;或者活跃对象的大小而增加&#xff1b;支持8MB~4TB级别的堆&#x…

微信小程序 获取地址信息(uniapp)

参考API地址&#xff1a;微信小程序JavaScript SDK | 腾讯位置服务 <script> // 引入SDK核心类&#xff0c;js文件根据自己业务&#xff0c;位置可自行放置var QQMapWX require(../../js/uploadImg/qqmap-wx-jssdk.js);export default {data(){return{qqmapsdk:}},onL…

【HarmonyOS4.0】第四篇-ArkUI基础实战

一、ArkUI框架简介 ArkUI开发框架是方舟开发框架的简称&#xff0c;它是一套构建 HarmonyOS / OpenHarmony 应用界面的声明式UI开发框架&#xff0c;它使用极简的UI信息语法、丰富的UI组件以及实时界面语言工具&#xff0c;帮助开发者提升应用界面开发效率 30%&#xff0c;开发…

Swift单元测试Quick+Nimble

文章目录 使用QuickNimble1、苹果官方测试框架XCTest的优缺点2、选择QuickNimble的原因&#xff1a;3、QuickNimble使用介绍集成&#xff1a;Quick关键字说明&#xff1a;Nimble中的匹配函数等值判断&#xff1a;使用equal函数是否是同一个对象&#xff1a;使用beIdenticalTo函…

Android14之刷机模式总结(一百七十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

电脑文件mfc100u.dll丢失的解决方法分析,怎么修复mfc100u.dll靠谱

mfc100u.dll丢失了要怎么办&#xff1f;其实很多人都遇到过这样的电脑故障吧&#xff0c;说这个mfc100u.dll文件已经不见了&#xff0c;然后一些程序打不开了&#xff0c;那么这种情况我们要怎么解决呢&#xff1f;今天我们就来给大家详细的说说mfc100u.dll丢失的解决方法。 一…

【unity小技巧】实现没有动画的FPS武器摇摆和摆动效果

文章目录 前言开始完结 前言 添加程序摇摆和摆动是为任何FPS游戏添加一些细节的非常简单的方法。但是并不是所以的模型动画都会配有武器摆动动画效果&#xff0c;在本文中&#xff0c;将实现如何使用一些简单的代码实现武器摇摆和摆动效果&#xff0c;这比设置动画来尝试实现类…

Golang中for和for range语句的使用技巧、对比及常见的避坑

前言 基础语法不再赘述&#xff0c;写这个原因是之前的某次面试被问道了&#xff0c;我知道会导致问题但具体答下来不是很通顺。再回想自己开发过程中&#xff0c;很多地方都是使用到了for/for range&#xff0c;但是却从没注意过一些细节&#xff0c;因此专门学习一下进行记录…

K8S中SC、PV、PVC的理解

存储类&#xff08;StorageClass&#xff09;定义了持久卷声明&#xff08;PersistentVolumeClaim&#xff09;所需的属性和行为&#xff0c;而持久卷&#xff08;PersistentVolume&#xff09;是实际的存储资源&#xff0c;持久卷声明&#xff08;PersistentVolumeClaim&#…

平衡搜索二叉树(AVL树)

前言 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查 找元素相当于在顺序表中搜索元素&#xff0c;效率低下。因此&#xff0c;两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述…

vue中鼠标拖动触发滚动条的移动

前言 在做后端管理系统中&#xff0c;像弹窗或大的表单时&#xff0c;经常会有滚动条的出现&#xff0c;但有些时候如流程、图片等操作时&#xff0c;仅仅使用鼠标拖动滚动条操作不太方便&#xff0c;如果使用鼠标拖拽图片或容器来触发滚动条的移动就比较方便了 功能设计 如…

LeetCode(454)四数相加 II⭐⭐

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) &#xff0c;使得 A[i] B[j] C[k] D[l] 0。 为了使问题简单化&#xff0c;所有的 A, B, C, D 具有相同的长度 N&#xff0c;且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间&#…

Pinia处学习

修改数据的三种方式: mutate本次变化的数据,state时store中的数据

大模型训练营Day2 homework

1.使用 InternLM-Chat-7B 模型生成 300 字的小故事 2.熟悉 hugging face 下载功能&#xff0c;使用 huggingface_hub python 包&#xff0c;下载 InternLM-20B 的 config.json 文件到本地&#xff1a; 下面开始下载和推广大模型的相关的包&#xff1a; 这里需要在本地上&…

uniapp自定义底部导航栏

1.新建 nav-custom.vue组件 <template><view class"nav-box" :style"{height:heightpx,background:bgColor}"><!-- 自定义导航栏 --><view class"status_bar" :style"{height:statusBarHeightpx}"><!-- u…

@Transactional 事务注解

第一、先简单介绍一下Spring事务的传播行为 所谓事务的传播行为是指&#xff0c;如果在开始当前事务之前&#xff0c;一个事务上下文已经存在&#xff0c;此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量&…

HarmonyOS应用开发学习笔记 UIAbility组件间交互 UIAbility启动,页面跳转结果回调

1、 HarmoryOS Ability页面的生命周期 2、 Component自定义组件 3、HarmonyOS 应用开发学习笔记 ets组件生命周期 4、HarmonyOS 应用开发学习笔记 ets组件样式定义 Styles装饰器&#xff1a;定义组件重用样式 Extend装饰器&#xff1a;定义扩展组件样式 5、HarmonyOS 应用开发…

MySQL的体系结构(超全总结版)

MySQL组成 连接池组件管理服务和工具组件SQL接口组件查询分析器组件优化器组件缓冲组件插件式存储引擎物理文件 存储引擎 InnoDB存储引擎 主要面向OLTP(在线事务处理)方面的应用&#xff0c;特点是行锁设计、支持外键&#xff0c;默认情况下读取操作不会产生锁。通过使用多…