000x起始
为了拉近自己与互联网公司的思维方式,决定买一本《可伸缩服务架构-框架与中间件》,买回来就看了第一章节,作者以一个ID生成器作为例子讲述了个非常不错的技术需求和解决方案,用实例直观的说明了可伸缩架构的设计方式。并且由于ID生成业务本身的简单性质,整个项目代码非常易于理解。但是,我正好注意到作者一句"ID的生成取决于网络I/O和CPU的性能,网络I/O一般不是瓶颈"。一想这跟我的认知不一致啊,一个号码生成器,又没有密集计算,又没有复杂计算,cpu成为瓶颈,这不科学啊。于是就开始查找原先实现上的一些有性能疑问的地方,开始了这次性能调优的地方。
001x分析
源代码(https://github.com/cloudatee/vesta-id-generator)里提供了三种锁的实现,最快的一种工作方式就是无锁的实现,在我本机测试了几把,其实挺快了,不过既然怀疑有浪费CPU的嫌疑,也值得一看,阅读代码后判断初步出问题的点就是在cas失败后的重新计算流程过长。这里需要提一句,ID生成器的业务需求十分简单,不过作者为了达到可伸缩,容错和分布式做的设计确实又很大的参考价值,这个项目几乎就是一个可伸缩服务的最小例子。
010x理想的模型
作者把id生成最核心的部分放在了IdPopulator接口里,而这个populateId的方法主流程为
- 1.取当前时间戳
- 2.比较上次生成的Id时间戳
- 3.两个时间戳相同则增加sequnce值
- 4.两个时间戳不同则重置sequnce值
- 5.用cas保存当前id
- 6.保存失败则从1.开始
- 7.保存成功则返回id
查看项目文档注意到一名叫历华的开发已注意到获取时间这个操作可能比较慢,而我则把注意力放在了作者性能测试结果中的最大响应时间比较慢这一点,我认为应该是高并发时重试次数过多导致的。所以开始进行如下思考,populateId方法其实只需要保证每次调用发出去的id都不同就可以,id中的时间戳部分的生成其实可以与它无关,而且在同一个时间片中,id的时间戳总是一致的,不需要反复取。那如果针对当前时间片,预先准备好一个queue,把这个时间片的最大id生成好并放在里面,那只要不停的pop即可。
011x实际实现
实际写的时候发现,既然同一时间片中的ID的sequnce是连号的,那也不用放queue里了,用无锁的方式每次+1后更新即可,应该快过在queue里pop。
这样也省了原本想象的模型里那个用来填充queue的线程,只需要一个Timer在每一秒开始时将当前时间戳和sequnce重置。
100x性能测试结果
处理完之后果然平均时间变为0.5us,最大响应时间也变为个位数的ms,还没测试对rest接口性能的影响,不过至少这个Id生成服务就不会占用过多的cpu了。