如何从微小细节着手,参与开源贡献(一)
大家好,我是 bin ! 在上篇文章 我为 Netty 贡献源码 | 且看 Netty 如何应对 TCP 连接的正常关闭,异常关闭,半关闭场景 发布之后,有很多小伙伴留言给我,问的问题最多的是:“我该如何从零开始参与开源项目 ?”。
看得出来大家对参与开源的热情是非常高涨的,都非常期望为开源社区贡献自己的一份力量。既然是从零开始,那么摆在我们面前有两个大的问题:
1.如何找到合适的开源项目参与进来 ?
2.到底该向开源项目贡献些什么 ?
针对第一个问题,我们最好是从平时工作中经常使用到的知名开源项目或者是我们在技术调研中准备未来采用的知名开源项目入手,并深入研究下去,开启自己的开源贡献之路。
针对第二个问题,首先来澄清一个常见的误解就是:为开源做贡献必须得提交代码。这其实是一个很大的误解,事实上,代码固然重要,但是在开源项目中除了代码之外还有一部分非常核心非常重要的内容经常会被忽视甚至轻视。
比如:查看 README 的时候,发现了损坏的链接,又或者拼写错误,项目文档写的不够清晰,文档过于老旧等等一系列的问题,请不要坐视不理,径直绕开,伸出你的援助之手,解决这些你能看到的问题。而这正是开源的精髓之所在!
关于非代码相关的贡献部分,我们可以为开源项目撰写和改善文档,为开源项目编写入门教程以及使用示例,或者为开源项目撰写博客可以是源码解析的也可以是最佳实践的。
而关于代码相关的贡献部分,我们也可以完善项目的单元测试,完善项目中的 example 示例工程。
以上这些内容都是我们从零开始参与开源贡献非常好的素材,着眼于微小的细节,开启我们的开源贡献之路。正所谓:“不积跬步,无以至千里;不积小流,无以成江海”
为了让大家能够轻松从零开始上手参与开源贡献,bin 特地从好友小楼那里选取了一个现成的素材,这个素材是一个 Sentinel 关于时间戳获取的性能优化点,目前还没有相关 PR 提交修复,先到先得,哈哈。
下面的时间交给小楼,让我们来一起惊叹: “小小的时间戳,大大的性能考究” !
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:
以后数字社会的发展趋势就是开源,协作融合才能更好的发展,商袖一直在努力。