Quantcast
Channel: 数据库内核月报
Viewing all 689 articles
Browse latest View live

MySQL · 源码分析 · 常用SQL语句的MDL加锁源码分析

$
0
0

前言

MySQL5.5版本开始引入了MDL锁用来保护元数据信息,让MySQL能够在并发环境下多DDL、DML同时操作下保持元数据的一致性。本文用MySQL5.7源码分析了常用SQL语句的MDL加锁实现。

MDL锁粒度

MDL_key由namespace、db_name、name组成。

namespace包含:

  • GLOBAL。用于global read lock,例如FLUSH TABLES WITH READ LOCK。

  • TABLESPACE/SCHEMA。用于保护tablespace/schema。

  • FUNCTION/PROCEDURE/TRIGGER/EVENT。用于保护function/procedure/trigger/event。

  • COMMIT。主要用于global read lock后,阻塞事务提交。

  • USER_LEVEL_LOCK。用于user level lock函数的实现,GET_LOCK(str,timeout), RELEASE_LOCK(str)。

  • LOCKING_SERVICE。用于locking service的实现。

MDL锁类型

  • MDL_INTENTION_EXCLUSIVE(IX) 意向排他锁,锁定一个范围,用在GLOBAL/SCHEMA/COMMIT粒度。

  • MDL_SHARED(S) 用在只访问元数据信息,不访问数据。例如CREATE TABLE t LIKE t1;

  • MDL_SHARED_HIGH_PRIO(SH) 也是用于只访问元数据信息,但是优先级比排他锁高,用于访问information_schema的表。例如:select * from information_schema.tables;

  • MDL_SHARED_READ(SR) 访问表结构并且读表数据,例如:SELECT * FROM t1; LOCK TABLE t1 READ LOCAL;

  • MDL_SHARED_WRITE(SW) 访问表结构且写表数据, 例如:INSERT/DELETE/UPDATE t1 … ;SELECT * FROM t1 FOR UPDATE;LOCK TALE t1 WRITE

  • MDL_SHARED_WRITE_LOW_PRIO(SWLP) 优先级低于MDL_SHARED_READ_ONLY。语句INSER/DELETE/UPDATE LOW_PRIORITY t1 …; LOCK TABLE t1 WRITE LOW_PRIORITY。

  • MDL_SHARED_UPGRADABLE(SU) 可升级锁,允许并发update/read表数据。持有该锁可以同时读取表metadata和表数据,但不能修改数据。可以升级到SNW、SNR、X锁。用在alter table的第一阶段,使alter table的时候不阻塞DML,防止其他DDL。

  • MDL_SHARED_READ_ONLY(SRO) 持有该锁可读取表数据,同时阻塞所有表结构和表数据的修改操作,用于LOCK TABLE t1 READ。

  • MDL_SHARED_NO_WRITE(SNW) 持有该锁可以读取表metadata和表数据,同时阻塞所有的表数据修改操作,允许读。可以升级到X锁。用在ALTER TABLE第一阶段,拷贝原始表数据到新表,允许读但不允许更新。

  • MDL_SHARED_NO_READ_WRITE(SNRW) 可升级锁,允许其他连接读取表结构但不可以读取数据,阻塞所有表数据的读写操作,允许INFORMATION_SCHEMA访问和SHOW语句。持有该锁的的连接可以读取表结构,修改和读取表数据。可升级为X锁。使用在LOCK TABLE WRITE语句。

  • MDL_EXCLUSIVE(X) 排他锁,持有该锁连接可以修改表结构和表数据,使用在CREATE/DROP/RENAME/ALTER TABLE 语句。

MDL锁持有时间

  • MDL_STATEMENT 语句中持有,语句结束自动释放

  • MDL_TRANSACTION 事务中持有,事务结束时释放

  • MDL_EXPLICIT 需要显示释放

MDL锁兼容性

Scope锁活跃锁和请求锁兼容性矩阵如下。

         | Type of active   |
Request  |   scoped lock    |
type     | IS(*)  IX   S  X |
---------+------------------+
IS       |  +      +   +  + |
IX       |  +      +   -  - |
S        |  +      -   +  - |
X        |  +      -   -  - |

+号表示请求的锁可以满足。
-号表示请求的锁无法满足需要等待。

Scope锁等待锁和请求锁优先级矩阵

         |    Pending      |
Request  |  scoped lock    |
type     | IS(*)  IX  S  X |
---------+-----------------+
IS       |  +      +  +  + |
IX       |  +      +  -  - |
S        |  +      +  +  - |
X        |  +      +  +  + |
+号表示请求的锁可以满足。
-号表示请求的锁无法满足需要等待。

object上已持有锁和请求锁的兼容性矩阵如下。

Request   |  Granted requests for lock                  |
 type     | S  SH  SR  SW  SWLP  SU  SRO  SNW  SNRW  X  |
----------+---------------------------------------------+
S         | +   +   +   +    +    +   +    +    +    -  |
SH        | +   +   +   +    +    +   +    +    +    -  |
SR        | +   +   +   +    +    +   +    +    -    -  |
SW        | +   +   +   +    +    +   -    -    -    -  |
SWLP      | +   +   +   +    +    +   -    -    -    -  |
SU        | +   +   +   +    +    -   +    -    -    -  |
SRO       | +   +   +   -    -    +   +    +    -    -  |
SNW       | +   +   +   -    -    -   +    -    -    -  |
SNRW      | +   +   -   -    -    -   -    -    -    -  |
X         | -   -   -   -    -    -   -    -    -    -  |

object上等待锁和请求锁的优先级矩阵如下。

Request   |         Pending requests for lock          |
 type     | S  SH  SR  SW  SWLP  SU  SRO  SNW  SNRW  X |
----------+--------------------------------------------+
S         | +   +   +   +    +    +   +    +     +   - |
SH        | +   +   +   +    +    +   +    +     +   + |
SR        | +   +   +   +    +    +   +    +     -   - |
SW        | +   +   +   +    +    +   +    -     -   - |
SWLP      | +   +   +   +    +    +   -    -     -   - |
SU        | +   +   +   +    +    +   +    +     +   - |
SRO       | +   +   +   -    +    +   +    +     -   - |
SNW       | +   +   +   +    +    +   +    +     +   - |
SNRW      | +   +   +   +    +    +   +    +     +   - |
X         | +   +   +   +    +    +   +    +     +   + |

常用语句MDL锁加锁分析

使用performance_schema可以辅助分析加锁。利用下面语句打开MDL锁分析,可以看到在只有当前session访问的时候,SELECT语句对metadata_locks表加了TRANSACTION周期的SHARED_READ锁,即锁粒度、时间范围和锁类型分别为:TABLE, TRANSACTION, SHARED_READ,在代码位置sql_parse.cc:5996初始化锁。。后面的锁分析也按照锁粒度-时间范围-锁类型介绍。

UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME ='global_instrumentation';
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES' WHERE NAME ='wait/lock/metadata/sql/mdl';
select * from performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: performance_schema
          OBJECT_NAME: metadata_locks
OBJECT_INSTANCE_BEGIN: 46995934864720
            LOCK_TYPE: SHARED_READ
        LOCK_DURATION: TRANSACTION
          LOCK_STATUS: GRANTED
               SOURCE: sql_parse.cc:5996
      OWNER_THREAD_ID: 26
       OWNER_EVENT_ID: 163

使用performance_schema很难完整分析语句执行中所有的加锁过程,可以借助gdb分析,在 MDL_context::acquire_lock设置断点。

下面会结合performance_schema和gdb分析常用语句的MDL加锁源码实现。

FLUSH TABLES WITH READ LOCK

语句执行会加锁GLOBAL-EXPLICIT-SHARED和COMMIT-EXPLICIT-SHARED。

select * from performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: GLOBAL
        OBJECT_SCHEMA: NULL
          OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 46996001973424
            LOCK_TYPE: SHARED
        LOCK_DURATION: EXPLICIT
          LOCK_STATUS: GRANTED
               SOURCE: lock.cc:1110
      OWNER_THREAD_ID: 27
       OWNER_EVENT_ID: 92
*************************** 2. row ***************************
          OBJECT_TYPE: COMMIT
        OBJECT_SCHEMA: NULL
          OBJECT_NAME: NULL
OBJECT_INSTANCE_BEGIN: 46996001973616
            LOCK_TYPE: SHARED
        LOCK_DURATION: EXPLICIT
          LOCK_STATUS: GRANTED
               SOURCE: lock.cc:1194
      OWNER_THREAD_ID: 27
       OWNER_EVENT_ID: 375

相关源码实现剖析。当FLUSH语句是FLUSH TABLES WITH READ LOCK的时候,lex->type会添加REFRESH_TABLES和REFRESH_READ_LOCK标记,当没有指定表即进入reload_acl_and_cache函数,通过调用lock_global_read_lock和make_global_read_lock_block_commit加对应锁,通过对应的锁来阻止元数据修改和表数据更改。DDL语句执行时会请求GLOBAL的INTENTION_EXCLUSIVE锁,事务提交和外部XA需要记录binlog的语句执行会请求COMMIT的INTENTION_EXCLUSIVE锁。

sql/sql_yacc.yy
flush_options:
          table_or_tables
          {
            Lex->type|= REFRESH_TABLES;
            /*
              Set type of metadata and table locks for
              FLUSH TABLES table_list [WITH READ LOCK].
            */
            YYPS->m_lock_type= TL_READ_NO_INSERT;
            YYPS->m_mdl_type= MDL_SHARED_HIGH_PRIO;
          }
          opt_table_list {}
          opt_flush_lock {}
        | flush_options_list
        ;


opt_flush_lock:
          /* empty */ {}
        | WITH READ_SYM LOCK_SYM
          {
            TABLE_LIST *tables= Lex->query_tables;
            Lex->type|= REFRESH_READ_LOCK;

sql/sql_parse.cc
  ...
  case SQLCOM_FLUSH:
    if (first_table && lex->type & REFRESH_READ_LOCK)//当指定表的时候,对指定表加锁。
    {
      if (flush_tables_with_read_lock(thd, all_tables))
    }
    ...
    if (!reload_acl_and_cache(thd, lex->type, first_table, &write_to_binlog))

sql/sql_reload.cc
reload_acl_and_cache
{
  if (options & (REFRESH_TABLES | REFRESH_READ_LOCK))
  {
    if ((options & REFRESH_READ_LOCK) && thd)
    {
      ...
      if (thd->global_read_lock.lock_global_read_lock(thd))//当未指定表的时候,加全局锁
        return 1;
      ...
      if (thd->global_read_lock.make_global_read_lock_block_commit(thd))//当未指定表的时候,加COMMIT锁
}

//对GLOBAL加EXPLICIT的S锁。
sql/lock.cc
bool Global_read_lock::lock_global_read_lock(THD *thd)
{
  ...
  MDL_REQUEST_INIT(&mdl_request,
                 MDL_key::GLOBAL, "", "", MDL_SHARED, MDL_EXPLICIT);
  ...
}
//对COMMIT加EXPLICIT的S锁。
bool Global_read_lock::make_global_read_lock_block_commit(THD *thd)
{
  ...
  MDL_REQUEST_INIT(&mdl_request,
                 MDL_key::COMMIT, "", "", MDL_SHARED, MDL_EXPLICIT);
  ...
}

sql/handler.cc
事务提交和外部XA事务的commit\rollback\prepare均需要加COMMIT的IX锁.
int ha_commit_trans(THD *thd, bool all, bool ignore_global_read_lock)
{
  ...
  if (rw_trans && !ignore_global_read_lock) //对于内部表slave status table的更新可以忽略global read lock
  {
    MDL_REQUEST_INIT(&mdl_request,
                 MDL_key::COMMIT, "", "", MDL_INTENTION_EXCLUSIVE,
                 MDL_EXPLICIT);

    DBUG_PRINT("debug", ("Acquire MDL commit lock"));
    if (thd->mdl_context.acquire_lock(&mdl_request,
                                  thd->variables.lock_wait_timeout))
  }
  ...
}
sql/xa.cc
bool Sql_cmd_xa_commit::trans_xa_commit(THD *thd)
{
  ...
  MDL_request mdl_request;
  MDL_REQUEST_INIT(&mdl_request,
                   MDL_key::COMMIT, "", "", MDL_INTENTION_EXCLUSIVE,
                   MDL_STATEMENT);
  if (thd->mdl_context.acquire_lock(&mdl_request,
                                    thd->variables.lock_wait_timeout))
  ...
}
bool Sql_cmd_xa_rollback::trans_xa_rollback(THD *thd)
{
  ...
  MDL_request mdl_request;
  MDL_REQUEST_INIT(&mdl_request,
                   MDL_key::COMMIT, "", "", MDL_INTENTION_EXCLUSIVE,
                   MDL_STATEMENT);
}
bool Sql_cmd_xa_prepare::trans_xa_prepare(THD *thd)
{
  ...
  MDL_request mdl_request;
  MDL_REQUEST_INIT(&mdl_request,
                   MDL_key::COMMIT, "", "", MDL_INTENTION_EXCLUSIVE,
                   MDL_STATEMENT);
}

//写入语句的执行和DDL执行需要GLOBAL的IX锁,这与S锁不兼容。
sql/sql_base.cc
bool open_table(THD *thd, TABLE_LIST *table_list, Open_table_context *ot_ctx)
{
  if (table_list->mdl_request.is_write_lock_request() &&
  {
    MDL_request protection_request;
    MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);

    if (thd->global_read_lock.can_acquire_protection())
      DBUG_RETURN(TRUE);

    MDL_REQUEST_INIT(&protection_request,
                     MDL_key::GLOBAL, "", "", MDL_INTENTION_EXCLUSIVE,
                     MDL_STATEMENT);
  }
}
bool
lock_table_names(THD *thd,
                 TABLE_LIST *tables_start, TABLE_LIST *tables_end,
                 ulong lock_wait_timeout, uint flags)
{
  if (need_global_read_lock_protection)
  {
    /*
      Protect this statement against concurrent global read lock
      by acquiring global intention exclusive lock with statement
      duration.
    */
    if (thd->global_read_lock.can_acquire_protection())
      return true;
    MDL_REQUEST_INIT(&global_request,
                     MDL_key::GLOBAL, "", "", MDL_INTENTION_EXCLUSIVE,
                     MDL_STATEMENT);
    mdl_requests.push_front(&global_request);
  }
}

LOCK TABLE t READ [LOCAL]

LOCK TABLE t READ LOCAL会加锁TABLE-TRANSACTION-SHARED_READ。

select * from performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: test
          OBJECT_NAME: t
            LOCK_TYPE: SHARED_READ
        LOCK_DURATION: TRANSACTION
          LOCK_STATUS: GRANTED
               SOURCE: sql_parse.cc:5996

LOCK TABLE t READ会加锁TABLE-TRANSACTION-SHARED_READ_ONLY。

select * from performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: test
          OBJECT_NAME: t
            LOCK_TYPE: SHARED_READ_ONLY
        LOCK_DURATION: TRANSACTION
          LOCK_STATUS: GRANTED
               SOURCE: sql_parse.cc:5996

这两个的区别是对于MyISAM引擎,LOCAL方式的加锁与insert写入不冲突,而没有LOCAL的时候SHARED_READ_ONLY会阻塞写入。不过对于InnoDB引擎两种方式是一样的,带有LOCAL的语句执行后面会升级为SHARED_READ_ONLY。

源码分析

table_lock:
          table_ident opt_table_alias lock_option
          {
            thr_lock_type lock_type= (thr_lock_type) $3;
            enum_mdl_type mdl_lock_type;

            if (lock_type >= TL_WRITE_ALLOW_WRITE)
            {
              /* LOCK TABLE ... WRITE/LOW_PRIORITY WRITE */
              mdl_lock_type= MDL_SHARED_NO_READ_WRITE;
            }
            else if (lock_type == TL_READ)
            {
              /* LOCK TABLE ... READ LOCAL */
              mdl_lock_type= MDL_SHARED_READ;
            }
            else
            {
              /* LOCK TABLE ... READ */
              mdl_lock_type= MDL_SHARED_READ_ONLY;
            }

            if (!Select->add_table_to_list(YYTHD, $1, $2, 0, lock_type,
                                           mdl_lock_type))
              MYSQL_YYABORT;
          }

lock_option:
          READ_SYM               { $$= TL_READ_NO_INSERT; }
        | WRITE_SYM              { $$= TL_WRITE_DEFAULT; }
        | LOW_PRIORITY WRITE_SYM
          {
            $$= TL_WRITE_LOW_PRIORITY;
            push_deprecated_warn(YYTHD, "LOW_PRIORITY WRITE", "WRITE");
          }
        | READ_SYM LOCAL_SYM     { $$= TL_READ; }
        ;

sql/sql_parse.cc
TABLE_LIST *st_select_lex::add_table_to_list(THD *thd,
               Table_ident *table,
               LEX_STRING *alias,
               ulong table_options,
               thr_lock_type lock_type,
               enum_mdl_type mdl_type,
               List<Index_hint> *index_hints_arg,
                                             List<String> *partition_names,
                                             LEX_STRING *option)
{
  // Pure table aliases do not need to be locked:
  if (!MY_TEST(table_options & TL_OPTION_ALIAS))
  {
    MDL_REQUEST_INIT(& ptr->mdl_request,
                     MDL_key::TABLE, ptr->db, ptr->table_name, mdl_type,
                     MDL_TRANSACTION);
  }                                             
}

//对于Innodb引擎
static bool lock_tables_open_and_lock_tables(THD *thd, TABLE_LIST *tables)
{
  ...
  else if (table->lock_type == TL_READ &&
           ! table->prelocking_placeholder &&
           table->table->file->ha_table_flags() & HA_NO_READ_LOCAL_LOCK)
  {
    /*
      In case when LOCK TABLE ... READ LOCAL was issued for table with
      storage engine which doesn't support READ LOCAL option and doesn't
      use THR_LOCK locks we need to upgrade weak SR metadata lock acquired
      in open_tables() to stronger SRO metadata lock.
      This is not needed for tables used through stored routines or
      triggers as we always acquire SRO (or even stronger SNRW) metadata
      lock for them.
    */
    bool result= thd->mdl_context.upgrade_shared_lock(
                                    table->table->mdl_ticket,
                                    MDL_SHARED_READ_ONLY,
                                    thd->variables.lock_wait_timeout);
  ...
}

LOCK TABLE t WITH WRITE

LOCK TABLE t WITH WRITE会加锁:GLOBAL-STATEMENT-INTENTION_EXCLUSIVE,SCHEMA-TRANSACTION-INTENTION_EXCLUSIVE,TABLE-TRANSACTION-SHARED_NO_READ_WRITE。

select OBJECT_TYPE,OBJECT_SCHEMA,OBJECT_NAME,LOCK_TYPE,LOCK_DURATION,SOURCE from performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: GLOBAL
        OBJECT_SCHEMA: NULL
          OBJECT_NAME: NULL
            LOCK_TYPE: INTENTION_EXCLUSIVE
        LOCK_DURATION: STATEMENT
               SOURCE: sql_base.cc:5497
*************************** 2. row ***************************
          OBJECT_TYPE: SCHEMA
        OBJECT_SCHEMA: test
          OBJECT_NAME: NULL
            LOCK_TYPE: INTENTION_EXCLUSIVE
        LOCK_DURATION: TRANSACTION
               SOURCE: sql_base.cc:5482
*************************** 3. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: test
          OBJECT_NAME: ti
            LOCK_TYPE: SHARED_NO_READ_WRITE
        LOCK_DURATION: TRANSACTION
               SOURCE: sql_parse.cc:5996

相关源码

bool
lock_table_names(THD *thd,
                 TABLE_LIST *tables_start, TABLE_LIST *tables_end,
                 ulong lock_wait_timeout, uint flags)
{
  ...
  while ((table= it++))
  {
    MDL_request *schema_request= new (thd->mem_root) MDL_request;
    if (schema_request == NULL)
      return true;
    MDL_REQUEST_INIT(schema_request,
                     MDL_key::SCHEMA, table->db, "",
                     MDL_INTENTION_EXCLUSIVE,
                     MDL_TRANSACTION);
    mdl_requests.push_front(schema_request);
  }
  if (need_global_read_lock_protection)
  {
    /*
      Protect this statement against concurrent global read lock
      by acquiring global intention exclusive lock with statement
      duration.
    */
    if (thd->global_read_lock.can_acquire_protection())
      return true;
    MDL_REQUEST_INIT(&global_request,
                     MDL_key::GLOBAL, "", "", MDL_INTENTION_EXCLUSIVE,
                     MDL_STATEMENT);
    mdl_requests.push_front(&global_request);
  }
  ...
  // Phase 3: Acquire the locks which have been requested so far.
  if (thd->mdl_context.acquire_locks(&mdl_requests, lock_wait_timeout))
    return true;
}

在open_table中也会请求锁。

SHARED_NO_READ_WRITE的加锁源码参考LOCK TABLE WITH READ的源码分析。
  • SELECT查询语句的执行

SELECT语句的执行加锁TABLE-TRANSACTION-SHARED_READ锁。

select * from performance_schema.metadata_locks\G
*************************** 1. row ***************************
          OBJECT_TYPE: TABLE
        OBJECT_SCHEMA: test
          OBJECT_NAME: t1
            LOCK_TYPE: SHARED_READ
        LOCK_DURATION: TRANSACTION
          LOCK_STATUS: GRANTED
               SOURCE: sql_parse.cc:5996

源码分析:

class Yacc_state
{
  void reset()
  {
    yacc_yyss= NULL;
    yacc_yyvs= NULL;
    yacc_yyls= NULL;
    m_lock_type= TL_READ_DEFAULT;
    m_mdl_type= MDL_SHARED_READ;
    m_ha_rkey_mode= HA_READ_KEY_EXACT;
  }
}

调用add_table_to_list初始化锁,调用open_table_get_mdl_lock获取锁。

static bool
open_table_get_mdl_lock(THD *thd, Open_table_context *ot_ctx,
                        TABLE_LIST *table_list, uint flags,
                        MDL_ticket **mdl_ticket)
{
  bool result= thd->mdl_context.acquire_lock(mdl_request,
                                           ot_ctx->get_timeout());
}

INSERT/UPDATE/DELETE语句

在open table阶段会获取GLOBAL-STATEMENT-INTENTION_EXCLUSIVE,TABLE-TRANSACTION-SHARED_WRITE。

在commit阶段获取COMMIT-MDL_EXPLICIT-INTENTION_EXCLUSIVE锁。

select OBJECT_TYPE,OBJECT_SCHEMA,OBJECT_NAME,LOCK_TYPE,LOCK_DURATION,SOURCE from performance_schema.metadata_locks\G
OBJECT_TYPE: GLOBAL
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
  LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: STATEMENT
     SOURCE: sql_base.cc:3190
*************************** 2. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: test
OBJECT_NAME: ti
  LOCK_TYPE: SHARED_WRITE
LOCK_DURATION: TRANSACTION
     SOURCE: sql_parse.cc:5996
*************************** 3. row ***************************
OBJECT_TYPE: COMMIT
OBJECT_SCHEMA: NULL
OBJECT_NAME: NULL
  LOCK_TYPE: INTENTION_EXCLUSIVE
LOCK_DURATION: EXPLICIT
     SOURCE: handler.cc:1758
sql/sql_yacc.yy
insert_stmt:
          INSERT                       /* #1 */
          insert_lock_option           /* #2 */

          insert_lock_option:
                    /* empty */   { $$= TL_WRITE_CONCURRENT_DEFAULT; }
                  | LOW_PRIORITY  { $$= TL_WRITE_LOW_PRIORITY; }
                  | DELAYED_SYM
                  {
                    $$= TL_WRITE_CONCURRENT_DEFAULT;

                    push_warning_printf(YYTHD, Sql_condition::SL_WARNING,
                                        ER_WARN_LEGACY_SYNTAX_CONVERTED,
                                        ER(ER_WARN_LEGACY_SYNTAX_CONVERTED),
                                        "INSERT DELAYED", "INSERT");
                  }
                  | HIGH_PRIORITY { $$= TL_WRITE; }
                  ;

//DELETE语句
delete_stmt:
          DELETE_SYM
          opt_delete_options


//UPDATE
update_stmt:
  UPDATE_SYM            /* #1 */
  opt_low_priority      /* #2 */
  opt_ignore            /* #3 */
  join_table_list       /* #4 */
  SET                   /* #5 */
  update_list           /* #6 */

opt_low_priority:
          /* empty */ { $$= TL_WRITE_DEFAULT; }
        | LOW_PRIORITY { $$= TL_WRITE_LOW_PRIORITY; }
        ;

opt_delete_options:
          /* empty */                          { $$= 0; }
        | opt_delete_option opt_delete_options { $$= $1 | $2; }
        ;

opt_delete_option:
          QUICK        { $$= DELETE_QUICK; }
        | LOW_PRIORITY { $$= DELETE_LOW_PRIORITY; }
        | IGNORE_SYM   { $$= DELETE_IGNORE; }
        ;

sql/parse_tree_nodes.cc
bool PT_delete::add_table(Parse_context *pc, Table_ident *table)
{
  ...
  const enum_mdl_type mdl_type=
  (opt_delete_options& DELETE_LOW_PRIORITY) ? MDL_SHARED_WRITE_LOW_PRIO
                                             : MDL_SHARED_WRITE;
  ...
}

bool PT_insert::contextualize(Parse_context *pc)
{
  if (!pc->select->add_table_to_list(pc->thd, table_ident, NULL,
                                   TL_OPTION_UPDATING,
                                   yyps->m_lock_type,
                                   yyps->m_mdl_type,
                                   NULL,
                                   opt_use_partition))
   pc->select->set_lock_for_tables(lock_option);


}

bool PT_update::contextualize(Parse_context *pc)
{
  pc->select->set_lock_for_tables(opt_low_priority);
}

void st_select_lex::set_lock_for_tables(thr_lock_type lock_type)
{
  bool for_update= lock_type >= TL_READ_NO_INSERT;
  enum_mdl_type mdl_type= mdl_type_for_dml(lock_type);
  ...
  tables->mdl_request.set_type(mdl_type);
  ...
}

inline enum enum_mdl_type mdl_type_for_dml(enum thr_lock_type lock_type)
{
  return lock_type >= TL_WRITE_ALLOW_WRITE ?
         (lock_type == TL_WRITE_LOW_PRIORITY ?
          MDL_SHARED_WRITE_LOW_PRIO : MDL_SHARED_WRITE) :
         MDL_SHARED_READ;
}

最终调用open\_table加锁

bool open_table(THD *thd, TABLE_LIST *table_list, Open_table_context *ot_ctx)
{
  if (table_list->mdl_request.is_write_lock_request() &&
     ...
  {
     MDL_REQUEST_INIT(&protection_request,
                  MDL_key::GLOBAL, "", "", MDL_INTENTION_EXCLUSIVE,
                  MDL_STATEMENT);
     bool result= thd->mdl_context.acquire_lock(&protection_request,
                                                ot_ctx->get_timeout());
  }
  ...
  if (open_table_get_mdl_lock(thd, ot_ctx, table_list, flags, &mdl_ticket) ||
  ...
}

在commit阶段调用ha_commit_trans函数时加COMMIT的INTENTION_EXCLUSIVE锁,源码如FLUSH TABLES WITH READ LOCK所述。

如果INSERT/UPDATE/DELETE LOW_PRIORITY语句TABLE上加MDL_SHARED_WRITE_LOW_PRIO锁。

ALTER TABLE ALGORITHM=COPY[INPLACE]

ALTER TABLE ALGORITHM=COPY

COPY方式ALTER TABLE在open_table阶段加GLOBAL-STATEMENT-INTENTION_EXCLUSIVE锁,SCHEMA-TRANSACTION-INTENTION_EXCLUSIVE锁,TABLE-TRANSACTION-SHARED_UPGRADABLE锁。

在拷贝数据前将TABLE-TRANSACTION-SHARED_UPGRADABLE锁升级到SHARED_NO_WRITE。

拷贝完在交换表阶段将SHARED_NO_WRITE锁升级到EXCLUSIVE锁。

源码解析:

GLOBAL、SCHEMA锁初始化位置和LOCK TABLE WRITE位置一致都是在lock_table_names函数中。在open_table中也会请求锁。

sql/sql_yacc.yy
alter:
          ALTER TABLE_SYM table_ident
          {
            THD *thd= YYTHD;
            LEX *lex= thd->lex;
            lex->name.str= 0;
            lex->name.length= 0;
            lex->sql_command= SQLCOM_ALTER_TABLE;
            lex->duplicates= DUP_ERROR;
            if (!lex->select_lex->add_table_to_list(thd, $3, NULL,
                                                    TL_OPTION_UPDATING,
                                                    TL_READ_NO_INSERT,
                                                    MDL_SHARED_UPGRADABLE))

bool mysql_alter_table(THD *thd, const char *new_db, const char *new_name,
                       HA_CREATE_INFO *create_info,
                       TABLE_LIST *table_list,
                       Alter_info *alter_info)
{
  //升级锁
  if (thd->mdl_context.upgrade_shared_lock(mdl_ticket, MDL_SHARED_NO_WRITE,
                                           thd->variables.lock_wait_timeout)
      || lock_tables(thd, table_list, alter_ctx.tables_opened, 0))
  ...
  if (wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_RENAME))

}


bool wait_while_table_is_used(THD *thd, TABLE *table,
                              enum ha_extra_function function)
{
  DBUG_ENTER("wait_while_table_is_used");
  DBUG_PRINT("enter", ("table: '%s'  share: 0x%lx  db_stat: %u  version: %lu",
                       table->s->table_name.str, (ulong) table->s,
                       table->db_stat, table->s->version));

  if (thd->mdl_context.upgrade_shared_lock(
             table->mdl_ticket, MDL_EXCLUSIVE,
             thd->variables.lock_wait_timeout))

ALTER TABLE INPLACE的加锁:

INPLACE方式在打开表的时候也是加GLOBAL-STATEMENT-INTENTION_EXCLUSIVE锁,SCHEMA-TRANSACTION-INTENTION_EXCLUSIVE锁,TABLE-TRANSACTION-SHARED_UPGRADABLE锁。

在prepare前将TABLE-TRANSACTION-SHARED_UPGRADABLE升级为TABLE-TRANSACTION-EXCLUSIVE锁。

在prepare后会再将EXCLUSIVE根据不同引擎支持情况降级为SHARED_NO_WRITE(不允许其他线程写入)或者SHARED_UPGRADABLE锁(其他线程可以读写,InnoDB引擎)。

在commit前,TABLE上的锁会再次升级到EXCLUSIVE锁。

sql/sql_table.cc
static bool mysql_inplace_alter_table(THD *thd,
                                      TABLE_LIST *table_list,
                                      TABLE *table,
                                      TABLE *altered_table,
                                      Alter_inplace_info *ha_alter_info,
                                      enum_alter_inplace_result inplace_supported,
                                      MDL_request *target_mdl_request,
                                      Alter_table_ctx *alter_ctx)
{
  ...
  else if (inplace_supported == HA_ALTER_INPLACE_SHARED_LOCK_AFTER_PREPARE ||
           inplace_supported == HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE)
  {
    /*
      Storage engine has requested exclusive lock only for prepare phase
      and we are not under LOCK TABLES.
      Don't mark TABLE_SHARE as old in this case, as this won't allow opening
      of table by other threads during main phase of in-place ALTER TABLE.
    */
    if (thd->mdl_context.upgrade_shared_lock(table->mdl_ticket, MDL_EXCLUSIVE,
                                             thd->variables.lock_wait_timeout))
  ...
  if (table->file->ha_prepare_inplace_alter_table(altered_table,
                                                ha_alter_info))
  ...
  if ((inplace_supported == HA_ALTER_INPLACE_SHARED_LOCK_AFTER_PREPARE ||
     inplace_supported == HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE) &&
    !(thd->locked_tables_mode == LTM_LOCK_TABLES ||
      thd->locked_tables_mode == LTM_PRELOCKED_UNDER_LOCK_TABLES) &&
    (alter_info->requested_lock != Alter_info::ALTER_TABLE_LOCK_EXCLUSIVE))
  {
    /* If storage engine or user requested shared lock downgrade to SNW. */
    if (inplace_supported == HA_ALTER_INPLACE_SHARED_LOCK_AFTER_PREPARE ||
        alter_info->requested_lock == Alter_info::ALTER_TABLE_LOCK_SHARED)
      table->mdl_ticket->downgrade_lock(MDL_SHARED_NO_WRITE);
    else
    {
      DBUG_ASSERT(inplace_supported == HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE);
      table->mdl_ticket->downgrade_lock(MDL_SHARED_UPGRADABLE);
    }
  }
  ...
  // Upgrade to EXCLUSIVE before commit.
  if (wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_RENAME))
  ...
  if (table->file->ha_commit_inplace_alter_table(altered_table,
                                               ha_alter_info,
                                               true))
}

CREATE TABLE 加锁

CREATE TABLE先加锁GLOBAL-STATEMENT-INTENTION_EXCLUSIVE,SCHEMA-MDL_TRANSACTION-INTENTION_EXCLUSIVE,TABLE-TRANSACTION-SHARED。

表不存在则升级表上的SHARED锁到EXCLUSIVE。

bool open_table(THD *thd, TABLE_LIST *table_list, Open_table_context *ot_ctx)
{
  ...
  if (!exists)
  {
    ...
    bool wait_result= thd->mdl_context.upgrade_shared_lock(
                         table_list->mdl_request.ticket,
                         MDL_EXCLUSIVE,
                         thd->variables.lock_wait_timeout);
    ...
  }
  ...
}

DROP TABLE 加锁

drop table语句执行加锁GLOBAL-STATEMENT-INTENTION_EXCLUSIVE,SCHEMA-MDL_TRANSACTION-INTENTION_EXCLUSIVE,TABLE-EXCLUSIVE。

drop:
          DROP opt_temporary table_or_tables if_exists
          {
            LEX *lex=Lex;
            lex->sql_command = SQLCOM_DROP_TABLE;
            lex->drop_temporary= $2;
            lex->drop_if_exists= $4;
            YYPS->m_lock_type= TL_UNLOCK;
            YYPS->m_mdl_type= MDL_EXCLUSIVE;
          }

Influxdb · 源码分析 · Influxdb cluster实现探究

$
0
0

背景

Influxdb cluster在0.12版本的release notes中声明,将cluster代码闭源,并且将cluster版本作为商业版售卖。

虽然cluster版本毕源已经一年多的时间,从目前官网中release notes来看,总体的设计没有发生变化。

本文主要探讨influxdb cluster 0.11版本的实现细节,学习参考。本文将参考官方博客中的一篇设计文稿,同时参考Influxdb最后的一个cluster开源版本的源代码,分析influxdb cluster实现,水平有限,欢迎指正。

参考博文 :

https://www.influxdata.com/blog/influxdb-clustering-design-neither-strictly-cp-or-ap/

https://www.influxdata.com/blog/influxdb-clustering/

https://docs.influxdata.com/enterprise_influxdb/v1.3/concepts/clustering/

https://docs.influxdata.com/enterprise_influxdb/v1.3/guides/anti-entropy/

InfluxDB Clustering Design – neither strictly CP or AP

cluster设计约定

下述为官方的influxdb cluster设计约定。

  • 时间序列数据总是新数据,且数据写入后是不会发生变化的。
  • 如果同一时间点写入了多次同样的数据,数据肯定是重复写入的
  • 基本不会出现删除数据的情况, 删除数据基本上都是对过期数据的一个大批量的过期删除。
  • 基本不会存在更新存量数据的情况,并发的修改不会出现
  • 绝大对数的写入都是最近时间的数据
  • 数据的规模会非常大,很多场景下数据量大小会在TB和PB量级。
  • 能够写入和查询数据会比强一致性更加重要
  • 很多time series非常短暂的存在,所以time series 数量比较大。

需求

  • Horizontally scalable - 设计需要能够支持数百个节点,并且可以扩展到数千个节点。 读写能力需要按照节点数量线性增长。
  • Available - 对于读写能力,我们需要AP design,既可用性优先。Time series不断写入增长,对大多数最近的写入数据不需要强一致性。
  • 需要支持十亿级别的 time series, 一个独立的series由 measurement name + tag set 组成。 这个量级的需求原因是我们会存在短暂的 time series的假设。
  • Elastic - 架构设计上集群中节点可以删除和增加。

cluster design

系统架构图

image.png

Cluster Metadata – CP

首先解释这个CP的含义是CAP理论中的CP,C: consistence一致性 , P表示PARTITION TOLERANCE,CP表示cluster 元数据的服务,更加着重保障系统的一致性。

图中上半部分是metadata node, 存储元数据包括:

  • 集群中主机信息,包括meta node和data node.
  • databases, retention policies, continuous queries
  • users and permissions
  • Shard groups - 每个shards的开始和结束时间
  • Shars - shard id 落在哪个server上的元信息

每个metadata 节点会通过一个简单的http 对外提供服务,其中raft的实现是: https://github.com/hashicorp/raft,raft底层数据存储使用Boltdb。

每个server (datanode)会保持一份cluster metadata的数据拷贝,会定时调用http接口获取存量元数据感知元数据的变化。当request请求到该data node时,如果cache miss, 此时调用metadata获取对应的元数据。

Cluster Data Write – AP

AP的含义是CAP理论中的AP,表示系统系统设计着重保障系统的可用性,舍弃一致性。下述介绍一个write请求的访问流程。

Shard Group

image.png

前提:

  • 如果一个集群中存在server 1,2,3,4 , client的写入请求可以随意访问server1,2,3,4的任何一台上,然后由data node路由到其他的server上,如本例子中请求落在了1中。

Shard Group是什么?

  • influxdb中将数据根据时间进行分区写入到不同的shard中。如influxdb会划分24 hours为一个时间分区,根据写入point的时间归属写入到不同的shard中。
  • 由于influxdb cluster为了能够写入能力随机器数量横向扩展,所以虽然还按照24 hours作为一个shard,但是会将写入数据进行hash到不同的后端server中存储。例如有10台机器,那么写入会打散在10台机器上。写入性能提高了10倍,Shard Group就是指这10台机器组成的shard。
  • 当然由于有数据冗余的存在,如设置用户数据需要写入2份,那么实际上10台机器写入性能提高5倍。

image.png

  type ShardGroupInfo struct {
  	ID        uint64
  	StartTime time.Time
  	EndTime   time.Time
  	DeletedAt time.Time
  	Shards    []ShardInfo
  }

一个Shard Group映射多个ShardInfo。并且Shard Group有StartTime, EndTime等属性。

type ShardInfo struct {
	ID     uint64
	Owners []ShardOwner
}

每个ShardInfo属于多个ShardOwner,如用户设置数据需要写入2份,那么每个ShardInfo既对应存在两个ShardOwner (data nodes)。

Steps:
  1. 根据写入point的timestamp, 获取写入的数据属于哪个shard group。如果shard group信息在data node中不存在,调用metadata service获取。
  2. cluster metadata service会负责规划这些数据分配到哪些节点上。shard groups一般会在写入请求到来之前提前创建,防止批量请求的数据量过大冲击到cluster metadata service。
  3. measurement name和tagset相加进行hash并且根据shard group中ShardInfo的数量进行取余,那么最近的一些时序数据就会平均写入到每一台服务器上,分散写入压力。注意此时的hash算法不是一致性hash, 原因是
  • 写入:集群规模resize后我们并不需要考虑将老的shard group重新hash, 因为此时老的shard group已经没有了写入,这样就不需要再对写入请求重新负载均衡。
  • 对于老的数据读能力的横向扩展比较简单,仅仅需要将写入shards拷贝到集群中其他的data node上。

Write Points to Shard

write定位到对应server的shard后,开始写入到shard流程,如下图所示

  • 根据shard group流程计算出requests写入3副本,且对应的shard存储的机器为2,3,4
  • Http write request 发送到了server 1,server 1会将请求路由到2,3,4,并且根据下述的写入级别判断是否应该返回给client此次写入成功。

image.png

每个写入需要设置一致性级别 (consistency level)

  • Any - 任何一台 server 接收到了写入请求,虽然写入失败,但是只要写入到 hinted handoff,此时就算成功。hinted handoff模块后续会详做介绍,这里可以理解为写入失败,但是写入到了一个失败队列文件中,这个失败队列会不断的重试这个请求,但是实际上也并不保证这个写入一定成功。
  • one - 任何一台server写入成功
  • quorun - 多数派写入成功
  • all - 所有写入成功

值得注意的是:

  • 按照这种写入设计,不能保证一个shard对应的server 2,3,4,其中任何一台server上的数据是全量最新的。不过本文最开头也描述了influxdb cluster设计哲学,时序数据的场景,成功写入和查询数据比强一致性更加重要。
  • 如果client写入失败,实际上部分主机上的写入可能是成功的,由于hinted handoff的存在,理论上写入最终是会成功写入的。这种行为也是符合本文开头的设计:时序数据的场景数据一旦生成是不会变化,既如果client发现写入失败重试对于server中存储的数据仅仅也是出现两份同样的数据,后续做一次compaction就可以了。

Hinted Handoff

下面我们详细描述下Hinted Handoff的工作机制。

Hinted handoff 帮忙我们快速恢复这些短时间内的写入失败,如server 重启或者由于gc 中断, 高查询导致系统负载过高而导致server的临时不可用。

在我们先前示例中,当写入到达server 1后, 会尝试写入到server 2, 3, 4. 当写入4失败时,server 1的hinted handoff会不断尝试将失败的数据写入到server 4中。

hinted handoff的实现机制:

  • hinted handoff实际后端存储为 cluster中所有其他的data node分别存储很多的queue files.

image.png

  • Queue files默认10MB一个,分成很多的Segment,方便清理过期数据。

image.png

  • 每个queue file格式如下,每个Block对应写入失败的points,Footer存储当前读取的offset point,所以每次写入时会重新写这个offset, 且读写文件不能同时进行。

image.png

  • 看看hinted handoff提供的配置项功能,默认hinted handoff是有最大大小和最大存活时间的限制,所以理论上还是会存在数据丢失的可能。Blog中声称会提供anti-entropy的一个模块来保证数据的最终一致性,但是在0.11版本开源的cluster代码并没有看到这个模块。
type Config struct {
	Dir              string        `toml:"dir"`
	MaxSize          int64         `toml:"max-size"`   		// 默认一个队列最大大小为1G
	MaxAge           toml.Duration `toml:"max-age"`		    // 默认Age为7天
	RetryRateLimit   int64         `toml:"retry-rate-limit"`	// 重试的频率,单位为bytes per second. 默认不限制 
	RetryInterval    toml.Duration `toml:"retry-interval"`	  // 重试失败的时间间隔,按照指数级增长直到达到最大重试间隔。默认1秒
	RetryMaxInterval toml.Duration `toml:"retry-max-interval"`		// 重试最大间隔,默认1分钟
	PurgeInterval    toml.Duration `toml:"purge-interval"`			// 清理间隔,既将由于过期,或者不活跃的节点的数据做清理,默认1小时
}

Anti-Entropy Repair

Anti-entropy repaire确保我们能够达到我们数据的最终一致性。集群中的servers 会定期的交换信息确保他们拥有同样的数据。

  • Anti-entropy repaire使用Merkil Tree来对每个shard中数据进行比较,并取两个比较数据之间差异的并集。
  • Anti-entropy还能够将shard数据迁移到目标的data node中。

这么设计的原因,如官方在开头中对时序数据使用场景的假设:

  1. 所有的数据写入都是针对最近的时间。所以老的shard不应该会有频繁的数据的变更。

非常遗憾的是,0.11版本的cluster源代码并没有实现Anti-Entropy Repair的功能。具体实现细节不做介绍。

Conflict Resolution

当存在update同一条数据的场景时,就会出现冲突的情况。influxdb cluster解决冲突的办法非常简单:the greater value wins。这种方案使得解决冲突的代价非常低,但是实际上也带来了一些问题。

如:

  1. 当一条update请求到server 1,需要更新server 1, 2, 3三副本中的数据。此时update 在server 3上执行失败了,那么数据在server 3上实际上仍然为老数据。
  2. 后续当对同一条记录执行update操作时,此时三副本的server执行全部成功,此时server1, 2, 3上面 的数据全部被update成功。
  3. 第一条update记录在server 3上通过hinted handoff模块写入成功。此时server 1,2,3上面的数据将会不一致。最终通过Anti-Entropy Repair模块将数据做最终校验,按照文中描述可能会取两次update更大的值。实际上update操作的结果是不可预期的。

influxdb cluster的设计基本不过多考虑这种时序数据中少见的delete和update操作。但是将update/deletec操作的一致性级别设置为ALL保证delete和update成功是减少上述问题发生概率的方法(即使设置成ALL,如果一旦delete失败也是有不符合预期的情况存在),但是设置为ALL后服务的高可用性会有一定的影响。

Design Conclusion

官方对influxdb cluster的设计概述:

influxdb cluster的设计既不是纯粹的CP或存粹的AP系统,cluster一部分是CP系统,但是数据保证了最终一致性。一部分是AP系统,如果出现较长时间meta nodes和data nodes之间的分区,可用性是不能保证的。上述设计历经influxDB cluster的三次迭代,根据实际的需求做了很多的trade-offs,设计更多的倾向cluster需要实现的最重要的目标: 数据可以水平扩展和写入更低的开销。

总结

虽然非常遗憾cluster后续版本没有开源,但是influxdb cluster总体设计已经非常完善,并且开源出来的0.11版本也是非常具有参考价值。

目前开源的Influxdb cluster 0.11 版本缺乏能力包括:

  • 宕机迁移功能,如果data nodes存在主机宕机且无法恢复,那么这台主机上存量的shards需要迁移到其他的机器上。
  • 运维管理功能,比如meta nodes主机如何替换上线另外一台meta nodes等。 如官方收费版提供的功能: https://docs.influxdata.com/enterprise_influxdb/v1.3/features/cluster-commands/

如果能够补齐以下两块能力,你觉得使用influxdb cluster符合你的业务场景么?你会用么?

MySQL · 源码分析 · 权限浅析

$
0
0

两个权限问题

初始化的 Test Database 的权限

如果要使用 MySQL 数据库,要由高权限账号创建一个用户,再赋予这个用户相应的权限,用户就可以连接到数据库进行权限范围内的操作。参考官方文档 Create user , Grant privilegs

所以我们执行以下语句,创建一个用户 messi,并且只赋予所有数据库上的 SELECT 权限:

mysql> create user 'messi'@'%' identified by 'pass';
Query OK, 0 rows affected (0.00 sec)

mysql> grant SELECT on *.* to 'messi'@'%';
Query OK, 0 rows affected (0.00 sec)

接着用刚刚创建的账号登录 MySQL,执行如下操作:

mysql> use test;

mysql> CREATE TABLE `chelsea` (`id` int, `goal` int);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into chelsea values(2, 3);
Query OK, 1 row affected (0.01 sec)

mysql> select * from chelsea;
+------+------+
| id   | goal |
+------+------+
|    2 |    3 |
+------+------+
1 row in set (0.00 sec)

这是怎么一回事呢? 明明在创建用户的时候只赋予了 SELECT 权限,竟然可以执行 INSERT 操作了。

Super 权限

Super 权限相当于 Linux 的 Root 权限,但是它能够为所欲为吗?

mysql> use performance_schema;

mysql> CREATE TABLE `chelsea` (`id` int, `goal` int);
ERROR 1142 (42000): CREATE command denied to user 'root'@'127.0.0.1' for table 'chelsea'

我们尝试在 performance_schema 表中创建一张表,可是看到 root 用户被无情的告知权限不足。

以上述两个问题为引,这篇文章简单介绍一下 MySQL 的权限体系。

权限简介

官方文档对权限有比较详细的描述,为了方便我把其中的表格列在下面。第一列表示所有的权限,可以在 Grant 语句中指定的,第二列是对应权限存储在系统数据库 mysql 几张表中的定义,第三列表示权限作用的范围,Global(Server administration)对应 mysql.user 表,Database 对应 mysql.db 表,Tables 对应 mysql.tables_priv 表,Columns 对应 mysql.columns_priv 表,Stored routines 对应 mysql.procs_priv 表。

PrivilegeColumnContext
ALL [PRIVILEGES]Synonym for “all privileges”Server administration
ALTERAlter_privTables
ALTER ROUTINEAlter_routine_privStored routines
CREATECreate_privDatabases, tables, or indexes
CREATE ROUTINECreate_routine_privStored routines
CREATE TABLESPACECreate_tablespace_privServer administration
CREATE TEMPORARY TABLESCreate_tmp_table_privTables
CREATE USERCreate_user_privServer administration
CREATE VIEWCreate_view_privViews
DELETEDelete_privTables
DROPDrop_privDatabases, tables, or views
EVENTEvent_privDatabases
EXECUTEExecute_privStored routines
FILEFile_privFile access on server host
GRANT OPTIONGrant_privDatabases, tables, or stored routines
INDEXIndex_privTables
INSERTInsert_privTables or columns
LOCK TABLESLock_tables_privDatabases
PROCESSProcess_privServer administration
PROXY Seeproxies_privtable Server administration
REFERENCESReferences_privDatabases or tables
RELOADReload_privServer administration
REPLICATION CLIENTRepl_client_privServer administration
REPLICATION SLAVERepl_slave_privServer administration
SELECTSelect_privTables or columns
SHOW DATABASESShow_db_privServer administration
SHOW VIEWShow_view_privViews
SHUTDOWNShutdown_privServer administration
SUPERSuper_privServer administration
TRIGGERTrigger_privTables
UPDATEUpdate_privTables or columns
USAGESynonym for “no privileges”Server administration

权限存储

GRANT 语句赋予对应用户相应的权限,会根据不同的语法存储到不同的表中,以链接中官方文档中的语句为例:

Global Privileges

GRANT ALL ON *.* TO 'someuser'@'somehost';
GRANT SELECT, INSERT ON *.* TO 'someuser'@'somehost';

其中 *.* 表示所有数据的所有表,对应的权限会保存在 mysql.user 表中,和 user 相关联。

Database Privileges

GRANT ALL ON mydb.* TO 'someuser'@'somehost';
GRANT SELECT, INSERT ON mydb.* TO 'someuser'@'somehost';

其中 mydb.* 表示 mydb Database 下的所有表,对应的权限会保存在 mysql.db 表中,和 db 相关联。

Table Privileges

GRANT ALL ON mydb.mytbl TO 'someuser'@'somehost';
GRANT SELECT, INSERT ON mydb.mytbl TO 'someuser'@'somehost'; 

对应的权限保存在 mysql.tables_priv 中,和 db , user 关联。

Column Privileges

GRANT SELECT (col1), INSERT (col1,col2) ON mydb.mytbl TO 'someuser'@'somehost';

对应的权限保存在 mysql.tables_priv 中,和 db, table, user 关联。

Stored Routine Privileges

GRANT CREATE ROUTINE ON mydb.* TO 'someuser'@'somehost';
GRANT EXECUTE ON PROCEDURE mydb.myproc TO 'someuser'@'somehost';

对应的权限保存在 mysql.procs_priv 中,和 routine_name, db,user 关联。

认证过程

在源码中,每一种 Privilege 都用一个 bit 位表示,具体宏定义在 sql_acl.h中,几乎所有的语句操作都需要进行权限验证,根据不同的语句类型,可以获取到需要哪些权限,参考函数 mysql_execute_command,获得一个重要的长整型参数 want_access, 表示需要的权限有哪些。

整体认证的思路比较简单,对于权限的判断自然是自上而下的,假如一个用户有对某个数据库的写权限,自然不必继续判断对该数据库下的某个表是否有写权限。 从上述存储的表中可以查到对应的权限,然后和 want_access 进行位操作,判断是否包含了 want_access 需要的全部权限。 考虑一下这种情况,假如一个用户对某个数据库只有SELECT 权限,但是对该数据库其中的一张表只有 INSERT 权限,假如对表请求的 want_access 是 3 (二进制 11 表示 SELECT 和 INSERT 权限),自上而下先先判断数据库的权限,无法满足,接着再判断表的权限,依然无法满足。但是数据库的 SELECT 权限实际上表示对表也有 SELECT 权限,只是没有保存到mysql.tables_priv 表中罢了。所以在自上而下认证的过程中需要把上级已经获得的权限传递给下级。权限的使用频率非常高,如果每次都从数据库中查找效率太低,MySQL 将其缓存起来,在早期月报中就讨论过权限的缓存,可以参考。

下面从源码角度看一下上述过程是怎么实现的,主要的函数有两个,check_access 判断 Global 和 Database 级别, check_grant 判断 Table 级别,一般会先调用 check_access 再接着调用 check_grant ,对于 Column 级别的需要查表判断对应列是否存在等,暂且不讨论,判断的原理都相似。

bool
check_access(THD *thd, ulong want_access, const char *db, ulong *save_priv,
             GRANT_INTERNAL_INFO *grant_internal_info,
             bool dont_check_global_grants, bool no_errors)

其中 save_priv 就是传递给下级的权限,一般会在 check_grant 中使用。在函数开头就初始化为 0.

if ((db != NULL) && (db != any_db))
{
   const ACL_internal_schema_access *acces
	...
}

这部分是对 Performance_schema 和 Informantion_schema 判断的逻辑,下一节会详细介绍。

if ((sctx->master_access & want_access) == want_access)
{
	...
	
	if(..)
		*save_priv|= sctx->master_access | db_access;
	else
		*save_priv|= sctx->master_access;
	
	DBUG_RETURN(FALSE);
}

sctx->master_access 是从 mysql.user 表中获得的 Global 级别的权限,在用户和数据库建立连接就会初始化,上述代码表示全局的级别已经满足了 want_access 申请的权限。由于还要调用 check_grant , 在末尾把全局权限放到 save_priv 中。

if (((want_access & ~sctx->master_access) & ~DB_ACLS) ||
      (! db && dont_check_global_grants))
{	
	 ...
	 
	 DBUG_RETURN(TRUE);		
}

DB_ACLS 是一个宏定义,表示 db 级别的所有权限集合,根据判断条件来看,如果申请的权限没有全部在 sctx->master_access 中满足,并且也不属于 DB_ACLS 的一种,那么认为是无法获得的。或者是传入参数 db 为空,并且参数 dont_check_global_grants 为 true,也返回校验失败。这个逻辑还没有走到过,暂且记下。

  if (db == any_db)
  {
    /*
      Access granted; Allow select on *any* db.
      [out] *save_privileges= 0
    */
    DBUG_RETURN(FALSE);
  }

这个是处理一些通用的情况,不涉及具体的 db。

if (db && (!thd->db || db_is_pattern || strcmp(db,thd->db)))
    db_access= acl_get(sctx->get_host()->ptr(), sctx->get_ip()->ptr(),
                       sctx->priv_user, db, db_is_pattern);
  else
    db_access= sctx->db_access;

到这里 Global 级别就判断完了,thd->db 表示当前的数据库是哪一个,也就是执行了 use db 命令之后切换的数据库,切换之后该数据的权限会放到 sctx->db_access 中,上述判断就是如果 db 不是当前 db,就从缓存里面查找。

  db_access= (db_access | sctx->master_access);
  *save_priv|= db_access;

传递 db_access 下去。

if ( (db_access & want_access) == want_access ||
      (!dont_check_global_grants &&
       need_table_or_column_check))
  {
    DBUG_RETURN(FALSE);
  }
  
  ...
  
 DBUG_RETURN(TRUE);

表示 db_access 已经可以满足 want_access 或者需要 table/column 级别的校验。如果上述校验都没有通过,则返回校验失败。

仔细看完 check_access 函数,check_grant 就相对简单一些, 看下主要逻辑


for (tl= tables;
       tl && number-- && tl != first_not_own_table;
       tl= tl->next_global)
  {
  		if (access)
    { 
    	...
    	// Information_schema && performance_schema
	 }
	 
	 want_access= orig_want_access;
    want_access&= ~sctx->master_access;     // 判断当前的 Global access 是否能够满足
    if (!want_access)
      continue;       
	 
	 if (!(~t_ref->grant.privilege & want_access) ||
        t_ref->is_anonymous_derived_table() || t_ref->schema_table)
    {
    	// t_ref->grant.privilege 实际上就是 check_access 中的 save_priv, 这里表示继承而来的权限能够满足
    	// 其它条件处理继承表和特殊表,暂且不论
    }
	 
	 if (is_temporary_table(t_ref))
    {
    	// 处理临时表权限
    }
    
    GRANT_TABLE *grant_table= table_hash_search(sctx->get_host()->ptr(),
                                                sctx->get_ip()->ptr(),
                                                t_ref->get_db_name(),
                                                sctx->priv_user,
                                                t_ref->get_table_name(),
                                                FALSE);
    // 查找对应的 table 的权限,从 mysql.table_priv 中
    
    ...
    t_ref->grant.privilege|= grant_table->privs; // 把刚刚获取的 table_privs 增加到继承的权限中
    ...
    
    if (!(~t_ref->grant.privilege & want_access))
      continue;
    // 判断加上 table_privs 之后是否可以满足条件
    
    ...
  }

问题分析

再来看一下文章开头说的 test Database 权限问题,我们执行的 grant 语句相当于是给 mysql.user 表增加一条记录,是全局级别的。根据上述判断逻辑,Global 的权限满足不了,就要去 mysql.db 中判断,查一下很容易就可以发现,对于任意的用户都可以在 test Database 上增删改查。

mysql> select * from db\G
*************************** 1. row ***************************
                 Host: %
                   Db: test
                 User:
          Select_priv: Y
          Insert_priv: Y
          Update_priv: Y
          Delete_priv: Y
          ...

因为 test 是系统初始化的数据库,意图是让更多的用户可以使用,其实这个也可以在 mysql_system_tables_data.sql 中找到一条记录,赋予了 test Database 权限。

-- Fill "db" table with default grants for anyone to
-- access database 'test' and 'test_%' if "db" table didn't exist
CREATE TEMPORARY TABLE tmp_db LIKE db;
INSERT INTO tmp_db VALUES ('%','test','','Y','Y','Y','Y','Y','Y','N','Y','Y','Y','Y','Y','Y','Y','Y','N','N','Y','Y');
INSERT INTO tmp_db VALUES ('%','test\_%','','Y','Y','Y','Y','Y','Y','N','Y','Y','Y','Y','Y','Y','Y','Y','N','N','Y','Y');
INSERT INTO db SELECT * FROM tmp_db WHERE @had_db_table=0;
DROP TABLE tmp_db;

假如我们换一种赋权限的方式:

grant SELECT on test.* to 'messi'@'%';

这样 INSERT 语句就会失败,因为在查找的时候,并不是随意找一个可以匹配的,而是找最匹配的一个,看看是不是有需要的权限。

Performance_schema 和 Information_schema

这两个系统表比较特殊,Performance_schema 只有表结构定义文件,没有数据文件,数据来自 mysql 表,而 Information_schema 表连表结构定义都没有,当需要查询的时候在内存中构造。所以对于这两个表的存取权限就是独立的一套机制。同样分为 db 级别和 table 级别,在 check_access 和 check_grant 中调用。

ACL_internal_shcema_access

ACL_internal_shcema_access 是一个父类,有两个子类分别表示两种数据库,类图如下:

校验的时候首先根据传入的 db 名称获得对应的子类:

const ACL_internal_schema_access *access;
access= get_cached_schema_access(grant_internal_info, db);

然后再调用子类的 check 函数完成校验,对于 Information_schema 表,只允许 SELECT 权限,如果申请其它权限并且在 DB_ACL 宏定义中,那么就继续从 table 级别判断,否则的话就拒绝。

对于 Performance_schema 表,函数里表述的也非常清楚,定义了一个变量 always_forbbiden , 如果申请的权限全部包括在其中,就拒绝,否则走 table 级别判断。源码中屏蔽的权限有:

  const ulong always_forbidden= /* CREATE_ACL | */ REFERENCES_ACL
    | INDEX_ACL | ALTER_ACL | CREATE_TMP_ACL | EXECUTE_ACL
    | CREATE_VIEW_ACL | SHOW_VIEW_ACL | CREATE_PROC_ACL | ALTER_PROC_ACL
    | EVENT_ACL | TRIGGER_ACL ;

ACL_internal_table_access

同样的,ACL_internal_table_access 也是父类,但是却有众多子类,使用方式和上述 schema_access 有较大区别。校验首先根据 db 名称和 table 名称查找对应的 ACL_internal_table_access 子类,查找过程分为两步:

  1. 根据 db 名称找到对应的 ACL_internal_schema_access
  2. 调用 ACL_internal_schema_access 的 look_up 方法查找

其中 IS_internal_schema_access 的 look_up 非常简单,直接返回 NULL,表示 information_schema 不支持 table 级别的校验。

相对 PFS_internal_schema_access 复杂一些,首先根据 table name 去查 PFS_engine_table_share,这个类里面有对应 table 的 acl 信息:

const ACL_internal_table_access *
PFS_internal_schema_access::lookup(const char *name) const
{
  const PFS_engine_table_share* share;
  share= PFS_engine_table::find_engine_table_share(name);
  if (share)
    return share->m_acl
  ...
  return &pfs_unknown_acl
 }

而 PFS_engine_table::find_engine_table_share(name) 这个函数是根据 name 从一个静态数组 all_shars 中比较获取, 而 all_share 的初始化是从不同的类中的静态成员变量 m_share 中获取,以表 performance_schema.user 为例,有一个类 table_users :

/** Table PERFORMANCE_SCHEMA.USERS. */
class table_users : public cursor_by_user
{
public:
  /** Table share */
  static PFS_engine_table_share m_share;
  ...
}

其实 performance_schema 中的每一张表都对应一个类,它们有共同的父类,继承结构查看这里。而每一个类中都有一个静态变量 m_share,编译时就会初始化,仍然以 table_users 为例:

PFS_engine_table_share
table_users::m_share=
{
  { C_STRING_WITH_LEN("users") },
  &pfs_truncatable_acl,
  &table_users::create,
  NULL, /* write_row */
  table_users::delete_all_rows,
  NULL, /* get_row_count */
  1000, /* records */
  sizeof(PFS_simple_index), /* ref length */
  &m_table_lock,
  &m_field_def,
  false /* checked */
};

其中 pfs_truncatable_acl 就是我们需要的 ACL_internal_table_access 具体的子类,它像 ACL_internal_shcema_access 的校验一样,在 check 函数里定义了 always_forbidden 变量,代表这个类型的权限都被拒绝。这里权限并不是每一个表对应一种,代码中定义几种不同类型的权限,提供给所有的表去使用,看一下类图: 如果想知道具体某个表的权限,代码里查一下就清楚了。所以其实 performance_schema 中 table 的权限都是写死在代码里的(显然 super 用户也无能为力)。

问题分析

最后我们看下文章开头提出的问题,super 用户无法在 performance_schema 中创建一个表,其实很明显,一个新创建的表名是在代码中是没有定义的,所以根本找不到对应的 PFS_engine_table_share, 看上面的 look_up 代码,返回的是 pfs_unknown_acl ,而这个类的 always_forbidden 变量屏蔽了 CREATE 权限,自然 Super 用户就没办法了~(PS,可以试验一下 DROP 一个现有的表,重新 CREATE 是没问题的)

PgSQL · 源码分析 · AutoVacuum机制之autovacuum worker

$
0
0

背景

根据之前月报的分析,PostgreSQL数据库为了定时清理因为MVCC 引入的垃圾数据,实现了自动清理机制。其中涉及到了两种辅助进程:

  • autovacuum launcher
  • autovacuum worker

其中,autovacuum launcher 主要负责调度autovacuum worker,autovacuum worker进程进行具体的自动清理工作。本文主要是对autovacuum worker进行分析。

相关参数

除了之前月报提到的参数track_counts,autovacuum,autovacuum_max_workers,autovacuum_naptime,autovacuum_vacuum_cost_limit,autovacuum_vacuum_cost_delay,autovacuum_freeze_max_age,autovacuum_multixact_freeze_max_age之外,autovacuum worker还涉及到以下参数:

  • log_autovacuum_min_duration:所有运行超过此时间或者因为锁冲突而退出的autovacuum 会被打印在日志中,该参数每个表可以单独设置。
  • autovacuum_vacuum_threshold :与下文的autovacuum_vacuum_scale_factor配合使用,该参数每个表可以单独设置。
  • autovacuum_analyze_threshold:与下文的autovacuum_analyze_scale_factor配合使用,该参数每个表可以单独设置。
  • autovacuum_vacuum_scale_factor :当表更新或者删除的元组数超过autovacuum_vacuum_threshold+ autovacuum_vacuum_scale_factor* table_size会触发VACUUM,该参数每个表可以单独设置。
  • autovacuum_analyze_scale_factor:当表插入,更新或者删除的元组数超过autovacuum_analyze_threshold+ autovacuum_analyze_scale_factor* table_size会触发ANALYZE,该参数每个表可以单独设置。
  • vacuum_cost_page_hit:清理一个在共享缓存中找到的缓冲区的估计代价。它表示锁住缓冲池、查找共享哈希表和扫描页内容的代价。默认值为1。
  • vacuum_cost_page_miss:清理一个必须从磁盘上读取的缓冲区的代价。它表示锁住缓冲池、查找共享哈希表、从磁盘读取需要的块以及扫描其内容的代价。默认值为10。
  • vacuum_cost_page_dirty:当清理修改一个之前干净的块时需要花费的估计代价。它表示再次把脏块刷出到磁盘所需要的额外I/O。默认值为20。

其中,autovacuum_vacuum_threshold和autovacuum_vacuum_scale_factor参数配置会决定VACUUM 的频繁程度。因为autovacuum会消耗一定的资源,设置的不合适,有可能会影响用户的其他正常的查询。对PostgreSQL使用者来说,一般有2种方案:

  • 调大触发阈值,在业务低峰期,主动去做VACUUM。在VACUUM过程中,性能可能会出现抖动。
  • 调小触发阈值,将清理工作分摊到一段时间内。但是参数如果设置不合理,会使得正常查询性能都会下降。

为了降低对并发正常查询的影响,autovacuum引入了vacuum_cost_delay,vacuum_cost_page_hit,vacuum_cost_page_miss,vacuum_cost_page_dirty,vacuum_cost_limit参数。在VACUUM和ANALYZE命令的执行过程中,系统维护着一个内部计数器来跟踪各种被执行的I/O操作的估算开销。当累计的代价达到一个阈值(vacuum_cost_limit),执行这些操作的进程将按照vacuum_cost_delay所指定的休眠一小段时间。然后它将重置计数器并继续执行,这样就大大降低了这些命令对并发的数据库活动产生的I/O影响。

autovacuum worker 的启动

根据之前月报的分析,autovacuum launcher 在选取合适的database 之后会向Postmaster 守护进程发送PMSIGNAL_START_AUTOVAC_WORKER信号。Postmaster 接受信号会调用StartAutovacuumWorker函数:

  • 调用StartAutoVacWorker 启动worker
  • 调用成功,则释放后台进程slot进行善后处理,否则向autovacuum launcher发送信息,标记其为失败的autovacuum worker

StartAutoVacWorker 函数调用AutoVacWorkerMain 函数启动worker 进程:

  • 注册信号处理函数
  • 更新GUC参数配置:
    • zero_damaged_pages 参数强制设置为off,这个参数会忽略掉坏页,在自动清理的过程中,这样设置太危险
    • statement_timeout,lock_timeout,idle_in_transaction_session_timeout 为0,防止这些配置阻碍清理任务
    • default_transaction_isolation 设置为read committed,相对于设置为serializable,没增加死锁的风险,同时也不会阻塞其他的事务
    • synchronous_commit 设置为local,这样就允许我们不受备库的影响能够进行正常的清理任务
  • 读取共享内存中的AutoVacuumShmem 结构中的av_startingWorker 并更新需要清理的databaseoid 和wi_proc,放在运行中的autovacuum worker进程列表里面,并更新av_startingWorker 为NULL唤醒autovacuum launcher
  • 更新统计信息中autovacuum 的开始时间
  • 连接数据库,并读取最新的xid 和multixactid
  • 调用do_autovacuum 函数清理数据

do_autovacuum 函数的具体流程

do_autovacuum 函数会去遍历选中数据库的所有relation对象,进行自动清理工作,具体过程如下:

  • 初始化内存上下文
  • 更新统计信息
  • 获取effective_multixact_freeze_max_age
  • 设置default_freeze_min_age,default_freeze_table_age,default_multixact_freeze_min_age,default_multixact_freeze_table_age
  • 遍历pg_class所有的对象,并相应的进行处理:
    • 孤儿临时表(创建的session已经退出)打标并等待删除
    • 判断relation是否需要vacuum,analyze,wraparound,判断方法如下:
      • 该表统计信息中标记为dead的元组数大于autovacuum_vacuum_threshold+ autovacuum_vacuum_scale_factor* reltuples时,需要vacuum
      • 该表统计信息中从上次analyze之后改变的元组数大约autovacuum_analyze_threshold+ autovacuum_analyze_scale_factor* reltuples时,需要analyze
      • vacuum_freeze_table_age < recentXid - autovacuum_freeze_max_age 或者 relminmxid < recentMulti - multixact_freeze_max_age,为了防止XID 的回卷带来的问题,详见文档,标记为wraparound,这时必须强制vacuum
  • 根据上个步骤得到所有需要进行vacuum or analyze的对象,遍历所有对象,进行如下操作:
    • 重载最新的GUC 参数
    • 检查是否有其他的worker 进程正在对该relation进行清理,如果有,则跳过
    • 再次检查该relation是否需要清理,并生成用于追踪的autovac_table 结构
    • 根据上文所说的参数,对所有的worker 做资源平衡
    • 调用函数autovacuum_do_vac_analyze,进行vacuum or analyze
    • 释放缓存,更新之前月报分析的MyWorkerInfo结构
  • 更新该database的datfrozenxid

可以看出,do_autovacuum中利用共享内存AutoVacuumShmem 获取当前其他worker 的运行情况,避免并行worker 造成冲突。在此过程中调用函数autovacuum_do_vac_analyze 时会传递autovac_table 为参数,其定义如下:

/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
typedef struct autovac_table
{
	Oid			at_relid;
	int			at_vacoptions;	/* bitmask of VacuumOption */
	VacuumParams at_params;
	int			at_vacuum_cost_delay;
	int			at_vacuum_cost_limit;
	bool		at_dobalance;
	bool		at_sharedrel;
	char	   *at_relname;
	char	   *at_nspname;
	char	   *at_datname;
} autovac_table;

其中at_vacoptions指示vacuum的类型,具体如下:

typedef enum VacuumOption
{
	VACOPT_VACUUM = 1 << 0,		/* do VACUUM */
	VACOPT_ANALYZE = 1 << 1,	/* do ANALYZE */
	VACOPT_VERBOSE = 1 << 2,	/* print progress info */
	VACOPT_FREEZE = 1 << 3,		/* FREEZE option */
	VACOPT_FULL = 1 << 4,		/* FULL (non-concurrent) vacuum */
	VACOPT_NOWAIT = 1 << 5,		/* don't wait to get lock (autovacuum only) */
	VACOPT_SKIPTOAST = 1 << 6,	/* don't process the TOAST table, if any */
	VACOPT_DISABLE_PAGE_SKIPPING = 1 << 7	/* don't skip any pages */
} VacuumOption;

在autovacuum中,只涉及到VACOPT_SKIPTOAST,VACOPT_VACUUM,VACOPT_ANALYZE,VACOPT_NOWAIT。其中默认有VACOPT_SKIPTOAST选项,即会自动跳过TOAST表,关于TOAST表的autovacuum,我们在之后的月报详细分析。而VACOPT_VACUUM,VACOPT_ANALYZE,VACOPT_NOWAIT对应上文的vacuum,analyze,wraparound。

at_params存储vacuum的相关参数,其结构VacuumParams定义如下:

/*
 * Parameters customizing behavior of VACUUM and ANALYZE.
 */
typedef struct VacuumParams
{
	int			freeze_min_age; /* min freeze age, -1 to use default */
	int			freeze_table_age;	/* age at which to scan whole table */
	int			multixact_freeze_min_age;	/* min multixact freeze age, -1 to
											 * use default */
	int			multixact_freeze_table_age; /* multixact age at which to scan
											 * whole table */
	bool		is_wraparound;	/* force a for-wraparound vacuum */
	int			log_min_duration;	/* minimum execution threshold in ms at
									 * which  verbose logs are activated, -1
									 * to use default */
} VacuumParams;

vacuum函数的具体流程

vacuum 函数会根据传递的at_vacoptions 参数和at_params 参数对对应的对象进行VACUUM,既可以被autovacuum调用,又被用户手动执行VACUUM命令调用。所以这里的对象可以是relation,也可以是一个database,如果是database则会默认去vacuum该数据库所有relation 对象。autovacuum 调用vacuum函数时,这里的对象是具体的某个relation,其过程如下:

  • 检查at_vacoptions 参数正确性
  • 更新统计信息(autovacuum 在之前已经做过了,所以跳过)
  • 设置上下文
  • 如果需要VACUUM,则调用vacuum_rel 函数
  • 如果需要ANALYZE,则调用analyze_rel函数
  • 释放上下文,更新该database的datfrozenxid(autovacuum在do_autovacuum中已经做了,无需再做)

vacuum_rel函数具体去做VACUUM,这里根据at_vacoptions 参数的不同可以分为:

  • LAZY vacuum:只是找到dead的元组,把它们的状态标记为可用状态。但是它不进行空间合并。
  • FULL vacuum:除了 LAZY vacuum,还进行空间合并,因此它需要锁表。

autovacuum 是调用的LAZY vacuum。对于不断增长的表来说,LAZY vacuum显然是更合适的,LAZY vacuum主要完成:

  • 遍历该relation所有的页面,标记dead 元组为可用
  • 清理无用的index
  • 更新visibility map
  • 更新数据统计信息

LAZY vacuum该过程的调用函数关系为vacuum_rel—>lazy_scan_heap—>lazy_vacuum_heap—>lazy_vacuum_page,整个过程我们可以简单概括为:

  • 清理无用的index
  • 遍历所有的relation(table级数据库对象):
    • 遍历relation所有的page:
      • 把page 放在缓存中
      • 更新页面的组织形式(详见之前的月报),将无效元组对应的iterm设置为UNUSED,并将页面所有的tuples重新布局,使页尾保存空闲的空间,并将本页面打标为含有未使用的空间PD_HAS_FREE_LINES
      • 更新页面的free space 信息
      • 设置页面为脏页,等待后台进程将该页面刷新到磁盘
    • 更新该relation的统计信息
  • 更新visibility map
  • freeze tuple操作

总结

至此,我们得到的database 就是已经经过自动清理后的database。不过本文中还有很多问题没有涉及到:

  • 为了避免XID 回卷,freeze tuple等操作是如何实现的
  • FULL vacuum的具体操作是如何实现的
  • TOAST 表的vacuum 是如何实现的

我们会在之后的月报中一一进行分析,敬请期待。

MSSQL · 最佳实践 · 数据库恢复模式与备份的关系

$
0
0

摘要

在SQL Server备份专题分享中,前三期我们分享了三种常见的数据库备份、备份策略的制定以及如何查找备份链。本期我们将分享数据库的三种恢复模式与备份之间的关系,SQL Server的三种数据库恢复模式包括:

简单恢复模式(Simple)

完全恢复模式(Full)

大容量日志恢复模式(Bulk-logged)

SQL Server三种恢复模式

简单恢复模式(Simple)

简单恢复模式下的数据库事务日志会伴随着Checkpoint或者Backup操作而被清理,最大限度的保证事务日志最小化。

工作原理

按照我个人的理解,简单恢复模式(Simple)这个名字不足以很好的描述数据库的工作原理,准确的说法应该是”Checkpoint with truncate log”。详细的解释就是:所有已经提交的事务,会伴随着数据库的Checkpoint或者Backup操作的完成而被清理掉,仅保留少许用于实例重启时Recovery所需必要少量日志。这样做的好处是,数据库的事务日志非常的少,空间占用小,节约存储开销,不需要专职的DBA去维护和备份数据库日志。但是坏处也是显而易见的,比如:

无法实现数据库的日志备份

基于简单恢复模式的数据库无法实现任意时间点恢复(point-in-time recovery)

数据最多能够恢复到上一次的备份文件(可以是完全或者差异备份),无法恢复到最近可用状态

适用场景

基于以上数据库简单恢复模式的工作原理和缺点,因此简单恢复模式的适用场景就非常清楚了,包括:

数据库存储的是非关键数据(比如:日志信息等)

数据库在任何时间,任何场景下都没有任意时间点恢复的需求

在数据库灾难发生时,可以接受部分数据库丢失

数据库中数据变化频率非常低

数据库在可以预见的时间内没有高可用(HA)需求(比如:Database Mirroring, AlwaysOn, Log Shipping等)

设置简单恢复模式

在介绍完简单恢复模式使用场景之后,让我们来看看如何将数据库修改为简单恢复模式,以下两种方法任选其一即可。 方法一,使用SSMS IDE界面操作 右键点击需要修改恢复模式的数据库名 -> Properties -> Options -> 在右侧Recovery model中选择Simple -> OK 01.png

方法二,使用语句修改 如果你觉得使用SSMS IDE修改操作繁琐,你也可以使用ALTER DATABASE语句来修改数据库的恢复模式为Simple,以下语句是将AdventureWorks2008R2数据修改为简单恢复模式。

USE [master]
GO
ALTER DATABASE [AdventureWorks2008R2] SET RECOVERY SIMPLE WITH NO_WAIT
GO

应用举例

数据库为简单恢复模式的应用举例如下图所示:

02.png

注:以上图片来自于网络:https://sqlbak.com/academy/simple-recovery-model/

10:00和22:00数据库进行了完全备份(Full Backup)

16:00数据库完成了差异备份(Differential Backup)

19:00一些重要的数据库被误删除

在这种情况之下,我们最多能够找回到16:00这个差异备份文件中的数据,而16:00 - 22:00之间的数据将会丢失而无法找回。即我们最多能够找回异常发生时间点的前一个备份文件中的数据,找回方法是,先还原10:00这个Full Backup,然后还原16:00这个Differential Backup。还原数据库的方法类似于如下语句:

USE [master]
GO

RESTORE DATABASE [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\10:00_Full.bak' WITH NORECOVERY, REPLACE

RESTORE DATABASE [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\16:00_Diff.bak' WITH RECOVERY

通过以上方法,在简单恢复模式下的数据库,无法找回数据删除时间点靠前的所有数据,最多能够找回前一个有效备份的数据。

完全恢复模式(Full)

与SQL Server数据库简单恢复模式相反的一种模式叫着:完全恢复模式(Full),以下会对数据库完全恢复的工作原理、使用场景、设置以及应用举例四个方面来了解。

工作原理

相对于简单恢复模式而言,完全恢复模式我们可以叫着“Checkpoint without truncate log”,换句话说,SQL Server数据库引擎本身不会自己主动截断事务日志,也因此,完全恢复模式下的数据库相对于简单恢复模式的数据库,事务日志文件涨的更快,大得更多。这些数据库日志文件中包含了近期所有已经提交的事务,直到事务日志备份发生且成功结束。所以,完全恢复模式下的数据库:

允许数据库日志备份

可以实现任意时间点的恢复(point-in-time recovery)

可以恢复到灾难发生时间点非常近的数据,最大限度保证数据不丢失

适用场景

基于以上对完全恢复模式的介绍,让我们来看看完全恢复模式的适用场景,包括:

数据库中存储的是非常关键的业务数据(比如:订单信息、支付信息等)

对数据安全有着非常强烈的需求,需要在任何时间,任何情况下找回最多的数据

在灾难发生时,仅能接受极少数据的丢失

对数据库的高可用(HA)要求极高(比如:Database Mirroring、Alwayson等有强需求)

对数据库有任意时间点恢复(point-in-time recovery)的能力要求

需要数据库能够实现页级别的还原能力

当然,完全恢复模式下的数据库事务日志文件增长速度和涨幅相对简单模式更大,所以,也需要DBA对数据库事务日志做定期维护,监控和备份管理。

设置完全恢复模式

将数据库设置为完全恢复模式,同样也有两种方法。 方法一、使用SSMS IDE修改数据库为完全恢复模式,同“设置简单恢复模式”中方法一。 方法二、使用ALTER DATABASE语句将数据库设置为完全恢复模式,如下语句。

USE [master]
GO
ALTER DATABASE [AdventureWorks2008R2] SET RECOVERY FULL WITH NO_WAIT
GO

应用举例

对于完全恢复模式应用举例,如下图所示:

03.png

注:以上图片来自网络 https://sqlbak.com/academy/full-recovery-model/

对于该图的场景做如下解释:

10:00和22:00:对数据库做了完全备份

16:00:对数据库做了差异备份

12:00、14:00、18:00和20:00对数据库做了事务日志备份

19:00灾难发生,数据库中一些关键数据被误删除

那么,接下来的问题就是,我们如何利用数据库的备份信息,将19:00误删除的数据找回?也就是将数据库还原到18:59:59这个时间点的状态。根据数据库的所有备份信息,我们可以按照如下思路找回被误删除的数据。 首先,我们需要还原10:00的完全备份文件,并且状态为norecovery; 其次,我们还原16:00的差异备份文件,状态也为norecovery; 然后,还原18:00的事务日志备份文件,状态依然为norecovery; 最后,还原20:00的事务日志备份文件,需要特别注意的是这里需要指定还原到的时间点(使用STOPAT关键字)为18:59:59,并且将状态设为recovery带上线。 将以上的文字描述找回数据的步骤,以代码的形式表达出来如下:

USE [master]
GO
RESTORE DATABASE [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\10:00_Full.bak.bak' WITH NORECOVERY, REPLACE

RESTORE DATABASE [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\16:00_Diff.bak' WITH NORECOVERY

RESTORE LOG [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\18:00_Log.trn' WITH NORECOVERY

RESTORE LOG [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\20:00_Log.trn' WITH STOPAT = '2018-02-20 18:59:59', RECOVERY

通过以上步骤,在数据库完全恢复模式下,借助于备份信息,我们可以成功找回被误删除的数据,而假如数据库是工作在简单模式下,则不能达到此效果。

大容量日志恢复模式(Bulk-logged)

大容量日志恢复模式是简单恢复模式和完全恢复模式的结合体,是工作在完全恢复模式下对Bulk Imports操作的改良和适应。

工作原理

在SQL Server数据库系统中,有一种快速导入数据的方法叫Bulk Imports,比如:BCP、Bulk INSERT或者INSERT INTO …SELECT。如果这些Bulk操作发生在完全恢复模式下的数据库,将会产生大量的日志信息,对SQL Server性能影响较大。大容量日志恢复模式的存在就是为了解决这个问题的,工作在Bulk-logged模式下的数据库在Bulk Imports的时候,会记录少量日志,防止事务日志的暴涨,以保证SQL Server性能的稳定和高效。可以简单的将Bulk-logged模式理解为:在没有Bulk Imports操作的时候,它与完全恢复模式等价,而当存在Bulk Imports操作的时候,它与简单恢复模式等价。所以,处于Bulk-logged模式下的数据库无法实现任意时间点恢复(point-in-time recovery),这个缺点与Simple模式类似。

适用场景

基于大容量日志模式的原理解释,它的适用场景包括:

Bulk Imports操作,比如:BCP、Bulk INSERT和INSERT INTO…SELECT

SELECT INTO操作

关于索引的一些操作:CREATE/DROP INDEX、ALTER INDEX REBUILD或者DBCC DBREINDEX

Bulk-logged模式最常用的使用场景是在做Bulk操作之前切换到Bulk-logged,在Bulk操作结束之后切换回Full模式

设置大容量日志恢复模式

将数据库设置为大容量日志模式,还是有两种方法。 方法一、使用SSMS IDE修改数据库为大容量日志恢复模式,同“设置简单恢复模式”中方法一。 方法二、使用ALTER DATABASE语句将数据库设置为大容量日志恢复模式,如下语句。

USE [master]
GO
ALTER DATABASE [AdventureWorks2008R2] SET RECOVERY Bulk_LOGGED WITH NO_WAIT
GO

应用举例

对于大容量日志恢复模式应用例子,参见如下图所示:

04.png

注:以上图片来自网络 https://sqlbak.com/academy/Bulk-logged-recovery-model/

对于该图的场景做如下解释:

10:00:对数据库做了完全备份

12:00、14:00、16:00和18:00:对数据库做了事务日志备份,其中16:00的日志备份(黄颜色标示)是在修改为Bulk-logged的模式后进行的

20:00:对数据库做了差异备份

14:30:将数据库修改为Bulk-logged模式

15:00:灾难发生,重要的数据被误删除

那么,接下来的问题就是,我们如何利用这些备份文件,尽可能的找回更多的数据,使丢失的数据最少。在原理部分,我们已经知道了,处于Bulk-logged模式下的数据库,无法实现任意时间点的恢复,因此16:00这个事务日志备份文件就无法使用,即使尝试使用也会报告如下错误:

This log backup contains Bulk-logged changes. It cannot be used to stop at an arbitrary point in time.

The STOPAT clause specifies a point too early to allow this backup set to be restored. Choose a different stop point or use RESTORE DATABASE WITH RECOVERY to recover at the current point.

RESTORE LOG is terminating abnormally

最终,我们能够找回的数据最多能够使用到14:00这个事务日志备份。找回的步骤如下: 首先:我们需要还原10:00的完全备份文件,并且状态为norecovery; 其次:我们还原12:00的事务日志备份文件,状态也为norecovery; 最后:还原14:00的视图日志备份,并且将状态设为recovery带上线。 将以上的文字描述步骤用代码来表达,如下:

USE [master]
GO
RESTORE DATABASE [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\10:00_Full.bak.bak' WITH NORECOVERY, REPLACE

RESTORE LOG [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\12:00_Log.trn' WITH NORECOVERY

RESTORE LOG [AdventureWorks2008R2] 
FROM DISK = 'D:\Backup\14:00_Log.trn' WITH RECOVERY

通过以上步骤,工作在Bulk-logged模式下的数据库,无法实现任意时间恢复;因此一般该模式是使用在Bulk操作的过程中。

最后总结

本期分享了SQL Server三种恢复模式的工作原理、适用场景以及典型应用举例,以此来探讨数据库恢复模式与备份之间的关系,从中我们很清楚的理解了数据库恢复模式与备份的协同工作,来保证我们数据的安全性,以及在灾难发生的时候,可以最大限度减少数据损失。

PgSQL · 最佳实践 · 利用异步 dblink 快速从 oss 装载数据

$
0
0

摘要

总所周知,阿里云的 PostgreSQL 和 HybridDB for PostgreSQL 和 oss 是全面互通的。 HybridDB for PostgreSQL 由于是 MPP 架构天生包括多个计算节点,能够以为并发的方式读写 oss 上的数据。PostgreSQL 在这方面要差一点,默认情况下只能单进程读写 OSS,不过通过 dblink 的加持,我们也能让 OSS 中的数据快速装载到 PostgreSQL。本文就给大家讲讲这其中的黑科技。

一.准备工作

首先,创建我们要用到的插件。

create extension dblink;
create extension oss_fdw;

二.创建异步化存储过程

-- 异步数据装载的准备工作
CREATE OR REPLACE FUNCTION rds_oss_fdw_load_data_prepare(t_from text, t_to text)
  RETURNS bool AS
$BODY$
DECLARE
	t_exist  int;
	curs1 refcursor;
	r	record;
	filepath text;
	fileindex int8;
	s1 text;
	s2 text;
	s3 text;
	c int = 0;
	s4 text;
	s5 text;
	ss4 text;
	ss5 text;
	sql text;
BEGIN
	create table if not exists oss_fdw_load_status(id BIGSERIAL primary key, filename text, size int8, rows int8 default 0, status int default 0);

	select count(*) into t_exist from oss_fdw_load_status;

	if t_exist != 0 then
		RAISE NOTICE 'oss_fdw_load_status not empty';
		return false;
	end if;

	-- 通过 oss_fdw_list_file 函数,把外部表 t_from 匹配的 OSS 中的文件列到表中
	insert into oss_fdw_load_status (filename, size) select name,size from oss_fdw_list_file(t_from) order by size desc;

	select count(*) into t_exist from oss_fdw_load_status;
	if t_exist = 0 then
		RAISE NOTICE 'oss_fdw_load_status empty,not task found';
		return false;
	end if;

	return true;
END;
$BODY$
	LANGUAGE plpgsql;

-- 数据装载的工作函数
CREATE OR REPLACE FUNCTION rds_oss_fdw_load_data_execute(t_from text, t_to text, num_work int, pass text)
  RETURNS bool AS
$BODY$
DECLARE
	t_exist  int;
	curs1 refcursor;
	r	record;
	filepath text;
	fileindex int8;
	s1 text;
	s2 text;
	s3 text;
	c int = 0;
	s4 text;
	s5 text;
	ss4 text;
	ss5 text;
	sql text;
	db text;
	user text;
BEGIN
	select count(*) into t_exist from oss_fdw_load_status;
	if t_exist = 0 then
		RAISE NOTICE 'oss_fdw_load_status empty';
		return false;
	end if;

	s4 = 'oss_loader';
	s5 = 'idle';
	ss4 = '''' || s4 ||'''';
	ss5 = '''' || s5 ||'''';
	sql = 'select count(*) from pg_stat_activity where application_name = ' || ss4 || ' and state != ' || ss5;

	select current_database() into db;
	select current_user into user;

	-- 通过游标,不断获取单个任务
	OPEN curs1 FOR SELECT id, filename FROM oss_fdw_load_status order by id;
	loop
		fetch curs1 into r;
		if not found then
			exit;
		end if;
		fileindex = r.id;
		filepath = r.filename;

		s1 = '''' || t_from ||'''';
		s2 = '''' || t_to ||'''';
		s3 = '''' || filepath ||'''';

		LOOP
			-- 查看当前正在工作的任务数,过达到并发数就在这里等待
			select a into c from dblink('dbname='||db ||' user='||user || ' password='||pass ,sql)as t(a int);
			IF c < num_work THEN
				EXIT;
			END IF;
			RAISE NOTICE 'current runing % loader', c;
			perform pg_sleep(1);
		END LOOP;

		-- 通过 DBLINK 创建异步任务
		perform dis_conn('oss_loader_'||fileindex);
		perform dblink_connect('oss_loader_'||fileindex, 'dbname='||db ||' user='||user || ' application_name=oss_loader' || ' password='||pass);
		perform dblink_send_query('oss_loader_'||fileindex, format('
			begin;
			select rds_oss_fdw_load_single_file(%s,%s,%s,%s);
			end;'
			, fileindex, s1, s2, s3)
		);
		RAISE NOTICE 'runing loader task % filename %',fileindex, filepath;
	end loop;
	close curs1;

	-- 任务分配完成,等待所有任务完成
	LOOP
		select a into c from dblink('dbname='||db ||' user='||user || ' password='||pass ,sql)as t(a int);
		IF c = 0 THEN
			EXIT;
		END IF;
		RAISE NOTICE 'current runing % loader', c;
		perform pg_sleep(1);
	END LOOP;

	return true;
END;
$BODY$
	LANGUAGE plpgsql;

-- 单个文件的数据装在函数
CREATE OR REPLACE FUNCTION rds_oss_fdw_load_single_file(taskid int8, t_from text, t_to text, filepath text)
  RETURNS void AS
$BODY$
DECLARE
	rowscount int8 = 0;
	current text;
	sql text;
BEGIN
	-- 配置 GUC 参数,指定要导入的 OSS 上的文件
	perform set_config('oss_fdw.rds_read_one_file',filepath,true);
	select current_setting('oss_fdw.rds_read_one_file') into current;
	RAISE NOTICE 'begin load %', current;

	-- 通过动态 SQL 导入数据
	EXECUTE 'insert into '|| t_to || ' select * from ' || t_from;
	GET DIAGNOSTICS rowscount = ROW_COUNT;

	-- 导入完成后,把结果保存到状态表中
	RAISE NOTICE 'end load id % % to % % rows', taskid, filepath, t_to, rowscount;
	update oss_fdw_load_status set rows = rowscount,status = 1 where id = taskid;
	return;

EXCEPTION
	when others then
	RAISE 'run rds_oss_fdw_load_single_file with error';
END;
$BODY$
	LANGUAGE plpgsql;

-- 关闭连接不报错
create or replace function dis_conn(name) returns void as $$  
declare  
begin  
  perform dblink_disconnect($1);  
  return;  
exception when others then  
  return;  
end;  
$$ language plpgsql strict;  


三.使用函数装载数据

1. 准备数据

select rds_oss_fdw_load_data_prepare('oss_table','lineitem');

执行后,会看到表 oss_fdw_load_status 中,保存了准备导入的所有文件列表,用户可以做适当的删减定制。

2. 数据装载

 select rds_oss_fdw_load_data_execute('oss_table','lineitem',10,'mypassword');

函数 rds_oss_fdw_load_data_execute 会等待数据导入的完成才返回。

3. 查询状态

期间,我们可以通过下列 SQL 查看正在工作的异步会话状态

 select application_name, state, pid,query, now() - xact_start as xact  from pg_stat_activity where state != 'idle' and application_name='oss_loader' order by xact desc;

4.管理状态

同时,我们也可以随时中断数据导入工作

select pg_terminate_backend(pid),application_name, state ,query from pg_stat_activity where state != 'idle' and pid != pg_backend_pid() and application_name='oss_loader';

5. 查看进度

我们也很容易看到整个数据装载的进度(单位 MB)

select
(
select sum(size)/1024/1024 as complete from oss_fdw_load_status where status = 1
)a,
(
select sum(size)/1024/1024 as full from oss_fdw_load_status
)b;

6. 性能

使用 TPCC 100GB的数据进行装载测试,耗时 10 分钟,平均 170MB/S

select rds_oss_fdw_load_data_prepare('t_oss2','lineitem');

select rds_oss_fdw_load_data_execute('t_oss2','lineitem',10,'123456Zwj');

select sum(size)/1024/1024 from oss_fdw_load_status;
      ?column?      
--------------------
 22561.919849395752
(1 row)

select pg_size_pretty(pg_relation_size(oid)) from pg_class where relname = 'lineitem';
 pg_size_pretty 
----------------
 101 GB
(1 row)

总结

本文使用 plsql + dblink 的方式加速了 OSS 的数据导入。另外,大家也可以关注到以下三点

  • 1. PostgreSQL 默认的过程语言 pl/pgsql 相当好用,和 SQL 引擎紧密结合且学习成本低。我们推荐用户把业务逻辑用它实现。使用过程语言相对于在客户端执行 SQL,消除了服务器到和客户端的网络开销,有天然的性能优势。
  • 2. dblink 的异步接口非常适合做性能加速,且和过程语言紧密结合。推荐在 SQL 和 过程语言中使用。
  • 3. 阿里云开发的 oss_fdw 能在 PostgreSQL 和 OSS 之间做快速的数据交换。oss_fdw 支持 CSV 和压缩方式 CSV 数据的读和写,且很容易用并行加速。oss_fdw 的性能相对于 jdbc insert 和 copy 有压倒的性能优势。

MySQL · 源码分析 · 新连接的建立

$
0
0

之前已经有过两篇有关连接的月报 网络通信模块浅析mysql认证阶段漫游

本文先介绍新连接建立的主要调用栈,再分析thread cache 和每个连接的资源限制

mysql 支持三种连接方式

  • socket
  • named pipe
  • shared memory

named pipe 和 shared memory 只能在本地连接数据库,适用场景较少,本文主要介绍 socket 连接方式

1.代码栈分析

从主线程开始

main

  mysqld_admin

    network_init
      
      /* 
        Connection_acceptor 是一个模板类
        template <typename Listener> class Connection_acceptor
        三种连接方式分别传入各自的 Listener
          Mysqld_socket_listener
          Named_pipe_listener
          Shared_mem_listener
        最常用的是 Mysqld_socket_listener
      */
      Connection_acceptor<Mysqld_socket_listener>::init_connection_acceptor

  Connection_acceptor
    
    Mysqld_socket_listener::listen_for_connection_event
      
      /* 监听socket文件,没有新连接时,线程在这里等待 */
      poll
      
      /* 返回新连接的信息 */
      return channel_info
    
    Connection_handler_manager::process_new_connection
      
      /* 检查max_connections */
      check_and_incr_conn_count
      
      /* 
        Connection_handler 有两个子类,
        Per_thread_connection_handler:一个连接一个线程
        One_thread_connection_handler:一个线程处理所有连接
        我们一般使用 Per_thread_connection_handler
      */
      m_connection_handler->add_connection(Per_thread_connection_handler::add_connection)
      
        /* 查看 thread_cache 中是否有空闲thread,如有,使用cached thread */
        if (!check_idle_thread_and_enqueue_connection(channel_info))
          DBUG_RETURN(false);
        
        /* 建立新线程,从 handle_connection 开始执行 */
        mysql_thread_create(handle_connection)
        
        
        Global_THD_manager::get_instance()->inc_thread_created();
         

用户连接线程栈如下

handle_connection
  
  my_thread_init
  
  for (;;)
  
    THD *thd= init_new_thd(channel_info);
      

    /* 第一次循环执行prepare,后面跳过 */    
    thd_prepare_connection(thd)
      
      login_connection
        
        check_connection
        
          /* 权限认证 */  
          acl_authenticate
        
      prepare_new_connection_state

    /* 第二次循环开始,执行do_command */
    while (thd_connection_alive(thd))
      
      do_command(thd)
 
    end_connection(thd);
    
    close_connection(thd, 0, false, false);
    
    thd->release_resources();
    
    /* 进入 thread cache,等待新连接复用 */
	channel_info= Per_thread_connection_handler::block_until_new_connection();
    if (channel_info == NULL)
      break;

2.thread_cache

参数 thread_cache_size 控制了 thread_cache 的大小, 设为0时关闭 thread_cache,不缓存空闲thread

mysql> show status like 'Threads%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_cached    | 1     |
| Threads_connected | 1     |
| Threads_created   | 2     |
| Threads_running   | 1     |
+-------------------+-------+
4 rows in set (0.02 sec)

Threads_cached:缓存的 thread,新连接建立时,优先使用cache中的thread

Threads_connected:已连接的 thread

Threads_created:建立的 thread 数量

Threads_running:running状态的 thread 数量

Threads_created = Threads_cached + Threads_connected

Threads_running <= Threads_connected

MySQL 建立新连接非常消耗资源,频繁使用短连接,又没有其他组件实现连接池时,可以适当提高 thread_cache_size,降低新建连接的开销

2.1 thread cache 源码分析

channel_info连接信息
waiting_channel_info_listchannel_info的等待链表
COND_thread_cacheblock线程被唤醒的信号量,唤醒后从waiting_channel_info_list取出头部channel_info建立新连接
blocked_pthread_count被block的线程数
max_blocked_pthreads被block的最大线程数,也就是thread_cache_size
wake_pthreadwaiting_channel_info_list的链表长度
2.1.1 block_until_new_connection

handle_connection 线程结束之前,会执行 block_until_new_connection,尝试进入 thread cache 等待其他连接复用

如果 blocked_pthread_count < max_blocked_pthreads,blocked_pthread_count++,然后等待被 COND_thread_cache 唤醒,唤醒之后 blocked_pthread_count– , 返回 waiting_channel_info_list 中的一个 channel_info ,进行 handle_connections 的下一个循环

2.1.2 check_idle_thread_and_enqueue_connection

检查是否 blocked_pthread_count > wake_pthread (有足够的block状态线程用来唤醒) 如有 插入 channel_info 进入 waiting_channel_info_list,并发出 COND_thread_cache 信号量

3.每个连接的限制

除了参数 max_user_connections 限制每个用户的最大连接数,还可以对每个用户制定更细致的限制

以下四个限制保存在mysql.user表中

  • MAX_QUERIES_PER_HOUR 每小时最大请求数(语句数量)
  • MAX_UPDATES_PER_HOUR 每小时最大更新数(更新语句的数量)
  • MAX_CONNECTIONS_PER_HOUR 每小时最大连接数
  • MAX_USER_CONNECTIONS 这个用户的最大连接数
GRANT
    priv_type [(column_list)]
      [, priv_type [(column_list)]] ...
    ON [object_type] priv_level
    TO user [auth_option] [, user [auth_option]] ...
    [REQUIRE {NONE | tls_option [[AND] tls_option] ...}]
    [WITH {GRANT OPTION | resource_option} ...]

resource_option: {
  | MAX_QUERIES_PER_HOUR count
  | MAX_UPDATES_PER_HOUR count
  | MAX_CONNECTIONS_PER_HOUR count
  | MAX_USER_CONNECTIONS count
}

ALTER USER 'jeffrey'@'localhost' WITH MAX_QUERIES_PER_HOUR 90;

3.1 源码分析

3.1.1 USER_RESOURCES

保存用户连接限制的结构体,作为成员属性存在于各个和连接限制相关的类

typedef struct user_resources {

  /* MAX_QUERIES_PER_HOUR */
  uint questions; 

  /* MAX_UPDATES_PER_HOUR */
  uint updates;

  /* MAX_CONNECTIONS_PER_HOUR */
  uint conn_per_hour;

  /* MAX_USER_CONNECTIONS */
  uint user_conn;

  enum {QUERIES_PER_HOUR= 1, UPDATES_PER_HOUR= 2, CONNECTIONS_PER_HOUR= 4,
        USER_CONNECTIONS= 8};
  uint specified_limits;
} USER_RESOURCES;


3.1.2 ACL_USER

保存用户认证相关信息的类 USER_RESOURCES 是它的成员属性

ACl_USER 对象保存在数组 acl_users 中,每次mysqld启动时,从mysql.user表中读取数据,初始化 acl_users,初始化过程在函数 acl_load 中

调用栈如下

main
  
  mysqld_main
  
    acl_init
      
      acl_reload
        
        acl_load
3.1.3 USER_CONN

保存用户资源使用的结构体,建立连接时,调用 get_or_create_user_conn 为 THD 绑定 USER_CONN 对象

每个用户第一个连接创建时,建立一个新对象,存入 hash_user_connections

第二个连接开始,从 hash_user_connections 取出 USER_CONN 对象和 THD 绑定

同一个用户的连接,THD 都和同一个 USER_CONN 对象绑定

typedef struct user_conn {

  char *user;

  char *host;

  ulonglong reset_utime;

  size_t len;

  /* 当前用户连接数 */
  uint connections;

  /* 每小时连接数,请求数,更新数使用情况(实时更新) */
  uint conn_per_hour, updates, questions;

  /* 本用户资源限制 */
  USER_RESOURCES user_resources;
} USER_CONN;

get_or_create_user_conn 调用栈如下

handle_connection
  
  thd_prepare_connection(thd)
    
    login_connection
      
      check_connection
        
        acl_authenticate
            
          get_or_create_user_conn

3.1.4 资源限制在源码中的位置

MAX_USER_CONNECTIONS MAX_CONNECTIONS_PER_HOURcheck_for_max_user_connections
MAX_QUERIES_PER_HOUR MAX_UPDATES_PER_HOURcheck_mqh

调用栈如下

handle_connection
  
  thd_prepare_connection(thd)
    
    login_connection
      
      check_connection
        
        acl_authenticate
          
          check_for_max_user_connections
  
  do_command
  
    dispatch_command
    
      mysql_parse
       
        check_mqh

MySQL · 引擎特性 · INFORMATION_SCHEMA系统表的实现

$
0
0

简介

在MySQL中, INFORMATION_SCHEMA 信息数据库提供了访问数据库元数据的方式,其中保存着关于MySQL服务器所维护的所有其他数据库的信息。如数据库名,数据库的表,列的数据类型与访问权限等。在INFORMATION_SCHEMA中,有数个只读表。它们实际上是视图,也不是基本表,因此,你将无法看到与之相关的任何文件。下面将介绍如何新加一个INFORMATION_SCHEMA系统表,以能够通过查询表的方式来查询我们希望得到的元数据信息。

INFORMATION_SCHEMA表是作为MySQL的插件来实现的

INFORMATION_SCHEMA和我们经常讲到的引擎插件(Engine plugin)MYSQL_STORAGE_ENGINE_PLUGIN = 1类似,作为一个MySQL的插件来实现的。INFORMATION_SCHEMA的插件类型是MYSQL_INFORMATION_SCHEMA_PLUGIN = 4 。

#define MYSQL_UDF_PLUGIN             0  /* User-defined function        */
#define MYSQL_STORAGE_ENGINE_PLUGIN  1  /* Storage Engine               */
#define MYSQL_FTPARSER_PLUGIN        2  /* Full-text parser plugin      */
#define MYSQL_DAEMON_PLUGIN          3  /* The daemon/raw plugin type */
#define MYSQL_INFORMATION_SCHEMA_PLUGIN  4  /* The I_S plugin type */
#define MYSQL_AUDIT_PLUGIN           5  /* The Audit plugin type        */
#define MYSQL_REPLICATION_PLUGIN     6  /* The replication plugin type */
#define MYSQL_AUTHENTICATION_PLUGIN  7  /* The authentication plugin type */
#define MYSQL_VALIDATE_PASSWORD_PLUGIN  8   /* validate password plugin type */

INFORMATION_SCHEMA表插件接口

Mysql为要定义的INFORMATION_SCHEMA表提供了如下插件接口。主要通过st_mysql_plugin 结构实现的。

struct st_mysql_plugin
{
  int type;             /* the plugin type (a MYSQL_XXX_PLUGIN value)   */
  void *info;           /* pointer to type-specific plugin descriptor   */
  const char *name;     /* plugin name                                  */
  const char *author;   /* plugin author (for I_S.PLUGINS)              */
  const char *descr;    /* general descriptive text (for I_S.PLUGINS)   */
  int license;          /* the plugin license (PLUGIN_LICENSE_XXX)      */
  int (*init)(MYSQL_PLUGIN);  /* the function to invoke when plugin is loaded */
  int (*deinit)(MYSQL_PLUGIN);/* the function to invoke when plugin is unloaded */
  unsigned int version; /* plugin version (for I_S.PLUGINS)             */
  struct st_mysql_show_var *status_vars;
  struct st_mysql_sys_var **system_vars;
  void * __reserved1;   /* reserved for dependency checking             */
  unsigned long flags;  /* flags for plugin */
};

此接口要提供这个插件的类型。这里我们要添加一个INFORMATION_SCHEMA表,所以type就是MYSQL_INFORMATION_SCHEMA_PLUGIN; name就是你要定义的表名字, 就像INFORMATION_SCHEMA中的表INNODB_SYS_DATAFILES 一样。 这里比如INNODB_MY_TABLE; 另外比较重要的两个字段就是init和deinit,这两个字段分别是在这个插件在装载和卸载时调用的函数。如果没有特殊的需求deinit函数可以直接使用系统提供的i_s_common_deinit 通用函数。 在st_mysql_plugin结构中,init字段用来提供在这个插件装载时调用的函数。其实现主要是通过为ST_SCHEMA_TABLE结构提供INFORMATION_SCHEMA表结构定义和表填充数据函数。

i_s_innodb_my_table_init(
    void* p)    /*!< in/out: table schema object */
{ 
        ST_SCHEMA_TABLE*        schema;

        schema = reinterpret_cast<ST_SCHEMA_TABLE*>(p);
        schema->fields_info = innodb_my_table_field;
        schema->fill_table = i_s_innodb_my_table_fill_table;
  
        DBUG_RETURN(0);
} 

其中innodb_my_table_field就是此新加入INFORMATION_SCHEMA表结构定义;innodb_my_table_fill_table就是当我们查询这张表时,其中显示的数据就是通过这个函数提供的。

例如我们定义的新表插件接口如下:

UNIV_INTERN struct st_mysql_plugin      i_s_innodb_my_table =
{
        /* the plugin type (a MYSQL_XXX_PLUGIN value) */
        /* int */
        STRUCT_FLD(type, MYSQL_INFORMATION_SCHEMA_PLUGIN),

        /* pointer to type-specific plugin descriptor */
        /* void* */
        STRUCT_FLD(info, &i_s_info),

        /* plugin name */
        /* const char* */
        STRUCT_FLD(name, "INNODB_MY_TABLE”),

        /* plugin author (for SHOW PLUGINS) */
        /* const char* */
        STRUCT_FLD(author, "Alibaba"),
  
        /* general descriptive text (for SHOW PLUGINS) */
        /* const char* */
        STRUCT_FLD(descr, "InnoDB My table info.”),

        /* the plugin license (PLUGIN_LICENSE_XXX) */
        /* int */
        STRUCT_FLD(license, PLUGIN_LICENSE_GPL),

        /* the function to invoke when plugin is loaded */
        /* int (*)(void*); */
        STRUCT_FLD(init, i_s_innodb_my_table_init),

        /* the function to invoke when plugin is unloaded */
        /* int (*)(void*); */
        STRUCT_FLD(deinit, i_s_common_deinit),
        /* plugin version (for SHOW PLUGINS) */
        /* unsigned int */
        STRUCT_FLD(version, INNODB_VERSION_SHORT),

        /* struct st_mysql_show_var* */
        STRUCT_FLD(status_vars, NULL),

        /* struct st_mysql_sys_var** */
        STRUCT_FLD(system_vars, NULL),

        /* reserved for dependency checking */
        /* void* */
        STRUCT_FLD(__reserved1, NULL),

        /* Plugin flags */
        /* unsigned long */
        STRUCT_FLD(flags, 0UL),
};

INFORMATION_SCHEMA表的结构定义

表结构定义在一个ST_FIELD_INFO结构中,这个结构定义了要添加到INFORMATION_SCHEMA表的各个字段,包括字段名、字段类型和字段长度等信息。这里就涉及到如何将这里定义的各个字段与在st_mysql_plugin 结构中定的表信息关联起来。这里就要用到在st_mysql_plugin 结构里定义的初始化函数有关了。实现init函数的参数,是一个void类型的输入参数,这个参数在information schema的插件中,就是一个ST_SCHEMA_TABLE结构指针,就在这个结构中要提供这个表的字段信息,以及查询表时调用填充数据的函数。

static ST_FIELD_INFO innodb_my_table_field[] =
{ 
#define IDX_MY_TABLE_FIELD_0        0
        {STRUCT_FLD(field_name,         “field0”),
         STRUCT_FLD(field_length,       MY_INT64_NUM_DECIMAL_DIGITS),
         STRUCT_FLD(field_type,         MYSQL_TYPE_LONGLONG),
         STRUCT_FLD(value,              0),
         STRUCT_FLD(field_flags,        MY_I_S_UNSIGNED),
         STRUCT_FLD(old_name,           ""),
         STRUCT_FLD(open_method,        SKIP_OPEN_TABLE)},
  
 
#define IDX_MY_TABLE_FIELD_1        1        
        {STRUCT_FLD(field_name,         “field1”),
         STRUCT_FLD(field_length,       MY_INT64_NUM_DECIMAL_DIGITS),
         STRUCT_FLD(field_type,         MYSQL_TYPE_LONGLONG),
         STRUCT_FLD(value,              0),
         STRUCT_FLD(field_flags,        MY_I_S_UNSIGNED),
         STRUCT_FLD(old_name,           ""),
         STRUCT_FLD(open_method,        SKIP_OPEN_TABLE)},
….
}

填充函数就是用来实现往INFORMATION_SCHEMA里填充数据的函数,可以根据具体的需求和要填充的数据,根据实际情况读取引擎内部的状态信息,写入到这个表中。比如上面在初始化装载函数中赋值给field_table字段的函数i_s_innodb_my_table_fill_table()。

加表到INFORMATION_SCHEMA元数据库

通过上述几步就把这个表定义出来了。 如何把这个表真正的加入到INFORMATION_SCHEMA里,客户端可以通过查询语句查询这张表呢?为了实现这个目标就要把这张表加入到ha_innodb.cc文件里,从mysql_declare_plugin(innobase) 到mysql_declare_plugin_end 之间的结构里。在这里我们可以看到已经定义了一系列的INFORMATION_SCHEMA表,包含常见到的i_s_innodb_trx、i_s_innodb_locks和i_s_innodb_sys_tables等表,只要把我们新实现的插件接口i_s_innodb_my_table加入到这个结构中,就成功把这张表加入了INFORMATION_SCHEMA元数据库中了。


MySQL · 最佳实践 · 在线收缩UNDO Tablespace

$
0
0

概述

Undo log一直都是事务多版本控制中的核心组件,它具有以下的核心功能

  • 交易的回退:事务在处理过程中遇到异常的时候可以rollback(撤销)所做的全部修改
  • 交易的恢复:数据库实例崩溃时,将磁盘的不正确数据恢复到交易前
  • 读一致性:被查询的记录有事务占用,转向回滚段找事务开始前的数据镜像

虽然Undo log是如此的重要,但在MySQL 5.6(包括5.6)之前Undo tablespace里面的undo数据文件是无法收缩的。也就是说在实例的运行过程中如果遇到有大的事务,会把undo log的文件撑的非常大。进而浪费大量的空间甚至把磁盘打爆。同时也增加了数据库物理备份的时间。在实际的工作中不止一次遇到这类问题。好在MySQL5.7中新增了一个非常有用的功能允许用户在线truncate undo log,进而是undo log文件进行收缩。

5.7 在线truncate undo log

必须使用独立的undo表空间,该功能主要由以下参数控制

  • innodb_undo_directory,指定单独存放undo表空间的目录,默认为.(即datadir),可以设置相对路径或者绝对路径。该参数实例初始化之后虽然不可直接改动,但是可以通过先停库,修改配置文件,然后移动undo表空间文件的方式去修改该参数;
  • innodb_undo_tablespaces,指定单独存放的undo表空间个数,例如如果设置为3,则undo表空间为undo001、undo002、undo003,每个文件初始大小默认为10M。该参数我们推荐设置为大于等于3,原因下文将解释。该参数实例初始化之后不可改动;
  • innodb_undo_logs,指定回滚段的个数(早期版本该参数名字是innodb_rollback_segments),默认128个。每个回滚段可同时支持1024个在线事务。这些回滚段会平均分布到各个undo表空间中。该变量可以动态调整,但是物理上的回滚段不会减少,只是会控制用到的回滚段的个数。
  • innodb_undo_tablespaces>=2。因为truncate undo表空间时,该文件处于inactive状态,如果只有1个undo表空间,那么整个系统在此过程中将处于不可用状态。为了尽可能降低truncate对系统的影响,建议将该参数最少设置为3;
  • innodb_undo_logs>=35(默认128)。因为在MySQL 5.7中,第一个undo log永远在系统表空间中,另外32个undo log分配给了临时表空间,即ibtmp1,至少还有2个undo log才能保证2个undo表空间中每个里面至少有1个undo log;
  • innodb_max_undo_log_size,undo表空间文件超过此值即标记为可收缩,默认1G,可在线修改;
  • innodb_purge_rseg_truncate_frequency,指定purge操作被唤起多少次之后才释放rollback segments。当undo表空间里面的rollback segments被释放时,undo表空间才会被truncate。由此可见,该参数越小,undo表空间被尝试truncate的频率越高。

MySQL 5.7的undo表空间的truncate示例

(1) 首先确保如下参数被正确设置:

  • innodb_max_undo_log_size = 100M
  • innodb_undo_log_truncate = ON
  • innodb_undo_logs = 128
  • innodb_undo_tablespaces = 3
  • innodb_purge_rseg_truncate_frequency = 10

(2) 创建表:


mysql> create table t1( id int primary key auto_increment, name varchar(200));
Query OK, 0 rows affected (0.13 sec)

(3)插入测试数据

mysql> insert into t1(name) values(repeat('a',200));
mysql> insert into t1(name) select name from t1;
mysql> insert into t1(name) select name from t1;
mysql> insert into t1(name) select name from t1;
mysql> insert into t1(name) select name from t1;

这时undo表空间文件大小如下,可以看到有一个undo文件已经超过了100M:


-rw-r----- 1 mysql mysql  13M Feb 25 17:59 undo001
-rw-r----- 1 mysql mysql 128M Feb 25 17:59 undo002
-rw-r----- 1 mysql mysql  64M Feb 25 17:59 undo003

此时,为了,让purge线程运行,可以运行几个delete语句:

mysql> delete from t1 limit 1;
mysql> delete from t1 limit 1;
mysql> delete from t1 limit 1;
mysql> delete from t1 limit 1;

再查看undo文件大小:

-rw-r----- 1 mysql mysql  13M Feb 25 18:05 undo001
-rw-r----- 1 mysql mysql  10M Feb 25 18:05 undo002
-rw-r----- 1 mysql mysql  64M Feb 25 18:05 undo003

可以看到,超过100M的undo文件已经收缩到10M了。

小结

在MySQL 5.7中我们有了一个有效的方法可以在数据库实例运行的过程中动态的回收undo log占用的空间。

PgSQL · 应用案例 · 自定义并行聚合函数的原理与实践

$
0
0

背景

PostgreSQL 9.6开始就支持并行计算了,意味着聚合、扫描、排序、JOIN等都开始支持并行计算。对于聚合操作来说,并行计算与非并行计算是有差异的。

例如avg聚合,对一张表进行计算时,一个任务中操作和多个并行任务操作,算法是不一样的。

PostgreSQL提供了一套标准的接口,可以支持聚合函数的并行操作。

自定义并行聚合的原理和例子

创建聚合函数的语法如下:

CREATE AGGREGATE name ( [ argmode ] [ argname ] arg_data_type [ , ... ] ) (  
    SFUNC = sfunc,  
    STYPE = state_data_type  
    [ , SSPACE = state_data_size ]  
    [ , FINALFUNC = ffunc ]  
    [ , FINALFUNC_EXTRA ]  
    [ , COMBINEFUNC = combinefunc ]  
    [ , SERIALFUNC = serialfunc ]  
    [ , DESERIALFUNC = deserialfunc ]  
    [ , INITCOND = initial_condition ]  
    [ , MSFUNC = msfunc ]  
    [ , MINVFUNC = minvfunc ]  
    [ , MSTYPE = mstate_data_type ]  
    [ , MSSPACE = mstate_data_size ]  
    [ , MFINALFUNC = mffunc ]  
    [ , MFINALFUNC_EXTRA ]  
    [ , MINITCOND = minitial_condition ]  
    [ , SORTOP = sort_operator ]  
    [ , PARALLEL = { SAFE | RESTRICTED | UNSAFE } ]  
)  

相比非并行,多了一个过程,那就是combinefunc的过程(也叫partial agg)。

非并行模式的聚合流程大致如下:

循环  
sfunc( internal-state, next-data-values ) ---> next-internal-state  
  
最后调用一次(可选)  
ffunc( internal-state ) ---> aggregate-value  

pic

并行模式的聚合流程大致如下,如果没有写combinefunc,那么实际上聚合过程并没有实现并行而只是扫描并行:

pic

下面这个例子,我们可以观察到一个COUNT操作的并行聚合。

postgres=# set max_parallel_workers=4;  
SET  
postgres=# set max_parallel_workers_per_gather =4;  
SET  
postgres=# set parallel_setup_cost =0;  
SET  
postgres=# set parallel_tuple_cost =0;  
SET  
postgres=# alter table test set (parallel_workers =4);  
ALTER TABLE  
postgres=# explain (analyze,verbose,timing,costs,buffers) select count(*) from test;  
                                                                  QUERY PLAN                                                                     
-----------------------------------------------------------------------------------------------------------------------------------------------  
 -- final并行,可有可无,看具体的聚合算法  
 Finalize Aggregate  (cost=15837.02..15837.03 rows=1 width=8) (actual time=57.296..57.296 rows=1 loops=1)  
   Output: count(*)  
   Buffers: shared hit=3060  
   ->  Gather  (cost=15837.00..15837.01 rows=4 width=8) (actual time=57.287..57.292 rows=5 loops=1)  
         Output: (PARTIAL count(*))  
         Workers Planned: 4  
         Workers Launched: 4  
         Buffers: shared hit=3060  
           
	 -- 一下就是combinefunc完成的聚合并行(显示为PARTIAL agg)  
	 ->  Partial Aggregate  (cost=15837.00..15837.01 rows=1 width=8) (actual time=52.333..52.333 rows=1 loops=5)  
               Output: PARTIAL count(*)  
               Buffers: shared hit=12712  
               Worker 0: actual time=50.917..50.918 rows=1 loops=1  
                 Buffers: shared hit=2397  
               Worker 1: actual time=51.293..51.294 rows=1 loops=1  
                 Buffers: shared hit=2423  
               Worker 2: actual time=51.062..51.063 rows=1 loops=1  
                 Buffers: shared hit=2400  
               Worker 3: actual time=51.436..51.436 rows=1 loops=1  
                 Buffers: shared hit=2432  
               ->  Parallel Seq Scan on public.test  (cost=0.00..15212.00 rows=250000 width=0) (actual time=0.010..30.499 rows=200000 loops=5)  
                     Buffers: shared hit=12712  
                     Worker 0: actual time=0.013..30.343 rows=190269 loops=1  
                       Buffers: shared hit=2397  
                     Worker 1: actual time=0.010..30.401 rows=192268 loops=1  
                       Buffers: shared hit=2423  
                     Worker 2: actual time=0.013..30.467 rows=190350 loops=1  
                       Buffers: shared hit=2400  
                     Worker 3: actual time=0.009..30.221 rows=192861 loops=1  
                       Buffers: shared hit=2432  
 Planning time: 0.074 ms  
 Execution time: 60.169 ms  
(31 rows)  

了解了并行聚合的原理后,我们就可以写自定义聚合函数的并行计算了。

例子

例如我们要支持一个数组的聚合,并且在聚合过程中我们要实现对元素去重。

1、创建测试表

create table test(id int, col int[]);  

2、生成测试数据

CREATE OR REPLACE FUNCTION public.gen_arr(integer, integer)  
 RETURNS integer[]  
 LANGUAGE sql  
 STRICT  
AS $function$  
  select array(select ($1*random())::int from generate_series(1,$2));  
$function$;  
  
insert into test select random()*1000, gen_arr(500,10) from generate_series(1,10000);  

3、创建聚合函数

例子1,没有combinefunc,只支持扫描并行。

数组去重函数

postgres=# create or replace function uniq(int[]) returns int[] as $$  
  select array( select unnest($1) group by 1);  
$$ language sql strict parallel safe;  
CREATE FUNCTION  

数组合并与去重函数

postgres=# create or replace function array_uniq_cat(anyarray,anyarray) returns anyarray as $$  
  select uniq(array_cat($1,$2));   
$$ language sql strict parallel safe;  
CREATE FUNCTION  

聚合函数(不带COMBINEFUNC)

create aggregate arragg (anyarray) (sfunc = array_uniq_cat, stype=anyarray, PARALLEL=safe);  

并行查询例子:

postgres=# set max_parallel_workers=4;  
SET  
postgres=# set max_parallel_workers_per_gather =4;  
SET  
postgres=# set parallel_setup_cost =0;  
SET  
postgres=# set parallel_tuple_cost =0;  
SET  
postgres=# alter table test set (parallel_workers =4);  
ALTER TABLE  
postgres=# explain (analyze,verbose,timing,costs,buffers) select id, arragg(col) from test group by id ;  

很明显没有设置COMBINEFUNC时,未使用并行聚合。

postgres=# explain (analyze,verbose,timing,costs,buffers) select id, arragg(col) from test group by id ;  
                                                            QUERY PLAN                                                               
-----------------------------------------------------------------------------------------------------------------------------------  
 HashAggregate  (cost=4139.74..4141.74 rows=200 width=36) (actual time=602.957..603.195 rows=1001 loops=1)  
   Output: id, arragg(col)  
   Group Key: test.id  
   Buffers: shared hit=6  
   ->  Gather  (cost=0.00..163.37 rows=15748 width=36) (actual time=0.328..43.734 rows=10000 loops=1)  
         Output: id, col  
         Workers Planned: 4  
         Workers Launched: 4  
         Buffers: shared hit=6  
         -- 只有并行扫描,没有并行聚合。  
	 ->  Parallel Seq Scan on public.test  (cost=0.00..163.37 rows=3937 width=36) (actual time=0.017..0.891 rows=2000 loops=5)  
               Output: id, col  
               Buffers: shared hit=124  
               Worker 0: actual time=0.019..0.177 rows=648 loops=1  
                 Buffers: shared hit=8  
               Worker 1: actual time=0.022..0.180 rows=648 loops=1  
                 Buffers: shared hit=8  
               Worker 2: actual time=0.017..3.772 rows=7570 loops=1  
                 Buffers: shared hit=94  
               Worker 3: actual time=0.015..0.189 rows=648 loops=1  
                 Buffers: shared hit=8  
 Planning time: 0.084 ms  
 Execution time: 603.450 ms  
(22 rows)  

例子2,有combinefunc,支持并行聚合。

drop aggregate arragg(anyarray);  
  
create aggregate arragg (anyarray) (sfunc = array_uniq_cat, stype=anyarray, COMBINEFUNC = array_uniq_cat, PARALLEL=safe);   

使用了并行聚合。

postgres=# explain (analyze,verbose,timing,costs,buffers) select id, arragg(col) from test group by id ;  
                                                               QUERY PLAN                                                                  
-----------------------------------------------------------------------------------------------------------------------------------------  
 Finalize HashAggregate  (cost=1361.46..1363.46 rows=200 width=36) (actual time=285.489..285.732 rows=1001 loops=1)  
   Output: id, arragg(col)  
   Group Key: test.id  
   Buffers: shared hit=36  
   ->  Gather  (cost=1157.46..1159.46 rows=800 width=36) (actual time=63.654..74.163 rows=4297 loops=1)  
         Output: id, (PARTIAL arragg(col))  
         Workers Planned: 4  
         Workers Launched: 4  
         Buffers: shared hit=36  
         -- 并行聚合  
	 ->  Partial HashAggregate  (cost=1157.46..1159.46 rows=200 width=36) (actual time=57.367..57.727 rows=859 loops=5)  
               Output: id, PARTIAL arragg(col)  
               Group Key: test.id  
               Buffers: shared hit=886  
               Worker 0: actual time=54.788..54.997 rows=857 loops=1  
                 Buffers: shared hit=213  
               Worker 1: actual time=56.881..57.255 rows=861 loops=1  
                 Buffers: shared hit=213  
               Worker 2: actual time=55.415..55.813 rows=856 loops=1  
                 Buffers: shared hit=212  
               Worker 3: actual time=56.453..56.854 rows=838 loops=1  
                 Buffers: shared hit=212  
               ->  Parallel Seq Scan on public.test  (cost=0.00..163.37 rows=3937 width=36) (actual time=0.011..0.736 rows=2000 loops=5)  
                     Output: id, col  
                     Buffers: shared hit=124  
                     Worker 0: actual time=0.009..0.730 rows=1981 loops=1  
                       Buffers: shared hit=25  
                     Worker 1: actual time=0.012..0.773 rows=2025 loops=1  
                       Buffers: shared hit=25  
                     Worker 2: actual time=0.015..0.741 rows=1944 loops=1  
                       Buffers: shared hit=24  
                     Worker 3: actual time=0.012..0.751 rows=1944 loops=1  
                       Buffers: shared hit=24  
 Planning time: 0.073 ms  
 Execution time: 285.949 ms  
(34 rows)  

实际上并行聚合与分布式数据库聚合阶段原理是一样的,分布式数据库自定义聚合可以参考末尾的文章。

例子3,将多个一元数组聚合为一个一元数组

PostgreSQL内置的array_agg会将数组聚合为多元数组,有些场景无法满足需求。

                                    List of functions
   Schema   |          Name           | Result data type |  Argument data types  |  Type  
------------+-------------------------+------------------+-----------------------+--------
 pg_catalog | array_agg               | anyarray         | anyarray              | agg
 pg_catalog | array_agg               | anyarray         | anynonarray           | agg
postgres=# \set VERBOSITY verbose
postgres=# select array_agg(info) from (values(array[1,2,3]),(array[2,3,4,5])) t(info);
ERROR:  2202E: cannot accumulate arrays of different dimensionality
LOCATION:  accumArrayResultArr, arrayfuncs.c:5270
postgres=# select array_agg(info) from (values(array[1,2,3]),(array[3,4,5])) t(info);
     array_agg     
-------------------
 {\{1,2,3\},\{3,4,5\}}
(1 row)

如果要将数组合并为一元数组,可以自定义一个聚合函数如下:

postgres=# create aggregate arragg (anyarray) (sfunc = array_cat, stype=anyarray, PARALLEL=safe);  
CREATE AGGREGATE

postgres=# select arragg(info) from (values(array[1,2,3]),(array[3,4,5])) t(info);
    arragg     
---------------
 {1,2,3,3,4,5}
(1 row)

postgres=# select arragg(info) from (values(array[1,2,3]),(array[2,3,4,5])) t(info);
     arragg      
-----------------
 {1,2,3,2,3,4,5}
(1 row)

参考

https://www.postgresql.org/docs/10/static/sql-createaggregate.html

https://www.postgresql.org/docs/10/static/xaggr.html#XAGGR-PARTIAL-AGGREGATES

《PostgreSQL aggregate function customize》

《Greenplum 最佳实践 - 估值插件hll的使用(以及hll分式聚合函数优化)》

《Postgres-XC customized aggregate introduction》

MySQL · 源码分析 · InnoDB的read view,回滚段和purge过程简介

$
0
0

笔者最近开始学习InnoDB的内部机制,参照之前的几篇文章整理出InnoDB多版本部分相关的一些实现原理。

InnoDB undo log 漫游

性能优化·5.7 Innodb事务系统

InnoDB 事务系统

[MySQL 5.6] Innodb 新特性之 multi purge thread

innodb purge操作

对于undo日志,第1篇文章写得非常清楚,图文并茂。本文有关undo的大部分内容也是取自此文,这里只是以笔者的视角重新组织描述一下。

在此特别感谢前面同学多年的积累和热心分享:)

笔者属于学习阶段,如描述有问题请多指正。

Read view

InnoDB支持MVCC多版本,其中RC(Read Committed)和RR(Repeatable Read)隔离级别是利用consistent read view(一致读视图)方式支持的。 所谓consistent read view就是在某一时刻给事务系统trx_sys打snapshot(快照),把当时trx_sys状态(包括活跃读写事务数组)记下来,之后的所有读操作根据其事务ID(即trx_id)与snapshot中的trx_sys的状态作比较,以此判断read view对于事务的可见性。

Read view中保存的trx_sys状态主要包括

  • low_limit_id:high water mark,大于等于view->low_limit_id的事务对于view都是不可见的
  • up_limit_id:low water mark,小于view->up_limit_id的事务对于view一定是可见的
  • low_limit_no:trx_no小于view->low_limit_no的undo log对于view是可以purge的
  • rw_trx_ids:读写事务数组

RR隔离级别(除了Gap锁之外)和RC隔离级别的差别是创建snapshot时机不同。 RR隔离级别是在事务开始时刻,确切地说是第一个读操作创建read view的;RC隔离级别是在语句开始时刻创建read view的。

创建/关闭read view需要持有trx_sys->mutex,会降低系统性能,5.7版本对此进行优化,在事务提交时session会cache只读事务的read view。

下次创建read view,判断如果是只读事务并且系统的读写事务状态没有发生变化,即trx_sys的max_trx_id没有向前推进,而且没有新的读写事务产生,就可以重用上次的read view。

Read view创建之后,读数据时比较记录最后更新的trx_id和view的high/low water mark和读写事务数组即可判断可见性。

如前所述,如果记录最新数据是当前事务trx的更新结果,对应当前read view一定是可见的。

除此之外可以通过high/low water mark快速判断:

  • trx_id < view->up_limit_id的记录对于当前read view是一定可见的;
  • trx_id >= view->low_limit_id的记录对于当前read view是一定不可见的;

如果trx_id落在[up_limit_id, low_limit_id),需要在活跃读写事务数组查找trx_id是否存在,如果存在,记录对于当前read view是不可见的。

由于InnoDB的二级索引只保存page最后更新的trx_id,当利用二级索引进行查询的时候,如果page的trx_id小于view->up_limit_id,可以直接判断page的所有记录对于当前view是可见的,否则需要回clustered索引进行判断。

如果记录对于view不可见,需要通过记录的DB_ROLL_PTR指针遍历history list构造当前view可见版本数据。

回滚段

InnoDB也是采用回滚段的方式构建old version记录,这跟Oracle方式类似。

记录的DB_ROLL_PTR指向最近一次更新所创建的回滚段;每条undo log也会指向更早版本的undo log,从而形成一条更新链。通过这个更新链,不同事务可以找到其对应版本的undo log,组成old version记录,这条链就是记录的history list。

分配rollback segment

MySQL 5.6对于没有显示指定READ ONLY事务,默认为是读写事务。在事务开启时刻分配trx_id和回滚段,并把当前事务加到trx_sys的读写事务数组中。

5.7版本对于所有事务默认为只读事务,遇到第一个写操作时,只读事务切换成读写事务分配trx_id和回滚段,并把当前事务加到trx_sys的读写事务数组中。

分配回滚段的工作在函数trx_assign_rseg_low进行,分配策略是采用round-robin方式。

从5.6开始支持独立的undo表空间,InnoDB支持128个undo回滚段,请参照第1篇文章。

  • rseg0:预留在系统表空间ibdata中
  • rseg1~rseg32:这32个回滚段存放于临时表的系统表空间中
  • rseg33~rseg127:根据配置存放到独立undo表空间中(如果没有打开独立Undo表空间,则存放于ibdata中)

trx_assign_rseg_low判断,如果支持独立的undo表空间,在undo表空间有可用回滚段的情况下避免使用系统表空间的回滚段。

rseg->skip_allocation为TRUE表示rseg所在的表空间要被truncate,应该避免使用此rseg分配回滚段。此种情况,必须保证有至少2个活跃的undo表空间,并且至少2个活跃的undo slot。

分配成功时,递增rseg->trx_ref_count,保证rseg的表空间不会被truncate。

临时表操作不记redo log,最终调用get_next_noredo_rseg函数进行分配;其他情况调用get_next_redo_rseg。

回滚段实际上是undo文件组织方式,每个回滚段维护了一个段头页(segment header),该page划分了1024个slot(TRX_RSEG_N_SLOTS),每个slot对应到一个undo log对象。

理论上,InnoDB最多支持 96 (128 - 32 /* temp-tablespace */) * 1024个普通事务。

但如果是临时表的事务,可能还需要多分配1个slot(临时表的系统表空间)。

  • 只读阶段为临时表分配的,在临时表的系统表空间中分配
  • 读写阶段在undo表空间分配

分配undo log

Insert数据只对当前事务或者提交之后可见,所以insert的undo log在事务commit后就可以释放了。

Update/delete的undo记录通常用来维护old version记录,为查询提供服务;只有当trx_sys中没有任何view需要访问那个old version的数据时才可以被释放。

InnoDB对insert和update/delete分配不同的undo slot

  • insert的undo slot记在trx->rsegs.m_redo.insert_undo,调用trx_undo_assign_undo分配
  • update的undo slot记在trx->rsegs.m_redo.undate_undo,调用trx_undo_assign_undo分配

trx_undo_assign_undo

I. 检查cached队列是否有缓存的undo log(内存中数据结构是trx_undo_t)

  • 如果存在,把这个undo log从cached队列移除
  • reuse的逻辑:

    a.insert undo:重新初始化undo page的header信息(trx_undo_insert_header_reuse),并在redo log记一条MLOG_UNDO_HDR_REUSE日志

    b.update undo:在undo page的header上分配新的undo header(trx_undo_header_create),并在redo log记一条MLOG_UNDO_HDR_CREATE日志

  • 预留xid空间
  • 重新初始化undo(trx_undo_mem_init_for_reuse)把undo->state设置为TRX_UNDO_ACTIVE,并把undo->state写入到第一个undo page的TRX_UNDO_SEG_HDR+TRX_UNDO_STATE位置上

注1:TRX_UNDO_SEG_HDR表示segment header起始offset 注2:undo segment与事务trx是一一对应关系,undo segment header的状态(TRX_UNDO_STATE)跟事务当前状态也是一一对应的

如下图(引自第1篇文章)

图片.png

undo segment是个独立的段,每个undo segment包含1个header page(第1个undo page)和若干个记录undo日志的undo page。

第1个undo page中存储的是元信息: 首先存储的是undo page的元信息,位于TRX_UNDO_PAGE_HDR到TRX_UNDO_SEG_HDR之间。

TRX_UNDO_PAGE_START:指向page中第一个undo log TRX_UNDO_PAGE_FREE:指向page中下一个undo log要写到的位置 TRX_UNDO_PAGE_NODE:undo segment所有page组成一个双向链表,每个page的TRX_UNDO_PAGE_NODE字段作为连接件,第一个undo page中的TRX_UNDO_PAGE_LIST作为表头


/* undo page header */
#define TRX_UNDO_PAGE_HDR   FSEG_PAGE_DATA
#define TRX_UNDO_PAGE_TYPE  0   /*!< TRX_UNDO_INSERT or
                    TRX_UNDO_UPDATE */
#define TRX_UNDO_PAGE_START 2   /*!< Byte offset where the undo log
                    records for the LATEST transaction
                    start on this page (remember that
                    in an update undo log, the first page
                    can contain several undo logs) */
#define TRX_UNDO_PAGE_FREE  4   /*!< On each page of the undo log this
                    field contains the byte offset of the
                    first free byte on the page */
#define TRX_UNDO_PAGE_NODE  6   /*!< The file list node in the chain
                    of undo log pages */

/*-------------------------------------------------------------*/
#define TRX_UNDO_PAGE_HDR_SIZE  (6 + FLST_NODE_SIZE)
                    /*!< Size of the transaction undo
                    log page header, in bytes */

之后是undo segment的元信息,位于TRX_UNDO_SEG_HDR到TRX_UNDO_SEG_HDR+TRX_UNDO_SEG_HDR_SIZE

TRX_UNDO_STATE:表示undo segment的状态,一个undo segment可以包含多个undo log,但至多只有1个active undo log,也就是最近的undo log TRX_UNDO_LAST_LOG:指向最近的undo log的header信息 TRX_UNDO_FSEG_HEADER:存储的是undo segment对应的file segment信息,在fseg_create_general中设置(4字节space id,4字节的page no,2字节的page offset)

undo segment从buffer pool移除被persist到磁盘时,就写到file segment指定的位置上


#define TRX_UNDO_SEG_HDR    (TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_HDR_SIZE)
#define TRX_UNDO_STATE      0   /*!< TRX_UNDO_ACTIVE, ... */
#define TRX_UNDO_LAST_LOG   2   /*!< Offset of the last undo log header
                    on the segment header page, 0 if
                    none */
#define TRX_UNDO_FSEG_HEADER    4   /*!< Header for the file segment which
                    the undo log segment occupies */
#define TRX_UNDO_PAGE_LIST  (4 + FSEG_HEADER_SIZE)
                    /*!< Base node for the list of pages in
                    the undo log segment; defined only on
                    the undo log segment's first page */

/*-------------------------------------------------------------*/
/** Size of the undo log segment header */
#define TRX_UNDO_SEG_HDR_SIZE   (4 + FSEG_HEADER_SIZE + FLST_BASE_NODE_SIZE)

再之后是undo log header信息,所有的undo log header都存储在第一个undo page上。

II. 从cached队列分配undo失败时,需要真正分配一个undo segment(trx_undo_seg_create)

首先要从rseg分配一个slot(trx_rsegf_undo_find_free),每个rseg至多支持1024个slot。找到空slot返回index。

如果当前rseg已满,trx_undo_seg_create返回DB_TOO_MANY_CONCURRENT_TRXS向上层报错,表示并发事务太多无法创建undo segment。

然后在rseg对应的table space创建一个新的file segment,file segment信息记在segment header的TRX_UNDO_FSEG_HEADER(fseg_create_general)。

trx_undo_seg_create在创建file segment之后,把新创建segment的page no写到rseg对应slot上建立映射关系,并返回新创建segment的page。

file segment与undo segment的映射关系,还有rseg[slot]与file segment对应page的映射关系都是在trx_undo_seg_create绑定的。cached undo不会更新这两个映射关系。

III. trx_undo_seg_create返回的page上创建新的undo header;上层负责初始化trx_undo_t数据结构

trx_undo_create为新创建的undo header创建内存数据结构trx_undo_t(trx_undo_mem_create),把undo->state设置为TRX_UNDO_ACTIVE。

IV. 分配好的trx_undo_t会加入到事务的insert_undo_list或者update_undo_list队列上

写入undo log

trx_undo_assign_undo分配undo之后,就可往其中写入undo记录。写入的page来自undo->last_page_no,初始情况下等于hdr_page_no。

update undo包含一个重要的部分:记录的当前回滚段指针要写到undo log里面,以便维护记录的历史数据链。

read view需要读老版本数据时,会通过记录中当前的回滚段指针开始向前找到可见版本的数据。

完成Undo log写入后,构建新的回滚段指针并返回(trx_undo_build_roll_ptr),这个指针也就是clustered索引记录的DB_ROLL_PTR。

回滚段指针包括rseg->id、日志所在的page no、以及page内偏移量,需要记录到clustered索引记录中。这里rseg->id用来确定rseg->space,真正用于定位undo log位置的其实是<rseg->space, undo->page,undo->page_offset>三元组。

事务prepare

设置undo->state为TRX_UNDO_PREPARED,并把这个状态写到第一个undo page的(TRX_UNDO_SEG_HDR+TRX_UNDO_STATE)位置上。

除此之外,prepare阶段还要更新xid信息。

事务commit

在事务commit阶段,需要把undo->state设置为完成状态,并把undo加到undo segment的history list。正在提交的undo header被指向history list的第一项,表示当前事务history list最近的undo。

undo->state完成状态包括3种,在trx_undo_set_state_at_finish设置

  • undo只占一个page,而且第一个undo page已使用的空间小于3/4 (TRX_UNDO_PAGE_REUSE_LIMIT):状态设置为TRX_UNDO_CACHED
  • 不满足1的情况下,如果是insert_undo(TRX_UNDO_INSERT):状态设置为TRX_UNDO_TO_FREE
  • 不满足1和2的情况下,状态设置为TRX_UNDO_TO_PURGE,表示undo可能需要purge线程清理

cached undo会被到cached队列上,这个队列就是trx_undo_assign_undo提到的cached队列

设置完undo->state之后,需要把这个状态写入到第一个undo page的(TRX_UNDO_SEG_HDR+TRX_UNDO_STATE)位置上

把undo加到undo segment header的history list

Insert的old version没有实际意义,所以insert undo在事务commit时就可以释放了。

trx_undo_set_state_at_finish里面有cached策略,如果只占1个undo page,并且undo page已使用的空间不足pagesize的3/4可以被reuse,其实大部分insert undo都属于这种情况。

Update undo需要维护history list。这里先提一下trx->no,它维护了事务trx commit顺序,跟事务的trx_id一样,也是使用max_trx_id递增产生。

另外,purge_sys(purge的全局数据结构)维护个最小堆,每个rollback segment第1次事务提交时向最小堆插入数据,旨在找到trx_no最小的rollback segment进行purge。后面每次处理完1个rseg后,会把下一个undo记录的trx_no压入到这个最小堆,作为rseg的cursor。

事务commit时按照trx->no顺序,把事务当前的undo log挂到undo segment history list的表头,指向事务最近的undo log。

History list里的undo都是已提交事务的,当前事务所修改的undo log都记录在这里,按照从新->老方式排列,最老的undo log在尾部。

undo加入到history list的方式是:以undo log的TRX_UNDO_HISTORY_NODE作为连接件,加入到第一个undo page的TRX_RSEG_HISTORY。

一般来说,每次调用trx_purge_add_update_undo_to_history都会把undo加入到history list,只有在undo page无法被reuse时才更新history list大小(可以认为是个优化,最后一次更新history length)。

在此之后,trx_purge_add_update_undo_to_history会把undo log header的TRX_UNDO_TRX_NO更新为trx_no。

如果undo->del_marks是FALSE,这个函数也会更新TRX_UNDO_DEL_MARKS(undo segment创建或者reuse被初始化为TRUE),澄清这不是delete marker。

如果undo segment自创建以来(也可能是上次purge完成之后)中第1个事务commit,还需要更新purge有关的一些参数,指向下次purge从哪里开始执行。

老版本数据purge

旧版本数据不再被任何view访问就可以被删除了。5.6以上版本支持独立purge线程,用户可以通过参数Innodb_purge_threads设置purge线程个数。

有两类purge线程:

  • coordinator thread:srv_purge_coordinator_thread,全局只有1个
  • worker thread:srv_worker_thread,系统有innodb_purge_threads - 1个

coordinator thread负责启动worker thread参与到purge工作中。

增加purge线程的策略是:trx_sys->rseg_history_len比上次循环变大了或者rseg_history_len超过某一阈值,需要引进更多的worker thread。

减少purge线程的策略是:如果之前使用多个purge 线程,trx_sys->rseg_history_len并没有变大,可能需要减少worker thread。

在进行purge之前,首先要确定purge线程要做哪些工作,也就是说哪些undo log可以被purged。

purge也是通过read view来确定工作范围,被称为purge view。如果系统有活跃read view,就选取最老的read view作为purge view。

如果不存在就给trx_sys的状态打个snapshot,作为purge view,可以被purge的undo log其trx_no一定是小于系统中所有已提交事务的trx->no。

这里插一句,在事务commit时,会把产生的trx->no加入到trx_sys->serialisation_list链表,这个链表是按照trx->no升序次序排列,也就是维护了trx commit顺序。

InnoDB初始化的时候会初始化purge_sys数据结构,其中一个工作就是创建purge graph。

这是总共3层结构的图:

  • 第1层是fork节点
  • 第2次是thrd节点(表示purge thread)
  • 第3层是node节点(表示purge task)

所有的thrd节点被链入到fork->thrs链表中;fork地址存储在purge_sys->query,可以通过purge_sys直接访问。

执行purge的时候总是遍历purge_sys->query->thrs链表,给每个purge线程分配purge任务(trx_purge_attach_undo_recs)。

解析undo log的调用路径如下:


srv_purge_coordinator_thread -> srv_do_purge -> trx_purge ->
        trx_purge_attach_undo_recs -> trx_purge_fetch_next_rec -> 
               trx_purge_get_next_rec

purge_sys->next_stored为FALSE时,表示rseg_iter当前指向的rseg无效,需要把rseg_iter移到下一个有效的rseg(TrxUndoRsegsIterator::set_next)。

purge_sys->purge_queue维护了一个最小堆,每次pop最顶元素,可以得到trx_no最小的rollback segment(TrxUndoRsegsIterator::set_next)。

5.7支持临时表的noredo的rollback segment,set_next遇到redo rollback segment和noredo rollback segment同时存在的情况会一股脑把这两个rollback segment都pop出来加入到 purge_sys->rseg_iter->m_trx_undo_rsegs数组中,也在TrxUndoRsegsIterator::set_next实现。

如果没有rollback segment需要purge话,purge_sys->rseg设置为NULL,purge线程会去睡眠(trx_purge_choose_next_log)。

一般情况下都是有rollback segment需要处理的,purge_sys->rseg更新成purge_sys->rseg_iter->m_trx_undo_rsegs的第1项(至多2项)。

purge_sys中的相应成员也要更新,指向当前rseg上次purge到的位置(TrxUndoRsegsIterator::set_next)。

update undo的del_marks域正常情况下都是TRUE,因为update/delete操作都需要对old value进行标记删除。

如果purge_sys->rseg->last_del_marks是FALSE的话,表示这是一个dummy的undo log,不需要做物理删除。这种情况下,把purge_sys->offset设置成0,做个标记表示这个undo log不需要被purged(trx_purge_read_undo_rec)。

正常情况下purge_sys->rseg->last_del_marks是TRUE,可以通过<purge_sys->rseg->space, purge_sys->hdr_page_no, purge_sys->hdr_offset>读取undo log记录(trx_purge_read_undo_rec)。

并把purge_sys以下四个域设置成undo log记录相应的信息(trx_purge_read_undo_rec)。

    purge_sys->offset = offset; /* undo log记录的offset */
    purge_sys->page_no = page_no; /* undo log记录的pageno */
    purge_sys->iter.undo_no = undo_no; /* undo log记录的undo_no,trx内部undo的序列号 */
    purge_sys->iter.undo_rseg_space = undo_rseg_space; /* undo log的tablespace */

为了保证purge_sys以上4个域一定是指向下一个有效undo log,每次读取undo log时都会捎带着读取下一个undo log,并把上面这四个域更新为下一个undo log的信息,方面后续访问(trx_purge_get_next_rec)。

如果是dummy undo,trx_purge_get_next_rec会去读prev_undo(trx_purge_rseg_get_next_history_log),用prev_log信息更新rseg中下一个purge信息。

在此之后,还会把rseg->last_trx_no压入最小堆,待后面继续处理这个rseg。 然后调用trx_purge_choose_next_log选择下一个处理的rseg,并读取第一个undo log(trx_purge_get_next_rec)。

就这样挨个读取undo log,trx_purge_attach_undo_recs中有一个大循环,每次调用trx_purge_fetch_next_rec读到一个undo log后,把它存放到purge节点(purge graph的第三级节点) node->undo_recs数组里面,循环下一次执行切换到下一个thr(purge 线程)。

循环的结束条件是:

  • 没有新的undo log
  • 处理过的undo log达到batch size(一般是300)

达到循环结束条件后,trx_purge_attach_undo_recs返回。如果n_purge_threads > 1 (需要worker线程参与purge),coordinator线程会以round-robin方式启动n_purge_threads - 1个worker线程。

不管有没有worker线程参与purge,coordinator线程都会调用que_run_threads(在trx_purge上下文)去处理purge任务。

purge任务如何处理呢?通俗的说purge就是删除被标记delete marker的记录项。

大致过程如下:


srv_purge_coordinator_thread -> srv_do_purge -> trx_purge ->
        que_run_threads -> que_run_threads_low -> que_thr_step
               row_purge_step -> row_purge -> row_purge_record ->
                       row_purge_del_mark -> row_purge_remove_sec_if_poss

一般删除的原则是先删除二级索引再删除clustered索引(row_purge_del_mark)。

另一种情况是聚集索引in-place更新了,但二级索引上的记录顺序可能发生变化,而二级索引的更新总是标记删除 + 插入,因此需要根据回滚段记录去检查二级索引记录序是否发生变化,并执行清理操作(row_purge_upd_exist_or_extern)。

前面提到过在parse undo log时,可能遇到dummy undo log。返回到row_purge执行时需要判读是否是dummy undo,如果是就什么也不做。

truncate undo space

trx_purge在处理完一个batch(通常是300)之后,调用trx_purge_truncate_historypurge_sys对每一个rseg尝试释放undo log(trx_purge_truncate_rseg_history)。

大致过程是:把每个purge过的undo log从history list移除,如果undo segment中所有的undo log都被释放,可以尝试释放undo segment,这里隐式释放file segment到达释放存储空间的目的。

由于篇幅有限,这部分就不深入介绍了。

MySQL · 源码分析 · 原子DDL的实现过程

$
0
0

众所周知,MySQL8.0之前的版本DDL是非原子的。也就是说对于复合的DDL,比如DROP TABLE t1, t2;执行过程中如果遇到server crash,有可能出现表t1被DROP掉了,但是t2没有被DROP掉的情况。即便是一条DDL,比如CREATE TABLE t1(a int);也可能在server crash的情况下导致建表不完整,有可能在建表失败的情况下遗留.frm或者.ibd文件。

上面情况出现的主要原因就是MySQL不支持原子的DDL。从图1可以看出,MySQL8.0以前,metadata在Server layer是存储在MyISAM引擎的系统表里,对于事务型引擎Innodb则自己存储一份metadata。这也导致MySQL存在如下一些弊端:

  1. metadata由于存储在Server layer以及存储引擎(这里特指Innodb),两份系统表很容易造成数据的不一致。
  2. 两份系统表存储的信息有所不同,访问Server layer以及存储引擎需要使用不同API,这种设计导致了不能很好的统一对系统metadata的访问。另外两份API,也同时增加了代码的维护量。
  3. 由于Server layer的metadata存储在非事务引擎(MyISAM)里,所以在进行crash recovery的时候就不能维持原子性。
  4. DDL的非原子性使得Replication处理异常情况变得更加复杂。比如DROP TABLE t1, t2; 如果DROP t1成功,但是DROP t2失败,Replication就无法保证主备一致性了。

atomic-ddl-1.png

图1: MySQL Data Dictionary before MySQL8.0

MySQL8.0为了解决上面的缺陷,引入了事务型DDL。首先我们看一下MySQL8.0 metadata存储的架构变化:

atomic-ddl-2.png

图2: MySQL Data Dictionary in MySQL8.0

图2我们可以看到,Server layer(后面简称SL)以及Storage Engine(后面简称SE) 使用同一份data dictionary(后面简称DD)用来存储metadata。SL和SE将各自需要的metadata存入DD中。由于DD使用Innodb作为存储引擎,所以crash recovery的时候,DD可以安全的进行事务回滚。

下面我们介绍一下MySQL8.0为了实现原子DDL,在源码层面引入的一些重要数据结构:

class Dictionary_client
/*
   这个类提供了SL以及SE统一访问DD的接口。每一个THD都有一个访问DD的Dictionary_client类型的成员。 如果需要操作DD,直接调用相关接口函数即可。
   这个类成员函数的主要方法是去访问一个多session共享的cache来操作DD存储的各种对象。和其他cache一样,如果在访问过程中,在这个cache里没有找到对应的对象,那么后台会自动读取DD中的相关metadata,进而构建相应的数据表。
*/
{
public:

/*
  这个类是用来辅助Dictionary_client自动释放获取的DD对象。该类会自动跟踪当前Dictionary_client获取的每个DD对象。当Dictionary_client对象生命期结束的时候,该对象会自动释放当前session获取的DD对象。
  这个类对象可以进行嵌套,Dictionary_client中的m_current_releaser成员变量始终会指向嵌套堆栈最顶层的一个Auto_releaser对象。如果当前的Auto_releaser对象结束了生命期,它会释放掉自己记录的位于共享cache中的DD对象,同时把m_current_releaser指向上一个老的Auto_releaser对象。
*/
class Auto_releaser
{
  friend class Dictionary_client;

  private:
    Dictionary_client *m_client; // 用来指向当前的Dictionary_client对象
    Object_registry m_release_registry; // 用来记录从共享cache中获取的DD对象,以便自动释放
    Auto_releaser *m_prev; // 用来形成列表,以方便当前实例生命期结束的时候,将Dictionary_client对象中的Auto_releaser重新指向之前创建的实例。

    /**
      注册一个DD对象
    */
    template <typename T>
    void auto_release(Cache_element<T> *element)
    {
      // Catch situations where we do not use a non-default releaser.
      DBUG_ASSERT(m_prev != NULL);
      m_release_registry.put(element);
    }

    /**
	  当一个Auto_releaser对象结束生命期的时候,有的DD对象并不能结束生命期,该函数用来把一个DD对象转移给上一个Auto_releaser对象。
    */
    template <typename T>
    void transfer_release(const T* object);

    /**
      移除一个DD对象
     */
    template <typename T>
    Auto_releaser *remove(Cache_element<T> *element);

    // Create a new empty auto releaser. Used only by the Dictionary_client.
    Auto_releaser();

  public:
    /**
      Create a new auto releaser and link it into the dictionary client
      as the current releaser.

      @param  client  Dictionary client for which to install this auto
                      releaser.
    */
    explicit Auto_releaser(Dictionary_client *client);

    // Release all objects registered and restore previous releaser.
    ~Auto_releaser();

    // Debug dump to stderr.
    template <typename T>
    void dump() const;
};

private:
  std::vector<Entity_object*> m_uncached_objects; // Objects to be deleted.
  Object_registry m_registry_committed;   // Registry of committed objects.
  Object_registry m_registry_uncommitted; // Registry of uncommitted objects.
  Object_registry m_registry_dropped;     // Registry of dropped objects.
  THD *m_thd;                             // Thread context, needed for cache misses.
  Auto_releaser m_default_releaser;       // Default auto releaser.
  Auto_releaser *m_current_releaser;      // Current auto releaser.

...
}


/**
  该类定义了共享的DD对象缓存,该类取代了8.0之前的table_cache。数据库对象会根据对象类型从不同的map中获取对象。所有对DD对象的访问都需要经过该缓存。
*/
class Shared_dictionary_cache
{
private:
  // 设置一些缓存的最大容量,目前看来都是硬编码
  static const size_t collation_capacity= 256;
  static const size_t column_statistics_capacity= 32;
  static const size_t charset_capacity= 64;
  static const size_t event_capacity= 256;
  static const size_t spatial_reference_system_capacity= 256;
  /**
    Maximum number of DD resource group objects to be kept in
    cache. We use value of 32 which is a fairly reasonable upper limit
    of resource group configurations that may be in use.
  */
  static const size_t resource_group_capacity= 32;

  /* 下面是各种不同类型DD对象缓存map */
  Shared_multi_map<Abstract_table> m_abstract_table_map;
  Shared_multi_map<Charset>        m_charset_map;
  Shared_multi_map<Collation>      m_collation_map;
  Shared_multi_map<Column_statistics> m_column_stat_map;
  Shared_multi_map<Event>          m_event_map;
  Shared_multi_map<Resource_group> m_resource_group_map;
  Shared_multi_map<Routine>        m_routine_map;
  Shared_multi_map<Schema>         m_schema_map;
  Shared_multi_map<Spatial_reference_system> m_spatial_reference_system_map;
  Shared_multi_map<Tablespace>     m_tablespace_map;

  template <typename T> struct Type_selector { }; // Dummy type to use for
                                                  // selecting map instance.

  /**
    Overloaded functions to use for selecting map instance based
    on a key type. Const and non-const variants.
  */
  Shared_multi_map<Abstract_table> *m_map(Type_selector<Abstract_table>)
  { return &m_abstract_table_map; }
  Shared_multi_map<Charset>        *m_map(Type_selector<Charset>)
  { return &m_charset_map; }
  Shared_multi_map<Collation>      *m_map(Type_selector<Collation>)
  { return &m_collation_map; }
  Shared_multi_map<Column_statistics> *m_map(Type_selector<Column_statistics>)
  { return &m_column_stat_map; }
  Shared_multi_map<Event>        *m_map(Type_selector<Event>)
  { return &m_event_map; }
  Shared_multi_map<Resource_group> *m_map(Type_selector<Resource_group>)
  { return &m_resource_group_map; }
  Shared_multi_map<Spatial_reference_system> m_spatial_reference_system_map;
  Shared_multi_map<Tablespace>     m_tablespace_map;

  template <typename T> struct Type_selector { }; // Dummy type to use for
                                                  // selecting map instance.

  /**
    Overloaded functions to use for selecting map instance based
    on a key type. Const and non-const variants.
  */
  Shared_multi_map<Abstract_table> *m_map(Type_selector<Abstract_table>)
  { return &m_abstract_table_map; }
  Shared_multi_map<Charset>        *m_map(Type_selector<Charset>)
  { return &m_charset_map; }
  Shared_multi_map<Collation>      *m_map(Type_selector<Collation>)
  { return &m_collation_map; }
  Shared_multi_map<Column_statistics> *m_map(Type_selector<Column_statistics>)
  { return &m_column_stat_map; }
  Shared_multi_map<Event>        *m_map(Type_selector<Event>)
  { return &m_event_map; }
  Shared_multi_map<Resource_group> *m_map(Type_selector<Resource_group>)
  { return &m_resource_group_map; }
  Shared_multi_map<Routine>        *m_map(Type_selector<Routine>)
  { return &m_routine_map; }
  Shared_multi_map<Schema>         *m_map(Type_selector<Schema>)
 { return &m_schema_map; }
  Shared_multi_map<Spatial_reference_system> *
    m_map(Type_selector<Spatial_reference_system>)
  { return &m_spatial_reference_system_map; }
  Shared_multi_map<Tablespace>     *m_map(Type_selector<Tablespace>)
  { return &m_tablespace_map; }


  const Shared_multi_map<Abstract_table> *m_map(Type_selector<Abstract_table>) const
  { return &m_abstract_table_map; }
  const Shared_multi_map<Charset>        *m_map(Type_selector<Charset>) const
  { return &m_charset_map; }
  const Shared_multi_map<Collation>      *m_map(Type_selector<Collation>) const
  { return &m_collation_map; }
  const Shared_multi_map<Column_statistics> *
    m_map(Type_selector<Column_statistics>) const
  { return &m_column_stat_map; }
  const Shared_multi_map<Schema>         *m_map(Type_selector<Schema>) const
  { return &m_schema_map; }
  const Shared_multi_map<Spatial_reference_system> *
    m_map(Type_selector<Spatial_reference_system>) const
  { return &m_spatial_reference_system_map; }
  const Shared_multi_map<Tablespace>     *m_map(Type_selector<Tablespace>) const
  { return &m_tablespace_map; }
  const Shared_multi_map<Resource_group> *m_map(
  { return &m_abstract_table_map; }
  const Shared_multi_map<Charset>        *m_map(Type_selector<Charset>) const
  { return &m_charset_map; }
  const Shared_multi_map<Collation>      *m_map(Type_selector<Collation>) const
  { return &m_collation_map; }
  const Shared_multi_map<Column_statistics> *
    m_map(Type_selector<Column_statistics>) const
  { return &m_column_stat_map; }
  const Shared_multi_map<Schema>         *m_map(Type_selector<Schema>) const
  { return &m_schema_map; }
  const Shared_multi_map<Spatial_reference_system> *
    m_map(Type_selector<Spatial_reference_system>) const
  { return &m_spatial_reference_system_map; }
  const Shared_multi_map<Tablespace>     *m_map(Type_selector<Tablespace>) const
  { return &m_tablespace_map; }
  const Shared_multi_map<Resource_group> *m_map(
    Type_selector<Resource_group>) const
  { return &m_resource_group_map; }

  /**
    根据DD对象类型获取对应的map对象。
  */
  template <typename T>
  Shared_multi_map<T> *m_map()
  { return m_map(Type_selector<T>()); }

  template <typename T>
  const Shared_multi_map<T> *m_map() const
  { return m_map(Type_selector<T>()); }

  Shared_dictionary_cache()
  { }

public:
  static Shared_dictionary_cache *instance();

  // Set capacity of the shared maps.
  static void init();

  // Shutdown the shared maps.
  static void shutdown();

  // Reset the shared cache. Optionally keep the core DD table meta data.
  static void reset(bool keep_dd_entities);
  // Reset the table and tablespace partitions.
  static bool reset_tables_and_tablespaces(THD *thd);

  /**
    根据DD类型及名称来验证对象是否在缓存中。
  */
  template <typename K, typename T>
  bool available(const K &key)
  { return m_map<T>()->available(key); }

  /**
    该函数用来输出调试信息。
  */
  template <typename T>
  void dump() const
  {
#ifndef DBUG_OFF
    fprintf(stderr, "================================\n");
    fprintf(stderr, "Shared dictionary cache\n");
    m_map<T>()->dump();
    fprintf(stderr, "================================\n");
#endif
  }
};

} // namespace cache


/**
  这个类抽象了对DD对象的metadata进行存储的方法。它是一个静态类。对于新创建的对象(表,索引,表空间等)都会通过该类进行一个clone, clone之后该类会将该对象的metadata存储到对应的系统表中。另外,它也提供接口用来从系统表中获取metadata并生成调用需要的DD对象。

该类同时也提供了一个缓存,每次调用存储新对象的时候,它会自动将一个对象clone缓存起来。该类成员函数中core_xxx都是负责操作缓存。
*/
class Storage_adapter
{
friend class dd_cache_unittest::CacheStorageTest;

private:

  /**
    Use an id not starting at 1 to make it easy to recognize ids generated
    before objects are stored persistently.
  */
  static const Object_id FIRST_OID= 10001;


  /**
    为新的对象产生一个ID标识。
  */
  template <typename T>
  Object_id next_oid();


  /**
	根据对象名称从缓存中返回一个对象的clone。
  */
  template <typename K, typename T>
  void core_get(const K &key, const T **object);


  Object_registry m_core_registry;   // Object registry storing core DD objects.
  mysql_mutex_t m_lock;              // Single mutex to protect the registry.
  static bool s_use_fake_storage;    // Whether to use the core registry to
                                     // simulate the storage engine.

  Storage_adapter()
  { mysql_mutex_init(PSI_NOT_INSTRUMENTED, &m_lock, MY_MUTEX_INIT_FAST); }
  ~Storage_adapter()
  {
    mysql_mutex_lock(&m_lock);
    m_core_registry.erase_all();
    mysql_mutex_unlock(&m_lock);
    mysql_mutex_destroy(&m_lock);
  }


public:

  /* 这里可以获取到单例。 */
  static Storage_adapter *instance();

  /**
    根据对象类型返回缓存区中所有对象的数量。
  */
  template <typename T>
  size_t core_size();

  /**
    获取对象ID标识
  */
  template <typename T>
  Object_id core_get_id(const typename T::Name_key &key);

  /**
	该函数可以根据对象类型及名称获取对象。如果该对象已经被缓存,那么调用core_get获取clone对象。否则会根据对象类型到对应的metadata数据表中查找并构造一个对象。
  */
  template <typename K, typename T>
  static bool get(THD *thd,
                  const K &key,
                  enum_tx_isolation isolation,
                  bool bypass_core_registry,
                  const T **object);
  /**
    缓存中清除一个对象.
  */
  template <typename T>
  void core_drop(THD *thd, const T *object);


  /**
    从对象所对应的各个metadata数据表中清除相关数据.
  */
  template <typename T>
  static bool drop(THD *thd, const T *object);
  /**
    缓冲区中添加一个DD对象
  */
  template <typename T>
  void core_store(THD *thd, T *object);

  /**
    该函数会根据DD对象类型,将metadata存入相关的系统表中。后面的建表语句中会对该函数进行详细的解释。
  */
  template <typename T>
  static bool store(THD *thd, T *object);

  /**
    同步缓存中的DD对象。
  */
  template <typename T>
  bool core_sync(THD *thd, const typename T::Name_key &key, const T *object);

  /**
    Remove and delete all elements and objects from core storage.
  */
  void erase_all();
  /**
    备份缓存中的对象。
  */
  void dump();
};

} // namespace cache
} // namespace dd



接下来我们以CREATE TABLE为例从源码上简单看一下MYSQL8.0是如何实现原子DDL的。

CREATE TABLE实现的流程图如下:

create-table.jpg

这里我们看一下CREATE TABLE过程中新增加的几个比较重要的函数(这里主要看Innodb存储引擎):

/*
  该函数将会为Innodb存储引擎创建它自己需要的系统列。实际上就是把原来Innodb自己的系统表统一到DD中。
*/
int
ha_innobase::get_extra_columns_and_keys(
  const HA_CREATE_INFO*,
  const List<Create_field>*,
  const KEY*,
  uint,
  dd::Table*  dd_table)
{
  DBUG_ENTER("ha_innobase::get_extra_columns_and_keys");
  THD*      thd     = ha_thd();
  dd::Index*    primary     = nullptr;
  bool      has_fulltext    = false;
  const dd::Index*  fts_doc_id_index  = nullptr;

  /* 检查各个定义的索引是否合法。*/
  for (dd::Index* i : *dd_table->indexes()) {
    /* The name "PRIMARY" is reserved for the PRIMARY KEY */
    ut_ad((i->type() == dd::Index::IT_PRIMARY)
          == !my_strcasecmp(system_charset_info, i->name().c_str(),
          primary_key_name));

    if (!my_strcasecmp(system_charset_info,
           i->name().c_str(), FTS_DOC_ID_INDEX_NAME)) {
      ut_ad(!fts_doc_id_index);
      ut_ad(i->type() != dd::Index::IT_PRIMARY);
      fts_doc_id_index = i;
    }

    /* 验证索引算法是否有效 */
    switch (i->algorithm()) {
	...
  }

  /* 验证并处理全文索引 */
  if (has_fulltext) {
    ...
  }

  /* 如果当前没有定义主键,Innodb将自动增加DB_ROW_ID作为主键。 */
  if (primary == nullptr) {
    dd::Column* db_row_id = dd_add_hidden_column(
      dd_table, "DB_ROW_ID", DATA_ROW_ID_LEN,
      dd::enum_column_types::INT24);

    if (db_row_id == nullptr) {
      DBUG_RETURN(ER_WRONG_COLUMN_NAME);
    }

    primary = dd_set_hidden_unique_index(
      dd_table->add_first_index(),
      primary_key_name,
      db_row_id);
  }

  /* 为二级索引增加主键列 */
  std::vector<const dd::Index_element*,
        ut_allocator<const dd::Index_element*>> pk_elements;

  for (dd::Index* index : *dd_table->indexes()) {
    if (index == primary) {
      continue;
    }
    pk_elements.clear();
    for (const dd::Index_element* e : primary->elements()) {
      if (e->is_prefix() ||
         std::search_n(index->elements().begin(),
               index->elements().end(), 1, e,
               [](const dd::Index_element* ie,
            const dd::Index_element* e) {
                 return(&ie->column()
                  == &e->column());
               }) == index->elements().end()) {
        pk_elements.push_back(e);
      }
    }

    for (const dd::Index_element* e : pk_elements) {
      auto ie = index->add_element(
        const_cast<dd::Column*>(&e->column()));
      ie->set_hidden(true);
      ie->set_order(e->order());
    }
  }

  /* 增加系统列 DB_TRX_ID, DB_ROLL_PTR. */
  dd::Column* db_trx_id = dd_add_hidden_column(
    dd_table, "DB_TRX_ID", DATA_TRX_ID_LEN,
    dd::enum_column_types::INT24);
  if (db_trx_id == nullptr) {
    DBUG_RETURN(ER_WRONG_COLUMN_NAME);
  }

  dd::Column* db_roll_ptr = dd_add_hidden_column(
    dd_table, "DB_ROLL_PTR", DATA_ROLL_PTR_LEN,
    dd::enum_column_types::LONGLONG);
  if (db_roll_ptr == nullptr) {
    DBUG_RETURN(ER_WRONG_COLUMN_NAME);
  }

  dd_add_hidden_element(primary, db_trx_id);
  dd_add_hidden_element(primary, db_roll_ptr);

  /* Add all non-virtual columns to the clustered index,
  unless they already part of the PRIMARY KEY. */

  for (const dd::Column* c : const_cast<const dd::Table*>(dd_table)->columns()) {
    if (c->is_hidden() || c->is_virtual()) {
      continue;
    }

    if (std::search_n(primary->elements().begin(),
          primary->elements().end(), 1,
          c, [](const dd::Index_element* e,
          const dd::Column* c)
          {
            return(!e->is_prefix()
             && &e->column() == c);
          })
        == primary->elements().end()) {
      dd_add_hidden_element(primary, c);
    }
  }

  DBUG_RETURN(0);
}


template <typename T>
Dictionary_client::store(T* object)
{
    ...

	/* 调用下面函数完成存储 */
	if (Storage_adapter::store(m_thd, object))
       return true;
    ...
}


/* 该函数负责将DD对象写入对应的系统表中。 */
template <typename T>
bool Storage_adapter::store(THD *thd, T *object)
{
  // 如果是测试或者未到真正需要建表的阶段,只存入缓存,不进行持久化存储。
  if (s_use_fake_storage ||
      bootstrap::DD_bootstrap_ctx::instance().get_stage() <
          bootstrap::Stage::CREATED_TABLES)
  {
    instance()->core_store(thd, object);
    return false;
  }

  // 这里会验证DD对象的有效性
  if (object->impl()->validate())
  {
    DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    return true;
  }

  // 切换上下文,包括更新系统表的时候关闭binlog、修改auto_increament_increament增量、设置一些相关变量等与修改DD相关的上下文。
  Update_dictionary_tables_ctx ctx(thd);
  ctx.otx.register_tables<T>();
  DEBUG_SYNC(thd, "before_storing_dd_object");

  // object->impl()->store 这里会将DD对象存入相关的系统表。具体比如表,列, 表空间是如何持久化到系统表中的,由于篇幅有限,我们将在以后的月报中继续剖析。
  if (ctx.otx.open_tables() || object->impl()->store(&ctx.otx))
  {
    DBUG_ASSERT(thd->is_system_thread() || thd->killed || thd->is_error());
    return true;
  }
  // Do not create SDIs for tablespaces and tables while creating
 // dictionary entry during upgrade.
  if (bootstrap::DD_bootstrap_ctx::instance().get_stage() >
          bootstrap::Stage::CREATED_TABLES &&
      dd::upgrade_57::allow_sdi_creation() &&
      sdi::store(thd, object))
    return true;

  return false;
}

综上篇章简要的描述了MySQL8.0实现原子DDL的背景以及一些重点的数据结构,并对CREATE TABLE过程,以及创建过程中用到的几个重要函数进行了分析。但是原子DDL的实现是一个非常大的工程,本篇月报由于篇幅问题,只是挖了冰山一角。以后的月报会继续对原子DDL的实现进行分析,希望大家持续关注。

MongoDB · Feature · In-place update in MongoDB

$
0
0

There is a great new feature in the release note of MongoDB 3.5.12.

Faster In-place Updates in WiredTiger

This work brings improvements to in-place update workloads for users running the WiredTiger engine, especially for updates to large documents. Some workloads may see a reduction of up to 7x in disk utilization (from 24 MB/s to 3 MB/s) as well as a 20% improvement in throughput.

I thought wiredtiger has impeletementd the delta page feature introduced in the bw-tree paper, that is, writing pages that are deltas from previously written pages. But after I read the source code, I found it’s a totally diffirent idea, in-place update only impacted the in-meomry and journal format, the on disk layout of data is not changed.

I will explain the core of the in-place update implementation.

MongoDB introduced mutable bson to descirbe document update as incremental(delta) update.

Mutable BSON provides classes to facilitate the manipulation of existing BSON objects or the construction of new BSON objects from scratch in an incremental fashion.

Suppose you have a very large document, see 1MB

{
   _id: ObjectId("59097118be4a61d87415cd15"),
   name: "ZhangYoudong",
   birthday: "xxxx",
   fightvalue: 100,
   xxx: .... // many other fields
}

If the fightvalue is changed from 100 to 101, you can use a DamageEvent to describe the update, it just tells you the offset、size、content(kept in another array) of the change.

struct DamageEvent {
    typedef uint32_t OffsetSizeType;
    // Offset of source data (in some buffer held elsewhere).
    OffsetSizeType sourceOffset;

    // Offset of target data (in some buffer held elsewhere).
    OffsetSizeType targetOffset;

    // Size of the damage region.
    size_t size;
};

So if you have many small changes for a document, you will have DamageEvent array, MongoDB add a new storage interface to support inserting DamageEvent array (DamageVector).

bool WiredTigerRecordStore::updateWithDamagesSupported() const {
    return true;
}

StatusWith<RecordData> WiredTigerRecordStore::updateWithDamages(
    OperationContext* opCtx,
    const RecordId& id,
    const RecordData& oldRec,
    const char* damageSource,
    const mutablebson::DamageVector& damages) {

}

WiredTiger added a new update type called WT_UPDATE_MODIFIED to support MongoDB, when a WT_UPDATE_MODIFIED update happened, wiredTiger first logged a change list which is transformed from DamageVector into journal, then kept the change list in memory associated with the original record.

When the record is read, wiredTiger will first read the original record, then apply every operation in change list, returned the final record to the client.

So the core for in-place update:

  1. WiredTiger support delta update in memory and journal, so the IO of writing journal will be greatly reduced for large document.
  2. WiredTiger’s data layout is kept unchanged, so the IO of writing data is not changed.

MSSQL · 最佳实践 · 利用文件组实现冷热数据隔离备份方案

$
0
0

摘要

在SQL Server备份专题分享中,前四期我们分享了:三种常见的数据库备份、备份策略的制定、如何查找备份链以及数据库的三种恢复模式与备份之间的关系。本次月报我们分享SQL Server如何利用文件组技术来实现数据库冷热数据隔离备份的方案。

场景引入

假设某公司有一个非常重要的超大的数据库(超过10TB),面临如下场景:

该数据库中存储了近10年的用户支付信息(payment),非常重要

每年的数据归档存储在年表中,历史年表中的数据只读不写(历史payment信息无需再修改),只有当前年表数据既读又写

每次数据库全备耗时太长,超过20小时;数据库还原操作耗时更长,超过30小时

如何优化设计这个数据库以及备份恢复系统,可以使得备份、还原更加高效?

文件组简介

文件组的详细介绍不是本次分享的重点,但是作为本文介绍的核心技术,有必要对其优点、创建以及使用方法来简单介绍SQL Server中的文件组。

使用文件组的优点

SQL Server支持将表、索引数据存放到非Primary文件组,这样当数据库拥有多个文件组时就具备了如下好处:

分散I/O压力到不同的文件组上,如果不同文件组的文件位于不同的磁盘的话,可以分散磁盘压力。

针对不同的文件组进行DBCC CHECKFILEGROUP操作,并且同一个数据库可以多个进程并行处理,减少大数据维护时间。

可以针对文件组级别进行备份和还原操作,更细粒度控制备份和还原策略。

创建数据库时创建文件组

我们可以在创建数据库时直接创建文件组,代码如下:

USE master
GO

EXEC sys.xp_create_subdir 'C:\SQLServer\Data\'
EXEC sys.xp_create_subdir 'C:\SQLServer\Logs\'

CREATE DATABASE [TestFG]
 ON  PRIMARY 
( NAME = N'TestFG', FILENAME = N'C:\SQLServer\Data\TestFG.mdf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FG2010] 
( NAME = N'FG2010', FILENAME = N'C:\SQLServer\Data\FG2010.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FG2011] 
( NAME = N'FG2011', FILENAME = N'C:\SQLServer\Data\FG2011.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FG2012] 
( NAME = N'FG2012', FILENAME = N'C:\SQLServer\Data\FG2012.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB )
 LOG ON 
( NAME = N'TestFG_log', FILENAME = N'C:\SQLServer\Logs\TestFG_log.ldf' , SIZE = 5MB , FILEGROWTH = 50MB)
GO

注意: 为了保证数据库文件组I/O的负载均衡能力,请将所有文件的初始大小和自动增长参数保持一致,以保证轮询调度分配算法正常工作。

单独创建创建组

如果数据库已经存在,我们也同样有能力添加文件组,代码如下:

--Add filegroup FG2013
USE master
GO
ALTER DATABASE [TestFG] ADD FILEGROUP [FG2013];

-- Add data file to FG2013
ALTER DATABASE [TestFG]
ADD FILE (NAME = FG2013, SIZE = 5MB , FILEGROWTH = 50MB ,FILENAME = N'C:\SQLServer\Data\FG2013.ndf')
TO FILEGROUP [FG2013]
GO

USE [TestFG]
GO
SELECT * FROM sys.filegroups

最终文件组信息,展示如下: 01.png

使用文件组

文件组创建完毕后,我们可以将表和索引放到对应的文件组。比如: 将聚集索引放到PRIMARY文件组;表和索引数据放到FG2010文件组,代码如下:

USE [TestFG]
GO
CREATE TABLE [dbo].[Orders_2010](
	[OrderID] [int] IDENTITY(1,1) NOT NULL,
	[OrderDate] [datetime] NULL,
	CONSTRAINT [PK_Orders_2010] PRIMARY KEY CLUSTERED 
	(
		[OrderID] ASC
	) ON [PRIMARY]
) ON [FG2010]
GO


CREATE NONCLUSTERED INDEX IX_OrderDate
ON [dbo].[Orders_2010] (OrderDate)
ON [FG2010];

方案设计

文件组的基本知识点介绍完毕后,根据场景引入中的内容,我们将利用SQL Server文件组技术来实现冷热数据隔离备份的方案设计介绍如下。

设计分析

由于payment数据库过大,超过10TB,单次全量备份超过20小时,如果按照常规的完全备份,会导致备份文件过大、耗时过长、甚至会因为备份操作对I/O能力的消耗影响到正常业务。我们仔细想想会发现,虽然数据库本身很大,但是,由于只有当前年表数据会不断变化(热数据),历史年表数据不会修改(冷数据),因此正真有数据变化操作的数据量相对整个库来看并不大。那么,我们将数据库设计为历史年表数据放到Read only的文件组上,把当前年表数据放到Read write的文件组上,备份系统仅仅需要备份Primary和当前年表所在的文件组即可(当然首次还是需要对数据库做一次性完整备份的)。这样既可以大大节约备份对I/O能力的消耗,又实现了冷热数据的隔离备份操作,还达到了分散了文件的I/O压力,最终达到数据库设计和备份系统优化的目的,可谓一箭多雕。

以上文字分析,画一个漂亮的设计图出来,直观展示如下: 02.png

设计图说明

以下对设计图做详细说明,以便对设计方案有更加直观和深入理解。 整个数据库包含13个文件,包括:

1个主文件组(Primary File Group):用户存放数据库系统表、视图等对象信息,文件组可读可写。

10个用户自定义只读文件组(User-defined Read Only File Group):用于存放历史年表的数据及相应索引数据,每一年的数据存放到一个文件组中。

1个用户自定义可读写文件组(User-defined Read Write File Group):用于存放当前年表数据和相应索引数据,该表数据必须可读可写,所以文件组必须可读可写。

1个数据库事务日志文件:用于数据库事务日志,我们需要定期备份数据库事务日志。

方案实现

设计方案完成以后,接下来就是方案的集体实现了,具体实现包括:

创建数据库

创建年表

文件组设置

冷热备份实现

创建数据库

创建数据库的同时,我们创建了Primary文件组和2008 ~ 2017的文件组,这里需要特别提醒,请务必保证所有文件组中文件的初始大小和增长量相同,代码如下:

USE master
GO

EXEC sys.xp_create_subdir 'C:\DATA\Payment\Data\'
EXEC sys.xp_create_subdir 'C:\DATA\Payment\Log\'

CREATE DATABASE [Payment]
 ON  PRIMARY 
( NAME = N'Payment', FILENAME = N'C:\DATA\Payment\Data\Payment.mdf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2008] 
( NAME = N'FGPayment2008', FILENAME = N'C:\DATA\Payment\Data\Payment_2008.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2009] 
( NAME = N'FGPayment2009', FILENAME = N'C:\DATA\Payment\Data\Payment_2009.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2010] 
( NAME = N'FGPayment2010', FILENAME = N'C:\DATA\Payment\Data\Payment_2010.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2011] 
( NAME = N'FGPayment2011', FILENAME = N'C:\DATA\Payment\Data\Payment_2011.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2012] 
( NAME = N'FGPayment2012', FILENAME = N'C:\DATA\Payment\Data\Payment_2012.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2013] 
( NAME = N'FGPayment2013', FILENAME = N'C:\DATA\Payment\Data\Payment_2013.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2014]
( NAME = N'FGPayment2014', FILENAME = N'C:\DATA\Payment\Data\Payment_2014.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2015] 
( NAME = N'FGPayment2015', FILENAME = N'C:\DATA\Payment\Data\Payment_2015.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2016] 
( NAME = N'FGPayment2016', FILENAME = N'C:\DATA\Payment\Data\Payment_2016.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB ), 
 FILEGROUP [FGPayment2017] 
( NAME = N'FGPayment2017', FILENAME = N'C:\DATA\Payment\Data\Payment_2017.ndf' , SIZE = 5MB ,FILEGROWTH = 50MB )
 LOG ON 
( NAME = N'Payment_log', FILENAME = N'C:\DATA\Payment\Log\Payment_log.ldf' , SIZE = 5MB , FILEGROWTH = 50MB)
GO

考虑到每年我们都要添加新的文件组到数据库中,因此2018年的文件组单独创建如下:

--Add filegroup FGPayment2018
USE master
GO
ALTER DATABASE [Payment] ADD FILEGROUP [FGPayment2018];

-- Add data file to FGPayment2018
ALTER DATABASE [Payment]
ADD FILE (NAME = FGPayment2018, SIZE = 5MB , FILEGROWTH = 50MB ,FILENAME = N'C:\DATA\Payment\Data\Payment_2018.ndf')
TO FILEGROUP [FGPayment2018]
GO

最终再次确认数据库文件组信息,代码如下:

USE [Payment]
GO
SELECT file_name = mf.name, filegroup_name = fg.name, mf.physical_name,mf.size,mf.growth 
FROM sys.master_files AS mf
	INNER JOIN sys.filegroups as fg
	ON mf.data_space_id = fg.data_space_id
WHERE mf.database_id = db_id('Payment')
ORDER BY mf.type;

结果展示如下图所示: 03.png

创建年表

数据库以及相应文件组创建完毕后,接下来我们创建对应的年表并插入一些测试数据,如下:

USE [Payment]
GO
CREATE TABLE [dbo].[Payment_2008](
	[Payment_ID] [bigint] IDENTITY(12008,100) NOT NULL,
	[OrderID] [bigint] NOT NULL,
	CONSTRAINT [PK_Payment_2008] PRIMARY KEY CLUSTERED 
	(
		[Payment_ID] ASC
	) ON [FGPayment2008]
) ON [FGPayment2008]
GO

CREATE NONCLUSTERED INDEX IX_OrderID
ON [dbo].[Payment_2008] ([OrderID])
ON [FGPayment2008];

CREATE TABLE [dbo].[Payment_2009](
	[Payment_ID] [bigint] IDENTITY(12009,100) NOT NULL,
	[OrderID] [bigint] NOT NULL,
	CONSTRAINT [PK_Payment_2009] PRIMARY KEY CLUSTERED 
	(
		[Payment_ID] ASC
	) ON [FGPayment2009]
) ON [FGPayment2009]
GO

CREATE NONCLUSTERED INDEX IX_OrderID
ON [dbo].[Payment_2009] ([OrderID])
ON [FGPayment2009];

--这里省略了2010-2017的表创建,请参照以上建表和索引代码,自行补充
CREATE TABLE [dbo].[Payment_2018](
	[Payment_ID] [bigint] IDENTITY(12018,100) NOT NULL,
	[OrderID] [bigint] NOT NULL,
	CONSTRAINT [PK_Payment_2018] PRIMARY KEY CLUSTERED 
	(
		[Payment_ID] ASC
	) ON [FGPayment2018]
) ON [FGPayment2018]
GO

CREATE NONCLUSTERED INDEX IX_OrderID
ON [dbo].[Payment_2018] ([OrderID])
ON [FGPayment2018];

这里需要特别提醒两点:

限于篇幅,建表代码中省略了2010 - 2017表创建,请自行补充

每个年表的Payment_ID字段初始值是不一样的,以免查询所有payment信息该字段值存在重复的情况

其次,我们检查所有年表的文件组分布情况如下:

USE [Payment]
GO
SELECT table_name = tb.[name], index_name = ix.[name], located_filegroup_name = fg.[name] 
FROM sys.indexes ix
	INNER JOIN sys.filegroups fg
	ON ix.data_space_id = fg.data_space_id
	INNER JOIN sys.tables tb
	ON ix.[object_id] = tb.[object_id] 
WHERE ix.data_space_id = fg.data_space_id
GO

查询结果截取其中部分如下,我们看到所有年表及索引都按照我们的预期分布到对应的文件组上去了。 04.png

最后,为了测试,我们在对应年表中放入一些数据:

USE [Payment]
GO
SET NOCOUNT ON
INSERT INTO [Payment_2008] SELECT 2008;
INSERT INTO [Payment_2009] SELECT 2009;
--省略掉2010 - 2017,自行补充
INSERT INTO [Payment_2018] SELECT 2018;

文件组设置

年表创建完完毕、测试数据初始化完成后,接下来,我们做文件组读写属性的设置,代码如下:

USE master
GO
ALTER DATABASE [Payment] MODIFY FILEGROUP [FGPayment2008] READ_ONLY;
ALTER DATABASE [Payment] MODIFY FILEGROUP [FGPayment2009] READ_ONLY;
--这里省略了2010 - 2017文件组read only属性的设置,请自行补充
ALTER DATABASE [Payment] MODIFY FILEGROUP [FGPayment2018] READ_WRITE;

最终我们的文件组读写属性如下:

USE [Payment]
GO
SELECT name, is_default, is_read_only FROM sys.filegroups
GO

截图如下:

05.png

冷热备份实现

所有文件组创建成功,并且读写属性配置完毕后,我们需要对数据库可读写文件组进行全量备份、差异备份和数据库级别的日志备份,为了方便测试,我们会在两次备份之间插入一条数据。备份操作的大体思路是:

首先,对整个数据库进行一次性全量备份

其次,对可读写文件组进行周期性全量备份

接下来,对可读写文件组进行周期性差异备份

最后,对整个数据库进行周期性事务日志备份

--Take a one time full backup of payment database
USE [master];
GO
BACKUP DATABASE [Payment]
	TO DISK = N'C:\DATA\Payment\BACKUP\Payment_20180316_full.bak' 
	WITH COMPRESSION, Stats=5
;
GO

-- for testing, init one record
USE [Payment];
GO
INSERT INTO [dbo].[Payment_2018] SELECT 201801;
GO

--Take a full backup for each writable filegoup (just backup FGPayment2018 as an example)
BACKUP DATABASE [Payment]
	FILEGROUP = 'FGPayment2018'
	TO DISK = 'C:\DATA\Payment\BACKUP\Payment_FGPayment2018_20180316_full.bak' 
	WITH COMPRESSION, Stats=5
;
GO

-- for testing, insert one record
INSERT INTO [dbo].[Payment_2018] SELECT 201802;
GO

--Take a differential backup for each writable filegoup (just backup FGPayment2018 as an example)
BACKUP DATABASE [Payment]
   FILEGROUP = N'FGPayment2018'
   TO DISK = N'C:\DATA\Payment\BACKUP\Payment_FGPayment2018_20180316_diff.bak'
   WITH DIFFERENTIAL, COMPRESSION, Stats=5
 ;
GO

-- for testing, insert one record
INSERT INTO [dbo].[Payment_2018] SELECT 201803;
GO

-- Take a transaction log backup of database payment
BACKUP LOG [Payment]
TO DISK = 'C:\DATA\Payment\BACKUP\Payment_20180316_log.trn';
GO

这样备份的好处是,我们只需要对可读写的文件组(FGPayment2018)进行完整和差异备份(Primary中包含系统对象,变化很小,实际场景中,Primary文件组也需要备份),而其他的9个只读文件组无需备份,因为数据不会再变化。如此,我们就实现了冷热数据隔离备份的方案。 接下来的一个问题是,万一Payment数据发生灾难,导致数据损失,我们如何从备份集中将数据库恢复出来呢?我们可以按照如下思路来恢复备份集:

首先,还原整个数据库的一次性全量备份

其次,还原所有可读写文件组最后一个全量备份

接下来,还原可读写文件组最后一个差异备份

最后,还原整个数据库的所有事务日志备份

-- We restore full backup
USE master
GO
RESTORE DATABASE [Payment_Dev]
FROM DISK=N'C:\DATA\Payment\BACKUP\Payment_20180316_full.bak' WITH
MOVE 'Payment' TO 'C:\DATA\Payment_Dev\Data\Payment_dev.mdf',
MOVE 'FGPayment2008' TO 'C:\DATA\Payment_Dev\Data\FGPayment2008_dev.ndf',
MOVE 'FGPayment2009' TO 'C:\DATA\Payment_Dev\Data\FGPayment2009_dev.ndf',
MOVE 'FGPayment2010' TO 'C:\DATA\Payment_Dev\Data\FGPayment2010_dev.ndf',
MOVE 'FGPayment2011' TO 'C:\DATA\Payment_Dev\Data\FGPayment2011_dev.ndf',
MOVE 'FGPayment2012' TO 'C:\DATA\Payment_Dev\Data\FGPayment2012_dev.ndf',
MOVE 'FGPayment2013' TO 'C:\DATA\Payment_Dev\Data\FGPayment2013_dev.ndf',
MOVE 'FGPayment2014' TO 'C:\DATA\Payment_Dev\Data\FGPayment2014_dev.ndf',
MOVE 'FGPayment2015' TO 'C:\DATA\Payment_Dev\Data\FGPayment2015_dev.ndf',
MOVE 'FGPayment2016' TO 'C:\DATA\Payment_Dev\Data\FGPayment2016_dev.ndf',
MOVE 'FGPayment2017' TO 'C:\DATA\Payment_Dev\Data\FGPayment2017_dev.ndf',
MOVE 'FGPayment2018' TO 'C:\DATA\Payment_Dev\Data\FGPayment2018_dev.ndf',
MOVE 'Payment_log' TO 'C:\DATA\Payment_Dev\Log\Payment_dev_log.ldf',
NORECOVERY,STATS=5;
GO

-- restore writable filegroup full backup
RESTORE DATABASE [Payment_Dev]
   FILEGROUP = N'FGPayment2018'
   FROM DISK = N'C:\DATA\Payment\BACKUP\Payment_FGPayment2018_20180316_full.bak'
   WITH NORECOVERY,STATS=5;
GO

-- restore writable filegroup differential backup
RESTORE DATABASE [Payment_Dev]
   FILEGROUP = N'FGPayment2018'
   FROM DISK = N'C:\DATA\Payment\BACKUP\Payment_FGPayment2018_20180316_diff.bak'
   WITH NORECOVERY,STATS=5;
GO

-- restore payment database transaction log backup
RESTORE LOG [Payment_Dev]
FROM DISK = N'C:\DATA\Payment\BACKUP\\Payment_20180316_log.trn'
WITH NORECOVERY;
GO

-- Take database oneline to check
RESTORE DATABASE [Payment_Dev] WITH RECOVERY;
GO

最后检查数据还原的结果,按照我们插入的测试数据,应该会有四条记录。

USE [Payment_Dev]
GO
SELECT * FROM [dbo].[Payment_2018] WITH(NOLOCK)

展示执行结果,有四条结果集,符合我们的预期,截图如下:

06.png

最后总结

本篇月报分享了如何利用SQL Server文件组技术来实现和优化冷热数据隔离备份的方案,在大大提升数据库备份还原效率的同时,还提供了I/O资源的负载均衡,提升和优化了整个数据库的性能。

PgSQL · 内核优化 · Hybrid DB for PG 赋能向量化执行和查询子树封装

$
0
0

背景

Hybrid DB for postgresql简介:

随着大数据时代的不断演进, 用户对于数据的分析能力的需要提出了越来越高的要求。 Hybrid DB for postgres(本文后续将会使用HDBP来代表)是一款基于Greenplum开源项目的分析型数据库。 为阿里云用户提供数据分析计算服务,HDBP数据库继承开源Greenplum的处理复杂SQL查询的支持能力,同时HDBP又是一款由阿里云技术团队支持的分析类型数据库产品,阿里云技术团队会不断的在HDBP的各个方面进行持续维护与增强。

本文将主要介绍HDBP在查询性能提升方面对数据库内核的赋能,使得HDBP查询执行引擎能够比开源Greenplum执行效率高出2-10+倍。 并且HDBP还在持续提升执行引擎的效率。 下图为我们本次性能优化的测试结果:蓝色柱代表优化前的查询执行时间这里用1作为一个比较基线, 红色柱代表优化后查询性能提升的倍数。后续测试部分会详细测试说明。

testrescolumn.jpg

本项目优化理论依据:

首先我们将会介绍一下本次性能优化项目的理论依据,HDBP性能优化项目主要是基于以下两个主要的研究结论展开:

向量化执行能力

通过研究我们发现Greenplum虽然提供了列存储的能力,但是实现没有向量化执行引擎提供向量化处理能力,在处理列存表的时候,是分别读取一条Tuple的各个字段内容,拼装完成后返回,后续的执行过程还是行存执行引擎完成。向量化执行能够带来的性能上面的提升就无法获得了。 在本次HDBP数据库内核优化项目中我们引入了一种灵活的向量化处理模型,使得HDBP执行引擎可以在一条查询中灵活的在行存处理模式和向量化处理模式两种执行方式中切换。 后续我们将会给大家介绍这块的设计与实现。

查询子树的封装执行

通过分析发现查询计划树中某些部分,是需要执行完成以后然后才能够继续后续的执行,在分析型数据库当中,往往这部分查询子树往往也是整个查询中耗时比较多的部分,由于这部分查询子树的执行相对独立,给我们提供了封装这部分子树的可能性,进过优化处理,最终获得这部分子树最大的执行效率。

我们的设计目标:

  1. 用户无感知(即用户查询无需做任何修改);
  2. 性能提升收益最大化;
  3. 架构灵活;
  4. 能够快速上线。 基于上述目标我们在设计的性能优化框架的时候就首先致力于一个灵活且能够满足互联网快速迭代的执行引擎框架的设计与实现。

pic1.png

如图1,给我们展示的是一查询和它对应的查询计划树, 其中方框所包含的部分就是查询子树部分。 我们首先将这部分时间减少到最低,理论上就能够提升整个查询执行效率的效果。

pic2.jpg

通过图2,所示,我们将查询子树封装到我们性能优化处理框架当中,并且在性能优化处理框架中我们实现了向量化处理能力。这样我们就能够达到对用户无感知的性能加速的效果。并且这样的实现使得我们后续的查询执行优化及功能快速上线这两个方面都有了非常大的灵活度。 通过前面的描述,HDBP性能优化项目会主要会构建一个新的向量化执行引擎框架,在这个框架中我们会将查询子树封装进去,在框架中对这个查询子树的执行过程进行优化处理,最终达到查询子树执行性能的提升。

当前项目状况:

  1. 当前阿里云已经架构起来了优化执行引擎的框架代码基础框架建设。 为后续直接在这个框架上进行快速迭代开发,实现更多的优化能力提供了架构基础。
  2. 向量化执行能力的提供,使得向量化处理的收益可以不断的收获。
  3. 聚合函数优化执行的代码,这部分功能已经上线,并且已经在客户那边进行了实际场景的测试, 经过验证能够达到实验室测试同样的提升效果。

测试效果:

我们在用tpch10G数据结果集,在一台机器上部署了一个3节点测试环境测试进行如下查询的测试:

testquery.jpg

testres.jpg

从测试结果来看,我们的优化项目框架还是起到了很好的性能加速效果。通过性能测试我们还发现了几个性能优化点:

  1. 列存扫描效率,向量化数据准备过程,这部分还有很大的提升空间;
  2. 表达式计算效率提升,查询5由于过滤算子比较多, 表达式目前向量化计算效率还有优化空间;
  3. hash Agg计算Hash值效率,计算Hash值部分可以继续优化;

结语:

上述性能优化框架是突破了开源Greenplum局限,虽然目前它只实现了聚集函数的加速,但是对它的成功探索过程中,也让我们看到了执行引擎优化的更多可以尝试的方面,更加广阔的优化空间。后续我们会不断突破局限,让HDBP给用户带来更多的计算价值。


MySQL · 特性分析 · innodb_buffer_pool_size在线修改

$
0
0

InnoDB Buffer Pool缓存了表数据和二级索引在内存中,提高数据库效率,因此设置innodb_buffer_pool_size到合理数值对实例性能影响很大。当size设置偏小,会导致数据库大量直接磁盘的访问,而设置过大会导致实例占用内存太多,容易发生OOM。在MySQL 5.7之前innodb_buffer_pool_size的修改需要重启实例,在5.7后支持了动态修改innodb_buffer_pool_size。本文会根据源码介绍该特性。

innodb_buffer_pool_size 设置范围

innodb_buffer_pool_size默认值是128M,最小5M(当小于该值时会设置成5M),最大为LLONG_MAX。当innodb_buffer_pool_instances设置大于1的时候,buffer pool size最小为1GB。同时buffer pool size需要是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍数。innodb_buffer_pool_chunk_size默认为128M,最小为1M,实例启动后为只读参数。

static MYSQL_SYSVAR_LONGLONG(buffer_pool_size, innobase_buffer_pool_size,
  PLUGIN_VAR_RQCMDARG,
  "The size of the memory buffer InnoDB uses to cache data and indexes of its tables.",
  innodb_buffer_pool_size_validate,
  innodb_buffer_pool_size_update,
  static_cast<longlong>(srv_buf_pool_def_size),//128M
  static_cast<longlong>(srv_buf_pool_min_size),//5M
  LLONG_MAX, 1024*1024L);

#define BUF_POOL_SIZE_THRESHOLD   (1024 * 1024 * 1024) //1GB

static
int
innodb_buffer_pool_size_validate(
{
  ...
  //当srv_buf_pool_instances > 1,要求size不小于1GB。
  if (srv_buf_pool_instances > 1 && intbuf < BUF_POOL_SIZE_THRESHOLD) {
    buf_pool_mutex_exit_all();

    push_warning_printf(thd, Sql_condition::SL_WARNING,
          ER_WRONG_ARGUMENTS,
          "Cannot update innodb_buffer_pool_size"" to less than 1GB if"" innodb_buffer_pool_instances > 1.");
    return(1);
  }
  ...
  ulint requested_buf_pool_size
  = buf_pool_size_align(static_cast<ulint>(intbuf));
}



/** Calculate aligned buffer pool size based on srv_buf_pool_chunk_unit,
if needed.
@param[in]  size  size in bytes
@return aligned size */
UNIV_INLINE
ulint
buf_pool_size_align(
  ulint size)
{
  const ulint m = srv_buf_pool_instances * srv_buf_pool_chunk_unit;
  size = ut_max(size, srv_buf_pool_min_size);

  if (size % m == 0) {
    return(size);
  } else {
    return((size / m + 1) * m);
  }
}

buffer pool resize流程

  • 如果开启了AHI(adaptive hash index,自适应哈希索引)就关闭AHI,这里因为AHI是通过buffer pool中的B+树页构造而来。

  • 如果新设定的buffer pool size小于原来的size,就需要计算需要删除的chunk数目withdraw_target。

  • 遍历buffer pool instances,锁住buffer pool,收集free list中的chunk page到withdraw,直到withdraw_target或者遍历完,然后释放buffer pool锁。

  • 停止加载buffer pool。

  • 如果free list中没有收集到足够的chunk,则重复遍历收集,每次重复间隔时间会指数增加1s、2s、4s、8s…,以等待事务释放资源。

  • 锁住buffer pool,开始增减chunk。

  • 如果改变比较大,超过2倍,会重置page hash,改变桶大小。

  • 释放buffer_pool,page_hash锁。

  • 改变比较大时候,重新设置buffer pool大小相关的内存结构。

  • 开启AHI。

/** Resize the buffer pool based on srv_buf_pool_size from
srv_buf_pool_old_size. */
void
buf_pool_resize()
{
  /* disable AHI if needed */
  btr_search_disable(true);

  /* set withdraw target */
  for (ulint i = 0; i < srv_buf_pool_instances; i++) {
    if (buf_pool->curr_size < buf_pool->old_size) {
      ...
      while (chunk < echunk) {
        withdraw_target += chunk->size;
        ++chunk;
      }
      ...
    }
  }

  /* wait for the number of blocks fit to the new size (if needed)*/
  for (ulint i = 0; i < srv_buf_pool_instances; i++) {
    buf_pool = buf_pool_from_array(i);
      if (buf_pool->curr_size < buf_pool->old_size) {

      should_retry_withdraw |=
       buf_pool_withdraw_blocks(buf_pool);
    }
  }
  ...
  if (should_retry_withdraw) {
  ib::info() << "Will retry to withdraw "<< retry_interval
    << " seconds later.";
  os_thread_sleep(retry_interval * 1000000);

  if (retry_interval > 5) {
    retry_interval = 10;
  } else {
    retry_interval *= 2;
    }

    goto withdraw_retry;
  }
  ...
  /* add/delete chunks */
  for (ulint i = 0; i < srv_buf_pool_instances; ++i) {
    if (buf_pool->n_chunks_new < buf_pool->n_chunks) {
      /* delete chunks */
      /* discard withdraw list */
    }
  }
  /* reallocate buf_pool->chunks */
  if (buf_pool->n_chunks_new > buf_pool->n_chunks) {
    /* add chunks */
  }
  ...
  const bool  new_size_too_diff
  = srv_buf_pool_base_size > srv_buf_pool_size * 2
    || srv_buf_pool_base_size * 2 < srv_buf_pool_size;

  /* Normalize page_hash and zip_hash,
  if the new size is too different */
}

resize过程中的等待和阻塞

在支持动态修改innodb_buffer_pool_size之前,该值的修改需要修改配置项然后重启实例生效。而重启实例会导致用户连接强制断开,导致一段时间的实例不可用,如果有大事务在回滚就需要等待很长时间。

动态修改innodb_buffer_pool_size只有在收集回收块;查找持有block阻止buffer pool收集回收chunk的事务;resizing buffer pool操作时会阻塞用户写入。而这几部分操作都是内存操作,会较快完成。

如果对innodb_buffer_pool_size修改量很大,同时遇到page cleaner工作时间久,就可能导致一段时间的阻塞。例如下面一个较为极端的例子,innodb_buffer_pool_instances为1,innodb_buffer_pool_size由18GB改为5M,innodb_buffer_pool_chunk_size为1M,page cleaner loop花费近48s,导致收集回收块会花费很长时间,可以看到在测试机器上用时近48s。而这期间的写入操作也会被阻塞。

02:54:09.798912Z 0 [Note] InnoDB: Withdrawing blocks to be shrunken.
02:54:09.798935Z 0 [Note] InnoDB: buffer pool 0 : start to withdraw the last 1151680 blocks.
02:54:57.660725Z 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 47685ms. The settings might not be optimal. (flushed=0 and evicted=0, during the time.)
02:54:57.687189Z 0 [Note] InnoDB: buffer pool 0 : withdrawing blocks. (1151680/1151680)
02:54:57.687237Z 0 [Note] InnoDB: buffer pool 0 : withdrew 1151653 blocks from free list. Tried to relocate 27 pages (1151680/1151680).
02:54:57.753014Z 0 [Note] InnoDB: buffer pool 0 : withdrawn target 1151680 blocks.


> insert into t values(10000001, 2);
Query OK, 1 row affected (9.03 sec)

正常不需要等待时的内存操作会很快。

03:31:57.734231Z 0 [Note] InnoDB: Resizing buffer pool from 1887436800 to 5242880 (unit=1048576).
03:31:58.480061Z 0 [Note] InnoDB: Completed to resize buffer pool from 1887436800 to 5242880.
...
03:31:46.453250Z 10 [Note] InnoDB: Resizing buffer pool from 524288 (new size: 1887436800 bytes)
03:31:57.734231Z 0 [Note] InnoDB: Resizing buffer pool from 1887436800 to 5242880 (unit=1048576).

另一个方面,如果当前有事务占用大量buffer pool数据导致无法收集到足够的chunk,resize过程也会变久。下面极端测试中当执行xa rollback回滚大事务的时候,innodb_buffer_pool_chunk_size由16M改为5M,即等待了较久时间才完成回收chunk的收集。不过这段时间并不会完全阻塞用户的操作。

> xa begin 'y';
Query OK, 0 rows affected (0.00 sec)

> update t set c2 = 2 where c2 =1;
Query OK, 999996 rows affected (8.32 sec)
Rows matched: 999996  Changed: 999996  Warnings: 0

> xa end 'y';
Query OK, 0 rows affected (0.00 sec)

> xa prepare 'y';
Query OK, 0 rows affected (0.11 sec)

> xa rollback 'y';
Query OK, 0 rows affected (10.10 sec)

InnoDB: Resizing buffer pool from 16777216 to 5242880 (unit=1048576).
InnoDB: Withdrawing blocks to be shrunken.
InnoDB: buffer pool 0 : start to withdraw the last 704 blocks.
InnoDB: buffer pool 0 : withdrew 239 blocks from free list. Tried to relocate 126 pages (689/704).
InnoDB: buffer pool 0 : withdrew 0 blocks from free list. Tried to relocate 0 pages (689/704).
...
InnoDB: Will retry to withdraw 1 seconds later.
InnoDB: buffer pool 0 : start to withdraw the last 704 blocks.
...
InnoDB: buffer pool 0 : will retry to withdraw later.
InnoDB: Will retry to withdraw 2 seconds later.
...
InnoDB: buffer pool 0 : will retry to withdraw later.
InnoDB: Will retry to withdraw 4 seconds later.
InnoDB: buffer pool 0 : start to withdraw the last 704 blocks.
...
InnoDB: Will retry to withdraw 8 seconds later.
InnoDB: buffer pool 0 : withdrawn target 704 blocks.

从上面可以看到innodb_buffer_pool_size的online修改相比重启对用户实例的影响降低了很多,但也最好选择业务低峰期和没有大事务操作时候进行,同时要修改MySQL配置文件,防止重启后恢复到原来的值。

MySQL · myrocks · 事务锁分析

$
0
0

概述

MyRocks中RocksDB作为基于快照的事务引擎,其在事务支持上有别于InnoDB,有其自身的特点。在早期的月报[myrocks之事务处理]中,我们对锁的实现有过简单的分析,本文会以一些例子来介绍MyRocks是如果来加锁解锁的。

锁类型

MyRocks早期只支持排他锁,支持SELEC… IN SHARE MODE后,MyRocks才开始引入共享锁。

 /* Type of locking to apply to rows */
 enum { RDB_LOCK_NONE, RDB_LOCK_READ, RDB_LOCK_WRITE } m_lock_rows;

#587是关于共享锁的一个有趣BUG,有兴趣的同学可以看看。
MyRocks的锁都是内存锁,因此MyRocks事务不宜持有过多的锁,以避免占用过多的内存。
MyRocks通过参数rocksdb_max_row_locks来控制单个事务所持有锁的总数。另外,rocksdb锁系统还支持以下参数
max_num_locks:系统锁个数总限制
expiration_time:锁过期时间

如果锁个数超出限制,客户端会返回下面的错误

failed: 12054: Status error 10 received from RocksDB: Operation aborted: Failed to acquire lock due to max_num_locks limit

隔离级别

MyRocks的事务隔离级只支持的READ-COMMITED和REPEATABLE-READ。隔离级别的支持和snapshot密切相关,隔离级别为READ-COMMITED时,事务中每的个stmt都会建立一个snapshot, 隔离级别为REPEATABLE-REA时,只在事务开启后第一个stmt建立一次snapshot。MyRocks中隔离级别不同不会影响加锁和解锁的行为,因此,后面在分析MyRocks的加锁解锁时不区分隔离级别。

隐式主键

MyRocks支持创建无主键的表,但RocksDB作为KV存储,是需要KEY的。因此,RocksDB内部会给表增加一个名为”HIDDEN_PK_ID”的隐式主键列,此值自增,类似与自增列。此列对于MySQL server层是透明的,读取表数据时会自动跳过”HIDDEN_PK_ID”列。

对于无主键的表,MyRocks的锁都是加在隐式主键上的。

对于binlog复制来说,MyRocks隐式主键并不会提升复制速度,因为隐式主键对server层是透明的,主键列不会记入binlog。 因此,建议MyRocks表都指定主键。

加锁分析

以此表结构来分析各类语句的加锁情况。

create table t1(id int primary key, c1 int unique, c2 int, c3 int, key idx_c2(c2)) engine=rocksdb;
insert into t1 values(1,1,1,1);
insert into t1 values(2,2,2,2);
insert into t1 values(3,3,3,3);
insert into t1 values(4,4,4,4);
  • 示例 select
select * from t1;

MVCC, 普通读不加锁

  • 示例 select .. in share mode
select * from t1 where id=1 in share mode;

对主键id=1记录加S锁

  • 示例 select .. for update
select * from t1 where id=1 for update;

对主键id=1记录加X锁

  • 示例 insert
begin;
insert into t1 values(1,1,1,1);
rollback;

主键id=1加X锁 唯一索引c1=1加X锁

  • 示例 delete by主键
begin;
delete from t1 where id=1;
rollback;

主键id=1加X锁

  • 示例 delete by唯一索引
begin;
delete from t1 where c1=2;
rollback;

主键id=2加X锁,其他索引不加锁

  • 示例 delete by普通索引
begin;
delete from t1 where c2=3;
rollback;

主键id=3加X锁,其他索引不加锁

  • 示例 delete by无索引
begin;
delete from t1 where c3=4;
rollback;

对主键每条加X锁,其他索引不加锁
实际上server层过滤不符合条件的行会释放锁,最终只对主键id=4加X锁

  • 示例 delete by 主键不存在的行
begin;
delete from t1 where id=100;
rollback;

主键id=100加X锁

  • 示例 delete by 其他索引不存在的行
begin;
delete from t1 where c1=100;
rollback;

没有锁可以加

以上例子基本可以覆盖所有加锁的情况,再举例几个例子练习下

  • 示例 select for update
begin;
select * from t1 where  c2=3 for update;
rollback;

主键id=3加X锁, 其他索引不加锁

  • 示例 update更新无索引列
begin;
update t1 set c3=5 where c3=4;
rollback;

对主键每条加X锁,其他索引不加锁
实际上server层过滤不符合条件的行会释放锁,最终只对主键id=4加X锁

  • 示例 update更新索引列
begin;
update t1 set c2=5 where c3=4;
rollback;

对主键每条加X锁,其他索引不加锁
实际上server层过滤不符合条件的行会释放锁,最终只对主键id=4加X锁
同时会对唯一索引c2=5加X锁

对于无主键表的表说,RocksDB内部会有隐式主键,所加锁都在隐式主键上

解锁

事务提交或回滚时都会将事务所持有的锁都释放掉。
另外一种情况是,对于不满足查询条件的记录,MySQL会提前释放锁。

总结

  • MyRocks只会对主键和唯一索引加锁,普通索引不会加锁。
  • 只有插入或更新了唯一索引时,才会对唯一索引加锁,对唯一索引加锁的目的是为了保证唯一性。
  • 按主键锁定查找不存在的行时,会对不存在的行主键加X锁。
  • 按二级索引查找时,只会对主键加锁,不会对二级锁引加锁。
  • S锁只应用于SELECT … IN SHARE MODE语句。

堆栈

最后提供一些堆栈信息,方便学习

  • 走唯一索引对主键加锁
    #0  rocksdb::TransactionLockMgr::TryLock
    #1  rocksdb::PessimisticTransactionDB::TryLock
    #2  rocksdb::PessimisticTransaction::TryLock
    #3  rocksdb::TransactionBaseImpl::GetForUpdate
    #4  myrocks::Rdb_transaction_impl::get_for_update
    #5  myrocks::ha_rocksdb::get_for_update
    #6  myrocks::ha_rocksdb::get_row_by_rowid
    #7  get_row_by_rowid
    #8  myrocks::ha_rocksdb::read_row_from_secondary_key
    #9  myrocks::ha_rocksdb::index_read_map_impl
    #10 myrocks::ha_rocksdb::read_range_first
    #11 handler::multi_range_read_next
    #12 QUICK_RANGE_SELECT::get_next
    #13 rr_quick
    #14 mysql_delete
    #15 mysql_execute_command
    #16 mysql_parse
    #17 dispatch_command
    
  • 提交时解锁
    #0  rocksdb::TransactionLockMgr::UnLockKey
    #1  rocksdb::TransactionLockMgr::UnLock
    #2  rocksdb::PessimisticTransactionDB::UnLock
    #3  rocksdb::PessimisticTransaction::Clear
    #4  rocksdb::PessimisticTransaction::Commit
    #5  myrocks::Rdb_transaction_impl::commit_no_binlog
    #6  commit
    #7  myrocks::rocksdb_commit
    #8  ha_commit_low
    #9  TC_LOG_DUMMY::commit
    #10 ha_commit_trans
    #11 trans_commit
    #12 mysql_execute_command
    #13 mysql_parse
    #14 dispatch_command
    

PgSQL · 特性分析 · 事务ID回卷问题

$
0
0

背景

在之前的月报 PgSQL · 特性分析 · MVCC机制浅析中,我们了解到了:

  • 事务ID(XID)使用32位无符号数来表示,顺序产生,依次递增
  • 每个元组会来用(t_xmin, t_xmax)来标示自己的可用性
  • t_xmin 存储的是产生这个元组的事务ID,可能是insert或者update语句
  • t_xmax 存储的是删除或者锁定这个元组的XID
  • 每个事务只能看见t_xmin比自己XID 小且没有被删除的元组

其中需要注意的是,XID 是用32位无符号数来表示的,也就是说如果不引入特殊的处理,当PostgreSQL的XID 到达40亿,会造成溢出,从而新的XID 为0。而按照PostgreSQL的MVCC 机制实现,之前的事务就可以看到这个新事务创建的元组,而新事务不能看到之前事务创建的元组,这违反了事务的可见性。本文将这种现象称为XID 的回卷问题。

使用64位无符号数表示XID 可以缓解甚至解决这个问题。但是因为数据页中每个元组都会存储(xmin,xmax),这样势必会造成元组头部信息继续扩大,至少扩大2个字节。两者权衡,PostgreSQL 社区更加期望将这些内容放在缓存中,而只用数据页的一个bit 位来达到64位XID 的效果(详见 邮件列表)。目前来看,PostgreSQL 社区也是逐渐在实现这点。我们接下来主要来分析PostgreSQL 是如何解决这个问题的。

两个事务ID的比较方法

在详细讲解PostgreSQL 解决这个问题的方法之前,我们需要先了解下两个XID 是如何进行比较的。这部分代码非常的简单易懂,具体如下:

/*
 * TransactionIdPrecedes --- is id1 logically < id2?
 */
bool
TransactionIdPrecedes(TransactionId id1, TransactionId id2)
{
	/*
	 * If either ID is a permanent XID then we can just do unsigned
	 * comparison.  If both are normal, do a modulo-2^32 comparison.
	 */
	int32		diff;

	if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2))
		return (id1 < id2);

	diff = (int32) (id1 - id2);
	return (diff < 0);
}

其中,值得注意的是diff = (int32) (id1 - id2)。如果发生了XID 回卷后,即使id1=4294967290比id2=5(回卷后的XID)大,但因为相减后diff大于2^31,结果值转成int32后会变成一个负数,从而让判断逻辑与事务回卷前都是一样的: (int32)(id1 - id2) < 0。但是如果这里的id2 是回卷前的XID,则这里就会出现问题。所以,PostgreSQL 就要保证一个数据库中两个有效的事务之间的年龄最多是2^31,即20亿。

也就是说:

  • PostgreSQL中是使用2^31取模的方法来进行事务的比较
  • 同一个数据库中,存在的最旧和最新两个事务之间的年龄最多是2^31,即20亿

这时,我们可以把PostgreSQL 中事务ID理解为一个循环可重用的序列串。对其中的任一普通XID(特殊XID 除外)来说,都有20亿个相对它来说过去的事务,都有20亿个未来的事务,事务ID 回卷的问题得到了解决。但是可以看出这个问题得到解决的前提在同一个数据库中存在的最旧和最新两个事务之间的年龄是最多是2^31,接下来我们将分析PostgreSQL 如何做到的这一点。

冻结清理

为了实现同一个数据库中的最新和最旧的两个事务之间的年龄不超过2^31,PostgreSQL 引入了冻结清理(freeze)的功能。通过之前的月报 PgSQL · 源码分析 · AutoVacuum机制之autovacuum worker可知,在autovacuum过程中,会自动对符合条件的元组进行freeze。为了不让问题扩散,我们会在下文具体分析符合什么条件的元组才需要freeze,这里会先分析不同的PostgreSQL版本freeze具体的实现。

9.4之前冻结清理实现

在9.4之前的版本中,freeze实现的方法很简单。就是对符合条件的元组直接更新元组信息(HeapTupleFields结构体)中的t_xmin 属性为一个特殊的XID,FrozenTransactionId(FrozenTransactionId 为2,initdb产生的catalog所对应的XID 为1,0代表无效的XID)。

以FrozenTransactionId为t_xmin的元组将会被其他所有的事务可见,这样该元组原来对应的XID 相当于被回收了,经过不断的处理,就可以控制一个数据库的最老的事务和最新的事务的年龄不超过20亿。

但是这样的实现有很多问题:

  • 当前可见的数据页(通过visibility map可以快速定位)需要全部扫描,带来大量的IO扫描
  • 符合条件的元组需要更新xmin,造成大量的脏页,带来大量的IO

9.4之后冻结清理实现

为了解决之前老版本存在的问题,9.4之后(包含9.4)不直接修改HeapTupleFields中的t_xmin,而是:

  • 只更新元组头部信息(HeapTupleHeaderData结构体)的t_infomask为HEAP_XMIN_FROZEN,表示该元组已经被冻结清理过(frozen)
  • 有些插入操作,也可以直接将记录置为frozen,例如大批量的COPY数据,insert into等
  • 整个page 如果所有记录已经frozen,则再vm文件中标记为FROZEN,冻结清理会跳过该页,减少了IO扫描

其中值得注意的是,如果vm页损坏了,可以通过vacuum DISABLE_PAGE_SKIPPING强制扫描所有的数据页。

可以看出,9.4之后对freeze的实现进行了很多方面的优化,提高了其性能。不过如果是9.4之前的数据通过pg_upgrade的脚本导入的数据,仍然会发现有t_xmin 为2的元组。当然除了上文讲到的autovaccum可以周期性地进行freeze之外,我们还可以执行VACUUM FREEZE命令来强制freeze。

至此,我们弄清楚了freeze是怎么实现的,接下来会去分析元组满足什么样的条件才会触发周期性的freeze。在PostgreSQL,这个条件是由一系列的参数设置来实现的,研究好这些参数的含义,将会更加有利于我们的日常运维。

涉及到的参数

与freeze相关的参数主要有三个:

  • vacuum_freeze_min_age
  • vacuum_freeze_table_age
  • autovacuum_freeze_max_age

vacuum_freeze_min_age表示表中每个元组需要freeze的最小年龄。这里值得一提的是每次表被freeze 之后,会更新pg_class 中的relfrozenxid 列为本次freeze的XID。表年龄就是当前的最新的XID 与relfrozenxid的差值,而元组年龄可以理解为每个元组的t_xmin与relfrozenxid的差值。所以,这个参数也可以被简单理解为每个元组两次被freeze之间的XID 差值的一个最小值。增大该参数可以避免一些无用的freeze 操作,减小该参数可以使得在表必须被强制清理之前保留更多的XID 空间。该参数最大值为20亿,最小值为2亿。

普通的vacuum 使用visibility map来快速定位哪些数据页需要被扫描,只会扫描那些脏页,其他的数据页即使其中元组对应的xmin非常旧也不会被扫描。而在freeze的过程中,我们是需要对所有可见且未被all-frozen的数据页进行扫描,这个扫描过程PostgreSQL 称为aggressive vacuum。每次vacuum都去扫描每个表所有符合条件的数据页显然是不现实的,所以我们要选择合理的aggressive vacuum周期。PostgreSQL 引入了参数vacuum_freeze_table_age来决定这个周期。

vacuum_freeze_table_age表示表的年龄大于该值时,会进行aggressive vacuum,即扫描表中可见且未被all-frozen的数据页。该参数最大值为20亿,最小值为1.5亿。如果该值为0,则每次扫描表都进行aggressive vacuum。

直到这里,我们可以看出:

  • 当表的年龄超过vacuum_freeze_table_age则会aggressive vacuum
  • 当元组的年龄超过vacuum_freeze_min_age后可以进行freeze

为了保证上文中整个数据库的最老最新事务差不能超过20亿的原则,两次aggressive vacuum之间的新老事务差不能超过20亿,即两次aggressive vacuum之间表的年龄增长(vacuum_freeze_table_age)不能超过20亿减去vacuum_freeze_min_age(只有元组年龄超过vacuum_freeze_min_age才会被freeze)。但是看上面的参数,很明显不能绝对保证这个约束,为了解决这个问题,PostgreSQL 引入了autovacuum_freeze_max_age 参数。

autovacuum_freeze_max_age表示如果当前最新的XID 减去元组的t_xmin 大于等于autovacuum_freeze_max_age,则元组对应的表会强制进行autovacuum,即使PostgreSQL已经关闭了autovacuum。该参数最小值为2亿,最大值为20亿。

也就是说,在经过autovacuum_freeze_max_age-vacuum_freeze_min_age的XID 增长之后,这个表肯定会被强制地进行 一次freeze。因为autovacuum_freeze_max_age最大值为20亿,所以说在两次freeze之间,XID 的增长肯定不会超过20亿,这就保证了上文中整个数据库的最老最新事务差不能超过20亿的原则。

值得一提的是,vacuum_freeze_table_age设置的值如果比autovacuum_freeze_max_age要高,则每次vacuum_freeze_table_age生效地时候,autovacuum_freeze_max_age已经生效,起不到过滤减少数据页扫描的作用。所以默认的规则,vacuum_freeze_table_age要设置的比autovacuum_freeze_max_age小。但是也不能太小,太小的话会造成频繁的aggressive vacuum。

另外我们通过分析源码可知,vacuum_freeze_table_age在最后应用时,会去取min(vacuum_freeze_table_age,0.95 * autovacuum_freeze_max_age)。所以官方文档推荐vacuum_freeze_table_age=0.95 * autovacuum_freeze_max_age。

freeze 操作会消耗大量的IO,对于不经常更新的表,可以合理地增大autovacuum_freeze_max_age和vacuum_freeze_min_age的差值。

但是如果设置autovacuum_freeze_max_age 和vacuum_freeze_table_age过大,因为需要存储更多的事务提交信息,会造成pg_xact 和 pg_commit 目录占用更多的空间。例如,我们把autovacuum_freeze_max_age设置为最大值20亿,pg_xact大约占500MB,pg_commit_ts大约是20GB(一个事务的提交状态占2位)。如果是对存储比较敏感的用户,也要考虑这点影响。

而减小vacuum_freeze_min_age则会造成vacuum 做很多无用的工作,因为当数据库freeze 了符合条件的row后,这个row很可能接着会被改变。理想的状态就是,当该行不会被改变,才去freeze 这行。

但是遗憾的是,无论参数怎么调优,都存在一个问题,freeze是不能主动预测的,只能被动触发,所以更提倡用户进行主动预测需要freeze 的时机,选择合适的时间(比如说应用负载较低的时间)主动执行vacuum freeze命令。接下来我们会具体讨论如何去做关于vacuum freeze 的运维。

运维建议

由于参数设置问题或者其他问题,造成freeze 失败,导致数据库最老的表年龄达到了1000万的时候,数据库会打印如下的warning:

    WARNING:  database "mydb" must be vacuumed within 177009986 transactions
HINT:  To avoid a database shutdown, execute a database-wide VACUUM in "mydb".

根据提示,对该数据库执行vacuum free命令,可以解决这个潜在的问题。注意因为非超级用户没有权限更新database的datfrozenxid,只能使用超级用户执行acuum free database_name。

如果数据库可用的XID 空间还有100万的时候,即当前最新XID 与数据库最老的XID 的差值还差100万达到20亿,则PostgreSQL 会变为只读并拒绝开启任何新的事务,同时在日志中打印如下错误信息:

ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
HINT:  Stop the postmaster and vacuum that database in single-user mode.

如果出现了这种情况,根据提示,用户可以以单用户模式(single-user mode,详见链接)的方法启动PostgreSQL并执行vacuum freeze命令。

可以看出,参数的正确设置是非常重要的。但是上文说过即使参数设置的比较合适,因为不能预测freeze 发生的时间,如果freeze发生的时间正好是数据库比较繁忙的时间,这就会造成IO资源争抢,导致正常的业务受损。用户可以自己监控数据库和表的年龄,在业务比较空闲的时间主动执行以下操作:

  • 查询当前所有表的年龄,SQL 语句如下:
    SELECT c.oid::regclass as table_name,
         greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age
    FROM pg_class c
    LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
    WHERE c.relkind IN ('r', 'm');
    
  • 查询所有数据库的年龄,SQL 语句如下:
    SELECT datname, age(datfrozenxid) FROM pg_database;
    
  • 设置vacuum_cost_delay为一个比较高的数值(例如50ms),这样可以减少普通vacuum对正常数据查询的影响
  • 设置vacuum_freeze_table_age=0.5 * autovacuum_freeze_max_age,vacuum_freeze_min_age为原来值的0.1倍
  • 对上面查询的表依次执行vacuum freeze,注意要预估好时间。

目前已经有很多实现好的开源PostgreSQL vacuum freeze监控管理工具,比如说flexible-freeze),它能够:

  • 确定数据库的高峰和低峰期
  • 在数据库低峰期创建一个cron job 去执行flexible_freeze.py
  • flexible_freeze.py 会自动对具有最老XID的表进行vacuum freeze

总结

至此,我们已经从各个角度分析了PostgreSQL 中出现的事务ID 回卷问题的解决方法。总结起来就是:

  1. XID 可循环利用
  2. XID 比较实用mod 2^31的方法
  3. 同一个数据库中,存在的最旧和最新两个事务之间的年龄最大为2^31,即20亿
  4. 当元组满足一定条件时,将其freeze,从而实现了将其对应的XID回收的操作
  5. 通过vacuum_freeze_min_age,vacuum_freeze_table_age,autovacuum_freeze_max_age参数配合,让freeze 操作更平滑,更高效

不过,上文中我们并没有涉及Multixacts ID 的回卷问题。Multixacts ID 的回卷和XID 的回卷问题大体相似,我们这里不再过多赘述,有兴趣的同学可以去查找下相关资料。

MariaDB · 源码分析 · thread pool

$
0
0

1. thread pool 简介

MariaDB 共有三种线程调度方式

  • one-thread-per-connection 每个连接一个线程

  • no-threads 所有连接共用一个线程

  • pool-of-threads 线程池

no-threads 只适用于简单的系统,并发数稍高性能就会严重下降

one-thread-per-connection 在多数情况下性能优良,是个合适的选择,生产系统也常用此配置。但在高并发、短连接的业务场景下,使用 one-thread-per-connection 会频繁得创建和销毁线程,严重影响性能

pool-of-threads 适用于高并发短连接的业务场景,线程复用,避免频繁创建和销毁线程带来的性能损耗

MariaDB 的 thread pool 在 win 和 unix 系统的实现不同,本文分析 unix 系统下的实现

thread pool 由若干个 thread_group 组成,每个 thread_group 有若干个 worker 线程,和 0~1 个 listenr 线程

server 接收到连接请求时,将这个连接分配给一个 group 处理,listener 线程负责监听请求,worker 线程处理请求内容

2.代码概览

struct scheduler_functions 内部声明了连接建立相关的属性和方法,这里使用函数指针实现多态

每种线程调度方式,分别实例化一个struct scheduler_functions,给相关变量和函数指针赋值

thread pool 相关实现在 sql/threadpool_common.cc 和 sql/threadpool_unix.cc 中

sql/scheduler.h

struct scheduler_functions
{
  uint max_threads, *connection_count;
  ulong *max_connections;
  bool (*init)(void);
  bool (*init_new_connection_thread)(void);                                                                                                                                                                 
  void (*add_connection)(THD *thd);
  void (*thd_wait_begin)(THD *thd, int wait_type);
  void (*thd_wait_end)(THD *thd);
  void (*post_kill_notification)(THD *thd);
  bool (*end_thread)(THD *thd, bool cache_thread);
  void (*end)(void);
};

-------------------------------------------------

sql/threadpool_common.cc

static scheduler_functions tp_scheduler_functions=                                                                                                                                                          
{
  0,                                  // max_threads
  NULL,
  NULL,
  tp_init,                            // init
  NULL,                               // init_new_connection_thread
  tp_add_connection,                  // add_connection
  tp_wait_begin,                      // thd_wait_begin
  tp_wait_end,                        // thd_wait_end
  post_kill_notification,             // post_kill_notification
  NULL,                               // end_thread
  tp_end                              // end
};

  • tp_init

初始化 thread_group

main

  mysqld_main
    
    network_init
    
      /* 调用相应 thread_scheduler 下的 init 函数,这里是 tp_init */
      MYSQL_CALLBACK_ELSE(thread_scheduler, init, (), 0)
      tp_init
      
        /* thread_group_t 申请内存 */
        all_groups= (thread_group_t *)
          my_malloc(sizeof(thread_group_t) * threadpool_max_size, MYF(MY_WME|MY_ZEROFILL));
          
        /* 初始化 thread_group_t 数组 */
        for (uint i= 0; i < threadpool_max_size; i++) 
        {
          thread_group_init(&all_groups[i], get_connection_attrib());  
        }
		
		/* 设置最大执行时间,worker thread */
		pool_timer.tick_interval= threadpool_stall_limit;
        
        /* 开启 timer thread*/
        start_timer(&pool_timer);          
          

  • tp_add_connection

创建 connection,将 connection 分配到一个 thread_group,插入 group 队列

唤醒或创建一个 worker thread,如果 group 中没有活跃的 worker thread

main

  mysqld_main
  
    handle_connections_sockets
    
      create_new_thread
      
        /* 调用相应 thread_scheduler 下的 add_connection 函数,这里是 tp_add_connection */
        MYSQL_CALLBACK(thd->scheduler, add_connection, (thd));
        tp_add_connection
        
          /* 申请一个 connection */
          connection_t *connection= alloc_connection(thd);
          
          /* round-robin 映射到一个 group */
          thread_group_t *group= 
            &all_groups[thd->thread_id%group_count];
            
          queue_put(group, connection)
          
            /* connection 插入队列 */
            thread_group->queue.push_back(connection);
            
            /*
              如果没有活跃 woker 线程,唤醒一个处理新连接 
              此时连接尚未 login,认证等工作在 worker 线程中完成
            */
            if (thread_group->active_thread_count == 0)
              wake_or_create_thread(thread_group);                                        

wake_or_create_thread

  /* 取出 thread_group->waiting_threads 中的 waiting thread (如有) */
  if (wake_thread(thread_group) == 0)
    DBUG_RETURN(0);
  
  /* 
    没有活跃线程时,立即创建一个,在其他地方也有这个逻辑
    保证每个 thread_group 只是存在一个活跃线程
  */
  if (thread_group->active_thread_count == 0)
    DBUG_RETURN(create_worker(thread_group));
  
  /* 
    对创建新的 woker thread 限流,距离上个线程创建间隔一定时间才允许再次创建 worker thread
    这个 group 线程总数越多,要求的间隔越长
    4线程以下间隔为0,大于等于 4/8/16 线程时,要求间隔分别为 50*1000/100*1000/200*1000 微秒
  */  
  if (time_since_last_thread_created >
       microsecond_throttling_interval(thread_group))
  {
    DBUG_RETURN(create_worker(thread_group));
  }
  
  
  • worker_main

从 queue 中取出和处理 event

在 group 没有 listener 时会变成 listener

没取到 event 时,进入休眠状态,等待被唤醒

最后进入休眠状态的 worker thread 最先被唤醒

worker_main

  for(;;)
  {                                                                                                                                                              
    connection = get_event(&this_thread, thread_group, &ts);
    
    /* 没有 event 时,跳出循环,结束 worker thread */
    if (!connection)
      break;
    
    handle_event(connection);
  }


get_event

  for(;;)
  {
    /* 
      取出 tp_add_connection/listener 插入的 connection
    */
    connection = queue_get(thread_group);
    
    /* 取到 connection,跳出循环,进入下一步 handle_event */
    if(connection)
      break;
    
    /* 如果没有 listener thread,这个线程变为 listener */
    if(!thread_group->listener)
    {
      /* listener 变为 worker 处理一个 connection */
      connection = listener(current_thread, thread_group);
      
      /* listener 已变为 worker */
      thread_group->listener= NULL;

      /* 跳出循环,进入 handle_event 处理 connection */
      break;
    }
    
    /* 进入休眠状态前,尝试非阻塞方式获取一个 connection */
    if (io_poll_wait(thread_group->pollfd,&nev,1, 0) == 1)
    {
      thread_group->io_event_count++;
      connection = (connection_t *)native_event_get_userdata(&nev);
      break;
    }
    
    /*
      进入休眠状态
      最后休眠的 worker thread 最先被唤醒,更容易命中 cache
    */
    current_thread->woken = false;
    thread_group->waiting_threads.push_front(current_thread);
    
    /* 等待被唤醒 */
    mysql_cond_wait
  }
  
  /* 返回获取到的 connection */
  DBUG_RETURN(connection);


handle_event

  if (!connection->logged_in)
  {
    /* login connection */
    err= threadpool_add_connection(connection->thd);
      
      login_connection
    
    connection->logged_in= true;
  }
  else
  {
    /* 处理 connection 请求 */
    err= threadpool_process_request(connection->thd);
    
      do_command
  }
  
  /* 
    告诉客户端可以读
    可能会变动 connection 所属的 group 
  */
  err= start_io(connection);
  
    /* 
      group_count 允许动态改变,所以处理 connection 的 group 可能发生变动
      这里检查 group 是否需要变动
    */
    thread_group_t *group = 
      &all_groups[connection->thd->thread_id%group_count];
      
    if (group != connection->thread_group)
      change_group(connection, connection->thread_group, group)
    
    return io_poll_start_read(group->pollfd, fd, connection);
  
  • listener

listener 线程通过 epoll_wait 监听 group 关联的描述符,epoll使用一个文件描述符管理多个描述符

监听获得的 event 插入队列,如果插入前队列为空,listener 变成 worker 线程处理第一个event,其余插入队列,否则所有 event 插入队列

如果 group 中没有活跃线程,唤醒或者创建一个 worker 线程

这里考虑一种情况,如果监听只获得一个 event 且队列为空,那么这个 event 将会被 listener 处理,队列中不会插入新的 event,此时只需要 listener 变成 worker 线程处理 event,不需要再唤醒其他 worker 线程

listener

  for(;;)
  {
    /* 监听事件 */
    cnt = io_poll_wait(thread_group->pollfd, ev, MAX_EVENTS, -1);
    
    /* 如果队列为空,listener 处理第一个事件,一定概率(只有一个事件)可以不再唤醒 worker thread */
    bool listener_picks_event= thread_group->queue.is_empty();
    
    
    /* 第一个事件留给 listener 处理,其余放入队列,或者全部放入队列 */
    for(int i=(listener_picks_event)?1:0; i < cnt ; i++) 
    {
      connection_t *c= (connection_t *)native_event_get_userdata(&ev[i]);
      thread_group->queue.push_back(c);
    }
    
    if (listener_picks_event)
    {
      /* Handle the first event. */
      retval= (connection_t *)native_event_get_userdata(&ev[0]);
      mysql_mutex_unlock(&thread_group->mutex);
      break;
    }
    
    /* 
      队列中已经有一些 event,但是没有活跃线程
      唤醒一个 worker 线程处理队列中 event
      唤醒失败,并且仅有一个线程(仅有listener),创建一个 worker 线程
    */
    if(thread_group->active_thread_count==0)
    {    
      if(wake_thread(thread_group))
      {    
        if(thread_group->thread_count == 1)
          create_worker(thread_group);   
      }    
    }
    
  }
  
  DBUG_RETURN(retval);

  • timer_thread

检查 listener、worker thread 的运行情况和 connection 超时情况,每隔 thread_pool_stall_limit 检查一次

如果 listener 不存在,且检查周期内没有监听到新的 event,则创建一个 worker thread(自动变成 listener)

如果没有活跃线程(运行状态的 worker thread),且检查周期内没有处理 event,创建一个 worker thread

关闭长时间空闲的 connection

timer_thread

  for(;;)
  {
    /*
      等待一个 tick_interval,即 thread_pool_stall_limit
      正常情况下回等待至超时,只有 stop_timer 会发出 timer->cond 信号
    */
    set_timespec_nsec(ts,timer->tick_interval*1000000);
    err= mysql_cond_timedwait(&timer->cond, &timer->mutex, &ts);
    
    /* ETIMEDOUT 等待超时,代表 timer 没有被 shutdown */
    if (err == ETIMEDOUT)
    {
      timer->current_microtime= microsecond_interval_timer();
      
      /* 遍历 thread_group,查看是否 stall */
      for (i= 0; i < threadpool_max_size; i++)
      {
        if(all_groups[i].connection_count)
           check_stall(&all_groups[i]);
      }
      
      /* 关闭空闲连接 */
      if (timer->next_timeout_check <= timer->current_microtime)
        timeout_check(timer);
    }
  }

check_stall

  /*
    thread_group->io_event_count 代表插入队列的 event 数量
    thread_group 没有 listener,且没有 event 在队列中,这个检查周期内 group 没有监听连接信息
    唤醒或者创建一个 worker 线程,这个线程会自动成为 listener
  */
  if (!thread_group->listener && !thread_group->io_event_count)                                                                                                                                             
  {
    wake_or_create_thread(thread_group);
    return;
  }
  
  /* Reset io event count */
  thread_group->io_event_count= 0;
  
  /*
    thread_group->queue_event_count 代表已经出队的 event 数量
    队列非空,并且出队 event 数量为0,这个检查周期内 worker thread 没有处理 event
    唤醒或创建一个 worker thread, thread_group 标记为 stalled, 下个 event 出队时 reset stalled标记
  */
  if (!thread_group->queue.is_empty() && !thread_group->queue_event_count)
  {
    thread_group->stalled= true;
    wake_or_create_thread(thread_group);
  }
  
  /* Reset queue event count */
  thread_group->queue_event_count= 0;


  • tp_wait_begin/tp_wait_end

线程进入阻塞状态前/阻塞状态结束后,分别调用者两个函数汇报状态,用于维护 active_thread_count 即活跃线程数量

tp_wait_begin 将活跃线程数 -1,活跃线程为 0 时,唤醒或创建一个线程

tp_wait_eng 将活跃线程数 +1

这里需要注意,不是所有的等待都需要调用 tp_wait_begin,预期内短时间结束的等待,比如 mutex,可以不调用 tp_wait_begin

行锁或者磁盘IO这种长时间的等待,需要调用 tp_wait_begin,汇报这个线程暂时无法处理请求,需要开启新的线程

线程池和连接池

线程池是在 server 端的优化,避免频繁的创建和销毁线程

连接池是在 client 端的优化,减少连接创建时间,节省资源

连接池和线程池是两个不同的概念,可以同时使用

PgSQL · 应用案例 · 毫秒级文本相似搜索实践一

$
0
0

背景

在现实生活中,很多地方会用到相似搜索,例如

1、打车,要去某个地方,我们输入的目的地可能和数据库里面存的并不完全一致。所以只能通过相似搜索来实现。

2、搜索问题,同样的道理,我们搜的问题可能和存的问题不完全一致。只能通过相似搜索来匹配。

3、搜索兴趣点,等。

实际上PostgreSQL就可以支持相似搜索,包括图片、数组、文本等相似搜索。对于文本,可以使用pg_trgm插件来实现相似搜索。

这是纯英文字符串的测试,100亿量级(每行32个随机英文字母+数组的组合),模糊查询毫秒级别。

《PostgreSQL 百亿数据 秒级响应 正则及模糊查询》

相似查询使用同样的插件和索引。本文针对随机中文,相似搜索进行测试,看看PostgreSQL 单机性能如何?

构建测试样本数据

1、生成随机中文的函数

-- 生成随机汉字符串    
create or replace function gen_hanzi(int) returns text as $$    
declare    
  res text;    
begin    
  if $1 >=1 then    
    select string_agg(chr(19968+(random()*20901)::int), '') into res from generate_series(1,$1);    
    return res;    
  end if;    
  return null;    
end;    
$$ language plpgsql strict;    

2、使用分区表来提高写入、查询性能

如何建分区表,请参考:

《PostgreSQL 查询涉及分区表过多导致的性能问题 - 性能诊断与优化(大量BIND, spin lock, SLEEP进程)》

《PostgreSQL 商用版本EPAS(阿里云ppas) - 分区表性能优化 (堪比pg_pathman)》

《PostgreSQL 传统 hash 分区方法和性能》

《PostgreSQL 10 内置分区 vs pg_pathman perf profiling》

《PostgreSQL 10.0 preview 功能增强 - 内置分区表》

本文为了测试方便,未使用以上分区方法,请注意。

3、建父表(为了加速导入,使用了unlogged table,生成请勿使用)

create unlogged table tbl(id int primary key, info text);  
  
alter table tbl set (parallel_workers =64);  
  
create extension pg_trgm;  

4、建64个子表

do language plpgsql $$  
declare  
begin  
  for i in 0..63  
  loop  
    execute format('drop table if exists tbl%s ', i);  
    execute format('create unlogged table tbl%s (like tbl including all) inherits(tbl)', i);  
    -- 提前设置好表级并行度,方便后面做并行测试  
    execute format('alter table tbl%s set (parallel_workers =64)', i);  
  end loop;  
end;  
$$;  

5、往分区中写入10亿条测试数据

快速写入方法如下,使用dblink异步调用并行加载。

create or replace function conn(    
  name,   -- dblink名字    
  text    -- 连接串,URL    
) returns void as $$      
declare      
begin      
  perform dblink_connect($1, $2);     
  return;      
exception when others then      
  return;      
end;      
$$ language plpgsql strict;   

64个分区,每行64个随机汉字,每个分区写入15625000行,总共插入10亿行。

create extension dblink;  
  
do language plpgsql $$  
declare  
begin  
  for i in 0..63  
  loop  
    perform conn('link'||i,  'hostaddr=127.0.0.1 user=postgres dbname=postgres');   
    perform dblink_send_query('link'||i, format('insert into tbl%s select generate_series(1, 15625000), gen_hanzi(64)', i));  
  end loop;  
end;  
$$;  

这种并行写入方法,把CPU用了个精光,马力全开,高速写入10亿条随机文本。

top - 14:49:48 up 217 days,  4:29,  3 users,  load average: 64.33, 63.08, 46.16  
Tasks: 756 total,  65 running, 691 sleeping,   0 stopped,   0 zombie  
%Cpu(s): 96.5 us,  3.5 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st  
KiB Mem : 52807456+total,  7624988 free, 19696912 used, 50075267+buff/cache  
KiB Swap:        0 total,        0 free,        0 used. 37125398+avail Mem   

写入完毕:

10亿记录,表占用空间223GB,写入记录耗时18分钟。

样本如下:

postgres=# select * from tbl limit 10;  
 id |                                                               info                                                                 
----+----------------------------------------------------------------------------------------------------------------------------------  
  1 | 懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁内橰畁蜫征瘭缆竟  
  2 | 荓嚅鑀鑬抾诐裹坲雚囻卥饸數拰絔刦霨礸诿廓琫颧仯瞱捲瘰弶瓴鹝逼倭舌飂陭盒寚芘怦敍种椡檱玠肙羡兎蒿眤粆焙蟸儌樛裦窽美影诳哜帪粊圊鈵疧  
  3 | 齷楣莁艪箉髎岒險旚舲瞞靻薀岹滺扡習坍敮鯭鈳鈫篖刀繹芲截孞讼咺茅讎瀝曡湓鶦戊糥钫秤彲沤熻雲筝銵妮宊鰂焜埒躐採薨銐鐚梶唕俓响寏蓘鉛緬  
  4 | 鶹愈篭怞迭烲调侺辖帘颬歎儨劵磘鼪痐芪踖譱梮脁翦荣蠖膹訰闥曬糦琬攀迮偳真耷獦捼臱捗玕竷肥皽羬姘癃嗗躂撴鍉垊鞵玊賮耦喞睹癦溊咺鲒薋隨  
  5 | 鼅崄眹狆犁妅蠝頖虼椝漮暄瓴靰湛揑屿懿浛咏螈媤蚴輦萝嵵帋諗婢閖臙姂勵奮纈睶擳最濧鵯舜鄕摎坫裠蒩洽靟颧貘鷮肋餼蓽瀌綴鑳耗棦估瘈鲿嫲竾  
  6 | 嚈譺勏浺勔璶歅蛰春膒遜你暖巳颿徙鲋霈鈣阣籡把琲焮钢輗牞欅谱罐頃钹欤鳑抏濸燢翓坄訇懁馠譧穗埮蒂诰哔篥繮鳷墡鋸熃篏蟵惶予单鼧翘鵗鐻鳼  
  7 | 骄圥浏況裸皓圣鲹炎钊睫穼祧掶腐喧鐤红恈蝷傀踗濇捶躟甜拸滒狎垎氩涭悳譸豭鮬执閐飀蓴詵炆忋搷蘼錛毞窻爘縦抌璘沙葓訍宓姊鼅籥纘囯骎鹄榢  
  8 | 虢謌斩髈胷廄耘毇腊釣臾柡蕙丷钛埋繝垃繣鳶跖棋壤馟栬蝉碒焚舲眱貽棯抙勀搒閐掄阪憲雎表閯弊減闦吀矦璞嶃嚤燯鵘煯糓靓讛摷灀崐颩饱鯍懳層  
  9 | 仨砆剏摬溋昁宕坍尋沟睨剌犟侩磫舢塎鳚翕箽稈瞂枲避駂盃覄鎎狪鵷偍珒痘咜訾陣沝韔下窨擎睳绵襭礜堺毩荪啰鶾徂腸疛礴牒澹偒就探甼娃旯鬎臛  
 10 | 沌薧碙謩緖碤昬钣偱霠繫箎侶鱔归圦驭烔誝灣鰈嵋鈜鹚歼嘘珰睿済潙妵貓啛葎砗蔱嵍遂稰徾螾壶赌襴喥麞銙偭濍綒狐氰賜敇櫤墳浟郕舲赧悉跧穕柤  
(10 rows)  

6、创建索引(实际生产中,索引可以先建好,这里主要是为了加速生成速度)

do language plpgsql $$  
declare  
begin  
  create index idx_tbl_info on tbl using gin(info gin_trgm_ops);  
  
  for i in 0..63  
  loop  
    perform conn('link'||i,  'hostaddr=127.0.0.1 user=postgres dbname=postgres');   
    perform dblink_send_query('link'||i, format('create index idx_tbl%s_info on tbl%s using gin(info gin_trgm_ops);', i, i));  
  end loop;  
end;  
$$;  

10亿记录,GIN倒排索引占用空间332GB,创建索引耗时接近180分钟。

索引创建速度解释:

由于这个CASE写入的字符串是几万个汉字里面的完全随机的汉字,所以GIN倒排索引的TOKEN特别多。这个CASE比正常生产数据的索引要大很大,正常索引不会这么大,正常创建速度也会比这个快。但是创建GIN索引的速度相比BTREE确实是要慢很多的(结构所致)。如果是BTREE索引,应该会在5分钟内创建完成。

相似查询SQL用法

1、查看当前相似度阈值

select show_limit();  
 show_limit   
------------  
        0.3  
(1 row)  

2、设置当前会话相似度阈值,其他设置详见末尾部分

select set_limit(0.9);  

3、相似搜索的响应速度与用户设置的相似度有关,用户设置的相似度匹配到的值越多,速度越慢。匹配到的值越少(即精度越高),响应速度越快。

-- 响应速度更慢  
postgres=# select set_limit(0.1);  
 set_limit   
-----------  
       0.1  
(1 row)  
  
-- 响应速度更快  
postgres=# select set_limit(0.9);  
 set_limit   
-----------  
       0.9  
(1 row)  

4、根据输入文本,查询与之相似的文本,并按相似排序输出。

select similarity(info, '输入搜索词') as sml, -- 计算输入词与存储字符串的相似度  
  * from tbl   
  where info % '输入搜索词'    -- 相似度超过阈值  
  order by sml desc            -- 按相似度排序(倒排,越相似的排在越前面)  
  limit 10;    

SQL耗时:71毫秒

postgres=# select set_limit(0.7);  
 set_limit   
-----------  
       0.7  
(1 row)  
  
select similarity(info, '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁') as sml, -- 计算输入词与存储字符串的相似度  
  * from tbl   
  where info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'    -- 相似度超过阈值  
  order by sml desc            -- 按相似度排序(倒排,越相似的排在越前面)  
  limit 10;    
  
 sml  | id |                                                               info                                                                 
------+----+----------------------------------------------------------------------------------------------------------------------------------  
 0.75 |  1 | 懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁内橰畁蜫征瘭缆竟  
(1 row)  
  
Time: 71.627 ms  

5、SQL执行计划如下,使用到了索引扫描,所以非常快。

explain select similarity(info, '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁') as sml, -- 计算输入词与存储字符串的相似度  
  * from tbl   
  where info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'    -- 相似度超过阈值  
  order by sml desc            -- 按相似度排序(倒排,越相似的排在越前面)  
  limit 10;   
  
  
  
                                                                                         QUERY PLAN                                                                                            
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=1025136.11..1025137.35 rows=10 width=204)  
   ->  Gather Merge  (cost=1025136.11..1148791.31 rows=999944 width=204)  
         Workers Planned: 8  
         ->  Sort  (cost=1024135.97..1024448.45 rows=124993 width=204)  
               Sort Key: (similarity(tbl2.info, '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)) DESC  
               ->  Result  (cost=554.09..1021434.91 rows=124993 width=204)  
                     ->  Parallel Append  (cost=554.09..1019872.50 rows=124993 width=200)  
                           ->  Parallel Bitmap Heap Scan on tbl2  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl2_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl3  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl3_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl4  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl4_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl5  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl5_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl7  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl7_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl8  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl8_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl9  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl9_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl10  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl10_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl11  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl11_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl12  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl12_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl13  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl13_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl14  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl14_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl16  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl16_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl17  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl17_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl18  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl18_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl19  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl19_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl20  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl20_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl21  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl21_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl22  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl22_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl23  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl23_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl24  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl24_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl25  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl25_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl26  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl26_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl28  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl28_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl29  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl29_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl30  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl30_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl31  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl31_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl33  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl33_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl34  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl34_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl35  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl35_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl36  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl36_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl37  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl37_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl38  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl38_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl39  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl39_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl41  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl41_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl42  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl42_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl44  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl44_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl45  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl45_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl46  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl46_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl47  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl47_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl48  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl48_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl49  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl49_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl50  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl50_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl51  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl51_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl52  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl52_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl53  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl53_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl55  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl55_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl56  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl56_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl57  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl57_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl58  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl58_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl59  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl59_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl61  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl61_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl62  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl62_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl63  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl63_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl0  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl0_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl1  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl1_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl6  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl6_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl15  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl15_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl27  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl27_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl32  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl32_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl40  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl40_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl43  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl43_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl54  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl54_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Bitmap Heap Scan on tbl60  (cost=554.09..15935.51 rows=1953 width=200)  
                                 Recheck Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                                 ->  Bitmap Index Scan on idx_tbl60_info  (cost=0.00..550.19 rows=15625 width=0)  
                                       Index Cond: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
                           ->  Parallel Seq Scan on tbl  (cost=0.00..0.00 rows=1 width=36)  
                                 Filter: (info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'::text)  
(265 rows)  

6、强制使用并行,PostgreSQL 11支持多个分区并行执行,速度将更快。

set enable_parallel_append =on;  
set max_parallel_workers_per_gather =16;  
set parallel_setup_cost =0;  
set parallel_tuple_cost =0;  
set min_parallel_table_scan_size =0;  
set min_parallel_index_scan_size =0;  
set enable_parallel_append =on;  
postgres=# select set_limit(0.7);  
 set_limit   
-----------  
       0.7  
(1 row)  
  
explain select similarity(info, '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁') as sml, -- 计算输入词与存储字符串的相似度  
  * from tbl   
  where info % '懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁'    -- 相似度超过阈值  
  order by sml desc            -- 按相似度排序(倒排,越相似的排在越前面)  
  limit 10;   

加了并行后,SQL耗时:40毫秒(因为本来就已经很快了,所以加并行度性能几乎没有提高,当计算量很大时,性能会提升明显)

 sml  | id |                                                               info                                                                 
------+----+----------------------------------------------------------------------------------------------------------------------------------  
 0.75 |  1 | 懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁内橰畁蜫征瘭缆竟  
(1 row)  
  
Time: 40.298 ms  

相似查询性能压测

1、构建压测函数,从现有记录中,使用主键随机提取一条字符串(并加工处理,生成一个新的具有一定相似度的字符串)。

使用 substring(info,1,28)||gen_hanzi(4)||substring(info,29,28)将得到相似度为0.75的一个新字符串。

从第1位开始,取28位,然后插入4个随机中文,再从29位开始取28位。这个字符串作为相似查询的输入。相似度为0.75。  
  
postgres=# select substring(info,1,28)||gen_hanzi(4)||substring(info,29,28) newval, info, similarity(substring(info,1,28)||gen_hanzi(4)||substring(info,29,28) , info) from tbl limit 10;  
-[ RECORD 1 ]--------------------------------------------------------------------------------------------------------------------------------  
newval     | 懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦焳邹祧鵅莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁  
info       | 懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁内橰畁蜫征瘭缆竟  
similarity | 0.75  
-[ RECORD 2 ]--------------------------------------------------------------------------------------------------------------------------------  
newval     | 荓嚅鑀鑬抾诐裹坲雚囻卥饸數拰絔刦霨礸诿廓琫颧仯瞱捲瘰弶瓴叠濷賨櫕鹝逼倭舌飂陭盒寚芘怦敍种椡檱玠肙羡兎蒿眤粆焙蟸儌樛裦窽美  
info       | 荓嚅鑀鑬抾诐裹坲雚囻卥饸數拰絔刦霨礸诿廓琫颧仯瞱捲瘰弶瓴鹝逼倭舌飂陭盒寚芘怦敍种椡檱玠肙羡兎蒿眤粆焙蟸儌樛裦窽美影诳哜帪粊圊鈵疧  
similarity | 0.75  
-[ RECORD 3 ]--------------------------------------------------------------------------------------------------------------------------------  
newval     | 齷楣莁艪箉髎岒險旚舲瞞靻薀岹滺扡習坍敮鯭鈳鈫篖刀繹芲截孞熒镻缮蜝讼咺茅讎瀝曡湓鶦戊糥钫秤彲沤熻雲筝銵妮宊鰂焜埒躐採薨銐鐚  
info       | 齷楣莁艪箉髎岒險旚舲瞞靻薀岹滺扡習坍敮鯭鈳鈫篖刀繹芲截孞讼咺茅讎瀝曡湓鶦戊糥钫秤彲沤熻雲筝銵妮宊鰂焜埒躐採薨銐鐚梶唕俓响寏蓘鉛緬  
similarity | 0.75  

压测函数如下

-- 使用随机字符串进行相似搜索(用于压测)    
create or replace function get_tbl(int) returns setof record as    
$$    
declare    
  str text;    
begin    
  perform set_limit(0.7);  
    
  -- 从第1位开始,取28位,然后插入4个随机中文,再从29位开始取28位。这个字符串作为相似查询的输入。相似度为0.75。  
  select substring(info,1,28)||gen_hanzi(4)||substring(info,29,28) into str from tbl where id=$1 limit 1;      
  
  return query execute format($_$select similarity(info, %L) as sml,   -- 计算输入词与存储字符串的相似度  
     * from tbl   
     where info %% %L             -- 相似度超过阈值  
     order by sml desc            -- 按相似度排序(倒排,越相似的排在越前面)  
     limit 10$_$,  str, str);      
end;    
$$ language plpgsql strict;    

查询测试

postgres=# select * from get_tbl(1) as t(sml float4, id int, info text);  
 sml  | id |                                                               info                                                                 
------+----+----------------------------------------------------------------------------------------------------------------------------------  
 0.75 |  1 | 懛瑌娺罊鶩凳芹緔茙蠡慺礛唾霹蹺憙胣緗犭昉鬪蒽麴牵癰嚒巈蔦莥钶们鞀楝嬦眥条弘娸霵鐲鑚夊涨鮗傞屽嶋莁豓舸鮉蟙材骘媨迁内橰畁蜫征瘭缆竟  
(1 row)  
  
Time: 92.229 ms  

压测脚本

vi test.sql  
\set id random(1,15625000)  
select * from get_tbl(1) as t(sml float4, id int, info text);  

压测

-- 并行度调低  
alter role postgres set max_parallel_workers_per_gather =2;  
  
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 32 -j 32 -T 120  
  
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 64
number of threads: 64
duration: 120 s
number of transactions actually processed: 51503
latency average = 149.175 ms
latency stddev = 20.054 ms
tps = 428.589421 (including connections establishing)
tps = 428.699150 (excluding connections establishing)
statement latencies in milliseconds:
         0.003  \set id random(1,15625000)
       149.311  select * from get_tbl(1) as t(sml float4, id int, info text);

性能瓶颈分析:

1、CPU跑满,IO也蛮高

top - 19:32:05 up 217 days,  9:11,  3 users,  load average: 38.04, 21.38, 11.92  
Tasks: 768 total,  57 running, 710 sleeping,   0 stopped,   1 zombie  
%Cpu(s): 82.0 us, 12.8 sy,  0.0 ni,  4.1 id,  1.1 wa,  0.0 hi,  0.0 si,  0.0 st  
KiB Mem : 52807456+total, 11373780 free, 14563392 used, 50213737+buff/cache  
KiB Swap:        0 total,        0 free,        0 used. 35995504+avail Mem   

CPU耗费主要是bitmapscan, cpu进行tuple recheck造成。

IO耗费,主要是数据+索引已经接近600GB,超过了内存大小,涉及到大量的IO访问。

小结

数据构造性能指标:

10亿文本数据写入耗时: 18分钟。

GIN索引生成耗时: 180分钟。

空间占用:

10亿文本: 223 GB

索引: 332 GB

性能指标:

CASE单次相似搜索响应速度整机压测相似搜索TPS整机压测相似搜索RT
10亿行,每行64个随机中文40毫秒428149毫秒

CPU跑满,IO也蛮高

CPU耗费主要是bitmapscan, cpu进行tuple recheck造成。

IO耗费,主要是数据+索引已经接近600GB,超过了内存大小,涉及到大量的IO访问。

小结

1、PostgreSQL 11 , append 并行,使得性能有大幅度提升。

10亿条随机中文字符串(长度64)的相似搜索,耗时仅XXX秒。

postgres=# show enable_parallel_append ;  
 enable_parallel_append   
------------------------  
 on  
(1 row)  

2、本例子使用分区表的好处:

2.1、数据写入并行度提高,

2.2、创建索引变快,

2.3、维护索引也变快。

同时PostgreSQL 11已经支持多个分区并行扫描(enable_parallel_append),同时支持了并行+merge sort,所以海量数据相似搜索的大计算量情况下性能也不是问题。

在PostgreSQL 11之前,可以使用dblink异步调用来支持多个分区的并行扫描。例子:

《PostgreSQL dblink异步调用实现 并行hash分片JOIN - 含数据交、并、差 提速案例》

3、相似搜索中很关键的一个点是相似度,通过show_limit()可以查看相似度限制,通过set_limit可以设置相似度阈值。相似度值越大,表示需要的匹配度越高,1表示完全匹配。

postgres=# select show_limit();  
 show_limit   
------------  
        0.3  
(1 row)  
  
postgres=# select set_limit(0.9);  
 set_limit   
-----------  
       0.9  
(1 row)  

4、通过similarity或word_similarity可以查看两个字符串的相似度值。

postgres=# select similarity('abc','abcd');  
 similarity   
------------  
        0.5  
(1 row)  
  
postgres=# select word_similarity('abc','abcd');  
 word_similarity   
-----------------  
            0.75  
(1 row)  
  
postgres=# select word_similarity('abc','abc');  
 word_similarity   
-----------------  
               1  
(1 row)  
  
postgres=# select similarity('abc','abc');  
 similarity   
------------  
          1  
(1 row)  

相似算法详情请参考

https://www.postgresql.org/docs/devel/static/pgtrgm.html

5、响应速度与用户设置的相似度有关,用户设置的相似度匹配到的值越多,速度越慢。匹配到的值越少(即精度越高),响应速度越快。

-- 响应速度更慢  
postgres=# select set_limit(0.1);  
 set_limit   
-----------  
       0.1  
(1 row)  
  
-- 响应速度更快  
postgres=# select set_limit(0.9);  
 set_limit   
-----------  
       0.9  
(1 row)  

实际生产中使用,我们可以先使用高的limit去搜索,逐渐缩小这个阈值,从而达到较快响应速度。这个逻辑可以封装到UDF中,用户调用UDF即可进行搜索。

例子

create or replace function get_res(
  text,     -- 要按相似搜的文本
  int8,     -- 限制返回多少条
  float4 default 0.3,   -- 相似度阈值,低于这个值不再搜搜
  float4 default 0.1    -- 相似度递减步长,直至阈值
) returns setof record as $$  
declare  
  lim float4 := 1;  
begin  
  -- 判定
  if not ($3 <= 1 and $3 > 0) then 
    raise notice '$3 must >0 and <=1';
    return;
  end if;
  
  if not ($4 > 0 and $4 < 1) then
    raise notice '$4 must >0 and <=1';
    return;
  end if;
  loop  
    -- 设置相似度阈值  
    perform set_limit(lim);  
      
    return query select similarity(info, $1) as sml, * from tbl where info % $1 order by sml desc limit $2;  

    -- 如果有,则退出loop  
    if found then  
      return;  
    end if;  
  
    -- 否则继续,降低阈值  
    -- 当阈值小于0.3时,不再降阈值搜索,认为没有相似。  
    if lim < $3 then  
      return;  
    else  
      lim := lim - $4;  
    end if;  
  end loop;  
end;  
$$ language plpgsql strict;  
select * from get_res('输入搜索文本', 输入限制条数, 输入阈值, 输入步长) as t(sml float4, id int, info text);  

使用这个UDF搜索,既快又准。

postgres=# select * from get_res('四餧麾鄟賃青乖涢鰠揃擝垭岮操彴淒鋺約韉夗缝特鏋邜鯩垭縳墙靰禮徛亦猰庴釅恎噡鈛翱勜嘹雍岈', 10, 0.4, 0.05) as t(sml float4, id int, info text);
   sml    | id |                                                               info                                                               
----------+----+----------------------------------------------------------------------------------------------------------------------------------
 0.602941 |  1 | 彿睰掇贼展跃鬠唂四餧麾鄟賃青乖涢鰠揃擝垭岮操彴淒鋺約韉夗缝特鏋邜鯩垭縳墙靰禮徛亦猰庴釅恎噡鈛翱勜嘹雍岈擦寵淽蒸佊鴁糜婡籹侰亇浰鶙
(1 row)

Time: 75.957 ms

6、如果要设置PG实例、数据库、用户级的阈值,可以通过这两个参数来指定

pg_trgm.similarity_threshold  
  
pg_trgm.word_similarity_threshold   

他们分别作用于以下操作符和函数(详见 https://www.postgresql.org/docs/devel/static/pgtrgm.html )。

text % text  
similarity(text, text)  
  
与  
  
text <% text  
word_similarity(text, text)  

设置举例

postgres=# alter system set pg_trgm.similarity_threshold =0.9;  
ALTER SYSTEM  
  
postgres=# select pg_reload_conf();  
 pg_reload_conf   
----------------  
 t  
(1 row)  
  
-- 永久生效  
  
postgres=# show pg_trgm.similarity_threshold;  
 pg_trgm.similarity_threshold   
------------------------------  
 0.9  
(1 row)  

参考

《PostgreSQL 查询涉及分区表过多导致的性能问题 - 性能诊断与优化(大量BIND, spin lock, SLEEP进程)》

《PostgreSQL 商用版本EPAS(阿里云ppas) - 分区表性能优化 (堪比pg_pathman)》

《PostgreSQL 传统 hash 分区方法和性能》

《PostgreSQL 10 内置分区 vs pg_pathman perf profiling》

《PostgreSQL 10.0 preview 功能增强 - 内置分区表》

《PostgreSQL 全文检索之 - 位置匹配 过滤语法(例如 ‘速度 <1> 激情’)》

《PostgreSQL 模糊查询 与 正则匹配 性能差异与SQL优化建议》

《PostgreSQL 遗传学应用 - 矩阵相似距离计算 (欧式距离,…XX距离)》

《多流实时聚合 - 记录级实时快照 - JSON聚合与json全文检索的功能应用》

《PostgreSQL - 全文检索内置及自定义ranking算法介绍 与案例》

《用PostgreSQL 做实时高效 搜索引擎 - 全文检索、模糊查询、正则查询、相似查询、ADHOC查询》

《HTAP数据库 PostgreSQL 场景与性能测试之 17 - (OLTP) 数组相似查询》

《HTAP数据库 PostgreSQL 场景与性能测试之 16 - (OLTP) 文本特征向量 - 相似特征(海明…)查询》

《HTAP数据库 PostgreSQL 场景与性能测试之 14 - (OLTP) 字符串搜索 - 全文检索》

《HTAP数据库 PostgreSQL 场景与性能测试之 13 - (OLTP) 字符串搜索 - 相似查询》

《HTAP数据库 PostgreSQL 场景与性能测试之 12 - (OLTP) 字符串搜索 - 前后模糊查询》

《HTAP数据库 PostgreSQL 场景与性能测试之 9 - (OLTP) 字符串模糊查询 - 含索引实时写入》

《HTAP数据库 PostgreSQL 场景与性能测试之 7 - (OLTP) 全文检索 - 含索引实时写入》

《多国语言字符串的加密、全文检索、模糊查询的支持》

《Greenplum 模糊查询 实践》

《全文检索 不包含 优化 - 阿里云RDS PostgreSQL最佳实践》

《17种文本相似算法与GIN索引 - pg_similarity》

《PostgreSQL 模糊查询最佳实践 - (含单字、双字、多字模糊查询方法)》

《PostgreSQL 10.0 preview 功能增强 - JSON 内容全文检索》

《PostgreSQL结合余弦、线性相关算法 在文本、图片、数组相似 等领域的应用 - 3 rum, smlar应用场景分析》

《PostgreSQL结合余弦、线性相关算法 在文本、图片、数组相似 等领域的应用 - 2 smlar插件详解》

《PostgreSQL结合余弦、线性相关算法 在文本、图片、数组相似 等领域的应用 - 1 文本(关键词)分析理论基础 - TF(Term Frequency 词频)/IDF(Inverse Document Frequency 逆向文本频率)》

《导购系统 - 电商内容去重\内容筛选应用(实时识别转载\盗图\侵权?) - 文本、图片集、商品集、数组相似判定的优化和索引技术》

《PostgreSQL 全表 全字段 模糊查询的毫秒级高效实现 - 搜索引擎颤抖了》

《从难缠的模糊查询聊开 - PostgreSQL独门绝招之一 GIN , GiST , SP-GiST , RUM 索引原理与技术背景》

《从相似度算法谈起 - Effective similarity search in PostgreSQL》

《聊一聊双十一背后的技术 - 毫秒分词算啥, 试试正则和相似度》

《PostgreSQL 全文检索加速 快到没有朋友 - RUM索引接口(潘多拉魔盒)》

《PostgreSQL 文本数据分析实践之 - 相似度分析》

《中文模糊查询性能优化 by PostgreSQL trgm》

《PostgreSQL 行级 全文检索》

《PostgreSQL 百亿数据 秒级响应 正则及模糊查询》

《PostgreSQL chinese full text search 中文全文检索》

《聊一聊双十一背后的技术 - 毫秒分词算啥, 试试正则和相似度》

《PostgreSQL 1000亿数据量 正则匹配 速度与激情》

Viewing all 689 articles
Browse latest View live