IPv6地域信息查询优化

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 集团内部数据资产团队维护了IPv6相关的地域数据,同时也提供了SDK查询特定IPv6的地域信息(参考语雀文档全球IP图谱SDK使用说明)。我在mac机器上进行了相关的随机查询测试(总次数1000万次),mmap的内存模式平均耗时为3us左右(1000万次查询总耗时30.2s),其性能和他们之前做的性能测试报告基本一致。对于大部分查询场景,这个性能基本能够满足需求。但对于SLS SQL的场景,单次查

集团内部数据资产团队维护了IPv6相关的地域数据,同时也提供了SDK查询特定IPv6的地域信息(参考语雀文档全球IP图谱SDK使用说明)。我在mac机器上进行了相关的随机查询测试(总次数1000万次),mmap的内存模式平均耗时为3us左右(1000万次查询总耗时30.2s),其性能和他们之前做的性能测试报告基本一致。对于大部分查询场景,这个性能基本能够满足需求。但对于SLS SQL的场景,单次查询需要处理10亿(甚至100亿)级别条IPv6的地域解析,当前的性能还是不够的。因此,需要在数据资产团队已有SDK的基础上,探索针对SQL使用场景下的优化。

当前已有设计

数据保存格式

在优化说明之前,需要熟悉下IPv6地址的结构(参考IPv6 address)。IPv6有128位(或16个字节)组成,其中前64位基本是用来识别子网的。换句话说,可以用IPv6的前64位来确定对应IPv6的地域。因此,IPv6的地域查询,可以等价于下面一张巨大的查询表:

IPv6前8个字节

地域信息

0:0:0:0

region_info_1

0:0:0:1

region_info_2

.......

......

ffff:ffff:ffff:ffff

region_info_N

不难发现,按照上面的方式保存IPv6和地域的映射关系,是不切实际的,因为这需要2^64个映射项。实际上,对于地址连续的IPv6地址,其地域信息通常是相同的。因此,可以采用范围条目的形式来映射地域信息,具体如下表所示。

IPv6前8个字节

地域信息

[0:0:0:0, 1fff:ffff:ffff:ffff]

region_info_1

[2000:0:0:0, 2000:ffff:ffff:ffff]

region_info_2

.......

......

[ff00:0:0:0, ffff:ffff:ffff:ffff]

region_info_N

采用范围条目的映射方式后,可以显著减少映射项的数量(当前为120万个左右)。数据资产团队将IPv6的地域信息存在单个文件中,该文件由4部分组成:

文件部分

说明

元数据

元数据定义了下面3个部分的数据位置以及地域信息具体包含的字段名称

范围条目映射索引

为了避免将全量的范围条目映射加载到内存中,这里会每128条范围条目映射抽取第一条出来作为索引,该索引可以常驻内存中,加快IPv6地址的过滤和查找。

全量范围条目映射

这部分包含了全量的范围条目到地域信息的文件地址映射,相对来说数据量较大。

地域具体信息

这部分包含了地域的具体数据。

文件的具体结构,如下图所示。

文件加载流程

首先会读取文件前16个字节的MD5摘要,并校验文件数据是否受损。其次,读取元数据部分的内容。对于范围条目索引,总是加载到JVM堆中,以加快IPv6范围条目的过滤。而对于全量范围条目和地域信息数据,可以选择加载到JVM堆中、通过mmap映射到堆外或者从磁盘读取。对于我们SQL的场景,采用mmap映射的方式较为合适,兼顾性能的同时,不占用JVM堆的空间。

地域查询流程

IPv6地域的查询流程如下所示,其中范围条目过滤采用二分查找算法。因此,不难理解,地域查询的复杂度是O(logN)的,其中N是数据文件中IPv6范围条目的总数。

可优化点挖掘

直接阅读代码

潜在问题

通过对源码的分析发现,范围条目过滤的实现有比较大的改进空间。当前实现存在以下两个问题:

  • 匹配范围条目时,总是按照字节逐次对比(两个8字节对比,平均需要比较4次,才能区分大小)
int compareByteBuffer(byte[] ipv6PrefixBytes, ByteBuffer buffer) {
  for(byte kb : ipv6PrefixBytes) {
    byte bb = buffer.get();
    int re = GravityBytesUtil.compareUnByte(kb, bb);
    if(re != 0) {
      return re;
    }
  }
  return 0;
}
  • 范围条目过滤过程中,产生大量的临时对象,这主要来自两个方面:
    • ByteBuffer的产生。范围条目索引在JVM对中是以byte[]形式保存的,每次提取条目内容时,都要转成ByteBuffer。
    private ByteBuffer wrapCell(int index) {
        int position = index * cellSize;
        return ByteBuffer.wrap(indexArr, position, cellSize);
    }
  • 临时过滤结果的产生。每次查询都会产生IndexResult和ItemResult,但它们是可以避免的,因为真正有的信息其实就是地域信息在文件的位置(即address)。

优化方案

知道上面的问题后,改进其实也比较简单。

  • 总是采用long进行8字节范围对比。在加载范围条目索引时,避免采用byte[]进行保存,而应该采用两个long[]数组以及一个int[]数组来分别保存范围条目的start、end以及地域信息的位置。在过滤IPv6的地址时,其前8个字节也转为long,这样比较的时候只需要比较一次long即可。同时,这样改造还解决了产生大量ByteBuffer的问题。 但这里有个问题,IPv6的前8个字节转成java long后,可能是负数,而IPv6的前8个字节组成的long值应该解释为unsigned。因此,IPv6的前8个字节转成long后,不能直接用比较运算符进行比较。不过,java有个Long.compareUnsigned专门解决这种情况的比较问题。
public static int compareIpv6Range(long keyValue, long prefixStart, long prefixEnd) {
  if (Long.compareUnsigned(keyValue, prefixStart) < 0) {
    return -1;
  }
  if (Long.compareUnsigned(keyValue, prefixEnd) > 0) {
    return 1;
  }
  return 0;
}
  • 范围条目查询直接使用int来指示结果。对于indexCker.queryIndex,直接命中索引中的范围条目,则返回地域地址;否则,返回-index-1,其中index为全量范围条目要进一步进行过滤的起始位置。对于,itemCker.searchValue的返回结果,也是类似处理。

优化效果

经过上面的优化后,每次地域查询的时间降为了2.1us,性能提升了30%左右。这个提升应该主要是采用long比较代替字节逐个带来的,可以看到,上面的优化还是非常有效果的。

通过jvisualvm热点分析

潜在问题

通过CPU采样发现,有很大一部分时间花在了IPv6字符串的解析上。通过上面的查询流程知道,查询时会通过正则去判断IPv6格式是否合法(即GravityBytesUtil.isIPv6),然后再提取其前8个字节(即GravityBytesUtil.parseIPv6_V64)。通过代码分析发现,整个解析流程会遍历IPv6字符串好几遍,尤其是正则匹配较为消耗CPU。

优化方案

理论上,通过一遍扫面即可完成判断IPv6是否合法以及提取出它的前8个字节,而无需借助于正则。通过查找资料发现,libc库中有个非常高效函数将IPv6字符串解析为byte数组,只需要扫面一遍即可(具体参考)。根据已有的C实现,我自己用java实现类似的逻辑。就IPv6解析效率来说,提升了4-5倍。

优化效果

在范围比较优化基础上,引入IPv6解析优化后,每次地域查询的时间从2.1us降为了1.5us,性能进一步提升了30%左右。和数据资产团队最初的实现相比,性能至此提升100%。

结合SLS SQL场景

潜在问题

目前地区信息包含了20多个字段的内容,但在文件中却是作为一个字符串整体存放的,各个字段的内容通过$字符进行分割。每个地域信息的存储结构具体如下所示,其中第一部分为地域信息的长度(2个字节),第二部分是地域信息的具体内容。

因此,从文件中读取地域信息后,需要先split字符串,然后再组装为地域信息结构体。这样操作存在以下问题:

  • 读放大。对于SQL查询场景,每个地域相关的function通常只需要其中一个字段的信息。
  • 字符串切分消耗CPU(即下面splitNoRegex函数)以及产生大量的临时字符串对象

优化方案

为了避免读放大和不必要的字符串切割,需要将每个字段的内容单独存储,优化后的单个地域信息存储结果如下所示。需要说明的是,每个字段的end offset,是相对于第一个字段内容的开始位置的。之所以采用end offset来记录字段内容的位置,为了便于快速获取某个字段内容的起始位置及其大小。

 

对于SLS SQL的场景,目前只有用到地域信息中的9个字段,通过统计发现,9个字段的内容总长度不会超过255。因此,每个字段的end offset采用1个字节即可。给定地域信息的地址,获取某个字段的内容,其代码如下所示,其中pointerBytes表示的是存储单个字段end offset所需要的字节数,fieldDataOffset表示的是第一个字段内容的偏移量。

    public byte[] queryFieldInfo(int address, int fieldId) {
        final int fieldStartFp, fieldBytes;
        if (fieldId == 0) {
            mappedBuffer.position(address);
            fieldStartFp = 0;
            fieldBytes = readFieldPointer();
        } else {
            mappedBuffer.position(address + (fieldId - 1) * pointerBytes);
            fieldStartFp = readFieldPointer();
            fieldBytes = readFieldPointer() - fieldStartFp;
        }

        if (fieldBytes <= 0) {
            return null;
        }

        mappedBuffer.position(address + fieldDataOffset + fieldStartFp);
        byte[] data = new byte[fieldBytes];
        mappedBuffer.get(data);

        return data;
    }

优化效果

在范围比较和IPv6解析优化基础上,引入地域信息结构存储优化后,每次地域查询的时间从1.5us降为了0.3us。而和数据资产团队最初的实现相比,性能至此提升了10倍(即从3us降为了0.3us)。

JVM堆优化前后对比

下图是优化前执行随机压测查询时堆变化情况,可以明显看到堆的使用波动非常大,最高达到116M,Young GC后又会下降回30M。很明显,这种现象是因为执行过程中有大量的临时对象产生导致的。

优化前

下图是优化后执行随机压测查询时堆变化情况,可以看到堆的使用最大不超过35M,使用量的波动明显比优化前小很多,这证实了上述优化对临时变量产生的治理是非常有效的。

优化后

总结

本分主要对数据资产团队的IPv6地域查询设计和实现进行了分析,在他们已有的基础上进行了一定的优化,主要包括减少临时变量的产生、改进IPv6前8个字节对比实现、采用遍历一次的IPv6解析算法以及分离存储地域信息的各字段。整体优化效果非常明显,性能提升了一个数量级(单次查询从3us降为了0.3us),对于SLS SQL海量数据处理来说,这里的优化还是非常有意义的。

对于java程序而言,虽然JVM会自动回收不再使用的对象,但为了更好的性能,需要尽可能避免临时对象的生成(申请内存、初始化、对象销毁都有overhead)。另外,正则表达式使用起来虽然方便,但对于性能要求高的场景,可能并不适合。尤为重要的是,数据处理场景减少数据读写的放大,对性能的提升是显著的。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6月前
|
监控 网络协议 安全
阿里云DNS常见问题之阿里云DNS的自定义线路用途不知道如何解决
阿里云DNS(Domain Name System)服务是一个高可用和可扩展的云端DNS服务,用于将域名转换为IP地址,从而让用户能够通过域名访问云端资源。以下是一些关于阿里云DNS服务的常见问题合集:
|
6月前
|
搜索推荐 定位技术
如何快速查询自己的IP地址?
IP地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
如何快速查询自己的IP地址?
|
4月前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用问题之“主集群和从集群地域映射表”指的是什么
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
PolarDB产品使用问题之“主集群和从集群地域映射表”指的是什么
|
4月前
|
关系型数据库 MySQL 分布式数据库
PolarDB产品使用问题之如何查看连接信息
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
4月前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用问题之已经配置公网地址但依然无法连接上,该如何排查
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
5月前
|
运维 关系型数据库 分布式数据库
PolarDB产品使用问题之已经正确配置了白名单,但仍然无法连接到PolarDB的访问地址,该怎么办
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
5月前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用问题之切换后,公网和私网地址是否会自动对调
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
5月前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用问题之外网连接的带宽上限是多少
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
6月前
|
域名解析 弹性计算 网络协议
阿里云DNS常见问题之不知道GTM线路解析如何解决
阿里云DNS(Domain Name System)服务是一个高可用和可扩展的云端DNS服务,用于将域名转换为IP地址,从而让用户能够通过域名访问云端资源。以下是一些关于阿里云DNS服务的常见问题合集: