架构(十七)翻译监控

一、引言

作者最近做的一个功能是需要监控一个翻译转换,根据国家和语言进行分组,然后定时把监控情况放到ck里面。为什么是分组和定时监控呢?因为调用比较高的系统的qps在单机一万多,70台机器,可怕的高频调用注定他不能实时分析。

二、方案设计

1、需求

先明确一下功能点,翻译监控很多读者可能不太理解,大家可以类比redis缓存监控,只不过由于高频不能做实时监控,要根据国家、语言把redis缓存进行分组监控,方便监控一段时间内的国家语言维度对应key的缓存访问频率。

2、分析

首先要增加切点,因为这种监控肯定要方便发布、无需更新pom,所以要在字节码增强里面做

要实现这样的监控,首先要有一个存储,把国家+语言+key作为唯一键,存储他的频率,这种肯定是需要一个map了。

还需要定时将map存储的数据发送到ck,那就需要开启一个可配置频率的定时任务。定时任务的开启也是一个问题,因为在javaagent里面使用不了Spring的定时包,原因之前的文章说过。所以必须使用jdk原生的工具,jdk倒是使用延时队列和lock阻塞实现了一个ScheduledThreadPoolExecutor。

使用ScheduledThreadPoolExecutor要注意它的无界队列,但是他没有提供更改队列数量的方法,那我们有两个方案:一个是使用信号量进行阻塞,防止大量任务进入队列;另外一种是封闭这个线程池,只在开启一次,队列不进行二次任务进入

这里就带来了并发问题,map在被定时任务读取发送到ck的时候,还有高频的写入操作,加锁就太影响性能了。那么有什么巧妙的设计可以规避吗?可以设置两个map,一个用来读一个用来写,在定时任务读取的时候设置标志位,切换map的使用。

三、代码

分析完了还是要在代码中实践,会发现更多问题

1、map存储

使用了一个AtomicBoolean标识目前使用的map

java 复制代码
public class MonitorMapHandleUtils {
    private static final ConcurrentHashMap<String, Integer> firstMap = new ConcurrentHashMap<>();

    private static final ConcurrentHashMap<String, Integer> secondMap = new ConcurrentHashMap<>();


    private static AtomicBoolean useFirst = new AtomicBoolean(true);

    private static final String SEMICOLON = ";";

    /**
     * country+ key + locale维度分组
     * 
     * @param key
     */
    public static void pushMap(Object key, Object locale, String country) {

        String key = country + SEMICOLON + key + SEMICOLON + locale;
        if (useFirst.get()) {
            firstMap.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
            return;
        }
        secondMap.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    }

    public static void runSendMap() {
        if (useFirst.get()) {
            sendMap(firstMap);
            return;
        }
        sendMap(secondMap);
    }

    /**
     * 控制分钟级别,不会有useFirst连续设置的风险
     * 
     * @param map
     */
    protected static void sendMap(ConcurrentHashMap<String, Integer> map) {
        useFirst.set(!useFirst.get());
        map.forEach((k, v) -> MonitorMapHandleUtils.sendCk(k, v));
        map.clear();
    }

    private static void sendCk(String key, Integer count) {
    }
}

2、定时发送

这里其实是不断的在取延时多久执行,配置中心都是推送客户端缓存的,所以这里的检查不会有多少性能损耗

使用start开启定时任务,可以思考下为什么要这样做。比如有的同事说为什么不在static静态块里面处理?

首先即使在静态块处理,这个类也必须是主动使用的,否则不会被按需加载,类的主动使用包括以下几种情况:

java 复制代码
创建类的实例。

访问类的静态方法。

访问类的静态字段,除了声明为final的字段,它们是编译时常量。

使用java.lang.reflect包的方法对类进行反射调用。

初始化一个类的子类(首先会初始化父类)。

加载的时候必须保证所需的包已经加载好了,这里有什么操作?配置中心的首次检查,更新延时时间,所以我们必须让这个定时类的方法被调用,而且是在配置中心包加载好之后才能调用。

这样的话就要把MonitorMapHandleUtils使用的变量或者方法放在MonitorDynamicScheduledTask,然后调用,这样才能保证配置中心一定是已经加载好的,这取决于调用的时机,只有被调用到的时候这些类才会按需加载,这样代码结构看起来就会比较诡异,互相依赖

定时类只放开了一个方法,并且调用一次之后就不再允许处理,避免多次调用,所以就不会有无界队列过多任务的风险

java 复制代码
public class MonitorDynamicScheduledTask {

    private static final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
    /**
     * 初始延迟时间,单位为分钟
     */
    private static volatile int currentDelay = 240;

    private static AtomicBoolean startFlag = new AtomicBoolean(false);

    /**
     * 开启仅执行一次,并发执行多次也没关系,瞬时不影响
     */
    public static void start() {
        if (startFlag.get()) {
            return;
        }
        MonitorDynamicScheduledTask.checkDelay();
        scheduleTask(currentDelay);
        startFlag.set(true);
    }

    /**
     * 进入队列,不会有递归调用的方法栈问题
     * 
     * @param delayInHours
     */
    private static void scheduleTask(int delayInHours) {
        scheduler.schedule(() -> {
            if (Config.monitorRun()) {
                MonitorMapHandleUtils.runSendMap();

                MonitorDynamicScheduledTask.checkDelay();

                // 重新调度下一次执行
                MonitorDynamicScheduledTask.scheduleTask(currentDelay);
            }
        }, delayInHours, TimeUnit.MINUTES);
    }

    private static void checkDelay() {
        // 检查配置中心是否有更新
        int newDelay = Config.monitorInterval();
        if (newDelay != currentDelay && newDelay > 0) {
            currentDelay = newDelay;
        }
    }
}

四、总结

翻译监控不难,难的是在agent无侵入的情况下去监控,要考虑性能损耗、类加载时机、包加载时机,即使是简单的并发也要考虑不能用锁造成性能损耗

相关推荐
Daniel 大东5 分钟前
BugJson因为json格式问题OOM怎么办
java·安全
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸5 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象6 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了6 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·6 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic7 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王7 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康7 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神7 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式