我们是做iut物联网项目,所以性能指标是硬性的,这次指标是50000点数(点数是指每一个设备),持续发报警和消息到后台,1秒一发,但是是一秒之内发完,不是瞬时的,所以极限并发是50000.
我刚来公司不久,所以来了就接到性能优化的任务还是挺兴奋的。
首先说一下我们目前的硬件情况,服务器两台rancher,4核8线cpu,内存8G的腾讯云服务器,数据库服务器一样的配置,一台nginx服务器双核cpu,4G内存,还有一台跳板机,配置不值一提。。。
软件情况:5个springboot的应用,中间应用协调其他4个应用,使用dubbo+zookeeper做分布式协调,数据库选用的postgresql(政治原因),缓存使用的redis,消息队列使用的是nats(政治原因),mongodb用于记录日志,elasticsearch用于查询统计。
部署情况:rancher使用的是kubernetes和helm部署应用,使用grafana和rancher监控应用的健康情况,数据库服务器的部署都是纯docker,因为是物联网项目,所以还有5台windows服务器去部署嵌入式方面的东西(这块我不是很懂,其实只要知道压力是它创造的就行了。。),然后关键来了,数据库服务器上面部署着4个postgre,一个redis集群(6个点),一个redis单点,zookeeper集群(3个),嗯,暂时是这样。然后rancher上部署的应用每个都是两个点(5个应用,每个应用部署两个点)。
然后开始做优化,我们目前的优化首先要做到它不崩溃,而不是数据对不对,现在的问题是dubbo连接超时现象太严重,我们有一个springboot应用是元数据,很多像报警应用和消息应用都在调用它,所以它超时的现象很严重,我们先测试的1W点的性能,目前是完全不够的,我们报警应用的逻辑是前端发消息,到报警这里直接处理,测极限情况,也就是1W点一起报警,发现根本扛不住。
第一,有很多调用元数据的地方,超时现象严重。
第二,nats发消息速度太快,后端根本扛不住。
所以我们做的第一个优化是将元数据的一些数据在报警这里建了一张宽表,这里面就不走元数据了,然后发现速度果然有了明显提升,(我觉得这块是设计的问题,但是我刚来不久,还是怂着吧),由于nats发消息太快,我们用数据库的一张表做了一下缓冲,然后起多个线程去扫这张表,每个线程使用hash对一个字段取模,然后每个线程处理的都不一样,这样把压力分开,然后为了避免线程创建太多有问题,我们建了两个点,然后使用脚本传参数的方式把它唯一化。
目前来说是稳定的,因为只有1W点,才1/5,数据量积攒的也不多,而且公司终于同意加内存了,两个rancher是16G了,所以还好。
我们有三种消息类型,叫做alarm,cov,interval,interval是每5min一发,alarm和cov是1s一发。
目前来说,能稳定住,但是我们疲劳测试了一段时间,在前端访问的时候发现了很多问题,有些查询很慢,有的是可以加索引解决。但是有些查询,比如count,这个有些统计没有办法,count就是根据条件查数量,加了索引也没啥用,我们有两个地方在postgresql的基础上没有办法解决,一个是count,一个是历史记录的查询,这个历史记录很多,有上千万到亿条,我们当时评估了一下觉得这个历史就像日志一样只是个记录,所以我们引入了mongodb,这个也安装在了数据库服务器上,然后由于数据量比较大,查询的时候mongo也不能很快反应回来,所以我们在mongo上加了索引,在5千万量级数据上能够保证5S返回结果。
这个问题解决之后就是count的问题了,可能是mongo压力比较大,我们的业务有先查询再删除再插入的操作,mongo直接把数据库服务器的cpu吃满了,内存也占了30%,这样一下子导致整体性能慢了,频繁报错,我们对postgre做了很多优化,比如加大working_mem,加索引,加process都不管用。时间实在是太慢,因为服务器配置就那样,没啥提升空间了。
我们想了很久,终于决定引入elasticsearch,虽然有点大材小用了,但是确实管用,引入es以后使用rest的方式插入数据和查询数据,确实解决的查询慢的问题,基本是秒出,但是有个隐藏的bug,elasticsearch也是部署在数据库服务器上的。
之后我们开始测试5W点的性能,1s是扛不住的,不一会就会报错,这次是cov的应用报错了,唯一键冲突,并发量太高的情况下报的,这个问题很常见,我们首先就对代码检查,看看有没有事务的问题,结果发现还真有,事务比较乱,传播行为没有,高并发情况下肯定会出问题,然后我们的思路是,对于cov是更新操作比较多,所以我们把逻辑改为先更新,根据更新结果判断是否插入,然后在插入操作的时候引入redisson分布式锁(因为有两个点),在锁内在查询一次,再决定是否插入还是更新,类似于单例模式的双重校验锁。
结果发现还是有报错,但是锁已经优化了差不多了,我们决定在事务上做优化,把每次数据库上的查询细化出来,对于某些操作做个小事务,让它相互不影响,结果成功了,没有唯一键冲突的报错了。取而代之的是数据库连接打不开,我们对这个经验不是很足,就觉得把数据库连接加大就可以了,经过验证不是那么回事,数据库连接数是有一个公式的,就是cpu核心数×2+存储数量,也就是说连接池大小不能设的太高,比如你设500个连接,他有可能倍数甚至指数级上升,直接把数据库干死了。
通过网上的资料,了解到了一些数据库连接池的性能,我们用的是druid,通过对比发现它的性能不是最好的,网上的资料有个排名,最差的是c3p0,druid的性能已经很不错了,但是相比hikaricp还是差了很多,这个连接池在高并发的情况下也能保持稳定,而且代码量极少,druid有很多其他的功能,比如监控,但是我们用不着,我们只需要快就行了,换了连接池之后从上线测试到转天上班回来还是很稳定,觉得很不错。
然后我自己嘬了一把,我看这个连接池这么快,我就把其他的应用都换了,然后又重新测试了一下,结果发现又出现链接不够的问题了,我就慌了,各种测试都不行,我们架构说还原吧,我再看看,结果还原了也不行了。我感觉那天跑了一晚上没问题可能是运气好,目前这个问题是一直存在的,并不是我换了hikaricp的问题。
然后转眼在看数据库服务器,4个postgre,7个redis实例,1个elasticsearch,1个mongo,8G4核cpu,我都佩服这个服务器了,真能扛,感觉我们优化到头了,我们在这种情况下能够保证10S一发是没问题的,1S实在是扛不住。
也不知道哪天有灵感了,我们架构师和我都想起来了,还有个redis集群,我们对他的利用很少,只存了一点点数据,所以我们决定在这上面做点事情:1.先把alarm的那张中间表改到redis上,这样数据库的压力会小一点,因为那张表总会有积压。
目前的思路是,每一次alarm请求后就处理这条消息(其实这个场景很像实时计算了),我们架构师想了一个一致性hash分桶的算法,本想用令牌桶,但是令牌桶我感觉适合秒杀抢商品业务,这里的数据不能丢的,所以我就否了这个。
我简单说一下他做的这个插件的思路,每次请求根据请求的唯一ID做hash,然后将桶得个数固定(本来定的是1024的,觉得太多了,就改成300多了),然后hash后对桶的个数取模,所以在redis里面会有和桶相同个数的key,每个key就是一个list,里面放着数据,然后在使用一个queue去接受桶的数据(注意:这个Queue只有一个数据,如果没有消费是会等待的,这样做是保证顺序的),我们使用的java的transferQueue,有兴趣大家可以了解一下。然后开启很多现成去轮询这个Queue消费数据,一个线程扫桶放Q,这样就把redis利用起来了,放上面测试后果然有效,然后cov的场景和alarm也很像,我们对cov也做了这样的优化。
经过上述步骤的重重优化,现在能够保证应用不会崩溃,但是redis里面还是有积压。也许是上天看我们太可怜了,我无意中看到一篇文章,是讲postgresql的并行查询的配置,然后我试了一试对数据库的配置做了优化。
终于成功了,我们抗住了压力,跑了24H没有出现问题,然后我们发现我们还有Windows服务器了,是部署嵌入式方面的东西的,感觉很闲,我就把elasticsearch放到上面去了,然后数据库服务器的压力减轻了好多,这个es太猛了。
这次优化经历挺难忘的,学到了很多东西,在有限的机器上做到最好,其实感觉还有优化的点:1.mongodb也可以放到另一台windows上,这个也很占资源。2.还有一些查询可以把es利用起来。
ps:可能中间时间太长忘记了,我们对dubbo也做了一些优化,比如经常超时的那个问题,有可能是连接数太多,我们把线程池改成了cached,这样就没有这个问题了,还有消息模式dispatcher改为了message,这样能过滤一些无用的消息占连接。
后期还会改这一块,因为感觉线程不能让它不可控,无限大,还是要评估一下然后设一个固定值比较好。
网友评论