17. 分布式处理Part 1: ORM、RPC、登录¶
游戏service由多个服务器组成,以便能够处理大量的请求。因此,对于设计并实现具有扩展功能的游戏service来说,实现顺利的跨服通信和跨服负荷的分布式处理功能是十分必要的,它能使游戏service虽然由多个服务器组成,但会看上去是通过一个服务器运行的。通常为了有效实现该目的,需要投入大量的时间和努力。而iFun引擎提供了仅需简单的设置即可实现有效分布式处理的功能。
17.1. 在分布式环境中运行ORM¶
:ref:`object`中已经介绍了即使没有单独的DB处理,iFun引擎ORM也能自动处理DB任务。但如果在多个服务器中访问相同的对象,则会怎样呢?比如会出现向游戏用户好友送礼物时,须要在多个服务器中访问好友背包等游戏对象的情况。
虽然我们可以想到通过同步point来使用DB这一简单的方法,但该方法会使DB陷入瓶颈。同时,这也意味着会缩减每个服务器可以处理的同时在线人数。
因此,最有效的方法是无需访问DB,直接在游戏服务器之间调整对象。但为此需要实现跨服RPC,以及防止访问object时导致服务器之间发生deadlock问题,这较为复杂。而在iFun引擎中,只要通过:ref:`distribution-configuration`打开分布式处理功能,即可便利地使用这些功能。
并且无需对ORM做任何修改。 例如,在调用Fetch函数时,若对象object已经在DB中通过其他服务器的cache加载,则ORM会自动通过RPC消息向相应服务器借用object并返回。
Important
使用ORM时,所有服务器须使用相同的object model定义。同时,所有服务器须连接到相同的DB服务器上。
为了将服务器包含在同一服务器组中,须要按如下所示,在MANIFEST.json的 AppInfo
会话中使用相同的 app_id
。该app ID并不是客户端app id,而是用于区分服务器组的ID,因此将其指定为在服务器组中共享的随机字符串即可。
{
...
"AppInfo": {
"app_id": "my_server_app_id_shared_among_all_the_servers"
}
...
}
17.2. 管理分布式服务器¶
17.2.1. 用标签来区分分布式服务器¶
在很多情况下,须要对特定目的的服务器组进行区分。 例如,在room-lobby方式的游戏中,需要将负责lobby功能的服务器组和room服务器组区分开,或是在特定服务器中只开放初级副本。
iFun Engine为了简化这些过程,提供了以RPC服务器为单位的标签(Tag)标记功能。
标签是程序员为了区分服务器而标记的昵称,将何种标签用于何种服务器上,以及各个标签具有何种涵义,均由程序员决定。
标签的标记方法如以下示例所示,可在代码中直接调用,也可在下面的 分布式处理功能的设置参数 的 rpc_tags
中列出tag列表。
一个服务器可以拥有一个以上标签,多个服务器也可拥有同一个标签。
下面的示例中,Server1和Server2均包含在lobby服务器组中,Server1负责其中的master作用。 为此,为两个服务器共同标记 “lobby” 标签,并给Server1标记 “master” 标签。
Server1的代码
Rpc::AddTag("lobby");
Rpc::AddTag("master");
Rpc.AddTag ("lobby");
Rpc.AddTag ("master");
Server2的代码
Rpc::AddTag("lobby");
Rpc.AddTag ("lobby");
当其他服务器需要lobby服务器列表时,可对其进行搜索,即可返回Server1和Server2。 若搜索master标签,则仅返回Server1。
Server3的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Rpc::PeerMap peers;
Rpc::GetPeersWithTag(&peers, "lobby");
Rpc::PeerMap masters;
Rpc::GetPeersWithTag(&masters, "master");
// master 라는 태그가 다른 목적으로도 사용될 수 있어서,
// 명시적으로 lobby 태그의 서버들 중에서 master 를 찾고 싶다면 다음처럼 할 수 있습니다.
for (Rpc::PeerMap::iterator it = peers.begin(); it != peers.end(); ++it) {
Rpc::Tags tags;
GetPeerTags(&tags, it->first);
if (tags.find("master") != tags.end()) {
// Found.
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeersWithTag(out peers, "lobby");
Dictionary<Guid, System.Net.IPEndPoint> masters;
Rpc.GetPeersWithTag(out masters, "master");
// master 라는 태그가 다른 목적으로도 사용될 수 있어서,
// 명시적으로 lobby 태그의 서버들 중에서 master 를 찾고 싶다면 다음처럼 할 수 있습니다.
foreach (var pair in peers)
{
SortedSet<string> tags;
Rpc.GetPeerTags(out tags, pair.Key);
if (tags.Contains ("master"))
{
// Found.
}
}
|
17.2.2. 提取服务器列表¶
17.2.2.1. Rpc::GetPeers(): 提取所有服务器列表¶
static size_t Rpc::GetPeers(Rpc::PeerMap *ret, bool include_self=false)
public static UInt64 Rpc.GetPeers (out Dictionary<Guid, PeerEndpoint> ret, bool include_self = false)
17.2.2.2. Rpc::GetPeersWithTag(): 提取拥有特定标签的服务器列表¶
static size_t GetPeersWithTag(Rpc::PeerMap *ret, const Tag &tag, bool include_self=false)
public static UInt64 Rpc.GetPeersWithTag (out Dictionary<Guid, PeerEndpoint> ret, Rpc.Tag tag, bool include_self = false)
17.2.3. 不同服务器的公网IP¶
在前面的 获取服务器的IP地址 已经介绍了获取本地服务器公网IP的方法有 HardwareInfo::GetExternalIp() 和 HardwareInfo::GetExternalPorts() 。
类似地,为获取不同服务器的IP和端口,还提供了 Rpc::GetPeerExternalIp() 和 Rpc::GetPeerExternalPorts() 。
static boost::asio::ip::address Rpc::GetPeerExternalIp(const Rpc::PeerId &peer)
public static System.Net.IPAddress Rpc.GetPeerExternalIp (Rpc.PeerId peer)
static HardwareInfo::ProtocolPortMap Rpc::GetPeerExternalPorts (const Rpc::PeerId &peer)
public static Dictionary<HardwareInfo.FunapiProtocol, ushort> Rpc.GetPeerExternalPorts (Rpc.PeerId peer)
17.2.4. 跨服共享状态和信息¶
根据情况的不同,有时须要在服务器之间共享服务器的状态信息。 例如,为确保游戏服务器之间的负载均衡,就必然须要了解各服务器的同时在线人数。 类似地,对游戏服务进行监控的工具也须要了解所有服务器的状态值。
iFun引擎提供了可以轻松跨服共享任意状态值的方法。 可通过 Rpc::SetStatus() 函数指定服务器的状态或信息。
static void Rpc::SetStatus(const Json &status);
public static void Rpc.SetStatus (JObject status)
可通过 Rpc::GetPeerStatus() 获取其他服务器中指定的状态值。
static Json Rpc::GetPeerStatus(const Rpc::PeerId &peer);
public static JObject Rpc.GetPeerStatus (Rpc.PeerId peer)
Tip
调用 Rpc::SetStatus() 函数后,将立即传输给其他服务器。因此,对于经常更新的信息(同时在线人数、房间数等),建议使用 定时器 定期更新,而不是在值发生变化时每次都调用 Rpc::SetStatus()
。
示例: 跨服共享房间数量信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int64_t g_match_room_count;
void UpdateServerStatus(const Timer::Id &, const WallClock::Value &) {
Json status;
status["room_count"] = g_match_room_count;
Rpc::SetStatus(status);
}
static bool Start() {
...
Timer::ExpireRepeatedly(WallClock::FromSec(10), UpdateServerStatus);
...
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | static UInt64 the_match_room_count = 0;
public static void UpdateServerStatus(UInt64 id, DateTime at)
{
JObject status = new JObject ();
status["room_count"] = the_match_room_count;
Rpc.SetStatus (status);
}
public static bool Start()
{
...
Timer.ExpireRepeatedly (WallClock.FromSec (10), UpdateServerStatus);
...
}
|
示例: 在PvP服务器中选择房间数量最少的服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | Rpc::PeerMap servers;
Rpc::GetPeersWithTag(&servers, "pvp");
Rpc::PeerId target;
int64_t minimum_room_count = std::numeric_limits<int64_t>::max();
for (const auto &pair: servers) {
const Rpc::PeerId &peer_id = pair.first;
Json status = Rpc::GetPeerStatus(peer_id);
if (status.IsNull()) {
continue;
}
if (not status.IsObject() ||
not status.HasAttribute("room_count", Json::kInteger)) {
LOG(ERROR) << "wrong server status: " << status.ToString();
continue;
}
if (status["room_count"].GetInteger() < minimum_room_count) {
minimum_room_count = status["room_count"].GetInteger();
target = peer_id;
}
}
// target is the least overloaded.
...
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | Dictionary<Guid, System.Net.IPEndPoint> servers;
Rpc.GetPeersWithTag(out servers, "pvp");
Log.Info ("Check Server Status");
Log.Info ("FindWith Tags = {0}", servers.Count.ToString());
System.Guid target;
UInt64 minimum_room_count = UInt64.MaxValue;
foreach (var pair in servers)
{
System.Guid peer_id = pair.Key;
Log.Info ("peer id = {0}", peer_id.ToString());
JObject status = Rpc.GetPeerStatus (peer_id);
if (status == null) {
Log.Info("Status is null");
continue;
}
if (status ["room_count"] == null)
{
Log.Error ( "wrong server status: {0}", status.ToString());
continue;
}
if (status ["room_count"].Type != JTokenType.Integer)
{
Log.Error ( "wrong server status: {0}", status.ToString());
continue;
}
if ( (UInt64) status ["room_count"] < minimum_room_count) {
minimum_room_count = (UInt64) status ["room_count"];
target = peer_id;
}
}
// target 변수가 가장 적은 부하를 받고 있으니 이 서버를 이용하도록 합니다.
...
|
17.3. 在分布式环境中管理客户端¶
17.3.1. 查找客户端访问的服务器¶
当须要在多个服务器中了解哪些用户访问了哪个服务器时,可按如下方式处理。
17.3.1.1. 通过Account ID查找¶
Step 1: 生成Session时,与account ID联动
string id = "target_id";
if (not AccountManager::CheckAndSetLoggedIn(id, session)) {
LOG(WARNING) << id << " is already logged in";
return;
}
string id = "target_id";
if (!AccountManager.CheckAndSetLoggedIn (id, session))
{
Log.Warning ("{0} is already logged in", id);
return;
}
Step 2: 关闭Session时,解除与account ID的联动
string id = "target_id";
AccountManager::SetLoggedOut(id);
string id = "target_id";
AccountManager.SetLoggedOut (id);
Important
AccountManager::CheckAndSetLoggedIn() 和 AccountManager::SetLoggedOut() 已按照 觉察不愿意的回滚 中所介绍的内容,通过 ASSERT_NO_ROLLBACK
标记了标签。因此,当这两个函数在会发生回滚的情况下被使用时,会发生assertion。
Step 3: 通过account ID查找server
string id = "target_id"; Rpc::PeerId peer_id = AccountManager::Locate(id); if (not peer_id.is_nil()) { LOG(INFO) << id << " is connected to " << peer_id; }string id = "target_id"; System.Guid peer_id = AccountManager.Locate (id); if (peer_id != Guid.Empty) { Log.Info("{0} is connected to {1}", id, peer_id.ToString ()); }
17.3.2. 将数据包发送给其他服务器的客户端¶
可通过 AccountManager::CheckAndSetLoggedIn() 向与会话连接的account ID发送数据包。 即使不是其他服务器,发给自身也可以。
下面的示例中,我们假设 AccountManager::CheckAndSetLoggedIn(“target_account_id”) 已在某处运行。
1 2 3 4 5 | Json msg;
msg["message"] = "hello!";
msg["from"] = "my_id";
AccountManager::SendMessage("chat", msg, "target_account_id");
|
1 2 3 4 5 | JObject msg = new JObject ();
msg ["message"] = "hello!";
msg ["from"] = "my_id";
AccountManager.SendMessage ("chat", msg, "target_account_id");
|
Important
仅在已通过 AccountManager::CheckAndSetLoggedIn() 为会话指定account时,才可行。
Important
AccountManager::SendMessage() 和_已按照 觉察不愿意的回滚 中所介绍的内容,通过 ASSERT_NO_ROLLBACK
标记了标签。因此,当这两个函数在会发生回滚的情况下使用时,会发生assertion。
Tip
若使用 (高级)运用RPC的跨服通信 中介绍的功能,则可以直接实现向在其他服务器中游戏的用户发送数据包。
17.3.3. 向所有客户端发送数据包¶
17.3.3.1. 无视登录与否,向服务器的所有会话发送数据包¶
无论是否已经登录,为了向连接在特定服务器组上的所有会话传输消息,使用 Session::BroadcastGlobally() 函数。
函数参数中, TransportProtocol
仅可以使用 kTcp
和 kUdp
类型。
下面的示例中,向连接在所有服务器上的会话发送数据包。
如果不是向所有服务器发送,而是向拥有 game 标签的服务器上连接的所有会话发送数据包,将第7行更改成 Rpc::GetPeersWithTag(&peers, "game", true);
即可。
1 2 3 4 5 6 7 8 9 | void BroadcastToAllSessions() {
Json msg;
msg["message"] = "hello!";
Rpc::PeerMap peers;
Rpc::GetPeers(&peers, true);
Session::BroadcastGlobally("world", msg, peers, kDefaultEncryption, kTcp);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public static void BroadcastToAllSessions()
{
JObject msg = new JObject ();
msg ["message"] = "hello";
Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeers(out peers, true);
Session.BroadcastGlobally ("world",
msg,
peers,
Session.Encryption.kDefault,
Session.Transport.kTcp);
}
|
Tip
当想要向所有已连接在本地服务器上的会话发送消息时,请参考 向所有会话传输消息 中所介绍的 Session::BroadcastLocally() 。
Important
Session::BroadcastGlobally() 和 Session::BroadcastLocally() 已按照 觉察不愿意的回滚 中所介绍的内容,通过 ASSERT_NO_ROLLBACK
标记了标签。因此,当这两个函数在会发生回滚的情况下被使用时,会发生assertion。
17.3.3.2. 向服务器上已登录的所有客户端发送数据包。¶
在iFun引擎中,用户已经登录就意味着已调用 AccountManager::CheckAndSetLoggedIn() ,但仍未调用 AccountManager::SetLoggedOut() 。
向已登录的所有客户端发送数据包时,使用 AccountManager::BroadcastLocally() 和 AccountManager::BroadcastGlobally() 。前者仅向访问当前服务器的客户端发送数据包,后者向连接在多个服务器上的所有客户端发送数据包。
函数参数中, TransportProtocol
仅可以使用 kTcp
和 kUdp
类型。
示例: 向登录到本地服务器上的所有客户端发送数据包
1 2 3 4 5 6 | void BroadcastToAllLocalClients() {
Json msg;
msg["message"] = "hello!";
AccountManager::BroadcastLocally("world", msg, kDefaultEncryption, kTcp);
}
|
1 2 3 4 5 6 7 8 9 10 | public void BroadcastToAllLocalClients()
{
JObject msg = new JObject();
msg["message"] = "hello";
AccountManager.BroadcastLocally("world",
msg,
Session.Encryption.kDefault,
Session.Transport.kTcp);
}
|
示例: 向所有服务器上的所有客户端发送数据包
1 2 3 4 5 6 7 8 9 | void BroadcastToAllClients() {
Json msg;
msg["message"] = "hello!";
Rpc::PeerMap peers;
Rpc::GetPeers(&peers, true);
AccountManager::BroadcastGlobally("world", msg, peers, kDefaultEncryption, kTcp);
}
|
Important
在上述示例中,如果仅向拥有”game”标签的服务器发送数据包,将第6行更改成``Rpc::GetPeersWithTag(&peers, “game”, true);``即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public void BroadcastToAllClients()
{
JObject msg = new JObject();
msg["message"] = "hello";
Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeers(out peers, true);
AccountManager.BroadcastGlobally("world",
msg,
peers,
Session.Encryption.kDefault,
Session.Transport.kTcp);
}
|
Important
在上述示例中,如果仅向拥有”game”标签的服务器发送数据包,将第7行更改成``Rpc.GetPeersWithTag(out peers, “game”, true);``即可。
Important
AccountManager::BroadcastGlobally() 和 AccountManager::BroadcastLocally() 已按照 觉察不愿意的回滚 中所介绍的内容,通过 ASSERT_NO_ROLLBACK
标记了标签。因此,当这两个函数在会发生回滚的情况下被使用时,会发生assertion。
17.3.4. 将客户端移动到其他服务器上¶
17.3.4.1. 设置MANIFEST.json¶
在MANIFEST.json的 AccountManager
项中指定 redirection_secret
。
将32 bytes的随机16进制数字符串转换成hex字符串输入即可。(64字符)
{
"AccountManager": {
// hex 형식으로 표현한 비밀 값
"redirection_secret": "a29fd424579997bf91e3..."
}
}
运用以下命令可轻松创建。
$ python -c "import os; print ''.join('%02x' % ord(c) for c in os.urandom(32))"
该值在创建用于移动客户端的token时作为随机seed来使用。
Important
redirection_secret
值须设置为所有服务器使用相同的值。请安全保管该值,防止泄露至外部。必要时,请参考 MANIFEST.json 内容加密 。
17.3.4.2. 클라이언트를 다른 서버로 이동하기¶
매치메이킹 후 게임 서버로 이동하는 것처럼, 클라이언트가 한 서버에서 다른 서버로 옮겨가야할 경우에 AccountManager::RedirectClient() 를 함수를 통하여 클라이언트를 다른 서버로 이동 시킬 수 있습니다.
1 2 3 4 5 6 7 | class AccountManager : private boost::noncopyable {
...
static bool RedirectClient(
const Ptr<Session> &session, const Rpc::PeerId &peer_id,
const string &extra_data) ASSERT_NO_ROLLBACK;
...
};
|
1 2 3 4 5 6 | class AccountManager {
...
public static bool RedirectClient(
Session session, System.Guid peer_id, string extra_data);
...
}
|
새로 연결하는 서버로 세션의 정보를 전달하려면 extra_data
필드를 통해
전달 할 수 있습니다.
AccountManager::RedirectClient() 함수 사용시에 아래 내용을 유의해주세요.
Warning
정상적으로 클라이언트를 다른 서버로 이동하기 위해서는 옮겨갈 클라이언트가 AccountManager::CheckAndSetLoggedIn() 함수를 이용해서 로그인한 상태여야 합니다.
Warning
extra_data
값은 클라이언트를 통해서 전달되기 때문에,
클라이언트를 통해 공유해서 안되는 정보는 Rpc를 통해서 서버간에 직접
전송해야 합니다.
Warning
AccountManager::RedirectClient() 함수 호출 후 로그아웃, 로그인 과정은 엔진 내부에서 처리하고 있으므로 별도로 추가적인 로그인, 로그아웃 관련 작업은 필요하지 않습니다.
Important
게임 서버가 TCP (권장) 혹은 UDP를 사용하게끔 설정되어야 합니다. HTTP 는 요청-응답 형태의 프로토콜이므로 클라이언트가 요청하지 않은 패킷을 서버가 먼저 보낼 수 없어서 지원되지 않습니다.
예제: 특정 서버로 클라이언트를 이동 시키기
1 2 3 4 5 6 7 | Rpc::PeerId destination_server = ... // Selected from the result of Rpc::GetPeers().
std::string extra_data = "";
if (not AccountManager::RedirectClient(session, destination_server, extra_data)) {
return;
}
|
1 2 3 4 5 6 7 8 | System.Guid destination_server = ... // Selected from the result of Rpc.GetPeers().
string extra_data = "";
if (!AccountManager.RedirectClient (session, destination_server, extra_data))
{
return;
}
|
17.3.4.3. 이동 메시지 처리 과정¶
RedirectClient
함수가 호출 되고 서버 이동이 진행되는 과정은 아래와 같습니다.
우선, 기존 서버에서 유저를 로그아웃
(AccountManager::SetLoggedOut() 함수에 해당)
시킵니다. 로그아웃이 정상적으로 성공한다면 클라이언트 측으로 이동할 서버의
정보 및 랜덤 인증 토큰이 담긴 메시지(_sc_redirect
) 를 전송 한 뒤 세션을
종료합니다.
이동 메시지 처리 과정 중 로그아웃, 로그인 과정은 엔진 내부에서 처리하고
있으므로 별도로 추가적인 로그인, 로그아웃 관련 작업은 필요하지 않습니다.
클라이언트 측은 이동 메시지를 받고 난 뒤 기존 서버와의 연결을 해제하고 이동 메시지에 포함된 새 서버의 정보를 통해 연결을 시도하고 기존 서버로부터 받은 랜덤 인증 토큰을 이용해서 새 서버에서 인증을 시도해야합니다.
클라이언트가 정상적으로 이동했다면, 이동할 서버는 랜덤 인증 토큰을 이용하여 클라이언트를 검증합니다. 검증이 정상적으로 끝났다면 서버는 다시 클라이언트를 로그인 시킵니다.
Note
서버로부터 받은 이동 메시지는 아이펀 엔진이 제공하는 플러그인에서 자동으로 처리하기 때문에 클라이언트에서 수작업으로 처리하실 필요는 없습니다.
참고로 플러그인은 다음과 같은 작업을 처리합니다.
기존 서버와의 연결을 해제
새 서버와 연결
기존 서버로부터 받은 랜덤 인증 토큰을 이용해서 새 서버에서 인증 시도
클라이언트 플러그인은 2단계를 처리하는 동안에 호출될 콜백을 지원합니다. 예를 들어, 암호화 타입 지정하기, 넘겨 받은 flavor 정보에 따라 추가적인 설정하기 등의 작업을 할 수 있습니다.
자세한 내용은 클라이언트 플러그인 설명 중 跨服移动 를 참고하세요.
17.3.4.4. 새 서버에서 옮겨온 클라이언트에 대한 처리¶
클라이언트는 새 서버에 접속 후, 이전 서버가 보내준 랜덤 토큰으로 인증 과정을 거칩니다. 이 인증 과정은 아이펀 엔진이 자체적으로 수행하지만, 그 결과에 따른 후속 처리는 게임 서버에서 직접해야됩니다.
인증 결과를 받기 위해서는 다음처럼 콜백함수를 설정해야됩니다.
1 2 3 4 5 | bool MyProject::Start() {
...
AccountManager::RegisterRedirectionHandler(OnClientRedirected);
...
}
|
1 2 3 4 5 6 | public static bool Start ()
{
...
AccountManager.RegisterRedirectionHandler (OnClientRedirected);
...
}
|
이제 클라이언트가 이동해서 들어오는 경우 아이펀 엔진은 앞에서 등록된 콜백함수를 호출해줍니다.
이 때 원래 서버에서 AccountManager::RedirectClient() 에 인자로 넘긴 extra_data
를 클라이언트로부터 받아서 같이 넘겨줍니다.
1 2 3 4 5 6 7 8 9 10 11 12 | void OnClientRedirected(const std::string &account_id,
const Ptr<Session> &session,
bool success,
const std::string &extra_data) {
if (success) {
// Authentication succeeded.
...
} else {
// Authenticated failed.
...
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public static void OnClientRedirected (string account_id,
Session session,
bool success,
string extra_data)
{
if (success)
{
// Authentication succeeded.
...
}
else
{
// Authenticated failed.
...
}
}
|
17.4. Zookeeper的设置与管理¶
iFun引擎为实现分布式系统,使用了Zookeeper。
本部分对Zookeeper的使用方法做出了简单的说明。更详细的内容请参考 Zookeeper官方网站 。
17.4.1. 安装Zookeeper¶
Zookeeper可通过以下命令语运行。
Tip
为了能够安装最新版本,请从 Zookeeper官方网站 下载安装。
Ubuntu:
$ sudo apt-get update
$ sudo apt-get install zookeeper zookeeperd
$ sudo service zookeeper start
CentOS 6:
$ sudo yum install zookeeper
$ sudo service zookeeper start
CentOS 7:
$ sudo yum install zookeeper
$ sudo systemctl enable zookeeper
$ sudo systemctl start zookeeper
17.4.2. 使用Command-line工具¶
在想要查看iFun引擎创建的Zookeeper数据时,可使用zkCli.sh。可通过如下命令使用cli访问,并且可通过?(问号)命令语查看可使用的命令语。
$ cd /usr/share/zookeeper/bin/
$ ./zkCli.sh
...
[zk: localhost:2181(CONNECTED) 1]
17.4.3. iFun引擎创建的Zookeeper目录¶
iFun引擎为Zookeeper创建如下所示directory。不可随意创建、修改或删除其他目录。
/{{ProjectName}}/servers
/{{ProjectName}}/keys
/{{ProjectName}}/objects
/{{ProjectName}}/active_accounts
17.4.4. Zookeeper处理的性能分析¶
iFun引擎中,提供了对用于共享对象的Zookeeper相关处理时间进行统计 测量的功能。若想使用该功能,须激活如下参数。
对象功能设置参数 的
enable_database
分布式处理功能设置参数 的
rpc_enabled
事件功能设置参数 的
enable_event_profiler
ApiService 的
api_service_port
可按如下方式调用所提供的API来查看统计。
-
GET
http://{server ip}:{api-service-port}/v1/counters/funapi/distribution_profiling/
¶
统计结果是处理Zookeeper命令时所花费的时间,其种类和涵义如下。
统计种类
说明
all_time
累积的统计
last1min
1分钟之前的统计
execution_count
相关命令的处理次数
execution_time_mean_in_sec
平均处理时间
execution_time_stdev_in_sec
处理时间标准偏差
execution_time_max_in_sec
最长执行时间
统计结果示例
{
"zookeeper": {
"nodes": "localhost:2181",
"client_count": 10,
"all_time": {
"execution_count": 105213,
"execution_time_mean_in_sec": 0.00748,
"execution_time_stdev_in_sec": 0.026617,
"execution_time_max_in_sec": 0.249311
},
"last1min": {
"execution_count": 0,
"execution_time_mean_in_sec": 0.0,
"execution_time_stdev_in_sec": 0.0,
"execution_time_max_in_sec": 0.0
}
}
}
17.4.5. 查看Zookeeper状态¶
1) 从Zookeeper服务器获取统计:
$ echo stat | nc localhost 2181
Zookeeper version: 3.4.5--1, built on 06/10/2013 17:26 GMT
Clients:
/0:0:0:0:0:0:0:1:38670[0](queued=0,recved=1,sent=0)
/0:0:0:0:0:0:0:1:38457[1](queued=0,recved=9469,sent=9469)
Latency min/avg/max: 0/31/334
Received: 1177235
Sent: 1417245
Connections: 2
Outstanding: 0
Zxid: 0x80eb3a9
Mode: standalone
Node count: 10
2) Zookeeper统计初始化:
$ echo srst | nc localhost 2181
Server stats reset.
3) 查看Zookeeper状态:
imok
是 “I’m OK”,表示运行正常。
$ echo ruok | nc localhost 2181
imok
17.4.6. Zookeeper设置向导¶
请参考以下建议,构建应用到实际服务中的Zookeeper集群。
17.4.6.1. 建议节点数¶
Zookeeper集群的最低服务器数量为3台,以奇数数量安装。增加数量并不是提升性能,而是提升故障处理能力。但由于内部同步的问题,性能会下降。
17.4.6.2. 建议设备配置¶
CPU 4核/内存值为”预计同时在线人数x单个用户的平均数据大小”的2倍以上(由于每个游戏的单个用户平均数据大小和预计的同时在线人数均不相同,所以我们很难提供准确值。)
Note
Zookeeper的性能在游戏服务器中创建的ORM对象数量越多时,就会变得越发重要。每小时创建的对象数量越多,建议以高性能设备仅维持较少数量的zookeeper节点数。
17.4.6.3. 各节点的建议硬盘构成¶
需要2个独立的物理硬盘。
硬盘分别用于Zookeeper data和Zookeeper transaction log。在Zookeeer的设置中分别与
dataDir
和dataLogDir
相对应。如果须要同时使用已安装了OS的硬盘,Zookeeper transaction log(即
dataLogDir
)会产生更多的硬盘I/O,所以最好与Zookeeper data(即dataDir
)使用OS相同的硬盘。若有SSD,最好用于transaction log (即
dataLogDir
)。
17.4.6.4. 设置JVM¶
JVM Heap应设置成比系统内存小。否则会发生内存swap,这样会导致整体性能迅速下降。堆内存大小等JVM的设置可在 /etc/default/zookeeper
(Ubuntu环境)和 /etc/zookeeper/java.env
(CentOS)文件中进行。
17.4.6.5. 建议Zookeeper选项¶
Note
具体说明请参考 Zookeeper Configuration 。
17.4.6.5.1. globalOutstandingLimit¶
将Zookeeper服务器的队列长度限制在一定数量内。 默认值为1000个。当Zookeeper服务器中流入量大于处理量时, 根据队列的长度限制,发送到Zookeeper服务器的请求会出现延时。 查看 Zookeeper性能分析 的结果, 若处理延时,最好将该值调大。但如果调得太大, 会增加等待的请求数,从而占用更多内存,
这在Zookeeper中有可能造成Out of memory问题,敬请注意。 在
/etc/zookeeper/conf/zoo.cfg
文件中按如下所示输入即可。globalOutstandingLimit=1000
17.4.6.5.2. forceSync¶
指定Zookeeper是否
fsync()
。将该值设置为no
,可加快ZooKeeper的处理速度。 若该选项为no
,当Zookeeper服务所在服务器崩溃时,会出现硬盘无法写入的情况, 但iFun引擎不会使用常备数据,所以不会发生问题。在
/etc/zookeeper/conf/zoo.cfg
文件中按如下所示输入即可。forceSync=no
17.4.6.5.3. -Xmx{heap-size}m¶
须要将Zookeeper服务器的最大堆内存大小设置为合理的值。 否则硬盘中会使用swap文件,这会对性能产生影响。 在
/etc/zookeeper/conf/environment
文件中,有一个可以输入JAVA_OPTS
Java 选项的变量,对其可以MB为单位,按如下所示输入合理的堆内存最大值。# MB 단위이며 여기서는 6GB 를 입력했습니다. JAVA_OPTS="-Xmx6000m"若物理内存容量为4GB ,输入3GB左右即可。输入的容量须小于物理内存 容量。在决定该值时,最好的方法是 通过负载测试来测量内存使用量,据此设置值。
17.4.6.5.4. autopurge.snapRetainCount¶
除了已指定个数的最近的snapshot文件以外, 其他snapshot文件均予以删除。以
autopurge.purgeInterval
中设置的时间为单位, 定期执行。如不应用该选项,会保留所有snapshot文件,这会导致硬盘 容量不足。所以须要考虑删除snapshot文件的单独 维护作业。
默认值为3 个,最小值也为3个。 在
/etc/zookeeper/conf/zoo.cfg
文件中按如下所示输入即可。autopurge.snapRetainCount=3
17.4.6.5.5. autopurge.purgeInterval¶
将按照所设置的时间周期,运行自动删除snapshot文件的 Zookeeper task。默认值为0,此时不运行。 单位为小时。除了
autopurge.snapRetainCount
中指定的数量的最近的snapshot文件以外, 其他snapshot文件均予以删除。在
/etc/zookeeper/conf/zoo.cfg
文件中按如下所示输入即可。# 여기서는 3개를 남기고 모두 지우도록 입력했습니다. autopurge.snapRetainCount=3 # 1 시간 마다 동작합니다. autopurge.purgeInterval=1
17.5. (高级)运用RPC的跨服通信¶
iFun引擎为了进行跨服通信而支持RPC功能。 先通过Protobuf定义要使用的RPC消息,再注册收到相应RPC消息时所调用的处理器函数即可。
17.5.1. 定义RPC消息¶
跨服通信通过Protobuf进行。创建项目后,src目录下将同时生成{{ProejctName}}_rpc_messages.proto文件。
对于要创建的RPC消息,须要以extend的方式来定义 FunRpcMessage
。
Note
关于Google Protobuf的extension和语法,请参考 Google Protocol Buffers 的说明。
Important
当对 FunRpcMessage
进行extend时,字段编号须从32开始使用。0到31由iFun引擎使用。
下面的示例定义了发送字符串的 MyRpcMessage
和 EchoRpcMessage
。
1 2 3 4 5 6 7 8 9 10 11 12 13 | message MyRpcMessage {
optional string message = 1;
}
message EchoRpcMessage {
optional MyRpcMessage request = 1;
optional MyRpcMessage reply = 2;
}
extend FunRpcMessage {
optional MyRpcMessage my_rpc = 32;
optional EchoRpcMessage echo_rpc = 33;
}
|
17.5.2. 编写消息处理器¶
定义接收并处理Message的handler函数。Handler根据是否须要发送明确的RPC响应,有两种形式。
17.5.2.1. 不发送明确响应的message的handler¶
当收到RPC消息后没有必要发送响应时,可按如下所示形式创建处理器。
void OnMyRpc(const Rpc::PeerId &sender, const Rpc::Xid &xid,
const Ptr<const FunRpcMessage> &request) {
BOOST_ASSERT(request->HasExtension(my_rpc));
const MyRpcMessage &msg = request->GetExtension(my_rpc);
LOG(INFO) << msg.message() << " from " << sender;
}
public static void OnMyRpcHandler(Guid sender, Guid xid, FunRpcMessage request) {
MyRpcMessage msg = null;
if (!request.TryGetExtension_my_rpc (out msg))
{
return;
}
Log.Info ("{0} from {1}", msg.message, sender);
}
Note
当通过不明确发送响应的形式编写时,iFun引擎将从内部发送dummy响应。
17.5.2.2. 须发送明确响应的message的handler¶
须发送响应的处理器会通过最后一个参数接收 Rpc::ReadyBack
形式的finisher。
在处理器中所有处理均结束后,必须和RPC响应一同调用该finisher。
否则将处于继续等待RPC响应的状态。
Handler for “echo”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void OnEchoRpc(const Rpc::PeerId &sender, const Rpc::Xid &xid,
const Ptr<const FunRpcMessage> &request,
const Rpc::ReadyBack &finisher) {
BOOST_ASSERT(request->HasExtension(echo_rpc));
const EchoRpcMessage &echo = request->GetExtension(echo_rpc);
const MyRpcMessage &echo_req = echo.request();
LOG(INFO) << echo_req.message() << " from " << sender;
Ptr<FunRpcMessage> reply(new FunRpcMessage);
reply->set_type("echoreply");
EchoRpcMessage *echo2 = reply->MutableExtension(echo_rpc);
MyRpcMessage *echo_reply = echo2->mutable_reply();
echo_reply->set_message(echo_req.message());
finisher(reply);
}
|
Handler for “echoreply”:
1 2 3 4 5 6 7 8 9 10 11 | void OnEchoReplyRpc(const Rpc::PeerId &sender, const Rpc::Xid &xid,
const Ptr<const FunRpcMessage> &reply) {
if (not reply) {
LOG(ERROR) << "rpc call failed";
return;
}
const EchoRpcMessage &echo = reply->GetExtension(echo_rpc);
const MyRpcMessage &echo_reply = echo.reply();
LOG(INFO) << "reply " << echo_reply.message() << " from " << sender;
}
|
Handler for “echo”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void OnEchoRpcHandler(Guid sender, Guid xid, FunRpcMessage request, Rpc.ReadyBack finisher)
{
Log.Info ("OnEchoRpcHandler");
EchoRpcMessage echo_req = null;
if (!request.TryGetExtension_echo_rpc (out echo_req))
{
return;
}
Log.Info ("{0} from {1}", echo_req.request.message, sender);
FunRpcMessage reply = new FunRpcMessage();
reply.type = "echoreply";
EchoRpcMessage echo2 = new EchoRpcMessage();
echo2.reply = new MyRpcMessage();
echo2.reply.message = echo_req.request.message;
reply.AppendExtension_echo_rpc(echo2);
finisher (reply);
}
|
Handler for “echoreply”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static void OnEchoReplyRpc(Guid sender, Guid xid, FunRpcMessage reply)
{
if (reply == null)
{
Log.Error ("rpc call failed");
return;
}
EchoRpcMessage echo = null;
if (!reply.TryGetExtension_echo_rpc(out echo))
{
return;
}
Log.Info ("{0} from {1}", echo.reply.message, sender);
}
|
Note
由于是利用RPC生成时分配的事务ID(XID)来对RPC请求的响应做出判断,所以与RPC请求不同,其响应中的类型字符串并不重要。输入非空格的随机字符串即可。请求和响应时所使用的XID由iFun引擎自动设置。
17.5.3. 注册消息处理器¶
最后,根据RPC类型,映射并注册处理器。 在服务器的Install()函数中按如下所示添加代码。
17.5.3.1. 没有响应的handler¶
Rpc::RegisterVoidReplyHandler("my", OnMyRpc);
Rpc.RegisterVoidReplyHandler ("my", OnMyRpc);
17.5.3.2. 有响应的handler¶
Rpc::RegisterHandler("echo", OnEchoRpc);
Rpc.RegisterHandler ("echo", OnEchoRpc);
17.5.4. 查看接收消息的服务器ID¶
可运用 提取服务器列表 或 查找客户端访问的服务器 中介绍的方法,查看接收RPC消息的服务器ID。
17.5.5. 传输消息¶
若已通过上面的方法掌握了要发送的服务器的PeerId ,即可按如下所示发送message。
17.5.5.1. 不接收响应的消息¶
1 2 3 4 5 6 7 8 9 10 | Rpc::PeerMap peers;
Rpc::GetPeers(&peers);
Rpc::PeerId target = peers.begin()->first;
Ptr<FunRpcMessage> request(new FunRpcMessage);
// type 은 RegisterHandler 에 등록된 type 과 같아야합니다.
request->set_type("my");
MyRpcMessage *msg = request->MutableExtension(my_rpc);
msg->set_message("hello!");
Rpc::Call(target, request);
|
1 2 3 4 5 6 7 8 9 10 11 12 | Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeers (out peers);
Guid key = peers.First ().Key;
FunRpcMessage request = new FunRpcMessage ();
request.type = "my";
MyRpcMessage echomsg = new MyRpcMessage ();
echomsg.message = "hello";
request.AppendExtension_echo_rpc (echomsg);
Rpc.Call (key, request);
|
17.5.5.2. 接收响应的消息¶
1 2 3 4 5 6 7 8 9 10 | Rpc::PeerMap peers;
Rpc::GetPeers(&peers);
Rpc::PeerId target = peers.begin()->first;
Ptr<FunRpcMessage> request(new FunRpcMessage);
request->set_type("echo");
EchoRpcMessage *echo = request->MutableExtension(echo_rpc);
MyRpcMessage *echo_request = echo->mutable_request();
echo_request->set_message("hello!");
Rpc::Call(target, request, OnEchoReplyRpc);
|
1 2 3 4 5 6 7 8 9 10 11 12 | Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeers (out peers);
Guid key = peers.First ().Key;
FunRpcMessage reply_request = new FunRpcMessage ();
reply_request.type = "echo";
EchoRpcMessage reply_msg = new EchoRpcMessage ();
reply_msg.reply.message = "hello";
reply_request.AppendExtension_echo_rpc (reply_msg);
Rpc.Call (key, reply_request, OnEchoReplyRpc);
|
Important
Rpc::Call() 和_已按照 觉察不愿意的回滚 中所介绍的内容,通过 ASSERT_NO_ROLLBACK
标记了标签。因此,当这两个函数在会发生回滚的情况下使用时,会发生assertion。
17.6. 分布式处理功能的设置参数¶
17.6.1. AccountManager¶
负责玩家的登录、退出、跨服移动等功能。该功能在具有多台服务器的分布式环境中也可运行。
redirection_strict_check_server_id: 在client移动到其他服务器中进行连接时,对服务器的ID进行验证。(type=bool, default=true)
redirection_prefer_hostname: Choose between DNS hostname and IP address when client moves to a new server. (type=bool, default=true)
redirection_secret: client移动到其他服务器中进行连接时,实施验证的密钥(type=string)
17.6.2. RpcService¶
控制跨服通信。
rpc_enabled: 激活RPC功能及依赖于RPC的其他功能。(type=bool, default=false)
rpc_threads_size: 负责RPC处理的线程数(type=uint64, default=4)
rpc_port: RPC服务器中使用的TCP端口编号。(type=uint64, default=8015)
rpc_nic_name: 用于RPC通信的网络接口(NIC)。为确保安全,以及减少外部云网络使用量,最好选择连接在内网网卡上。(type=string, default=””)
rpc_use_public_address: 为进行RPC处理,要强制使用public IP,而不是NIC地址。这在云环境等NIC的IP为private IP、public IP为单独的IP时很有用。(type=bool, default=false)
rpc_tags: 保存在相应服务器中的标签。可导入代码内拥有特定标签的服务器列表。
示例: 当如此指定时,可仅选取代码内拥有dungeon_server标签的代码,或具有level:1-5标签的服务器。
"rpc_tags": [ "dungeon_server", "level:1-5" ]
rpc_message_logging_level: RPC消息的日志级别。如为0,则不保存日志。如为1,则保留事务ID、相对服务器ID、消息类型、长度。如为2,则在前面的信息中还包括消息体。(type=uint64, default=0)
几乎不需要直接更改设置的参数
rpc_backend_zookeeper: RPC通信时,使用Zookeeper(type=bool, default=true)
rpc_disable_tcp_nagle: 使用TCP会话时,通过设置TCP_NODELAY套接口选项来关闭Nagle算法。(type=bool, default=true)
enable_rpc_reply_checker: 若设置为true,当5秒以内没有RPC响应时,将显示警告框。(type=bool, default=true)
17.6.3. ZookeeperClient¶
iFun引擎在为了进行跨服通信而使用Zookeeper时,会控制与Zookeeper的连接。
zookeeper_nodes: Zookeeper服务器列表。用逗号分隔的‘IP:Port’形式的列表。(type=string, default=”localhost:2181”)
zookeeper_client_count: 指定每次在Zookeeper中建立几个连接。(type=uint64, default=4)
zookeeper_session_timeout_in_second: Zookeeper会话的超时时间。(type=uint64, default=60)
zookeeper_log_level: Zookeeper库的日志级别。(type=uint64, default=1)
Important
通过分布式功能进行通信的服务器须拥有相同的Zookeeper设置,其中包括Zookeeper node地址。 须连接在相同的Zookeeper服务器上。
Important
当一个设备上运行2个以上服务器时,须在MANIFEST.json的 SessionService
、 RpcService
、 ApiService
的设置中防止接口重叠。