持久化机制常见问题
1 持久化属性值与预期不一致问题
1.1 在组件初始化的ctor阶段进行数据库读写
!!!组件不应该在app的ctor阶段对self.db进行数据库读写,因为这个时候还没有初始化persist_client,读到的数据还没从持久化恢复,写的数据也不会持久化!!!
组件读写数据库应该至少在app初始化的init阶段或之后
在app的ctor阶段,内存数据库self.db还没有从持久化恢复数据,读到的是datas.yaml里的预置数据(对应的数据表如果没有配置datas.yaml,则表里没有数据)
在app的ctor阶段,内存数据库self.db还没有挂钩,这时候插入/修改数据不会触发持久化
1.2 重启后查到的持久化属性值与重启前查到的不同
此类问题大部分是因为在重启前后两次查询之间,组件业务里对属性值有修改。开发对自己组件的代码逻辑不熟悉,会误以为是持久化机制的问题,可以用以下手段排查:
在libmc4lua的hook.lua和row.lua中增加定位日志打印调用栈。
在修改数据的必经之处将调用栈打印出来,方便找到业务组件中修改属性值的具体代码位置。
需要在libmc4lua仓库中的src/lualib/database/hook.lua和src/lualib/database/row.lua文件中增加定位日志。 注意需要把代码中table_name判断相等的值改为实际需要定位的表名。
1.3 包含usage: CSR属性的表,设置属性值后能持久化,但重启后读不到持久化的值
包含usage: CSR属性的表,只会加载CSR文件中有配置的对象的数据到内存数据库(根据主键值从持久化数据库恢复数据)。 这种表重启后,代码里面从内存数据库读不到持久化的值问题,有三种可能原因: 1)CSR中没有配置该类的对象。-> 排查sr文件配置 2)对象添加失败,导致没有加载到内存数据库。-> 排查组件日志中AddObject failed关键字 3)读内存数据库的代码在on_add_object回调里或回调执行之前。-> 这个时候对象数据还没存入内存数据库。
组件注册的on_add_object回调里面拿到的对象属性已经用持久化数据赋值,在执行完回调之后会存入内存数据库,add_object_complete阶段就可以在内存数据库查到对象的数据。
2 数据库写入量
从一键收集里的mdb_info.log可以看到数据库写入量的统计。统计数据按最近写入的3天,同一日期的写入数据量累计值。
2.1 本地持久化
本地持久化的数据库写入量,一键收集时导出到AppDump/组件名/mdb_info.log文件的local persistence write statistics部分,按表名统计
2.2 远程持久化
远程持久化的数据库写入量,一键收集时导出到AppDump/persistence/mdb_info.log文件的remote persistence table write statistics部分(按表名统计), 以及remote persistence service write statistics部分(按组件名统计)
3 持久化数据库里查不到
需要排查是否属于以下几种不持久化的场景
3.1 在datas.yaml中配置有预置值的属性,业务没有修改过属性值的情况下不会持久化
只有修改过属性值之后才会存进持久化数据库,组件启动时用持久化数据覆盖预置数据。 该机制主要是为了避免大量静态数据存进持久化数据库影响性能和flash寿命。
3.2 在CSR文件中有配置的属性,业务没有修改过属性值的情况下不会持久化
只有修改过属性值之后才会存进持久化数据库,组件启动时用持久化数据覆盖CSR配置数据。 该机制主要是为了避免CSR配置值持久化后导致后续CSR修改不生效。
3.3 通过mdbctl设置的属性值,不会持久化
mdbctl是调试工具,调试时调用setprop或者call命令绕过业务逻辑直接设置的属性值,原则上不持久化
4 数据库打开失败/没有创建
关键日志:open sqlite3 db failed, unable to open database file 需要排查以下问题:
4.1 数据库文件所在的目录不存在
如果传入的数据库文件路径所在的文件夹不存在也会导致打开数据库失败。
用框架机制创建的数据库存放在固定的目录,这些目录在启动脚本中会创建,确保目录存在。这种错误在DT中更可能出现,需要排查测试数据库文件所在目录是否创建
4.2 数据库文件或所在目录的权限问题
组件所在进程没有权限访问已有数据库文件或者文件所在的目录,会导致无法正常打开/创建数据库。需要排查对应的文件/目录权限。 特别需要注意以下几点:
4.2.1 组件所在进程改为用安全沙箱启动,导致打开属主为root的已有数据库失败
数据库文件属主权限不匹配导致。需要在启动脚本中执行chown修改对应数据库文件的属主为secbox。
4.2.2 组件所在进程用安全沙箱启动,secbox.cfg中没有足够的权限配置导致打开数据库失败
需要检查secbox.cfg中的capabilities配置项
最小系统组件的掉电持久化数据在/data/trust/persistence.local,这个目录权限是755,必须在secbox.cfg配置CAP_DAC_OVERRIDE才能正常创建和打开数据库
4.3 数据库写入异常,打印错误日志
如果写入失败,出现数据库相关错误日志(例如constraint fail等),可以参考 《ORM数据库操作说明》中的"异常场景"章节进行定位 以下是几种典型场景:
4.3.1 database is locked 数据库被锁
问题是因为不同地方打开了同一个数据库。 通常是这两种原因:
- 业务操作数据库时,有人为或测试用例用sqlite命令行去操作数据库;
- 代码里面不同地方打开了同一个数据库。
!!!使用同一个打开的句柄,在同一线程操作数据库是不会锁住的
!!!测试发现的此类问题,绝大部分是非问题,建议不断追问测试同事,命令执行记录有没有使用sqlite命令行操作数据库!!!
4.3.2 constraint fail 开启了flash写保护
问题是因为flash写保护开启导致掉电持久化数据库写入被拦截。
flash写保护是否开启是通过文件夹标志/dev/shm/.nowritetoflash判断,DT环境里建议检查这个路径是否存在。
环境上可以使用busctl开关写保护: 开启flash写保护命令
busctl --user set-property bmc.kepler.soctrl /bmc/kepler/Managers/1/SOC/NandFlashCtrl bmc.kepler.Managers.SOC.NandFlashCtrl WriteProtection b true关闭flash写保护命令
busctl --user set-property bmc.kepler.soctrl /bmc/kepler/Managers/1/SOC/NandFlashCtrl bmc.kepler.Managers.SOC.NandFlashCtrl WriteProtection b false4.3.3 UNIQUE constraint failed 违反主键/唯一键的唯一性限制
问题是因为多条数据中主键(PrimaryKey)或者唯一键(UniqueKey)属性的值重复。
案例
1. 配置远程持久化的数据修改后立即AC掉电,数据未落盘
【案例背景】升级CPLD生效之后会删除配置了远程持久化的生效标志,从组件侧观察删除成功,但AC后发现实际没有删除。
【案例分析】修改远程持久化数据时,是先保存到组件侧的内存数据库,然后触发RPC调用,通知持久化服务persistence写入到持久化数据库。从组件侧观察删除成功是内存数据库删除成功了,但真正落盘到持久化需要经历完整的RPC调用,其耗时可能受到以下因素影响:
- CPU占用率。在CPU占用率高的情况下DBus调用耗时会增加,极端场景可能达到30~40s才发送到持久化服务。
- 风暴抑制机制。持久化服务按表为单位,对频繁写入有抑制措施。如果同一个表1分钟内超过30次写入则会触发抑制,按一分钟周期对事务进行合并处理。
【案例结论】对数据落盘即时性要求高的场景,应该配置为本地持久化。
2. 不同组件之间远程持久化表名相同,读写数据时可能相互干扰
【案例背景】nsm组件和event_policy组件都配置了名称为t_snmp_config的表,并且都配置了掉电持久化类型的属性Id,两个组件在读写该表数据时会相互干扰。
【案例分析】组件初始化加载远程持久化数据到内存数据库时,是按表名读取持久化数据,然后再根据组件定义的字段过滤;组件保存远程持久化数据时,是将表名、字段名和字段值作为参数发送给持久化服务进行写入。如果不同组件之间持久化表名相同并且字段名也相同,就会读写到其他组件的数据。
【案例结论】不同组件之间应该避免持久化表名重复(目前manifest门禁已有检查)。
3. 使用db.Table()创建对象,然后给字段赋值并调用save,保存失败,报错UNIQUE constraint failed
【案例背景】bmc_network组件使用db.Table方式创建new_port对象,原意是判断数据库中是否存在满足Id = new_port_id的数据,不存在时给字段赋值然后保存。但实际上在赋值和save的时候都报错UNIQUE constraint failed
【案例分析】
- 不能使用db.Table()得到的对象判断数据是否存在,创建对象相当于
select or insert操作,数据不存在时会插入一条新的数据 - 通过db.Table()创建对象来插入数据时,不应该用一个字段创建对象然后给其他字段赋值。对象创建和字段赋值时都会调用save,导致保存时主键冲突
【案例结论】用select来判断数据是否存在;通过db.Table()创建对象来插入数据时,应该把所有字段值放到一个lua表作为参数
4. 先清空本地持久化表数据,然后逐条插入200+条数据,导致flash写入量过大
【案例背景】 power_strategy组件电源功率统计信息以10分钟为周期,将持久化数据清空再逐条重新插入,出现实际flash写入量(20M)远大于实际数据量(300KB)的情况。 【案例分析】
- 包括sqlite在内的绝大部分数据库使用B+树结构存储数据,每次插入/删除一条数据时,都可能涉及周边数据节点和上级索引节点的调整,具体原理参见此文章
- 批量插入/删除数据时,如果逐条保存,没有使用批量提交的事务机制,则每条数据写入时都会调整一遍B+树结构,频繁的结构调整导致写入量远大于数据量。
【案例结论】 涉及批量逐条插入/删除的本地持久化场景,需要使用框架提供的批量提交接口db:tx(),避免频繁的结构调整导致写入量过大。
【排查指导】 1.判断是否使用本地掉电持久化,若不使用本地掉电持久化,则不涉及; 2.判断是否有批量(>=5条)操作(删除/插入)场景,没有批量操作场景,则不涉及;(常见的有时序类型数据持久化场景,比如周期性的采样数据、统计信息等) 观察&测试写入量方法: 触发本地掉电持久化业务,触发前后通过iostat -k -d 1 1命令查询持久化文件分区写入量信息,对比前后写入量是否超出预计(超过数据库文件大小)
【整改示例】 将插入数据库放在db:tx()的回调函数中执行,参考该示例:
!! 这种整改只对本地持久化有效,远程持久化数据库从设计上不应该有大量数据存储,目前不支持批量事务提交 !! (本地持久化指的是 model.json 中配置了 "tableLocation": "Local")
5. db对象属性是lua表时,修改表内元素后没有将表赋值给属性,导致修改没有持久化
【案例场景】general_hardware组件t_customsize_sign表的StartSlotItems属性是数组类型,在该属性持久化已有值为空数组的情况下,业务代码先把该属性赋值为lua空表,然后往表中添加元素{0, 4},预期调用:save()保存后持久化值变为[[0, 4]],实际上仍为[]。
【案例分析】 db对象通过lua的__newindex元方法捕获属性赋值,然后将赋的值与原有值比对。如果值不同则说明属性有变动,将属性值添加到待保存的self.__new_datas列表中;如果值相同则说明属性无变动,不做处理。 该场景由于StartSlotItems属性原有值为空表,赋值为空表后前后值相同,这个表不会添加到待保存的self.__new_datas列表中;并且修改表内元素后没有再次将表赋值给属性,导致框架无法感知该属性值有变化。
【案例结论】 db对象的复杂类型(数组/结构体/字典)属性作为lua表时,如果修改表内元素,必须在完成修改后将表赋值给属性,否则会导致修改持久化失败。
6. 不同CSR对象主键值冲突,日志打印AddObject failed错误,对象之间数据相互干扰
【案例场景】MDS中配置有表名并且包含CSR属性的类,其中一个属性配置了"primaryKey": true,并且这个属性在CSR中没有配置,或者在多个CSR对象中配置了相同的值,就会出现主键冲突,并且后续不同对象的数据读写会相互干扰。
【案例分析】 自发现对象添加过程中,会把CSR对象的数据保存到内存数据库,后续对这个对象属性的读写都与内存数据库同步。如果不同对象的主键值冲突,它们在内存数据库中就无法共存。
【案例结论】 不同CSR对象之间,配置为主键的属性值不能相同。
【整改指导】
- 如果CSR属性不需要支持数据持久化,建议去掉MDS中的持久化配置(tableName也需要去掉)
- 如果多个CSR对象共用一份持久化数据,建议MDS中用另外一个类配置持久化(只要tableName相同就能继承原有数据)
- 如果多个CSR对象持久化数据需要分开处理:
- 如果冲突的几个CSR对象同时存在是合理的,需要修改主键配置,改为在其它一个或多个属性中配置
"primaryKey": true,并确保不同对象之间的主键值结合是唯一的。涉及到需要继承原有持久化数据的,可以修改MDS类的表名,新增一个类配置tableName为原表名,在组件初始化的时候做一些判断和数据迁移操作。 - 如果冲突的几个CSR对象同时存在是不合理的,则需要修改CSR配置,去掉冗余的对象。
- 如果冲突的几个CSR对象同时存在是合理的,需要修改主键配置,改为在其它一个或多个属性中配置
7. datafs_reset深度还原,导致分区数据清空,包括所有掉电持久化数据
【案例场景】测试环境升级BMC后失联,IP和证书等数据丢失。问题根因是环境升级前设置了datafs_reset标志,触发了深度还原导致分区数据全部丢失。
【案例分析】 分析升级前后日志,关键点如下:
- framework.log中persistence组件在升级重启后打印
database file in the primary partition does not existfailed to recover from backup database说明主备数据库全都不存在。 由于框架代码不存在删除远程持久化数据库的动作,这种情况只可能是:烧片后首次启动、数据库被人为删除、深度还原清空了分区数据 - linux_kernel_log中,在升级触发重启前打印
proc_datafs_reset_file_ops_write,175,user set datafs_reset flag证实触发了深度还原。深度还原的实现机制是先写入1到标志位/proc/sys_info/datafs_flag,重启后这个标志位是1就会触发/data和/data/trust分区数据清空,包括持久化数据库。这行日志就是写入标志位时打印的。 【案例结论】 出现持久化数据全部丢失时,应该在linux_kernel_log日志中搜索datafs_reset关键字,确认是否执行过深度还原
8. 属性值从true改成false,查看持久化数据库已经修改,但执行ACCycle掉电重启后又变成true
【案例场景】在通电开机策略设置为"与之前保持一致"(fructrl组件PowerOnStrategy属性值是LastState)时,ACCycle掉电重启后上电状态需要与掉电前保持一致。掉电前环境处于下电状态,但掉电后变成上电状态,与预期不符。 环境下电时,fructrl组件将掉电持久化属性PwrStateBeforeACLost从true改成false。此时用sqlite命令查看持久化数据库,属性值确认已经修改,但是执行ACCyle后又变回true,导致重启后是上电状态。 【案例分析】 在持久化服务侧,只有一种场景会导致业务持久化数据发生变化:主数据库损坏或者丢失时,从备份数据库恢复。 排除这种场景后,基本上可以确认是组件侧的行为导致数据变化。
查看framework.log中persistence组件启动过程是否存在关键日志
file in the primary partition does not exist, copy it from the backup partition或者file in the primary partition is damaged, copy it from the backup partition。 确认没有这些关键日志后,需要在组件侧定位数据修改的来源。在libmc4lua/src/lualib/database/row.lua中获取调用栈信息,从而定位修改数据的业务组件代码位置。
业务组件如果使用ORM机制持久化,一般是通过设置对象的属性值来修改数据。可以在row.lua中的__newindex元方法中打印调用栈来定位。 加日志时可以用if条件对表名和属性名进行筛选,避免日志刷屏。
- 加的日志没有打印,但持久化数据还是变了,是因为ACCyle时存在日志丢失问题,可以在环境上创建数据库,将时间和调用栈记录到数据库。
从调用栈可以看出,掉电前是fructrl组件set_ACLost函数把属性设置回true导致的问题,这个函数是SetACLost资源树方法的实现。 在libmc4lua/src/lualib/sd_bus/init.lua的shm_call函数中记录调用栈信息,定位到是power_mgmt组件调用了该资源树方法。 【案例结论】 掉电重启后,持久化数据与重启前设置的值不一致时,如果排除了主数据库损坏或者丢失,需要从业务侧排查属性设置操作,可以在libmc4lua代码中记录调用栈信息来辅助定位。
常见问题
1. 集成测试报错insert or update failed: attempt to write a readonly datebase
【问题原因】前面有用例失败导致集成测试退出,退出时调用clear_test_data把持久化数据库在内的测试目录删了,所以报错数据库写失败 【解决方式】需要往上查看集成测试日志,找到失败的用例,定位具体用例失败原因
2. mdbctl设置持久化属性值后,持久化数据库没有修改
【问题原因】mdbctl的setprop和call功能不支持持久化 【解决方式】使用其它方式修改属性值
3. 集成测试报错insert or update failed: datebase is locked, table_name: t_master_key
【问题原因】集成测试环境变量需要配置持久化备份数据库路径,否则主备是用同一个数据库就会冲突互锁 【解决方式】在集成测试的.conf文件中增加配置 POWEROFF_BAK_PATH = TEST_DATA_DIR .. 'backup'
4. 组件打印错误日志construct object falied: key conflict, XXX
【问题原因】ORM对象创建时,对象名冲突,或者对象主键值、唯一键值与数据库已有数据冲突。 【解决方式】排查对象名冲突、主键值冲突、唯一键值冲突。自发现对象可以在before_add_ojbect回调函数中设置对象的属性值
5. 组件打印错误日志database process commit list failed, attempt to call a nil value (field 'create_mdb_object')
【问题原因】组件使用了ORM机制,成功插入/更新到数据库后,ORM机制会调用组件实现的create_mdb_object创建一个mdb对象,而组件如果没有实现create_mdb_object就会报这个错。这个报错不是数据库写失败,是在已经写成功之后创建对象失败。 【解决方式】补充实现create_mdb_object
FAQ
1. 对象卸载的时候会不会删除数据?
开启了ORM机制后,自发现对象卸载的时候会自动删除对象的数据。 ORM机制会调用mc.mdb.object_manage的on_delete_object注册对象卸载回调,在回调中调用ORM对象的析构函数,从数据库中删除该对象的数据。
已知历史问题
1. 快速删除、保存、插入导致数据错乱
由于数据库数据有两份,内存对象数据和数据库数据,所以做了同步钩子,同步双方数据,由于同步过程是异步处理,所以可能出现以下场景:
- 场景1
- 业务的想法:1. 增加数据A,删除数据A
- 实际的流程:增加新数据A(同步到内存对象),删除数据A(在同步前就删除了),同步到内存对象后创建了新的内存对象,然后新的内存对象又同步增加了数据A,结果实际上数据库中残留了数据A。
- 场景2
- 业务的想法:1. 删除数据A 2. 增加数据B
- 实际的流程:删除数据A(同步删除内存对象),增加数据B(在同步前增加了),之后删除数据A,可能就把新的内存对象给删除了
该问题已修复
2. 用给对象属性赋值的方式保存持久化数据,概率性出现由于sync_db_objs被垃圾回收导致的持久化失败的问题
业务组件可以直接给db对象的属性赋值,框架通过db对象的__newindex元方法触发on_need_save信号,ORM层通过这个信号把db对象加入到sync_db_objs表中进行批量保存。但是由于sync_db_objs表设置了弱引用__mode = 'kv',从对象加入之后到执行批量保存这个时间窗口可能被垃圾回收,所以概率性会出现持久化流程中断(概率性因为跟垃圾回收的节奏有关)。
该问题已修复
- lua弱引用表(
__mode = 'kv'或__mode = 'k'或__mode = 'v')作为键/值保存在表中的变量不会增加其引用计数,当变量在表外没有被引用时会被垃圾回收。
【反例】
-- 弱引用表sync_db_objs用来存放需要持久化的db对象
local sync_db_objs = setmetatable({}, {__mode = 'kv'})
table.on_need_save:on(function(obj)
-- 将需要持久化的obj添加到待持久化的对象列表sync_db_objs中
sync_db_objs[obj] = context.get_context() or context.new()
-- 由于sync_db_objs是弱引用表,离开当前函数作用域之后obj可能被垃圾回收
end)
local function save_all_objs()
for obj, ctx in pairs(sync_db_objs) do
-- 遍历sync_db_objs进行持久化时,表中对象可能已经被垃圾回收,造成数据丢失
obj:save()
end
end【正例】
-- 弱引用表g_timer_pool的作用是提供缓存池,避免频繁重复创建定时器
local g_timer_pool = setmetatable({}, {__mode = 'kv'})
local function new_timer(interval, cb)
local timer = table.remove(g_timer_pool)
if not timer then
-- 表中定时器已被垃圾回收时可以重新创建,不影响业务功能
local raw, id = new_dbus_timer(interval)
timer = {raw = raw, cb = cb, id = id}
else
-- 表中定时器没有被垃圾回收时可以重复利用,避免频繁创建新定时器的消耗
timer.cb = cb
timer.id = timer.raw:restart(interval)
end
g_dbus_timers[timer.id] = timer
return setmetatable(timer, c_timer)
end3. 多持久化类型表恢复数据失败问题
问题背景:bios组件t_smbios_info表包含掉电、复位两种持久化类型的属性。主键Id是掉电持久化。内存数据库已有datas.yaml预置数据。修改复位持久化属性SmBiosStatus后,重启BMC,修改的属性值没有恢复到内存数据库。
问题根因:修改复位持久化属性时,主键值只会写到复位持久化数据库。persistence按sensitive_data.json中属性对应的持久化类型对读取的数据进行筛选,Id被筛选掉了。
解决方法:构建时生成的sensitive_data.json中增加每个表的主键信息persistence筛选读到的数据时,只要是主键值都不丢弃
该问题只存在于persistence/1.70.5和1.70.6版本,后续版本已修复