一次数据查询设计实现始末

现有一文本文件,文件内容每行记录由3个字段组成,字段间以’\t’分隔,每行以’\n’分隔,3个字段分别为”身份证号码 手机号码 姓名“。现需要提供一个服务,业务可通过服务根据身份证号码或者手机号码精确查询相关记录。那么如何设计,使得查询响应延迟低、吞吐高和资源消耗低。

一、数据分析

1. 数据总量

“10亿条由身份证号码、手机号码和姓名组成的记录”,如果这些记录以文本格式存储,那么

  • 身份证号码为定长记录,需要18个字节表示
  • 手机号码为定长记录,需要11个字节表示
  • 姓名为变长记录,假设平均一个姓名需要3个汉字来表示,并且使用UTF8编码。那么则姓名平均需要9个字节表示
  • 每行记录包含三个’\t’和一个’\n’,需要4个字节表示

总共则需要$(18 + 11 + 9 + 4 ) × 1000000000$字节来进行存储,即整个记录文件大小大约有39.2GB。而这些记录如果全部装载入内存,则至少需要$(18 + 11 + 9 ) × 1000000000$字节,约35.4GB。如果使用Java实现,还需要考虑到额外的存储占用。

2. 身份证号码

这里的身份证号码统一指中华人员共和国公民身份证上给出的公民身份号码。

该号码格式:

六位地址码+八位出生日期码(yyyyMMdd)+三位顺序码+一位校验码

码号 值域 总数 说明
地址码 N/A <32000 六位行政区划代码(不包括港澳台)。
理论总数不超过310000个,实际上不超过32000个
出生日期码 [19000101, $\infty$] 365×年数 理论上无限个,但实际(截止2019年)已有的不超过43800
顺序码 [000, 999] 1000
校验码 [0, 10] 11 实际应用中,10使用X进行表示

截止今年(2019),身份证号码的理论值域空间大小不超过1401600000000,但实际不会超过1500000000。

其中,校验码可以由前面17位数字通过算法计算得到(具体算法参见附录)。因此,实际存储时可以去掉。这样记录中一个身份证就可以使用长整型来表示,那么单个身份证号码存储就可以节省10个字节。这样,记录中的身份证号码存储至多需要7.5GB。

3. 手机号码

这里的手机号码统一指中华人民共和国大陆地区手机所用号码

该号码格式:

三位网络识别码+四位地区码+四位用户号码

码号 值域 总数 说明
网络识别码 N/A <60 主要包括电信、移动和联通三大运营商
地区码 [0000, 9999] 10000 可用于识别手机号码归属地
用户号码 [0000, 9999] 10000 随机分配

手机号码的理论值域空间大小不超过6000000000。可以看出手机号码的值域空间比身份证号码的要大,换句话说,记录中身份证号码重复的可能性要比手机号码更高。

由于手机号码由纯数字组成,这样记录中的一个手机号码就可以使用长整型来表示,那么单个手机号码存储就可以节省7个字节。这样,记录中的手机号码存储至多需要7.5GB。

4. 姓名

我国是个多民族国家,虽然姓名组成比较复杂,但姓名的重复率还是很高的。比如常见的“王伟”,“张伟”等。特别地,在<身份证号码,手机号码,姓名>三元组中,可能会存在一个身份证号码对应多个手机号码的情况,使得姓名的重复率进一步上升。而姓名的值域空间大小则不会超过当前中国人口大小(<15亿)。

这里我们可以将数据中的姓名取出并进行去重处理,同时可以使用一个整型来对去重后的姓名进行编码。这样记录中一个姓名可以使用编码后的一个整型值表示,那么单个姓名存储就可以固定为4个字节。这样,记录中的姓名存储(包括编码和姓名本身)至多需要(假设记录中姓名去重后最多有一亿个)1.2GB。

当然,也可以进一步地将姓名拆分为单个汉字去重后编码,但这样编码后,复杂度上升,单个姓名长度不再固定,每个汉字编码后占用2个字节,这样算下来实际节省空间也没有提高多少。

二、存储设计

通过数据分析后,我们发现如果采用数据全部加载到内存的方式,对整个资源要求是比较高的。因此,自然会想到将“一部分”数据放在内存,“另一部分”数据放在磁盘上。但关键在于哪部分数据在内存,哪部分在磁盘,并且需要解决以下问题:

  • 内存中的数据如何组织
  • 如何从较慢的磁盘上快速地读取数据
  • 数据如何缓存和淘汰

该章节我们先来主要回答前两个问题,第三个问题在下一个章节中描述。

1. 磁盘文件格式

为了能够在磁盘文件中能够更快的读取内存中不存在的记录,设计时应:

  • 磁盘文件尽可能紧凑(体积尽可能小),这样每次I/O读取到尽可能多的记录
  • 能够快速定位记录在文件中的位置(偏移量)

因此,为了能够满足这些要求,磁盘文件格式设计为二进制格式。

身份证号码可索引文件

用于通过身份证号码检索记录的场景。

总体结构

通过一级和二级索引,能够快速定位记录在文件中的位置。

元数据结构

从左到右依次是:

  • Len表示文件所采用的编码字符串长度,比如”utf8“长度为4
  • Charset表示文件所采用的具体编码,比如”utf8“
  • idx表示索引字段下标,即构建该索引文件时读取的文本文件中索引字段所在下标
  • Range表示如何对索引字段进行划分,用于构建二级索引。其中第一个表示一级索引中key如何得到及其所占用字节数,第二个表示二级索引中key如何得到及其所占用字节数,第三个表示内容中每条记录前缀如何得到及其所占用字节数
    • Start表示取值时在索引字段中的起始位置(包含)
    • End表示取值时在索引字段中的结束位置(包含)
    • Size表示该Range表示的值存储时所占用字节数
  • FieldNum表示内容中每条记录由多少个字段组成
  • FieldSize表示内容中每条记录所占用字节数
    • Index表示记录中第几个字段
    • Size表示记录中相应字段所占用字节数
  • Length表示整个元数据(不包含Length所占用的4个字节)的长度

元数据的作用在于其中的信息可用于指导程序正确地解析二进制文件。

一级索引结构

从左到右依次是:

  • Num表示一级索引中的索引条目数量,最大支持32767个
  • Range1表示通过元数据中第一个Range描述由索引字段生成的值,每个值是唯一的
  • Offset表示符合Range1值的二级索引在文件中的位置(偏移量)
  • Length表示整个一级索引(不包含Length所占用的4个字节)的长度

一级索引的作用在于能够根据索引字段的部分信息先缩小需要读取的数据范围。

二级索引结构

从左到右依次是:

  • Num表示二级索引中的索引条目数量,最大支持32767个
  • Range2表示元数据中第二个Range描述由索引字段生成的值,每个值是唯一的
  • Num表示符合Range2值的记录数,最大支持16777215个
  • Offset表示Range2值的第一条记录在文件中的位置(偏移量),最大支持1TB大小文件
  • Length表示整个二级索引(不包含Length所占用的4个字节)的长度

二级索引的作用在于能够根据索引字段的部分信息进一步缩小需要读取的数据范围,并定位到符合要求的记录可能的位置。

内容结构

从左到右依次是:

  • Range3表示元数据中第三个Range描述由索引字段生成的值,每个值是唯一的
  • Phone Num表示一个手机号码
  • Name Seq表示姓名编号

每个二级索引下的记录按照Range3值排序,这样可以方便进行最后的快速检索。

索引文件构建

索引构建时需要对原始数据做一定的处理。

  1. 提取出原始数据中姓名字段,并生成姓名索引文件$Name_{index}$和姓名编码文件$Name_{code}$
  2. 使用姓名编码文件$Name_{code}$中的编码替换原始数据中的姓名,生成文件$SF_1$
  3. 对文件$SF_1$中的身份证号码字段进行切分,按照一级索引、二级索引和内容前缀的顺序并使用”,”重新拼接,生成文件$SF_2$
  4. 对文件$SF2$中的记录按照重新组合后的身份证号码进行排序,生成文件$SF_3$
  5. 对文件$SF3$进行处理,生成最终的身份证号码可索引文件$Index_{sfz}$

假设对10亿条数据进行索引构建时,参数设置如下:

参数 设置
charset utf8
Range1 start=0;end=5;size=4
Range2 start=6;end=9;size=2
Range3 start=10;end=16;size=4
FieldSize1 index=0;size=4
FieldSize2 index=1;size=8
FieldSize3 index=2;size=4

那么,整个索引文件$Index_{sfz}$大小不会超过15GB:

  • 元数据大小:36 byte
  • 一级索引大小至多:(6 + 12 * 32000) byte
  • 二级索引大小至多:(6 + 10 * 100) * 32000 byte
  • 内容大小:16 * 1000000000 byte

手机号码可索引文件

用于通过手机号码检索记录的场景。

总体结构

和身份证号码可索引文件的总体结构相同。

元数据结构

和身份证号码可索引文件的元数据结构相同。

一级索引结构

和身份证号码可索引文件的一级索引结构相同。

二级索引结构

和身份证号码可索引文件的二级索引结构相同。

内容结构

从左到右依次是:

  • Range3表示元数据中第三个Range描述由索引字段生成的值,每个值是唯一的
  • SFZ表示一个身份证号码(其中校验位已去除)
  • Name Seq表示姓名编号

每个二级索引下的记录按照Range3值排序。

索引文件构建

索引构建时需要对原始数据做一定的处理。

  1. 使用身份证索引构建过程中生成的姓名编码文件$Name_{code}$中的编码替换原始数据中的姓名,生成文件$MF_1$
  2. 对文件$MF_1$中的手机号码字段进行切分,按照一级索引、二级索引和内容前缀的顺序并使用”,”重新拼接,生成文件$MF_2$
  3. 对文件$MF2$中记录按照重新组合后的手机号码进行排序,生成文件$MF_3$
  4. 对文件$MF3$进行处理,生成最终的手机号码可索引文件$Index_{mob}$

假设对10亿条数据进行索引构建时,参数设置如下:

参数 设置
charset utf8
Range1 start=0;end=2;size=2
Range2 start=3;end=6;size=2
Range3 start=7;end=10;size=2
FieldSize1 index=0;size=2
FieldSize2 index=1;size=8
FieldSize3 index=2;size=4

那么,整个索引文件$Index_{mob}$大小不超过13GB:

  • 元数据大小:36 byte
  • 一级索引大小至多:(6 + 10 * 60) byte
  • 二级索引大小至多:(6 + 10 * 10000) * 60 byte
  • 内容大小:14 * 1000000000 byte

姓名字典文件

用于姓名编号到姓名的转换。该文件结构比较简单。

总体结构
索引结构

从左到右依次是:

  • Len1表示第一个姓名长度
  • Len2表示第二个姓名长度
  • Len3表示第三个姓名长度,以此类推
  • Length表示整个二级索引(不包含Length所占用的4个字节)的长度

可通过长度累加计算生成对应的姓名在文件中的位置。

内容结构

其中内容的存储顺序和索引的存储顺序依次对应。

索引文件构建

在身份证号码可索引文件中已经生成文件$Name_{index}$ 。此索引文件的大小为取决于姓名去重后数量。

  • 索引大小:姓名去重后数量 * 1 byte
  • 内容大小:3 * 姓名去重后总长度 byte

2. 内存中的索引结构

使用时,将身份证号码可索引文件和手机号码可索引文件中的索引部分全部加载到内存,姓名字典文件全部加载到内存中。

身份证号码索引

索引构建

加载所有的一二级索引到内存中,构建散列表:

1
Map<Integer, Map<Integer, Long>> index;

如果构建一二级索引时,使用身份证号码中的地址码作为一级索引,出生年份作为二级索引,那么整个索引个数不超过3840000个。

索引使用

可以通过如下步骤使用索引:

  1. 使用Range1的start和end获取待查询身份证号码对应的一级索引值$Value_1$
  2. 通过索引值$Value_1$,查询索引Index,得到二级索引$Index_{2}$。如果无法找到,则不存在该身份证号码,进入第九步
  3. 使用Range2的start和end获取待查询身份证号码对应的二级索引值$Value_2$
  4. 通过索引值$Value_2$,查询二级索引$Index_{2}$,得到记录在索引文件中的位置${Offset}$和对应的记录条数$Num$。如果无法找到,则不存在该身份证号码,进入第九步
  5. 使用Range3的start和end获取待查询身份证号码对应的记录前缀$Prefix$
  6. 通过位置${Offset}$读取索引文件中的记录,并比对记录中的前缀是否等于$Prefix$
  7. 如果相等,则继续读取下一条记录,直到找到的记录前缀不等于$Prefix$ ,返回获得的记录;如果不相等,继续读取下一条记录,直到找到的记录前缀等于$Prefix$ 或者$Num$为零(即无剩余记录)
  8. 如果获得记录数不为空,则构造查询结果。将查询使用的身份证号码反填到结果中,并使用姓名编号通过姓名字典获取对应的姓名。得到最终查询结果,并放回
  9. 结束本次查询

手机号码索引

索引构建

加载所有的一二级索引到内存中,构建散列表:

1
Map<Integer, Map<Integer, Long>> index;

如果构建一二级索引时,使用手机号码中的用户号码作为一级索引,网络识别码作为二级索引,那么整个索引个数不超过600000个。

索引使用

可以通过如下步骤使用索引:

  1. 使用Range1的start和end获取待查询手机号码对应的一级索引值$Value_1$
  2. 通过索引值$Value_1$,查询索引Index,得到二级索引$Index_{2}$。如果无法找到,则不存在该手机号码,进入第九步
  3. 使用Range2的start和end获取待查询手机号码对应的二级索引值$Value_2$
  4. 通过索引值$Value_2$,查询二级索引$Index_{2}$,得到记录在索引文件中的位置${Offset}$和对应的记录条数$Num$。如果无法找到,则不存在该手机号码,进入第九步
  5. 使用Range3的start和end获取待查询手机号码对应的记录前缀$Prefix$
  6. 通过位置${Offset}$读取索引文件中的记录,并比对记录中的前缀是否等于$Prefix$
  7. 如果相等,则继续读取下一条记录,直到找到的记录前缀不等于$Prefix$ ,返回获得的记录;如果不相等,继续读取下一条记录,直到找到的记录前缀等于$Prefix$ 或者$Num$为零(即无剩余记录)
  8. 如果获得记录数不为空,则构造查询结果。将查询使用的手机号码反填到结果中,计算并回填身份证号码因构建时去除的校验位,使用姓名编号通过姓名字典获取对应的姓名。得到最终查询结果,并返回
  9. 结束本次查询

姓名字典

索引结构

加载整个字典文件到内存中,并按照索引中的顺序给每个姓名依次赋予编号。这个索引可以使用以下数组表示:

1
String[] index = new String[姓名个数];

这个索引占用内存大小和姓名个数有关。

索引使用

通过身份证号码索引或者手机号码索引获取得到的记录中取得姓名编号$Seq$,直接通过index[Seq]得到相应的姓名。

3. 索引的选择问题

索引的选择对性能的影响主要在于数据的分布情况。如果一二级索引没有很好的”区分度“或者说一致性分布不佳,则会导致部分索引下的记录数据量过多,进而导致查询有大概率落入这些区域,造成更多的磁盘I/O和搜索时间。

同时,整个索引(包括一级索引和二级索引)的数量也要控制,否则会造成索引加载后,内存占用过高或者不能将索引全部加载到内存,导致索引效率下降。

身份证号码索引选择

从身份证号码的组成来看,顺序码和出生月日的随机性和均匀性较好。因此,这里选择出生月日(身份证10位到14位)作为一级索引,顺序码(身份证15位到17位)作为二级索引。使用两千万真实身份证号码进行测试:

统计项 统计值
一级索引数量 3650
二级索引数量 2988063
记录总数 26888239
记录最大数量 421
记录最小数量 1
记录平均数量 9
记录数量标准差 13.2

整体散列效果还是可以的。这种索引理论上总共有3650000个。

现在有10亿条记录,每个二级索引下平均记录数为274条左右。每条记录大小为16byte,那么平均每个二级索引下的记录大小4KB左右,而每次I/O读取的一个磁盘页大小一般为4KB,这样每次平均只需要一次I/O就可以完成数据的搜索。

手机号码索引选择

从手机号码的组成来看,用户号码的随机性和均匀性较好。因此,这里选择用户号码(手机号码8位到11位)作为一级索引,网络识别码加一位网络识别码(手机号码1位到4位)作为二级索引。使用两千万真实手机号码进行测试:

统计项 统计值
一级索引数量 10000
二级索引数量 2787015
记录总数 26888239
记录最大数量 4625
记录最小数量 1
记录平均数量 9.7
记录数量标准差 16.76

整体散列效果还是可以的。这种索引理论上不超过6000000个。

现在有10亿条记录,每个二级索引下平均记录数为200条左右。每条记录大小为14byte,那么平均每个二级索引下的记录大小3KB左右,而每次I/O读取的一个磁盘页大小一般为4KB,这样每次平均只需要一次I/O就可以完成数据的搜索。

三、关于缓存

虽然每次I/O很小,但随着访问量增大,I/O变得频繁。因此,可以使用缓存,减少磁盘I/O,提升性能。

1. “免费的午餐”

我们知道计算机在速度上是“阶级”分明的。CPU运行速度最快,内存存取速度跟不上,于是有了L1、L2甚至L3缓存。同样磁盘速度相对于内存而言,又是一个量级的差距,因此操作系统往往会充分利用系统中空闲的内存来作为磁盘读取和写入数据的缓存,来提高I/O性能。在Linux系统中,这部分工作通过PageCache和BufferCache来实现(2.4以后,统一使用PageCache实现),比如我们在系统中执行free -m命令时:

1
2
3
4
             total       used       free     shared    buffers     cached
Mem: 257931 251719 6211 3 659 205733
-/+ buffers/cache: 45326 212604
Swap: 0 0 0

其中的cached即读写文件时的缓存,包括页缓存和可回收Slab缓存,而buffers则为读写磁盘时的缓存(一般不大)。这里我们重点关注cached ,可以从/proc/meminfo中得到根据详细的信息:

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 210684424 kB
SwapCached: 0 kB
SReclaimable: 7465988 kB

下面我们看一下,这一机制对性能的影响。

首先,先清空系统缓存。

1
$ sudo echo 3 > /proc/sys/vm/drop_caches

查看系统缓存情况。

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 121468 kB
SwapCached: 0 kB
SReclaimable: 86560 kB

读取索引文件(记录总数三千万不到),构建索引后,查看系统缓存情况。

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 291524 kB
SwapCached: 0 kB
SReclaimable: 82520 kB

可以看到页缓存明显增加,新增了大约166MB。

使用100000条数据模拟查询,查看系统缓存情况。

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 667220 kB
SwapCached: 0 kB
SReclaimable: 82520 kB

可以看出页缓存进一步增加,又新增了大约366MB。而此时,查询性能情况。

1
2
3
4
5
# 第一次查询
Total time: 3730 ms
Avg time: 0.037300 ms
Max time: 2 ms
Min time: 0 ms

后续使用相同的数据重复两次查询,得到的性能情况。

1
2
3
4
5
6
7
8
9
10
11
# 第二次查询
Total time: 429 ms
Avg time: 0.004290 ms
Max time: 1 ms
Min time: 0 ms

# 第三次查询
Total time: 423 ms
Avg time: 0.004230 ms
Max time: 12 ms
Min time: 0 ms

可以看出,页缓存对性能的提升很明显。

这部分内存由操作系统自动管理的(比如通过LRU策略进行缓存页淘汰)。当系统内存不足时,如果页缓存是“干净”的,则直接使用;否则,就需要先将“脏页”回写到磁盘后,才能使用,此时会产生磁盘I/O。这里,由于我们只是做记录查询,并不涉及到文件写入,因此索引文件占用的缓存,不会产生后续的磁盘I/O(或者flush操作),对系统影响较小。

获取磁盘页大小

磁盘最小可访问单位称为sector(一般大小为512 bytes),而操作系统则以block为单位来实现块设备(比如磁盘)的I/O操作。对磁盘而言,block大小必须是sector的整数倍。

我们可以通过blockdev命令获取当前磁盘的block大小,比如我使用的这台机器的block大小为:

1
2
$ sudo blockdev --getbsz /dev/sda1
4096 #大小为4KB

当我们以block大小的整数倍进行磁盘数据读取时,往往会达到比较好的I/O性能。

2. 应用缓存

这里不进行任何应用层面的缓存,先看看测试结果,避免过早优化。

四、性能测试

1. 测试环境

  • OS: CentOS Linux release 7.3
  • CPU: 16 cores
  • Memory: 32 GB
  • JVM: Java HotSpot(TM) 64-Bit Server VM 1.8.0_181
  • VM Page Size: 4096 bytes
  • Block Size: 4096 bytes
  • JVM Parameters: -Xms4g -Xmx4g -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=64m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/logs/index.hprof

2. 测试数据

原始数据

按照身份证号码和手机号码组成规则,随机生成记录。其中姓名随机从25000000个姓名中选取

  • 数据总量:1000000000
  • 文本文件大小:39 GB

索引文件

身份证号码可索引文件
统计项 统计值
文件大小 15 GB
一级索引数量 3650
二级索引数量 3650000
记录总数 1000000000
记录最大数量 360
记录最小数量 184
记录平均数量 274
记录数量标准差 16.55
手机号码可索引文件
统计项 统计值
文件大小 14 GB
一级索引数量 10000
二级索引数量 4500000
记录总数 1000000000
记录最大数量 305
记录最小数量 152
记录平均数量 222
记录数量标准差 14.90
姓名索引文件
统计项 统计值
文件大小 241 MB
索引个数 25000000
记录总数 25000000

3. 测试用例

进行下面每项测试之前,均会清空系统缓存。

数据重复查询

使用多份相同数据(每份100000条)查询,其中查询数据全部均在索引中。这种情况下,会有效利用系统缓存,后续查询性能较好(不会产生读磁盘的I/O),系统缓存占比稳定。

不同数据查询

使用多份不同数据(每份100000条)查询,其中查询数据全部均在索引中。这种情况下,系统缓存作用有限(后续查询每次都会产生读磁盘的I/O),每次查询性能基本一致,系统缓存占比上升。

缺失数据查询

使用多份不同数据(每份100000条)查询,其中查询数据中存在部分(50%)不在索引中。这种情况下,有些查询不会产生I/O,有些则会需要遍历数据产生更多的I/O。

混合查询

使用多份随机抽取数据(每份100000条)进行查询,这些数据可能重复或者不在索引中。这种情况下,性能表现更接近实际使用情况。

4. 测试结果

以身份证号码查询为例。

运行时间和缓存使用情况

数据重复查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一次查询
Total time: 17465 ms
Avg time: 0.174650 ms
Max time: 3 ms
Min time: 0 ms

# 第二次查询
Total time: 1118 ms
Avg time: 0.011180 ms
Max time: 5 ms
Min time: 0 ms

#第三次查询
Total time: 1060 ms
Avg time: 0.010600 ms
Max time: 10 ms
Min time: 0 ms

可以看出系统缓存对性能具有很大的提升。此时,系统缓存情况:

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1586556 kB
SwapCached: 0 kB
SReclaimable: 99764 kB
不同数据查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 第一次查询
Total time: 17610 ms
Avg time: 0.176100 ms
Max time: 3 ms
Min time: 0 ms

# 第二次查询
# 发生两次minor gc
[GC (Allocation Failure) [PSYoungGen: 1048576K->960K(640512K)] 2599747K->1552139K(3437056K), 0.0941071 secs] [Times: user=0.06 sys=0.54, real=0.09 secs]
[GC (Allocation Failure) [PSYoungGen: 466880K->928K(931840K)] 2018059K->1552115K(3728384K), 0.0623918 secs] [Times: user=0.08 sys=0.26, real=0.06 secs]

Total time: 18221 ms
Avg time: 0.182210 ms
Max time: 63 ms
Min time: 0 ms

# 第三次查询
# 发生两次minor gc
[GC (Allocation Failure) [PSYoungGen: 466848K->928K(931840K)] 2018035K->1552123K(3728384K), 0.0223861 secs] [Times: user=0.07 sys=0.43, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 466848K->832K(931840K)] 2018043K->1552027K(3728384K), 0.0320038 secs] [Times: user=0.07 sys=0.53, real=0.03 secs]

Total time: 17604 ms
Avg time: 0.176040 ms
Max time: 33 ms
Min time: 0 ms

可以看出不同数据查询时,系统缓存的作用就比较有限了,同时JVM的gc对查询响应造成了一定影响。此时,系统缓存情况(明显升高):

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 4704748 kB
SwapCached: 0 kB
SReclaimable: 105124 kB
缺失数据查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 第一次查询
Total time: 18243 ms
Avg time: 0.182430 ms
Max time: 4 ms
Min time: 0 m

# 第二次查询
# 发生两次minor gc
[GC (Allocation Failure) [PSYoungGen: 1048576K->928K(640512K)] 2599747K->1552107K(3437056K), 0.0126270 secs] [Times: user=0.07 sys=0.17, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 466848K->896K(931840K)] 2018027K->1552083K(3728384K), 0.0321966 secs] [Times: user=0.09 sys=0.43, real=0.04 secs]

Total time: 18000 ms
Avg time: 0.180000 ms
Max time: 33 ms
Min time: 0 ms

# 第三次查询
# 发生两次minor gc
[GC (Allocation Failure) [PSYoungGen: 466816K->608K(931840K)] 2018003K->1551803K(3728384K), 0.0102723 secs] [Times: user=0.22 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 466528K->896K(931840K)] 2017723K->1552091K(3728384K), 0.0699209 secs] [Times: user=0.05 sys=0.47, real=0.07 secs]

Total time: 16955 ms
Avg time: 0.169550 ms
Max time: 71 ms
Min time: 0 ms

可以看出存在缺失时,系统性能并未得到提高,因为部分数据需要遍历完二级索引下所有内容,才能够确认是否存在。此时,系统缓存情况(由于存在需要遍历的情况,导致缓存略微升高):

1
2
3
4
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 4804712 kB
SwapCached: 0 kB
SReclaimable: 105060 kB

I/O使用情况

这里使用pidstat工具查看。

数据重复查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 第一次查询
04:59:50 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
04:59:51 PM 130285 0.00 7.92 0.00 java
04:59:52 PM 130285 8048.00 16.00 0.00 java
04:59:53 PM 130285 43072.00 0.00 0.00 java
04:59:54 PM 130285 49452.00 0.00 0.00 java
04:59:55 PM 130285 50292.00 0.00 0.00 java
04:59:56 PM 130285 51128.00 0.00 0.00 java
04:59:57 PM 130285 52108.00 0.00 0.00 java
04:59:58 PM 130285 53636.00 0.00 0.00 java
04:59:59 PM 130285 53728.00 0.00 0.00 java
05:00:00 PM 130285 53692.00 0.00 0.00 java
05:00:01 PM 130285 55040.00 0.00 0.00 java
05:00:02 PM 130285 57048.00 0.00 0.00 java
05:00:03 PM 130285 58036.00 0.00 0.00 java
05:00:04 PM 130285 60212.00 0.00 0.00 java
05:00:05 PM 130285 63564.00 0.00 0.00 java
05:00:06 PM 130285 56996.00 0.00 0.00 java
05:00:07 PM 130285 61492.00 0.00 0.00 java
05:00:08 PM 130285 63192.00 0.00 0.00 java
05:00:09 PM 130285 62884.00 0.00 0.00 java
05:00:10 PM 130285 67424.00 0.00 0.00 java
05:00:11 PM 130285 64876.00 0.00 0.00 java
05:00:12 PM 130285 10348.00 0.00 0.00 java
05:00:13 PM 130285 0.00 0.00 0.00 java
05:00:14 PM 130285 0.00 0.00 0.00 java
05:00:15 PM 130285 0.00 0.00 0.00 java
Average: 130285 43833.19 0.96 0.00 java

# 第二次查询
05:01:00 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:01:01 PM 130285 0.00 0.00 0.00 java
05:01:02 PM 130285 0.00 12.00 0.00 java
05:01:03 PM 130285 0.00 4.00 0.00 java
05:01:04 PM 130285 0.00 0.00 0.00 java
05:01:05 PM 130285 0.00 0.00 0.00 java
05:01:06 PM 130285 0.00 0.00 0.00 java
05:01:07 PM 130285 0.00 0.00 0.00 java
05:01:08 PM 130285 0.00 0.00 0.00 java
05:01:09 PM 130285 0.00 0.00 0.00 java
05:01:10 PM 130285 0.00 0.00 0.00 java
05:01:11 PM 130285 0.00 0.00 0.00 java
05:01:12 PM 130285 0.00 0.00 0.00 java
05:01:13 PM 130285 0.00 0.00 0.00 java
05:01:14 PM 130285 0.00 0.00 0.00 java
05:01:15 PM 130285 0.00 0.00 0.00 java
05:01:16 PM 130285 0.00 0.00 0.00 java
05:01:17 PM 130285 0.00 0.00 0.00 java
05:01:18 PM 130285 0.00 0.00 0.00 java
05:01:19 PM 130285 0.00 0.00 0.00 java
05:01:20 PM 130285 0.00 0.00 0.00 java
05:01:21 PM 130285 0.00 8.00 0.00 java
05:01:22 PM 130285 0.00 0.00 0.00 java
05:01:23 PM 130285 0.00 0.00 0.00 java
05:01:24 PM 130285 0.00 0.00 0.00 java
05:01:25 PM 130285 0.00 0.00 0.00 java
Average: 130285 0.00 0.96 0.00 java

# 第三次查询
05:03:20 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:03:21 PM 130285 0.00 8.00 0.00 java
05:03:22 PM 130285 0.00 4.00 0.00 java
05:03:23 PM 130285 0.00 12.00 0.00 java
05:03:24 PM 130285 0.00 0.00 0.00 java
05:03:25 PM 130285 0.00 0.00 0.00 java
05:03:26 PM 130285 0.00 0.00 0.00 java
05:03:27 PM 130285 0.00 0.00 0.00 java
05:03:28 PM 130285 0.00 0.00 0.00 java
05:03:29 PM 130285 0.00 0.00 0.00 java
05:03:30 PM 130285 0.00 0.00 0.00 java
05:03:31 PM 130285 0.00 0.00 0.00 java
05:03:32 PM 130285 0.00 0.00 0.00 java
05:03:33 PM 130285 0.00 0.00 0.00 java
05:03:34 PM 130285 0.00 0.00 0.00 java
05:03:35 PM 130285 0.00 0.00 0.00 java
05:03:36 PM 130285 0.00 0.00 0.00 java
05:03:37 PM 130285 0.00 0.00 0.00 java
05:03:38 PM 130285 0.00 0.00 0.00 java
05:03:39 PM 130285 0.00 0.00 0.00 java
05:03:40 PM 130285 0.00 0.00 0.00 java
05:03:41 PM 130285 0.00 0.00 0.00 java
05:03:42 PM 130285 0.00 0.00 0.00 java
05:03:43 PM 130285 0.00 0.00 0.00 java
05:03:44 PM 130285 0.00 0.00 0.00 java
05:03:45 PM 130285 0.00 0.00 0.00 java
Average: 130285 0.00 0.96 0.00 java

可以看出系统缓存对I/O的影响。

不同数据查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 第一次查询
05:07:03 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:07:04 PM 130480 0.00 0.00 0.00 java
05:07:05 PM 130480 6560.00 0.00 0.00 java
05:07:06 PM 130480 44812.00 0.00 0.00 java
05:07:07 PM 130480 50796.00 0.00 0.00 java
05:07:08 PM 130480 50912.00 0.00 0.00 java
05:07:09 PM 130480 52296.00 0.00 0.00 java
05:07:10 PM 130480 52388.00 0.00 0.00 java
05:07:11 PM 130480 54704.00 0.00 0.00 java
05:07:12 PM 130480 54504.00 0.00 0.00 java
05:07:13 PM 130480 54952.00 0.00 0.00 java
05:07:14 PM 130480 56172.00 0.00 0.00 java
05:07:15 PM 130480 58564.00 0.00 0.00 java
05:07:16 PM 130480 61392.00 8.00 0.00 java
05:07:17 PM 130480 61380.00 8.00 0.00 java
05:07:18 PM 130480 62504.00 0.00 0.00 java
05:07:19 PM 130480 61820.00 0.00 0.00 java
05:07:20 PM 130480 64320.00 0.00 0.00 java
05:07:21 PM 130480 62772.00 0.00 0.00 java
05:07:22 PM 130480 64944.00 0.00 0.00 java
05:07:23 PM 130480 66764.00 0.00 0.00 java
05:07:24 PM 130480 53240.00 4.00 0.00 java
05:07:25 PM 130480 0.00 0.00 0.00 java
05:07:26 PM 130480 0.00 0.00 0.00 java
05:07:27 PM 130480 0.00 0.00 0.00 java
05:07:28 PM 130480 0.00 0.00 0.00 java
Average: 130480 43831.84 0.80 0.00 java

# 第二次查询
05:07:55 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:07:56 PM 130480 0.00 0.00 0.00 java
05:07:57 PM 130480 24824.00 16.00 0.00 java
05:07:58 PM 130480 64304.00 0.00 0.00 java
05:07:59 PM 130480 68604.00 0.00 0.00 java
05:08:00 PM 130480 72124.00 0.00 0.00 java
05:08:01 PM 130480 70976.00 0.00 0.00 java
05:08:02 PM 130480 72056.00 0.00 0.00 java
05:08:03 PM 130480 69000.00 0.00 0.00 java
05:08:04 PM 130480 70408.00 0.00 0.00 java
05:08:05 PM 130480 73116.00 0.00 0.00 java
05:08:06 PM 130480 72160.00 0.00 0.00 java
05:08:07 PM 130480 74628.00 0.00 0.00 java
05:08:08 PM 130480 75816.00 0.00 0.00 java
05:08:09 PM 130480 75796.00 0.00 0.00 java
05:08:10 PM 130480 75312.00 0.00 0.00 java
05:08:11 PM 130480 78968.00 0.00 0.00 java
05:08:12 PM 130480 80120.00 0.00 0.00 java
05:08:13 PM 130480 79496.00 0.00 0.00 java
05:08:14 PM 130480 77220.00 0.00 0.00 java
05:08:15 PM 130480 81280.00 0.00 0.00 java
05:08:16 PM 130480 83493.07 0.00 0.00 java
05:08:17 PM 130480 23280.00 16.00 0.00 java
05:08:18 PM 130480 0.00 0.00 0.00 java
05:08:19 PM 130480 0.00 0.00 0.00 java
05:08:20 PM 130480 0.00 0.00 0.00 java
Average: 130480 58529.23 1.28 0.00 java

# 第三次查询
05:08:39 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:08:40 PM 130480 0.00 0.00 0.00 java
05:08:41 PM 130480 1592.00 0.00 0.00 java
05:08:42 PM 130480 84484.00 0.00 0.00 java
05:08:43 PM 130480 82408.00 0.00 0.00 java
05:08:44 PM 130480 81532.00 0.00 0.00 java
05:08:45 PM 130480 81908.00 0.00 0.00 java
05:08:46 PM 130480 86284.00 8.00 0.00 java
05:08:47 PM 130480 82124.00 8.00 0.00 java
05:08:48 PM 130480 84728.00 0.00 0.00 java
05:08:49 PM 130480 85772.00 8.00 0.00 java
05:08:50 PM 130480 86520.00 0.00 0.00 java
05:08:51 PM 130480 88544.00 0.00 0.00 java
05:08:52 PM 130480 89620.00 0.00 0.00 java
05:08:53 PM 130480 88520.00 0.00 0.00 java
05:08:54 PM 130480 89848.00 0.00 0.00 java
05:08:55 PM 130480 90216.00 0.00 0.00 java
05:08:56 PM 130480 91260.00 0.00 0.00 java
05:08:57 PM 130480 91752.00 0.00 0.00 java
05:08:58 PM 130480 90956.00 0.00 0.00 java
05:08:59 PM 130480 90600.00 0.00 0.00 java
05:09:00 PM 130480 86072.00 0.00 0.00 java
05:09:01 PM 130480 0.00 0.00 0.00 java
05:09:02 PM 130480 0.00 0.00 0.00 java
05:09:03 PM 130480 0.00 0.00 0.00 java
05:09:04 PM 130480 0.00 0.00 0.00 java
Average: 130480 66189.60 0.96 0.00 java

可以看到整个查询过程中,产生了大量的读磁盘操作,此时系统缓存作用已失效。

缺失数据查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 第一次查询时
05:13:08 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:13:09 PM 130623 0.00 0.00 0.00 java
05:13:10 PM 130623 14756.00 16.00 0.00 java
05:13:11 PM 130623 51836.00 4.00 0.00 java
05:13:12 PM 130623 60256.00 0.00 0.00 java
05:13:13 PM 130623 56628.00 0.00 0.00 java
05:13:14 PM 130623 58560.00 0.00 0.00 java
05:13:15 PM 130623 59576.00 0.00 0.00 java
05:13:16 PM 130623 58652.00 0.00 0.00 java
05:13:17 PM 130623 60588.00 0.00 0.00 java
05:13:18 PM 130623 62436.00 0.00 0.00 java
05:13:19 PM 130623 60796.00 0.00 0.00 java
05:13:20 PM 130623 63520.00 0.00 0.00 java
05:13:21 PM 130623 62196.00 0.00 0.00 java
05:13:22 PM 130623 64756.00 0.00 0.00 java
05:13:23 PM 130623 66768.00 0.00 0.00 java
05:13:24 PM 130623 69020.00 0.00 0.00 java
05:13:25 PM 130623 68440.00 0.00 0.00 java
05:13:26 PM 130623 67936.00 0.00 0.00 java
05:13:27 PM 130623 68668.00 8.00 0.00 java
05:13:28 PM 130623 68524.00 0.00 0.00 java
05:13:29 PM 130623 70744.00 0.00 0.00 java
05:13:30 PM 130623 11944.00 12.00 0.00 java
05:13:31 PM 130623 0.00 0.00 0.00 java
05:13:32 PM 130623 0.00 0.00 0.00 java
05:13:33 PM 130623 0.00 0.00 0.00 java
Average: 130623 49064.00 1.60 0.00 java

# 第二次查询时
05:14:06 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:14:07 PM 130623 0.00 0.00 0.00 java
05:14:08 PM 130623 14780.00 12.00 0.00 java
05:14:09 PM 130623 73016.00 4.00 0.00 java
05:14:10 PM 130623 70944.00 0.00 0.00 java
05:14:11 PM 130623 74692.00 0.00 0.00 java
05:14:12 PM 130623 73040.00 0.00 0.00 java
05:14:13 PM 130623 71388.00 0.00 0.00 java
05:14:14 PM 130623 74496.00 0.00 0.00 java
05:14:15 PM 130623 75664.00 0.00 0.00 java
05:14:16 PM 130623 74124.00 0.00 0.00 java
05:14:17 PM 130623 78780.00 0.00 0.00 java
05:14:18 PM 130623 80544.00 0.00 0.00 java
05:14:19 PM 130623 80900.00 0.00 0.00 java
05:14:20 PM 130623 79080.00 0.00 0.00 java
05:14:21 PM 130623 81560.00 0.00 0.00 java
05:14:22 PM 130623 80052.00 0.00 0.00 java
05:14:23 PM 130623 82700.00 0.00 0.00 java
05:14:24 PM 130623 81516.00 0.00 0.00 java
05:14:25 PM 130623 79516.00 0.00 0.00 java
05:14:26 PM 130623 80132.00 0.00 0.00 java
05:14:27 PM 130623 76940.00 12.00 0.00 java
05:14:28 PM 130623 0.00 4.00 0.00 java
05:14:29 PM 130623 0.00 0.00 0.00 java
05:14:30 PM 130623 0.00 0.00 0.00 java
05:14:31 PM 130623 0.00 0.00 0.00 java
Average: 130623 59330.83 1.28 0.00 java

# 第三次查询时
05:15:24 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
05:15:25 PM 130623 0.00 0.00 0.00 java
05:15:26 PM 130623 11612.00 0.00 0.00 java
05:15:27 PM 130623 83676.00 12.00 0.00 java
05:15:28 PM 130623 85748.00 0.00 0.00 java
05:15:29 PM 130623 86284.00 0.00 0.00 java
05:15:30 PM 130623 85180.00 0.00 0.00 java
05:15:31 PM 130623 87876.00 0.00 0.00 java
05:15:32 PM 130623 88352.00 0.00 0.00 java
05:15:33 PM 130623 84836.00 16.00 0.00 java
05:15:34 PM 130623 86608.00 0.00 0.00 java
05:15:35 PM 130623 88252.00 0.00 0.00 java
05:15:36 PM 130623 85608.00 0.00 0.00 java
05:15:37 PM 130623 89340.00 0.00 0.00 java
05:15:38 PM 130623 90892.00 0.00 0.00 java
05:15:39 PM 130623 89996.00 0.00 0.00 java
05:15:40 PM 130623 88764.00 0.00 0.00 java
05:15:41 PM 130623 95248.00 0.00 0.00 java
05:15:42 PM 130623 92192.00 0.00 0.00 java
05:15:43 PM 130623 93600.00 0.00 0.00 java
05:15:44 PM 130623 84732.00 0.00 0.00 java
05:15:45 PM 130623 0.00 0.00 0.00 java
05:15:46 PM 130623 0.00 0.00 0.00 java
05:15:47 PM 130623 0.00 0.00 0.00 java
05:15:48 PM 130623 0.00 0.00 0.00 java
05:15:49 PM 130623 0.00 0.00 0.00 java
Average: 130623 63951.84 1.12 0.00 java

可以看出相比非缺失数据查询,其产生的磁盘I/O,并未有出现明显的差异。说明缺失的情况下,并不一定减少磁盘的I/O。

CPU使用情况

以不同数据查询为例, 这里使用pidstat命令查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
05:19:54 PM       PID    %usr %system  %guest    %CPU   CPU  Command
05:19:55 PM 130864 0.00 0.00 0.00 0.00 33 java
05:19:56 PM 130864 52.00 10.00 0.00 62.00 33 java
05:19:57 PM 130864 71.00 12.00 0.00 83.00 33 java
05:19:58 PM 130864 41.00 9.00 0.00 50.00 33 java
05:19:59 PM 130864 16.00 9.00 0.00 25.00 33 java
05:20:00 PM 130864 13.00 9.00 0.00 22.00 33 java
05:20:01 PM 130864 12.00 9.00 0.00 21.00 33 java
05:20:02 PM 130864 13.00 8.00 0.00 21.00 33 java
05:20:03 PM 130864 12.00 10.00 0.00 22.00 33 java
05:20:04 PM 130864 13.00 8.00 0.00 21.00 33 java
05:20:05 PM 130864 12.00 9.00 0.00 21.00 33 java
05:20:06 PM 130864 12.00 10.00 0.00 22.00 33 java
05:20:07 PM 130864 12.00 9.00 0.00 21.00 33 java
05:20:08 PM 130864 12.00 10.00 0.00 22.00 33 java
05:20:09 PM 130864 11.00 10.00 0.00 21.00 33 java
05:20:10 PM 130864 10.00 11.00 0.00 21.00 33 java
05:20:11 PM 130864 11.00 9.00 0.00 20.00 33 java
05:20:12 PM 130864 9.00 12.00 0.00 21.00 33 java
05:20:13 PM 130864 11.00 9.00 0.00 20.00 33 java
05:20:14 PM 130864 10.00 11.00 0.00 21.00 33 java
05:20:15 PM 130864 11.00 9.00 0.00 20.00 33 java
05:20:16 PM 130864 10.00 8.00 0.00 18.00 33 java
05:20:17 PM 130864 0.00 0.00 0.00 0.00 33 java
05:20:18 PM 130864 0.00 0.00 0.00 0.00 33 java
05:20:19 PM 130864 0.00 0.00 0.00 0.00 33 java
Average: 130864 14.96 8.04 0.00 23.00 - java

整个查询过程中,系统负载水平较为稳定。计算能力并未成为系统的性能瓶颈。

内存使用情况

系统内存

以不同数据查询为例, 这里使用pidstat命令查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
05:22:53 PM       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
05:22:54 PM 130864 0.00 0.00 9227292 3801256 1.44 java
05:22:55 PM 130864 2216.00 0.00 9227292 3808828 1.44 java
05:22:56 PM 130864 2874.00 0.00 9227292 3819616 1.45 java
05:22:57 PM 130864 128.00 0.00 9227292 3819692 1.45 java
05:22:58 PM 130864 42.00 0.00 9227292 3819692 1.45 java
05:22:59 PM 130864 11456.00 0.00 9227292 3820340 1.45 java
05:23:00 PM 130864 50.00 0.00 9227292 3820340 1.45 java
05:23:01 PM 130864 17.00 0.00 9227292 3820340 1.45 java
05:23:02 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:03 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:04 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:05 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:06 PM 130864 13.00 0.00 9227292 3820340 1.45 java
05:23:07 PM 130864 5.00 0.00 9227292 3820340 1.45 java
05:23:08 PM 130864 1.00 0.00 9227292 3820340 1.45 java
05:23:09 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:10 PM 130864 27.00 0.00 9227292 3820340 1.45 java
05:23:11 PM 130864 19.00 0.00 9227292 3820340 1.45 java
05:23:12 PM 130864 21.00 0.00 9227292 3820340 1.45 java
05:23:13 PM 130864 21.00 0.00 9227292 3820340 1.45 java
05:23:14 PM 130864 20.00 0.00 9227292 3820340 1.45 java
05:23:15 PM 130864 9.00 0.00 9227292 3820340 1.45 java
05:23:16 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:17 PM 130864 0.00 0.00 9227292 3820340 1.45 java
05:23:18 PM 130864 2.00 0.00 9227292 3820340 1.45 java
Average: 130864 676.84 0.00 9227292 3819035 1.45 java

可以看出这个过程中的内存使用量较为平稳,没有出现大幅度地波动。

JVM情况

以不同数据查询为例。整个查询(三次)过程中,共产生四次Minor GC。

1
2
3
4
[GC (Allocation Failure) [PSYoungGen: 1048576K->960K(640512K)] 2599747K->1552139K(3437056K), 0.0870845 secs] [Times: user=0.04 sys=0.44, real=0.08 secs] 
[GC (Allocation Failure) [PSYoungGen: 466880K->768K(931840K)] 2018059K->1551955K(3728384K), 0.0046564 secs] [Times: user=0.07 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 466688K->864K(931840K)] 2017875K->1552059K(3728384K), 0.0057137 secs] [Times: user=0.08 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 466784K->640K(931840K)] 2017979K->1551835K(3728384K), 0.0069967 secs] [Times: user=0.08 sys=0.02, real=0.01 secs]

这些Minor GC会对查询的响应造成一定的延迟。

堆使用情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Heap Usage:
PS Young Generation
Eden Space:
capacity = 477102080 (455.0MB)
used = 132110816 (125.99069213867188MB)
free = 344991264 (329.0093078613281MB)
27.690262008499314% used
From Space:
capacity = 477102080 (455.0MB)
used = 655360 (0.625MB)
free = 476446720 (454.375MB)
0.13736263736263737% used
To Space:
capacity = 477102080 (455.0MB)
used = 0 (0.0MB)
free = 477102080 (455.0MB)
0.0% used
PS Old Generation
capacity = 2863661056 (2731.0MB)
used = 1588423816 (1514.838996887207MB)
free = 1275237240 (1216.161003112793MB)
55.46828988968169% used

1390 interned Strings occupying 98760 bytes.

由于整个程序的特点(启动时加载全部的索引和姓名),老生代(大约1.5 GB)是整个程序占用内存最大部分,其中包含了25000000个姓名和全部的一二级索引。这里我们每次在内存中构建完索引后,可以主动调用一下System.gc() ,清空构建过程中产生的大量可回收对象。避免后续查询时频繁地触发GC(特别是Full GC)。

混合查询结果

身份证号码查询

共300000条记录,分三次查询。其中大约四分之一数据重复,一半数据不在索引中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一次查询
Total time: 17822 ms
Avg time: 0.178220 ms
Max time: 3 ms
Min time: 0 ms

# 第二次查询
Total time: 12577 ms
Avg time: 0.125770 ms
Max time: 19 ms
Min time: 0 ms

# 第三次查询
Total time: 8846 ms
Avg time: 0.088460 ms
Max time: 5 ms
Min time: 0 ms

可以看到这种情况下系统缓存对性能还是具有一定的提升作用。第二次查询,由于发生两次Minor GC导致最大响应时间增高。

手机号码查询

共300000条记录,分三次查询。其中大约六分之一数据重复,一半数据不在索引中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一次查询
Total time: 15896 ms
Avg time: 0.158960 ms
Max time: 14 ms
Min time: 0 ms

# 第二次查询
Total time: 13391 ms
Avg time: 0.133910 ms
Max time: 24 ms
Min time: 0 ms

# 第三次查询
Total time: 11262 ms
Avg time: 0.112620 ms
Max time: 29 ms
Min time: 0 ms

可以看到这种情况下系统缓存对性能还是具有一定的提升作用。第二三次查询,由于各发生两次Minor GC导致最大响应时间增高。

五、改进

1. 性能问题在哪里

从测试结果来看,目前的性能已接近万级qps。但我们也发现,在缺失数据查询场景中,无论是执行时间还是磁盘I/O都没有明显变化,那是否是由于部分缺失数据需要遍历完二级索引下所有内容引起的呢?同时,也可以发现无论是否存在,每次都需要遍历二级索引进行线性搜索,能否避免呢或者降低搜索时间复杂度?

2. 问题验证

缺失数据遍历数据问题

将缺失数据查询所用的数据,拆分为两组。一组完全在索引中(100000条),一组完全不在索引中(100000条)。进行两次独立测试,每次测试前情况系统缓存。这里以身份证号码查询为例:

完全在索引中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 17682 ms
Avg time: 0.176820 ms
Max time: 3 ms
Min time: 0 ms

# 磁盘I/O
10:25:01 AM PID kB_rd/s kB_wr/s kB_ccwr/s Command
10:25:02 AM 3003 0.00 0.00 0.00 java
10:25:03 AM 3003 22144.00 16.00 0.00 java
10:25:04 AM 3003 50736.00 0.00 0.00 java
10:25:05 AM 3003 52044.00 0.00 0.00 java
10:25:06 AM 3003 54080.00 0.00 0.00 java
10:25:07 AM 3003 54608.00 0.00 0.00 java
10:25:08 AM 3003 57152.00 0.00 0.00 java
10:25:09 AM 3003 57556.00 0.00 0.00 java
10:25:10 AM 3003 58092.00 8.00 0.00 java
10:25:11 AM 3003 59168.00 0.00 0.00 java
10:25:12 AM 3003 61504.00 0.00 0.00 java
10:25:13 AM 3003 63504.00 8.00 0.00 java
10:25:14 AM 3003 64176.00 0.00 0.00 java
10:25:15 AM 3003 65352.00 0.00 0.00 java
10:25:16 AM 3003 64480.00 0.00 0.00 java
10:25:17 AM 3003 66592.00 0.00 0.00 java
10:25:18 AM 3003 66276.00 0.00 0.00 java
10:25:19 AM 3003 67908.00 0.00 0.00 java
10:25:20 AM 3003 68532.00 0.00 0.00 java
10:25:21 AM 3003 40052.00 4.00 0.00 java
10:25:22 AM 3003 0.00 0.00 0.00 java
10:25:23 AM 3003 0.00 0.00 0.00 java
10:25:24 AM 3003 0.00 0.00 0.00 java
10:25:25 AM 3003 0.00 0.00 0.00 java
10:25:26 AM 3003 0.00 0.00 0.00 java
Average: 3003 43758.24 1.44 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1616060 kB
SwapCached: 0 kB
SReclaimable: 102020 kB
完全不在索引中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 19666 ms
Avg time: 0.196660 ms
Max time: 3 ms
Min time: 0 ms

# 磁盘I/O
10:56:34 AM PID kB_rd/s kB_wr/s kB_ccwr/s Command
10:56:35 AM 3808 0.00 0.00 0.00 java
10:56:36 AM 3808 15660.00 20.00 0.00 java
10:56:37 AM 3808 58608.00 0.00 0.00 java
10:56:38 AM 3808 64164.00 0.00 0.00 java
10:56:39 AM 3808 62640.00 0.00 0.00 java
10:56:40 AM 3808 65272.00 0.00 0.00 java
10:56:41 AM 3808 64512.00 0.00 0.00 java
10:56:42 AM 3808 66060.00 0.00 0.00 java
10:56:43 AM 3808 62556.00 0.00 0.00 java
10:56:44 AM 3808 65788.00 0.00 0.00 java
10:56:45 AM 3808 63308.00 0.00 0.00 java
10:56:46 AM 3808 61720.00 0.00 0.00 java
10:56:47 AM 3808 67920.00 0.00 0.00 java
10:56:48 AM 3808 67028.00 0.00 0.00 java
10:56:49 AM 3808 69164.00 0.00 0.00 java
10:56:50 AM 3808 70332.00 0.00 0.00 java
10:56:51 AM 3808 70528.00 0.00 0.00 java
10:56:52 AM 3808 70580.00 0.00 0.00 java
10:56:53 AM 3808 70896.00 0.00 0.00 java
10:56:54 AM 3808 71904.00 0.00 0.00 java
10:56:55 AM 3808 74636.00 0.00 0.00 java
10:56:56 AM 3808 54936.00 0.00 0.00 java
10:56:57 AM 3808 0.00 0.00 0.00 java
10:56:58 AM 3808 0.00 0.00 0.00 java
10:56:59 AM 3808 0.00 0.00 0.00 java
Average: 3808 53507.08 0.80 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1861960 kB
SwapCached: 0 kB
SReclaimable: 99268 kB
验证结论

可以看到缺失数据确实造成了更多磁盘I/O和更长的运行时间。其实,从身份证号码索引选择上也能够说明这点,出生月日和顺序码虽然能够得到较好的散列,但对身份证号码的唯一性识别能力较弱。

内容遍历问题

构造两份数据(每份100000条),一份数据分布在二级索引下内容中处于靠前部分,另一份数据分布在二级索引下内容中处于靠后部分。这样靠前部分数据需要的遍历时间更少,而靠后部分数据则需要更多时间。

靠前部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 16815 ms
Avg time: 0.168150 ms
Max time: 3 ms
Min time: 0 ms

# 磁盘I/O
04:03:22 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
04:03:23 PM 6833 0.00 0.00 0.00 java
04:03:24 PM 6833 21072.00 16.00 0.00 java
04:03:25 PM 6833 52096.00 24.00 0.00 java
04:03:26 PM 6833 55812.00 0.00 0.00 java
04:03:27 PM 6833 57224.00 0.00 0.00 java
04:03:28 PM 6833 57376.00 0.00 0.00 java
04:03:29 PM 6833 59780.00 0.00 0.00 java
04:03:30 PM 6833 60488.00 0.00 0.00 java
04:03:31 PM 6833 61204.00 0.00 0.00 java
04:03:32 PM 6833 60820.00 0.00 0.00 java
04:03:33 PM 6833 62728.00 0.00 0.00 java
04:03:34 PM 6833 66012.00 0.00 0.00 java
04:03:35 PM 6833 67892.00 0.00 0.00 java
04:03:36 PM 6833 67268.00 0.00 0.00 java
04:03:37 PM 6833 69724.00 0.00 0.00 java
04:03:38 PM 6833 67656.00 0.00 0.00 java
04:03:39 PM 6833 69760.00 0.00 0.00 java
04:03:40 PM 6833 71436.00 0.00 0.00 java
04:03:41 PM 6833 53516.00 0.00 0.00 java
04:03:42 PM 6833 0.00 0.00 0.00 java
04:03:43 PM 6833 0.00 0.00 0.00 java
04:03:44 PM 6833 0.00 0.00 0.00 java
04:03:45 PM 6833 0.00 0.00 0.00 java
04:03:46 PM 6833 0.00 0.00 0.00 java
04:03:47 PM 6833 0.00 0.00 0.00 java
Average: 6833 43274.56 1.60 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1602380 kB
SwapCached: 0 kB
SReclaimable: 102388 kB
靠后部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 19347 ms
Avg time: 0.193470 ms
Max time: 3 ms
Min time: 0 ms

# 磁盘I/O
04:06:04 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
04:06:05 PM 6983 0.00 0.00 0.00 java
04:06:06 PM 6983 628.00 4.00 0.00 java
04:06:07 PM 6983 40944.00 0.00 0.00 java
04:06:08 PM 6983 48680.00 0.00 0.00 java
04:06:09 PM 6983 48752.00 0.00 0.00 java
04:06:10 PM 6983 49624.00 0.00 0.00 java
04:06:11 PM 6983 50756.00 0.00 0.00 java
04:06:12 PM 6983 51796.00 0.00 0.00 java
04:06:13 PM 6983 52288.00 0.00 0.00 java
04:06:14 PM 6983 53596.00 0.00 0.00 java
04:06:15 PM 6983 54880.00 0.00 0.00 java
04:06:16 PM 6983 56328.00 0.00 0.00 java
04:06:17 PM 6983 54424.00 0.00 0.00 java
04:06:18 PM 6983 55652.00 0.00 0.00 java
04:06:19 PM 6983 58164.00 0.00 0.00 java
04:06:20 PM 6983 60672.00 8.00 0.00 java
04:06:21 PM 6983 60712.00 0.00 0.00 java
04:06:22 PM 6983 58704.00 0.00 0.00 java
04:06:23 PM 6983 61080.00 0.00 0.00 java
04:06:24 PM 6983 61832.00 0.00 0.00 java
04:06:25 PM 6983 63476.00 0.00 0.00 java
04:06:26 PM 6983 49176.00 12.00 0.00 java
04:06:27 PM 6983 0.00 0.00 0.00 java
04:06:28 PM 6983 0.00 0.00 0.00 java
04:06:29 PM 6983 0.00 0.00 0.00 java
Average: 6983 43686.56 0.96 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1615152 kB
SwapCached: 0 kB
SReclaimable: 101716 kB
验证结论

记录位置的不同确实会造成运行时间变长(增加了2.5s),但由于每次磁盘I/O的单位是4KB,而一个二级索引下全部内容大小平均不超过4KB,因此在磁盘I/O上两者差别不大。

3. 改进方案

Bloom Filter

主要解决数据不在索引中引起的数据遍历问题,降低磁盘I/O和搜索时间。

设计

构造一个BloomFilter内容文件。内存中构建索引时,同时根据文件中的内容反序列化出BloomFilter。在读取磁盘上的数据之前增加BloomFilter过滤步骤。即每次查询通过索引获取访问位置后,首先通过BloomFilter验证记录是否可能存在。通过验证后,再通过访问位置读取磁盘数据,进行后续搜索。此处BloomFilter使用Guava实现。

空间代价评估

这里构造BloomFilter时设置放入全部十亿条身份证号码或者手机号码,并且设置期望的false positive(即不存在但被误判为存在)概率不高于1%。

此时整个BloomFilter序列化后,文件大小为1.2 GB。加载入内存需要消耗时间,且内存使用量增高。

时间代价评估

设查询时身份证号码或者手机号码不在索引中的概率为$P_{miss}$ ,每次通过BloomFilter过滤执行时间为$T_f$,而过滤误判的概率为$P_{error}$ 。查询记录不在索引中时,读取磁盘进行搜索的平均时间为$T_{s}$ms。则在系统在使用BloomFilter前后的代价分别为:
$$
\begin{cases} Lost(before) = P_{miss} \times T_s \\ Lost(after) = T_f + P_{miss} \times P_{error} \times T_s \end{cases}
$$
即当以下不等式成立时,BloomFilter的使用能够提高系统性能。
$$
Lost(before) - Lost(after) = P_{miss} \times T_s \times (1 - P_{error}) > T_f
$$
考虑十亿条记录中每个身份证号码或者手机号码都是唯一的极端情况下,且业务查询每个身份证号码或者手机号码是随机的。那么:

  • 身份证号码的$P_{miss} \ge 0.29$
  • 手机号码的$P_{miss} \ge 0.78$

这里我们设置误判概率$P_{error} \le 0.01$,则
$$
T_f < 0.29 \times T_s \ge 0.06 \quad ms
$$
此时,BloomFilter的使用能够提高系统性能。

这里使用100000条(身份证号码)混合查询数据(其中50067条数据存在于索引中)进行测试,结果如下:

1
2
3
4
5
6
7
passed = 50549
denied = 49451

Total time: 155.921255 ms
Avg time: 0.001559 ms
Max time: 16.535327 ms
Min time: 0.000393 ms

其中有482条误判。执行时间满足要求,即使用BloomFilter可以提高系统性能。

内容前缀缓存

设计

缓存每个二级索引中开始(最小)、中间和结束(最大)的内容前缀。查询时,通过比较待查询的内容前缀是否小于开始前缀或者大于结束内容前缀,来快速判断待查询的内容前缀是否存在。如果待查询的内容前缀存在,则比较其与中间前缀的大小。如果大于中间前缀,说明位于内容后半段,则直接读取后半段内容进行搜索;否则读取前半段内容进行搜索。这样减少了内容前缀位于后半段时的搜索时间(平均降低50%)。

缓存采用热启动的方式,即在读取一二级索引时,生成缓存。为了降低启动加载时间,构建索引文件时,在二级索引中增加以下相关内容:

  • MinKey表示该二级索引下最小内容前缀(即第一条)
  • MidKey表示该二级索引下中间内容前缀
  • MaxKey表示该二级索引下最大内容前缀(可能是最后一条,如果最大前缀只有一条的话)
  • MidDelta表示中间内容前缀和第一条之间相差几条记录,可用于生成中间内容前缀所在位置
  • MaxDelta表示最大内容前缀和第一条之间相差几条记录,可用于生成最大内容前缀所在位置
空间代价评估
索引类型 空间代价
身份证号码索引 二级索引个数 * (内容前缀长度 * 3 + 4) = 56 MB
手机号码索引 二级索引个数 * (内容前缀长度 * 3 + 4) = 43 MB
时间代价评估

设查询时身份证号码或者手机号码不在索引中的概率为$P_{miss}$ ,每次通过缓存判断时间为$T_j$,而通过缓存判断依然不再记录中的概率为$P_{error}$ 。查询记录不在索引中时,读取磁盘进行搜索的平均时间为$T_{s}$ms。查询记录在索引中时,未使用缓存时,读取磁盘进行搜索的平均时间为$T_m$ ms。而使用缓存判断后,读取磁盘进行搜索的平均时间为$\alpha T_m$ ms。则在系统在使用缓存前后的代价分别为:
$$
\begin{cases} Lost(before) = P_{miss} \times T_s + (1-P_{miss}) \times T_m \\ Lost(after) = T_j + (1 - P_{miss}) \times \alpha T_m + P_{error} \times T_s\end{cases}
$$
即当以下不等式成立时,缓存的使用能够提高系统性能。
$$
Lost(before) - Lost(after) = T_m (1 - P_{miss}) (1 - \alpha) > T_j - T_s(P_{error} - P_{miss})
$$
并且存在以下约束条件 :
$$
\begin{cases} T_j << T_s \\ 0 \le P_{miss} \le 1 \\ 0 \lt \alpha \le 1 \end{cases}
$$
因此,性能一定能够得到提升,但提升多少,还是主要看使用缓存后,$P_{error}$和读取磁盘进行搜索的平均时间情况。

4. 改进效果

这里以身份证号码查询为例

使用Bloom Filter后的测试结果

使用Bloom Filter后,程序使用内存量相比之前多出了1GB左右。典型的时间换空间。

完全在索引中数据查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 17908.957785 ms
Avg time: 0.179090 ms
Max time: 15.298127 ms
Min time: 0.009509 ms

# 磁盘I/O
02:16:29 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
02:16:30 PM 36203 0.00 0.00 0.00 java
02:16:31 PM 36203 0.00 0.00 0.00 java
02:16:32 PM 36203 23732.00 16.00 0.00 java
02:16:33 PM 36203 49884.00 0.00 0.00 java
02:16:34 PM 36203 51668.00 0.00 0.00 java
02:16:35 PM 36203 53612.00 0.00 0.00 java
02:16:36 PM 36203 53720.00 0.00 0.00 java
02:16:37 PM 36203 56368.00 0.00 0.00 java
02:16:38 PM 36203 56192.00 0.00 0.00 java
02:16:39 PM 36203 57924.00 0.00 0.00 java
02:16:40 PM 36203 58616.00 0.00 0.00 java
02:16:41 PM 36203 60624.00 0.00 0.00 java
02:16:42 PM 36203 62588.00 0.00 0.00 java
02:16:43 PM 36203 63588.00 0.00 0.00 java
02:16:44 PM 36203 64836.00 0.00 0.00 java
02:16:45 PM 36203 63692.00 0.00 0.00 java
02:16:46 PM 36203 66376.00 0.00 0.00 java
02:16:47 PM 36203 65316.00 0.00 0.00 java
02:16:48 PM 36203 67832.00 0.00 0.00 java
02:16:49 PM 36203 67904.00 0.00 0.00 java
02:16:50 PM 36203 51464.00 8.00 0.00 java
02:16:51 PM 36203 0.00 4.00 0.00 java
02:16:52 PM 36203 0.00 0.00 0.00 java
02:16:53 PM 36203 0.00 0.00 0.00 java
02:16:54 PM 36203 0.00 0.00 0.00 java
Average: 36203 43837.44 1.12 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 2789188 kB
SwapCached: 0 kB
SReclaimable: 106168 kB

与之前相比,无明显差别。

完全不在索引中数据查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 432.726604 ms
Avg time: 0.004327 ms
Max time: 12.856279 ms
Min time: 0.000800 ms

# 磁盘I/O
02:33:18 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
02:33:19 PM 36473 0.00 0.00 0.00 java
02:33:20 PM 36473 0.00 0.00 0.00 java
02:33:21 PM 36473 14948.00 16.00 0.00 java
02:33:22 PM 36473 464.00 0.00 0.00 java
02:33:23 PM 36473 0.00 0.00 0.00 java
02:33:24 PM 36473 0.00 0.00 0.00 java
02:33:25 PM 36473 0.00 0.00 0.00 java
02:33:26 PM 36473 0.00 0.00 0.00 java
02:33:27 PM 36473 0.00 0.00 0.00 java
02:33:28 PM 36473 0.00 0.00 0.00 java
02:33:29 PM 36473 0.00 0.00 0.00 java
02:33:30 PM 36473 0.00 0.00 0.00 java
02:33:31 PM 36473 0.00 0.00 0.00 java
02:33:32 PM 36473 0.00 0.00 0.00 java
02:33:33 PM 36473 0.00 0.00 0.00 java
02:33:34 PM 36473 0.00 0.00 0.00 java
02:33:35 PM 36473 0.00 0.00 0.00 java
02:33:36 PM 36473 0.00 0.00 0.00 java
02:33:37 PM 36473 0.00 0.00 0.00 java
02:33:38 PM 36473 0.00 8.00 0.00 java
02:33:39 PM 36473 0.00 0.00 0.00 java
02:33:40 PM 36473 0.00 0.00 0.00 java
02:33:41 PM 36473 0.00 0.00 0.00 java
02:33:42 PM 36473 0.00 0.00 0.00 java
02:33:43 PM 36473 0.00 0.00 0.00 java
Average: 36473 616.48 0.96 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1713868 kB
SwapCached: 0 kB
SReclaimable: 82000 kB

与之前相比,无论是运行时间、磁盘I/O还是系统缓存都有了很大的提升。

混合查询

使用混合查询用例中的测试数据(100000条)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 10221.723065 ms
Avg time: 0.102217 ms
Max time: 13.377899 ms
Min time: 0.001182 ms

# 磁盘I/O
02:41:46 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
02:41:47 PM 36656 0.00 0.00 0.00 java
02:41:48 PM 36656 8480.00 24.00 0.00 java
02:41:49 PM 36656 43936.00 0.00 0.00 java
02:41:50 PM 36656 44032.00 0.00 0.00 java
02:41:51 PM 36656 45076.00 0.00 0.00 java
02:41:52 PM 36656 46752.00 0.00 0.00 java
02:41:53 PM 36656 47796.00 0.00 0.00 java
02:41:54 PM 36656 47348.00 0.00 0.00 java
02:41:55 PM 36656 49084.00 0.00 0.00 java
02:41:56 PM 36656 50328.00 0.00 0.00 java
02:41:57 PM 36656 50908.00 0.00 0.00 java
02:41:58 PM 36656 50520.00 0.00 0.00 java
02:41:59 PM 36656 14832.00 0.00 0.00 java
02:42:00 PM 36656 0.00 0.00 0.00 java
02:42:01 PM 36656 0.00 0.00 0.00 java
02:42:02 PM 36656 0.00 0.00 0.00 java
02:42:03 PM 36656 0.00 0.00 0.00 java
02:42:04 PM 36656 0.00 0.00 0.00 java
02:42:05 PM 36656 0.00 0.00 0.00 java
02:42:06 PM 36656 0.00 0.00 0.00 java
02:42:07 PM 36656 0.00 0.00 0.00 java
02:42:08 PM 36656 0.00 0.00 0.00 java
02:42:09 PM 36656 0.00 0.00 0.00 java
02:42:10 PM 36656 0.00 0.00 0.00 java
02:42:11 PM 36656 0.00 0.00 0.00 java
Average: 36656 19963.68 0.96 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 2187460 kB
SwapCached: 0 kB
SReclaimable: 97808 kB

与之前相比,运行时间有了显著的提升。

验证效果

BloomFilter的使用确实能够明显地提升查询性能(不论是查询时间,还是磁盘I/O)。

使用内容前缀缓存后的测试结果

靠前部分数据查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 16982.256299 ms
Avg time: 0.169823 ms
Max time: 47.892767 ms
Min time: 0.006427 ms

# 磁盘I/O
02:55:07 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
02:55:08 PM 36882 0.00 0.00 0.00 java
02:55:09 PM 36882 0.00 0.00 0.00 java
02:55:10 PM 36882 44720.00 4.00 0.00 java
02:55:11 PM 36882 53808.00 0.00 0.00 java
02:55:12 PM 36882 56392.00 0.00 0.00 java
02:55:13 PM 36882 57428.00 0.00 0.00 java
02:55:14 PM 36882 59060.00 12.00 0.00 java
02:55:15 PM 36882 60844.00 4.00 0.00 java
02:55:16 PM 36882 60624.00 0.00 0.00 java
02:55:17 PM 36882 60828.00 0.00 0.00 java
02:55:18 PM 36882 61384.00 0.00 0.00 java
02:55:19 PM 36882 64644.00 0.00 0.00 java
02:55:20 PM 36882 65768.00 0.00 0.00 java
02:55:21 PM 36882 67020.00 0.00 0.00 java
02:55:22 PM 36882 66268.00 0.00 0.00 java
02:55:23 PM 36882 69688.00 0.00 0.00 java
02:55:24 PM 36882 68328.00 0.00 0.00 java
02:55:25 PM 36882 68916.00 0.00 0.00 java
02:55:26 PM 36882 72812.00 0.00 0.00 java
02:55:27 PM 36882 30740.00 8.00 0.00 java
02:55:28 PM 36882 0.00 0.00 0.00 java
02:55:29 PM 36882 0.00 0.00 0.00 java
02:55:30 PM 36882 0.00 0.00 0.00 java
02:55:31 PM 36882 0.00 0.00 0.00 java
02:55:32 PM 36882 0.00 0.00 0.00 java
Average: 36882 43570.88 1.12 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1759340 kB
SwapCached: 0 kB
SReclaimable: 103344 kB

与之前相比,无明显差别。

靠后部分数据查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 13197.228538 ms
Avg time: 0.131972 ms
Max time: 4.570671 ms
Min time: 0.002547 ms

# 磁盘I/O
03:00:51 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
03:00:52 PM 37189 0.00 0.00 0.00 java
03:00:53 PM 37189 0.00 0.00 0.00 java
03:00:54 PM 37189 27264.00 16.00 0.00 java
03:00:55 PM 37189 33348.00 0.00 0.00 java
03:00:56 PM 37189 33208.00 0.00 0.00 java
03:00:57 PM 37189 35184.00 0.00 0.00 java
03:00:58 PM 37189 36184.00 0.00 0.00 java
03:00:59 PM 37189 36340.00 0.00 0.00 java
03:01:00 PM 37189 35732.00 0.00 0.00 java
03:01:01 PM 37189 35900.00 0.00 0.00 java
03:01:02 PM 37189 35804.00 0.00 0.00 java
03:01:03 PM 37189 37952.00 0.00 0.00 java
03:01:04 PM 37189 38460.00 0.00 0.00 java
03:01:05 PM 37189 39380.00 0.00 0.00 java
03:01:06 PM 37189 38592.00 0.00 0.00 java
03:01:07 PM 37189 26228.00 0.00 0.00 java
03:01:08 PM 37189 0.00 0.00 0.00 java
03:01:09 PM 37189 0.00 0.00 0.00 java
03:01:10 PM 37189 0.00 0.00 0.00 java
03:01:11 PM 37189 0.00 0.00 0.00 java
03:01:12 PM 37189 0.00 0.00 0.00 java
03:01:13 PM 37189 0.00 0.00 0.00 java
03:01:14 PM 37189 0.00 0.00 0.00 java
03:01:15 PM 37189 0.00 0.00 0.00 java
03:01:16 PM 37189 0.00 0.00 0.00 java
Average: 37189 19583.04 0.64 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1163084 kB
SwapCached: 0 kB
SReclaimable: 103024 kB

与之前相比,性能有较为明显地提升。

混合查询

使用混合查询用例中的测试数据(100000条)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 17523.954852 ms
Avg time: 0.175240 ms
Max time: 5.308643 ms
Min time: 0.001480 ms

# 磁盘I/O
03:08:04 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
03:08:05 PM 37460 0.00 0.00 0.00 java
03:08:06 PM 37460 0.00 0.00 0.00 java
03:08:07 PM 37460 37376.00 16.00 0.00 java
03:08:08 PM 37460 50028.00 0.00 0.00 java
03:08:09 PM 37460 52016.00 0.00 0.00 java
03:08:10 PM 37460 52592.00 0.00 0.00 java
03:08:11 PM 37460 52592.00 0.00 0.00 java
03:08:12 PM 37460 54444.00 0.00 0.00 java
03:08:13 PM 37460 56756.00 0.00 0.00 java
03:08:14 PM 37460 56404.00 0.00 0.00 java
03:08:15 PM 37460 54556.00 0.00 0.00 java
03:08:16 PM 37460 57464.00 0.00 0.00 java
03:08:17 PM 37460 56928.00 0.00 0.00 java
03:08:18 PM 37460 59416.00 0.00 0.00 java
03:08:19 PM 37460 59432.00 0.00 0.00 java
03:08:20 PM 37460 62476.00 0.00 0.00 java
03:08:21 PM 37460 61860.00 0.00 0.00 java
03:08:22 PM 37460 62708.00 0.00 0.00 java
03:08:23 PM 37460 62676.00 0.00 0.00 java
03:08:24 PM 37460 63864.00 0.00 0.00 java
03:08:25 PM 37460 2796.00 4.00 0.00 java
03:08:26 PM 37460 0.00 8.00 0.00 java
03:08:27 PM 37460 0.00 0.00 0.00 java
03:08:28 PM 37460 0.00 0.00 0.00 java
03:08:29 PM 37460 0.00 0.00 0.00 java
Average: 37460 40655.36 1.12 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 1685812 kB
SwapCached: 0 kB
SReclaimable: 102536 kB

与之前相比,性能提升不明显。

验证效果

使用内容前缀缓存后,确实能够对靠后部分数据的查询性能有一定提升。但总体上,提升效果不明显。

双管齐下

混合查询

使用混合查询用例中的测试数据(100000条)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 运行时间
Total time: 8900.574408 ms
Avg time: 0.089006 ms
Max time: 13.780685 ms
Min time: 0.001219 ms

# 磁盘I/O
03:23:41 PM PID kB_rd/s kB_wr/s kB_ccwr/s Command
03:23:42 PM 37684 0.00 0.00 0.00 java
03:23:43 PM 37684 1488.00 16.00 0.00 java
03:23:44 PM 37684 41672.00 0.00 0.00 java
03:23:45 PM 37684 45888.00 0.00 0.00 java
03:23:46 PM 37684 45500.00 0.00 0.00 java
03:23:47 PM 37684 47456.00 0.00 0.00 java
03:23:48 PM 37684 47728.00 0.00 0.00 java
03:23:49 PM 37684 47360.00 0.00 0.00 java
03:23:50 PM 37684 49928.00 0.00 0.00 java
03:23:51 PM 37684 48904.00 0.00 0.00 java
03:23:52 PM 37684 49288.00 8.00 0.00 java
03:23:53 PM 37684 9076.00 12.00 0.00 java
03:23:54 PM 37684 0.00 0.00 0.00 java
03:23:55 PM 37684 0.00 0.00 0.00 java
03:23:56 PM 37684 0.00 0.00 0.00 java
03:23:57 PM 37684 0.00 0.00 0.00 java
03:23:58 PM 37684 0.00 0.00 0.00 java
03:23:59 PM 37684 0.00 0.00 0.00 java
03:24:00 PM 37684 0.00 0.00 0.00 java
03:24:01 PM 37684 0.00 0.00 0.00 java
03:24:02 PM 37684 0.00 0.00 0.00 java
03:24:03 PM 37684 0.00 0.00 0.00 java
03:24:04 PM 37684 0.00 0.00 0.00 java
03:24:05 PM 37684 0.00 0.00 0.00 java
03:24:06 PM 37684 0.00 0.00 0.00 java
Average: 37684 17371.52 1.44 0.00 java

# 系统缓存
$ cat /proc/meminfo | grep -E "Cached|SReclaimable"
Cached: 2276696 kB
SwapCached: 0 kB
SReclaimable: 98120 kB

性能得到进一步地提升。此时系统性能已达到万级qps,查询响应也在可接受范围内。

JVM的堆内存使用情况

索引构建后的,JVM的堆内存使用情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 5368709120 (5120.0MB)
NewSize = 1073741824 (1024.0MB)
MaxNewSize = 1073741824 (1024.0MB)
OldSize = 4294967296 (4096.0MB)
NewRatio = 2
SurvivorRatio = 10
MetaspaceSize = 67108864 (64.0MB)
CompressedClassSpaceSize = 58720256 (56.0MB)
MaxMetaspaceSize = 67108864 (64.0MB)
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 895483904 (854.0MB)
used = 0 (0.0MB)
free = 895483904 (854.0MB)
0.0% used
From Space:
capacity = 89128960 (85.0MB)
used = 0 (0.0MB)
free = 89128960 (85.0MB)
0.0% used
To Space:
capacity = 89128960 (85.0MB)
used = 0 (0.0MB)
free = 89128960 (85.0MB)
0.0% used
PS Old Generation
capacity = 4294967296 (4096.0MB)
used = 2874165616 (2741.0179290771484MB)
free = 1420801680 (1354.9820709228516MB)
66.91938303411007% used

900 interned Strings occupying 63016 bytes.

整个程序占用内存最大的是老生代(大约2.7 GB),其中包含了25000000个姓名、全部的一二级索引和Bloom过滤器。与之前相比,改进后内存使用量新增了1.2 GB。如果查询场景中,数据命中率很高,可以关闭Bloom过滤器减少内存使用,毕竟此时的收益较低。

5. 关于JVM启动参数配置

启动参数设置时,主要考虑两点:

  • 老生代存储空间压力大: 需要能够存储索引、姓名字典和Bloom过滤器。
  • 垃圾回收对查询响应的影响: 由于使用场景存在响应延迟的要求,我们需要尽量减少GC的stop-the-world事件对响应时间的影响。

因此,最终JVM启动参数设置如下:

1
2
3
4
5
6
7
8
9
10
11
# 基本设置和GC日志(可用于后期性能分析)
-server -XX:+AlwaysPreTouch -XX:+PrintCommandLineFlags -Xloggc:/home/gc-%t.log -XX:NumberOfGCLogFiles=5 -XX:+UseGCLogFileRotation -XX:GCLogFileSize=20m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintGCCause -XX:+PrintTenuringDistribution

# 设置堆内存布局,保证老生代大小
-Xmn1g -Xms5g -Xmx5g -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=64m -XX:-UseAdaptiveSizePolicy -XX:SurvivorRatio=10

# 选择垃圾回收器及设置
-XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=85 -XX:+ExplicitGCInvokesConcurrent -XX:+CMSScavengeBeforeRemark

# 开启内存溢出dump和错误日志
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/index.hprof -XX:ErrorFile=/home/hs_err_pid%p.log

六、后记

整个过程中涉及到了不少知识点,通过整个设计实现过程,将这些知识点进行了串联和实践,有了更多地理解,也发现之前一些想当然的东西。痛并快乐着吧:P。

算是抛砖引玉吧,如果你有更好地设计或者改进,欢迎交流。

附录

1. 身份证校验码计算

第一步身份证号码(除第18位校验码外)每一位(从1开始)按照以下方法计算相应的权重:
$$
W_i = 2^{18 - i} \mod 11
$$
第二步使用计算得到权重进行身份证号码(除第18位校验码外)加权求和:
$$
S = \sum_{i=1}^{17} a_i . W_i
$$
第三步求余得到校验码$a_{18}$,如果等于10,则使用X表示:
$$
a_{18} = (12 - (S \mod 11))\mod 11
$$
参考代码

1
2
3
4
5
6
7
8
9
10
char[] ch = new char[18];
System.arraycopy(oldCertNum.toCharArray(), 0, ch, 0, oldCertNum.length());

int sum = 0;
for (int i = 0; i < ch.length - 1; i++) {
sum += ((1 << (17 - i)) % 11) * (ch[i] - '0');
}

int code = (12 - (sum % 11)) % 11;
ch[17] = code < 10 ? (char) ('0' + code) : 'X';

参考资料

  1. Bloom Filter
  2. Linux Page Cache
  3. Linux性能工具
  4. Linux Kernel Development (3rd Edition)
  5. Operating System Concepts (10th Edition)
  6. JVM 参数查询优化服务
  7. Java Garbage Collection Basics
  8. Java Tuning White Paper
0%