標題 新增找內文!

看整串內容篇數:0 / 0

146GP-BP

#1 【心得】打造極致伺服器

發表:2013-11-09 20:23:48看他的文開啟圖片

happyjohn369(曙)

LV7 / 人類 / 初心者
巴幣:1501
GP:37
經驗:

哈囉大家好 我是曙 (好爛的開場白
由於小弟最近想創個服 所以查了一些資料                  請先按"開啟圖片"
在這邊把他整理一下 分享給大家 (也算是給自己備忘一下)

這篇文章大概會分成
  1. 硬體
  2. 作業系統
  3. Ramdisk
  4. Java
  5. 伺服器選用
  6. 伺服器設定
  7. VPN vs. IP直連 (2樓)
  8. Lag診斷 (2樓)
從頭開始打造一台伺服器
內容可能會有一些比較偏技術方面的東西 不過小弟會儘量去解釋
本文基本上只討論效能,不會對系統穩定,資料安全,資訊安全有太多著墨

     硬體  
名詞解釋:
RAM:
中文名稱:隨機存取記憶體
用來暫存運算時所產生的資料
資料關機即揮發 ,固需在關機時將資料寫入硬碟
(這是為甚麼不能直接拔掉電腦電源的原因之一)
CPU:
中文名稱:中央處理器
電腦的心臟 ,處理大部分的I/O,中斷,運算
強項在邏輯運算和整數運算上
對浮點數的運算較弱
因為其內部浮點運算單元較少
GPU:
中文名稱:圖形處理器
專為繪圖而生的晶片
由於3D繪圖需要用到大量的浮點數運算
因此GPU的設計比CPU更適合做浮點運算
GPU內含數百個可做平行處理的小核心
雖然每個核心的運算功能沒有CPU那麼強大
但是透過平行處理 其運算效能仍然很可觀
double:
中文名稱:雙精度浮點數
為一種資料型別
浮點數(有理小數)的一種
佔用8Byte的空間儲存,比float(4Byte)來的精確
要知道該加強電腦哪部份的硬體
首先您必須要知道 伺服器,網路,客戶端負責的工作分別是甚麼
伺服器負責:
世界的各種運算/事件監聽
網路就只是很單純的負責傳送資料而已
客戶端負責:
繪製3D模型,鍵盤/滑鼠的事件監聽
在瞭解了他們在遊戲時所負責的工作後
您因該可以知道他們在硬體上的需求分別是甚麼了
如果還不是很清楚 沒關係小弟說明一下:
伺服器的硬體需求:
RAM(儲存預運算的資料 以加快速度)
●CPU(進行世界的各種運算)
PS.各種運算中 ,最複雜的是實體的運算
因為實體的運算很多是浮點數(double)的運算
有概念的大大應該知道GPU的浮點運算能力比CPU好非常多
(其實GPU如果夠強 所有的運算都會比CPU強上非常多)
可是Minecraft伺服器好像不支援用GPU做浮點運算
(Java已可使用Rootbeer GPU做編譯,利用GPU運算,不過要改一下寫法)
所以就算伺服器的GPU(顯卡)裝再好 ,對運算能力一點幫助都沒有
小弟覺得 如果Minecraft用GPU做平行處理的話 效能會很可觀
網路的硬體需求: 上傳速度愈快愈好(伺服器   資料    >客戶端=上傳)
客戶端的硬體需求:
●OpenGL
(其實這應該算軟體,只是因為大部份硬體都和他有關係,所以把他放進來)
PS.OpenGL是一個開源的繪圖庫(3D,2D)
minecraft並不是直接使用OpenGL ,而是使用一個叫lwjgl的Java遊戲庫
然而lwjgl使用OpenGL來做3D的繪圖
OpenGL是一個協定而不是一套軟體
各家顯卡會自行撰寫OpenGL的驅動程式
所以繪圖時的GPU,CPU,RAM的使用量是由個驅動程式所決定的
像合版的OpenGL驅動程式通常是用CPU下去做運算
而不是用GPU(或是用整合在北橋內的GPU)
到這邊您應該已經清楚 ,伺服器該添購甚麼設備 ,甚麼東西不該買了巴
RAM容量選擇 )
CraftBukkit的Wiki上有寫關於RAM的容量建議值:
When choosing RAM amounts, it is HIGHLY recommended to run a Craftbukkit Server
1024MB (1GB) of RAM per 10 Player Slots. If your player slot amount is below 10, we recommend a 768MB plan at minimum. Due to Java's high memory consumption of the server and plugins you can (and will) never have enough RAM.
他的意思大概是這樣:
當10個玩家上線時,建議的RAM容量為1024MB(1GB)

但當上線人數小於10人的時候 ,最低RAM的容量建議為768MB
因為Java和插件本身就會消耗一些記憶體
看到這邊各位大大因該已經知道
Minecraft伺服器在耗記憶體是非常恐怖的

大概每位玩家需要分配100MB的記憶體給他

另外 Java的最大Heap
(之後會解釋)空間(-Xmx)
建議不要設成和物理記憶體一樣的大小
應該設成 [物理記憶體-作業系統使用的記憶體-(0.5M*最大玩家數)]
後面那個-(0.5MB*最大玩家數)是因為Minecraft伺服器採用NIO進行連線
這個NIO有一個東西叫Buffer ,這個東西會直接使用Heap以外的記憶體
因此 ,要為他保留一小塊記憶體 ,防止產生OutOfMemoryError(OOME)
RAM使否要用ECC )
不一定要用啦 ,如果預算允許的話 ,就買吧!
硬碟容量 )
     其實Minecraft伺服器的硬碟用量非常小 ,500GB~1T就夠用好幾年了
                                                           (有裝紀錄插件除外) (感謝佴弌大大補充)
網路選擇 )
   一般ADSL就可以撐的住了
   如果頻寬在100M以上的建議要求使用FTTH
   雖然沒有獨立線路那麼好 ,不過總比一般ADSL好一些
   如果可以 ,建議儘量使用種花電信的網路(ADSL)
   不要使用Cable(如:bb寬頻)開服
   畢竟種花是台灣最大的網路業者 ,線路也都是他在管的
   因此用種花的網路應該可以分到較好的線路(但是很燒錢)
   此外 ,Cable的頻寬是共享的 ,因此您所分到的頻寬並不固定
多主機 )
如果預算允許買多主機的話 那下面有幾種可行的方式
1.伺服器串聯:
  伺服器串聯其實就是多個伺服器負責多個世界
  而且世界分散在各伺服器中
  如果多個邏輯伺服器在同一個實體伺服器 ,那就叫多重世界
2.叢集運算(Cluster):
  叢集運算也是多伺服器負責多個世界(或一個世界)
  但是所有世界只集中在同一台伺服器
  其他伺服器就只是提供運算能力而已
  所以基本上需要一台伺服器(Master)有硬碟即可
  另外 ,做叢集運算的時候
  連接內部網路的交換器建議使用支援Gigabit,Jumbo Frame的交換器
  不然效能會很差
  而且至少要有兩台電腦才能組成叢集運算
    作業系統   
基本上這部份是最難打的 ,要保持公正很難 ,因為小弟本身就看Windows不順眼
但是小弟會儘量以最公正的說法說 (歪了要跟我講XD
由於各版Windows,Linux的特性不相同 ,所以我分析的東西不一定適用所有版本
另外 ,因為小弟手上沒有Mac,Unix
所以這兩種不列入討論,只討論Win和Linux這兩種
(FreeBSD基本上和Linux都是Unix-like的作業系統 ,因此也不討論)
PS.Windows有時候簡稱Win
1)就RAM使用量而言
名詞解釋:
Cache:
中文名稱: 快取
Cache是Linux特有的一種記憶體使用方式
他會將常用的檔案儲存到未使用的記憶體中
以加快檔案讀寫速度
但是當記憶體滿了後 ,Cache就不能再工作了
VideoRAM:
中文名稱: 顯示記憶體
這種東西通常出現在合板上
因為合板沒有獨立的顯示記憶體
故需將系統的記憶體切一小快當作顯示記憶體
前面有說過: Minecraft伺服器的記憶體需求是非常恐怖的
所以作業系統要保留足夠的記憶體給伺服器
測試環境:
  虛擬機:Oracle VM VirtualBox
  CPU*1
  RAM:1024MB(VideoRAM:27MB)
作業系統:
  Windows7 旗艦版-64bit
  Debian7.0.0-64bit (圖形介面為LXDE)
測試方式:
  待機2分鐘後測量RAM的使用量
測試結果:
  Windows: 576MB
  Linux: 160MB(不含Cache),374MB(包含Cache)
從測試結果來看 ,就算把Cache算進去 ,Linux的RAM使用量也遠低於Windows
因此 Linux勝
2)從CPU使用率來看
雖然Minecraft伺服器的CPU使用率不會很高(除了進行大量實體運算外)
但是作業系統最好不要佔用太多CPU,以備不時之需
測試環境:
  虛擬機:Oracle VM VirtualBox
  CPU*1
  RAM:1024MB(VideoRAM:27MB)
作業系統:
  Windows7 旗艦版-64bit
  Debian7.0.0-64bit (圖形介面為LXDE)
測試方式:
  待機2分鐘後測量CPU的使用率
測試結果:
  Windows: 24%
  Linux: 1%
到這邊 ,小弟只能說: Windows真的太耗資源了
因此 仍然是Linux勝
3)圖形介面
接下來要比較兩者的圖形介面
其實圖形介面這部份是按照自己最舒服的管理方式
下去選擇圖形或文字介面
但是一般會建議伺服器不要使用圖形介面 以節省資源
名詞解釋:
圖形介面:
圖形介面簡單來說就是視窗(window)啦
但詳細一點的定義圖形介面
應該要包含視窗(window),視窗管理員(window manager)
                 ,鍵盤/滑鼠事件監聽,螢幕繪製這幾個部份
文字介面:
文字介面俗稱黑盒子(就是這個Box裡的樣子)
在Linux稱為終端機
在Windwos稱為DOS或MS-DOS
是一個只有文字的世界
在這個世界裡 你不會看到任何的圖形(ASCII藝術除外)
Windwos的圖形介面:
圖形介面應該算是Windows的核心之一(畢竟他都叫"Window"s了阿)
事實上 ,他的圖形介面真的是和核心綁在一起啟動的
這樣做的優點是: 圖形介面反應快速
但是缺點是: 圖形介面只要一掛掉 ,整個系統就跟著掛了
然而Windows將這兩個東西綁在一起也造成了一個很麻煩的情況:
啟動Windows就一定要啟動圖形介面
(WinServer可以使用ServerCore模式單獨啟動文字介面,不過仍然會啟動一個視窗)

^ Windows Server的ServerCore功能
如果真的要用純文字介面的話
就用Win98的DOS模式吧
Linux的圖形介面:
Linux的圖形介面是單獨的一套軟體
沒有和核心綁在一起
所以Linux圖形介面的優缺點和Windows剛剛好相反
優點是: 可單獨啟動"純"文字介面,圖形介面掛掉只要回文字介面把他重啟就好了
缺點是: 圖形介面反應比Windows慢一點點

^ Linux的"純"文字介面(圖為Gentoo Linux)
綜合起來看
Windows的圖形介面 勝
可是用過Linux文字介面的大大都知道
Linux的文字介面比DOS友善多了
尤其是在Shell Script上
最後再次強調
選用圖形介面還是文字介面 ,以管理方便為主
沒有必要為了節省更多資源 ,而讓自己礙手礙腳的
作業系統的比較就到這邊 ,雖然好像朝Linux一面倒
不過Windows也有他的優點 ,像驅動程式支援多等等
最重要的是選擇最方便自己管理的系統
PS:如果要入門Linux最好從Ubuntu,CentOS開始
沒有一定的基礎不要隨便碰Debian,Gentoo ,光安裝就會搞死你
    Ramdisk  
在這邊先恭喜您讀到這篇文章的1/4了
這個章節要討論的是Minecraft伺服器到底有沒有需要用到Ramdisk
名詞解釋:
Ramdisk:
中文名稱:記憶體硬碟
由於現在的RAM容量愈做愈大
速度也愈來愈快
另外RAM的讀寫速度是普通硬碟的十餘倍
因此就有人想: 為甚麼不把多餘的RAM模擬成硬碟呢
這個構想就是Ramdisk(或稱RamDisk)
首先先來測一下Ramdisk和普通硬碟的速度到底差多少
測試環境:
實體機
CPU: Intel Genuine(由於之前是品牌電腦 所以CPUID被改過)
RAM: DDR2-800 1GB
HDD: Seagate 350GB SATA2
OS: Gentoo Linux
Linux Kernel:3.10.7-r1
測試檔大小:4MB
檔案大小之所以使用4MB的原因是
Minecraft伺服器最常讀寫的的檔案是地圖檔
而地圖資料夾下最大的檔案是region下的r.x.x.mca檔案
每個檔案的大小最大約在4MB上下
所以測試檔大小使用4MB
測試方法:
使用`dd bs=64k count=64 if=/dev/zero of="Test.img" `,
`dd bs=64k count=64 if="Test.img" of=/dev/null `
輸出/讀入一個大小約為4MB的檔案到Ramdisk和普通硬碟
dd指令會順便輸出所花時間和平均傳輸速度
利用平均傳輸速度比較兩者快慢
輸出結果:
Ramdisk: 讀:2.1GB/s 寫:1.0GB/s
普通硬碟: 讀:28.3MB/s 寫:110.0MB/s
^ (應該有錯 ,讀取不太可能比寫入慢)
普通硬碟+Cache: 讀:2.0GB/s 寫:160.0MB/s
PS:普通硬碟在讀取測試時
有先釋放Cache,這樣數據比較真實
經過測試證實 ,Ramdisk的讀寫速度比普通硬碟快上很多
也比SSD快上許多
(一般SSD的讀取速度約在500MB/s上下,寫入速度約200MB~400MB/s)
但是Ramdisk同時也會消耗大量記憶體

兩者之間的取捨就由各位大大決定了 (如果RAM多到滿出來的話)
但是 ,Ramdisk和Cache的概念很像
所以在Linux基本上不需要用到Ramdisk這個東西
Windwos就要稍微評估一下了
另外 ,如果您已經決定要採用Ramdisk的話
一定要買一台UPS(不斷電系統) ,不然哪天台電發神經 ,你的資料就GG了
     Java   
Minecraft畢竟是用Java寫出來的遊戲 ,所以Java的效能就非常重要
在這一個章節裡 ,我們會討論到JVM的選擇,GC優化 ,這些比較偏Java核心的東西
所以可能會偏難一點點點點點...
名詞解釋:
JVM(Java Virtual Machine):
中文名稱: Java虛擬機
專門執行已編譯的Java Bytecode的機器
原始的JVM是採直譯的方式執行Bytecode
但在J2SE1.3.1時加入了一種名為HotSpot的JVM
在HotSpot出現之前 Java的效能非常的差
可以說是HotSpot改變了Java的歷史
HotSpot不是完全採直譯的方式下去執行Bytecode
他會將常用的部份Bytecode預先編譯 ,以加快直譯速度
OpenJDK(IcedTea):
OpenJDK是Java的開放原始碼計劃 ,採用GPL授權
原始碼皆由官方(現為Oracle)提供
不過部份商業程式不公開原始碼
所以理論上效能應該和OracleJDK一模一樣
PS:在本篇 對Java的講法有Java,Jre,JDK,JVM,JSE這幾種

1)Java6? Java7?
理論上Jre7的效能會比Jre6好一些
Jre7也支援了一些Jre6不支援的語法 ,像:兩個char間的equls,switch可使用字串等
而且Jre6已經在今年(2013)2月由Oracle宣佈停止維護了
所以有任何漏洞都不會再進行修補
像前幾個月爆發的Neutrino攻擊軟體 ,就是鎖定還沒有升級的Jre6進行攻擊
因此 ,不管從任角度來看
Java7絕對是比Java6還要更佳的選擇

2)OpenJDK? OracleJDK?
前面有提到 ,OpenJDK其實就是OracleJDK的開源版
所以這兩者您要採用哪一種都無所謂
只是 ,在Linux上預設是採用OpenJDK(也可使用OracleJDK)
而Windows上預設使用OracleJDK(OpenJDK也行 只是編譯過程較為複雜)
另外 自行編譯OpenJDK會有較佳的效能(畢竟是在自己電腦上編譯的嘛)
編譯時 ,gcc的參數建議加上-O3進行最佳化
再來 有些大大可能會有一個問題:前面HotSpot說的那麼好 ,那OpenJDK有沒有HotSpot?
這個問題的答案是: 沒有
但是 ,OpenJDK上有個"OpenJDK VM" ,它是HotSpot的開源版
它的效能理論上來說和HotSpot一模一樣
所以不需要擔心OpenJDK效能比較爛的問題

2.5)32bits or 64bits
64bits的JVM可以操作更多的RAM ,這點是無傭致疑的
但是 ,在Java6時 ,多個機構測試指出:
        "在64bits的VM上執行的程式一般會比32bits的慢上大約15%"
另外 ,使用64bits會比使用32bits多消耗約10~30%的記憶體
原因是因為指標膨脹和各種資料型別補白對齊的關係
上面這些效能因素好像對64bits很不利
但在Oracle的Q&A裡有提到這樣一句話:
The good news is that with AMD64 and EM64T platforms running in 64-bit mode, the Java VM gets some additional registers which it can use to generate more efficient native instruction sequences.  These extra registers increase performance to the point where there is often no performance loss at all when comparing 32 to 64-bit execution speed.
他的意思大概是這樣:
使用AMD64或EM64T的系統架構時 ,JVM會得到額外的暫存器
這些暫存器可以用來生成更有效的本地指令
所以效能基本上會提升得和32bits差不多

3)Server mode
HotSpot有一個功能: 他可以選擇要使用ServerVM還是ClientVM
所謂的ServerVM和ClientVM到底差在哪裡呢?
最主要的差別是編譯速度和運作效能
ServerVM編譯速度慢,運作效能佳
ClientVM編譯速度快,運作效能差
他們運作的效能差多少呢, 差這麼多
這張圖您基本上只要比較紅色和黃色線條就好了(越大越好)
由這張圖可以知道, ServerVM和ClientVM的運作效能可以差到十餘倍
既然ServerVM的性能那麼好, 那為甚麼還要ClientVM
這個答案很簡單: 編譯的速度(ServerVM的編譯速度大概比ClientVM慢10%)
重點來了, ServerVM要怎麼啟動?
JSE5以後的JVM會自動判斷應該啟動ServerVM還是ClientVM
判斷標準如下:
當實體機含有2顆以上的CPU和2GB以上的RAM時 自動啟動ServerVM
如果您的伺服器不符合上面的要求, 要強制啟動ServerVM也行
只要在啟動的那行指令加入-server就好了
檢查預設啟動的VM:
要檢查預設是啟用ServerVM還是ClientVM
只要在cmd輸入java -version就好了
上面框起來的地方就是預設啟動的VM模式

名詞解釋:
GC(Garbage Collection):
中文名稱:垃圾收集
GC並非Java特有的機制 ,像Python也有類似的機制
GC最主要的工作是回收已經沒有使用到的物件(垃圾) ,並釋放空間
其他像C++之類的物件導向語言
在物件使用完後必須使用delete釋放空間
否則幾次下來 ,空間會被垃圾佔滿
Java則沒有這樣的困擾, JVM會自動幫您回收沒有在使用的物件
這種機制減輕程式設計師的負擔
但是因為這種機制, Java並沒有像C++之類的解構式這個東西
因為您永遠不知道物件甚麼時候會被回收
Java的GC執行時間不一定
有可能是定時執行,也有可能在空間快用完時才執行
當GC執行時 ,除了GC所使用的執行緒外
其他的執行緒都會暫停(stop-the-world)
因此GC所佔用的時間是愈短愈好
Heap:
中文名稱: 堆積
原為一種資料結構
但在JVM裡指的是JVM所管理的一塊記憶體區域
程式運作時 ,大部分的資料會放在這裡

3)GC是啥 能吃嗎?
雖然上面的名詞解釋那邊已經簡單介紹過GC了
不過, GC可以說是JVM的核心技術之一 ,不好好介紹一下怎麼行呢
所以下面小弟會詳細一點說明GC的運作流程
首先 ,必須先知道所謂的"垃圾年代"
哪泥! 垃圾還有年代? 沒錯, 在Java中, 垃圾是有分年代的
分成"新生代"和"老年代"(因該還有更好的翻法)
新生代(Young generation):
  絕大多數新建的物件會被放在這一區
  因為大多數的物件在建立後沒多久就會變成垃圾
  (據IBM統計:約有98%的物件在建立沒多就後就會變成垃圾)
  因此這一區也是GC執行最頻繁的一區
  在這一區所執行的GC叫作minorGC
老年代(Old generation):
  在新生代那邊沒被清掉的物件,經過一段時間後會被移到老年代這邊
  老年代所佔用的空間比較多 ,因此他的GC間隔較長(通常是在空間用完時才執行)
  每次GC執行時間也較長
  在這一區所執行的GC稱為majorGC (或稱FullGC)

                                
   Eden      Sf      St        <<新生代
                                
             Old             
                                
                                      <<老年代
                                
                                
^ 一般的GC演算法分區

垃圾判定:
Java的垃圾判定使用了一種叫GC Root的東西作為依據
當一個物件和GC Root之間沒有任何關係時 就會被判定為垃圾
GC Root                                              
       └                   └                    └       
^ GC Root示意圖(   為存活的物件,   為垃圾物件)

新生代的GC演算法:
一般新生代的演算法為:"標記-清除""複製" 這兩種
標記-清除(Mark-Sweep)的運作方式如下:
                                                 
                                                 
                                                 
                                                 
^ 第一步-標記垃圾物件 (   為存活的物件,   為被標記的垃圾物件,   為可用空間)
                                                 
                                                 
                                                 
                                                 
^ 第二步-清除被標記的垃圾 (   為存活的物件,   為可用空間)
標記-清除演算法就是這麼簡單
但是 ,在上面的圖示中 ,各位大大應該發現的一個問題:
記憶體空間過於零散 ,這樣會導致配置續要連續記憶體的物件(如:陣列)時
找不到夠大的連續記憶體而觸發另一次的GC(進行資料壓縮)
其實他還有一個缺點: 效率差: 標記和清除的效率都很差
但是他是最基礎的演算法 ,後面要介紹的演算法幾乎都是建築在他上面的
複製演算法:
複製演算法將新生代切成兩塊一樣的區塊 ,像這樣:
                        |                        
                        |                        
                        |                        
                        |                        
每次只對其中一塊進行GC
                        |                        
                        |                        
                        |                        
                        |                        
^ 假設記憶體這是目前的使用狀況 (   為存活的物件,   為垃圾物件,   為可用空間)
(現在要對右邊那塊進行GC)
                        |                        
                        |                        
                        |                        
                        |                        
^ 清除垃圾物件(   為存活的物件,   為可用空間)
                        |                        
                        |                        
                        |                        
                        |                        
^ 將存活的物件複製到另一塊記憶體上(   為存活的物件,   為可用空間)
                        |                        
                        |                        
                        |                        
                        |                        
^ 清除已複製的物件(   為存活的物件,  為可用空間)
複製演算法比標記-清除演算法來的複雜一些
但是他幾乎改進了標記-清除演算法的所有缺點
第一:他不會有空間零散的問題
第二:由於每次只對一半的空間進行GC ,因此效率比標記-清除演算法好一些
因為這些優點 讓他成為現在商業虛擬機的GC演算法首選
但是他有一個致命的缺點: 他只能使用50%的記憶體(另外50%閒置)
這個缺點他媽的太惡毒了
但是之前有提到"跟據IBM統計:約有98%的物件在建立沒多就後就會變成垃圾"
因此 ,目前虛擬機並不是直接將新生代切成兩塊一樣的區塊
而是切成一塊較大的Edan和兩塊較小的FromSurvivor和ToSurvivor
大部份新建的物件會被放在Edan, 當執行一次MinorGC時
Edan和其中一塊的Survivor上存活的物件會被移到另一塊Survivor
這樣重複幾次後 沒有被殺掉的垃圾就會被移到老年代
                                                 
                                                   <<Edan
                                                 
                        |                          <<兩個Survivor
                        |                        
                                                 
                                                   <<Old
                                                 
^ 假設目前記憶體的是用是這樣
  (   為存活的物件,   為垃圾,    為未使用的空間)
                                                 
                                                   <<Edan
                                                 
                        |                          <<兩個Survivor
                        |                        
                                                 
                                                   <<Old
                                                 
^ 第一步-清除垃圾(   為存活的物件,    為未使用的空間)
                                                 
                                                   <<Edan
                                                 
                        |                          <<兩個Survivor
                        |                        
                                                 
                                                   <<Old
                                                 
^ 第二步-複製存活的物件到另一個Survivor(   為存活的物件,    為未使用的空間)

                                                 
                                                   <<Edan
                                                 
                        |                          <<兩個Survivor
                        |                        
                                                 
                                                   <<Old
                                                 
^ 第三步-清除Edan和原本的Survivor(   為存活的物件,    為未使用的空間)

                                                 
                                                   <<Edan
                                                 
                        |                          <<兩個Survivor
                        |                        
                                                 
                                                   <<Old
                                                 
^ 第n步-這樣重複幾次後 ,依然存活的物件就會被移到老年代
  (   為存活的物件,    為未使用的空間)

這樣的作法所浪廢的記憶體大約是10%(其中一個Survivor)(以預設值8:1下去計算)
在JVM裡有一個參數叫-XX:SurvivorRatio=<n>
他可以設定Edan和Survivor的空間比(預設是8:1)
這個數值設的愈大 ,浪費的記憶體就愈少
但就會比較容易發生Survivor的空間不夠 ,跑去向老年代借空間的情況
這個數值設愈小 ,浪費的記憶體愈多 ,但是比較不會發生Survivor空間不夠的情況

新生代的垃圾收集器:
前面所介紹到的僅止於是演算法而已
真正下去收集垃圾的是垃圾收集器
每個收集器的特性都不同 適用的情況也不同
小弟下面列出幾個HotSport內建的收集器供各位大大選擇:
收集器名稱 JVM參數 說明
1.Serial GC -XX:+UseSerialGC 最古老的演算法
採單一執行緒
採用"標記-清除-壓縮"
(壓縮即為記憶體整理)
的方式執行GC
是ClientVM的預設GC
2.ParNew GC -XX:+UseParNewGC 多執行緒版的Serial GC
3.Parallel Scavenge GC -XX:+UseParallelGC
傳輸量優先的GC
(傳輸量公式:
  Tr/(Tr+Tgc)
  Tr:執行程式的時間
  Tgc:執行GC的時間
)

不適合用在Minecraft伺服器,
適合用在不常與使用者互動
              的後台運算
為ServerVM的預設GC
4.Garbage First
   (G1) GC
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
最新的GC演算法
支援壓縮
適合長時間執行
在Java7u4時正式被支援
是否有bug仍不知道
(JDK6中
  兩個參數需同時使用)

老年代的GC演算法:
老年代的要求和新生代不同 ,所以新生代的GC演算法不適用於老年代
老年代的GC演算法大概就只有這種:

標記-整理(Mark-Compact):
由於老年代的物件存活率較新生代多
如果使用複製演算法 ,會因為要進行較多的物件複製
效能會變較差 ,因此 ,老年代改採"標記-整理"演算法
這種演算法的運作流程如下:
                                                 
                                                 
                                                 
                                                 
^ 假設老年代的空間使用如圖 (   為存活的物件,   為垃圾物件,   為可用空間)

                                                 
                                                 
                                                 
                                                 
^ 第一步-清除垃圾 (   為存活的物件,   為可用空間)

                                                 
                                                 
                                                 
                                                 
^ 第二步-空間整理 (   為存活的物件,   為可用空間)

就是這樣而已
因為老年代的特性 ,因此在垃圾清除,整理上都會較有效率
但是因為老年代的空間真的太大了 ,導致清除時間會較MinorGC長一些
因此一般的GC優話就是要優化FullGC這部份

老年代的垃圾收集器:
收集器名稱 JVM參數 說明
1.Serial Old GC -XX:+UseSerialGC Serial GC的老年代版本
2.
Parallel Old GC
-XX:+UseParallelOldGC Parallel Scavenge GC
的老年代版本
3.CMS GC -XX:+UseConcMarkSweepGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=value
-XX:+UseCMSInitiatingOccupancyOnly
效能很好的GC演算法
預設不支援壓縮
時間久了會造成空間破碎
導致另一次GC發生
(進行壓縮)
4.Garbage First
   (G1) GC
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
G1 GC是同時跨新生代
老年代的GC
(特色是全域GC次數少)

收集器的搭配:
上面介紹那麼多種的GC收集器 ,其實他們可以互相搭配使用
搭配組合如下(就只有這幾種而已):
新生代收集器 老年代收集器
Serial GC Serial Old GC
Serial GC CMS GC + Serial Old GC
ParNew GC Serial Old GC
ParNew GC CMS GC + Serial Old GC
Parallel Scavenge GC Serial Old GC
Parallel Scavenge GC Parallel Old GC
G1 GC G1 GC
PS:CMS GC + Serial Old GC中的Serial Old GC是用來當作備用GC的

CMS和G1的演算法:
為甚麼會特別解釋這兩個收集器的演算法呢?
因為這兩個收集器是所有收集器中 ,性能較好的兩個收集器
而且這兩個收集器實現了並排運作(concurrent)
簡單來說就是 ,他們並不是所有的步驟都要stop-the-world
因此效率較高 ,那下面就讓小弟稍微解釋以下這兩個收集器的演算法:

CMS的演算法)
  CMS是基於"標記-清除"的一種演算法
  但是他將"標記-清除"演算法拆成下面的幾個步驟
                                                   
                                                   
                                                   
                                                   
  ^ 假設老年代的空間使用如圖 (   為存活的物件,   為垃圾物件,   為可用空間)

                                                   
                                                   
                                                   
                                                   
  ^ 第一步-初始標記 (   為存活的物件,   為垃圾物件,    為已標記的物件,   為可用空間)
     初始標記只是簡單的標記一下和GC Root直接關聯的物件而已
     (其實這步是要標記存活的物件
      但是為了說明方便 ,因此上圖顯示為標記垃圾)

                                                   
                                                   
                                                   
                                                   
  ^ 第二步-並排標記 (   為存活的物件,   為垃圾物件,    為已標記的物件,   為可用空間)
     並排標記就只是進行GC Root追蹤和後續的標記而已
     這個時候 ,其他執行緒仍可運作 ,只是要分配部份資源給GC執行緒
     (這時有可能產生新的垃圾)

                                                   
                                                   
                                                   
                                                   
  ^ 第三步-重新標記 (   為存活的物件,   為垃圾物件,    為已標記的物件,   為可用空間)
     這步只是要標記並排標記時所產生的垃圾(這時仍會執行stop-the-world)

                                                   
                                                   
                                                   
                                                   
  ^ 第四步-並排清除 (   為存活的物件,   為可用空間)
     這個時候 ,其他執行緒仍可運作 ,只是要分配部份資源給GC執行緒

  由於CMS是基於"標記-清除"的演算法
  因此他繼承了"標記-清除"演算法的一個缺點: 會產生零碎的空間
  這個的缺點已經在前面提過了 ,在此不再贅述
  然而"標記-清除"演算法的另一個缺點: 效能差
  在CMS上看不見 ,因為CMS將最耗時間的兩個步驟和其他執行緒並排執行
  這也是CMS效能優越的原因
  但是因為並排執行 ,導致系統的部份資源需分配給GC執行續
  會使系統效能下降一些
  如果您的CPU數量不足4個時 ,系統會有將近50%的資源分配給GC執行緒
  因此 ,要使用CMS之前 ,請先確定您的CPU數至少有4顆以上

G1的演算法)
  G1收集器和CMS一樣是並排執行的收集器 ,這意味著他的效能有一定的保證
  只是G1不是基於"標記-清除"的收集器 ,而是基於"標記-整理"
  這表示他不會產生空間碎片 ,適合長期運作
  而且他的運作方式很特別 ,他是將新生代和老年代切成一塊一塊的區塊(Region)
  然後根據每個區塊的垃圾量 ,安排清理順序
  由於優先清理垃圾量最多的區塊(這也是Garbage First的由來)
  所以他可以保證在有限的時間內收集最多的垃圾
  下面是他的步驟:
                                                   
                                                   
                                                   
                                                   
  ^ 假設Heap的使用如圖
     (   為老年代區塊,   為新生代區塊,
                為有垃圾的老年代區塊,   為有垃圾的新生代區塊,   為可用空間)

  新生代,老年代回收(簡化過):
                                                   
                                                   
                                                   
                                                   
  ^ 首先 ,先將垃圾區塊的垃圾清掉(優先清理垃圾量最多的區塊)
      (使用了"並排初始標記,並排標記,重新標記,並排清除"和CMS有些不同)

                                                   
                                                   
                                                   
                                                   
  ^ 然後 ,將不完整的區塊 集中,壓縮

                                                   
                                                   
                                                   
                                                   
  ^ 後 ,整理一下區塊的排序

  G1 GC是一款能保證限制執行時間的收集器(更能比其他的演算法做到時間限制)
  他擁有CMS的所有優點 ,但是並不會讓空間產生碎片

4)GC觀察
在優化GC之前 應該先來觀察一下GC 這樣才知道優化到底有沒有用
觀察GC有下列幾種方式

1.jstat
  在使用jstat觀察GC之前 請先使用jps查一下要觀察的那個程式的PID
  然後輸入"jstat -gc <你要觀察的程式的PID> <隔幾毫秒監測一次>"
  之後您應該會得到類似這樣的畫面

  您要注意的是後5欄 ,他們代表著GC的效能
  他們分別是
YGC minorGC所執行的總次數
YGCT minorGC所花費的總時間
FGC FullGC所執行的次總數
FGCT FullGC所花費的總時間
GCT GC所花費的總時間
要注意: YGCT和FGCT都使指總時間
因此單次GC平均執行時間為(YGCT/YGC),(FGCT/FGC)
上面那個例子因為連一次GC都還沒執行就被切掉了 所以那5個數值皆為0
2.-verbosegc (-verbose:gc)
第2種方式是在執行前下的那道指令內含有-verbosegc這個參數
加入這個參數後 會在每次GC時在文字視窗顯示類似下面的訊息
[GC 648345K->181909K(823168K), 0.0455280 secs]
[Full GC 271568K->190509K(936256K), 0.2545170 secs]
您因該注意的是x.xxxxxx secs那個地方
那是指這次GC所佔用的時間
另外 -verbosegc可以搭配下面的參數使用
參數名稱 說明
-Xloggc:<檔案名稱> 將GC的訊息導向到檔案
-XX:+PrintGCDetails 顯示詳細的訊息
-XX:+PrintGCTimeStamps 顯示因為GC而產生的停頓時間
-XX:+UseGCLogFileRotation 開啟紀錄檔分頁的功能
需要和-Xloggc搭配使用
-XX:NumberOfGCLogFiles=<n> 設定分頁檔的數量(以.0, .1, ... .n-1命名)
需要和-Xloggc搭配使用
-XX:GCLogFileSize=<n> 設定分頁檔大小
需要和-Xloggc搭配使用
-XX:+PrintGCDateStamps 顯示GC所發生的日期
YYYY-MM-DDTHH-ii-SS.mmm-TZ 為格式
YYYY:四位的西元年
MM:兩位的月份
DD:兩位日期
HH:小時(24小時制)
ii:兩位的分鐘
SS:秒
mmm:毫秒
TZ:時區
此外 還有兩種視窗化的監測方式-jconsole和VisualVM 小弟就不贅述了
5)GC大亂鬥!?
介紹了這麼多 ,到底哪種GC適合Minecraft
為了找出這個答案 ,小弟做了以下的測試:
●用-verbosegc開啟Minecraft
●開一個世界 隨便亂跑
●隨便丟雪求
●放一大堆動物再把他們殺掉
●點燃一大堆TNT
●丟一堆經驗球 再把他們吸起來
(這次測試的是客戶端並非伺服端
      但是因為只是測試各種收集器的效能 因此不論是用客戶端或伺服端皆可測試)
上面的東西理論上來說應該會造成很多垃圾才對
以下是測試結果(愈低愈好):
這裡有幾點要注意:
1.CMS的MinorGC為ParNewGC的測試數據 ,因為CMS是老年代的收集器
2.CMS GC,G1 GC 都已經將並排運行的時間扣除了 ,因為這段時間仍可工作
3.CMS的完整輸出(-XX:+PrintGCDetails)好像隱藏了非並排執行的時間
   所以CMS的FullGC時間才會這麼短
從上面的結果可以看出來:
CMS GC和G1 GC已經遙遙領先其他兩個GC了
因此 ,如果您要架Minecraft伺服器, GC選擇首推CMS GC和G1 GC
6)GC優化
終於寫到這邊了 ,也感謝您讀到這邊
經過前面的洗禮後 ,小弟想各位大大應該都對GC有一定程度的理解了
在優化之前
您可以先判斷一下 ,自己需不需要進行GC優化
如果您有下列情況的其中一點,那可能就需要進行GC優化
●伺服器的Log經常出現Can't keep up! Did the system time change or is the server overloaded?
●MinorGC的時間經常超過0.1sec
●MinorGC的執行的過於頻繁(正常約10秒一次)
●FullGC執行的時間經常超過1sec
●FullGC執行的太過頻繁(正常約10分鐘一次)
優化完後請務必持續監控GC效能至少一天
然後再去微調參數 找出最佳的參數 (GC優化有可能愈改愈慢)
請務必牢記一點 找出GC的最佳參數非常困難
除非非常幸運 否則不可能試一次就找出來
優化最主要是優化FullGC的時間或減少FullGC的次數
優化步驟如下:
1.觀察觀察再觀察
這邊小弟使用VisualVM+VisualGC作為觀察的工具 觀察結果如下:
啟動參數:
-Xmx4096M
-Xms4096M
-XX:+UseG1GC
-XX:SurvivorRatio=16
-XX:PermSize=128M

^ VisualVM顯示Heap的使用(打錯了 ,是Heap不是Hape)

^ VisualGC(VisualVM的插件之一)顯示新生代,老年代,永久代的空間使用
在這觀察的10分鐘裡(實際觀察時請至少在正常人數的狀態下觀察1小時)
MinorGC的間隔約為2分鐘,單次MinorGC的執行時間約為0.086sec
因此 ,這部伺服器不需要進行GC優化(當然 伺服器負擔太輕了)
但是 ,還是有研究價值
從這張圖片可以看的出來 ,伺服器待機時的記憶體約在500MB左右
但是 當第一位玩家登入時 ,伺服器開始載入地圖 因此所需的記憶體爆增
當記憶體增加到一定的程度時 ,第一次MinorGC執行 ,釋放大量的空間
(這邊講的空間都是指JVM內部的空間(Heap)分配 不是指向主機要求的記憶體)
玩家到處走動時 ,要載入的記憶體增加 ,因此GC會不斷執行
但是當玩家停在原地時 ,記憶體的需求趨緩(但是還是會增加)因此GC間隔變長
當然 ,這只是一位玩家登入時的記憶體使用情形
再來來觀察老年代
由於預設值是在經過15次MinorGC後 ,存活的物件才會被移到老年代
因此老年代的空間使用量才會這麼少
(經過14分鐘的測試 執行了15次的MinorGC 老年代的使用空間增加到119.58MB)
因此可以推測出Minecraft伺服器的大部份物件都是很短命的物件
接下來 ,有個東西叫永久代 ,這個東西在之前都沒有提過 在這裡稍微講一下
永久代又稱方法區 ,顧名思義就是存放方法(和常數)的地方
簡單來說就是bukkit的主程式和插件的程式都要載入到這裡
那在這次的觀察中 ,永久代的使用量大約都在32MB上下
因此128MB的永久代(就是-XX:PermSize=128M這個參數)顯的有點太大了
這也是這台伺服器唯一需要優化的地方
經過考慮後 決定將永久代的大小設為35MB
因此需將啟動參數的-XX:PermSize=128M改為-XX:PermSize=35M
2.選用你要用的GC
選用的參數請參考前面
這裡沒有甚麼標準 ,只是單純挑你喜歡的GC而已
3.啟動參數優化
這是優化的最後一個步驟
關於啟動參數 ,精華區有一篇很棒的文可以參考(他是使用CMS GC+ParNewGC)
那既然CMS的參數已經被講完了 ,那我講一下G1的好了
選項 (<n>為任意正整數) 說明
-XX:+UseG1GC 使用G1 GC
-XX:MaxGCPauseMillis=<n> 最大允許的GC執行時間
不一定會達成
但JVM會盡最大努力去達成他
單位:豪秒(ms)
-XX:InitiatingHeapOccupancyPercent=<n> 當資料佔用Heap多少%的大小時
觸發FullGC,設成0表示固定GC周期
預設值:45
-XX:NewRatio=<n> 老年代和新生代的比率
預設值:2 (老:新=2:1)
-XX:SurvivorRatio=<n> Edan/Survivor的比率
預設值:8 (E:S=8:1)
-XX:MaxTenuringThreshold=<n> 一個物件在經歷幾次MinorGC後
就會被放進老年代
預設值:15
-XX:ParallelGCThreads=<n> 在平行階段(stop-the-world)
使用多少個執行緒下去蒐集垃圾
預設值:每個JVM皆不同
-XX:ConcGCThreads=<n> 在並排執行階段
使用多少的執行緒下去蒐集垃圾
預設值:每個JVM皆不同
-XX:G1ReservePercent=<n> 設定保留的Heap分頁數量
預設值:10
-XX:G1HeapRegionSize=<n> 每個Region的大小
最小值:1MB
最大值:32MB
設太小或太大都會導致操作過於頻繁
然後 ,有一些通用的記憶體參數也可以使用
參數 說明
-Xms 最小的Heap大小
當Heap的空閒大小小於40%時
會自動擴大Heap 直到Xmx的大小
-Xmx 最大的Heap大小
當Heap的空閒大小大於70%時
會自動縮小Heap 直到Xms的大小
-Xmn 新生代的大小
官方建議值為整個Heap的3/8
-XX:+DisableExplicitGC 忽略來自System.gc()的GC請求
System.gc()會對效能造成影響
因此 請忽略他
讓收集器自行去判斷要不要執行GC
-XX:MaxPermSize=<n>
設定永久代的最大大小
預設值:實體記憶體/4
-XX:PermSize=<n> 設定永久代初始的大小
預設值:實體記憶體/64
最後再強調一次: GC的優化參數很難試一次就成功
一定要經過多次調整後 ,才能找出最佳參數
而且每次調整後都必須經過至少一天的觀察 ,才能確定那個數值有沒有用
甚至當伺服器平均人數變多的時候 ,還要再調整一次GC
好啦 終於結束落落長的GC了 (灑花
接下來就進入下一章吧
PS:關於GC如果您想再更深入的學習的話
下面有幾個網站小弟覺得很不錯 ,推薦給您
成为JavaGC专家Part I   <<簡字注意
成为JavaGC专家Part Il  <<簡字注意
成为JavaGC专家(3)       <<簡字注意
G1 Garbage Collector - Big Heaps and Low Pauses?    <<英文注意
Oracle Java HotSpot VM Options   <<英文注意
^英文注意


當然 ,Java的優化不只GC這邊 ,還有即時編譯器優化等等
但是 ,做好GC優化應該就會非常有感覺變快了
如您有心要做好其他的優化的話 ,去買本書吧 ,相信我 ,書很有用
     伺服器選用  
伺服器的選用請參考精華區的這篇 ,他做了很詳細的分析
基本上就是模組,插件的差別
就這樣 (好像太短了
     伺服器設定  
這次 ,我要說明的是bukkit.yml這個東西(server.properties太多人教了)
首先 ,請先裝bukkit,並且至少執行過一次
然後你就可以在伺服器的資料夾發現可愛強大的bukkit.yml
強大在哪裡呢 ,讓我細細說給你聽 (噢不又要說一大堆了
名詞解釋:
chunk:
Minecraft載入世界的基本單位
其大小為16x16x256 blocks (長,寬,高)
tick:
Minecraft的計時單位
分為GameTick和RedstoneTick(還有BlockTick)
GameTick為Minecraft世界運作的基礎
幾乎所有的世界更新都以GameTick為標準(紅石,方塊更新除外)
RedstoneTick為紅石電路更新的基礎(像中繼器每一格為1紅石Tick)
GameTick的長度為1/20秒(當主機負荷不了時 會自動下修)
RedstoneTick的長度為0.1秒
(資料取自中文MinecraftWiki-刻)
這邊設定檔的Tick都是指GameTick
首先 先來看一下他最上層的tag有甚麼?
● settings: 伺服器的基本設定
spawn-limits: 生物產生數量(和流暢度有關)
chunk-gc: 嘿嘿~沒錯,又是GC
ticks-per: 間隔設定(生物生成間隔,自動儲存間隔)
auto-updater: 自動升級設定
● alises: 指令別名(選用)
database: 資料庫設定
●worlds 世界產生器設定(選用)
他們的細項如下:
settings:
  allow-end: 是否允許終界
  warn-on-overload: 系統過載時是否顯示警告訊息
  permissions-file: 權限的設定檔(並非權限插件的設定檔)
  update-folder: 伺服器的升級資料夾路徑(相對或絕對皆可)
  ping-packet-limit: 已停用
  use-exact-login-location: 是否允許玩家重生在方塊中
  world-container: 存放世界的根資料夾(預設沒有此選項)
  plugin-profiling: 是否允許使用/timings測量插件的事件處理時間
  connection-throttle: 客戶端重複連線的時間間隔(可防DDOS)(單位:ms)
  query-plugins: 是否允許遠端查詢插件列表
  deprecated-verbose:
    ^當插件註冊一個過期的事件時,是否顯示警告(true/false/default)
  shutdown-message: 關服時傳給客戶端的訊息
spawn-limits:  
  monsters: 一個chunk上的最大生怪量
  animals: 一個chunk上的最大生動物量
  water-animals: 一個chunk上的最大生烏賊量
  ambient: 一個chunk上的最大生蝙蝠量
chunk-gc:
  period-in-ticks: 每個chunk每次GC的間隔tick數(設成0時停用GC)
  load-threshold: 載入多少個chunk後才開始執行GC(設成0時停用GC)
ticks-per:
  animal-spawns: 每次生動物的間隔(tick)
  monster-spawns: 每次生怪的間隔(tick)
  autosave: 自動儲存的時間間隔(tick)
auto-updater:
  enabled: 是否啟用自動更新
  on-broken: 當所使用的Bukkit版本發現重大Bug時,是否自動通知(字串陣列)
      選項有"warn-ops","warn-console" 預設值為["warn-ops","warn-console"]
  on-update: 當發現新版本時,是否自動通知(字串陣列)
      選項有"warn-ops","warn-console" 預設值為["warn-ops","warn-console"]
  perferred-channal: 哪種版本是伺服器的首選
                                選項有"rb","beta","dev" 預設值為"rb"
  host: 更新檢查伺服器 預設值為"dl.bukkit.org"
alises:
  指令別名1: 指令
  指令別名2: [指令1,指令2]   //連續執行指令
database:
  username: 使用者名稱
  password: 使用者密碼
  isolation: SERIALIZABLE (定值)
  driver: JDBC驅動程式(SQLite: org.sqlite.JDBC ; MySQL: com.mysql.jdbc.Driver)
  url: JDBC連接路徑
worlds:
  世界名稱:
    generator: 世界產生器名稱

就大概這樣 ,但是 ,chunk GC的部份請儘量不要開啟
因為該項功能是透過System.gc()實作的
這個函數可以強制執行GC
但是會對整體效能造成一定的影響(不一定是好的)
而且JVM會自行偵測需不需要執行GC 所以就讓JVM自己判斷就好了
不需要再畫蛇添足的使用這個功能

啟動參數:
參數名稱 短參數 說明
--help -? 顯示啟動參數說明
--bukkit-settings <file>.yml -b <file>.yml 設定Bukkit設定檔的位置
--config <config file> -c <config file> 伺服器設定檔的位置
--date-format <格式> -d <格式> 紀錄檔所用的日期格式
--host <IP>
--server-ip <IP>
-h <IP> 綁定伺服器IP
--log-append <true/false> 是否累加紀錄檔
true:累加紀錄檔
false:覆寫紀錄檔
--log-count <number> 設定紀錄檔分頁數量
--log-limit <# of lines> 幾行就進行紀錄檔分頁
0=不分頁
--log-pattern <name> 紀錄檔的主檔名
-log-strip-color 讓紀錄檔紀錄顏色
--noconsole 不產生伺服器終端
適合背景運作的伺服器
--nojline 不使用JLine作為終端機
這對沒有Visual C++ 2008的使用者很好用
Linux/Unix的使用者可以忽略這個參數
--online-mode <true/false> -o <true/false> 設定使否啟用正版驗證
--plugins <directory> -P <directrory> 設定插件的根資料夾
--port <port number>
--server-port <port number>
-p <port number> 指定伺服器開啟的Port
--size <# of players>
--max-players <# of players>
-s <# of players> 指定最大玩家數
--version -v 顯示Bukkit的版本
--world-dir <worlds dir>
--universe <worlds dir>
-W <worlds dir> 指定世界的根資料夾
--world <world name>
--level-name <world name>
-w <world name> 指定世界名稱

請接下樓
最後編輯:2013-11-29 21:41:50 ◆ Origin: <182.234.64.xxx>

顯示稍舊的 54 則留言

看整串內容篇數:0 / 0

板務人員
本板熱門推薦
【心得】建築‧郊外溫馨小屋安安,我是繪璃。----------------------...(繼續閱讀
【心得】mcpe解謎地圖 孩子-骸子小弟看到巴哈上有好多厲害的地圖,在玩了很多張後,小弟我也試著...(繼續閱讀
【心得】(Craft)Bukkit可能停如題,根據這篇,(Craft)Bukkit將有可能不更新至1...(繼續閱讀
【情報】【小品解謎】逃離夜市小吃哈囉~大家好,我是楓(對!我改名了這個地圖是花了2個小時A_...(繼續閱讀
【心得】甚麼!進擊的蜘蛛人?你看過嗎?大家安安^_^,沒錯!我就是之前發過<RPG地圖>...(繼續閱讀