架构师回忆录の非主流面试攻略(解析高性能开发工具附代码)
Posted 玖富IT饭米粒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了架构师回忆录の非主流面试攻略(解析高性能开发工具附代码)相关的知识,希望对你有一定的参考价值。
点击蓝字关注玖富IT人内刊
下一个头条就是你
事情得从几年前我来玖富面试说起,
面试官是当时的带头大哥老程,
老程问我:“高并发不就那么三招,分布式,静态化,加缓存,你还有啥本事”
我心想坏了,今天出门没看黄历,遇到行家了……
但最终的结果是我成功入职玖富并走到了今天,
当时老程听我说起这些另类的手段也是眼前一亮,
那么现在就来谈谈当时我通过面试这关的几个
非主流高性能开发工具。
叙述人|冯金鑫
友情出镜|程 云
还是从数据库说起,我们常用的为关系型网络数据库,而最容易出问题的就是数据库,一旦在访问量较大的时候发生慢查询或锁定,很容易导致上层服务宕机,当然,通过各种手段可以缓解上层宕机,例如服务降级,熔断,限流等,但终究的解决办法还是将存储服务的速度提高上去。
干货的
分割线
No.1
首先介绍的是进程内的数据库服务,应用场景是在一些高速不需要全网数据同步的应用中,这种内嵌式nosql和内嵌式关系数据库,往往能给应用带来意外的便利。
H2DB为纯java编写的内嵌式数据库,在很多demo程序中,作为示例程序使用,特点是小巧轻便,一个jar就可以完成数据库的启动和连接,不需要额外的配置,因此,H2 经常被忽略的一点就是他的性能,尤其在H2的使用直接内存方式启动的时候,建立合适的索引与数据库结构,可以很容易达到1w级别的qps。
当然,这个方式省去了两个重要的因素,一个是数据全放在内存之中,省去了持久化,也就避免了大量的IO操作,系统不会出现大量的IOWait,另外一个因素就是不是网络数据库,不接受外部的tcp方式访问,也就省去了进程间通信的消耗。当进程停止后,再次启动,数据就会丢失,更多的情况,我们希望找到一种方式兼容h2的进程内高速访问和数据持久化。
经过探索,我们确实找到了一种比较合适的方法,首先使用文件替代内存方式作为h2的启动方式(jdbc:h2:file:~/.h2/DBName),然后我们在文件存储上找到解决方案,我们在linux上执行一个命令
mount -t tmpfs -o size=8g tmpfs /mnt/memdisk
通过这个命令,我们创建了一个最大为8g的内存盘作为存储介质,将h2的数据文件存储到这个内存盘上,那么只要操作系统不宕机,h2的数据就不会丢失。这种方式适合于有存储窗口时间的应用,并且h2的数据量不会超过事先设定的内存盘大小,在适当的时间,还是需要将内存盘的文件copy到物理硬盘上。
No.2
第二个出场的是一种高效的内嵌式队列FQueue。FQueue是我们国人开发的一款高性能的消息队列系统,纯java编写,使用物理磁盘进行存储,利用文件存储顺序读写性能很高的特点,将消息存储于物理文件上,每秒能达到10w+的高速消息读写,同时占用内存极小。
在应用中,我们经常遇到很多异步处理的场景,例如发送短信,请求第三方接口以及异步调用等,通常的情况下,我们处理这类的情况时,会将消息放到一个队列中,例如activemq或kafka等,也有一些情况,我们直接放到jvm的LinkedBlockQueue队列中,从速度上看,jvm进程内的速度最快,使用也最方便,不需要配置和部署,缺点是占用内存,队列过长的话,容易造成进程内存溢出,发生OOM,而jms类的网络队列,支持分布式与持久化,但稍微有点重量级。
那么综合两者的优点,兼容方便,轻量,持久化的进程内队列fqueue就非常合适了。使用以下API接口使用方式
FQueue fqueue = new FQueue("dataqueue", 10 * 1024 * 1024);//初始化一个10M大小,文件名为dataqueue的队列
queue.addElement(data.getBytes()); //将数据放入队列
byte[] data = queue.fetch(); //从队列中获得数据
Fqueue将元素顺序存储在文件中,不用担心内存被撑爆,也不用担心数据的丢失,在处理过程中,FQueue会自动记录当前处理文件的偏移量,文件内所有元素消费完成后,删除该文件,不占用磁盘空间。
需要值得注意是fqueue的作者并没有把fqueue作为一个阻塞队列来实现,在拉取消息的时候不支持take和poll方法,而fetch方法为非阻塞提取元素方法,同时fqueue为进程内队列,不同jvm无法队列元素。
既然为非阻塞队列,在部分开发者使用的时候,就感觉不如linkedblockqueue方便直接了,那么我们可以再前进一步,既然作者没写,我们就封装一下,利用jdk的Condition类在进程内处理下,当队列中元素为null时,调用Condition的await()方法,让当前线程等待,在addElement的时候,调用Condition的signal()方法,处罚等待线程去获得对象。
No.3
第三个介绍的是另类的使用mysql的方式。
本身mysql是相当常用的数据库,轻便免费开源,同时,我们在应用中配合一些非关系型数据库实现高速操作,例如mongodb,memcache,这些nosql速度快,但在事务性,原子性一致性,主从同步等方面不如mysql成熟,我们希望找到能同时兼容mysql和nosql的优点的方式,在这种情况下可以使用mysql的两个特殊插件,handlersocket和TDH_Socket,两种插件的优点比较类似,不同点是handlersocket为日本人编写,TDH_Socket为国内阿里团队开发,原理比较类似,都是从直接从innodb的API直接入手,跳过sql的解析,使用支持大量连接的epoll进行网络操作,内部合并多个请求操作,减少cpu和IO,通过client的API进行操作,比直接通过jdbc执行sql语句要快3-5倍左右,当直接落到主键索引的时候,qps可以达到1w+,可以替代memcache的kvdb方式。
重要的一点是,通过handlersocket写入的数据支持mysql的主从同步和数据的原子性。那么这个启动了handlersocket插件的mysql数据库就同时拥有nosql和关系型数据库的优点。但是,不完美的地方就是,目前大部分云端数据库不支持自己安装mysql插件。
另一个mysql的非主流使用方式,java客户端在与mysql服务器进行连接的时候,我们通常使用的连接串如下:
jdbc:mysql://127.0.0.1/mydb?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&useCompression=true&useNewIO=true
可以检查下上面的连接串,useCompression通过压缩减少数据量,useNewIO可以允许建立更多的连接,这样的目的是减少网络通信对性能的影响。
那么另一种访问方式就是unix domain socket,不把mysql当做网络数据库,而当成一个本地存储使用。Unixsocket是进程间通信的一个方式,不经过网络tcp/ip协议,避开了网络连接和数据验证,但在我们java开发上使用的不多,主要也是这玩意不跨平台,从名字上看windows就不支持这个,java本身也没有支持这个。那么在一些比较特殊的场景上,java作为上层业务服务层,想要调用本地的一些服务,就可以走这种高效的通信方式,但java本身不支持unixsocket,可以使用第三方的junixsocket,作为java的连接unixsocket的方式。以mysql为例,我们常常在配置mysql时,见到
socket=/data/mysql/mysql.sock
这个字样,通常我们不去管他,同时我们使用netstat -na|grep unix|grep mysql可以看到
unix 2 [ ACC ] STREAM LISTENING 86211262 /data/mysql/mysql.sock
这种结果,那么通过这个,我们就可以使用unixsocket连接mysql
代码如下
Class.forName("com.mysql.jdbc.Driver").newInstance();
Properties props = new Properties();
props.put("user", "*****");
props.put("password", "****");
props.put("socketFactory", AFUNIXDatabaseSocketFactory.class.getName());
props.put("junixsocket.file", "/data/mysql/mysql.sock");
Connection conn = DriverManager.getConnection("jdbc:mysql:///mydb", props);
经过这样的处理,本地mysql就可以避开大量建立连接的开销,也可以提高部分性能。
No.4
第四个为lucene的封装服务,内嵌式应用solr。
在很多应用中,我们经常感觉mysql的索引字段不够用,但建立过多的索引会导致mysql的响应速度变慢,查询解析优化器偶尔也会使用不适当的索引。那么在这个情况下,可以进行全字段索引的lucene就非常合适了,从数据量上看,lucene本身单机可以承载数亿条记录,经过solr或elasticsearch的封装分core处理,几乎承载能力无限。
当然对于这些场景的应用,一般为非严格事务型应用,就是主要为检索查询但不限于全文检索类服务,例如原始数据在mysql中,定时增量或全量同步到lucene存储中。直接操作lucene的底层API比较繁琐,数据同步和shard分片直接通过文件IO比较苦难,这时solr和elasticsearch这两基于lucene的服务就非常适合于这种场景了,二者都有丰富的字段,支持数字和文本,日期以及多值存储。
如果想要最快的速度,就像我们使用h2db的方式,省去网络通信和序列化反序列化,但仍需要保存的有数据的持久化和主从同步,这个时候,我们可以采用solr的内嵌式方式,这个内嵌solr,并不是使用EmbeddedSolrServer作为内嵌对象,因为在这个对象的使用上,不支持主从同步。
我们进行另一种封装,以下面代码为例
public class FDirectSolrConnection extends DirectSolrConnection {
public FDirectSolrConnection(SolrCore c) {
super(c);
}
}
使用继承DirectSolrConnection的类启动一个solrcore,同时重写父类方法
public String request(String path, SolrParams params,HttpServletResponse response)
方法,这样就即拥有了solr的主从同步,dataimportHandler功能,又通过jvm进程内调用,避开了网络通信和对象序列化和反序列化的时间。
经过测试,使用内嵌的solr比通过solrj调用的方式并发可提高30%左右。
No.5
最后介绍的是两种java内嵌式的kvdb。
Kvdb是应用内经常使用的缓存处理方式,例如在jdbc查询的时候,一些场景下,数据变化不频繁,我们会采用两种缓存方式,一种为mysql的querycache,数据库通过数据变化触发缓存的失效,这个缓存通常设置的不大,主要原因为这个cache的锁比较厉害,是全局锁,数据更新频繁的时候,这个锁引起的数据库性能下降比较严重,在主库上使用的较少。
另一种是在java查询后,将查询结果放在数据库之外的缓存中,下一次同样的查询就直接从二级缓存中直接返回,减少数据库的压力,这些常用的有ehcache,memcache和redis。
在这些主流的存储之外,有一些本地进程内存储表现也相当优秀,一个是Berkeley DB,一个是leveldb。
本身berkeley DB和leveldb官方都是为c开发实现,java语言调用需要走网络接口或JNI方式,这样本身存储性能优异,但加上网络IO就多了一层开销,那么今天介绍这两种KVDB的对应java实现,一个是oracle官方的java版本的berkeley DB,一个是org.iq80.leveldb实现的java版本的leveldb存储。
首先我们要获得的是超快的本地存储的IO性能,经过测试,在百万级别的数据量下,两个kv存储,都可以提供5w+的qps,在更大的数据量情况下,iq80的性能表现更高,不会随数据量的增加产生性能下降。同时,由于leveldb的特点,存储的key有序和提供超高速的数据写入速度,leveldb在按照指定key查找和按照范围查询的情况下性能表现优异。
第二,我们考虑实现一点点封装,本身这两个kv存储的API是面向二进制字节数组,而java上层调用通常为对象形式,把对象转换成byte[]以及反序列化回来,需要高速序列化和反序列化的组件,这块我们有几个选择:
1) JDK自带的序列化
2) Hessian方式
3) Fastjson方式
4) Avro的序列化反序列化
5) ProtoBuf的方式
6) 我们内部自我开发的一种table表方式的序列化反序列化的方式
按照通常的写法,此处本应该是我们后来自己开发的table表方式最终胜出,不过事实相反,是ProtoBuf的性能最好,在10属性的java对象上,PB可以提供近40万次的序列化反序列化操作。
那么相比于redis和memcache,本地内嵌面向对象的kv缓存,更具有轻量级,灵活性的特点,尤其是配置方便,在不需要多节点共享缓存的情况下表现优异。
以上是几种非主流的内嵌式存储,在特定的场景下,可以发挥灵便的作用,我们开发的时候,不局限于教科书,根据业务场景和性能要求找到适合自己应用的存储。
玖富丨IT饭米粒《科技第三弹》End
to be continued……
以上是关于架构师回忆录の非主流面试攻略(解析高性能开发工具附代码)的主要内容,如果未能解决你的问题,请参考以下文章
2021年互联网行业,1K+Java面试题精选!(内附答案解析及面试攻略)
2021年互联网行业,1K+Java面试题精选!(内附答案解析及面试攻略)
2021年互联网行业,1K+Java面试题精选!(内附答案解析及面试攻略)