假设我们正在开发一个多人在线网络游戏。游戏中,玩家需要在虚拟世界中进行合作或者展开竞争。玩家之间也常常存在各种交易,包括钱、道具等。因此游戏开发人员必须保证玩家没有作弊,规则如下:如果玩家交易额显著高于正常水平或者玩家登录的IP地址与之前20次登录的不一样,那么交易将被标记可疑。除了实时标记交易以外,我们还希望可以将这些数据导入到Apache Hadoop以方便数据科学家训练、测试他们的算法与模型。

为了提高实时的事件标记的效率,我们尽可能利用游戏服务器的内存。游戏系统包含了多台游戏服务器,因此在设计中,我们会在内存中保存每一个用户最近的20次登录记录以及最新的20次交易明细(数据是分布式存储的)。

游戏服务器主要扮演两个不同的角色:接收并传播用户行为,实时处理交易信息并对可疑事件进行标记。为了高效的扮演第二个角色,我们需要将任何一个用户的交易历史保存在一台服务器的内存中。这就意味着我们不得不在服务器之间传输消息,毕竟接收用户行为数据的服务器未必包含了该用户的交易历史。为了保持角色之间的松偶和,我们利用Kafka在不同服务器之间传输消息。

Kafka的特性使得它可以很好的满足我们的需求:可扩展、数据分区、低延迟以及处理大量异构消费者的能力。在该案例中,我们为登录与交易处理定义了一个主题。之所以使用同一个主题主要是因为我们希望在处理交易事件前已经获得用户登录信息了(Kafka保证主题中的消息顺序,但不保证不同主题间的消息顺序)。

当用户登录或者交易时,接收服务器(图中Accept)会将数据立即发送到Kafka。消息使用用户id作为主键,事件本身作为消息的值域。这样就可以保证同一用户的所有事件,包括登录事件与交易事件,都被保存在同一个分区中。每个处理服务器(图中Process)运行一个Kafka消费者,所有的消费者被配置到同一个组,这样,每个处理服务器将会处理部分Kafka分区,同时同一个用户的所有数据都会被同一个处理服务器处理。当处理服务器读取到用户的交易事件时,它首先将该交易事件加入到用户的交易历史中(缓存在内存),接着它利用缓存的历史数据对可疑的事件进行标记,在标记过程中无需额外的网络或者磁盘开销。

值得注意的是在实际系统中,我们一般根据处理服务器的数量来配置Kafka分区数量,比如为每一个处理服务器分配一个分区,或者为每一个处理服务器上的CPU核分配一个分区(当前Kafka集群为所有主题分配的分区总数一般不超过10,000)。

该方案看上去仿佛是一种迂回的方式:游戏服务器将数据发送到Kafka,另一台游戏服务器又从Kafka中进行读取。然而,这个设计松偶和了游戏服务器的两个角色,因此可以帮助我们很方便的为不同的角色配置相应的资源。另外,由于Kafka本身设计是以高吞吐以及低延迟为初衷的,通过Kafka作一次“迂回”其实并没有带来显著的开销。根据测试,一个3节点的Kafka集群,处理每秒100万条消息的平均延迟只有3毫秒。

当游戏服务器发现可疑的交易后,它会把标记后的消息发送到一个新的Kafka主题,例如图中的警告(Alert),因此告警服务器可以将该消息通知出去。同时,一个独立的服务将上述的两个主题中的数据导入Hadoop用以后续的分析。