Mackerel MCPを使ってAIでISUCONを解いてみる
Mackerel のMCPをご存知だろうか。GitHub - mackerelio-labs/mcp-server: Local MCP (Model Context Protocol) server for integrating LLMs with Mackerel.で開発が進められていて、現時点ではホストメトリックの取得やAPM系の統計情報の取得ができたりする。このMackerel MCPを利用してISUCONをAIで解いてみて、どこまでスコアを伸ばせるかを試すのがこの記事の主題。今回はISUCON9の予選を題材として使う。
結論としては、初期状態の1810点から最終的に50280点まで改善できた。Docker上で動かしたため、環境差異によるスコア変動はあると思うが、ISUCON9 オンライン予選 全てのチームのスコア(参考値) のスコアと見比べると2位ぐらいまで改善できた。(1位のチームは凄すぎる…)
以降では、Mackerel MCPを使えるようにするための準備について説明し、AIがどのようにMackerel MCPおよび他ツールを使いながら改善していったかを振り返る。
Mackerel MCPを使えるようにする
Mackerelにデータを投稿し、MCPを介して取得できるようにするための準備を行う。
Mackerelに投稿する
ISUCONを改善するために次の2つをMackerelに投稿する。
1. DockerのCPU・メモリ使用率と言ったリソース使用量を知るためのホストメトリック
mackerel-agentをコンテナとして起動し、enable_docker_plugin=1 にすることでDockerのホストメトリックを投稿することができる。
2. アプリケーションのパフォーマンスを知るためのOpenTelemetryトレース
適当にトレースを送信すると、ベンチマークを回しまくるとお金が掛かりそう。ということで、遅い場合やエラーになった場合にのみ送信するテイルサンプリングを行うためにotel-collectorを利用した。
テイルサンプリングの設定や計装はAIがよしなにやってくれた。ただ、dbx.Get を dbx.GetContext のようにcontextを使う形に書き換えないとDBスパンがトレースに紐づかないので、これは明示的に指示した。
Mackerel MCPを呼び出せるようにする
Dockerを利用するかnpxを利用するか選べる。どちらでも良いので今回はDockerを使った。
claude mcp add-json mackerel '
{
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-e",
"MACKEREL_APIKEY",
"ghcr.io/mackerelio-labs/mcp-server:latest"
],
"env": {
"MACKEREL_APIKEY": "${MACKEREL_APIKEY}"
}
}
'
これで MACKEREL_APIKEY 環境変数にRead/Write権限を持つAPIキーをセットすればMackerel MCPを介してデータが取得できるはず。
AIによるISUCON
今回はClaude Codeにプロンプトを与える形で進めた1。AIによる改善ログを眺めると、段階を経て改善している様子が見て取れた。
与えたプロンプト
指示を端的にまとめるとこうなる。
- 目標: ISUCONのスコアを最大化する
- ツール: Mackerel MCPとその他必要に応じて使う
- 記録: どのように発見し、何を根拠に、どう改善したか、スコアはどう変化したかを記録する
以上の指示を31回繰り返して50280点に到達した。
ISUCON特有のプロンプトとして、筆者のISUCON参加経験を元に工夫した点が2点ある。
1. 1つずつ変更を加えて、振る舞いが変わらないことを確認しながら進める
複数の改善案を一気に実装するとベンチマークがコケたときにどこが原因なのかわかりにくくなり、調査に時間がかかるという経験を反映している。AIくんは振る舞いを変えずに変更することが得意なのか、ベンチマークがコケることはほぼなかったので、もしかしたらこの指示はなくてもうまくやってくれたかもしれない…
2. どういった変更がスコアに効くのかマニュアルをちゃんと読む
ボトルネック潰しに奔走してしまい、スコアにダイレクトに効く変更を加えられなかった経験を反映している。マニュアルをAI向けに整形したりせず、そのまま読んでくれとしか言っていないが、ちゃんと理解して改善してくれていた。
第一段階: 統計情報から改善
Mackerel MCPで取得できるHTTP統計情報・DB統計情報を利用した改善が行われた。この改善によって、DBクエリのボトルネックは大体解消された。スコアはそこまで伸びきらず、15160点で止まっていた。
ここで行われた改善は次のようなものがある。
- DB統計情報:
SELECT * FROM users WHERE id = ?が43,687回実行、73,320msかかっている- → 更新時のキャッシュ無効化付きでユーザーをメモリにキャッシュ
- HTTP統計情報:
POST /buyが5.3%エラー率(取引エンドポイント中最高)、P95 が999msかかっている
ログを観察していると、フルスキャンするクエリを改善するために EXPLAIN しているところがあった。Mackerel MCPでは EXPLAIN の情報は取れないので、データベースに直接繋いでいた。
第二段階: マニュアルから改善
第一段階で明らかなボトルネックは解消されたからか、第二段階ではマニュアルを読んでスコアを向上させる動きが見られた。この改善によってスコアが大幅に上がり、43500点まで上がった。
AIはマニュアルを読んで Campaign という値を変更することで、リクエストが増え、そのリクエストを処理できればスコアが上がるということを理解して改善していた。
効果がどこまであったかはわかっていないが、「大技」subagentというのを作って改善してもらっていた。第一段階までの改善は「小さい変更で確実に」という志向に見えていたので、「複雑でも大きい改善」を志向する「大技」subagentを作ったという経緯。
特に Campaign=3 を安定化させる変更は大技で、自分だったら思いつくか怪しいし、思いついても時間内に実装しきれる自信はない…
第三段階: プロファイルから改善
ここまでで大体のボトルネックは解消されているが、どの処理がCPUやメモリを使っているかということは知らない状態。なので、プロファイルを取得して改善するよう明示的に指示した(プロンプトが悪くて自律的にやってくれなかった)。この改善ではスコアはあまり伸ばせず50280点で止まった。
プロファイルはMackerelでは取得できないので、pprof を利用して取得した。
プロファイルの結果から、bcrypt でCPUを83%消費していることがわかった。しかし、暗号強度を弱めるわけにも行かず、キャッシュ程度の手しか打てずにスコアはあまり伸びなかった。
解説にもある通り、スコアを更に伸ばすにはログイン処理だけを行うサーバを用意すればよい。しかし、与えたプロンプトではどうしてもそういったことはしてくれなかった… Mackerel MCPでホストメトリックを取得して、CPU使用率に空きがあることがわかってもログイン処理だけ分離するということはやってもらえなかった。
感想
これまでISUCONに参戦するときは、まずはslow queryを出すようにして、pt-query-digest の設定をして、アクセスログを取って、alp で解析して… とやっていたことがMackerelにトレースを送るだけで同じような情報が取れるのは便利だと思う。
そして、統計情報があれば明らかなボトルネックは潰せるということもわかった。なので、新規開発時のようにどこにボトルネックがあるかわからないという状況では、先にo11yを整えておくとボトルネック潰しが効率的にできそうだと思った。
一方、プロファイルでアプリケーションのどこがリソースを利用しているのかもMackerelで取れると便利だなと感じた。ISUCONではプロファイルは任意のタイミングで行えばよいが、本番運用している場合はいつプロファイルが必要になるかわからないので継続的に取っておきたい。
宣伝
今回のISUCONの改善のためにMackerelに送ったスパン数は405万スパンでした。Mackerelでは500万スパン/月 が無料枠としてもらえるので、今回の検証は無料でできました。
ぜひMackerel MCPを使ってみてください。スコア対決しましょう!
GraphQL Federationで段階的に新システムに移行する戦略
GraphQL Federationで旧システムへのリクエスト数を増やさないための移行順序 - ぴょこぴょこブログ の続き。
旧システムにリクエストを増やさないようにしながら段階的に新システムに移行する戦略を考えている。
結論としては、次の3ステップを繰り返すことで旧システムにリクエストを増やさないように段階的に移行できると考えている。
- 非エンティティフィールド(scalar, Query, Mutationのフィールド)を新システムに移行する
- エンティティフィールドを
@shareableを付けて新システムに移行する - エンティティフィールドとそのエンティティのフィールドが新システムに移行されたら
@shareableをやめて旧システムから削除し、新システムのみにする
具体例
スキーマが次のようになっていて、Tag エンティティを移行したいとする。
# Itemエンティティ
type Item @key(fields: "id") {
id: ID!
name: String!
tags: [Tag!]!
}
# Tagエンティティ
type Tag @key(fields: "id") {
id: ID!
category: String!
item: Item
}
type Query {
getItem(id: ID!): Item
}
1. 非エンティティフィールドの Tag.category を新システムに移行する
query GetItem {
getItem(id: "1") {
tags {
category
item {
name
}
}
}
}
というクエリが来ると、currentへのリクエストは1回だけ。newへのリクエストも1回だけ。
query GetItem__current_service__0 {
getItem(id: "1") { # Item に辿れる
tags {
__typename
id
item {
name
}
}
}
}
query GetItem__new_service__1($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Tag {
category
}
}
}
2. エンティティフィールドの Tag を @shareable を付けて新システムに移行する
@shareableを付けてcurrentとnewの両方のサブグラフで解決可能であることをRouterに伝える。
type Item @key(fields: "id") {
id: ID!
name: String!
tags: [Tag!]! @shareable
}
1で Tag.category を移行しているので、次のクエリが来た時、Item.tags の解決はnewで行われる。
query GetItem {
getItem(id: "1") {
tags {
category
}
}
}
Routerはリクエスト数が少なくなるように実行計画を立てる。
Tag のフィールドで未移行のものにアクセスする場合は Item.tags の解決をcurrentで行ったほうがリクエスト数が少なくなるならcurrentで解決される。
3. エンティティフィールドとそのエンティティのフィールドが新システムに移行されたら @shareable をやめて旧システムから削除し、新システムのみにする
エンティティフィールドとそのエンティティのフィールドが新システムに移行されたnewのスキーマは次のようになる。
# Itemエンティティ
type Item @key(fields: "id") {
id: ID!
name: String!
tags: [Tag!]! @shareable
}
# Tagエンティティ
type Tag @key(fields: "id") {
id: ID!
category: String!
item: Item @shareable
}
type Query {
getItem(id: ID!): Item
}
こうなるとどのようにクエリしてもcurrentで解決するものがないので全てnewで解決される。 currentは不要になったので削除できる。
注意点
二重管理に注意
@shareable が付いている限りは両方のサブグラフで解決できる必要があるので、その期間はcurrentとnewで実装が二重管理になる。
修正が同期されないと不整合が起きうるので、静的解析ツールを作るなどしてcurrentのロジックの修正がnewに波及するかを判断できるようにしておくのが良いと考えている。
移行ステップの順番に注意
移行ステップの1と2は順番に行われることを期待している。
逆になると、全てのフィールドが移行されるまでRouterはcurrentでしか解決せずビッグバン的なリリースになる。
@shareable が付いていてcurrentで解決できる & フィールドがcurrentにしかないのでRouterはnewで解決するメリットがないと判断する。
GraphQL Federationで旧システムへのリクエスト数を増やさないための移行順序
GraphQL Federationを使って段階的に旧システムから新システムに移行しようとしている。 移行に際して、旧システムにリクエストが増えることはオーバヘッド増加の観点で避けたい。 そこで、どのような移行順序なら旧システムへのリクエストを減らせるか調査した。
結論から言うと、グラフとして辿れるならリクエストは増えない。 逆にグラフの途中で新システムへのリクエストが必要な場合に、旧システムへのリクエストが増える。 つまり、エンティティを返すフィールドを考え無しに移行すると旧システムへのリクエストが増える。
具体例を用いて説明する。
前提
旧システムをcurrent、新システムをnewと呼ぶ。
currentのスキーマは次のようになっているとする。
# Itemエンティティ
type Item @key(fields: "id") {
id: ID!
name: String!
tags: [Tag!]!
}
# Tagエンティティ
type Tag @key(fields: "id") {
id: ID!
category: String!
item: Item
}
type Query {
getItem(id: ID!): Item
}
currentへのリクエストが増えない例
グラフとして辿れる場合はcurrentへのリクエストが増えない。今回だと、Item.name をnewに移行しても Item に辿ることに影響しないので問題ない。
次のようなクエリが呼ばれたとする。
query GetItem {
getItem(id: "1") {
name
tags {
item {
name
}
}
}
}
このとき、Item -> Tag -> Item とグラフを辿る必要がある。Item.name を移行してもグラフを辿ることができる。
currentへのリクエストは1回だけ。
query GetItem__current_service__0 {
getItem(id: "1") { # Item に辿れる
__typename
id
tags { # Tag に辿れる
item { # Itemに辿れる
__typename
id
# nameはnewに移行したのでない
}
}
}
}
newへのリクエストは2回。
query GetItem__new_service__1($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Item {
name
}
}
}
query GetItem__new_service__2($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Item {
name
}
}
}
currentへのリクエストが増える例
グラフとして辿れない場合はcurrentへのリクエストが増える。今回だと、Tag.item をnewに移行すると、currentが Item に辿れずリクエストが増える。
次のようなクエリが呼ばれたとする。
query GetItem {
getItem(id: "1") {
name
tags {
item {
name
}
}
}
}
このとき、Item -> Tag -> Item とグラフを辿る必要がある。Item.tags を移行すると Tag -> Item の Item に辿れない。
currentへのリクエストは2回。
query GetItem__current_service__0 {
getItem(id: "1") {
__typename
id
name
tags {
__typename
id
}
}
}
query GetItem__current_service__2($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Item {
name
}
}
}
newへのリクエストは1回。
query GetItem__new_service__1($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Tag {
item {
__typename
id
}
}
}
}
Item -> Tag -> Item の解決が current -> new -> current になり、currentへのリクエストが2回になった。
Query/Mutationを移行すると?
考え方は変わらない。
エンティティを返すフィールドを current に残したままと仮定すると、Query/Mutationを new に移行すると new -> current (-> new)1 となる。
なので、current へのリクエストは増えない。
-
(->
new) はエンティティ以外を返すフィールドが移行済みの場合のパスを示す。↩