상황
에이전트 게이트웨이처럼 들어온 메시지를 받아 작업을 시작시키는 서비스가, 보조 명령줄 도구를 호출할 때 동시에 몇 개만 돌도록 작은 풀을 둔다고 하자. 같은 서비스가 백그라운드 작업의 상태를 짧은 주기로 확인하는 관찰자도 돌린다. 어느 날 사용자는 "답이 늦게, 끝에 한꺼번에 온다"고 말하고, 확인해 보니 호스트의 load average도 높다.
흔한 오진
load average가 높으니 호스트가 과부하라는 결론을 내리고, 동시에 돌고 있는 작업 수를 줄이라는 처방을 내놓기 쉽다. 그런데 CPU 사용률을 보면 거의 절반이 유휴 상태다. load average는 실행 대기뿐 아니라 여러 종류의 대기를 한 숫자로 뭉쳐 보여 주는 지표라서, 그것만으로는 "누가 무엇을 기다리는가"를 알 수 없다. 이 단계에서 부하를 탓하면 증상은 그대로 남고 멀쩡한 작업만 멈춘다.
실제로 일어난 일
요청이 도착한 시각과 처리가 시작된 시각의 차이를 재 보면, 한가할 때는 2초 안팎이던 것이 문제 구간에서는 수십 초, 길게는 2분 넘게 벌어져 있다. 그 사이 보조 도구 풀의 슬롯은 항상 꽉 차 있었고, 차지한 쪽은 수백 밀리초마다 상태를 묻는 백그라운드 관찰자들이었다. 사용자 요청에 필요한 호출은 한 번에 1~3초짜리인데, 선입선출 줄에서 폴링 호출들 뒤에 계속 밀렸다. CPU가 아니라 슬롯이 병목이었다.
무엇을 재야 하는가
세 가지만 있으면 된다. 첫째, 요청별 도착 시각과 처리 시작 시각의 차이. 둘째, 공유 풀의 동시 점유 수와 누가 점유하고 있는지. 셋째, 그 풀을 통과하는 호출 하나의 실제 소요 시간. 대기 시간은 길고 호출 시간은 짧으며 풀이 늘 가득 차 있다면, 원인은 연산량이 아니라 줄 서는 방식이다. 호스트 쪽 의심이 남으면 load average 대신 유휴 비율, 시스템 시간, 문맥 전환 수를 본다.
고치는 방향
사용자와 직접 맞닿은 호출에는 우선순위를 주고, 백그라운드 호출이 쓸 수 있는 슬롯 수에 상한을 둔다. 이미 다른 경로로 변화를 통지받는 관찰자는 폴링 주기를 느리게 바꾼다. 풀 크기를 늘리는 것만으로는 부족하다. 폴링은 늘어난 슬롯도 금방 채우기 때문이다.
확인 방법
수정 후에 같은 지표를 다시 잰다. 도착에서 시작까지의 대기가 한가할 때 수준으로 돌아왔는지, 부하가 걸린 구간에서도 사용자 호출이 폴링 뒤에 서지 않는지 본다. "이제 빨라진 것 같다"는 느낌이 아니라 전후 두 구간의 대기 시간 숫자로 끝을 확인한다.
The setup
Picture a service, such as an agent gateway, that accepts incoming messages and kicks off work, and that runs helper command-line calls through a small pool so only a few execute at once. The same service also runs observers that check the status of background jobs on a short interval. One day a user says replies arrive late and all at once at the end, and a quick look shows the host's load average is high.
The usual misdiagnosis
High load average, so the host must be overloaded, so cut the number of concurrent jobs. But CPU utilization shows the machine nearly half idle. Load average folds several kinds of waiting into a single number, so on its own it cannot tell you who is waiting for what. Blaming load at this point leaves the symptom in place and stops perfectly healthy work.
What was actually happening
Measure the gap between when each request arrived and when work on it began. In quiet periods it was around two seconds; in the bad window it stretched to tens of seconds and, at worst, past two minutes. Throughout that window every slot in the helper pool was occupied, and the occupants were background observers asking for status every few hundred milliseconds. The calls a user request needed took one to three seconds each, but they kept landing behind the polling calls in a first-in, first-out line. The bottleneck was slots, not CPU.
What to measure
Three numbers are enough. First, per request, the gap between arrival and start of work. Second, how many slots of the shared pool are occupied at once, and by whom. Third, the real duration of a single call through that pool. If waits are long, calls are short, and the pool is always full, the cause is how work queues, not how much compute it needs. If you still suspect the host, look at idle percentage, system time, and context switches instead of load average.
Which way to fix it
Give calls that sit directly in a user's path priority, and cap how many slots background calls may take. Observers that already get change notifications some other way should poll on a slower fallback cadence. Just enlarging the pool is not enough, because polling quickly fills the new slots too.
How to check
After the fix, take the same measurements again. Confirm that the arrival-to-start wait is back to quiet-period levels and that user calls no longer queue behind polling even while the host is busy. Close the issue on the before-and-after wait numbers, not on a feeling that things seem faster now.
场景
设想一个服务,比如代理网关:它接收传入的消息并启动工作,调用辅助命令行工具时通过一个小型池子限制同时只跑几个。同一个服务还运行着一些观察者,以很短的间隔去查询后台任务的状态。某天用户反映“回复来得很慢,最后一下子全冒出来”,一查,主机的 load average 也很高。
常见的误诊
load average 高,所以主机过载,所以要减少并发任务——这条推理来得很顺。可是看 CPU 利用率,机器有将近一半是空闲的。load average 把好几种等待揉成了一个数字,单凭它无法知道“谁在等什么”。这时候怪罪负载,症状不会消失,反而会把本来正常的工作停掉。
实际发生了什么
去量每个请求从到达到开始处理之间的时间差:空闲时大约两秒,出问题的时段拉长到几十秒,最长超过两分钟。这段时间里,辅助工具池的槽位始终是满的,占着槽位的是每隔几百毫秒就来问一次状态的后台观察者。用户请求所需的调用每次只要一到三秒,却在先进先出的队列里一再排在轮询调用后面。瓶颈是槽位,不是 CPU。
该量什么
三样东西就够了。第一,每个请求到达与开始处理之间的时间差。第二,共享池同时被占用的槽位数量,以及占用者是谁。第三,经过这个池子的单次调用的实际耗时。如果等待很长、调用很短、池子又一直是满的,那么原因在于排队方式,而不在于计算量。如果仍怀疑主机本身,就去看空闲比例、系统态时间和上下文切换次数,而不是 load average。
修复方向
给直接面向用户的调用更高优先级,并给后台调用能占用的槽位数设上限。那些已经能通过其他途径收到变更通知的观察者,把轮询改成较慢的兜底频率。只把池子调大是不够的,因为轮询很快就会把新增的槽位也填满。
如何确认
修复之后用同样的指标再量一次:从到达到开始的等待是否回到了空闲时段的水平,主机忙碌时用户调用是否仍会排在轮询后面。用前后两个时段的等待时间数字来收尾,而不是凭“好像快了”的感觉。
状況
エージェントゲートウェイのように、届いたメッセージを受けて作業を始めさせるサービスが、補助のコマンドラインツールを呼ぶときに同時実行数を小さなプールで絞っているとする。同じサービスは、バックグラウンド作業の状態を短い間隔で確かめるオブザーバーも動かしている。ある日ユーザーが「返事が遅く、最後にまとめて届く」と言い、調べるとホストの load average も高い。
よくある誤診
load average が高いからホストが過負荷だ、だから同時に動いている作業を減らせ、という処方に飛びつきやすい。ところが CPU 使用率を見ると、マシンは半分近くアイドルだ。load average は複数種類の待ちを一つの数字にまとめた指標なので、それだけでは「誰が何を待っているのか」は分からない。この段階で負荷のせいにすると、症状は残ったまま、健全な作業だけが止まる。
実際に起きていたこと
リクエストごとに、到着時刻と処理開始時刻の差を測ってみる。暇な時間帯は 2 秒前後だったのが、問題の時間帯には数十秒、長いものでは 2 分を超えていた。その間、補助ツールのプールのスロットは常に満杯で、占めていたのは数百ミリ秒ごとに状態を問い合わせるバックグラウンドのオブザーバーだった。ユーザーのリクエストに必要な呼び出しは一回 1〜3 秒なのに、先入れ先出しの行列でポーリングの呼び出しの後ろに押し出され続けた。ボトルネックは CPU ではなくスロットだった。
何を測るべきか
三つあれば足りる。一つ目は、リクエストごとの到着から処理開始までの差。二つ目は、共有プールが同時に何スロット埋まっていて、誰が占めているか。三つ目は、そのプールを通る呼び出し一回の実際の所要時間。待ちが長く、呼び出しは短く、プールが常に満杯なら、原因は計算量ではなく並び方にある。それでもホストを疑うなら、load average ではなくアイドル率、システム時間、コンテキストスイッチ数を見る。
直し方の方向
ユーザーの経路に直接ある呼び出しに優先度を与え、バックグラウンドの呼び出しが使えるスロット数に上限を設ける。別の経路ですでに変更通知を受け取れるオブザーバーは、ポーリング間隔を遅いフォールバック周期に変える。プールを大きくするだけでは足りない。ポーリングは増えたスロットもすぐに埋めてしまうからだ。
確かめ方
修正後に同じ指標をもう一度測る。到着から開始までの待ちが暇な時間帯の水準に戻ったか、ホストが忙しい時間帯でもユーザーの呼び出しがポーリングの後ろに並ばなくなったかを見る。「速くなった気がする」という感覚ではなく、前後二つの時間帯の待ち時間の数字で締めくくる。