RDS Auroraを使っているところで、OSの空きメモリが少なくなったアラートが出たので、それについて細かく考察したら、それなりの量になったのでまとめた感じです。
別にAuroraじゃなくRDS MySQLでも、MySQL Serverでも同じ話なのですが、クラウドならではの側面もあるなということでタイトルはRDSにしております。
RDSのメトリクス監視
RDSはブラックボックスとはいえ、必要なメトリクスはだいたい揃っているので、CloudWatch を見たり……APIで取得してどっかに送りつけたりして利用します。なので、まずは接続数とメモリについて復習です。
接続数
SHOW STATUS 的には Threads_connected です。CloudWatch Metrics 的には、DBInstanceIdentifier → DatabaseConnections です。
見た感じ、どちらも同じ値を返すようなので、どちらを使ってもいいです。強いていうならば、AWS以外の環境もあるなら、STATUSの方で共通にした方が気持ちマシかもってくらいです。
RDS空きメモリ容量
RDSにどのくらいの空きメモリ容量があるかは、CloudWatch Metrics にあって、DBInstanceIdentifier → FreeableMemory です。
公式の Amazon RDS のモニタリングの概要 にあるとおり、
解放可能なメモリ (MB)
使用可能な RAM の容量。MariaDB、MySQL、Oracle、および PostgreSQL DB インスタンスの場合、このメトリクスは /proc/meminfo の MemAvailable フィールドの値を報告します。単位: バイト
MemAvailable を返しているだけなので、2GB以下とか空き容量という値だけでアラート条件にするなら、これだけ使えばいいですね。
OSのメトリクス
ただ、OSの総メモリ容量がCloudWatch Metricsにないので、(MemAvailable / 総メモリ容量) みたいに割合で条件にしたい場合は、取ってくるか……インスタンスタイプに対応する固定値で送りつけるか、しないとです。ここでは、OSのメトリクスを取得することにします。
OSとしてのメモリ関連含むメトリクスは CloudWatchLogs にてちょっと面倒な形で提供されています(参考:拡張モニタリング)
この雰囲気からわかる通り、任意のホストのデータを取得するのが難しい構成になっているので、APIで取得する時はちと工夫が必要になります。
ただ面倒ではあるけど、よく見るCPUの user や idle, メモリの total や free などを取得できるので、必要ならやるっきゃないやつです。
Python例
私は親切だけど面倒がりなので、手元のPythonを適当にコピペして紹介しておきます。こんな感じでどうでしょう。必要なOSメトリクスを定義しておいて……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
self.os_metrics = { 'cpuUtilization': { 'idle' : 'cpu-idle', 'user' : 'cpu-user', 'wait' : 'cpu-wait', 'nice' : 'cpu-nice', 'steal' : 'cpu-steal', 'system': 'cpu-system', 'irq' : 'cpu-irq', }, 'loadAverageMinute': { 'fifteen': 'load-longterm', 'five' : 'load-midterm', 'one' : 'load-shortterm', }, 'memory': { 'buffers' : 'memory-buffered', 'free' : 'memory-free', 'cached' : 'memory-cached', 'active' : 'memory-active', 'inactive' : 'memory-inactive', 'pageTables': 'memory-pagetables', 'dirty' : 'memory-dirty', 'mapped' : 'memory-mapped', 'slab' : 'memory-slab', 'total' : 'memory-total', }, } |
直近に更新されたLogStreamをチェックして、それぞれから最新のLogEventを1件取得し、JSONをロードして各ホストのデータとしてまとめる、と。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
def getCloudWatchLogEvents(self): if self.log_events is not None: return self.log_events cloudwatchlogs = self.getCloudWatchLogs() res = cloudwatchlogs.describe_log_streams( logGroupName = "RDSOSMetrics", orderBy = "LastEventTime", descending = True, limit = 30, )['logStreams'] active_time = calendar.timegm(datetime.utcnow().timetuple()) - 3600 streams = [x['logStreamName'] for x in res if (x['lastEventTimestamp'] / 1000) > active_time] instances = self.getInstances() results = {} for stream in streams: res = cloudwatchlogs.get_log_events( logGroupName="RDSOSMetrics", logStreamName=stream, limit=1, )['events'] if not res: return results values = json.loads(res[0]['message']) hostname = values['instanceID'] if hostname in results or hostname not in instances: continue results[hostname] = {} for category,metrics in list(self.os_metrics.items()): for metric,name in list(metrics.items()): value = values[category][metric] if category == 'memory': value = value * 1024 results[hostname][name] = value self.log_events = results return results |
これは全ホストのメトリクスを送るのが目的なので、こうなりましたが、任意のホストデータを探すにしても、似たようなことして途中で止める、とかになるでしょう。
まぁどんなことしてるかの雰囲気だけ伝われば、ということで。
メモリのメトリクス
これで、メモリの total が取得できるので、分母に使えるようになります。他に昔ながらの free + cached + buffers もあるのですが、最近だと素直に MemAvailable を使ったほうがいいですね(参考:freeの出力が大幅改善された話)
そのあたり実際に比べてみると、こんな記録がとれたことがあります。
昔からこの3つの足し算を空き容量(=明け渡し可を含む)とするのって怪しさ満点でしたが、1GBも差があるので、まぁそういうことだったんでしょう。
MySQLのメモリと接続数の計算
次にさらなる基本の確認ですが、MySQLのメモリ使用量はパラメータによって計算できるもので、となっています(参考:MySQL でメモリ不足が発生したときのパラメータチューニング やそこの参考文献リンク)。my.cnf を見て計算してくれるツールとかもあるので、あまりこの辺を知らない人は知っておくべきところですね。
で今回の話で大事なのは、使用メモリ量の変動は接続数の変化によって起きる、という点です。Global の方は最大値が一定ですからね。
でも Thread だって max_connections があるから最大値決まるじゃん?って思うじゃないですか。昔はそれでよかったんですよ、データベースサーバーのスペックがだいたい固定だったから。クラウドになってからは、複数のDBがそれぞれ適したインスタンスタイプを選択できるようになり、事情が変わってきました。
max_connections の今
Aurora はパラメータは基本いじる必要はないのですが、最大接続数 max_connections はデフォルトだと今はこのようになっています。昔書いた記事(Amazon Auroraを始めるためのパラメータ資料 | 外道父の匠)の時と少し変わっています。- GREATEST({log(DBInstanceClassMemory/805306368)*45},{log(DBInstanceClassMemory/8187281408)*1000})
max_connections がインスタンスタイプが large から上がっていくごとに、1000, 2000, 3000, 4000, 5000 となっていくとして、これだとタイプが混在した時に問題が発生します。
クライアントの接続数
ここで接続数はどう計算するかの復習です。メインはWEB/APサーバー群であり、基本は
他にジョブ系や手動クライアント、裏側の管理システムからの接続などがありますが、今はメインだけで考えます。
例えば、WEBサーバー 50台、1台あたりvCPUs=16でworker数を24 にしたとします。50 * 24 = 1200 接続が各DBサーバーに対して発生することがわかります。
DB(A) を large, DB(B) を xlarge, DB(C) を 2xlarge にしたいと思ったとき、これはデフォルト設定では実現できません。large が最大1000, xlarge が2000 なので、large に対する接続でエラーが発生するからです。
これがリリース前ならば、インスタンスタイプやデータの分割条件を再考したり、いくらでも調整できるのですが、リリースから結構時間が経過して、費用削減のために一部のDBインスタンスのスペックを下げようとなった時に、そういうタイプのズレが生じる・生じさせる必要が起きる、という状況とします。
この場合、メモリが足りるのであれば、max_connections を2000や4000など固定値に設定することが簡単です。それでエラーなく費用削減ができるのであれば、そうするべきで、この流れを長い目で見たとき、最初から4000や8000など固定値にすることも間違いではないということです(実際にそうしてたりします)。
OS空きメモリを考える
Auroraの空きメモリ
再掲になりますが Amazon Auroraを始めるためのパラメータ資料 | 外道父の匠 で計算した通り、インスタンスタイプごとに余裕をもたせるメモリ容量が異なっており、(理解ありきですが)多少は設定値を無茶しても大丈夫な状態と言えます。特にタイプが大きいほど猶予があるので、むしろ活用してやるくらいの気概があっても変じゃないと思います。いや、それはいいすぎか。ただ、max_connections を共通固定値にしたとき、小さいインスタンスタイプのDBは、もし本来のデフォルト値以上を超えた時にメモリオーバーするリスクがあります。キャパオーバーすると通常は OOM Killer に殺られるわけですが…… Aurora の場合はどうなるか試してないし見えないのでわかりません。
とにかく、キャパオーバーのリスクを検知する必要はある、ということです。
現時点での台数は安全である
リスクのある設定状態のDBに対して、WEBサーバーを一定台数起動したとき、もしくはAutoscalingで最大台数まで起動したとき、接続エラーやメモリオーバーが発生しないことは、当然ですが確定しておく必要があります。また、その状態は空きメモリ容量のアラート閾値にひっかからない条件である必要もあります。
メモリアラートが発生した状況とは
閾値が5GB以下とか、10%以下 にしたとして、その状況がどういう状態かというと、それ以上WEBサーバーを増やすことが危険であるという指標になります。突然アクセス増加してもWEBサーバーを増設して対応できない、Autoscalingの最大値を増やすことができない、となり、先に接続数周りの調整を余儀なくされるので、対応に遅延が発生すること間違いなしです。
DBメモリの調整
WEBサーバーを増やしたことでアラートになったのか、DBサーバーを費用削減のためにスペックダウンしてそうなったのか、メモリ空き容量がピンチになったらやることは……DBインスタンスタイプを上げる
これが一番簡単ですね。というかWEBサーバーを増やすほど盛り上がったのなら、こちらも必要があるなら上げるのが当然で、嬉しい悲鳴系。WEBサーバー台数を減らす
トラフィックやCPUリソースのグラフ波形を確認するなどして、現在のWEBサーバーの台数が適切かを検討します。もし多いと判断すれば、単に台数を減らすことで接続数は減りますし、WEBサーバー費用の削減にもなるので一石二鳥です。インスタンスタイプを1つ上げて台数を半分にする、という対応でも台数は減りますが、適切な接続数はvCPUsの数を元に計算するので、1台あたりの接続数が増えることになり結果は変わりません。
ただ、CPU性能が向上するパターンでのタイプ変更なら、リクエストあたりの処理が短くなって必要な同時接続数が減る……という意味では、ほーーーんの少しだけマシにできるかもしれません。
アプリケーション接続数を減らす
アプリケーション・リソースと同時接続数の試算例 | 外道父の匠 で触れましたが、WEBサーバー1台あたりの適切な受け入れ接続数というものがあります。すっげー大雑把に言うなら、最低でもvCPUsと同じ数。多すぎてもvCPUsの倍くらいまで。だいたいは1.5倍くらいが目安にはなると思います。意味としては、CPUリソースを使い切ることができつつ、過剰にメモリを消費しない、といったところ。
もし、理解の低い状態で設定されたり、なんとなくテンプレを使い回されて、結構多めのアプリケーション・プロセス数になっていたとしたら、プロセス単体・合計の消費メモリ容量や、平均レスポンスタイムから、多少の余裕はありつつも十分な接続数を試算します。
それを設定に反映することで、サーバー1台あたりの接続数が減り、掛ける台数分の接続数がDBサーバーから減少し、DBの空きメモリも増加する……!?
接続数の削減後にDBメモリ消費量が減らない
……かと思いきや、どっこい空きません。実際の記録
クライアントの接続数を調整し、空きメモリ増加を期待した48時間範囲のグラフです。CloudWatch Metricsより、上がDatabaseConnections、下がFreeableMemoryです。3000以上接続数が減ったのに、メモリが微動だにしていないことがわかります。
他の接続関連の設定
接続に関する設定に、interactive_timeout と wait_timeout があるのですが、まさかなと思って観察してみたけど、これらの秒数が経過したあともメモリに変化はありませんでした。
MySQL memory not release after connection closed !!
2年以上前の内容ですが、5.6だし中の人の解答っぽいので、これが真実でしょう。要は、MySQLとしてはメモリをリリースしてるけど、OSが高速化のために残しちゃってるから、MySQLのせいじゃねンだわ。って感じです。
AuroraはMySQLと似て非なるものですが、この辺までは手を加えなかった……クリティカルではないと判断した、といったところでしょうか。DBユーザー側としては、すっきり開放されることが心地よいですが、まぁ仕方ないですね。
AuroraはThread Pool (2020/11/24 22:00追記)
Aurora の BlackBelt のこの辺に書いているのですが実質的には、最大使用メモリはキャパオーバーしない前提で使うので、解放されないことで困ることはほぼないのですが……
監視の事情では、一度アラート閾値を超えた使用量になった時に、接続を削減してアラート解除しようとなるものの、空き容量が増えないせいでアラート解除条件を満たせない、という困りごとが発生します。その場合、アラートを一時的に無効化したり、閾値を変更したり、という話になるので、あまりイケてない運用になってしまいます。
再起動は大正義
他に色々探しましたけど、グッとくるものもはありませんでした。結局、Auroraインスタンスを再起動したらメモリ消費量は想定通りの減少した値になりました。って冷めた結論でフィニッシュです。
リソースの増加方向は、きっちり計算しないと死ねますが、こういう減らす方向の場合は、クライアント減らしてからサーバーを減らせばだいたい安全だったりするし、多少の数値的なゴミが残っても死ぬわけじゃないんで、次の機会に整値できればいいし、しなくてもえぇやんってところもあるので、こんなもんでしょう。
インフラやDB管理者にとっては接続数って結構上位にくる重要要素なのですが、あまり知らない人にとっては、ほどよい勉強用の事例としてよかったんじゃないかな~と思って書いてみました:-)