Postgres 在存储网络配置和策略的探索

在我司防火墙设备的配置都是以对象的形式存储在mongodb数据库里面。而我们需要对配置或者策略进行计算,这时就需要将配置数据load到内存中进行计算,这样就会面临两个问题

  • mongodb的性能瓶颈
  • 大对象配置导致的性能问题

不只是关系型数据库

postgresql 自称世界上最先进的开源关系型数据库,但其实postgres丰富的数据类型以及支持存储json的特性这让它完全可以像mongodb一样来存储文档,并且postgres的性能更好
image.png

postgres和mongodb 在文档数据方面的性能比较 postgres vs mongodb

丰富的数据类型

如果没有足够丰富的数据类型,postgres也不会来分mongodb的蛋糕

网络地址类型

PostgreSQL提供用于存储 IPv4、IPv6 和 MAC 地址的数据类型, 用这些数据类型存储网络地址比用纯文本类型好,因为这些类型提供输入错误检查以及特殊的操作符和函数

名字 存储尺寸 描述
cidr 7或19字节 IPv4和IPv6网络
inet 7或19字节 IPv4和IPv6主机以及网络
macaddr 6字节 MAC地址
macaddr8 8 bytes MAC地址(EUI-64格式)

操作符

操作符 描述 例子
< 小于 inet '192.168.1.5' < inet '192.168.1.6'
<= 小于等于 inet '192.168.1.5' <= inet '192.168.1.5'
= 等于 inet '192.168.1.5' = inet '192.168.1.5'
>= 大于等于 inet '192.168.1.5' >= inet '192.168.1.5'
> 大于 inet '192.168.1.5' > inet '192.168.1.4'
<> 不等于 inet '192.168.1.5' <> inet '192.168.1.4'
<< 被包含在内 inet '192.168.1.5' << inet '192.168.1/24'
<<= 被包含在内或等于 inet '192.168.1/24' <<= inet '192.168.1/24'
>> 包含 inet '192.168.1/24' >> inet '192.168.1.5'
>>= 包含或等于 inet '192.168.1/24' >>= inet '192.168.1/24'
&& 包含或者被包含contains or is contained by inet '192.168.1/24' && inet '192.168.1.80/28'
~ 按位 NOT ~ inet '192.168.1.6'
& 按位 AND inet '192.168.1.6' & inet '0.0.0.255'
| 按位 OR
+ inet '192.168.1.6' + 25
- inet '192.168.1.43' - 36
- inet '192.168.1.43' - inet '192.168.1.19'

函数

函数 返回类型 描述 例子 结果
abbrev(inet) text 缩写显示格式文本 abbrev(inet '10.1.0.0/16') 10.1.0.0/16
abbrev(cidr) text 缩写显示格式文本 abbrev(cidr '10.1.0.0/16') 10.1/16
broadcast(inet) inet 网络广播地址 broadcast('192.168.1.5/24') 192.168.1.255/24
family(inet) int 抽取地址族;4为 IPv4, 6为 IPv6 family('::1') 6
host(inet) text 抽取 IP 地址为文本 host('192.168.1.5/24') 192.168.1.5
hostmask(inet) inet 为网络构造主机掩码 hostmask('192.168.23.20/30') 0.0.0.3
masklen(inet) int 抽取网络掩码长度 masklen('192.168.1.5/24') 24
netmask(inet) inet 为网络构造网络掩码 netmask('192.168.1.5/24') 255.255.255.0
network(inet) cidr 抽取地址的网络部分 network('192.168.1.5/24') 192.168.1.0/24
set_masklen(inet, int) inet inet值设置网络掩码长度 set_masklen('192.168.1.5/24', 16) 192.168.1.5/16
set_masklen(cidr, int) cidr cidr值设置网络掩码长度 set_masklen('192.168.1.0/24'::cidr, 16) 192.168.0.0/16
text(inet) text 抽取 IP 地址和网络掩码长度为文本 text(inet '192.168.1.5') 192.168.1.5/32
inet_same_family(inet, inet) boolean 地址是来自于同一个家族吗? inet_same_family('192.168.1.5/24', '::1') false
inet_merge(inet, inet) cidr 包括给定网络的最小网络 inet_merge('192.168.1.5/24', '192.168.2.5/24') 192.168.0.0/22

json

JSON 数据类型是用来存储 JSON(JavaScript Object Notation) 数据的。这种数据也可以被存储为text,但是 JSON 数据类型的优势就在于能强制要求每个被存储的值符合 JSON 规则。也有很多 JSON 相关的函数和操作符可以用于存储在这些数据类型中的数据

json类型也是postgres可以做文档数据库的基础

jsonjsonb。它们 几乎接受完全相同的值集合作为输入。主要的实际区别之一是效率。json数据类型存储输入文本的精准拷贝,处理函数必须在每 次执行时必须重新解析该数据。而jsonb数据被存储在一种分解好的 二进制格式中,它在输入时要稍慢一些,因为需要做附加的转换。但是 jsonb在处理时要快很多,因为不需要解析。jsonb也支持索引,这也是一个令人瞩目的优势。

我们可以这样来索引一个json里面的key-value,不需要load到内存

假如我们要根据一个地址对象搜索策略

select * from policy WHERE source @> '[{
        "name":"aaa"
    }]';

image.png

假如我想获取目的地址name=xad的策略的源地址

select source  from policy WHERE destination @> '[{
        "name":"xad"
    }]';

image.png

使用postgres提供的处理函数进行处理

select jsonb_array_elements(source) #> '{value,address}'  from policy WHERE destination @> '[{
        "name":"xad"
    }]';

image.png

操作符 右操作数类型 描述 例子 例子结果
-> int 获得 JSON 数组元素(索引从 0 开始,负整数从末尾开始计) '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 {"c":"baz"}
-> text 通过键获得 JSON 对象域 '{"a": {"b":"foo"}}'::json->'a' {"b":"foo"}
->> int text形式获得 JSON 数组元素 '[1,2,3]'::json->>2 3
->> text text形式获得 JSON 对象域 '{"a":1,"b":2}'::json->>'b' 2
#> text[] 获取在指定路径的 JSON 对象 '{"a": {"b":{"c": "foo"}}}'::json#>'{a,b}' {"c": "foo"}
#>> text[] text形式获取在指定路径的 JSON 对象 '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' 3
操作符 右操作数类型 描述 例子
@> jsonb 左边的 JSON 值是否在顶层包含右边的 JSON 路径/值项? '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb
<@ jsonb 左边的 JSON 路径/值项是否被包含在右边的 JSON 值的顶层? '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb
? text 键/元素字符串是否存在于 JSON 值的顶层? '{"a":1, "b":2}'::jsonb ? 'b'
`? text[] 这些数组字符串中的任何一个是否做为顶层键存在?
?& text[] 是否所有这些数组字符串都作为顶层键存在? '["a", "b"]'::jsonb ?& array['a', 'b']
` ` jsonb
- text 从左操作数删除键/值对或者string 元素。键/值对基于它们的键值来匹配。 '{"a": "b"}'::jsonb - 'a'
- text[] 从左操作数中删除多个键/值对或者string元素。键/值对基于它们的键值来匹配。 '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[]
- integer 删除具有指定索引(负值表示倒数)的数组元素。如果 顶层容器不是数组则抛出一个错误。 '["a", "b"]'::jsonb - 1
#- text[] 删除具有指定路径的域或者元素(对于 JSON 数组,负值 表示倒数) '["a", {"b":1}]'::jsonb #- '{1,b}'

数组

PostgreSQL允许一个表中的列定义为变长多维数组。可以创建任何内建或用户定义的基类、枚举类型、组合类型或者域的数组。

这给我们设计数据结构提供了非常高的灵活度。

防火墙策略

那么我们可以根据postgres提供的丰富数据类型来构建策略表

create TABLE public.policy (
  id  uuid primary key not null,
  device uuid not null,
  action policy_action not null,
  name VARCHAR(128),
  description TEXT,
  disabled BOOLEAN,
  line INT,
  scheduler json,
  source inet[],
  destination inet[],
  service json,
  helper json
);

一个例子:

INSERT INTO "public".policy
    VALUES (
      '11ffbcf4-4994-4275-8f81-10442677b852',
      '212fbcf4-4221-4212-8182-12342677b352',
      'permit',
      'policy1',
      'this is desc',
       FALSE,
       999,
       null,
       '{192.168.1.1, 192.168.1.2, 192.168.1.3}',
       '{192.168.1.4, 192.168.1.5}',
        null,
        null);

假如我要查询源地址包含 192.168.1.1的策略 我们可以通过以下的sql语句达成,而不是通过将数据load到内存进行操作

SELECT * FROM "public".policy WHERE '192.168.1.1' = ANY (source);

假如我要查询源地址是 192.168.1.0/24网段内的策略

SELECT * FROM "public".policy WHERE '192.168.1.0/24' >> ANY (source);

使用postgres里面inet类型提供的操作符,我们可以在数据库层面进行一次源地址和目的地址查找,即使需要精确的匹配策略(包括service和scheduler)我们也不会将此设备的所有policy都load到内存

但是在真正业务面前,这个地址字段的设计还是过于理想化的,因为我们的输入的地址类型不只是hostsubnet 还有fqdnrange类型,postgres都不支持,所以我们可能还是需要使用varchar类型来兼容多种网络地址类型。这样的话我们索引地址对象得需要强制将类型转成inet才能完成并且不支持fqdn和range,除非我们将range转成多个ip存入,fqdn类型放到helper里面来解决类型的问题。

使用官方提供的网络地址类型来存储策略的地址是不能够满足我们的需求的,而且其提供的函数过于鸡肋,所以可以使用json来存储策略的地址对象和服务对象等,但这样在数据库层面就不能进行网络地址计算,而只能将地址当作字符串来检索

create TABLE public.policy (
  id  uuid primary key not null,
  device uuid not null,
  action VARCHAR not null,
  name VARCHAR(128),
  description TEXT,
  disabled BOOLEAN,
  line INT,
  scheduler jsonb,
  source jsonb,
  destination jsonb,
  service jsonb,
  helper jsonb
);

这时我们可以通过三种方式来检索策略,测试数据为35W条策略每条策略五个随机源地址和五个随机目的地址

  1. 使用基础sql查询,但模糊查询无法使用索引
select * from policy where source::TEXT || destination::TEXT  like '%192.168.1.11%';

image.png

  1. 使用json类型的操作符
select * from policy where source || destination @> '[{
        "id":"aa",
        "name":"aaa",
        "type":"WILDCARD",
        "value":{
            "address":"192.168.1.11"
        },
        "description":"a"
    }]'::jsonb;

image.png

我们为source和destination 字段添加gin索引

CREATE INDEX idxgin ON policy USING gin (source,destination);
select * from policy where source @> '[{
        "id":"aa",
        "name":"aaa",
        "type":"WILDCARD",
        "value":{
            "address":"192.168.1.11"
        },
        "description":"a"
    }]'::jsonb or destination @> '[{
        "id":"c93db18e-b1f6-44f4-af86-35a41d65ad0a",
        "value":{
            "address":"22.33.147.81/32"
        },
        "description":"created by NetworkString"
    }]'::jsonb;

image.png

  1. postgres提供的全文检索
select * from policy where to_tsvector(source::TEXT || destination::TEXT || service::TEXT) @@ plainto_tsquery('192.168.1.11');

image.png

tsvector值是一个排序的可区分词位的列表,词位是被正规化合并了同一个词的不同变种的词。排序和去重是在输入期间自动完成的

tsquery值存储要用于搜索的词位,并且使用布尔操作符&(AND)、|(OR)和!(NOT)来组合它们

全文检索似乎不支持模糊查询,只支持以什么开头的查询

索引

postgres提供了多种适用于不同场景的索引供我们选择使用

  • B-tree 索引: CREATE INDEX命令创建适合于大部分情况的B-tree 索引。

  • Hash 索引: 只能处理简单等值比较。不论何时当一个索引列涉及到一个使用了=操作符的比较时,查询规划器将考虑使用一个Hash索引。下面的命令将创建一个Hash索引:

    CREATE INDEX name ON policy USING HASH (column);
    
  • GiST 索引: GiST索引并不是一种单独的索引,而是可以用于实现很多不同索引策略的基础设施 postgres的标准库提供了用于多种二维几何数据类型的GiST操作符。

    -- 它将找到离给定目标点最近的10个位置。
    SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;
    
  • BRIN 索引: (块范围索引的缩写)存储有关存放在一个表的连续物理块范围上的值摘要信息。与 GiST、SP-GiST 和 GIN 相似,BRIN 可以支持很多种不同的索引策略,并且可以与一个 BRIN 索引配合使用的特定操作符取决于索引策略。

  • GIN 索引: GIN 索引是“倒排索引”,它适合于包含多个组成值的数据值,例如数组。倒排索引中为每一个组成值都包含一个单独的项,它可以高效地处理测试指定组成值是否存在的查询。

    CREATE INDEX idxgin ON policy USING gin (source,destination);
    

总结

postgres自身强大的性能以及支持json类型以及简单丰富操作符,完全可以替代掉mongo用来作为配置和策略的存储数据库。

防火墙配置的数据库表设计

  1. 继续将大的配置对象使用这种json文档的格式存储,只是将mongo换成postgres,使用postgres来提升性能的下限,有分析表明在多种情况下postgres在文档数据存储方面的性能是mongodb的4-15倍,在大内存的数据集前表现的更加明显
create TABLE public.vmconfig (
  id  uuid primary key not null,
  device uuid not null,
  addresses jsonb,
  services jsonb,
  policies jsonb,
);
  1. 将大的配置对象拆分到不同的表里面使用外键关联,这样通过延迟加载来减少内存消耗
create TABLE public.address (
  id  uuid primary key not null,
  name varchar(128),
  description text,
  device uuid not null, //关联的设备
  type address_type,
  value jsonb,
  version config_version //配置的版本
);

create TABLE public.service (
  id  uuid primary key not null,
  name varchar(128)
  description text,
  device uuid not null, //关联的设备
  type service_type,
  value jsonb
  version config_version //配置的版本
);

create TABLE public.policy (
  id  uuid primary key not null,
  device uuid not null,  //关联的设备
  action policy_action not null,
  name VARCHAR(128),
  description TEXT,
  disabled BOOLEAN,
  line INT,
  scheduler uuid,
  source uuid[],
  destination uuid[],
  service uuid[],
  helper json,
  version config_version //配置的版本
);

create TABLE public.scheduler (
  .....
  .....
);