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

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

CNDT 2022に参加してきた(1)

event.cloudnativedays.jp

参加してきた。1日目。オフラインイベントめっちゃ久々だった。最後に行ったのなんだろうと考えてみたら2019のOSCのFallが最後だったっぽい。このときはLT下記憶がある。これが2019の11月末なのでちょうど3年ぶり。すごい。

セッションのメモ

event.cloudnativedays.jp

Terraformを使うのに自分たちが抱えている課題のちょうど先を行っていてとてもおもしろいなと思った。SREがボトルネックにならない仕組みやガードレールの話なんかは面白かった。紹介されていたAtrantisは少し自分たちには機能が多すぎるかなと思ったので小さく何かしらをやってみたい。(OPAとかをしっかりやっていくとか指標があるならよいのかもしれない)

event.cloudnativedays.jp

Self-Hosted Runnersの運用話。会社でも使っているが実はブラックボックスなものでしか無くてなんか便利なやつくらいの認識だったのでとても新鮮だった。

event.cloudnativedays.jp

マルチリージョン運用の話。マルチリージョン環境でのテストの観点にレイテンシを見るのは大事。リージョン切り替え時間を図るのもの大事。

event.cloudnativedays.jp

アタックサーフェスマネジメント。攻撃対象を把握した上でどう守っていくか。docker-slim、distroless、そもそもLinuxコマンドを入れていかないアプリケーション設計。などなど。エフェメラルコンテナは1.25でstableになっていた。

event.cloudnativedays.jp

SLI /SLOの概念からわかりやすく説明されていてとてもわかり易かった。SLOをIaC化していく話でSLOベースの運用のIaC化のプラクティスは確立していないとのことでこれからどうなっていくのかを追っていく必要がありそう。OpenSLOは単体で運用するのではなく周辺のエコシステムと組み合わせて使う必要がある。SLO-as-codeはとてもおもしろそう〜。SLO generator / OpenSLO 要チェック。

SLOとユーザー満足度の評価はどうしていくとよいのかは今の悩みのひとつなので色々とやっていきたい。

今後やりたい

  • Terraform実行環境の整備
  • コンテナのダイエット
  • SLOのIaC化(もう少し枯れてからになるかも...)

【SQL】INNNER JOINではWHERE区はJOINの前に実行されるかも知れない

要約

タイトルは間違っていてJOINより前に(論理的に)実行されることはない。SQLオプティマイザが最適化する過程でWHEREで絞った状態でJOINされることがあるという話。

本題

qiita.com

  • from テーブル指定
  • join テーブルの結合
  • where 条件指定
  • group by グループ化
  • sum,avgなど 関数
  • having 集計後の絞り込み
  • select, distinct 検索
  • order by 並べ替え
  • limit 取得件数の指定

考察

SQLの順序は上記のようになっている。今回取り上げたいのはjoinとwhereの順番で例えばテーブルaというのが1億レコードあってテーブルbというのが1億レコードあってそれをINNNER JOINする方法は以下のようなクエリになる。(カラムはてきとう)。

SELECT *
FROM a
INNER JOIN b
    ON a.id = b.id

このクエリにテーブルaの検索条件を加える。ageというカラムがあるとして50以上の場合を抽出する。

SELECT *
FROM a
INNER JOIN b
    ON a.id = b.id
WHERE a.age > 50

このときSQLの実行順序がJOIN -> WHEREならINNNER JOINされたテーブルからWHEREで行を抽出するのかです。aもbもほぼ同一のidのレコードがある場合はwhereで判定すべき文は1億レコードになります。(MySQLの場合は結合はNLJなので計算量はO(MN)になる。WHEREで絞らないとなかなかにしんどい)

オプティマイザの役割

qiita.com

結論を書くとオプティマイザがSQLを書き換えていい感じに最適化された上で実行してくれる。なのでINNER JOINはWHEREより先に実行はされるがSQLの内容通りの実行順序ではなくなる。例えばさっきの例のクエリは以下のように書き換えられる。

SELECT *
FROM a
INNER JOIN b
    ON a.id = b.id
    AND a.age > 50

少し思ったのはオプティマイザがこのような選択をしない場合はあるのだろうか?という点。ググってみてもあらゆるDBで最適化は効きそうだったが効かない場合は明示的に最適化後のように書いて上げる必要があったりするのだろうか?誤った統計情報が残ってると起きたりもあったり??

OUTER JOINの場合は置き換えられるケースもある

SELECT *
FROM a
OUTER JOIN b
    ON a.id = b.id
WHERE a.age > 50

SELECT *
FROM a
LEFT OUTER JOIN b
    ON a.id = b.id
    AND a.age > 50

は置き換えが可能。ただしageがテーブルbでの比較の場合は

SELECT *
FROM a
LEFT OUTER JOIN b
    ON a.id = b.id
    AND b.age > 50

は置き換えられない。b.ageがNULLの場合でも外部結合だと結果がNULLのレコードとして帰って来てしまうから。

参考

stackoverflow.com

【Nginx】nginx upstream パッシブヘルスチェックのmax_failsは何を対象にカウントするのか

仕事でこのfailsは何をfailsとしてカウントしてるんだっけ?となったので調べった。

公式を読む。max_failsは指定された回数失敗するとupstreamから外されるということが書いてある。この失敗とは?についての言及はここには無い

max_fails=number
sets the number of unsuccessful attempts to communicate with the server that should happen in the duration set by the fail_timeout parameter to consider the server unavailable for a duration also set by the fail_timeout parameter. By default, the number of unsuccessful attempts is set to 1. The zero value disables the accounting of attempts. What is considered an unsuccessful attempt is defined by the proxy_next_upstream, fastcgi_next_upstream, uwsgi_next_upstream, scgi_next_upstream, memcached_next_upstream, and grpc_next_upstream directives.

で、よくわからないのでぐぐってみると以下の記事がヒットした。内容を読むとconnectionタイムアウトかread/writeのタイムアウトが失敗とみなすらしい。加えてL7レベルでの失敗もproxy_next_upstreamに定義することで追加することも出来る模様。

recruit.gmo.jp

errorは対向のサーバがダウンしている時などが該当します。timeoutはproxy_connect_timeout、proxy_send_timeout、proxy_read_timeoutいずれかの閾値が該当します。invalid_headerは様々な理由がありそうですが、対向のサーバが正常に動作していないといったことが考えられます。

またproxy_next_upstreamでhttp_500, http_502, http_503, http_504が定義されている時は、これらも失敗とみなされます。

この話を以下のような最小構成のconfigから見つけるのは少し難しいかもと感じてしまった。(自分で0から書くならよいが初めて触ったconfigだと自分の場合は気づくのに時間がかかりそう...)

upstream app {
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
}

server {
    listen 80 default_server;

    location / {
        proxy_pass http://app;
    }
}

おまけ

暇(じゃないけど)なのでソースを読んで見る

max_failsの判定処理は以下

static ngx_int_t
ngx_stream_upstream_get_chash_peer(ngx_peer_connection_t *pc, void *data)
{
            if (peer->max_fails // max_failsが設定されているかつ現在のfail数が設定値より大きいかつfail_timeoutの時間以下である場合に選定先から外れる
                && peer->fails >= peer->max_fails
                && now - peer->checked <= peer->fail_timeout)
            {
                continue;
            }

peer->failsが失敗数peerはupstreamとのコネクションなど(結構巨大)を管理している構造体。failsをインクリメントしてるのは以下。

void
ngx_stream_upstream_free_round_robin_peer(ngx_peer_connection_t *pc, void *data,
    ngx_uint_t state)
{

    if (state & NGX_PEER_FAILED) { // NGX_PEER_FAILEDの場合にfailsはインクリメントされる
        now = ngx_time();

        peer->fails++;
        peer->accessed = now;
        peer->checked = now;

        if (peer->max_fails) {
            peer->effective_weight -= peer->weight / peer->max_fails;

            // ログレベルをWARNにしておけばNginxのエラーログから中身を見ることもできるっぽい
            if (peer->fails >= peer->max_fails) {
                ngx_log_error(NGX_LOG_WARN, pc->log, 0,
                              "upstream server temporarily disabled");
            }
        }

NGX_PEER_FAILEDになるケースは以下

static void
ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u,
    ngx_uint_t ft_type) // 第3引数はエラータイプを表している
{
    ngx_msec_t  timeout;
    ngx_uint_t  status, state; 

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http next upstream, %xi", ft_type);

    if (u->peer.sockaddr) {

        if (u->peer.connection) {
            u->state->bytes_sent = u->peer.connection->sent;
        }

        if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_403
            || ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) // statusが403か404の場合はfailsはインクリメントしない
        {
            state = NGX_PEER_NEXT;

        } else {
            state = NGX_PEER_FAILED; // ここに入る必要がある
        }

        u->peer.free(&u->peer, u->peer.data, state);
        u->peer.sockaddr = NULL;
    }

NGX_PEER_FAILEDに入るかどうかはngx_http_upstream_next()の呼び出し元で決める模様。ngx_http_upstream_next()を呼んだ時点で403と404以外はfailsはインクリメントされる。

static ngx_int_t
ngx_http_upstream_test_next(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
        if (u->peer.tries > 1
            // next_upstreamこれが設定された値
            // proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | http_429 | non_idempotent
            // のように指定されているので立っているフラグでエラーかどうかを判定していく
            && ((u->conf->next_upstream & mask) == mask) 
            && !(u->request_sent && r->request_body_no_buffering)
            && !(timeout && ngx_current_msec - u->peer.start_time >= timeout))
        {
            ngx_http_upstream_next(r, u, un->mask);  // http_500だけ指定されていてupstreamが503を返した場合はngx_http_upstream_next()は呼ばない
            return NGX_OK;
        }

指定できる値は以下の構造体で管理されているので実は書き換えることで410とか599とか謎なステータスで次のupstreamへ処理を移すことも可能

static ngx_http_upstream_next_t  ngx_http_upstream_next_errors[] = {
    { 500, NGX_HTTP_UPSTREAM_FT_HTTP_500 },
    { 502, NGX_HTTP_UPSTREAM_FT_HTTP_502 },
    { 503, NGX_HTTP_UPSTREAM_FT_HTTP_503 },
    { 504, NGX_HTTP_UPSTREAM_FT_HTTP_504 },
    { 403, NGX_HTTP_UPSTREAM_FT_HTTP_403 },
    { 404, NGX_HTTP_UPSTREAM_FT_HTTP_404 },
    { 429, NGX_HTTP_UPSTREAM_FT_HTTP_429 },
    { 0, 0 }
};

【RDS】IOPSとスループットが思ってたのと違った数値が出たので調べた

RDSのiopsが1,000となっていればinnodb_page_sizeの数値をかけ合わせた数値がスループットになると思っていた。この例で言えば1,000[iops] * 16,384[byte] ≒ 16[MB]あたり。だがメトリクスを見るとwriteが多い時間帯はこのような計算式当てはめることが出来なかった。

innodb_page_sizeとは

innodbが管理するページのサイズ。このページにはレコードが入っていてinnodbはこの単位でしかIOを発行できない。1レコード読む際も必ずこのサイズのデータのやり取りが発生する

speakerdeck.com

EBSのブロックサイズ

EBSはローカルディスクでは無いのでブロックサイズは意識しなくてよい?(ネットワークを介して通信をする。重要なのは帯域であってブロックサイズは重要性は低そう?)

blog.takuros.net

この辺は以下の回答コメントで割と腑に落ちた

stackoverflow.com

EBS性能測定とかも色々見たがブロックサイズを上げることで性能が上限まで線形にスケールしていそう

qiita.com

なんで合わないのかの推測

innodb_page_sizeはあくまでもテーブルスペースのデータのやり取りの話になるのでMySQLは他にも色々仕事をしてるのでそのへんが実はblock sizeが16Kibではないのではという予想。redo log bufferのflushやらpage cleanerとかバックグラウンドで動いてるスレッドは多々ありこの辺がblock size 1MBあたりででIOしてれば上記で上げた計算式からはずれそう。

あとはこんなオプションもあったりする。5.6からデフォルトでオンでダーティページのflushの際に近くのダーティページのflushを1IOで行うというオプション。16Kib * 隣接するページとなる。

dev.mysql.com

感想

ストレージの性能指標難しい