其中Name、Namespace、Port分为Kubernetes中Service相应的Name、Namespace、Port
Transform:
K8s:
Name: transform-svc
Namespace: default
Port: 8081
其中Name、Namespace、Port分为Kubernetes中Service相应的Name、Namespace、Port
Transform:
K8s:
Name: transform-svc
Namespace: default
Port: 8081
能否详细讲解一下解决的什么问题?感谢!
此PR是为了实现基于go-zero的微服务能够和k8s Service进行互通。
我们业务有大量的python、Java服务,对外暴露gRPC接口,未使用注册中心,有相应的k8s Service资源。
k8s Service是一种标准的服务暴露方式,go-zero提供此种支持,应该会扩大其使用场景,方便和现有系统集成。
Transform:
Endpoints:
- transform-svc:8081
这样就可以了的
transform-svc:8081 这样使用,负载均衡有问题的,因为是长连接且依赖k8s的虚IP,当后端服务扩容时,新副本无法及时分配到新请求
这个情况你可以通过zrpc/proxy.go加个代理解决,这个实现是参考了哪里的做法,还是自己实现的?能否详细讲下实现原理?谢谢!
自己实现的哈,之前用Java实现过,上线运行了2年左右。
每个k8s Service都关联有唯一的Endpoints对象,保存了所有ready和not ready的Pod,deployment扩缩容时,会实时更新Endpoints下的地址列表,使用k8s的informer sdk 来watch我们感兴趣的Endpoints,再更新到gRPC,就是这样的。
好的好的,给我点时间仔细研读下代码哈,我的微信:kevwan,可以进一步沟通下哈
我倾向于改成:
Transform:
Endpoints:
- k8s:///transform-svc.ns:8081
的形式,你觉得呢?原有配置文件无需修改
可以可以,这样更简洁,我调整下
使用 headless service 基于 grpc dns resolve 做服务发现和 lb 是不是更好?
headless现在就支持的,但是在高并发情况下滚动更新时会有错误发生,之前我们就是用的这个,后来切到etcd的。
滚动更新时出错是啥原因?健康检查没配好吗?
这个 PR 的实现应该需要给 pod 都配上 RBAC 权限才能拿到 ep 资源。
仅需要ep的读权限即可,安全性可控。
DNS的延迟比较大,滚动更新后,可能导致一段时间拿不到最新的Pod IP,而旧的Pod已全部不可用
headless grpc dns resolver 30 分钟才会刷新, 新增的 pod 会有很长时间感知不到, 除非断开重连
下个版本支持合入这个PR.
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
}
}
}
你仔细看代码, 其实不是 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 秒刷新
@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 失败时轮询调用。 https://github.com/grpc/grpc-go/blob/7464f756aebd7057a912185e3df6d66790c6d589/resolver_conn_wrapper.go#L117
resetTransport: connect 和 reconnect 时。 https://github.com/grpc/grpc-go/blob/39a500abb9b14b9d9bedb07a8323810a46690c09/clientconn.go#L1134
grpclb_remote_balancer:远程 LB,使用 client LB 如 round_robin 的话跟这个无关。 https://github.com/grpc/grpc-go/blob/9dfe677337d567bdea82c6c11603127a7d83cdac/balancer/grpclb/grpclb_remote_balancer.go#L383
poll 这块已经是 resolver 通用逻辑了,并不是 dns-resolver 独有的逻辑。里面有个 t := time.NewTimer(ccr.cc.dopts.resolveNowBackoff(i))
,具体时间要看 Backoff 的逻辑:
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 = 120s
,backoff *= 1 + bc.Config.Jitter*(grpcrand.Float64()*2-1)
计算出的结果是 backoff * [0.8 ~ 1.2]
,最大值也是 144s。这还是失败多次后重试,backoff 指数增长的结果。retries 较小时,也就接近 dns-resolver 的最小限制 30s。
而且 bc.Config 是可以配置的,不存在这个频率控制不了。所以 30 min 怎么得出来了的?
resolver 失败时会调用的话, pod 增加的时候也不会触发这个错误啊, 也就是还是感知不到 pod 增加.
所以这个项目 https://github.com/letsencrypt/boulder/pull/5311 都用 MaxConnectionAge 强行触发错误了
@zcong1993 你是对的,扩容的情况 DNS 没法通知到 client。
我今天额外测试了下, kill 一个 pod, 等了30 分钟新的 pod 都没有请求, 所以我说的 30 分钟也不严谨, 可能与 dns 缓存啥的都有关系. 总之结论就是确实没法用
DNS服务如coredns本身有缓存,客户端操作系统也可能有缓存,编程语言本身也有自己的缓存策略,例如golang没有缓存,java有缓存等等。DNS是一种非常古老的协议,压根没有考虑服务发现场景下的优化。
本来想自己写的,没想到有大佬已经写了,代码拿走了,好人一生平安
这个 PR 还没合并吗?
1.2.0版本合并,在测试中
非常感谢 @shiquan1988 , #988 我提供了一个更简单的实现,这个关了哈。