Concurrent queries
This document describes how to perform safe concurrent queries with ip2region.
Take implementing a region info lookup http api as an example: the input is an ip address, which is searched and returned as region info. The pseudocode of the controller handler is as follows:
// perform the region info lookup for the given ip address
func _doSearch(ipBytes) (string, error) {
// -> see the implementations of the different concurrency-safe approaches below
}
// search handler
// input -> controller input object
// output -> controller output object
func searchHanler(input, output) {
// 1, get / parse the input ip address
ipString = input.getString("ip")
ipBytes, err = util.parseIP(ipString)
if err != nil {
output.errorf("invalid ip address: %s", err)
return
}
// 2, call _doSearch to search the region info
region, err = _doSearch(ipBytes)
if err != nil {
output.errorf("failed to search(%s): %s", ip_str, err)
return
}
// 3, return the region info
output.JSON(region)
}First, remember one thing: the underlying xdb search APIs are not concurrency-safe implementations by default, because the search process needs to perform file addressing and read operations multiple times, and none of these operations are concurrency-safe. That is, if you directly create a searcher and expose the search object for concurrent calls, the results can be completely unpredictable. To let the searchHandler function above be called safely under concurrency, there are the following three approaches:
1. Create/destroy a searcher on demand
Since the implementation is not concurrency-safe, avoid making concurrent calls: wherever contention may happen, create a dedicated searcher and destroy it right after use. The _doSearch function above is then implemented as follows:
func _doSearch(ipBytes) (string, error) {
// create an xdb searcher of the corresponding version based on ipBytes
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 {
// unknown case, abnormal anyway
return "", errorf("invalid ip bytes")
}
// close the searcher immediately when the function returns
defer searcher.close()
// search and return the region info
return searcher.search(ipBytes)
}If you want to use the xdb searcher with the vector-index cache, you need to load the vector-index cache in advance when the service starts. The cache only needs to be loaded once and is shared globally; each vector-index cache takes a fixed 512KiB of memory. The detailed implementation of _doSearch is as follows:
// load the xdb vectorIndex info as global variables when the service starts
v4Index = loadVectorIndexFromFile ("v4 xdb path")
v6Index = loadVectorIndexFromFile ("v6 xdb path")
func _doSearch(ipBytes) (string, error) {
// create an xdb searcher with the vectorIndex cache based on ipBytes
var searcher = nil
if ipBytes.length == 4 {
// ipv4 -> use the global v4Index cache
searcher = newWithVectorIndex(xdb.IPv4, "v4 xdb path", v4Index)
} else if ipBytes.length == 16 {
// ipv6 -> use the global v6Index cache
searcher = newWithVectorIndex(xdb.IPv6, "v6 xdb path", v6Index)
} else {
// unknown case, abnormal anyway
return "", error("invalid ip bytes")
}
// close the searcher immediately when the function returns
defer searcher.close()
// search and return the region info
return searcher.search(ipBytes)
}The advantage of this approach is that concurrent queries are safe and resources are occupied on demand. The disadvantage is that under very high concurrency, frequently creating and destroying resources introduces extra computation overhead and slows down the overall query response.
2. Use the full in-memory cache mode
Although the underlying xdb APIs are not concurrency-safe implementations, an xdb searcher with the full in-memory cache can perform concurrent queries safely, because in full in-memory cache mode there are no file addressing or read operations during the search — no resource contention, so no unpredictable results.
If you want the fully memory-based search mode, you need to initialize the global full in-memory cached searchers in advance when the service starts — initialize only once and share them globally. The detailed implementation of _doSearch is as follows:
// load the content when the service starts to create the global fully in-memory cached searchers
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) {
// call the global searcher of the corresponding version based on the length of ipBytes
if ipBytes.length == 4 {
return v4InMemSearcher.search(ipBytes)
} else if ipBytes.length == 16 {
return v6InMemSearcher.search(ipBytes)
} else {
return "", errorf("invalid ip bytes")
}
}The advantage of this approach is that concurrent queries are safe and the response is the fastest. The disadvantage is that the whole xdb file must be loaded into memory, so it has the largest memory footprint.
3. Use the ip2region search service
This is the calling approach most recommended by the community. Currently only the Go and Java bindings provide this implementation; if you need it for another language, feel free to open an Issue.
If the ip2region service uses the full in-memory cache mode, it works exactly like approach 2 above. If it uses the no-cache or vector-index cache search mode, the ip2region service maintains a searcher pool: each query rents a searcher from the pool (waiting if none is available), and returns it to the pool immediately after the query finishes, which ensures both search efficiency and concurrency safety. With this approach, the detailed implementation of _doSearch is as follows:
// create the v4/v6 configurations with a cache policy, xdb file paths, and the initial number of searchers
// initialize with 20 searchers created
v4Config = newV4Config(VIndexCache, "v4 xdb path", 20)
v6Config = newV6Config(VIndexCache, "v6 xdb path", 20)
// create the global ip2region search service object
var ip2region = newIp2Region(v4Config, v6Config)
func _doSearch(ipBytes) (string, error) {
// directly call the global ip2region to search the region info and return it
return ip2region.search(ipBytes)
}This approach is almost all advantages: concurrency-safe, mixed v4/v6 queries, a resource pool that avoids repeatedly creating and destroying resources, and configurable pool size/resource usage.
Besides the three approaches above, there are of course other options — for example, wrap the Searcher with another layer and add a synchronization lock to the search method. This also ensures concurrency safety, but query efficiency under concurrency is limited, and when the concurrency is very high the query time is stretched severely.
Summary for concurrent scenarios
-
If your language binding provides the ip2region search service implementation, prefer calling through the ip2region search service;
VIndexCache + a fixed searcher poolis recommended. -
For bindings without an ip2region search service implementation, prefer
creating a searcher with the vector-index cache on demandfor safe concurrent queries, provided your runtime supports global caching — the vector index only needs to be loaded once and then shared globally. For example, PHP + FPM is not suitable, because the vector index cannot be easily shared globally through a variable. -
When system resources are scarce, on IoT devices, or in other similar cases, use
creating a searcher with no cache on demandfor safe concurrent queries. -
When query concurrency is very high, the runtime supports global resource caching, and system resources are sufficient, use
a global searcher with the full in-memory cachefor safe concurrent queries.
PS1: ip2region was designed early on to query quickly even on a mechanical hard drive. Unless the concurrency is so high that file-based queries can no longer meet the requirement, do not consider the fully memory-based mode — on servers that come with SSDs, a searcher with the vector-index cache solves all problems.