ETCD源码分析(二)Client端Watch流程分析一

Etcdv3 client watcher原理分析 Etcdv3 client watcher原理分析背景etcdv3 client watcher整体架构源码分析 背景 最近在实现高可用流控防护组件sentinel的golang版本 ,扩展数据源这一块,在golang生态里面是etcd是非常重要的一个角色。需求是这样,每个集成sentinel的app instance会与etcd建立长连接来做动态数据源的更新。由于动态数据源的类型有多个,在Jav... 阅读详情

书接上文,我们在《ETCD源码分析Client端启动流程分析》中,深入源码查看ETCDClient端如何启动,如何向Server建立GRPC连接的。在本文中,我们继续探究ETCD的Client端是如何实现Watch逻辑的。
由于Watch逻辑整体还是比较复杂的,建议观看本文的小伙伴,把ETCD的V3.5版本源码打开,对比着文章查看。

ETCD源码Watch启动流程

首先对Watch流程进行分解,先介绍下ETCD客户端是如何启动Watch的。

在这里插入图片描述

Watch模块通过Watcher接口对外提供三个功能:

  • Watch() 用于创建对某个key或者某些key的Watch流;
  • RequestProgress() 用于告知客户端当前集群最新的reversion;
  • Close() 用于关闭所有的Watch流。

内部有一个私有的watcher实例,其中最关键的成员就是watchGrpcStream,存储了底层的GRPC Stream,提供了GRPC流的管理;请求、响应的管理;缓冲队列的控制等等,由于其中运用了大量的Channel,加上一些数据结构的处理,代码初看起来还是比较绕的。

不废话,让我们从Watch开始,先看看整个Watch流程是怎么启动的。

func (w *watcher) Watch(ctx context.Context, key string, opts ...OpOption) WatchChan {
	// 刚开始对opts做了些处理,
	// 我们不care,专心看主要逻辑

    // 每调用一次Watch
    // 都会创建一个watchRequest,这个wr会
    // 经过层层流转,最终发送到ETCD服务端
    // 并在客户端服务端之间
    // 创建出一条GRPC的Stream
    // 
    // Watch可能会被多次调用,*但是*(强调一下)
    // 但是以后就会共用最初创建的那条GRPC的Stream。
    // Watch机制的代码做的这么复杂,也就是
    // 为了处理共用一条Stream连接的问题。
	wr := &watchRequest{
		ctx:            ctx,
		createdNotify:  ow.createdNotify,
		key:            string(ow.key),
		end:            string(ow.end),
		rev:            ow.rev,
		progressNotify: ow.progressNotify,
		fragment:       ow.fragment,
		filters:        filters,
		prevKV:         ow.prevKV,
		retc:           make(chan chan WatchResponse, 1),
	}

	ok := false
	// 上面说一般是共用GRPC的Stream连接
	// 其实如果你传进来的ctx带的value不一样
	// 它就不会共用连接了。
	// 不过我们一般是用不上的,所以读者们可以忽略
	// ,记住是共用连接就好。
	// ps. 一般传进来的ctx有两种:
	//     1. context.Background()
	//     2. clientv3.WithRequireLeader(ctx)
	//     后面还会详细介绍
	ctxKey := streamKeyFromCtx(ctx)

	var closeCh chan WatchResponse
	for {
	    // 创建或重用GrpcStream
		// 如果ctxKey不变的话,
		// 就用streams中的缓存。
		// 
		wgs := w.streams[ctxKey]
		if wgs == nil {
			wgs = w.newWatcherGrpcStream(ctx)
			w.streams[ctxKey] = wgs
		}
		donec := wgs.donec
		reqc := wgs.reqc
		...

		select {
		// 上面代码创建出的wgs,
		// 其内部创建的gorotine是核心处理逻辑
		// 这里也只是把 wr 通过 reqc 通道
		// 发到核心goroutine就完事了。
		case reqc <- wr:
			ok = true
			
		// 如果创建的wgs被done了
		//(包括关闭、异常等等情况)
		// Watch()报告失败,流程终止。
		case <-donec:
		    ...
		}

		if ok {
			select {
			// wr 正确发送以后,
			// 其retc会收到一个chan WatchResponse
			// 而retc本身是个
			// chan chan WatchResponse
			// 应该怎么叫?channel的channel吧。
			case ret := <-wr.retc:

			    // 只有走到这里,
			    // 才是正常结束Watch()函数
				return ret
				...
			}
		}
	}

    // 异常结束
	close(closeCh)
	return closeCh
}

通过以上分析我们就会发现整个Watch()函数就做了三件事:

  1. 创建或使用缓存的watchGrpcStream
  2. 将WatchRequest发送到watchGrpcStream
  3. watchGrpcStream会为WatchRequest生成 chan WatchResponse用于接收结果
    并通过chan chan WatchResponse 返回给用户。

这里简单介绍一下 Watch(ctx context.Context, key string, opts …OpOption) WatchChan 这个函数支持哪些context,不同的context会让Watch产生不同的行为。

  1. context.WithBackground() / context.WithCancel()
    最常用的ctx,设置此ctx,watch会保证底层连接创建成功
    也就是会不断重试,直到连接成功或者ETCD服务端返回严重错误
    或者调用ctx的cancel()方法,也可以强行结束watch
  2. context.WithTimeout()
    这个就是Timeout时间后,自动帮你调用cancel()
    也就是timeout时间后,不管你的Watch流是正常还是异常
    都会强行结束,一般不能用这种context
  3. clientv3.WithRequireLeader(ctx)
    在ctx中加入 RequireLeader 标志,这个标志处理的问题是:
    若ETCD服务集群发生网络分区,且你的客户端Watch流正好连接少数一方的节点
    那你的Watch就卡住了,不会有响应,白白浪费资源。
    加入这个标志,Watch在发现自己连接的节点是少数一方节点时,尝试重新连接到其它的节点,这样就自动的处理了分区带来的不可用问题。
ETCD源码Watch监控流程

接下来,我们进入watchGrpcStream 的内部,看看它到底是如何处理连接复用问题的。
看代码之前要记住一点,watchGrpcStream做了两件事:1. 管理自己的底层GRPC连接(处理Request、Response);2. 管理共用底层连接的watcherStream。

为了简化描述,我们需要为几个关键角色起个别名
watchGrpcStream :wgs
watcherStream : ws
watchRequest : wr
watchResposne :wresp

// wgs 的主要流程就在run()内部,这是核心处理逻辑
func (w *watchGrpcStream) run() {
	...
	// 关闭流程会在单独介绍,此处不管
	closing := make(map[*watcherStream]struct{})
	defer func() {
		...
	}()

	// 启动底层GRPC连接
	if wc, closeErr = w.newWatchClient(); closeErr != nil {
		return
	}
	
	var cur *pb.WatchResponse
	for {
		select {
		// Watch() 方法内new出来的WatchRequest,
		// 最终会传递到这里。
		case req := <-w.reqc:
			switch wreq := req.(type) {
			case *watchRequest:
				outc := make(chan WatchResponse, 1)
				// 每个 wr 都会建立虚拟的Stream
				// 因为 watcherStream 实际上在共用底层连接
				// 所以我叫它叫虚拟的Stream
				ws := &watcherStream{
					initReq: *wreq, // wr 和 ws 是一对一关系
					// 每个 ws 都会在ETCD服务端生成watchId
					// 用于标识这个虚拟的Stream
					id:      -1, 
					outc:    outc,
					recvc: make(chan *WatchResponse),
				}
				ws.donec = make(chan struct{})
				
				// 建立好一个ws,WatiGroup都会加1
				// 记录建立了多少个ws
				w.wg.Add(1)
				// 虚拟的Stream开始提供服务
				go w.serveSubstream(ws, w.resumec)

				// ws 进入resuming队列,注意这个处理
				// 与之对应的还有一个substreams的hash set
				// 这两个数据结构都是用来存放 ws 的
				// resuming和substreams就好像预备党员和党员的关系
				// 初始创建的 ws 只是预备役,wgs会帮它发送 wr
				// *只有底层grpc连接收到对应的 wresp 后,*
				// *它才能提升到 substreams 中*
				w.resuming = append(w.resuming, ws)
				// 为保证wr的发送顺序(发wr收到wresp才发送下一个wr)
				// 如果w.resuming不是1(说明resuming中还有没拿到wresp的wr)就不能发
				if len(w.resuming) == 1 {
					// wc 是 wgs 内部GRPC连接的管理者(GRPC 的client)
					if err := wc.Send(ws.initReq.toPB()); err != nil {
						w.lg.Debug("error when sending request", zap.Error(err))
					}
				}
			...
			}
		// 底层收到了ETCD发来的一个 WatchResponse
		case pbresp := <-w.respc:
			// cur 专门用来支持 Fragment 功能
			// 也就是ETCD服务器可能会把比较大的wresp分片发送
			// cur 是用来把这些分片重新整合成完整的 wresp
			if cur == nil || pbresp.Created || pbresp.Canceled {
				cur = pbresp
			} else if cur != nil && cur.WatchId == pbresp.WatchId {
				// merge new events
				...
			}

			switch {
			case pbresp.Created:
				if len(w.resuming) != 0 {
					// 收到响应后,resuming队列的
				    // 第一个ws成功从预备役转为正式役
				    //(被ETCD服务端分配了一个WatchId)
					if ws := w.resuming[0]; ws != nil {
						w.addSubstream(pbresp, ws)
						// dispatchEvent
						// 会把wresp分给substreams里的 ws
						// 根据 wresp 中的 WatchId 匹配
						w.dispatchEvent(pbresp)
						w.resuming[0] = nil
					}
				}
				// 顺序发送下个resuming队列的ws(保证了顺序发送接收)
				if ws := w.nextResume(); ws != nil {
					if err := wc.Send(ws.initReq.toPB()); err != nil {
						w.lg.Debug("error when sending request", zap.Error(err))
					}
				}
			...
			// 如果 wresp 既不是Created响应,
			// 也不是Cancel响应,也不是Fragment响应
			// ps. (wresp的响应类型分为这三种,
			//     Cancel响应会在关闭流程介绍)
			//     那就转发给 ws 处理
			default:
				// 转发到 ws 中处理
				ok := w.dispatchEvent(cur)
				if ok {
					break
				}

				// 客户端这里没有 ws 可以处理这个 wresp
				// 那就通知ETCD服务器关闭这个虚拟的watchStream
				if _, ok := cancelSet[pbresp.WatchId]; ok {
					break
				}
				// cancelSet用来去重
				cancelSet[pbresp.WatchId] = struct{}{}
				// 通知服务端关闭 pbresp.WatchId 
				// 对应的虚拟Stream
				cr := &pb.WatchRequest_CancelRequest{
					CancelRequest: &pb.WatchCancelRequest{
						WatchId: pbresp.WatchId,
					},
				}
				req := &pb.WatchRequest{RequestUnion: cr}
				if err := wc.Send(req); err != nil {
					...
				}
			}
		// 以下在关闭流程中会详细介绍
		case err := <-w.errc:
			...
		case <-w.ctx.Done():
			...
		case ws := <-w.closingc:
			...
		}
	}
}

这里我尽量详细的把源码的每个部分它的作用、设计注释出来。对照源码本身去看,应该能很快明白其中原理。

比较反对一些源码分析文章,抛出一大段源码,做几个寥寥注释,看的人云里雾里的那种。我觉得分析源码,要么解释详尽,要么图文并茂。当然这是题外话了。

最后,我们通过一张流程图,再总结下 wgs 的整个流程。

watchGrpcStreamwatchClientwatcherStreamnewWatchClient()w.respc<-wresploopwcnew watcerStream(wr)WatchChan <- wrespclose()alt[ws.recvc][w.ctx.Done()]loopwsaddSubstream()dispathEvent()closeSubstream()alt[wresp.Cre-ated][wresp.Can-celed]alt[w.reqc][w.respc]loopwatchGrpcStreamwatchClientwatcherStream

从上图可以看出:

  1. 图中三个角色都有自己内部的死循环:
    • watchGrpcStream 的死循环在总览大局,既负责处理用户侧发来的WatchRequest,又负责接收watchClient发来的WatchResponse,并把它适当的转发到对应的watcherStream
    • watchClient 的死循环就做一件事:接收ETCD服务端发来的WatchResponse并把它发给watchGrpcStream
    • watcherStream 的死循环负责把 WatchResponse通过WatchChan发回用户侧,并维护结果缓冲队列(用户侧来不及处理的消息会被缓存在此)。

以上我们详细介绍了watchGrpcStream,接下来,我们进入watchClient、watcherStream的循环中看看。

func (w *watchGrpcStream) newWatchClient() (pb.Watch_WatchClient, error) {
	// -------Start--------
	close(w.resumec)
	w.resumec = make(chan struct{})
	w.joinSubstreams()
	for _, ws := range w.substreams {
		ws.id = -1
		w.resuming = append(w.resuming, ws)
	}
	// strip out nils, if any
	var resuming []*watcherStream
	for _, ws := range w.resuming {
		if ws != nil {
			resuming = append(resuming, ws)
		}
	}
	w.resuming = resuming
	w.substreams = make(map[int64]*watcherStream)

	// connect to grpc stream while accepting watcher cancelation
	stopc := make(chan struct{})
	donec := w.waitCancelSubstreams(stopc)
	// ------- END --------
	// 这里有一大段代码,开始我也没理解
	// 后来梳理了关闭流程才知道原因
	// 其实是跟重启有关,这里我们暂时可以不关心。
	
	// 其实除去上面那大段复杂代码,
	// watchClient 的逻辑其实很简单
	// 下面这里先通过GRPC的conn开启一个 wc
	wc, err := w.openWatchClient()
	...
	// 再启动goroutine循环,用来接收
	// ETCD服务端发来的 WatchResponse
	// 并转发给 watchGrpcStream
	go w.serveWatchClient(wc)
	return wc, nil
}

再看下 watcherStream ,这里主要看它内部的死循环的处理逻辑:

func (w *watchGrpcStream) serveSubstream(ws *watcherStream, resumec chan struct{}) {
	... 
	// 上面代码也是在处理关闭流程
	emptyWr := &WatchResponse{}
	for {
		curWr := emptyWr
		outc := ws.outc

		// outc 就是 WatchChan,就是用户侧接收的channel。
		// ws.buf 是缓冲队列,这里的处理是取队头数据
		if len(ws.buf) > 0 {
			curWr = ws.buf[0]
		} else {
			outc = nil
		}
		// 多路复用的四种情况
		// 1. outc <- *curWr outc不阻塞,那就把
		//    队头的WatchResponse发出去
		// 2. wr, ok := <-ws.recvc recvc有数据
		//   (数据是watchGrpcStream通过dispathEvent发过来的)
		// 3. <-w.ctx.Done() 上下文关闭,进入关闭流程
		// 4. <-resumec 这也是重启流程用的,这个分支如果执行
		//    ws 就从“正式党员”跌回“预备党员”
		//   (也就是说,从substreams移除,回到resuming)
		// select语句的特性,如果多个条件都可以执行,
		// 会随机选择一个执行。
		select {
		case outc <- *curWr:
			if ws.buf[0].Err() != nil {
				return
			}
			ws.buf[0] = nil
			ws.buf = ws.buf[1:]
		case wr, ok := <-ws.recvc:
			if !ok {
				// 关闭ws的recvc,会让处理循环退出
				return
			}
			if wr.Created {
				// 如果retc不为nil,才能进入下面代码
				if ws.initReq.retc != nil {
					// outc 通过retc发出去,会被Watch()本身的循环接收到
					// 忘记了的可以翻到最上面看看
					ws.initReq.retc <- ws.outc
					// 假如没有这句代码会发生什么?
					// 我们知道Watch机制有自动重试功能
					// 出现错误,Watch会自动再次重试(会在关闭流程详细介绍)
					// 重试会重新发送wr,收到ETCD服务端发过来Created标志的wresp
					// 所以这个判断可能会进多次
					// 但是对用户侧来说,重试是无感知的,
					// 用户侧不希望收到多次Created的wresp
					// 所以把 retc 置 nil 是必要的
					// 这样下次不会进来这段代码了。
					ws.initReq.retc = nil

					if ws.initReq.createdNotify {
						// 设置了WithCreatedNotify(),wresp会马上发出去
						// 不会进入缓冲队列
						ws.outc <- *wr
					}
					// 只有rev为0才更新进度
					// 这里处理的是:
					// 假如第一次创建Watch拿到当前Revision(假设是2)
					// 但是Watch因为某些原因重启了,这时ETCD服务器的Revision变成100了
					// 重启就丢失了2-100之间的值
					// 所以:在Watch流Created的时候,
					// 如果rev不为0,就不管ETCD服务器的Revision
					// 用initReq.rev原来的值
					if ws.initReq.rev == 0 {
						nextRev = wr.Header.Revision
					}
				}
			} else {
				nextRev = wr.Header.Revision
			}
			// 更新下当前的Watch进度
			if len(wr.Events) > 0 {
				nextRev = wr.Events[len(wr.Events)-1].Kv.ModRevision + 1
			}
			ws.initReq.rev = nextRev
			...
			ws.buf = append(ws.buf, wr)
		case <-w.ctx.Done():
			return
		case <-ws.initReq.ctx.Done():
			return
		case <-resumec:
			resuming = true
			return
		}
	}
}

以上,watcherStream的处理逻辑总结为以下两点:

  1. 把ETCD服务器的WatchResponse发出去
  2. 缓冲队列的维护
ETCD源码Watch关闭流程

关闭流程限于篇幅,将在下篇文章详细介绍。

参考文献
  • https://etcd.io/docs/v3.5/learning/data_model/
  • https://github.com/etcd-io/etcd/pull/7795/files
  • https://github.com/etcd-io/etcd/pull/6647/files
彻底搞懂 etcd 系列文章(九):etcd compact 和 watch API 0 专辑概述etcd 是云原生架构中重要的基础组件,由 CNCF 孵化托管。etcd 在微服务和 Kubernates 集群中不仅可以作为服务注册与发现,还可以作为 key-value ... 阅读详情

相关推荐

【ETCD】【实操篇(八)】秒懂 ETCD Watch:三种高效方式监听 Key 变化,不容错过

在分布式系统中,etcd是一个高可用的键值存储,用于存储和共享配置信息、服务发现、协调等。向etcd集群执行事务操作可以通过多种方式进行,本文将演示通过etcdctl、go客户端、java客户端三种方式。

zhangshenglu1的博客 1197

ETCD源码分析(二)Client端Watch流程分析二

书接上文,继续分析Client端Watch流程的关闭流程的处理。首先我们再回顾下Watch中的关键角色和设计理念: 角色 说明 watcher 对外接口Watcher的实现,重点是Watch()方法 watchGrpcStream 桥梁,管理内部GRPC连接、管理内部的虚拟Stream、消息接收和分发 watchClient 对应底层的GRPC连接客户端 watcherStream 虚拟的Stream,处理服务端和客户端的通信 要分析关闭流程,我们首先来找找有哪些情况会导致关

csdnYF的博客 906

断网无忧:ETCD客户端自动重连技术揭秘

之前在设计开发自动限流计数集群系统时,使用了ETCD来实现服务协调和集群选举。去年公司在做断网演练期间发现,ETCD客户端在和服务端断开连接之后,没有办法自动重新连接服务,恢复业务。具体表现如下图,机器心跳正常,但在ETCD上注册的服务离线。于是需要设计实现ETCD客户端与ETCD服务端的自动重连功能。确保在任何网络闪断或者网络临时中断的情况下,ETCD客户端能够在一定时间内自动注册到ETCD服务端,恢复服务。本文将和大家分享如何通过代理模式和观察者模式优雅的实现该自动重连方案。

跳小闹成长记 1745

使用etcd的proto文件生成编译etcd client v3的c++客户端代码

https://blog.csdn.net/wufolangren/article/details/88758454

phymat.nico的专栏 440

Go语言云原生实战150篇完整学习路径:从入门到精通Go后端开发

160篇文章从Go的第一个到分布式系统的架构设计,覆盖了Go后端开发的完整知识体系。学习路径的核心原则是: 先学语法再学并发,先写Web再搞微服务,先跑项目再看源码。最大的经验是: 不要跳过基础。我见过太多人goroutine都写不好就去学K8s Operator,结果生产环境一个数据竞争排查三天。把基础打扎实,后面的进阶是水到渠成的事。Go的哲学是简单,但简单不代表容易。160篇的内容量不小,按自己的节奏学,每篇的代码都动手跑一遍,踩坑经验记下来,3个月后你会发现Go后端开发真的不难。

白话机器学习 324

Etcd C++ client 源码

自己封装的etcd c++ client, 有线上项目使用先例。接口参考头文件,附带测试代码,有需要的话可以随意修改,写的不合适的代码还请见谅。

C/C++开发,基于cectd源码打造etcd数据库的win/linux静态编译的c-SDK包

目前市面上满足c/c++的etcd-client-SDK不多,支持win的更寥寥无几,为了实现支持c/c++访问etcd的跨平台和快速移植的 sdk包,在源码cetcd包基础上做进一步调整,满足win/linux支持静态编译的etcd-c-client-SDK包。 首先提供开源cetcd包源码下载网址:https://gitee.com/mirrors/cetcd ,该源码包已经实现了linux下可编译动静态库,但需要动态链接第三方库。下面给出具体改造过程: 【1】下载源码 1)先下载开源cetcd源

技术需要分享 1526

若依Vue整合积木报表实战:Token安全校验全流程解析

本文详细解析了在若依Vue后台系统中安全集成积木报表的全流程,重点阐述了Token安全校验的核心机制。通过前端菜单配置、Token传递、后端权限拦截及自定义Token服务实现,构建了一套完整的企业级报表访问安全防线,确保核心业务数据的安全可控。

t8u9v0的博客 578

etcd的Watch原理

在 Kubernetes 中,各种各样的控制器实现了 Deployment、StatefulSet、Job 等功能强大的 Workload。控制器的核心思想是监听、比较资源实际状态与期望状态是否一致,若不一致则进行协调工作,使其最终一致。那么当你修改一个 Deployment 的镜像时,Deployment 控制器是如何高效的感知到期望状态发生了变化呢?要回答这个问题,我们需要从etcd的Watch特性说起,他是Kubernetes控制器的工作基础。

qq_61039408的博客 3178

代码复现——SymAgent:一种用于知识图谱复杂推理的神经符号自学习Agent框架

根据《SymAgent: A Neural-Symbolic Self-Learning Agent Framework for Complex Reasoning over Knowledge Graphs》(《SymAgent:一种用于知识图谱复杂推理的神经符号自学习Agent框架》)论文思路,简单复现代码,以实现SymAgent中的Agent-Planner和Agent-Executor模块。Agent-Planner利用大语言模型的归纳推理能力从知识图谱中提取符号规则,指导高效的问题分解。

qq_60040306的博客 1606

spark 适配达梦数据库

spark 适配达梦数据库 版本 spark3.0.0 scala2.12.10 DM Database Server 64 V8 步骤一: 引驱动包: SBT方式: "com.dameng" % "Dm8JdbcDriver18" % "8.1.0.157", maven方式: <dependency> <groupId>com.dameng</groupId> <artifactId>Dm8JdbcDriver18</artifac

_@Eva@_的博客 2091

使用etcd实现动态分布式选主

日常开发中经常会有后台运行的worker类任务,由于服务是分布式的,我们可能会有多个分布式的worker同时在运行,有时候我们需要分布式下只有一个worker在运行,这时候就可以用到etcd的分布式选主。 etcd中concurrency包下已经帮我们实现好了选主,我们只需要调用其api实现就可以了,下面我们分析下etcd是如何实现选主机制的。直接进行源码分析: // Campaign puts a value as eligible for the election on the prefix

liyunlonghere 5692

CST高阶技巧:用周期性边界快速仿真共面波导色散特性(含JDM求解器优化)

本文详细介绍了在CST仿真软件中,利用周期性边界条件结合JDM本征模求解器,高效分析共面波导色散特性的高阶技巧。该方法通过仿真一个单元胞而非整条传输线,大幅提升计算效率,并提供了从建模、边界设置、求解器优化到后处理的完整工作流程,是高频电路设计的实用指南。

weixin_29280219的博客 403

client-go: Informer机制之reflector源码分析

client-go: Informer机制之reflector源码分析 目的 为了能充分了解Inform机制的原理,我们需要了解Inform机制的起点——reflector,那么,reflector是如何将数据从api-server拉下来?又是如何将数据存入本地的呢?解决这两个疑问就是本篇文章的重点。希望大家也能在此过程中能顺便了解k8s中list-watch机制缓存对象数据的原理。 源码分析的大致流程 首先,需要了解reflector结构体中的各个属性,然后是reflector是如何初始化,最后针对ref

学习学习再学习 1652

A2B音频总线在智能座舱中的应用

车载A2B链路音频是智能座舱的核心功能,涵盖车载音响、语音识别、e-Call、消噪及回声消除等应用,随着汽车智能网联化的发展,对音频的开发要求也越来越高,需要加入更先进、功能丰富的信息娱乐功能以满足消费者的用户体验。但传统的模拟并行音频信号传输方式,难以在功能增加与整车轻量化(线缆的重量及成本减少)之间取得均衡。这时候就需要A2B音频总线了。A2B音频总线用于连接车辆中的音频设备,如麦克风(MIC)、功放(AMP)、多媒体主机(Head Unit)和T-BOX等。通过一条低成本的非屏蔽双绞线UTP,进行跨距

怿星科技的博客 3935

vs2017配置liblas库+测试代码

之前已经配置好pcl(vs2017配置pcl),可以单独配置liblas,也可以在原有pcl属性表的基础上继续配置

孙悟空 4023

K8s 如何提供更高效稳定的编排能力?K8s Watch 实现机制浅析

首先,kubectl 也是通过 Cobra 命令行解析参数(--watch,或 --watch-only),然后调用 Run 调用 cli-runtime 包下面的 Watch 接口,之后通过 RESTClient.Watch 向 APIServer 发起 Watch 请求,获得一个流式 watch.Interface,然后不断从其中 ResultChan 获取 watch.Event。ETCD3 则通过基于 HTTP2 的 gRPC 实现 Watch stream,性能得到了很大的提升。

astraw99 的博客 63

lammps原子组速度的计算方法

在lammps摩擦、冲击、压痕等模拟过程中,涉及到某个原子组的整体移动,如果要输出这个原子组的整体移动速度,可以使用vcm()函数。下面是一个具体的使用案例,金刚石球在盒子内移动,输出移动过程中金刚石球在x方向的质心速度。vcm()是lammps自带的一个函数,返回原子组质心在设定方向的速度。其中,ID为group-ID,dim为方向,可设置x、y或者z方向。大家好,我是小马老师。

lammps_jiayou的博客 343

配置中心挂了服务还能启动吗?四象限、长轮询与灰度发布全解析

在微服务架构中,配置管理是保障系统稳定性的关键环节。配置中心作为集中管理配置的核心组件,其可用性直接影响服务启动与运行。本文从配置中心的基本原理出发,梳理了四象限容灾分析模型,解释本地缓存与远程拉取的协作机制;同时深入长轮询技术,说明配置动态刷新的低延迟实现方式。结合Nacos与Apollo的实战经验,介绍了灰度发布策略及最佳实践。通过具体示例演示Spring Cloud Alibaba接入Nacos的完整流程,帮助开发者理解配置中心挂了服务能否启动的判定逻辑,掌握高可用配置管理的核心技能。

weixin_30367169的博客 347

Windows 11 安装、使用 Git:下载、安装、概念、命令、分支模型

本文详细介绍了在 Windows 11 系统上安装与使用 Git 的完整流程,涵盖从官方或国内镜像下载安装包、关键安装选项配置,到 Git 核心概念(如工作区、暂存区、分支、远程仓库等)的通俗解释。同时梳理了常用命令与五种主流 Git 分支模型(如 Git Flow、GitHub Flow 等),帮助开发者根据项目规模选择合适的工作流,是面向初学者与进阶用户的实用指南。

寒水馨 · 轻量级技术文档 · hanshuixin.org 2389

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

juju reactive charm (by quqi99)

sudo snap install charm --classic mkdir -p /bak/work/charms/layers export JUJU_REPOSITORY=/bak/work/charms export LAYER_PATH=$JUJU_REPOSITORY/layers cd $LAYER_PATH charm create simple cd simple sudo ...

技术并艺术着 8732

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

服务发现原理分析与源码解读

在微服务架构中,有许多绕不开的技术话题。比如服务发现、负载均衡、指标监控、链路追踪,以及服务治理相关的超时控制、熔断、降级、限流等,还有RPC框架。这些都是微服务架构的基础,只有打牢这些基础,才敢说对微服务是有了一点理解,出门也好意思和别人打招呼了,被人提问的时候也能侃侃而谈了,线上出了问题往往也能寻根溯源内心不慌了,旁边的女同事小芳看着你的时候也是满眼的小可爱了。在《微服务实践》公众号,之前写了《go-zero微服务实战系列》的系列文章,这个系列的文章更多的是偏向业务功能和高并发下的服务优化等。和。...

JavaMonsterr的博客 357

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

【etcd】2.mvcc与etcd watch

4. 读源码1. MVCCMVCC(Multi-Version Concurrency Control)多版本并发控制:一种机制,用于实现分布式系统中的并发控制。集群的版本号Revision:逻辑时间戳,全局单调递增,任何 key 的增删改都会使其自增key的版本号CreateRevision:创建 key 时集群的 Revision, 直到删除前都不变ModRevision:修改 key 时集群的 Revision, key 更新时会自增Version:初始为1,更新时自增。

lixiaonan0318的博客 1375

推荐使用:etcd-client - 强大的Rust版etcd v3客户端

推荐使用:etcd-client - 强大的Rust版etcd v3客户端 在分布式系统中,数据一致性是关键挑战之一。而Etcd,一个高度可用的键值存储系统,为服务发现和配置管理提供了强大的解决方案。现在,有了etcd-client,你可以更方便地在Rust应用程序中集成和利用etcd的强大功能。 1、项目介绍 etcd-client是一个专为Rust设计的异步etcd v3 API客户端。它基于...

gitblog_00069的博客 816

etcd watch 实现原理

在 etcd 中,watch 是一个非常重要的特性,它可以让客户端监控 etcd 中的 key 或者一组 key,当 key 发生变化时,etcd 会通知客户端。本文将介绍 etcd watch 的实现原理。# 当 /test 的值发生变化时,会输出如下信息PUT/testaPUT/testbDELETE/test。

daemon365的博客 1189

c++11多线程编程(九):std::async介绍与实例

本节讨论下在C++11中怎样使用std::async来执行异步task。 C++11中引入了std::async 什么是std::async std::async()是一个接受回调(函数或函数对象)作为参数的函数模板,并有可能异步执行它们. template future::type> async(launch policy, Fn&& fn, Args&&...args);std

小麒麟的成长之路 3万+
上一篇: ETCD源码分析Client端启动流程分析
下一篇: ETCD源码分析(二)Client端Watch流程分析二
csdnYF
博客等级 码龄12年 406粉丝 32原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值