client-go源码学习(四):自定义Controller的工作原理、WorkQueue

news2024/11/25 13:02:45

本文基于Kubernetes v1.22.4版本进行源码学习,对应的client-go版本为v0.22.4

4、自定义Controller的工作原理

在这里插入图片描述

Controller中主要使用到Informer和WorkQueue两个核心组件

Controller可以有一个或多个Informer来跟踪某一个resource。Informer跟Kubernetes API Server保持通讯获取资源的最新状态并更新到本地的cache中,一旦跟踪的资源有变化,Informer就会调用callback把关心的变更的Object放到WorkQueue里面

Worker执行真正的业务逻辑,计算和比较WorkQueue里items的当前状态和期望状态的差别,然后通过client-go向Kubernetes API Server发送请求,直到驱动这个集群向用户要求的状态演化

5、WorkQueue

WorkQueue支持3种队列类型:

  • FIFO队列
  • 延迟队列
  • 限速队列

1)、FIFO队列

FIFO队列支持最基本的队列方法,WorkQueue中的限速及延迟队列都基于FIFO队列的Interface接口实现,其提供如下方法:

// vendor/k8s.io/client-go/util/workqueue/queue.go
type Interface interface {
	Add(item interface{}) // 给队列添加元素
	Len() int // 返回当前队列的长度
	Get() (item interface{}, shutdown bool) // 获取队列头部的一个元素
	Done(item interface{}) // 标记队列中该元素已被处理
	ShutDown() // 关闭队列
	ShuttingDown() bool // 查询队列是否正在关闭
}

FIFO队列数据结构如下:

// vendor/k8s.io/client-go/util/workqueue/queue.go
type Type struct {
	queue []t // 定义队列,具有顺序性,待处理元素列表

	dirty set // 标记所有需要被处理的元素

	processing set // 当前正在被处理的元素

	cond *sync.Cond

	shuttingDown bool // 是否正在关闭

	metrics queueMetrics

	unfinishedWorkUpdatePeriod time.Duration
	clock                      clock.Clock
}

type empty struct{}
type t interface{}
type set map[t]empty

FIFO队列的核心方法:Add、Get、Done,代码如下:

// vendor/k8s.io/client-go/util/workqueue/queue.go
func (q *Type) Add(item interface{}) {
	q.cond.L.Lock()
	defer q.cond.L.Unlock()
	// 队列正在关闭,直接返回
	if q.shuttingDown {
		return
	}
	// dirty中已经包含该元素,直接返回
	if q.dirty.has(item) {
		return
	}

	q.metrics.add(item)
	// 添加到dirty中
	q.dirty.insert(item)
	// 元素正在被处理,直接返回
	if q.processing.has(item) {
		return
	}
	// 添加到待处理元素列表
	q.queue = append(q.queue, item)
	// 通知有新元素到了,此时有协程阻塞就会被唤醒
	q.cond.Signal()
}

func (q *Type) Get() (item interface{}, shutdown bool) {
	q.cond.L.Lock()
	defer q.cond.L.Unlock()
	// 队列中没有数据,阻塞协程
	for len(q.queue) == 0 && !q.shuttingDown {
		q.cond.Wait()
	}
	// 协程被激活但还没有数据,说明队列被关闭了
	if len(q.queue) == 0 {
		// We must be shutting down.
		return nil, true
	}
	// 弹出队列中的第一个元素
	item, q.queue = q.queue[0], q.queue[1:]

	q.metrics.get(item)
	// 加入到processing中,从dirty中移除
	q.processing.insert(item)
	q.dirty.delete(item)

	return item, false
}

func (q *Type) Done(item interface{}) {
	q.cond.L.Lock()
	defer q.cond.L.Unlock()

	q.metrics.done(item)
	// 从processing中移除
	q.processing.delete(item)
	// 判断dirty中,看看处理期间是不是又被添加,如果是,就在放到队列中
	if q.dirty.has(item) {
		q.queue = append(q.queue, item)
		q.cond.Signal()
	}
}

FIFIO存储过程如下:

在这里插入图片描述

  1. 通过Add方法往FIFO队列中分别插入1、2、3这3个元素,此时queue和dirty中分别存有1、2、3元素,processing为空
  2. 然后通过Get方法获取最先进入的元素(1元素),此时queue和dirty中分别存有2、3元素,而1元素被放入processing中,表示该元素正在被处理
  3. 最后,当处理完1元素时,通过Done方法标记该元素已经被处理完成,此时processing中的1元素会被删除

FIFO并发存储过程如下:

在这里插入图片描述

  1. 在并发场景下,goroutine A通过Get方法获取1元素,1元素被添加到processing中
  2. 此时,goroutine B通过Add方法插入另一个1元素,此时在processing中已经存在相同的元素,所以后面的1元素并不会被直接添加到queue中,当前FIFO队列中的dirty中存有1、2、3元素,processing中存有1元素
  3. 在goroutine A通过Done方法标记元素1处理完成后,如果dirty中存有1元素,则将1元素追加到queue的尾部。正在处理中的元素还没有调用Done时,此时再添加该元素应该是最新的,处理中的应该是过时的,所以在goroutine A调用Done方法之后,该元素会重新入队

dirty和processing都是用Hash Map实现的,所以不需要考虑无序,只保证去重即可

2)、延迟队列

延迟队列,基于FIFO队列接口封装,在原有功能上增加了AddAfter方法,其原理是延迟一段时间后再将元素插入FIFO队列。延迟队列数据结构如下:

// vendor/k8s.io/client-go/util/workqueue/delaying_queue.go
type DelayingInterface interface {
	Interface

	AddAfter(item interface{}, duration time.Duration)
}

type delayingType struct {
	Interface

	clock clock.Clock

	stopCh chan struct{}

	stopOnce sync.Once

	heartbeat clock.Ticker

	waitingForAddCh chan *waitFor // 所有延迟添加的元素封装成waitFor放到chan中,默认初始大小为1000

	metrics retryMetrics
}

type waitFor struct {
	data    t
	readyAt time.Time
	index int
}

延迟队列的AddAfter方法,该方法有一个duration(延迟时间)参数,该参数用于指定元素延迟插入FIFO队列的时间。如果duration小于或等于0,会直接将元素插入FIFO队列中,代码如下:

// vendor/k8s.io/client-go/util/workqueue/delaying_queue.go
func (q *delayingType) AddAfter(item interface{}, duration time.Duration) {
	// 如果队列关闭,直接返回
	if q.ShuttingDown() {
		return
	}

	q.metrics.retry()

	// 不需要延迟,直接将元素插入FIFO队列中
	if duration <= 0 {
		q.Add(item)
		return
	}

	select {
	case <-q.stopCh:
  // 把元素封装成waitFor传入chan
	case q.waitingForAddCh <- &waitFor{data: item, readyAt: q.clock.Now().Add(duration)}:
	}
}

AddAfter方法就是简单把元素添加到chan中, 所以核心实现在从chan中获取数据的waitingLoop方法, 如下所示:

// vendor/k8s.io/client-go/util/workqueue/delaying_queue.go
func (q *delayingType) waitingLoop() {
	defer utilruntime.HandleCrash()

	never := make(<-chan time.Time)

	var nextReadyAtTimer clock.Timer

	// waitForPriorityQueue把需要延迟的元素形成了一个队列,队列按照元素的延迟添加的时间(readyAt)从小到大排序
	waitingForQueue := &waitForPriorityQueue{}
	heap.Init(waitingForQueue)
	
	waitingEntryByData := map[t]*waitFor{}

	for {
		if q.Interface.ShuttingDown() {
			return
		}

		// 获取当前时间
		now := q.clock.Now()

		// waitingForQueue中是否有元素
		for waitingForQueue.Len() > 0 {
			// 获取waitingForQueue中的第一个元素
			// waitingForQueue中的元素按照延迟添加的时间从小到大排序,第一个元素为时间最小的元素
			entry := waitingForQueue.Peek().(*waitFor)
			// 元素指定延迟添加的时间过了吗?如果没有过就跳出循环
			if entry.readyAt.After(now) {
				break
			}

			// 指定延迟添加的时间过了,就把元素从waitingForQueue中拿出来放入FIFO队列中
			entry = heap.Pop(waitingForQueue).(*waitFor)
			q.Add(entry.data)
			delete(waitingEntryByData, entry.data)
		}

		nextReadyAt := never
		// 如果waitingForQueue中有元素,用第一个元素指定的时间减去当前时间作为等待时间
		if waitingForQueue.Len() > 0 {
			if nextReadyAtTimer != nil {
				nextReadyAtTimer.Stop()
			}
			entry := waitingForQueue.Peek().(*waitFor)
			nextReadyAtTimer = q.clock.NewTimer(entry.readyAt.Sub(now))
			nextReadyAt = nextReadyAtTimer.C()
		}

		select {
		case <-q.stopCh:
			return

		case <-q.heartbeat.C():
		// waitingForQueue中需要等待时间信号,时间到就会有信号
		case <-nextReadyAt:
		// 从chan中获取元素的,AddAfter()放入chan中的元素
		case waitEntry := <-q.waitingForAddCh:
			// 如果时间已经过了就直接放入FIFO队列中,没过就插入到有序队列
			if waitEntry.readyAt.After(q.clock.Now()) {
				insert(waitingForQueue, waitingEntryByData, waitEntry)
			} else {
				q.Add(waitEntry.data)
			}

			// 把chan中的元素全部取出,用了default意味着chan中没有数据就会立刻停止
			drained := false
			for !drained {
				select {
				case waitEntry := <-q.waitingForAddCh:
					if waitEntry.readyAt.After(q.clock.Now()) {
						insert(waitingForQueue, waitingEntryByData, waitEntry)
					} else {
						q.Add(waitEntry.data)
					}
				default:
					drained = true
				}
			}
		}
	}
}

延迟队列运行原理如下:

在这里插入图片描述

将元素1添加到waitingForAddCh中,通过waitingLoop方法消费元素数据。当元素的延迟时间不大于当前时间时,说明还需要延迟元素插入FIFO队列的时间,此时将该元素放入到waitingForQueue中。当元素的延迟时间大于当前时间时,则将该元素插入FIFO队列中。另外,还会遍历waitingForQueue中的元素,按照上述逻辑验证时间

3)、限速队列

限速队列,基于延迟队列和FIFO队列接口封装,在原有功能上增加了AddRateLimited、Forget、NumRequeues方法。限速队列接口定义如下:

// vendor/k8s.io/client-go/util/workqueue/rate_limiting_queue.go
type RateLimitingInterface interface {
	DelayingInterface

	AddRateLimited(item interface{}) // 往队列里加入一个元素

	Forget(item interface{}) // 停止元素重试

	NumRequeues(item interface{}) int // 查询这个元素被处理多少次了
}

限速队列的重点在于它提供的4种限速算法接口。其原理是,限速队列利用延迟队列的特性,延迟某个元素的插入时间,达到限速目的。RateLimiter接口定义如下:

// vendor/k8s.io/client-go/util/workqueue/default_rate_limiters.go
type RateLimiter interface {

	When(item interface{}) time.Duration // 获取元素需要等待多长时间

	Forget(item interface{}) // 抛弃该元素,意味着该元素已经被处理了

	NumRequeues(item interface{}) int // 获取元素放入队列的次数
}

WorkQueue提供了4种限速算法:

  • 令牌桶算法(BucketRateLimiter)
  • 排队指数算法(ItemExponentialFailureRateLimiter)
  • 计数器算法(ItemFastSlowRateLimiter)
  • 混合模式(MaxOfRateLimiter),将多种限速算法混合使用
1)令牌桶算法

令牌桶算法是通过Go第三方库golang.org/x/time/rate实现的。令牌桶算法内部实现了一个存放token的桶,初始时桶是空的,token会以固定速率往桶里填充,直到将其填满为止,多余的token会被丢弃。每个元素都会从令牌桶得到一个token,只有得到token的元素才允许通过,而没有得到token的元素处于等待状态。实现元素如下图:

WorkQueue在默认的情况下会实例化令牌桶,代码如下:

rate.NewLimiter(rate.Limit(10), 100)

在实例化rate.NewLimiter时传入r和b两个参数:r参数表示每秒往桶里填充的token数量;b参数表示令牌桶的大小,即令牌桶最多存放的token数量

2)排队指数算法

排队指数算法将相同元素的排队数作为指数,排队数增大,速率限制呈指数级增长,但其最大值不会超过maxDelay。代码实现如下:

// vendor/k8s.io/client-go/util/workqueue/default_rate_limiters.go
type ItemExponentialFailureRateLimiter struct {
	failuresLock sync.Mutex
	failures     map[interface{}]int // 记录元素的排队数,每调用一次When累加一次

	baseDelay time.Duration // 延迟基数
	maxDelay  time.Duration // 最大延迟时间
}

func (r *ItemExponentialFailureRateLimiter) When(item interface{}) time.Duration {
	r.failuresLock.Lock()
	defer r.failuresLock.Unlock()

	// 累加元素的排队数
	exp := r.failures[item]
	r.failures[item] = r.failures[item] + 1

	// 通过元素的排队数计算延迟时间,公式是 2^i * baseDelay,呈指数级增长
	// The backoff is capped such that 'calculated' value never overflows.
	backoff := float64(r.baseDelay.Nanoseconds()) * math.Pow(2, float64(exp))
	// 计算后的延迟时间和最大延迟时间取最小值
	if backoff > math.MaxInt64 {
		return r.maxDelay
	}

	calculated := time.Duration(backoff)
	if calculated > r.maxDelay {
		return r.maxDelay
	}

	return calculated
}

func (r *ItemExponentialFailureRateLimiter) NumRequeues(item interface{}) int {
	r.failuresLock.Lock()
	defer r.failuresLock.Unlock()

	return r.failures[item]
}

func (r *ItemExponentialFailureRateLimiter) Forget(item interface{}) {
	r.failuresLock.Lock()
	defer r.failuresLock.Unlock()

	// 清空元素的排队数
	delete(r.failures, item)
}

元素的排队数统计是有限速周期的,一个限速周期是指从执行AddRateLimited方法到执行完Forget方法之间的时间。如果该元素被Forget方法处理完,则清空排队数

该算法提供了3个主要字段:failures用于统计元素排队数,每当AddRateLimited方法插入新元素时,会为该字段加1;baseDelay是延迟基数(默认为5ms),maxDelay是最大延迟时间(默认为1000s)。通过元素的排队数计算延迟时间,公式是 2 i ∗ b a s e D e l a y 2^i * baseDelay 2ibaseDelay

3)计数器算法

计数器算法的原理是:限制一段时间内允许通过的元素数量,例如在1分钟内只允许通过1000个元素,每插入一个元素,计算器自增1,当计数器到100的阈值且还在限速周期内时,则不允许元素再通过。WorkQueue在此基础上扩展了fast和slow速率,代码实现如下:

// vendor/k8s.io/client-go/util/workqueue/default_rate_limiters.go
type ItemFastSlowRateLimiter struct {
	failuresLock sync.Mutex
	failures     map[interface{}]int // 统计元素排队数

	maxFastAttempts int           // 用于控制从fastDelay转换到slowDelay
	fastDelay       time.Duration // 短延迟时间
	slowDelay       time.Duration // 长延迟时间
}

func (r *ItemFastSlowRateLimiter) When(item interface{}) time.Duration {
	r.failuresLock.Lock()
	defer r.failuresLock.Unlock()

	// 累加元素的排队数
	r.failures[item] = r.failures[item] + 1
	// 元素的排队数超过阈值用长延迟时间,否则用短延迟时间
	if r.failures[item] <= r.maxFastAttempts {
		return r.fastDelay
	}

	return r.slowDelay
}

func (r *ItemFastSlowRateLimiter) NumRequeues(item interface{}) int {
	r.failuresLock.Lock()
	defer r.failuresLock.Unlock()

	return r.failures[item]
}

func (r *ItemFastSlowRateLimiter) Forget(item interface{}) {
	r.failuresLock.Lock()
	defer r.failuresLock.Unlock()

	// 清空元素的排队数
	delete(r.failures, item)
}

假设fastDelay是5ms,slowDelay是10s,maxFastAttempts是3。在一个限速周期内通过AddRateLimited方法插入4个相同的元素,那么前3个元素使用fastDelay的5ms,当触发maxFastAttempts时,第4个元素使用slowDelay的10s

4)混合模式

混合模式内部有多个限速器,每次返回所有限速器里延迟最大的时间。代码如下:

// vendor/k8s.io/client-go/util/workqueue/default_rate_limiters.go
type MaxOfRateLimiter struct {
	limiters []RateLimiter // 限速器数组
}

func (r *MaxOfRateLimiter) When(item interface{}) time.Duration {
	ret := time.Duration(0)
	// 获取所有限速器里最大的延迟时间
	for _, limiter := range r.limiters {
		curr := limiter.When(item)
		if curr > ret {
			ret = curr
		}
	}

	return ret
}

func (r *MaxOfRateLimiter) NumRequeues(item interface{}) int {
	ret := 0
	for _, limiter := range r.limiters {
		curr := limiter.NumRequeues(item)
		if curr > ret {
			ret = curr
		}
	}

	return ret
}

func (r *MaxOfRateLimiter) Forget(item interface{}) {
	for _, limiter := range r.limiters {
		limiter.Forget(item)
	}
}

参考:

《Kubernetes源码剖析》

深入浅出kubernetes之client-go的workqueue

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

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

相关文章

安装 Visual Studio Code、MinGW-w64、CMake

文章目录1.安装 Visual Studio Code1.1 下载1.2 安装2.安装 MinGW-w642.1 下载2.2 解压到合适的目录下2.3 添加到环境变量2.4 测试是否安装成功3.安装 CMake3.1 下载3.2 解压到合适的目录下3.3 添加到环境变量3.4 测试是否安装成功1.安装 Visual Studio Code 1.1 下载 Visual…

Options API

computed计算属性 1、复杂data的处理方式 我们知道&#xff0c;在模板中可以直接通过插值语法显示一些data中的数据。 但是在某些情况&#xff0c;我们可能需要对数据进行一些转化后再显示&#xff0c;或者需要将多个数据结合起来进行显示&#xff1b; 比如我们需要对多个d…

自动驾驶控制算法之车辆纵向控制(project)

本文为深蓝学院-自动驾驶控制与规划-第二章作业 目录 1 project introduction 2 思路提示 3 解决积分饱和的方法 3.1 IC 积分遇限削弱法 3.2 BC 反馈抑制抗饱和 4 ROSLGSVL联合仿真 1 project introduction 本项目希望大家根据PID控制方法实现一个巡航控制系统。我们已…

深度学习之边缘检测算法论文解读(EDTER: Edge Detection with Transformer)

引言 边缘检测是计算机视觉中最基本的问题之一&#xff0c;具有广泛的应用&#xff0c;例如图像分割[8&#xff0c;23&#xff0c;39&#xff0c;44&#xff0c;45&#xff0c;47]、对象检测[23]和视频对象分割[5&#xff0c;57&#xff0c;59]。给定输入图像&#xff0c;边缘…

webpack-dev-server:静态资源目录配置

目录 webpack-dev-server Webpack项目-配置自动打包 访问错误信息分析 简单配置静态资源访问目录 完整配置静态资源访问目录 directory属性 staticOptions属性 publicPath属性 serveIndex属性 watch属性 完整配置webpack.config.js示例 默认显示index.html内容配置 默…

fsdb DUMP的操作记录

参考链接&#xff1a; https://blog.csdn.net/ohuo666/article/details/124973939https://blog.csdn.net/ohuo666/article/details/124973939 https://blog.csdn.net/yuexiangallan/article/details/121760768https://blog.csdn.net/yuexiangallan/article/details/121760768…

基于 DolphinDB 的行情中心解决方案

随着国内量化金融的高速发展&#xff0c;行情数据所包含的微观交易结构信息越来越受到券商自营团队、资管团队以及各类基金的重视。这些交易团队迫切希望拥有一个与生产环境类似的投研仿真环境&#xff0c;提升研发的效率和质量。作为国内领先的高性能时序数据库厂商&#xff0…

【地铁上的Redis与C#】数据类型(七)--List类型

我们这篇文章开始讲解list类型。 什么是list list是一个存储空间保存多个数据&#xff0c;底层使用双向链表存储结构实现的一种Redis数据类型&#xff0c;。list类型一般用在存储多个数据&#xff0c;并需要对数据进入存储空间的顺序进行区分的情况下。list的存储方式是一个存…

气泡法检漏技术特点分析和新型压力衰减法测试技术

摘要&#xff1a;针对传统的气泡法检漏技术&#xff0c;本文详细介绍了气泡法的基本原理、气泡法中的两种标准方法——加压法和真空法以及对应的标准规范&#xff0c;并对这两种气泡法进行了对比分析。本文还对气泡法的技术特点进行了分析&#xff0c;指出了气泡法检漏技术的局…

使用ruoyi-vue控制数据权限

说在前面 啥是数据权限?例如校长可以看到全部学生的信息,系主任可以看到该院系的学生信息,老师可以看到本班的学生信息,学生自己只能查看自己的信息 对于ruoyi的角色,我们只能控制用户可以访问那些菜单以及接口,而不能控制接口返回的数据 假如有这样一个需求,不同用户上传各…

赛狐ERP | 亚马逊卖家FBA常见问题解析!

许多亚马逊卖家在使用亚马逊FBA时经常会遇到一些问题&#xff0c;如FBA如何收费、如何提升发货数量、物流方式问题等问题&#xff0c;这些问题是常见问题&#xff0c;也是亚马逊卖方们应该把握的问题。下面就来跟大家分享一些常见的亚马逊FBA相关问题。一、亚马逊FBA如何收费&a…

OSCP_vulnhub—GOLDENEYE: 1

vulnhub—GOLDENEYE: 1About信息搜集查看js泄露POP3 密码破解及登录使用admin/xWinter1995x!登录 getshell提权About GoldenEye: 1 ~ VulnHub Download (Mirror): https://download.vulnhub.com/goldeneye/GoldenEye-v1.ova DHCP service: Enabled IP address: Automaticall…

【Kubernetes 入门实战课】Day01——虚拟机创建及安装

文章目录前言一、实验环境选择二、选择合适的虚拟机软件1.VirtualBox2.VMWare三、Linux版本四、如何配置虚拟机1、安装VMware2、虚拟网络设置1、虚拟机网络编辑器2、设置 VMnet 8 网段3、创建虚拟机1、创建虚拟机2、启动虚拟机3、虚拟机配置4、虚拟机管理前言 俗话说&#xff…

[oeasy]python0052_ raw格式字符串_单引号_双引号_反引号_ 退格键

转义字符 回忆上次内容 最近玩的是\n、\r 之外的转义序列 \a是 ␇ (bell)\t是 水平制表符\v是 换行不回车 通过 16 进制数值转义 \xhh把(hh)16 进制对应的 ascii 字符输出 通过 8 进制数值转义 \ooo把(nnn)8 进制对应的 ascii 字符输出 这次加了 转义输出 反斜杠本身 \\ 输出…

24.Isaac教程--路径规划的区域划分

路径规划的区域划分 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 为了赋予地图区域关于通过它们规划的路径的语义意义&#xff0c;引入了三个新的规划器影响区域&#xff1a; No-replanning zones&#xff1a;当机器人通过这个多边形区域时&a…

python textwrap 模块,这里有你需要的学习资料

Python 标准库中的 textwrap 模块可以把长文本按照指定的宽度进行换行。这个模块提供了一些高级的文本处理功能&#xff0c;例如按照指定的宽度进行自动换行&#xff0c;保留段首缩进等。 python textwrap 模块python textwrap 模块主要函数textwrap.wrap(text, width70, \*\*k…

ruoyi-vue版本(四)@PreAuthorize 注解在若依里面的作用,springsecurity 框架相关的配置

目录1 需求2 实现2.1 背景2.2 实现2.3 springsecurity 框架相关的配置2.3.1 EnableGlobalMethodSecurity详解2.3.2 认证失败处理类AuthenticationEntryPointImpl2.3.3 自定义退出处理类LogoutSuccessHandlerImpl2.3.4 filter 为啥会多次执行3 PreAuthorize 注解1 需求 我们打开…

Flink面试题汇总

1&#xff0c;简单介绍一下 Flink Flink 是一个框架和分布式处理引擎&#xff0c;用于对无界和有界数据流进行有状态计算。并且 Flink 提供了数据分布、容错机制以及资源管理等核心功能。Flink提供了诸多高抽象层的API以便用户编写分布式任务&#xff1a; DataSet API&#xf…

nacos多网卡服务器引发的问题

1、背景 某一日晚上&#xff0c;公司的一个微服务上线重启后&#xff0c;双节点&#xff0c;只有一个节点注册到了nacos服务器&#xff0c;另外一个节点一直在nacos界面上无法看到&#xff0c;两个节点的服务启动也没有报错&#xff0c;api网关转发给服务时&#xff0c;时不时有…

深度学习入门基础——卷积计算

卷积计算 卷积是数学分析中的一种积分变换的方法&#xff0c;在图像处理中采用的是卷积的离散形式。这里需要说明的是&#xff0c;在卷积神经网络中&#xff0c;卷积层的实现方式实际上是数学中定义的互相关 &#xff08;cross-correlation&#xff09;运算&#xff0c;与数学…