• 统计接口QPS


      现在记录话单的时候想加一个参数:每秒接口调用的并发量,也就是所谓的QPS(Queries per second)。QPS即每秒请求数,是对一个特定的接口在规定时间内请求流量的衡量标准。那么如何实现QPS的计算呢?我想到的是两种方案:

      1、一定时间内(比如一分钟)的请求总量/统计时间段(比如一分钟),最终得出就是每秒的并发量,它是基于某一段时间来统计的

      2、直接统计一秒钟内的请求总量,就是按每秒的时间段来统计,简单粗暴

      方案一的适用场景应该是报表、运维统计之类的,只关心QPS曲线;如果用来做并发量校验,明显只能用方案二,需要实时获取QPS。那么如何统计一秒内的并发量?假设某一个时间点有接口到来,那么就开始统计该接口,在一秒之内,来多少个累加多少次。一秒之后,统计数清零。之后的某一个时间点,又有接口到来,又开始统计一秒之内的接口调用量,如此循环往复。

      那么如何维护一个一秒之内的接口计数器呢?我觉得失效缓存是一个合适的选择,缓存的键即为接口名,值就是接口统计数,过期时间一秒。为了避免引入第三方中间件,我们自己实现该过期缓存,需要维护一个定时器和一个优先级队列,每秒清理一次队列中已过期的缓存。

      废话说完了,看代码:

      1、缓存的值

    import lombok.Getter;
    import lombok.Setter;
    
    import java.util.concurrent.atomic.AtomicLong;
    
    /**
     * 内部类,缓存对象,按失效时间排序,越早失效越前
     * @author wulf
     * @since 20200422
     */
    @Getter
    @Setter
    public class CacheNode implements Comparable<CacheNode> {
        private String key;
        private AtomicLong callQuantity;
        private long expireTime;
    
        public CacheNode(String key, AtomicLong callQuantity, long expireTime) {
            this.key = key;
            this.callQuantity = callQuantity;
            this.expireTime = expireTime;
        }
    
    
        @Override
        public int compareTo(CacheNode o) {
            long dif = this.expireTime - o.expireTime;
            if (dif > 0) {
                return 1;
            } else if (dif < 0) {
                return -1;
            }
            return 0;
        }
    }

      2、过期缓存:

    import com.wlf.bean.CacheNode;
    
    import java.util.Map;
    import java.util.PriorityQueue;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicLong;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 带过期时间的缓存
     *
     * @author wulf
     * @since 2020/04/21
     */
    public class ExpiredCache {
    
        // 缓存key=接口名,value=接口调用量、过期时间戳
        private Map<String, CacheNode> cache = new ConcurrentHashMap<>();// 重入锁
        private ReentrantLock lock = new ReentrantLock();
    
        // 失效队列
        private PriorityQueue<CacheNode> queue = new PriorityQueue<>();
    
        // 启动定时任务,每秒清理一次过期缓存
        private final static ScheduledExecutorService scheduleExe = new ScheduledThreadPoolExecutor(10);
    
        // 构造函数中启动定时任务,执行对已过期缓存的清理工作,每秒执行一次
        public ExpiredCache() {
            scheduleExe.scheduleAtFixedRate(new CleanExpireCacheTask(), 1L, 1L, TimeUnit.SECONDS);
        }
    
        /**
         * 内部类,清理过期缓存对象
         */
        private class CleanExpireCacheTask implements Runnable {
    
            @Override
            public void run() {
                long currentTime = System.currentTimeMillis();
                // 取出队列中的队头元素,对已过期的元素执行清除计划,剩下没有过期则退出
                while (true) {
                    lock.lock();
                    try {
                        CacheNode cacheNode = queue.peek();
                        // 已经把队列清空了,或者所有过期元素已清空了,退出
                        if (cacheNode == null || cacheNode.getExpireTime() > currentTime) {
                            return;
                        }
    
                        // 开始大清理了
                        cache.remove(cacheNode.getKey());
                        queue.poll();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
    
        /**
         * 根据缓存key获取values
         *
         * @param cacheKey
         * @return
         */
        public CacheNode getCacheNode(String cacheKey) {
            return cache.get(cacheKey);
        }
    
        /**
         * 加入缓存,设置存活时间
         *
         * @param cacheKey
         * @param ttl      缓存的存活时间
         *                 return
         */
        public AtomicLong set(String cacheKey, long ttl) {
    
            // 若缓存中已存在缓存节点,不需要更新过期时间,仅更新QPS值
            CacheNode oldNode = cache.get(cacheKey);
            if (oldNode != null) {
                AtomicLong oldQps = oldNode.getCallQuantity();
                oldQps.incrementAndGet();
            } else {
                // 否则新创建CacheNode对象,失效时间=当前时间+缓存存活时间
                AtomicLong qps = new AtomicLong(1);
                CacheNode newNode = new CacheNode(cacheKey, qps, System.currentTimeMillis() + ttl * 1000);
    
                // 放入缓存,加入过期队列
                cache.put(cacheKey, newNode);
                queue.add(newNode);
            }
            return cache.get(cacheKey).getCallQuantity();
        }
    
    }

      3、在切面中统计接口QPS:

    package com.wlf.cdr;
    
    import com.wlf.javabean.ots.TranslateCdr;
    import com.wlf.utils.ExpiredCache;
    import com.wlf.utils.IPUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    @Slf4j
    @Aspect
    @Component
    public class CdrAsept {
        private final static SimpleDateFormat SF = new SimpleDateFormat("yyyyMMddHHmmss");
    
        // 话单格式:接口名称|话单记录时间|接口时延|调用方IP|本地IP|用户ID|用户名|源语言|目标语言|结果码|QPS
        private final static String CDR_FORMAT = "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}";
    
        // 过期缓存
        private ExpiredCache expiredCache = new ExpiredCache();
    
        @Around("execution(* com.wlf.translateprovider.controller.TranslateController.*(..))")
        public Object recordCdr(ProceedingJoinPoint joinPoint) throws Throwable {
    
            long startTime = System.currentTimeMillis();
            String startDate = SF.format(new Date(startTime));
    
            // 白名单校验
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest httpServletRequest = attributes.getRequest();
            String localIp = IPUtil.getLocalIp();
            String remoteIp = IPUtil.getRemoteIp(httpServletRequest);
            TranslateCdr cdr = new TranslateCdr();
            cdr.setRemoteIp(remoteIp);
            CdrThreadLocal.setTranslateCdr(cdr);
    
            // 获取接口名
            String requestPath = httpServletRequest.getRequestURI();
            String cacheKey = requestPath.substring(requestPath.lastIndexOf("/") + 1, requestPath.length());
    
            // 设置过期时间为1秒
            long qps = expiredCache.set(cacheKey, 1).get();
    
            Object result = joinPoint.proceed();
    
            long endTime = System.currentTimeMillis();
            cdr = CdrThreadLocal.getTranslateCdr();
            if (cdr != null) {
                log.error(CDR_FORMAT, cacheKey, startDate, endTime - startTime, remoteIp, localIp, cdr.getUserId(),
                        cdr.getUserName(), cdr.getFrom(), cdr.getTo(), cdr.getResultCode(), qps);
            }
            CdrThreadLocal.delThreadLocal();
            return result;
        }
    }

      在切面中只需set一下,如果这时缓存有数据,就累加统计数,没有就设置统计数为1,再get出来的得到QPS。但这里为了兼顾吞吐量,让接口的调用不受QPS统计的影响,并没有在切面或者过期缓存的set方法加锁,因此对两个并发时间很短的接口,统计数会相同。

  • 相关阅读:
    JDBC 删除数据两种方式,PreparedStatement表示预编译的 SQL 语句的对象,防止sql注入
    MySQL 主键外键
    JDBC 增删改查
    MySQL数据库语句
    反射
    如何安装、配置、登陆MySQL
    如何干净卸除MySQL数据库
    IO流总结
    字符流五种读写 字符流 BufferedWriter BufferedReader 带缓冲区的字符流
    单例模式
  • 原文地址:https://www.cnblogs.com/wuxun1997/p/12753548.html
Copyright © 2020-2023  润新知