本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议 》和 《 阿里云开发者社区知识产权保护指引 》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单 进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
+关注继续查看

集团内部数据资产团队维护了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数组,只需要扫面一遍即可(具体参考 inet_pton6 )。根据已有的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)。另外,正则表达式使用起来虽然方便,但对于性能要求高的场景,可能并不适合。尤为重要的是,数据处理场景减少数据读写的放大,对性能的提升是显著的。

PolarDB for PostgreSQL 采用iprange和SPGiST index 实现超光速 - 全球化业务根据来源IP智能DNS路由就近分配本地机房访问, 提升用户体验
[《如何获得IP地址对应的地理信息库, 实现智能DNS解析? 就近路由提升全球化部署业务的访问性能》](../202211/20221124_09.md) 上一篇信息提到了如何获取IP地址段的地理信息库, 本篇信息将使用PolarDB for PostgreSQL来加速根据来源IP快速找到对应的IP地址段, 将用到PolarDB for PostgreSQL的SPGiST索引和inet数据类型. 相比于把IP地址段存储为2个int8字段作between and的匹配, SPGiST索引和inet数据类型至少可以提升20倍性能.
PolarDB-X 1.0-常见问题-VPC 问题-PolarDB-X能否切换经典网络到VPC网络?
支持。PolarDB-X不仅支持直接购买VPC类型的PolarDB-X实例,同时也支持由Classic(经典网络类型)切换到VPC。 PolarDB-X切换到VPC后,原本Classic类型的ECS将无法访问PolarDB-X,只允许VPC环境的ECS访问PolarDB-X。因此,在PolarDB-X切换VPC过程中需要将应用停机并切换ECS的类型。
码农小达人