单条SQL执行的内存使用问题

监控(prometheus、granfana)中内存是整个实例的内存使用,是实际使用的内存包括还未GC的对象,是golang语言层面提供的统计数据,是准确的内存开销。

慢日志、information_schema.processlist中内存使用是tidb根据记录数等进行统计评估的只能做到“比较准确”,存在的问题是:1、统计的一些记录(如chunk)内存占用不一定很准,通常来说比较准确;2、可能存在一些内存开销没有统计进来的情况,但是更新的版本内存统计更全面;3、“销毁”的对象是没有办法统计进来的,但是在gogc未发生前还是会占用内存的。

SQL内存占用的计算逻辑(show processlist或者慢日志等看到的)是比较复杂的,主要是通过内存追踪的方式统计的:

什么是内存追踪

在tidb-server中引入了一套内存追踪框架,用于统计每一个连接中语句执行过程中内存使用情况,以方便当语句占用内存过大时触发oom-action的行为,保护实例整体资源可用。

内存追踪的最小单元是一个tracker,将其插入到语句执行的各个阶段,根据语句->算子->算子内数据处理形成树形层次,每一次数据处理产生的内存使用都会统计到tracker中,并把内存使用情况累加到parent tracker中,最终汇总到root tracker,因此就形成了一颗内存追踪树。

这里的root tracker获取到的内存就是你从慢日志中看到的语句内存占用大小。

内存追踪的实现

对于内存追踪最核心的结构体是Tracker

type Tracker struct {
   bytesLimit           atomic.Value
   actionMuForHardLimit actionMu
   actionMuForSoftLimit actionMu
   mu                   struct {
      // The children memory trackers. If the Tracker is the Global Tracker, like executor.GlobalDiskUsageTracker,
      // we wouldn't maintain its children in order to avoiding mutex contention.
      children map[int][]*Tracker
      sync.Mutex
   }
   parMu struct {
      parent *Tracker // The parent memory tracker.
      sync.Mutex
   }
   label int // Label of this "Tracker".
   // following fields are used with atomic operations, so make them 64-byte aligned.
   bytesConsumed       int64            // Consumed bytes.
   bytesReleased       int64            // Released bytes.
   maxConsumed         atomicutil.Int64 // max number of bytes consumed during execution.
   SessionID           uint64           // SessionID indicates the sessionID the tracker is bound.
   NeedKill            atomic.Bool      // NeedKill indicates whether this session need kill because OOM
   NeedKillReceived    sync.Once
   IsRootTrackerOfSess bool // IsRootTrackerOfSess indicates whether this tracker is bound for session
   isGlobal            bool // isGlobal indicates whether this tracker is global tracker
}

其中比较重要的属性:

actionMuForHardLimit:硬限制动作链实现了ActionOnExceed接口,当内存使用bytesConsumed达到了硬限制上限后触发的oom-action行为,硬限制上限即为tidb_mem_quota_query参数设置值。

actionMuForSoftLimit:软限制动作链实现了ActionOnExceed接口,当内存使用bytesConsumed当达到了软限制上限后触发的oom-action行为,软限制上限即为0.8 * tidb_mem_quota_query参数设置值,目前软限制动作只有hashAgg算子落盘行为,其它均为硬限制动作。

bytesConsumed:当前tracker追踪的内存大小,并汇报给父节点,根节点包含了整个语句使用的内存大小,当大于语句级内存控制变量tidb_mem_quota_query时触发oom-action行为。

maxConsumed:曾经使用过的最大内存,主要用于processlist显示当前算子的最大内存消耗。

mu:记录父节点的tracker,用于形成追踪树。

该结构体最重要的一个方法是Consume(bs int64),用于将内存消耗bs加到bytesConsumed中。当bs为正数时说明正在消耗内存,比如从磁盘中读取chunk数据到内存就增加内存占用,当bs为负数时候说明正在释放内存,比如从内存中写入磁盘临时文件。其核心逻辑如下:

  1. 通过当前的tracker递归调用getParent()方法,将当前的内存消耗值tracker.bytesConsumed累加到父节点上。

  2. 对于层级中的每一个tracker判断其经过子节点累加后的tracker.bytesConsumed是否大于tracker.maxConsumed,如果大于,那么将tracker.maxConsumed设置为tracker.bytesConsumed。

  3. 循环结束后找到rootTracker(当前会话的顶层tracker),如果rootTracker的bytesConsumed比硬限制设置大,则记录找到rootExceed(=rootTracker),如果rootTracker的bytesConsumed比软限制设置大,则记录找到rootExceedForSoftLimit(=rootTracker)。

  4. 如果当前实例内存占用超过tidb_server_memory_limit,那么将占用内存最多的一条语句杀掉。这里会判断当前的rootTracker是否已经被系统标记为被执行对象,如果是则触发Cancel动作。

  5. 如果存在rootExceed,说明语句内存超过限制值,则触发硬限制的oom-action行为。

  6. 如果存在rootExceedForSoftLimit,说明语句内存超过限制值*0.8,则触发软限制的oom-action行为。

因此通过不停的调用Consume()方法对当前消耗的内存进行记录,对算子、语句内存消耗进行统计并触发oom-action行为。

3 个赞