写点什么

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

  • 2024-12-27
    北京
  • 本文字数:2894 字

    阅读完需:约 9 分钟

作者:京东物流 冯志文


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

背景

在分布式架构中,代码复用是个难题。那么如何处理代码功能共享的问题呢?本文结合日常实践中的案例,介绍几种分布式架构中管理代码复用性的技术。包括代码复制、共享代码库(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);}
复制代码


二、共享代码库

共享代码库是共享技术的常用技术之一。共享代码库是一个外部组件(比如 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),复杂性不仅体现在版本变更的通知,也存在旧版本弃用的情况。



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


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


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


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


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

三、共享服务

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


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

1、变更风险

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


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

2、性能

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

3、可伸缩性

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


四、边车和 ServiceMesh 服务网格

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


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



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


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


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


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


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


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


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


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



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

五、总结

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

发布于: 刚刚阅读数: 2
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
探讨篇(三):代码复用的智慧 - 提升架构的效率与可维护性_京东科技开发者_InfoQ写作社区