前情提要
大约在2个月前我开发了一个ip查询接口,当时的需求只需要支持单个IP结果的查询即可,所以在选择上选用了es进行存储,v4和v6单独分索引,结果大概10ms左右就可以返回,需求顺利交付,但是随着业务的变化与增长,有另外一个同事过来说需要支持一下批量数据的查询,此时就出现了一个问题,另外一个同事的需求是一次查询至多1000个地址,并批量返回,这个情况对于es来说就有一些慢了,在起初接口提供给他的时候,接口经常出现超时,所以就促成了此次优化。
本次优化相关指标:
数据量:700万
查询要求:500ms完成至少1000个ip的结果返回
优化方案
初次尝试
其实初次尝试也没有什么比较好的方案,所以国际惯例,先优化代码。通过分析代码以及日志发现了几个问题:
1.日志中有非ip格式的数据传入,未做合法性校验导致pannic。
2.ip转换的时候会经过一个计算的过程,而这个过程是可以进行批量处理的。
3.索引过多(第一个版本分了3个索引),需要多次查询才能得到准确结果,这会多次去访问es增加了耗时。
第一个版本的伪代码如下:
// 1.接收IP数据
ipList := c.getParams("ipList")
// 2.传入转换
for ip in ipList {
transIp(ip)
}
// 3.1 查询第一个索引 找到对应的IpIndex
ipIndex := es.find(transIp)
// 3.2 查询第二个索引 通过ipIndex找到CityIndex
cityIndex := es.find(ipIndex)
// 3.3 查询第三个索引 通过cityIndex找到对应的城市
city := es.find(cityIndex)
复制代码
通过统计三步查询成为耗时大户,占据大概100ms左右,此时若是单个Ip查询还凑合,如果是1000个一定会炸。
故第一步便是先修复代码里的逻辑错误:
1.增加合法性校验,避免第三方服务传入一些奇怪的字符导致服务pannic。
2.批量处理合成逻辑避免串行耗时。
3.合并索引,将取消IpIndex和CityIndex索引,将第一和第三个索引合并为一个,减少查询次数。
优化过后流程如下:
第一阶段的优化方案
此时已经基本解决了查询耗时的问题,为什么说基本呢,因为借助缓存,我们在第一次请求超时过后,线程仍然会继续执行下去,执行完成后会写入Redis,下一次再进行查询的时候,也就不会访问DB了,那么也就不会超时了,当时由于没有好的思路,所以暂时使用了这一比较挫的方案替代。
几天过后……
再次尝试
距离上次修改大概3周左右,在和前部门的一个组长聊到这个事情,他的第一反应就是数据库选型出了问题,此时为才恍然大悟,于是修改了之前的计划,决定替换掉Elastic Search使用MySQL来进行使用。
使用MySQL来进行优化
有了之前的经验,我迅速组织好了表结构(这里的结构是做过一次合并之后的索引的结构)大概结构如下:
TableName ip_db
id
IpIndex // 此处就是之前提到的IpIndex用于查询Index
IpMessage
……
这里为什么要单独强调IpIndex呢,因为这和我们的查询业务有关,Ip查询是通过IpIndex来找到对应的地址,那么这也告诉我们需要在IpIndex这个字段上添加索引了。接下来我们需要做的就是选择合适的索引进行添加。
索引选择
在选择索引之前,我先贴上这次查询的SQL
select * from ip_db where ipIndex <= xxxx order by ipIndex desc limit 1;
普通索引:普通索引可能会重复,查找到满足条件的第一个记录后,继续查找下个记录,直到碰到第一个不满足记录就返回。
唯一索引:不可重复,和主键一样,查到第一个满足条件的,就停止搜索。
二者性能差距忽略不计,但是我们的IpIndex是不可以重复的,所以我们必须选择唯一索引。
接下来就是选择数据结构,可供选择的数据结构有Hash和btree。
Hash索引一般用于精准查找性能极高(o(1)),无法支持范围查询。
Btree索引B-tree索引更适合进行范围查询,用于查询中的order by 操作。
综上看来,我们两种索引都可以选择,但是Btree,因为Btree支持范围查询,而我们的SQL也更多的是范围查询,其中的=意义不大,直接ipIndex相等的概率很小。
当然mysql的InnoDB中存在一种优化机制,我们称之为自适应Hash索引,当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree索引之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。但这是一个完全自动的、内部的行为,用于无法控制,但用户可以选择完全关闭该动能。
最后的配置
create unique index ip_search_index
on ip_data.ip_db (ipIndex desc);
复制代码
当然上述优化,我们同是结合数据库连接池,就完成了本次优化。
最后
经过优化之后,700w的数据访问单个ip查询在10μs左右,综合1000个ip查询耗时在100ms以内,优化前为5s-6s波动。总的来说本次优化所使用的技术不难,重要的事思考的过程,一步一步完成从单个ip10ms查询到单个ip10μs,同时能够承载1000个ip同时查询大大优化了用户体验,通过强化了容错性。