一、负载均衡
负载均衡(LB)在微服务架构演进中具有非常重要的意义,负载均衡是高可用网络基础架构的关键组件,我们的期望是调用是平均分配在所有的服务器服务器上的,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
1.1 负载均衡三种技术方案
目前市面上最常见的负载均衡技术方案主要有三种:
基于硬件负载均衡
硬件的负载均衡那就比较牛逼了,比如大名鼎鼎的 F5 Network Big-IP,也就是我们常说的 F5,它是一个网络设备,你可以简单的理解成类似于网络交换机的东西,完全通过硬件来抗压力,性能是非常的好,每秒能处理的请求数达到百万级,即 几百万/秒 的负载,当然价格也就非常非常贵了,十几万到上百万人民币都有。
因为这类设备一般用在大型互联网公司的流量入口最前端,以及政府、国企等不缺钱企业会去使用。一般的中小公司是不舍得用的。
采用 F5 这类硬件做负载均衡的话,主要就是省心省事,买一台就搞定,性能强大,一般的业务不在话下。而且在负载均衡的算法方面还支持很多灵活的策略,同时还具有一些防火墙等安全功能。但是缺点也很明显,一个字:贵。
软件负载均衡是指使用软件的方式来分发和均衡流量。软件负载均衡,分为7层协议 和 4层协议。
网络协议有七层,基于第四层传输层来做流量分发的方案称为4层负载均衡,例如 LVS,而基于第七层应用层来做流量分发的称为7层负载均衡,例如 Nginx。这两种在性能和灵活性上是有些区别的。
基于4层的负载均衡性能要高一些,一般能达到 几十万/秒 的处理量,而基于7层的负载均衡处理量一般只在 几万/秒 。
基于软件的负载均衡的特点也很明显,便宜。在正常的服务器上部署即可,无需额外采购,就是投入一点技术去优化优化即可,因此这种方式是互联网公司中用得最多的一种方式。
主要有以下几种
upstream apigateway {
server 127.0.0.1:8080 max_fails=1 fail_timeout=10s weight=100;
server localhost:8080 max_fails=1 fail_timeout=5s weight=20;
server in-prod-common-goserver-2 backup;
server in-prod-common-goserver-4 backup;
server in-prod-common-goserver-5 backup;
server in-prod-common-goserver-7 backup;
server in-prod-common-goserver-8 backup;
#server in-prod-common-goserver-2 max_fails=1 fail_timeout=2s weight=1;
#server in-prod-common-goserver-4 max_fails=1 fail_timeout=2s weight=1;
#server in-prod-common-goserver-5 max_fails=1 fail_timeout=2s weight=1;
#server in-prod-common-goserver-7 max_fails=1 fail_timeout=2s weight=1;
keepalive 1024; #保持连接
}
location / {
proxy_pass http://apigateway;
proxy_redirect default;
client_max_body_size 20M;
}
1.3 七层与四层负载均衡
所谓四层和七层负载均衡是按照网络层次OSI来划分的负载均衡类型(也可以按照其他的规则来分类,比如:应用的地理结构),简单来说:
7 层负载均衡有什么好处呢?
1.5 负载均衡算法
pick_first
pick_first每次都是尝试连接第一个地址,如果连接失败就会尝试下一个,直到连接成功为止,之后的RPC请求都会使用这个连接
round_robin
round_robin会对每个地址建立连接,之后的RPC请求会依次通过这些连接发送到后端
type rrPicker struct {
// subConns is the snapshot of the roundrobin balancer when this picker was
// created. The slice is immutable. Each Get() will do a round robin
// selection from it and return the selected SubConn.
subConns []balancer.SubConn
mu sync.Mutex
next int
}
func (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
p.mu.Lock()
sc := p.subConns[p.next]
p.next = (p.next + 1) % len(p.subConns)
p.mu.Unlock()
return balancer.PickResult{SubConn: sc}, nil
}
// lbPicker does two layers of picks:
//
// First layer: roundrobin on all servers in serverList, including drops and backends.
// - If it picks a drop, the RPC will fail as being dropped.
// - If it picks a backend, do a second layer pick to pick the real backend.
//
// Second layer: roundrobin on all READY backends.
//
// It’s guaranteed that len(serverList) > 0.
type lbPicker struct {
mu sync.Mutex
serverList []*lbpb.Server
serverListNext int
subConns []balancer.SubConn // The subConns that were READY when taking the snapshot.
subConnsNext int
stats *rpcStats
}
func newLBPicker(serverList []*lbpb.Server, readySCs []balancer.SubConn, stats *rpcStats) *lbPicker {
return &lbPicker{
serverList: serverList,
subConns: readySCs,
subConnsNext: grpcrand.Intn(len(readySCs)),
stats: stats,
}
}
func (p *lbPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
p.mu.Lock()
defer p.mu.Unlock()
// Layer one roundrobin on serverList.
s := p.serverList[p.serverListNext]
p.serverListNext = (p.serverListNext + 1) % len(p.serverList)
// If it’s a drop, return an error and fail the RPC.
if s.Drop {
p.stats.drop(s.LoadBalanceToken)
return balancer.PickResult{}, status.Errorf(codes.Unavailable, “request dropped by grpclb”)
}
// If not a drop but there’s no ready subConns.
if len(p.subConns) <= 0 {
return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
}
// Return the next ready subConn in the list, also collect rpc stats.
sc := p.subConns[p.subConnsNext]
p.subConnsNext = (p.subConnsNext + 1) % len(p.subConns)
done := func(info balancer.DoneInfo) {
if !info.BytesSent {
p.stats.failedToSend()
} else if info.BytesReceived {
p.stats.knownReceived()
}
}
return balancer.PickResult{SubConn: sc, Done: done}, nil
}
// rrPicker does roundrobin on subConns. It’s typically used when there’s no
// response from remote balancer, and grpclb falls back to the resolved
// backends.
//
// It guaranteed that len(subConns) > 0.
type rrPicker struct {
mu sync.Mutex
subConns []balancer.SubConn // The subConns that were READY when taking the snapshot.
subConnsNext int
}
func newRRPicker(readySCs []balancer.SubConn) *rrPicker {
return &rrPicker{
subConns: readySCs,
subConnsNext: grpcrand.Intn(len(readySCs)),
}
}
func (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
p.mu.Lock()
defer p.mu.Unlock()
sc := p.subConns[p.subConnsNext]
p.subConnsNext = (p.subConnsNext + 1) % len(p.subConns)
return balancer.PickResult{SubConn: sc}, nil
}
三、grpc自定义负载均衡
可以这样设置BalancingPolicy
四、扩展点
在 Kubernetes 中 gRPC 负载均衡有问题?
gRPC 的 RPC 协议是基于 HTTP/2 标准实现的,HTTP/2 的一大特性就是不需要像 HTTP/1.1 一样,每次发出请求都要重新建立一个新连接,而是会复用原有的连接。
所以这将导致 kube-proxy 只有在连接建立时才会做负载均衡,而在这之后的每一次 RPC 请求都会利用原本的连接,那么实际上后续的每一次的 RPC 请求都跑到了同一个地方。
注:使用 k8s service 做负载均衡的情况下
k8s负载均衡
userspace 模式
v1.0及之前版本的默认模式;
在 userspace 模式下,service 的请求会先从用户空间进入内核 iptables,然后再回到用户空间,
由 kube-proxy 完成后端 Endpoints 的选择和代理工作,这样流量从用户空间进出内核带来的性能损耗是不可接受的。
请求到达 iptables 时会进入内核,而 kube-proxy 监听是在用户态,
这样请求就形成了从用户态到内核态再返回到用户态的传递过程, 降低了服务性能。
因此,userspace 性能差。
iptables 模式
v1.1 版本中开始增加了 iptables mode,并在 v1.2 版本中正式取代 userspace 成为默认模式;
通过 Iptables 实现一个四层 TCP NAT ;
kube_proxy 只负责创建 iptables 的 nat 规则,不负责流量转发。
iptables 模式虽然克服了 userspace 那种 内核态–用户态 之间反复传递的缺陷,
但是在集群规模大的情况下,iptables rules 过多会导致性能显著下降。
因此,iptables 性能勉强适中 。
ipvs 模式
在 1.8 以上的版本中,kube-proxy 组件增加了 ipvs 模式;
ipvs 基于 NAT 实现,不创建反向代理, 也不创建 iptables 规则,通过 netlink 创建规则;
而 netlink 通过 hashtable 组织 service,其控制面和转发面的性能都是 O(1) 的,而且直接工作在内核态,因此在性能上比 userspace 和 iptables 都更优秀。
ingress-controller 是实现反向代理和负载均衡的程序,
通过监听 Ingress 这个 api 对象里的配置规则并转化成 Nginx 的配置 , 然后对外部提供服务
Ingress 对于上面提到的 “如何修改 Nginx 配置” 这个问题的解决方案是:
把 “修改 Nginx 配置各种域名对应哪个 Service ” 这些动作抽象为一个 Ingress 对象,
然后直接改 yml 创建/更新就行了,不用再修改 nginx 。
而 ingress-controller 通过与 k8s API 交互,动态感知集群中 Ingress 规则的变化并读取它,
然后按照模板生成一段 Nginx 配置,再写到 Nginx Pod 里,最后再 reload 一下生效。
大概的访问路径如下:
用户访问 --> LB --> ingress-nginx-service --> ingressController-ingress-nginx-pod --> ingress字段中调用的后端pod
注意后端 pod 的 service 只提供 pod 归类,
归类后 ingress 会将此 service 中的后端 pod 信息提取出来,
然后动态注入到 ingress-nginx-pod 中的 ingress 字段中,
如此,后端 pod 就能被调用了。
参考链接
https://github.com/grpc/grpc-go
https://www.bilibili.com/read/cv6653581
https://blog.csdn.net/y_xianjun/article/details/81327708
https://www.jianshu.com/p/9ce0e17f2941
https://zhuanlan.zhihu.com/p/32841479