罪惡潛伏在各個(gè)角落。 --VN
游戲中的業(yè)務(wù)邏輯鹿鳖,通常是不建議在網(wǎng)絡(luò)數(shù)據(jù)包接收線程池中處理的(如Netty Worker線程組)扁眯,最好在自己的業(yè)務(wù)線程池中處理。但是翅帜,不管在哪里處理姻檀,我們的業(yè)務(wù)邏輯最好能快速完成。因?yàn)榕核Γ螒驑I(yè)務(wù)線程池的線程數(shù)量通常都是有限的施敢,如果某個(gè)業(yè)務(wù)邏輯處理時(shí)間過(guò)長(zhǎng),那是可能阻塞其他玩家數(shù)據(jù)返回的狭莱,玩家體驗(yàn)起來(lái)會(huì)感覺(jué)游戲很“卡”僵娃。
比如業(yè)務(wù)線程池開(kāi)啟了8個(gè)線程,假如游戲的登錄邏輯處理要1s鐘腋妙,因?yàn)樵诘卿涍壿嬂锬梗赡芤啻蜗虻谌狡脚_(tái)發(fā)送賬號(hào)驗(yàn)證信息,這些就是額外的網(wǎng)絡(luò)開(kāi)銷骤素;還有的第一次登錄邏輯里匙睹,可能會(huì)從數(shù)據(jù)庫(kù)load很多功能模塊數(shù)據(jù),這些就是額外的磁盤讀取開(kāi)銷济竹,這些開(kāi)銷加起來(lái)就導(dǎo)致了登錄邏輯處理時(shí)間過(guò)長(zhǎng)痕檬,在高并發(fā)情況下,比如同時(shí)有9個(gè)玩家同時(shí)登錄送浊,每個(gè)業(yè)務(wù)線程分別處理一個(gè)玩家的登錄梦谜,那第9個(gè)玩家首先就得先等1s,等前面8個(gè)玩家處理完后,然后自己的登錄邏輯再處理1s唁桩,即需總計(jì)等待2s才能登錄到游戲中闭树。
有的游戲業(yè)務(wù)線程邏輯,還是按玩家id根據(jù)線程池大小求余綁定到指定業(yè)務(wù)線程處理的荒澡,在極端的情況下报辱,如果上述9個(gè)玩家被分配到了同一個(gè)業(yè)務(wù)線程去處理,那么第2個(gè)玩家需要等2s鐘才能登錄進(jìn)游戲单山,而第9個(gè)玩家需要等9s鐘才能登錄進(jìn)游戲碍现。這對(duì)玩家的游戲體驗(yàn)肯定是極其不好的。
因此饥侵,我們?cè)谟螒蜷_(kāi)發(fā)過(guò)程中鸵赫,可以寫一個(gè)工具,監(jiān)測(cè)每條協(xié)議的調(diào)用時(shí)長(zhǎng)躏升,通過(guò)長(zhǎng)時(shí)間的監(jiān)測(cè)統(tǒng)計(jì)辩棒,最終選出最耗時(shí)的那幾條協(xié)議處理,從而不斷優(yōu)化它們膨疏,使業(yè)務(wù)邏輯處理時(shí)間越來(lái)越短一睁,提高游戲服的消息吞吐量及玩家的游戲體驗(yàn)。
在這個(gè)工具里佃却,我們可以對(duì)上行協(xié)議(即發(fā)往服務(wù)端的協(xié)議)進(jìn)行調(diào)用時(shí)長(zhǎng)監(jiān)控者吁,從而優(yōu)化業(yè)務(wù)處理時(shí)長(zhǎng);對(duì)下行協(xié)議(即服務(wù)端返回給客戶端的協(xié)議)進(jìn)行包長(zhǎng)監(jiān)控饲帅,從而及早把控客戶端能否處理過(guò)來(lái)复凳。
比如,對(duì)于protobuf協(xié)議可以這樣設(shè)計(jì)灶泵,因?yàn)槊織lprotobuf協(xié)議都是一個(gè)內(nèi)部類:
private static final ConcurrentMap<Class<? extends Message>, Stats> STATS = new ConcurrentHashMap<>();//protobuf協(xié)議 -> 監(jiān)控統(tǒng)計(jì)
public static void stats(Class<? extends Message> clazz, long time, int size) {
Stats stats = STATS.get(clazz);
if (stats == null) {//說(shuō)明是新的protobuf協(xié)議
stats = new Stats(clazz);
Stats old = STATS.putIfAbsent(clazz, stats);
if (old != null)
stats = old;
}
stats.stats(time, size);//協(xié)議處理時(shí)長(zhǎng)和包長(zhǎng)記錄
}
統(tǒng)計(jì)類Stats設(shè)計(jì)如下:
public static class Stats {
private Class<? extends Message> clazz;
private long count = 0;
private long total = 0;
private long min = Long.MAX_VALUE;
private long max = Long.MIN_VALUE;
private long minSize = 0;
private long maxSize = 0;
private long totalSize = 0;
public Stats(Class<? extends Message> clazz) {
this.clazz = clazz;
}
private synchronized void stats(long time, int size) {
count++;
total += time;
min = Math.min(min, time);
max = Math.max(max, time);
totalSize += size;
minSize = Math.min(minSize, size);
maxSize = Math.max(maxSize, size);
}
@Override
public synchronized String toString() {
StringBuilder sb = new StringBuilder();
sb.append("proto:").append(clazz.getSimpleName()).append(",")
.append("called:").append(count).append(",")
.append("avg:").append(count==0?0:total/count).append("ms,")
.append("min:").append(min==Long.MAX_VALUE?0:min).append("ms,")
.append("max:").append(max==Long.MIN_VALUE?0:max).append("ms,")
.append("avgSize:").append(count==0?0:totalSize/count).append("bytes,")
.append("minSize:").append(minSize==Long.MAX_VALUE?0:minSize).append("bytes,")
.append("maxSize:").append(maxSize==Long.MIN_VALUE?0:maxSize).append("bytes.");
return sb.toString();
}
}
在業(yè)務(wù)線程中監(jiān)控協(xié)議處理如下:
long time = System.currentTimeMillis();
method.invoke(instance, session, msg);//游戲協(xié)議處理
time = System.currentTimeMillis() - time;
HandlerStatistic.stats(msg.getClass(), time, packet.getBytes().length);
返回協(xié)議監(jiān)控如下:
conn.write(new Packet(Packet.HEAD_TCP, cmd, bytes));//返回客戶端協(xié)議
HandlerStatistic.stats(message.getClass(), 0, bytes.length);
最后在關(guān)服時(shí)導(dǎo)出到文件中:
public static void dump(File file) {
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(file);
fileWriter.append("==================statistic of handler begin(")
.append(new Date().toString())
.append(")==================\n");
for (Map.Entry<Class<? extends Message>, Stats> entry : STATS.entrySet()) {
Stats stats = entry.getValue();
fileWriter.append(stats.toString()).append("\n");
}
fileWriter.append("==================statistic of handler end(")
.append(new Date().toString())
.append(")==================\n");
fileWriter.flush();
} catch (IOException e) {
log.error("write file failed", e);
} finally {
if (fileWriter != null) {
try {
fileWriter.close();
} catch (IOException e) {
log.error("close file writer failed", e);
}
}
}
}
在導(dǎo)出邏輯里育八,我們還可以對(duì)最大時(shí)長(zhǎng),最大返回包長(zhǎng)做排序功能赦邻,提取前x名的請(qǐng)求協(xié)議或返回協(xié)議打印髓棋,從而優(yōu)化它們。
最終打印信息如下(下面的把處理時(shí)間過(guò)長(zhǎng)的篩選出來(lái)了):
==================statistic of handler begin(Thu Apr 30 15:49:38 HKT 2020)==================
proto:LoginReq_101001,called:29,avg:154ms,min:60ms,max:947ms,avgSize:167bytes,minSize:0bytes,maxSize:188bytes.
proto:GroupChatReq_109001,called:12,avg:132ms,min:16ms,max:819ms,avgSize:53bytes,minSize:0bytes,maxSize:58bytes.
proto:PvpReq_106101,called:11,avg:37ms,min:15ms,max:64ms,avgSize:2bytes,minSize:0bytes,maxSize:2bytes.
proto:FightReq_1203004,called:6,avg:24ms,min:7ms,max:62ms,avgSize:15bytes,minSize:0bytes,maxSize:15bytes.
proto:ThresholdGetHangUpRewardReq_50003,called:2,avg:51ms,min:49ms,max:54ms,avgSize:2bytes,minSize:0bytes,maxSize:2bytes.
proto:OnekeyMailReq_104005,called:2,avg:37ms,min:25ms,max:50ms,avgSize:20bytes,minSize:0bytes,maxSize:20bytes.
proto:FactionHallReq_128004,called:1,avg:18ms,min:18ms,max:18ms,avgSize:2bytes,minSize:0bytes,maxSize:2bytes.
......
==================statistic of handler end(Thu Apr 30 15:49:38 HKT 2020)==================
這樣惶洲,針對(duì)處理時(shí)間過(guò)長(zhǎng)和包長(zhǎng)返回過(guò)長(zhǎng)的按声,我們可以查閱源碼看是否有可優(yōu)化的地方。
相應(yīng)地恬吕,對(duì)于數(shù)據(jù)庫(kù)的增刪改查操作签则,我們也可做如此類似工具監(jiān)測(cè),以監(jiān)控業(yè)務(wù)中是否時(shí)間過(guò)長(zhǎng)的IO處理铐料。