4.10 endpoint controller

EndpointSubset

EndpointSubset 是一组具有公共端口集的地址,扩展的端点集是 Addresses (Pod IP 地址) 和 Ports (Service 名称和端口号) 的笛卡尔积。

下面是一个典型的 EndpointSubset 示例:

  Name: "test",Subsets: [{Addresses: [{"ip": "10.10.1.1"},{"ip": "10.10.2.2"}],Ports: [{"name": "a","port": 8675},{"name": "b","port": 309}]}
]

将上面的 Subset 转换为对应的端点集合:

a: [ 10.10.1.1:8675, 10.10.2.2:8675 ]
b: [ 10.10.1.1:309, 10.10.2.2:309 ]

EndPointController

首先来看看 Endpoints 控制器对象,该对象是实现 Endpoints 功能的核心对象。

// Controller manages selector-based service endpoints.
type Controller struct {client           clientset.InterfaceeventBroadcaster record.EventBroadcastereventRecorder    record.EventRecorder// serviceLister is able to list/get services and is populated by the shared informer passed to// NewEndpointController.serviceLister corelisters.ServiceLister// servicesSynced returns true if the service shared informer has been synced at least once.// Added as a member to the struct to allow injection for testing.servicesSynced cache.InformerSynced// podLister is able to list/get pods and is populated by the shared informer passed to// NewEndpointController.podLister corelisters.PodLister// podsSynced returns true if the pod shared informer has been synced at least once.// Added as a member to the struct to allow injection for testing.podsSynced cache.InformerSynced// endpointsLister is able to list/get endpoints and is populated by the shared informer passed to// NewEndpointController.endpointsLister corelisters.EndpointsLister// endpointsSynced returns true if the endpoints shared informer has been synced at least once.// Added as a member to the struct to allow injection for testing.endpointsSynced cache.InformerSynced// Services that need to be updated. A channel is inappropriate here,// because it allows services with lots of pods to be serviced much// more often than services with few pods; it also would cause a// service that's inserted multiple times to be processed more than// necessary.queue workqueue.TypedRateLimitingInterface[string]// workerLoopPeriod is the time between worker runs. The workers process the queue of service and pod changes.workerLoopPeriod time.Duration// triggerTimeTracker is an util used to compute and export the EndpointsLastChangeTriggerTime// annotation.triggerTimeTracker *endpointsliceutil.TriggerTimeTrackerendpointUpdatesBatchPeriod time.Duration
}
初始化

NewEndpointController 方法用于 EndPoint 控制器对象的初始化工作,并返回一个实例化对象,控制器对象同时订阅了 Service, Pod, EndPoint 三种资源的变更事件。

// NewEndpointController returns a new *Controller.
func NewEndpointController(ctx context.Context, podInformer coreinformers.PodInformer, serviceInformer coreinformers.ServiceInformer,endpointsInformer coreinformers.EndpointsInformer, client clientset.Interface, endpointUpdatesBatchPeriod time.Duration) *Controller {broadcaster := record.NewBroadcaster(record.WithContext(ctx))recorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "endpoint-controller"})e := &Controller{client: client,queue: workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[string](),workqueue.TypedRateLimitingQueueConfig[string]{Name: "endpoint",},),workerLoopPeriod: time.Second,}serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: e.onServiceUpdate,UpdateFunc: func(old, cur interface{}) {e.onServiceUpdate(cur)},DeleteFunc: e.onServiceDelete,})e.serviceLister = serviceInformer.Lister()e.servicesSynced = serviceInformer.Informer().HasSyncedpodInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc:    e.addPod,UpdateFunc: e.updatePod,DeleteFunc: e.deletePod,})e.podLister = podInformer.Lister()e.podsSynced = podInformer.Informer().HasSyncedendpointsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{DeleteFunc: e.onEndpointsDelete,})e.endpointsLister = endpointsInformer.Lister()e.endpointsSynced = endpointsInformer.Informer().HasSyncede.triggerTimeTracker = endpointsliceutil.NewTriggerTimeTracker()e.eventBroadcaster = broadcastere.eventRecorder = recordere.endpointUpdatesBatchPeriod = endpointUpdatesBatchPeriodreturn e
}

启动控制器

根据控制器的初始化方法 NewEndpointController 的调用链路,可以找到控制器开始启动和执行的地方。

// cmd/kube-controller-manager/app/core.gofunc startEndpointsController(ctx context.Context, controllerContext ControllerContext, controllerName string) (controller.Interface, bool, error) {go endpointcontroller.NewEndpointController(ctx,controllerContext.InformerFactory.Core().V1().Pods(),controllerContext.InformerFactory.Core().V1().Services(),controllerContext.InformerFactory.Core().V1().Endpoints(),controllerContext.ClientBuilder.ClientOrDie("endpoint-controller"),controllerContext.ComponentConfig.EndpointController.EndpointUpdatesBatchPeriod.Duration,).Run(ctx, int(controllerContext.ComponentConfig.EndpointController.ConcurrentEndpointSyncs))return nil, true, nil
}
具体逻辑方法

Controller.Run 方法执行具体的初始化逻辑。

// Run will not return until stopCh is closed. workers determines how many
// endpoints will be handled in parallel.
func (e *Controller) Run(ctx context.Context, workers int) {defer utilruntime.HandleCrash()// Start events processing pipeline.e.eventBroadcaster.StartStructuredLogging(3)e.eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: e.client.CoreV1().Events("")})defer e.eventBroadcaster.Shutdown()defer e.queue.ShutDown()logger := klog.FromContext(ctx)logger.Info("Starting endpoint controller")defer logger.Info("Shutting down endpoint controller")if !cache.WaitForNamedCacheSync("endpoint", ctx.Done(), e.podsSynced, e.servicesSynced, e.endpointsSynced) {return}for i := 0; i < workers; i++ {go wait.UntilWithContext(ctx, e.worker, e.workerLoopPeriod)}go func() {defer utilruntime.HandleCrash()e.checkLeftoverEndpoints()}()<-ctx.Done()
}

e.worker 方法本质上就是一个无限循环轮询器,不断从队列中取出 EndPoint 对象,然后进行对应的操作。

// worker runs a worker thread that just dequeues items, processes them, and
// marks them done. You may run as many of these in parallel as you wish; the
// workqueue guarantees that they will not end up processing the same service
// at the same time.
func (e *Controller) worker(ctx context.Context) {for e.processNextWorkItem(ctx) {}
}func (e *Controller) processNextWorkItem(ctx context.Context) bool {eKey, quit := e.queue.Get()if quit {return false}defer e.queue.Done(eKey)logger := klog.FromContext(ctx)err := e.syncService(ctx, eKey)e.handleErr(logger, err, eKey)return true
}

syncService

Controller 的回调处理方法是 syncService 方法,该方法是 EndPoint 控制器操作的核心方法,通过方法的命名,可以知道 EndPoint 主要关注的对象是 Service。

func (e *Controller) syncService(ctx context.Context, key string) error {startTime := time.Now()logger := klog.FromContext(ctx)// 通过 key 解析出 Service 对象对应的 命名空间和名称namespace, name, err := cache.SplitMetaNamespaceKey(key)if err != nil {return err}defer func() {logger.V(4).Info("Finished syncing service endpoints", "service", klog.KRef(namespace, name), "startTime", time.Since(startTime))}()// 获取 Service 对象service, err := e.serviceLister.Services(namespace).Get(name)if err != nil {if !errors.IsNotFound(err) {return err}// Delete the corresponding endpoint, as the service has been deleted.// TODO: Please note that this will delete an endpoint when a// service is deleted. However, if we're down at the time when// the service is deleted, we will miss that deletion, so this// doesn't completely solve the problem. See #6877.err = e.client.CoreV1().Endpoints(namespace).Delete(ctx, name, metav1.DeleteOptions{})if err != nil && !errors.IsNotFound(err) {return err}e.triggerTimeTracker.DeleteService(namespace, name)return nil}// Service 类型为 ExternalName// 直接返回if service.Spec.Type == v1.ServiceTypeExternalName {// services with Type ExternalName receive no endpoints from this controller;// Ref: https://issues.k8s.io/105986return nil}// Service 的标签选择器为 nil// 这种情况下关联不到 EndPoint 对象// 直接返回if service.Spec.Selector == nil {// services without a selector receive no endpoints from this controller;// these services will receive the endpoints that are created out-of-band via the REST API.return nil}logger.V(5).Info("About to update endpoints for service", "service", klog.KRef(namespace, name))// 获取 Service 的标签选择器关联的 Pod 列表pods, err := e.podLister.Pods(service.Namespace).List(labels.Set(service.Spec.Selector).AsSelectorPreValidated())if err != nil {// Since we're getting stuff from a local cache, it is// basically impossible to get this error.return err}// We call ComputeEndpointLastChangeTriggerTime here to make sure that the// state of the trigger time tracker gets updated even if the sync turns out// to be no-op and we don't update the endpoints object.endpointsLastChangeTriggerTime := e.triggerTimeTracker.ComputeEndpointLastChangeTriggerTime(namespace, service, pods)// 初始化端点集合对象subsets := []v1.EndpointSubset{}// 初始化已就绪的 EndPoint 对象计数var totalReadyEps int// 初始化未就绪的 EndPoint 对象计数var totalNotReadyEps int
	// 遍历 Pod 列表
	for _, pod := range pods {// ShouldPodBeInEndpoints :// pod 处于终止状态(phase == v1.PodFailed || phase == v1.PodSucceeded)// pod IP 还未分配// pod 正在被删除但是 includeTerminating 为 trueif !endpointsliceutil.ShouldPodBeInEndpoints(pod, service.Spec.PublishNotReadyAddresses) {logger.V(5).Info("Pod is not included on endpoints for Service", "pod", klog.KObj(pod), "service", klog.KObj(service))continue}// 实例化一个 EndpointAddress 对象ep, err := podToEndpointAddressForService(service, pod)if err != nil {// this will happen, if the cluster runs with some nodes configured as dual stack and some as not// such as the case of an upgrade..logger.V(2).Info("Failed to find endpoint for service with ClusterIP on pod with error", "service", klog.KObj(service), "clusterIP", service.Spec.ClusterIP, "pod", klog.KObj(pod), "error", err)continue}epa := *epif endpointsliceutil.ShouldSetHostname(pod, service) {epa.Hostname = pod.Spec.Hostname}// Allow headless service not to have ports.if len(service.Spec.Ports) == 0 {if service.Spec.ClusterIP == api.ClusterIPNone {// 构建一个新的对象添加到 subset中,这里 ports 为空数组subsets, totalReadyEps, totalNotReadyEps = addEndpointSubset(logger, subsets, pod, epa, nil, service.Spec.PublishNotReadyAddresses)// No need to repack subsets for headless service without ports.}} else {for i := range service.Spec.Ports {servicePort := &service.Spec.Ports[i]portNum, err := podutil.FindPort(pod, servicePort)if err != nil {logger.V(4).Info("Failed to find port for service", "service", klog.KObj(service), "error", err)continue}// 根据 Service 端口对象 + 端口号构建一个对象epp := endpointPortFromServicePort(servicePort, portNum)var readyEps, notReadyEps int// 将构建好的对象追加到端点集合里subsets, readyEps, notReadyEps = addEndpointSubset(logger, subsets, pod, epa, epp, service.Spec.PublishNotReadyAddresses)// 累加已就绪的 EndPoint 对象计数totalReadyEps = totalReadyEps + readyEps// 累加未就绪的 EndPoint 对象计数totalNotReadyEps = totalNotReadyEps + notReadyEps}}}// 计算并确定最后的 EndPoint 对象集合 (新的 EndPoint Set)subsets = endpoints.RepackSubsets(subsets)// 通过 informer 获取 Service 对象对应的 EndPoint Set// 也就是当前的 EndPoint Set (旧的 EndPoint Set)// See if there's actually an update here.currentEndpoints, err := e.endpointsLister.Endpoints(service.Namespace).Get(service.Name)if err != nil {if !errors.IsNotFound(err) {return err}currentEndpoints = &v1.Endpoints{ObjectMeta: metav1.ObjectMeta{Name:   service.Name,Labels: service.Labels,},}}// 如果 Service 的资源版本号未设置,就需要创建新的 EndPoints createEndpoints := len(currentEndpoints.ResourceVersion) == 0// Compare the sorted subsets and labels// Remove the HeadlessService label from the endpoints if it exists,// as this won't be set on the service itself// and will cause a false negative in this diff check.// But first check if it has that label to avoid expensive copies.compareLabels := currentEndpoints.Labelsif _, ok := currentEndpoints.Labels[v1.IsHeadlessService]; ok {compareLabels = utillabels.CloneAndRemoveLabel(currentEndpoints.Labels, v1.IsHeadlessService)}// When comparing the subsets, we ignore the difference in ResourceVersion of Pod to avoid unnecessary Endpoints// updates caused by Pod updates that we don't care, e.g. annotation update.// 对新的和旧的 EndPoint Set进行排序 + 比较操作// 如果新的 Set 和旧的 Set 比较之后,没有任何差异// 并且 Service 的版本号也不需要创建// 直接返回就可以了if !createEndpoints &&endpointSubsetsEqualIgnoreResourceVersion(currentEndpoints.Subsets, subsets) &&apiequality.Semantic.DeepEqual(compareLabels, service.Labels) &&capacityAnnotationSetCorrectly(currentEndpoints.Annotations, currentEndpoints.Subsets) {logger.V(5).Info("endpoints are equal, skipping update", "service", klog.KObj(service))return nil}// 深度拷贝当前的 EndPoint Set// 重新设置相关的 (最新) 属性newEndpoints := currentEndpoints.DeepCopy()newEndpoints.Subsets = subsetsnewEndpoints.Labels = service.Labelsif newEndpoints.Annotations == nil {newEndpoints.Annotations = make(map[string]string)}if !endpointsLastChangeTriggerTime.IsZero() {newEndpoints.Annotations[v1.EndpointsLastChangeTriggerTime] =endpointsLastChangeTriggerTime.UTC().Format(time.RFC3339Nano)} else { // No new trigger time, clear the annotation.delete(newEndpoints.Annotations, v1.EndpointsLastChangeTriggerTime)}if truncateEndpoints(newEndpoints) {newEndpoints.Annotations[v1.EndpointsOverCapacity] = truncated} else {delete(newEndpoints.Annotations, v1.EndpointsOverCapacity)}if newEndpoints.Labels == nil {newEndpoints.Labels = make(map[string]string)}if !helper.IsServiceIPSet(service) {newEndpoints.Labels = utillabels.CloneAndAddLabel(newEndpoints.Labels, v1.IsHeadlessService, "")} else {newEndpoints.Labels = utillabels.CloneAndRemoveLabel(newEndpoints.Labels, v1.IsHeadlessService)}logger.V(4).Info("Update endpoints", "service", klog.KObj(service), "readyEndpoints", totalReadyEps, "notreadyEndpoints", totalNotReadyEps)if createEndpoints {// No previous endpoints, create them// 创建新的 EndPoints_, err = e.client.CoreV1().Endpoints(service.Namespace).Create(ctx, newEndpoints, metav1.CreateOptions{})} else {// Pre-existing// 更新已有 EndPoints_, err = e.client.CoreV1().Endpoints(service.Namespace).Update(ctx, newEndpoints, metav1.UpdateOptions{})}if err != nil {if createEndpoints && errors.IsForbidden(err) {// A request is forbidden primarily for two reasons:// 1. namespace is terminating, endpoint creation is not allowed by default.// 2. policy is misconfigured, in which case no service would function anywhere.// Given the frequency of 1, we log at a lower level.logger.V(5).Info("Forbidden from creating endpoints", "error", err)// If the namespace is terminating, creates will continue to fail. Simply drop the item.if errors.HasStatusCause(err, v1.NamespaceTerminatingCause) {return nil}}if createEndpoints {e.eventRecorder.Eventf(newEndpoints, v1.EventTypeWarning, "FailedToCreateEndpoint", "Failed to create endpoint for service %v/%v: %v", service.Namespace, service.Name, err)} else {e.eventRecorder.Eventf(newEndpoints, v1.EventTypeWarning, "FailedToUpdateEndpoint", "Failed to update endpoint %v/%v: %v", service.Namespace, service.Name, err)}return err}return nil
}

通过 Controller.syncService 方法的源代码,我们可以看到: EndPoint 对象每次同步时,都会执行如下的操作:

  1. 根据参数 key 获取指定的 Service 对象
  2. 获取 Service 对象的标签选择器关联的 Pod 列表
  3. 通过 Service 和 Pod 列表计算出最新的 EndPoint 对象 (新) 集合
  4. 通过 informer 获取 Service 对象对应的 EndPoint 对象 (旧) 集合
  5. 如果新集合与旧集合对比,没有任何差异,说明不需要更新,直接退出方法即可
  6. 根据 Service 资源版本号确定 EndPoints 对象的操作 (创建或更新) 并执行
    想要原文可以加作者v:mkjnnm

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

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

相关文章

financial是“财务”吗-《分析模式》漫谈14

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 “Analysis Patterns”的Preface&#xff08;前言&#xff09;有这么一句&#xff1a; David Creager, Steve Shepherd, and their team at Citibank worked with me in developing t…

鱼哥好书分享活动第27期:看完这篇《云原生安全》了解云原生环境安全攻防实战技巧!

鱼哥好书分享活动第27期&#xff1a;看完这篇《云原生安全》了解云原生安全攻防实战技巧&#xff01; 主要内容&#xff1a;读者对象&#xff1a;本书目录&#xff1a;了解更多&#xff1a;赠书抽奖规则: 当前全球数字化的发展逐步进入深水区&#xff0c;云计算模式已经广泛应用…

免费【2024】springboot 超市在线销售系统的设计与实现

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…

【MATLAB源码-第238期】基于simulink的三输出单端反激flyback仿真,通过PWM和PID控制能够得到稳定电压。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 概述 反激变换器是一种广泛应用于电源管理的拓扑结构&#xff0c;特别是在需要隔离输入和输出的应用中。它的工作原理是利用变压器的储能和释放能量来实现电压转换和隔离。该图展示了一个通过脉宽调制&#xff08;PWM&#…

7.24 补题

C 小w和大W的决斗 链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 题目描述 小w和大W为了比出谁更聪明。决定进行一场游戏。游戏内容如下: 两人轮流操作&#xff0c;小w先进行操作&#xff0c;每次操作可以选择下列两个其一: 选择数组中的一…

唐老狮 UGUI 实战学习笔记

仅作学习&#xff0c;不做任何商业用途 不是源码&#xff0c;不是源码! 是我通过"照虎画猫"写的&#xff0c;可能有些小修改 不提供素材&#xff0c;所以应该不算是盗版资源&#xff0c;侵权删 using System.Collections; using System.Collections.Generic; using …

深度解析Linux-C——结构体(初始化,结构体数组,结构体大小,位段操作,联合体,内存对齐,C的预处理,宏和带参宏,条件编译)

目录 结构体的三种初始化 结构体的两种引用 结构体数组 结构体大小 结构体实现位段操作 联合体 内存对齐 C的预处理 带参宏 条件编译 结构体的三种初始化 定义如下结构体 struct student {char name[100]; int age; float height; } ; 1、定义变量时初始化 s…

Matrix Equation(高斯线性异或消元+bitset优化)

题目&#xff1a; 登录—专业IT笔试面试备考平台_牛客网 思路&#xff1a; 我们发现对于矩阵C可以一列一列求。 mod2&#xff0c;当这一行相乘1的个数为奇数时&#xff0c;z(i,j)为1&#xff0c;偶数为0&#xff0c;是异或消元。 对于b[i&#xff0c;j]*c[i,j],b[i,j]可以…

【MQTT(4)】开发一个客户端,QT-Android安卓手机版本,Mosquitto替换成libhv库

我们采用 libhv是一个类似于libevent、libev、libuv的跨平台网络库&#xff0c;提供了更易用的接口和更丰富的协议。 https://github.com/ithewei/libhv?tabreadme-ov-file 编译脚本如下 Android compile WITH_MQTT #https://developer.android.com/ndk/downloads #export…

tof系统标定流程步骤详解

1、tof标定概述 系统校准是一个减少ToF系统中系统误差影响的过程,如图1.1所示。本文件旨在介绍校准方法、设备和软件 1.1 系统误差 1.1.1 周期误差 谐波失真导致的相位(距离)相关误差。 1.1.2 固定相位模式噪声 由于解调信号的时延取决于可见像素位置以及VCSEL和传感器…

c++中的最长递增子序列(Longest Increasing Subsequence)(算法章完结)

前言 hello大家好啊&#xff0c;我是文宇。 今天是最后一篇算法&#xff08;暂时性的&#xff0c;以后可能还有&#xff09; 最长递增子序列&#xff08;Longest Increasing Subsequence&#xff09; 最长递增子序列&#xff08;Longest Increasing Subsequence&#xff0c…

【Golang 面试基础题】每日 5 题(十)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

【机器学习】解开反向传播算法的奥秘

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 解开反向传播算法的奥秘反向传播算法的概述反向传播算法的数学推导1. 前向传播2…

Linux进程——程序地址空间详解

文章目录 程序地址空间地址空间与物理内存什么是程序地址空间管理程序地址空间虚拟地址与物理地址的映射页表的结构及其作用程序地址空间的作用 程序地址空间 我们之前学习内存的时候&#xff0c;有说内存的分布大概是这样的 其中堆由下而上&#xff0c;栈由上而下 除此之外&…

JavaScript青少年简明教程:函数及其相关知识(下)

JavaScript青少年简明教程&#xff1a;函数及其相关知识&#xff08;下&#xff09; 继续上一节介绍函数相关知识。 箭头函数&#xff08;Arrow Function&#xff09; 箭头函数是 ES6&#xff08;ECMAScript 2015&#xff09;及更高版本中引入的语法&#xff0c;用于简化函数…

LeetCode:删除排序链表中的重复元素(C语言)

1、问题概述&#xff1a;给定一个已排序链表的头&#xff0c;删除重复元素&#xff0c;返回已排序的链表 2、示例 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2] 示例 2&#xff1a; 输入&#xff1a;head [1,1,2,3,3] 输出&#xff1a;[1,2,3] 3…

深度解析Memcached:内存分配算法的优化之旅

&#x1f525; 深度解析Memcached&#xff1a;内存分配算法的优化之旅 Memcached是一个高性能的分布式内存缓存系统&#xff0c;广泛用于提高Web应用程序的性能。它通过减少数据库查询次数来加速数据检索。然而&#xff0c;Memcached的性能在很大程度上取决于其内存分配算法的…

前端三方库零碎(持续更新)

本文主要记录开发实践过程中遇到的前端库&#xff0c;做个记录总结&#xff0c;以备不时之需 easyui-datagrid EasyUI 是一个基于 jQuery 的用户界面插件库&#xff0c;提供了丰富的用户界面组件和工具&#xff0c;其中包括 datagrid&#xff08;数据表格&#xff09;组件。E…

2024年国际高校数学建模大赛(IMMCHE)问题A:金字塔石的运输成品文章分享(仅供学习)

2024 International Mathematics Molding Contest for Higher Education Problem A: Transportation of Pyramid Stones&#xff08;2024年国际高校数学建模大赛&#xff08;IMMCHE&#xff09;问题A&#xff1a;金字塔石的运输&#xff09; 古埃及金字塔石材运输优化模型研究…

spring(一)

一、spring特点 1.非侵入式&#xff1a;使用 Spring Framework 开发应用程序时&#xff0c;Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染&#xff1b;对功能性组件也只需要使用几个简单的注解进行标记&#xff0c;完全不会破坏原有结构&#xff0c;反而能…