12.19.2014

user process limit on Linix

最近在Linux遇到了一個ulimits (limits)問題,因為對這部份之前沒研究,infra team的人又在忙沒空看,只好自己上網找解,噴掉了我快2天的時間,想說跟大家分享一下!這個錯會在你執行指令時發生,如下:

[apprm@OTALOG logs]$ ls -lrt

-bash: fork: retry: Resource temporarily unavailable
-bash: fork: retry: Resource temporarily unavailable
-bash: fork: retry: Resource temporarily unavailable
-bash: fork: retry: Resource temporarily unavailable
-bash: fork: Resource temporarily unavailable

一開始google,很多篇都說是因為file descriptor用完了(也就是open file限制),查了後並沒有問題,後來發現其實是因為"max user processes"限制,下面來介紹一下面對這個問題可能的原因。

1. File descriptor用完了

一般來說,會出錯這個錯是因為file descriptor用完了,在Linux有分幾種不同的file descriptor限制
1.1 系統整體的file descriptor限制
[apprm@OTALOG ~]$ sysctl fs.file-nr
fs.file-nr = 2656 0 10240
這個指令會印出3個數字,第一個是目前使用的file descriptor數目,第二個是分配但未使用的數目,第三個則是系統最大的file descriptor。以上面來看,明顯問題不是在這。
1.2 特定user的file descriptor限制
[apprm@OTALOG ~]$ ulimit -Hn
8192
[apprm@OTALOG ~]$ ulimit -Sn
8192
[apprm@OTALOG ~]$ lsof -u apprm  | wc -l
4058
user的file descriptor限制分為soft limit和hard limit。soft limit一般而言不會是問題,因為process在執行時若有需要可自行突破上限;而hard limit則不行,除非有root權限才能突破hard limit限制。從執行的指令來看,hard limit設20480,soft limit設為8192,目前使用者用了4058,看來問題也不是在user limit。

若你在1.1或1.2的使用值非常接近或等於上限,恭喜你找到問題點!你要去修改相對應的檔案 針對1.1,要在/etc/sysctl.conf加上一行"fs.file-max = 10240",10240視情況而定(必需大於hard limit) 針對1.2,要在/etc/security/limits.conf 分別設定soft limit和hard limit * soft nofile 8192 * hard nofile 8192

2. 到達最大process(max user processes)限制

若問題不是出在file descriptor,那應該就是因為max user processes限制(預設為1024,root不受限制)。Linux並沒有thread,只有light weight processes(LWPs),基本上LWPs就跟其他作業系統的thread一樣,所以你可能只起一個application server instance,但為了performance,可能一開始就建立了200個thread的thread pool,在計算每個user process時,包含了user起的所有process以及LWPs,因此當server instance一多,很容易就達到預設上限
[apprm@OTALOG logs]$ ulimit -a | grep processes
max user processes              (-u) 1024
[apprm@OTALOG logs]$ ps -eLF | grep ^apprm | wc -l
1023
在第一個查詢我們得知目前的max user processes為1024,第二個查詢發現,apprm已經使用了1023個processes!Bingo!找到問題了!
修正方式是在要在/etc/security/limits.conf 加入下面設定 (2048可視需求調整) apprm - nproc 2048

12.14.2014

常用的grep指令

grep在Unix中是相當常用的指令,可以用來快速找到想要的檔案內容,下面我列出幾個我常用的指令給大家參考:


1. -v 排除特定字串

  • 如:grep Exception logfile.txt | grep -v ERROR

2. -c 計算特定字串出現的次數

  • 如:grep -c UNIX example.txt

3. -C 列印出特定字串前後文

  • 如:grep -C 6 Exception logfile.log (印出前後各6行)

4. egrep 使用extended的grep來做regular expression查詢

  • 如:egrep 'Error|Exception' logfile.txt (找出Error或Exception)

5. -i  不分大小寫

  • 如:grep -i Exception logfile.log

6. zgrep  在gz壓縮檔內的檔找特定字串

  • 如:zgrep -i Error *.gz

7. -l 列出有符合字串的檔名

  • 如:grep -l main *.java

8. --color 在搜尋結果中,highlight符合的字串

  • 如:grep Exception logfile.txt --color

12.07.2014

Java的十個物件導向設計原則

對於開發者而言,想必大家都知道design pattern的重要性;但這篇不是要講design pattern,我們要來看看物件導向程式設計的基本原則,其中包含了SOLID(Single responsibility, Open-closed, Liskov substitution, Interface segregation 和 Dependency inversion),這些基本原則可以幫助你寫出更簡潔、易維護、低耦合的系統。

不要自我重覆(DRY:Don't Repeat Yourself)

不要寫出重覆的程式(功能)!若有一部份的程式出現在不同地方,你可以考慮使用抽象化(Abstraction: 將相同的邏輯往parent搬)或委派(Delegation:將相同的任務委派給另一個物件,或另一個method完成),若有hard-code value重覆出現,則使用public final取代之,藉此可以提升維護性
這個原則很重要,但要小心不能濫用,這裡的程式並不是指程式碼,而是指功能。假設我們使用同樣的程式來驗證資料A以及資料B,雖然現在A及B有著相同格式,若我們使用委派,將相同的驗證抽成一個共用method,代表我們認為A和B的格式永遠不會變動,一旦未來其中一個格式有變動,改了驗證程式就會造成另一個資料驗證問題(若沒有Unit test特別容易發生)。因此要特別注意,這個原則適用在相同的功能而不是相似的程式


將變動封裝起來(Encapsulate Changes)

軟體開發中唯一不變的就是”變動”,變動可能來自於需求的改變、使用不同的工具等,要減少變動影響範圍,我們要將可能會變動的程式封裝起來。例如要發動車子,內部有很多零件必需協同和作才能完成,但用戶不需要知道這些細節,他們只需要將鑰匙轉動即可。由於封裝將細節隱藏起來,只提供主要介面供使用者驅動,若內部實作細節有任何異動,用戶端程式可以不用做任何異動。封裝簡單說就是將變數及method儘可能private化,至於要怎麼判斷程式碼是否有做到封裝,依我的經驗,若有段程式從物件A get不同參數值或呼叫A的不同method,很可能這段程式是屬於A的責任,你已經違反了封裝的概念。不要小看封裝,若你的實作散落各地,實作內容變動愈大,你所要修改的類別可能愈多

開放關閉設計原則(Open Close Design Principle)

有點難理解的原則,又開放又關閉的!其實指的是不同東西,開放指的是類別及方法要能夠擴充(加新功能或不同實作),關閉指的是關閉修改(refactoring例外)。理想上當你要加入新功能,應該引入新的類別、方法或變數,而不是去修改既有已經完成測試的程式,這可以避免修改造成的regression bug。
但要如何使程式能夠擴充呢?基本上我們可以運用多型(polymorphism),藉由相同介面(interface或super class),讓我們可以在擴充行為時不影響client端以及既有的程式

單一責任原則(Single Responsibility Principle)

責任指的是變動的理由,一個類別應該只有一個變動的理由例如一個類別負責編輯報表以及列印報表,這個類別就有二個變動的理由;一個是資料的異動,另一個是報表的格式異動。將二個不同責任綁在一起,除了會造成閱讀困難外,也會使其不好維護,畢竟我們不能保證修改其中一個功能時,對另一個功能不會造成任何影響。


相依性注入或反轉原則(Dependency Injection or Inversion Principle)

相信有使用Spring Framework的都對這個原則不漠生,一般我們要使用一個物件可能會直接new 一個instance,但DI告訴我們不要這樣,你應該動態注入相依的物件,這種方式可以帶來下面二種好處:
  1. 讓我們在撰寫測試時可以利用mock object,讓我們專注在該測的功能上
  2. 可藉由修改設定檔注入不同實作(Polymorphism),而不必修改程式。

傾向使用Composition多於繼承(Favor Composition over Inheritance)

使用繼承我們可以將共同的程式碼移至super class,使用composition我們則是將共同的程式碼移至composed class,在不考慮使用繼承是不是好的設計狀況下,二種差別在於彈性:使用繼承我們無法動態變動實作,使用composition,我們可以藉由polymorphism,讓不同實作有相同介面,藉由執行期的相依性注入抽換不同實作,任何時候我們都可以切換不同的實作且不用改程式。

介面分離原則(ISP:Interface Segregation Principle)

這個原則指的是client不應實作一個他不提供的功能介面會違反這個原則,大多是因為一個介面負責了超過一個功能(違反單一責任原則),而client只需要其中部份功能。在介面的設計上必須特別小心,因為一旦發佈了後,日後所做的任何修改都會使現有的實作無法運行。

Liskov 替換原則(Liskov Substitution Principle)

這個原則指的是子類別的實作必需可以替代父類別!也就是說父類別的實作必需在子類別也能運作LSP和單一責任原則以及介面分離原則有很強的關聯,若一個類別負責許多許責任,子類別一旦無法支援所有行為,就會違反LSP。要遵守LSP,則子類別必須是加強父類別的功能,不能減少。舉個教科書常見的錯誤例子來說:父類別Rectangle定義了長及寬變數,以及取得面積的方法,子類別Square繼承了父類別的長及寬變數以及行為,當我們以polymorphism的角度使用Square類別(Rectangle r = new Square())時,使用者認為他必需設定長及寬來計算面積,但當他設定長=10,寬=5並計算面積時,發現回傳是25(因為正方形四邊都一樣)而不是50!因為polymorphism開發時操作的是super class或interface,因此違反了這個原則可能帶來用戶錯誤的預期

針對介面而非實作(Programming for Interface not implementation)

這個原則其實就是要我們善用polymorphism,定義變數或方法變數或回傳變數時使用介面而非實作可以帶來彈性,當要替換不同實作時,這可以讓你減少異動

委派原則(Delegation Principle)

不要試著在一個類別中做所有的事,把責任委派給該負責的類別。例如hash code和equals method,要比較二個物件是否相同,我們並不會在client端寫程式去比,而是交由二個物件本身自己去比較,這樣的好處是可以減少重覆的程式碼,讓系統更好維護。

你可能對下面主題有興趣:
  1. [OO概念]封裝,繼承,多型
  2. [Java 概念]Interface and abstract class

12.06.2014

Garbage Collector的種類

前面(什麼是Garbage Collection)提到身為開發者,Java會自動回收記憶體,讓開發者可以專注在業務邏輯的處理,但在回收的過程中,會造成系統無法回應(取決於Major/Minor GC);因此Java提供了許多不同的Garbage Collector,讓我們可以依據系統的效能需求,選擇最適合的Garbage Collector。
如下圖所示,有七種Garbage Collector,分為上下二個區塊,上面為Young generation,下面為Old/Tenured generation,若二個Garbage Collector之間存在連線,代表可以搭配使用。

http://blog.csdn.net/java2000_wl/article/details/8030172

Serial Collector(-XX:+UseSerialGC)

屬於單一執行緒的copy collector,運作原理是將存活的物件從"from" space (ex.survivor 1)搬至"to" space(ex.survivor 2),並回收"from" space,他主要是設計給heap size需求很小的應用程式使用,由於屬於"Stop-the-world" (當他在執行時,其他任務都必需暫停) collector,所以並不適用於對response time要求高的系統,但相對地也因為單執行緒,讓他可以有最高的GC效率,較為適合用於不要求即時性(ex background job server)的系統。


ParNew Collector(-XX:+UseParNewGC)

Parallel copy collector其實就是multi-thread版的Serial Collector,其餘行為都跟Serial collector相同,效率則因為multi-thread較Serial Collector好。


Parallel Scavenge Collector(-XX:+UseParallelGC)

運作方式跟Parallel copy collector相同,但演算法有針對超過10GB heaps的multi-CPU環境做過最佳化。他的目標是盡可能地取得最大throughput[註1]同時降低GC造成的暫停。若你使用這個collector,則在old generation你可以使用原使的mark-sweep collector(Serial Old)。


CMS(-XX:+UseConcMarkSweepGC)

CMS不同於其他garbage collector,他不會進行heap space compaction(因為compaction的過程很耗時且會造成系統停頓),使他適用於高互動式的系統;但相對地,他也較容易造成記憶體碎片化(fragmentation);雖然CMS不會進行Full GC,但當出現promotion failed/concurrent mode failure時,此時會使用Mark Compact garbage collector (Serial Old GC)進行Full GC,CMS運作分為下面幾個階段:
Initial mark - 這個階段會找出application有直接(root)reference reference的活物件, stop-the-world 。
Concurrent mark – 這個階段會針對上階段結果,追蹤整個object graph,並標記活著的物件,整個過程會和application同時運行。
Concurrent pre clean – 這個階段是針對上一階段標記為活著的物件再做檢查,目的是為了減少Remark造成stop-the-world的時間,整個過程會和application同時運行。
Remark - 因為在進行mark階段,application是持續在運行,新的物件並不會被標記,此時application再度停止運行,並針對在Concurrent mark階段有變動的物件進行最終標記 , stop-the-world 。

Serial Old(-XX:+UseSerialGC)

Serial Old (Mark Sweep Compact:MSC) - 屬於stop-the-world collector,運作原理是先找出活著的物件做標記,之後清除未用的物件,最後再進行compaction。


Parallel Old(-XX:+UseParallelOldGC)

Parallel Scavenge的Old generation版本。


G1(-XX:+UseG1GC)

G1GC在Java7時加入,他的目標是在不犧牲throughput的狀況下,達成low latency。傳統GC會將heap分為3個區塊:young generation,old generation以及permanent generation,每個區塊都設定了固定大小;G1GC則是將heap分割成一塊塊相同大小的region,每個resion大約是1MB~32MB的連續記憶體空間,再將一個個region組成region sets,同一個region set扮演相同的角色(eden, survivor或old),因為region set沒固定大小,若服務中大部份的物件存活期都很短,則young generation會佔大部份;反之則tenured generation佔大部份,因此使用G1GC時,調整新世代大小的參數就不重要了,這種方式提供了記憶體使用上的更大彈性。

G1GC的GC過程跟CMS很類似,都是分階段標記後再回收,差別在於回收的方式。G1GC將記憶體分割為許多region,GC時並不會一次對所有region都進行回收,而是會依據允許的時間(預設為200ms),能回收多少region就做多少,藉此精準地控制停頓的時間,GC會先回收垃圾最多的region,以釋放最多空間,這也就是為什麼他叫Garbage First的原因。回收的過程採用驅離(evacuation)的方式,從一個或多個region將活著的物件搬至另一個region,藉此同時進行記憶體回收以及compaction,避免了heap破碎化的問題。



[註1]Throughput的定義是扣除GC花費的時間,系統可以使用的時間比;也就是[系統可用時間]/[總系統執行時間]。

12.02.2014

關於GC的JVM參數

了解GC相關的JVM參數對於在乎即時性的系統相當重要,例如高流量低latency的電子商務系統或線上交易系統,每一秒都相當重要。然而GC tuning需要不斷的trial-and-error以及profiling來找出最適合的設定,並沒有特定參數適合所有系統
在tuning的過程中,profiling可以幫助我們了解application會產生什麼物件,以及這些物件的平均生命週期。舉例來說,若你的系統大多數是生命週期很短的物件,加大Eden space會有效降低minor GC的次數;但也不能無限制的擴大,因為Young generation愈大,代表Tenured generation愈小,可能因此導致Tenured generation很快就滿了,必需頻繁地進行Full GC,降低整體系統的throughput[註1]。

接下來讓我們來看與GC相關的重要參數:


  1. Java heap size相關的JVM options (必要)
    • -Xms1024m  設定初始Java heap size (必要)理想的大小為使用量佔60%,這樣可以確保當忽然湧進大量使用者時,系統仍有一定的成長空間。建議Xms:Xmx比例為1:1或1:1.5,設為相等較為常見,以避免每次GC完後重新分配記憶體大小(CPU time)。
    • -Xmx1024m  設定最大Java heap size (必要)
    • -XX:NewRatio=3  設定Young跟Tenured generation的比例,一般建議設為3~4(譬如總heap為1024m,若設為3,則Young space為256m,Tenured space為768m)
    • -XX:NewSize=512m  設定default Young generation的大小,一般會設的跟max一樣,若你需要比NewRatio更精準的控制大小時才會使用
    • -XX:MaxNewSize=512m  設定max Young generation的大小
    • -Xss     設定Java thread stack size
  2. Perm Gen size相關的JVM options (可以解決java.lang.OutOfMemoryError:Perm Gen Space)
    • -XX:PermSize=256m  設定預設Perm space(建議和最大值相同)
    • -XX:MaxPermSize=256m  設定最大Perm space
  3. 列印GC log相關的JVM option (必要)
    •  -verbose:gc   Log garbage collector執行結果以及花了多少時間 (必要),開啟以利後續GC tuning
    • -XX:+PrintGCDetails   除了包含 -verbose:gc的資訊外,額外提供new generation大小以及GC花費的時間 (必要)
    • -XX:-PrintGCTimeStamps     印出GC執行時的時間點 (必要)
    • -XX:+PrintGCDateStamps    印出GC執行時的日期
    • -XX:+HeapDumpOnOutOfMemoryError   當OOM error發生時,自動產生heap dump (必要)
    • -XX:HeapDumpPath           將產生的heap dump檔案放至指定目錄 (必要)
  4. 指定Garbage collector的參數 
Classification Option Remarks
Serial GC -XX:+UseSerialGC
Parallel GC -XX:+UseParallelGC
-XX:ParallelGCThreads=value
Parallel Compacting GC -XX:+UseParallelOldGC
CMS GC -XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=value
-XX:+UseCMSInitiatingOccupancyOnly
‑XX:+CMSConcurrentMTEnabled
G1GC -XX:+UseG1GC available from Java 7 

[註1]Throughput的定義是扣除GC花費的時間,系統可以使用的時間比;也就是[系統可用時間]/[總系統執行時間]。

12.01.2014

什麼是Garbage Collection以及他在Java如何運作

JAVA並不像C或C++必需由開發者負責記憶體分配/回收的任務,JVM會依據設定,找出沒被參照的物件,自動進行記憶體空間回收,這使得讓開發者在開發時可以專注在業務邏輯上。雖然開發者可以不用管記憶體的分配/回收,但為了系統效能最佳化,我們還是有必要了解Garbage Collection(以下簡稱GC)是如何運作的,在開始之前,我們必須強調GC雖然可以自動回收沒被參照的物件,但無法解決程式設計不當所造成的memory leak。

首先來看GC的幾個重點

  1. 不論物件的scope(local variable/member variable),所有的物件都是儲存在Heap space!
  2. GC是JVM用來回收記憶體的機制,符合GC條件的物件會在GC過程中,釋放其佔用的記憶體空間(Heap space)
  3. GC讓開發者不用處理記憶體管理,在開發時可以專注在業務邏輯上
  4. Java透過一個叫Garbage Collector的daemon(背景)thread來進行GC
  5. 開發者無法強制執行GC,GC只會在JVM認為需要(根據Heap Size)時才會執行
  6. System.gc()和Runtime.gc()[註1]會送GC請求給JVM,但JVM並不一定會執行GC
  7. Heap space沒空間存放新建立的物作,則JVM會丟出OutOfMemoryError java.lang.OutOfMemoryError heap space
  8. Minor GC和Major/Full都是"Stop the World"事件,只是Minor GC時間非常短(幾百milli-seconds),使用者較不容易察覺;而Full GC時間相對上長很多,且heap size愈大時間愈久;因此應儘量避免或減少Full GC發生。


物件什麼時候會符回GC條件

物件若被JVM認為沒有在使用,即符合GC條件。判斷方式則是:JVM會從根物件開始走訪所有reference,把走到的做註記,當全部走完後,沒被註記的物件代表沒有人能夠取得,即符合GC條件。
一般而言,假設我們要讓物件A符合回收條件,可以通過下列方式達成:
  1. 將所有參照到A的變數設為null (ex. sample = null;)
  2. 在method中建立的物件A,一旦離開了該method,則所有local variable參照都會失效
  3. 將parent object reference設為null。parent object會參照到物件A,當parent object的reference消失時,則物件A也符合GC條件
  4. 若物件A只有被WeakHashMap參照到,則符合條件

Heap Generations

Heap可分為3個generations,分別稱為: Young/New generation, Tenured/Old generation和Permanent(Perm) generation,其中Young generation可以進一步分為3個區塊: Eden space, Survivor 1和Survivor 2。當物件一開始被產生時,他被放置在Eden space,當Eden space滿了,無法存放新物件時,JVM會啟動Minor GC,把存活的物件往Survivor 1或2移及以原本在Survivor 1或2的存活物件往另一個Survivor空間移,當JVM執行多次Minor GC後,會把符合條件的存活物件往Tenured generation移,這個過程我們稱為Minor GC。Tenured generation滿了時,JVM會執行GC,回收Tenured generation的空間,我們稱之為Major/Full GC
http://javahash.com/java-memory-model-structures/

Permanent generation儲存class和method相關的metadata以及interned String[註2]。至於perm space會不會進行GC,則是依JVM而定,Sun/Oracle實作的JVM會進行GC,要確認你用的JVM是否會進行GC,可以寫支小程式建立數百萬個字串看看是否會出現GC log或 OutofMemoryError。

物件的旅程

在了解Heap Generation後,我們來看看一個物件生命週期中,他在這些generation間的旅程(並不一定會走完全程,若期間符合GC條件,則提前結束)。
  1. JVM配置Eden記憶體空間給新建立的物件
  2. 當Minor GC時(Eden space無法分配記憶體空間給新建立的物件),物件從Eden被移到Survival space
  3. 當Minor GC時,物件從Survival space被移到另一個Survival space(根據設定,這可能會發生很多次)
  4. 當Minor GC時,物件從Survival space移(promote)至Tenured/Old generation
  5. Major GC(old generation無法分配空間給被promote的物件)

[註1] System.gc()和Runtime.gc()其實做一樣的事,System.gc()轉發request給Runtime.gc(),差別是System.gc()是class level而Runtime.gc()是instance level,System.gc()較為方便而已
[註2]All literal strings and string-valued constant expressions are interned.透過String.intern(),可以讓相同值的字串只存一份在記憶體,在JDK6,intern的字串存放於Permanent generation,在JDK7之後移至Young/Old generation
你可能對下面主題有興趣:
  1. 關於GC的JVM參數