iohao / ioGame

无锁异步化、事件驱动架构设计的 java netty 网络编程框架; 轻量级,无需依赖任何第三方中间件或数据库就能支持集群、分布式; 适用于网络游戏服务器、物联网、内部系统及各种需要长连接的场景; 通过 ioGame 你可以很容易的搭建出一个集群无中心节点、集群自动化、分布式的网络服务器;FXGL、Unity、UE、Cocos Creator、Godot、Netty、Protobuf、webSocket、tcp、socket;java Netty 游戏服务器框架; Java Netty Game Server.
http://game.iohao.com
GNU Affero General Public License v3.0
918 stars 205 forks source link

关于 StatActionInOut.TimeRange 的 inRange(long time) 方法的准确性 #335

Closed SaltedFishKnight closed 4 months ago

SaltedFishKnight commented 4 months ago

boolean inRange(long time) { return time >= this.start && time <= this.end; }

我注意到判断 action 执行时间的范围是闭区间,这在统计次数上会产生一些错误

假如按照原来的代码,当我要记录 [10, 20] [20, 30] 区间的 action 执行次数

此时捕获到有一个 time 为 20 的 action 被执行,根据源代码,这次 action 会被统计到 [10, 20] 区间,但实际上应该统计到 [20, 30] 区间

统计区间不应该有重叠,原理就和频率分布直方图一样,各数据组的边界范围应该是半开半闭区间,即 [10, 20) [20, 30)

只要将 time <= this.end 改为 time < this.end,同时在文档里说明是左闭右开区间

iohao commented 4 months ago

可以尝试自定义配置

private void setListener(TimeRangeInOut inOut) {
    inOut.setListener(new TimeRangeInOut.ChangeListener() {

        @Override
        public List<TimeRangeInOut.TimeRangeMinute> createListenerTimeRangeMinuteList() {
            return List.of(
                    TimeRangeInOut.TimeRangeMinute.create(10, 19),
                    TimeRangeInOut.TimeRangeMinute.create(20, 29)
            );
        }

    });
}
SaltedFishKnight commented 4 months ago

让我详细地展开说一下

假设 action 有捕获到以下这些执行时间:19.00 ms,19.12 ms,19.56 ms,19.99 ms,20.00 ms,20.23 ms,20.45 ms,20.99 ms

1、第一种设置

StatActionInOut.TimeRange.create(10, 19),
StatActionInOut.TimeRange.create(20, 29),

统计区间为 [10, 19],[20, 29],但是统计时间的最小单位是 ms,所以 19.12 ms,19.56 ms,19.99 ms 都被截断为 19 ms,导致 19.12 ms,19.56 ms,19.99 ms 被统计到 [10, 19],但实际上它们不应该被统计,因为它们是落入 (19, 20) 区间的

2、第二种设置

StatActionInOut.TimeRange.create(10, 20),
StatActionInOut.TimeRange.create(20, 29),

统计区间为 [10, 20],[20, 29],但是统计时间的最小单位是 ms,所以 20.23 ms,20.45 ms,20.99 ms 都被截断为 20 ms,因为统计次数是找到第一个符合的区间

public TimeRange getTimeRange(long time) {
    return this.timeRangeList.stream()
            .filter(timeRange -> timeRange.inRange(time))
            .findFirst()
            .orElse(this.lastTimeRange);
}

导致 20.23 ms,20.45 ms,20.99 ms 被统计到 [10, 20],实际上应该统计到 [20, 29]

所以不管是哪种设置,在闭区间的情况下,要么是缺少统计区间,要么是统计区间重叠

action 执行时间不论是多少,它最终落入的区间是 [0, +∞),如果你要完美的切割统计区间,应该是左闭右开区间

iohao commented 4 months ago

感谢你的详细说明。

但目前对称更符合人类理解,要么两边包含(默认实现)、要么两边不包含。而左闭右开、左开右闭并不太符合。

同样,默认实现的两边包含在配置上可以减少使用者的心智负担。如下示例配置

private void setListener(TimeRangeInOut inOut) {
    inOut.setListener(new TimeRangeInOut.ChangeListener() {

        @Override
        public List<TimeRangeInOut.TimeRangeMinute> createListenerTimeRangeMinuteList() {
            return List.of(
                    // 只统计 0、1、59 分钟这 3 个时间点
                    TimeRangeInOut.TimeRangeMinute.create(0, 0),
                    TimeRangeInOut.TimeRangeMinute.create(1, 1),
                    TimeRangeInOut.TimeRangeMinute.create(59, 59)
            );
        }

    });
}
SaltedFishKnight commented 4 months ago

不好意思,我才看到你写的代码是 TimeRangeInOut 插件,我说的是 StatActionInOut 插件,我回复的代码已经修正过来了

我没有使用 TimeRangeInOut,但我看了源代码,TimeRangeInOut 是有着 StatActionInOut 相同的问题的

如果说 StatActionInOut.TimeRange 统计单位时间为 ms,在一毫秒内的几个 action 统计次数存在错误是可以接受的;但 TimeRangeInOut.TimeRangeMinute 统计单位时间为 minute,当统计单位时间从 ms 扩大到 minute,这无疑都会放大这两种统计错误:缺少统计区间、统计区间重叠

而且当你的 action 调用次数的数据量足够大时,StatActionInOut 和 TimeRangeInOut 都肯定会出现严重的统计数据失真

iohao commented 4 months ago

是我看错了,看成 TimeRangeInOut 了。StatActionInOut 与 TimeRangeInOut 类似 ,两者都不会出现统计数据失真,统计数据是符合预期的。

使用示例

private void setListener(StatActionInOut inOut) {
    inOut.setListener(new StatActionInOut.StatActionChangeListener() {

        @Override
        public List<StatActionInOut.TimeRange> createTimeRangeList() {
            return List.of(
                    StatActionInOut.TimeRange.create(1000, 1999),
                    StatActionInOut.TimeRange.create(2000, Long.MAX_VALUE, "> 2000"));
        }
    });
}

以下是模拟的测试数据

cmd[1 - 1], 执行[50]次, 异常[0]次, 平均耗时[1663], 最大耗时[2924], 总耗时[83163] 
    1000 ~ 1999 ms 的请求共 [26] 个
    > 2000 ms 的请求共 [24] 个
cmd[1 - 2], 执行[50]次, 异常[0]次, 平均耗时[1699], 最大耗时[2940], 总耗时[84984] 
    1000 ~ 1999 ms 的请求共 [19] 个
    > 2000 ms 的请求共 [31] 个