長文預警。該文主要介紹因線上OOM而引起的問題定位、分析問題的緣由、以及如何解決問題。在分析問題緣由時候爲了能更詳細的呈現出引起問題的緣由,去翻了hdfs 提供的Java Api主要的類FileSystem的部分代碼。因爲這部分源代碼的分析實在是太太太長了,能夠直接跳過看最後的結論,固然有興趣的能夠看下。java
一日,忽然收到若干線上告警。因而趕忙查看日誌,在日誌中大量線程報出OOM錯誤:apache
Exception in thread "http-nio-8182-exec-29" java.lang.OutOfMemoryError: Java heap space緩存
因而使用jstat命令查看該進程內存使用狀況:jstat -gcutil 12492 1000 100bash
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2925 328.850 330.122
0.00 0.00 99.89 99.89 96.78 94.41 200 1.272 2935 329.908 331.180
0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2944 330.853 332.125
0.00 0.00 99.89 99.89 96.78 94.41 200 1.272 2955 332.002 333.274
0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2964 332.940 334.212
0.00 0.00 100.00 99.89 96.78 94.41 200 1.272 2973 333.924 335.196
複製代碼
能夠看出,該進程老年代內存耗盡,致使OOM,且引起了頻繁的FGC。而在對堆參數配置中是徹底能知足項目運行的,因而查看了其餘幾個節點的內存使用狀況,老年代使用率都高達98以上且FGC次數也在增長。ide
因爲線上環境影響業務,便dump出內存快照,而後臨時重啓了節點,重啓以後查看內存使用狀況: jstat -gcutil 18190 1000 10oop
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
1.04 0.00 50.39 22.87 95.96 93.41 1680 20.542 4 0.136 20.679
1.04 0.00 50.39 22.87 95.96 93.41 1680 20.542 4 0.136 20.679
1.04 0.00 50.39 22.87 95.96 93.41 1680 20.542 4 0.136 20.679
複製代碼
雖然暫時業務恢復,但該問題仍是須要解決的。從上能初步分析出問題是因爲內存泄漏,致使在運行一段時間以後OOM。測試
在將dump出的快照導入MAT中查看,並無找到特別大的對象,可是看見不少個org.apache.hadoop.conf.Configuration實例。在代碼中使用了hdfs的API操做hdfs,該類爲鏈接hdfs的配置類。以下: fetch
因而在本地debug啓動一個與線上相同代碼的進程,並dump出該內存快照。在MAT中查看該Configuration類的實例,僅一個實例。到此,差很少能定位是經過Java Api與hdfs交互時,致使某些對象不能回收出現的問題。ui
而後在本地編寫測試接口,經過測試接口訪問hdfs,發現該Configuration類實例在增長,且在執行GC的時候並不能回收。this
至此,內存泄漏的源頭能夠說找到了,至於爲何會出現問題則須要查看這段代碼了。
大體能確認,致使內存泄漏的緣由是與hdfs交互時某段代碼bug。因而翻開了項目中與hdfs交互的類,發現了等價於下面的代碼的訪問hdfs代碼:
public Path createDir(String name) throws IOException, InterruptedException {
Path path = new Path(name);
Configuration configuration = new Configuration();
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://***:8020"), configuration, "hdfs");;
if (fileSystem.mkdirs(path)) {
return path;
}
return null;
}
複製代碼
也就是說,在每次與hdfs交互時,都會與hdfs創建一次鏈接,並建立一個FileSystem對象。但在使用完以後並未調用close()方法釋放鏈接。
此處可能會有疑問,此處的Configuration實例和FileSystem實例都是局部變量,在該方法執行完成以後,這兩個對象都應該是會被回收的,怎麼會致使內存泄漏呢?
在此,若是想知道該問題,就須要去翻FileSystem類的代碼了。FileSystem的get方法以下:
public static FileSystem get(URI uri, Configuration conf) throws IOException {
String scheme = uri.getScheme();
String authority = uri.getAuthority();
if (scheme == null && authority == null) { // use default FS
return get(conf);
}
if (scheme != null && authority == null) { // no authority
URI defaultUri = getDefaultUri(conf);
if (scheme.equals(defaultUri.getScheme()) // if scheme matches default
&& defaultUri.getAuthority() != null) { // & default has authority
return get(defaultUri, conf); // return default
}
}
String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);
if (conf.getBoolean(disableCacheName, false)) {
return createFileSystem(uri, conf);
}
return CACHE.get(uri, conf);
}
複製代碼
重點看一下最後的6行代碼,其中String.format("fs.%s.impl.disable.cache", scheme)在鏈接hdfs時候該參數名爲fs.hdfs.impl.disable.cache,能夠從倒數第5行代碼看出該參數默認值爲false。也就是默認狀況下會經過CACHE對象返回FileSystem。
那接下來看一下CACHE.get方法:
FileSystem get(URI uri, Configuration conf) throws IOException{
Key key = new Key(uri, conf);
return getInternal(uri, conf, key);
}
private FileSystem getInternal(URI uri, Configuration conf, Key key) throws IOException{
FileSystem fs;
synchronized (this) {
fs = map.get(key);
}
if (fs != null) {
return fs;
}
fs = createFileSystem(uri, conf);
synchronized (this) { // refetch the lock again
FileSystem oldfs = map.get(key);
if (oldfs != null) { // a file system is created while lock is releasing
fs.close(); // close the new file system
return oldfs; // return the old file system
}
// now insert the new file system into the map
if (map.isEmpty()
&& !ShutdownHookManager.get().isShutdownInProgress()) {
ShutdownHookManager.get().addShutdownHook(clientFinalizer, SHUTDOWN_HOOK_PRIORITY);
}
fs.key = key;
map.put(key, fs);
if (conf.getBoolean("fs.automatic.close", true)) {
toAutoClose.add(key);
}
return fs;
}
}
複製代碼
從這段代碼中能夠看出:
在看完了上面的代碼以後,在看一下CACHE這個變量在FileSystem中是怎樣引用的:
/** FileSystem cache */
static final Cache CACHE = new Cache();
複製代碼
也就是說,該CACHE對象會一直存在不會被回收。而每次建立的FileSystem都會以Cache.Key爲key,FileSystem爲Value存儲在Cache類中的Map中。那至於在緩存時候是否對於相同hdfs URI是否會存在屢次緩存,就須要查看一下Cache.Key的hashCode方法了,以下:
@Override
public int hashCode() {
return (scheme + authority).hashCode() + ugi.hashCode() + (int)unique;
}
複製代碼
可見,schema和authority變量爲String類型,若是在相同的URI狀況下,其hashCode是一致。unique在FilSystem.getApi下也不用關心,由於每次該參數的值都是0。那麼此處須要重點關注一下ugi.hashCode()。
至此,來小結一下:
但還有一個問題,既然FileSystem提供了Cache來緩存,那麼在本例中對於相同的hdfs鏈接是不會出現每次獲取FileSystem都往Cache的Map中添加一個新的FileSystem。惟一的解釋是Cache.key的hashCode每次計算出來了不同的值,在Cache.Key的hashCode方法中決定相同的hdfs URI計算hashCode是否一致是由UserGroupInformation的hashCode方法決定的,接下來看一下該方法。
其方法定義以下:
@Override
public int hashCode() {
return System.identityHashCode(subject);
}
複製代碼
該方法調用了本地方法identityHashCode,identityHashCod方法對不一樣的對象返回的hashCode將會不同,即便是實現了hashCode()的類。那麼此處問題關鍵就轉化爲UserGroupInformation類的subject是否在每次計算hashCode的時候是同一個對象。
因爲該hashCode是計算Cache.key的hashCode時調用的,所以須要看Cache.Key初始化時候,是如何初始化UserGroupInformation該對象的,以下:
Key(URI uri, Configuration conf, long unique) throws IOException {
scheme = uri.getScheme()==null ?
"" : StringUtils.toLowerCase(uri.getScheme());
authority = uri.getAuthority()==null ?
"" : StringUtils.toLowerCase(uri.getAuthority());
this.unique = unique;
this.ugi = UserGroupInformation.getCurrentUser();
}
複製代碼
繼續看UserGroupInformation的getCurrentUser()方法,以下:
public synchronized
static UserGroupInformation getCurrentUser() throws IOException {
AccessControlContext context = AccessController.getContext();
Subject subject = Subject.getSubject(context);
if (subject == null || subject.getPrincipals(User.class).isEmpty()) {
return getLoginUser();
} else {
return new UserGroupInformation(subject);
}
}
複製代碼
其中比較關鍵的就是是否能經過AccessControlContext獲取到Subject對象。在本例中經過get(final URI uri, final Configuration conf,final String user)獲取時候,在debug調試時,發現此處每次都能獲取到一個新的Subject對象(後面會解釋爲什麼每次都能獲取到一個新的Subject對象)。此處,先看一下獲取AccessControlContext的代碼,以下:
public static AccessControlContext getContext()
{
AccessControlContext acc = getStackAccessControlContext();
if (acc == null) {
// all we had was privileged system code. We don't want // to return null though, so we construct a real ACC. return new AccessControlContext(null, true); } else { return acc.optimize(); } } 複製代碼
其中比較關鍵的是getStackAccessControlContext方法,該方法調用了Native方法,以下:
private static native AccessControlContext getStackAccessControlContext();
複製代碼
該方法會返回當前堆棧的保護域權限的AccessControlContext對象。(關於該方法更多細節未深究,懂的大佬可指出來一下)
那麼此處爲何會返回不一樣的Subject對象呢?因爲在本例中是經過get(final URI uri, final Configuration conf,final String user) Api獲取的,所以折回去看一下這個方法,以下:
public static FileSystem get(final URI uri, final Configuration conf,
final String user) throws IOException, InterruptedException {
String ticketCachePath =
conf.get(CommonConfigurationKeys.KERBEROS_TICKET_CACHE_PATH);
UserGroupInformation ugi =
UserGroupInformation.getBestUGI(ticketCachePath, user);
return ugi.doAs(new PrivilegedExceptionAction<FileSystem>() {
@Override
public FileSystem run() throws IOException {
return get(uri, conf);
}
});
}
複製代碼
在該方法中,先經過UserGroupInformation.getBestUGI方法獲取了一個UserGroupInformation對象,而後在經過UserGroupInformation的doAs方法去調用了get(URI uri, Configuration conf)方法。
先看一下UserGroupInformation.getBestUGI方法的實現,此處關注一下傳入的兩個參數ticketCachePath,user。ticketCachePath是獲取配置hadoop.security.kerberos.ticket.cache.path的值,在本例中該參數未配置,所以ticketCachePath爲空。user參數因爲是本例中傳入的用戶名,所以該參數不會爲空。實現以下:
public static UserGroupInformation getBestUGI(
String ticketCachePath, String user) throws IOException {
if (ticketCachePath != null) {
return getUGIFromTicketCache(ticketCachePath, user);
} else if (user == null) {
return getCurrentUser();
} else {
return createRemoteUser(user);
}
}
複製代碼
getBestUGI參數的兩個參數,如上所分析ticketCachePath爲空,user不爲空,所以最終會執行createRemoteUser方法。實現以下:
public static UserGroupInformation createRemoteUser(String user) {
return createRemoteUser(user, AuthMethod.SIMPLE);
}
public static UserGroupInformation createRemoteUser(String user, AuthMethod authMethod) {
if (user == null || user.isEmpty()) {
throw new IllegalArgumentException("Null user");
}
Subject subject = new Subject();
subject.getPrincipals().add(new User(user));
UserGroupInformation result = new UserGroupInformation(subject);
result.setAuthenticationMethod(authMethod);
return result;
}
複製代碼
從代碼中,能夠看出會經過createRemoteUser方法,來建立一個UserGroupInformation對象。在createRemoteUser方法中,建立了一個新的Subject對象,並經過該對象建立了UserGroupInformation對象。至此,UserGroupInformation.getBestUGI方法執行完成。
接下來看一下UserGroupInformation.doAs方法(FileSystem.get(final URI uri, final Configuration conf, final String user)執行的最後一個方法),以下:
public <T> T doAs(PrivilegedExceptionAction<T> action
) throws IOException, InterruptedException {
try {
logPrivilegedAction(subject, action);
return Subject.doAs(subject, action);
………… 省略多餘的
複製代碼
而後在調用Subject.doAs方法,以下:
public static <T> T doAs(final Subject subject,
final java.security.PrivilegedExceptionAction<T> action)
throws java.security.PrivilegedActionException {
java.lang.SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(AuthPermissionHolder.DO_AS_PERMISSION);
}
if (action == null)
throw new NullPointerException
(ResourcesMgr.getString("invalid.null.action.provided"));
// set up the new Subject-based AccessControlContext for doPrivileged
final AccessControlContext currentAcc = AccessController.getContext();
// call doPrivileged and push this new context on the stack
return java.security.AccessController.doPrivileged
(action,
createContext(subject, currentAcc));
}
複製代碼
最後在調用AccessController.doPrivileged方法,以下:
public static native <T> T
doPrivileged(PrivilegedExceptionAction<T> action,
AccessControlContext context)
throws PrivilegedActionException;
複製代碼
該方法爲Native方法,該方法會使用指定的AccessControlContext來執行PrivilegedExceptionAction,也就是調用該實現的run方法。即FileSystem.get(uri, conf)方法。
至此,就可以解釋在本例中,經過get(final URI uri, final Configuration conf,final String user) 方法建立FileSystem時,每次存入FileSystem的Cache中的Cache.key的hashCode都不一致的狀況了,小結一下:
在FileSystem中,有兩個重載的get方法,以下:
public static FileSystem get(final URI uri, final Configuration conf,
final String user)
public static FileSystem get(URI uri, Configuration conf)
複製代碼
在前面已經詳細的解讀了第一個方法,從代碼中能夠看第一個最終仍是會調用第二個方法。惟一不一樣的地方就是在初始化Cache.key獲取UserGroupInformation對象的時候,以下:
Key(URI uri, Configuration conf, long unique) throws IOException {
scheme = uri.getScheme()==null ?
"" : StringUtils.toLowerCase(uri.getScheme());
authority = uri.getAuthority()==null ?
"" : StringUtils.toLowerCase(uri.getAuthority());
this.unique = unique;
this.ugi = UserGroupInformation.getCurrentUser();
}
複製代碼
該方法會調用UserGroupInformation.getCurrentUser方法,以下:
public synchronized
static UserGroupInformation getCurrentUser() throws IOException {
AccessControlContext context = AccessController.getContext();
Subject subject = Subject.getSubject(context);
if (subject == null || subject.getPrincipals(User.class).isEmpty()) {
return getLoginUser();
} else {
return new UserGroupInformation(subject);
}
}
複製代碼
在直接調用get(URI uri, Configuration conf)方法時,因爲未像get(final URI uri, final Configuration conf, final String user)方法建立Subject對象,所以此處Subject會返回空,會繼續執行getLoginUser方法。以下:
public synchronized
static UserGroupInformation getLoginUser() throws IOException {
if (loginUser == null) {
loginUserFromSubject(null);
}
return loginUser;
}
複製代碼
由代碼可見,loginUser成員變量是關鍵,查看一下該成員定義,以下:
/**
* Information about the logged in user.
*/
private static UserGroupInformation loginUser = null;
複製代碼
也就是說,一旦該loginUser對象初始化成功,那麼後續會一直使用該對象。如上一節所示,UserGroupInformation.hashCode方法將會返回同樣的hashCode值。也就是能成功的使用到緩存在FileSystem的Cache。
在FileSystem中,還提供了了newInstance等Api。該系列Api每次都會返回一個新的FileSystem,具體實現參見FileSystem代碼。
~~以上爲我的理解,因爲水平有限,若有疏漏,望多多指教 ~~