发现一个开源项目优化点,点进来就是你的了

hello,大家好呀,我是小楼。

最近无聊(摸)闲逛(鱼)github时,发现了一个阿里开源项目可以贡献代码的地方。

不是写单测、改代码格式那种,而是比较有挑战的 性能优化,最关键的是还不难,仔细看完本文后,有点基础就能写出来的那种,话不多说,发车!

发现一个开源项目优化点,点进来就是你的了

相信大家在日常写代码获取时间戳时,会写出如下代码:

long ts = System.currentTimeMillis();

读者中还有一些Gopher,我们用Go也写一遍:

UnixTimeUnitOffset = uint64(time.Millisecond / time.Nanosecond)
ts := uint64(time.Now().UnixNano()) / UnixTimeUnitOffset

在一般情况下这么写,或者说在99%的情况下这么写一点问题都没有,但有位大佬研究了Java下时间戳的获取:

http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html

他得出了一个结论:并发越高,获取时间戳越慢!

发现一个开源项目优化点,点进来就是你的了

发现一个开源项目优化点,点进来就是你的了

具体到细节咱也不是很懂,大概原因是由于只有一个全局时钟源,高并发或频繁访问会造成严重的争用。

缓存时间戳

我最早接触到用缓存时间戳的方式来优化是在Cobar这个项目中:

https://github.com/alibaba/cobar

由于Cobar是一款数据库中间件,它的QPS可能会非常高,所以才有了这个优化,我们瞅一眼他的实现:

  • 起一个单独的线程每隔20ms获取一次时间戳并缓存起来
  • 使用时间戳时直接取缓存

https://github.com/alibaba/cobar/blob/master/server/src/main/server/com/alibaba/cobar/util/TimeUtil.java

/**
 * 弱精度的计时器,考虑性能不使用同步策略。
 *
 * @author xianmao.hexm 2011-1-18 下午06:10:55
 */
public class TimeUtil {
    private static long CURRENT_TIME = System.currentTimeMillis();

    public static final long currentTimeMillis() {
        return CURRENT_TIME;
    }

    public static final void update() {
        CURRENT_TIME = System.currentTimeMillis();
    }
}

https://github.com/alibaba/cobar/blob/master/server/src/main/server/com/alibaba/cobar/CobarServer.java

timer.schedule(updateTime(), 0L, TIME_UPDATE_PERIOD); // TIME_UPDATE_PERIOD 是 20ms
...

// 系统时间定时更新任务
private TimerTask updateTime() {
    return new TimerTask() {
        @Override
        public void run() {
            TimeUtil.update();
        }
    };
}

Cobar之所以这么干,一是因为往往他的QPS非常高,这样可以减少获取时间戳的CPU消耗或者耗时;其次是这个时间戳在Cobar内部只做统计使用,就算不准确也并无大碍,从实现上看也确实是 弱精度

后来我也在其他的代码中看到了类似的实现,比如Sentinel(不是Redis的Sentinel,而是阿里开源的限流熔断利器Sentinel)。

Sentinel作为一款限流熔断的工具,自然是自身的开销越小越好,于是同样都是出自阿里的Sentinel也用了和Cobar类似的实现: 缓存时间戳

原因也很简单,尽可能减少对系统资源的消耗,获取时间戳的性能要更优秀,但又不能和Cobar那样搞个弱精度的时间戳,因为Sentinel获取到的时间戳很可能就决定了一次请求是否被限流、熔断。

所以解决办法也很简单,直接将缓存时间戳的间隔改成 1毫秒

去年我还写过一篇文章《低开销获取时间戳》,里面有Sentinel这段代码:

发现一个开源项目优化点,点进来就是你的了

甚至后来的Sentinel-Go也采取了一模一样的逻辑:

发现一个开源项目优化点,点进来就是你的了

以前没有多想,认为这样并没有什么不妥。

直到前两天晚上,没事在Sentinel-Go社区中瞎逛,看到了一个issue,大受震撼:

https://github.com/alibaba/sentinel-golang/issues/441

提出这位issue的大佬在第一段就给出了非常有见解的观点:

发现一个开源项目优化点,点进来就是你的了

说的比较委婉,什么叫「负向收益」?

我又搜索了一下,找到了这个issue:

https://github.com/alibaba/Sentinel/issues/1702

发现一个开源项目优化点,点进来就是你的了

TimeUtil吃掉了50%的CPU,这就是「负向收益」,还是比较震惊的!

发现一个开源项目优化点,点进来就是你的了

看到这个issue,我简单地想了下:

  • 耗时:获取时间戳在一般情况下耗时几乎都不会影响到系统,尤其是我们常写的业务系统
  • CPU:假设每毫秒缓存一次时间戳,抛开其他开销不说,每秒就有1000次获取时间戳的调用,如果每次请求中只有1次获取时间戳的操作,那么至少得有1000QPS的请求,才能填平缓存时间戳的开销,况且还有其他开销

但这只是我们的想当然,如果有数据支撑就又说服力了。为此前面提出「负向收益」的大佬做了一系列分析和测试,我们白嫖一下他的成果:

发现一个开源项目优化点,点进来就是你的了

发现一个开源项目优化点,点进来就是你的了

发现一个开源项目优化点,点进来就是你的了

发现一个开源项目优化点,点进来就是你的了

看完后我跪在原地,久久不能起身。

发现一个开源项目优化点,点进来就是你的了

课代表来做个总结:

  • 缓存时间戳开销最大的地方是sleep和获取时间戳
  • 理论上来说单机QPS需要大于4800才会有正向收益,真实测试结果也是在4000QPS以内都没有正向收益
  • 如果不要这个缓存时间戳,获取时间戳耗时会增加,但这在可接受范围内
  • 鉴于常规情况下QPS很少会达到4K,所以最后结论是在Sentinel-Go中默认禁用这个特性

这一顿操作下来,连Sentinel社区的大佬也觉得很棒,竖起来大拇指:

发现一个开源项目优化点,点进来就是你的了

然而做了这么多测试,最后的修改就只是把true改成false:

发现一个开源项目优化点,点进来就是你的了

自适应算法

本来我以为看到这位大佬的测试已经是非常有收获了,没想到接下去的闲逛又让我发现了一个更了不得的东西。

既然上面分析出来,在QPS比较高的情况下,收益才能抵消被抵消,那么有没有可能实现一个自适应的算法,在QPS较低的时候直接从系统获取,QPS较高时,从缓存获取。

果不其然,Sentinel(Java版,版本>=1.8.2)已经实现了!

issue参考:https://github.com/alibaba/Sentinel/pull/1746

我们捋一下它的实现:

发现一个开源项目优化点,点进来就是你的了

我们首先看最核心的缓存时间戳的循环(每毫秒执行1次),在这个循环中,它将缓存时间戳分成了三个状态:

  • RUNNING:运行态,执行缓存时间戳策略,并统计写时间戳的QPS(把对缓存时间戳的读写QPS分开统计)
  • IDLE:空闲态(初始状态),什么都不做,只休眠300毫秒
  • PREPARE:准备态,缓存时间戳,但不统计QPS

这三个状态怎么流转呢?答案在开头调用的 check方法中:

发现一个开源项目优化点,点进来就是你的了

首先check逻辑有个间隔,也就是每隔一段时间(3秒)来做一次状态转换;

其次如果当前状态是 空闲态并且读QPS大于 HITS_UPPER_BOUNDARY(1200),则切换为 准备态

如果当前状态是 运行态且读QPS小于 HITS_LOWER_BOUNDARY(800),则切换为 空闲态

发现似乎少了切换到 运行态的分支,看上面的循环中,第三个 准备态的分支运行一次就将状态切换为 运行态了。

这是为啥?其实 准备态只是为了让程序从 空闲态切换到 运行态时过渡的更平滑,因为 空闲态下缓存时间戳不再更新,如果没有过渡直接切换到 运行态,那可能切换后获取的时间戳是有误差的。

文字可能不直观,我们画一个状态流转图:

发现一个开源项目优化点,点进来就是你的了

最后这些准备好了,获取时需要做两件事:一是统计读时间戳的QPS,二是获取时间戳;如果是 空闲态准备态则直接获取系统时间返回,如果是 运行态则从缓存中拿时间戳。

发现一个开源项目优化点,点进来就是你的了

当程序比较空闲时,不会缓存时间戳,降低CPU的消耗,QPS较高时缓存时间戳,也能降低CPU的消耗,并且能降低获取时间戳的时延,可谓是一举两得。

但这中间我有个疑问,这里QPS的高低边界不知道是如何得出的,是拍脑袋还是压测出来的,不过这个数值似乎并不一定绝对准确,可能和机器的配置也有关系,所以我倾向这个值可以配置,而不是在代码中写死,关于这点,这段代码的作者也解释了原因:

发现一个开源项目优化点,点进来就是你的了

最后可能你会问,这QPS咋统计呀?

这可是Sentinel的强项,利用 LeapArray统计,由于这不是本文重点,就不展开了,有兴趣可以参考我之前的文章《Sentinel-Go 源码系列(三)滑动时间窗口算法的工程实现》,虽然文章是Go的,但算法和Java的是一模一样,甚至实现都是照搬。

有没有测试数据支撑呢?有另一位大佬在评论区贴出了他的测试数据,我们看一下:

发现一个开源项目优化点,点进来就是你的了

在低负载下,CPU消耗降低的特别明显,高负载则没什么变化,这也符合我们的预期。

看到这里你是不是觉得该点题了?没错,Sentinel-Go还没实现上述的自适应算法,这是个绝佳的机会,有技术含量,又有参考(Java版),是不是心动了?

社区中也有该issue:

https://github.com/alibaba/sentinel-golang/issues/419

发现一个开源项目优化点,点进来就是你的了

这个issue在2021年8月有个哥们认领了,但截止目前还没贡献代码,四舍五入等于他放弃了,所以你懂我意思吧?

最后说一句

如果你觉得文章还可以,麻烦动动小手,点个 关注在看赞,你的鼓励是我持续创作的动力!

对了,如果觉得还不过瘾,可以再看看这些相关文章:

感谢阅读,我们下期再见~

搜索关注微信公众号”捉虫大师”,后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

Original: https://www.cnblogs.com/zhuochongdashi/p/16309130.html
Author: 捉虫大师
Title: 发现一个开源项目优化点,点进来就是你的了

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/575544/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SpringMVC(5)-ssm整合实现增删改查-mybatis层

    mybatis层编写完毕后的项目目录 1.右键SpringMVC2项目-》new-》Modual-》选择maven项目(我的项目名为Study09_ssm),输入模块名,点击Fin…

    Java 2023年6月9日
    071
  • Nginx作为代理服务

    一、代理相关概念 1、什么是代理? 代理不直接让客户端请求源服务器,由代理服务器来代为办理。 生活中如 代理理财,代理收快递都是同一个道理。 2、代理的分类 按应用场景进行分类 正…

    Java 2023年5月30日
    081
  • SpringBoot系列之IDEA项目中设置热部署教程

    1、新建SpringBoot项目 环境准备 JDK 1.8 SpringBoot2.2.1 Maven 3.2+ 开发工具 smartGit IntelliJ IDEA2018 创…

    Java 2023年5月30日
    092
  • App 性能优化

    App 性能优化 1、onBindViewHolder 运行在 UI 线程,不宜进行逻辑等耗时操作,只适合把数据填入视图; 2、使用 support 包下面的 DiffUtil 局…

    Java 2023年6月7日
    090
  • Android-Java-普通类与抽象类(覆盖)&方法重载

    执行结果: 覆盖,复写,重写的规则: 返回值 方法名 参数类型与数量 必须一致,并且,子类的修饰权限要大于等于父类(父类public 子类public 👌,父类protected …

    Java 2023年5月29日
    099
  • spring IOC的理解,原理与底层实现?

    从总体到局部 控制反转:理论思想,原来的对象是由使用者来进行控制,有了spring之后,可以把整个对象交给spring来帮我们进行管理DI(依赖注入):把对应的属性的值注入到具体的…

    Java 2023年6月8日
    072
  • windows安装jdk8

    win10系统安装jdk8全过程 一 下载安装文件 jdk的安装与配置是Java学习的第一步,下面记录一下具体过程。首先根据自己系统下载对应版本。下载地址http://www.or…

    Java 2023年5月30日
    072
  • Maven 3 从入门到入门

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月9日
    084
  • HashMap源码个人理解

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月5日
    081
  • Spring boot 2.0 之优雅停机

    spring boot 框架在生产环境使用的有一段时间了,它”约定大于配置”的特性,体现了优雅流畅的开发过程,它的部署启动方式( java -jar xxx…

    Java 2023年5月30日
    072
  • 程序造假显得很忙

    一 背景 现实总有些奇葩的需求,比如你需要占用 50%的 cpu,或者你需要占用 80%的 cpu 的程序,这个程序没有其他作用,仅仅是为了占用 cpu,空跑,但是其实还隐藏一个需…

    Java 2023年5月30日
    054
  • 自定义注解,利用AOP实现日志保存(数据库),代码全贴,复制就能用

    前言 1,在一些特定的场景我们往往需要看一下接口的入参,特别是跨系统的接口调用(下发,推送),这个时候的接口入参就很重要,我们保存入参入库,如果出问题就可以马上定位是上游还是下游的…

    Java 2023年6月14日
    082
  • 强烈推荐!史上最有深度的Java学习视频,Mybatis、Javaweb、SSM框架项目

    前言 今天给大家带来三个有深度的Java学习视频,在学习Java的进阶之路上,只需10分钟轻松Get教学视频的核心要点。 正文 一、Mybatis 视频介绍 文字内容: 本视频由张…

    Java 2023年6月9日
    080
  • CSS阶段试手作品——CYTUS Ⅱ官网制作

    网页Cytus Ⅱ的制作 整体布局 单栏布局,整体按顺序分为以下几个板块: 页首下载、新闻表单、角色轮播图、视频、页尾以及侧边栏 要尽可能把自己的所有知识用上,所以可能会有和原网页…

    Java 2023年6月16日
    077
  • 订单及其状态机的设计实现

    状态机简介: 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。 【规则的抽象】 有限状态机一般都有以下特点: (1)可以用状态来描述事物,并且任一时刻,事物…

    Java 2023年6月9日
    0173
  • SpringBoot自定义starter

    一、starter的构成 xxx-starter—–>xxx-starter-autoconfigurer 启动器—–>…

    Java 2023年5月30日
    082
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球