v2ray / Planning

Deprecated. Please use v2ray-core for feature requests
60 stars 13 forks source link

匹配 geoip 规则时可以选择从磁盘上搜索读取数据 #74

Closed eycorsican closed 6 years ago

eycorsican commented 6 years ago

现在 geoip 是启动时把记录解析为 ip 规则,全部加载到内存中,这样做会耗费较多内存,特别是有好多 geoip 规则的情况,比如下面这个配置:

                {
                    "type": "field",
                    "ip": [
                        "geoip:us",
                        "geoip:jp",
                        "geoip:gb",
                        "geoip:de",
                        "geoip:kr",
                        "geoip:fr",
                        "geoip:ca",
                        "geoip:it",
                        "geoip:au",
                        "geoip:nl",
                        "geoip:ru",
                        "geoip:in",
                        "geoip:se"
                    ],
                    "outboundTag": "proxy"
                }

MaxMind 的 geoip 数据库提供了一种二进制数据文件格式:http://maxmind.github.io/MaxMind-DB/ ,用这样的方式去读取性能上对于客户端来说可以满足,也几乎不会耗费内存,对于移动端来说还是很有意义的,golang 也有一些开源的实现,比如这个:https://github.com/oschwald/maxminddb-golang

VictoriaRaymond commented 6 years ago

那iOS的文件读取有什么技巧吗?需不需要对IP查询做缓存?

我的想法是对路由中每一个"ip":[]单独做一个文件,使用某种特殊的格式。配置文件中可以设置当条目多于一定数量时,将当前的"ip":[]写入文件。这样配置的格式不用大改。

eycorsican commented 6 years ago

没有什么特别技巧,我的实现是把 geoip 规则看作跟 ip 规则完全不同的另一个类型,在 Router 中为 geoip 规则加上了它的 Matcher:

func NewGeoIPMatcher(geoipCodes []string) *GeoIPMatcher {
    return &GeoIPMatcher{
        geoipCodes: geoipCodes,
    }
}

func (v *GeoIPMatcher) Apply(ctx context.Context) bool {
    ips := make([]net.IP, 0, 4)
    if resolver, ok := proxy.ResolvedIPsFromContext(ctx); ok {
        resolvedIPs := resolver.Resolve()
        for _, rip := range resolvedIPs {
            if !rip.Family().IsIPv4() {
                continue
            }
            ips = append(ips, rip.IP())
        }
    }

    dest, ok := proxy.TargetFromContext(ctx)
    if ok && dest.Address.Family().IsIPv4() {
        ips = append(ips, dest.Address.IP())
    }

    for _, ip := range ips {
        code, err := lookupGeoIP(ip)
        if err != nil {
            return false
        }

        for _, c := range v.geoipCodes {
            if code == c {
                return true
            }
        }
    }
    return false
}

你说的把 ip 规则转化为某种读取格式这个方法可能会比较优雅。

我只考虑到客户端,觉得对这个做查询缓存没必要,如果考虑上服务端的话情况可能不一样。

VictoriaRaymond commented 6 years ago

我又想了一下,还有一种方法。

MaxMind 的数据库,IPv4有303746条数据,IPv6有84732条数据,用最朴素的方法存,大约需要2.8MB的空间(303746 5 + 84732 17),这是上限。而你上面给出的geoip列表,IPv4共有55060条数据,IPv6没统计,按空间1:1来算,IPv4+IPv6大约需要500kB的空间。假如用户再多指定一点,1MB的空间应该可以支撑所有的IP规则。在这种情况应该不用写入磁盘了吧。

算法方面我有办法使用上面数据模型进行查询,不会带来性能上的损失。

eycorsican commented 6 years ago

期待。

看起来像是把磁盘搜索变成内存搜索,不放磁盘上搜索是因为算法方面是要依赖 Go 的特性吗?

VictoriaRaymond commented 6 years ago

基本思路是这样的:c73e899f54f0e4d6d4c2982265efc73c2e4c9b73

配置中新增了一个 geoip 项,需要多个release才能切换到新的配置项。但从API调用的话,你现在就可以测试了。

放内存的原因主要是空间占用足够小。Golang的map有很大的overhead,而新的算法只依赖于朴素的数组,可以有效地减少内存的使用。

新的算法要求传入的GeoIP按country code分类,存储的时候同一country code的IP列表只会存一份,即在多个规则使用同一个GeoIP,也不会重复占用空间。

GreaterFire commented 6 years ago

Why not use mmap() to map the file on disk to the physical memory? Because it is demand paging, this is a nice way to save memory.

http://man7.org/linux/man-pages/man2/mmap.2.html https://godoc.org/golang.org/x/exp/mmap

VictoriaRaymond commented 6 years ago

@GreaterFire It is hard to tell whether iOS considers memory from mmap() as residence memory.