alibaba / alibaba/pilotscope

JOB bugs

Open
#13 0 comments 2 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
167
Forks
27
PR merge metrics
No merged PRs in 30d

Description

My English is not good, and then I'll switch to Chinese. Explaining in my native language won't lead to too many mistakes.

在跑JOB这个benchmark时候踩好几个坑,在issue里做一些说明,如果后面有人踩坑可以看看,大致的思路应该是对的,但是实现起来也不是很优雅,所以也就不pr了。

#### HASH_TABLE_SIZE

这个问题是在`SUBQUERY_CARD_PULL_ANCHOR`和`CARD_PUSH_ANCHOR`时会出现的,也就是获取subquery时候的报错,因为JOB中存在非常多的subquery,定义在`src/backend/pilotscope/parse_json.c`中的`HASH_TABLE_SIZE`定义的只有1023,肯定不够,我设置了16384是没有问题的,反正肯定要大于10k。

这个问题肯定会遇到,所以放第一个说,后面需要分条件说明了,就是是否设置了`geqo = off`

#### geqo

geqo是是否启用遗传算法来优化join order,默认为true

在subquery手动注入到pg的工作中,可以看到他们是关闭了geqo的:https://github.com/heriec/End-to-End-CardEst-Benchmark/blob/master/dockerfile/init_pgsql.sh

但是pilotscope是没有这样说的要求的,这在JOB中生成非常多的subquery时,会触发geqo(stats不会存在这个问题)。

##### geqo = on

具体调用路径在`src/backend/optimizer/path/allpaths.c`中的`make_rel_from_joinlist`

这个函数里面的

```sql
if (levels_needed == 1)
{
/*
* Single joinlist node, so we're done.
*/
return (RelOptInfo *) linitial(initial_rels);
}
else
{
/*
* Consider the different orders in which we could join the rels,
* using a plugin, GEQO, or the regular join search code.
*
* We put the initial_rels list into a PlannerInfo field because
* has_legal_joinclause() needs to look at it (ugly :-().
*/
root->initial_rels = initial_rels;

if (join_search_hook)
return (*join_search_hook) (root, levels_needed, initial_rels);
else if (enable_geqo && levels_needed >= geqo_threshold)
return geqo(root, levels_needed, initial_rels);
else
return standard_join_search(root, levels_needed, initial_rels);
}
```

当开启`enable_geqo` 同时`levels_needed >= geqo_threshold`就会调用`geqo_eval()`

也就是说:

- 如果 **表的数量 < geqo_threshold(默认 12)** → 用动态规划算法。
- 如果 **表的数量 ≥ geqo_threshold** → 启动 GEQO。

调用`geqo()` 会在 planner 决定用 GEQO 代替标准的动态规划 join search 时触发。

geqo 的调用在 `src/backend/optimizer/geqo/geqo_main.c`然后调用一些其他函数,直到调用到 位于`src/backend/optimizer/geqo/geqo_eval.c`的

```sql
geqo_eval(PlannerInfo *root, Gene *tour, int num_gene)
```

就不贴详细代码了,自己看源码,具体就是遗传算法期间临时创建一个内存 cxt,用完会被释放

```c
mycontext = AllocSetContextCreate(CurrentMemoryContext,
"GEQO",
ALLOCSET_DEFAULT_SIZES);
```

所以在 subquery 调用的时候会调用`src/backend/pilotscope/utils/hashtable.c`的`create_entry`函数创建 entry:

```c
Entry* entry = (Entry*)palloc(sizeof(Entry));
entry->key = (char*)palloc(strlen(key) + 1);
entry->value = (char*)palloc(strlen(value) + 1);
```

分配内存空间会被分配到`GEQO`这个 cxt 上

```c
mycontext = AllocSetContextCreate(CurrentMemoryContext,
"GEQO",
ALLOCSET_DEFAULT_SIZES);
```

如果没有使用就是使用`MessageContext`,`GEQO`是他的 children(pg里的内存是树状的)

所以当 GEQO 的内存在释放后,之前创建的Entry是存在 count_table 的,但是GEQO会释放这块的内存,导致后面从count_table中get时候内存找不到对应数据,只是为了解决这个bug的话,我就把create_entry时单独处理了一下,感觉不是很好的处理方式,仅作思路参考:

```c
// create entry
Entry* create_entry(const char* key, const char* value)
{
MemoryContext oldcxt = NULL;
if (strcmp(CurrentMemoryContext->name, "MessageContext") != 0)
{
oldcxt = MemoryContextSwitchTo(CurrentMemoryContext->parent);
}
Entry* entry = (Entry*)palloc(sizeof(Entry));
entry->key = (char*)palloc(strlen(key) + 1);
entry->value = (char*)palloc(strlen(value) + 1);
entry->next = NULL;
strcpy(entry->key, key);
strcpy(entry->value, value);
if (oldcxt != NULL)
{
MemoryContextSwitchTo(oldcxt);
}
return entry;
}
```

##### geqo = off

好,你说我不这么麻烦了,我直接关了不就行了,哈哈,那你就遇到下一个坑,还是JOB过大的subquery导致的,在上面的`HASH_TABLE_SIZE`我们给`count_table`分配容量时候太小出现问题了,现在在`CARD_PUSH_ANCHOR`时存subquery2card又出现问题了,具体在调用函数`store_aimodel_subquery2card()`里面的,在`src/backend/pilotscope/anchor2struct.c`中:

```c
int table_size = card_push_anchor->card_num * card_push_anchor->card_num;
table = create_hashtable(table_size);
```

> table是个全局对象

table_size定义为$card\_num^2$,这就导致了card_num大于10k时,非常大,我们知道他是为了防止碰撞,但是这样需要分配太大的空间,毕竟都是简化实现的,这里也不存在什么动态扩容的代码(你要想实现你就自己写),所以也就选合理的table_size,保守一点就用「**略大于 n 的素数**」作为哈希表容量:

```c
int is_prime(int n) {
if (n < 2) return 0;
if (n == 2) return 1;
if (n % 2 == 0) return 0;
for (int i = 3; i * i <= n; i += 2)
if (n % i == 0)
return 0;
return 1;
}

int next_prime(int x) {
if (x <= 2) return 2;
if (x % 2 == 0) x++;
while (!is_prime(x)) x += 2;
return x;
}
```

使用:

```c
// avoid hash confict
int n = card_push_anchor->card_num;
int table_size = next_prime(n * 2);
```

> 多说一句,qeqo生成的subquery_2_card顺序和普通的dp生成的顺序是不一样的,具体可以自己debug看下

上面就是JOB所有踩坑的部分了,至少这么写能跑完JOB,如果有错误敬请指正

水平有限,本以为小bug,结果排查了很久。这个工作还是很有意思的,目前看只能大厂来做真正把AI4DB的东西放到db中,工作量太大了还不好发paper...

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reproducing the JOB benchmark, then inspect HASH_TABLE_SIZE in src/backend/pilotscope/parse_json.c and create_entry in src/backend/pilotscope/utils/hashtable.c. Compare the geqo paths through src/backend/optimizer/path/allpaths.c and src/backend/optimizer/geqo/geqo_eval.c, then inspect store_aimodel_subquery2card() in src/backend/pilotscope/anchor2struct.c. Done means JOB completes reliably with both geqo settings without lost entries or hash-table failures.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, postgresql
Domain
databases
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.