ð批处理的嵌套循环,以将读取请求减少到分布式存储
#sql #database #yugabytedb #distributed

当查询的高选择性(从大型内表中获得几行)是由于联接条件引起的,嵌套环连接方法很棒。但是,由于从外表中有许多行,性能降低:嵌套环是唯一一个内部表多次访问的联接方法,每个外排一次。在分布式SQL 数据库中,每个访问都可以是一个远程调用,涉及网络延迟

yugabytedb 用新的联接方法解决了问题:批处理的嵌套环。这个想法与我在以前的two statementsgenerating them with jOOQ的一些帖子中所揭露的技术相同:从外表中读取多个行,并使用 访问连接条件的价值数组 。 Yugabytedb的存储层在一个呼叫中读取许多点或范围方面非常有效。

使用数组您要控制大小,这就是为什么您通过设置批量大小来启用这种新的加入方法。

yb_bnl_batch_size

该功能已添加到Yugabytedb 2.15中,并且仍在2.17中的Beta中,通过将yb_bnl_batch_size设置为高于默认值1
,启用了批处理的嵌套循环。

yugabyte=# create table demo (id int primary key);
CREATE TABLE

yugabyte=# show yb_bnl_batch_size;
 yb_bnl_batch_size
-------------------
 1
(1 row)

yugabyte=# explain (costs off) /*+ nestloop(a b) */ 
           select * from demo a join demo b using(id);

                 QUERY PLAN
--------------------------------------------
 Nested Loop
   ->  Seq Scan on demo a
   ->  Index Scan using demo_pkey on demo b
         Index Cond: (id = a.id)
(4 rows)

yugabyte=# set yb_bnl_batch_size=5;
SET

yugabyte=# explain (costs off) /*+ nestloop(a b) */ 
           select * from demo a join demo b using(id);

                          QUERY PLAN
--------------------------------------------------------------
 YB Batched Nested Loop Join
   Join Filter: (a.id = b.id)
   ->  Seq Scan on demo a
   ->  Index Scan using demo_pkey on demo b
         Index Cond: (id = ANY (ARRAY[a.id, $1, $2, $3, $4]))
(5 rows)

计划节点从Nested Loop更改为YB Batched Nested Loop JoinIndex Cond显示了将yb_bnl_batch_size设置为5时的5个值的数组。它们显示为参数,但第一个显示了外列。

我在这里设置了5个值,但您可能想要更大的批量尺寸。哪个值是最好的?与往常一样……这取决于。

与许多参数通过网络调用来控制提取大小的许多参数,值,行数,100至1000之间的数量可能很好。这是网络延迟与可接受的缓冲区大小之间的良好折衷。更高的收益不会带来明显的收益,并且某些副作用可能比收益更重要。一旦该值足够大,可以将网络延迟减少到响应时间的一小部分,这并不重要。

有另一个原因是更高是没有用的原因:目标是减少远程呼叫,但它们本身受到一个呼叫中的操作数量的限制:--ysql_request_limit在yugabytedb中默认为1024。

yb_bnl_batch_size和ysql_request_limit

为了验证这一点,我运行了一个简单的查询,将两个与2000行的表连接在一起。具有ysql_request_limit的各种值(设置为群集级别)和yb_bnl_batch_size(在会话级别设置,或使用Set()提示查询级别)。

如果您想复制它,这是我在笔记本电脑上使用Docker的方式:

for p in 1024 ; do
for r in 100 1024 2000 ; do
for b in 1 10 100 500 1023 1024 1025 2000; do
docker run --rm yugabytedb/yugabyte:latest bash -c '
yugabyted start --listen 0.0.0.0 --tserver_flags="'ysql_prefetch_limit=$p,ysql_request_limit=$r'"
cat /root/var/logs/{master,tserver}.err
until postgres/bin/pg_isready ; do sleep 0.1 ; done | uniq &&
curl -s $(hostname):9000/varz?raw | grep -E "ysql_.*_limit"
ysqlsh -e <<SQL
create table demo ( id bigint primary key, x int) split into 10 tablets;
insert into demo select generate_series(1,2000), 0 ;
set yb_enable_expression_pushdown to on;
explain (analyze, dist, buffers, costs off)
/*+ nestloop(a b) leading((a b)) Set(yb_bnl_batch_size '$b') */
with a as (select * from demo)
select * from a join demo b using(id) where b.x=0;
SQL
'
done
done 
done | tee log.txt
grep -E -- "ysql_prefetch_limit|ysql_request_limit|yb_bnl_batch_size|Index Scan .* loops|Index Read Requests" log.txt

awk '
/^INSERT / {t=gensub(re,"\\1","1")}
/ysql_prefetch_limit/ {p=gensub(re,"\\1","1")}
/ysql_request_limit/  {r=gensub(re,"\\1","1")}
/yb_bnl_batch_size/   {b=gensub(re,"\\1","1")}
/Index Scan .* loops/ {l=gensub(re,"\\1","1")}
/Index Read Requests/ {i=gensub(re,"\\1","1")}
/Table Read Requests/ {t=gensub(re,"\\1","1")}
/^ *Execution Time/{
printf "%5d read requests=%5d loops=%5d yb_bnl_batch_size=%5d %6.2fms ysql_request_limit=%5d ysql_prefetch_limit=%5d\n",l*i,i,l,b,$(NF-1),r,p
}
' re="^.*[= ]([0-9.]+)[^0-9]*$" log.txt | sort -n

结果每次测试显示一行,从设置或用awk解析的执行计划(explain (analyze, dist))中收集值。

read requestsloops的值来自执行计划,必须乘以一起乘以在所有循环中访问内表的总读取请求。这就是我在第一列中放置的结果:

    1 read requests=    1 loops=    1 yb_bnl_batch_size= 2000 495.11ms ysql_request_limit= 2000 ysql_prefetch_limit= 1024
    2 read requests=    1 loops=    2 yb_bnl_batch_size= 1024 207.83ms ysql_request_limit= 1024 ysql_prefetch_limit= 1024
    2 read requests=    1 loops=    2 yb_bnl_batch_size= 1024 274.37ms ysql_request_limit= 2000 ysql_prefetch_limit= 1024
    2 read requests=    2 loops=    1 yb_bnl_batch_size= 2000 259.81ms ysql_request_limit= 1024 ysql_prefetch_limit= 1024
   20 read requests=    1 loops=   20 yb_bnl_batch_size=  100  58.44ms ysql_request_limit= 2000 ysql_prefetch_limit= 1024
   20 read requests=    1 loops=   20 yb_bnl_batch_size=  100  60.40ms ysql_request_limit= 1024 ysql_prefetch_limit= 1024
   20 read requests=    1 loops=   20 yb_bnl_batch_size=  100  64.33ms ysql_request_limit=  100 ysql_prefetch_limit= 1024
   20 read requests=   10 loops=    2 yb_bnl_batch_size= 1024  61.67ms ysql_request_limit=  100 ysql_prefetch_limit= 1024
   20 read requests=   20 loops=    1 yb_bnl_batch_size= 2000  79.35ms ysql_request_limit=  100 ysql_prefetch_limit= 1024
  200 read requests=    1 loops=  200 yb_bnl_batch_size=   10  83.23ms ysql_request_limit=  100 ysql_prefetch_limit= 1024
  200 read requests=    1 loops=  200 yb_bnl_batch_size=   10  94.62ms ysql_request_limit= 1024 ysql_prefetch_limit= 1024
  200 read requests=    1 loops=  200 yb_bnl_batch_size=   10 120.66ms ysql_request_limit= 2000 ysql_prefetch_limit= 1024
 2000 read requests=    1 loops= 2000 yb_bnl_batch_size=    1 572.09ms ysql_request_limit=  100 ysql_prefetch_limit= 1024
 2000 read requests=    1 loops= 2000 yb_bnl_batch_size=    1 645.31ms ysql_request_limit= 2000 ysql_prefetch_limit= 1024
 2000 read requests=    1 loops= 2000 yb_bnl_batch_size=    1 812.39ms ysql_request_limit= 1024 ysql_prefetch_limit= 1024

loops的数量仅取决于行的外部数量和yb_bnl_batch_size:2000,当没有批处理(批次大小为1),200个,批次大小为10,20,批次大小为100,2个循环,带有批处理仅当所有2000行适合一批时,大小为1000,而1个循环。这是简单的数学:外排的数量除以批量大小。

我想验证的事情是读取请求如何遵循循环数量。当yb_bnl_batch_sizeysql_request_limit小时,它们是相等的。但是,在上面时,由于请求限制,一个循环分为多个读取调用。这就是为什么2000的批次尺寸显示了两个2读取1循环的请求,默认的ysql_request_limit of 1024。

大型yb_bnl_batch_size减少读取请求的有效性受到可以在一个请求中发送的读取操作的数量的限制:ysql_request_limit和数字是否可以在一个请求中获取的行:ysql_prefetch_limit。两者都默认为1024。

有关ysql_request_limit的更多信息

yugabytedb reuse sql层的postgresql(称为ysql)。这是无状态的,称分布式交易和存储层DOCDB读取和写入数据。 DOCDB是一个交易键值数据库,其中SQL行和索引ENTRES存储到文档中(以及单个列更改的子登记)。允许水平可伸缩性,即使在遥远区域中,DOCDB节点也可以在不同的数据中心中。为了保持高性能,将读/写请求尽可能分组。这可以在两个级别上完成:pggate中的请求和ybclient中的RPC。

我们在一个Index Cond中看到的IN (ARRAY[...])实际上是在此处转换为多个读取请求。您可以将其视为带有yb_debug_log_docdb_requestsApplying operation。这是ysql_request_limit适用的地方。请注意,在此示例中,我创建了带有10片平板电脑的演示表,并使用哈希碎片。每个值仍有一个读取操作(直到实现#7836)。随着范围碎片,它们被批处理,可见为Buffering operation,带有yb_debug_log_docdb_requests

批处理与延迟的成本

我显示了经过的时间。批处理的嵌套环总是比每个外行一个循环快。但是,该查询速度更快,批量大小为100而不是1000,即使涉及10倍的读取请求。这是因为我正在将其运行在单个节点上,其中读取请求的延迟较低。在这种特殊情况下,批处理的小开销很重要。

我在节点之间的单一区域群集上进行了类似的测试:

Image description

它与最佳响应时间相同,批次大小为100:

    2 read requests=    1 loops=    2 yb_bnl_batch_size= 1000     208.24ms
    3 read requests=    1 loops=    3 yb_bnl_batch_size=  800     162.11ms
    4 read requests=    1 loops=    4 yb_bnl_batch_size=  600     134.91ms
    4 read requests=    2 loops=    2 yb_bnl_batch_size= 1200     216.53ms
    5 read requests=    1 loops=    5 yb_bnl_batch_size=  400      93.12ms
   10 read requests=    1 loops=   10 yb_bnl_batch_size=  200      71.44ms
   20 read requests=    1 loops=   20 yb_bnl_batch_size=  100      61.44ms
  200 read requests=    1 loops=  200 yb_bnl_batch_size=   10     164.11ms
 2000 read requests=    1 loops= 2000 yb_bnl_batch_size=    1    1175.69ms

然后,我在多区域群集中使用两个节点的90毫秒:

进行了相同的操作。

Image description

当然,这对于加入并不理想,您应该考虑进行地理分区。但是我这样做是为了说明请求的数量比批处理的小开销更重要:

    2 read requests=    1 loops=    2 yb_bnl_batch_size= 1000    1474.44ms
    3 read requests=    1 loops=    3 yb_bnl_batch_size=  800    1828.48ms
    4 read requests=    1 loops=    4 yb_bnl_batch_size=  600    1861.53ms
    4 read requests=    2 loops=    2 yb_bnl_batch_size= 1200    1551.61ms
    5 read requests=    1 loops=    5 yb_bnl_batch_size=  400    2075.12ms
   10 read requests=    1 loops=   10 yb_bnl_batch_size=  200    2485.99ms
   20 read requests=    1 loops=   20 yb_bnl_batch_size=  100    3879.42ms
  200 read requests=    1 loops=  200 yb_bnl_batch_size=   10   13376.93ms
 2000 read requests=    1 loops= 2000 yb_bnl_batch_size=    1  126277.28ms

在这种情况下,拥有较大的批量尺寸,例如1024,就像请求限制一样。响应时间仍然很长:1.5秒即可加入2000行,另一个联接方法(哈希或合并)可能会更好。但是,伟大的事情是,由于批处理时间,响应时间仍然可以接受。 PostgreSQL非批次嵌套环在这里需要2分钟,而Yugabytedb批处理的循环却降至2秒。

说明了yugabytedb如何重新使用PostgreSQL:从中获取所有功能,而不是从头开始开发新的数据库,而是优化了在(Geo-)分布式时提供高性能必须提供的高性能的方法。

总而言之,在大多数情况下,将yb_bnl_batch_size从128设置为1024可能是很好的。当您在高潜伏期之间加入时,1024可能是最好的。在AZS之间具有低延迟性的单个区域中,128可能会更好。或将其设置为512,以进行良好的通用默认值。

此功能仍在预览中,并且很快就会出现GA,当查询计划者估计其成本(以避免像我上文一样暗示)。您的反馈将有助于改善甚至可以在将来设定更好的违约。注意:结果取决于分片方法,平板电脑的数量,行数以及请求和预取限制。这里的数字是针对一种特定情况,但您可以通过变化来复制它。