前言:
在深入了解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協定