MyCat : 主流程学习与源码入门

1,459 阅读6分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一 .前言

这一篇开始深入学习 Mycat 的相关源码 , 以及从源码中试图分析出可定制点以及扩展的方式.

从 Git 上可以拉取对应的源码 Mycat-Server , 其核心启动类为 MycatStartup , Mycat 是国内开源 , 相对而言注释很清晰 , 这一篇主要是代码搬运和学习

Mycat 项目结构

|-- main
|   |-- java
|   |   `-- io
|   |       `-- mycat
|   |           |-- MycatServer.java
|   |           |-- MycatShutdown.java
|   |           |-- MycatStartup.java : 启动类 , 会构建 MycatServer 进行启动
|   |           |-- backend : 结果返回等处理
|   |           |-- buffer : NIO Buffer 实现 , 仿 Netty
|   |           |-- cache 
|   |           |-- catlets : Join 语句等解析
|   |           |-- config 
|   |           |-- manager
|   |           |-- memory
|   |           |-- migrate : 自动扩容等
|   |           |-- net : NIO 连接 , 数据库连接相关类
|   |           |-- route : Route 规则处理
|   |           |-- server : TODO
|   |           |-- sqlengine : SQL 执行器
|   |           |-- statistic
|   |           `-- util
|   `-- resources
|       |-- cacheservice.properties
|       |-- ehcache.xml
|       |-- log4j2.xml
|       |-- rule.xml : 分片规则
|       |-- schema.xml : 节点规则
|       |-- server.xml : Mysql 数据信息
|       |-- zkconf : Zookeeper 集群配置
|       |   |-- rule.xml
|       |   |-- schema.xml
|       |   |-- server.xml
|       `-- zkdownload
|           `-- auto-sharding-long.txt


二. 源码启动和格局

Mycat 的启动类是 , 其会调用 MycatServer 进行初始化操作

public final class MycatStartup {

    public static void main(String[] args) {
        
        // S1 : 如果通过 Zookeeper 进行配置 ,此处会进行初始化
        ZkConfig.getInstance().initZk();
        
        // S2 : 获取 Home 路径
        String home = SystemConfig.getHomePath();
        
        // S3 : 获取 MycatServer 实例对象
        MycatServer server = MycatServer.getInstance();

        // S4 : 启动
        server.startup();
    }
}

2.1 核心启动类

private MycatServer() {

    //读取文件配置
    this.config = new MycatConfig();

    //定时线程池,单线程线程池
    scheduler = Executors.newSingleThreadScheduledExecutor();

    //心跳调度独立出来,避免被其他任务影响
    heartbeatScheduler = Executors.newSingleThreadScheduledExecutor();

    //SQL记录器
    this.sqlRecorder = new SQLRecorder(config.getSystem().getSqlRecordCount());

    /**
     * 是否在线,MyCat manager中有命令控制
     * | offline | Change MyCat status to OFF |
     * | online | Change MyCat status to ON |
     */
    this.isOnline = new AtomicBoolean(true);

    //缓存服务初始化
    cacheService = new CacheService();

    //路由计算初始化
    routerService = new RouteService(cacheService);

    // load datanode active index from properties
    dnIndexProperties = loadDnIndexProps();
    try {
        //SQL解析器
        sqlInterceptor = (SQLInterceptor) Class.forName(
                config.getSystem().getSqlInterceptor()).newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    //catlet加载器
    catletClassLoader = new DynaClassLoader(SystemConfig.getHomePath()
            + File.separator + "catlet", config.getSystem().getCatletClassCheckSeconds());

    //记录启动时间
    this.startupTime = TimeUtil.currentTimeMillis();
    if (isUseZkSwitch()) {
        String path = ZKUtils.getZKBasePath() + "lock/dnindex.lock";
        dnindexLock = new InterProcessMutex(ZKUtils.getConnection(), path);

    }

}

三. 连接流程一览

如果初期对整个源码流程不太了解 , 最简单的深入方式就是读 log , log 会为我们展现整个流程的方方面面.

3.1 获取配置

配置的加载通过 XMLSchemaLoader 进行扫描 ,通过 ConfigInitializer 进行初始化处理 , 整个配置的整体获取流程如下 :

// S1 : 构建 MycatServer 时构建 MycatConfig
- MycatServer.getInstance()
    |- this.config = new MycatConfig();

// S2 : 构建 ConfigInitializer 对象
public MycatConfig() {
    //读取schema.xml,rule.xml和server.xml
    ConfigInitializer confInit = new ConfigInitializer(true);
    //....
}    

// S3 : ConfigInitializer 初始化流程
3-1 > new XMLSchemaLoader() : 初始化 Schema.xml 加载器 , 读取rule.xml和schema.xml
3-2 > new XMLConfigLoader(schemaLoader) : 读取 Server.xml 文件
3-3 > initDataHosts : 加载 dataHosts
3-4 > initDataNodes : 加载 dataNodes
3-5 > getFirewallConfig + initCobarCluster : 加载权限配置


// -------------------

// 补充 : Schema.xml 的加载 -> XMLSchemaLoader
- 准备  new XMLRuleLoader(ruleFile) , 如果未配置会使用默认配置
    |- private final static String DEFAULT_XML = "/rule.xml";
    |- private final static String DEFAULT_XML = "/schema.xml";
- getResourceAsStream 加载流和解析为 HashMap
    |- private final Map<String, TableRuleConfig> tableRules;
    |- private final Map<String, AbstractPartitionAlgorithm> functions;
    
    
// 补充 : Server 的加载 -> XMLConfigLoader
C- XMLServerLoader # load
    |- XMLServerLoader.class.getResourceAsStream("/server.xml") : 获取 Resource 流
    |- ConfigUtil.getDocument : 获取 Document
    |- loadSystem : 加载System标签
    |- loadUsers : 加载User标签
    |- ClusterConfig : 加载集群配置
    |- loadFirewall : 加载防火墙

3.2 建立连接

此处主要是Mycat与物理库的连接方式 , Mycat Connect 的创建起点是 PhysicalDatasource # createNewConnection , 这里会使用 ThreadPoolExecutor 进行创建 MySQLConnection , 整个连接的整体创建流程如下 :

// S1 : server.startup() 启动时对应用进行加载
- startup 中做了很多事情 , 包括初始化工厂类 , 初始化连接池 , 初始化 AIO 处理器等等
- 此环节仅关注 Mysql Connection 的连接

<---------------------------------->

// S2 : 初始化 datahost 
Map<String, PhysicalDBPool> dataHosts = config.getDataHosts()
for (PhysicalDBPool node : dataHosts.values()) {
    // 构建数据库对应映射索引
    String index = dnIndexProperties.getProperty(node.getHostName(), "0");
    // 初始化节点
    node.init(Integer.parseInt(index));
    // 建立心跳关联
    node.startHeartbeat()
}

<---------------------------------->

// S3 : PhysicalDBPool 对 Source 进行初始化 , 此环节会初始化连接池
- ds.getConnection(this.schemas[i % schemas.length], true, getConHandler, null)

<---------------------------------->

// S4 : PhysicalDatasource # getConnection : 获取连接
- increamentCount.longValue()+totalConnectionCount : 获取最大连接数
- createNewConnection(handler, attachment, schema) : 如果连接数小于最大连接数 ,创建连接

<---------------------------------->

// S5 : 通过线程池创建连接
MycatServer.getInstance().getBusinessExecutor().execute(....)

<---------------------------------->

// S6 : 连接池创建连接 -> MySQLConnectionFactory
- MySQLConnectionFactory -> make -> new MySQLConnection -> create Socket

<---------------------------------->

// 补充 : InetSocketAddress
如果是异步处理 , 会调用 AsynchronousSocketChannel 进行 InetSocketAddress 的处理

image.png

四. 查询流程一览

上文看了 Mycat 与 物理库之间的关联关系 , 下面来看一下整个查询过程中的整体关联

4.1 Navicat 连接

Mycat 与外部的连接通过 NIOReactor 进行处理 , NIO 真的在各大框架中有非常广泛的应用 , 这里先不关注具体的实现方式

// S1 : MycatServer # startup 中对 NIO 进行初始化
- NIOReactorPool reactorPool = new NIOReactorPool
- connector = new NIOConnector
- ((NIOConnector) connector).start()

1-1 : 建立 Manager 连接 -> 9066
manager = new NIOAcceptor(DirectByteBufferPool.LOCAL_BUF_THREAD_PREX + NAME
                    + "Manager", system.getBindIp(), system.getManagerPort(), mf, reactorPool);
                    
1-2 : 建立 Server 连接 -> 8066 
server = new NIOAcceptor(DirectByteBufferPool.LOCAL_BUF_THREAD_PREX + NAME
                    + "Server", system.getBindIp(), system.getServerPort(), sf, reactorPool);


<---------------------------------->

// S2 : 通过 NIOReactorPool 对 NIOReactor 进行创建
for (int i = 0; i < poolSize; i++) {
    NIOReactor reactor = new NIOReactor(name + "-" + i);
    reactor.startup();
}

<---------------------------------->

// S3 : NIOReactor 中 死循环处理请求
public void run() {
    for (;;) {
        //.... 处理请求
    }

}

4.2 Mycat 查询流程

MySQL 查询流程主要集中在 MySQLConnection 的父类 AbstractConnection # onReadData 中 , 以一个 select 语句来看整个流程


// 整体查询流程
- NIOReactor : NIO 处理请求
- FrontendConnection # handle : connection 建立连接
- FrontendCommandHandler # handle : command 处理命令
- ServerQueryHandler # query : 处理 Query 命令
- ServerConnection # execute : 核心命令执行
- SingleNodeHandler # execute : 单节点处理
- MySQLConnection # MySQLConnection
- MySQLConnection # synAndDoExecute
- MySQLConnection # sendQueryCmd : 发送 SQL Query 命令

查询主流程


// S1 : NIOReactor 接收请求 , 异步读取
- con.asynRead()

// S2 : 中层数据转换和处理

<---------------------------------->

// S3 : FrontendCommandHandler 对 Command 进行分发处理
public void handle(byte[] data){
    // 取第四个 byte 做 Switch 判断
    switch (data[4]){
        // 主要基于 MySQLPacket 内的 final byte
        // 可以看到 , 通过对应的 Handler 进行处理
        - ServerParse.SELECT -> SelectHandler.handle
        - ServerParse.SHOW -> ShowHandler.handle
        - ServerParse.SET -> SetHandler.handle
        - ServerParse.START -> StartHandler.handle
        - ....
    }
}

// 以 Select 为例
<---------------------------------->

// S4 : ServerConnection 进行 Route 和 Excute
RouteResultset = MycatServer.getInstance().getRouterservice().route(...)
session.execute(rrs, rrs.isSelectForUpdate()?ServerParse.UPDATE:type) : Session 执行查询

<---------------------------------->

// S5 : MultiNodeQueryHandler 多节点处理
for (final RouteResultsetNode node : rrs.getNodes()){
    // For 循环进行处理
    PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
    dn.getConnection(dn.getDatabase(), autocommit, node, this, node);
}

<---------------------------------->

// S6 : PhysicalDBNode # getConnection 连接池获取连接
dbPool.getRWBanlanceCon(schema,autoCommit, handler, attachment, this.database);

<---------------------------------->

// S7 : PhysicalDatasource 获取 Source 资源
BackendConnection con = this.conMap.tryTakeCon(schema, autocommit) : 从 Map 中获取已经建立好的连接
takeCon(con, handler, attachment, schema) : 绑定对应前端请求的handler

<---------------------------------->

// S8 : MySQLConnection 进行处理
xaTXID = sc.getSession2().getXaTXID()+",'"+getSchema()+"'" : 构建事务 ID
synAndDoExecute(xaTXID, rrn, sc.getCharsetIndex(), sc.getTxIsolation(),autocommit, sc.isTxReadonly(), sc.getSqlSelectLimit()) : 异步执行

<---------------------------------->

// S9 : MySQLConnection # synAndDoExecute 异步执行
- 前置 : 整个过程中包括 AutoCommit , tx 的相关处理
- 处理 : 构建 CommandPacket 用于发送 Command
- 执行 : sendQueryCmd 发送命令


整合数据

上文对数据集进行了查询 , 下面对查询的结果进行聚合

// S1 : MySQLConnectionHandler 中进行 handle 处理

// S2 : MySQLConnectionHandler # handleData  进行多节点查询
- 对 resultStatus 进行分析 , 包括三个大类以及四种方式
private static final int RESULT_STATUS_INIT = 0;
private static final int RESULT_STATUS_HEADER = 1;
private static final int RESULT_STATUS_FIELD_EOF = 2;

OkPacket > byte FIELD_COUNT = 0x00;
RequestFilePacket > byte FIELD_COUNT = (byte) 0xff;
ErrorPacket > byte FIELD_COUNT = (byte) 251;
EOFPacket > byte FIELD_COUNT = (byte) 0xfe;

// S3 : MultiNodeQueryHandler # fieldEofResponse 获取返回体


// S4 : DataMergeService # addPack 进行数据合并
- server.getBusinessExecutor().execute(this) : 将自身作为Runnable放入线程池
- run : 线程池默认调用 run 命令执行线程
    - for 循环从 BlockingQueue 中弹出数据
    - result.get(pack.dataNode).add(row) : 讲 Result 加入 Map
    - Response 结果 : source.write(source.writeToBuffer(eof, buffer))

PS:整体过程比较复杂,初期就不花大量时间深入了 , 后续逐步的学习

image.png

总结

这一篇从 Mycat 的启动到一次完整的查询进行了学习和了解 , 在后续的文章中 , 会逐步的对其中的部分核心功能进行学习 , 同时会结合生产过程中出现的问题进行分析以及定制点的扩展

参考文档

<分布式数据库架构-基于 Mycat 中间件>