grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结
大纲
- 1 DNS负载均衡
- 2 客户端负载均衡
- 3 反向代理负载均衡 (nginx)
- 4 k8s集群环境下处理方式
grpc是基于http2协议实现,所以可以有几种负载均衡的方式
grpc DNS负载均衡
DNS负载均衡的原理是使用 DNS轮询机制。一个域名配置多个IP地址,每次发起连接请求前,客户端主机请求域名解析服务,获取对应域名的IP。域名解析服务轮询输出域名对应的IP实现负载均衡。
注意:由于grpc是基于http2协议(长连接) 所以当创建连接的时候就已经选择好了后端的服务。所以测试的时候需要启动多个grpc客户端程序验证
本次测试使用K8S 首选DNS服务器CoreDNS
使用CoreDNS实现自定义的DNS服务器
使用CoreDNS来实现一个自定义的域名解析服务
关于CoreDNS的安装使用可以参考《k8s kubernetes 核心组件 CoreDNS 域名解析服务 学习总结》
step1 创建区域配置文件
例如我们创建一个 db.mygrpc.com 区域配置文件内容如下
@ IN SOA grpc.mygrpc.com. liuyijiang3430.qq.com. (
1000 ; serial number
1h ; refresh interval
10m ; retry interval
3w ; expiry period
1h ; negative TTL
)
grpc IN A 192.168.0.211
IN A 192.168.0.210
step2 创建Corefile配置文件
创建Corefile配置文件,加入我们自定义的区域配置(mygrpc.com)
mygrpc.com {
file db.mygrpc.com
loadbalance round_robin
}
. {
forward . 114.114.114.114
cache 30
errors
log
}
使用./coredns 启动CoreDNS 注意Corefile配置文件和coredns在同一个文件夹下
step3 修改主机域名解析服务
修改主机的DNS域名服务器地址,以下为win11为例子
执行nslookup命令 查询grpc.mygrpc.com这个域名 (两次执行nslookup命令之间稍微等待以下,可以刷新下缓存)
执行ping 命令也可以看到域名对应的ip 动态切换了
到此 已经实现了DNS域名的负载均衡
测试grpc DNS负载均衡客户端程序
实验前已经在192.168.0.210 192.168.0.211上启动了my-grpc-demo-server项目(此项目是一个grpc服务端程序)
客户端代码很简单 (代码见my-grpc-base-demo/Client.java)
- 1 创建ManagedChannel 和 SearchServiceGrpc.SearchServiceBlockingStub
- 2 循环执行查询方法,注意每次都会创建一个新的ManagedChannel
创建channel
main方法循环创建channel 访问接口
注意需要关闭JVM的DNS缓存
java.security.Security.setProperty("networkaddress.cache.ttl", "0");
使用DNS负载均衡需要注意
- 1 DNS负载均衡不是每一个grpc channel可以轮询调用后端接口(http2使用长连接,连接创建后就会一直使用),而是集群环境下有多个客户端,每个客户端连接的是不同的后端服务
- 2 DNS负载均衡适合有大量的客户端程序集群环境,每个客户端启动后会随机获取后端域名对应的IP实现创建连接
- 3 DNS负载均衡类似不同的人访问网站会得到不同的IP入口
grpc 客户端负载均衡
客户端负载均衡的核心是: 客户端能发现所有的后端服务,并且自己实现负载均衡算法。
客户端负载均衡需要自行实现服务发现这部分,负载均衡算法grpc已经提供了两个常用的round_robin pick_first
本例子使用grpc-java 实现,项目使用maven构建 pom.xml文件中引入
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.26.0</version>
</dependency>
grpc-java 实现客户端负载均衡流程
- 1 自定义两个类分别继承NameResolver , NameResolverProvider
- 2 重写NameResolver start方法 得到listener
- 3 给listener设置EquivalentAddressGroup列表 EquivalentAddressGroup就是后端服务的ip与端口
最简单版的代码实现
所有的复杂方式例如基于ectd zookeeper等都是基于以下最简单的代码实现的
实例代码:SimpleNameResolver.java
//自定义一个简单的名字解析器
public class SimpleNameResolver extends NameResolver {
/**
* 必须配置一个服务权限
*/
@Override
public String getServiceAuthority() {
return "authority:none";
}
@Override
public void shutdown() {
}
/**
* 创建等效地址组
* 这些地址就是会被负载均衡轮询的地址
* 数据可以从zookeeper nacos etcd coreDNS中获 这里写死两个地址127.0.0.1:55331 127.0.0.1:55332
*/
private List<EquivalentAddressGroup> initAddressGroups() {
/**
* 创建等效地址组
*/
List<EquivalentAddressGroup> addressGroups = new ArrayList<>();
/**
* 两个GRPC 服务端程序地址
*/
SocketAddress address1 = new InetSocketAddress("127.0.0.1",55331);
SocketAddress address2 = new InetSocketAddress("127.0.0.1",55332);
/**
* 创建等效地址组
*/
EquivalentAddressGroup group1 = new EquivalentAddressGroup(address1);
EquivalentAddressGroup group2 = new EquivalentAddressGroup(address2);
addressGroups.add(group1);
addressGroups.add(group2);
return addressGroups;
}
/**
* 核心是重写start(Listener2 listener) 方法 手动给 listener设置多个等效地址
* listener.onResult(rr);
*
* 以下代码是可以正常执行的
*/
@Override
public void start(Listener2 listener) {
System.out.println("================ start(Listener2 listener) ====================");
ConfigOrError configOrError = ConfigOrError.fromError(Status.NOT_FOUND);
ResolutionResult rr = ResolutionResult.newBuilder()
.setAddresses(initAddressGroups()) //传入效地址组 即会被负载轮询的地址
.setAttributes(Attributes.EMPTY)
.setServiceConfig(configOrError)
.build();
/**
* listener设置多个等效地址
*
* 这样grpc客户端就可以使用这些存在的GRPC服务端程序地址
*
* 在创建ManagedChannel channel时 指定负载均衡策略为轮询 就可以实现最简单的客户端负载均衡
* ManagedChannelBuilder.defaultLoadBalancingPolicy("round_robin")
*/
listener.onResult(rr);
}
}
SimpleNameResolver的作用就是获取所有的后端服务,这里还可以自行实现从zookeeper nacos etcd coreDNS中获取后端服务
实例代码:SimpleNameResolverProvider.java
//NameResolver提供者
public class SimpleNameResolverProvider extends NameResolverProvider {
/**
* 定义此NameResolverProvider 的DefaultScheme
*/
private static final String SCHEME = "simple";
/**
* 重写newNameResolver方法
* 此方法可以根据URI 即ManagedChannelBuilder.forTarget方法出入的参数
*
* 解析得到相关数据,做初始化功能,例如创建zookeepr nacos etcd等客户端
*
*/
@Override
public NameResolver newNameResolver(URI targetUri, Attributes params) {
if (!SCHEME.equals(targetUri.getScheme())) {
return null;
}
return new SimpleNameResolver();
}
@Override
protected boolean isAvailable() {
return true;
}
@Override
protected int priority() {
return 1;
}
@Override
public String getDefaultScheme() {
return SCHEME;
}
SimpleNameResolverProvider 的作用就是提供NameResolver
创建Channel代码如下
//远程连接管理器,管理连接的生命周期
private ManagedChannel channel;
private SearchServiceGrpc.SearchServiceBlockingStub blockingStub;
public ClientLoadbalancer() {
/**
* 测试简单负载均衡
* .forTarget("simple:///")
*/
NameResolverRegistry.getDefaultRegistry().register(new SimpleNameResolverProvider());
/**
* traget
* 通道不会直接调用与解析程序匹配的 URI。 而是创建一个匹配的解析程序
*/
channel = ManagedChannelBuilder
// 设置连接的目标地址
.forTarget("simple:///")
/**
* 设置轮询策略
*/
.defaultLoadBalancingPolicy("round_robin")
.usePlaintext()
.build();
//初始化远程服务Stub
blockingStub = SearchServiceGrpc.newBlockingStub(channel);
}
运行程序可以看到,客户端请求轮询到两个后端grpc服务上了
使用客户端负载均衡需要注意
- 1 需要自行开发服务发现的功能与负载策略(负载策略可以直接使用round_robin,此策略实现了服务端宕机后的剔除与重新连接)
- 2 需要自行开发服务注册的功能
- 3 需要自己处理服务端宕机后,客户端对服务端的剔除或者重新连接
反向代理负载均衡
个人觉得反向代理负载均衡是最适合grpc的一种负载均衡的方式
因为grpc使用的是http2协议,任何支持http2协议的反向代理都可以实现负载均衡。并且不需要客户端有额外的代码开发工作,客户端专注业务功能实现
可以使用nginx ,Envoy等实现grpc负载均衡
非k8s集群环境 grpc nginx负载均衡
安装nginx
nginx从1.13.10开始支持grpc 同时nginx安装时需要指定 http2模块 (–with-http_v2_module)
nginx 安装 注意需要开启http2模块 --with-http_v2_module
ubuntu 安装nginx
apt-get install openssl libssl-dev
apt-get install libpcre3-dev
./configure --prefix=/ops/openresty/openresty --with-http_v2_module
nginx 配置
upstream grpc-server{
server 192.168.0.210:55333;
server 192.168.0.211:55333;
}
server {
# 注意使用http2表示此server支持http2
listen 885 http2;
charset utf-8;
# 注意域名
server_name localhost;
# 注意使用grpc_xx
location / {
grpc_pass grpc://grpc-server;
grpc_set_header Host $host;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
grpc_set_header X-Real-IP $remote_addr;
}
}
客户端代码,使用ManagedChannelBuilder.forAddress 连接到nginx
//nginx服务ip
host= "192.168.0.160";
port = 885;
channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
//初始化远程服务Stub
blockingStub = SearchServiceGrpc.newBlockingStub(channel)
运行测试客户端
k8s集群环境下处理方式
其实k8s集群环境和普通的主机环境部署基本上思路是一致的。k8s只是对主机的一种虚拟化而已。由于grpc 使用http2协议实现,所以创建连接后会一直复用连接,k8s的service域名轮询访问就没有意义了
k8s集群环境下实现负载均衡还是可以使用
- 客户端负载均衡
- 反向代理负载均衡(反向代理需要部署在k8s集群内)
以下使用 nginx反向代理做grpc负载均衡测试
本次测试grpc客户端 服务端都部署在k8s集群内
Step1 安装nginx
关于k8s nginx的搭建可参考《k8s 部署nginx 实现集群统一配置,自动更新nginx.conf配置文件 总结》
注意事项
- 1 nginx部署使用hostname + subdomain + headless service 实现集群颞部域名访问
- 2 nginx的配置文件中server_name配置使用自定义的域名
- 3 upstream 中也使用Grpc服务端域名配置
k8s自定义Pod域名可以参考《k8s-Pod域名学习总结》
nginx部署yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: grcp-nginx
template:
metadata:
labels:
app: grcp-nginx
spec:
imagePullSecrets:
- name: myaliyunsecret
#注意使用hostname + subdomain 实现对nginx在集群内部的域名访问
hostname: nginx-host
subdomain: nginx-inner-domain
s:
# 使用基于nginx:1.23.3 为基础镜像制作的可热更新的nginx
- image: registry.cn-hangzhou.aliyuncs.com/jimliu/nginx-auto-reload:latest
name: grpc-nginx-containers
volumeMounts:
- mountPath: "/etc/nginx/conf.d/"
name: config-volume
volumes:
- name: config-volume
# 使用configmap实现统一配置nginx
configMap:
name: grcp-nginx-config
---
apiVersion: v1
kind: Service
metadata:
# # 注意name为 pod中 subdomain 的名称
name: nginx-inner-domain
spec:
selector:
app: grcp-nginx
clusterIP: None #注意 clusterIP 为None
nginx configmap yaml
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: grcp-nginx-config
data:
nginx.conf: |
# upstream使用 gpc服务端的域名访问
upstream grpc-server{
server grpc-server-host.grpc-server-inner-domain.default.svc.cluster.local:55333;
}
# 集群内部使用
server {
listen 1885 http2;
charset utf-8;
# 指定server_name 为nginx的域名
server_name nginx-host.nginx-inner-domain.default.svc.cluster.local;
location / {
grpc_pass grpc://grpc-server;
grpc_set_header Host $host;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
grpc_set_header X-Real-IP $remote_addr;
}
}
Step2 部署grpc服务端程序
注意事项
- 1 程序部署时需要指定hostname + subdomain 同时创建一个headless service 与nginx upstream配置一致即可
- 2 replicas指定3个 用于实现负载均衡
grpc服务端程序 deploy.yaml如下
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-server-deploy
spec:
replicas: 3 #部署三个grpc 服务端
selector:
matchLabels:
app: grpc-server
template:
metadata:
labels:
app: grpc-server
spec:
imagePullSecrets:
- name: myaliyunsecret
# hostname + subdomain 自定义Pod的域名
hostname: grpc-server-host
subdomain: grpc-server-inner-domain
s:
- name: grpc-server-containers
image: registry.cn-hangzhou.aliyuncs.com/jimliu/grpc-server:latest
imagePullPolicy: Always
ports:
-Port: 55333
protocol: TCP
name: http2
---
# 内部访问
apiVersion: v1
kind: Service
metadata:
name: grpc-server-inner-domain # 注意name为 pod中 subdomain 的名称
spec:
selector:
app: grpc-server
clusterIP: None #注意 clusterIP 为None
部署服务端
Step3 部署grpc客户端程序
grpc客户端程序也是一个springboot项目,创建channel的代码如下
application.properties
打包好项目后,创建镜像,并推送到阿里云私库
client端的部署yaml 如下
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-client-deploy
spec:
replicas: 1
selector:
matchLabels:
app: grpc-client
template:
metadata:
labels:
app: grpc-client
spec:
imagePullSecrets:
- name: myaliyunsecret
# hostname + subdomain 自定义Pod的域名
hostname: grpc-client-host
subdomain: grpc-client-inner-domain
s:
- name: grpc-server-containers
image: registry.cn-hangzhou.aliyuncs.com/jimliu/grpc-client:latest
imagePullPolicy: Always
ports:
-Port: 5522
protocol: TCP
name: http2
---
# 外部访问的接口
apiVersion: v1
kind: Service
metadata:
name: grpc-client-service
spec:
ports:
- protocol: TCP
port: 15522
targetPort: 5522
nodePort: 15522 #暴露一个集群外访问的端口
name: http
selector:
app: grpc-client
type: NodePort
部署grpc 客户端项目
Setp4 测试结果
浏览器访问接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkHlbC0X-1679823441159)(5.3-5.png)]
kubectl logs -f 查看三个服务端日志