全球同服
单点模块
虽然Zeze提供了分布式能力,但有时系统内有一些全局单点模块,不能提供足够的并发能 力。举个例子,存在一种游戏内的即时排行榜,角色数值变化马上更新排行榜。由于这个排 行榜只包含一个数据列表,所有的更新请求需要排队互斥进入列表。当同时在线角色数量很 大,更新非常多,那么这个排行榜就会成为一个并发瓶颈。如果全局单点的数据可以按一定 规则分开存储,并在需要的时候汇总。能这样做的数据仍然可以用很小的代价并提供足够的 并发性能。比如上面的排行榜,分成128个分组数据,把角色hash分散到这些分组中。每 个分组独立进行排名,每个分组都保存足够数量的排名。当需要全局排名时,把所有的分组 整合起来就能得到最终的排名。这样排行榜的并发就增加了128倍。
分组数量(ConcurrentLevelSource)
分组数量决定了最大的并发度。一般来说设置足够大,并留有一定余地即可。比如128。嗯, 这个数字比较漂亮。分组数量一般来说不好随便改。比如对于排行榜来说,修改这个参数, 对导致分组数据全部失效(需要作废掉重来)。
负载分配
为了提高数据的Cache命中,访问同一个分组的请求需要转发到同一台服务器执行。分组 数量是固定的。但服务器数量开始一般小于分组数量。RedirectHash会根据hash把负载分 配到实际服务器中。每个服务器可能处理多个分组。当然分组数量决定了最大服务器数量。
相关Api
@RedirectHash
大量共享模块的优化
游戏内的帮派,即时通讯里面的群等拥有一定量成员列表的模块属于这种类型。帮派(群) 本身是自然分布的。但成员可能登录在多台Server上,实际上可能所有的Server都有零星 几个登录,当他需要访问成员列表时,就会把成员列表缓存到本Server,此时只是多占用内 存。此时成员列表发生了修改,就会作废所有Server上的缓存,性能表现出一定的突发特 性。对于成熟的帮派(群),每天的修改次数只有几个,总体性能不会明显造成问题。但是 我们如果精益求精,也是有办法解决这个问题的。只需要把群的所有操作发送给同一台 Server处理,就避免了大量共享问题。
实现
把群操作定向到同一台Server,由Arch.Linkd完成。Linkd拦截所有的群操作,并根据群编 号哈希定向到同一台Server即可。
- Linkd::LinkdService重载dispatchUnknownProtocol:
switch (moduleId) {
case ModuleFriend.ModuleId:
switch (protocolId) {
case CreateGroup.ProtocolId_:
// 创建群,随机找一台服务器。
if (ChoiceHashSend(Random.getInstance().nextInt(), moduleId, dispatch))
return; // 失败尝试继续走默认流程?
break;
case GetGroupMemberNode.ProtocolId_:
... 其他所有的群操作
if (ChoiceHashSend(DecodeGroupIdHash(data), moduleId, dispatch))
return; // 失败尝试继续走默认流程?
break;
}
break;
}
- DecodeGroupIdHash
private static int DecodeGroupIdHash(Zeze.Serialize.ByteBuffer bb) {
var rpc = new RpcGroupId();
rpc.decode(bb);
return rpc.Argument.hashCode();
}
public static class GroupId extends Zeze.Transaction.Bean {
public String Group;
@Override
public void encode(ByteBuffer bb) {
throw new UnsupportedOperationException();
}
@Override
public void decode(ByteBuffer bb) {
// 所有的群操作的参数的第一个变量必须时Group,
// 并且variable.id必须等于1.
// 这个类的目的是优化。
// 因为Lindk只需要得到Group即可,不需要解析出完整的参数信息。
// 这个decode的实现必须符合Bean的编码规范。
// 如果不在乎decode完整协议的性能开销,也可以不需要这个类。
// 直接decode出完整协议即可。
int _t_ = bb.ReadByte();
int _i_ = bb.ReadTagSize(_t_);
if (_i_ == 1) {
Group = bb.ReadString(_t_);
_i_ += bb.ReadTagSize(_t_ = bb.ReadByte());
}
// 由于Group,DepartmentId默认值时,不会Encode任何东西,这里就不做是否
// 存在值的验证了。
}
@Override
public int hashCode() {
final int _prime_ = 31;
int _h_ = 0;
_h_ = _h_ * _prime_ + Group.hashCode();
return _h_;
}
}
- ChoiceHashSend
这个方法实际上是Linkd选择负载的正常包装。也列出来吧。
private boolean ChoiceHashSend(int hash, int moduleId, Dispatch dispatch) {
var provider = new OutLong();
if (linkdApp.linkdProvider.choiceHashWithoutBind(moduleId, hash, provider)) {
var providerSocket = linkdApp.linkdProviderService.GetSocket(provider.value);
if (null != providerSocket) {
// ChoiceProviderAndBind 内部已经处理了绑定。这里只需要发送。
return providerSocket.Send(dispatch);
}
}
return false;
}