バックエンド最適化
Cache
Copy on Write An Overview of Query Optimization in Relational Systems 論文紹介 - Google スライド
O/R Mapper
# ISUCONメモ
https://github.com/FujishigeTemma/isucon9-qualify/blob/master/go/main.go https://github.com/tohutohu/isucon9/blob/master/webapp/go/main.go
https://www.youtube.com/watch?v=0DhBLswwcRs
https://gist.github.com/941/8c64842b71995a2d448315e2594f62c2 https://gist.github.com/south37/d4a5a8158f49e067237c17d13ecab12a https://isucon.net/archives/54822761.html
# 事前講習
工事中 :::spoiler ssh接続 ssh鍵 githubで公開リポジトリに置くと失格 マニュアルを全員読む(情報をまとめることが得意な人がいたら) 初期ベンチ ベンチの振れ幅、挙動を調べる 1コミット1ベンチ コミットコメントにログとか載せると良き キャッシュが必要以上に残るとフェイル タイムアウトか間違ったキャッシュならタイムアウトを取る
変更するファイル webapp/go webapp/sql 各種設定ファイル nginxやmysqlの設定などはgit内に移して元の場所にシンボリックリンクを貼る
# 開始直後
マニュアルを全員で読む
- 点数に関わる記述が重要情報
- キャッシュの許され方
- 「N秒キャッシュしてもよい」と書いてある
不要なデーモンを止める
| |
ssh configの設定 ベンチ1回目 リポジトリ内にシンボリックリンクを置く git init git remote git add . git commit -m “Initial commit” お昼食べる 利用するサイトの動作確認 忘れて無ければ全ページのスクショを撮る ラストのチェックのときにCSSとかをざっと見る 使いそうなファイル群を固めてローカルに持ってくる tar xvcf webapp scp user@ip:~/webapp.tar . 更新箇所のチェック エンドポイントの数 更新があるエンドポイント mysqlとnginxのログ設定 解析ツールのインストール ベンチ2回目 秘伝のタレ ベンチ3回目
| |
https://github.com/reyu0722/piscon2021
- ItemとかUserとかで取れる部分はキャッシュする -> 71500
- Item以外をメモリだけで管理するようにした (他にもいくつか) -> 87840
- 超大変だった :::
# 各種解析ツール 推測するな計測せよ
# パフォーマンスモニタリング
CPU占有率の高いプロセスを特定します htop dstat dstat -tlamp
# メトリクス
高級なパフォーマンスモニタリングツール netdata NewRelic Splunk https://dev.classmethod.jp/articles/netdata/
# アクセス解析
パスパラメータ/クエリパラメータ別のアクセス数を表示してくれます 主に見るべきはavg 次にcount kataribe alp
# プロファイリング
pprof fgprof
# スロークエリ
mysqldumpslow pt-query-digest
# SQL全般
# Makefile
TODO: 秘伝のMakefileを作る https://gist.github.com/azonti/dee0547cb561dfdec4a90e093a418bdc https://github.com/FujishigeTemma/isucon10-final/blob/master/Makefile https://github.com/tohutohu/isucon9/blob/master/Makefile https://git.trap.jp/eiya/202008piscon/src/branch/master/go/Makefile
# アプリ
- クエリ最適化
- 変更の少ないデータのキャッシュ化
- やらなくてよい処理を省く
- 強いセキュリティを弱くして高速化(gorilla/sessions->自前実装 net/http->fasthttp bcrypt->SHA256など)
- APIの並列化
- FOR UPDATEアプリケーションで排他制御
- gollira/sessionはセキュアなセッションをcookieだけで実現しているので、暗号化のコストが結構かかる -> ランダムな値とmapを使った実装に差し替え
- jsonのエンコード/デコードをgo-json
# プロファイラの設定
pprof https://github.com/google/pprof
標準で入っているプロファイラです。デファクトスタンダードという感じ。
| |
プロファイリングした画像をdiscordやslackに届けるシェルです。
| |
fgprof https://github.com/felixge/fgprof
一部適切な表示とならないことがある
| |
http://localhost:6060/debug/fgprof
# MySQL チューニング
# MySQL設定
別のサーバーからDBへアクセスできるようにする
/etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 127.0.0.1をコメントアウト
bind-address = 0.0.0.0
http://dsas.blog.klab.org/archives/50860867.html
- グローバルバッファ mysqld全体でそのバッファが1つだけ確保されるもの
- スレッドバッファ スレッド(コネクション)ごとに確保されるもの チューニングの際にはグローバル/スレッドの違いを意識するようにしましょう。 なぜなら、スレッドバッファに多くのメモリを割り当てると、コネクションが増えたとたんにアッという間にメモリ不足になってしまうからです。
| |
| |
# スロークエリ
/etc/mysql/mysql.conf.d/mysqld.cnf
| |
sudo mysqldumpslow -s t -t 10 /path/to/slow.log
pt-query-digest /path/to/slow.log
複数のSQLクエリをIN、JOIN、LIMIT句、外部キーでまとめる(N+1問題など)
不要なカラムやクエリを削除する
転送量を減らす。取得するカラムを減らす、特にでかいもの(VARCHAR(4096)など)が入っているのは削るべき
https://qiita.com/ikenji/items/b868877492fee60d85ce
INDEX B+ツリーのインデックスを適切に設定することで検索速度を高める。二分探索ができるものなら速くなる。 更に言えば上記のBツリーインデックスとハッシュインデックスが存在し、ハッシュインデックスの方が速いが、等価比較しかできない。MySQLは自動的にそれらの選択をしている。 これが多すぎるとINSERTでくそおもになるので注意
ここで役立つコマンド
mysql -uユーザ名 -pパスワード DB名 -e'EXPLAIN ~~;'
LIKEも最初の方がワイルドカードではなければ使える
- 速くなるもの WHERE, ORDER BY, JOIN句
- 効かないもの LIKE, OR, 演算, 関数処理, IS NULL, 否定形
- NOT NULLを出来る限りつける
- ORをUNION/UNION ALLに変換する
- ORDER BYでDESCとASCの混合->MySQL 8.0では 降順インデックスで適用できる
- 空間インデックス
SRID 0を付ける https://matsuu.hatenablog.com/entry/2020/09/13/131145
プレフィックスインデックス 非常に長い文字列にインデックスを付ける必要がある場合、値全体ではなく最初の数文字にインデックスを付けることで、記憶域を節約し、パフォーマンスを改善することができます。
- プレフィックスインデックスでは、選択性も低下するため、十分な選択性が得られる長さを持ち、記憶域を節約するくらい短いプレフィックスを選択すること
- 欠点として、ORDER BY 句や GROUP BY 句を使用するクエリにプレフィックスインデックスを使用できない
- 適切なサイズは、下記クエリで選択性を計測し、収束する値を見ることで発見できる。選択性とは、インデックス付けされた値の数と、テーブル内の行の合計数の比率のこと
マルチインデックス クエリ文がINDEX作成時のカラム順に基づいていないと、INDEXが使えない
- 等しい (=)、より大きい (>)、より小さい (<)、BETWEENなどの検索条件のWHERE句で使用されるか、結合に含まれる列を、先頭に配置する
- クエリによるカラム順序の制限がない場合、最も選択的な列をインデックスの先頭に配置する。カラム毎の選択性は下記クエリで計測する
- ちゃんと出来てないとEXPLAIN type=index:フルインデックススキャンと言われる 1種類の値に対し多くのデータが存在するようなカラムを先に置く 重複の多いものを先に置く それに伴うようにクエリのカラムを先に置く
https://lukesilvia.hatenablog.com/entry/20080315/1205583930 Using filesort レコードをソートして取り出す方法を決定するには、MySQL はパスを余分に実行しなくてはならないことを示す。 join type に従ってすべてのレコードをスキャンし、WHERE 条件に一致する全てのレコードに、ソートキー + 行ポインタを格納して、ソートは実行される。 その後キーがソートされる。 最後に、ソートされた順にレコードが取り出される。
Using temporary クエリの解決に MySQL で結果を保持するテンポラリテーブルの作成が必要であることを示す。これは一般に、GROUP BY を実行したカラムセットと異なるカラムセットに対して ORDER BY を実行した場合に発生する。 UNIONとかも…?
https://qiita.com/katsukii/items/3409e3c3c96580d37c2b https://nishinatoshiharu.com/overview-multicolumn-indexes/
インデックスオンリースキャン SELECTするカラムが少ないときINDEXにそのカラムを置くとそれを取ってくるだけでよい
# クエリーキャッシュ Qcache
※注意 MySQL 8.0以降には存在しない メモリ上にクエリのバイト列とその結果を保存して再度同じ(大文字・小文字を区別する)クエリが来たらDBを探さずそれを返す。更新がかかるとキャッシュがフラッシュされる。 ディスクI/Oの多発の解決 INSERT,UPDATEが少なくSELECTが多いアプリに有効
SQLで画像を入れるとAXと呼ばれるやつが入るらしい https://stackoverflow.com/questions/52426874/how-do-i-extract-microsoft-sql-varbinarymax-field-to-image-using-golang
キーキャッシュのヒット率 = 100 - ( key_reads / key_read_requests × 100 )
mysqlをmariadbに変える? (あまり必要はない)
| |
proxysqlを利用する? mariadbならそのままquery cache使えばよさそう
http://dsas.blog.klab.org/archives/50860867.html
# 再起試験対策
1台目サーバと2台目サーバの再起動タイミングがずれるとアプリからのDB接続に失敗
/etc/systemd/system/isuumo.go.service
| |
再起動試験用DB待ち
| |
# golang
| |
https://qiita.com/go_sagawa/items/11929cd0883608a6888d
- redis
| |
sudo nano /etc/redis/redis.conf
bind 127.0.0.1 ::1のコメントアウト
protected-mode yes→protected-mode no
requirepass hogehogeに
| |
sudo echo never > /sys/kernel/mm/transparent_hugepage/enabled
sudo nano rc.local→追記: echo never > /sys/kernel/mm/transparent_hugepage/enabled
# JSON
https://github.com/json-iterator/go を試してみる
| |
https://github.com/francoispqt/gojay https://github.com/goccy/go-json https://github.com/minio/simdjson-go Marshal / Encoder: goccy/go-json > francoispqt/gojay Unmarshal: francoispqt/gojay > goccy/go-json Decoder: francoispqt/gojay » goccy/go-json
メモリ上にキャッシュ
map- 読み込み書き込みともに多く行う ->
sync.Map - 読み込み多く行う ->
sync.RWMutex+map - ロックが気になるならシャーディングをするとよい
- 読み込み書き込みともに多く行う ->
- sessionメモリに持つ
gorilla/sessionsはコピーのためにencoding/gobが無駄に呼び出される
singleflight
メモリ効率化
挿入後の結果をとらずに返す
- timeはミリ秒を四捨五入すること
.Round- デフォルトが秒までだけど、
DATETIMEのあとに数字がある場合は異なるので要確認 - https://dev.mysql.com/doc/refman/5.6/ja/fractional-seconds.html
- デフォルトが秒までだけど、
- timeはミリ秒を四捨五入すること
session
pat.Parammisc
- キーが100個程度までならmapよりarrayを線形探索したほうがよいらしい
- sliceもmapもcapacityを指定する
i, item := range itemsをするとitemにコピーが走るのでitems[i]を使う- 画像を返してる箇所はキャッシュのヘッダーを設定する
- GOGC
400~1200とか- 有効にしたときは毎回再起動すること
カバリングインデックス
http://akouryy.hatenablog.jp/entry/2020/09/13/130415
# MySQL8
- NOWAIT, SKIP LOCKED: https://qiita.com/hmatsu47/items/7675b026e65762d2445f
- HASH JOIN: https://qiita.com/hmatsu47/items/e473a3e566b910d61f5b
- Multi-valued index(jsonカラム用): https://qiita.com/hmatsu47/items/3e49a473bc36aeefc706
- GROUP BYをLATERALにする?: https://qiita.com/hmatsu47/items/040d65d118d0ecec6381
パーティショニングとは
# HTTP チューニング
https://qiita.com/yumin/items/5de33b068ead564ebcbf
# メモ
https://github.com/cs3238-tsuzu/sqlx-selector
http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#if https://stackoverflow.com/questions/8591600/nginx-proxy-pass-based-on-whether-request-method-is-post-put-or-delete
得点につながるエンドポイントの確認 大量アクセスかつ同じものが使える→singleflight
lockfree map