從零寫分佈式RPC框架 系列 2.0 (3)RPC-Server和RPC-Client模塊改造

2.0版本RPC-Server改動不大,主要變化在於RPC-Client使用了服務地址緩存,並引入監控機制,第一時間獲取zk集羣中服務地址信息變化並刷新本地緩存。另外,RPC-Client還使用了RpcClientProperties開放對負載均衡策略和序列化策略的選擇。

系列文章:

專欄:從零開始寫分佈式RPC框架

手寫通用類型負載均衡路由引擎(含隨機、輪詢、哈希等及其帶權形式)
實現 序列化引擎(支持 JDK默認、Hessian、Json、Protostuff、Xml、Avro、ProtocolBuffer、Thrift等序列化方式)
從零寫分佈式RPC框架 系列 2.0 (1)架構升級
從零寫分佈式RPC框架 系列 2.0 (2)RPC-Common模塊設計實現
從零寫分佈式RPC框架 系列 2.0 (3)RPC-Server和RPC-Client模塊改造

RPC-Server

1 結構圖

結構圖注意,RpcService註解移動到了RPC-Common模塊下,另外新加了ServiceInfo代表將存到註冊中心的服務信息(也在RPC-Common模塊下),其他的除了RpcServer基本沒有變化

2 RpcService註解

主要是多了weight和workerThreads,分別代表權重和最大工作線程數。

/**
 * @version V1.0
 * @author: lin_shen
 * @date: 2018/10/31
 * @Description:
 * RPC服務註解(標註在rpc服務實現類上)
 * 使用@Service註解使被@RpcService標註的類都能被Spring管理
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Service
public @interface RpcService {
    Class<?> value();
    int weight() default 1;
    int workerThreads() default 10;
}

3 ServiceInfo

/**
 * @version V1.0
 * @author: lin_shen
 * @date: 18-11-13
 * @Description: 服務信息,用於存儲到註冊中心
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceInfo implements WeightGetAble {

    private String host;
    private int port;
    /**
     * 權重信息
     */
    private int weight;
    /**
     * 最大工作線程數
     */
    private int workerThreads;

    public ServiceInfo (ServiceInfo serviceInfo){
        this.host = serviceInfo.host;
        this.port = serviceInfo.port;
        this.weight = serviceInfo.weight;
        this.workerThreads = serviceInfo.workerThreads;
    }

    @Override
    public int getWeightFactors() {
        return getWeight();
    }
}

4 RpcServer

RpcServer主要是多了對 serviceSemaphoreMap 和 serviceRpcServiceMap的管理。其中serviceSemaphoreMap 將作爲參數傳入RpcServerHandler提供限流信息,而serviceRpcServiceMap將註冊到ZK集羣。

/**
 * @version V1.0
 * @author: lin_shen
 * @date: 2018/10/31
 * @Description: TODO
 */
@Log4j2
@AutoConfigureAfter({ZKServiceRegistry.class})
@EnableConfigurationProperties(RpcServerProperties.class)
public class RpcServer implements ApplicationContextAware, InitializingBean {

    /**
     * 存放 服務名稱 與 服務實例 之間的映射關係
     */
    private Map<String,Object> handlerMap=new HashMap<>();

    /**
     * 存放 服務名稱 與 信號量 之間的映射關係
     * 用於限制每個服務的工作線程數
     */
    private Map<String, Semaphore> serviceSemaphoreMap=new HashMap<>();

    /**
     * 存放 服務名稱 與 服務信息 之間的映射關係
     */
    private Map<String, RpcService> serviceRpcServiceMap=new HashMap<>();

    @Autowired
    private RpcServerProperties rpcProperties;

    @Autowired
    private ZKServiceRegistry rpcServiceRegistry;

    /**
     * 在類初始化時執行,將所有被@RpcService標記的類納入管理
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        //獲取帶有@RpcService註解的類
        Map<String,Object> rpcServiceMap=applicationContext.getBeansWithAnnotation(RpcService.class);
        //以@RpcService註解的value的類的類名爲鍵將該標記類存入handlerMap和serviceSemaphoreMap
        if(!CollectionUtils.isEmpty(rpcServiceMap)){
            for(Object object:rpcServiceMap.values()){
                RpcService rpcService=object.getClass().getAnnotation(RpcService.class);
                String serviceName=rpcService.value().getName();
                handlerMap.put(serviceName,object);
                serviceSemaphoreMap.put(serviceName,new Semaphore(rpcService.workerThreads()));
                serviceRpcServiceMap.put(serviceName,rpcService);
            }
        }

    }


    /**
     * 在所有屬性值設置完成後執行,負責啓動RPC服務
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        //管理相關childGroup
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        //處理相關RPC請求
        EventLoopGroup childGroup=new NioEventLoopGroup();

        try {
            //啓動RPC服務
            ServerBootstrap bootstrap=new ServerBootstrap();
            bootstrap.group(bossGroup,childGroup);
            bootstrap.channel(NioServerSocketChannel.class);
            bootstrap.option(ChannelOption.SO_BACKLOG,1024)
                    .childOption(ChannelOption.TCP_NODELAY,true)
                    .handler(new LoggingHandler(LogLevel.INFO));
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel channel) throws Exception {
                    ChannelPipeline pipeline=channel.pipeline();
                    //解碼RPC請求
                    pipeline.addLast(new RemotingTransporterDecoder());
                    //編碼RPC請求
                    pipeline.addFirst(new RemotingTransporterEncoder());
                    //處理RPC請求
                    pipeline.addLast(new RpcServerHandler(handlerMap,serviceSemaphoreMap));
                }
            });
            //同步啓動,RPC服務器啓動完畢後才執行後續代碼
            ChannelFuture future=bootstrap.bind(rpcProperties.getPort()).sync();
            log.info("server started,listening on {}",rpcProperties.getPort());

            //啓動後註冊服務
            registry();

            //釋放資源
            future.channel().closeFuture().sync();
        }catch (Exception e){
            log.entry("server exception",e);
        }finally {
            //關閉RPC服務
            childGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

    }

    private void registry() throws UnknownHostException {
        //註冊RPC服務地址
        String hostAddress=InetAddress.getLocalHost().getHostAddress();
        int port=rpcProperties.getPort();

        for(String interfaceName:handlerMap.keySet()){
            ServiceInfo serviceInfo=
                    new ServiceInfo(hostAddress,port,serviceRpcServiceMap.get(interfaceName).weight(),serviceRpcServiceMap.get(interfaceName).workerThreads());
            String serviceInfoString= JSON.toJSONString(serviceInfo);
            rpcServiceRegistry.register(interfaceName,serviceInfoString);
            log.info("register service:{}=>{}",interfaceName,serviceInfoString);
        }
    }
}

RPC-Client

1 結構圖

RPC-Client
新增RpcClientProperties提供配置屬性讀入(路由策略和序列化方式),ZKServiceDiscovery增加ConcurrentMap<String,List> servicePathsMap來管理服務地址列表。RpcClient相應作出調整。

2 RpcClientProperties

注意這裏屬性用的是枚舉類型而不是字符串,另外默認路由策略是隨機,默認序列化策略是json

@Data
@ConfigurationProperties(prefix = "rpc.client")
public class RpcClientProperties {
    private RouteStrategyEnum routeStrategy= RouteStrategyEnum.Random;
    private SerializeTypeEnum serializeType=SerializeTypeEnum.JSON;
}

3 ZKServiceDiscovery

這裏使用了IZkChildListener 來對目標路徑下子節點變化進行監控,如果發生變化(新增或刪減)則重新執行discover方法拉取最新服務地址列表。
zkChildListenerMap的作用是管理服務和對應的服務地址列表監聽器,避免重複註冊監聽器。

/**
 * @version V1.0
 * @author: lin_shen
 * @date: 2018/10/31
 * @Description: zookeeper服務註冊中心
 */
@Component
@Log4j2
@EnableConfigurationProperties(ZKProperties.class)
public class ZKServiceDiscovery {

  @Autowired
  private ZKProperties zkProperties;

  /**
   * 服務名和服務地址列表的Map
   */
  private ConcurrentMap<String,List<String>> servicePathsMap=new ConcurrentHashMap<>();

  /**
   * 服務監聽器 Map,監聽子節點服務信息
   */
  private ConcurrentMap<String, IZkChildListener> zkChildListenerMap=new ConcurrentHashMap<>();

  private ZkClient zkClient;

  @PostConstruct
  public void init() {
    // 創建 ZooKeeper 客戶端
    zkClient = new ZkClient(zkProperties.getAddress(), zkProperties.getSessionTimeOut(), zkProperties.getConnectTimeOut());
    log.info("connect to zookeeper");
  }

  /**
   *
   * 根據服務名獲取服務地址並保持監控
   * @param serviceName
   * @return
   */
  public void discover(String serviceName){
    log.info("discovering:"+serviceName);
    String servicePath=zkProperties.getRegistryPath()+"/"+serviceName;
    //找不到對應服務
    if(!zkClient.exists(servicePath)){
      throw new RuntimeException("can not find any service node on path: "+servicePath);
    }
    //獲取服務地址列表
    List<String> addressList=zkClient.getChildren(servicePath);
    if(CollectionUtils.isEmpty(addressList)){
      throw new RuntimeException("can not find any address node on path: "+servicePath);
    }
    //保存地址列表
    List<String> paths=new ArrayList<>(addressList.size());
    for(String address:addressList){
      paths.add(zkClient.readData(servicePath+"/"+address));
    }
    servicePathsMap.put(serviceName,paths);
    //保持監控
    if(!zkChildListenerMap.containsKey(serviceName)){
      IZkChildListener iZkChildListener= (parentPath, currentChilds) -> {
        //當子節點列表變化時重新discover
        discover(serviceName);
        log.info("子節點列表發生變化 ");
      };
      zkClient.subscribeChildChanges(servicePath, iZkChildListener);
      zkChildListenerMap.put(serviceName,iZkChildListener);
    }
  }

  public List<String> getAddressList(String serviceName){
      List<String> addressList=servicePathsMap.get(serviceName);
      if(addressList==null||addressList.isEmpty()){
          discover(serviceName);
          return servicePathsMap.get(serviceName);
      }
      return addressList;
  }

}

4 RpcClient

主要是配合RemotingTransporter做了調整和升級,整體變化不大。
另外一個要注意的就是 ConcurrentMap<String,RouteStrategy> serviceRouteStrategyMap,用於在使用輪詢策略時,爲不同的服務調用保管對應的輪詢器(輪詢器內部存儲index記錄,是有狀態的)。

@Log4j2
@Component
@AutoConfigureAfter(ZKServiceDiscovery.class)
@EnableConfigurationProperties(RpcClientProperties.class)
public class RpcClient {

    @Autowired
    private ZKServiceDiscovery zkServiceDiscovery;

    @Autowired
    private RpcClientProperties rpcClientProperties;

    /**
     * 維持服務的 輪詢 路由狀態
     * 不同服務狀態不同(服務列表也不同)
     * 非輪詢無需維持狀態
     */
    private ConcurrentMap<String,RouteStrategy> serviceRouteStrategyMap=new ConcurrentHashMap<>();

    /**
     * 存放請求編號與響應對象的映射關係
     */
    private ConcurrentMap<Long, RemotingTransporter> remotingTransporterMap=new ConcurrentHashMap<>();

    @SuppressWarnings("unchecked")
    public <T> T create(final Class<?> interfaceClass){
        //創建動態代理對象
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                (proxy, method, args) -> {
                    //創建RPC請求對象
                    RpcRequest rpcRequest=new RpcRequest();
                    rpcRequest.setInterfaceName(method.getDeclaringClass().getName());
                    rpcRequest.setMethodName(method.getName());
                    rpcRequest.setParameterTypes(method.getParameterTypes());
                    rpcRequest.setParameters(args);
                    //獲取RPC服務信息列表
                    String serviceName=interfaceClass.getName();
                    List<String> addressList=zkServiceDiscovery.getAddressList(serviceName);

                    List<ServiceInfo> serviceInfoList=new ArrayList<>(addressList.size());
                    for(String serviceInfoString:addressList){
                        serviceInfoList.add(JSON.parseObject(serviceInfoString,ServiceInfo.class));
                    }
                    //根據配置文件獲取路由策略
                    log.info("使用負載均衡策略:"+rpcClientProperties.getRouteStrategy());
                    log.info("使用序列化策略:"+rpcClientProperties.getSerializeType());
                    RouteStrategy routeStrategy ;
                    //如果使用輪詢,則需要保存狀態(按服務名保存)
                    if(RouteStrategyEnum.Polling==rpcClientProperties.getRouteStrategy()){
                        routeStrategy=serviceRouteStrategyMap.getOrDefault(serviceName,RouteEngine.queryClusterStrategy(RouteStrategyEnum.Polling));
                        serviceRouteStrategyMap.put(serviceName,routeStrategy);
                    }else {
                        routeStrategy= RouteEngine.queryClusterStrategy(rpcClientProperties.getRouteStrategy());
                    }
                    //根據路由策略選取服務提供方
                    ServiceInfo serviceInfo = routeStrategy.select(serviceInfoList);

                    RemotingTransporter remotingTransporter=new RemotingTransporter();
                    //設置flag爲請求,雙路,非ping,非其他,序列化方式爲 配置文件中SerializeTypeEnum對應的code
                    remotingTransporter.setFlag(new RemotingTransporter.Flag(true,true,false,false,rpcClientProperties.getSerializeType().getCode()));

                    remotingTransporter.setBodyContent(rpcRequest);

                    log.info("get serviceInfo:"+serviceInfo);
                    //從RPC服務地址中解析主機名與端口號
                    //發送RPC請求
                    RpcResponse rpcResponse=send(remotingTransporter,serviceInfo.getHost(),serviceInfo.getPort());
                    //獲取響應結果
                    if(rpcResponse==null){
                        log.error("send request failure",new IllegalStateException("response is null"));
                        return null;
                    }
                    if(rpcResponse.getException()!=null){
                        log.error("response has exception",rpcResponse.getException());
                        return null;
                    }

                    return rpcResponse.getResult();
                }
        );
    }

    private RpcResponse send(RemotingTransporter remotingTransporter,String host,int port){
        log.info("send begin: "+host+":"+port);
        //客戶端線程爲1即可
        EventLoopGroup group=new NioEventLoopGroup(1);
        try {
            //創建RPC連接
            Bootstrap bootstrap=new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel channel) throws Exception {
                    ChannelPipeline pipeline=channel.pipeline();
                    pipeline.addLast(new RemotingTransporterDecoder())
                            .addFirst(new RemotingTransporterEncoder())
                            .addLast(new RpcClientHandler(remotingTransporterMap));
                }
            });
            ChannelFuture future=bootstrap.connect(host,port).sync();
            Channel channel=future.channel();
            log.info("invokeId: "+remotingTransporter.getInvokeId());
            //寫入RPC請求對象
            channel.writeAndFlush(remotingTransporter).sync();
            channel.closeFuture().sync();
            log.info("send end");
            //獲取RPC響應對象
            return (RpcResponse) remotingTransporterMap.get(remotingTransporter.getInvokeId()).getBodyContent();
        }catch (Exception e){
            log.error("client exception",e);
            return null;
        }finally {
            group.shutdownGracefully();
            //移除請求編號和響應對象直接的映射關係
            remotingTransporterMap.remove(remotingTransporter.getInvokeId());
        }

    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章