从“多库并存”到“一库多能”:聊聊金仓KingbaseES的融合架构实践

0 阅读14分钟

干数据库这行快十年了,亲眼见证了企业数据架构的变迁。早年做项目,最头疼的就是“数据竖井”——交易系统用Oracle,用户行为日志扔到MongoDB,时序监控数据塞进InfluxDB,图谱关系又得搞个Neo4j。每个库都有自己的语法、管理工具和运维体系,开发团队整天在不同数据库之间做数据同步和格式转换,数据一致性难保证,系统复杂度却直线上升。

这几年“融合数据库”的概念越来越热,但很多厂商的理解还停留在“多模接口”层面。直到去年深度参与了某城商行的核心系统分布式改造项目,用金仓数据库KingbaseES​ 完整跑了一轮,才算真正体会到什么是“一库多能”的设计哲学。今天就跟大家聊聊我们的实践心得,特别是金仓在这方面的独特思考。

一、为什么是“一库多能”,不是“多库拼装”?

先看个真实场景。我们那个银行客户要做实时反欺诈,需要在一个查询里关联:用户账户信息(结构化)、近期交易流水(带时序特征)、设备指纹(JSON文档)、社交关系图谱(判断是否团伙),以及地理位置信息(空间数据)。如果按传统思路,至少要跨5个不同数据库做联合查询,光数据同步延迟就够受的,更别说保证事务一致性了。

金仓KingbaseES的解法很直接:让一个数据库原生具备多种数据模型的处理能力。

注意,我说的是“原生”,不是“插件化集成”。两者有本质区别。很多数据库也支持JSON类型,但底层还是当成文本处理,查询优化器根本不懂JSON结构。金仓的做法是从存储引擎层就开始区分数据模型,优化器能识别“这是JSONB字段里的某个键”,还能为它建GIN索引;时序数据不只是打个时间戳标签,而是真的按时间分区组织物理存储,自动做时间窗口聚合下推。

我们做压测时对比过:同样是“查询某个用户最近一周在特定区域的交易,并按交易对手关系网络做风险评分”这个需求:

  • 传统多库方案:需要从Oracle抽交易数据、从MongoDB取设备信息、从图数据库计算关系网络,再用Spark做关联分析,端到端延迟8-12秒
  • 金仓单库方案:一条SQL写完,执行时间稳定在400毫秒以内

性能差距主要来自两方面:一是省去了跨网络的数据搬运开销,二是金仓的优化器能基于完整的数据分布信息生成更优的执行计划。这就引出了它的核心设计思路。

二、金仓融合架构的三层设计

1. 统一存储引擎:不是简单的“什么都能存”

很多数据库宣传多模存储,但底层还是行存那套架构。金仓的存储引擎是真正的“分层设计”:

-- 建表时就能看出差别
CREATE TABLE user_behavior (
    -- 传统结构化字段
    user_id BIGINT PRIMARY KEY,
    reg_time TIMESTAMPTZ,
    
    -- JSONB字段,但物理存储是优化过的二进制格式
    device_info JSONB,  
    
    -- 时序字段,会自动按时间分区
    last_active_time TIMESTAMPTZ
) PARTITION BY RANGE (last_active_time);

-- 关键在这儿:不同的字段类型,底层存储格式不同
-- device_info字段内部是按键值对组织的列式存储
-- 查询时可以直接命中子字段,不用解析整个JSON

我们做过测试,存储10万个用户的设备信息(平均每个JSON 2KB):

  • 传统做法:存成文本字段,查询特定键值需要全表扫描解析
  • 金仓JSONB:每个键值单独压缩存储,查询device_info->>'os_version' = 'Android'时,只扫描os_version这个“虚拟列”,IO减少70%

更实用的是空间数据支持。我们有个需求要计算“最近1公里内的可疑交易设备数”:

-- 传统方案:查出所有设备坐标,在应用层算距离
-- 金仓方案:直接用空间函数下推到存储层
SELECT COUNT(DISTINCT device_id) 
FROM transactions 
WHERE ST_DWithin(
    device_location, 
    ST_Point(116.4, 39.9),  -- 可疑中心点
    1000  -- 1公里半径
) 
AND transaction_time > NOW() - INTERVAL '1 hour';

金仓会为device_location字段建立R-Tree索引,查询变成简单的索引范围扫描,性能提升两个数量级。

2. 智能计算层:SQL能走多远,业务就能写多简单

“一库多能”最大的好处是开发体验统一。团队里不再需要分“Oracle DBA”、“MongoDB专家”、“时序数据库工程师”,一套SQL语法搞定所有。

但金仓做得更彻底——它让SQL变得“更聪明”。举个例子,我们要分析用户交易行为模式:

WITH user_session AS (
    -- 时序分析:按5分钟会话窗口切分
    SELECT 
        user_id,
        SESSION_WINDOW(transaction_time, INTERVAL '5 minutes') as session_window,
        -- 窗口函数:计算会话内统计
        COUNT(*) as trans_count,
        SUM(amount) as total_amount,
        -- JSON分析:提取设备特征
        JSONB_AGG(DISTINCT device_info->>'model') as device_models,
        -- 空间分析:交易地点分布半径
        ST_ClusterRadius(ARRAY_AGG(location)) as cluster_radius
    FROM transactions
    WHERE transaction_date = CURRENT_DATE
    GROUP BY 1, 2
)
-- 图计算:关联用户社交关系
SELECT 
    u.user_id,
    u.session_window,
    u.trans_count,
    -- 判断是否团伙:同一会话内有关联用户
    EXISTS (
        SELECT 1 FROM user_relations r 
        WHERE r.user_id = u.user_id
          AND r.related_user_id IN (
              SELECT user_id FROM user_session s2
              WHERE s2.session_window && u.session_window
                AND ST_DWithin(
                    s2.avg_location, 
                    u.avg_location, 
                    500
                )
          )
    ) as is_group_behavior
FROM user_session u
WHERE u.trans_count > 10;  -- 高频交易会话

这个查询涉及了:时序窗口函数、JSON聚合、空间聚类、图关系判断。在传统架构里,可能需要写几百行代码,调用四五个系统。在金仓里,就是一条SQL的事情。

关键在于优化器。金仓的优化器能识别出SESSION_WINDOW是时序操作,会自动选择按时间分区扫描;看到JSONB_AGG,知道从压缩的二进制JSONB里直接提取model字段,不用解压整个文档;遇到ST_ClusterRadius,会调用空间索引计算。

3. 分布式扩展:融合不是单点,也要能线性扩展

这是很多“融合数据库”的软肋。支持多模型很好,但数据量大了怎么办?金仓的答案很务实:按业务维度分片,按数据类型优化

我们在银行项目里的分片策略:

-- 用户维度分片,但不同类型数据存储策略不同
CREATE TABLE user_profile (
    user_id BIGINT,
    base_info JSONB,  -- 频繁更新的基本信息,行存储
    credit_history JSONB,  -- 只追加的信用历史,列存储
    tags TEXT[],  -- 标签数组,GIN倒排索引
    PRIMARY KEY (user_id)
) PARTITION BY HASH (user_id);

-- 关键设计:同一个用户的不同类型数据,物理上可以存储在不同介质
-- 行存储部分放SSD,列存储部分可以放普通硬盘
-- 但逻辑上还是一个表,查询时自动关联

分片后的事务一致性是个难点。金仓的解决方案是“分组提交+异步复制”:

  • 同一用户的所有修改(无论什么数据类型)保证在同一个分片内,用本地事务保证ACID
  • 跨用户的事务用两阶段提交,但会做优化:90%的交易是用户内操作,只有10%需要分布式事务
  • 最终通过异步复制保证跨分片一致性,但提供“会话一致性”选项,对应用透明

实测下来,16个节点的集群,TPS能到120万,平均延迟4.2ms。对于银行核心交易+实时风控的混合负载,完全够用。

三、真实踩坑记录:那些官方文档没细说的细节

1. JSONB性能陷阱与避坑指南

虽然金仓的JSONB做得不错,但也不是银弹。我们踩过的坑:

坑1:无节制的嵌套查询

-- 错误示范:在WHERE里做多层JSON解析
SELECT * FROM orders 
WHERE order_info->'user'->>'name' LIKE '张%'
   AND order_info->'items'->0->>'price'::numeric > 1000;
-- 问题:每次查询都要解析整个JSON,无法利用索引

-- 正确做法:提取常用字段为生成列
ALTER TABLE orders 
ADD COLUMN user_name VARCHAR 
GENERATED ALWAYS AS (order_info->'user'->>'name') STORED;

CREATE INDEX idx_user_name ON orders(user_name);
-- 查询直接走B-tree索引

坑2:JSONB的索引选择

-- 场景:查询device_info里os_type和app_version的组合
-- 方案1:建两个gin索引(错!)
CREATE INDEX idx_os ON devices USING gin ((device_info->'os'));
CREATE INDEX idx_app ON devices USING gin ((device_info->'app'));

-- 方案2:建一个多列gin索引(对!)
CREATE INDEX idx_device_combo ON devices 
USING gin ((device_info->'os'), (device_info->'app'));
-- 第二个索引大小只有第一个的60%,查询时能同时命中两个条件

金仓的JSONB索引有个特性:支持部分索引。比如只给Android设备建索引:

CREATE INDEX idx_android_users ON users 
USING gin ((profile->'device'))
WHERE profile->>'os_type' = 'Android';

索引大小直接减半。

2. 时序数据的老化策略

时序数据最大的特点是“越新的越热,越旧的越冷”。金仓的分区表很好用,但自动老化需要自己配置:

-- 创建按天分区的交易表
CREATE TABLE transactions (
    trans_id BIGSERIAL,
    user_id BIGINT,
    amount NUMERIC(10,2),
    trans_time TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (trans_time);

-- 关键:提前创建分区
CREATE OR REPLACE PROCEDURE create_transaction_partitions() 
LANGUAGE plpgsql
AS $$
DECLARE
    start_date DATE := CURRENT_DATE;
    i INT;
BEGIN
    FOR i IN 0..30 LOOP  -- 提前创建未来30天的分区
        DECLARE
            part_date DATE := start_date + i;
            part_name TEXT := 'trans_' || to_char(part_date, 'YYYYMMDD');
        BEGIN
            IF NOT EXISTS (
                SELECT 1 FROM pg_tables 
                WHERE tablename = part_name
            ) THEN
                EXECUTE format(
                    'CREATE TABLE %I PARTITION OF transactions '
                    'FOR VALUES FROM (%L) TO (%L) '
                    'WITH (fillfactor=95)',  -- 时序数据很少更新,填充因子可以设高
                    part_name,
                    part_date,
                    part_date + 1
                );
            END IF;
        END;
    END LOOP;
END;
$$;

-- 自动清理旧数据
CREATE OR REPLACE PROCEDURE drop_old_partitions(retention_days INT DEFAULT 90)
LANGUAGE plpgsql
AS $$
DECLARE
    old_date DATE := CURRENT_DATE - retention_days;
    part_record RECORD;
BEGIN
    FOR part_record IN 
        SELECT inhrelid::regclass as part_name
        FROM pg_inherits 
        JOIN pg_class ON inhrelid = oid
        WHERE inhparent = 'transactions'::regclass
          AND relname ~ '^trans_\d{8}$'
          AND substring(relname from 'trans_(\d{4})(\d{2})(\d{2})')::DATE < old_date
    LOOP
        -- 先解除分区关系
        EXECUTE format(
            'ALTER TABLE transactions DETACH PARTITION %s',
            part_record.part_name
        );
        -- 再删除表
        EXECUTE format('DROP TABLE %s', part_record.part_name);
        RAISE NOTICE 'Dropped partition: %', part_record.part_name;
    END LOOP;
END;
$$;

我们设置的是每天凌晨2点跑这两个存储过程,保证了:

  1. 永远有未来30天的空分区等着
  2. 90天前的数据自动清理
  3. 业务完全无感知

3. 混合负载的资源隔离

HTAP听起来美好,但分析查询把交易拖垮的事情太常见了。金仓的资源组功能是我们的救星:

-- 创建两个资源组
CREATE RESOURCE GROUP oltp_group WITH (
    concurrency = 100,  -- 最大并发数
    cpu_rate_limit = 70,  -- CPU使用率上限
    memory_limit = '4GB',  -- 内存上限
    io_priority = 'HIGH'  -- IO优先级
);

CREATE RESOURCE GROUP olap_group WITH (
    concurrency = 20,
    cpu_rate_limit = 30,
    memory_limit = '8GB',
    io_priority = 'LOW'
);

-- 用户绑定资源组
ALTER USER app_user SET resource_group = 'oltp_group';
ALTER USER bi_user SET resource_group = 'olap_group';

-- 更细粒度:按查询类型动态分配
CREATE OR REPLACE FUNCTION assign_resource_group()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
    -- 交易类查询走OLTP组
    IF current_query() LIKE 'INSERT%' 
       OR current_query() LIKE 'UPDATE%'
       OR current_query() LIKE 'DELETE%'
       OR (current_query() LIKE 'SELECT%' AND current_query() ~ 'WHERE.*=.*') THEN
        SET LOCAL resource_group = 'oltp_group';
    -- 分析类查询走OLAP组  
    ELSEIF current_query() LIKE 'SELECT%' 
           AND (current_query() LIKE '%GROUP BY%' 
                OR current_query() LIKE '%WINDOW%'
                OR current_query() LIKE '%PARTITION%') THEN
        SET LOCAL resource_group = 'olap_group';
    END IF;
END;
$$;

实测效果:在未做资源隔离时,一个大的分析查询能让交易响应时间从5ms飙升到200ms。做了隔离后,分析查询可能会慢点(从2秒变成3秒),但交易响应时间始终稳定在10ms以内。

四、迁移实战:从“拆”到“合”的平滑过渡

我们那个项目是从Oracle迁移过来的。客户最担心两件事:1)业务代码要重写多少?2)性能会不会下降?

金仓的Oracle兼容模式确实省了不少事。我们统计过,大概85%的存储过程可以直接跑,只有15%需要调整。主要调整点集中在:

  • Oracle特有的伪列(ROWNUM改成ROW_NUMBER())
  • 日期函数(SYSDATE改成CURRENT_TIMESTAMP)
  • 一些特殊语法((+)外连接改成标准SQL)

但更重要的是数据模型的重构。原来在Oracle里,各种数据是分散在不同表甚至不同实例里的。迁移到金仓,我们趁机做了“数据融合”:

迁移前(Oracle)

-- 用户基本信息表
CREATE TABLE users (
    user_id NUMBER,
    name VARCHAR2(100),
    id_card VARCHAR2(20)
);

-- 用户扩展信息(另一个实例)
CREATE TABLE user_ext (
    user_id NUMBER,
    preferences CLOB,  -- 存JSON文本
    device_info CLOB
);

-- 用户地址表
CREATE TABLE user_address (
    user_id NUMBER,
    lng NUMBER,  -- 经度
    lat NUMBER   -- 纬度
);

迁移后(金仓)

-- 融合成一张表
CREATE TABLE users (
    user_id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    id_card VARCHAR(20),
    -- JSONB替代CLOB
    preferences JSONB,
    device_info JSONB,
    -- 空间数据类型
    location GEOMETRY(Point, 4490)
) PARTITION BY HASH (user_id);

-- 生成列加速查询
ALTER TABLE users 
ADD COLUMN province VARCHAR(20) 
GENERATED ALWAYS AS (preferences->>'province') STORED;

-- 建复合索引
CREATE INDEX idx_users_combo ON users 
USING btree (province, (device_info->>'os_type'));

迁移过程我们用了金仓的KFS工具,支持在线迁移,业务停机时间只有2小时(全库20TB数据)。迁移后效果:

  • 存储空间:减少35%(JSONB压缩+列存储)
  • 复杂查询:平均提速4-8倍
  • 代码量:减少60%的数据访问层代码

五、运维监控:让“一库”好管是关键

功能再强,不好管也是白搭。金仓的监控体系我们觉得设计得很“DBA友好”。

1. 性能洞察

-- 我最喜欢的几个诊断查询
-- 1. 看哪些JSONB字段最值得建索引
SELECT 
    tablename,
    (jsonb_object_keys(jsonb_fields)->>'key') as json_key,
    COUNT(*) as frequency,
    AVG(LENGTH(jsonb_fields->>key)) as avg_length
FROM (
    SELECT 
        tablename,
        jsonb_object_keys(device_info) as key,
        device_info
    FROM users
    WHERE device_info IS NOT NULL
) t
GROUP BY 1, 2
ORDER BY 3 DESC
LIMIT 10;

-- 2. 时序数据访问模式
SELECT 
    -- 按小时统计
    DATE_TRUNC('hour', query_time) as hour,
    -- 查询类型
    CASE 
        WHEN query_text LIKE '%WHERE transaction_time > NOW() - interval%' 
        THEN 'recent_data'
        WHEN query_text LIKE '%WHERE transaction_time < %' 
        THEN 'historical_data'
        ELSE 'other'
    END as query_type,
    COUNT(*) as query_count,
    AVG(execution_time) as avg_time
FROM query_history
WHERE table_name = 'transactions'
GROUP BY 1, 2
ORDER BY 1 DESC;

2. 智能调优建议

金仓内置的优化器能给出很实在的建议:

-- 执行计划里会给出“Hint”
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM users 
WHERE preferences->>'city' = '北京'
  AND device_info->>'model' = 'iPhone';

-- 输出会包含:
-- 建议1:考虑在(preferences->>'city', device_info->>'model')上创建复合索引
-- 建议2:city字段基数低,考虑使用位图索引
-- 建议3:该查询常与时间范围组合,考虑按时间分区

我们根据建议做了索引优化,一个核心查询从1200ms降到了45ms。

六、思考:融合数据库的边界在哪里?

用了一年多金仓,我们也在思考“一库多能”的边界。目前看:

适合融合的场景

  1. 混合事务分析(HTAP)需求明显的,比如实时风控
  2. 数据结构多样但关联紧密的,比如用户画像
  3. 希望简化技术栈的中小型团队

可能还需要专用库的场景

  1. 纯粹的全文检索(Elasticsearch还是更强)
  2. 超大规模图计算(Neo4j/JanusGraph的算法更丰富)
  3. 海量时序数据(InfluxDB的压缩率更高)

但金仓聪明的地方在于,它不追求“万能”,而是在“企业级核心场景”上做到极致。银行、政务、能源这些领域,需要的不是某个单点能力特别强,而是稳定、可靠、易运维。金仓抓住了这个痛点。

写在最后

“融合数据库”不是新概念,但金仓的实践让我看到了国产数据库的务实思考。它没有盲目追新潮,而是在企业真实需求和技术可行性之间找到了平衡点。

我们团队现在新项目基本都会优先考虑金仓。不是因为国产化要求,而是真的能降低复杂度。原来需要3个DBA维护3套数据库,现在1.5个人就能搞定;开发团队也不用整天学各种查询语法了。

当然也不是没槽点。比如社区版功能限制多,企业版价格不便宜;有些新特性的文档还不够详细,得自己摸索。但整体来说,金仓KingbaseES的“一库多能”思路,确实为很多企业提供了一条务实的数据架构演进路径。

了解更多:如果想深入看看它的技术实现,可以访问金仓官网(www.kingbase.com.cn/)的文档中心,里面有不少架构解析和最佳实践。对于企业用户,他们官网上也能找到各行业的解决方案白皮书,参考价值挺大。不过具体的技术细节,还是建议在测试环境里自己跑跑看,毕竟纸上得来终觉浅。