go grpc-go 连接变动,导致全服 gRPC 重连 BUG 排查

文章讲述了在项目中gRPC网络遇到的问题,当节点变更时,连接会重建。问题源于gRPCResolver中地址对象的处理不当,特别是Attributes字段的变化导致连接错误。文章分析了1.410版本的代码矛盾,并指出在最新代码中需要Address.Equal方法确保地址相等,但项目代码仍存在问题。
摘要由CSDN通过智能技术生成

问题描述

项目中遇到一个问题,每当有节点变更时,整个 gRPC 网络连接会重建

然后我对该问题做了下排查

最后发现是 gRPC Resolver 使用上的一个坑

问题代码

func (r *xxResolver) update(nodes []*registry.Node) {
	state := resolver.State{
		Addresses: make([]resolver.Address, 0, 10),
	}

	var grpcNodes []*registry.Node // grpc的节点
	for _, n := range nodes {
		if _, ok := n.ServicePorts["grpc"]; ok {
			grpcNodes = append(grpcNodes, n)
		}
	}

	for _, n := range grpcNodes {
		port := n.ServicePorts["grpc"]
		addr := resolver.Address{
			Addr:       fmt.Sprintf("%s:%d", n.Host, port),
			 问题代码在下面这行 
			Attributes: attributes.New(registry.NodeKey{}, n, registry.NodeNumber{}, len(grpcNodes)),   问题代码在这行 
		}
		state.Addresses = append(state.Addresses, addr)
	}

	if r.cc != nil {
		r.cc.UpdateState(state)
	}
}

这段代码的意思是:

  • xxResolver 是个 gRPC 名字解析器实现
    • 名字解析器就是收集有哪些 ip 列表(没接触过 gRPC Resolver ,你可以把它看成是类似 DNS 的域名解析过程)
  • nodes 里服务发现维护的节点集合
  • 从 nodes 中搜集相关 IP 列表
  • 最后更新 gRPC 连接器/平衡器(cc)的状态

在构建 resolver.Address 时,字段 Attributes 添加了 NodeNumber 属性;而这个 NodeNumber 值是变化的

发生 bug 的原因

项目基于 gRPC 1.410 版本。该版本本身就存在前后矛盾的 bug 代码:

  • 不同处的代码对 resolver.Address 对象的等于操作不一致

具体两处代码分别如下:

func (b *baseBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
	// TODO: handle s.ResolverState.ServiceConfig?
	if logger.V(2) {
		logger.Info("base.baseBalancer: got new ClientConn state: ", s)
	}
	// Successful resolution; clear resolver error and ensure we return nil.
	b.resolverErr = nil
	// addrsSet is the set converted from addrs, it's used for quick lookup of an address.
	addrsSet := make(map[resolver.Address]struct{})
	for _, a := range s.ResolverState.Addresses {
		// Strip attributes from addresses before using them as map keys. So
		// that when two addresses only differ in attributes pointers (but with
		// the same attribute content), they are considered the same address.
		//
		// Note that this doesn't handle the case where the attribute content is
		// different. So if users want to set different attributes to create
		// duplicate connections to the same backend, it doesn't work. This is
		// fine for now, because duplicate is done by setting Metadata today.
		//
		// TODO: read attributes to handle duplicate connections.
		aNoAttrs := a
		aNoAttrs.Attributes = nil
		addrsSet[aNoAttrs] = struct{}{}
		if scInfo, ok := b.subConns[aNoAttrs]; !ok {
			// a is a new address (not existing in b.subConns).
			//
			// When creating SubConn, the original address with attributes is
			// passed through. So that connection configurations in attributes
			// (like creds) will be used.
			sc, err := b.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{HealthCheckEnabled: b.config.HealthCheck})
			if err != nil {
				logger.Warningf("base.baseBalancer: failed to create new SubConn: %v", err)
				continue
			}
			b.subConns[aNoAttrs] = subConnInfo{subConn: sc, attrs: a.Attributes}
			b.scStates[sc] = connectivity.Idle
			sc.Connect()
		} else {
			// Always update the subconn's address in case the attributes
			// changed.
			//
			// The SubConn does a reflect.DeepEqual of the new and old
			// addresses. So this is a noop if the current address is the same
			// as the old one (including attributes).
			scInfo.attrs = a.Attributes
			b.subConns[aNoAttrs] = scInfo
			b.cc.UpdateAddresses(scInfo.subConn, []resolver.Address{a})
		}
	}
	for a, scInfo := range b.subConns {
		// a was removed by resolver.
		if _, ok := addrsSet[a]; !ok {
			b.cc.RemoveSubConn(scInfo.subConn)
			delete(b.subConns, a)
			// Keep the state of this sc in b.scStates until sc's state becomes Shutdown.
			// The entry will be deleted in UpdateSubConnState.
		}
	}
	// If resolver state contains no addresses, return an error so ClientConn
	// will trigger re-resolve. Also records this as an resolver error, so when
	// the overall state turns transient failure, the error message will have
	// the zero address information.
	if len(s.ResolverState.Addresses) == 0 {
		b.ResolverError(errors.New("produced zero addresses"))
		return balancer.ErrBadResolverState
	}
	return nil
}

baseBalancer.UpdateClientConnState 比较 Addresses 对象,事先处理了aNoAttrs.Attributes = nil

即,不考虑属性字段内容,即只要 ip port 一样就是同个连接

func (ac *addrConn) tryUpdateAddrs(addrs []resolver.Address) bool {
	ac.mu.Lock()
	defer ac.mu.Unlock()
	channelz.Infof(logger, ac.channelzID, "addrConn: tryUpdateAddrs curAddr: %v, addrs: %v", ac.curAddr, addrs)
	if ac.state == connectivity.Shutdown ||
		ac.state == connectivity.TransientFailure ||
		ac.state == connectivity.Idle {
		ac.addrs = addrs
		return true
	}

	if ac.state == connectivity.Connecting {
		return false
	}

	// ac.state is Ready, try to find the connected address.
	var curAddrFound bool
	for _, a := range addrs {
		if reflect.DeepEqual(ac.curAddr, a) {
			curAddrFound = true
			break
		}
	}
	channelz.Infof(logger, ac.channelzID, "addrConn: tryUpdateAddrs curAddrFound: %v", curAddrFound)
	if curAddrFound {
		ac.addrs = addrs
	}

	return curAddrFound
}

addrConn.tryUpdateAddrs 内用的是 DeepEqual ,这里 Addresses 字段又是起作用的。

导致连接先关闭,后重建

问题解决

基于 gRPC 1.410 代码中的相互矛盾点,然后看了最新的代码:

// Equal returns whether a and o are identical.  Metadata is compared directly,
// not with any recursive introspection.
//
// This method compares all fields of the address. When used to tell apart
// addresses during subchannel creation or connection establishment, it might be
// more appropriate for the caller to implement custom equality logic.
func (a Address) Equal(o Address) bool {
	return a.Addr == o.Addr && a.ServerName == o.ServerName &&
		a.Attributes.Equal(o.Attributes) &&
		a.BalancerAttributes.Equal(o.BalancerAttributes) &&
		a.Metadata == o.Metadata
}

加了 Address.Equal 统一了 Address 等号操作的定义(说明官方也发现了这个问题 = =|)

但是很不幸,Attributes 字段也必须相等,才算地址相等

因此,项目中的代码还是写错的

grpc-go是一款受欢迎的Go语言实现的高性能RPC框架,用于构建分布式系统。它基于Google的开源框架gRPC,并为Go语言提供了相应的接口和库。下面我将对grpc-go源码进行简要剖析。 在grpc-go源码中,最重要的是其核心组件:Transport、Balancer、Resolver和Server。Transport负责网络传输,提供基于TCP、HTTP/2和TLS的通信功能。Balancer用于负载均衡,可根据策略将请求分配到不同的服务节点。Resolver负责服务发现,帮助客户端找到可用的服务实例。Server则是用于接收和处理来自客户端的请求。 在Transport层,grpc-go使用了高效的HTTP/2协议作为底层通信协议。通过HTTP2Transport接口,它可以方便地与底层通信协议进行交互。此外,Transport还利用了框架提供的拦截器机制,可以实现一些额外的功能,比如认证、日志记录等。 在Server端,grpc-go提供了一个灵活的框架,可以通过定义服务接口和实现服务方法来处理请求。它还支持多种传输模式,包括独立的HTTP/2、TCP以及TLS加密等。Server还提供了流式调用和双向流式调用的支持,可以处理大量并行请求。 在Client端,grpc-go提供了方便的接口和功能,用于与服务端进行通信。客户端可以根据服务接口定义的方法来发起请求,并且可以设置超时时间、重试机制等。此外,客户端还支持流式和双向流式调用,可以实现高效的数据交互。 总结来说,grpc-go源码剖析主要集中在核心组件的实现,包括Transport、Balancer、Resolver和Server等。通过这些组件的协同工作,grpc-go能够实现高性能、高效的RPC通信。同时,grpc-go还提供了丰富的功能和接口,方便开发人员使用和扩展。通过理解grpc-go源码,开发人员可以更好地利用这个框架构建高效的分布式系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fananchong2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值