grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结

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

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第1张图片

使用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

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第2张图片

step2 创建Corefile配置文件

创建Corefile配置文件,加入我们自定义的区域配置(mygrpc.com)

mygrpc.com {
  file db.mygrpc.com
  loadbalance round_robin
}


. {
  forward . 114.114.114.114
  cache 30
  errors
  log
}

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第3张图片

使用./coredns 启动CoreDNS 注意Corefile配置文件和coredns在同一个文件夹下

step3 修改主机域名解析服务

修改主机的DNS域名服务器地址,以下为win11为例子
grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第4张图片

执行nslookup命令 查询grpc.mygrpc.com这个域名 (两次执行nslookup命令之间稍微等待以下,可以刷新下缓存)

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第5张图片

执行ping 命令也可以看到域名对应的ip 动态切换了

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第6张图片

到此 已经实现了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
grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第7张图片

main方法循环创建channel 访问接口

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第8张图片

注意需要关闭JVM的DNS缓存
java.security.Security.setProperty("networkaddress.cache.ttl", "0");

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第9张图片

使用DNS负载均衡需要注意

  • 1 DNS负载均衡不是每一个grpc channel可以轮询调用后端接口(http2使用长连接,连接创建后就会一直使用),而是集群环境下有多个客户端,每个客户端连接的是不同的后端服务
  • 2 DNS负载均衡适合有大量的客户端程序集群环境,每个客户端启动后会随机获取后端域名对应的IP实现创建连接
  • 3 DNS负载均衡类似不同的人访问网站会得到不同的IP入口

grpc 客户端负载均衡

客户端负载均衡的核心是: 客户端能发现所有的后端服务,并且自己实现负载均衡算法

客户端负载均衡需要自行实现服务发现这部分,负载均衡算法grpc已经提供了两个常用的round_robin  pick_first

本例子使用grpc-java 实现,项目使用maven构建 pom.xml文件中引入


	io.grpc
	grpc-all
	1.26.0

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 initAddressGroups() {
		/**
		 * 创建等效地址组
		 */
		List 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服务上了

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第10张图片

使用客户端负载均衡需要注意

  • 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)

运行测试客户端

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第11张图片

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
      containers:
          # 使用基于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;
         }
      
      }

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第12张图片

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
            containers:
                - name: grpc-server-containers
                  image: registry.cn-hangzhou.aliyuncs.com/jimliu/grpc-server:latest
                  imagePullPolicy: Always
                  ports: 
                      - containerPort: 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

部署服务端

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第13张图片

Step3 部署grpc客户端程序

grpc客户端程序也是一个springboot项目,创建channel的代码如下

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第14张图片

application.properties

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第15张图片

打包好项目后,创建镜像,并推送到阿里云私库

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第16张图片

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
            containers:
                - name: grpc-server-containers
                  image: registry.cn-hangzhou.aliyuncs.com/jimliu/grpc-client:latest
                  imagePullPolicy: Always
                  ports: 
                      - containerPort: 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 客户端项目

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第17张图片

Setp4 测试结果

浏览器访问接口

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第18张图片

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第19张图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkHlbC0X-1679823441159)(5.3-5.png)]

kubectl logs -f 查看三个服务端日志

grpc 负载均衡 ( DNS负载均衡,java客户端负载均衡,nginx反向代理负载均衡,k8s集群环境负载均衡 ) 学习总结_第20张图片

你可能感兴趣的:(docker+k8s,grpc,java,kubernetes,spring,boot,nginx,grpc)