{
    "componentChunkName": "component---src-templates-blog-blog-detail-tsx",
    "path": "/blog/tidb-api-union-scan",
    "result": {"pageContext":{"blog":{"id":"Blogs_232","title":"TiDB 下推 API 实现细节 - Union Scan","tags":["TiDB","分布式计算"],"category":{"name":"产品技术解读"},"summary":"TiDB 集群的架构分为上层的 SQL 层和底层的 KV 层，SQL 层通过调用 KV 层的 API 读写数据，由于 SQL 层的节点和 KV 层节点通常不在一台机器上，所以，每次调用 KV 的 API 都是一次 RPC, 而往往一个普通的 Select 语句的执行，需要调用几十到几十万次 KV 的接口，这样的结果就是性能非常差，绝大部分时间都消耗在 RPC 上。为了解决这个问题，TiDB 实现了下推 API，把一部分简单的 SQL 层的执行逻辑下推到 KV 层执行，让 KV 层可以理解 Table 和 Column，可以批量读取多行结果，可以用 Where 里的 Expression 对结果进行过滤, 可以计算聚合函数，大幅减少了 RPC 次数和数据的传输量。","body":"TiDB 集群的架构分为上层的 SQL 层和底层的 KV 层，SQL 层通过调用 KV 层的 API 读写数据，由于 SQL 层的节点和 KV 层节点通常不在一台机器上，所以，每次调用 KV 的 API 都是一次 RPC, 而往往一个普通的 `Select` 语句的执行，需要调用几十到几十万次 KV 的接口，这样的结果就是性能非常差，绝大部分时间都消耗在 RPC 上。\n\n为了解决这个问题，TiDB 实现了下推 API，把一部分简单的 SQL 层的执行逻辑下推到 KV 层执行，让 KV 层可以理解 Table 和 Column，可以批量读取多行结果，可以用 `Where` 里的 Expression 对结果进行过滤, 可以计算聚合函数，大幅减少了 RPC 次数和数据的传输量。\n\nTiDB 的下推 API 通过把 SQL 层的计算下推到 KV 层，大幅减少 RPC 次数和数据传输量，使性能得到数量级的提升。但是当我们一开始启用下推 API 的时候，发现了一个问题，就是当事务写入了数据，但是还未提交的时候，又执行了 `Select` 操作。\n\n这个时候，刚刚写入的未提交的脏数据读不到，得到的结果是错误的，比如我们在一个空表 t 执行：\n\n``` sql\nbegin;\ninsert t values (1);\nselect * from t;\n```\n\n这时我们期待的结果是一条记录 “1”，但是启用下推 API 后得到的结果是空。\n\n导致这个问题的原因是我们的事务在提交之前，写入的数据是 buffer 在 SQL 层，并没有写入 KV, 而下推 API 直接从 KV 读取数据，得到的结果直接返回，所以得到了空的结果。\n\n但是既然 KV 层读取不到未提交的脏数据，那在启用下推 API 之前，是如何得到正确结果的呢？\n\n这就涉及到 SQL 层的 Buffer 实现。当初为了解决未提交事务的 Buffer 可见性问题，SQL 层实现了一个 UnionStore 的结构，UnionStore 对 Buffer 和 KV 层接口做了一个封装，事务对 KV 的读写都经过 UnionStore，当 UnionStore 遇到读请求时，会先在 Buffer 里找，Buffer 找不到时，才会调用 KV 层的接口，读取 KV 层的数据。所以相当于把 Buffer 和 KV 的数据做了一个 Merge，返回 Merge 后的正确结果。Buffer 的数据是用 goleveldb 的 MemDB 存储的，所以是有序的，当需要遍历数据的时候，UnionStore 会同时创建 Buffer 的 Iterator 和 KV 的 Iterator，遍历的算法类似 LevelDB，把两个 Iterator merge 成一个。\n\nUnionStore 的实现是基于 Key Value 的，但是下推 API 返回的结果是基于 Row 的，也就是说，我们虽然有脏数据 Buffer 和下推 API 返回的结果集, 但是我们没有办法把这两部分数据合并在一起, 所以我们为了绕过这个问题，加了一个判断条件，当事务写入了 Buffer，包含了脏数据以后，就不走下推 API，而是使用基础的 KV API。\n\n在我们刚刚开始启用下推 API 的时候，因为性能基准比较低，而且带脏数据的下推请求只占很小的一部分，所以我选择暂时绕过这个问题。\n\n但是当全面启用下推 API 以后，整体性能已经大幅提升，这时带脏数据的请求无法走下推 API 这个 worst case 问题就渐渐凸显出来。\n\n比如说，我们如果需要在一个事务里 `UPDATE` 多个行，就一定会遇到下推 API 无法使用，降级到基础 KV API 的问题。\n\n假设我们创建一个表，插入了两行数据：\n\n```sql\ncreate table t (c int);\ninsert t values (1), (4);\n```\n\n这时我们执行这样一个事务：\n\n```sql\nbegin;\nupdate t set c = 2 where c = 1;\nupdate t set c = 3 where c = 4;\n```\n\n`UPDATE` 语句执行的过程分两步，第一步是先读取到需要更新的数据，第二步把更新的数据写入 Buffer。\n\n也就是 `UPDATE` 包含了一次 `SELECT` 请求。当第一个 `UPDATE` 语句执行的时候，因为没有脏数据，所以读请求会走下推 API，但是第一个 `UPDATE` 语句执行完后，事务就有了脏数据，再执行第二个 `UPDATE` 的时候，无法使用下推 API, 会导致性能大幅下降。\n\n解决这个问题的方案，最容易想到的是在 KV 层实现 UnionStore 相同的算法，当发送下推 API 请求时，把 Buffer 一并传下去。\n\n但是这个方案的缺点也很明显，就是计算和存储不在同一节点，不符合就近计算原则。脏数据是在 SQL 层生成并存储的，本来应该在 SQL 进行 Merge，但是却要传输到 KV 层去 Merge，如果 Buffer 的数据很多，传输 Buffer 带来的开销就会很大。\n\n最终我们设计实现了一个更好的方案 Union Scan，在不需要把 Buffer 传输到 KV 层，不修改 KV 层的情况下，解决了脏数据的可见性问题。\n\n### 下面是这个算法的简介\n\n脏数据缓存在 SQL 层，要让它可见，一定是需要 Merge 的，当我们使用下推 API, 只拿到了一堆 Row，这时怎么 Merge 呢？\n\n如果我们不做 Merge，直接返回给用户结果集，错误表现的就是少了某些 row，多了某些 row，或某些 row 的数据是旧的。\n\n如果我们把 `INSERT`, `UPDATE`, `DELETE` 的修改操作，以 row 为单位记录下来，这样和下推 API 返回的结果就是同样的形式了，就可以很方便的做 Merge 的计算了。\n\n所以 Union Scan 的算法就是以 Row 为单位，把事务的修改操作保存起来，最终和下推 API 返回的结果集进行 Merge，返回给客户端。\n\n我们为每个事务在对某个 table 执行写操作时，创建一个 dirtyTable 保存这个事务的修改，dirtyTable 包含两个 map，一个是 addedRows，用来保存新写入的 row，另一个是 removedRows，用来保存删除的 row，对于不同的操作，我们需要对这两个 map 做不同的操作。\n\n对于 `INSERT`，我们需要把 row 添加到 addedRows 里。\n\n对于 `DELETE`，我们需要把 row 从 addedRows 里删掉，然后把 row 添加到 removedRows 里。\n\n对于 `UPDATE`，相当于先执行 `DELETE`, 再执行 `INSERT。`\n\n当我们从下推 API 得到了结果集之后，我们下面把它叫做快照结果集，Merge 的算法如下：\n\n对于每一条快照结果集里的 Row，在 removedRows 里查找，如果有，那么代表这一条结果已经被删掉，那么把它从结果集里删掉，得到过滤后的结果集。\n\n把 addedRows 里的所有 Row，放到一个 slice 里，并对这个 slice 用快照结果集相同的顺序排序，生成脏数据结果集。\n\n返回结果的时候，将过滤后的快照结果集与脏数据结果集进行 Merge。\n\n实现了 Union Scan 以后，所有的读请求都可以使用下推 API 加速，大幅提升了 worst case 的性能。","date":"2016-06-18","author":"周昱行","fillInMethod":"writeDirectly","customUrl":"tidb-api-union-scan","file":{"name":"blank.md","ext":".md","localFile":{"internal":{"content":"---\n---"}}},"relatedBlogs":[]}}},
    "staticQueryHashes": ["1327623483","1820662718","3081853212","3430003955","3649515864","4265596160","63159454"]}