[zeromicro/go-zero]支持客户端使用K8s Service进行服务发现

2024-03-08 361 views
7
配置方法

其中Name、Namespace、Port分为Kubernetes中Service相应的Name、Namespace、Port

Transform:
  K8s:
    Name: transform-svc
    Namespace: default
    Port: 8081

回答

4

能否详细讲解一下解决的什么问题?感谢!

7

此PR是为了实现基于go-zero的微服务能够和k8s Service进行互通。

我们业务有大量的python、Java服务,对外暴露gRPC接口,未使用注册中心,有相应的k8s Service资源。

k8s Service是一种标准的服务暴露方式,go-zero提供此种支持,应该会扩大其使用场景,方便和现有系统集成。

9
Transform:
  Endpoints:
    - transform-svc:8081

这样就可以了的

7

transform-svc:8081 这样使用,负载均衡有问题的,因为是长连接且依赖k8s的虚IP,当后端服务扩容时,新副本无法及时分配到新请求

2

这个情况你可以通过zrpc/proxy.go加个代理解决,这个实现是参考了哪里的做法,还是自己实现的?能否详细讲下实现原理?谢谢!

0

自己实现的哈,之前用Java实现过,上线运行了2年左右。

每个k8s Service都关联有唯一的Endpoints对象,保存了所有ready和not ready的Pod,deployment扩缩容时,会实时更新Endpoints下的地址列表,使用k8s的informer sdk 来watch我们感兴趣的Endpoints,再更新到gRPC,就是这样的。

9

好的好的,给我点时间仔细研读下代码哈,我的微信:kevwan,可以进一步沟通下哈

6

我倾向于改成:

Transform:
   Endpoints:
   - k8s:///transform-svc.ns:8081

的形式,你觉得呢?原有配置文件无需修改

2

可以可以,这样更简洁,我调整下

2

使用 headless service 基于 grpc dns resolve 做服务发现和 lb 是不是更好?

6

headless现在就支持的,但是在高并发情况下滚动更新时会有错误发生,之前我们就是用的这个,后来切到etcd的。

1

滚动更新时出错是啥原因?健康检查没配好吗?

这个 PR 的实现应该需要给 pod 都配上 RBAC 权限才能拿到 ep 资源。

0

仅需要ep的读权限即可,安全性可控。

DNS的延迟比较大,滚动更新后,可能导致一段时间拿不到最新的Pod IP,而旧的Pod已全部不可用

6

headless grpc dns resolver 30 分钟才会刷新, 新增的 pod 会有很长时间感知不到, 除非断开重连

1

下个版本支持合入这个PR.

3

grpc-go 源码中是 30 秒:

var (
    // To prevent excessive re-resolution, we enforce a rate limit on DNS
    // resolution requests.
    minDNSResRate = 30 * time.Second
)

func (d *dnsResolver) watcher() {
    defer d.wg.Done()
    for {
        // Sleep to prevent excessive re-resolutions. Incoming resolution requests
        // will be queued in d.rn.
        t := time.NewTimer(minDNSResRate)
        select {
        case <-t.C:
        case <-d.ctx.Done():
            t.Stop()
            return
        }
    }
}

https://github.com/grpc/grpc-go/blob/4a19753e9dfdf7c54c4b44ae419876e94ef3a0cc/internal/resolver/dns/dns_resolver.go#L78

4

你仔细看代码, 其实不是 30 秒刷一次, 那个是减少 d.rn 刷新频率的

// https://github.com/grpc/grpc-go/blob/v1.29.1/internal/resolver/dns/dns_resolver.go#L202-L207
func (d *dnsResolver) watcher() {
    defer d.wg.Done()
    for {
                // 这里还有个 select 卡着, d.rn 这个频率我们控制不了
        select {
        case <-d.ctx.Done():
            return
        case <-d.rn:
        }

        state, err := d.lookup()
        if err != nil {
            d.cc.ReportError(err)
        } else {
            d.cc.UpdateState(*state)
        }

        // Sleep to prevent excessive re-resolutions. Incoming resolution requests
        // will be queued in d.rn.
        t := time.NewTimer(minDNSResRate)
        select {
        case <-t.C:
        case <-d.ctx.Done():
            t.Stop()
            return
        }
    }
}

具体可以参考这两个 issue https://github.com/grpc/grpc/issues/12295 https://github.com/letsencrypt/boulder/issues/5307

我是测试过的, 肯定不是 30 秒刷新

2

@zcong1993 我们来读下 grpc-go 的代码吧。

// ResolveNow invoke an immediate resolution of the target that this dnsResolver watches.
func (d *dnsResolver) ResolveNow(resolver.ResolveNowOptions) {
    select {
    case d.rn <- struct{}{}:
    default:
    }
}

d.rn 由 ResolveNow 写入,ResolveNow 被包装了几次,被很多地方调用:

poll 这块已经是 resolver 通用逻辑了,并不是 dns-resolver 独有的逻辑。里面有个 t := time.NewTimer(ccr.cc.dopts.resolveNowBackoff(i)),具体时间要看 Backoff 的逻辑:

https://github.com/grpc/grpc-go/blob/31911ed09e900af678b113c261622851108f5f23/internal/backoff/backoff.go#L68

func (bc Exponential) Backoff(retries int) time.Duration {
    if retries == 0 {
        return bc.Config.BaseDelay
    }
    backoff, max := float64(bc.Config.BaseDelay), float64(bc.Config.MaxDelay)
    for backoff < max && retries > 0 {
        backoff *= bc.Config.Multiplier
        retries--
    }
    if backoff > max {
        backoff = max
    }
    // Randomize backoff delays so that if a cluster of requests start at
    // the same time, they won't operate in lockstep.
    backoff *= 1 + bc.Config.Jitter*(grpcrand.Float64()*2-1)
    if backoff < 0 {
        return 0
    }
    return time.Duration(backoff)
}
var DefaultConfig = Config{
    BaseDelay:  1.0 * time.Second,
    Multiplier: 1.6,
    Jitter:     0.2,
    MaxDelay:   120 * time.Second,
}

默认值 max = 120sbackoff *= 1 + bc.Config.Jitter*(grpcrand.Float64()*2-1) 计算出的结果是 backoff * [0.8 ~ 1.2],最大值也是 144s。这还是失败多次后重试,backoff 指数增长的结果。retries 较小时,也就接近 dns-resolver 的最小限制 30s。

而且 bc.Config 是可以配置的,不存在这个频率控制不了。所以 30 min 怎么得出来了的?

3

resolver 失败时会调用的话, pod 增加的时候也不会触发这个错误啊, 也就是还是感知不到 pod 增加.

所以这个项目 https://github.com/letsencrypt/boulder/pull/5311 都用 MaxConnectionAge 强行触发错误了

9

@zcong1993 你是对的,扩容的情况 DNS 没法通知到 client。

8

我今天额外测试了下, kill 一个 pod, 等了30 分钟新的 pod 都没有请求, 所以我说的 30 分钟也不严谨, 可能与 dns 缓存啥的都有关系. 总之结论就是确实没法用

5

DNS服务如coredns本身有缓存,客户端操作系统也可能有缓存,编程语言本身也有自己的缓存策略,例如golang没有缓存,java有缓存等等。DNS是一种非常古老的协议,压根没有考虑服务发现场景下的优化。

7

本来想自己写的,没想到有大佬已经写了,代码拿走了,好人一生平安

1

这个 PR 还没合并吗?

4

1.2.0版本合并,在测试中

9

非常感谢 @shiquan1988 , #988 我提供了一个更简单的实现,这个关了哈。