Skip to content

[中文] Tianmu Performance Schema Tables For 2.0

hustjieke edited this page Sep 22, 2022 · 7 revisions

StoneDB 2.0 版本中,将启用次引擎.在 StoneDB 1.0 版本中,只支持基于磁盘的列存引擎 Tianmu。同 InnoDB 角色一样,可以作为 MySQL 的另外一个主引擎。在 StoneDB 2.0 版本中,我们将引入 MySQL 8.0.2 中的新特性:次引擎。

为什么我们要用次引擎?

在我们开始介绍 Tianmu 内存引擎之前,我们先基于这个话题作一些讨论。现在已经有一些解决方案,比如在 1.0 版本使用双主引擎。

  • 首先,次引擎是一个 MySQL 框架,用于提供多引擎的能力。基于通用的接口和框架,MySQL 可以根据每个工作负载的类型将不同的工作负载路由到相应的引擎,发挥其优势以提供优质的服务。次引擎也可以使 MySQL 拥有增强多模能力的机会,例如让 ClickHouse 成为一个次引擎来提供分析服务。
  • 其次,逻辑上讲,将子任务路由到次引擎,主任务放到主引擎去执行,是很自然的一个想法。
  • 最后,众所周知,Oracle 已经将次引擎的特性用在了它们的在线服务中。MySQL HeatWave: 一个内置机器学习的内存查询加速器。

HeatWave, 对当前应用不做任何改变,就能将分析和混合负载的性能提高几个数量级。启用 HeatWave 后,MySQL HeatWave 比 Amazon Redshift 快 6.5 倍,成本只有一半,比 Snowflake 快 7 倍,成本只有五分之一,比 Amazon Aurora 快 1,400 倍,成本只有一半。客户对存储在 MySQL 数据库中的数据进行分析,无需单独的分析数据库和 ETL 复制。 https://www.oracle.com/mysql/heatwave/

为什么我们要使用基于内存的列式引擎?

在给出这个答案之前,我们先讨论当前分析处理系统存在的一些挑战。 传统上,要获得一个好的分析查询性能意味着要满足几个要求。在一个典型的数仓或者各种类型的数据库,要求如下:

  • 你必须了解用户的访问模式
  • 您必须提供良好的性能,通常需要创建索引、物化视图和 OLAP 多维数据集。 lQLPJxagcNi-_obNA3bNBy2w93CZnjQkZLsDB-66EECHAA_1837_886 为了提升性能,StoneDB 在 1.0 的版本中使用了基于列的数据格式。列式数据格式以列的形式组织数据,而不是行。例如,在一个大的sales表,sales ID 位于一列中,sales regions位于另一列中。

image

分析工作负载通常在扫描时访问少数列,但扫描操作会影响整个数据集。基于这个原因,对分析工作负载来说列格式是最有效的。因为,正如基于列的格式所描述的那样,列是单独存储的,分析查询只能访问需要的列,并避免读取无关紧要的数据。例如,一份按地区划分的销售总额报告可以快速处理许多行,却只需要访问很少的列。

行存和列存没有孰优孰劣之分,在不同的应用场景中会有不同的表现,具体为"增改查"的区别。

数据库系统通常会强制用户在基于列格式和基于行格式之间进行选择。例如,如果数据格式是基于列的,那么数据库在内存和磁盘上都以基于列的格式存储数据。获得一种格式的优势意味着失去另一种格式的优势。

  • 行存的优势在于写入和修改的效率很高,同时对于每行数据的完整性上具有天生的优势,适用于面向事务型处理的应用场景。
  • 列存的优势在于读取的效率很高,并且可以使用高效的压缩算法来节省储存空间,适用于查询密集型的应用场景

因此,应用程序要么实现快速分析,要么实现快速交易,但不能同时实现两者。混合工作负载数据库的性能问题并不能通过仅以一种单一格式存储数据来解决。

基于我们上面讨论的内容,在 2.0 版本中,我们尝试引入一个新的数据格式引擎,即基于内存列的存储,用于分析工作负载。内存中的基于列的引擎也称Tianmu

Tianmu 内存列式引擎概述

在#436 中,给出了基于内存列的引擎的一些简要描述。数据在加载到基于列的内存引擎之前被压缩和编码。并非所有类型的数据都适合编码和压缩,在 #423 中,我们定义了可以编码和压缩的数据类型。

在基于内存列的引擎中,它以压缩列格式保存表、分区或列的副本,该格式针对扫描操作进行了优化。

NUMA & MySQL

Linux kerner: What is NUMA?

NUMA 的内存分配策略有 localalloc、preferred、membind、interleave。

localalloc: 进程从当前 node 上请求 alloc memory;

preferred: 指定一个推荐 node 来获取内存,如果被推荐的 node 上没有足够内存,可以尝试别的 node

membind: 可以指定若干个 node,进程只能从这些指定的 node 上请求分配内存

interleave: 进程从指定的若干个 node 上以 Round Robin (轮询调度)算法请求分配内存。

Next picture is from wikipedia image

MySQL 在 NUMA 架构上会出现的问题,ref from Jeremy Cole

https://blog.jcole.us/2010/09/28/mysql-swap-insanity-and-the-numa-architecture/

https://blog.jcole.us/2012/04/16/a-brief-update-on-numa-and-mysql/

这两篇文章的结论都推荐使用 NUMA 内存分配 policy 是numactl --interleave=all

MySQL 数据库外部请求随机性强,各个线程访问内存在地址上平均分布,Interleave 的内存分配模式相较默认模式可以带来一定程度的性能提升

MySQL Buffer Pool

在 mysql 中,InnoDB 缓冲池是数 GB 的范围,内存分布在不同的 NUMA 节点中。而且,cross-NUMA访问是多核系统的性能瓶颈。因此,NUMA 节点中的内存分配算法(或策略)应谨慎选择。在 innobase/buf/buf0buf.cc 中,它使用 buf_block_alloc 函数来分配一个缓冲块,并传播到所有 buffer pool 实例。

buf_block_t *buf_block_alloc(
    buf_pool_t *buf_pool) /*!< in/out: buffer pool instance,
                          or NULL for round-robin selection
                          of the buffer pool */
{
  buf_block_t *block;
  ulint index;
  static ulint buf_pool_index;

  if (buf_pool == nullptr) {
    /* We are allocating memory from any buffer pool, ensure
    we spread the grace on all buffer pool instances. */
    index = buf_pool_index++ % srv_buf_pool_instances;
    buf_pool = buf_pool_from_array(index);
  }

  block = buf_LRU_get_free_block(buf_pool);

  buf_block_set_state(block, BUF_BLOCK_MEMORY);

  return (block);
}

Refer to NUMA memory allocation policies in RedHat.

在 MySQL 中,我们建议在 NUMA 架构中使用 interleave 内存分配策略。结构set_numa_interleave_t 用于将内存分配策略设置为MPOL_INTERLEAVE

struct set_numa_interleave_t {
  set_numa_interleave_t() {
    if (srv_numa_interleave) {
      ib::info(ER_IB_MSG_47) << "Setting NUMA memory policy to"
                                " MPOL_INTERLEAVE";
      struct bitmask *numa_nodes = numa_get_mems_allowed();
      if (set_mempolicy(MPOL_INTERLEAVE, numa_nodes->maskp, numa_nodes->size) !=
          0) {
        ib::warn(ER_IB_MSG_48) << "Failed to set NUMA memory"
                                  " policy to MPOL_INTERLEAVE: "
                               << strerror(errno);
      }
      numa_bitmask_free(numa_nodes);
    }
  }

  ~set_numa_interleave_t() {
    if (srv_numa_interleave) {
      ib::info(ER_IB_MSG_49) << "Setting NUMA memory policy to"
                                " MPOL_DEFAULT";
      if (set_mempolicy(MPOL_DEFAULT, nullptr, 0) != 0) {
        ib::warn(ER_IB_MSG_50) << "Failed to set NUMA memory"
                                  " policy to MPOL_DEFAULT: "
                               << strerror(errno);
      }
    }
  }
};

一般情况下,在 MySQL 中,会创建多个缓冲池实例,由innodb_buffer_pool_instances控制。缓冲池实例的数量应根据缓冲池的大小进行调整。

对于具有数 GB buffer pool 的系统,将 buffer pool 划分为单独的一个个实例,通过减少不同线程读取和写入缓存页面时的争用来提高并发性。

并且,MySQL 缓冲池由 buffer blockscontrol blocksindex page, data page, undo page, insert buffer, AHI(adaptive hash index), lock information, data dictionary等组成。

缓冲池大小由 innodb_buffer_pool_size 设置。每个缓冲池实例的大小需要满足:

 size_of_an_instance = innodb_buffer_pool_size / innodb_buffer_pool_instances

storage/innobase/buf/buf0buf.cc 中,buf_pool_init 函数在 MySQL 启动时创建缓冲池。

/** Creates the buffer pool.
@param[in]  total_size    Size of the total pool in bytes.
@param[in]  n_instances   Number of buffer pool instances to create.
@return DB_SUCCESS if success, DB_ERROR if not enough memory or error */
dberr_t buf_pool_init(ulint total_size, ulint n_instances) {
  ulint i;
  const ulint size = total_size / n_instances;
  ...
  NUMA_MEMPOLICY_INTERLEAVE_IN_SCOPE;

  /* Usually buf_pool_should_madvise is protected by buf_pool_t::chunk_mutex-es,
  but at this point in time there is no buf_pool_t instances yet, and no risk of
  race condition with sys_var modifications or buffer pool resizing because we
  have just started initializing the buffer pool.*/
  buf_pool_should_madvise = innobase_should_madvise_buf_pool();

  buf_pool_resizing = false;

  buf_pool_ptr =
      (buf_pool_t *)ut_zalloc_nokey(n_instances * sizeof *buf_pool_ptr);

  buf_chunk_map_reg = UT_NEW_NOKEY(buf_pool_chunk_map_t());

  std::vector<dberr_t> errs;

  errs.assign(n_instances, DB_SUCCESS);

#ifdef UNIV_LINUX
  ulint n_cores = sysconf(_SC_NPROCESSORS_ONLN);

  /* Magic nuber 8 is from empirical testing on a
  4 socket x 10 Cores x 2 HT host. 128G / 16 instances
  takes about 4 secs, compared to 10 secs without this
  optimisation.. */

  if (n_cores > 8) {
    n_cores = 8;
  }
#else
  ulint n_cores = 4;
#endif /* UNIV_LINUX */

  dberr_t err = DB_SUCCESS;

  for (i = 0; i < n_instances; /* no op */) { //initialize every instance, using multi-thread.
    ulint n = i + n_cores;

    if (n > n_instances) {
      n = n_instances;
    }

    std::vector<std::thread> threads;

    std::mutex m;

    for (ulint id = i; id < n; ++id) { // create threads to do initialization concurrently.
      threads.emplace_back(std::thread(buf_pool_create, &buf_pool_ptr[id], size,
                                       id, &m, std::ref(errs[id])));
    }

    ...

    /* Do the next block of instances */
    i = n;
  }

  buf_pool_set_sizes();
  buf_LRU_old_ratio_update(100 * 3 / 8, FALSE);

  btr_search_sys_create(buf_pool_get_curr_size() / sizeof(void *) / 64);

  buf_stat_per_index =
      UT_NEW(buf_stat_per_index_t(), mem_key_buf_stat_per_index_t);

  return (DB_SUCCESS);
}

image

缓冲池的其他操作函数可以在 storage/innobase/buf/buf0buf.cc 这个文件中找到。

数据加载

在stoneDB 2.0 版本中,所有数据都存储在内存中。关于内存我们在上面已经讨论过了。现在,我们深入了解细节。

与 StoneDB 1.0 版本一样,表的所有数据都以列格式单独存储。一列,一份文件。与 on-disk 不同,内存中数据的组织方式会略有变化。 列数据子池将被划分为 N 个分区(称为 In-memory column-based unit,IMCU),N 取决于加载表的大小。这意味着表的所有数据都将作为分区加载到内存中。如#423 所述,如果一个列被定义为 TIANM_COLUMN,这意味着该列将被加载到 StoneDB 2.0 版本的列数据池中。因此,分区是数据加载的地方。每个分区存储一个数据库对象(表)。例如,Tianmu 加载了两张表table_a和table_b,table_a的数据只存储在partion1中,table_b的数据存储在partion2中。 table_a 的数据和 table_a 的数据不会混合存储在同一个分区中。

内存具有复杂的内存管理机制,例如内存分配、内存释放、内存对齐等,甚至是跨 NUMA 访问问题,因此我们必须仔细设计数据组织。否则,性能会导致性能下降。

每个 Partition 包含多个内存中基于列的数据包(IMCDP 或缩写为 Chunk)。每个块包含 65536 行数据。

image