1 duckdb简介
DuckDB是一个高性能的分析型关系数据库,旨在实现高效的数据分析。它易于安装,运行速度非常快,并且支持进程内(in-process)运行,为用户提供了极大的便利。DuckDB项目于2018年启动,经过近7年的不断打磨,如今已发展成为一个拥有30多万行C++引擎代码、4.2万次代码提交、解决了4000个issue的成熟开源项目,GitHub和社交媒体平台上的关注者数以万计,每月下载量高达数百万次。
DuckDB背后的核心思想是保留SQLite的简单性和易用性,同时通过R/Python和RDBMS之间的快速分析处理和快速数据传输进行增强,以处理OLAP工作负载。该项目之所以被命名为“DuckDB”,是因为创建者认为鸭子具有弹性并且可以靠任何东西为生,这种特性与他们设想的数据库系统运行方式相似。
DuckDB是嵌入式分析型数据库,使用起来十分灵活,其使用方式大致可以分为3类,嵌入程序中、DuckDB CLI,以及嵌入数据库管理工具中。
2 Oracle 告警日志分析
在数据库的日常运维和故障分析诊断时,查看和分析Oracle 告警日志是必不可少的一步,也是十分关键的一步。Oracle告警日志通常比较大,动辄几万行或者几十万行,查询起来有一定难度。熟悉linux的dba一般使用Linux的grep,awk,sed等工具进行分析,windows比较熟悉的通常将告警日志考出来用Excel打开进行查询分析。前面一种方法需要熟练掌握Linux的几个文本工具,后面的方法如果告警文件比较大,文件打开会很慢或者干脆打不开。
除了上面的方法之外,也可以通过Oracle的外部表对告警文件进行分析,Oracle的外部表配置起来相对复杂一些,如果是生产库,也可能影响到数据库的性能。本文介绍一种通过DuckDB解析Oracle告警文件,它的原理同Oracle外部表相同,不过操作更简单,对原数据库不做任何操作,可以进行复杂的查询和分析。
3 DuckDB CLI 下载
DuckDB CLI是DuckDB的命令行接口,基于SQLLITE 命令行shell开发,因此,它的功能同SQLLITE类似,但它的语法遵循postgresql习惯(有少数例外)。
DuckDB CLI下载后解压即可使用,下载的地址如下
https://duckdb.org/docs/installation/?version=stable&environment=cli&platform=linux&download_method=direct&architecture=x86_64
复制
linux 平台AMD64架构使用curl命令将压缩包下载到当前目录并解压
curl --fail --location --progress-bar --output duckdb_cli-linux-amd64.zip https://github.com/duckdb/duckdb/releases/download/v1.2.0/duckdb_cli-linux-amd64.zip && unzip duckdb_cli-linux-amd64.zip
复制
在不提供文件名参数时,DuckDB CLI打开一个临时的内存中的数据库如下
duckdb
v1.2.0 5f5512b827
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D
复制
如果要打开或者新建永久数据库,提供一个文件名
duckdb my_database.duckdb
复制
4 read_csv函数
在duckdbd中,使用read_csv函数访问csv文件,使用这个函数,可以将csv文件的内容导入到duckdb数据库中,也可以直接查询csv文件中的内容。这个函数的参数有30几个,这里介绍几个常用的
参数名称 | 参数说明 | 参数类型 | 缺省值 |
---|---|---|---|
columns | 列的名称和类型 , 使用结构体来定义(例如{‘col1’: ‘INTEGER’, ‘col2’: ‘VARCHAR’}). Using this option disables auto detection. | STRUCT | 空 |
dateformat | 解析和写日期时用的格式 | VARCHAR | 空 |
sep | 列分隔符 | VARCHAR | , |
header | 第一行是否是表头 | BOOL | false |
names or column_names | 每一列的名称 | VARCHAR[] | 空 |
read_csv的第一个参数是要读取的文件名,文件的扩展名可以不是.csv,如果第一行是表头,参数header的值应该为true,如果第一行不是表头,需要提供columns参数或者names参数,如提供的是columns参数,会关闭列数据类型自动检测,提供names,则只提供列名称,不设置列的数据类型。
5直接查询告警日志
5.1 查询前10行
启动DuckDB后,可以直接读取并查询Oracle告警日志,使用下面的SQL查询告警日志的前10行
SELECT * FROM read_csv('/root/alert_FREE.log', names = ['content']) limit 10;
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ content │
│ varchar │
├────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Adjusting the default value of parameter "parallel_max_servers" from 0 to 1. │
│ PARALLEL_MAX_SERVERS (with value 1) is insufficient. This may affect transaction recovery performance. │
│ Stored outlines will be desupported in the next release. │
│ Starting ORACLE instance (normal) (OS id: 589) │
│ 2024-04-24T21:19:39.269634+00:00 │
│ **************************************************** │
│ Sys-V shared memory will be used for creating SGA │
│ **************************************************** │
│ 2024-04-24T21:19:39.273414+00:00 │
│ ********************************************************************** │
├────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 10 rows │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
复制
5.2 从第2000行开始查询10行
可以从任意指定行开始查询一定数量的行,如下面的SQL语句从第2000行开始查询10行
SELECT * FROM read_csv('/root/alert_FREE.log', names = ['content']) limit 10 offset 2000;
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ content │
│ varchar │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ The priority of process VKTM cannot be elevated. │
│ │
│ Failed to elevate VKTM's priority from 0 to 1, policy 5 │
│ Error : Category(-2), Opname(skgdism_send), Loc(sp.c:setpr:0), ErrMsg(Operation not permitted) Dism(128) │
│ Using default pga_aggregate_limit of 2048 MB │
│ 2024-05-06T06:39:39.697423+00:00 │
│ Errors in file /opt/oracle/diag/rdbms/free/FREE/trace/FREE_lgwr_1556.trc (incident=9773) (PDBNAME=CDB$ROOT): │
│ ORA-00800: soft external error, arguments: [Set Priority Failed], [LGWR], [Check traces and OS configuration], [Ch… │
│ Incident details in: /opt/oracle/diag/rdbms/free/FREE/incident/incdir_9773/FREE_lgwr_1556_i9773.trc │
│ --ATTENTION-- │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 10 rows │
复制
6 处理数据,进行更灵活方便的查询
上面的直接查询告警日志虽然能够查询任意指定位置开始的指定数量的行,但是还是不够灵活,如果能够按照日期查询,按照日志消息中是否包含指定字符串(如ORA-)来进行查询,分析起来更加简单、快捷。这需要对告警日志的数据进行适当的处理,可以按照下面的步骤进行
6.1 创建表
Oracle 告警日志的内容如下
2024-12-23T01:30:05.550936+00:00
Thread 1 advanced to log sequence 91 (LGWR switch), current SCN: 25077317
Current log# 1 seq# 91 mem# 0: /opt/oracle/oradata/FREE/redo01.log
复制
观察Oracle告警日志,发现里面都是一行是时间,后面跟着发生在这个时间点的事件,基本上一行就是一条消息。因此,在创建表时,只需一列即可,建表语句如下:
create table tab_alert(content varchar(2000));
复制
6.2 载入数据
insert into tab_alert select * from read_csv('/root/alert_FREE.log'); --导入后表中的数据如下 select * from tab_alert limit 20 offset 200; │ │ 2024-04-24T21:21:53.584766+00:00 │ Errors in file /opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_728.trc: │ ORA-19625: error identifying file /ade/b/298079266/oracle/oradata/FREE/pdbseed/system01.dbf │ │ ORA-27037: unable to obtain file status │ Linux-x86_64 Error: 2: No such file or directory │ Additional information: 7 │ Switch of datafile 2 complete to datafile copy │ checkpoint is 2046537
复制
6.3 提取时间数据到相应列
现在表中每一行只有一列数据,时间和时间是混在一起的,下面要做的是将时间数据提取出来,形成单独的列,另外,为了在后续处理里中保持消息的顺序,在每一行数据前加一个行号,使用下面的select语句
select row_number() OVER () line_n, --行号 content, case regexp_matches(content,'(\d+)-(\d+)-(\d+)') when false then null else regexp_extract(content,'(\d+)-(\d+)-(\d+)') end as mes_day, --消息日期 case regexp_matches(content,'(\d+):(\d+):(\d+)') when false then null else regexp_extract(content,'(\d+):(\d+):(\d+)') end as mes_time --消息时间 from tab_alert
复制
regexp_extract函数提出符合条件的字符串,(\d+)-(\d+)-(\d+)匹配如2024-04-24的字符串(日志中的日期),2024-04-24匹配21:19:39的字符串(日志中的时间),这条语句的执行结果如下
│ 14 │ 2024-04-24T21:19:39.274318+00:00 │ 2024-04-24 │ 21:19:39 │
│ 15 │ Domain name: kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-podd6b… │ │ │
│ 16 │ 2024-04-24T21:19:39.274690+00:00 │ 2024-04-24 │ 21:19:39 │
│ 17 │ Per process system memlock (soft) limit = 64K │ │ │
│ 18 │ 2024-04-24T21:19:39.275065+00:00 │ 2024-04-24 │ 21:19:39 │
│ 19 │ Expected per process system memlock (soft) limit to lock │ │ │
│ 20 │ instance MAX SHARED GLOBAL AREA (SGA) into memory: 1532M │ │ │
复制
6.4 使用分析函数,为每一行增加时间信息
通过上面的SQL,日期和时间信息已经提出到单独的列中,但是每一条消息(时间除外)的时间和日期的数据是空的,下一步是给每条消息增加时间信息。每条消息的时间信息都是它前面最近的时间和日期为非空的列,使用分析函数将时间和日期添加到对应的行
select line_n,content,
last_value(mes_day ignore nulls) over (order by line_n) as mes_day,--忽略空值,取上一个非空的mes_day
last_value(mes_time ignore nulls) over (order by line_n) as mes_time,--忽略空值,取上一个非空的mes_time
from (
select row_number() OVER () line_n,
content,
case regexp_matches(content,'(\d+)-(\d+)-(\d+)')
when false then null
else
regexp_extract(content,'(\d+)-(\d+)-(\d+)')
end as mes_day,
case regexp_matches(content,'(\d+):(\d+):(\d+)')
when false then null
else
regexp_extract(content,'(\d+):(\d+):(\d+)')
end as mes_time
from tab_alert
);
复制
上面这条语句的查询结果如下
line_n │ content │ mes_day │ mes_time │
│ int64 │ varchar │ varchar │ varchar │
├────────┼─────────────────────────────────────────────────────────────────
│ 15 │ Domain name: kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-podd6b… │ 2024-04-24 │ 21:19:39 │
│ 16 │ 2024-04-24T21:19:39.274690+00:00 │ 2024-04-24 │ 21:19:39 │
│ 17 │ Per process system memlock (soft) limit = 64K │ 2024-04-24 │ 21:19:39 │
│ 18 │ 2024-04-24T21:19:39.275065+00:00 │ 2024-04-24 │ 21:19:39 │
│ 19 │ Expected per process system memlock (soft) limit to lock │ 2024-04-24 │ 21:19:39
复制
每一条消息都有了自己的时间信息,这样,可以通过时间和消息的内容来进行查询了。
6.5 创建视图
为了简化后续的查询,可以创建一个视图,后面只对这个视图进行查询就可以了
create view v_alert as
select line_n,content,
last_value(mes_day ignore nulls) over (order by line_n) as mes_day,
last_value(mes_time ignore nulls) over (order by line_n) as mes_time,
from (
select row_number() OVER () line_n,
content,
case regexp_matches(content,'(\d+)-(\d+)-(\d+)')
when false then null
else
regexp_extract(content,'(\d+)-(\d+)-(\d+)')
end as mes_day,
case regexp_matches(content,'(\d+):(\d+):(\d+)')
when false then null
else
regexp_extract(content,'(\d+):(\d+):(\d+)')
end as mes_time
from tab_alert
);
复制
6.6 查询告警日志
select * from v_alert where content like '%ORA-%';
┌────────┬─────────────────────────────────────────────────────────────────────────────────────┬────────────┬──────────┐
│ line_n │ content │ mes_day │ mes_time │
│ int64 │ varchar │ varchar │ varchar │
├────────┼─────────────────────────────────────────────────────────────────────────────────────┼────────────┼──────────┤
│ 48 │ ORA-27167: Attempt to determine if Oracle binary image is stored on remote server… │ 2024-04-24 │ 21:19:39 │
│ 49 │ ORA-27300: OS system dependent operation:parse_df failed with status: 2 │ 2024-04-24 │ 21:19:39 │
│ 50 │ ORA-27301: OS failure message: No such file or directory │ 2024-04-24 │ 21:19:39 │
复制
可以看到每条错误信息的日期和时间,也可以根据时间查询,比如每一天的,每一小时的,或者是几分钟的。
6.7 将处理过程持久化
之前运行的DuckDB处于内存模式,在DuckDB重启后,处理的过程会丢失,如何将处理过程持久化?
有两种方式可以考虑,一种是持久化数据库,参照本文前面第三章,这种方式两个弊端,一是占用本地存储空间,二是如果告警日志内容变化,需要清除表的内容,重新导入数据,显得不那么自动化。另一种方式是将处理的过程存入一个文件中,在DuckDB启动时执行这个文件,命令如下
./duckdb -init alert.sql
复制
这种方式每次启动都是重新载入数据,也不占用本地存储空间,比较适合Oracle告警日志这种经常变化的文件。
7 注意事项
7.1 DuckDB CSV reader 顺序问题
The CSV reader 根据 preserve_insertion_order 配置选项来决定是否保留插入顺序. 当参数值为 true (缺省值), 插入的顺序和文件中行的读取顺序相同. 如果值是false, 则不能保证插入后的顺序和文件中行的顺序相同.
7.2 时间的提取
在提取时间时,可以分别提出时间和日期,也可以一起提取。
7.3 Windows操作系统下的使用
DuckDB在Windows操作系统,也有CLI可用,更简单方便的使用方式是下载一个通用数据库管理软件,如DBeaver,安装上DuckDB的java驱动就可以直接使用了。