前言:

得物音讯核心每天推送数亿音讯给得物用户,每天疏导数百万的无效用户点击,为得物App提供了弱小,高效且低成本的用户触达渠道。这么宏大的零碎,如何去监控零碎的稳定性,保障故障尽早发现,及时响应至关重要。为此,咱们搭建了得物音讯核心SLA体系,相干架构如图:

本文次要介绍咱们如何实现SLA监控体系,并一步步重构优化的,作为过来工作的经验总结分享给大家。 

1. 背景

得物音讯核心次要承接上游各业务的音讯推送申请,如营销推送、商品推广、订单信息等。音讯核心承受业务申请后,会依据业务需要去执行【音讯内容测验,防疲劳,防反复,用户信息查问,厂商推送】等节点,最初再通过各手机厂商及得物自研的在线推送通道触达用户。整体推送流程简化如下:

咱们心愿可能对各个节点提供精确的SLA监控指标和告警能力,从而实现对整体零碎的稳定性保障。上面是咱们设计的局部指标:

  • 监控指标

    • 节点推送量
    • 节点推送耗时
    • 节点耗时达标率
    • 整体耗时达标率
    • 节点阻塞量
    • 其余指标
  • 告警能力

    • 节点耗时告警
    • 节点阻塞量告警
    • 其余告警能力

那咱们如何实现对这些指标的统计呢?最简略的计划就是在每个节点的入口和进口减少统计代码,如下:

这就是咱们的 计划0 咱们用这种计划疾速落地了SLA-节点推送数量统计的开发。然而这个计划的毛病也很显著,思考以下几个问题:

  • 如何实现另一个SLA指标开发?比方节点耗时统计,也须要在每个节点的办法外部减少统计代码。
  • 如果有新的节点须要做SLA统计,怎么办?须要把所有的统计指标在新节点下面再实现一遍。如何防止SLA统计逻辑异样导致推送主流程失败?到处加try{}catch()去捕捉异样吗?
  • 如何分工?除了节点耗时统计外还有很多其余指标要实现。最简略的分工形式就是依照SLA指标来分工,各自领几个指标去开发,问题在于各个指标的统计逻辑都耦合在一起,依照统计指标来分工事实上变的不可能。
我的项目开发不好分工,通常意味着代码耦合度过高,是典型的代码坏滋味,须要及时重构。 

2. 痛点和指标

从下面的几个问题登程,咱们总结出 计划0 的几个痛点,以及咱们后续重构的指标。 

2.1 痛点

  • 监控节点不清晰。音讯推送服务波及多个不同的操作步骤。这些步骤咱们称之为节点。然而这些节点的定义并不明确,只是咱们团队外部约定俗成的一些概念。这就导致日常沟通和开发中有很多含糊空间。
在我的项目开发过程中,常常会碰到长时间的争执找不到解法。起因往往是大家对根底的概念了解没有买通,各说各话。这时候要果决进行争执,首先对基本概念给出统一的定义,而后再开始探讨。含糊的概念是高效沟通的死敌。
  • 保护艰难。

    • 每个节点的统计都须要批改业务节点的代码,统计代码扩散在整个零碎的各个节点,保护起来很麻烦;
    • 同时推送流程的次要逻辑被吞没在统计代码中。典型的代码如下,一个业务办法中可能有三分之一是SLA统计的代码。
protected void doRepeatFilter(MessageDTO messageDTO) {    //业务逻辑:防反复过滤    //...    //业务逻辑:防反复过滤    if (messageSwitchApi.isOpenPushSla && messageDTO.getPushModelList().stream()        .anyMatch(pushModel -> pushModel.getReadType().equals(MessageChannelEnums.PUSH.getChannelCode()))) {        messageDTO.setCheckRepeatTime(System.currentTimeMillis());        if (messageDTO.getQueryUserTime() > 0) {            long consumeTime = messageDTO.getCheckRepeatTime() - messageDTO.getQueryUserTime();            //SLA耗时统计逻辑            messageMonitorService.monitorPushNodeTimeCost(                MessageConstants.MsgTimeConsumeNode.checkRepeatTime.name(), consumeTime, messageDTO);        }    }}
  • 影响性能

    • SLA监控逻辑都在推送线程中解决,有些监控统计比拟耗时,升高了推送效率。
    • 统计代码会频繁的写Redis缓存,对缓存压力较大。最好是能把局部数据写入本地缓存,定时去合并到Redis中。
  • 难以扩大

    • 新的节点须要监控时,没方法疾速接入,须要到处复制监控逻辑。
    • 新的监控指标要实现的话,须要批改每个业务节点的代码。  

2.2 指标

理清问题之后,针对零碎既有的缺点,咱们提出了以下的重构指标:

  • 主流程的归主流程,SLA 的归 SLA。

    • SLA 监控代码从主流程逻辑中剥离进去,彻底防止SLA代码对主流程代码的净化。
    • 异步执行SLA 逻辑计算,独立于推送业务主流程,避免SLA异样拖垮主流程。
  • 不同监控指标的计算互相独立,防止代码耦合。
  • 实现SLA监控代码一次开发,到处复用。

    • 疾速反对新监控指标的实现。
    • 复用已有监控指标到新的节点,现实的形式是在节点办法上加个注解就能实现对该节点的统计和监控。 

3. 分步解题

3.1 节点定义

SLA 是基于节点来实现的,那么节点的概念不容许有含糊的空间。因而在重构开始之前,咱们把节点的概念通过代码的模式固定下来。

public enum NodeEnum {    MESSAGE_TO_PUSH("msg","调用推送接口"),    FREQUENCY_FILTER("msg","防疲劳"),    REPEAT_FILTER("push","防反复"),    CHANNEL_PUSH("push","手机厂商通道推送"),    LC_PUSH("push","自研长连推送")    //其余节点...    }

3.2 AOP

接下来思考解耦主流程代码和SLA监控代码。最间接的形式当然就是AOP了。

以上是节点耗时统计的优化设计图。把每个推送节点作为AOP切点,把每个节点上的SLA统计迁徙到AOP中对立解决。到这里,就实现了SLA代码和主流程代码的解耦。但这还只是万里长征第一步。如果有其余的统计逻辑要实现怎么办?是否要全副沉积在AOP代码外面? 

3.3 观察者模式

SLA有很多个统计指标。咱们不心愿把所有指标的统计逻辑都沉积在一起。那么如何进行解耦呢?答案是观察者模式。咱们在AOP切点之前收回节点进入事件(EnterEvent),切点退出之后收回节点退出事件(ExitEvent)。把各个指标统计逻辑形象成节点事件处理器。各个处理器去监听节点事件,从而实现统计指标间的逻辑解耦。 

3.4 适配器

这里还须要思考一个问题。各个节点的出参和入参都不统一,咱们如何能力把不同节点的出入参对立成event对象来散发呢?如果咱们间接在AOP代码中去判断切点所属的节点,并取出该节点的参数,再去生成event对象,那么AOP的逻辑复杂度会迅速收缩,并且须要常常变动。比拟好的形式是利用适配器模式。AOP负责找到切点对应的适配器,由适配器去负责把节点参数转为event对象,于是计划演变如下:

4. 整体计划

当初咱们对每个问题都找到了相应的解法,再把这些解法串起来,造成残缺的重构计划。重构之后,SLA逻辑流程如下:

到这里计划整体设计实现。在入手实现之前,还须要和计划相干成员同步设计方案,梳理潜在危险,并进行分工。波及到全流程的重构,光有纸面的计划,很难保障计划评估的完整性和有效性。咱们心愿可能验证计划可行性,尽早的裸露计划的技术危险,保障我的项目相干小伙伴对计划的了解没有大的偏差。这里举荐一个好的形式是提供计划原型。 

4.1 原型

计划原型是指对既有计划的一个最简略的实现,用于技术评估和计划阐明。 

正所谓talk is cheap, show me the code。对程序员来说,方案设计的再完满,也没有可运行的代码有说服力。咱们大略花了两个小时工夫基于现有设计疾速实现了一个原型。原型代码如下:

  • AOP切面类EventAop,负责把加强代码织入切点执行前后。
public class EventAop {    @Autowired    private EventConfig eventConfig;    @Autowired    private AdaptorFactory adaptorFactory;    @Around("@annotation(messageNode)")    public Object around(ProceedingJoinPoint joinPoint, MessageNode messageNode) throws Throwable {        Object result = null;        MessageEvent enterEvent = adaptorFactory.beforProceed(joinPoint, messageNode);        eventConfig.publishEvent(enterEvent);        result = joinPoint.proceed();        MessageEvent exitEvent = adaptorFactory.postProceed(joinPoint, messageNode);        eventConfig.publishEvent(exitEvent);        return result;    } }
  • 事件配置类EventConfig, 这里间接应用Spring event 播送器,负责散发event。 
public class EventConfig {    @Bean    public ApplicationEventMulticaster applicationEventMulticaster() { //@1        //创立一个事件播送器        SimpleApplicationEventMulticaster result = new SimpleApplicationEventMulticaster();        return result;    }    public void publishEvent(MessageEvent event) {        this.applicationEventMulticaster().multicastEvent(event);    }}
  • MessageEvent, 继承Spring event提供的ApplicationEvent类。
public class MessageEvent extends ApplicationEvent {}
  • 节点适配器工厂类, 获取节点对应的适配器,把节点信息转换为MessageEvent对象。
public class AdaptorFactory {    @Autowired    private DefaultNodeAdaptor defaultNodeAdaptor;    //反对切点之前产生事件    public MessageEvent beforeProceed(Object[] args, MessageNode messageNode) {        INodeAdaptor adaptor = getAdaptor(messageNode.node());        return adaptor.beforeProceedEvent(args, messageNode);    }    //反对切点之后产生事件    public MessageEvent afterProceed(Object[] args, MessageNode messageNode, MessageEvent event) {        INodeAdaptor adaptor = getAdaptor(messageNode.node());        return adaptor.postProceedEvent(args, event);    }    private INodeAdaptor getAdaptor(NodeEnum nodeEnum) {        return defaultNodeAdaptor;    }}

4.2 技术审查

在整体计划和原型代码的根底上,咱们还须要审查计划中所用的技术,是否有危险,评估这些危险对既有性能,分工排期等的影响面。比方咱们这边用到的次要是Spring AOP, Spring Event机制,那么他们可能潜在以下问题,须要在开发前就做好评估的:

  • Spring  AOP的问题:Spring AOP中公有办法无奈加强。bean本人调用本人的public办法也无奈加强。
  • Spring Event的问题:默认事件处理和事件散发是在同一个线程中运行的,实现时须要配置Spring事件线程池,把事件处理线程和业务线程分隔开。 
潜在的技术问题要充沛沟通。每个成员的技术背景不同,你眼里很简略的技术问题,可能他人半天就爬不进去。计划设计者要充沛预知潜在的技术问题,提前沟通,防止无谓的问题排查,进而晋升开发效率。 

4.3 老本收益剖析

  • 老本 

一套残缺的计划思考的不仅仅是技术可行性,还须要思考实现老本。咱们通过一个表格来简略阐明我此次重构前后的老本比照。

  • 收益

    • 代码清晰。SLA统计逻辑和流程逻辑解耦。SLA各个指标的统计齐全解耦,互不依赖。
    • 晋升开发效率。SLA指标统计一次开发,到处复用。只有在须要监控的代码上加上节点注解。
    • 进步性能。SLA逻辑在独立的线程中执行,不影响主流程。
    • 进步稳定性。SLA逻辑和主流程解耦,SLA频繁变更也不影响主流程代码,也不会因为SLA异样拖垮主流程。
    • 不便分工排期。重构也解决了不好分工的难题。因为各个指标通过重构实现了逻辑隔离,实现时齐全能够独立开发。因而咱们能够简略的依照SLA统计指标来安顿分工。
代码重构最难的不是技术,而是决策。决定零碎是否要重构,何时重构才是最难的局部。往往一个团队会破费大量工夫去纠结是否要重构,然而到最初都没人敢做出最终决策。之所以难是因为不足决策资料。能够思考引入老本收益表等决策工具,对重构进行定性、定量分析,帮忙咱们决策。 

5. 避坑指南

实现的过程也碰到的一些比拟有意思的坑,上面列出来供大家参考。 

5.1 AOP生效

Spring AOP应用cglib 和jdk动静代理实现对原始bean对象的代理加强。不论是哪种形式,都会为原始bean对象生成一个代理对象,咱们调用原始对象时,实际上是先调用代理对象,由代理对象执行切片逻辑,并用反射去调用原始对象的办法,实际上运行如下代码。

public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) throws Throwable {      //应用反射调用原始对象的办法     ReflectionUtils.makeAccessible(method);     return method.invoke(target, args);}

这时候如果被调用办法是原始对象自身的办法,那就不会调用代理对象了。这就导致代理对象逻辑没有执行,从而不触发代码加强。具体原理参考以下例子,假如咱们有一个服务实现类AServiceImpl,提供了method1(), method2()两个办法。其中method1()调用了method2()。

@Service("aService")public class AServiceImpl implements AService{    @MessageNode(node = NodeEnum.Node1)    public void method1(){        this.method2();    }    @MessageNode(node = NodeEnum.Node2)    public void method2(){    }}

咱们的AOP代码通过@MessageNode注解作为切点织入统计逻辑。

@Component("eventAop")@Aspect@Slf4jpublic class EventAop {    @Around("@annotation(messageNode)")    public Object around(ProceedingJoinPoint joinPoint, MessageNode messageNode) throws Throwable {        /**            节点开始统计逻辑...        **/        //执行切点        result = joinPoint.proceed();        /**            节点完结统计逻辑...        **/        return result;    }   }

Spring启动的时候,IOC容器会为AserviceImpl生成两个实例,一个是原始AServiceImpl对象,一个是加强过后的ProxyAserviceImpl对象,办法method1的调用过程如下,能够看到从method1()去调用method2()办法的时候,没有走代理对象,而是间接调用了指标对象的method2()办法。

5.1.1 解决办法

  • 注入代理对象aopSelf,被动调用代理对象aopSelf的办法。

示例代码如下:

@Service("aService")public class AServiceImpl implements AService{    @Autowired    @Lazy    private AService aopSelf;    @MessageNode(node = NodeEnum.Node1)    public void method1(){        aopSelft.method2();    }    @MessageNode(node = NodeEnum.Node2)    public void method2(){    }}
  • 以上办法治标不治本。咱们从头探索为何会呈现自调用的代码加强?起因是咱们要对两个不同的节点进行SLA统计加强。然而这两个节点的办法定义在同一个Service类当中,这显然违反了编码的繁多性能准则。因而更好的形式应该是形象出一个独自的类来解决。代码如下:
@Service("aService")public class AServiceImpl implements AService{    @Autowired    private BService bService;    @MessageNode(node = NodeEnum.Node1)    public void method1(){        bService.method2();    }}@Service("bService")public class BServiceImpl implements BService{    @MessageNode(node = NodeEnum.Node2)    public void method2(){    }}
重构可能帮忙咱们发现并且定位代码坏滋味,从而领导咱们对坏代码进行从新形象和优化。 

5.2 通用依赖包

实现中碰到的另一个问题,是如何提供通用依赖。因为音讯核心内局部很多不同的微服务,比方咱们有承接内部业务推送申请的Message服务,还有把业务申请转发给各个手机厂商的Push服务,还有推送达到后,给得物App打上小红点的Hot服务等等,这些服务都须要做SLA监控。这时候就须要把现有的计划形象出公共依赖,供各服务应用。 

咱们的做法是把【节点定义,AOP配置,Spring Event配置,节点适配器接口类】形象到common依赖包,各个服务只须要依赖这个common包就能够疾速接入SLA统计能力。 

这里有一个比拟有意思的点,像【AOP配置,Spring Event配置, 节点适配器】这些Bean的配置,是要凋谢给各个服务本人配置,还是间接在common包里默认提供配置?比方上面的这个Bean配置,决定了Spring Event解决线程池的要害配置项,如外围线程数,缓存池大小。

@Beanpublic ThreadPoolExecutorFactoryBean applicationEventMulticasterThreadPool() {    ThreadPoolExecutorFactoryBean result = new ThreadPoolExecutorFactoryBean();    result.setThreadNamePrefix("slaEventMulticasterThreadPool-");    result.setCorePoolSize(5);    result.setMaxPoolSize(5);    result.setQueueCapacity(100000);    return result;}

这个配置交给各个服务本人治理,灵活性会高一点,但同时意味着服务对common包的应用老本也高一点,common包使用者须要本人去决定配置内容。绝对的,在common包中间接提供呢,灵活性升高,然而用起来不便。前面参照 Spring Boot 约定大于配置的设计规范,还是决定间接在common包中提供配置,逻辑是各个服务对这些配置的需要不会有太大差异,为了这点灵活性晋升应用老本,不是很有必要。当然如果后续有服务的确须要不同的配置,还能够依据实际需要灵便反对。

约定大于配置,也能够叫做约定优于配置(convention overconfiguration),也称作按约定编程,是一种软件设计范式,指在缩小软件开发人员需做决定的数量,取得简略的益处,而又不失灵活性。

咱们都晓得Spring, Spring Boot的理念很先进,而实际中可能借鉴先进理念领导开发实际也算是一种工程人员的幸福,正如孔老夫子所说:就有道而正焉,堪称好学矣。 

5.3 业务后果

代码实现之后,又进行了性能压测,线上灰度公布,线上数据察看等等步骤,在这里就不再赘述。那么SLA技术演进到这里,为咱们的推送业务获取了哪些业务后果呢?上面提供比拟典型的后果。 

音讯推送服务会调用手机厂商的推送服务接口,咱们要监控厂商推送接口的耗时性能怎么办呢?在重构之前,咱们须要在各个厂商推送接口之前和之后减少统计代码,计算耗时并写入缓存。在重构之后,咱们要做的,只是简略的增加一个节点注解即可实现。比方咱们想统计OPPO推送接口的SLA指标,只需增加如下注解:

@MessageNode(node = NodeEnum.OPPO_PUSH, needEnterEvent = true)public MessageStatisticsDTO sendPush(PushChannelBO bo) {    if (bo.getPushTokenDTOList().size() == 1) {        return sendToSingle(bo);    } else {        return sendToList(bo);    }}

而后咱们在控制台就能很快发现OPPO推送的统计信息。比方咱们看管制台上OPPO推送瓶颈耗时20s,那阐明OPPO的推送连贯必定有超时,连接池的配置须要优化。

除了监控指标,咱们还反对实时告警,比方上面这个节点阻塞告警,咱们可能及时发现零碎中沉积多的节点,迅速排查是否节点有卡顿,还是上游调用量猛增,从而把潜在的线上问题扼杀在摇篮之中。

6. 展望未来

SLA上线一周之内,咱们就曾经依赖这套技术发现零碎中的潜在问题,然而事实上对SLA的业务收益咱们齐全能够有更大的设想空间。比如说:

  • 咱们目前监控的次要还是技术上的指标,像节点耗时,节点阻塞等。后续咱们能够把业务相干的统计也反对上,咱们能够迅速定位是哪个业务方的调用导致系统阻塞等等业务性能数据。
  • 其次咱们能够统计各业务推送的ROI数据,目前音讯服务反对数亿的音讯,然而这外面哪些推送收益高,哪些收益低目前是没有明确的指标的。咱们能够收集各业务的推送量,点击量等信息去计算业务推送的ROI指标。
  • 当咱们有了业务性能数据,业务ROI指标,咱们就有机会对业务推送做精细化的管控,比方明天推送资源缓和,我是否能够暂缓低ROI的业务推送。比方高ROI推送明天曾经触达过用户,咱们是否能够勾销当天的相似推送,避免打搅用户等等。 

这些都是音讯核心SLA可能为业务进行推送赋能的方向,而且这些方向能够基于目前的SLA技术架构迅速低成本的落地,真正实现技术服务于业务,技术推动业务。 

7. 总结

以上是音讯核心SLA重构演进的整个过程。对于音讯服务来说,SLA的开发没有止境,咱们要继续关注零碎的外围指标,不断完善监控工具。正因为如此,咱们更须要夯实SLA的技术根底,在灵便轻量的技术底座下来实现更简单的性能。回过头看这个重构过程,咱们也总结了以下一些教训供大家参考。

  • 不要胆怯重构,也不要适度设计。重构的目标不在于炫技,而在于解决理论问题,进步开发效率。
  • 要有原型。简单的设计往往难以发展,解决办法是从最小实际开始。计划原型就是方案设计的最小实际,有了原型之后再审查设计,演进计划会不便很多。
  • 对技术充沛掌控。预知所用技术的潜在危险,保障有足够的技术能力去解决。并且要把危险提前裸露给团队成员,缩小踩坑几率,防止无谓的开发调试老本。

 *文/吴国锋
@得物技术公众号