日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

作者:valar

前言

長(zhǎng)文預(yù)警。該文主要介紹因線上OOM而引發(fā)的問(wèn)題定位、分析問(wèn)題的原因、以及如何解決問(wèn)題。在分析問(wèn)題原因時(shí)候?yàn)榱四芨敿?xì)的呈現(xiàn)出引發(fā)問(wèn)題的原因,去翻了hdfs 提供的JAVA Api主要的類FileSystem的部分代碼。由于這部分源代碼的分析實(shí)在是太太太長(zhǎng)了,可以直接跳過(guò)看最后的結(jié)論,當(dāng)然有興趣的可以看下。

風(fēng)起

一日,突然收到若干線上告警。于是趕緊查看日志,在日志中大量線程報(bào)出OOM錯(cuò)誤:

Exception in thread "http-nio-8182-exec-29" java.lang.OutOfMemoryError: Java heap space

于是使用jstat命令查看該進(jìn)程內(nèi)存使用情況:jstat -gcutil 12492 1000 100

 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

可以看出,該進(jìn)程老年代內(nèi)存耗盡,導(dǎo)致OOM,且引發(fā)了頻繁的FGC。而在對(duì)堆參數(shù)配置中是完全能滿足項(xiàng)目運(yùn)行的,于是查看了其他幾個(gè)節(jié)點(diǎn)的內(nèi)存使用情況,老年代使用率都高達(dá)98以上且FGC次數(shù)也在增加。

由于線上環(huán)境影響業(yè)務(wù),便dump出內(nèi)存快照,然后臨時(shí)重啟了節(jié)點(diǎn),重啟之后查看內(nèi)存使用情況: jstat -gcutil 18190 1000 10

 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

雖然暫時(shí)業(yè)務(wù)恢復(fù),但該問(wèn)題還是需要解決的。從上能初步分析出問(wèn)題是由于內(nèi)存泄漏,導(dǎo)致在運(yùn)行一段時(shí)間之后OOM。

定位

在將dump出的快照導(dǎo)入MAT中查看,并沒(méi)有找到特別大的對(duì)象,但是看見(jiàn)很多個(gè)org.Apache.hadoop.conf.Configuration實(shí)例。在代碼中使用了hdfs的API操作hdfs,該類為連接hdfs的配置類。如下:

 

線上內(nèi)存泄漏引發(fā)OOM問(wèn)題分析和解決

 

于是在本地debug啟動(dòng)一個(gè)與線上相同代碼的進(jìn)程,并dump出該內(nèi)存快照。在MAT中查看該Configuration類的實(shí)例,僅一個(gè)實(shí)例。到此,差不多能定位是通過(guò)Java Api與hdfs交互時(shí),導(dǎo)致某些對(duì)象不能回收出現(xiàn)的問(wèn)題。

然后在本地編寫測(cè)試接口,通過(guò)測(cè)試接口訪問(wèn)hdfs,發(fā)現(xiàn)該Configuration類實(shí)例在增加,且在執(zhí)行GC的時(shí)候并不能回收。

至此,內(nèi)存泄漏的源頭可以說(shuō)找到了,至于為什么會(huì)出現(xiàn)問(wèn)題則需要查看這段代碼了。

原因

大致能確認(rèn),導(dǎo)致內(nèi)存泄漏的原因是與hdfs交互時(shí)某段代碼bug。于是翻開了項(xiàng)目中與hdfs交互的類,發(fā)現(xiàn)了等價(jià)于下面的代碼的訪問(wèn)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;
 }

也就是說(shuō),在每次與hdfs交互時(shí),都會(huì)與hdfs建立一次連接,并創(chuàng)建一個(gè)FileSystem對(duì)象。但在使用完之后并未調(diào)用close()方法釋放連接。
此處可能會(huì)有疑問(wèn),此處的Configuration實(shí)例和FileSystem實(shí)例都是局部變量,在該方法執(zhí)行完成之后,這兩個(gè)對(duì)象都應(yīng)該是會(huì)被回收的,怎么會(huì)導(dǎo)致內(nèi)存泄漏呢?

FileSystem是怎樣獲取的

在此,如果想知道該問(wèn)題,就需要去翻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);
 }

重點(diǎn)看一下最后的6行代碼,其中String.format("fs.%s.impl.disable.cache", scheme)在連接hdfs時(shí)候該參數(shù)名為fs.hdfs.impl.disable.cache,可以從倒數(shù)第5行代碼看出該參數(shù)默認(rèn)值為false。也就是默認(rèn)情況下會(huì)通過(guò)CACHE對(duì)象返回FileSystem。

那接下來(lái)看一下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;
 }
 }

從這段代碼中可以看出:

  1. 在Cache類內(nèi)部維護(hù)了一個(gè)Map,該Map用于緩存已經(jīng)連接好的FileSystem對(duì)象,Map的Kep為Cache.Key對(duì)象。每次都會(huì)通過(guò)Cache.Key獲取FileSystem,如果未獲取到,才會(huì)繼續(xù)創(chuàng)建的流程。
  2. 在Cache類內(nèi)部維護(hù)了一個(gè)Set(toAutoClose),該Set用于存放需自動(dòng)關(guān)閉的連接。在客戶端關(guān)閉時(shí)會(huì)自動(dòng)關(guān)閉該集合中的連接。

在看完了上面的代碼之后,在看一下CACHE這個(gè)變量在FileSystem中是怎樣引用的:

 /** FileSystem cache */
 static final Cache CACHE = new Cache();

也就是說(shuō),該CACHE對(duì)象會(huì)一直存在不會(huì)被回收。而每次創(chuàng)建的FileSystem都會(huì)以Cache.Key為key,F(xiàn)ileSystem為Value存儲(chǔ)在Cache類中的Map中。那至于在緩存時(shí)候是否對(duì)于相同hdfs URI是否會(huì)存在多次緩存,就需要查看一下Cache.Key的hashCode方法了,如下:

 @Override
 public int hashCode() {
 return (scheme + authority).hashCode() + ugi.hashCode() + (int)unique;
 }

可見(jiàn),schema和authority變量為String類型,如果在相同的URI情況下,其hashCode是一致。unique在FilSystem.getApi下也不用關(guān)心,因?yàn)槊看卧搮?shù)的值都是0。那么此處需要重點(diǎn)關(guān)注一下ugi.hashCode()。

至此,來(lái)小結(jié)一下:

  1. 在獲取FileSystem時(shí),F(xiàn)ileSystem內(nèi)置了一個(gè)static的Cache,該Cache內(nèi)部有一個(gè)Map,用于緩存已經(jīng)獲取的FileSystem連接。
  2. 參數(shù)fs.hdfs.impl.disable.cache,用于控制FileSystem是否需要緩存,默認(rèn)情況下是false,即緩存。
  3. Cache中的Map,Key為Cache.Key類,該類通過(guò)schem,authority,UserGroupInformation,unique 4個(gè)參數(shù)來(lái)確定一個(gè)Key,如上Cache.Key的hashCode方法。

但還有一個(gè)問(wèn)題,既然FileSystem提供了Cache來(lái)緩存,那么在本例中對(duì)于相同的hdfs連接是不會(huì)出現(xiàn)每次獲取FileSystem都往Cache的Map中添加一個(gè)新的FileSystem。唯一的解釋是Cache.key的hashCode每次計(jì)算出來(lái)了不一樣的值,在Cache.Key的hashCode方法中決定相同的hdfs URI計(jì)算hashCode是否一致是由UserGroupInformation的hashCode方法決定的,接下來(lái)看一下該方法。


UserGroupInformation.hashCode

其方法定義如下:

 @Override
 public int hashCode() {
 return System.identityHashCode(subject);
 }

該方法調(diào)用了本地方法identityHashCode,identityHashCod方法對(duì)不同的對(duì)象返回的hashCode將會(huì)不一樣,即使是實(shí)現(xiàn)了hashCode()的類。那么此處問(wèn)題關(guān)鍵就轉(zhuǎn)化為UserGroupInformation類的subject是否在每次計(jì)算hashCode的時(shí)候是同一個(gè)對(duì)象。
由于該hashCode是計(jì)算Cache.key的hashCode時(shí)調(diào)用的,因此需要看Cache.Key初始化時(shí)候,是如何初始化UserGroupInformation該對(duì)象的,如下:

 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();
 }

繼續(xù)看UserGroupInformation的getCurrentUser()方法,如下:

 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();
 }
 }

其中比較關(guān)鍵的是getStackAccessControlContext方法,該方法調(diào)用了Native方法,如下:

 private static native AccessControlContext getStackAccessControlContext();

該方法會(huì)返回當(dāng)前堆棧的保護(hù)域權(quán)限的AccessControlContext對(duì)象。(關(guān)于該方法更多細(xì)節(jié)未深究,懂的大佬可指出來(lái)一下)

那么此處為什么會(huì)返回不同的Subject對(duì)象呢?由于在本例中是通過(guò)get(final URI uri, final Configuration conf,final String user) Api獲取的,因此折回去看一下這個(gè)方法,如下:

 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);
 }
 });
 }

在該方法中,先通過(guò)UserGroupInformation.getBestUGI方法獲取了一個(gè)UserGroupInformation對(duì)象,然后在通過(guò)UserGroupInformation的doAs方法去調(diào)用了get(URI uri, Configuration conf)方法。

先看一下UserGroupInformation.getBestUGI方法的實(shí)現(xiàn),此處關(guān)注一下傳入的兩個(gè)參數(shù)ticketCachePath,user。ticketCachePath是獲取配置hadoop.security.kerberos.ticket.cache.path的值,在本例中該參數(shù)未配置,因此ticketCachePath為空。user參數(shù)由于是本例中傳入的用戶名,因此該參數(shù)不會(huì)為空。實(shí)現(xiàn)如下:

 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參數(shù)的兩個(gè)參數(shù),如上所分析ticketCachePath為空,user不為空,因此最終會(huì)執(zhí)行createRemoteUser方法。實(shí)現(xiàn)如下:

 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;
 }

從代碼中,可以看出會(huì)通過(guò)createRemoteUser方法,來(lái)創(chuàng)建一個(gè)UserGroupInformation對(duì)象。在createRemoteUser方法中,創(chuàng)建了一個(gè)新的Subject對(duì)象,并通過(guò)該對(duì)象創(chuàng)建了UserGroupInformation對(duì)象。至此,UserGroupInformation.getBestUGI方法執(zhí)行完成。

接下來(lái)看一下UserGroupInformation.doAs方法(FileSystem.get(final URI uri, final Configuration conf, final String user)執(zhí)行的最后一個(gè)方法),如下:

 public <T> T doAs(PrivilegedExceptionAction<T> action
 ) throws IOException, InterruptedException {
 try {
 logPrivilegedAction(subject, action);
 return Subject.doAs(subject, action);
 ………… 省略多余的

然后在調(diào)用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));
 }

最后在調(diào)用AccessController.doPrivileged方法,如下:

 public static native <T> T
 doPrivileged(PrivilegedExceptionAction<T> action,
 AccessControlContext context)
 throws PrivilegedActionException;

該方法為Native方法,該方法會(huì)使用指定的AccessControlContext來(lái)執(zhí)行PrivilegedExceptionAction,也就是調(diào)用該實(shí)現(xiàn)的run方法。即FileSystem.get(uri, conf)方法。

至此,就能夠解釋在本例中,通過(guò)get(final URI uri, final Configuration conf,final String user) 方法創(chuàng)建FileSystem時(shí),每次存入FileSystem的Cache中的Cache.key的hashCode都不一致的情況了,小結(jié)一下:

  1. 在通過(guò)get(final URI uri, final Configuration conf,final String user)方法創(chuàng)建FileSystem時(shí),由于每次都會(huì)創(chuàng)建新的UserGroupInformation和Subject對(duì)象。
  2. 在Cache.Key對(duì)象計(jì)算hashCode時(shí),影響計(jì)算結(jié)果的是調(diào)用了UserGroupInformation.hashCode方法。
  3. UserGroupInformation.hashCode方法,計(jì)算為:System.identityHashCode(subject)。即如果Subject是同一個(gè)對(duì)象則返回相同的hashCode,由于在本例中每次都不一樣,因此計(jì)算的hashCode不一致。
  4. 綜上,就導(dǎo)致每次計(jì)算Cache.key的hashCode不一致,便會(huì)重復(fù)寫入FileSystem的Cache。

FileSystem的兩個(gè)get方法

在FileSystem中,有兩個(gè)重載的get方法,如下:

 public static FileSystem get(final URI uri, final Configuration conf,
 final String user) 
 
 public static FileSystem get(URI uri, Configuration conf)

在前面已經(jīng)詳細(xì)的解讀了第一個(gè)方法,從代碼中可以看第一個(gè)最終還是會(huì)調(diào)用第二個(gè)方法。唯一不同的地方就是在初始化Cache.key獲取UserGroupInformation對(duì)象的時(shí)候,如下:

 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();
 }

該方法會(huì)調(diào)用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);
 }
 }

在直接調(diào)用get(URI uri, Configuration conf)方法時(shí),由于未像get(final URI uri, final Configuration conf, final String user)方法創(chuàng)建Subject對(duì)象,因此此處Subject會(huì)返回空,會(huì)繼續(xù)執(zhí)行g(shù)etLoginUser方法。如下:

 public synchronized 
 static UserGroupInformation getLoginUser() throws IOException {
 if (loginUser == null) {
 loginUserFromSubject(null);
 }
 return loginUser;
 }

由代碼可見(jiàn),loginUser成員變量是關(guān)鍵,查看一下該成員定義,如下:

 /**
 * Information about the logged in user.
 */
 private static UserGroupInformation loginUser = null;

也就是說(shuō),一旦該loginUser對(duì)象初始化成功,那么后續(xù)會(huì)一直使用該對(duì)象。如上一節(jié)所示,UserGroupInformation.hashCode方法將會(huì)返回一樣的hashCode值。也就是能成功的使用到緩存在FileSystem的Cache。

解決

  1. 使用public static FileSystem get(URI uri, Configuration conf): 該方法是能夠使用到FileSystem的Cache的,也就是說(shuō)對(duì)于同一個(gè)hdfs URI是只會(huì)有一個(gè)FileSystem連接對(duì)象的。 使用此Api可通過(guò)System.setProperty("HADOOP_USER_NAME", "hive")方式設(shè)置訪問(wèn)用戶。(如果有更優(yōu)雅方式,望大佬指出) 默認(rèn)情況下fs.automatic.close=true,即所有的連接都會(huì)通過(guò)ShutdownHook關(guān)閉。
  2. 使用public static FileSystem get(final URI uri, final Configuration conf, final String user): 該方法如上分析,會(huì)導(dǎo)致FileSystem的Cache失效,且每次都會(huì)添加至Cache的Map中,導(dǎo)致不能被回收。 在使用時(shí),一種方案是:保證對(duì)于同一個(gè)hdfs URI只會(huì)存在一個(gè)FileSystem連接對(duì)象。 另一種方案是:在每次使用完FileSystem之后,調(diào)用close方法,該方法會(huì)將Cache中的FileSystem刪除。

在FileSystem中,還提供了了newInstance等Api。該系列Api每次都會(huì)返回一個(gè)新的FileSystem,具體實(shí)現(xiàn)參見(jiàn)FileSystem代碼。

反思

  • 在使用開源包時(shí),需詳細(xì)了解其實(shí)現(xiàn),否則可能因?yàn)橐粫r(shí)疏忽出現(xiàn)問(wèn)題。
  • code review 是很有必要的。
  • 完善線上的監(jiān)控機(jī)制。

~~以上為個(gè)人理解,由于水平有限,如有疏漏,望多多指教 ~~

分享到:
標(biāo)簽:OOM
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定