一、性能压测
(一)压测工具 ——ycsb
优势
• 使用比较简单
• 支持多种开源数据库:redis,mongo,cassandra,hbase
• 方便对不同数据库做性能对比
市面上有很多压测工具,这里主要选择了ycsb这一款压测工具。选择它的原因很简单,因为它的使用上比较简单,并且它支持多种开源数据库,像redis,mongo,cassandra,hbase ,所以你一旦掌握了,它就可以对多种数据库进行压测,并且方便对不同数据库做性能对比,这样可以选择对你最适合的数据库,它支持对多种数据库进行压测,如果你下载的完整压缩包的话会比较大。
cassandra专用压缩包:
下载
tar -xvf ycsb-0.15.0.tar.gz
cd ycsb-0.15.0
(二)目录结构
•bin ——执行文件目录
•lib ——依赖jar 包
•workloads ——压测模型定义文件
压缩之后进到它的目录下面,可以看到它的目录结构主要有三个目录,分别是bin,lib,workloads ,bin目录主要是执行文件目录,而lib主要是依赖的jar目录,像这个包它由于只支持cassandra的压测,所以它的lib包下面的jar非常的少,而我们的重点关注在于workloads的这一个目录。
在这一个目录里面你可以看到他预先存在一些定义好的压测模型,你可以根据他提供的这种默认的压测模型进行略微的修改,很快的得到你需要的一个压测模型。
(一)定义压测模型
文档地址:https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties
•recordcount —— 定义压测初始数据量,尽可能匹配实际数据
量
•operationcount —— 压测过程中,执行的读写次数,建议运行
半小时级别以上
•fieldcount —— 字段数
•fieldlength —— 字段长度,fieldlength + fieldcount 决定一
行的大小
•readallfields —— 是否读所有字段
•readproportion + updateproportion + insertproportion +
scanproportion —— 定义读写比例,可以纯读,纯写,或者读
写混合(比例可调)
10w 数据量,10w 次纯读,每行10 个字段,每个字段200 bytes 大小
一次正确的压测,首先最重要的是你需要定义你的压测模型,首先你需要定义你初始的数据量,这个数据量最好是匹配你实际的数据量,比如你有异议的数据,你在压测的时候你就需要准备1亿的数据,如果你只准备了1万或者10万的数据,压出来的性能和你实际的性能会相差甚远。你在压测过程中需要执行的一个读写次数,还有一个比较重要的就是因为以表结构来举例子的话,表是有很多行组成的,每一行它其实有多个列,每列它其实会有一个大小,你需要定义你的字段数以及每个字段的长度,这样去匹配实际业务模型,才能得到一个比较准确的压测。
(一)建表
// 副本数设置和实际生产保持一致,副本数会影响数据大小,也会影响读写性能。
// 副本数不一样,压测出来的结果会非常不同
create keyspace ycsb WITH replication = {'class': 'NetworkTopologyStrategy', 'cn-shanghai-g': 2};
create table ycsb.usertable (
y_id varchar primary key,
field0 varchar,
field1 varchar,
field2 varchar,
field3 varchar,
field4 varchar,
field5 varchar,
field6 varchar,
field7 varchar,
field8 varchar,
field9 varchar);
我们需要对我们的数据库做一个初始化,ycsb这一款压缩工具它默认的命名空间是 ycsb,然后它默认的压缩表是usertable,所以通过这两条命令你可以直接创建出他所需要的namespace和table表。这里面需要注意的是在创建namespace的时候需要指定一个副本数,这一个副本数也会影响整体的一个数据大小,同时会影响读写性能。所以在设置压测的副本数的时候,和实际应用保持一致是最好的。
(二)构造测试数据
./bin/ycsb load cassandra2-cql -s -threads 20 -P workloads/read
注意事项:
• 通过内网访问进行压测
• 用来压测的ecs 规格不宜太小
• 构造数据的性能可以看做纯写入下的db 性能,通过调整线程数逼近
db 的写入性能上限
• 测试数据量一定要逼近真实数据量大小,否则,cache 命中率不一样,
测试读的时候,响应会截然不同
• 构造测试数据,一定要持续一段时间,这样才能观察到db 后台任务
运行对写入性能的影响
你在构造你的压测数据的时候,你数据量一定要足够大,然后一定要持续一段时间,因为任何一款数据库它都存在一些周期运行的任务,或者说一些后台的任务,如果你持续的时间比如说只有一分钟,很可能后台的任务或者周期任务他还没有执行,那么你压测出来的表现就并不是最真实的一个表现,因为任何一个周期任务,它其实都会抢占你的CPU资源,或者说磁盘的io资源,它会影响你的读写的性能,完成了测试数据的构造,那么通过把漏的命令改成乱命令,我们就可以对你的实际的压测产品做一个压测。
(一)读场景性能压测
./bin/ycsb run cassandra2-cql -s -threads 20 -P workloads/read
注意事项
• 读性能压测,一定要持续一段时间,数据库都有缓存,需要等缓存命中
率比较稳定时,其性能表现才比较真实
完成了测试数据的构造,那么通过把load的命令改成run命令,我们就可以对你的实际的压测产品做一个压测。这里其实就是一个单纯的读产品的压测,在压测完成之后,也能看到在这一个多场景下的性能情况。压测的时候,你的应用cassandra里面的缓存其实是空的,只有在持续一段时间之后,你的数据库的缓存它的命中率比较稳定了,压测出来的数据才是比较真实的。
二、性能调优
(一)Cassandra读写原理
在进行场景介绍之前,首先介绍一下Cassandra的读写原理,这样方便后面讲解它的优化思路的时候,大家能够更加快速的理解。
首先讲Cassandra的写请求,他写请求其实非常的简单,一条写请求进来之后会首先记录一条log,也就是Commit Log或者说Write Ahead Log。在Log里面记录完成之后,会把它插入到内存里面对应的一个结构Memtable里面去,一旦插入完成,写请求就结束了。写请求这么简单,它的设计导致Cassandra在写场景非常优异,绝大部分使用场景下,你如果正确的使用写入基本上都不会成为瓶颈。因为只涉及到一次磁盘写入和一次内存操作,这一次磁盘写入其实是因为是写log的形式,所以它其实就是直接append,磁盘写入的话性能是非常高的。
我们来看一下Cassandra如何完成一次读请求。每一次写入到内存Memtable,内存中的Memtable会越来越多,内存放不下,为了避免内存水位过高,Cassandra会周期性的把内存里面的Memtable flash到磁盘,形成一个SSTable文件。那么SSTable文件逐渐增多,会有周期性的任务叫Compaction,把多个SSTable文件合并成一个大的SSTable文件,一条读请求进来后,会对内存里面的Memtable和磁盘上的SSTable做多路归并排序,然后找到你所需要的数据再返回给你。
(二)准备工作:完善的监控
任何一次的性能调优都需要有完善的监控,完善的监控是所有性能调优的出发点以及终点。
下图是Cassandra上你能看到的监控图,一个完善的监控,基本上需要具备CPU、内存、磁盘、网络等最基本的系统级别监控,同时还需要能够看到Cassandra应用内部的状态,比如读写OPS、读写响应、GC情况、Compaction情况,Cache命中率以及各线程池当前状态。
场景一:写入稳定,读取延迟逐渐升高
首先我来介绍场景一,场景一的特点是写入稳定,读取延迟逐渐升高,这是一个很典型的案例,之前介绍的如何处理一个读请求,读请求其实是会对内存里的Memtable和磁盘上的SSTable做多路归并排序。如果说你的SSTable数量越多,其实它可能涉及到的io次数就会越多,而我们都知道磁盘io是很慢的,所以当你的读取延迟在逐渐升高的时候,你可以想到的一个可能性就是说你的文件数在越来越多,Compaction的过程被阻塞了或者太慢了,导致你的文件数一直在增多。
通过nodetool compactionstats查看pending task,通过这个数据我们可以去定位是不是你的Compaction任务太慢了或者卡住了,导致有太多堆积的Compaction任务没有完成。如果是的话,可以通过调整你的Compaction并发数,比如说同时执行多个Compaction,或者对你的机器做升级使你拥有更富裕的CPU资源来完成你的Compaction任务,起到一个优化的效果。
场景2——写少读多,对响应延迟较为敏感
场景二是写入很少,但读取很多,对读取的响应延迟较为敏感,这里需要介绍一下cassandra默认的compacion策略叫做Size-tiered compacion。在compacion执行很多次之后,磁盘上的sstable会形成类似于根据大小进行分层的一种分布设计。这种默认的compacion设计,有好处也有劣势,每一个sstable是可能存在重叠,在最坏的情况下,一个读取请求进来,可能需要对每一个sstable做一次seek操作,才能知道当前sstable里面有没有对应的数据,文件数越多,涉及的IO越长,延迟越长。
这时可以考虑使用另一种compacion策略,叫做leveled-tiered compacion。能够把磁盘上的sstable分成若干层,每一层里面的sstable相互之间没有重叠关系,因为有这个保证,所以一条读取请求进来之后,每一层里面最多只有一个sstable纳入到归并排序当中,层级次数决定涉及这一次读取请求需要处理sstable的上线,导致读延迟更低,也更加可控。
由于需要保证每一个层级里面的sstable相互之间不重叠,所以对写放大较为严重和较多compacion任务的执行,比较适合于读比较多,写入比较少的场景。
场景3——写入量级特别大
场景三写入量级特别大,如:IoT行业、物联网行业、滴滴轨迹记录等行业,很少读取,但写入量级特别大。
针对这种场景有几个优化方式:
1)方式一:
·常识:网络延迟>磁盘写入延迟。
·通过batch insert的方式,降低客户端和服务端的交互次数,从而减少网络延迟的影响。
需要知道网络延迟远远高于磁盘写入延迟,写入量级特别大的时候,可以考虑批量写入方式,来使网络延迟减少客户端和服务端的交互次数,从而减少网络延迟的影响。
·Cassandra batch可以实现跨表级的事务,但是性能损耗较大。
使用注意事项:
·只做一张表的batch insert;
·使用unlogged的batch insert;
·每一个batch insert 包含的insert数建议不超过10。
2)方式二:
如果前一个方式应用之后,还是不能满足写入要求,遇到第二个瓶颈,commit log flush 目前cassandra 是单线程做的,写入量级太大的情况下,会成为瓶颈。
原理是在cassandra里面,默认每10秒对commit log做一次sync操作,sync操作把内存里面的commit log刷到盘里面去,每次sync都会产生一次磁盘写入的峰值,而这一次写录其实还没有真正写入到磁盘的介质当中,会写入到磁盘的pageCache里面,默认策略为周期性将pageCache中写入持久化。目前很多用户使用各大云厂商的云盘产品都有写入流量的限流策略。默认参数下面,每一次写入磁盘都是一次大批量的写入,会产生较大峰值,很有可能触发写入流量的限流策略。
在这个情况下,调节Linux内核参数:
Vm.dirty_writebark_centisecs=10
Vm.dirty_expire_centisecs=20
可以提高 pageCache下刷云盘的频率,从而导致每一次刷下去的大小都会减小,不容易触发限流策略,导致了io Write显着的降低,对于写延迟会有质的提升。
3)方式3:
由于默认每10秒一次sync,如果写入量级特别大,10秒内会堆积大量commit log,需要被sync到磁盘,而在cassandra的实现里面,对于commit log的sync操作,是单线程处理的操作。所以当sync的速度跟不上写入的速度的时候,写入就被block住等待。
如上图所示,含义是根据上一次sync完成的时间点和当下期望的时间点,如果sync小于期望sync的时间点,证明sync的速率跟不上写入的速率,就会做一个等待操作,降低写入的TPS让sync能够追上。
所以搭建一个完整的监控系统,要看waitingOnCommit监控指标发现这一瓶颈。每一个commit log都是一个单独文件,云cassandra通过文件级别并发做sync提升了sync环节的处理效率,解决了这一问题。
4)方式4:
为了数据可靠性,往往建议设置多个副本。多副本的场景下,接受节点往往会生成一个副本同时向其他节点发送请求,生成足够的副本数。每个节点和集群所有其他节点之间会维护一个队列来保存节点之间通信的消息。
举例:3副本,10wtps写入,会产生20w内部消息通信,副本数越多,内部消息通信放大越严重当量级过高时,cassandra默认的队列实现会产生严重的锁冲突,导致性能下降。通过iprofiler的jvm性能工具可以方便的监测锁冲突的严重情况。
在这个场景下面,可以通过降低副本数,达到减少内部通信的量级规避问题。或者增加节点数,因为节点数增加了,变相的节点之间的队列数就增加了,每一个队列的负载相对就会减少,从而达到降低锁冲突的效果,但是这两个措施都需要业务改造,并且都有对应的成本。比如副本数降低,数据可靠性就降低了,或者集群节点数增加,运维成本就会增加,增加的是整个经济成本。
云cassandra通过实现了一个高性能的无锁队列,参考开源的实现disruptor,性能非常棒,替换了cassandra默认的队列实现,大幅提升了这一场量下的性能上限。
场景4——读响应时间长,磁盘IO重
读响应时间比较长,io比较重,这里有一个通用的使用误区,在cassandra之前的版本,如果使用了压缩,它默认的大小是64k,某种程度上是合理的,因为压缩它是需要针对一个指定的大小进行一个压缩,才能达到一个比较理想的压缩率,磁盘的存储成本才能够降低,但是默认的64k还是太大,所以在社区讨论已经建议调整为16k,在新版版本里,默认的压缩的快大小就是16k,同时这个大小可以满足实现比较理想的一个压缩率,好处就是io会显著的降低。
所以当block的大小设置的太大的时候,磁盘io会增加特别多,通过alert表属型进行调整之后,做一个major compaction,才能使调整真正的生效。
除了调整block的大小之外,磁盘io重,就是因为cache的命中没有命中到,走到磁盘,产生磁盘io,可以通过nodetool info工具,看到对应如chunk cache和key cache的大小以及对应的命中率。通过cache如果实例上有较富余的内存资源,可以调整chunk cache和key cache的大小,从而去提升cache的命中率,这样落到磁盘上的io就会显著地减少,降低io,从而读响应时间会更加的快。
如果场景是80%的读请求集中在20%的row上面,可以考虑开启row cache功能,它默认是关闭的,因为它会带来内存的压力,开启便会有比较好的一个效果。
key cache、chunk cache和row cache三个cache的大小是没有一个最佳的比例的,根据实际情况,去调整三个cache的大小关系,调整完后以监控数据为准,监控数据实际效果好的就是最佳的一个大小比例。
这也是一个完善的监控是很重要的,就是它不仅能够发现问题,还能去验证优化效果是否达到了预期,优化措施是否有效果。
场景5——读响应毛刺较高
读响应毛刺很高,较高的概率是由于gc导致的,如较长的full gc或者很频繁的full gc,首先就需要排查cassandra是否存在内存泄漏,其次去参考jvm的GC调参最佳实践,调参GC相关的参数,也能实现GC的优化。
考虑使用cassandra自带的Speculative特性,其工作原理就是客户端向服务端的一个节点发送了请求,根据自己设置的一个域值,在超过这个阈值之后,如果请求还没有得到相应的话,会向第二个节点发送一个请求,这时候是针对一个请求,发送了两次,请求到服务端这两次请求任何一次请求优先返回之后,都算成功生效。
思路就是一个集群内可能周期性的会有一些慢节点,通过这个阈值来判断是否请求到了一个慢节点,如果是,就换一个节点,从而规避掉慢节点,避免请求一直被卡住,如果阈值设置的过小,会产生较多的额外请求,会增加整个集群的负载和压力,需要根据监控去不断的调整的阈值,找到一个比较合适的点。
(三)性能调优总结
性能调优
第一,一定要有监控,有监控才能去做分析,去评估优化的效果,监控是所有性能优化的出发点和终点。
第二,熟练掌握相关的工具,如jprofile是一个比较好的监控GC或内存泄漏的一个工具。
自带的nodetool,其实会暴露内部的一些情况,如线程池的使用状况,但也可比较好排查性能瓶颈位于哪一个环节,需要对原理有一定的理解,才能去分析可以采取的措施,以及说当前处于瓶颈的会涉及到的环节,哪些环节可能成为的性能瓶颈。
优化的时候,可以考虑从业务改造上去进行规避,也可以考虑从源码级别去做性能的优化,思路比较灵活。