皮皮网

【仿词库网源码】【微端源码】【内涵段子源码】grpc go源码

2024-12-24 13:27:17 来源:trust 源码

1.direct io 详解
2.golang:context介绍
3.gRPC 流量控制详解
4.Windows平台C++ 使用VS2015 编译gRPC(总结)
5.CockroachDB 源码闲逛 - II (insert a row)
6.PolarisMesh源码系列--Polaris-Go注册发现流程

grpc go源码

direct io 详解

       DDD课程:

       DDD 案例实战课 - 天涯兰 - 掘金小册

       Netty 实现微信课程:

       Netty 入门与实战:仿写微信 IM 即时通讯系统

       MySQL 原理详解:

       MySQL的源码日志、事务原理-undolog、源码redolog、源码binlog两阶段提交详解 - 学新通

       不太帅的源码程序员:图解 MySql 原理

       MySQL架构原理(详解)-腾讯云开发者社区-腾讯云

       Redis底层数据结构(图文详解):

       Redis底层数据结构(图文详解)_玄郭郭的博客-CSDN博客

       epoll 源码详解:

       Epoll源码深度剖析--转自坚持,每天进步一点点 - Desh - 博客园

       C++ 标准库参考手册:

       Eajack Lau:Cpp标准库速查手册

       Go-lang 多环境配置:

       Go 多环境下配置管理方案(多种方案)

       C++ 多线程和多进程编程:

       c++ 多线程和多进程编程_c++多线程和多进程_CapticalAmerican的源码博客-CSDN博客

       多线程和多进程的区别(C++) - Vae永Silence - 博客园

       C++5万字面试题(加精):

       五万字长文 C C++ 面试知识总结(中)

       Sentinel 原理详解:

       sentinel运行原理详解_sentinel原理_龚厂长的博客-CSDN博客

       sentinel 基本原理 - monkeydai - 博客园

       Nginx 加锁原理:

       nginx分布式锁以及accept锁简单整理_yun的博客-CSDN博客

       Spring Cloud 原理:

       mikechen.cc/.html

       GraalVM和Spring Native尝鲜,一步步让Springboot启动飞起来,源码仿词库网源码ms完成启动

       Dubbo 原理详解:

       秒懂Dubbo框架(原理篇) - 高楼

       Netty 原理详解:

       授人以渔:这可能是源码目前最透彻的Netty原理架构解析

       Grpc golang 源码详解:

       解析grpc架构与原理-腾讯云开发者社区-腾讯云

       LVS 原理详解:

       深入浅出 | 阿里巴巴处理千万并发访问利器——LVS - 老男孩教育

       Nginx、LVS、源码Keepalived的源码关系(加精):

       Nginx、LVS、源码Keepalived的源码关系_lvs nginx keepalived_Dnils的博客-CSDN博客

       Nacos 原理详解:

       Nacos原理详解(注册中心,配置中心)-腾讯云开发者社区-腾讯云

       Nacos实现原理详细讲解_nacos原理详解_木鱼-的博客-CSDN博客

       Nacos 持久化详解:

       东小西:Nacos「持久化」

       Nacos 配置中心详解:

       Spring Cloud Alibaba(六)Nacos配置中心原理分析

       Nacos 长轮询详解(加精):

       如何用Java实现简单的长轮询?-java教程-PHP中文网

       Nacos 架构与原理深度分析:

       Nacos架构与原理深度分析_nacos架构原理_Doker 多克的博客-CSDN博客

       Apollo原理分析及功能详解(配置管理、集群管理等):

       Apollo原理分析及功能详解(配置管理、源码集群管理等)_apollo的源码作用_、楽.的源码博客-CSDN博客

       Nacos 实现 AP+CP原理[Raft 算法 ]:

       Nacos 实现 AP+CP原理[Raft 算法 ]

       Nacos 高可用:

       敏中午:Nacos 高可用介绍

       MySQL 复制与高可用水平扩展架构实战:

       MySQL复制与高可用水平扩展架构实战_mysql 水平扩展_小二上酒8的博客-CSDN博客

       Apollo 架构详解:

       Apollo(阿波罗)架构深度剖析-CSDN社区

       apollo @value 原理详解:

       企业级代码探究: @Value + Apollo动态刷新原理~

       NSQ 原理详解:

       NSQ(分布式消息队列) - Martin - 博客园

       ZeroQ 原理详解:

       ZeroMQ详解 - 南哥的天下 - 博客园

       Redis 实现延时队列详解:

       redis zrangebyscore 详解

       如何用 Redis 实现延迟队列?

       灵感来袭,基于Redis的源码分布式延迟队列(续)

       Redis实战篇:巧用zset实现延迟队列

       RocketMQ 延时队列原理(加精):

       RocketMq延时队列的实现原理 - MaXianZhe - 博客园

       Linux 磁盘管理详解:

       Linux磁盘与文件系统管理_yu.deqiang的博客-CSDN博客

       Linux文件系统、磁盘I/O是怎么工作的-一口Linux-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台

       Linux 缓存IO、直接IO、内存映射

       如何高效的传输文件 -- 零拷贝技术

       什么是mmap?零拷贝?DMA?

       Java NIO之大文件缓存MappedByteBuffer详解

       NIO源码解析-FileChannel高阶知识点map和transferTo、transferFrom(加精)

       RocketMQ 如何基于mmap+page cache实现磁盘文件的高性能读写?

       面试官:RocketMQ 如何基于mmap+page cache实现磁盘文件的高性能读写?

       Rocket MQ : 拒绝神化零拷贝

       Linux内存分配小结--malloc、brk、mmap:

       Linux内存分配小结--malloc、brk、mmap - bdy - 博客园

       Linux 操作系统原理 - 内存管理 - 内存分配算法

       Linux 操作系统原理 — 内存管理 — 内存分配算法

golang:context介绍

       1 前言

       æœ€è¿‘实现系统的分布式日志与事务管理时,在寻求所谓的全局唯一Goroutine ID无果之后,决定还是简单利用Context机制实现了基本的想法,不够高明,但是好用.于是对它当初的设计比较好奇,便有了此文.

       Context是golang官方定义的一个package,它定义了Context类型,里面包含了Deadline/Done/Err方法以及绑定到Context上的成员变量值Value,具体定义如下:

typeContextinterface{ //返回Context的超时时间(超时返回场景)Deadline()(deadlinetime.Time,okbool)//在Context超时或取消时(即结束了)返回一个关闭的channel//即如果当前Context超时或取消时,Done方法会返回一个channel,然后其他地方就可以通过判断Done方法是否有返回(channel),如果有则说明Context已结束//故其可以作为广播通知其他相关方本Context已结束,请做相关处理.Done()<-chanstruct{ }//返回Context取消的原因Err()error//返回Context相关数据Value(keyinterface{ })interface{ }}那么到底什么Context?

       å¯ä»¥å­—面意思可以理解为上下文,比较熟悉的有进程/线程上线文,关于golang中的上下文,一句话概括就是: goroutine的相关环境快照,其中包含函数调用以及涉及的相关的变量值. 通过Context可以区分不同的goroutine请求,因为在golang Severs中,每个请求都是在单个goroutine中完成的.

       æœ€è¿‘在公司分析gRPC源码,proto文件生成的代码,接口函数第一个参数统一是ctx context.Context接口,公司不少同事都不了解这样设计的出发点是什么,其实我也不了解其背后的原理.今天趁着妮妲台风妹子正面登陆深圳,全市停工,停课,停业,在家休息找了一些资料研究把玩一把.

       Context通常被译作上下文,它是一个比较抽象的概念.在公司技术讨论时也经常会提到上下文.一般理解为程序单元的一个运行状态,现场,快照,而翻译中上下又很好地诠释了其本质,上下上下则是存在上下层的传递,上会把内容传递给下.在Go语言中,程序单元也就指的是Goroutine.

       æ¯ä¸ªGoroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个Context变量中,传递给要执行的Goroutine中. 上下文则几乎已经成为传递与请求同生存周期变量的标准方法.在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理,即一个请求Request,会在多个Goroutine中处理. 而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束.

       æ³¨ï¼šå…³äºŽgoroutine的理解可以移步这里.

2 为什么使用context

       ç”±äºŽåœ¨golang severs中,每个request都是在单个goroutine中完成,并且在单个goroutine(不妨称之为A)中也会有请求其他服务(启动另一个goroutine(称之为B)去完成)的场景,这就会涉及多个Goroutine之间的调用.如果某一时刻请求其他服务被取消或者超时,则作为深陷其中的当前goroutine B需要立即退出,然后系统才可回收B所占用的资源. 即一个request中通常包含多个goroutine,这些goroutine之间通常会有交互.

       é‚£ä¹ˆ,如何有效管理这些goroutine成为一个问题(主要是退出通知和元数据传递问题),Google的解决方法是Context机制,相互调用的goroutine之间通过传递context变量保持关联,这样在不用暴露各goroutine内部实现细节的前提下,有效地控制各goroutine的运行.

       å¦‚此一来,通过传递Context就可以追踪goroutine调用树,并在这些调用树之间传递通知和元数据. 虽然goroutine之间是平行的,没有继承关系,但是Context设计成是包含父子关系的,这样可以更好的描述goroutine调用之间的树型关系.

3 怎么使用context

       ç”Ÿæˆä¸€ä¸ªContext主要有两类方法:

3.1 顶层Context:Background

       è¦åˆ›å»ºContext树,首先就是要创建根节点

//返回一个空的Context,它作为所有由此继承Context的根节点funcBackground()Context

       è¯¥Context通常由接收request的第一个goroutine创建,它不能被取消,没有值,也没有过期时间,常作为处理request的顶层context存在.

3.2 下层Context:WithCancel/WithDeadline/WithTimeout

       æœ‰äº†æ ¹èŠ‚点之后,接下来就是创建子孙节点.为了可以很好的控制子孙节点,Context包提供的创建方法均是带有第二返回值(CancelFunc类型),它相当于一个Hook,在子goroutine执行过程中,可以通过触发Hook来达到控制子goroutine的目的(通常是取消,即让其停下来).再配合Context提供的Done方法,子goroutine可以检查自身是否被父级节点Cancel:

select{ case<-ctx.Done()://dosomeclean…}

       æ³¨ï¼šçˆ¶èŠ‚点Context可以主动通过调用cancel方法取消子节点Context,而子节点Context只能被动等待.同时父节点Context自身一旦被取消(如其上级节点Cancel),其下的所有子节点Context均会自动被取消.

       æœ‰ä¸‰ç§åˆ›å»ºæ–¹æ³•ï¼š

//带cancel返回值的Context,一旦cancel被调用,即取消该创建的contextfuncWithCancel(parentContext)(ctxContext,cancelCancelFunc)//带有效期cancel返回值的Context,即必须到达指定时间点调用的cacel方法才会被执行funcWithDeadline(parentContext,deadlinetime.Time)(Context,CancelFunc)//带超时时间cancel返回值的Context,类似Deadline,前者是时间点,后者为时间间隔//相当于WithDeadline(parent,time.Now().Add(timeout)).funcWithTimeout(parentContext,timeouttime.Duration)(Context,CancelFunc)

       ä¸‹é¢æ¥çœ‹æ”¹ç¼–自Advanced Go Concurrency Patterns视频提供的一个简单例子:

packagemainimport("context""fmt""time")funcsomeHandler(){ //创建继承Background的子节点Contextctx,cancel:=context.WithCancel(context.Background())godoSth(ctx)//模拟程序运行-Sleep5秒time.Sleep(5*time.Second)cancel()}//每1秒work一下,同时会判断ctx是否被取消,如果是就退出funcdoSth(ctxcontext.Context){ vari=1for{ time.Sleep(1*time.Second)select{ case<-ctx.Done():fmt.Println("done")returndefault:fmt.Printf("work%dseconds:\n",i)}i++}}funcmain(){ fmt.Println("start...")someHandler()fmt.Println("end.")}

       è¾“出结果:

       æ³¨æ„,此时doSth方法中case之done的fmt.Println("done")并没有被打印出来.

       è¶…时场景:

packagemainimport("context""fmt""time")functimeoutHandler(){ //创建继承Background的子节点Contextctx,cancel:=context.WithTimeout(context.Background(),3*time.Second)godoSth(ctx)//模拟程序运行-Sleep秒time.Sleep(*time.Second)cancel()//3秒后将提前取消doSthgoroutine}//每1秒work一下,同时会判断ctx是否被取消,如果是就退出funcdoSth(ctxcontext.Context){ vari=1for{ time.Sleep(1*time.Second)select{ case<-ctx.Done():fmt.Println("done")returndefault:fmt.Printf("work%dseconds:\n",i)}i++}}funcmain(){ fmt.Println("start...")timeoutHandler()fmt.Println("end.")}

       è¾“出结果:

4 context是一个优雅的设计吗?

       ç¡®å®ž,通过引入Context包,一个request范围内所有goroutine运行时的取消可以得到有R效的控制.但是这种解决方式却不够优雅.

4.1 context 像病毒一样扩散

       ä¸€æ—¦ä»£ç ä¸­æŸå¤„用到了Context,传递Context变量(通常作为函数的第一个参数)会像病毒一样蔓延在各处调用它的地方. 比如在一个request中实现数据库事务或者分布式日志记录, 创建的context,会作为参数传递到任何有数据库操作或日志记录需求的函数代码处. 即每一个相关函数都必须增加一个context.Context类型的参数,且作为第一个参数,这对无关代码完全是侵入式的.

       æ›´å¤šè¯¦ç»†å†…容可参见:Michal Strba 的context-should-go-away-go2文章

       Google Group上的讨论可移步这里.

4.2 Context 不仅仅只是cancel信号

       Context机制最核心的功能是在goroutine之间传递cancel信号,但是它的实现是不完全的.

       Cancel可以细分为主动与被动两种,通过传递context参数,让调用goroutine可以主动cancel被调用goroutine.但是如何得知被调用goroutine什么时候执行完毕,这部分Context机制是没有实现的.而现实中的确又有一些这样的场景,比如一个组装数据的goroutine必须等待其他goroutine完成才可开始执行,这是context明显不够用了,必须借助sync.WaitGroup.

funcserve(lnet.Listener)error{ varwgsync.WaitGroupvarconnnet.Connvarerrerrorfor{ conn,err=l.Accept()iferr!=nil{ break}wg.Add(1)gofunc(cnet.Conn){ deferwg.Done()handle(c)}(conn)}wg.Wait()returnerr}4.3 context.value

       context.Value相当于goroutine的TLS(Thread Local Storage),但它不是静态类型安全的,任何结构体变量都必须作为字符串形式存储.同时,所有context都会在其中定义变量,很容易造成命名冲突.

5 总结

       context包通过构建树型关系的Context,来达到上一层Goroutine能对传递给下一层Goroutine的控制.对于处理一个Request请求操作,需要采用context来层层控制Goroutine,以及传递一些变量来共享.

       Context对象的生存周期一般仅为一个请求的处理周期.即针对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源.

       æ¯æ¬¡åˆ›å»ºä¸€ä¸ªGoroutine,要么将原有的Context传递给Goroutine,要么创建一个子Context并传递给Goroutine.

       Context能灵活地存储不同类型,不同数目的值,并且使多个Goroutine安全地读写其中的值.

       å½“通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权.

       åœ¨å­Context被传递到的goroutine中,应该对该子Context的Done信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回.

6 致谢

       pkg/context

       context-should-go-away-go2

       ç†è§£ Go Context 机制

       context-isnt-for-cancellation

       context-is-for-cancelation

       thread-local-a-convenient-abomination

gRPC 流量控制详解

       gRPC 流量控制详解

       流量控制, 一般来说指的是在网络传输中, 发送者主动限制自身发送数据的速率或发送的数据量, 以适应接收者处理数据的速度. 当接收者的处理速度较慢时, 来不及处理的数据会被存放在内存中, 而当内存中的数据缓存区被填满之后, 新收到的数据就会被扔掉, 导致发送者不得不重新发送, 就会造成网络带宽的浪费.

       流量控制是一个网络组件的基本功能, 我们熟知的 TCP 协议就规定了流量控制算法. gRPC 建立在 TCP 之上, 也依赖于 HTTP/2 WindowUpdate Frame 实现了自己在应用层的流量控制.

       在 gRPC 中, 流量控制体现在三个维度:

       采样流量控制: gRCP 接收者检测一段时间内收到的数据量, 从而推测出 on-wire 的数据量, 并指导发送者调整流量控制窗口.

       Connection level 流量控制: 发送者在初始化时被分配一个 quota (额度), quota 随数据发送减少, 并在收到接收者的反馈之后增加. 发送者在耗尽 quota 之后不能再发送数据.

       Stream level 流量控制: 和 connection level 的流量控制类似, 只不过 connection level 管理的是一个发送者和一个接收者之间的全部流量, 而 stream level 管理的是 connection 中诸多 stream 中的一个.

       在本篇剩余的部分中, 我们将结合代码一起来看看这三种流量控制的实现原理和实现细节.

       本篇中的源代码均来自 /grpc/grpc-go, 并且为了方便展示, 在不影响表述的前提下截断了部分代码.

       流量控制是双向的, 为了减少冗余的叙述, 在本篇中我们只讲述 gRPC 是如何控制 server 所发送的流量的.

       gRPC 中的流量控制仅针对 HTTP/2 data frame.

采样流量控制原理

       采样流量控制, 准确来说应该叫做 BDP 估算和动态流量控制窗口, 是一种通过在接收端收集数据, 以决定发送端流量控制窗口大小的流量控制方法. 以下内容翻译自 gRPC 的一篇官方博客, 介绍了采样流量控制的意义和原理.

       BDP 估算和动态流量控制这个 feature 缩小了 gRPC 和 HTTP/1.1 在高延迟网络环境下的性能差距.

       Bandwidth Delay Product (BDP), 即带宽延迟积, 是网络连接的带宽和数据往返延迟的乘积. BDP 能够有效地告诉我们, 如果充分利用了网络连接, 那么在某一刻在网络连接上可以存在多少字节的数据.

       计算 BDP 并进行相应调整的算法最开始是由 @ejona 提出的, 后来由 gRPC-C Core 和 gRPC-Java 实现. BDP 的想法简单而实用: 每次接收者得到一个 data frame, 它就会发出一个 BDP ping frame (一个只有 BDP 估算器使用的 ping frame). 之后, 接收者会统计指导收到 ACK 之前收到的字节数. 这个大约在 1.5RTT (往返时间) 中收到的所有字节的总和是有效 BDP1.5 的近似值. 如果该值接近当前流量窗口的大小 (例如超过 2/3), 接收者就需要增加窗口的大小. 窗口的大小被设定为 BDP (所有采样期间接受到的字节总和) 的两倍.

       BDP 采样目前在 gRPC-go 的 server 端是默认开启的.

       结合代码, 一起来看看具体的实现方式.

代码分析

       我们以 client 发送 BDP ping 给 server, 并决定 server 端的流量控制窗口为例.

       在 gRPC-go 中定义了一个bdpEstimator , 是用来计算 BDP 的核心:

type?bdpEstimator?struct?{ //?sentAt?is?the?time?when?the?ping?was?sent.sentAt?time.Timemu?sync.Mutex//?bdp?is?the?current?bdp?estimate.bdp?uint//?sample?is?the?number?of?bytes?received?in?one?measurement?cycle.sample?uint//?bwMax?is?the?maximum?bandwidth?noted?so?far?(bytes/sec).bwMax?float//?bool?to?keep?track?of?the?beginning?of?a?new?measurement?cycle.isSent?bool//?Callback?to?update?the?window?sizes.updateFlowControl?func(n?uint)//?sampleCount?is?the?number?of?samples?taken?so?far.sampleCount?uint//?round?trip?time?(seconds)rtt?float}

       bdpEstimator 有两个主要的方法 add 和 calculate :

//?add?的返回值指示?是否发送?BDP?ping?frame?给?serverfunc?(b?*bdpEstimator)?add(n?uint)?bool?{ b.mu.Lock()defer?b.mu.Unlock()//?如果?bdp?已经达到上限,?就不再发送?BDP?ping?进行采样if?b.bdp?==?bdpLimit?{ return?false}//?如果在当前时间点没有?BDP?ping?frame?发送出去,?就应该发送,?来进行采样if?!b.isSent?{ b.isSent?=?trueb.sample?=?nb.sentAt?=?time.Time{ }b.sampleCount++return?true}//?已经有?BDP?ping?frame?发送出去了,?但是还没有收到?ACKb.sample?+=?nreturn?false}

       add 函数有两个作用:

       决定 client 在接收到数据时是否开始采样.

       记录采样开始的时间和初始数据量.

func?(t?*ing?flow?control?windows//?for?the?transport?and?the?stream?based?on?the?current?bdp//?estimation.func?(t?*ingWindowUpdateHandler?负责处理来自?client?的?window?update?framefunc?(l?*loopyWriter)?incomingWindowUpdateHandler(w?*incomingWindowUpdate)?error?{ if?w.streamID?==?0?{ //?增加?quotal.sendQuota?+=?w.incrementreturn?nil}......}

       sendQuota 在接收到来自 client 的 window update 后增加.

//?processData?负责发送?data?frame?给?clientfunc?(l?*loopyWriter)?processData()?(bool,?error)?{ ......//?根据发送的数据量减少?sendQuotal.sendQuota?-=?uint(size)......}

       并且 server 在发送数据时会减少 sendQuota .

Client 端//?add?的返回值指示?是否发送?BDP?ping?frame?给?serverfunc?(b?*bdpEstimator)?add(n?uint)?bool?{ b.mu.Lock()defer?b.mu.Unlock()//?如果?bdp?已经达到上限,?就不再发送?BDP?ping?进行采样if?b.bdp?==?bdpLimit?{ return?false}//?如果在当前时间点没有?BDP?ping?frame?发送出去,?就应该发送,?来进行采样if?!b.isSent?{ b.isSent?=?trueb.sample?=?nb.sentAt?=?time.Time{ }b.sampleCount++return?true}//?已经有?BDP?ping?frame?发送出去了,?但是还没有收到?ACKb.sample?+=?nreturn?false}0

       trInFlow 是 client 端控制是否发送 window update 的核心. 值得注意的是 client 端是否发送 window update 只取决于已经接收到的数据量, 而管这些数据是否被某些 stream 读取. 这一点是 gRPC 在流量控制中的优化, 即因为多个 stream 共享同一个 connection, 不应该因为某个 stream 读取数据较慢而影响到 connection level 的流量控制, 影响到其他 stream.

//?add?的返回值指示?是否发送?BDP?ping?frame?给?serverfunc?(b?*bdpEstimator)?add(n?uint)?bool?{ b.mu.Lock()defer?b.mu.Unlock()//?如果?bdp?已经达到上限,?就不再发送?BDP?ping?进行采样if?b.bdp?==?bdpLimit?{ return?false}//?如果在当前时间点没有?BDP?ping?frame?发送出去,?就应该发送,?来进行采样if?!b.isSent?{ b.isSent?=?trueb.sample?=?nb.sentAt?=?time.Time{ }b.sampleCount++return?true}//?已经有?BDP?ping?frame?发送出去了,?但是还没有收到?ACKb.sample?+=?nreturn?false}1

       这里 limit * 1/4 的限制其实是可以浮动的, 因为 limit 的数值会随着 server 端发来的 window update 而改变.

Stream level 流量控制原理

       Stream level 的流量控制和 connection level 的流量控制原理基本上一致的, 主要的区别有两点:

       Stream level 的流量控制中的 quota 只针对单个 stream. 每个 stream 即受限于 stream level 流量控制, 又受限于 connection level 流量控制.

       Client 端决定反馈给 server window update frame 的时机更复杂一点.

       Stream level 的流量控制不光要记录已经收到的数据量, 还需要记录被 stream 消费掉的数据量, 以达到更加精准的流量控制. 实际上, client 会记录:

       pendingData: stream 收到但还未被应用消费 (未被读取) 的数据量.

       pendingUpdate: stream 收到且已经被应用消费 (已被读取) 的数据量.

       limit: stream 能接受的数据上限, 被初始为 字节, 受到采样流量控制的影响.

       delta: delta 是在 limit 基础上额外增加的数据量, 当应用试着去读取超过 limit 大小的数据是, 会临时在 limit 上增加 delta, 来允许应用读取数据.

       Client 端的逻辑是这样的:

       每当 client 接收到来自 server 的 data frame 的时候, pendingData += 接收到的数据量 .

       每当 application 在从 stream 中读取数据之前 (即 pendingData 将被消费的时候),

Windows平台C++ 使用VS 编译gRPC(总结)

       若要在Windows平台使用VS编译gRPC,首先确保您的开发环境支持最新版本。由于gRPC自3..1版本开始依赖protobuf 3.x,且C++的constexpr特性在VS及更早版本中不被支持,因此推荐使用VS及以上版本进行编译。

       对于编译环境的配置,建议您采用以下步骤:

       下载并安装CMake-gui,后续步骤将通过其进行操作。

       安装Active State Perl,通过命令行验证安装是否成功。

       安装Golang,并同样通过命令行进行测试。

       尽管Git可能遇到问题,但您可以手动从GitHub下载gRPC代码,版本选择1..0或更高版本。同时,需要下载并解压gRPC的第三方库,如BoringSSL、微端源码Protobuf、benchmark等,确保选择正确的版本。

       在编译过程中,将gRPC源代码解压至无中文字符的目录,针对Windows 位系统,选择x版本。对于HelloWorld示例,需要在项目配置中添加特定预处理器定义,如_WIN_WINNT和安全警告开关。

       确保项目中的编译设置正确匹配,例如调整运行时库版本,以避免LIBCMTD/LIBCMT、MSVCRTD/MSVCRT之间的冲突。最终的编译输出包括bin和lib文件,其中java和go有单独的库。

       在使用gRPC时,将helloworld.proto文件复制到适当位置,生成pb和grpc.pb文件,并在客户端和服务器项目中集成。通过设置头文件路径、预处理器定义、库目录和附加依赖项,连接所有依赖,完成gRPC的测试和集成。

CockroachDB 源码闲逛 - II (insert a row)

       本文将深入探讨 CockroachDB 的启动过程以及处理一条简单 SQL(如插入一行数据)的具体流程。CockroachDB 使用 Go 语言中流行的 Cobra 库来构建其命令行界面(CLI),在使用 `start` 命令启动服务端后,代码从特定位置开始执行。

       启动初期,CockroachDB 会准备好各种日志和 pprof 功能。pprof 功能允许通过开关控制定期导出 CPU 和内存(通过 go/jemalloc)的性能分析报告,并定期清除旧的 pprof 数据,这有助于在排查问题时找到事故现场的堆栈或性能数据。

       之后,服务端使用一个端口同时处理 PostgreSQL、HTTP 和 gRPC 协议,代码进入 `Server.start()` 方法。这个方法包含复杂的内涵段子源码逻辑用于节点发现和 bootstrap。主要关注点在于 SQL 处理,尤其是 PostgreSQL 协议下的客户端连接。

       当客户端通过 PG 协议连接到服务端时,代码进入 `pgwire.Server#ServeConn` 方法。通过校验版本等步骤后,进入 `conn.serveImpl` 方法,这是处理请求的主要逻辑。在这里,每个客户端连接由两个 goroutine 分别处理读取协议解析和命令执行。这种设计允许在执行过程中同时接收客户端连接事件,例如在执行大规模 SQL 过程时,通过关闭其中一个 goroutine 可以在 SQL 执行的同时响应客户端的 `FIN` 指令。

       在客户端连接的两个 goroutine 准备好后,发送的 SQL 语句开始在 `coordinator-side` 进行处理。首先,`read goroutine` 解析网络包,并根据不同的 PG cmd 分发到相应的方法进行处理。对于简单的文本执行查询,`handleSimpleQuery` 方法相对简单。为了区分不同批量的命令,当一组命令推送到 `stmtBuf` 后,会插入一个哨兵 `Sync` 来标记当前批次结束以及后续命令属于下一个批次。

       随后,`process goroutine` 从 `stmtBuf` 中获取命令,根据不同的命令类型分发到相应的 `exec*` 方法。例如,简单查询产生的 `ExecStmt` 会进入 `execStmt` 方法,在此之前会创建 `stmtRes` 来封装后续返回客户端响应的缓冲区刷新逻辑。

       在处理 SQL 语句时,CockroachDB 会维护一个状态机(StateMachine),用于管理当前连接的事务状态。状态机的定义和行为主要与事务相关,包括 `noTxn`、`open`、`abort`、`implicit` 等状态。在处理插入一行数据的简单语句(如 `INSERT INTO t (a) VALUES (1);`)时,流程如下:

       首先,客户端与服务端建立连接,力软源码启动两个 goroutine。当插入语句发送到服务端后,`read goroutine` 开始解析并放置到 `stmtBuf`。

       随后,`process goroutine` 从 `stmtBuf` 拿出命令,识别为 `ExecStmt`。由于执行此语句前未开始事务,当前连接的状态机处于 `stateNoTxn`。因此,执行 `execStmtInNoTxnState` 方法,因为没有事务,仅执行 `execStmtInNoTxnState` 的默认分支,返回 `eventTxnStart` 事件和 `eventTxnStartPayload`。此时,状态机应用 `noTxnToOpen` 进程,为隐式事务的启动做准备。服务端通过 `client.NewTxn` 创建事务,获取时间戳并准备 `sender` 和 `coordinator` 等工作。接着,设置 `advanceInfo` 为 `advanceOne`、`noRewind`(无需回移 `stmtBuf`,通常重试时需要回移)和 `txnState` 为 `txnStart`。事务状态为 `open` 后,`execCmd` 会从 `stmtBuf` 中继续取出插入语句并执行。

       当当前事务状态为 `open` 且为 `implicit` 时,`execStmtInOpenState` 方法继续执行。由于当前 SQL 不是 `BEGIN`、`COMMIT` 等操作,挂载了 `handleAutoCommit` 的 `defer` 函数,并处理 `AS OF` 时间逻辑后,进入 `dispatchToExecutionEngine` 方法。

       在 `makeExecPlan` 方法中,创建逻辑计划。接下来,评估是否能够分布执行逻辑计划(对于插入操作,CockroachDB 当前不支持分布式计划)。然后,为逻辑计划准备上下文,调用 `execWithDistSQLEngine`。项目源码管理对于不可分布执行的情况,创建简化版的 `planCtx`,用于生成物理计划。在此步骤中,生成物理计划(如 `row count` 算子)并最终生成执行流程。

       在准备和生成流程后,服务端启动在本地节点的执行流程。通过 `local execution` 的 `setup` 和 `run` 方法,执行生成的处理器(如 `planNodeToRowSource`)。在 `run` 方法中,执行 `rowCountNode` 算子,进而触发 `insertNode` 的 `BatchNext`,以火山模式(一次过一个批处理的多个行)执行插入操作。

       插入操作中,`BatchNext` 分批处理,根据 `maxInsertBatchSize`(默认为 )进行分批。对于非最后一批次,会通过 `txn.Run` 发送至存储节点,将数据分批存储。在 `checkHelper` 函数中,检查表约束,分为 `eval` 和 `input` 模式,前者是老逻辑,后者在插入前检查约束结果,作为插入算子的输入,有利于优化插入操作。

       添加批处理时,调用 `initResult` 准备每个 `CPut` 的结果。如果批处理中某个命令失败(如序列化失败),会在 `initResult` 中保存序列化失败信息。

       之后,将准备好的批处理发送至 `replica-side`。在 `finalize` 中,将 `EndTransactionRequest` 添加到批处理的末尾,通过 `txn.Run` 发起。此时,批处理中包含一个条件 `put` 和一个结束事务请求,服务端通过 `DistSender.Send` 将批处理发送至 `replica-side`。批处理中的 `result` 包含 `err` 信息,用于验证批处理序列化无误。

       在 `replica-side`,请求到达节点的存储层,找到相关范围的副本对象并处理等待逻辑。对于写入操作,使用 Raft 进行 `Replica.executeWriteBatch`。在此方法中,使用 `Latch` 机制来优化对交叠和非交叠批处理的处理,同时执行批处理命令的 `evaluateWriteBatch` 方法将所有命令应用到数据中,生成 `engine.Batch` 并构建 `ProposalData`。最终,通过 Raft 提出修改,实现数据的最终一致性。

       最后,执行成功或失败后,结果会沿原路径返回至客户端。

       总结,本文详细阐述了 CockroachDB 从启动到处理简单 SQL(如插入操作)的全过程。通过深入分析,读者能够更好地理解 CockroachDB 的内部工作机制,为后续阅读代码提供基础。未来计划将关注点扩展到重试处理逻辑,进一步探索 `stmtBuf` 和状态机在 CockroachDB 中的使用。

PolarisMesh源码系列--Polaris-Go注册发现流程

       北极星是腾讯开源的一款服务治理平台,其目标在于解决分布式和微服务架构中的服务管理、流量管理、配置管理、故障容错和可观测性问题。与Spring Cloud、Apache Dubbo和Istio等其他流行技术相比,北极星提供了独特的优势与服务注册发现的实现。

       从功能实现角度看,Spring Cloud、Apache Dubbo、Istio和北极星都实现了服务治理的关键功能,但它们的实现思路有所不同。Spring Cloud在Spring Boot框架基础上扩展,继承了其灵活性,能够方便地集成服务注册发现、服务治理和可观测组件。而北极星则直接从下一代架构基金会制定的服务治理标准出发,构建服务治理的模型,并基于此模型构建控制面和数据面,提供了统一的服务治理框架。

       ServiceMesh采用Sidecar模式解耦业务逻辑和服务治理逻辑,将服务治理能力下沉到基础设施,增强整体架构的灵活性。然而,这种模式在性能上有所损耗,并且对中小团队的灵活性和扩展性提出了挑战。Istio虽然提供了基于虚拟机/物理机的部署方式,但对Kubernetes的依赖较高,非Kubernetes环境的团队可能难以部署。

       北极星Mesh则通过融合和兼容多种技术,提供了一种自顶向下的正向思考过程。它先基于服务治理标准构建模型,然后围绕该模型构建控制面和数据面,支持与ServiceMesh的集成,为未来发展留有空间。此外,北极星Mesh通过插件机制为框架扩展预留了灵活性。

       本文重点分析了Polaris-Go SDK在服务注册和发现过程中的技术实现和源码阅读。服务注册流程相对简单,线性操作,通过gRPC服务接口实现。服务发现流程则更为复杂,涉及本地缓存与远程服务器信息的懒加载同步,以及处理实例信息、服务信息、路由信息和限流信息等复杂内容。在服务发现过程中,gRPC接口被用于关键点的处理。

       综上所述,北极星服务治理平台通过实现服务治理标准,提供了全面的服务发现和治理方案。其客户端与服务器端的数据同步与交互设计了良好的服务治理模型和通信机制,确保了可靠性和稳定性。同时,通过插件机制,Polaris-Go SDK框架提供了灵活的扩展能力。这一分析仅是基于现有信息,如有错误或遗漏,欢迎指正。

Golang 设计模式之装饰器模式

       本期和大家交流的是设计模式中的装饰器模式。

       装饰器模式的基本定义是:在不变更原对象结构的基础上,动态地为对象增加附属能力。它与“继承”有一定的相似之处,但侧重点不同,可以将装饰器模式视为“继承”的一种补充手段。

       为了更好地理解装饰器模式,以下是一个实际案例的分析:

       通过编程的方式,我们可以还原上述场景问题。一种常见的实现方式是采用继承。然而,这种实现方式需要对子类的等级和种类进行枚举,包括一系列一级和二级子类。这种固定的等级架构存在一些问题。

       因此,在这种“加料”场景中,使用继承的设计模式可能并不合适。我们可以改变思路,不再关注对所有组合种类的枚举,而是将注意力放在“加料”的过程中。

       在这种实现思路下,就诞生了基于“装饰器模式”的实现架构。例如,一份鸡蛋培根盖浇饭可以由一份白米饭(核心类)加上一份鸡蛋(装饰器1)和一份培根(装饰器2)组成,其中鸡蛋和培根的装饰顺序不限制。这样,无论后续有多少种新的“菜品”加入,我们只需声明其对应的装饰器类即可。

       比如,双份鸡蛋盖浇饭 = 一份白米饭(核心类)+ 一份鸡蛋(装饰器1)+一份鸡蛋(装饰器1);鸡蛋火腿青椒盖浇饭 = 一份白米饭(核心类)+ 一份鸡蛋(装饰器1)+一份青椒(装饰器2)+一份火腿(装饰器3);双份牛肉青椒盖浇饭 = 一份白米饭(核心类)+ 一份青椒(装饰器4)+一份牛肉(装饰器5)+一份牛肉(装饰器5)。

       至此,问题得到了圆满解决。接下来,我们对装饰器模式和继承模式进行对比总结。

       下面进入代码实战环节,通过编程实现一个搭配食材的案例,展示装饰器模式的实现细节。

       这个案例非常简单,我们需要在主食的基础上添加配菜,最终搭配出美味可口的食物套餐。其中主食包括米饭 rice 和面条 noodle 两条,配菜包括老干妈 LaoGanMa(老干妈拌饭顶呱呱)、火腿肠 HamSausage 和煎蛋 FriedEgg 三类。

       事实上,如果需要,主食和配菜也可以随时进行扩展。在装饰器模式中,这种扩展行为的成本并不高。

       接下来,展示一下总体的 UML 类图。

       首先是对应于装饰器模式中核心类的是原始的主食 Food,我们声明了一个 interface,其中包含两个核心方法,Eat 和 Cost,含义分别为食用主食以及计算出主食对应的花费。

       接下来是装饰器部分,我们声明了一个 Decorate interface,它们本身是在强依附于核心类(主食)的基础上产生的,只能起到锦上添花的作用,因此在构造器函数中,需要传入对应的主食 Food。

       接下来分别声明三个装饰器的具体实现类,对应为老干妈 LaoGanMaDecorator、火腿肠 HamSausageDecorator 和煎蛋 FriedEggDecorator。

       每个装饰器类的作用是对食物进行一轮装饰增强,因此需要在构造器函数中传入待装饰的食物,然后通过重写食物的 Eat 和 Cost 方法,实现对应的增强装饰效果。

       下面提供另一种闭包实现装饰增强函数的实现示例,其实现也是遵循着装饰器模式的思路,但在形式上会更加简洁直观一些。

       其中核心的处理方法 handleFunc 对应的是装饰器模式中的核心类,Decorate 增强方法对应的则是装饰器类,每次在执行 Decorate 的过程中,都会在 handleFunc 前后增加一些额外的附属逻辑。

       为了加深理解,以下摘出一个实际项目中应用到装饰器模式的使用案例进行分析。

       这里给到的案例是 grpc-go 中对拦截器链 chainUnaryInterceptors 的实现。

       在 grpc-go 服务端模块中,每次接收到来自客户端的 grpc 请求,会根据请求的 path 映射到对应的 service 和 handler 进行执行逻辑的处理,但在真正调用 handler 之前,会先经历一轮对拦截器链 chainUnaryInterceptors 的遍历调用。

       下面我们来观察一下其中具体的源码细节。

       首先,对于拦截器类 UnaryServerInterceptor,本身是一个函数的类型。

       下面是生成拦截器链的方法 chainUnaryInterceptors。该方法入参是用户定义好的一系列拦截器 interceptors,内部会按照顺序对拦截器进行组装,最终通过层层装饰增强的方式,将整个执行链路压缩成一个拦截器 UnaryServerInterceptor 的形式进行方法。

       在这个过程中,就体现了我们今天讨论的装饰器模式的设计思路。核心业务处理方法 handler 对应的就是装饰器模式中的核心类,每一轮通过拦截器 UnaryServerInterceptor 对 handler 进行增强的过程,对应的就是一次“装饰”的步骤。

       下面给出一个具体实现的装饰器的代码示例,可以看到其中在核心方法 handler 前后分别执行了对应的附属逻辑,起到了装饰的效果。

       如果各位读友们想了解更多关于 grpc-go 的内容,可以阅读我之前发表的相关话题文章。

       本期和大家交流了设计模式中的装饰器模式。装饰器模式能够动态地为对象增加某种特定的附属能力,相比于继承模式显得更加灵活,且符合开闭原则,可以作为继承模式的一种有效补充手段。