gRPC阅读(4)——负载均衡

负载均衡算法 常见的负载均衡算法如下: RoundRobin(轮询) Weight-RoundRobin(加权轮询) 不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。 Random(随机) Weight-Random(加权随机) 通过系统的随机算法,根据后端服务器的列表随机选取其中的一台服务器进行访问 源地址哈希法 源地址哈希的思想是根据获取客户端的 IP 地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一 IP 地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问 最小连接数法 最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器 Consistent-Hash(一致性哈希算法) 常见的是 Ketama 算法(虚拟节点),该算法是用来解决 cache 失效导致的缓存穿透的问题的,当然也可以适用于 gRPC 长连接的场景 自适应算法(P2C:多选二,二选一):即从可用节点列表中随机选择两个节点,计算它们的负载率,选择负载率较低的进行请求 基于最小负载策略:该策略是 linkerd 的默认负载均衡器。当确定发送请求的位置时,linkerd 随机从负载均衡器池中选择两个副本,并选择两者中最少负载的副本。负载由每个副本的未完成请求数决定。该算法为单个副本提供了可管理的负载上限,与具有相似性能的其他算法相比,开销较少 峰值 EWMA(预测)策略:该算法是上述的变体,同样在发送请求时仍然在两个副本之间进行选择。不一样的是,该算法需要保持观察到的延迟的动态平均值,并且使用它来对每个副本的未完成请求的数量进行加权。这种方法对延迟波动更敏感,并通过向较慢的后端发送更少的请求来允许他们恢复时间(可以通过调参来改变对请求延时的敏感度) gRPC负载均衡 接着上篇,通过DNS进行服务发现获取服务器IP list之后,调用ccResolverWrapper.UpdateState更新状态: // UpdateState is called by resolver implementations to report new state to gRPC // which includes addresses and service config. func (ccr *ccResolverWrapper) UpdateState(s resolver.State) error { ... return ccr.cc.updateResolverStateAndUnlock(s, nil) } func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error) error { ... var ret error // 应用最新的从dns发现的ServiceConfig if cc.dopts.disableServiceConfig { channelz.Infof(logger, cc.channelz, "ignoring service config from resolver (%v) and applying the default because service config is disabled", s.ServiceConfig) cc.maybeApplyDefaultServiceConfig() } else if s.ServiceConfig == nil { cc.maybeApplyDefaultServiceConfig() } else { if sc, ok := s.ServiceConfig.Config.(*ServiceConfig); s.ServiceConfig.Err == nil && ok { configSelector := iresolver.GetConfigSelector(s) if configSelector != nil { if len(s.ServiceConfig.Config.(*ServiceConfig).Methods) != 0 { channelz.Infof(logger, cc.channelz, "method configs in service config will be ignored due to presence of config selector") } } else { configSelector = &defaultConfigSelector{sc} } cc.applyServiceConfigAndBalancer(sc, configSelector) } else { ... } } // ServiceConfig的负载均衡配置 var balCfg serviceconfig.LoadBalancingConfig if cc.sc != nil && cc.sc.lbConfig != nil { balCfg = cc.sc.lbConfig } // 负载均衡器 bw := cc.balancerWrapper cc.mu.Unlock() // 应用服务发现结果 uccsErr := bw.updateClientConnState(&balancer.ClientConnState{ResolverState: s, BalancerConfig: balCfg}) if ret == nil { ret = uccsErr // prefer ErrBadResolver state since any other error is // currently meaningless to the caller. } return ret } // ccBalancerWrapper解耦ClientConn和Balancer,在updateClientConnState调用时才会创建Balancer // 并且ccBalancerWrapper使用gracefulswitch.Balancer支持Balancer的优雅切换 func (ccb *ccBalancerWrapper) updateClientConnState(ccs *balancer.ClientConnState) error { errCh := make(chan error) uccs := func(ctx context.Context) { defer close(errCh) if ctx.Err() != nil || ccb.balancer == nil { return } name := gracefulswitch.ChildName(ccs.BalancerConfig) if ccb.curBalancerName != name { ccb.curBalancerName = name channelz.Infof(logger, ccb.cc.channelz, "Channel switches to new LB policy %q", name) } // 通知gracefulBalancer状态更新 err := ccb.balancer.UpdateClientConnState(*ccs) if logger.V(2) && err != nil { logger.Infof("error from balancer.UpdateClientConnState: %v", err) } errCh <- err } onFailure := func() { close(errCh) } ccb.serializer.ScheduleOr(uccs, onFailure) return <-errCh } // gsb更新状态 func (gsb *Balancer) UpdateClientConnState(state balancer.ClientConnState) error { // 获取最新的balancer balToUpdate := gsb.latestBalancer() gsbCfg, ok := state.BalancerConfig.(*lbConfig) if ok { // Switch to the child in the config unless it is already active. if balToUpdate == nil || gsbCfg.childBuilder.Name() != balToUpdate.builder.Name() { var err error // 切换到新的balancer balToUpdate, err = gsb.switchTo(gsbCfg.childBuilder) if err != nil { return fmt.Errorf("could not switch to new child balancer: %w", err) } } // Unwrap the child balancer's config. state.BalancerConfig = gsbCfg.childConfig } if balToUpdate == nil { return errBalancerClosed } // 通知真正的Balancer状态更新 return balToUpdate.UpdateClientConnState(state) } // gsb优雅切换balancer func (gsb *Balancer) switchTo(builder balancer.Builder) (*balancerWrapper, error) { gsb.mu.Lock() if gsb.closed { gsb.mu.Unlock() return nil, errBalancerClosed } bw := &balancerWrapper{ builder: builder, gsb: gsb, lastState: balancer.State{ ConnectivityState: connectivity.Connecting, Picker: base.NewErrPicker(balancer.ErrNoSubConnAvailable), }, subconns: make(map[balancer.SubConn]bool), } balToClose := gsb.balancerPending // nil if there is no pending balancer if gsb.balancerCurrent == nil { gsb.balancerCurrent = bw } else { gsb.balancerPending = bw } gsb.mu.Unlock() // 关闭旧的balancer balToClose.Close() // 创建新的balancer newBalancer := builder.Build(bw, gsb.bOpts) if newBalancer == nil { gsb.mu.Lock() if gsb.balancerPending != nil { gsb.balancerPending = nil } else { gsb.balancerCurrent = nil } gsb.mu.Unlock() return nil, balancer.ErrBadResolverState } bw.Balancer = newBalancer return bw, nil } // pick_first balancer状态更新 func (b *pickfirstBalancer) UpdateClientConnState(state balancer.ClientConnState) error { ... cfg, ok := state.BalancerConfig.(pfConfig) ... // 展开endpoints/addrs var addrs []resolver.Address ... // balancer之前维护了子连接 // 如果连接的地址在addrs中,则保持连接 // 否则断开并用addrs重连 if b.subConn != nil { b.cc.UpdateAddresses(b.subConn, addrs) return nil } // balancer之前没有维护子连接 // 创建新的子连接 var subConn balancer.SubConn subConn, err := b.cc.NewSubConn(addrs, balancer.NewSubConnOptions{ StateListener: func(state balancer.SubConnState) { // 子连接状态更新,回调通知balancer b.updateSubConnState(subConn, state) }, }) ... b.subConn = subConn b.state = connectivity.Idle // 通知cc现在是Connecting状态 b.cc.UpdateState(balancer.State{ ConnectivityState: connectivity.Connecting, Picker: &picker{err: balancer.ErrNoSubConnAvailable}, }) b.subConn.Connect() return nil } 可以看到在pick_first策略下,会将所有地址都试一遍,连上其中一个后,后续picker返回的都是那个连接,相当于不做负载均衡: ...

十一月 20, 2024 · by NOSAE

堆 vs 胜者树 vs 败者树

外部排序算法 本文介绍这三种数据结构是因为在外部排序算法中进行多路归并时,常常需要在内存中选择多路数据中的最小值,而这三者都能实现相同的功能,因此放在一起比较。 对外部排序算法熟悉的读者可以跳过该小节。 外部排序是用于对超出计算机内存容量的大型数据集进行排序的一种算法。在排序过程中,需要将数据集分成多个较小的子集,并在内存中对每个子集进行排序,然后再将排序后的子集合并起来。这种算法通常会利用硬盘等外部存储设备来协助处理数据,因此被称为“外部排序”。 外部排序的好处和必要性在于: 内存无法装入大量待排序数据,使用外部排序算法可以避免占用过多的内存。 外部排序算法还可以通过将数据集分成多个块并对每个块进行并行处理来进一步提高性能。 多路归并算法通常使用一个优先队列(也称为最小堆)来保存各个子集中的数据。在合并过程中,首先从各个子集中读取一个元素,并将它们插入到优先队列中。队列会自动将它们排序,因此队列顶端的元素是当前最小的元素。我们将队列顶端的元素取出,并将它插入到磁盘文件中。然后我们从该元素所在的子集中读取下一个元素,并将它插入到队列中,这样队列中的元素数保持不变。这个过程一直重复,直到所有元素都被读取出来,合并完成。 堆 堆(Heap)是一种特殊的完全二叉树数据结构,分为最大堆和最小堆: 最大堆:每个节点的值都大于等于其子节点,根节点是最大值。 最小堆:每个节点的值都小于等于其子节点,根节点是最小值。 堆常用于实现优先队列,支持快速获取最大值或最小值,基本操作(插入和删除)时间复杂度为 O(logn)。 具体操作如下: 取出一个元素总是发生在堆顶,因为堆顶的元素是最小的(小顶堆中)。 使用堆中最后一个元素来填补空缺位置 对顶部元素进行下沉,如果左右孩子有比自己小的,则选择选择最小的那个进行交换。重复进行下沉操作,以满足堆的性质。即每次下沉需要进行两次比较操作 胜者树 胜者树(Winner Tree)是一种特殊的完全二叉树,用于多路归并排序。它的叶子节点表示参与归并的多个数据流,非叶子节点存储比较中获胜者(较小或较大值),根节点最终存储全局最小值或最大值。 胜者树支持快速找到当前最小值并动态更新(时间复杂度 O(logk),k 为数据流数量),常用于归并排序和流式处理场景。 胜者树满足: 胜者树一棵完全二叉树。其中的叶结点是要排序的元素,非叶结点是两个子结点中胜者的代表。 根节点代表着所有元素中的胜者。 当每次有新的元素填充进来,需要不断上浮,每次上浮与兄弟元素比较一次即可,但是每个节点指向的是父节点,而不是兄弟节点,所以需要先拿到父节点才能拿到兄弟节点,即上浮需要两次访存 败者树 败者树是胜者树的一种变体,它也是一棵完全二叉树。和胜者树不同的是,败者树的节点存储的是败者:在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。 当每次有新的元素填充进来,需要不断上浮,每次上浮与父节点比较一次即可,即一次访存 总结 堆:每次新增元素需要下沉元素,每次下沉需要两次访存+两次比较 胜者树:每次新增元素需要上浮元素,每次上浮需要两次访存+一次比较 败者树:每次新增元素需要上浮元素,每次上浮需要一次访存+一次比较 参考 https://cloud.tencent.com/developer/article/2238630 https://silver-birch-wawa.github.io/2020/05/15/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E8%B4%A5%E8%80%85%E6%A0%91%EF%BC%9F/

十一月 19, 2024 · by NOSAE

gRPC阅读(3)—— 服务发现

服务发现概述 平时用浏览器上过网都知道,输入一个网址比如google.com就能访问内容,背后是DNS帮我们将google.com解析成IP地址,最终浏览器才能基于TCP协议,从本地连接到这个服务提供商的IP地址。所以DNS属于服务发现的其中一种方式。 所以服务发现提供的就是通过自动化的方式帮助服务在网络中找到彼此,无需手动配置。 一个好的服务发现需要: 服务地址动态变化:服务的 IP 或端口可能因为容器化或自动扩展而频繁改变。 高可用:需要在服务实例宕机时快速感知并移除不健康的实例。 负载均衡:服务发现需要为调用方提供负载均衡能力,选择最佳的服务实例。 服务发现通常与负载均衡同时实现,分为两种方式: 客户端服务发现(如eureka、consul):在客户端做负载均衡,选择一个实例进行调用,优点是避免集中式LB可能存在的瓶颈,性能较好,但是每个客户端需要维护服务端列表,服务端这部分的负载可能变高。并且更新LB或其他相关组件的策略时需要所有客户端都一起更新,管理不方便。并且需要多语言支持 代理服务发现(如k8s+coreDNS、nginx+consul):客户端将请求发送到负载均衡器(如 API 网关),由负载均衡器查询服务注册中心并将请求转发给目标服务实例。 独立LB进程:LB与消费者在同一个主机中,但分别作为不同的进程,避免了需要多语言支持,以及LB的更新不需要调用方改代码。 服务发现的核心组件有:注册中心、服务提供者、客户端(服务消费者) 服务发现的关键功能有:服务注册、服务查询、健康检查、动态更新 gRPC服务发现 gRPC使用客户端服务发现,gRPC中称为名称解析(Name Resolution),默认情况下使用DNS-resolver。通过服务发现解析出IP列表后就通过LB组件进行负载均衡并建立连接。 下面基于target=localhost:50052这个服务端地址来进行分析,并且是默认的DNS作为resolver(不用官方例子的50051端口是因为被mac的launchd进程占用了)。 首先gRPC在创建cc(ClientConn)的时候,使用initParsedTargetAndResolverBuilder创建resolver.Builder。这一步决定的是采用什么服务发现机制,默认是DNS。 func (cc *ClientConn) initParsedTargetAndResolverBuilder() error { logger.Infof("original dial target is: %q", cc.target) // 尝试直接解析target并获取相应的resolver.Builder var rb resolver.Builder parsedTarget, err := parseTarget(cc.target) if err == nil { rb = cc.getResolver(parsedTarget.URL.Scheme) if rb != nil { cc.parsedTarget = parsedTarget cc.resolverBuilder = rb return nil } } // target没有指定schema(比如我们的localhost:50052是没有指定schema的)或者无法匹配schema对应的resolver.Builder // 那么使用默认的schema,即dns defScheme := cc.dopts.defaultScheme if internal.UserSetDefaultScheme { defScheme = resolver.GetDefaultScheme() } // 此处canonicalTarget为dns:///localhost:50052 // "//"与第三个"/"之间的是authority canonicalTarget := defScheme + ":///" + cc.target // 再次尝试target并获取相应的resolver.Builder,此处会拿到dns.dnsBuilder parsedTarget, err = parseTarget(canonicalTarget) if err != nil { return err } rb = cc.getResolver(parsedTarget.URL.Scheme) if rb == nil { return fmt.Errorf("could not get resolver for default scheme: %q", parsedTarget.URL.Scheme) } // 保存parsedTarget和resolverBuilder cc.parsedTarget = parsedTarget cc.resolverBuilder = rb return nil } 那么resolverBuilder在什么时候会Build一个resolver出来呢?在ide的帮助下,可以直接定位到这个函数中: ...

十一月 18, 2024 · by NOSAE

请不要再称数据库是CP或者AP (Please stop calling databases CP or AP)

Note 文章转载自https://blog.the-pans.com/cap/ 其它参考: quorum-rw 后分布式时代: 多数派读写的’少数派’实现 经Martin Kleppman本人同意,这篇文章是他英文原文的中文翻译。Authorized by Martin Kleppmann, this is a Chinese translation of his original blog post. ...

十一月 17, 2024 · by NOSAE

向量数据库概述

Note 文章转载自https://guangzhengli.com/blog/zh/vector-database/ 也许你最近可能听过这样的新闻,某向量数据库的初创公司刚写好 PPT,就获得了几千万的投资,某公司的开源的向量数据库因其代码的简陋而登上了 Hackernews 等等。在过去几个月时间中, AI 应用的发展如火如荼,带动了 AI 应用技术栈上下游的火爆,而向量数据库就是其中最热门的之一。 ...

十一月 16, 2024 · by NOSAE