簡體   English   中英

MySQL XDev API - Node.js 連接器 - client.getSession() 在幾個小時后永遠不會解析也不會拋出

[英]MySQL XDev API - Node.js Connector - client.getSession() never resolve nor throw after few hours

我在 nodejs 中使用 MySQL XDevAPI 連接器連接到同一服務器上的 MySQL 數據庫。 它成功運行了幾個小時,直到某個時候await pool.getSession()永遠不會解決也不會拋出任何錯誤。

  • MySQL 版本為 8.0.29-0ubuntu0.20.04.3
  • NodeJS 是 18.0.0
  • 用於 nodejs 的 Mysql XDevConnector 是 v 8.0.29,安裝了 yarn

我創建我的客戶池:

{
    pooling : { 
        enabled: true, 
        maxSize: 25, 
        maxIdleTime : 1000, 
        queueTimeout: 2000 
    }
}

正如我所說,幾個小時后,服務器將順利運行,突然間,連接池將停止為我提供新會話。 它發生之前的時間從 10/12 小時到 48 小時不等(使用它的網站幾乎沒有收到流量)。

我試圖通過添加一個帶有超時的Promise.race來規避這個問題,如下所示:

await Promise.race([
    pool.getSession(),
    new Promise((resolve, reject) => {
        setTimeout(() => reject(
            new DBConnectionError(
                'XDEV_GET_SESSION_NEVER_RETURNS',
                'Here we go again...'
            )
        ), 10000)
    })
]);

有時,達到超時允許池為后續的pool.getSession工作幾分鍾,直到錯誤再次出現。 有時需要多次調用(3-4),有時,我需要重新啟動我的應用程序。 這是一種非常奇怪和隨機的行為。

我通過調用await con.close()在任何事務/請求后釋放每個連接,並且我的比賽超時是 queueTimeout 的五倍。

我究竟做錯了什么 ?

通過進一步調查,我發現這個問題更多地與遺留代碼在模擬嵌套事務中做愚蠢的事情有關。

它沒有重用當前連接,而是在某個地方創建了一個新連接,並且如果滿足某個條件(在這種精確情況下,用戶會話因不活動而死亡),它永遠不會將連接返回到池中,因為代碼沒有沒有遵循應用程序設計指南。

這種特殊情況導致錯誤以看似“隨機”的頻率發生,因為用戶會話的生命周期在用戶和應用程序使用之間可能存在很大差異。

這是一個非常特定於項目的相關問題,依賴於私有代碼中的錯誤,但如果有人遇到與 mysql 連接器類似的問題,我將其作為參考:仔細調查以找到未發布的連接。

我相信maxIdleTimeout參數可以通過自動返回到池的 idl 連接來避免這種情況,但它似乎不是這樣工作的。

- - - 編輯 - - - -

即使使用我的固定代碼,該錯誤再次出現。

所以,我進行了四種類型的測試:

  1. 原始測試* @mysql/xdevapi模塊
  2. 原始測試* mysql2模塊
  3. 使用我的應用架構測試@mysql/xdevapi模塊
  4. 使用我的應用架構測試mysql2模塊

[*] 通過原始測試,我的意思是在不涉及任何私有代碼的情況下測試包。 只有涉及的模塊。

原始測試

順序的

我們嘗試執行 500 個請求,一次一個,看看會發生什么:

mysql2


const mysql = require('mysql2/promise');

const MAX_CON = Number.parseInt(process.argv[2]) || 120;
const NB_REQ = Number.parseInt(process.argv[3]) || 500;

(async() => {

    try{
        const client = new mysql.createPool(
            {
                host : '127.0.0.1',
                port : 3306,
                user : 'test',
                password : 'test',
                database : 'test',
                connectionLimit : MAX_CON
            }
        );

        const before = Date.now();
        for(let i = 0; i < NB_REQ; i++){
            const con = await client.getConnection();
            await con.query('SELECT 1 from user');
            await con.release();
        }

        const time = (Date.now() - before) / 1000; //in seconds
        const opBySecond = NB_REQ / time;
        console.log('The end : ', { opBySecond, time, NB_REQ, MAX_CON });

        await client.end();

        process.exit(0);
    }catch(e){
        console.log(e);
    }

})();

@mysql/xdevapi


const mysql = require('@mysql/xdevapi');

const MAX_CON = Number.parseInt(process.argv[2]) || 120;
const NB_REQ = Number.parseInt(process.argv[3]) || 500;

(async() => {

    try{
        const client = await mysql.getClient(
            {
                host : '127.0.0.1',
                port : 33060,
                user : 'test',
                password : 'test',
                schema : 'test'
            },
            {
                pooling : {
                    maxSize : MAX_CON
                }
            }
        );

        const before = Date.now();
        for(let i = 0; i < NB_REQ; i++){
            const con = await client.getSession();
            const prep = await con.sql('SELECT 1 from user');
            await prep.execute();
            await con.close();
        }

        const time = (Date.now() - before) / 1000;
        const opBySecond = NB_REQ / time;
        console.log('The end : ', { opBySecond, time, NB_REQ, MAX_CON });

        await client.close();

        process.exit(0);
    }catch(e){
        console.log(e);
    }

})();

兩個腳本都運行500請求,其中包含60連接池和各自的命令:

  • node squential-mysql2.js 60 500
  • node squential-xdevapi.js 60 500

結果:

司機 操作/秒 時間(秒)
mysql2 11 111.11 0.045
xdevapi 3 703.70 0.135

@mysql/xdevapi在順序執行時比mysql2慢 3 到 4 倍。

平行

mysql2


const mysql = require('mysql2/promise');

const MAX_CON = Number.parseInt(process.argv[2]) || 60;
const NB_REQ = Number.parseInt(process.argv[3]) || 500;

(async() => {

    try{
        const client = new mysql.createPool(
            {
                host : '127.0.0.1',
                port : 3306,
                user : 'test',
                password : 'test',
                database : 'test',
                connectionLimit : MAX_CON
            }
        );

        const before = Date.now();
        const promises = [];
        for(let i = 0; i < NB_REQ; i++){
            promises.push(new Promise(async resolve => {
                const con = await client.getConnection();
                await con.query('SELECT 1 from user');
                await con.release();
                resolve();
            }))
        }
        await Promise.allSettled(promises);

        const time = (Date.now() - before) / 1000; //in seconds
        const opBySecond = NB_REQ / time;
        console.log('The end : ', { opBySecond, time, NB_REQ, MAX_CON });

        await client.end();

        process.exit(0);
    }catch(e){
        console.log(e);
    }

})();

@mysql/xdevapi


const mysql = require('@mysql/xdevapi');

const MAX_CON = Number.parseInt(process.argv[2]) || 60;
const NB_REQ = Number.parseInt(process.argv[3]) || 500;

(async() => {

    try{
        const client = await mysql.getClient(
            {
                host : '127.0.0.1',
                port : 33060,
                user : 'test',
                password : 'test',
                schema : 'test'
            },
            {
                pooling : {
                    maxSize : MAX_CON
                }
            }
        );

        const before = Date.now();
        const promises = [];
        for(let i = 0; i < NB_REQ; i++){
            promises.push(new Promise(async resolve => {
                const con = await client.getSession();
                const prep = await con.sql('SELECT 1 from user');
                await prep.execute();
                await con.close();
                resolve()
            }));
        }

        await Promise.allSettled(promises);

        const time = (Date.now() - before) / 1000;
        const opBySecond = NB_REQ / time;
        console.log('The end : ', { opBySecond, time, NB_REQ, MAX_CON });

        await client.close();

        process.exit(0);
    }catch(e){
        console.log(e);
    }

})();

情況更糟。

兩個腳本都運行500請求,其中包含60連接池和各自的命令:

  • node parallel-mysql2.js 60 500
  • node parallel-xdevapi.js 60 500

結果:

司機 操作/秒 時間(秒)
mysql2 8 771.92 0.057
xdevapi 76.66 6.522

@mysql/xdevapi在並行執行時比mysql2慢 110 倍。 但它並不止於此。

如果你嘗試node parallel-xdevapi.js 120 500你可能會得到一個錯誤:

/test/node/node_modules/@mysql/xdevapi/lib/DevAPI/Connection.js:920
            return state.error || new Error(errors.MESSAGES.ER_DEVAPI_CONNECTION_CLOSED);
                                  ^

Error: This session was closed. Use "mysqlx.getSession()" or "mysqlx.getClient()" to create a new one.
    at Object.getError (/test/node/node_modules/@mysql/xdevapi/lib/DevAPI/Connection.js:920:35)
    at Socket.<anonymous> (/test/node/node_modules/@mysql/xdevapi/lib/DevAPI/Connection.js:677:40)
    at Object.onceWrapper (node:events:642:26)
    at Socket.emit (node:events:527:28)
    at TCP.<anonymous> (node:net:715:12)

Node.js v18.0.0

node parallel-mysql2.js 120 500輸出:

司機 操作/秒 時間(秒)
mysql2 7 142.86 0.07

對於拋出的錯誤,我的猜測是mysql/xdevapi不處理一次連接的mysql-server max 客戶端。 在我的機器上,它是默認值 128,因為我有多個使用數據庫的進程,使用maxSize=120池運行腳本會使進程崩潰。

這就是為什么我在 dev 中沒有收到任何錯誤,但在 prod 中出現了一些危險的崩潰:兩個進程的maxSize=25池和並行啟動的 4 個 nodejs 進程,它達到了最大 200 個 mysql 客戶端連接。 任何連接負載都會使一個或多個池崩潰,因此該進程將無法從中恢復。 從這一點開始,所有對client.getSession()的后續調用都將無限期掛起。

應用測試

我重構了整個應用程序數據庫連接處理以確保它是一致的,然后我創建了一個可以處理連接器插件/插件的數據庫組件。

我創建了兩個插件:一個使用@mysql/xedvapi驅動程序,另一個使用mysql2 當我使用autocannon運行一些基准測試時,兩者都顯示出與上述相似的結果。

我還創建了一個連接包裝器,它可以幫助我了解是否有任何連接未關閉。 沒有任何連接處於打開狀態,並且在每次查詢后總是返回到池中。

結論

總之,我想說,此時@mysql/xdevapi包似乎有問題,在版本8.0.29和 mysql 8.0.29 ,在 nodejs v18.0.0 ,ubuntu 20.04上。

要么是因為當 mysql 服務器無法接受更多連接時它不處理邊緣情況,要么是因為它的性能很慢,特別是當查詢不是順序的(這是像 nodejs 應用程序這樣的 web 服務器中的大多數用例)。

我現在通過從@mysql/xdevapi切換到mysql2解決了我的問題。

-------------- EDIT2 ----------------

對於那些對這個討論感興趣或可能對這個討論感興趣的人,請點擊這個鏈接到mysql 錯誤報告

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM