美文网首页
SMProxy分析

SMProxy分析

作者: nightfallLemon | 来源:发表于2019-09-25 22:59 被阅读0次

    前言:

        在深入了解SMProxy之前,一直认为连接池是对mysql连接对象进行统一管理的处理,但是随之而来的问题是现有的php框架都没有自带mysql连接池,如何以最小的代价替代框架的数据库模块一直是一个难题。
        在深入了解SMProxy之后,发现SMProxy的奇妙之处就在于你并不需要对框架的数据库模块进行任何的修改,即可使用SMProxy架构,它是基于mysql客户端与mysql服务端的中间件,通过swoole/server自己模拟与mysql报文交互并内部管理连接池对象来提升效率。
    

    优点:

    • 明显客观的性能提升,减少创建连接的资源消耗
    • 无需对框架进行任何的修改即可使用

    缺点:

    • 需要对mysql协议有一定的了解,如果希望改动,则需要对协议有更深入的理解。
    • 如果发生错误,增加了排查错误的成本。

    知识点的补充:

    • 协议中常用的数据, 10进制,16进制,2进制。
    • 为啥使用16进制表示字节中的内容, 在二进制中每4个位表示16进制中的一位, 二进制与16进制相互转化比十进制快的多 。
    • 为了协议中运用到的|,&运算更好理解一点, 我给予了一个简易的称呼(当然这可能并不准确)
      • | 运算符 = 取大值, 例如 2|8 = 8
      • & 运算符= 取小值, 例如 2&8 = 2

    swoole:
    运用到的知识点 swoole/server以及swoole/client, 不做更多的介绍
    tcp 粘包问题: https://www.cnblogs.com/JsonM/articles/9283037.html
    client -> tcp buffer(等待cpu指令, 如果buffer缓存达到上限,就会直接发送到server, 所有有可能一次性接受多个数据) -> server

    mysql 协议分析
    https://www.cnblogs.com/davygeek/p/5647175.html

    SMProxy:

    执行流程图

    image.png

    接下来将针对mysql与SMProxy的swoole服务交互进行一定的说明:(如果对以下报文有疑问,可以仔细翻看mysql协议https://www.cnblogs.com/davygeek/p/5647175.html)

    再进行tcp交互之中,需要服务端向mysql客户端发送握手初始化报文,只要发送符合mysql协议的握手报文,mysql客户端便会进行下一步发送认证请求的操作。

    // 位于SMProxy/src/Handler/Frontend/FrontendAuthticator
    public function getHandshakePacket(int $server_id)
    {
        $rand1 = RandomUtil::randomBytes(8);
        $rand2 = RandomUtil::randomBytes(12);
        $this->seed = array_merge($rand1, $rand2);
        $hs = new HandshakePacket();
        $hs->packetId = 0;
        // 以下根据握手报文
        // 协议版本号
        $hs->protocolVersion = Versions::PROTOCOL_VERSION;
        // 服务器版本号信息
        $hs->serverVersion   = Versions::SERVER_VERSION;
        // 服务器线程
        $hs->threadId = $server_id;
        // 随机数
        $hs->seed = $rand1;
        // 填充值,服务器权能标识,
        $hs->serverCapabilities = $this->getServerCapabilities();
        // 字符编码
        $hs->serverCharsetIndex = (CharsetUtil::getIndex(CONFIG['server']['charset'] ?? 'utf8mb4') & 0xff);
        // 服务器状态
        $hs->serverStatus = 2;
        // 服务器权能标识+填充值
        $hs->restOfScrambleBuff = $rand2;
        return getString($hs->write());
    }
    
    //位于 SMProxy/src/HandshakePacket
    public function write()
    {
        // default init 256,so it can avoid buff extract
        $buffer = [];
        // 写入消息头长度
        BufferUtil::writeUB3($buffer, $this->calcPacketSize());
        // 写入序号 -- 消息头的
        $buffer[] = $this->packetId;
        // 写入协议版本号
        $buffer[] = $this->protocolVersion;
        // 写入服务器版本信息
        BufferUtil::writeWithNull($buffer, getBytes($this->serverVersion));
        // 写入服务器线程ID
        BufferUtil::writeUB4($buffer, $this->threadId);
        // 挑战随机数 9个字节 包含一个填充值
        BufferUtil::writeWithNull($buffer, $this->seed);
        // 服务器权能标识
        BufferUtil::writeUB2($buffer, $this->serverCapabilities);
        // 1字节 字符编码
        $buffer[] = $this->serverCharsetIndex;
        // 服务器状态
        BufferUtil::writeUB2($buffer, $this->serverStatus);
        if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) {
            // 服务器权能标志 16位
            BufferUtil::writeUB2($buffer, $this->serverCapabilities);
            // 挑战长度+填充值+挑战随机数
            $buffer[] = max(13, count($this->seed) + count($this->restOfScrambleBuff) + 1);
            $buffer = array_merge($buffer, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        } else {
            // 10字节填充数
            $buffer = array_merge($buffer, self::$FILLER_13);
        }
        // +12字节挑战随机数
        if ($this ->serverCapabilities & Capabilities::CLIENT_SECURE_CONNECTION) {
            BufferUtil::writeWithNull($buffer, $this->restOfScrambleBuff);
        }
        if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) {
            BufferUtil::writeWithNull($buffer, getBytes($this->pluginName));
        }
        return $buffer;
    }
    

    当mysql客户端发送登录认证报文后,这时进行登录账号密码校验的是swoole/server而不是mysql服务端,所以在配置文件server.json中的root跟password正是mysql客户端请求的账号与密码,而swoole/server与mysql服务端交互锁需要的账号密码位于database的配置中。

    // 位于SMProxy/src/SMProxyServer
    private function auth(BinaryPacket $bin, \swoole_server $server, int $fd)
    {
        // 如果数据长度是20, -- 可能自定义的,  4-20是密码, 最后4位不知道干啥
        if ($bin->data[0] == 20) {
            // 密码长度是16 , 判断账号密码
            $checkAccount = $this->checkAccount($server, $fd, $this->source[$fd]->user, array_copy($bin->data, 4, 20));
            if (!$checkAccount) {
                // 发送ERROR报文
                $this ->accessDenied($server, $fd, 4);
            } else {
                if ($server->exist($fd)) {
                    // 发送OK报文
                    $server->send($fd, getString(OkPacket::$SWITCH_AUTH_OK));
                }
                // 认证标志设置为true
                $this->source[$fd]->auth = true;
            }
        } elseif ($bin->data[4] == 14) {
            // 序号等于14
            if ($server->exist($fd)) {
                // 无需认证即登录
                $server->send($fd, getString(OkPacket::$OK));
            }
        } else {
            $authPacket = new AuthPacket();
            // 读取报文信息 登录认证报文
            $authPacket->read($bin);
            // 判断账号密码
            $checkAccount = $this->checkAccount($server, $fd, $authPacket->user ?? '', $authPacket->password ?? []);
            if (!$checkAccount) {
                // 密码校验失败
                if ($authPacket->pluginName == 'mysql_native_password') {
                    // 发送ERROR报文
                    $this ->accessDenied($server, $fd, 2);
                } else {
                    // 记录用户数据
                    $this->source[$fd]->user = $authPacket ->user;
                    $this->source[$fd]->database = $authPacket->database;
                    // 填充数
                    $this->source[$fd]->seed = RandomUtil::randomBytes(20);
                    // 发送EOF报文
                    $authSwitchRequest = array_merge(
                        [254],
                        getBytes('mysql_native_password'),
                        [0],
                        $this->source[$fd]->seed,
                        [0]
                    );
                    if ($server->exist($fd)) {
                        $server->send($fd, getString(array_merge(getMysqlPackSize(count($authSwitchRequest)), [2], $authSwitchRequest)));
                    }
                }
            } else {
                // 账号正确 发送OK报文, 并记录数据
                if ($server->exist($fd)) {
                    $server->send($fd, getString(OkPacket::$AUTH_OK));
                }
                $this->source[$fd]->auth = true;
                $this->source[$fd]->database = $authPacket->database;
            }
        }
    }
    

    当进行tcp握手以及登录认证成功之后,mysql端便可以传输执行语句等操作,而这里SMProxy还对语句进行一定的分析,来判断读,写还是事物等。如果是读语句,并配置了读数据库,那么读语句只会从读池里获取链接,如果读数据库没有配置才会去获取写数据库,所以这里使用的时候需要注意,如果公司的读数据库的配置低于写数据库,可能使用该架构会对读数据库造成一定的压力。

    到了这一步,SMProxy的工作也大概做完了,swoole/server会把mysql客户端传送上来的执行命令文本,发送给mysql服务端,mysql服务端返回的数据SMProxy也不再做过多的处理,而又因为客户端是mysql客户端,可以直接解析mysql服务端返回的报文。

    SMProxy架构的基本流程描述完毕了,而连接池以及mysql报文等更详细的处理,我也备注在代码里,并上传到github中,有想更深入了解的朋友可以下载下来并查看,注释并没有非常完善, 但是大部分的语句我都添加了自己的见解(也有存在解读错误的情况)

    https://github.com/linjinmin/SMProxy-

    总而言之,SMProxy是一个非常优秀的框架。

    参考文章来源:

    https://www.cnblogs.com/JsonM/articles/9283037.html // tcp粘包问题
    https://www.cnblogs.com/davygeek/p/5647175.html // mysql协议

    相关文章

      网友评论

          本文标题:SMProxy分析

          本文链接:https://www.haomeiwen.com/subject/wezwuctx.html