浅析 Kubernetes 控制器的工作原理

7.5 Operator核心技术:深入理解Reconcile Loop、Informer、Workqueue 本文深入解析了Kubernetes Operator的核心技术组件,重点介绍了Reconcile Loop的设计原理与实现。Reconcile Loop遵循声明式API和期望状态驱动的设计理念,具有幂等性、自愈能力和最终一致性等特性。文章通过Go代码示例展示了Reconcile方法的基本结构、核心协调逻辑以及幂等性设计实现,并配以流程图说明其工作原理。同时提供了性能指标监控的实现方案,帮助开发者构建高效可靠的Operator。这些核心技术是Operator实现自动状态管理和自愈能力的基础。 阅读详情

Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致,它们就是 Kubernetes 的大脑。例如,ReplicaSet 控制器负责维护集群中运行的 Pod 数量;Node 控制器负责监控节点的状态,并在节点出现故障时及时做出响应。总而言之,在 Kubernetes 中,每个控制器只负责某种类型的特定资源。对于集群管理员来说,了解每个控制器的角色分工至关重要,如有必要,你还需要深入了解控制器的工作原理。

本文我将会带你深入了解 Kubernetes 控制器的内部结构、基本组件以及它的工作原理。本文使用的所有代码都是从 Kubernetes 控制器的当前实现代码中提取的,基于 Go 语言的 client-go 库。

1. 控制器的模型

Kubernetes 官方文档给出了控制器最完美的解释:

In applications of robotics and automation, a control loop is a non-terminating loop that regulates the state of the system. In Kubernetes, a controller is a control loop that watches the shared state of the cluster through the API server and makes changes attempting to move the current state towards the desired state. Examples of controllers that ship with Kubernetes today are the replication controller, endpoints controller, namespace controller, and serviceaccounts controller.

翻译:

在机器人设计和自动化的应用中,控制循环是一个用来调节系统状态的非终止循环。而在 Kubernetes 中,控制器就是前面提到的控制循环,它通过 API Server 监控整个集群的状态,并确保集群处于预期的工作状态。Kubernetes 自带的控制器有 ReplicaSet 控制器,Endpoint 控制器,Namespace 控制器和 Service Account 控制器等。

官方文档:Kube-controller-manager

Kubernetes 控制器会监视资源的创建/更新/删除事件,并触发 Reconcile 函数作为响应。整个调整过程被称作 “Reconcile Loop”(调谐循环)或者 “Sync Loop”(同步循环)。Reconcile 是一个使用 object(Resource 的实例)的命名空间和 object 名来调用的函数,使 object 的实际状态与 object 的 Spec 中定义的状态保持一致。调用完成后,Reconcile 会将 object 的状态更新为当前实际状态。

什么时候才会触发 Reconcile 函数呢?以 ReplicaSet 控制器为例,当收到了一个关于 ReplicaSet 的事件或者关于 ReplicaSet 创建 Pod 的事件时,就会触发 Reconcile 函数。

为了降低复杂性,Kubernetes 将所有的控制器都打包到 kube-controller-manager 这个守护进程中。下面是控制器最简单的实现方式:

for {
  desired := getDesiredState()
  current := getCurrentState()
  makeChanges(desired, current)
}
复制代码

2. 水平触发的 API

Kubernetes 的 API 和控制器都是基于水平触发的,可以促进系统的自我修复和周期协调。水平触发这个概念来自硬件的中断,中断可以是水平触发,也可以是边缘触发。

  • 水平触发 : 系统仅依赖于当前状态。即使系统错过了某个事件(可能因为故障挂掉了),当它恢复时,依然可以通过查看信号的当前状态来做出正确的响应。
  • 边缘触发 : 系统不仅依赖于当前状态,还依赖于过去的状态。如果系统错过了某个事件(“边缘”),则必须重新查看该事件才能恢复系统。

Kubernetes 水平触发的 API 实现方式是:监视系统的实际状态,并与对象的 Spec 中定义的期望状态进行对比,然后再调用 Reconcile 函数来调整实际状态,使之与期望状态相匹配。

水平触发的 API 也叫声明式 API。

水平触发的 API 有以下几个特点:

  • Reconcile 会跳过中间过程在 Spec 中声明的值,直接作用于当前 Spec 中声明的值。
  • 在触发 Reconcile 之前,控制器会并发处理多个事件,而不是串行处理每个事件。

举两个例子:

例 1:并发处理多个事件

用户创建了 1000 个副本数的 ReplicaSet,然后 ReplicaSet 控制器会创建 1000 个 Pod,并维护 ReplicaSet 的 Status 字段。在水平触发系统中,控制器会在触发 Reconcile 之前并发更新所有 Pod(Reconcile 函数仅接收对象的 Namespace 和 Name 作为参数),只需要更新 Status 字段 1 次。而在边缘触发系统中,控制器会串行响应每个 Pod 事件,这样就会更新 Status 字段 1000 次。

例 2:跳过中间状态

用户修改了某个 Deployment 的镜像,然后进行回滚。在回滚过程中发现容器陷入 crash 循环,需要增加内存限制。然后用户更新了 Deployment 的内容,调整内存限制,重新开始回滚。在水平触发系统中,控制器会立即停止上一次回滚动作,开始根据最新值进行回滚。而在边缘触发系统中,控制器必须等上一次回滚操作完成才能进行下一次回滚。

3. 控制器的内部结构

每个控制器内部都有两个核心组件:Informer/SharedInformer 和 Workqueue。其中 Informer/SharedInformer 负责 watch Kubernetes 资源对象的状态变化,然后将相关事件(evenets)发送到 Workqueue 中,最后再由控制器的 worker 从 Workqueue 中取出事件交给控制器处理程序进行处理。

事件 = 动作(create, update 或 delete) + 资源的 key(以 namespace/name 的形式表示)

Informer

控制器的主要作用是 watch 资源对象的当前状态和期望状态,然后发送指令来调整当前状态,使之更接近期望状态。为了获得资源对象当前状态的详细信息,控制器需要向 API Server 发送请求。

但频繁地调用 API Server 非常消耗集群资源,因此为了能够多次 get 和 list 对象,Kubernetes 开发人员最终决定使用 client-go 库提供的缓存机制。控制器并不需要频繁调用 API Server,只有当资源对象被创建,修改或删除时,才需要获取相关事件。client-go 库提供了 Listwatcher 接口用来获得某种资源的全部 Object,缓存在内存中;然后,调用 Watch API 去 watch 这种资源,去维护这份缓存;最后就不再调用 Kubernetes 的任何 API:

lw := cache.NewListWatchFromClient(
      client,
      &v1.Pod{},
      api.NamespaceAll,
      fieldSelector)
复制代码

上面的这些所有工作都是在 Informer 中完成的,Informer 的数据结构如下所示:

store, controller := cache.NewInformer {
	&cache.ListWatch{},
	&v1.Pod{},
	resyncPeriod,
	cache.ResourceEventHandlerFuncs{},
复制代码

尽管 Informer 还没有在 Kubernetes 的代码中被广泛使用(目前主要使用 SharedInformer,下文我会详述),但如果你想编写一个自定义的控制器,它仍然是一个必不可少的概念。

你可以把 Informer 理解为 API Server 与控制器之间的事件代理,把 Workqueue 理解为存储事件的数据结构。

下面是用于构造 Informer 的三种模式:

ListWatcher

ListWatcher 是对某个特定命名空间中某个特定资源的 list 和 watch 函数的集合。这样做有助于控制器只专注于某种特定资源。fieldSelector 是一种过滤器,它用来缩小资源搜索的范围,让控制器只检索匹配特定字段的资源。Listwatcher 的数据结构如下所示:

cache.ListWatch {
	listFunc := func(options metav1.ListOptions) (runtime.Object, error) {
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Do().
			Get()
	}
	watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
		options.Watch = true
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Watch()
	}
}
复制代码

Resource Event Handler

Resource Event Handler 用来处理相关资源发生变化的事件:

type ResourceEventHandlerFuncs struct {
	AddFunc    func(obj interface{})
	UpdateFunc func(oldObj, newObj interface{})
	DeleteFunc func(obj interface{})
}
复制代码
  • AddFunc : 当资源创建时被调用
  • UpdateFunc : 当已经存在的资源被修改时就会调用 UpdateFunc。oldObj 表示资源的最近一次已知状态。如果 Informer 向 API Server 重新同步,则不管资源有没有发生更改,都会调用 UpdateFunc。
  • DeleteFunc : 当已经存在的资源被删除时就会调用 DeleteFunc。该函数会获取资源的最近一次已知状态,如果无法获取,就会得到一个类型为 DeletedFinalStateUnknown 的对象。

ResyncPeriod

ResyncPeriod 用来设置控制器遍历缓存中的资源以及执行 UpdateFunc 的频率。这样做可以周期性地验证资源的当前状态是否与期望状态匹配。

如果控制器错过了 update 操作或者上一次操作失败了,ResyncPeriod 将会起到很大的弥补作用。如果你想编写自定义控制器,不要把周期设置太短,否则系统负载会非常高。

SharedInformer

通过上文我们已经了解到,Informer 会将资源缓存在本地以供自己后续使用。但 Kubernetes 中运行了很多控制器,有很多资源需要管理,难免会出现以下这种重叠的情况:一个资源受到多个控制器管理。

为了应对这种场景,可以通过 SharedInformer 来创建一份供多个控制器共享的缓存。这样就不需要再重复缓存资源,也减少了系统的内存开销。使用了 SharedInformer 之后,不管有多少个控制器同时读取事件,SharedInformer 只会调用一个 Watch API 来 watch 上游的 API Server,大大降低了 API Server 的负载。实际上 kube-controller-manager 就是这么工作的。

SharedInformer 提供 hooks 来接收添加、更新或删除某个资源的事件通知。还提供了相关函数用于访问共享缓存并确定何时启用缓存,这样可以减少与 API Server 的连接次数,降低 API Server 的重复序列化成本和控制器的重复反序列化成本。

lw := cache.NewListWatchFromClient(…)
sharedInformer := cache.NewSharedInformer(lw, &api.Pod{}, resyncPeriod)
复制代码

Workqueue

由于 SharedInformer 提供的缓存是共享的,所以它无法跟踪每个控制器,这就需要控制器自己实现排队和重试机制。因此,大多数 Resource Event Handler 所做的工作只是将事件放入消费者工作队列中。

每当资源被修改时,Resource Event Handler 就会放入一个 key 到 Workqueue 中。key 的表示形式为 <resource_namespace>/<resource_name>,如果提供了 <resource_namespace>,key 的表示形式就是 <resource_name>。每个事件都以 key 作为标识,因此每个消费者(控制器)都可以使用 workers 从 Workqueue 中读取 key。所有的读取动作都是串行的,这就保证了不会出现两个 worker 同时读取同一个 key 的情况。

Workqueue 在 client-go 库中的位置为 client-go/util/workqueue,支持的队列类型包括延迟队列,定时队列和速率限制队列。下面是速率限制队列的一个示例:

queue :=
workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
复制代码

Workqueue 提供了很多函数来处理 key,每个 key 在 Workqueue 中的生命周期如下图所示:

 

 

 

如果处理事件失败,控制器就会调用 AddRateLimited() 函数将事件的 key 放回 Workqueue 以供后续重试(如果重试次数没有达到上限)。如果处理成功,控制器就会调用 Forget() 函数将事件的 key 从 Workqueue 中移除。注意:该函数仅仅只是让 Workqueue 停止跟踪事件历史,如果想从 Workqueue 中完全移除事件,需要调用 Done() 函数。

现在我们知道,Workqueue 可以处理来自缓存的事件通知,但还有一个问题:控制器应该何时启用 workers 来处理 Workqueue 中的事件呢?

控制器需要等到缓存完全同步到最新状态才能开始处理 Workqueue 中的事件,主要有两个原因:

  1. 在缓存完全同步之前,获取的资源信息是不准确的。
  2. 对单个资源的多次快速更新将由缓存合并到最新版本中,因此控制器必须等到缓存变为空闲状态才能开始处理事件,不然只会把时间浪费在等待上。

这种做法的伪代码如下:

controller.informer = cache.NewSharedInformer(...)
controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

controller.informer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, controller.HasSynched)
{
	log.Errorf("Timed out waiting for caches to sync"))
}

// Now start processing
controller.runWorker()
复制代码

所有处理流程如下所示:

 

 

 

控制器处理事件的流程

4. 参考资料


作者:米开朗基杨
链接:https://juejin.im/post/5ca08e2f6fb9a05e2c5ddba6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

kubernetes技术详解,带你深入了解k8s 在Docker 作为高级容器引擎快速发展的同时,在Google内部,容器技术已经应用了很多年Borg系统运行管理着成千上万的容器应用。Kubernetes项目来源于Borg,可以说是集结了Borg设计思想的精华,并且吸收了Borg系统中的经验和教训。Kubernetes对计算资源进行了更高层次的抽象,通过将容器进行细致的组合,将最终的应用服务交给用户。kubernetes的本质是一组服务器集群,它可以在集群的每个节点上运行特定的程序,来对节点中的容器进行管理。自我修复。 阅读详情

相关推荐

Kubernetes K8S之资源控制器RC、RS、Deployment详解

Kubernetes的资源控制器ReplicationController(RC)、ReplicaSet(RS)、Deployment(Deploy)详解与示例

踏歌行的专栏 3517

k8s实战之kubectl

本课程将详细介绍k8s所有命令,以及命令的go源码分析,学习知其然,知其所以然

k8s之控制器

控制器也是管理pod的一种手段自主式pod:pod退出或意外关闭后不会被重新创建控制器管理的 Pod:在控制器的生命周期里,始终要维持 Pod 的副本数目Pod控制器是管理pod的中间层,使用Pod控制器之后,只需要告诉Pod控制器,想要多少个什么样的Pod就可以了,它会创建出满足条件的Pod并确保每一个Pod资源处于用户期望的目标状态。如果Pod资源在运行中出现故障,它会基于指定策略重新编排Pod。

hotslow的博客 924

K8S 资源控制器

1. 概述 kubernetes 控制器会监听资源的 创建/更新/删除 事件,并触发 Reconcile 函数作为响应。整个调整过程被称作 “Reconcile Loop”(调谐循环) 或者 “Sync Loop“(同步循环) Reconcile 是一个使用资源对象的命名空间和资源对象名称来调用的函数,使得资源对象的实际状态与 资源清单中定义的状态保持一致。调用完成后,Reconcile 会将资源对象的状态更新为当前实际状态。 for { desired := getDesiredState() c

elihe2011的专栏 3181

K8S系列:Deployment报错:missing required field “selector“ in io.k8s.api.apps.v1.DeploymentSpec

selector属性,添加红色部分代码即可。

NIO4444 920

给IC设计新人的Tessent Scan入门指南:从RTL到ATPG的完整DFT流程解析

本文为IC设计新手提供Tessent Scan的完整DFT流程指南,从RTL代码检查到ATPG测试向量生成,详细解析扫描链配置、插入及验证的关键步骤。通过实战案例和工具命令示例,帮助工程师快速掌握Tessent工具链的应用技巧,提升芯片可测试性设计的效率和质量。

weixin_32541663的博客 451

什么是Kubernetes?K8s基础与工作原理

Kubernetes,通常简称为K8s,是一个用于自动化部署、扩展和管理容器化应用程序的开源容器编排平台。它由 Google 于 2014 年开源,后来交由 CNCF(Cloud Native Computing Foundation)管理。Kubernetes 的名字来自希腊语,意思是“舵手”或“飞行员”,表明它的作用是为容器化应用提供控制与调度。

java开发 2669

【成功安装】Ubuntu22.04极简方式安装Nvidia驱动

今天下午安装了实机的Ubuntu,面临在Linux上安装Nvidia的问题,被这个问题困扰了一整个下午,各种报错整的我头大。后来发现了不需要按照传统的方式去Nvidia官网下载,再命令行安装,直接在线安装即可。

清筱明的博客 5346

一、kubernetes k8s

k8s的全称:kubernetes k8sk8s的版本:1.301.20------------用的最多的版本,1.18-1.211.24------------>k8s的镜像不再使用docker,containerd。

2401_87896172的博客 3661

游戏资源解包神器NSCB实战:手把手教你提取Switch游戏视频(nsm转MP4)

本文详细介绍了如何使用NSCB工具链从Switch游戏中提取nsm视频和hca音频资源,并将其转换为可编辑的MP4和MP3格式。通过NSC Builder、CriPakTools等工具的分步操作指南,帮助内容创作者高效获取游戏资源,同时提供音视频同步合成、批量处理等高级技巧,为创意素材制作提供完整解决方案。

weixin_29197835的博客 891

企业财务共享服务中心业务蓝图规划方案【附全文阅读】

《企业财务共享服务中心业务蓝图规划方案》为集团企业提供财务共享中心建设全流程指导,涵盖战略定位、组织架构、业务流程、信息系统、运营管理和变革实施六大维度。方案明确共享中心"交易处理+战略支撑"双重职能,设计专业分组架构,筛选标准化高、价值明确的流程(如核算、资金结算等)纳入共享范围,构建业财一体化平台,并配套绩效与风险管控体系。结合项目型企业管理特点,分阶段推进实施,助力企业实现财务集中化与标准化转型。

cdfunlove的博客 994

什么是Kubernetes(K8s)?

更新时间: 2024-02-02 16:41:38内容Kubernetes(通常简称为K8s)是一款用于自动部署、扩缩和管理容器化应用程序的开源容器编排平台。Kubernetes已发展为现代企业实现敏捷开发、快速迭代、资源优化及灵活扩展的关键技术组件之一。它拥有庞大的开源社区和丰富的生态系统。围绕Kubernetes已经形成了众多工具、插件和解决方案,其使企业能够实现更高的资源利用率、更优的业务连续性和稳定性、更快的创新速度。为什么需要。

weixin_46575696的博客 2420

为什么先进工艺需要check那么多corner?

那么hold slack = launch clock path delay + data path delay - capture_clock_path_delay = -capture_clock_path_delay,也就是说,capture clock path delay越大,hold越难满足。而且,我们在设计的时候,一般clock频率都会过约,所以是有较大的margin的。有的foundry还会对rc的取值范围进行了约束,如cworst_T,采用的是1.5 sigma的取值范围。

芯冰乐 2087

深入浅出 Kubernetes 控制器的工作原理

Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致,它们就是 Kubernetes 的大脑。例如,ReplicaSet 控制器负责维护集群中运行的 Pod 数量;Node 控制器负责监控节点的状态,并在节点出现故障时及时做出响应。总而言之,在 Kubernetes 中,每个控制器只负责某种类型的特定资源。对于集群管理员来说,了解每个控制器的角色分工至关重要,如有必要,你还需要深入了解控制器的工作原理。 本文我将会带你深入了解 Kubernetes 控制器的内部结构、基本组件以及

huakai_sun的博客 1749

linux驱动由浅入深系列:块设备驱动之一(高通eMMC分区实例)

linux驱动由浅入深系列:块设备驱动之一(高通eMMC分区实例)linux驱动由浅入深系列:块设备驱动之二(块设备驱动结构分析)块设备驱动的模型还是基本基于字符设备驱动的,可以简单理解为块设备仅仅增加了操作缓冲区,对用户请求顺序进行队列重拍等等。字符设备驱动的相关分析可以查看本博客相关的博文。 按照本博客的行文习惯,在具体分析块设备驱动代码之前,我们会从整体上了解一下研究对象的特征,以及用户空间...

Radia的专栏 1万+

大模型微调部署实战及类GPT工具的高效使用

本文主要介绍了专栏《大模型微调部署实战及类GPT工具的高效使用》的核心内容,希望对使用大语言模型的同学们有所帮助。 文章目录 1. 前言 2. 专栏亮点 3. 你的收获 4. 详细目录

herosunly的博客 22万+

k8s kubernetes

(Container Runtime Interface): 容器运行时接口,用来运行容器,k8s通过这个接口去兼容不同的容器,目前主要的CRI有CRI-O和containered,dockershim在k8s的1.24版本后从kubelet中剥离出来,主要由docker维护(不是亲儿子k8s也不会管了):containerd由docker开发,是docker用来操作runc的handler,与docker平级,docker生成的images等资源与containerd生成的images等资源不共享。

六月长安的博客 2735

上下相机对位,上下贴合,上下相机映射对位场景案例

上下相机对位,上下贴合,上下相机映射对位场景案例

视觉人机器视觉的博客 1万+

【kubernetes】关于k8s集群的pod控制器

1、statefulset用于部署有状态的应用(有实时数据需要存储的应用)2、部署statefulset之前,需要先创建一个headless service(表示clusterip为node的特殊service类型),statefulset的配置清单模板需要去指定servicename为headless service的名称。3、statefulset创建的pod名称是固定不变的,且唯一的。

liu_xueyin的博客 1675

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

【Kubernetes】K8s的工作原理

K8s概述 我清晰地记得曾经读到过的一篇博文,上面是这样写的, “云端教父AWS云端架构策略副总裁Adrian Cockcroft曾指出,两者虽然都是运用容器技术,但最大的差异是,Docker是要解决应用程序开发(Developing)问题,而Kubernetes是要解决更上层的应用程序运维问题(Operation)。开发问题是早期的痛点,但随着企业越来越依赖容器技术,内部应用越来越多是云原生应用时,运维会是企业IT的新痛点。”大佬的一番话,明确地指出K8S的生存土壤! 学习一项技术,除了需要明确这项技

satiling的专栏 1336

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

【云原生_K8S系列】Kubernetes 控制器之 Deployment

Deployment 是 Kubernetes 中的一个控制器,它管理 Pod 的副本集(ReplicaSet)。通过定义 Deployment,用户可以描述一个应用的期望状态,例如需要多少个副本、使用什么镜像以及如何进行滚动更新。Deployment 会根据这些定义自动创建和管理 Pod,确保应用始终处于期望状态。动态创建和销毁 Pod查看升级进度和状态升级回滚,并具有版本记录暂停和恢复升级滚动重启Kubernetes Deployment 是管理应用生命周期的强大工具。

didiplus 2726

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

【Kubernetes】k8s集群的Pod控制器

k8s集群的Pod控制器

weixin_68840588的博客 1394

Python 解析 Kubernetes Ingress 控制器的工作原理

本文介绍了 Kubernetes Ingress 控制器的基本工作原理,并展示了如何使用 Python 编写脚本来解析和分析 Ingress 资源。通过这种方式,开发者可以更方便地调试和优化他们的 Ingress 配置,确保应用程序能够正确地对外提供服务。希望这篇博客对你有所帮助!如果你有任何问题或建议,请随时留言交流。```

2501_91095393的博客 1793

【Kubernetes知识点问答题】kubernetes 控制器

摘要:K8s控制器负责维护集群状态与期望状态的一致性。ReplicaSet确保Pod副本数恒定,Deployment提供声明式更新能力用于应用部署和版本管理。DaemonSet确保每个节点运行特定Pod,常用于集群守护进程。StatefulSet管理有状态应用,维护Pod持久标识符。Job处理一次性任务,CronJob执行定时任务。这些控制器共同构成了K8s强大的编排能力。(150字)

2201_75486314的博客 1157

【云原生_K8S系列】Kubernetes 控制器简介

Kubernetes是一个开源的容器编排平台,旨在自动化部署、扩展和管理容器化应用。Kubernetes 的核心组件之一是控制器(Controller),它负责确保集群中的实际状态与用户定义的期望状态一致。控制器是Kubernetes控制平面的一个重要组成部分,通过持续监控集群的状态并执行必要的操作来维护集群的健康和稳定。

didiplus 1918
上一篇: 设计模式(Python)-简单工厂,工厂方法和抽象工厂模式
下一篇: 访问者模式-Visitor Pattern
cbmljs
博客等级 码龄10年 329粉丝 130原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值