微服务网关白名单设计 | 水木双

506 阅读3分钟

场景

我们的微服务网关(go 编写)需要增加具体的内网系统接入白名单认证,白名单有 IP 地址、系统名称两种数据,内网的系统部署方式各式各样, 微服务、单机、多机负载均衡、k8s、docker 原生等等,数据总数约 40W 个。我们的目的是需要验证 IP 是否在我们的白名单列表中即可,存在则可访问服务,不存在则拒绝访问。

解决方案

方案 1

IP 白名单列表不适合放在数据库,数据库查询有耗时,选择放在 nosql 上,最终我们选择了 redis。数据结构的选择, redis 支持 string、list、hash、set、zset,把白名单实体(IP 或者应用名)当做一个 key,实际的数据我们可以不储存,我们只要寻找是否存在 key 来判断是否存在白名单就可以了,所以具体用什么数据类型不重要,直接用 string 就好,所以总结一下方案 1,白名单实体做 redis 的 key,用判断 redis 的 key 是否 exists 去验证白名单。

优点: 用 key exists 的方式判断白名单实体是否存在比较快速,性能好

缺点: 每次验证都要连接 redis,并发量大的时候,比较耗资源

方案 2

把所有的白名单实体,包括 IP、应用名以 string 的数据结构全部储存在 redis 的一个 key,每次验证白名单时,都从 key 获取 value,然后判断应用名、IP 地址是否存在 string(strings.Contain 之类的判断)中,以此来验证是否可授权访问服务

优点:几乎没有

缺点: 每次验证都要连接 redis,而且要在超大字符串中判断某实体(字符串格式)是否存在,性能低

直接 pass

方案 3

在方案 1 的基础上,每次网关启动的时候,先装载 redis 数据,然后将 redis 的 key 转为原生的 map(原生数据结构效率高,不依赖于任何第三方数据结构实现),每次服务请求,判断来源是否存在 map 的 key 中,以此来验证白名单。

白名单初始化规则: 每次网关启动时,即从 redis 读取数据,转为原生 map

白名更新规则: 网关提供更新已经装载了的白名单数据接口,当加入白名单时,数据写到 redis 之后,即调用网关接口,更新装载在每个网关服务内存里的白名单数据

具体技术细节

map 在并行读写时的处理

map 支持并行同时读,但是不支持并行读写。分析下我们的场景,我们的白名单数据 map 是读多写少,90% 的场景都是在读,网关在进行黑白名单验证的时候,会同时对 map 进行读写,为避免出现读写冲突锁错误,我们需要用 sync.map 来线程安全处理 map 数据。这里还有一个第三方的 concurrent_map 方案,经测试和 sync.map 性能几乎差不多,因为我们又是读多写少的场景,所以这里用原生的 sync.map 即可。

map 的设计优化

因为我们的白名单实体只是作为 map 的 key,所以对于 map 的 value 来说,我们用空结构体来定义 struct{} 来定义,go 的空结构体是可寻址的,是不用占内存空间的,节省内存资源。

巨大 map 获取 key 时的性能问题(约 40W 个 key)

通过预设容量来提高巨大 map 的查询时间,尽量减少 rehash,以空间换时间。

whilteList := make(map[int]struct{}, 400000)

if v, ok := whiteList[key]; ok {
  
    
} else {
  
    
}


参考

blog.csdn.net/weixin_4319…

leokongwq.github.io/2016/10/15/…