kubelet源码分析 syncLoopIteration(一) configCh

news2025/1/16 4:47:37

kubelet源码分析 syncLoopIteration

syncLoopIteration里有四个chan管道。分别是configCh、plegCh、syncCh、housekeepingCh。这篇主要聊一下这四个管道的由来。
在这里插入图片描述

一、configCh

configCh是通过list&watch的API SERVER获得的数据。然后在本地进行比对,推送到configCh管道中
1.cmd/kubelet/server.go中会根据pkg/kubelet/kubelet.go的一些初始化数据,将config.go的数据初始化,并将config.go中的update管道放到这里使用(LIST&WATCH会推送到这个管道里)最后kubelet.run会带上这个update管道。代码都是初始化的切比较乱,不贴这里了。go k.Run(podCfg.Updates())
2.1 初始化数据并监听apiserver

  • kubelet.go中初始化结构体
  • 监听三种数据来源,分别是api、file、http
  • channel函数会创建监听
  • ChannelWithContext创建goroutine执行listen
	//初始化config.go中结构体
	cfg := config.NewPodConfig(config.PodConfigNotificationIncremental, kubeDeps.Recorder)
	if kubeCfg.StaticPodPath != "" {
		klog.InfoS("Adding static pod path", "path", kubeCfg.StaticPodPath)
		config.NewSourceFile(kubeCfg.StaticPodPath, nodeName, kubeCfg.FileCheckFrequency.Duration, cfg.Channel(ctx, kubetypes.FileSource))
	}
	//添加三种数据来源,分别是api、file、http
	if kubeCfg.StaticPodURL != "" {
		klog.InfoS("Adding pod URL with HTTP header", "URL", kubeCfg.StaticPodURL, "header", manifestURLHeader)
		config.NewSourceURL(kubeCfg.StaticPodURL, manifestURLHeader, nodeName, kubeCfg.HTTPCheckFrequency.Duration, cfg.Channel(ctx, kubetypes.HTTPSource))
	}
	//NewSourceApiserver会初始化LIST&WATCH
	if kubeDeps.KubeClient != nil {
		klog.InfoS("Adding apiserver pod source")
		config.NewSourceApiserver(kubeDeps.KubeClient, nodeName, nodeHasSynced, cfg.Channel(ctx, kubetypes.ApiserverSource))
	}
		

2.2.初始化LIST&WATCH

  • 创建监听服务,如果node准备好了,则开启服务
  • 注册send函数,函数主要将apiserver传过来的数据重新组装后传给后续使用
  • run函数开始触发ListAndWatch函数
  • 函数中会触发watch执行
func NewSourceApiserver(c clientset.Interface, nodeName types.NodeName, nodeHasSynced func() bool, updates chan<- interface{}) {
	lw := cache.NewListWatchFromClient(c.CoreV1().RESTClient(), "pods", metav1.NamespaceAll, fields.OneTermEqualSelector("spec.nodeName", string(nodeName)))
	klog.InfoS("Waiting for node sync before watching apiserver pods")
	go func() {
		for {
			if nodeHasSynced() {
				klog.V(4).InfoS("node sync completed")
				break
			}
			time.Sleep(WaitForAPIServerSyncPeriod)
			klog.V(4).InfoS("node sync has not completed yet")
		}
		klog.InfoS("Watching apiserver")
		newSourceApiserverFromLW(lw, updates)
	}()
}

func newSourceApiserverFromLW(lw cache.ListerWatcher, updates chan<- interface{}) {
   //这个send函数,流程2.3会用到,会通过这个函数把数据推到chan中
	send := func(objs []interface{}) {
		var pods []*v1.Pod
		for _, o := range objs {
			pods = append(pods, o.(*v1.Pod))
		}
		//所有api过来的数据,都以SET类型重新传入
		updates <- kubetypes.PodUpdate{Pods: pods, Op: kubetypes.SET, Source: kubetypes.ApiserverSource}
	}
	r := cache.NewReflector(lw, &v1.Pod{}, cache.NewUndeltaStore(send, cache.MetaNamespaceKeyFunc), 0)
	go r.Run(wait.NeverStop)
}

func (r *Reflector) Run(stopCh <-chan struct{}) {
	klog.V(3).Infof("Starting reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
	wait.BackoffUntil(func() {
		if err := r.ListAndWatch(stopCh); err != nil {
			r.watchErrorHandler(r, err)
		}
	}, r.backoffManager, true, stopCh)
	klog.V(3).Infof("Stopping reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
}
//ListAndWatch函数代码过多,不全贴了,
err = watchHandler(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.expectedTypeName, r.setLastSyncResourceVersion, r.clock, resyncerrc, stopCh)

2.3.watch开始执行,收到数据后进行验证

  • add/update/delete虽然触发的函数不一样,但是执行流程都类似的。
  • 先把这个数据更新/添加/删除在本地的缓存中。
  • 然后遍历所有的已存在的pod数据,然后执行上面流程2中的send函数。
  • 把所有pod数据通过send函数推送到一个podList后整体推送到chan管道
  • 这次的watch就结束了。等待下次变化
		case watch.Added:
				err := store.Add(event.Object)
				if err != nil {
					utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", name, event.Object, err))
				}
			case watch.Modified:
				err := store.Update(event.Object)
				if err != nil {
					utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", name, event.Object, err))
				}
			case watch.Deleted:
				err := store.Delete(event.Object)
				if err != nil {
					utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", name, event.Object, err))
				}
			case watch.Bookmark:
			default:
				utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", name, event))
			}
	
func (u *UndeltaStore) Add(obj interface{}) error {
	if err := u.Store.Add(obj); err != nil {//更新本地缓存
		return err
	}
	u.PushFunc(u.Store.List())//触发流程2的send函数,把所有pod遍历一遍推过去
	return nil
}

3.上面都是推送的流程,下面开始接收的流程。

  • 执行channel函数,初始化管道。然后监听这个管道,并返回给流程2.1
  • 通过流程2.1中的send函数,最终数据会在这个channel管道中接收到。然后在liseten中执行
  • 执行过交给merge函数
//流程2.1的函数,这里主要用到Channel函数
if kubeDeps.KubeClient != nil {
		klog.InfoS("Adding apiserver pod source")
		config.NewSourceApiserver(kubeDeps.KubeClient, nodeName, nodeHasSynced, cfg.Channel(ctx, kubetypes.ApiserverSource))
	}
	
func (c *PodConfig) Channel(ctx context.Context, source string) chan<- interface{} {
	c.sourcesLock.Lock()
	defer c.sourcesLock.Unlock()
	c.sources.Insert(source)
	return c.mux.ChannelWithContext(ctx, source)
}

func (m *Mux) ChannelWithContext(ctx context.Context, source string) chan interface{} {
	if len(source) == 0 {
		panic("Channel given an empty name")
	}
	m.sourceLock.Lock()
	defer m.sourceLock.Unlock()
	channel, exists := m.sources[source]
	if exists {
		return channel
	}
	newChannel := make(chan interface{})
	m.sources[source] = newChannel
    //监听新数据
	go wait.Until(func() { m.listen(source, newChannel) }, 0, ctx.Done())
	return newChannel
}

func (m *Mux) listen(source string, listenChannel <-chan interface{}) {
	for update := range listenChannel {
		m.merger.Merge(source, update)
	}
}

4.处理数据并推送到configCh
4.1 处理数据

  • 接收到数据首先判断来源是否已经注册了
  • 验证podList是需要更新还是删除还是调解等(流程4.2)
  • 将数据推送到kubelet坚挺的chan管道中
func (s *podStorage) Merge(source string, change interface{}) error {
	s.updateLock.Lock()
	defer s.updateLock.Unlock()

	seenBefore := s.sourcesSeen.Has(source)
	//流程4.2
	adds, updates, deletes, removes, reconciles := s.merge(source, change)
	firstSet := !seenBefore && s.sourcesSeen.Has(source)

	switch s.mode {
	case PodConfigNotificationIncremental:
		if len(removes.Pods) > 0 {
			s.updates <- *removes
		}
		if len(adds.Pods) > 0 {
			s.updates <- *adds
		}
		if len(updates.Pods) > 0 {
			s.updates <- *updates
		}
		if len(deletes.Pods) > 0 {
			s.updates <- *deletes
		}
		if firstSet && len(adds.Pods) == 0 && len(updates.Pods) == 0 && len(deletes.Pods) == 0 {
			s.updates <- *adds
		}
		if len(reconciles.Pods) > 0 {
			s.updates <- *reconciles
		}

	case PodConfigNotificationSnapshotAndUpdates:
		if len(removes.Pods) > 0 || len(adds.Pods) > 0 || firstSet {
			s.updates <- kubetypes.PodUpdate{Pods: s.MergedState().([]*v1.Pod), Op: kubetypes.SET, Source: source}
		}
		if len(updates.Pods) > 0 {
			s.updates <- *updates
		}
		if len(deletes.Pods) > 0 {
			s.updates <- *deletes
		}

	case PodConfigNotificationSnapshot:
		if len(updates.Pods) > 0 || len(deletes.Pods) > 0 || len(adds.Pods) > 0 || len(removes.Pods) > 0 || firstSet {
			s.updates <- kubetypes.PodUpdate{Pods: s.MergedState().([]*v1.Pod), Op: kubetypes.SET, Source: source}
		}

	case PodConfigNotificationUnknown:
		fallthrough
	default:
		panic(fmt.Sprintf("unsupported PodConfigNotificationMode: %#v", s.mode))
	}

	return nil
}

4.2 判断pod

  • 取出老的pod信息
  • 注册函数updatePodsFunc
  • 用oldPods拿到老pod的数据,同时清空pods数据(通过直接创建一个新的清除旧的)
  • 执行函数
  • 遍历函数,如果annotations注解为空,初始化,并且赋值来源
  • 遍历oldpods,如果oldpods没有这个pod信息,则代表是添加。并且把这个pod信息添加到pods里
  • 如果有这个pods信息,则存入pods里(这里为了后续判断是否这个pod被删除了)
  • 验证这个pod需要进行什么类型的处理(流程4.3)
  • 遍历新的pods数据,如果有oldPods没有的,则是代表删除了
  • 将所有数据重新组装返回
func (s *podStorage) merge(source string, change interface{}) (adds, updates, deletes, removes, reconciles *kubetypes.PodUpdate) {
	s.podLock.Lock()
	defer s.podLock.Unlock()

	addPods := []*v1.Pod{}
	updatePods := []*v1.Pod{}
	deletePods := []*v1.Pod{}
	removePods := []*v1.Pod{}
	reconcilePods := []*v1.Pod{}
	pods := s.pods[source]
	if pods == nil {
		pods = make(map[types.UID]*v1.Pod)
	}
	updatePodsFunc := func(newPods []*v1.Pod, oldPods, pods map[types.UID]*v1.Pod) {
		filtered := filterInvalidPods(newPods, source, s.recorder)
		for _, ref := range filtered {
			if ref.Annotations == nil {
				ref.Annotations = make(map[string]string)
			}
			ref.Annotations[kubetypes.ConfigSourceAnnotationKey] = source
			if existing, found := oldPods[ref.UID]; found {
				pods[ref.UID] = existing
				needUpdate, needReconcile, needGracefulDelete := checkAndUpdatePod(existing, ref)
				if needUpdate {
					updatePods = append(updatePods, existing)
				} else if needReconcile {
					reconcilePods = append(reconcilePods, existing)
				} else if needGracefulDelete {
					deletePods = append(deletePods, existing)
				}
				continue
			}
			recordFirstSeenTime(ref)
			pods[ref.UID] = ref
			addPods = append(addPods, ref)
		}
	}

	update := change.(kubetypes.PodUpdate)
	switch update.Op {
	/*此处省略了其他类型的代码*/
	case kubetypes.SET:
		klog.V(4).InfoS("Setting pods for source", "source", source)
		s.markSourceSet(source)
		oldPods := pods
		pods = make(map[types.UID]*v1.Pod)
		updatePodsFunc(update.Pods, oldPods, pods)
		for uid, existing := range oldPods {
			if _, found := pods[uid]; !found {
				// this is a delete
				removePods = append(removePods, existing)
			}
		}

	default:
		klog.InfoS("Received invalid update type", "type", update)

	}

	s.pods[source] = pods

	adds = &kubetypes.PodUpdate{Op: kubetypes.ADD, Pods: copyPods(addPods), Source: source}
	updates = &kubetypes.PodUpdate{Op: kubetypes.UPDATE, Pods: copyPods(updatePods), Source: source}
	deletes = &kubetypes.PodUpdate{Op: kubetypes.DELETE, Pods: copyPods(deletePods), Source: source}
	removes = &kubetypes.PodUpdate{Op: kubetypes.REMOVE, Pods: copyPods(removePods), Source: source}
	reconciles = &kubetypes.PodUpdate{Op: kubetypes.RECONCILE, Pods: copyPods(reconcilePods), Source: source}

	return adds, updates, deletes, removes, reconciles
}

4.3检查pod如何处理

  • 先检查这个pod和老的pod的spec、labels、DeletionTimestamp、DeletionGracePeriodSeconds、Annotations是否有变化,如果有变化,则需要进行更新处理,说明配置文件变了
  • 如果没有变化,判断新老pod的status运行状态是否有变化,如果有变化,则是需要进行调解needReconcile
  • 如果pod的spec有变化,则要把老的pod信息更新成新的
  • 如果DeletionTimestamp不为nil,则代表删除。否则是更新
func checkAndUpdatePod(existing, ref *v1.Pod) (needUpdate, needReconcile, needGracefulDelete bool) {
	if !podsDifferSemantically(existing, ref) {
		if !reflect.DeepEqual(existing.Status, ref.Status) {
			existing.Status = ref.Status
			needReconcile = true
		}
		return
	}

	ref.Annotations[kubetypes.ConfigFirstSeenAnnotationKey] = existing.Annotations[kubetypes.ConfigFirstSeenAnnotationKey]

	existing.Spec = ref.Spec
	existing.Labels = ref.Labels
	existing.DeletionTimestamp = ref.DeletionTimestamp
	existing.DeletionGracePeriodSeconds = ref.DeletionGracePeriodSeconds
	existing.Status = ref.Status
	updateAnnotations(existing, ref)

	if ref.DeletionTimestamp != nil {
		needGracefulDelete = true
	} else {
		needUpdate = true
	}

	return
}

func podsDifferSemantically(existing, ref *v1.Pod) bool {
	if reflect.DeepEqual(existing.Spec, ref.Spec) &&
		reflect.DeepEqual(existing.Labels, ref.Labels) &&
		reflect.DeepEqual(existing.DeletionTimestamp, ref.DeletionTimestamp) &&
		reflect.DeepEqual(existing.DeletionGracePeriodSeconds, ref.DeletionGracePeriodSeconds) &&
		isAnnotationMapEqual(existing.Annotations, ref.Annotations) {
		return false
	}
	return true
}

5.这时候kubelet的configCh管道就接受到上面推送来的信息了,根据状态进行每种类型的处理

case u, open := <-configCh:
		if !open {
			klog.ErrorS(nil, "Update channel is closed, exiting the sync loop")
			return false
		}
		switch u.Op {
		case kubetypes.ADD:
			klog.V(2).InfoS("SyncLoop ADD", "source", u.Source, "pods", klog.KObjs(u.Pods))
			handler.HandlePodAdditions(u.Pods)
		case kubetypes.UPDATE:
			klog.V(2).InfoS("SyncLoop UPDATE", "source", u.Source, "pods", klog.KObjs(u.Pods))
			handler.HandlePodUpdates(u.Pods)
		case kubetypes.REMOVE:
			klog.V(2).InfoS("SyncLoop REMOVE", "source", u.Source, "pods", klog.KObjs(u.Pods))
			handler.HandlePodRemoves(u.Pods)
		case kubetypes.RECONCILE:
			klog.V(4).InfoS("SyncLoop RECONCILE", "source", u.Source, "pods", klog.KObjs(u.Pods))
			handler.HandlePodReconcile(u.Pods)
		case kubetypes.DELETE:
			klog.V(2).InfoS("SyncLoop DELETE", "source", u.Source, "pods", klog.KObjs(u.Pods))
			handler.HandlePodUpdates(u.Pods)
		case kubetypes.SET:
			klog.ErrorS(nil, "Kubelet does not support snapshot update")
		default:
			klog.ErrorS(nil, "Invalid operation type received", "operation", u.Op)
		}

		kl.sourcesReady.AddSource(u.Source)

下一篇kubelet源码分析 syncLoopIteration(二) plegCh

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

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

相关文章

Qt-Web混合开发-QtWebChannel实现Qt与Web通信交互-进阶功能(6)

Qt-Web混合开发-QtWebChannel实现Qt与Web通信交互-进阶功能&#x1f96c; 文章目录Qt-Web混合开发-QtWebChannel实现Qt与Web通信交互-进阶功能&#x1f96c;1、概述&#x1f33d;2、实现效果&#x1f346;3、实现功能&#x1f352;4、关键代码&#x1f95d;5、源代码&#x1f9…

Android基础学习(二十二)—— View的事件分发(1)

一、View的层级关系 二、View的事件分发机制 1、MotionEvent ——点击事件 点击事件用MotionEvent来表示 ACTION_DOWN&#xff1a;手指刚接触屏幕 ACTION_MOVE&#xff1a;手指在屏幕上移动 ACTION_UP&#xff1a;手指从屏幕上松开的一瞬间 点击事件的事件分发&#xff0…

OM6621系列国产M4F内核低功耗BLE5.1大内存SoC蓝牙芯片

目录OM6621系列简介OM6621P系列芯片特性应用领域OM6621系列简介 随着5G与物联网时代的到来&#xff0c;智慧城市、电动出行、智能家居、可穿戴设备等应用高速发展&#xff0c;低功耗蓝牙技术在近几年智能化浪潮中的地位也尤为重要。OM6621系列的开发即是为解决国内低功耗蓝牙应…

Linux安装docker 保姆级教程

一、docker介绍 Docker 是 2014 年最为火爆的技术之一&#xff0c;几乎所有的程序员都听说过它。Docker 是一种“轻量级”容器技术&#xff0c;它几乎动摇了传统虚拟化技术的地位&#xff0c;现在国内外已经有越来越多的公司开始逐步使用 Docker 来替换现有的虚拟化平台了。 二…

图为科技深圳人工智能产业协会重磅推出边缘计算机全新概念

人工智能作为提升区域竞争力的重要战略&#xff0c;全国各地都在推动发展&#xff0c;人工智能是未来科技创新发展的风向标&#xff0c;也是产业变革升级的关键驱动力&#xff0c;我国在《“十四五”数字经济发展规划》及《工业互联网创新发展行动计划(2021-2023年)》中&#x…

Linux基础(4)-进程管理

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【参考文章】 参考文章&#xff1a;https://howard2005.blog.csdn.net/article/details/127066383?spm1001.2014.3001.5502 文章目录一、查看进程1. 进程查看命令 - ps2. Liunx进程状态3. 观察进程变化命令 - top4. …

b站黑马的Vue快速入门案例代码——【axios+Vue】天知道(天气信息查询功能)

目录 目标效果&#xff1a; 更换的新接口接口文档&#xff1a; 天知道新的Get请求接口:http://ajax-api.itheima.net/api/weather html文件中注意因为接口更换&#xff0c;要修改原代码为如下红字部分&#xff1a; 重点原理&#xff1a; &#xff08;1&#xff09;v-on可以…

环形链表问题

文章目录环形链表问题1.环形链表题干思路延申问题总结2. 环形链表 II题干思路环形链表问题 环形链表就是一个链表没有结束的位置&#xff0c;链表的最后一个节点它会指向链表中的某一个节点形成一个环。 拿力扣的两到题目来看 1.环形链表 题干 给你一个链表的头节点 head …

JavaScript JSON解析

最近在uniapp中遇到了一个bug&#xff0c;排查后是json解析的问题。对uniapp开发比较熟悉的&#xff0c;应该会知道uni.navigateTo 这个API方法。这是官方提供用于跳转页面的方法。 有时候我们在跳转页面时会想传递一些参数&#xff0c;通常采用这样的方式 navigateTo(url, r…

oauth2.0--基础--6.1--SSO的实现原理

oauth2.0–基础–6.1–SSO的实现原理 1、什么是SSO 1.1、概念 在一个 多系统共存 的环境下&#xff0c;用户在一处登录后&#xff0c;就不用在其他系统中登录&#xff0c;就可以访问其他系统的资源。用户环境 浏览器&#xff1a;只能同一个浏览器&#xff0c;不会出现A浏览器…

zabbix部署【各模块超详细】

目录 安装zabbix 部署zabbix 配置zabbix 1. 修改语言 2. 监控linux端 3. 修改中文乱码 报警功能 报警音报警 邮件报警 脚本报警 邮件通知内容 图形模块 创建图形 创建聚合图形 percona mysql模板 nginx模板 克隆主机 网络发现 自动注册 主被动模式 &#x1f341;如果对你有帮助…

Handsontable复制列标题内容的功能

Handsontable复制列标题内容的功能 添加了通过使用3个新的上下文菜单选项复制列标题内容的功能&#xff1a;“使用标题复制”、“使用组标题复制”和“仅复制标题”。 添加了4个用于以编程方式复制列标题的新API方法&#xff1a;“copyCellsOnly()”、“copyWithColumnHeaders(…

vscode jupyter配置远程服务器开发

背景说明&#xff1a;本地vscode中使用jupyter编写本地python代码很方便&#xff0c;各种快捷键用的飞起。但是要做线上大数据分析时。在集群环境中搭建一个jupyter。使用网页端编写程序非常不习惯&#xff0c;所以想到能不能将线上的jupyter接口开出来&#xff0c;使用vscode远…

js-有关时间

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date 有关Date 返回格式 Sun Oct 10 2021 00:00:00 GMT0800 (中国标准时间) new Date() 无参数 获取当前时间new Date(value) 传入时间戳 传入一个时间戳 一个 Unix 时间戳&#xff08;U…

JVM运行流程/运行时数据区

JVM运行流程 程序在执行之前先要把 java代码 转换成 字节码文件 (.class文件), JVM 首先需要把字节码通过一定的方式 类加载器 (ClassLoader) 把文件加载到内存中 运行时数据区 (Runtime Data Area) , 而字节码文件是 JVM 的一套指令集规范, 并不能直接交给底层操作系统去执行…

【大数据技术Hadoop+Spark】Hive数据仓库架构、优缺点、数据模型介绍(图文解释 超详细)

一、Hive简介 Hive起源于Facebook&#xff0c;Facebook公司有着大量的日志数据&#xff0c;而Hadoop是实现了MapReduce模式开源的分布式并行计算的框架&#xff0c;可轻松处理大规模数据。然而MapReduce程序对熟悉Java语言的工程师来说容易开发&#xff0c;但对于其他语言使用…

Anaconda为虚拟环境安装第三方库与Spyder等软件的方法

本文介绍在Anaconda中&#xff0c;为Python的虚拟环境安装第三方库与Spyder等配套软件的方法。 在文章Anaconda中Python虚拟环境的创建、使用与删除&#xff08;https://blog.csdn.net/zhebushibiaoshifu/article/details/128334614&#xff09;中&#xff0c;我们介绍了在Anac…

提前做好网络安全分析,运维真轻松(二)

背景 某汽车总部已部署NetInside流量分析系统&#xff0c;使用流量分析系统提供实时和历史原始流量。汽车配件电子图册系统是某汽车集团的重要业务系统。本次分析重点针对汽车配件电子图册系统进行预见性分析&#xff0c;以供安全取证、性能分析、网络质量监测以及深层网络分析…

FRP搭建内网穿透

前言 内网穿透方式很多&#xff0c;可以用公网IP进行端口映射&#xff0c;DDNS等。现在我有个云服务器&#xff0c;使用它做中转作为内网穿透的工具。 可以在这个网址了解下原理基础&#xff1a;内网穿透工具的原理与开发实战 FRP内网穿透 FRP是一个内网穿透的反向代理应用…

电压放大器工作原理及特点是什么

很多人虽然经常使用电压放大器&#xff0c;但是对于电压放大器的工作原理以及特点是什么都不清楚&#xff0c;下面就来为大家讲解。 什么是电压放大器&#xff1f; 电压放大器是一种能够增加信号电压的装置。对于弱信号&#xff0c;通常采用多级放大级联方式分直接耦合、阻容耦…