架构(十七)翻译监控

一、引言

作者最近做的一个功能是需要监控一个翻译转换,根据国家和语言进行分组,然后定时把监控情况放到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无侵入的情况下去监控,要考虑性能损耗、类加载时机、包加载时机,即使是简单的并发也要考虑不能用锁造成性能损耗

相关推荐
结衣结衣.10 分钟前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
原野心存13 分钟前
java基础进阶知识点汇总(1)
java·开发语言
无理 Java1 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc
gobeyye1 小时前
spring loC&DI 详解
java·spring·rpc
鱼跃鹰飞1 小时前
Leecode热题100-295.数据流中的中位数
java·服务器·开发语言·前端·算法·leetcode·面试
我是浮夸1 小时前
MyBatisPlus——学习笔记
java·spring boot·mybatis
TANGLONG2221 小时前
【C语言】数据在内存中的存储(万字解析)
java·c语言·c++·python·考研·面试·蓝桥杯
杨荧1 小时前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源
Leighteen2 小时前
ThreadLocal内存泄漏分析
java
java6666688882 小时前
Java中的对象生命周期管理:从Spring Bean到JVM对象的深度解析
java·jvm·spring