apm监控得有多少细节啊🤔

1,976 阅读8分钟

前言


image.png

被掘金的运营小姐姐吐槽写了很多非技术内容,哈哈,今天正经一回梳理一下apm监控里面有多少细节,当聊起apm自研算很经典,一般公司都会去自研apm,它还是相对比较简单,但是里面蕴藏了蛮多细节,而往往这些细节决定着这个系统的性能、稳定性。

接下来一起梳理一下apm都有哪些细节吧~

apm结构图


image.png

容我偷个懒,基本架构就是如图所示。

生产日志过程:从左到右,apm面向对象是服务节点、网关、中间件。

收集日志过程:在每一步都需要做对应操作处理,比如说带上traceid、spanid、parentid、时间、来源、类型对吧,通过logback配置丢到中间的话是kafka,因为人家快呀,比如说我们有多套环境,3套甚至更多套环境一个apm,那么这个日志量是惊人的,而且生产速度也是极快的,比如某些新手上路写了死循环一直打印日志,很容易把日志堆满对吧。

解析、储存、分析日志过程:日志很多,那么它需要进行分类,然后储存起来,至于分析可以用离线方式,比如服务异常情况、用户上报数据。

apm日志类型

  • 运行日志

一般应用都会打印日志,比如请求参数、响应参数、异常堆栈,目的是为了方便排查问题。

  • 链路日志

目的是什么呢?让整个请求链路清晰,比如说我想去广州珠江新城,从某某点出发,然后经过哪些地铁站,出了哪个口,坐了什么公交车,经过几分钟,到达目的地。这是第一步对链路清晰收集、展示,第二步,对链路优化,比如说整条链路合不合理?有时一个数据绕来绕去,是不是应该合并一下,或者当初设计不妥当;另外mysql执行比较慢优化一下,接口请求慢,应用响应正常的,是不是网络波动,如果没有链路无从下手~

  • 启动日志

当然这个是附加的东西,比如说应用接口情况,哪些接口下线了,跟我们自动化测试结合起来,接口下线需要重写,或者参数变了需要预警,也是流量录制基础,接口的数据类型、返回数据类型、参数个数等等。

  • 请求日志

这个是为了分析接口的情况,当然了你可以用运行日志或者链路日志来代替,但是在我反复思考中,应该把它拆出来,功能更加的清晰。

分析什么呢,接口异常情况,当前节点qps、每天请求数量、接口慢情况,应用整体情况,为后面的预警打好基础。

apm细节

1、队列控制中间件插入速度,也是在保护对应中间件

在kafka->apm->es中间,加了一层缓存,用的是队列,因为kafka消费速度跟es插入速度不是对等,需要再搞一层缓冲,一开始有的同学设计是运行日志放队列缓存,其他日志直接塞es,因为运行日志是一个占大头的日志类型。

我做了一层优化,讲4种日志全部放在队列里面,因为如果很多地方调用es,那么你无法限制es插入速率的对吧,所以大家都过来队列里面,统一消费,我是怎么设计的呢?

参考rpc、xxl-job相关设计,快慢队列,这个设计他们初衷是当接口快速响应的时候,应该往快队列存放,让机器性能给它,然后慢队列给少点资源,这样整个节点吞吐量、处理情况会比较好。

伪代码

两个阻塞队列,命名为快队列、慢队列,快队列存链路日志、运行日志,慢队列存那些用户数据上报、启动日志这些扩展性日志,取的时候按6:4,比如说es每次插入数量量500,快队列拿300个,慢队列取出200个,这就是500个。

如果快队列只有200个,那么300-200=100,意味着慢队列取出200+100=300,给慢队列提个速。

/**
 * 快慢队列
 */
@Slf4j
public class FastSlowQueue {

    private BlockingQueue<LogDataVO> fastQueue;
    private BlockingQueue<LogDataVO> slowQueue;

    public BlockingQueue<LogDataVO> getFastQueue() {
        return fastQueue;
    }

    public BlockingQueue<LogDataVO> getSlowQueue() {
        return slowQueue;
    }

    /**
     * 缓存队列是否能操作
     */
    private boolean operationEnable = true;


    /**
     * 保存队列大小,单次保存数据的大小
     */
    private int PUSH_SIZE;

    public int getPushSize() {
        return PUSH_SIZE;
    }

    public void setOperationEnable(boolean operationEnable) {
        this.operationEnable = operationEnable;
    }

    public void setPushSize(int pushSize) {
        //当前消费范围控制在200-800之间
        if (pushSize <= 200) {
            pushSize = 200;
        } else if (pushSize >= 800) {
            pushSize = 800;
        }

        this.PUSH_SIZE = pushSize;
    }

    /**
     * 最大队列数量
     */
    private final int maxQueueSize = 1000000;

    public FastSlowQueue(int pushSize) {
        PUSH_SIZE = pushSize;
        if (fastQueue == null || slowQueue == null) {
            synchronized (FastSlowQueue.class) {
                if (fastQueue == null || slowQueue == null) {
                    fastQueue = new LinkedBlockingQueue<>(maxQueueSize);
                    slowQueue = new LinkedBlockingQueue<>(maxQueueSize);
                }
            }
        }
    }

    public boolean addToFastQueue(List<LogDataVO> dataList) {
        if (!operationEnable) {
            return false;
        }

        if (fastQueue.size() + dataList.size() > maxQueueSize) {
            log.warn("临时队列超限,丢弃日志,日志内容:{}", dataList);
            return false;
        }

        fastQueue.addAll(dataList);
        return true;
    }

    public boolean addToSlowQueue(List<LogDataVO> dataList) {
        if (!operationEnable) {
            return false;
        }

        if (slowQueue.size() + dataList.size() > maxQueueSize) {
            log.warn("临时队列超限,丢弃日志,日志内容:{}", dataList);
            return false;
        }

        slowQueue.addAll(dataList);
        return true;
    }

    public int getSize() {
        return fastQueue.size() + slowQueue.size();
    }

    /**
     * 批量拿到快慢队列数据
     *
     * @return
     */
    public List<LogDataVO> getBatchLogList() {
        List<LogDataVO> saveList = new ArrayList<>();

        //按照6:4比例拿取数据
        int fastSpeed = (int) (PUSH_SIZE * 0.6);
        int slowSpeed = (int) (PUSH_SIZE * 0.4);

        if (fastQueue.size() >= fastSpeed) {
            fastQueue.drainTo(saveList, fastSpeed);
        } else {
            fastQueue.drainTo(saveList, fastSpeed);
            //只有当快队列数据少了之后才会开始加大慢队列的获取速度
            slowSpeed += fastSpeed - fastQueue.size();
        }

        slowQueue.drainTo(saveList, slowSpeed);

        return saveList;
    }

}

2、监控

一谈到队列,就会有性能问题,比如说队列满了怎么办,丢弃吗,其实它需要一个监控来告诉你我当前的设计合不合理,而不是自己yy,定期打印队列核心数据,异常情况进行预警。当然也能智能调节数据在合理的范围,引出下一点,比如说每次插入500,这个值合不合理呢?

3、动态调整消费速率

我搞了一个滑动窗口类似的,统计日志生产速率,如果10秒里面生产速率提高了,那么我消费的速度也要加一加对吧,如果低了可以缓一缓。然后它可以无限提高吗,同样也会影响中间件性能,所以我给它定了200-800之间的滑动。

4、链路日志没有起到效果

我敏锐的发现我们开发同学平时用运行日志占绝大多数,因为要排除异常,链路日志好像就没没有卵用了。我一想问题在哪里呢?这就需要回到链路日志最初的设计初衷,链路清晰、性能优化。

我们的apm目前只是实现了像http、rpc请求的链路,大致上可以摸清,像mysql其实是另一个大头,包括慢查询的依据、代码优化都需要借助链路日志,所以我抽空将mysql的链路日志:请求的sql、参数、响应的数据打印出来。

5、细节中细节

是不是什么东西照打到apm呢?当然不是,如果超过一定长度,需要截断,这是对中间件的保护,占用太多空间了不好,而且很多数据都是没有什么用的,打印那么多干嘛对吧~

6、链路设计

这是后面补到一点,相信大家对apm链路设计蛮熟悉的吧,

看下以下这幅图,懒得美化了徒手咔咔画出来了。

image.png

常规设计左边这样对吧,跟我们文章子目录一样,一个大点一个小点,这样比较清晰的,链路也是有两条1->2->2.1,2.1代表在2这个节点里面分操作,而不是跳到另一个服务,那么在我们自研apm中不是这样设计的,同时也给我挖了个坑,因为前面提到我后面补了个mysql链路日志。

怎么设计的呢?右边这样,相当于每个操作都是一个新的节点,不管你是张三李四,来了咔咔给你一个新的标识,其实也可以整一个链路,1->2->3,然后带上对应的类型,比如说这是mysql操作,还有feign操作,链路还是能串联起来的,至于怎么看出是哪一层的?

只能这个节点往上找,如果是应用级别打印出来的,那就归属这一层的。

apm需要考虑什么

1、 性能

这个是不用讲的,kafka性能、es性能能否撑住这么大体量大的日志体系,这也需要压测,然后评估个2-3倍的量,kafka几个分区、几个消费者、apm服务几个节点。

2、内存

apm一天打满日志占多少g,因为很多中间件是买的,自己搭建很难有那个性能的,也耗费精力,那么买的时候要多少内存鸭?日志一直保存还是有过期机制对不对。

最后分享下最近的美景


image.png

希望我的文章对各位读者有所启发,谢谢观看~