Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!

为了新朋友能快速进入场景,再描述一遍这个项目的背景,这个项目是一个自研的Dubbo注册中心,上一张架构图

  • Consumer 和 Provider 的服务发现请求(注册、注销、订阅)都发给 Agent,由它全权代理

  • Registry 和 Agent 保持 Grpc 长链接,长链接的目的主要是 Provider 方有变更时,能及时推送给相应的 Consumer。为了保证数据的正确性,做了推拉结合的机制,Agent 会每隔一段时间去 Registry 拉取订阅的服务列表

  • Agent 和业务服务部署在同一台机器上,类似 Service Mesh 的思路,尽量减少对业务的入侵,这样就能快速的迭代了

这里的Registry就是今天的主角,熟悉Dubbo的朋友可以把它当做是一个zookeeper,不熟悉的朋友可以就把它当做是一个Web应用,提供了注册、注销、订阅接口,虽然它是用Go写的,但本文和Go本身关系不大,也会用一些伪代码来示意,所以也可以放心大胆地看下去。

一定要做性能优化吗

在做性能优化之前,我们得回答几个问题,性能优化带来的收益是什么?为什么一定要做优化性能?不优化行不行?

性能优化无非有两个目的:

  • 减少资源消耗,降低成本

  • 提高系统稳定性

如果只是为了降低成本,最好做之前估算一下大概能降低多少成本,如果吭哧吭哧干了大半个月,结果只省下了一丁点的资源,那是得不偿失的。

回到这个注册中心,为什么要做性能优化呢?

Dubbo应用启动时,会向注册中心发起注册,如果注册失败,则会阻塞应用的启动。

起初这个项目问题并不大,因为接入的应用并不多,而当我接手项目时,接入的应用越来越多。

话分两头,另一边集团也在逐渐使用容器替代虚拟机和物理机,在高峰期会用扩容的方式来抗住流量高峰,快速扩容就要求服务能在短时间内大量启动,无疑对注册中心是一个大的考验。

而导致这次优化的直接导火索是集团内的一次演练,他们发现一个配置中心的启动依赖,性能达不到标准而导致扩容失败,于是复盘下来,所有的启动依赖必须达到一定的性能要求,而这个标准被定为1000qps。

于是就有了本文。

指标度量

如果不能度量,就没法优化。

首先是把几个核心接口加上metric,主要是请求量、耗时(p99 / p95 / p90)、错误请求量,无论是哪个项目,这点算是基本的了,如果没加,得好好反思了。

其次对项目进行一次压测,不知道现在的性能,后面的优化也无法证明其效果了。

以注册接口为例,当时注册的性能大概是40qps,记住这个值,看我们是如何一步一步达到1000qps的。

压测成功的请求标准是:p99耗时在1秒以内,且无报错。

瓶颈在哪里

性能优化的最关键之处在于找到瓶颈在哪,否则就是无头苍蝇,到处瞎碰。

注册接口到底干了什么呢?我这里画个简图

  • 整个流程加锁,防止并发操作

  • Create App和Create Cluster是创建应用和集群,只会在应用第一次创建,如果创建过就直接跳过

  • Insert Endpoint是插入注册数据,即ip和port

  • 系统的底层存储是基于MySQL,Lock和UnLock也是基于MySQL实现的悲观锁

从这个流程图就能看出来,瓶颈大概率在锁上,这是个悲观锁,而且粒度是App,把整个流程锁住,同一时刻相同应用的请只允许一个通过,可想而知性能有多差。

至于MySQL如何实现一个悲观锁,我相信你会的,所以我就不展开。

为了证明猜想,我用了一个非常笨但很有效的方法,在每一个关键节点执行之后,记录下耗时,最后打印到日志里,这样就能一眼看出到底哪里慢,果然最慢的就是加锁。

锁优化

在优化锁之前,我们先搞清楚为什么要加锁,在我反复测试,读代码,看文档之后,发现事情其实很简单,这个锁是为了防止App、Cluster、Endpoint重复写入。

为什么防止重复写入要这么折腾呢?一个数据库的唯一索引不就搞定了?这无法考证,但现状就是这样,如何破解呢?

  • 首先是看这些表能否加唯一索引,有则尽量加上

  • 其次数据库悲观锁能否换成Redis的乐观锁?

这个其实是可以的,原因在于客户端具有重试机制,如果并发冲突了,则发起重试,我们堵这个概率很小。

上面两条优化下来只解决了部分问题,还有的表实在无法添加唯一索引,比如这里App、Cluster由于一些特殊原因无法添加唯一索引,他们发生冲突的概率很高,同一个集群发布时,很可能是100台机器同时拉起,只有一台成功,剩余99台在创建App或者Cluster时被锁挡住了,发起重试,重试又可能冲突,大家都陷入了无限重试,最终超时,我们的服务也可能被重试流量打垮。

这该怎么办?这时我想起了刚学Java时练习写单例模式中,有个叫「双重校验锁」的东西,我们看代码

public class Singleton {private static volatile Singleton instance = null;private Singleton() {}private static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

再结合我们的场景,App和Cluster只在创建时需要保证唯一性,后续都是先查询,如果存在就不需要再执行插入,我们写出伪代码

app = DB.get("app_name")
if app == null {redis.lock()app = DB.get("app_name")if app == null {app = DB.instert("app_name")}redis.unlock()
}

是不是和双重校验锁一模一样?为什么这样会性能更高呢?因为App和Cluster的特性是只在第一次时插入,真正需要锁住的概率很小,就拿扩容的场景来说,必然不会走到锁的逻辑,只有应用初次创建时才会真正被Lock。

性能优化有一点是很重要的,就是我们要去优化执行频率非常高的场景,这样收益才高,如果执行的频率很低,那么我们是可以选择性放弃的。

经过这轮优化,注册的性能从40qps提升到了430qps,10倍的提升。

读走缓存

经过上一轮的优化,我们还有个结论能得出来,一个应用或集群的基本信息基本不会变化,于是我在想,是否可以读取这些信息时直接走Redis缓存呢?

于是将信息基本不变的对象加上了缓存,再测试,发现qps从430提升到了440,提升不是很多,但苍蝇再小,好歹是块肉。

CPU优化

上一轮的优化效果不理想,但在压测时注意到了一个问题,我发现Registry的CPU降低的很厉害,感觉瓶颈从锁转移到了CPU。说到CPU,这好办啊,上火焰图,Go自带的pprof就能干。

可以清楚地看到是ParseUrl占用了太多的CPU,这里简单科普下,Dubbo传参很多是靠URL传参的,注册中心拿到Dubbo的URL,需要去解析其中的参数,比如ip、port等信息就存在于URL之中。

一开始拿到这个CPU profile的结果是有点难受的,因为ParseUrl是封装的标准包里的URL解析方法,想要写一个比它还高效的,基本可以劝退。

但还是顺腾摸瓜,看看哪里调用了这个方法。不看不知道,一看吓一跳,原来一个请求里的URL,会执行过程中多次解析URL,为啥代码会这么写?可能是其中逻辑太复杂,一层一层的嵌套,但各个方法之间的传参又不统一,所以带来了这么糟糕的写法,

这种情况怎么办呢?

  • 重构,把URL的解析统一放在一个地方,后续传参就传解析后的结果,不需要重复解析

  • 对URL解析的方法,以每次请求的会话为粒度加一层缓存,保证只解析一次

我选择了第二种方式,因为这样对代码的改动小,毕竟我刚接手这么庞大、混乱的代码,最好能不动就不动,能少动就少动。

而且这种方式我很熟悉,在Dubbo的源码中就有这样的处理,Dubbo在反序列化时,如果是重复的对象,则直接走缓存而不是再去构造一遍,代码位于org.apache.dubbo.common.utils.PojoUtils#generalize

截取一点感受下

private static Object generalize(Object pojo, Map<Object, Object> history) {...Object o = history.get(pojo);if (o != null) {return o;}history.put(pojo, pojo);...
}

根据这个思路,把ParseUrl改成带cache的模式

func parseUrl(url, cache) {if cache.get(url) != null {return cache.get(url)}u = parseUrl0(url)cache.put(url, u)return u
}

因为是会话级别的缓存,所以每个会话会new一个cache,这样能保证一个会话中对相同的url只解析一次。

可以看下这次优化的成果,qps直接到1100,达到目标~

最后说两句

可能有人看完就要喷了,这哪是性能优化?这分明是填坑!对,你说的没错,只不过这坑是别人挖的。

本文就以一种最小的代价来搞定对祖传代码的性能优化,当然并不是鼓励大家都去取巧,这项目我也正在重构,只是每个阶段都有不同的解法,比如老板要求你2周内接手一个新项目,并完成性能优化上线,重构是不可能的。

希望通过本文你能学到一些性能优化的基本知识,从为什么要做的拷问出发,建立度量体系,找出瓶颈,一步一步进行优化,根据数据反馈及时调整优化方向。

我的新书《深入理解Java核心技术》已经上市了,上市后一直蝉联京东畅销榜中,目前正在6折优惠中,想要入手的朋友千万不要错过哦~长按二维码即可购买~

长按扫码享受6折优惠

往期推荐

消息队列原理和选型:Kafka、RocketMQ 、RabbitMQ 和 ActiveMQ

技术总监被开除了....

重镑!虚拟机巨头 VMware 将被收购

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

祖传代码如何优化性能?相关推荐

  1. 为什么祖传代码被称为「屎山」?这个回答简直太形象了

    经常听说祖传代码会被人称之为「屎山」,不同人可能有不同的体会,最近看到一个回答,简直是把这个阐述得"活灵活现",大家来感受下吧. " 阅读本文大概需要 3 分钟. &qu ...

  2. 基于小波变换的图像边缘检测(matlab祖传代码注释)

    基于小波变换的图像边缘提取应用展示 上图为针对png格式无背景原图的边缘检测,对比各种边缘检测算子,小波变化的优势体现并不明显. 上图为针对含背景图片的边缘检测,小波变化的优势这里体现的比较明显. m ...

  3. 入职第一天,我接手了号称【屎山】的祖传代码,这还能卷吗???

    公司各种各样的祖传代码都是令新人虎躯一震的代码,因为有时候你根本不知道它是干嘛的,甚至觉得它毫无用处,关键是 还绝对不能动,碰一段改半年,别问我怎么知道的.最讽刺的是,你可能为了修改代码,也在里面拉了 ...

  4. 为什么祖传代码被称为“屎山”?

    任何设计人员,你几年之后再来看自己现在的作品,你就会觉得简直就是狗屎,拿出来真tm丢人. 如果你没有这种感觉,那说明你这行干不久了. 说到祖传代码不得不提之前在知乎上看到的两位网友的经历: 一 我刚入 ...

  5. 面向祖传代码 Debug,我挽回了一位准备跑路的程序员

    交流群的风格突然骤变,没有了往日的灌水扯淡,居然聊起了技术. 看了大家的全部的聊天记录,发现问题并没解决.群里难得这么多人聊技术,抱着问答不断,必有回响的原则,主动勾搭一起看看是什么问题. 大概了解其 ...

  6. C#串口数据读取及处理解决方案--祖传代码修改记

    文章目录 原方案 代码 基本思路 串口通信基本原理 采集数据流程 问题 重构 问题复盘 重构过程 ReadTo函数 多线程 代码 原方案 欢迎大家访问我的个人网站 www.joezhouman.com ...

  7. 祖传代码成「屎山」了,千万别动

    上面这个公号「涩郎」,是我的一个备用号,为了防止万一哪天大号失联,平时一周我也会发三篇左右的我的思考,读书笔记,认知感悟等文章,带领大家一起探索精神与财务自由之路. 大家好,我是校长. 我看有人问了这 ...

  8. 删除vue打包大小限制_如何优化 Vue 祖传代码

    目录 前言 为什么要优化 从哪里开始下手 现在开始 1.代码压缩 2.删除一些废弃的页面 3.使用 cdn 优化 4.修改路由引入方式 结果 前言 "这页面加载也太慢了!",一个宁 ...

  9. 某程序员动了公司祖传代码屎山,半年没改完,惭愧后交辞职报告

    前段时间,有这样的一个话题,非常的火热,那就是关于程序员的,新入职程序员吐槽老员工写的代码就像是"一坨屎"!这样的言论瞬间就引起了程序员们的讨论. 有程序员认为,别看现在像是一坨屎 ...

最新文章

  1. 青少年电子信息智能创新大赛 赛项说明(Python编程创新挑战赛)
  2. IOS中UITableViewCell的重用机制原理
  3. Bootstrap3系列:下拉菜单
  4. 《c语言从入门到精通》看书笔记——第7章 循环控制
  5. spring2.X(1)--新特性介绍
  6. 3.3 修改“时间”维度
  7. c++中的enum类
  8. Python中的几个重要函数
  9. 创建Maven项目时提示web.xml is missing and failOnMissingWebXml is set to true错误解决方案
  10. 用HTML+css制作一个简单的三级导航
  11. python xlwt_python – 使用xlwt写入现有工作簿
  12. 【论文笔记】Data Shapley: Equitable Valuation of Data for Machine Learning
  13. 施一公:如何做一名优秀的博士生
  14. springboot发送邮件
  15. 三次握手 resend
  16. 【ZFNet】Visualizing and Understanding Convolutional Networks (2013) 全文翻译
  17. 安装OTRS工单管理系统
  18. 从A至Z,用30个单词来概括过去十年的游戏行业
  19. 微型计算机使用的crt信号,《微型计算机及其接口技术》课程学习方法篇(2)...
  20. 基于matlab数字滤波器设计,毕业设计 基于MATLAB的数字滤波器设计

热门文章

  1. 电路原理 | 电路基本定理
  2. 远程控制android盒子,实现远程控制监控安卓手机的向日葵UUPro使用评测,淘汰的安卓手机可以派上用场了...
  3. 华为云 —— 购买与登录弹性云服务器ECS
  4. 【NGUI】Unity实现英雄联盟选择皮肤效果
  5. RPC(1)HttpClient详细使用 含demo
  6. 调试ASP程序时,遇到程序运行错误时怎么查看具体错误位置呢?
  7. 林铭的鸿蒙灵珠,终于知道了《儒道至圣》中方运和《真武世界》中天元谁更厉害了!...
  8. QT 之 编译错误总结(2)
  9. 开源 | Objective-C Swift 最轻量级 Hook 方案
  10. [附源码]Node.js计算机毕业设计高校心理咨询管理系统Express