ClickStack & HyperDX HOLs
GitHub

Host metrics and process-down methods on Linux and Windows EC2

Two questions for a fleet of thousands of EC2 hosts sending host metrics every minute to one central OTel gateway, and the Windows question that decides how game-server processes are watched:

  1. Volume per host — data points per minute (= rows in ClickStack's metrics tables) and bytes on the wire (OTLP/HTTP, gzip), Linux and Windows (#127).
  2. Process-down methods on Windows — what each way of watching a server process reports when the process stops, under which service account.
  3. One gateway for many networks — the source network reaches the gateway only through an NLB-backed VPC endpoint service (the shape of "many accounts send to one gateway account").

Verified on 2026-11-06, ap-northeast-2, one AWS account, otelcol-contrib 0.155.0: Linux t4g.small (Amazon Linux 2023 arm64, 2 vCPU) as host and gateway, Windows Server 2022 Datacenter t3.medium (2 vCPU, 1 adapter, 1 disk, 1 volume). Apply 04:43Z → destroy 05:27Z. Volume window 30.22 minutes (04:52:23–05:22:36Z); stop/start test 04:52–05:06Z. Raw results: results/.

1. Volume per host

Set (60 s interval) points / scrape series per-vCPU points wire bytes / min bytes / point
Linux base (linux-host scraper set) 130 131 16 per vCPU (32) 3,856 28.9
Windows base (same minus processes) 68 68 8 per vCPU (16) 2,912 43.1
process scraper, per matched process Windows 6 · Linux 7
windows_service, per service 1
check script (method D), per instance 1

2. Process-down methods on Windows

Two stand-in game server instances: GameServer.exe (a copy of the collector binary, so it is a real service) as the Windows service GameServer (gs1) and as a SYSTEM scheduled task (gs2), told apart by their config file on the command line. Events: S1 gs1 service stopped (04:52:32Z), S2 started (04:56:36Z), S3 gs2 process killed (04:59:40Z), S4 gs2 task started (05:03:45Z). Per-minute record: results/2026-11-06-timeline.json.

Method gs1 service stopped gs2 task process killed Tells instances apart Verdict
A hostmetrics process, include by name, LocalSystem gs1's series disappears gs2's series disappears yes — process.command_line, process.pid, process.owner good for CPU/memory per instance; down-detection needs an expected list in the alert
A under NT AUTHORITY\LocalService — — — sees no SYSTEM process: "Access is denied" reading their executable, so the name filter cannot match
B windowsperfcounters, object Process, instances GameServer, GameServer#1 the survivor is renamed GameServer; count 2 → 1 same no — #n follows start order, not identity; "negative value" error at each change not for process-down
C windows_service explicit 1 (stopped) in the same minute nothing (not a service) per service name best when the game server is a service
D check script → OTLP, expected list explicit gs1 = 0 explicit gs2 = 0 per expected instance works for any launch method

The same stop/start test on Managed ClickStack (2026-11-06 08:10–08:58Z): managed/windows_prep.py, then terraform apply with managed = true, so the gateway also wrote every set to the database windows_hmv on the Seoul service, and bin/run.sh killtest (events: results/2026-11-06-events-managed.jsonl). The prepared dashboard showed the same four behaviours as the minute table above — service state 1 only while the gs1 service was stopped, the check script 0 for gs1 and then gs2, the process scraper starting a new PID line at every restart. Screens are in the notes article on this run. First attempt: the clickhouse exporter's CREATE DATABASE failed with HTTP 403 because the ingest user lacked that right; windows_prep.py now grants it.

How an alert sees these: labs/process-down-alerts — a group-by alert does not fire for a series that stopped (A, B), but does for an explicit value (C, D).

3. Where the common fields should live (schema bench)

The service taxonomy and host.name are what every fleet query filters on. Three tables with the same 16.8 M synthetic rows (4,000 hosts × 70 metrics × 60 minutes, ServiceName constant, like host metrics): A the exporter's default gauge schema (attributes in Map, bloom filters on map keys and values, ORDER BY (ServiceName, MetricName, Attributes, time)), B A plus MATERIALIZED columns host and svc_group with skip indexes, C an own schema (create_schema: false) with ORDER BY (ServiceName, MetricName, svc_group, host, time). clickhouse local 26.8.34.13 (the ClickStack 2.42.0 image) on Docker Desktop, 2026-11-06, two runs agree; schema-bench/, results/2026-11-06-schema-bench.txt.

Query A default B materialized columns C taxonomy in the sort key
one host, one metric, 60 min 253,952 rows · 0.89 MiB · 96 ms 253,952 · 0.21 MiB · 21 ms with the group in the filter: 8,192 · 0.02 MiB · 1.2 ms (host alone: 188,416 · 4.8 ms)
one group (200 hosts), one metric 253,952 · 1.74 MiB · 59 ms 253,952 · 0.14 MiB · 14 ms 24,576 · 0.03 MiB · 1.2 ms
one host, every metric 16.8 M · 57 MiB · 1,353 ms 16.8 M · 4.9 MiB · 52 ms group + host: 1.14 M · 0.5 MiB · 45 ms

The same bench on ClickHouse Cloud (the Seoul service Managed ClickStack reads: 26.8.1.608, 2 replicas), 2026-11-06. Read after every table had merged into one part — the first Cloud run (results/2026-11-06-schema-bench-cloud.txt) read the tables right after OPTIMIZE … FINAL returned, before the merges had settled, and overstated B's storage (+46 %) and understated its speed; it is kept as that run's record, not as the result. Settled numbers, run 2 of 2: results/2026-11-06-schema-bench-cloud-settled.txt. A1 is the newer default these services' tables carry (ORDER BY (ServiceName, MetricName, toStartOfHour(TimeUnix), cityHash64(Attributes), TimeUnix) + a TimeUnix minmax index).

Query A0 default (0.155) A1 newer default B materialized columns C taxonomy in the sort key
one host, one metric 253,952 rows · 5.68 MiB 229,376 · 5.13 MiB 253,952 · 3.40 MiB group + host: 8,192 · 0.12 MiB
one group, one metric 253,952 · 6.97 MiB 229,376 · 6.30 MiB 253,952 · 3.64 MiB 24,576 · 0.35 MiB
one host, every metric 16.8 M · 187.8 MiB · 572 ms 15.7 M · 175.4 MiB · 690 ms 16.8 M · 32.0 MiB · 20 ms group + host: 1.14 M · 3.26 MiB · 16 ms
storage 41.32 MiB 39.00 MiB 44.89 MiB (+8.6 %) 41.62 MiB

Logs on ClickHouse Cloud (schema-bench/cloud_logs_bench.py, results/2026-11-06-logs-bench-cloud.txt, 2026-11-06 08:22Z): 10 M rows, 200 services in 20 groups, 4,000 hosts, 60 minutes, 2 % ERROR. G0 is the current default logs schema (ORDER BY (toStartOfFiveMinutes(Timestamp), ServiceName, Timestamp), text indexes on the map keys/values and lower(Body)); G1 adds svc_group/host MATERIALIZED columns without an index; G2 puts the taxonomy first in the sort key.

Query G0 default G1 + materialized, no index G2 taxonomy in the sort key
one group's ERRORs 1.78 M rows · 13.7 MiB · 290 ms 10 M · 10.9 MiB · 152 ms 0.53 M · 1.0 MiB · 17 ms
one host 180 K · 2.4 MiB · 18 ms 10 M · 19.5 MiB · 173 ms 98 K · 0.33 MiB · 6 ms
hasToken(lower(Body), 'timeout') in a group 1.78 M · 274 ms 10 M · 539 ms 0.53 M · 32 ms
one service, last 15 min 74 K · 38 ms — 98 K · 8 ms

4. One gateway for many networks

Check (from) Result
source VPC → interface endpoint, port 4319 connected; every Windows set arrived this way
source VPC → gateway's private address 10.77.1.18:4319 not reachable (no route)
gateway VPC → Windows host 10.78.1.91:3389 not reachable (one direction only)

Both VPCs were in one account (allowed principal = that account). A second account changes only the principal on the endpoint service; that is not run here. Transit Gateway was verified in labs/privatelink-gateway.

5. The standard profile's install.ps1, end to end (#141)

Verified on 2026-11-07, ap-northeast-2, the same Windows Server 2022 t3.medium (Windows PowerShell 5.1.20348) and gateway, terraform apply -var windows_mode=install (stand-in game servers only, no agent). otel-profiles/profiles/windows-host install.ps1 pushed with its built config and process-check.ps1 through SSM Run Command (bin/install-e2e.sh), exporting OTLP/gRPC to the gateway's 4317 through the endpoint service. Apply 02:20Z → destroy 03:13Z. Raw results: results/2026-11-07-install-*.

Check Result
V1 install, otelcol-contrib 0.155.0 exit clean in 108 s (most of it the download). Service otelcol: Automatic, LocalSystem, failure actions restart 5 s / 30 s / 60 s, reset 1 day. Task otelcol process-check: SYSTEM, highest, every 1 min. Five machine variables and the API key set
V2 data at the gateway 73 points per minute (29 metric names) with host.name, host.id, os.type, cloud.*, host.type, deploy.platform and the EC2 tags ec2.tag.lab.service.domain / .group. Per-process metrics for both GameServer.exe instances (owner NT AUTHORITY\SYSTEM)
V3 service stopped (02:42:40Z) windows.service.status 1 for 02:42–02:44, 4 again from 02:45 (started 02:45:44Z); process scraper: new PID
V3 task process killed first run: process.check.up was wrong — one instance gs1 gs2 = 0 all the time (below). Fixed run (03:00:14Z): gs2 = 0 for 03:00–03:02 while gs1 stayed 1; 1 again after the task restart (03:02:49Z)
V4 collector process killed (02:52:55Z) Service Control Manager 7031 "terminated unexpectedly … Restart the service" → running again at 02:53:01Z, 5.9 s. The minute 02:52 is missing at the gateway: the in-flight batch died with the process
V5 re-run, same version 119 s; still one service and one task; collector stopped 5 s (02:58:15 → 02:58:20Z); task last result 0
V5 re-run, 0.164.0 (2026-10-26) 123 s; the same config runs unchanged; 73 points per minute, all five signals; stopped 4 s

Bug found and fixed: process-check.ps1 on Windows PowerShell 5.1. @(Get-Content -Raw … | ConvertFrom-Json) turned the two-entry list into one element (5.1 emits a JSON array as one object), so $e.instance was gs1 gs2 and its -like pattern matched nothing: a constant 0, which an alert would read as "everything is down". Checked on the host: old form count 1 [gs1 gs2], new form ($parsed = …; @($parsed)) count 2 [gs1,gs2]. The lab's bin/check.ps1 (fixed instances) never took this path, which is why the 2026-11-06 run did not show it.

Not run: the Application event log had no otelcol warn/error entries, but whether the service writes its log there was not checked, so no claim either way; SSM State Manager (only Run Command); a second account.

AWS limits that bound one gateway (AWS documentation, read 2026-11-06)

Item Sustained Note
t4g.small 0.128 Gbps baseline "up to 5 Gbps" is a best-effort burst for minutes
m7g.large / c7g.large 0.937 Gbps xlarge 1.876 Gbps
one flow 5 Gbps outside a cluster placement group
interface endpoint 10 Gbps per AZ, scales to 100 Gbps PrivateLink quotas
Transit Gateway VPC attachment up to 100 Gbps per AZ each direction, 7.5 M pps
NLB no bandwidth cap stated scales by LCU
connection tracking per instance, not published watch conntrack_allowance_available

Run it

export AWS_PROFILE=<profile>                 # or session credentials in the environment
terraform -chdir=terraform init && terraform -chdir=terraform apply
bin/run.sh ready                             # until agents, GameServer and the tasks are up
bin/run.sh start; bin/run.sh killtest        # killtest takes ~14 min
bin/run.sh timeline; bin/run.sh n2; bin/run.sh logs
sleep 960; bin/run.sh end                    # end the 30-minute window
terraform -chdir=terraform destroy

# section 5: install.ps1 end to end
terraform -chdir=terraform apply -var windows_mode=install
bin/install-e2e.sh ready; bin/install-e2e.sh install 0.155.0; bin/install-e2e.sh state
bin/install-e2e.sh data 3; bin/install-e2e.sh killtest; bin/install-e2e.sh logs
terraform -chdir=terraform destroy -var windows_mode=install
Piece What
terraform/ gateway VPC (Linux host + internal NLB + endpoint service), source VPC (Windows host + interface endpoint), no route between them; SSM only; EC2 tags lab.service.domain / lab.service.group
collector/agent-linux.yaml hostmetrics base + process (one executable) + resourcedetection (env, system, ec2 with tag mapping)
collector/agent-windows.yaml base + methods A–D, one gateway port each; agent-windows-ls.yaml is method A under LocalService
collector/gameserver.yaml the stand-in game server's config
collector/gateway.yaml one OTLP/HTTP port per set to its own file, plus OTLP/gRPC 4317 for section 5; no batch processor
bin/analyze.py, bin/timeline.py volume report; per-minute method view. Sent to the gateway through SSM
bin/check.ps1 method D
bin/install-e2e.sh, bin/install_check.py section 5: push and run the profile's install.ps1 through SSM; what reached the gateway's windows-install.jsonl
terraform/user_data_windows_install.ps1.tftpl windows_mode = install: the two stand-in game servers only
local/ the Linux half in Docker, to check configs before applying
managed/windows_prep.py Managed ClickStack side of a managed = true run: database, ingest user (password only in the gitignored terraform/managed.auto.tfvars), metric source, dashboard; --cleanup
collector/gateway-managed.yaml overlay: the gateway also writes every set to that database (clickhouse exporter)
schema-bench/cloud_bench.py, cloud_logs_bench.py the metric and log schema benches on a Cloud service; --keep / --cleanup

For the configuration to use on real hosts, see the standard profile otel-profiles/profiles/windows-host — this lab's agent splits every method onto its own port to measure them, which a real agent does not.


수천 대 EC2가 1분마다 호스트 메트릭을 중앙 OTel gateway 하나로 보내는 구성에서 답해야 할 두 가지와, 게임 서버 프로세스를 어떻게 감시할지를 정하는 Windows 질문 하나를 다룹니다.

  1. 호스트당 수집량 — 분당 data point(ClickStack 메트릭 테이블의 행)와 전송 바이트(OTLP/HTTP, gzip), Linux와 Windows(#127).
  2. Windows의 프로세스 중지 감시 방법 — 프로세스가 멈췄을 때 방법마다 무엇을 보내는지, 서비스 계정에 따라 무엇이 달라지는지.
  3. 여러 네트워크에서 gateway 하나로 — 원천 네트워크는 NLB 기반 VPC endpoint service로만 gateway에 닿습니다("여러 계정이 gateway 계정 하나로 보낸다"의 형태).

검증: 2026-11-06, ap-northeast-2, AWS 계정 하나, otelcol-contrib 0.155.0. Linux t4g.small (Amazon Linux 2023 arm64, vCPU 2)이 호스트 겸 gateway, Windows Server 2022 Datacenter t3.medium (vCPU 2, 어댑터 1, 디스크 1, 볼륨 1). apply 04:43Z → destroy 05:27Z. 수집량 구간 30.22분 (04:52:23–05:22:36Z), 중지·재시작 시험 04:52–05:06Z. 원자료는 results/.

1. 호스트당 수집량

세트(60초 간격) 1회 수집 point 시계열 vCPU별 point 분당 전송 바이트 point당 바이트
Linux 기본(linux-host scraper 세트) 130 131 vCPU당 16 (32) 3,856 28.9
Windows 기본(같은 세트에서 processes 제외) 68 68 vCPU당 8 (16) 2,912 43.1
process scraper, 걸린 프로세스 하나당 Windows 6 · Linux 7
windows_service, 서비스 하나당 1
확인 스크립트(방법 D), 인스턴스 하나당 1

2. Windows 프로세스 중지 감시 방법

가짜 게임 서버 인스턴스 둘: GameServer.exe(collector 바이너리 사본이라 실제 서비스로 동작)를 Windows 서비스 GameServer(gs1)와 SYSTEM 예약 작업(gs2)으로 띄우고, 명령줄의 설정 파일로 구분했습니다. 사건: S1 gs1 서비스 중지(04:52:32Z), S2 재시작(04:56:36Z), S3 gs2 프로세스 강제 종료(04:59:40Z), S4 gs2 작업 재시작(05:03:45Z). 분 단위 기록: results/2026-11-06-timeline.json.

방법 gs1 서비스 중지 gs2 작업 프로세스 종료 인스턴스 구분 판정
A hostmetrics process, 이름 include, LocalSystem gs1 시계열이 사라짐 gs2 시계열이 사라짐 가능 — process.command_line, process.pid, process.owner 인스턴스별 CPU·메모리에 적합. 중지 감지는 알림 쪽 기대 목록이 필요
A, NT AUTHORITY\LocalService — — — SYSTEM 프로세스가 안 보임: 실행 파일을 읽을 때 "Access is denied", 이름 필터가 걸리지 않음
B windowsperfcounters, Process 객체, 인스턴스 GameServer, GameServer#1 남은 쪽이 GameServer로 이름이 바뀜, 2 → 1 같음 불가 — #n은 시작 순서이지 정체가 아님. 바뀔 때마다 "negative value" 오류 중지 감시에 쓰지 않음
C windows_service 같은 분에 명시적 1(stopped) 아무것도 없음(서비스가 아님) 서비스 이름별 게임 서버가 서비스면 가장 좋음
D 확인 스크립트 → OTLP, 기대 목록 명시적 gs1 = 0 명시적 gs2 = 0 기대 인스턴스별 실행 방식과 관계없이 동작

같은 중지·재시작 시험을 Managed ClickStack에서(2026-11-06 08:10–08:58Z): managed/windows_prep.py 뒤 managed = true로 terraform apply해 게이트웨이가 모든 세트를 서울 서비스의 windows_hmv DB에도 쓰게 하고 bin/run.sh killtest를 돌렸습니다(사건: results/2026-11-06-events-managed.jsonl). 준비된 대시보드에서 위 분 단위 표와 같은 네 동작이 보였습니다 — 서비스 상태는 gs1 서비스 중지 동안만 1, 확인 스크립트는 gs1과 gs2가 차례로 0, process scraper는 재시작할 때마다 새 PID 선. 첫 시도에서는 수집 사용자에게 CREATE DATABASE 권한이 없어 clickhouse exporter가 HTTP 403으로 시작하지 못했고, windows_prep.py가 이제 그 권한을 줍니다.

알림이 이것을 어떻게 보는지는 labs/process-down-alerts에 있습니다 — group by 알림은 끊긴 시계열(A, B)에는 울리지 않고, 명시적인 값(C, D)에는 울립니다.

3. 공통 필드를 어디에 둘 것인가(스키마 비교)

서비스 분류와 host.name은 플릿 전체 조회가 매번 거는 조건입니다. 같은 합성 데이터 1,680만 행 (호스트 4,000 × 메트릭 70 × 60분, 호스트 메트릭처럼 ServiceName은 하나)으로 테이블 셋을 비교했습니다. A는 exporter 기본 gauge 스키마(속성은 Map, map 키·값에 bloom filter, ORDER BY (ServiceName, MetricName, Attributes, time)), B는 A에 MATERIALIZED 컬럼 host, svc_group과 skip index를 더한 것, C는 직접 만든 스키마(create_schema: false)로 ORDER BY (ServiceName, MetricName, svc_group, host, time) 입니다. Docker Desktop의 clickhouse local 26.8.34.13(ClickStack 2.42.0 이미지), 2026-11-06, 두 번 돌려 같은 결과. 표는 위 English 절과 같습니다.

같은 비교를 ClickHouse Cloud에서(Managed ClickStack이 읽는 서울 서비스: 26.8.1.608, replica 2), 2026-11-06. 테이블마다 part가 하나로 병합된 뒤 읽었습니다. 첫 Cloud 실행 (results/2026-11-06-schema-bench-cloud.txt)은 OPTIMIZE … FINAL이 돌아온 직후, 병합이 끝나기 전에 읽어 B의 저장(+46%)을 크게, 속도를 작게 잡았습니다. 그 실행의 기록으로만 남기고 결과로 쓰지 않습니다. 확정값(2회 중 2회째): results/2026-11-06-schema-bench-cloud-settled.txt. 표와 로그 비교는 위 English 절과 같습니다.

4. 여러 네트워크에서 gateway 하나로

확인(출발) 결과
원천 VPC → interface endpoint, 4319 연결됨. Windows 세트 전부가 이 경로로 들어옴
원천 VPC → gateway 사설 주소 10.77.1.18:4319 연결 안 됨(경로 없음)
gateway VPC → Windows 호스트 10.78.1.91:3389 연결 안 됨(한 방향만 열림)

두 VPC는 같은 계정에 있었습니다(허용 principal = 그 계정). 계정이 둘이면 endpoint service의 principal만 바뀌며, 그건 여기서 돌리지 않았습니다. Transit Gateway는 labs/privatelink-gateway에서 검증했습니다.

5. 표준 프로파일의 install.ps1 처음부터 끝까지 (#141)

검증: 2026-11-07, ap-northeast-2, 같은 Windows Server 2022 t3.medium(Windows PowerShell 5.1.20348)과 gateway, terraform apply -var windows_mode=install(대역 게임 서버만, agent 없음). otel-profiles/profiles/windows-host의 install.ps1을 빌드한 설정·process-check.ps1과 함께 SSM Run Command로 보내 실행(bin/install-e2e.sh), OTLP/gRPC로 endpoint service를 거쳐 gateway 4317에 보냄. apply 02:20Z → destroy 03:13Z. 원자료 results/2026-11-07-install-*.

확인 결과
V1 설치, otelcol-contrib 0.155.0 108초에 정상 종료(대부분 내려받기). 서비스 otelcol: 자동, LocalSystem, 실패 시 5초·30초·60초 뒤 재시작, 하루 뒤 리셋. 작업 otelcol process-check: SYSTEM, 최고 권한, 1분마다. 머신 변수 다섯 개와 API 키 설정됨
V2 gateway 도착 분당 73 point(metric 29종), host.name, host.id, os.type, cloud.*, host.type, deploy.platform, EC2 태그 ec2.tag.lab.service.domain / .group 포함. GameServer.exe 두 인스턴스의 프로세스 메트릭(소유자 NT AUTHORITY\SYSTEM)
V3 서비스 중지(02:42:40Z) windows.service.status가 02:42–02:44에 1, 02:45부터 다시 4(02:45:44Z 시작). process scraper는 새 PID
V3 작업 프로세스 종료 첫 실행에서 process.check.up이 틀렸음 — 인스턴스가 gs1 gs2 하나로 늘 0(아래). 고친 뒤(03:00:14Z): 03:00–03:02에 gs2 = 0, gs1은 1 유지, 작업 재시작(03:02:49Z) 뒤 다시 1
V4 collector 프로세스 종료(02:52:55Z) 서비스 제어 관리자 7031 "terminated unexpectedly … Restart the service" → 02:53:01Z 다시 실행, 5.9초. gateway에서 02:52 분이 빠짐 — 보내던 batch가 프로세스와 함께 사라짐
V5 같은 버전 재실행 119초, 서비스·작업 여전히 하나씩, collector 5초 멈춤(02:58:15 → 02:58:20Z), 작업 마지막 결과 0
V5 0.164.0(2026-10-26)으로 재실행 123초, 같은 설정이 그대로 돎, 분당 73 point, 신호 다섯 가지 모두, 4초 멈춤

찾아서 고친 버그: Windows PowerShell 5.1의 process-check.ps1. @(Get-Content -Raw … | ConvertFrom-Json)이 두 항목짜리 목록을 한 항목으로 만들었습니다(5.1은 JSON 배열을 객체 하나로 내보냄). 그래서 $e.instance가 gs1 gs2가 되고 -like 패턴이 아무것도 맞추지 못해 늘 0이었습니다 — 알림이 "전부 내려감"으로 읽을 값입니다. 호스트에서 확인: 기존 형태 count 1 [gs1 gs2], 새 형태($parsed = …; @($parsed)) count 2 [gs1,gs2]. 실습의 bin/check.ps1(인스턴스 고정)은 이 경로를 타지 않아서 2026-11-06 실행에서는 드러나지 않았습니다.

돌리지 않은 것: Application 이벤트 로그에 otelcol warn/error 항목이 없었지만 서비스가 그곳에 로그를 쓰는지는 확인하지 않아서 결론을 내리지 않음. SSM State Manager(Run Command만). 두 번째 계정.

AWS 한도 표, 실행 명령, 구성 요소는 위 English 절과 같습니다. 실제 호스트에 쓸 설정은 표준 프로파일 otel-profiles/profiles/windows-host를 보세요 — 이 실습의 agent는 측정을 위해 방법마다 포트를 나눴고, 실제 agent는 그러지 않습니다.

Open this lab on GitHub →GitHub에서 이 실습 열기 →