SMProxy 分析 (基於 Swoole 開發的 MySQL 數據庫連接池

前言:

    在深入瞭解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:

執行流程圖

接下來將針對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協議

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章