数据库的内存治理是数据库内核设计中的重要模块,内存的可度量、可管控是数据库稳定性的重要保障。同样的,内存治理对图数据库 NebulaGraph 也至关重要。
图数据库的多度关联查问个性,往往使图数据库执行层对内存的需求量微小。本文次要介绍 NebulaGraph v3.4 版本中引入的新个性 Memory Tracker,心愿通过 Memory Tracker 模块的引入,实现细粒度的内存使用量管控,升高 graphd 和 storaged 产生被零碎 OOM kill 的危险,晋升 NebulaGraph 图数据库的内核稳定性。
注:为了同代码放弃对应,本文局部用词间接应用了英文,e.g. reserve 内存 quota。
可用内存
在进行 Memory Tracker 的介绍之前,这里先介绍下相干的背景常识:可用内存。
过程可用内存
在这里,咱们简略介绍下各个模式下,零碎是如何判断可用内存的。
物理机模式
数据库内核会读取系统目录 /proc/meminfo
,来确定以后环境的理论内存和残余内存,Memory Tracker 将“理论物理内存”作为“过程能够应用的最大内存”;
容器 /cgroup 模式
在 nebula-graphd.conf
文件中有一个配置项 FLAG_containerized
用来判断是否数据库跑在容器上。将 FLAG_containerized
(默认为 false)设置为 true 之后,内核会读取相干 cgroup path 下的文件,确定以后过程能够应用多少内存;cgroup 有 v1、v2 两个版本,这里以 v2 为例;
FLAG | 默认值 | 解释 |
---|---|---|
FLAG_cgroup_v2_memory_max_path | /sys/fs/cgroup/memory.max | 通过读取门路确定最大内存使用量 |
FLAG_cgroup_v2_memory_current_path | /sys/fs/cgroup/memory.current | 通过读取门路确定以后内存使用量 |
举个例子,在单台机器上别离管制 graphd 和 storaged 的内存额度。你能够通过以下步骤:
step1:设置 FLAG_containerized=true
;
step2:创立 /sys/fs/cgroup/graphd/
,/sys/fs/cgroup/storaged/
,并配置各自目录下的 memory.max
;
step3:在 etc/nebula-graphd.conf
,etc/nebula-storaged.conf
增加相干配置
--containerized=true
--cgroup_v2_controllers=/sys/fs/cgroup/graphd/cgroup.controllers
--cgroup_v2_memory_stat_path=/sys/fs/cgroup/graphd/memory.stat
--cgroup_v2_memory_max_path=/sys/fs/cgroup/graphd/memory.max
--cgroup_v2_memory_current_path=/sys/fs/cgroup/graphd/memory.current
Memory Tracker 可用内存
在获取“过程可用内存”当前,零碎须要将其换算成 Memory Tracker 可 track 的内存,“过程可用内存”与“Memory Tracker 可用内存”有一个换算公式;
memtracker_limit = (total – FLAGS_memory_tracker_untracked_reserved_memory_mb) * FLAGS_memory_tracker_limit_ratio
FLAG | 默认值 | 解释 | 反对动静改 |
---|---|---|---|
memory_tracker_untracked_reserved_memory_mb | 50 M | Memory Tracker 会治理通过 new/delete 申请的内存,但过程除了通过此种形式申请内存外,还可能存在其余形式占用的内存;比方通过调用底层的 malloc/free 申请,这些内存通过此 flag 管制,在计算时会扣除此局部未被 track 的内存。 | Yes |
memory_tracker_limit_ratio | 0.8 | 指定 Memory Tracker 能够应用的内存比例,在一些场景,咱们可能须要调小来避免 OOM。 | Yes |
这里来具体开展说下 memory_tracker_limit_ratio
的应用:
- 在混合部署环境中,存在多个 graphd 或 storaged 混合部署是须要调小。比方 graphd 只占用 50% 内存,则需在 nebula-graphd.conf 中将其手动改成 0.5;
-
取值范畴:
memory_tracker_limit_ratio
除了(0,1]
取值范畴外,还额定定义了两个非凡值:2
:通过数据库内核感知以后零碎运行环境的可用内存,动静调整可用内存。因为此种形式非实时,有肯定的概率会感知不精准;3
:limit 将被设成一个极大值,起到敞开 Memory Tracker 的成果;
Memory Tracker 的设计与实现计划
上面,讲下 Memory Tracker 的设计与实现。整体的 Memory Tracker 设计,蕴含 Global new/delete operator、MemoryStats、system malloc、Limiter 等几个子模块。这个局部着重介绍下 Global new/delete operator 和 MemoryStats 模块。
Global new/delete operator
Memory Tracker 通过 overload 全局 new/delete operator,接管内存的申请和开释,从而做到在进行真正的内存调配之前,进行内存额度调配的治理。这个过程合成为两个步骤:
- 第一步:通过 MemoryStats 进行内存申请的汇报;
- 第二步:调用 jemalloc 产生真正的内存调配行为;
jemalloc:Memory Tracker 不扭转底层的 malloc 机制,依然应用 jemalloc 进行内存的申请和开释;
MemoryStats
全局的内存应用状况统计,通过 GlobalMemoryStats 和 ThreadMemoryStats 别离对全局内存和线程外部内存进行治理;
ThreadMemoryStats
thread_local
变量,执行引擎线程在各自的 ThreadMemoryStats 中保护线程的 MemoryStats,包含“内存 Reservation 信息”和“是否容许抛异样的 throwOnMemoryExceeded”;
- Reservation
每个线程 reserve 了 1 MB 的内存 quota,从而防止频繁地向 GlobalMemoryStats 索要额度。不论是申请还是返还时,ThreadMemoryStats 都会以一个较大的内存块作为与全局替换的单位。
alloc:在本地 reserved 1 MB 内存用完了,才问全局要下一个 1 MB。通过此种形式来尽可能升高向全局 quota 申请内存的频率;
dealloc:返还的内存先加到线程的 reserved 中,当 reserve quota 超过 1 MB 时,还掉 1 MB,剩下的本人留着;
// Memory stats for each thread.
struct ThreadMemoryStats {ThreadMemoryStats();
~ThreadMemoryStats();
// reserved bytes size in current thread
int64_t reserved;
bool throwOnMemoryExceeded{false};
};
- throwOnMemoryExceeded
线程在遇到超过内存额度时,是否 throw 异样。只有在设置 throwOnMemoryExceeded
为 true 时,才会 throw std::bad_alloc
。须要敞开 throw std::bad_alloc
场景见 Catch std::bac_alloc
章节。
GlobalMemoryStats
全局内存额度,保护了 limit 和 used 变量。
- limit:通过运行环境和配置信息,换算失去 Memory Tracker 可治理的最大内存。limit 同 Limiter 模块的作用,具体内存换算见上文“Memory Tracker 可用内存”章节;
- used:原子变量,汇总所有线程汇报上来的已应用内存(包含线程 reserved 的局部)。如果 used + try_to_alloc > limit,且在
throwOnMemoryExceeded
为 true 时,则会抛异样std::bac_alloc
。
Catch std::bac_alloc
因为 Memory Tracker overload new/delete 会影响所有线程,包含三方线程。此时,throw bad_alloc
在一些第三方线程可能呈现非预期行为。为了杜绝此类问题产生,咱们采纳在代码门路上被动开启内存检测,抉择在算子、RPC 等模块被动开启内存检测;
算子的内存检测
在 graph/storage 的各个算子中,增加 try...catch
(在以后线程进行计算 / 分配内存) 和 thenError
(通过 folly::Executor
异步提交的计算工作 ),感知 Memory Tracker 抛出 std::bac_alloc
。数据库再通过 Status 返回错误码,使查问失败;
在进行一些内存调试时,可通过关上 nebula-graphd.conf
文件中的 FLAGS_memory_tracker_detail_log
配置项,并调小 memory_tracker_detail_log_interval_ms
察看查问前后的内存应用状况;
folly::future 异步执行
thenValue([this](StorageRpcResponse<GetNeighborsResponse>&& resp) {
memory::MemoryCheckGuard guard;
// memory tracker turned on code scope
return handleResponse(resp);
})
.thenError(folly::tag_t<std::bad_alloc>{},
[](const std::bad_alloc&) {// handle memory exceed})
同步执行
memory::MemoryCheckGuard guard; \
try {// ...} catch (std::bad_alloc & e) { \
// handle memory exceed
}
RPC 的内存检测
RPC 次要解决 Request/Response 对象的序列化 / 反序列化的内存额度管制问题,因为 storaged reponse 返回的数据均封装在 DataSet 数据结构中,所以问题转化为:DataSet 的序列化、反序列化过程中的内存检测。
序列化 :DataSet 的对象结构在 NebulaGraph 算子返回后果逻辑中,默认状况下,曾经开启内存检测;
反序列化 :通过 MemoryCheckGuard
显式开启,在 StorageClientBase::getResponse's onError
可捕捉异样;
错误码
为了便于分辨哪个模块产生问题,NebulaGraph 中还增加了相干错误码,别离示意 graphd 和 storaged 产生 memory exceeded 异样:
E_GRAPH_MEMORY_EXCEEDED = -2600, // Graph memory exceeded
E_STORAGE_MEMORY_EXCEEDED = -3600, // Storage memory exceeded
延长浏览
- 什么是 malloc 以及动态内存调配:https://en.wikipedia.org/wiki/C_dynamic_memory_allocation
-
jemalloc
- 原始论文:https://www.bsdcan.org/2006/papers/jemalloc.pdf
- Facebook 对 jemalloc 的优化:https://engineering.fb.com/2011/01/03/core-data/scalable-memory-allocation-using-jemalloc/
谢谢你读完本文 (///▽///)