tidb

· 2341 words · 5 minute read

整体架构 🔗

  二层(1 sql层无状态,处理用户请求和sql运算逻辑,2 kv层存储kv)。
  三个组件(tidb-server, tikv-server, pd-server)


tidb: 支持mysql协议,以支持事务的分布式kv存储引擎为底层存储的数据库。

存储格式 🔗

  存储的数据持久化到磁盘上。table表如何映射为kv存储?
  主键的kv结构:key=tableID_primaryID, value=[col1, col2...]
  二级索引: key=tableID_索引ID_primaryID, value=null

行数据的kv格式
key: t{tableID}_r{rowID}
val:[col1, col2, col3]

unique index的kv格式
key: t{tableID}_i{indexID}_columnsValue
value: rowID

non-unique index
key: t{tableID}_i{indexID}_columnsValue_rowID
value: null

row和index的key具有相同的prefix,一个table内的数据在tikv的key空间内是排列在一起的。

窥探一二 🔗

有一些模块是make it work, 有一些模块是make it fast。

模块 🔗

tidb-server的入口tidb-server/main.go, tidb-server中分三层:
mysql协议解析和转换为ast,protocol layer: server包负责,
sql layer: Sesssion(), RecordSet(), Plan(), Logical Plan, Physical Plan, Executor, distsql, store/tikv client
kv api layer: Mock-Tikv

模块 功能 其他
tidb-server 服务的入口main
server mysql协议,session管理逻辑 , protocol layer
plan 查询优化相关的逻辑
expression 表达式相关的逻辑
executor sql语句的执行器
distsql 把executor和tikv client做隔离
store/tikv sql层与存储引擎的交互
ddl ddl的执行逻辑
tablecodec sql到kv的编解码
kv 规定了kv引擎的接口,tikv实现定义的接口 kv api layer

server/conn.go 大循环中不断读取数据包
server/conn.go#Dispatch函数处理sql文本
处理sql文本的函数handleQuery收到sql文本

session/session.go#ExecuteStmt 拿到ast。visitor 模式进行遍历.

compiler.Compile对sql的ast做验证,优化动作的入口

生成执行器executor/adaptor.go#Exec()..buildExcecutor(): plan -> executor。

select a from t where c1 > 1;

执行器 projection(a) -> filter(c1 > 1) -> table scan(t)。 获取数据时rs.Next()方法链式调用到底层,当前层的处理依赖于下层返回的数据。

server/conn.go#writeResultset写回client

client <-->sql处理 <--> 生成执行计划 <---> 生成执行器
执行器的执行是:在需要给客户端返回数据的地方调Next()方法获取结果数据。

tidb sql执行过程 🔗


主要的包:parser, planner, executor

虽然架构上分计算层和存储层,为了避免大量的rpc,需要将计算尽量靠近存储节点。

创建计划 🔗

planbuilder.go#Build 根据ast.Node节点的类型来创建计划, 例如Insert, LogicalJoin, DataSource

逻辑优化 🔗

利用关系代数的变换规则进行查询语句表达式的等价变换
逻辑优化是基于预先定义的规则列表logicalOptRule pass定义处理

逻辑算子:

  • DataSource 负责数据源(可能包含下推的条件谓词)
  • Selection 选择。 where id = 5 中的where过滤条件
  • Projection 投影。 select c, a+b 中取c列的操作是投影, 也有表达式计算
  • Join 连接。 from t1, t2 where t1.c = t2.d 把t1 和 t2内连接,还有很多其他join方式
  • Sort。 select * from t order by c 中的order by
  • Aggregation 聚合操作。 (select sum(d) from t group by e)中的group by, sum。
  • Apply。 子查询

columnPruner 列裁剪(从上往下一路加column) plan/column_pruning.go

最大最小消除将max/min转为order by id + limit 1。 投影消除

PredicatePushDown谓词下推: 把过滤条件推到DataSource算子上,在leaf node中执行减少数据访问。

cop[tikv] root[tidb] 所有的 Join 操作都只能作为 root task 在 TiDB 上执行。

物理优化 🔗

物理优化是基于代价的方式。

dagPhysicalOptimize

tidb plan & executor 🔗

select name from person where age = 18

logical plan:

  • scan table person
  • selection age = 18
  • projection name

逻辑计划 对应多种物理计划
逻辑计划 -> 物理计划(具体的操作算子)
例如: 逻辑算子datasource (物理算子IndexReader, TableReader, IndexLookUpReader)

Join (HashJoin, SortMergeJoin, Index Lookup Join)

  • HashJoin: 估计出小表,小表生成hash表,大表执行constains key 匹配出结果
  • Index Lookup Join: outer表的数据与inner表的index key范围
  • Sort Merge Join: 通常在连接的列上有索引时可以考虑。按连接的列值进行排序,进行归并得到结果

执行计划的逻辑优化(基于规则的优化RBO),例如列裁剪,投影消除, 谓词下推(先筛选,再做数据连接)
执行计划的物理优化 (基于代价的优化CBO), 等价转换,

executor 物理计划执行过程 🔗

//session/session.go
func (s *session) ExecuteStmt(ctx context.Context, stmtNode ast.StmtNode) (sqlexec.RecordSet, error) {
    ...
    compiler := executor.Compiler{Ctx: s}
    // 生成查询计划以及优化
    stmt, err := compiler.Compile(ctx, stmtNode)
    ...
    logStmt(stmt, s)
    // 去tikv上获取数据
    recordSet, err := runStmt(ctx, s, stmt)
    ...
    return recordSet, nil
}

volcano火山模型。 迭代器模型。
每个物理算子均实现Open, Next, Close方法

func (p Projection) Next() {
    row := p.children.Next()
    return row.name
}
func (s Selection) Next() {
    row := s.children.Next()
    if row != nil && row.age = 18 {
        return row
    }
    return nil
}
func (s Scan) Next() {
    return person.read()
}

tikv-client 🔗

todo tidb-server里模拟tikv中的计算逻辑mock-tikv


tikv 🔗

分布式 事务 k-v pair

单机存储靠rocksdb来存储到磁盘上。分布式数据复制靠raft。

row -> region -> store -> database

分层 功能
kvservice / coprocessor api kv 请求, sql计算
txn mvcc, 2pc
shading + replica(multi raft) 日志复制,分布式一致性
storage engine rocksdb 单机存储
os file system 负责磁盘存储

并发控制,mvcc。 🔗

Timestamp Oracle(TSO) 递增时间戳作为版本号。
prewrite时的冲突检查: 1、检查column family lock上key是否被lock了, 2、在cf write上key的版本号是否小于当前事物持有的TSO, 否则rollback
prewrite时选择多个key中的一个作为primary key, Lock列加锁;其他的作为secondry key,Lock列加锁,外加存储primary key(异步提交失败后,下次查询会查询primary key若提交后会补充提交)。

rocksdb 读写路径 🔗

write: memtable(skiplistt) --> immutable table --> String store table (SST)
read: the same path

对 DB 感兴趣可以看看 https://github.com/joaoh82/rust_sqlite