指标与采集器

[!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):

  1. EntityList.getEntityString(entity) —— 已注册实体的注册名;
  2. 若返回 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 是完整抓取输出样例。