文章目录
  1. 1. 什么是服务注册发现?
  2. 2. 有了服务注册发现机制之后,如何进行RPC调用?
  3. 3. 实战:为gRPC添加服务注册发现
  4. 4. 实战:为服务提供方添加服务注册
    1. 4.1. 添加服务注册逻辑
  5. 5. 实战:为服务消费者提供服务发现
    1. 5.1. 发起服务调用
  6. 6. 运行
    1. 6.1. 附录:docker方式安装Nacos-Server
  7. 7. 小结

本文案例代码地址 : https://github.com/TaXueWWL/grpc-demo

有了前面的铺垫,我们已经对gRPC的java实现机制,代码编写手法、阻塞RPC以及双向流等内容有了全面、直观地了解。

本文我们继续本系列,为我们的gRPC添加服务注册发现。

什么是服务注册发现?

在RPC调用流程中,服务调用方需要知道服务提供方的地址和端口,才能发起RPC调用。

如果是直连式调用,则服务提供方需要提前配置服务提供方的地址和端口,也就是大白话说的 写死

这种硬编码配置方式应对变化的能力很差,如果服务提供方宕机,服务消费者无法及时更换调用的目标,即便服务提供方存在冗余的机器,消费者也需要修改配置文件,重启服务才能调用至新的服务提供方节点。

通俗地说就是,这种方式将服务提供方与服务消费方耦合在了一起,不够灵活。

因此就需要有服务注册发现机制。如下图所示:

registry.png

这里引用了dubbo框架的简易架构图。图中,服务提供方(provider)启动后会向注册中心(Registry)发起服务注册,将自己的ip、端口、其他元数据信息发送给注册中心。

注册中心维护了一个注册表,对上报的服务注册信息进行记录。

服务消费者(consumer)启动后会向注册中心(Registry)拉取服务提供方列表,也就是图中的 subscribe ,即:服务发现过程。

注意看,3.notify 是一条虚线,这里的含义是指,一旦服务提供方的注册信息发生变更,如现有节点下线(有可能是正常的关机,如版本发布;也有可能是意外宕机,都会导致服务下线。)或者新节点上线,都会造成注册中心中记录的服务注册信息发生变更,此时注册中心会通知服务消费者存在注册表信息变更,此时需要对最新的服务注册信息进行变更,一般有几种方式:

  1. 注册中心通过push方式主动推送给消费者,这种方式往往通过消费者向注册中心注册监听器方式实现;
  2. 消费者定时通过pull方式从注册中心拉取注册表信息并在本地进行更新;
  3. 消费者通过长轮询方式从注册中心拉取注册表信息(推拉结合)。

有了服务注册发现机制之后,如何进行RPC调用?

那么,有了服务注册发现机制之后的RPC调用过程是怎样的?

select-provider.png

如上图,实际上有了服务注册发现机制之后,服务消费者就不需要事先硬编码服务提供方的机器列表。

而是在运行时选择一台机器进行调用,这就是所谓的负载均衡策略,一般来说负载均衡有随机、轮询、加权随机、一致性哈希等方式。

图中使用的为轮询策略,则先选择192.168.21.1,下次选择192.168.21.2,然后重复这个过程。

如果服务提供方中某台机器下线,如192.168.21.1下线,则服务A的消费者能够感知到这个过程,拉取到全新的注册表信息后,下次调用就不会再去调用已下线的机器。

select-provider-2.png

图中,提供者1离线,消费者拉取到最新的注册表信息,选择了健康状态的提供者2,发起RPC调用。

毫不夸张地说,服务注册发现机制为RPC调用提供了运行时自愈的能力。

实战:为gRPC添加服务注册发现

有了前面的铺垫,那么我们就通过实战案例来讲解,如何为gRPC添加服务注册发现。

本文使用Nacos作为服务注册中心选型。

事实上,除了Nacos外,Zookeeper、Etcd、Redis等都可以作为服务注册中心。

之所以选择了Nacos,一方面是因为它足够成熟,Nacos起源于淘宝五彩石项目,支撑了双十一的海量流量;

成长于阿里云的ACM,开源后作为SpringCloudAlibaba默认的服务注册与配置中心,且它与CloudNative深度整合,是面向未来的一款工业级产品。

Nacos具备高可用能力,且拥有不错的可视化能力,因此我们选择它作为服务注册中心的选型。

实战:为服务提供方添加服务注册

首先为服务提供方添加服务注册能力。

定义服务注册配置类NacosRegistryConfig.java,封装服务提供方的ip、端口、服务名等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class NacosRegistryConfig {
private static final Logger logger = Logger.getLogger(NacosRegistryConfig.class.getName());
/**Nacos服务单地址*/
private String serverAddr;
/**注册端口,一般就是服务暴露端口*/
private int port;
/**权重*/
private double weight = 1.0;
/**服务名*/
private String serviceName;
/**当前服务ip*/
private String ip;

通过有参构造方法,在配置类创建期间获取到当前服务所处网络ip。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public NacosRegistryConfig(String serverAddr, int port, double weight, String serviceName) {
this.serverAddr = serverAddr;
this.port = port;
this.weight = weight;
this.serviceName = serviceName;
try {
InetAddress inetAddress = InetAddress.getLocalHost();
this.ip = inetAddress.getHostAddress();
} catch (UnknownHostException e) {
throw new RuntimeException("NacosRegistryConfig.getLocalHost failed.", e);
}
logger.info("NacosRegistryConfig construct done. serverAddr=[" + serverAddr +
"],serviceName=" + serviceName +
"],ip=[" + ip +
"],port=[" + port +
"],weight=[" + weight + "]");
}

编写服务注册核心方法,用于在服务提供方启动时向Nacos注册服务信息。注意:服务注册一定要在服务提供方服务发布之前!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public void register() {
try {
NamingService namingService = NamingFactory.createNamingService(serverAddr);
// 创建一个服务实例
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setHealthy(false);
instance.setWeight(weight);
instance.setInstanceId(serviceName + "-instance");
// 自定义服务元数据
Map<String, String> instanceMeta = new HashMap<>();
instanceMeta.put("language", "java");
instanceMeta.put("rpc-framework", "gRPC");
instance.setMetadata(instanceMeta);
// 声明一个集群
Cluster cluster = new Cluster();
cluster.setName("DEFAULT-CLUSTER");
// 为集群添加元数据
Map<String, String> clusterMeta = new HashMap<>();
clusterMeta.put("name", cluster.getName());
cluster.setMetadata(clusterMeta);
// 为实例添加集群名称
instance.setClusterName("DEFAULT-CLUSTER");
// 注册服务实例
namingService.registerInstance(serviceName, instance);
namingService.subscribe(serviceName, new EventListener() {
@Override
public void onEvent(Event event) {
System.out.println(((NamingEvent)event).getServiceName());
System.out.println(((NamingEvent)event).getInstances());
}
});
} catch (NacosException e) {
throw new RuntimeException("Register Services To Nacos Failed.", e);
}
}
}

这个方法通过声明并创建NamingService,并为其添加了实例信息,设置了ip、端口、元信息、集群名称等属性,并通过namingService.registerInstance 完成服务的注册操作。

重点看一下registerInstance方法实现。

方法签名:

1
2
3
4
5
6
7
8
/**
* register a instance to service with specified instance properties
*
* @param serviceName name of service
* @param instance instance to register
* @throws NacosException
*/
void registerInstance(String serviceName, Instance instance) throws NacosException;

接口方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
if (instance.isEphemeral()) {
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
long instanceInterval = instance.getInstanceHeartBeatInterval();
beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}

可以看到,Nacos实际上是将服务注册信息封装到了BeatInfo对象中,在启动阶段,服务提供方主动上报服务注册信息。

在运行时,服务提供方通过心跳与Nacos服务端进行通信,并通过心跳来传递服务注册信息。一旦客户端发生变更,服务端可以在下次心跳感知到该变更,并做相应地修改。

这个设计是比较巧妙的。

添加服务注册逻辑

接着我们需要在服务提供者启动逻辑中,添加服务注册逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OrderServerBoot {
private static final Logger logger = Logger.getLogger(OrderServerBoot.class.getName());
private Server server;
// Nacos服务端地址
private static final String NACOS_SERVER_ADDR = "nacos-server:8848";
@SneakyThrows
private void startServer() {
int serverPort = 10881;
server = ServerBuilder.forPort(serverPort)
.addService(new OrderServiceImpl())
.addService(new DoubleStreamServiceImpl())
.build();
// 服务注册
NacosRegistryConfig nacosRegistryConfig = new NacosRegistryConfig(NACOS_SERVER_ADDR, serverPort, 1.0, "grpc-server-demo");
nacosRegistryConfig.register();
server.start();
logger.info("OrderServerBoot started, listening on:" + serverPort);
// 优雅停机
addGracefulShowdownHook();
}

重点看如下代码:

// 服务注册
NacosRegistryConfig nacosRegistryConfig = new NacosRegistryConfig(NACOS_SERVER_ADDR, serverPort, 1.0, "grpc-server-demo");
nacosRegistryConfig.register();       

在gRPC服务提供方的startServer方法中,通过该方法调用,向Nacos注册了服务提供者的信息(serverName为grpc-server-demo,该名称将唯一标识一个服务),然后再执行服务端启动逻辑,发布服务。

运行服务提供方的启动方法,观察Nacos管理平台的服务注册信息如下:

1.png

2.png

从管理平台能够看到我们的服务已经成功注册到了Nacos中,并且成功上报了元信息。

实战:为服务消费者提供服务发现

继续给服务消费者添加服务发现能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NacosRegistryConfig {
private static final Logger logger = Logger.getLogger(NacosRegistryConfig.class.getName());
/**Nacos服务单地址*/
private String serverAddr;
/**服务名*/
private String providerServiceName;
/**提供者ip*/
private String providerIp;
/**提供者端口*/
private int providerPort;
public NacosRegistryConfig(String serverAddr, String providerServiceName) {
this.serverAddr = serverAddr;
this.providerServiceName = providerServiceName;
// 服务发现
findServerList();
}

与服务提供者类似,消费者也有一个NacosRegistryConfig服务发现配置类。

在构造方法中,构造注入Nacos服务端地址、服务提供方的服务名称,即上文中服务提供方注册到Nacos的 grpc-server-demo

构造方法中,完成服务发现,调用方法 findServerList()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void findServerList() {
try {
// 连接Nacos-server
NamingService namingService = NamingFactory.createNamingService(serverAddr);
// 获取服务提供者实例信息
List<Instance> instances = namingService.getAllInstances(providerServiceName);
// 随机策略 TODO 改为基于取模粘滞请求,基于userId取模
int serverSize = instances.size();
Random random = new Random();
int index = random.nextInt(serverSize);
System.out.println("serverSize:" + serverSize + "选择的机器:" + index);
Instance instance = instances.get(index);
// 获取ip 端口
this.providerIp = instance.getIp();
this.providerPort = instance.getPort();
// TODO还需要考虑对服务列表变更的处理
namingService.subscribe(providerServiceName, new EventListener() {
@Override
public void onEvent(Event event) {
System.out.println(((NamingEvent)event).getServiceName());
System.out.println(((NamingEvent)event).getInstances());
}
});
} catch (NacosException e) {
throw new RuntimeException("Register Services To Nacos Failed.", e);
}
}

解释下逻辑:

  • 首先建立到Nacos服务端的链接
  • 构造NamingService实例,通过NamingService.getAllInstances(String serviceName) 方法获取到服务端所有以serviceName命名的实例列表(serviceName为服务端注册的属性)
  • 这里使用随机策略从实例中随机获取一台服务端实例ip及端口(均为提供方注册好的)
  • 添加一个服务变更监听

当逻辑执行完成,便会为NacosRegistryConfig的providerIp、providerPort赋值。

提供get方法以方便外部快速获取到提供者的ip与端口。

1
2
3
4
5
6
7
public String getProviderIp() {
return providerIp;
}
public int getProviderPort() {
return providerPort;
}

发起服务调用

1
2
3
4
// 端口及ip
int port = nacosRegistryConfig.getProviderPort();
String providerIp = nacosRegistryConfig.getProviderIp();
OrderClientAgent orderClientAgent = new OrderClientAgent(providerIp, port);

接着只需要替换原先的获取服务提供方ip、端口的逻辑,改为从nacosRegistryConfig中获取,其余逻辑保持不变即可。

运行

先后启动服务提供方、服务消费方,查看控制台日志输出。

提供方

1
2
3
4
三月 21, 2022 10:57:50 上午 OrderServerBoot startServer
信息: OrderServerBoot started, listening on:10881
三月 21, 2022 10:57:50 上午 registry.NacosRegistryConfig <init>
信息: NacosRegistryConfig construct done. serverAddr=[nacos-server:8848],serviceName=grpc-server-demo],ip=[169.254.19.253],port=[10881],weight=[1.0]

消费方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
三月 21, 2022 10:58:30 上午 OrderClientBoot doPlaceOrder
信息: client placeOrder end. response:userId: 10086
,resultCode:SUCCESS
三月 21, 2022 10:58:30 上午 agent.OrderClientAgent queryOrders
信息: client queryOrders start. request:userId: 10086
三月 21, 2022 10:58:30 上午 OrderClientBoot doQueryOrder
信息: client queryOrders end. response:userId: 10086
totalPrice: "207.5000"
userOrder {
orderId: 2095135383
orderPrice: "12.50"
orderAmount: "15.00"
productId: 1
}
userOrder {
orderId: 2095135383
orderPrice: "10.00"
orderAmount: "2.00"
productId: 2
}
......

可以看到,我们仍旧能够成功发起服务调用。

这里提个问题,假设我们部署多个服务提供方,不修改消费者逻辑,是否仍然能够成功发起RPC调用?

答案是肯定的。由于服务注册发现的存在,消费者能够及时获取到提供方的服务变更信息,在运行期能够根据我们指定的策略,选择健康的提供者实例并发起RPC调用。

感兴趣的读者可以自行实验,本文的代码已上传github,地址:https://github.com/TaXueWWL/grpc-demo。读者需要自己安装nacos实例。

如果有linux环境且安装了docker,则可以通过docker方式启动一个Nacos实例。

附录:docker方式安装Nacos-Server

以下操作,笔者是在centos7实现的。

安装docker

1
yum -y install docker

设置开机启动

1
systemctl enable docker

启动docker

1
systemctl start docker

查看docker当前版本

1
docker version

接着安装Nacos-Server

Nacos-server镜像地址:

1
https://hub.docker.com/r/nacos/nacos-server

docker方式安装并启动Nacos-server(版本1.1.4,具体版本可以自行指定)

1
docker run --name nacos -e MODE=standalone -p 8848:8848 -d nacos/nacos-server:1.1.4

访问Nacos-server:

1
2
3
http://ip:8848/nacos/
默认用户名:nacos
默认密码:nacos

小结

本文重点介绍了服务注册发现的作用及意义,并实战了如何为gRPC添加了服务注册发现能力。

在后面的文章中,我们将继续探究gRPC的深层原理,并且会对Nacos的服务注册发现、配置管理等内容进行源码级别的学习与研究,敬请期待。

本文案例代码地址:https://github.com/TaXueWWL/grpc-demo



版权声明:

原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。

文章目录
  1. 1. 什么是服务注册发现?
  2. 2. 有了服务注册发现机制之后,如何进行RPC调用?
  3. 3. 实战:为gRPC添加服务注册发现
  4. 4. 实战:为服务提供方添加服务注册
    1. 4.1. 添加服务注册逻辑
  5. 5. 实战:为服务消费者提供服务发现
    1. 5.1. 发起服务调用
  6. 6. 运行
    1. 6.1. 附录:docker方式安装Nacos-Server
  7. 7. 小结
Fork me on GitHub