手游Java游戲服務(wù)器線上真實(shí)案例分析
靈域
內(nèi)存泄露
- 現(xiàn)象:項(xiàng)目上線一周左右蘸劈,客服反饋玩家操作反映很卡翎碑,而在線玩家并不多
- 后臺(tái):top發(fā)現(xiàn)CPU占用接近100%(單核)
- 排查問題:
- 初步推斷內(nèi)存泄露或者內(nèi)存不足引起大量fullgc昭灵,導(dǎo)致gc線程占用大量cpu
- 通過:jstat -gc pid 查看gc情況
- 從下面輸出可以看到fullgc次數(shù)達(dá)到81次,fullgc的時(shí)間差不多124秒衅鹿,即2分多鐘
- 初步斷定cpu過高的原因是因?yàn)榇罅縡ullgc愕够,而fullgc的原因通常是因?yàn)閮?nèi)存占用過高
- 通過:jmap -heap pid 查看堆內(nèi)存占用信息
- 從下面的輸出可以看到concurrent mark-sweep generation(老年代)的內(nèi)存占用已經(jīng)使用了98%
- 通過:jmap -histo pid 查看對(duì)內(nèi)存的對(duì)象數(shù)量和占用大小
- 從下面的輸出可以看到:
- 第一位的是[B,即byte[]數(shù)組浪讳,差不多占用了1個(gè)多G,因?yàn)槭莝hallow size缰盏,所以這里是實(shí)實(shí)在在的byte[]數(shù)組申請(qǐng)
- 在程序中,直接搜索byte[]的引用相關(guān)淹遵,通晨诓拢可以確定泄露的對(duì)象
- 通過程序查找(有一些第三方庫(kù)沒有關(guān)聯(lián)源代碼),發(fā)現(xiàn)引用byte[]的對(duì)象透揣,結(jié)合輸出發(fā)現(xiàn)遠(yuǎn)遠(yuǎn)達(dá)不到60多萬(wàn)個(gè)實(shí)例的數(shù)量級(jí)(如ByteBuffer/HeapByteBUffer)
- 總共有60多萬(wàn)個(gè)byte[],在直接導(dǎo)出的對(duì)象中實(shí)例中查找類似數(shù)量級(jí)的济炎,找到一個(gè)對(duì)象:
22: 599961 14399064 com.google.protobuf.LiteralByteString
- 查看LiteralByteString的源代碼(protobuf的源代碼),其持有一個(gè)byte數(shù)組的,而其被調(diào)用是通過ByteString#copyFrom調(diào)用辐真,調(diào)用一次方法都會(huì)new一個(gè)LiteralByteString對(duì)象
- 進(jìn)而查找ByteString#copyFrom的調(diào)用層次须尚,排查到了BFResult#buildReplaydata
- 即對(duì)于實(shí)際業(yè)務(wù)來(lái)說,要緩存每個(gè)玩家15天的戰(zhàn)報(bào)侍咱,而戰(zhàn)報(bào)的BattleReplayData都會(huì)持有一個(gè)ByteString.copyFrom返回的LiteralByteString(bindata),從而確定泄露的主要原因是內(nèi)存的戰(zhàn)報(bào)數(shù)據(jù)過大
- 另外注意protobuf中協(xié)議對(duì)象中的String類型的字段實(shí)現(xiàn)都是利用ByteString,所以也會(huì)持有LiteralByteString
- 總結(jié):
- 本地cache使用lru_cache耐床,將近期最少使用的數(shù)據(jù)移除內(nèi)存,保證本地cache在一個(gè)比較穩(wěn)定的數(shù)值
- 將戰(zhàn)報(bào)數(shù)據(jù)放在遠(yuǎn)程的redis中,從而避免本地jvm內(nèi)存過大從而引起頻繁gc
YGC YGCT FGC FGCT GCT
171 12.978 81 123.925 136.903
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 314048512 (299.5MB)
used = 188444480 (179.71466064453125MB)
free = 125604032 (119.78533935546875MB)
60.00489503991027% used
Eden Space:
capacity = 279183360 (266.25MB)
used = 164671168 (157.04266357421875MB)
free = 114512192 (109.20733642578125MB)
58.983160027875584% used
From Space:
capacity = 34865152 (33.25MB)
used = 23773312 (22.6719970703125MB)
free = 11091840 (10.5780029296875MB)
68.18645735432331% used
To Space:
capacity = 34865152 (33.25MB)
used = 0 (0.0MB)
free = 34865152 (33.25MB)
0.0% used
concurrent mark-sweep generation:
capacity = 2872311808 (2739.25MB)
used = 2842293544 (2710.6223526000977MB)
free = 30018264 (28.627647399902344MB)
98.9549092853919% used
num #instances #bytes class name
----------------------------------------------
1: 609212 1170636376 [B
2: 40153651 642458416 java.lang.Float
3: 8681504 347260160 san.game.attribute.value.complexValue
4: 2229486 204888400 [Ljava.lang.Object;
5: 6010272 144246528 san.game.attribute.value.byteValue
6: 5008560 120205440 san.game.attribute.value.floatValue
7: 4063436 97522464 java.util.ArrayList
8: 6010272 96164352 san.game.attribute.changeProcessor.attrChangeProcessor$esProcessor
9: 5728549 91656784 java.lang.Byte
10: 875024 42001152 san.game.talent.TalentSubitem
11: 333904 37397248 san.game.character.GameCharacter
12: 1001712 32054784 san.game.attribute.AttributeModifyer
13: 1268096 30434304 san.game.character.AttrModifySet$Attr
14: 598886 28746528 san.proto.SanCommon$BattleReplayData
15: 633698 25347920 san.game.skill.impls.NormalSkill
16: 1001712 24041088 san.game.attribute.value.intValue
17: 715739 22903648 java.util.HashMap$Node
18: 502983 19150384 [C
19: 272713 16792672 [I
20: 634048 15217152 san.game.character.AttrModifySet
21: 604388 14505312 java.lang.Long
22: 599961 14399064 com.google.protobuf.LiteralByteString
public static ByteString copyFrom(byte[] bytes, int offset, int size) {
byte[] copy = new byte[size];
System.arraycopy(bytes, offset, copy, 0, size);
return new LiteralByteString(copy);
}
public java.lang.String getPlayerName() {
java.lang.Object ref = playerName_;
if (ref instanceof java.lang.String) {
return (java.lang.String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
java.lang.String s = bs.toStringUtf8();
if (bs.isValidUtf8()) {
playerName_ = s;
}
return s;
}
}
public com.google.protobuf.ByteString getPlayerNameBytes() {
java.lang.Object ref = playerName_;
if (ref instanceof java.lang.String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8(
(java.lang.String) ref);
playerName_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
如何計(jì)算對(duì)象大小
- 在上一個(gè)例子中楔脯,通過jmap可以查看堆內(nèi)存中對(duì)象的實(shí)例和大小撩轰,如
num #instances #bytes class name
----------------------------------------------
1: 8095738 129531808 java.lang.Float
3: 1731886 69275440 san.game.attribute.value.complexValue
- 第二列是實(shí)例的數(shù)目,第三列是實(shí)例占用的字節(jié)數(shù)昧廷,第四列是類的名字
- HotSpot的對(duì)齊方式為8字節(jié)對(duì)齊:
- (對(duì)象頭 + 實(shí)例數(shù)據(jù) + padding) % 8等于0 且 0 <= padding < 8
- Float對(duì)象大小計(jì)算
- Float類中只有一個(gè) private final float value,4個(gè)字節(jié)堪嫂,即實(shí)例數(shù)據(jù)為4個(gè)字節(jié)
- 32位對(duì)象頭8個(gè)字節(jié),64位16個(gè)字節(jié)
- 通過:jinfo pid查看是否開啟指針壓縮(64bit 1.8 JVM)木柬,可以看到默認(rèn)開啟了指針壓縮皆串,即-XX:+UseCompressedOops,所以對(duì)象頭變?yōu)榱?2字節(jié)
- 所以對(duì)象大小:對(duì)象頭:12字節(jié) + 實(shí)例數(shù)據(jù):4字節(jié) = 16字節(jié) = 129531808 / 8095738
- complexValue對(duì)象大小計(jì)算
- 同上弄诲,對(duì)象頭:12字節(jié)
- 當(dāng)前類有3個(gè)Float引用+1個(gè)iRelateCalculator引用
- 引用在32bit上是4個(gè)字節(jié)愚战、64bit是8個(gè)字節(jié)、開啟指針壓縮后是4個(gè)字節(jié)齐遵,即當(dāng)前類的實(shí)例數(shù)據(jù)是:4 * 4 = 16字節(jié)
- 父類:iSimpleValue中有1個(gè)Float引用和一個(gè)iAttrChangeProcessor引用,即父類的實(shí)例數(shù)據(jù)是:2 * 4 = 8個(gè)字節(jié)
- 所以對(duì)象大兴濉:12 + 16 + 8 = 36
- 加上對(duì)其padding = 40(8的倍數(shù))= 69275440 / 1731886
- 總結(jié):
- 通過jmap -histo查看的對(duì)象內(nèi)存占用大小指的是shallow size
- HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍梗摇,換句話說就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。對(duì)象頭部分正好似8字節(jié)的倍數(shù)(1倍或者2倍)想许,因此當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊的話伶授,就需要通過對(duì)齊填充來(lái)補(bǔ)全
- 要考慮是否開啟指針壓縮
VM Flags:
Non-default VM flags:
-XX:CICompilerCount=3
-XX:+HeapDumpOnOutOfMemoryError
-XX:InitialHeapSize=4294967296 -XX:MaxHeapSize=4294967296
-XX:MaxNewSize=348913664 -XX:MaxTenuringThreshold=6
-XX:MinHeapDeltaBytes=196608 -XX:NewSize=348913664
-XX:OldPLABSize=16 -XX:OldSize=3946053632
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
如何找到j(luò)ava進(jìn)程占用cpu最高的線程調(diào)用
- jps:找到j(luò)ava進(jìn)程id
- top -H -p pid:列出pid下線程占用情況 或者top -Hp pid
- printf 0x%x tid:找到那個(gè)線程pid断序,轉(zhuǎn)為16進(jìn)制
- jstack pid > pid_stack.log :打印線程堆棧并重定向文件
- 在pid_stack.log中查詢上面找到的線程tid
- 本例來(lái)看:可以看到nio的這個(gè)線程cpu占用很高:
- 因?yàn)樵擁?xiàng)目網(wǎng)絡(luò)層不是是直接用nio2這個(gè)庫(kù)寫的,而非用網(wǎng)絡(luò)層框架netty等
- JDK NIO的BUG糜烹,例如臭名昭著的epoll bug违诗,它會(huì)導(dǎo)致Selector空輪詢,最終導(dǎo)致CPU 100%疮蹦。官方聲稱在JDK1.6版本的update18修復(fù)了該問題诸迟,但是直到JDK1.7版本該問題仍舊存在,只不過該bug發(fā)生概率降低了一些而已愕乎,它并沒有被根本解決
- 關(guān)于這個(gè)bug相關(guān)的文章以及其他框架如netty是如何解決這個(gè)問題
- cpu 100% 通常的思路是查看runnable的線程
"pool-1-thread-5" #16 prio=5 os_prio=0 tid=0x00007f5c94383800 nid=0x6004 runnable [0x00007f5c6dffe000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPoll.epollWait(Native Method)
at sun.nio.ch.EPollPort$EventHandlerTask.poll(EPollPort.java:194)
at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:268)
at sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
登陸壓力測(cè)試
- QA測(cè)試流程(python腳本)
- 從connect開始阵苇,到登陸中涉及到每一條協(xié)議,都做一個(gè)計(jì)時(shí)(從發(fā)送某條協(xié)議到收到該協(xié)議的reply的時(shí)間)感论,同時(shí)會(huì)對(duì)每個(gè)協(xié)議做響應(yīng)計(jì)數(shù)绅项,即收到多少響應(yīng)
- 壓測(cè)n人,如500人比肄,循環(huán)登陸快耿,1人登陸完退出才有新的加入,始終保持500人
- 跑n分鐘芳绩,如5分鐘润努,計(jì)算5分鐘之內(nèi)處理了多少登陸所有的請(qǐng)求,即最終收到了多少個(gè)done示括,即登陸完成的reply铺浇,然后用這個(gè)值 / 5 * 60 即得到每秒處理的登陸的數(shù)目
- 消息流程:
- CHECK_VERSION --- CHECK_VERSION_REPLY 檢查版本
- LOGIN --- LOGIN_REPLY 登陸驗(yàn)證
- MAIN_CHAR_CREATE --- MAIN_CHAR_CREATE_REPLY + LOAD_DATA_OVE 創(chuàng)建角色
- 流程分析:
- 檢查版本,這個(gè)沒有耗時(shí)
- 登陸驗(yàn)證垛膝,因?yàn)槭莾?nèi)部dev登陸鳍侣,所以沒有直接返回登陸成功
- 登陸成功后,去數(shù)據(jù)庫(kù)加載賬號(hào)角色信息
- 如果賬號(hào)角色下沒有信息吼拥,客戶端會(huì)發(fā)送創(chuàng)建角色
- 創(chuàng)建角色
- 去賬號(hào)中心獲取一個(gè)唯一id
- 存庫(kù)
- 存庫(kù)成功后向客戶端返回創(chuàng)建角色回復(fù)和加載數(shù)據(jù)結(jié)束的消息
- 優(yōu)化及改進(jìn)
- 數(shù)據(jù)庫(kù)部分
- 該項(xiàng)目最初的數(shù)據(jù)層這部分就是一個(gè)單線程+一個(gè)數(shù)據(jù)庫(kù)操作隊(duì)列+1個(gè)數(shù)據(jù)庫(kù)連接
- 登陸壓測(cè)的時(shí)候執(zhí)行幾十萬(wàn)次加載賬號(hào)角色的數(shù)據(jù)庫(kù)操作倚聚,全部阻塞在了這塊導(dǎo)致大量等待
- 解決:使用數(shù)據(jù)庫(kù)連接池+多線程+多隊(duì)列,改進(jìn)后處理數(shù)量級(jí)直線上升
- 邏輯優(yōu)化
- 創(chuàng)建角色存庫(kù)的時(shí)候凿可,調(diào)用的是通用的存庫(kù)方法惑折,該存庫(kù)方法大約執(zhí)行了8,9條的數(shù)據(jù)庫(kù)操作(角色其他相關(guān)信息等)
- 修正這里:創(chuàng)建角色存庫(kù)的時(shí)候只需要存儲(chǔ)角色基本信息
- 對(duì)于創(chuàng)建角色回復(fù)和加載數(shù)據(jù)完畢回復(fù)兩條消息不需要等待創(chuàng)建角色存庫(kù)回調(diào)返回后再發(fā)送給客戶端,在賬號(hào)中心獲取完id后直接回包
- 另外在加載賬號(hào)信息后server這邊可以直接主動(dòng)創(chuàng)建角色枯跑,而非是給客戶端回一個(gè)包客戶單再發(fā)送創(chuàng)建角色消息(歷史原因:上一個(gè)項(xiàng)目的游戲是可以讓玩家選擇創(chuàng)建角色的)惨驶,需要客戶端配合修改
- 其他優(yōu)化
- 數(shù)據(jù)庫(kù)連接數(shù)目、多線程數(shù)敛助、多隊(duì)列數(shù) 不斷的進(jìn)行調(diào)優(yōu) 確定一個(gè)合適的值
- 網(wǎng)絡(luò)層這塊在壓測(cè)部分出現(xiàn)了epollWait粗卜,即經(jīng)常cpu飆滿,原因上面已經(jīng)解釋了纳击,建議網(wǎng)絡(luò)層這部分修改為netty等nio框架
- 將數(shù)據(jù)庫(kù)操作細(xì)化续扔,如對(duì)于加載角色數(shù)據(jù)這樣的操作攻臀,屬于邏輯數(shù)據(jù)數(shù)據(jù)庫(kù)操作,會(huì)影響玩家感受的操作纱昧,這樣的操作會(huì)一個(gè)單獨(dú)的數(shù)據(jù)庫(kù)線程池去操作刨啸;而對(duì)于類似玩家下線存盤的操作放到另外一個(gè)數(shù)據(jù)庫(kù)線程池去操作;當(dāng)然必須要考慮到順序的問題识脆,如A玩家的存儲(chǔ)是在A線程设联,而加載是在B線程,二者的順序如果不確定的話會(huì)造成嚴(yán)重的問題
- 必須使用緩存存璃,建議使用redis
- 其他問題
- 項(xiàng)目中有一個(gè)策略即玩家下線后不從內(nèi)存移除仑荐,這個(gè)時(shí)間默認(rèn)是10分鐘
- 而持續(xù)壓測(cè)(連續(xù)5分鐘)倒進(jìn)了大約4,5w人,相當(dāng)于4,5w同時(shí)在線纵东,此時(shí)內(nèi)存撐不住了粘招,導(dǎo)致大量的fullgc,從而因?yàn)閏pu飆滿
- 解決:
- 修改斷線存庫(kù)的時(shí)間偎球,由10分鐘改為了2分鐘洒扎,但是處理大約30000多個(gè)請(qǐng)求后,壓測(cè)客戶端基本收不到請(qǐng)求了
- 懷疑原因是因?yàn)檫^了2分鐘衰絮,大量的數(shù)據(jù)開始從內(nèi)存移除(解決了fullgc問題)袍冷,開始大量的執(zhí)行存盤操作,從而登陸load這種數(shù)據(jù)庫(kù)操作一直在等待
- 所以需要平衡內(nèi)存猫牡、效率等問題夯膀,結(jié)合調(diào)優(yōu)數(shù)據(jù)做出最好的選擇
IOS刷單
- 充值流程
- 客戶發(fā)起充值-> SDK -> ios返回訂單
- 金山通行證去蘋果驗(yàn)證 -> 驗(yàn)證通過 -> 給xg(有效訂單)
- xg再次去蘋果驗(yàn)證 -> 驗(yàn)證通過-> 回調(diào)游戲
- 如何刷單
- 玩家利用軟件利用一個(gè)原始訂單偽造多個(gè)訂單 -> 發(fā)起了充值 -> 偽造ios驗(yàn)證中心返回一個(gè)偽訂單
- 而金山通行證服務(wù)器端未做排重處理 ->導(dǎo)致驗(yàn)證通過從轉(zhuǎn)xg -> xg驗(yàn)證該訂單也存在
- 回調(diào)游戲 -> 造成刷單
- 解決
- xg和金山通行證都應(yīng)該校驗(yàn)訂單重復(fù)等
禮包碼問題
- 現(xiàn)象:運(yùn)營(yíng)測(cè)試禮包碼時(shí)一直失敗乃秀,而其他人測(cè)試禮包碼則沒有任何問題
- 異常:
ERROR] GmVerifyGiftCard error : java.lang.IllegalArgumentException: Illegal character in query at index 72: http://charge.ly.xoyo.com/gm_center/gift_card_check.php?cardnum=000223ec
&serverid=10006&username=meizu%26meizu__117598875&channelid=meizu
- 即一致提示禮包碼參數(shù)異常
- 解決:
- 通過cat -A 2016-04-07_error.log
- -A, --show-all equivalent to -vET
- -E, --show-ends display $ at end of each line
- 即通過cat -A參數(shù)可以在行尾打印$
- 此時(shí)查看:發(fā)現(xiàn)carnum后面多了一$威酒,即輸入的禮包碼有一個(gè)回車換行
- 則只需要在server中對(duì)輸入的禮包碼進(jìn)行過濾即可
- 原因:
- 運(yùn)營(yíng)為測(cè)試游戲方便骗卜,是在pc使用的安卓模擬器進(jìn)行的禮包碼測(cè)試
- 而禮包碼測(cè)試是從excel粘貼而來(lái)的,但是運(yùn)營(yíng)粘貼的是單元格震庭,而不是單元格內(nèi)容瑰抵,粘貼單元格就會(huì)多一個(gè)換行
- 已在linux#vim測(cè)試并通過cat -A測(cè)試(即粘貼excel單元格確實(shí)會(huì)多一個(gè)換行)
http://charge.ly.xoyo.com/gm_center/gift_card_check.php?cardnum=000223ec$
&serverid=10006&username=meizu%26meizu__117598875&channelid=meizu $
金山云LB問題
- 現(xiàn)象:線上大量的登陸驗(yàn)證超時(shí)-SocketTimeoutException
- 原因:
- 登陸驗(yàn)證的url在外網(wǎng)可以訪問
- 但是ssh登陸游戲服務(wù)器后,用curl則無(wú)法訪問
- 總結(jié):
- 理論上和xg一點(diǎn)關(guān)系沒有器联,但是xg用的金山云二汛,使用的外網(wǎng)負(fù)載.如果你從外網(wǎng)訪問西瓜的負(fù)載不會(huì)有任何問題的
- 簡(jiǎn)單來(lái)說:就是金山云內(nèi)部機(jī)器相互訪問(內(nèi)部的機(jī)器訪問內(nèi)部機(jī)器的外網(wǎng)負(fù)載)有問題
- 金山云忽略了,可能內(nèi)網(wǎng)訪問一個(gè)沒有內(nèi)網(wǎng)策略的外網(wǎng)負(fù)載拨拓,他的路由沒有走公網(wǎng)肴颊,而是在金山云內(nèi)部的路由,造成response有問題
- 金山云自己判斷了你訪問的是外網(wǎng)千元,但是實(shí)際請(qǐng)求就沒有出外網(wǎng)苫昌,直接在內(nèi)部判斷了
- 臨時(shí)解決方案:
- 西瓜的入訪添加一個(gè)我們的內(nèi)網(wǎng)ip
- xg: 防火墻這塊加好后,需要提供ip給金山云的同事 修改一下底層配置
- 是金山云LB(負(fù)載均衡)的bug
- 建議:新服上線前幸海,都可能需要測(cè)試網(wǎng)絡(luò)連通性以及添加內(nèi)網(wǎng)防火墻了
線上玩家利用WPE抓包修改協(xié)議包
- 真實(shí)玩家利用服務(wù)器邏輯漏洞祟身,利用wpe修改包,達(dá)到作弊目的(運(yùn)營(yíng)同學(xué)打入玩家內(nèi)部物独,是一個(gè)15歲的00后)
- 如:
- 卡牌游戲上陣可以設(shè)置一個(gè)先鋒技袜硫,先鋒技可以加怒氣
- 但服務(wù)器邏輯未判斷只能有一張卡牌用先鋒技能
- 玩家利用wpe修改協(xié)議包,修改為5張上陣卡牌都使用了先鋒技(正车猜ǎ客戶端已經(jīng)屏蔽掉只能使用一個(gè)先鋒技)婉陷,這樣服務(wù)器計(jì)算怒氣很大,從而達(dá)到無(wú)敵
- 總結(jié):
- 服務(wù)器邏輯一定要嚴(yán)謹(jǐn),The Server is the man
- 可在協(xié)議設(shè)計(jì)這塊做的更好一些官研,讓作弊的成本最大化秽澳,可參考
合服后啟動(dòng)server失敗
- 現(xiàn)象:?jiǎn)?dòng)一段時(shí)間后,查看log(tail -f)一直停留在加載競(jìng)技場(chǎng)玩家數(shù)據(jù)中戏羽,沒有繼續(xù)啟動(dòng)下去担神;而正常的log會(huì)有加載競(jìng)技場(chǎng)玩家數(shù)據(jù)完畢的log,且會(huì)繼續(xù)啟動(dòng)
- 排查
- jmap/top/jstat 查看內(nèi)存 cpu gc等都沒有任何問題
- jstack導(dǎo)出線程堆棧始花,也沒發(fā)現(xiàn)有線程阻塞妄讯,不過導(dǎo)出線程堆棧之后 發(fā)現(xiàn)沒有主線程
- 反思
- 其實(shí)在用jstack查看線程堆棧的時(shí)候沒有main,即說明主線程退出了酷宵,肯定是有error-所以當(dāng)時(shí)就應(yīng)該直接查詢error亥贸,而不是對(duì)比啟動(dòng)成功的日志(會(huì)輸出加載競(jìng)技場(chǎng)數(shù)據(jù)完畢的消息)和啟動(dòng)失敗的日志
- 因?yàn)閱?dòng)成功后,主線程還會(huì)在浇垦,因?yàn)闀?huì)監(jiān)聽kill命令炕置,啟動(dòng)后的主線程的堆棧類似如下
- 即如果發(fā)現(xiàn)主線程不存在,則說明中間邏輯出了問題
- 而且出問題的時(shí)男韧,應(yīng)該直接查詢?nèi)罩镜膃rror或者異常信息朴摊,第一時(shí)間排查,而不是tail -f
- 原因
- 因?yàn)楹戏?數(shù)據(jù)庫(kù)幾十萬(wàn)條數(shù)據(jù))導(dǎo)致加載競(jìng)技場(chǎng)數(shù)據(jù)(5000人)時(shí)間過長(zhǎng)煌抒,超過10分鐘
- 主線程LogicServerMngr#loadGlobalData 有一個(gè)防御式編程
- 即主線程會(huì)每隔100s去檢查是否已經(jīng)加載了5000人仍劈,如果沒有加載完則一直while -> 如果加載完畢則直接返回 -> 主線程邏輯繼續(xù)跑;如果加載時(shí)間超過了10分鐘則直接返回
- 查看日志的時(shí)候有一個(gè)疏漏寡壮,即運(yùn)維通過tail -f查看日志贩疙,只看到了加載競(jìng)技場(chǎng)玩家的日志,但是其實(shí)在之前况既,主線程就已經(jīng)有error了:
- error:global data load timeout.
- error: Logic server manager startup failed!
- 因?yàn)檫@個(gè)日志是在主線程輸出的这溅,而加載競(jìng)技場(chǎng)玩家完畢的消息是在邏輯線程輸出的
- 是先從數(shù)據(jù)庫(kù)加載了競(jìng)技場(chǎng)的5000個(gè)玩家id
- 然后扔到db線程依次去加載這5000個(gè)玩家
- 主線程一個(gè)while,一直去輪訓(xùn)去檢查是否已經(jīng)加載了5000個(gè)玩家并做timeout判斷棒仍,主線程判斷超過了10分鐘悲靴,則timeout返回
- 此時(shí)異步線程依然在加載,一直在輸出莫其,導(dǎo)致后續(xù)的輸出覆蓋了之前的timeout輸出 -> 從而排查問題不好排查
- 總結(jié)
- 看來(lái)合服的話癞尚,導(dǎo)致數(shù)據(jù)庫(kù)非常大耸三,幾十萬(wàn)的數(shù)據(jù)庫(kù)量級(jí),從而使查詢變慢
- 做好優(yōu)化/用數(shù)據(jù)庫(kù)連接池/去掉部分垃圾數(shù)據(jù)
- 優(yōu)化啟動(dòng)浇揩,不在啟動(dòng)加載這么多少數(shù)據(jù) -> 或者直接加入到cache中
- 重啟以后仪壮,某個(gè)服成功啟動(dòng),這個(gè)應(yīng)該只是概率的胳徽,因?yàn)閿?shù)據(jù)庫(kù)處理速度誰(shuí)都不能保證积锅,重啟之后可能速度處理快了一點(diǎn).根本原因還是數(shù)據(jù)庫(kù)合服后過于龐大 --> 導(dǎo)致加載時(shí)間過長(zhǎng)
- 目前的解決辦法是修改這個(gè)timeout,改為30分鐘后养盗,則合服后的sever啟動(dòng)成功
"main" prio=10 tid=0x00007fca1c008800 nid=0x3a24 in Object.wait() [0x00007fca2471e000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000078a404548> (a java.lang.Object)
at java.lang.Object.wait(Object.java:503)
at san.server.QuitListener.waitBySignal(QuitListener.java:62)
- locked <0x000000078a404548> (a java.lang.Object)
at san.server.QuitListener.waitQuit(QuitListener.java:23)
at san.server.MainEntry.main(MainEntry.java:37)
long loadTime = System.currentTimeMillis();
while (!globalDataPersistence.checkArenaLoadOver()) {
if (System.currentTimeMillis() - loadTime >= 10 * 60 * 1000) {
LogMessage.error("global data load timeout.");
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
return true;
}
線上刷元寶(防御式編程)
- 現(xiàn)象
- 客服同事舉報(bào)某玩家vip等級(jí)有異常缚陷,剛開服不久便達(dá)到了vip頂級(jí)
- 原因:因?yàn)槟硞€(gè)事件處理拋出異常(該異常非畢現(xiàn)),導(dǎo)致很多邏輯出錯(cuò)
- 如領(lǐng)取郵件邏輯往核,郵件中是充值元寶箫爷,玩家增加元寶,同時(shí)增加vip經(jīng)驗(yàn)铆铆,提升vip等級(jí) ->(vip等級(jí)變化)此時(shí)拋出一個(gè)事件處理 -> 異常
- 導(dǎo)致玩家郵件未正常刪除蝶缀,玩家再次進(jìn)入游戲會(huì)繼續(xù)領(lǐng)取郵件,從而造成刷元寶
- 同時(shí)增加元寶這個(gè)統(tǒng)計(jì)也因?yàn)楫惓]有統(tǒng)計(jì)到數(shù)據(jù)庫(kù)中薄货,給排查人員造成了很大的困難
- 直到VIP等級(jí)達(dá)到上限翁都,不繼續(xù)走異常邏輯從而結(jié)束,數(shù)據(jù)庫(kù)中只記錄了一條最后的充值元寶記錄
- 解決
- 當(dāng)一個(gè)邏輯很復(fù)雜谅猾,含有多個(gè)子邏輯的時(shí)候柄慰,線上環(huán)境最好防御式編程,每個(gè)子邏輯都try/catch
- 本例這個(gè)異常是因?yàn)橐粋€(gè)記錄日志引起的異常税娜,而通常這種異常絕對(duì)不應(yīng)該讓程序邏輯出錯(cuò)坐搔,所以這種記錄日志的接口最好做一個(gè)包裝類,然后記錄日志的方法本身做try/catch
- 引申
- 想起了之前西游線上的一個(gè)類似問題敬矩,就是調(diào)度程序的問題概行,如0點(diǎn)的時(shí)候會(huì)有很多調(diào)度子邏輯,而如果其中一個(gè)調(diào)度子邏輯出現(xiàn)異常的時(shí)候弧岳,則影響了后面的自邏輯從而造成邏輯錯(cuò)誤
- 和上面一樣凳忙,對(duì)于這種調(diào)度邏輯,子邏輯一定要try/catch禽炬,尤其是對(duì)于這種一個(gè)方法內(nèi)子邏輯非常多的情況
域名擴(kuò)散
- 因重新部署環(huán)境需要將域名指向的ip替換為新的負(fù)載均衡地址
- 域名擴(kuò)散問題:即域名解析換后涧卵,有一個(gè)擴(kuò)散問題,即有一部分網(wǎng)絡(luò)還會(huì)訪問舊的解析地址腹尖,為了保證能訪問柳恐,則舊的服務(wù)器還需要維護(hù)兩三天
- 原因:
- dns是多級(jí)解析,每一級(jí)dns都可能緩存記錄;即使修改了dns的記錄乐设,要使其生效也需要較長(zhǎng)時(shí)間讼庇,這段時(shí)間dns仍然會(huì)將域名解析到舊的服務(wù)器,導(dǎo)致用戶訪問失敗
mysql的連接允許的閑置時(shí)間
- 現(xiàn)象:
- 當(dāng)seerver運(yùn)行一段時(shí)間后伤提,拋出異常:java.sql.SQLException: Could not retrieve transation read-only status server
- 原因:
- 使用了HikariCP巫俺,但是某些參數(shù)設(shè)置有一些問題
- Configure your HikariCP idleTimeout and maxLifeTime settings to be one minute less than the wait_timeout of MySQL
- mysql的連接允許的閑置時(shí)間认烁,當(dāng)超過閑置時(shí)間以后肿男,database端就會(huì)將此連接單方面廢棄,這時(shí)如果使用jdbc繼續(xù)使用之前的連接則拋異常
- 解決:
- config.setMaxLifetime(86400000 - TimeUnit.MINUTES.toMillis(1))
- config.setIdleTimeout(86400000 - TimeUnit.MINUTES.toMillis(1))
西游降魔篇3D
數(shù)據(jù)庫(kù)更新方式
- 服務(wù)器開服時(shí)用的數(shù)據(jù)庫(kù)表腳本必須是包括最新的數(shù)據(jù)庫(kù)表
- 只有當(dāng)真正線上數(shù)據(jù)庫(kù)表需要變化的時(shí)候却嗡,才需要發(fā)送更新郵件舶沛,將線上舊的服務(wù)器表更新,而不要提前發(fā)送更新數(shù)據(jù)表表的郵件 -> 即代碼版本和數(shù)據(jù)表的版本要一致
- 原因:
- 上一個(gè)大版本1.7.6在更新的時(shí)候窗价,因?yàn)橄雀聹y(cè)試服如庭,所以測(cè)試服的數(shù)據(jù)庫(kù)表更至最新。但是本人當(dāng)時(shí)(提前)發(fā)了一封郵件撼港,同時(shí)將線上的所有服務(wù)器提前增加新增的數(shù)據(jù)庫(kù)表
- 但是此時(shí)線上的大版本為1.7.0坪它,后續(xù)新開的服務(wù)器很多服務(wù)器都會(huì)清檔,用1.7.0的包新建服務(wù)器帝牡,而1.7.0的服務(wù)器中的數(shù)據(jù)庫(kù)表還是舊的.從而導(dǎo)致后續(xù)1.7.6版本更新的時(shí)候未做數(shù)據(jù)庫(kù)更新(1.7.6版本未發(fā)送數(shù)據(jù)庫(kù)更新郵件)
- 而ios的1.7.6版本又和android大區(qū)的更新時(shí)間不一致,導(dǎo)致我忽略了ios的大版本更新時(shí)間從而使ios新開的一些服務(wù)器的數(shù)據(jù)表是舊的
- 總結(jié):
- 注意運(yùn)維新建服務(wù)器的時(shí)候都會(huì)進(jìn)行清檔操作,在此之前對(duì)該服務(wù)器做的所有操作都會(huì)被清除
- 一定要明確線上ios和android通常版本更新的時(shí)間都不一樣往毡,ios通常會(huì)晚一些
- 運(yùn)維通常會(huì)提前準(zhǔn)備好服務(wù)器,部署環(huán)境靶溜,正式開服的時(shí)候只需要清檔即可
- 新版本線上更新時(shí)开瞭,如果增加了新的sql,需要通知運(yùn)維更新相關(guān)大區(qū)線上的游戲服務(wù)器同步更新數(shù)據(jù)庫(kù)
- 因?yàn)閕os版本和android版本更新時(shí)間不同罩息,通常是android版本先更新.所以會(huì)出現(xiàn)如下問題
- android 1.8.0 更新,數(shù)據(jù)庫(kù)更新嗤详,通知運(yùn)維更新所有android大區(qū)游戲服務(wù)器數(shù)據(jù)庫(kù)
- 此時(shí)ios大區(qū)還是1.7.5版本,數(shù)據(jù)庫(kù)也是1.7.5版本
- 因?yàn)閕os大區(qū)和android大區(qū)的數(shù)據(jù)庫(kù)是混在一起的 -> 所以運(yùn)維希望在更新android 1.8.0時(shí)候,順便將ios大區(qū)的數(shù)據(jù)庫(kù)也更新為1.8.0 -> 目前操作也是這樣的
- 如果不更新1.7.5版本的數(shù)據(jù)庫(kù) -> 即使將ios大區(qū)的數(shù)據(jù)庫(kù)提前更新至1.8.0 ->但是因?yàn)楹罄m(xù)ios大區(qū)開新服瓷炮,會(huì)清檔 -> 會(huì)重新用1.7.5下的數(shù)據(jù)庫(kù)更新 -> 從而以后在更新1.8.0的時(shí)候葱色,運(yùn)維需要查詢哪些服務(wù)器是新開的(更新數(shù)據(jù)庫(kù)的時(shí)候會(huì)將已部署的所有數(shù)據(jù)庫(kù)均進(jìn)行更新,但是因?yàn)樾麻_的服務(wù)器會(huì)進(jìn)行回檔娘香,所以要找出這些新開的服務(wù)器更新至1.8.0)苍狰,然后同步更新至1.8.0 -> 非常麻煩
- 總結(jié):
- 新版本如1.8.0更新數(shù)據(jù)庫(kù)的時(shí)候,請(qǐng)順便將還在運(yùn)營(yíng)如1.7.5版本的數(shù)據(jù)庫(kù)更新至1.8.0(即1.7.5的建庫(kù)腳本更新至1.8.0)
- 即使1.7.5版本開新服的時(shí)候茅主,數(shù)據(jù)庫(kù)也能保證是最新的 -> 即使清檔 -> 也沒有問題
- 注意這種方式均是增量更新舞痰,而不是對(duì)已有的sql進(jìn)行修改 -> 如果是對(duì)原有sql修改的話,必須要保證代碼版本和sql版本一致
IOS正版無(wú)法登陸(小米為發(fā)行方)
- 小米SDK的BUG诀姚,匿名用戶綁定帳號(hào)后响牛,繼續(xù)用匿名帳號(hào)去綁定帳號(hào)的接口驗(yàn)證,所以登陸失敗
- 部分網(wǎng)絡(luò)環(huán)境下,訪問 account.xiaomi.com 域名失敗呀打,導(dǎo)致小米帳號(hào)無(wú)法登陸
- 玩家綁定小米帳號(hào)后矢赁,修改密碼后,無(wú)法登陸贬丛,需要修改為原來(lái)的密碼才可以正常登陸
- 解決:
- 只能把問題反饋給小米撩银,由小米這邊進(jìn)行排查
Cause: com.mysql.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request
- 原因:
- SQL: select count(*) from player where vip >= ?
- 執(zhí)行這個(gè)sql的時(shí)候超時(shí),則直接拋出了異常
- player表數(shù)據(jù)過大豺憔,沒有索引導(dǎo)致查詢超時(shí)
- player_data和player二者合二為一了额获,將player_data中的text字段拷貝到了player表中,導(dǎo)致player表的數(shù)據(jù)激增恭应,因?yàn)閠ext是二進(jìn)制字段抄邀,是玩家數(shù)據(jù),非常大
- 解決
- player表增加索引
- 將player和player_data分開
大量玩家登陸上線頻繁gc
- 戰(zhàn)報(bào)數(shù)據(jù)較大昼榛,為了節(jié)省流量境肾,在encode的時(shí)候使用7z進(jìn)行壓縮
- 7z算法實(shí)現(xiàn)的很糟糕-7z庫(kù),里面每次會(huì)給自己分配8M內(nèi)存
- 可能會(huì)造成大量玩家登陸上線引起的頻繁GC,導(dǎo)致性能嚴(yán)重下降
- 解決:
關(guān)于新手引導(dǎo)問題引起的客戶端卡死
- 客戶端卡死問題太嚴(yán)重胆屿,后果也很嚴(yán)重
- 所以最好可以從代碼層次避免奥喻,即引導(dǎo)失敗或者一些代碼執(zhí)行失敗不會(huì)影響游戲業(yè)務(wù)流程,保證游戲客戶端穩(wěn)定性
線上數(shù)據(jù)庫(kù)某時(shí)刻流量極大而且服務(wù)器卡
- 原因:
- 玩家改名每次去數(shù)據(jù)庫(kù)查詢是否有重名,while條件則寫錯(cuò)了 -> 導(dǎo)致沒有重名繼續(xù)do非迹。环鲤。。一直沒有重名彻秆。楔绞。一直do..死循環(huán)。唇兑。
- 從而也造成了這一時(shí)刻數(shù)據(jù)庫(kù)流量極大酒朵,因?yàn)橐恢比ゲ閿?shù)據(jù)庫(kù)
- 分析:
- 服務(wù)端用了大量的while,已經(jīng)有很多地方出現(xiàn)了死循環(huán)
- 很多同學(xué)已經(jīng)打了很多補(bǔ)丁扎附,類似如果循環(huán)超過10000次蔫耽。。就break等
- 反思:禁用while
IOS Emoji表情存儲(chǔ)
- Emoji 字符的特殊之處是留夜,在存儲(chǔ)時(shí)匙铡,需要用到 4 個(gè)字節(jié)。而 MySQL 中常見的 utf8 字符集的 utf8_general_ci 這個(gè) collate 最大只支持 3 個(gè)字節(jié)碍粥。所以為了能夠存儲(chǔ) Emoji鳖眼,你需要改用 utf8mb4 字符集
- 對(duì) utf8mb4 字符集的支持是 MySQL 5.5 的新功能,所以你需要確保你使用的 MySQL 版本至少是 5.5
- 如果UTF8字符集且是Java服務(wù)器的話嚼摩,當(dāng)存儲(chǔ)含有emoji表情時(shí)钦讳,會(huì)拋出類似如下異常即字符集不支持的異常矿瘦,因?yàn)閁TF-8編碼有可能是兩個(gè)、三個(gè)愿卒、四個(gè)字節(jié)缚去,其中Emoji表情是4個(gè)字節(jié),而Mysql的utf8編碼最多3個(gè)字節(jié)琼开,所以導(dǎo)致了數(shù)據(jù)插不進(jìn)去
- 解決:
- 起名的時(shí)候過濾掉emoji表情易结,判斷字符是否在unicode BMP區(qū)域即可
- 改動(dòng)數(shù)據(jù)庫(kù)版本則相對(duì)影響較大
java.sql.SQLException: Incorrect string value: '\xF0\x9F\x92\x94' for column 'name' at row 1
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1073)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3593)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3525)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1986)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2140)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2620)
at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1662)
at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1581)
protobuf
- protobuf協(xié)議中的集合是一個(gè)UnmodifiableCollection(非常符合協(xié)議的定義,只讀屬性)
- 不能進(jìn)行移除等修改操作柜候,否則會(huì)拋出UnsupportedOperationException
上線回檔
- 原因:
- 每周四10點(diǎn)例行維護(hù)搞动,策劃提交了一版配置
- 但是獎(jiǎng)勵(lì)配置出了問題,正常獎(jiǎng)勵(lì)元寶是1改橘,現(xiàn)在是10000滋尉;正常獎(jiǎng)勵(lì)靈魂是1,現(xiàn)在是10000
- 而這些資源都是比較稀缺的飞主,而又有拍賣行可以交易
- 大量的玩家在刷這些資源,如果影響范圍較大的話高诺,可能會(huì)影響所有服務(wù)器的經(jīng)濟(jì)系統(tǒng)
- 解決:
- 問題:
- 因?yàn)檫@次例行維護(hù)碌识,只是更新配置表,所以沒有停服
- 沒有停服則沒有備份數(shù)據(jù)庫(kù)
- 而最近的一次備份庫(kù)是上午5點(diǎn)的多(現(xiàn)在備份是每天才備份一次虱而,每天5點(diǎn)多備份一次)
- 只能通過binlog查詢數(shù)據(jù)操作日志筏餐,然后進(jìn)行回檔,回檔到上午10點(diǎn)
- 反思:
- 例行維護(hù)的時(shí)候最好停服牡拇,運(yùn)維那邊停服的時(shí)候備份一下所有的庫(kù)
- 維護(hù)前一定要QA測(cè)試策劃修改的東西魁瞪,程序修改的東西
- 即一定有一個(gè)人知道這個(gè)版本修改了哪些東西,尤其是策劃修改的重要表的數(shù)值
- 能否將數(shù)據(jù)庫(kù)備份的周期再縮短一下
線上防刷(防御式編程)
- 先扣玩家元寶或者道具惠呼,然后再給玩家獎(jiǎng)勵(lì)
- 因?yàn)橄冉o玩家獎(jiǎng)勵(lì)导俘,沒問題,再扣玩家元寶或者道具的時(shí)候可能會(huì)拋出異常
- 從而導(dǎo)致玩家刷/復(fù)制
- 采用第一種方案則保證即使沒有給玩家獎(jiǎng)勵(lì)剔蹋,后續(xù)也可以進(jìn)行補(bǔ)償
- 對(duì)于客戶端傳過來(lái)的參數(shù)旅薄,必須要強(qiáng)制校驗(yàn),否則如果直接使用外掛(直接發(fā)網(wǎng)絡(luò)包)或者客戶端有bug則也會(huì)導(dǎo)致刷的問題出現(xiàn)(如使用wpe)
classloader
- Player對(duì)象不能持有XXManager這樣的東西
- 因?yàn)镻layer對(duì)象是由系統(tǒng)類加載器加載的 --> 而XXManager是自定義加載器加載的(path自定義,不在系統(tǒng)類加載器加載的path)泣崩,按照雙親原則少梁,Player對(duì)象是找不到XXManager的
- 而XXManager是可以持有Player對(duì)象的
- 因?yàn)閄XManager是自定義類加載加載的,當(dāng)加載Player的時(shí)候找不到矫付,則會(huì)按照雙親原則由系統(tǒng)類加載器加載Player,而恰恰可以加載到
在線執(zhí)行腳本
- 游戲服務(wù)器邏輯支持hotswap凯沪,動(dòng)態(tài)更新在線service業(yè)務(wù)時(shí),為什么還需要在線執(zhí)行腳本
- 如9.17的爭(zhēng)霸賽問題,因?yàn)楫惓B蛴牛斐蓤?zhí)行某個(gè)重要的方法拋出異常
- hotswap只支持將這段代碼進(jìn)行修補(bǔ)妨马,但是不能再次執(zhí)行這個(gè)方法
- 而腳本更新則可以直接更新一個(gè)腳本樟遣,腳本內(nèi)容為再次執(zhí)行這個(gè)方法;該方法可以做類似修改一些線上玩家錯(cuò)誤數(shù)據(jù)身笤,配置表數(shù)據(jù)等
排序bug
- 如果list有兩個(gè)A1,A2,A1的robtime為0豹悬,而A2的robTime = System.currentMillis
- 做compareto比較時(shí),因?yàn)橐獜?qiáng)轉(zhuǎn)為int...所以溢出液荸,造成A2排序的時(shí)候變成了負(fù)數(shù)
- 即: (int)(o.time - time) 這種方式不建議瞻佛,建議大小做判斷
- 參考effective java相關(guān)章節(jié)
A
{
long time;
}
compareto(A o)
{
return (int)(o.time - time)
}
if (robTime > o.robTime) {return -1;}
if (robTime < o.robTime) {return 1;}
return 0;
因?yàn)榭蛻舳藷o(wú)法熱更而需要做的一些妥協(xié)
- 設(shè)計(jì)的時(shí)候除了考慮性能問題,還要考慮客戶端是否支持熱更新
- 如果不支持熱更新娇钱,則一些設(shè)計(jì)如以前的設(shè)計(jì)是給客戶端數(shù)據(jù)伤柄,客戶端拼接數(shù)據(jù),如文本消息文搂,但是策劃要改文本消息适刀,消息中的參數(shù)都發(fā)生了變化
- 而客戶端不支持更新 -> 而服務(wù)器可以熱更,可能沒辦法的措施就是服務(wù)器發(fā)送給客戶端拼接后的文本煤蹭,如果策劃要改笔喉,則直接服務(wù)器修改
- 則客戶端無(wú)法熱更的情況下,不修改協(xié)議的情況下實(shí)現(xiàn)需求的變更
- 解決:
- 后續(xù)項(xiàng)目客戶端一定要支持代碼熱更新
服務(wù)器提示文本國(guó)際化
- 服務(wù)器代碼國(guó)內(nèi)版本和國(guó)際版本用一套代碼硝皂,包括配置文件
- 所以不能將語(yǔ)言這個(gè)變量不要和服務(wù)器代碼維護(hù)耦合在一起(如配置文件中有一個(gè)選項(xiàng)是多語(yǔ)言常挚,國(guó)內(nèi)版本是cn,國(guó)外版本如越南用vn稽物,但是如果這樣做的話奄毡,就相當(dāng)于維護(hù)了多套代碼)
- 解決
- 將lang這個(gè)變量放在具體的部署環(huán)境中
- 如運(yùn)維搭建越南游戲服務(wù)器的時(shí)候手動(dòng)env.sh中的lang=VN
- 而這個(gè)env.sh只會(huì)在第一次搭建的時(shí)候用到 -> 后續(xù)代碼更新或者版本更新的時(shí)候不會(huì)影響這個(gè)env.sh
- 舊方案
- 服務(wù)器包中有一個(gè)配置來(lái)描述語(yǔ)種
- 打包的時(shí)候指定語(yǔ)種 -> 用來(lái)覆蓋這個(gè)配置,相對(duì)比較麻煩
其他
- 涉及到給玩家東西的邏輯必須加上log贝或,否則和GM,玩家溝通則沒有證據(jù)
- 關(guān)鍵游戲邏輯業(yè)務(wù)加log吼过,便于排查線上問題
- 在一個(gè)已經(jīng)運(yùn)行了一段很長(zhǎng)時(shí)間的代碼上面增加代碼 -> 這段代碼最好try/catch -> 否則可能會(huì)影響之前的代碼
- server端支持邏輯熱更新是必須的,會(huì)很方便的解決一些問題咪奖;不過如果邏輯是無(wú)狀態(tài)的話盗忱,也可以將這些邏輯放在一個(gè)單獨(dú)的進(jìn)程,需要修改邏輯的時(shí)候直接kill再重啟