本文当将描述如何利用ip2region进行安全的并发查询。

以实现一个定位信息查询 http api为例:输入 ip 地址,查询并且返回定位信息,控制器处理函数伪代码逻辑如下:

// 完成指定 ip 地址的定位信息查询
func _doSearch(ipBytes) (string, error) {
  // -> 具体请查看下述的 不同并发安全方式 的具体实现
}

// 查询处理函数
// input -> 控制器输入对象
// output -> 控制器输出对象
func searchHanler(input, output) {
  // 1, 获取 / 解析 输入的 ip 地址
  ipString = input.getString("ip")
  ipBytes, err = util.parseIP(ipString)
  if err != nil {
    output.errorf("invalid ip address: %s", err)
    return
  }

  // 2, 调用 _doSearch 查询定位信息
  region, err = _doSearch(ipBytes)
  if err != nil {
    output.errorf("failed to search(%s): %s", ip_str, err)
    return
  }

  // 3, 返回定位信息
  output.JSON(region)
}

首先要记住一点:xdb 底层的查询 api 默认都不是并发安全的实现,因为查询过程需要多次进行文件寻址和读取操作,这些操作都不是并发安全的,也就是如果你直接创建一个查询器然后导出查询对象去进行并发调用,会导致一些完全无法预测的结果,上述searchHandler函数若想被安全的并发调用,有如下三种方式:

一,按需创建/销毁查询器

既然不是并发安全的实现,那就避免去进行并发调用,竞争的地方就单独创建一个查询器,用完立马销毁,则上述_doSearch函数实现如下:

func _doSearch(ipBytes) (string, error) {
  // 依据 ipBytes 的版本创建不同版本的 xdb 查询器
  var searcher = nil
  if ipBytes.length == 4 {
    // ipv4
    searcher = newWithFileOnly(xdb.IPv4, "v4 xdb path")
  } else if ipBytes.length == 16 {
    // ipv6
    searcher = newWithFileOnly(xdb.IPv6, "v6 xdb path")
  } else {
    // 不知道啥情况,反正不正常
    return "", errorf("invalid ip bytes")
  }

  // 函数结束后立即关闭查询器
  defer searcher.close()

  // 查询并返回定位信息
  return searcher.search(ipBytes)
}

如果想使用vIndex缓存的 xdb 查询对象,则在服务启动的时候需要就提前加载vIndex缓存,该缓存信息只需加载一次,并且全局共享,每份vIndex缓存固定占据512KiB的内存,则_doSearch的详细实现如下:

// 服务启动的时候加载 xdb 的 vectorIndex 信息作为全局变量
v4Index = loadVectorIndexFromFile ("v4 xdb path")
v6Index = loadVectorIndexFromFile ("v6 xdb path")

func  _doSearch(ipBytes) (string, error) {
  // 依据 ipBytes 创建带 vectorIndex 缓存的 xdb 查询对象
  var searcher = nil
  if ipBytes.length == 4 {
    // ipv4 -> 使用全局的 v4Index 缓存
    searcher = newWithVectorIndex(xdb.IPv4, "v4 xdb path", v4Index)
  } else  if  ipBytes.length == 16 {
    // ipv6 -> 使用全局的 v6Index 缓存
    searcher = newWithVectorIndex(xdb.IPv6, "v6 xdb path", v6Index)
  } else  {
    // 不知道啥情况,反正不正常
    return  "", error("invalid ip bytes")
  }

  // 函数结束后立即关闭查询器
  defer searcher.close()
  
  // 查询并返回定位信息结果
  return searcher.search(ipBytes)  
}

这种方式的优点是:并发查询安全且资源按需占用,缺点是:在并发量特别大的情况下,频繁的创建和销毁资源会导致一些额外的计算开销从而降低整体的查询响应速度。

二,使用全内存的缓存模式

虽然底层的 xdb api 不是并发安全的实现,但是 完全内存缓存的 xdb 查询器却可以安全的进行并发查询,因为完全内存缓存模式下查询过程中的文件寻址和读取操作都没有了,没有资源竞争就不会导致不可预测的结果。

如果想使用完全基于内存的查询方式,则在服务启动的时候就需要提前初始化全局的全内存缓存的查询器,只初始化一次然后全局共享使用,则_doSearch的详细实现如下:

// 服务启动的时候加载 content 创建全局的完全内存缓存的查询器
var v4Buffer = loadContentFromFile("v4 xdb path")
var v6Buffer = loadContentFromFile("v6 xdb path")
v4InMemSearcher = newWithBuffer(xdb.IPv4, v4Buffer)
v6InMemSearcher = newWithBuffer(xdb.IPv6, v6Buffer)

func _doSearch(ipBytes) (string, error) {
  // 依据 ipBytes 的长度调用不同版本的全局查询器完成查询
  if ipBytes.length == 4 {
    return v4InMemSearcher.search(ipBytes)
  } else if ipBytes.length == 16 {
    return v6InMemSearcher.search(ipBytes)
  } else {
    return "", errorf("invalid ip bytes")
  }
}

这种方式的优点是:并发查询安全且响应速度最快,缺点是:需要将整个 xdb 文件加载到内存中,内存资源占用最大。

三,使用 Ip2Region 查询服务

这是社区最推荐的调用方式,虽然目前只有 Go / Java 两个 binding 提供了这个实现,有其他语言有需要的欢迎提交 Issue。

如果 Ip2Regon 服务使用的是全内存缓存模式则其工作原理和上述方式二完全一致,如果使用的是 无缓存 或者 vIndex 缓存 的查询方式,Ip2Region 服务会维持一个 查询器池子,每次查询会从池子里面租借一个(没有则等待)查询器来进行查询,查询完成后即立马归还到池子中来确保查询效率和并发安全,使用该方式则_doSearch的详细实现如下:

// 指定 缓存策略, xdb 文件路径和初始查询器数量来创建 v4/v6 配置
// 初始化创建 20 个查询器
v4Config = newV4Config(VIndexCache, "v4 xdb path", 20)
v6Config = newV6Config(VIndexCache, "v6 xdb path", 20)

// 创建全局的 ip2region 查询服务对象
var ip2region = newIp2Region(v4Config, v6Config)

func _doSearch(ipBytes) (string, error) {
  // 直接调用全局的 ip2region 查询定位信息并返回
  return ip2region.search(ipBytes)
}

该方式绝大部分都是优点:并发安全,v4/v6混合查询,资源池子避免反复创建和销毁资源,池子大小/资源占用 配置可控。

除了上述三种方式外,当然还有其他的方式,例如:在 Searcher 上套一层,给 search 方法增加一个同步锁,这种方式也可以确保并发安全,不过并发场景的查询效率受限,并发量很大的时候查询时间会被严重拉长。

并发场景使用总结如下:

1,使用的语言 binding 提供了 Ip2Region 查询服务实现的,优先选择通过 Ip2Region 查询服务来调用,推荐VIndexCache + 固定查询器池子

2,没有 Ip2Region 查询服务实现的 binding 优先考虑使用 “按需创建带 vIndex 缓存的的查询器”来安全并发查询,前提是你的运行环境支持全局缓存,vIndex 需要仅需加载一次,然后全局共享使用,例如:PHP + FPM 就不合适,因为 vIndex 没法简单的通过变量去全局共享。

3,系统资源紧缺,IoT 设备或者其他情况的建议使用“按需创建无缓存的查询器”来安全并发查询。

4,查询并发量很大的情况下,运行环境支持全局资源缓存的并且系统资源充足的建议使用 “全内存缓存的全局查询器”来安全并发查询。

PS1:Ip2Region 早期设计是能在机械硬盘上进行快速查询的,除非并发量大到基于文件查询已经无法满足要求才考虑完全基于内存的模式,不然标配 SSD 的服务器下,带 vIndex 缓存的查询器能解决所有问题。