页面加载中...

代码编排?从中台、serverless、微服务引发的思考

| 随笔 | 0 条评论 | 70浏览

思路

我们日常开发,一直强调设计、重构其实是为了避免代码冗余的问题,尽可能地将相同逻辑地代码封装起来。

开源市场上,各种开发框架层出不穷,在我看来,这些开源软件终究为了:

  1. 提供开发运行时的IT组件,提升开发效率;
  2. 整合通用的功能逻辑,提高软件复用度;
  3. 良好的设计模式,提升系统的可拓展性。

细细思考一下,第2点最终也是为了第1点。在软件设计的基础上,又延申了23种设计模式。纵观这些设计模式,无一不是为了提供软件的拓展性、复用性等等。

如何创建连接池?如何操作redis?如何ORM?如何MVC?如何打印日志(小心log4j的bug)......

各种复杂的底层逻辑都尽可能地被这些框架封装完了,我们基本上只需要关注业务逻辑的实现。

纵观这些开源框架,都会抽象出的一个模型或者说组件,比如MybaitsmapperSpringbeanvuecomponent,然后开发人员在这抽象模型基础上进行业务逻辑的开发。

框架这些都是技术逻辑的封装,实际上我们还有很多业务逻辑,都面临这样的这样问题,所以可以思考一下是否能有类似的模块化、组件化的体系来解决业务逻辑复用的问题。

当然,业务逻辑的复用颗粒度是很难界定的,小到一个简单的文件上传的功能,中到订单查询的服务,大到用户权限认证的子系统。

为了方便说明,统一名词,我把这种属于模块化、组件化特性的服务、功能、方法等均统一称之为模组

那么是不是可以考虑,如何形成一套机制,把代码里相同的逻辑抽象出来,形成模组;实现一套编排平台技术,在平台上能够编排模组,平台将编排的结果直接生成开发人员喜闻乐见的代码工程。开发人员完成开发后,再由平台去解决部署的事宜。

将简单交给用户,复杂留给平台。让平台来告诉开发人员:xxx模组已经具备这个功能了,拿来就可以使用。这样对开发人员而言,真正做到只需关注业务逻辑的实现了。

正如前面所说,其实类似这样的模块化、组件化的思想在很多开源框架上都有体现,比如前端的reactvue,后端的SpringMybaits

前端的框架我不是特别熟,这里借鉴一下Spring框架的思想。

Spring 的思想

Spring的bean就可以看作模组,业务逻辑的开发其实就是对这些bean的组合。我们要使用一个bean,直接通过@Resouce或@AutoWired注解拿来即用。

同样的,我们把模组看作bean,那么应该提供什么的方式把模组拿来即用呢?

当然也不能完全把模组看作bean,模组内部之间是不应有相互的调用的,因为这样一来可能就会出现循环依赖的问题。

image-20211216160235161

虽然程序不会出错,但是在逻辑关系上,这样会陷入 ”剪不断、理还乱“ 的矛盾中。

解决循环依赖一个办法是引入事件机制,通过注册bean调用的事件来解耦bean之间的逻辑关系。

image-20211216153943868

Spring天生支持事件机制的,基于ApplicationListener接口可以很方便的注册自定义的事件,只需要继承ApplicationEvent类即可。

通过事件驱动,我们可以很直观通过事件把这些bean的逻辑关系串联起来。

image-20211216160936375

你会发现,这样的依赖关系,就是通过事件来触发代码,这就是serverless(无服务器)的一个思想。每一个method(方法),其实就是对应了serverless的函数。

但是这样可能又会引来新的问题:异步事件怎么处理?

其实也不难,我们一般只需要注册一个回调接口,等待异步事件处理完毕,然后等待触发这个回调接口,回调接口的实现就是我们要定义的功能逻辑。

image-20211216170306341

如果我们把这些bean的method拆分出来,独立部署成单个的微服务,这就是分而治之的思想,每一个bean就是一个模组。这样的拆分还有一个好处,可以解耦各个模组无关的依赖,比如ServiceA需要连接A大区的数据库,ServiceB需要连接B大区的数据库,在一个工程里,需要创建两个jdbc连接,而这两个jdbc连接可能ServiceC完全都用不到。根据单一职责原则,每个模组应该只聚焦属于它自己的逻辑。

在领域模型进行解耦,上下文关系,事件机制,这很符合DDD领域的思想。

反过来,假设我们已经有了ServiceA、ServiceB、ServiceC、ServiceD这些模组,需要组合成新的服务,模组之间的逻辑我也理清楚了,那我如何像使用bean一样使用这些模组呢?

所以,这就是需要考虑如何形成一套模组编排机制,支持自由组合模组。模组的编排本质是属于代码的编排,所以,我把这一类的编排统称为“代码编排”。

为什么是代码编排

个人认为,大部分业务可能更适合通过编码的方式来实现,业务复杂度不说,业务需求总是会各种变,不变的业务当然可以除外。 对于开发人员(比如我),我可能更愿意:通过编排页面,把我需要的模块功能先聚集在一起(可以不需要模块之间的组合逻辑),然后手动开发,编码这些模组之间的调用逻辑,最后实现整个业务系统。如果业务逻辑变化,我可以再引入新的模组,因此,这得需要编排平台生成的代码工程具备良好的拓展性。

—— 有点类似搭积木,假设市场上提供了1000个积木,我(开发人员)在编排页面上选择100个需要积木,然后搭建出我想要的模型出来。 也就说我只需要知道哪100个积木即可,我觉得这个应该是平台要帮助我做的事情。

代码编排思考

我们为模组编排之后生成的结果,表现为可执行的代码文件,这样的编排,我将其定义为“代码编排”。

编排的结果可以任意部署,如果是部署到Serverless平台上,那么这一类编排的结果,可以看作是FaaS(函数即服务)。

代码编排需要依托的模组的发展,有了足够数量的模组,并且形成了一个模组市场(或者说模组开源社区)。假设我们要实现某个中台能力,已经理清了业务逻辑,知道这个中台能力需要具备哪些功能,那么我们就可以从模组市场上选择具备这些功能的模组,通过编排平台打包,生成该中台能力的代码骨骼工程。

image-20211217100513846

当然,每个模组都可以独立部署,app里只需要生成调用模组的逻辑代码即可。

具体生成的代码,我写了个demo,可以见附录1。

这里可能还得需要强调一下:代码编排并完全不等于代码生成。传统的代码生成一般都是生产商提供了若干应用模型(比如创建博客的wordpress),通过固化好的模板代码,然后根据传入参数最后生成系统。

一些常见的PaaS产品

结合常见的PaaS产品:(目前先暂时不做前端代码编排的思考)

  1. 规则引擎:业务规则(是or否)的编排;属于代码级别的编排,但是颗粒度较小;
  2. 服务编排:接口API调用方式的编排;属于服务级别的编排;
  3. 低代码:业务流程的编排,也是接口API调用方式编排,基本上也可以归于服务编排。

规则引擎不需要多说,仅是规则的组合,true还是false的解释执行,严格来说都不属于编排。组合的结果以何种形式存在?如何解释执行组合编排的结果?这些问题倒是可以参考规则引擎的执行引擎原理。

服务编排平台的局限很大,仅用来组合运行态的服务,提供了若干组合逻辑的工具,比如参数处理、条件判断等工具。但是业务逻辑千变万化,不可能有个通用模式把全部逻辑给工具化,比如事务、异常捕获、异步处理等等,像这些逻辑,就得需要考虑手动编码去实现了。

低代码与服务编排本质上归属同一类,都是服务级别的编排。服务级别的编排一个问题就是依赖业务应用提供的服务能力,并且编排结果运行态一般都是HTTP服务,只能远程调用。想一下,如果我一个服务被编排到若干个中台能力,首先我这些中台能力运行环境都得需要先把与该服务的网络打通,极端情况下,假设我这个服务挂了,那我这几个中台能力都不可用了。

当然低代码的仅为前台业务应用而服务,与我所说的代码编排应用场景可能不太一样,关于代码编排和低代码的区别,我还做了一个对比。

代码编排与低代码的区别

两者思想都差不多,都是本着“少写代码”这一基本思想。

注:模组化平台 仅对 低代码平台的业务流程进行比较,即只是后端的对比。低代码的前端页面这里不做讨论。

低代码平台代码编排平台
颗粒度服务级别编排 ①代码级别编排 ②
定位编排中台能力,面向前台应用编排中台模组,面向中台能力
使用角色业务人员开发人员
执行引擎对业务流程的解释执行,输出为运行态的服务对代码的解释执行,输出为可执行的代码文件
部署编排结果统一部署在低代码平台环境上支持编排结果在任意环境部署,也可以直接内嵌在业务工程里
调试需要部署到专门的沙箱环境中,进行HTTP方式的联调可直接在本地环境进行调试
运行态独立的轻量级HTTP服务任意形态,比如微服务、服务网格、轻量级服务等
运行依赖依赖于业务系统提供的基础能力和服务代码编译后的执行文件,基本上无需外部依赖
日志依赖服务能力自身的日志输出平台可在模组调用前后进行日志埋点,方便后续问题查找
运维重量级运维,通过能力运营中心对能力服务统一管控轻量级运维,模组的运维由开发者自行管控
演进方向为前台业务场景而持续演进可以演进为构建业务应用、构建中间件甚至构建整个系统 ③

名词解释:

① 服务级别编排:将服务的调用方式以一定的逻辑串联起来,最后发布为一个新的服务(一般是HTTP服务),编排的本质是可视化业务流程。

② 代码级别编排:选取若干模组,模组的逻辑可以在页面串联,也可以自己进行二次编码,最后发布成可被解释执行的代码工程,编排的本质是功能逻辑的组合。

③ 模组化平台的演进方向:当期模组化仅面向中台能力,理论上来说,前中后台应用、业务系统,只要能拆分规范的模组(代码+文档),基于代码编排的原理,均可以通过编排平台进行组合。比如通过模组编排平台选取若干子系统模组,编排平台生成系统工程的代码骨骼,开发人员再对骨骼代码进一步填充。不过这得依赖模组的开源社区的成熟度。

一定程度上来说,代码编排将来是可以覆盖低代码后端的功能,甚至可以替代低代码。

编排平台技术发展的方向

考虑形成一套代码编排的工具,运行态的承载以serverless为主。

  • 代码编排平台:具备模组目录、可视化编排以及代码功能自动生成的功能。目前仅有服务编排平台、低代码平台做参考,平台可能需要自研实现;
  • Serverless平台:解决编排结果部署的问题。计划实现一套函数计算平台(FaaS),可以开源框架作为参考。

个人认为是代码编排可以作为一个单独的PaaS产品,用户模组编排组合以及工程代码文件生成。使用serverless来解决解决部署运行时的问题。当然了,代码编排的结果不一定非要部署到serverless平台上,serverless平台可以承载任意函数的部署。

想象你是一个将军,一个个模组就是你的小兵,代码编排就是如何排兵布阵,serverless就是你把小兵派上的战场。

参考资料

唯一能找到跟代码编排技术相关的社区文章:https://cloud.tencent.com/developer/article/1518034

附录

附录1. 编排页面生成后的代码

通过代码编排平台生成代码是一个难点。之前有做过这方面的研究,但是没能做出来,也跟当时个人技术水平有关。现正在出需研究

一个由编排平台生成后的代码逻辑demo如下:

/**
 代码编排后生成的代码,预留了一些拓展接口,供开发人员二次编码
 当然实际不应该以这种上下文逻辑代码方式来表述,而更应该通过事件机制来触发模组之间的调用
*/
class Main{
    Response hand(JsonObject pin){
         // 工厂模式来获取service
        Service serviceA = ServiceFatory.getService("a","http"); //a是http服务的模组
        Service serviceB = ServiceFatory.getService("b","jar"); //b就是一个jar包sdk的模组
        Service serviceC = ServiceFatory.getService("c","dubbo"); //c是一个dubbo 服务的模组
        // 注册的回调函数 SPI机制
		CallBackInterface callBackInterface = ServiceLoader.load(CallBackInterface.class).iterator().next(); 

        try{
            JsonObject pinA1 = callBackInterface.doSthBefore(serviceA,pin); // A服务调用之前自定义操作(AOP思想)
            JsonObject pinA2 = serviceA.call(pinA1);
            JsonObject pinA3 = callBackInterface.doSthAfter(serviceA,pinA2); //A服务调用之后自定义操作

            JsonObject result;
            if(pinA3.get("id") == 123){ // if...else 的逻辑是自定义编排的逻辑
                JsonObject pinB1 = callBackInterface.doSthBefore(serviceB,pinA3); // B服务调用之前自定义操作
                JsonObject pinB2 = serviceB.call(pinB1);
                result = callBackInterface.doSthAfter(serviceA,pinB2); //B服务调用之后自定义操作    
            }else{
                JsonObject pinC1 = callBackInterface.doSthBefore(serviceC,pinA3); // C服务调用之前自定义操作
                JsonObject pinC2 = serviceC.call(pinC1);
                result = callBackInterface.doSthAfter(serviceC,pinC2); //C服务调用之后自定义操作    
            }
            return Response.success(result);
        }catch(Exception e){
            JsonObject result = callBackInterface.doSthAfterException(e,pin);
            return Response.error(result);
        }
    }
}


/* 
为开发人员拓展的接口,封装服务调用前后需要做的操作,AOP思想。 
*/
interface CallBackInterface{
    JsonObject doSthBefore(Service service,JsonObject pin); //服务调用之前的操作
	JsonObject doSthAfter(Service service,JsonObject pin); // 服务调用之后的操作
	JsonObject doSthAfterException(Exception e,JsonObject pin); //出现异常后的操作    
}

/* 封装服务的调用方式和其他基本信息。不同类型的服务需要继承接口  */
interface Service{
    String name(); //方法名
    String type(); //服务类型
    JsonObject call(JsonObject pin); //调用方式
}

/* http类型的服务 */
interface HttpService extends Service{
    void setUrl(String url); //指定下http的请求地址
}

/* jar包类型的服务 */
interface JarService extends Service{
    void setJarName(String jarName); //指定jar信息
}

/* dubbo类型的服务 */
interface DubboService extends Service{
    void setZkAddress(String zkAddress); //注册中心地址
    void setOriginName(String originName); //原服务名
}

/* 创建模组服务的工厂 */
class ServiceFatory{
    
    static Map<String,Document> documentMap ; //文档全集,每个模组都对应一个文档。随平台启动而初始化。
    
    static Service getService(String name,String type){
        HttpService httpService = new MyHttpService(); //HTTP服务接口的实现,这里略
        Document document = documentMap.get(name);
        if(type.equals("http")){
            String url = document.get("url");
            httpService.setUrl(url);
            return httpService;
        }else{
            //其他类型代码略
        }
    }
}

/* 模组的文档,本质也是一个Map, 用来存储模组的基本信息 */
class Document extends HashMap{
    // 模组的基本信息,本质就是一个Map。
    // 通过put的方法设置基本属性,比如模组基本信息、原服务名、注册中心地址、调用依赖等等
}

发表评论

最新评论

    来第一个评论吧!