2023-07-29
原文作者:说好不能打脸 原文地址:https://yinwj.blog.csdn.net/article/details/51577512

4、Broker Server设计

那么如何赋予ESB中间件原子服务整合、服务路由编排的关键能力呢?读者当然可以自行实现ESB中间件中这两个关键模块的功能,但问题的关键是即使是一个团队负责这部分的开发工作也不可能在短时间内完成该模块的开发,而且功能完整度和模块性能也不一定能达到设计之初的要求。所以在我们自行设计的ESB中间件中,选择将原子服务整合功能和服务路由编排功能交给我们已经介绍过的Apache Camel组件。

另一方面,为了保证ESB中间件不会成为整个软件顶层设计中的性能瓶颈,ESB中间件将采用多运行节点的方式为各个业务系统服务(就是前文中提到的ESB-Broker Server节点)。在进行设计时,我们还必须保证业务系统注册的原子服务发生变化后,或者从流程编排工具/流程发布工具传来新的编排好的路由服务注册请求后,ESB-Broker Server节点 可以在不停止工作的情况下 响应这些变化。另外,ESB-Broker Server还需要具备动态伸缩能力:

换句话说,我们需要运行多个Apache Camel应用服务,并且能够随时启动一些新的或者停止一些在运行的应用服务。这些Apache Camel应用服务在启动时甚至可能没有加载任何的Service,而是在运行过程中根据某种事件通知对各种Camel的Service进行动态加载(包括但不限于Route、Processor)。

4-1、ESB-Broker Server设计

4-1-1、最直观的设计

那么问题来了,我们如何进行ESB-Broker Server节点的设计呢?读者可能首先想到的就是:在流程编排工具/流程发布工具(以下称为Studio)上进行Apache Camel工程的开发,然后人为的将Apache Camel工程打包发布到ESB-Broker Server节点上。这种“最直观”的处理方法还有一种改进方式:在每一个将可以承载ESB-Broker Server的节点上安装Docker Engine,并采用Swarm进行统一管理。这样就可以实现ESB-Broker Server节点的自动化管理了。如下图所示:

202307292201163131.png

Docker Swarm是Dotcloud公司(这个公司也是Docker的开发商)官方提供的Docker集群管理工具,通过这个工具管理人员可以轻松管理远程计算机上的多个Docker容器实例,包括Docker创建、启动、停止等操作。Docker Swarm还提供基于命令行的API接口,以便于其它程序能够通过它间接管理Docker集群。所以,我们可以用这个技术方案基于Studio进行Docker集群的管理,以达到自动管理ESB-Broker Server节点的目的。本章节不进行知识点的扩散讲解,不进行Docker和Docker Swarm的详细介绍。

但是以上的设计方案,包括对其的改进方案(我们暂且称为“第一方案”)还不能完全满足我们对ESB-Broker Server的设计要求:显然“第一方案”能够实现ESB-Broker Server节点的横向扩容,我们可以在整个ESB中间件服务快要达到性能瓶颈时立即开启新的ESB-Broker Server节点(是否能立即为ESB-Client提供服务,我们将在下文进行讲解),还可以在ESB中间件服务闲暇时关闭一些节点。

但无论是通过人工部署新的节点还是通过Docker容器将ESB-Broker Server包装成微服务进行部署,ESB-Broker Server中所注册的原子服务和路由编排都需要Studio中预先完成编辑,并且它们不能在ESB-Broker Server节点运行的过程中进行新增、修改或者删除。这是因为ESB中间件中没有一个合适的事件通知机制,在ESB-Broker Server运行时将Studio中产生的变化行为通知给前者。

4-1-2、进行设计方案调整

那么,我们至少需要在“第一方案”的基础上找到一种合适的事件通知机制,基于这个事件通知机制能够让ESB-Broker Server节点在运行期间发现原子服务注册情况的变化、发现流程编排情况的变化,并让前者做出相应的事件响应,最终实现对Processor处理器定义和Route路由定义的动态更新。

对于这个事件通知机制,ESB-Broker Server不但需要基于它的通知知道某种类型的事件已经发生了,还需要基于这个事件通知服务进行持续的数据记录,以便ESB-Broker Server能够查询到这个事件所携带的数据变化。举个例子,同一个原子服务可能存在多个版本的定义,当新的版本出现时,旧的版本数据应该同样存在并且能够被ESB-Broker Server查询到。

那么,采用消息队列进行事件通知就不是一个好的办法了。虽然消息队列可以进行事件通知,但是它不能对数据进行持久化保持,也不向消息队列的客户端提供查询历史数据的功能。但是zookeeper是满足我们对事件通知机制的功能要求的,它不但能够提供事件通知功能,还能为客户端提供历史数据的查询功能。所以,我们的事件通知机制就基于zookeeper进行设计:

202307292201170632.png

4-1-3、调整后的最终设计方案

在明确了ESB-Broker Server节点的工作职责、确定了基本的技术选型后,我们就可以对ESB-Broker Server进行结构设计了。首先ESB-Broker Server必须有Apache Camel的核心组件,才能保证ESB-Broker Server运行Camel Context;ESB-Broker Server中还必须集成ZK的客户端,这样才能保证它能够接收事件通知并且连接到ZK Server查询各种数据,我们将使用ZK的客户端组件Curator作为一个封装好的中间层;最后,当出现新的路由定义时,Apache Camel会动态加载新的RouteBuilder实现类或者新的处理器Processor实现类,所以我们还需要管理一组Classloader的加载路径。

202307292201177833.png

上图显示了ESB-Broker Server的设计结构,其中包括两个子模块:Zookeeper操作模块和Camel服务控制模块。前者负责和ESB中间件内置的zookeeper服务进行交互,后者负责Apache Camel组件的启动、Service加载、ClassLoader类加载器路径的管理。下面几个小节,笔者将对上图所示的几个模块进行描述。

让ESB-Broker Server支持在运行过程中动态加载新的处理器、新的路由定义后者修改现有处理器、修改现有路由定义,再或者删除已有路由定义。 其实质是对这些处理器、路由定义所涉及的Processor、Route的实现类进行动态加载管理 。所以我们相当一部分注意力需要放在如何根据业务数据的变化情况来管理Class类文件,并将这些Class定义按照条件加载到Camel Context上下文中。

4-2、zookeeper操作模块

zookeeper模块存在的目的是为了同步各个ESB-Broker Server节点的数据,当主控服务有新的业务流程定义、或发布新的处理器定义时、发生已有定义数据变化时,将这些事件通知到每一个ESB-Broker Server节点上并完成相应的操作。所以要对ESB-Broker Server中的zookeeper操作模块进行讨论,那么就首先要说清楚zookeeper服务端怎么来组织数据,以及需要组织哪些数据。有了以上的前提后,才可以把zookeeper客户端需要进行哪些数据项变化的监听和当事件发生后需要做哪些动作说清楚。

202307292201184454.png

我们将使用JAVA语言中Classloder的特性实现class文件的动态加载。这代表着主控服务只能使用JVM系列语言进行编写(最好就是原生的JAVA语言),这也代表着ESB-Broker Server节点也只能使用JVM系列语言进行编写(最好也使用原生的JAVA语言,这样好操作Apache Camel组件)。但是 这不代表参与ESB中间件集成的各个业务系统必须使用JVM系列语言 ,事实上这和各业务系统使用什么样的编程语言、编程技术没有任何关系。

4-2-1、zookeeper数据组织结构

我们首先需要讨论一下zookeeper服务端怎么来组织数据,因为zookeeper服务端的数据组织结构直接影响到ESB-Broker Server中zookeeper操作模块的编码细节。至少包括以下这些元素在内的数据数据需要被zookeeper服务器端所记录:业务系统标示、处理器标示、处理器class文件字节码、路由编排配置信息。另外要说明一下这只是示例性的设计,所以我们随后的例子只会演示如何做处理器动态变化和路由定义的动态变化。如果在实际工作中,那么需要被zookeeper服务端所记录的数据还会更精细,例如可能需要记录处理器的版本号信息。

202307292201192775.png

上图中的Zookeeper服务端结构以业务系统标识作为第一级Path结构,在其下分列处理器和路由配置两个Path(第二级Path结构)。在每一个二级Path结构下,都包含了处理器、路由配置信息的名称标示,其Path的data部分是对应的class的字节码信息。注意, 在zookeeper的规定中每一个Path node都可以存储数据,但是每个Path Node所存储的数据不能超过1M的大小 。对于class文件的字节码数据来说,虽然大多数情况下其大小是不会超过1M的,但如果真的超过怎么办呢?开发人员可以将这个处理器或路由定义所涉及到的Path Node拆成两个,并以后缀进行标识,如下图所示:

202307292201198396.png

4-2-2、Curator封装层与事件监听

有了zookeeper服务端的数据组织结构定义,就可以在此基础上实现zookeeper-client的原生监听代码了。但是毕竟zookeeper客户端的原生监听没有任何的业务形态,只是反映zookeeper服务层数据结构的变化。那么想要让ESB-Broker Server应用的代码结构更加清晰、更好维护、更好升级就 需要将这些原生的没有业务形态的事件监听转换成有业务形态的事件监听 。如下图所示:

202307292201204957.png

上图中,我们使用Apache Curator组件来封装zookeeper的原生客户端,这一层的作用主要是避免开发人员自行对zookeeper的数据形态进行判断等重复的、复杂的工作。举个例子,如果开发人员使用zookeeper原生客户端中的zk.getChildren(……)方法对某个业务系统下的processes节点进行监控时,无论这个节点下的子节点发生的事件是节点添加还是节点修改又或者是节点删除,开发人员收到的事件类型都是“NodeChildrenChanged”—— 开发人员需要通过原来在本地记录缓存的处理器描述数据和最新的processes节点下的数据进行比较,才能最终确定当前的NodeChildrenChanged事件到底是子节点添加操作还是子节点修改操作又或者是子节点的删除操作 。但如果使用了Apache Curator组件,这个过程就不需要再由开发人员进行实现了。

就如上文介绍的那样,Apache Curator是通用组件。如果ESB-Broker Server的开发人员在这一层直接做业务层的操作,会增加ESB-Broker Server中模块的耦合度,增大Apache Curator层的代码重复率。所以Apache Curator层最适合做的事情就是在这里将不具有业务层含义的事件转换为有业务含义的事件。以下的代码片段描述了业务层的事件接口定义:

    package test.esb.listener;
    
    /**
     * 这个接口用于定义和Camel Service相关的事件变化<br>
     * 注意,这个事件是业务层驱动事件,它来自于对上层Apache Curator事件的转换
     * @author yinwenjie
     */
    public interface ESBCamelServiceListener {
        /**
         * 当外界组件通知一个新的processor处理器被定义时,该事件被触发。
         * @param systemId 本次processor处理器变化,所涉及的业务系统唯一标识。在zookeeper数据结构中就是“systemNameA”。
         * @param packageName processor处理器定义涉及的class包名
         * @param className processor处理器定义涉及的class类名
         * @param contents processor处理器定义涉及的class内容,如果zookeeper数据结构中class分片存储,在业务级接口层面上也进行了合并
         */
        public void onProcessorAdded(String systemId , String packageName , String className , byte[] contents);
    
        /**
         * 当外界组件通知一个已有的processor处理器data部分发生变化时,该事件被触发。
         */
        public void onProcessorDataChanged(String systemId , String packageName , String className , byte[] contents);
    
        /**
         * 当外界组件通知一个新的RouteDefine路由被定义时,该事件被触发
         */
        public void onRouteDefineAdded(String systemId , String packageName , String className , byte[] contents);
    
        /**
         * 当外界组件通知一个已有的RouteDefine路由定义被改变时,主要就是路由定义内容被改变时,该事件被触发。
         */
        public void onRouteDefineChanged(String systemId , String packageName , String className , byte[] contents);
    
        /**
         * 当外界组件通知一个已有的RouteDefine路由定义被删除时,该事件被触发。
         */
        public void onRouteDefineDelete(String systemId , String packageName , String className);
    }

请注意以上是zookeeper操作模块的业务层接口定义。Apache Curator封装层的主要工作就是依据zookeeper服务端数据变化时所触发的事件类型,调用这个业务层接口相应的事件监听,并传递经过初步分析的必要数据。另外请注意,由于这里我们主要介绍的是zookeeper操作模块的主要设计思路,所以在给出业务层驱动事件接口定义时,只给出了Processor定义和Route定义变化时的事件接口。但实际上如果真的将设计应用到真实场景中,这些业务层接口定义远远不够。举个例子,当新的业务系统(systemNameB)集成到ESB中间件时zookeeper服务端的Path Node就会产生变化——产生一个新的节点“/systemNameB”和其下的子节点“/systemNameB/processes”、“/systemNameB/routes”。那么这个节点的变化事件需不需要被Apache Curator封装层监听呢?答案是肯定需要的!ESB-Broker Server需要为这个新加入的业务系统(systemNameB)建立一个新的classloader路径(后文会进行讲解),Apache Curator封装层需要为目录位置“/systemNameB/processes”和“/systemNameB/routes”建立新的监听。

为了保证Apache Curator封装层的代码能够进行更好的维护管理,在这层我们定义了两个监听器:ProcessesPathChildrenCacheListener和RoutesPathChildrenCacheListener。它们分别监听/SystemNameX/processes目录下子级Path Node的变化情况和/SystemNameX/routes目录下子级Path Node的变化情况。前者记录了某业务系统SystemNameX所发布的Camel Processor处理器定义列表,后者记录了某业务系统SystemNameX所发布的Camel Route路由定义列表。事件监听实现类的定义如下:

    public class ProcessesPathChildrenCacheListener implements PathChildrenCacheListener;
    
    public class RoutesPathChildrenCacheListener implements PathChildrenCacheListener;

以上两个类所实现的接口“org.apache.curator.framework.recipes.cache.PathChildrenCacheListener”是Apache Curator提供的一个事件接口定义,它将监听指定路径下子结点的创建、子节点的删除以及子节点结点数据的更新事件。又要举一个例子,如果您使用这个接口监听“/SystemNameX/processes”目录,那么当“processes”节点本身发生变化时(删除也好、修改节点数据也好),PathChildrenCacheListener事件接口定义将不会收到任何通知;但是如果“processes”节点下的子节点(例如:/SystemNameX/processes/X.X.ProcessorClass)发生变化时,这个事件接口就会收到通知了。

下面的代码片段描述了Apache Curator封装层是如何完成数据转换,注意:本文不打算介绍Apache Curator的基本使用,如果读者不清楚可以自行查阅资料。但是为了让这部分读者也能够清楚理解以下代码片段的处理过程,所以代码片段中给出了较详细的注释:

    ......
    /**
     * 如果“processes”节点下的子节点(例如:/SystemNameX/Processes/X.X.ProcessorClass)发生变化时,这个事件接口就会收到通知了。
     * @author yinwenjie
     */
    public class ProcessesPathChildrenCacheListener implements PathChildrenCacheListener {
    
        ......
    
        /**
         * 业务层的监听接口定义,就是上文中提到的ESBCamelServiceListener接口定义
         */
        private ESBCamelServiceListener esbCamelServiceListener;
    
        /**
         * 构造函数
         */
        public ProcessesPathChildrenCacheListener() {
            // JAVA SPI 机制进行实例化 业务层的事件通知对象
            // JAVA 中的 SPI 机制已经在之前介绍Dubbo的文章中信息介绍过了
            ServiceLoader<ESBCamelServiceListener> serviceLoader = ServiceLoader.load(ESBCamelServiceListener.class);
            this.esbCamelServiceListener = serviceLoader.iterator().next();
        }
    
        ......
    
        public void childEvent(CuratorFramework zkClient, PathChildrenCacheEvent event) throws Exception {
            /*
             * camelContext上线文的管理在主线程中,
             * curator将按照节点的变化情况通知ESBCamelServiceListener中相应的事件(业务层事件)
             * 
             * 该对象将在“/systemNameA/processes/”目录的子级Node Path发生变化时被触发。
             * */
            ChildData childData = event.getData();
            String path = childData.getPath();
            Type eventType = event.getType();
            byte[] fileClassBytes = childData.getData();
    
            // 请注意path结构是“/systemNameA/processes/xxxxxx”
            // 所以systemId是拆分后的第1个元素,而不是第0个元素
            String[] pathSplits = path.split("/");
            String systemId = pathSplits[1];
            String fullClassName = pathSplits[3];
            // 获得class的包名和类名
            int lastIndex = fullClassName.lastIndexOf(".");
            String className = fullClassName.substring(lastIndex + 1);
            String packageName = fullClassName.substring(0 , lastIndex);
    
            // 开始根据eventType在业务层进行相应的事件通知
            if(eventType == Type.CHILD_ADDED && fileClassBytes != null) {
                this.esbCamelServiceListener.onProcessorAdded(systemId, packageName, className, fileClassBytes);
            } else if(eventType == Type.CHILD_UPDATED && fileClassBytes != null) {
                this.esbCamelServiceListener.onProcessorDataChanged(systemId, packageName, className, fileClassBytes);
            } else {
                // 其它事件就不再需要反映到业务层了,只在Curator层做一个日志即可
                ProcessesPathChildrenCacheListener.LOGGER.info(event);
            }
        }
    
        ......
    }

以上代码片段我们只给出了ProcessesPathChildrenCacheListener监听器的主要处理过程,实际上Apache Curator封装层中另一个监听器RoutesPathChildrenCacheListener的主要处理过程也是类似的——根据zookeeper服务器上的数据组织结构分析出当前的事件类型、涉及的Camel Service —— Class包名和类名已经Class文件内容。这些信息将在zookeeper操作模块的业务层中,被存储到ESB-Broker Server本地,并通过已经指定好的ClassLoader工具动态加载到Broker服务中。

==========================================
(接下文)

阅读全文