指标与采集器
[!INFO] Git Commit:
5ea0bc3| Updated: 2026-10-01
本 mod 共暴露 14 个 mc_ 前缀的指标,由 9 个采集器类产生。另有 jwm_collector 开启时
由 DefaultExports 注册的 Prometheus JVM 标准指标(jvm_* / process_*),见文末。
所有指标类型为 Gauge(瞬时值)或 Histogram(分布),本 mod 不产生任何 Counter——
唯一带 _total 后缀的 mc_server_ticks_total_counter 与 mc_entities_total 实际都是 Gauge。
指标总表
| 指标名 | 类型 | 采集器 | 标签 | 配置开关 |
|---|---|---|---|---|
mc_server_tick_seconds |
Histogram | Ticks |
无 | ticks |
mc_dimension_tick_seconds |
Histogram | Ticks |
id, name |
ticks |
mc_server_ticks_total_counter |
Gauge | Ticks |
无 | ticks |
mc_entities_total |
Gauge | Entities |
dim, dim_id, id, type |
entities |
mc_dimension_tileentities |
Gauge | TileEntities |
id, name |
tileentities 且 tileentities_details = false |
mc_dimension_tileentities_detailed |
Gauge | TileEntities |
dim_id, dim, te_class, te_name |
tileentities 且 tileentities_details = true |
mc_dimension_chunks_loaded |
Gauge | Chunks |
id, name |
chunks |
mc_player_list |
Gauge | Players |
id, name, dim, dim_id |
players |
mc_player_stat_total |
Gauge | PlayerStatistics |
code, name, player_id, player_name |
player_statistics |
mc_teams_chunk_claims |
Gauge | Teams |
team_id, team_name, team_type, dim_id, dim_name |
teams + ServerUtilities |
mc_teams_chunk_loads |
Gauge | Teams |
同上 | teams + ServerUtilities |
mc_teams_players |
Gauge | Teams |
team_id, team_name, player_uuid, player_name |
teams + ServerUtilities |
mc_collector_refresh_duration_seconds |
Gauge | SelfMetrics |
collector |
self_metrics |
mc_collector_staleness_seconds |
Gauge | SelfMetrics |
collector |
self_metrics |
注意两个方块实体指标与两个 tile 采集器指标互斥:tileentities_details 决定输出哪一个
(详见 配置),不会同时出现。
Ticks —— tick 耗时
唯一的事件驱动型采集器,订阅 ServerTickEvent 与 WorldTickEvent 的 START / END 四个组合。
直方图分桶
private static final double[] TICK_BUCKETS = new double[] { 0.01, 0.025, 0.05, 0.10, 0.25, 0.5, 1.0 };
Ticks.java:74。7 个上界,即最慢一档超过 1 秒的 tick 全部落入 +Inf 桶——
单 tick 超过 1 秒在 1.7.10 中已属严重卡顿,无需更细分。下界 0.01 秒意味着
比 10ms 更快的 tick 无法从 _bucket{le="0.01"} 区分,只能靠 _sum / _count 算均值。
mc_server_tick_seconds(Ticks.java:82-86)——整个服务器一次 tick 的耗时,无标签。mc_dimension_tick_seconds(Ticks.java:88-93)——按维度分别计时,标签id(维度 ID) 与name(world.provider.getDimensionName())。
维度计时的热路径优化
每个维度 tick 只取一次 Histogram.Timer,并按维度 ID 缓存 child:
Histogram.Child dimTickChild(int id, Supplier<String> name) {
Histogram.Child child = this.dim_tick_children.get(id);
if (child == null) {
child = this.dim_tick_seconds.labels(Integer.toString(id), name.get());
this.dim_tick_children.put(id, child);
}
return child;
}
Ticks.java:144-151。缓存 Map<Integer, Histogram.Child> 初始容量为 3(:67、:78,
按「主世界 + 下界 + 末地」三维度估算)。注释说明动机是避免每 tick 的
labels(...) 查找与标签字符串分配。Supplier<String> 保证维度名只在缓存未命中时解析。
该 map 只在服务器线程访问,无并发问题。
mc_server_ticks_total_counter 实际是世界时间
this.server_total_ticks.set(this.mc_server.getEntityWorld().getTotalWorldTime());
Ticks.java:255-257,在 stopServerTick() 中于 END 阶段读取。
MinecraftServer.getEntityWorld() 返回的是 DIM0(主世界),与 help 文本
DIM0's total ticks 一致。所以这个"tick 计数"实际是主世界的总游戏时间
(每 tick +20),可被任意修改世界时间的指令影响,不是单调递增的服务器运行时长。
事件不配对的三种情况
| 情况 | 位置 | 处理 |
|---|---|---|
| 上一 tick 未结束又开始 | Ticks.java:160-178 |
受 collector_mc_dimension_tick_errors 控制(IGNORE / LOG / STRICT);无论策略如何都会关闭遗忘的 timer |
| 无计时器却收到 stop | Ticks.java:193-210 |
若该维度从未 tick 过则直接返回(容忍导出器中途重启,:195-198);否则按配置策略处理 |
| stop 的维度 ≠ 正在计时的维度 | Ticks.java:211-218 |
无条件抛 IllegalStateException,不受配置控制 |
第三种是唯一绕过配置的硬失败。
Entities —— 实体计数
mc_entities_total,4 个标签。用 TObjectIntHashMap<EntityKey> 按
(dim, dim_id, id, type) 四元组聚合,值恒为整数(adjustOrPutValue(key, 1, 1))。
玩家被排除:!(entityObj instanceof EntityPlayer)(Entities.java:42)——
玩家数量由 mc_player_list 单独表达,避免重复计数。
实体类型的确定分两级(Entities.java:44-48):
EntityList.getEntityString(entity)—— 已注册实体的注册名;- 若返回
null且实体实现了IMob,则退回entity.getClass().getName()—— Java 类全名。
type 为 null(未注册且非 IMob)的实体被完全跳过,不产生任何指标。
第二个分支意味着未注册的怪物会以 com.some.mod.EntityFoo 这样的类名形式出现在 type 标签中,
与注册名(如 Enderman)风格不一致。
遍历的维度来源是 mc_server.worldServers(Entities.java:33),
而 Chunks 与 TileEntities 用的是 DimensionManager.getWorlds()——
三者取到的维度集合在 1.7.10 中一致,但代码风格不统一。
TileEntities —— 方块实体计数
两种模式二选一,默认(tileentities_details = false)输出
mc_dimension_tileentities,标签 id + name,值为 world.loadedTileEntityList.size()。
size() == 0 的维度会被 continue 跳过(TileEntities.java:69),
因此默认模式下空维度不产生指标行——这是唯一会主动省略零值的采集器。
详细模式(tileentities_details = true)输出 mc_dimension_tileentities_detailed,
4 个标签 dim_id、dim、te_class、te_name,按类聚合计数。此模式不做 size 为 0 的跳过
(:48 的 continue 在读 loadedTileEntityList 之后,仍有该判断)。te_name 来自
access-transformer 开放的 TileEntity.classToNameMap,取值为 null 的类被跳过
(见 mod 元信息与打包数据)。
两个模式的标签顺序不同:默认模式是 id, name,详细模式是 dim_id, dim, ...——
id 与 dim_id 指同一维度 ID,但标签名不同,写查询时需注意。
Chunks —— 区块计数
mc_dimension_chunks_loaded,标签 id + name,值为
world.getChunkProvider().getLoadedChunkCount()。遍历 DimensionManager.getWorlds()
(Chunks.java:29)。不做零值跳过——所有维度都会出现,包括空维度。
Players —— 在线玩家
mc_player_list,标签 id(UUID)、name、dim、dim_id,值恒为 1——
是「存在性」指标而非计数指标,玩家总数需用 count(mc_player_list) 而非直接取值。
这也是 Prometheus 官方推荐的"在线玩家列表"模式。
空值处理(Players.java:40-47)值得注意——注释写明
WARNING: Either "id" or "name" can be null.:
| 字段 | 为 null 时 |
|---|---|
id |
替换为空字符串 ""(UUID 为 null 时) |
name |
用 ObjectUtils.defaultIfNull(profile.getName(), "") 替换为 "" |
world 为 null 时维度标签回退为 "Unknown" / 0(:49-54)。
dim 标签是本次 GTNH 移植新增的(README 的 Migrating 一节明确提示面板需相应调整)。
PlayerStatistics —— 玩家统计
mc_player_stat_total,4 个标签 code(statId)、name(本地化文本)、
player_id、player_name,值取自 StatList.generalStats 中的每个 StatBase。
离线玩家仍会被统计:类内持有一个 THashMap<UUID, PlayerInfo> players
(PlayerStatistics.java:26),Javadoc 写明用途是
to persist player stats after sign-out。sample() 只新增/更新条目,从不删除,
所以玩家下线后其统计数据继续以最后一次缓存的 StatisticsFile 上报。
两处值得注意的实现细节:
- 读取值用的是
writeStat(:96),源码自带注释NOTICE: Despite its name, this reads the value.; - 统计名称被缓存在
THashMap<StatBase, String> stat_names(:99-105), 首次遇到某StatBase时调func_150951_e().getUnformattedText()取本地化名称并缓存, 避免每次采样重复查翻译。两个 map 的初始容量分别硬编码为PLAYERS_INIT = 20与STATS_INIT = 23(:36、:42),后者注释写This was counted from StatList.(即从StatList实测得来,非拍脑袋)。
id 与 name 任一为 null 的玩家会被整体跳过(:72),不进入 players map。
Teams —— ServerUtilities 队伍
3 个指标,唯一的可选外部 mod 依赖。双重门控(PrometheusExporterMod.java:143-144):
if (cfg.teams && ModCompat.ServerUtilities.isLoaded())
this.addSampler(new Teams(this.mc_server, cfg.teams_interval_ticks));
ModCompat 是一个枚举缓存 Loader.isModLoaded 结果(ModCompat.java:25-34,
loaded 字段惰性求值后不再重复查询),目前只有 ServerUtilities("serverutilities") 一个成员。
运行时还有第三道门控:if (!Universe.loaded()) return null;(Teams.java:54)——
ServerUtilities 存在但数据尚未加载时返回 null,被 Sampler 规整为空列表
(见 采集模型与快照缓存)。
区块相关的两个指标按队伍 × 维度展开,且仅在 ClaimedChunks.isActive() 为真时填充
(Teams.java:58):
| 指标 | 值 |
|---|---|
mc_teams_chunk_claims |
该队伍在该维度的认领区块总数(teamChunks.size()) |
mc_teams_chunk_loads |
其中 forced != null ? forced : false 的数量 |
即强制加载区块数是认领数的子集。空集合的「队伍 × 维度」组合被 continue 跳过
(:64),不产生零值行。mc_teams_players 不受 ClaimedChunks 门控,始终填充
(:83-97),值为 1。
玩家标签用 member.getName() 而非 getDisplayName(),源码注释解释:
not using DisplayName as it will return getName if the player is offline(:93)。
队伍显示名通过 team.getTitle().getUnformattedText() 取纯文本(去掉了颜色格式码),
见 :68-69。
SelfMetrics —— 采集器自监控
2 个指标,标签 collector(即 Sampler.name,共 6 个取值:
entities、tileentities、chunks、players、player_statistics、teams)。
它遍历 scheduler.samplers() 实时生成,不缓存——因为只读 volatile 字段。
| 指标 | 含义 | 用途 |
|---|---|---|
mc_collector_refresh_duration_seconds |
各采集器上次成功刷新的耗时(秒) | 评估采集开销,据此调大间隔 |
mc_collector_staleness_seconds |
距上次成功刷新的秒数 | 确认数据新鲜度 |
SelfMetrics 的 Javadoc 明确说明它只读 volatile 标量,所以可在 HTTP 线程安全采集
(SelfMetrics.java:10-14)。因为 Sampler 在采样失败时不更新时间戳
(Sampler.java:96-98),staleness_seconds 持续增长即是采集器出错的信号,
而 refresh_duration_seconds 会停留在最后一次成功值。
JVM 标准指标
jwm_collector(默认 true)开启时执行:
if (cfg.jwm_collector) DefaultExports.register(CollectorRegistry.defaultRegistry);
PrometheusExporterMod.java:132。io.prometheus.client.hotspot.DefaultExports 注册的是
标准 JVM 指标(jvm_memory_*、jvm_threads_*、jvm_gc_*、process_* 等),
与本 mod 的 mc_* 命名空间不冲突。仓库中的 examples/output.txt 是完整抓取输出样例。