gamedo.core是gamedo游戏服务器框架的核心模块(正在开发中-进度:84%)
简单来说,gamedo.core是一个适用于MMO游戏( MMOG )服务器开发的核心模型,它内部集成了一些适用于游戏开发的,开箱即用的,且可以定制化配置的核心模块,帮助开发者快速搭建游戏原型,这些内置模块包括:
- 集成了类似于netty的线程模型(可以参考:thread model ),可以快速且高效的进行多线程并发操作
- 集成了 ECS 模块,并以此作为游戏服务的核心开发**之一(关于游戏开发中的组件系统,可以参考:component )
- 集成了高度灵活的游戏循环机制( 关于游戏开发中的游戏循环机制,可以参考:game loop )
- 集成了用于解除系统与系统之间、模块与模块之间可能会产生耦合的事件系统( 关于游戏开发中的事件机制,可以参考:event )
当使用spring-boot项目后,可以非常方便地使用gamedo.core项目,以下是gamedo.core线程池的开箱使用演示:
@SpringBootApplication
@Log4j2
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
final IGameLoop worker = Gamedo.worker().selectNext();
CompletableFuture.runAsync(() -> log.info("I'm in a worker thread."), worker)
.thenAcceptAsync(s -> log.info("then i came to some io thread"), Gamedo.io())
.thenAcceptAsync(s -> log.info("then i came back to the worker thread."), worker);
}
}
这是IGameLoop作为ScheduledExecutorService的实现,所带来理所应当的特性,同时IGameLoop作为 ECS 中的IEntity接口,也提供了组件(component)管理的功能,当将这两者结合,就带来了令人欣喜的线程模型:
@SuppressWarnings("ALL")
@SpringBootApplication
@Log4j2
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
final String id = "gamedo";
final EventGreeting event = new EventGreeting("hello " + id);
final IGameLoop worker = Gamedo.worker().selectNext();
//模拟使用io线程加载entity
CompletableFuture.supplyAsync(() -> loadEntity(id), Gamedo.io())
//加载完毕,将之安全发布到worker线程
.thenAccept(entity -> worker.submit(IGameLoopEntityManagerFunction.registerEntity(entity)))
//注册完毕,向其发送消息
.thenAccept(s -> worker.submit(IGameLoopEventBusFunction.post(event)));
}
private static IEntity loadEntity(String id) {
IEntity entity = new Entity(id);
entity.addComponent(MyComponent.class, new MyComponent(entity));
log.info("load entity from db, entity:{}", entity);
return entity;
}
@Value
public static class EventGreeting implements IEvent {
String content;
}
@Value
public static class EventResponse implements IEvent {
String response;
}
public static class MyComponent extends EntityComponent {
public MyComponent(IEntity owner) {
super(owner);
}
//每隔60秒执行一次心跳,可以声明任意数量的@Tick函数
@Tick(delay = 0, tick = 60, timeUnit = TimeUnit.SECONDS)
private void tick(Long currentMilliSecond, Long lastMilliSecond) {
log.info("ticking...");
}
//每天凌晨执行跨天调用,可以声明任意数量的@Cron函数
@Cron("@daily")
private void cron(Long currentTime, Long lastTriggerTime) {
log.info("it's a new day.");
}
//响应EventGreeting事件,可以声明任意数量的@Subscribe函数
@Subscribe
private void eventHello(EventGreeting event) {
log.info("receive greeting:{}", event.content);
//响应整个“世界”(也就是当前线程的主人手下的所有线程了)
final EventResponse response = new EventResponse("hello world!");
GameLoops.current().flatMap(gameLoop -> gameLoop.owner())
.ifPresent(gameLoopGroup -> gameLoopGroup.submitAll(IGameLoopEventBusFunction.post(response)));
}
}
}
执行完毕后,输出日志如下:
2021-08-03 23:59:58.961 INFO [ main] com.example.demo.Application : application start...
2021-08-03 23:59:58.962 INFO [ io-1] com.example.demo.Application : load entity from db, entity:Entity{hashCode=-1253235656, id=gamedo, componentMap=[MyComponent]}
2021-08-03 23:59:58.977 INFO [worker-1] com.example.demo.Application : receive greeting:hello gamedo
2021-08-03 23:59:58.978 INFO [worker-1] com.example.demo.Application : ticking...
2021-08-04 00:00:00.007 INFO [worker-1] com.example.demo.Application : it's a new day.
2021-08-04 00:00:58.981 INFO [worker-1] com.example.demo.Application : ticking...
2021-08-04 00:01:58.990 INFO [worker-1] com.example.demo.Application : ticking..
根据以上代码和日志可以得出如下分析:
-
entity实体是从io线程加载的
-
entity实体被安全发布到worker-1线程
-
entity的MyComponent组件跟随entity被注册到worker线程,由于注册了@Tick、@Cron、@Subscribe注解,因此自动具备了3种能力:
- 在worker线程内逻辑心跳(tick函数)
- 延迟执行(cron函数)
- 响应所有发布到worker线程的事件(eventGreeting函数)
gamedo.core暂时还未发布正式版,目前只能使用snapshots版本,配置如下:
<dependency>
<groupId>org.gamedo</groupId>
<artifactId>gamedo-spring-boot-starter</artifactId>
<version>${version}-SNAPSHOT</version>
</dependency>
<repository>
<id>oss.sonatype.org-snapshot</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
- IEntity IEntity代表ECS 中E,代表了每一个IEntity实例都具备组合任意组件(Component)的能力。在gamedo.core中,可以通过通过向某IEntity增加组件,扩展IEntity的能力。
- IGameLoop IGameLoop作为ScheduledExecutorService的实现,具备了后者所有的特性。同时,每一个IGameLoop实例的coreSize为1,代表内部仅维护了一个独立线程。
- IGameLoopGroup IGameLoopGroup管理了一组IGameLoop,类似于netty 4中的EventLoopGroup,虽然也继承了ScheduledExecutorService接口,但是其能力完全依赖于所管理的IGameLoop,IGameLoopGroup是真正意义上的线程池。
gamedo.core的线程模型借鉴了netty的线程模型:每一个IGameLoop实例代表一个线程,对应于Netty的EventLoop,而IGameLoopGroup则对应于Netty的EventLoopGroup,正如Netty的EventLoopGroup一样,IGameLoopGroup虽然继承了ExecutorService接口,但是自身只是一个IGameLoop容器,所有的功能则是通过轮询(round robin)的IGameLoop提供,在实际的应用中,每个IEntity实例应该被唯一的安全发布(safe publication)到某个IGameLoop(安全发布是《JCP》一书中关于线程安全话题的重要概念,详情可以查阅本书),这和Netty中任意Channel的整个生命周期都隶属于某一个线程(EventLoop)的理念也是一致的(这也是相对于Netty 3,Netty 4为什么能获得巨大性能提升的重要原因之一)。
IGameLoop作为IEntity和ScheduledExecutorService的扩展,同时具备了两者的能力:异步(延迟、周期)执行任务和组件管理,除此之外,IGameLoop还提供了一个线程安全的,可以与之通信的能力:IGameLoop#submit(GameLoopFunction),和ScheduledExecutorService不同的是:当提交线程不在本线程中时,任务被异步执行;当提交线程就在本线程内时,任务会同步立即执行,此时调用者可以通过CompletableFuture#getNow(Object)立刻得到结果。
IGameLoop#submit(GameLoopFunction)提供了线程安全的,由外部世界发起的,将任意数据类型X提交到IGameLoop线程,并且可以返回任意类型Y的双向通信能力。借助于IGameLoop#submit(GameLoopFunction),可以进一步使用IGameLoop的内置组件:IGameLoopEventBus,将事件发布到IGameLoop,单向地与之通讯,例如:
final IGameLoop iGameLoop = ...
final IEvent event = new SomeEvent();
final CompletableFuture<Integer> future = iGameLoop.submit(IGameLoopEventBusFunction.post(event))
gamedo.core的starter工程利用spring boot的自动装配功能(autoconfigure),为IGameLoop自动装配了若干必备且开箱即用的组件, 包括:
- IGameLoopEntityManager 提供线程内的IEntity管理机制
- IGameLoopEventBus 提供线程内的事件动态订阅、发布、处理机制
- IGameLoopScheduler 提供线程内的cron动态管理机制
- IGameLoopTickManager 提供线程内的逻辑心跳的动态管理机制
当某个IEntity实例被安全发布到IGameLoop上时,该实例及其所有组件都具备了事件订阅、cron延迟运行、逻辑心跳的能力,详情可以参考org.gamedo.annotation包内关于@Subscribe、@Cron以及@Tick的注释。这些组件的使用方式可以参考org.gamedo.gameloop.functions包内提供的IGameLoop*Function函数或者单元测试。
由于IGameLoop自身也是一个IEntity,因此理所当然可以被自己的IGameLoopEntityManager 组件管理。因此在gamedo.core的默认实现中,当IGameLoop被实例化之后,也会通过IGameLoopEntityManagerFunction#registerEntity(IEntity)函数注册自己,使得IGameLoop自身及其组件也具备事件订阅、cron延迟运行、逻辑心跳的基础能力,实例代码如下:
final GameLoop gameLoop = new GameLoop(config);
gameLoop.submit(IGameLoopEntityManagerFunction.registerEntity(gameLoop));
gamedo.core默认内置了3种线程池:
- worker cpu密集型线程池,该线程池类似于RxJava的Schedulers.computation()或Reactor的Schedulers.parallel(),默认线程数量为:Runtime.getRuntime().availableProcessors() + 1
- io io密集型线程池,该线程池类似于RxJava的Scheduler.io()或Reactor的Schedulers.boundedElastic() ,默认线程数量为:Runtime.getRuntime().availableProcessors() * 10,在实际应用中,这个值应该是根据分析或者监控工具进行指标检测,然后根据公式计算得出,在 《JCP》一书中,建议通过估算任务等待时间和计算时间的比值,来估算io密集型的线程数量,并给出了确切的计算方案。
- single 唯一线程的线程池,某些并发业务场景需要操作强一致性(例如经典的抢票行为),对于这种需求,可以将所有请求提交到本线程池,通过将并行请求串行化, 解决日常场景中的并发需求
可以在application.yml中对上述线程池进行调整,例如可以通过如下配置调整io线程,去掉IGameLoopScheduler和IGameLoopTickManager组件:
gamedo:
gameloop:
ios:
game-loop-id-prefix: test #线程名称前缀
game-loop-count: 1 #线程池内线程数量
game-loop-group-id: tests #线程池的名称
game-loop-id-counter: 1 #线程名称起始值
component-registers: #IGameLoop要挂载的组件
- all-interfaces:
- org.gamedo.gameloop.components.entitymanager.interfaces.IGameLoopEntityManager
implementation: org.gamedo.gameloop.components.entitymanager.GameLoopEntityManager
- all-interfaces:
- org.gamedo.gameloop.components.eventbus.interfaces.IGameLoopEventBus
implementation: org.gamedo.gameloop.components.eventbus.GameLoopEventBus
daemon: true #是否为后台线程池
在实际使用中,可能会对IGameLoop的组件进行扩展,可以使用上述配置中的:component-registers将其挂载到IGameLoop上。此外,也可以在代码中动态注册组件。
依赖于MicroMeter和Spring Boot Actuator的强大特性,gamedo.core内置了开箱即用的监控指标以及Grafana dashboard,可以简单通过配置开启或关闭这些监控指标。这些指标包括:
- IGameLoop 线程池监控,监控指标包括:已完成任务(Counter)、正在运行的任务(Gauge)、队列中的任务(Gauge)、队列当前容量(Gauge)、活跃线程数量(Gauge)、核心线程数(Gauge)、最大线程数(Gauge)、运行计时(Timer)、空闲计时(Timer)
- IGameLoopEntityManager 当前线程内的管理的IEntity数量(Gauge)
- IGameLoopEventBus 通过**@Subscribe**注解注册handle函数的实例(Gauge)、事件被消费的计时(Timer)
- IGameLoopTickManger 通过**@Tick**注解注册心跳函数的实例(Gauge)、心跳函数执行时的计时(Timer)
- IGameLoopScheduler 通过**@Cron**注解注册cron函数的实例的实例(Gauge)、cron函数被调用时的计时(Timer)
可以使用预制的Grafana dashboard模板(14865),直接监控gamedo.core的核心指标:
- slf4j MDC最佳实践落地
- log4j2.xml增加mdc字段
- ScheduledExecutorService在beforeExecutor和afterExecutor时,设置mdc字段,例如IEntity.getId
- 指标采集
- 每个线程的entity管理的Gauges统计
- @Cron执行Timer统计
- @Subscribe执行Timer采集
- @Tick执行Timer采集
- IGameLoop(ScheduledExecutorService)线程池指标采集
- 指标可视化:开箱即用的通用grafana dashboard id?
- 持久化继承:考虑将gamedo.persistence集成到starter项目?
- IGameLoop持续改进
- 自定义RejectedExecutionHandler设置
- jvm shutdown钩子注册,如何优雅退出?
- IGameLoopTickManager优化:对于tick间隔相同的心跳进行合批(类似@Cron),没必要每个@Tick都单独提交task
- 单元测试完善
- @Tick 心跳函数里重复注册、反注册自己
- @Subscribe handle函数里重复注册、反注册自己
- @Cron cron函数里重复注册、反注册自己