探讨篇(三):代码复用的智慧 - 提升架构的效率与可维护性

电子说

1.3w人已加入

描述

作者:京东物流 冯志文

前两篇从服务粒度和服务内的分层架构角度探讨,本文继续从服务间代码复用角度探讨。

背景

在分布式架构中,代码复用是个难题。那么如何处理代码功能共享的问题呢?本文结合日常实践中的案例,介绍几种分布式架构中管理代码复用性的技术。包括代码复制、共享代码库(jar包)、共享服务、边车服务。对于每一种技术,列出优缺点、适合场景权衡。

本文的观点源自我在学习与实践过程中的深思熟虑,尚处于不断探索和验证的阶段。希望能“抛砖引玉”,激发更多的讨论与交流。让我们共同进步,在探讨与实证中寻求真知。

一、代码复制

共享的代码被复制到每一个服务中。这种技术在服务早期比较流行。虽然现在代码复制比较少见,但它还是解决跨多个分布式服务的代码复用的有效技术。但这种缺点很明显,因为如果在代码中发现错误或需要对代码进行重构改造,需要在包含该代码库的所有服务变更。

这种技术在一些场景比较有用,比如服务需要的高度静态的一次性代码。这种类型的代码非常适合复制,因为它是静态的并且不包含任何错误。比如很多通用的业务识别逻辑就用这种方式,在不同应用代码库个应用中编写。

案例1:根据sendpay标位判断XXX代码

 

public static boolean isXXX(String sendpay) {
    boolean flag = false;
    String sendpay_x = Character.toString(sendpay.charAt(x));
    String sendpay_y = Character.toString(sendpay.charAt(y));
    if (("1".equals(sendpay_y) || "2".equals(sendpay_y) || "0".equals(sendpay_y))
            && "1".equals(sendpay_x)) {
        flag = true;
    }
    return flag;
}

 

案例2:新功能上线DUCC开关

 

/**
 * XXX功能 控制开关
 */
private boolean enableLargeApplianceSendMsg = false;

public boolean isEnableLargeApplianceSendMsg() {
    return enableLargeApplianceSendMsg;
}

public void setEnableLargeApplianceSendMsg(boolean enableLargeApplianceSendMsg) {
    this.enableLargeApplianceSendMsg = enableLargeApplianceSendMsg;
    log.info("enableLargeApplianceSendMsg: {}", enableLargeApplianceSendMsg);
}

 

代码复制技术  
优点 1、无代码共享
缺点 1、代码分散各应用中,变更起来比较复杂。 2、无法保证跨服务代码一致性,有时候容易遗漏某个地方未修改 3、缺乏跨服务版本控制能力
适合场景 1、关联应用较少,比如2-3个左右可以接受,如果牵扯应用较多(比如5+,具体如何确定“较多”的基准或者指标待定)则不适合,同时核心要思考是不是服务粒度拆分的太细了? 2、简单的静态代码(比如公共工具类,业务逻辑通用类) 3、变更频率较低

二、共享代码库

共享代码库是共享技术的常用技术之一。共享代码库是一个外部组件(比如JAVA的jar包),共享代码库比代码复用更高一层级。基于功能划分,提供较为独立的功能。比如目前采用了根据基础能力和业务模块抽象时效内核JAR包方式,详见下图中时效计算内核jar包。为什么要采用这种方式呢?后文会说明权衡点。

jar包引入看起来简单,在编译时被整合和共享。但其实也有利弊取舍和复杂性。其中最重要的是代码库的共享库粒度和版本控制。

1、依赖管理和变更控制

如果粗粒度共享库中任何类文件发生变更,都需要每个服务变更、测试、部署。这极大的增加了共享库更改的整体测试范围。粗粒度共享库的变更会影响多个服务,但会减少依赖关系。

将共享库分解为更小的基于功能的共享库(比如拆分为 ABCDE等),这样有利于变更控制和整体可维护性。但这会造成依赖管理的混乱。如下图,业务根据不同业务域拆分不同 jar包,导致jar包依赖复杂。

代码

共享库对少量的应用可能并不重要,但随着服务数量增加,变更管理和依赖管理相关的问题也会增加。

建议:避免大的粗粒度的共享库,尽可能争取更小的、功能分区的库,有利于变更控制而不是依赖管理。

2、版本控制策略

对于共享库来说,版本控制需要向后兼容(后退、考虑旧版本兼容),同时也需要高度敏捷性。比如a.jar包变更升级版本为1.1,只需要1个服务上线,其他N个服务不会有任何影响。这虽然看起来版本控制很简单,但同样存在权衡和隐藏的复杂性。比如下次N个应用也需要上线。并且依赖管理混乱(服务ABC依赖a.jar版本1.1,服务DEF依赖a.jar版本1.0),复杂性不仅体现在版本变更的通知,也存在旧版本弃用的情况。

共享代码库技术  
优点 1、减少重复代码 2、不受网络影响,性能更加稳定。对性能要求较高的场景使用该方式会有一定优势; 3、节省服务器硬件成本,尤其服务器QPS高,需要部署大量服务器资源的场景下。 4、支持版本变更
缺点 1、可维护性较差,依赖了该组件的服务都需要跟着一起升级,随着时间的推移,梳理维护起来会很麻烦; 2、组件升级成本高且风险较大,成本包括了开发维护升级各个服务的成本、测试验证的成本及运维发布的成本,需升级维护的服务越多,成本越高,对应的风险也越大。 3、容易jar包冲突 4、版本沟通可能很困难 5、依赖可能难以管理、版本弃用可能很复杂 6、如jar包过大,维护困难,并且调用方引入过多项目无用代码
适合场景 1、无隐形依赖,更新频率低和更新影响小的代码,比如通用的判断订单、运单校验 2、服务器资源硬件成本控制要求较为严格,尽量降低成本。 3、内部一些公共功能处理场景,不涉及到数据库资源层面的连接和调用,适合组件化的方式; 4、对性能要求较高的应用

为什么时效内核需要采用jar包给下单前下单后各应用这种方式呢?结合上面的优缺点,主要权衡核心点如下

1.应用场景相关:XX是下单前商详结算等高并发场景,下单后订单生产节奏控制。

2.降本:预估降低了服务器硬件成本XXX核左右

3.性能:通过JAR包的依赖的方式来较少RPC调用,提升了接口性能TP99,尤其是用户在商详、结算提单页面

4.更新频率低:由于时效内核更新频率较低,一年1-2次左右的改动点

三、共享服务

共享服务技术通过将共享功能服务化来避免重复使用。对应上面改造,把时效内核jar包进行服务化时效内核应用,具体架构图如下:

共享服务是分布式中常见的共享服务的方法,但也需要权衡,比如变更风险、性能、可伸缩性、容错性。

1、变更风险

使用共享服务变更共享代码是一把双刃剑。 只需要共享服务部署上线,但共享服务的变更可能在运行时破坏其他服务。那必须牵扯版本控制、共享代码库是在编译的时候绑定版本控制,降低更改风险。但如何在共享服务中版本化变更呢?使用API版本控制。但使用API版本控制有个问题,很多服务协议不是restful api,而是rpc或者消息mq,这样会使得版本控制复杂。

共享服务虽然版本控制可以帮助降低这风险,但它的应用和管理更复杂。

2、性能

共享功能服务必须进行服务间调用,存在网络延迟开销而影响性能。

3、可伸缩性

共享服务一定要随着调用服务的规模进行伸缩

共享服务技术  
优点 1、减少重复代码 2、高度解耦: 每个服务都是独立的,可以独立开发、部署和扩展,提高了系统的可维护性和可扩展性。 3、快速迭代: 服务可以根据需求快速更新和迭代,更容易适应业务变化。 4、资源隔离,互不影响,对调用方隐藏内部细节。
缺点 1、增加硬件成本 2、性能受到网络延迟影响 3、服务依赖导致 容错性、可用性、可伸缩性、吞吐量问题 4、版本控制可能困难 5、分布式固有问题:比如一致性、分布式事务处理等
适合场景 1、适合多语言的环境 2、共享功能频繁变化 3、不需要太多的服务器资源 4、对性能要求不高

四、边车和ServiceMesh服务网格

"边车服务"(Sidecar Pattern)这个术语来源于摩托车的边车(sidecar),这是一种附加在摩托车旁边的一轮车厢,可以搭载乘客或货物,但它不是摩托车本身的核心部分。

边车服务(Sidecar Pattern)在微服务架构中用于将一些与业务逻辑不直接相关的控制面(如注册发现、熔断限流、pfinder链路追踪监控、DUCC配置管理等)从应用程序中分离出来。这样,应用程序可以专注于业务逻辑,而边车服务则负责处理其他方面的问题。

代码

边车服务的关键特点包括:

复用性:由于边车服务可以被多个主应用共享,因此一些通用的功能(如服务发现、断路器、限流器等)可以在不同的服务之间重用,减少了代码的冗余

隔离性:边车为主应用提供了一个清晰的隔离层,使得主应用可以专注于业务逻辑,而不必关心其他非功能性的问题。 边车服务是主应用程序的附属,为主应用提供支持和增强功能。

易于维护:边车的引入使得对于共享功能的更新和维护变得更加简单,因为这些功能被集中到单独的服务中,不需要在每个应用中单独进行修改。

透明性:对于主应用程序来说,边车的存在应该是透明的,主应用不需要知道边车的具体实现细节。

独立性:边车服务可以独立于主应用程序更新和维护,无需修改主应用程序的代码。

通过使用边车模式,开发人员可以将关注点分离,使主应用程序更加简洁,只关注业务逻辑的实现,而将服务治理等通用性问题交给边车服务处理

如果每个服务都包含边车组件,那么它就形成了服务网格。每个服务右边的盒子都互相连接,形成一个“网格”

代码

服务架构是围绕各自领域组织的,但服务治理运维耦合需要横切这些领域

五、总结

技术最终是要服务于业务,每种技术选择没有绝对的好坏,各有优缺点,适合场景。具体应该用哪一种,需要根据成本、团队技能、系统的未来发展综合考虑,目前团队系统中上面几种情况都存在。正如软件架构定律:软件架构中的一切都是在权衡,架构背后的原因比方法更重要。

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分