架构(十七)翻译监控

一、引言

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

相关推荐
RainbowSea10 小时前
12. LangChain4j + 向量数据库操作详细说明
java·langchain·ai编程
RainbowSea10 小时前
11. LangChain4j + Tools(Function Calling)的使用详细说明
java·langchain·ai编程
考虑考虑14 小时前
Jpa使用union all
java·spring boot·后端
用户37215742613515 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊16 小时前
Java学习第22天 - 云原生与容器化
java
稻草人222217 小时前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构
渣哥17 小时前
原来 Java 里线程安全集合有这么多种
java
间彧17 小时前
Spring Boot集成Spring Security完整指南
java
间彧18 小时前
Spring Secutiy基本原理及工作流程
java
数据智能老司机18 小时前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构