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

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

【RDS】RDSでBlue/Greenデプロイ!すごい!!すごい!!!!

dev.classmethod.jp

AWS re:Invent 2022で発表されていた機能。最初見た瞬間「???」となった。謎技術だすごい!!となった。

仕組み的な話

docs.aws.amazon.com

公式ですでに詳しく書かれている。とてもわかり易いのが下の図でコンソールをポチポチするだけでこんな環境が用意される。上が既存の本番環境で下のgreenが新しく変更を加えるstaging環境と読んでいるものになる。既存のPrimaryにレプリカとして1台追加してそのレプリカにさらにスタンバイとリードレプリカを構築している。(binlog仕組みを使う。例ではbinlog_formatはMIXEDで設定されている)。構築した時点でStaging環境へはアクセス(read/write)が可能(Green 環境の DB インスタンスはデフォルトで読み取り専用なので変更は設定必要)になっているのでスキーマの変更などを行うことも可能になる。

Stagingで色々やった後にスイッチオーバーを実行することでStagingが本番環境へ昇格する。図は下のようなイメージで新本番が旧本番の識別子を引き継いで旧本番にはoldというsuffixがついている。アプリケーションは旧本番と新本番で設定変更が不要で接続を切り替えることが出来る(コネクション永続化とかアプリ独自のDNSキャッシュとか持ってるケースだとそうは行かないけど...)。

これ前から出来たよね..??

図を見た瞬間気づいたがこれは前から同じことを手動で行うことは可能だった。というのもRDSは多段レプリケーションを公式で提供しており同じ仕組みをGUIポチポチ+MySQLにログインして色々やってDNS切り替えてとかをやれば可能ではあった。(ちなみにこの機能が提供されたのは2013年だった。それはそれですごい。。。AWS Release Notes)。今回はそのへんがマネージドでやれるようになってくれたので便利だよねという感じ。特に記載にもあるガードレール(Switchover guardrails)なんかはレプリカの状態とかを目視で確認する部分をRDS側が吸収してくれるのでめっちゃよさそう。

本番でやるの大変そう..??

スイッチオーバーのタイミングで1分間くらいダウンタイムが必要となっている。これは静止点を取るために内部では多分テーブルロックなんかをやってるんじゃないかと思う(未検証なのでわからない)。スイッチオーバーのアクションのうち気になったものを書いてみる

1. 両方の環境でプライマリ DB インスタンスでの新しい書き込み操作を停止する
2. 接続をすべて切って新規接続の受付を停止する
3. Green 環境でレプリケーションが追いつくのを待つ
4. DBの名前変更
6. 新規接続を受け付ける

時間がかかりそうなのは1,3辺りで例えば書き込みが10分のクエリがあったらその間テーブルロックが取れたテーブルについてはクエリが刺さったりしそう。時間がかかるDDLとかもメタデータロックの問題が発生しそう。そこからさらにレプリカでも実行するのを待つのでダウンタイムは結構読むの難しい?スイッチオーバーのベストプラクティスには運用環境でトラフィックが最も少ない時間を特定しその時間にやることを進められている。writeがそこそこある環境だとサービスメンテ入れるなりをしないとオンラインではやれなそうだなと...スキーマ変更に限って言えば条件さえ揃ってしまえばMySQLのオンラインDDLなりpt-online-schema-changeのほうがよさそうに思える。スキーマの検証は本番環境しかDBが無い場合を除いて本番以外(dev環境とかintegrationとかローカルとか)でもやれると思うのでこの用途はあまり見えないかなという感じか

サービスをメンテナンスをいれる前提でのMySQLエンジンのアップデート時には工数がめっちゃ省けるのでよさそう。後はGreen環境を作るだけ作ってそこで負荷テストとかやるみたいな用途も。本番にいれるのは怖い設定とかレプリカラグがどれくらい出るか読めないようなDMLとかをそっちで流してからBlueで実行してスイッチオーバーはしないみたいなのもできそう。(ただこれめっちゃお金かかるよな...)

感想

前から出来たけど自動でかつ安全に実行できるようになったのでMySQLのアップデートは間違いなくかんたんに出来るようになったので便利!!

その他

ベストプラクティスにバイナリログの最適化をしましょうね書かれている

Optimize read replicas for binary log replication.

For example, if your DB engine version supports it, consider using GTID replication, parallel replication, and crash-safe replication in your production environment before deploying your blue/green deployment. These options promote consistency and durability of your data before you switch over your blue/green deployment. For more information about GTID replication for read replicas, see Using GTID-based replication for Amazon RDS for MySQL.

GTIDとパラレルレプリケーションはよいとして「crash-safe replication」ってなんだろと思って調べた。

nippondanji.blogspot.com

昔のMySQLのレプリカはクラッシュセーフじゃなかったらしい。IOスレッドとSQLスレッドはそれぞれどこまで適用したかを管理ファイルに書き込んでいたためSQLスレッドがトランザクションを実行してコミットした後にサーバがクラッシュするとSQLスレッドはどこまでbinlogを適用したかを管理ファイルに書き込むこと無く終了してしまう。サーバの再起動時にトランザクションが2回実行されて不整合が起きてしまうという。これを解決するのがrelay_log_info_repositoryをTABLEにするというもの。TABLEにすることでトランザクション内でテーブルの更新が走るためサーバクラッシュのタイミングがどこであろうと不整合は起きない。(管理ファイルへのfsync(2)をトランザクションに入れればいいじゃんと思ったがパフォーマンスとか的にだめだったのだろうか..??)

Terraformも早くもあるっぽい

github.com

applyすればGreen作ってスイッチオーバーまでやってくれる。MySQLアップデートめっちゃ楽になりそう

【MySQL】RDSのアップデートで書き込みスループットが2倍になるケースを考えてみる

帰宅前に以下の記事を読んですごい!となったがジムのランニングマシン使用中に「2倍ってどうやったら行くん?」ってなったので調べた。

dev.classmethod.jp

という機能がリリースされていた。中身自体はMySQLのDoublewrite Bufferの機能をオフにするというもの。Doublewrite Bufferを軽くふりかえるとシステムテーブルスペースに含まれる領域の一つで、バッファープールの内容が書き込まれます。バッファーと言いつつもメモリではなくディスクへの書き込みです。瞬断等により中途半端にデータが書き込まれた状態になった際にredoログから復元するかDoublewrite Bufferから修復するかという処理をするために用いられます。詳しくは公式を参照してください。(ZFSとかファイルシステムでアトミックな書き込みを保証されているのであれば実はすでにオフで運用しているケースも多くありそう)

dev.mysql.com

ここでアップデートの記事を見てみると以下のような記述があります。Doublewrite Bufferを用いた書き込みは最大2倍の時間がかかると述べられている。ここでのポイントはDoublewrite Bufferはシーケンシャルの追記で書き込みが行われる。.ibdへはランダムWriteになるという点。仮に前者のシーケンシャルIOをなくした所で2倍にはならんのでは?と思ったのだった。

But this method of writing takes up to twice as long, consumes twice as much I/O bandwidth, and reduces the throughput and performance of your database.

Doublewrite Bufferのパフォーマンスへの影響

載せれるデータが無かったが前にオンオフで大量Writeをしたときはだいたい10%も早くならなかった。というのも公式ドキュメントでもデータは 2 回書き込まれますが、二重書込みバッファには I/O オーバーヘッドの 2 倍や I/O 操作の 2 倍は必要ありません。と書かれている。ダーティ ページからの Doublewrite bufferのfsync(2)とDoublewrite bufferの内容をfsync(2)する際は前者はシーケンシャルIOで後者はランダムIOであるためだと思われる。

innodb_page_cleanersで指定されたスレッドはinnodb_doublewrite_pagesで指定された数だけDoublewrite Bufferへ書き込みます。このへんはバックグラウンドスレッドでやるのでIOPSの制御innodb_io_capacityやinnodb_io_capacity_maxあたりも効いてきます。バックグラウンドに潤沢にリソースを渡しているケースであればこの辺はよさそうです。(書き込まれた回数とかはInnodb_dblwr_pages_writtenで確認することが出来る)

2倍になるケースはあるのか

www.percona.com

PerconaがDoublewrite Bufferのパフォーマンスへの影響について言及しているブログを見つけました。

In usual workloads the performance impact is low-5% or so. 

5%程度のパフォーマンス劣化はあるかもと言っています。

But if you experience a heavy workload, especially if your data does not fit in the buffer pool, the writes in the doublewrite buffer will compete against the random reads to access the disk. In this case, you can see a sharp performance drop compared to the same workload without the doublewrite buffer-a 30% performance degradation is not uncommon.

データがバッファー プールに収まらない場合はワークロードのランダムリードと競合してパフォーマンス劣化するかもとも言っています。なるほど。それでも30%程度の劣化だとも書かれています。innodb_io_capacityの設定が甘いケースだと起きうるかも知れない。(めちゃめちゃWriteした直後にバックグラウンドスレッドが大量IOPSで動いているタイミングでbuffer poolに乗っていない場合など?)

Another case when you can see a big performance impact is when the doublewrite buffer is full. Then new writes must wait until entries in the doublewrite buffer are freed.

最後はこれ。doublewrite bufferが満杯になるケース。buffer自体は128 ページ*16KB(ページサイズ)が確保されているのでここがあふれるケースとdirty pageが増えcheckpointが走る。実行中のトランザクションを一時停止/新規トランザクションの開始を抑制につながる。このケースでは確かにスループットが2倍近くになるかもしれない。

感想

どこかで実験してみたい!!

【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;
    }
}