暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

openGauss数据库源码解析系列文章——openGauss简介(下)

Gauss松鼠会 2021-03-26
1227

Gauss松鼠会
学习 探索 分享数据库前沿知识 共建数据库技术交流圈
关注
上一篇openGauss数据库源码解析系列文章--openGauss简介(上),从openGauss概述、应用场景、系统结构、代码结构四个方面对openGauss进行了初步介绍。其中,openGauss的代码结构介绍了数据库系统通信管理、SQL引擎两方面内容,本篇接着从代码结构第三方面的内容——存储引擎以及openGauss的价值特性方面展开介绍。
(三)存储引擎

openGauss存储引擎是可插拔、自组装的,支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。

早期计算机程序通过文件系统管理数据,到了20世纪60年代这种方式就开始不能满足数据管理要求了,用户逐渐对数据并发写入的完整性、高效检索提出更高的要求。由于机械磁盘的随机读写性能问题,从20世纪80年代开始,大多数数据库一直在围绕着减少随机读写磁盘进行设计。主要思路是把对数据页面的随机写盘转化为对WAL(Write Ahead Log,预写式日志)日志的顺序写盘,WAL日志持久化完成,事务就算提交成功,数据页面异步刷盘。但是随着内存容量变大、保电内存、非易失性内存的发展,以及SSD技术逐渐的成熟,IO性能极大提高,经历了几十年发展的存储引擎需要调整架构来发挥SSD的性能和充分利用大内存计算的优势。随着互联网、移动互联网的发展,数据量剧增,业务场景多样化,一套固定不变的存储引擎不可能满足所有应用场景的诉求。因此现在的DBMS需要设计支持多种存储引擎,根据业务场景来选择合适的存储模型。

1. 数据库存储引擎要解决的问题

(1)存储的数据必须要保证ACID:原子性、一致性、隔离性、持久性。

(2)高并发读写,高性能。

(3)数据高效存储和检索能力。

2. openGauss存储引擎概述

openGauss整个系统设计支持多个存储引擎来满足不同场景的业务诉求。当前openGauss存储引擎有以下3种:

(1)行存储引擎。主要面向OLTP(Online Transaction Processing,在线交易处理)场景设计,例如订货发货,银行交易系统。

(2)列存储引擎。主要面向OLAP(Online Analytical Processing,联机分析处理)场景设计,例如数据统计报表分析。

(3)内存引擎。主要面向极致性能场景设计,例如银行风控场景。

创建表的时候可以指定行存储引擎、列存引擎的表、内存引擎的表,支持一个事务里包含对3种引擎表的DML(Data Manipulation Language,数据操作语言)操作,可以保证事务ACID性质。

1) storage源码组织

storage源码目录为:

/src/gausskernel/storage。storage源码文件如表1所示。

表1 storage源码文件

storage

access

基础行存储引擎方法

cbtree

hash

heap

index

...

buffer

缓冲区

freespace

空闲空间管理

ipc

进程内交互

large_object

大对象处理

remote

远程读

replication

复制备份

smgr

存储管理

cmgr

公共缓存方法

cstore

列存储引擎

dfs

分布式文件系统

file

文件类

lmgr

锁管理

mot

内存引擎

page

数据页

2) storage主流程

storage主流程代码如下。

    /* smgr/smgr.cpp, 存储管理 */
    ...
    /* 文件管理函数列表,包含磁盘初始化、开关、同步等操作函数 */
    static const f_smgr g_smgrsw[] = {
    /* 磁盘*/
    {mdinit,
    NULL,
    mdclose,
    mdcreate,
    mdexists,
    mdunlink,
    mdextend,
    mdprefetch,
    mdread,
    mdwrite,
    mdwriteback,
    mdnblocks,
    mdtruncate,
    mdimmedsync,
    mdpreckpt,
    mdsync,
    mdpostckpt,
    mdasyncread,
    mdasyncwrite}};
    /*
    * 存储管理初始化
    * 当服务器后端启动时调用
    */
    void smgrinit(void)
    {
    int i;
    /* 初始化所有存储相关管理器 */
    for (i = 0; i < SMGRSW_LENGTH; i++) {
    if (g_smgrsw[i].smgr_init) {
    (*(g_smgrsw[i].smgr_init))();
    }
    }

    /* 登记存储管理终止程序 */
    if (!IS_THREAD_POOL_SESSION) {
    on_proc_exit(smgrshutdown, 0);
    }
    }
    /*
    * 当后端服务关闭时,执行存储管理关闭代码
    */
    static void smgrshutdown(int code, Datum arg)
    {
    int i;
    /* 关闭所有存储关联服务 */
    for (i = 0; i < SMGRSW_LENGTH; i++) {
    if (g_smgrsw[i].smgr_shutdown) {
    (*(g_smgrsw[i].smgr_shutdown))();
    }
    }
    }

    3. 行存储引擎

    openGauss的行存储引擎设计上支持MVCC(Multi-Version Concurrency Control,多版本并发控制),采用集中式垃圾版本回收机制,可以提供OLTP业务系统的高并发读写要求。支持存储计算分离架构,存储层异步回放日志。如图1所示。

    |图1 行存储架构|

    行存储引擎的关键技术有:

    (1)基于CSN(Commit Sequence Number,待提交事务的序列号,它是一个64位递增无符号数)的MVCC并发控制机制,集中式垃圾数据清理。

    (2)并行刷日志,并行恢复。传统数据库一般都采用串行刷日志的设计,因为日志有顺序依赖关系,例如一个是事务产生的redo/undo log是有前后依赖关系的。openGauss的日志系统采用多个logwriter线程并行写的机制,充分发挥SSD的多通道IO能力。

    (3)基于大内存设计的Buffer manager。

    行存储buffer主流程代码如下。

      /* buffer/bufmgr.cpp, 基础行存储管理 */
      ...
      /* 查找或创建一个缓冲区 */
      Buffer ReadBufferExtended(
      Relation reln, ForkNumber fork_num, BlockNumber block_num, ReadBufferMode mode, BufferAccessStrategy strategy)
      {
      bool hit = false;
      Buffer buf;

      if (block_num == P_NEW) {
      STORAGE_SPACE_OPERATION(reln, BLCKSZ);
      }

      /* 以smgr(存储管理器)级别 打开一个缓冲区 */
      RelationOpenSmgr(reln);

      /* 拒绝读取非局部临时关系的请求,因为可能会获得监控不到的错误数据 */
      if (RELATION_IS_OTHER_TEMP(reln) && fork_num <= INIT_FORKNUM)
      ereport(ERROR,
      (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot access temporary tables of other sessions")));

      /* 读取缓冲区,更新pgstat数量反馈cache命中与否情况 */
      pgstat_count_buffer_read(reln);
      pgstatCountBlocksFetched4SessionLevel();
      buf = ReadBuffer_common(reln->rd_smgr, reln->rd_rel->relpersistence, fork_num, block_num, mode, strategy, &hit);
      if (hit) {
      pgstat_count_buffer_hit(reln);
      }
      return buf;
      }

      /* 释放一个缓冲区 */
      void ReleaseBuffer(Buffer buffer)
      {
      BufferDesc* buf_desc = NULL;
      PrivateRefCountEntry* ref = NULL;
      /* 错误释放处理 */
      if (!BufferIsValid(buffer)) {
      ereport(ERROR, (errcode(ERRCODE_INVALID_BUFFER), (errmsg("bad buffer ID: %d", buffer))));
      }

      ResourceOwnerForgetBuffer(t_thrd.utils_cxt.CurrentResourceOwner, buffer);

      if (BufferIsLocal(buffer)) {
      Assert(u_sess->storage_cxt.LocalRefCount[-buffer - 1] > 0);
      u_sess->storage_cxt.LocalRefCount[-buffer - 1]--;
      return;
      }
      /* 释放当前缓冲区 */
      buf_desc = GetBufferDescriptor(buffer - 1);

      PrivateRefCountEntry *free_entry = NULL;
      ref = GetPrivateRefCountEntryFast(buffer, free_entry);
      if (ref == NULL) {
      ref = GetPrivateRefCountEntrySlow(buffer, false, false, free_entry);}
      Assert(ref != NULL);
      Assert(ref->refcount > 0);

      if (ref->refcount > 1) {
      ref->refcount--;
      } else {
      UnpinBuffer(buf_desc, false);
      }
      }
      /* 标记写脏缓冲区 */
      void MarkBufferDirty(Buffer buffer)
      {
      BufferDesc* buf_desc = NULL;
      uint32 buf_state;
      uint32 old_buf_state;

      if (!BufferIsValid(buffer)) {
      ereport(ERROR, (errcode(ERRCODE_INVALID_BUFFER), (errmsg("bad buffer ID: %d", buffer))));}

      if (BufferIsLocal(buffer)) {
      MarkLocalBufferDirty(buffer);
      return;
      }

      buf_desc = GetBufferDescriptor(buffer - 1);

      Assert(BufferIsPinned(buffer));
      /* unfortunately we can't check if the lock is held exclusively */
      Assert(LWLockHeldByMe(buf_desc->content_lock));

      old_buf_state = LockBufHdr(buf_desc);

      buf_state = old_buf_state | (BM_DIRTY | BM_JUST_DIRTIED);

      /* 将未入队的脏页入队 */
      if (g_instance.attr.attr_storage.enableIncrementalCheckpoint) {
      for (;;) {
      buf_state = old_buf_state | (BM_DIRTY | BM_JUST_DIRTIED);
      if (!XLogRecPtrIsInvalid(pg_atomic_read_u64(&buf_desc->rec_lsn))) {
      break;
      }

      if (!is_dirty_page_queue_full(buf_desc) && push_pending_flush_queue(buffer)) {
      break;
      }

      UnlockBufHdr(buf_desc, old_buf_state);
      pg_usleep(TEN_MICROSECOND);
      old_buf_state = LockBufHdr(buf_desc);
      }
      }

      UnlockBufHdr(buf_desc, buf_state);

      /* 如果缓冲区不是“脏”状态,则更新相关计数 */
      if (!(old_buf_state & BM_DIRTY)) {
      t_thrd.vacuum_cxt.VacuumPageDirty++;
      u_sess->instr_cxt.pg_buffer_usage->shared_blks_dirtied++;

      pgstatCountSharedBlocksDirtied4SessionLevel();

      if (t_thrd.vacuum_cxt.VacuumCostActive) {
      t_thrd.vacuum_cxt.VacuumCostBalance += u_sess->attr.attr_storage.VacuumCostPageDirty;
      }
      }
      }

      4. 列存储引擎

      传统行存储数据压缩率低,必须按行读取,即使读取一列也必须读取整行。openGauss创建表的时候,可以指定行存储还是列存储。列存储表也支持DML操作,也支持MVCC。列存储架构如图2所示。

      |图2 列存储架构|

      列存储引擎有以下优势:

      (1)列的数据特征比较相似,适合压缩,压缩比很高。

      (2)当表列的个数比较多,但是访问的列个数比较少时,列存可以按需读取列数据,大大减少不必要的读IO,提高查询性能。

      (3)基于列批量数据Vector(向量)的运算,CPU的cache命中率比较高,性能比较好。列存储引擎更适合OLAP大数据统计分析的场景。

      1) 列存储源码组织
      列存储源码目录为:

      /src/gausskernel/storage/cstore。列存储源码文件如表2所示。

      表2 列存储源码文件

      cstore

      compression

      数据压缩与解压

      cstore_allocspace

      空间分配

      cstore_am

      列存储公共API

      cstore_***_func

      支持函数

      cstore_psort

      列内排序

      cu

      数据压缩单元

      cucache_mgr

      缓存管理器

      custorage

      持久化存储

      cstore_delete

      删除方法

      cstore_update

      更新方法

      cstore_vector

      缓冲区实现

      cstore_rewrite

      SQL重写

      cstore_insert

      插入方法

      cstore_mem_alloc

      内存分配

      2) 列存储主要API

      列存储主要API代码如下。

        /*  cstore_am.cpp */
        ...
        /* 扫描 APIs */
        void InitScan(CStoreScanState *state, Snapshot snapshot = NULL);
        void InitReScan();
        void InitPartReScan(Relation rel);
        bool IsEndScan() const;

        /* 晚读 APIs */
        bool IsLateRead(int id) const;
        void ResetLateRead();

        /* 更新列存储扫描计时标记*/
        void SetTiming(CStoreScanState *state);

        /* 列存储扫描*/
        void ScanByTids(_in_ CStoreIndexScanState *state, _in_ VectorBatch *idxOut, _out_ VectorBatch *vbout);
        void CStoreScanWithCU(_in_ CStoreScanState *state, BatchCUData *tmpCUData, _in_ bool isVerify = false);

        /* 加载数据压缩单元描述信息 */
        bool LoadCUDesc(_in_ int col, __inout LoadCUDescCtl *loadInfoPtr, _in_ bool prefetch_control, _in_ Snapshot snapShot = NULL);

        /* 从描述表中获取数据压缩单元描述*/
        bool GetCUDesc(_in_ int col, _in_ uint32 cuid, _out_ CUDesc *cuDescPtr, _in_ Snapshot snapShot = NULL);

        /* 获取元组删除信息*/
        void GetCUDeleteMaskIfNeed(_in_ uint32 cuid, _in_ Snapshot snapShot);

        bool GetCURowCount(_in_ int col, __inout LoadCUDescCtl *loadCUDescInfoPtr, _in_ Snapshot snapShot);
        /* 获取实时行号。*/
        int64 GetLivedRowNumbers(int64 *deadrows);

        /* 获得数据压缩单元*/
        CU *GetCUData(_in_ CUDesc *cuDescPtr, _in_ int colIdx, _in_ int valSize, _out_ int &slotId);

        CU *GetUnCompressCUData(Relation rel, int col, uint32 cuid, _out_ int &slotId, ForkNumber forkNum = MAIN_FORKNUM,
        bool enterCache = true) const;

        /* 缓冲向量填充 APIs */
        int FillVecBatch(_out_ VectorBatch *vecBatchOut);

        /* 填充列向量*/
        template <bool hasDeadRow, int attlen>
        int FillVector(_in_ int colIdx, _in_ CUDesc *cu_desc_ptr, _out_ ScalarVector *vec);

        template <int attlen>
        void FillVectorByTids(_in_ int colIdx, _in_ ScalarVector *tids, _out_ ScalarVector *vec);

        template <int attlen>
        void FillVectorLateRead(_in_ int seq, _in_ ScalarVector *tids, _in_ CUDesc *cuDescPtr, _out_ ScalarVector *vec);

        void FillVectorByIndex(_in_ int colIdx, _in_ ScalarVector *tids, _in_ ScalarVector *srcVec, _out_ ScalarVector *destVec);

        /* 填充系统列*/
        int FillSysColVector(_in_ int colIdx, _in_ CUDesc *cu_desc_ptr, _out_ ScalarVector *vec);

        template <int sysColOid>
        void FillSysVecByTid(_in_ ScalarVector *tids, _out_ ScalarVector *destVec);

        template <bool hasDeadRow>
        int FillTidForLateRead(_in_ CUDesc *cuDescPtr, _out_ ScalarVector *vec);

        void FillScanBatchLateIfNeed(__inout VectorBatch *vecBatch);

        /* 设置数据压缩单元范围以支持索引扫描 */
        void SetScanRange();

        /* 判断行是否可用*/
        bool IsDeadRow(uint32 cuid, uint32 row) const;

        void CUListPrefetch();
        void CUPrefetch(CUDesc *cudesc, int col, AioDispatchCUDesc_t **dList, int &count, File *vfdList);

        /* 扫描函数 */
        typedef void (CStore::*ScanFuncPtr)(_in_ CStoreScanState *state, _out_ VectorBatch *vecBatchOut);
        void RunScan(_in_ CStoreScanState *state, _out_ VectorBatch *vecBatchOut);
        int GetLateReadCtid() const;
        void IncLoadCuDescCursor();

        5. 内存引擎

        openGauss引入了MOT(Memory-Optimized Table,内存优化表)存储引擎,它是一种事务性行存储,针对多核和大内存服务器进行了优化。MOT是openGauss数据库出色的生产级特性(Beta版本),它为事务性工作负载提供更高的性能。MOT完全支持ACID特性,并包括严格的持久性和高可用性支持。企业可以在关键任务、性能敏感的在线事务处理(OLTP)中使用MOT,以实现高性能、高吞吐、可预测低延迟以及多核服务器的高利用率。MOT尤其适合在多路和多核处理器的现代服务器上运行,例如基于ARM(Advanced RISC Machine,高级精简指令集计算机器)/鲲鹏处理器的华为TaiShan服务器,以及基于x86的戴尔或类似服务器。MOT存储引擎如图3所示。

        |图3 openGauss内存引擎|

        MOT与基于磁盘的普通表并排创建。MOT的有效设计实现了几乎完全的SQL覆盖,并且支持完整的数据库功能集,如存储过程和自定义函数。通过完全存储在内存中的数据和索引、非统一内存访问感知(NUMA-aware)设计、消除锁和锁存争用的算法以及查询原生编译,MOT可提供更快的数据访问和更高效的事务执行。MOT有效的几乎无锁的设计和高度调优的实现,使其在多核服务器上实现了卓越的近线性吞吐量扩展。

        MOT的高性能(查询和事务延迟)、高可扩展性(吞吐量和并发量)等特点,在某些情况下低成本(高资源利用率)方面拥有显著优势。

        (1)低延迟(Low Latency):提供快速的查询和事务响应时间。
        (2)高吞吐量(High Throughput):支持峰值和持续高用户并发。
        (3)高资源利用率(High Resource Utilization):充分利用硬件。

        MOT的关键技术如下:

        (1)内存优化数据结构:以实现高并发吞吐量和可预测的低延迟为目标,所有数据和索引都在内存中,不使用中间页缓冲区,并使用持续时间最短的锁。数据结构和所有算法都是专门为内存设计而优化的。

        (2)免锁事务管理:MOT在保证严格一致性和数据完整性的前提下,采用乐观的策略实现高并发和高吞吐。在事务过程中,MOT不会对正在更新的数据行的任何版本加锁,从而大大降低了一些大内存系统中的争用。

        (3)免锁索引:由于内存表的数据和索引完全存储在内存中,因此拥有一个高效的索引数据结构和算法非常重要。MOT索引机制基于领域前沿的树结构Masstree,一种用于多核系统的快速和可扩展的键值(Key Value,KV)存储索引,以B+树的Trie实现。通过这种方式,高并发工作负载在多核服务器上可以获得卓越的性能。同时MOT应用了各种先进的技术以优化性能,如优化锁方法、高速缓存感知和内存预取。

        (4)NUMA-aware的内存管理:MOT内存访问的设计支持非统一内存访问(NUMA,Non-Uniform Memory Access)感知。NUMA-aware算法增强了内存中数据布局的性能,使线程访问物理上连接到线程运行的核心的内存。这是由内存控制器处理的,不需要通过使用互连(如英特尔QPI(Quick Path Interconnect,快速路径互连))进行额外的跳转。MOT的智能内存控制模块,为各种内存对象预先分配了内存池,提高了性能、减少了锁、保证了稳定性。

        (5)高效持久性:日志和检查点是实现磁盘持久化的关键能力,也是ACID的关键要求之一。目前所有的磁盘(包括SSD和NVMe(Non-Volatile Memory express,非易失性高速传输总线))都明显慢于内存,因此持久化是基于内存数据库引擎的瓶颈。作为一个基于内存的存储引擎,MOT的持久化设计必须实现各种各样的算法优化,以确保持久化的同时还能达到设计时的速度和吞吐量目标。

        (6)高SQL覆盖率和功能集:MOT通过扩展的openGauss外部数据封装(Foreign Data Wrapper,FDW)以及索引,几乎支持完整的SQL范围,包括存储过程、用户定义函数和系统函数调用。

        (7)使用PREPARE语句的查询原生编译:通过使用PREPARE客户端命令,可以以交互方式执行查询和事务语句。这些命令已被预编译成原生执行格式,也称为Code-Gen或即时(Just-in-Time,JIT)编译。这样可以实现平均30%的性能提升。

        (8)MOT和openGauss数据库的无缝集成:MOT是一个高性能的面向内存优化的存储引擎,已集成在openGauss软件包中。MOT的主内存引擎和基于磁盘的存储引擎并存,以支持多种应用场景,同时在内部重用数据库辅助服务,如WAL重做日志、复制、检查点和恢复高可用性等。

        1) 内存引擎源码组织
        内存引擎源码目录为:

        /src/gausskernel/storage/mot。内存引擎源码文件如表3所示。

        表3 内存引擎源码文件

        mot

        concurrency_control

        发控制管理

        infra

        辅助与配置函数

        memory

        内存数据管理

        storage

        持久化存储

        system

        全局控制API

        utils

        日志等通用方法

        2) 内存引擎主流程

        内存引擎主流程代码如下。

          /* system/mot_engine.cpp */
          ...
          /* 创建内存引擎实例 */
          MOTEngine* MOTEngine::CreateInstance(
          const char* configFilePath /* = nullptr */, int argc /* = 0 */, char* argv[] /* = nullptr */)
          {
          if (m_engine == nullptr) {
          if (CreateInstanceNoInit(configFilePath, argc, argv) != nullptr) {
          bool result = m_engine->LoadConfig();
          if (!result) {
          MOT_REPORT_ERROR(MOT_ERROR_INTERNAL, "System Startup", "Failed to load Engine configuration");
          } else {
          result = m_engine->Initialize();
          if (!result) {
          MOT_REPORT_ERROR(MOT_ERROR_INTERNAL, "System Startup", "Engine initialization failed");
          }
          }

          if (!result) {
          DestroyInstance();
          MOT_ASSERT(m_engine == nullptr);
          }
          }
          }
          return m_engine;
          }
          /* 内存引擎初始化 */
          bool MOTEngine::Initialize()
          {
          bool result = false;
          /* 初始化应用服务,开始后台任务 */
          do { // instead of goto
          m_initStack.push(INIT_CORE_SERVICES_PHASE);
          result = InitializeCoreServices();
          CHECK_INIT_STATUS(result, "Failed to Initialize core services");

          m_initStack.push(INIT_APP_SERVICES_PHASE);
          result = InitializeAppServices();
          CHECK_INIT_STATUS(result, "Failed to Initialize applicative services");

          m_initStack.push(START_BG_TASKS_PHASE);
          result = StartBackgroundTasks();
          CHECK_INIT_STATUS(result, "Failed to start background tasks");
          } while (0);

          if (result) {
          MOT_LOG_INFO("Startup: MOT Engine initialization finished successfully");
          m_initialized = true;
          } else {
          MOT_LOG_PANIC("Startup: MOT Engine initialization failed!");
          /* 调用方应在失败后调用DestroyInstance() */
          }

          return result;
          }
          /* 销毁内存引擎实例 */
          void MOTEngine::Destroy()
          {
          MOT_LOG_INFO("Shutdown: Shutting down MOT Engine");
          while (!m_initStack.empty()) {
          switch (m_initStack.top()) {
          case START_BG_TASKS_PHASE:
          StopBackgroundTasks();
          break;

          case INIT_APP_SERVICES_PHASE:
          DestroyAppServices();
          break;

          case INIT_CORE_SERVICES_PHASE:
          DestroyCoreServices();
          break;

          case LOAD_CFG_PHASE:
          break;

          case INIT_CFG_PHASE:
          DestroyConfiguration();
          break;

          default:
          break;
          }
          m_initStack.pop();
          }
          ClearErrorStack();
          MOT_LOG_INFO("Shutdown: MOT Engine shutdown finished");
          }
          五、价值特性

          openGauss相比其它开源数据库主要有复合应用场景、高性能和高可用等产品特点。

          (一)高性能

          1. CBO优化器

          openGauss优化器是典型的基于代价的优化(Cost-Based Optimization,简称CBO)。在这种优化器模型下,数据库根据表的元组数、字段宽度、NULL记录比率、唯一值(Distinct Value)、最常见值(Most Common Value, 简称MCV)等表的特征值以及一定的代价计算模型,计算出每一个执行步骤的不同执行方式的输出元组数和执行代价(cost),进而选出整体执行代价最小/首元组返回代价最小的执行方式进行执行。
          CBO优化器能够在众多计划中依据代价选出最高效的执行计划,最大限度的满足客户业务要求。

          2. 行列混合存储

          openGauss支持行存储和列存储2种存储模型,用户可以根据应用场景,建表的时候选择行存储还是列存储表。
          一般情况下,如果表的字段比较多(大宽表),查询中涉及到的列不很多的情况下,适合列存储。如果表的字段个数比较少,查询大部分字段,那么选择行存储比较好。
          在大宽表、数据量比较大的场景中,查询经常关注某些列,行存储引擎查询性能比较差。例如气象局的场景,单表有200~800个列,查询经常访问10个列,在类似这样的场景下,向量化执行技术和列存储引擎可以极大的提升性能和减少存储空间。行存表和列存表各有优劣,建议根据实际情况选择。

          (1)行存表。默认创建表的类型。数据按行进行存储,即一行数据紧挨着存储。行存表支持完整的增删改查。适用于对数据需要经常更新的场景。

          (2)列存表。数据按列进行存储,即一列所有数据紧挨着存储。单列查询IO小,比行存表占用更少的存储空间。适合数据批量插入、更新较少和以查询为主统计分析类的场景。列存表不适合点查询,insert插入单条记录性能差。

          行存表和列存表的选择原则如下:
          (1)更新频繁程度。数据如果频繁更新,选择行存表。
          (2)插入频繁程度。频繁的少量插入,选择行存表。一次插入大批量数据,选择列存表。
          (3)表的列数。表的列数很多,选择列存表。
          (4)查询的列数。如果每次查询时,只涉及了表的少数(<50%总列数)几个列,选择列存表。
          (5)压缩率。列存表比行存表压缩率高。但高压缩率会消耗更多的CPU资源。

          3. 自适应压缩

          当前主流数据库通常都会采用数据压缩技术。数据类型不同,适用于它的压缩算法不同。对于相同类型的数据,其数据特征不同,采用不同的压缩算法达到的效果也不相同。自适用压缩正是从数据类型和数据特征出发,采用相应的压缩算法,实现了良好的压缩比、快速的入库性能以及良好的查询性能。
          数据入库和频繁的海量数据查询是用户的主要应用场景。在数据入库场景中,自适应压缩可以大幅度地减少数据量,成倍提高IO操作效率,将数据簇集存储,从而获得快速的入库性能。当用户进行数据查询时,少量的IO操作和快速的数据解压可以加快数据获取的速率,从而在更短的时间内得到查询结果。例如,支持类手机号字符串的大整数压缩、支持numeric类型的大整数压缩、支持对压缩算法进行不同压缩水平的调整。

          4. 分区

          在openGauss系统中,数据分区是将实例内部的数据集按照用户指定的策略做进一步拆分的水平分表,将表按照指定范围划分为多个数据互不重叠的部分。
          对于大多数用户使用场景,分区表和普通表相比具有以下优点:
          (1)改善查询性能:对分区对象的查询可以仅搜索自己关心的分区,提高检索效率。
          (2)增强可用性:如果分区表的某个分区出现故障,表在其它分区的数据仍然可用。
          (3)方便维护:如果分区表的某个分区出现故障,需要修复数据,只修复该分区即可。
          (4)均衡I/O:可以把不同的分区映射到不同的磁盘以平衡I/O,改善整个系统性能。
          目前openGauss数据库支持的分区表为范围分区表、列表分区表、哈希分区表。

          (1)范围分区表:将数据基于范围映射到每一个分区,这个范围是由创建分区表时指定的分区键决定的。这种分区方式是最为常用的。范围分区功能,即根据表的一列或者多列,将要插入表的记录分为若干个范围(这些范围在不同的分区里没有重叠),然后为每个范围创建一个分区,用来存储相应的数据。

          (2)列表分区表:将数据基于各个分区内包含的键值映射到每一个分区,分区包含的键值在创建分区时指定。列表分区功能,即根据表的一列,将要插入表的记录中出现的键值分为若干个列表(这些列表在不同的分区里没有重叠),然后为每个列表创建一个分区,用来存储相应的数据。

          (3)哈希分区表:将数据通过哈希映射到每一个分区,每一个分区中存储了具有相同哈希值的记录。哈希分区功能,即根据表的一列,通过内部哈希算法将要插入表的记录划分到对应的分区中。

          用户在下发“CREATE TABLE”命令时增加PARTITION参数,即表示针对此表应用数据分区功能。
          用户可以在实际使用中根据需要调整建表时的分区键,使每次查询结果尽可能存储在相同或者最少的分区内(称为“分区剪枝”),通过获取连续I/O大幅度提升查询性能。
          实际业务中,时间经常被作为查询对象的过滤条件。因此,用户可考虑选择时间列为分区键,键值范围可根据总数据量、一次查询数据量调整。

          5. SQL by pass

          在典型的OLTP场景中,简单查询占了很大一部分比例,这种查询的特征是只涉及单表和简单表达式的查询。为了加速这类查询,提出了SQL by pass框架:在parse层对这类查询做简单的模式判别后,进入到特殊的执行路径里,跳过经典的执行器执行框架,包括算子的初始化与执行、表达式与投影等经典框架,直接重写一套简洁的执行路径,并且直接调用存储接口。这样可以大大加速简单查询的执行速度。

          6. 鲲鹏NUMA架构优化

          鲲鹏NUMA架构优化图如图4所示。

          |图4 鲲鹏NUMA架构优化图|
          openGauss架构优化要点如下:
          (1)openGauss根据鲲鹏处理器的多核NUMA架构特点,进行针对性一系列NUMA架构相关优化。一方面尽量减少跨核内存访问的时延问题,另一方面充分发挥鲲鹏多核算力优势。所提供的关键技术包括重做日志批量插入、热点数据NUMA分布、CLog分区等,大幅提升OLTP系统的处理性能。
          (2)openGauss基于鲲鹏芯片所使用的ARMv8.1架构,利用大规模系统扩展指令集(Large System Extension,简称LSE)实现高效的原子操作,有效提升CPU利用率,从而提升多线程间同步性能、XLog写入性能等。
          (3)openGauss基于鲲鹏芯片提供的更宽的L3缓存cacheline,针对热点数据访问进行优化,有效提高缓存访问命中率,降低Cache缓存一致性维护开销,大幅提升系统整体的数据访问性能。

          (二)高扩展

          在OLTP领域中,数据库需要处理大量的客户端连接。因此,高并发场景的处理能力是数据库的重要能力之一。
          对于外部连接最简单的处理模式是per-thread-per-connection模式,即来一个用户连接产生一个线程。这个模式好处是架构上处理简单,但是高并发下,由于线程太多,线程切换和数据库轻量级锁区域的冲突过大导致性能急剧下降,使得系统性能(吞吐量)严重下降,无法满足用户性能的SLA(Service-Level Agreement,服务等级协议)。
          因此,需要通过线程资源池化复用的技术来解决该问题。线程池技术的整体设计思想是线程资源池化、并且在不同连接之间复用。系统在启动之后会根据当前核数或者用户配置启动固定一批数量的工作线程,一个工作线程会服务一到多个连接会话,这样把会话和线程进行了解耦。因为工作线程数是固定的,因此在高并发下不会导致线程的频繁切换,而由数据库层来进行会话的调度管理。

          (三)高可用

          1. 主备机

          为了保证故障的可恢复,需要将数据写多份,设置主备多个副本,通过日志进行数据同步;可以实现在节点故障、停止后重启等情况下,保证故障之前的数据无丢失,以满足ACID特性。openGauss可以支持一主多备模式,备机接收主机发送过来的WAL日志并进行回访,保证和主机的数据一致;同时在主机发生故障时,备机可以参照升主机制进行升主。备机过多会消耗过量的资源,而备机太少会降低系统的可用性。
          主备之间可以通过“switchover”操作进行角色切换,主机故障后可以通过“failover”操作对备机进行升主。
          初始化安装或者备份恢复等场景中,需要根据主机重建备机的数据,此时需要Build(构建)功能,将主机的数据和WAL日志发送到备机。主机故障后重新以备机的角色加入时,也需要Build功能将其数据和日志与新主拉齐。Build包含全量Build和增量Build;全量Build要全部依赖主机数据进行重建,拷贝的数据量比较大,耗时比较长;而增量Build只拷贝差异文件,拷贝的数据量比较小,耗时比较短。一般情况下,优先选择增量Build来进行故障恢复;如果增量Build失败,再继续执行全量Build,直至故障恢复。
          openGauss除了流复制主备双机外,还支持逻辑复制。在逻辑复制中把主库称为源端库,备库称为目标端数据库。源端数据库根据预先指定好的逻辑解析规则对WAL文件进行解析,把DML操作解析成一定的格式的逻辑日志(例如可以解析成标准SQL语句)。源端数据库把逻辑日志发给目标端数据库,目标端数据库收到后进行回放,从而实现数据同步。逻辑复制只有DML操作。逻辑复制可以实现跨版本复制、异构数据库复制、双写数据库复制、表级别复制等。

          2. 逻辑备份

          openGauss提供逻辑备份能力,可以将用户表的数据以通用的text或者用户自定义格式备份到本地磁盘文件,并在同构/异构数据库中恢复该用户表的数据。

          3. 物理备份

          openGauss提供物理备份能力,可以将整个实例的数据以数据库内部格式备份到本地磁盘文件中,并在同构数据库中恢复整个实例的数据。
          物理备份主要分为全量备份和增量备份,它们的区别如下:全量备份包含备份时刻点上数据库的全量数据,耗时时间长(和数据库数据总量成正比),自身即可恢复出完整的数据库;增量备份只包含从指定时刻点之后的增量修改数据,耗时时间短(和增量数据成正比,和数据总量无关),但是必须要和全量备份数据一起才能恢复出完整的数据库。当前openGauss同时支持全量备份和增量备份。

          4.恢复到指定时间点(PITR)

          时间点恢复(Point In Time Recovery,PITR)基本原理是通过基础热备 + WAL预写日志 + WAL归档日志进行备份恢复。重放WAL记录的时候可以在任意点停止重放,这样就有一个在任意时间的数据库一致的快照。即可以把数据库恢复到自开始备份以来的任意时刻的状态。openGauss在恢复时可以指定恢复的停止点位置为LSN(Log Sequence Number,日志序列号)、时间、XID(Transaction ID,事务ID)以及用户创建的还原点。

          (四)可维护性

          1. 支持WDR诊断报告

          WDR(Workload Diagnosis Report)基于两次不同时间点系统的性能快照数据,生成这两个时间点之间的性能表现报表,用于诊断数据库内核的性能故障。
          WDR主要依赖两个组件:
          (1)SNAPSHOT性能快照:性能快照可以配置成按一定时间间隔从内核采集一定量的性能数据,持久化在用户表空间。任何一个SNAPSHOT(快照)可以作为一个性能基线,其它SNAPSHOT与之比较的结果,可以分析出与基线的性能表现。
          (2)WDR Reporter:报表生成工具基于两个SNAPSHOT,分析系统总体性能表现,并能计算出更多项具体的性能指标在这两个时间段之间的变化量,生成SUMMARY和DETAIL两个不同级别的性能数据。
          WDR报表是长期性能问题最主要的诊断手段。基于SNAPSHOT的性能基线,从多维度做性能分析,能帮助DBA掌握系统负载繁忙程度,各个组件的性能表现,性能瓶颈。SNAPSHOT也是后续性能问题自诊断和自优化建议的重要数据来源。

          2. 慢SQL诊断

          慢SQL能根据用户提供的执行时间阈值,记录所有超过阈值的执行完毕的作业信息。
          历史慢SQL提供表和函数两种维度的查询接口,方便用户统计慢SQL指标,对接第三方平台。用户从接口中能查询到作业的执行计划、开始执行时间、结束执行时间、执行查询的语句、行活动、内核时间、CPU时间、执行时间、解析时间、编译时间、查询重写时间、计划生成时间、网络时间、IO时间、网络开销、锁开销等。所有信息都是脱敏的。
          慢SQL提供给用户对于慢SQL诊断所需的详细信息,用户无需通过复现就能离线诊断特定慢SQL的性能问题。

          3. 支持一键式收集诊断信息

          提供多种套件用于捕获、收集、分析诊断数据,使问题可以诊断,加速诊断过程。能根据开发和定位人员的需要,从生产环境中将必要的数据库日志、数据库管理日志、堆栈信息等提取出来,定位人员根据获得信息进行问题的定界定位。
          一键式收集工具可以根据生产环境中问题的不同,从生产环境中获取不同的信息,从而提高问题定位定界的效率。用户可以通过改写配置文件,收集自己想要的信息:
          (1)通过操作系统命令收集操作系统相关的信息。
          (2)通过查询系统表或者视图获得数据库系统相关的信息。
          (3)数据库系统运行日志和数据库管理相关的日志。
          (4)数据库系统的配置信息。
          (5)数据库相关进程产生的core文件。
          (6)数据库相关进程的堆栈信息。
          (7)数据库进程产生的trace信息。
          (8)数据库产生的redo日志文件。
          (9)计划复现信息。

          (五) 数据库安全

          1. 访问控制

          管理用户对数据库的访问控制权限,涵盖数据库系统权限和对象权限。
          openGauss数据库支持基于角色的访问控制机制,将角色和权限关联起来,通过将权限赋予给对应的角色,再将角色授予给用户,可实现用户访问控制权限管理。其中登录访问控制通过用户标识和认证技术来共同实现,而对象访问控制则基于用户在对象上的权限,通过对象权限检查实现对象访问控制。用户在为相关的数据库用户分配完成任务所需要的最小权限从而将数据库使用风险降到最低。
          openGauss数据库支持三权分立权限访问控制模型,数据库角色可分为系统管理员、安全管理员和审计管理员。其中安全管理员负责创建和管理用户,系统管理员负责授予和撤销用户权限,审计管理员负责审计所有用户的行为。
          默认情况下,使用基于角色的访问控制模型。客户可通过设置参数来选择是否开启三权分立控制模型。

          2. 控制权和访问权分离

          针对系统管理员用户,实现表对象的控制权和访问权分离,提高普通用户数据安全性,限制管理员对象访问权限。
          该特性适用于如下场景,即对于有多个业务部门的企业,各部门间使用不同的数据库用户进行业务操作,同时存在同级别的数据库维护部门使用数据库管理员进行运维操作,业务部门希望在未经授权的情况下,管理员用户只能对各部门的数据进行控制操作(DROP、ALTER、TRUNCATE),但是不能进行访问操作(INSERT、DELETE、UPDATE、SELECT、COPY)。即针对管理员用户,表对象的控制权和访问权分离,提高用户数据的安全性。
          系统管理员可以在创建用户时指定INDEPENDENT属性,表示该用户为私有用户。针对该用户的对象,数据库管理员(包含初始用户和其它管理员用户)在未经其授权前,只能进行控制操作(DROP、ALTER、TRUNCATE),无权进行INSERT、DELETE、SELECT、UPDATE、COPY、GRANT、REVOKE、ALTER OWNER操作。

          3. 数据库加密认证

          采用基于RFC5802机制的口令加密认证方法。
          加密认证过程中采用单向Hash不可逆加密算法PBKDF2,可有效防止彩虹攻击。
          创建用户所设置的口令被加密存储在系统表中。整个认证过程中口令加密存储和传输,通过计算相应的hash值并与服务端存储的值比较来进行正确性校验。
          统一加密认证过程中的消息处理流程,可有效防止攻击者通过抓取报文猜解用户名或者口令的正确性。

          4. 数据库审计

          审计日志记录用户对数据库的启停、连接、DDL(Data Definition Language,数据定义语言)、DML、DCL(Data Control Language,数据控制语言)等操作。审计日志机制主要增强数据库系统对非法操作的追溯及举证能力。
          用户可以通过参数配置对哪些语句或操作记录审计日志。
          审计日志记录事件的时间、类型、执行结果、用户名、数据库、连接信息、数据库对象、数据库实例名称和端口号以及详细信息。支持按起止时间段查询审计日志,并根据记录的字段进行筛选。
          数据库安全管理员可以利用这些日志信息,重现导致数据库现状的一系列事件,找出非法操作的用户、时间和内容等。

          5. 全密态数据库等值查询

          密态数据库,与流数据库、图数据库一样,就是专门处理密文数据的数据库系统。数据以加密形态存储在数据库服务器中,数据库支持对密文数据的检索与计算,而与查询任务相关的词法解析、语法解析、执行计划生成、事务一致性保证、存储都继承原有数据库能力。
          密态数据库在客户端进行加密,需要在客户端进行大量的操作,包括管理数据密钥、加密敏感数据、解析并修改实际执行的SQL语句,并且识别返回到客户端的加密的数据信息。OpenGauss将这一系列的复杂操作,自动化的封装在前端解析中,对SQL查询中与敏感信息的加密替换,使得发送至数据库服务器侧的查询任务也不会泄露用户查询意图,减少客户端的复杂安全管理及操作难度,实现用户应用开发无感知。
          密态数据库通过技术手段实现数据库密文查询和计算,解决数据库云上隐私泄露问题及第三方信任问题。实现云上数据的全生命周期保护,实现数据拥有者与数据管理者读取能力分离。

          6. 网络通信安全特性

          支持通过SSL(Secure Sockets Layer,安全套接层)加密客户端和服务器之间的通信数据,保证客户的客户端与服务器通信安全。采用TLS 1.2(Transport Layer Security,传输层安全)协议标准,并使用安全强度较高的加密算法套件。

          7. 行级访问控制

          行级访问控制特性将数据库访问粒度控制到数据表行级别,使数据库达到行级访问控制的能力。不同用户执行相同的SQL查询操作,按照行访问控制策略,读取到的结果可能是不同的。
          用户可以在数据表创建行访问控制(Row Level Security)策略,该策略是指针对特定数据库用户、特定SQL操作生效的表达式。当数据库用户对数据表访问时,若SQL满足数据表特定的Row Level Security策略,在查询优化阶段将满足条件的表达式,按照属性(PERMISSIVE | RESTRICTIVE)类型,通过AND或OR方式拼接,应用到执行计划上。
          行级访问控制的目的是控制表中行级数据可见性,通过在数据表上预定义Filter,在查询优化阶段将满足条件的表达式应用到执行计划上,影响最终的执行结果。当前行级访问控制支持的SQL语句包括SELECT、UPDATE、DELETE。

          8. 资源标签

          资源标签(Resource Label)特性通过将数据库资源按照用户自定义的方式划分,实现资源分类管理的目的。管理员可以通过配置资源标签统一地为一组数据库资源进行安全策略的配置如审计或数据脱敏。
          资源标签能够将数据库资源按照“特征”、“作用场景”等分组归类,对指定资源标签的管理操作即对标签范围下所有的数据库资源的管理操作,能够大大降低策略配置的复杂度和信息冗余度、提高管理效率。
          当前资源标签所支持的数据库资源类型包括:SCHEMA、TABLE、COLUMN、VIEW、FUNCTION。

          9. 动态数据脱敏

          为了在一定程度上限制非授权用户对隐私数据的窥探,可以利用动态数据脱敏(Dynamic Data Masking)特性保护用户隐私数据。在非授权用户访问配置了动态数据脱敏策略的数据时,数据库将返回脱敏后的数据而达到对隐私数据保护的目的。
          管理员可以在数据列上创建动态数据脱敏策略,该策略指出针对特定用户场景应采取何种数据脱敏方式。在开启动态数据脱敏功能后,当用户访问敏感列数据时,系统将用户身份信息例如:访问IP、客户端工具、用户名来匹配相应的脱敏策略,在匹配成功后将根据脱敏策略对访问列的查询结果实施数据脱敏。
          动态数据脱敏的目的是在不改变源数据的前提下,通过在脱敏策略上配置针对的用户场景(FILTER)、指定的敏感列标签(LABEL)和对应的脱敏方式(MASKING FUNCTION)来灵活地进行隐私数据保护。

          10. 统一审计

          统一审计(Unified Auditing)利用策略和条件在数据库内部有选择地进行审计,管理员可以对数据库资源或资源标签统一地配置审计策略,从而达到简化管理、针对性地生成审计日志、减少审计日志冗余、提高管理效率的目的。
          管理员可以定制化的为操作行为或数据库资源配置审计策略,该策略针对特定的用户场景、用户行为或数据库资源进行审计。在开启了统一审计功能后,当用户访问数据库时,系统将根据用户身份信息如:访问IP、客户端工具、用户名来匹配相应的统一审计策略,之后根据策略信息对用户行为按照访问资源(LABEL)和用户操作类型(DML | DDL)进行统一审计。
          统一审计的目的是将现有的传统审计行为转变为针对性的跟踪审计行为,将目标之外的行为排除在审计之外,从而简化了管理,提高了数据库生成审计数据的安全性。

          11. 用户口令强度校验机制

          为了加固客户账户和数据的安全,禁止设置过低强度的口令,当初始化数据库、创建用户、修改用户时需要指定密码。密码必须满足强度校验,否则会提示用户重新输入密码。
          账户密码复杂度对用户密码大小写字母、数字、特殊字符的最少个数,最大最小长度,不能和用户名、用户名倒写相同,不能是弱口令等进行了限制,从而增强了用户账户的安全性。
          其中弱口令指的是强度较低,容易被破解的密码,对于不同的用户或群体,弱口令的定义可能会有所区别,用户需要自己添加定制化的弱口令。
          用户口令强度校验机制是否开启由参数password_policy控制,当该参数设置为1时表示采用密码复杂度校验,默认值为1。

          12. 数据加密存储

          提供对插入数据的加密存储。为用户提供数据加解密接口,针对用户识别的敏感信息列使用加密函数,使得数据加密后再存储在表内。
          当用户需要对整张表进行加密存储处理时,则需要为每一列单独书写加密函数,不同的属性列可使用不用的入参。
          当具有对应权限的用户需要查看具体的数据时,可通过解密函数接口对相应的属性列进行解密处理。

          (六)AI能力

          1. AI4DB

          包括参数智能调优与诊断、慢SQL发现、索引推荐、时序预测、异常检测等,能够为用户提供更便捷的运维操作和性能提升,实现自调优、自监控、自诊断等功能。

          2. DB4AI

          兼容MADlib生态,支持70+算法,性能相比MADlib on PostgreSQL 具有数倍提升。新增XGBoost、prophet、GBDT等高级且常用的算法套件,补充MADlib生态的不足。统一SQL到机器学习的技术栈,实现从数据管理到模型训练的SQL语句“一键驱动”。

          六、小结

          本章主要从openGauss功能概述、应用场景介绍、系统架构设计、代码结构解析和亮点特性阐释多方面介绍了openGauss数据库系统的设计,可以加深读者对openGauss的设计理念与代码逻辑有更深入的认识与理解。
          END
          Gauss松鼠会
          汇集数据库从业人员及爱好者
          互助解决问题 共建数据库技术交流圈
          最后修改时间:2021-03-26 08:04:25
          文章转载自Gauss松鼠会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论