靈域
內存泄露
- 現象:項目上線一周左右,客服反饋玩家操作反映很卡,而在線玩家并不多
- 后臺:top發現CPU占用接近100%(單核)
-
排查問題:
- 初步推斷內存泄露或者內存不足引起大量fullgc,導致gc線程占用大量cpu
- 通過:jstat -gc pid 查看gc情況
- 從下面輸出可以看到fullgc次數達到81次,fullgc的時間差不多124秒,即2分多鐘
- 初步斷定cpu過高的原因是因為大量fullgc,而fullgc的原因通常是因為內存占用過高
- 通過:jmap -heap pid 查看堆內存占用信息
- 從下面的輸出可以看到concurrent mark-sweep generation(老年代)的內存占用已經使用了98%
- 通過:jmap -histo pid查看對內存的對象數量和占用大小
-
從下面的輸出可以看到:
- 第一位的是[B,即byte[]數組,差不多占用了1個多G,因為是shallow size,所以這里是實實在在的byte[]數組申請
- 在程序中,直接搜索byte[]的引用相關,通常可以確定泄露的對象
- 通過程序查找(有一些第三方庫沒有關聯源代碼),發現引用byte[]的對象,結合輸出發現遠遠達不到60多萬個實例的數量級(如ByteBuffer/HeapByteBUffer)
-
總共有60多萬個byte[],在直接導出的對象中實例中查找類似數量級的,找到一個對象:
22: 599961 14399064 com.google.protobuf.LiteralByteString
- 查看LiteralByteString的源代碼(protobuf的源代碼),其持有一個byte數組的,而其被調用是通過ByteString#copyFrom調用,調用一次方法都會new一個LiteralByteString對象
- 進而查找ByteString#copyFrom的調用層次,排查到了BFResult#buildReplaydata
- 即對于實際業務來說,要緩存每個玩家15天的戰報,而戰報的BattleReplayData都會持有一個ByteString.copyFrom返回的LiteralByteString(bindata),從而確定泄露的主要原因是內存的戰報數據過大
- 另外注意protobuf中協議對象中的String類型的字段實現都是利用ByteString,所以也會持有LiteralByteString
-
總結:
- 本地cache使用lru_cache,將近期最少使用的數據移除內存,保證本地cache在一個比較穩定的數值
- 將戰報數據放在遠程的redis中,從而避免本地jvm內存過大從而引起頻繁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;
}
}
如何計算對象大小
- 在上一個例子中,通過jmap可以查看堆內存中對象的實例和大小,如
num #instances #bytes class name
----------------------------------------------
1: 8095738 129531808 java.lang.Float
3: 1731886 69275440 san.game.attribute.value.complexValue
- 第二列是實例的數目,第三列是實例占用的字節數,第四列是類的名字
-
HotSpot的對齊方式為8字節對齊:
- (對象頭 + 實例數據 + padding) % 8等于0且0 <= padding < 8
-
Float對象大小計算
- Float類中只有一個 private final float value,4個字節,即實例數據為4個字節
- 32位對象頭8個字節,64位16個字節
- 通過:jinfo pid查看是否開啟指針壓縮(64bit 1.8 JVM),可以看到默認開啟了指針壓縮,即-XX:+UseCompressedOops,所以對象頭變為了12字節
- 所以對象大小:對象頭:12字節 + 實例數據:4字節 = 16字節 = 129531808 / 8095738
-
complexValue對象大小計算
- 同上,對象頭:12字節
- 當前類有3個Float引用+1個iRelateCalculator引用
- 引用在32bit上是4個字節、64bit是8個字節、開啟指針壓縮后是4個字節,即當前類的實例數據是:4 * 4 = 16字節
- 父類:iSimpleValue中有1個Float引用和一個iAttrChangeProcessor引用,即父類的實例數據是:2 * 4 = 8個字節
- 所以對象大小:12 + 16 + 8 = 36
- 加上對其padding = 40(8的倍數)= 69275440 / 1731886
-
總結:
- 通過jmap -histo查看的對象內存占用大小指的是shallow size
- HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。對象頭部分正好似8字節的倍數(1倍或者2倍),因此當對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全
- 要考慮是否開啟指針壓縮
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
如何找到java進程占用cpu最高的線程調用
- jps:找到java進程id
- top -H -p pid:列出pid下線程占用情況 或者top -Hp pid
- printf 0x%x tid:找到那個線程pid,轉為16進制
- jstack pid > pid_stack.log :打印線程堆棧并重定向文件
- 在pid_stack.log中查詢上面找到的線程tid
-
本例來看:可以看到nio的這個線程cpu占用很高:
- 因為該項目網絡層不是是直接用nio2這個庫寫的,而非用網絡層框架netty等
- JDK NIO的BUG,例如臭名昭著的epoll bug,它會導致Selector空輪詢,最終導致CPU 100%。官方聲稱在JDK1.6版本的update18修復了該問題,但是直到JDK1.7版本該問題仍舊存在,只不過該bug發生概率降低了一些而已,它并沒有被根本解決
-
關于這個bug相關的文章以及其他框架如netty是如何解決這個問題
- 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)
登陸壓力測試
-
QA測試流程(python腳本)
- 從connect開始,到登陸中涉及到每一條協議,都做一個計時(從發送某條協議到收到該協議的reply的時間),同時會對每個協議做響應計數,即收到多少響應
- 壓測n人,如500人,循環登陸,1人登陸完退出才有新的加入,始終保持500人
- 跑n分鐘,如5分鐘,計算5分鐘之內處理了多少登陸所有的請求,即最終收到了多少個done,即登陸完成的reply,然后用這個值 / 5 * 60 即得到每秒處理的登陸的數目
-
消息流程:
- CHECKVERSION---CHECKVERSION_REPLY檢查版本
- LOGIN--- LOGIN_REPLY登陸驗證
- MAINCHARCREATE --- MAINCHARCREATEREPLY + LOADDATA_OVE 創建角色
-
流程分析:
- 檢查版本,這個沒有耗時
- 登陸驗證,因為是內部dev登陸,所以沒有直接返回登陸成功
- 登陸成功后,去數據庫加載賬號角色信息
- 如果賬號角色下沒有信息,客戶端會發送創建角色
-
創建角色
- 去賬號中心獲取一個唯一id
- 存庫
- 存庫成功后向客戶端返回創建角色回復和加載數據結束的消息
-
優化及改進
-
數據庫部分
- 該項目最初的數據層這部分就是一個單線程+一個數據庫操作隊列+1個數據庫連接
- 登陸壓測的時候執行幾十萬次加載賬號角色的數據庫操作,全部阻塞在了這塊導致大量等待
- 解決:使用數據庫連接池+多線程+多隊列,改進后處理數量級直線上升
-
邏輯優化
- 創建角色存庫的時候,調用的是通用的存庫方法,該存庫方法大約執行了8,9條的數據庫操作(角色其他相關信息等)
- 修正這里:創建角色存庫的時候只需要存儲角色基本信息
- 對于創建角色回復和加載數據完畢回復兩條消息不需要等待創建角色存庫回調返回后再發送給客戶端,在賬號中心獲取完id后直接回包
- 另外在加載賬號信息后server這邊可以直接主動創建角色,而非是給客戶端回一個包客戶單再發送創建角色消息(歷史原因:上一個項目的游戲是可以讓玩家選擇創建角色的),需要客戶端配合修改
-
其他優化
- 數據庫連接數目、多線程數、多隊列數 不斷的進行調優 確定一個合適的值
- 網絡層這塊在壓測部分出現了epollWait,即經常cpu飆滿,原因上面已經解釋了,建議網絡層這部分修改為netty等nio框架
- 將數據庫操作細化,如對于加載角色數據這樣的操作,屬于邏輯數據數據庫操作,會影響玩家感受的操作,這樣的操作會一個單獨的數據庫線程池去操作;而對于類似玩家下線存盤的操作放到另外一個數據庫線程池去操作;當然必須要考慮到順序的問題,如A玩家的存儲是在A線程,而加載是在B線程,二者的順序如果不確定的話會造成嚴重的問題
- 必須使用緩存,建議使用redis
-
其他問題
- 項目中有一個策略即玩家下線后不從內存移除,這個時間默認是10分鐘
- 而持續壓測(連續5分鐘)倒進了大約4,5w人,相當于4,5w同時在線,此時內存撐不住了,導致大量的fullgc,從而因為cpu飆滿
-
解決:
- 修改斷線存庫的時間,由10分鐘改為了2分鐘,但是處理大約30000多個請求后,壓測客戶端基本收不到請求了
- 懷疑原因是因為過了2分鐘,大量的數據開始從內存移除(解決了fullgc問題),開始大量的執行存盤操作,從而登陸load這種數據庫操作一直在等待
- 所以需要平衡內存、效率等問題,結合調優數據做出最好的選擇
IOS刷單
-
充值流程
- 客戶發起充值-> SDK -> ios返回訂單
- 金山通行證去蘋果驗證 -> 驗證通過 -> 給xg(有效訂單)
- xg再次去蘋果驗證 -> 驗證通過-> 回調游戲
-
如何刷單
- 玩家利用軟件利用一個原始訂單偽造多個訂單 -> 發起了充值 -> 偽造ios驗證中心返回一個偽訂單
- 而金山通行證服務器端未做排重處理 ->導致驗證通過從轉xg -> xg驗證該訂單也存在
- 回調游戲 -> 造成刷單
-
解決
禮包碼問題
- 現象:運營測試禮包碼時一直失敗,而其他人測試禮包碼則沒有任何問題
- 異常:
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
- 即一致提示禮包碼參數異常
-
解決:
- 通過cat -A 2016-04-07_error.log
- -A, --show-all equivalent to -vET
- -E, --show-ends display $ at end of each line
- 即通過cat -A參數可以在行尾打印$
- 此時查看:發現carnum后面多了一$,即輸入的禮包碼有一個回車換行
- 則只需要在server中對輸入的禮包碼進行過濾即可
-
原因:
- 運營為測試游戲方便,是在pc使用的安卓模擬器進行的禮包碼測試
- 而禮包碼測試是從excel粘貼而來的,但是運營粘貼的是單元格,而不是單元格內容,粘貼單元格就會多一個換行
- 已在linux#vim測試并通過cat -A測試(即粘貼excel單元格確實會多一個換行)
http://charge.ly.xoyo.com/gm_center/gift_card_check.php?cardnum=000223ec$
&serverid=10006&username=meizu%26meizu__117598875&channelid=meizu $
金山云LB問題
- 現象:線上大量的登陸驗證超時-SocketTimeoutException
-
原因:
- 登陸驗證的url在外網可以訪問
- 但是ssh登陸游戲服務器后,用curl則無法訪問
-
總結:
- 理論上和xg一點關系沒有,但是xg用的金山云,使用的外網負載.如果你從外網訪問西瓜的負載不會有任何問題的
- 簡單來說:就是金山云內部機器相互訪問(內部的機器訪問內部機器的外網負載)有問題
- 金山云忽略了,可能內網訪問一個沒有內網策略的外網負載,他的路由沒有走公網,而是在金山云內部的路由,造成response有問題
- 金山云自己判斷了你訪問的是外網,但是實際請求就沒有出外網,直接在內部判斷了
-
臨時解決方案:
- 西瓜的入訪添加一個我們的內網ip
- xg: 防火墻這塊加好后,需要提供ip給金山云的同事 修改一下底層配置
- 是金山云LB(負載均衡)的bug
- 建議:新服上線前,都可能需要測試網絡連通性以及添加內網防火墻了
線上玩家利用WPE抓包修改協議包
- 真實玩家利用服務器邏輯漏洞,利用wpe修改包,達到作弊目的(運營同學打入玩家內部,是一個15歲的00后)
-
如:
- 卡牌游戲上陣可以設置一個先鋒技,先鋒技可以加怒氣
- 但服務器邏輯未判斷只能有一張卡牌用先鋒技能
- 玩家利用wpe修改協議包,修改為5張上陣卡牌都使用了先鋒技(正常客戶端已經屏蔽掉只能使用一個先鋒技),這樣服務器計算怒氣很大,從而達到無敵
-
總結:
- 服務器邏輯一定要嚴謹,The Server is the man
-
可在協議設計這塊做的更好一些,讓作弊的成本最大化,可參考
合服后啟動server失敗
- 現象:啟動一段時間后,查看log(tail -f)一直停留在加載競技場玩家數據中,沒有繼續啟動下去;而正常的log會有加載競技場玩家數據完畢的log,且會繼續啟動
-
排查
- jmap/top/jstat 查看內存 cpu gc等都沒有任何問題
- jstack導出線程堆棧,也沒發現有線程阻塞,不過導出線程堆棧之后 發現沒有主線程
-
反思
- 其實在用jstack查看線程堆棧的時候沒有main,即說明主線程退出了,肯定是有error-所以當時就應該直接查詢error,而不是對比啟動成功的日志(會輸出加載競技場數據完畢的消息)和啟動失敗的日志
- 因為啟動成功后,主線程還會在,因為會監聽kill命令,啟動后的主線程的堆棧類似如下
- 即如果發現主線程不存在,則說明中間邏輯出了問題
- 而且出問題的時,應該直接查詢日志的error或者異常信息,第一時間排查,而不是tail -f
-
原因
- 因為合服(數據庫幾十萬條數據)導致加載競技場數據(5000人)時間過長,超過10分鐘
-
主線程LogicServerMngr#loadGlobalData 有一個防御式編程
- 即主線程會每隔100s去檢查是否已經加載了5000人,如果沒有加載完則一直while -> 如果加載完畢則直接返回 -> 主線程邏輯繼續跑;如果加載時間超過了10分鐘則直接返回
-
查看日志的時候有一個疏漏,即運維通過tail -f查看日志,只看到了加載競技場玩家的日志,但是其實在之前,主線程就已經有error了:
- error:global data load timeout.
- error: Logic server manager startup failed!
-
因為這個日志是在主線程輸出的,而加載競技場玩家完畢的消息是在邏輯線程輸出的
- 是先從數據庫加載了競技場的5000個玩家id
- 然后扔到db線程依次去加載這5000個玩家
- 主線程一個while,一直去輪訓去檢查是否已經加載了5000個玩家并做timeout判斷,主線程判斷超過了10分鐘,則timeout返回
- 此時異步線程依然在加載,一直在輸出,導致后續的輸出覆蓋了之前的timeout輸出 -> 從而排查問題不好排查
-
總結
- 看來合服的話,導致數據庫非常大,幾十萬的數據庫量級,從而使查詢變慢
- 做好優化/用數據庫連接池/去掉部分垃圾數據
- 優化啟動,不在啟動加載這么多少數據 -> 或者直接加入到cache中
- 重啟以后,某個服成功啟動,這個應該只是概率的,因為數據庫處理速度誰都不能保證,重啟之后可能速度處理快了一點.根本原因還是數據庫合服后過于龐大 --> 導致加載時間過長
- 目前的解決辦法是修改這個timeout,改為30分鐘后,則合服后的sever啟動成功
"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;
}
線上刷元寶(防御式編程)
-
現象
- 客服同事舉報某玩家vip等級有異常,剛開服不久便達到了vip頂級
-
原因:因為某個事件處理拋出異常(該異常非畢現),導致很多邏輯出錯
- 如領取郵件邏輯,郵件中是充值元寶,玩家增加元寶,同時增加vip經驗,提升vip等級 ->(vip等級變化)此時拋出一個事件處理 -> 異常
- 導致玩家郵件未正常刪除,玩家再次進入游戲會繼續領取郵件,從而造成刷元寶
- 同時增加元寶這個統計也因為異常沒有統計到數據庫中,給排查人員造成了很大的困難
- 直到VIP等級達到上限,不繼續走異常邏輯從而結束,數據庫中只記錄了一條最后的充值元寶記錄
-
解決
- 當一個邏輯很復雜,含有多個子邏輯的時候,線上環境最好防御式編程,每個子邏輯都try/catch
- 本例這個異常是因為一個記錄日志引起的異常,而通常這種異常絕對不應該讓程序邏輯出錯,所以這種記錄日志的接口最好做一個包裝類,然后記錄日志的方法本身做try/catch
-
引申
- 想起了之前西游線上的一個類似問題,就是調度程序的問題,如0點的時候會有很多調度子邏輯,而如果其中一個調度子邏輯出現異常的時候,則影響了后面的自邏輯從而造成邏輯錯誤
- 和上面一樣,對于這種調度邏輯,子邏輯一定要try/catch,尤其是對于這種一個方法內子邏輯非常多的情況
域名擴散
- 因重新部署環境需要將域名指向的ip替換為新的負載均衡地址
- 域名擴散問題:即域名解析換后,有一個擴散問題,即有一部分網絡還會訪問舊的解析地址,為了保證能訪問,則舊的服務器還需要維護兩三天
-
原因:
- dns是多級解析,每一級dns都可能緩存記錄;即使修改了dns的記錄,要使其生效也需要較長時間,這段時間dns仍然會將域名解析到舊的服務器,導致用戶訪問失敗
mysql的連接允許的閑置時間
-
現象:
- 當seerver運行一段時間后,拋出異常:java.sql.SQLException: Could not retrieve transation read-only status server
-
原因:
- 使用了HikariCP,但是某些參數設置有一些問題
- Configure your HikariCP idleTimeout and maxLifeTime settings to be one minute less than the wait_timeout of MySQL
- mysql的連接允許的閑置時間,當超過閑置時間以后,database端就會將此連接單方面廢棄,這時如果使用jdbc繼續使用之前的連接則拋異常
-
解決:
- config.setMaxLifetime(86400000 - TimeUnit.MINUTES.toMillis(1))
- config.setIdleTimeout(86400000 - TimeUnit.MINUTES.toMillis(1))
西游降魔篇3D
數據庫更新方式
- 服務器開服時用的數據庫表腳本必須是包括最新的數據庫表
- 只有當真正線上數據庫表需要變化的時候,才需要發送更新郵件,將線上舊的服務器表更新,而不要提前發送更新數據表表的郵件 -> 即代碼版本和數據表的版本要一致
-
原因:
- 上一個大版本1.7.6在更新的時候,因為先更新測試服,所以測試服的數據庫表更至最新。但是本人當時(提前)發了一封郵件,同時將線上的所有服務器提前增加新增的數據庫表
- 但是此時線上的大版本為1.7.0,后續新開的服務器很多服務器都會清檔,用1.7.0的包新建服務器,而1.7.0的服務器中的數據庫表還是舊的.從而導致后續1.7.6版本更新的時候未做數據庫更新(1.7.6版本未發送數據庫更新郵件)
- 而ios的1.7.6版本又和android大區的更新時間不一致,導致我忽略了ios的大版本更新時間從而使ios新開的一些服務器的數據表是舊的
-
總結:
- 注意運維新建服務器的時候都會進行清檔操作,在此之前對該服務器做的所有操作都會被清除
- 一定要明確線上ios和android通常版本更新的時間都不一樣,ios通常會晚一些
- 運維通常會提前準備好服務器,部署環境,正式開服的時候只需要清檔即可
-
新版本線上更新時,如果增加了新的sql,需要通知運維更新相關大區線上的游戲服務器同步更新數據庫
-
因為ios版本和android版本更新時間不同,通常是android版本先更新.所以會出現如下問題
- android 1.8.0 更新,數據庫更新,通知運維更新所有android大區游戲服務器數據庫
-
此時ios大區還是1.7.5版本,數據庫也是1.7.5版本
- 因為ios大區和android大區的數據庫是混在一起的 -> 所以運維希望在更新android 1.8.0時候,順便將ios大區的數據庫也更新為1.8.0 -> 目前操作也是這樣的
- 如果不更新1.7.5版本的數據庫 -> 即使將ios大區的數據庫提前更新至1.8.0 ->但是因為后續ios大區開新服,會清檔 -> 會重新用1.7.5下的數據庫更新 -> 從而以后在更新1.8.0的時候,運維需要查詢哪些服務器是新開的(更新數據庫的時候會將已部署的所有數據庫均進行更新,但是因為新開的服務器會進行回檔,所以要找出這些新開的服務器更新至1.8.0),然后同步更新至1.8.0 -> 非常麻煩
-
總結:
- 新版本如1.8.0更新數據庫的時候,請順便將還在運營如1.7.5版本的數據庫更新至1.8.0(即1.7.5的建庫腳本更新至1.8.0)
- 即使1.7.5版本開新服的時候,數據庫也能保證是最新的 -> 即使清檔 -> 也沒有問題
- 注意這種方式均是增量更新,而不是對已有的sql進行修改 -> 如果是對原有sql修改的話,必須要保證代碼版本和sql版本一致
IOS正版無法登陸(小米為發行方)
- 小米SDK的BUG,匿名用戶綁定帳號后,繼續用匿名帳號去綁定帳號的接口驗證,所以登陸失敗
- 部分網絡環境下,訪問 account.xiaomi.com 域名失敗,導致小米帳號無法登陸
- 玩家綁定小米帳號后,修改密碼后,無法登陸,需要修改為原來的密碼才可以正常登陸
-
解決:
Cause: com.mysql.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request
-
原因:
- SQL: select count(*) from player where vip >= ?
- 執行這個sql的時候超時,則直接拋出了異常
- player表數據過大,沒有索引導致查詢超時
- playerdata和player二者合二為一了,將playerdata中的text字段拷貝到了player表中,導致player表的數據激增,因為text是二進制字段,是玩家數據,非常大
-
解決
- player表增加索引
- 將player和player_data分開
大量玩家登陸上線頻繁gc
- 戰報數據較大,為了節省流量,在encode的時候使用7z進行壓縮
- 7z算法實現的很糟糕-7z庫,里面每次會給自己分配8M內存
- 可能會造成大量玩家登陸上線引起的頻繁GC,導致性能嚴重下降
-
解決:
關于新手引導問題引起的客戶端卡死
- 客戶端卡死問題太嚴重,后果也很嚴重
- 所以最好可以從代碼層次避免,即引導失敗或者一些代碼執行失敗不會影響游戲業務流程,保證游戲客戶端穩定性
線上數據庫某時刻流量極大而且服務器卡
-
原因:
- 玩家改名每次去數據庫查詢是否有重名,while條件則寫錯了 -> 導致沒有重名繼續do。。。一直沒有重名。。一直do..死循環。。
- 從而也造成了這一時刻數據庫流量極大,因為一直去查數據庫
-
分析:
- 服務端用了大量的while,已經有很多地方出現了死循環
- 很多同學已經打了很多補丁,類似如果循環超過10000次。。就break等
- 反思:禁用while
IOS Emoji表情存儲
- Emoji 字符的特殊之處是,在存儲時,需要用到 4 個字節。而 MySQL 中常見的 utf8 字符集的 utf8generalci 這個 collate 最大只支持 3 個字節。所以為了能夠存儲 Emoji,你需要改用 utf8mb4 字符集
- 對 utf8mb4 字符集的支持是 MySQL 5.5 的新功能,所以你需要確保你使用的 MySQL 版本至少是 5.5
- 如果UTF8字符集且是Java服務器的話,當存儲含有emoji表情時,會拋出類似如下異常即字符集不支持的異常,因為UTF-8編碼有可能是兩個、三個、四個字節,其中Emoji表情是4個字節,而Mysql的utf8編碼最多3個字節,所以導致了數據插不進去
-
解決:
- 起名的時候過濾掉emoji表情,判斷字符是否在unicode BMP區域即可
- 改動數據庫版本則相對影響較大
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協議中的集合是一個UnmodifiableCollection(非常符合協議的定義,只讀屬性)
- 不能進行移除等修改操作,否則會拋出UnsupportedOperationException
上線回檔
-
原因:
- 每周四10點例行維護,策劃提交了一版配置
- 但是獎勵配置出了問題,正常獎勵元寶是1,現在是10000;正常獎勵靈魂是1,現在是10000
- 而這些資源都是比較稀缺的,而又有拍賣行可以交易
- 大量的玩家在刷這些資源,如果影響范圍較大的話,可能會影響所有服務器的經濟系統
-
解決:
-
問題:
- 因為這次例行維護,只是更新配置表,所以沒有停服
- 沒有停服則沒有備份數據庫
- 而最近的一次備份庫是上午5點的多(現在備份是每天才備份一次,每天5點多備份一次)
- 只能通過binlog查詢數據操作日志,然后進行回檔,回檔到上午10點
-
反思:
- 例行維護的時候最好停服,運維那邊停服的時候備份一下所有的庫
- 維護前一定要QA測試策劃修改的東西,程序修改的東西
- 即一定有一個人知道這個版本修改了哪些東西,尤其是策劃修改的重要表的數值
- 能否將數據庫備份的周期再縮短一下
線上防刷(防御式編程)
-
先扣玩家元寶或者道具,然后再給玩家獎勵
- 因為先給玩家獎勵,沒問題,再扣玩家元寶或者道具的時候可能會拋出異常
- 從而導致玩家刷/復制
- 采用第一種方案則保證即使沒有給玩家獎勵,后續也可以進行補償
- 對于客戶端傳過來的參數,必須要強制校驗,否則如果直接使用外掛(直接發網絡包)或者客戶端有bug則也會導致刷的問題出現(如使用wpe)
classloader
-
Player對象不能持有XXManager這樣的東西
- 因為Player對象是由系統類加載器加載的 --> 而XXManager是自定義加載器加載的(path自定義,不在系統類加載器加載的path),按照雙親原則,Player對象是找不到XXManager的
-
而XXManager是可以持有Player對象的
- 因為XXManager是自定義類加載加載的,當加載Player的時候找不到,則會按照雙親原則由系統類加載器加載Player,而恰恰可以加載到
在線執行腳本
-
游戲服務器邏輯支持hotswap,動態更新在線service業務時,為什么還需要在線執行腳本
- 如9.17的爭霸賽問題,因為異常,造成執行某個重要的方法拋出異常
- hotswap只支持將這段代碼進行修補,但是不能再次執行這個方法
- 而腳本更新則可以直接更新一個腳本,腳本內容為再次執行這個方法;該方法可以做類似修改一些線上玩家錯誤數據,配置表數據等
排序bug
- 如果list有兩個A1,A2,A1的robtime為0,而A2的robTime = System.currentMillis
- 做compareto比較時,因為要強轉為int...所以溢出,造成A2排序的時候變成了負數
- 即: (int)(o.time - time) 這種方式不建議,建議大小做判斷
- 參考effective java相關章節
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;
因為客戶端無法熱更而需要做的一些妥協
- 設計的時候除了考慮性能問題,還要考慮客戶端是否支持熱更新
- 如果不支持熱更新,則一些設計如以前的設計是給客戶端數據,客戶端拼接數據,如文本消息,但是策劃要改文本消息,消息中的參數都發生了變化
- 而客戶端不支持更新 -> 而服務器可以熱更,可能沒辦法的措施就是服務器發送給客戶端拼接后的文本,如果策劃要改,則直接服務器修改
- 則客戶端無法熱更的情況下,不修改協議的情況下實現需求的變更
-
解決:
服務器提示文本國際化
- 服務器代碼國內版本和國際版本用一套代碼,包括配置文件
- 所以不能將語言這個變量不要和服務器代碼維護耦合在一起(如配置文件中有一個選項是多語言,國內版本是cn,國外版本如越南用vn,但是如果這樣做的話,就相當于維護了多套代碼)
-
解決
- 將lang這個變量放在具體的部署環境中
- 如運維搭建越南游戲服務器的時候手動env.sh中的lang=VN
- 而這個env.sh只會在第一次搭建的時候用到 -> 后續代碼更新或者版本更新的時候不會影響這個env.sh
-
舊方案
- 服務器包中有一個配置來描述語種
- 打包的時候指定語種 -> 用來覆蓋這個配置,相對比較麻煩
其他
- 涉及到給玩家東西的邏輯必須加上log,否則和GM,玩家溝通則沒有證據
- 關鍵游戲邏輯業務加log,便于排查線上問題
- 在一個已經運行了一段很長時間的代碼上面增加代碼 -> 這段代碼最好try/catch -> 否則可能會影響之前的代碼
- server端支持邏輯熱更新是必須的,會很方便的解決一些問題;不過如果邏輯是無狀態的話,也可以將這些邏輯放在一個單獨的進程,需要修改邏輯的時候直接kill再重啟
posted on 2016-07-26 19:04
landon 閱讀(2883)
評論(0) 編輯 收藏 所屬分類:
GameServer