zoukankan      html  css  js  c++  java
  • 谈长耗时任务的优化

    不论什么一个业务系统都有它的核心业务逻辑。在非常多情况下核心业务逻辑通常都有一些特点:牵扯面广,依赖关系多,流程复杂等。

    接下来。我就近期公司内一个真实的案例来简单谈谈对于一个长耗时的任务或者业务逻辑有哪些经常使用的优化手段。

    案例简述

    要说的这个系统案例是一个统一通信平台,它给客户提供收发短信、email、RTX消息等功能。

    不难想象最核心的业务当然是借助这个平台间接通过这些通讯 (sms/email/rtx)网关来收发消息。当然现实的场景远没这么简单,还有一些外围业务也环绕此展开:

    • 权限管理
    • 流量控制
    • 黑名单策略
    • 成员管理
    • 网关对接
    • ...
    以下是一个用户反映的在单批次群发大概3000条左右的信息时,系统出现的一些故障:
    1、信息员在发送的时候是通过批量发送进行处理的,对于这2913人进行发送的时候button一按,系统一直处于发送状态。没有发送成功提示!等了非常长时间也没出来。但运营商网关那里已经開始发送了。
    2、管理员通过管理帐号登录查看日志信息信息提示2913条信息发送成功但实际上是非常多不成功的。但我们短信系统内的日志却提示所有发送成功。我们也问了运营商的技术人员,他们答复网关反馈的代码仅仅有一次。可是时间上有长有短。所以还须要后台支持这边看一下。
    3、针对长短信的计数不准确,我们实际算2913人每人收到3条应该是8739条,而计数仅仅扣除了2913条!

    以上问题初步分析,大概能够总结出三个问题:
    • 请求处理时间超长,client长时间堵塞处于假死状态
    • 短信网关回执的异步写入产生混乱,不是时序问题就是出现处理异常
    • 长短信计算错误

    梳理业务&模块拆分

    业务梳理以及模块拆分是降低项目复杂度的手段之中的一个。

    由于这是一个老项目,经手过的维护人员非常多,加之没有规范并且维护人员技术水准以及对项目的了解程度各不同样,导致项目中的业务流程非常混乱,到处都是if/else、类似逻辑的方法反复定义、特例配置遍布整个项目。仅仅有拆分清楚了,剥离与独立部署才干成为可能。

    梳理的方式有非常大,应该是多重手段并用的结合。经常使用的手段有:

    • 慷慨法化小
    • 提取反复逻辑
    • 又一次推断方法的归属对象
    • 又一次推断逻辑的归属层次
    • 避免逻辑层次的反向依赖
    • ....
    当然,我个人觉得领域驱动设计(DDD)比通常情况下单纯的分层架构更符合面向组件划分的理念(请注意这里所说的是单纯的分层架构。由于DDD里也可能按逻辑分层)。由于领域对象(Domain Object)的视角跟面向组件在职责单一上都非常契合。

    分布式组件

    分离关键业务形成分布式组件相对于all-in-one的web系统而言,有助于提升整个系统的可靠性、稳定性以及吞吐量。这里将系统中相对耗时的发送消息业务从web系统中剥离出来,放到网络上一个独立的节点上排队处理,能够充分利用新节点的计算能力来实现并发处理。

    这个案例中,还有一个能够单独实现的组件是网关对接器:gateway-adapter。

    它的作用是为了适配网关接口。以及处理网关回执。这里所谓的分布式组件,能够是物理上的分布式(比方独立的物理节点)。也能够是逻辑上的分布式(比方仅仅是一个独立的JVM进程)。独立节点与否,能够參照节点资源的利用率,但仅仅要跑在独立的JVM进程上,就能够保证单个服务的稳定性。分布式的组件通常都是基于事件驱动的。它们之间的通信能够基于消息中间件。


    对于这个业务场景。能够将它大致拆分为三个服务组件:
    • 文件解析、验证组件
    • 发送消息业务逻辑处理组件
    • 消息发送网关适配组件

    缓存查询数据

    系统中有些业务数据的更新频次比較低。但读取的频次却非常高。对于这些读写频次区别比較大的业务数据。通常的优化手段有:分表、分库、读写分离、数据内存化(缓存)。
    眼下一些基于内存的缓存/数据库(如redis及memcached)的使用已经非常流行。它们的使用对于系统性能的提升是立竿见影的。使用它们的还有一优点是:不会破坏原有系统的数据结构。对于原来的某个表。大部分字段都是仅仅读的,但有个别字段写的频次却非常高。这时你提取字段拆分表,做读写分离的话,会破坏系统的表结构设计。进而会大改应用程序。

    而使用基于Key-Value的内存缓存。则会对于数据库以及应用程序作尽量少的修改,仅仅须要对关键业务增加一些对缓存訪问/处理的代码。但缓存的使用也会带来数据的可见性、一致性问题。这须要非常好的刷新、同步机制。


    并发&多线程

    将流程拆分为分布式组件的一大优势就是能够利用很多其它节点的计算能力来提升业务处理的吞吐量。将原来串行处理流程修改为并行处理的方式,将能够加快对消息处理的速度。按组件拆分后,这些组件都是完备的“服务”,它们之间由消息中间件来传递消息以串联时序关系。

    这些组件共享数据库,彼此之间并不产生依赖,这些服务也能够看作是任务处理器。

    这里多线程的主要应用场景在business-filter这个主业务逻辑组件上。在client有大批量发送的请求到来时,会在file-parser之后对待发送的目标进行拆分处理。拆分后的每一个组将由一个线程来处理。

    至于分组的參考值,这是个权衡值,它既须要保证吞吐量。还要保证单个处理线程在时间同意的范围内重回线程池。从而避免线程饥饿。

    context&pipeline&filter

    pipeline&filter是处理同一数据对象的经常使用方式。

    它能够用于拆分&重组&串联业务流程。拆分流程的优点显而易见。你甚至能够基于一定的策略,动态载入或卸载一个filter(利用classloader),你也能够对后来修改某个filter的逻辑带来的影响最小化(比方对于依据号码反查不到工号,有些用户选择的处理方案是中止发送。有些用户选择的方案是继续发送。这种修改将会被限制到某个特定的filter内,对外部全然透明)。

    context在非常多模式中都能起到一个粘合剂的作用。由于非常多模式须要在编程接口上保持一致性。而由于处理的业务不同。它们须要处理的数据也不尽同样,所以须要一个Transfer Object对象来将它们封装起来。同一时候保持编程接口的一致性。

    同步请求异步化

    上面案例引发的第一个问题就是页面持续在parsing状态。后台由于堵塞式的同步运行。http请求迟迟无法得到响应,导致页面假死,体验非常不好。这也是另外一个优化点,并且对于这一点的优化,其价值会比后端优化大得多,由于它是直面终端用户的。
    对于一个请求,后端假设耗费极长的时间。能够将同步请求异步处理。将请求高速响应给前端,然后以ajax的方式。轮询后端的请求进度。

    比方。就这个业务而言,我们就能够定义例如以下用于反映处理进度的数据结构:


    然后在模板页的某个位置给出进度显示。


    组件交互

    组件之间交互关系例如以下示意图:


    优化后处理流程

    我们能够以一个请求作为触发,来梳理一下经过优化后的业务处理流程:

    (1)web服务端接到“excel自己定义批量发送”请求后。简单得校验一下格式,将存放发送目标的excel直接保存到服务器的磁盘上,同一时候给同样宿主在该server上的file-parser 组件发送一条表示“有任务到达”的消息。并附带有要发送的消息内容以及发起本次请求的员工信息,在数据库中创建一条请求记录并生成一个唯一的requestID作为处理的批次号(它用于备案以及作分布式处理的日志追踪,字段大致例如以下所看到的),然后该http请求便迅速响应给client。但仅仅给出一个状态码:表示请求已被受理,正在处理中。


    (2)这时client首先须要有一点小改进:在页面的模板或者iframe(假设使用的话)区域。展示一个通知提示区(为的是不断跟踪处理状态),须要这个展示区的目的是它们是全局的,不会影响内容页切换。


    (3)上面的请求一旦被返回。就能够在模板中採用ajax请求来以poll或push的方式获取处理状态。採用哪种方式依据须要获知发送状态的精确程度而定。假设须要精确的话,可採用poll模式。
    (4)回到服务端。excel解析服务会对文件进行解析,提取出里面发送目标的email或手机号。
    (5)与此同一时候,它会在消息内容表中插入要发送的消息内容,并提取出该记录的ID作为对消息内容的引用
    (6)接下来它会将要发送的号码拆分为若干组,并封装入若干个传输消息内(此处的消息指的是组件之间传输的消息。而非发送消息)。

    同一时候会附带发送消息内容的ID以及批次号。

    将这些传输消息发送到专门用于处理发送服务的business filter所listen的队列上
    (7)business filter是一个分布式组件,用于接收文件解析服务发来的分组传输消息。收到传输消息后解析。然后对每一个传输消息(内部包括已分好组的若干个发送目标)。启用一个独立的线程进行并行处理。
    (8)对每一个线程而言(也能够说对每一个传输消息而言),将这个传输消息按发送目标拆分成一个个的子发送消息(以下提及的发送消息都指代这种子发送消息)。将发送消息对象包装进一个上下文对象中(Context此处的意义跟Transfer Object类似。在J2EE中也称之为Value Object)。然后这个发送消息将要流过一个pipeline的filter-chain,每一个filter都是一个子业务的封装,当中包括比方:

    1. 成员验证:比方要发送的号码是不是本系统的用户
    2. 流量控制:当前是否发送部门的流量已经超标
    3. 策略校验:是否满足用户的时间策略以及接受策略
    总之这些filter大致都是两种类型:
    • 内容筛选器
    • 内容扩充器

    (9)由于这些用filter包装起来的业务,大都在运行——查询&校验的动作,所以对这些业务须要查询的数据集构建合适的缓存,将能够有效得提升处理速度,当然这当中也不可避免会有部分的写操作。

    由于这里採用多线程并发处理。因此对竞争资源的写须要保护机制,通常通过同步来保证数据的一致性。

    (10)filter进行到chain的最后一步,会依据中间filter处理的状态来推断终于这条消息是否满足发送条件:

    • 假设不满足:
      • 记录错误/失败原因。
      • 更新批次号为当前RequestID的那条记录的相关字段;处理结束
    • 假设满足:
      • 记录相关信息到消息相应关系表,这里须要的非常多信息都是通过上面的内容扩充器filter进行填充的
      • 在数据库的下行队列表中创建一条该消息的发送记录。便于收到回执后改写消息的发送状态,并将这条记录的ID追加到消息中
      • 将消息发送到gateway-adapter相应的队列
      • 更新批次号为当前RequestID的那条记录的已投递数字段;处理结束

    (11)上面将消息发送到gateway-adapter服务相应的队列而不是将其直接发送到网关相应的队列是由于在消息发送给网关之前还有一些问题须要处理:

    • 通常网关会是一个独立并且“标准”的service provider,它会对接非常多系统。这时为了不让消息的格式过于混乱,它定义了满足它需求的标准格式。让所有服务使用者来适配它。而这个网关对接服务存在的目的就是为了适配网关的消息格式
    • 而对接服务存在的还有一个理由是:依据网关发回的回执来更新数据库中消息的状态:
      • 相应于下行消息表中当前messageId的消息状态更新
      • 相应于当前requestID的发送成功数/发送失败数字段的更新

    (12)对每一条消息进行处理,从而使得不同状态的累加数等于总记录数
    以上通过将长耗时的同步任务异步化处理的优化,不仅使得消息从串行同步处理变成了并行异步处理。并且改善了用户体验,在发送的过程中,点击错误日志或者发送过的消息状态就能够实时看到部分处理的结果。


  • 相关阅读:
    011 处理模型数据时@ModelAttribute的使用
    动态产生DataSource------待整理
    连接池问题
    maven加载第三方jar不能加载
    010 处理模型数据(ModelAndView,Map Model,@SessionAttributes)
    009 使用servlet API作为参数
    008 使用POJO对象绑定请求参数
    007 @CookieValue绑定请求中的cookie
    006 请求处理方法签名
    005 RequestMapping_HiddenHttpMethodFilter 过滤器
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/5197406.html
Copyright © 2011-2022 走看看