19学习提升:gRPC源码中的那些优秀设计(上)

Posted 海风极客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了19学习提升:gRPC源码中的那些优秀设计(上)相关的知识,希望对你有一定的参考价值。

gRPC作为高性能的RPC框架,离不开它优雅的设计和编码,无论是作为一名底层开发者还是上层的业务开发者,能够写出一手好的代码一直都是决定自身水平高低的一个重要体现,如果想要达到一个较高层次的水平,离不开长时间的学习和训练以及不断的感悟,而一些优秀的开源软件和框架往往存在着很多优秀的设计供我们学习,下面我们就以贯穿整个专栏的grpc-go源码说起,来尽情感受下gRPC框架的优雅设计。

基本架构


(图片来自https://grpc.io/docs/what-is-grpc/introduction/)

其实上图中只描述了作为一个RPC框架的必备组成:RPC客户端、RPC客户端Stub、RPC服务端、RPC服务端Stub、通信协议,其实除此之外,gRPC的基本架构包括以下组件:

  1. 客户端:客户端是请求服务的应用程序。客户端发送请求并等待响应。客户端可以是任何支持gRPC协议的应用程序。
  2. 服务器:服务器是提供服务的应用程序。服务器等待客户端的请求并响应。服务器可以是任何支持gRPC协议的应用程序。
  3. 协议:gRPC使用HTTP/2作为传输协议。HTTP/2使得gRPC具有高效、低延迟的传输特性。
  4. 序列化/反序列化:gRPC使用Protocol Buffers作为序列化/反序列化技术。Protocol Buffers是一种高效、可扩展、语言无关的数据格式。
  5. 服务定义:gRPC使用Protocol Buffers作为服务定义语言。服务定义文件描述了服务的方法、参数和返回类型。
  6. 代码生成器:gRPC提供

Nacos源码系列—关于服务注册的那些事

简介

首先我们在看Nacos源码之前,要先想想为什么我们要读源码?是为了装杯?还是为了在心仪的女神面前给她娓娓道来展示自己的代码功底?当然不全是!

这都不是我们读源码的最终目的。作为一名技术人,上面的都是浮云,真正激励我们的应该是能够提升我们技术功底和整体技术大局观。此乃大道也!闲言少叙,接下来我们就来看一看,看源码究竟有什么好处

  • 提升技术功底: 当我们去看源码的时候,能够学习源码里面优秀的设计思想,还含有设计模式和并发编程技术,解决问题的思路,能够知其所以然
  • 新技术学习能力: 当我们看多了源码,对于一个新技术或者框架的掌握速度会有大幅度提升,能根据经验或官网资料快速掌握底层的实现,技术更新迭代也可以更快的入手
  • 快速解决问题能力: 遇到问题,尤其是框架源码的问题,能够更快速的定位
  • 面试获取更高成功率: 现在出去面试,一般中高级一点的,都会问到框架源码级别的实现,如果能够说出来,可以提升面试的成功率和薪资待遇,源码面试是区别程序员水平另一面镜子
  • 认识更多圈子: 多活跃开源社区,熟读源码后多思考,发现问题或需求主动参与开源技术研发,与圈内大牛成为为朋友

阅读源码的方法

  1. 搭建入门demo: 我们可以先看一下官网提供的文档,搭建Demo,快速掌握框架的基本使用
  2. 看核心代码: 对于初次看源码的同学,不要太过于关注源码的细枝末节,先把主要核心流程梳理出来,找到其入口,分析静态代码,如果遇到问题,可以进行断点调试。
  3. 绘图和笔记: 梳理好核心功能后,可以用流程图记录下来,好记性不如烂笔头,同时对关键的源码部分可以进行备注,分析参数的变化。同时要善于用Debug,来观看源码的执行过程
  4. 复习总结: 当我们把框架的所有功能点的源码都分析完成后,回到主流程在梳理一遍,最后在自己脑袋中形成一个闭环,这样源码的核心内容和主流程就基本上理解了。

Nacos核心功能点

服务注册: Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如IP地址,端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。

服务心跳: 在服务注册后,Nacos Client会维护一个定时心跳来支持通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。

服务健康检查: Nacos Server会开启一个定时任务用来检查注册服务实例的健康状况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性设置为false(客户端服务发现时不会发现)。如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)

服务发现: 服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存

服务同步: Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性

Nacos源码下载

首先我们需要将Nacos的源码下载下来,下载地址:https://github.com/alibaba/nacos

我们将源码下下来以后,导入到idea中

proto编译

当我们导入成功以后,会出现程序包com.alibaba.nacos.consistency.entity不存在的错误提示,这是因为Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化,需要先将proto文件编译为对应的Java代码。

最简单的 不安装任何的东西 idea2021.2已经捆绑安装了这个。

可以通过mvn copmpile来在target自动生成他们。

Nacos缺少Istio依赖问题解决

我们只需要在文件根目录下执行以下命令即可:

mvn clean package -Dmaven.test.skip=true -Dcheckstyle.skip=true

做完以上两步,我们就可以启动Nacos的了

启动Nacos

首先我们找到 nacos-console这个模块,这个就是我们的管理后台,找到它的启动类,因为Nacos默认为集群启动,所以我们要设置它为单机启动,方便演示

设置命令:

启动成功后,账号密码:nacos/nacos

到这里我们Nacos的源码启动就完成了。

开启源码

我们先从客户端服务的注册开始说起,我们可以先想一想如果Nacos客户端要注册,会把什么信息传递给服务器?
这里我们可以看到在 nacos-client下的NamingTest有这么一些信息


@Ignore
public class NamingTest 

    @Test
    public void testServiceList() throws Exception 

        //Nacos Server连接信息
        Properties properties = new Properties();
        //Nacos服务器地址
        properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
        //连接Nacos服务的用户名
        properties.put(PropertyKeyConst.USERNAME, "nacos");
        //连接Nacos服务的密码
        properties.put(PropertyKeyConst.PASSWORD, "nacos");

        //实例信息
        Instance instance = new Instance();
        //实例IP,提供给消费者进行通信的地址
        instance.setIp("1.1.1.1");
        //端口,提供给消费者访问的端口
        instance.setPort(800);
        //权重,当前实例的权限,浮点类型(默认1.0D)
        instance.setWeight(2);
        Map<String, String> map = new HashMap<String, String>();
        map.put("netType", "external");
        map.put("version", "2.0");
        instance.setMetadata(map);

        //关键代码 创建自己的实例
        NamingService namingService = NacosFactory.createNamingService(properties);
        namingService.registerInstance("nacos.test.1", instance);

        ThreadUtils.sleep(5000L);

        List<Instance> list = namingService.getAllInstances("nacos.test.1");

        System.out.println(list);

        ThreadUtils.sleep(30000L);
        //        ExpressionSelector expressionSelector = new ExpressionSelector();
        //        expressionSelector.setExpression("INSTANCE.metadata.registerSource = dubbo");
        //        ListView<String> serviceList = namingService.getServicesOfServer(1, 10, expressionSelector);

    

上面就是客户端注册的一个测试类,模仿了真实的服务注册到Nacos的过程,包括NacosServer连接、实例的创建、实例属性的赋值、注册实例,所以在这个其中包含了服务注册的核心代码,从这里我们可以大致看出,它包含了两个类的信息:Nacos Server连接信息和实例信息

Nacos Server连接信息:

从上述中我们可以看到有关于Nacos Server连接信息是存储在Properties中,

  • Server地址:Nacos服务器地址,属性的key为serverAddr;
  • 用户名:连接Nacos服务的用户名,属性key为username,默认值为nacos;
  • 密码:连接Nacos服务的密码,属性key为password,默认值为nacos;

实例信息:

从上述测试中我们可以看到注册实例信息用instance进行承载,而实例信息又分为两部分,一个是基础实例信息,一个是元数据信息

实例基础信息:

  • instanceId:实例的唯一ID;
  • ip:实例IP,提供给消费者进行通信的地址;
  • port: 端口,提供给消费者访问的端口;
  • weight:权重,当前实例的权限,浮点类型(默认1.0D);
  • healthy:健康状况,默认true;
  • enabled:实例是否准备好接收请求,默认true;
  • ephemeral:实例是否为瞬时的,默认为true;
  • clusterName:实例所属的集群名称;
  • serviceName:实例的服务信息;

元数据:

元数据类型为HashMap,从当前Demo我们能够看到的数据只有两个

  • netType:网络类型,这里设置的值为external(外网)
  • version Nacos版本,这里为2.0

除此之外,我们在Instance类中还可以看到一些默认信息,这些方法都是通过get方法进行提供的

  //心跳间隙的key,默认为5s,也就是默认5秒进行一次心跳
    public long getInstanceHeartBeatInterval() 
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
                Constants.DEFAULT_HEART_BEAT_INTERVAL);
    

    //心跳超时的key,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康;
    public long getInstanceHeartBeatTimeOut() 
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
                Constants.DEFAULT_HEART_BEAT_TIMEOUT);
    

    //实例IP被删除的key,默认为30s,也就是30秒收不到心跳,实例将会被移除;
    public long getIpDeleteTimeout() 
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
                Constants.DEFAULT_IP_DELETE_TIMEOUT);
    

    //实例ID生成器key,默认为simple;
    public String getInstanceIdGenerator() 
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
                Constants.DEFAULT_INSTANCE_ID_GENERATOR);
    

为什么要说这个呢?从这些参数中我们就可以了解到,我们服务的心跳间隙是多少以及超时时间,传递什么参数配置什么参数,以此来了解我们的实例是否健康。同时我们也可以看到一个比较关键且核心的类,是真正创建实例的类 ——NamingService

NamingService

NamingService是Nacos对外提供的一个统一的接口,当我们点进去查看,可以看到大概一下几个方法,这些方法提供了不同的重载方法,方便我们用于不同的场景。

//服务实例注册
void registerInstance(...) throws NacosException;

//服务实例注销
void deregisterInstance(...) throws NacosException;

//获取服务实例列表
List<Instance> getAllInstances(...) throws NacosException;

//查询健康服务实例
List<Instance> selectInstances(...) throws NacosException;

//查询集群中健康的服务实例
List<Instance> selectInstances(....List<String> clusters....)throws NacosException;

//使用负载均衡策略选择一个健康的服务实例
Instance selectOneHealthyInstance(...) throws NacosException;

//订阅服务事件
void subscribe(...) throws NacosException;

//取消订阅服务事件
void unsubscribe(...) throws NacosException;

//获取所有(或指定)服务名称
ListView<String> getServicesOfServer(...) throws NacosException;

//获取所有订阅的服务
List<ServiceInfo> getSubscribeServices() throws NacosException;

//获取Nacos服务的状态
String getServerStatus();

//主动关闭服务
void shutDown() throws NacosException;

NamingService的实例化是通过NacosFactory.createNamingService(properties);实现的,内部源码是通过反射来实现实例化过程

 NamingService namingService = NacosFactory.createNamingService(properties);

    public static NamingService createNamingService(Properties properties) throws NacosException 
        try 
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            return (NamingService) constructor.newInstance(properties);
         catch (Throwable e) 
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        
    

接下来我们就来看一看NamingService的具体实现

//调用registerInstance方法
namingService.registerInstance("nacos.test.1", instance);
 @Override
    public void registerInstance(String serviceName, Instance instance) throws NacosException 
        //默认的分组为“DEFAULT_GROUP” 
        registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
    
 @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException 
        //检查心跳时间是否正常
        NamingUtils.checkInstanceIsLegal(instance);
        //通过代理注册服务
        clientProxy.registerService(serviceName, groupName, instance);
    

心跳检测代码

   //心跳间隙超过限制 返回错误
        if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
                || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) 
            throw new NacosException(NacosException.INVALID_PARAM,
                    "Instance heart beat interval must less than heart beat timeout and ip delete timeout.");
        

通过代理注册服务,我们了解到clientProxy代理接口是通过NamingClientProxyDelegate来完成,我们可以在init构造方法中得出,具体的实例对象

  private void init(Properties properties) throws NacosException 
        //使用NamingClientProxyDelegate来完成
         this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
    

NamingClientProxyDelegate实现
NamingClientProxyDelegate中,真正调用注册服务的并不是代理实现类,而且先判断当前实例是否为瞬时对象后,来选择对应的客户端代理来进行请求。

  @Override
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException 
        getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
    

如果当前实力是瞬时对象,则采用gRPC协议(NamingGrpcClientProxy)进行请求,否则采用Http协议(NamingHttpClientProxy),默认为瞬时对象,在2.0版本中默认采用gRPC协议进行与Nacos服务进行交互

    //判断当前实例是否为瞬时对象
      private NamingClientProxy getExecuteClientProxy(Instance instance) 
        return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
    

NamingGrpcClientProxy中的实现

    @Override
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException 
        NAMING_LOGGER.info("[REGISTER-SERVICE]  registering service  with instance ", namespaceId, serviceName,
                instance);
        //数据的缓存
        redoService.cacheInstanceForRedo(serviceName, groupName, instance);
        //gRPC进行服务调用
        doRegisterService(serviceName, groupName, instance);
    

大体关系图如下所示:

Nacos客户端在项目的应用

  1. 我们想要让某一个服务注册到Nacos中,首先要引入一个依赖:
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
  2. 在依赖中,去查看SpringBoot自动装配文件自动装配文件META-INF/spring.factories

  1. 通过SpringBoot的自动装配来加载EnableAutoConfiguration对应的类,这里我们可以看到很多有关于Nacos相关的类,怎么知道哪个是我们真正需要关心的类,服务在注册的时候走的是哪个,一般自动装配,我们都会找到带有“Auto”关键字的文件进行查看,然后在结合我们需要找的,我们是客户端注册服务,所以我们大体可以定位到NacosServiceRegistryAutoConfiguration这个文件

  1. 查看NacosServiceRegistryAutoConfiguration源码,在这里我们只需要关注最核心的nacosAutoServiceRegistration方法


而我们真正关心的只有三个类NacosAutoServiceRegistration类是注册的核心,我们来看一下它的继承关系

    @Bean
    @ConditionalOnBean(AutoServiceRegistrationProperties.class)
    public NacosAutoServiceRegistration nacosAutoServiceRegistration(
            NacosServiceRegistry registry,
            AutoServiceRegistrationProperties autoServiceRegistrationProperties,
            NacosRegistration registration) 
          return new NacosAutoServiceRegistration(registry,
          autoServiceRegistrationProperties, registration);
    
  1. 从上述内容中我们可以知道,Nacos服务自动注册是从NacosServiceRegistryAutoConfiguration类开始的,并自动注册到NacosAutoServiceRegistration类中。

在下图中我们可以看到,主要是调用了super 方法,所以我们继续查看该类的构造方法:AbstractAutoServiceRegistration

public class NacosAutoServiceRegistration
        extends AbstractAutoServiceRegistration<Registration> 

      public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
          AutoServiceRegistrationProperties autoServiceRegistrationProperties,
          NacosRegistration registration) 
        super(serviceRegistry, autoServiceRegistrationProperties);
        this.registration = registration;
      

AbstractAutoServiceRegistration实现了ApplicationListener接口,用来监听Spring容器启动过程中WebServerInitializedEvent事件,一般如果我们实现这个类的时候,会实现一个方法onApplicationEvent(),这个方法会在我们项目启动的时候触发

    @Override
    @SuppressWarnings("deprecation")
    public void onApplicationEvent(WebServerInitializedEvent event) 
        bind(event);
    

由此我们可以看到bind里面的这个方法

    @Deprecated
    public void bind(WebServerInitializedEvent event) 
  //获取 ApplicationContext对象
        ApplicationContext context = event.getApplicationContext();
    //判断服务的 Namespace
        if (context instanceof ConfigurableWebServerApplicationContext) 
            if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) 
                return;
            
        
    //记录当前服务的端口
        this.port.compareAndSet(0, event.getWebServer().getPort());
    //【核心】启动注册流程
        this.start();
    

start()方法调用register();方法来注册服务

    public void start() 
        if (!isEnabled()) 
            if (logger.isDebugEnabled()) 
                logger.debug("Discovery Lifecycle disabled. Not starting");
            
            return;
        

        // only initialize if nonSecurePort is greater than 0 and it isnt already running
        // because of containerPortInitializer below
    //如果服务是没有运行状态时,进行初始化
        if (!this.running.get()) 
    //发布服务开始注册事件
            this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
      //【核心】注册服务
            register();
            if (shouldRegisterManagement()) 
                registerManagement();
            
      //发布注册完成事件
            this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
      //服务状态设置为运行状态
            this.running.compareAndSet(false, true);
        

    

NacosServiceRegistry.register()方法,如下所示:

@Override
    public void register(Registration registration) 
 //判断ServiceId是否为空
        if (StringUtils.isEmpty(registration.getServiceId())) 
            log.warn("No service to register for nacos client...");
            return;
        
 //获取Nacos的服务信息
        NamingService namingService = namingService();
  //获取服务ID和分组
        String serviceId = registration.getServiceId();
        String group = nacosDiscoveryProperties.getGroup();
    //构建instance实例(IP/Port/Weight/clusterName.....)
        Instance instance = getNacosInstanceFromRegistration(registration);

        try 
     //向服务端注册此服务
            namingService.registerInstance(serviceId, group, instance);
            log.info("nacos registry,   : register finished", group, serviceId,
                    instance.getIp(), instance.getPort());
        
        catch (Exception e) 
            if (nacosDiscoveryProperties.isFailFast()) 
                log.error("nacos registry,  register failed...,", serviceId,
                        registration.toString(), e);
                rethrowRuntimeException(e);
            
            else 
                log.warn("Failfast is false.  register failed...,", serviceId,
                        registration.toString(), e);
            
        
    

NacosNamingService.registerInstance()方法,如下:

    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException 
        //检查超时参数是否异常,心跳超时时间(15s)必须大于心跳间隙(5s)
        NamingUtils.checkInstanceIsLegal(instance);
        //拼接服务名,格式:groupName@@serviceName
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        //判断是否为临时实例,默认为true
        if (instance.isEphemeral()) 
            //临时实例,定时向Nacos服务发送心跳
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        
        //【核心】发送注册服务实例请求
        serverProxy.registerService(groupedServiceName, groupName, instance);
    

registerService中我们可以看到Nacos服务注册接口需要的完整参数

    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException 

        NAMING_LOGGER.info("[REGISTER-SERVICE]  registering service  with instance: ", namespaceId, serviceName,
                instance);

        final Map<String, String> params = new HashMap<String, String>(16);
        //环境
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        //服务名称
        params.put(CommonParams.SERVICE_NAME, serviceName);
        //分组名称
        params.put(CommonParams.GROUP_NAME, groupName);
        //集群名称
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        //当前实例IP
        params.put("ip", instance.getIp());
        //当前实例端口
        params.put("port", String.valueOf(instance.getPort()));
        //权重
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));

        reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

    

补充

在这里我们会发现我们请求实例接口的地址为/nacos/v1/ns/instance,其实这个在官网中也有提供对应的地址给我们,并且是对应的

客户端注册流程图

总结

以上就是Nacos的客户端注册流程,阅读源码并没有我们想象中的那么难,道阻且长,行之将至,当你开始行动的时候,你就已经开始进步了,别管学多少,如果您对文中有疑问或者问题,欢迎在下方留言,小农看见了会第一时间回复,大家加油~

以上是关于19学习提升:gRPC源码中的那些优秀设计(上)的主要内容,如果未能解决你的问题,请参考以下文章

Spring源码解读之核心容器上节

gRPC-go源码剖析与实战专栏介绍

牛笔了,谷歌大佬全新开源:Android常用框架源码解析,值得收藏!

挤时间学JDK源码

gRPC-go源码:连接管理

设计模式——JDK中的那些工厂方法