地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【ProxySQL】DBサーバがOSごと落ちた場合に起きうるユーザー影響の度合いを調べた

C++門中で読みたかったので呼んでみた記事。

DBサーバが落ちた場合に切り離しまでにかかる時間

落ちた = OSごと通信不可となった場合とする。切り離しまでにかかる時間は概ね以下のように決まる

  • shun_on_failures
    • 1秒間あたりに失敗する接続の数の閾値。この値を超える接続失敗でそのバックエンドサーバとの通信は行われなくなる。

サーバダウンして新規接続要求がこの数発生しない場合は切り離し対象にならないので注意。接続失敗となる値は以下のタイムアウトの数値で決まる

  • connect_timeout_server
    • 単位はミリ秒。この時間以上のconnect(2)で接続は失敗となる。

shun_on_failures: 10 connect_timeout_server: 5000

という設定でタイムラインを書くと以下のような感じになる。

1 10:00:00 DBサーバダウン
2 10:00:00 クライアントが新規接続要求*10
3 10:00:05 新規接続失敗*10

コネクションプール = 最大接続数の状態だとこの状態には鳴らずに以下のケースになる。コネクションプールを使わずに新規接続を待つ場合はこの時間まではクライアントが待ちになるので最大で5秒間はユーザリクエストに待ちが生じる

DBサーバダウン時にすでにMySQLとやり取りしているコネクション

これはおそらくアプリケーションにエラーが帰る。(おそらくというのはC++を読み切れていない部分がありそうだから...)。

monitor

proxysql.com

ProxySQLにはMonitorスレッドがある。役割として以下のようになっていて中でもping checksというのが今回の要になる機能

a main thread responsible to start a thread for each monitor category (5 in total)
a thread scheduling connection checks
a thread scheduling ping checks
a thread scheduling read-only checks
a thread scheduling replication lag checks
a thread scheduling group replication monitoring
a threads pool (initially twice the size of mysql-threads)

ping checksは設定された値の感覚でpingを実行し設定された値でタイムアウトの処理を実行する。このタイムアウトした際の挙動をドキュメントだけでは読みきれなかったのでコードを見ながら確認していく。

ping自体の処理は以下。これをinterval感覚で実行している。大きい関数なので今回関係する場所のみを追っていく

github.com

ping

pingの実体のクエリはこれ。select 1にかかる時間でタイムアウトするかを見ていく。

new_query=(char *)"SELECT 1 FROM (SELECT hostname,port,ping_error FROM mysql_server_ping_log WHERE hostname='%s' AND port='%s' ORDER BY time_start_us DESC LIMIT %d) a WHERE ping_error IS NOT NULL AND ping_error NOT LIKE 'Access denied for user%%' AND ping_error NOT LIKE 'ProxySQL Error: Access denied for user%%' AND ping_error NOT LIKE 'Your password has expired.%%' GROUP BY hostname,port HAVING COUNT(*)=%d";

timeoutした際の処理

         for (j=0;j<i;j++) {
                char *buff=(char *)malloc(strlen(new_query)+strlen(addresses[j])+strlen(ports[j])+16);
                int max_failures=mysql_thread___monitor_ping_max_failures;
                sprintf(buff,new_query,addresses[j],ports[j],max_failures,max_failures);
                monitordb->execute_statement(buff, &error , &cols , &affected_rows , &resultset); // ここでpingのクエリを実行している
                if (!error) { // エラー時の処理
                    if (resultset) {
                        if (resultset->rows_count) {
                            // disable host
                            bool rc_shun = false;
                            rc_shun = MyHGM->shun_and_killall(addresses[j],atoi(ports[j])); // ここの中でクエリをkillしている
                            if (rc_shun) {
                                proxy_error("Server %s:%s missed %d heartbeats, shunning it and killing all the connections. Disabling other checks until the node comes back online.\n", addresses[j], ports[j], max_failures);
                            }
                        }
                        delete resultset;
                        resultset=NULL;
                    }
                free(buff);
            }
shun_and_killall
bool MySQL_HostGroups_Manager::shun_and_killall(char *hostname, int port) {
    time_t t = time(NULL);
    bool ret = false;
    wrlock();
    MySrvC *mysrvc=NULL;
    for (unsigned int i=0; i<MyHostGroups->len; i++) {
    MyHGC *myhgc=(MyHGC *)MyHostGroups->index(i);
        unsigned int j;
        unsigned int l=myhgc->mysrvs->cnt();
        if (l) {
            for (j=0; j<l; j++) {
                mysrvc=myhgc->mysrvs->idx(j);
                if (mysrvc->port==port && strcmp(mysrvc->address,hostname)==0) {
                    switch (mysrvc->status) {
                        case MYSQL_SERVER_STATUS_SHUNNED:
                            if (mysrvc->shunned_automatic==false) {
                                break;
                            }
                        case MYSQL_SERVER_STATUS_ONLINE:
                            if (mysrvc->status == MYSQL_SERVER_STATUS_ONLINE) {
                                ret = true;
                            }
                            mysrvc->status=MYSQL_SERVER_STATUS_SHUNNED;
                        case MYSQL_SERVER_STATUS_OFFLINE_SOFT:
                            mysrvc->shunned_automatic=true;
                            mysrvc->shunned_and_kill_all_connections=true;
                            mysrvc->ConnectionsFree->drop_all_connections(); // killはここでやっている。内部ではsocketに対してdeleteを呼び出している
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }
    wrunlock();
    return ret;
}

OFFLIME_HARD

hardを指定すると以下が呼び出される。shutdown(2)してclose(2)してるがshutdown(2)を呼んでるのはなんでだろ。(参照するプロセスが他にある場合に使えるとかそんな感じだっけか)

// Hard shutdown of socket
void MySQL_Data_Stream::shut_hard() {
    proxy_debug(PROXY_DEBUG_NET, 4, "Shutdown hard fd=%d. Session=%p, DataStream=%p\n", fd, sess, this);
    set_net_failure();
    if (encrypted) {
        // NOTE: SSL standard requires a final 'close_notify' alert on socket
        // shutdown. But for avoiding any kind of locking IO waiting for the
        // other part, we perform a 'quiet' shutdown. For more context see
        // MYSQL #29579.
        SSL_set_quiet_shutdown(ssl, 1);
        SSL_shutdown(ssl);
    }
    if (fd >= 0) {
        shutdown(fd, SHUT_RDWR);
        close(fd);
        fd = -1;
    }
}