jvm,apache-commons-pool的PhantomReference引起的一次線上內存崩掉的分析

前一段時間,臨部門的兄弟泰國站的項目,系統上線二天,或者重啓之後系統總是莫名的shutdown,我對這方面比較感興趣,也處理過一些這種問題,就寫下處理的過程:

左邊是沒有修改之前的,右邊是修改之後的,分析這個問題之前,我先介紹一下工具,用的是Mat(Memory Analyzer Tool),我比較喜歡用這個,導入內存dump快照:

一般選擇leak suspects report這個view就可以了,看下面的視圖:

從上面的視圖,可以看出com.mysql.jdbc.NonRegisteringDriver這個對相關佔有了85.98的內存,主要是這個對象所持有的ConcurrentHashMap佔有了絕大多數的內存。接下來轉換視圖,我一般用的是Histogram和Dominator_Tree這二個視圖,把上面的類複製進去,看一下情況:

從這二個圖上不難看出ConnectionPhantomReference這個對象太多,從代碼裏可以來看:

public class NonRegisteringDriver implements java.sql.Driver {
	private static final String ALLOWED_QUOTES = "\"'";

	private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";

	private static final String URL_PREFIX = "jdbc:mysql://";

	private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";

	public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";

	protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference>();
	
protected static void trackConnection(Connection newConn) {
		
		ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl) newConn, refQueue);
		connectionPhantomRefs.put(phantomRef, phantomRef);
	}

熟悉apache commons pool的不難看出來,使用的是common-pool的連接池,而這個方法是每創建一個連接就會放一個Connection對象在這個裏面,這個虛引用的作用,就是在你外部關閉鏈接,但是沒有釋放資源,做一個保底操作,在gc的時候,把持有的資源釋放掉:

public void run() {
		threadRef = this;
		while (running) {
			try {
				Reference<? extends ConnectionImpl> ref = NonRegisteringDriver.refQueue.remove(100);
				if (ref != null) {
					try {
						((ConnectionPhantomReference) ref).cleanup();
					} finally {
						NonRegisteringDriver.connectionPhantomRefs.remove(ref);
					}
				}

			} catch (Exception ex) {
				// no where to really log this if we're static
			}
		}
	}

在發生full gc的時候,會把對象放到refQueue中,最後會把連接所持有的資源釋放掉,但是這個釋放資源是巨耗時間的,所以內存計算導致docker崩掉並不稀罕。但是數據連接池都是有池化資源的概念的,資源循環利用,怎麼可能出現如此顯而易見的錯誤,這是不可能發生的基本上,但是事出必有因,只好進一步的分析問題,到底什麼問題導致這個現象,網上搜了一下,基本上都是草草了之,只有表現,沒有解釋根本原因,所以我不得不自己看這個問題,我的第一個猜測就是長時間連接不用,超過waittime,被回收掉,然後又創建,就這樣頻繁回收和創建,這個猜測的理論必須是minpool是0纔可以,但是minpool和maxpool並沒有問題,都是5和5

問題又一度陷在了這個上面,所有的一切都不符合常理,我只能去看源碼,看一下apache-commons-pool回收連接的代碼,這個項目用的是commons-pool 1.x而不是2.x,這個真的很重要,下面貼下代碼:

private class Evictor extends TimerTask {
        /**
         * Run pool maintenance.  Evict objects qualifying for eviction and then
         * invoke {@link GenericObjectPool#ensureMinIdle()}.
         */
        public void run() {
            try {
                evict();
            } catch(Exception e) {
                // ignored
            } catch(OutOfMemoryError oome) {
                // Log problem but give evictor thread a chance to continue in
                // case error is recoverable
                oome.printStackTrace(System.err);
            }
            try {
                ensureMinIdle();
            } catch(Exception e) {
                // ignored
            }
        }
    }
public void evict() throws Exception {
        assertOpen();
        synchronized (this) {
            if(_pool.isEmpty()) {
                return;
            }
            if (null == _evictionCursor) {
                _evictionCursor = (_pool.cursor(_lifo ? _pool.size() : 0));
            }
        }

        for (int i=0,m=getNumTests();i<m;i++) {
            final ObjectTimestampPair pair;
            synchronized (this) {
                if ((_lifo && !_evictionCursor.hasPrevious()) ||
                        !_lifo && !_evictionCursor.hasNext()) {
                    _evictionCursor.close();
                    _evictionCursor = _pool.cursor(_lifo ? _pool.size() : 0);
                }

                pair = _lifo ?
                        (ObjectTimestampPair) _evictionCursor.previous() :
                        (ObjectTimestampPair) _evictionCursor.next();

                _evictionCursor.remove();
                _numInternalProcessing++;
            }

            boolean removeObject = false;
            final long idleTimeMilis = System.currentTimeMillis() - pair.tstamp;
            if ((getMinEvictableIdleTimeMillis() > 0) &&
                    (idleTimeMilis > getMinEvictableIdleTimeMillis())) {
                removeObject = true;
            } else if ((getSoftMinEvictableIdleTimeMillis() > 0) &&
                    (idleTimeMilis > getSoftMinEvictableIdleTimeMillis()) &&
                    ((getNumIdle() + 1)> getMinIdle())) { // +1 accounts for object we are processing
                removeObject = true;
            }
            if(getTestWhileIdle() && !removeObject) {
                boolean active = false;
                try {
                    _factory.activateObject(pair.value);
                    active = true;
                } catch(Exception e) {
                    removeObject=true;
                }
                if(active) {
                    if(!_factory.validateObject(pair.value)) {
                        removeObject=true;
                    } else {
                        try {
                            _factory.passivateObject(pair.value);
                        } catch(Exception e) {
                            removeObject=true;
                        }
                    }
                }
            }

            if (removeObject) {
                try {
                    _factory.destroyObject(pair.value);
                } catch(Exception e) {
                    // ignored
                }
            }
            synchronized (this) {
                if(!removeObject) {
                    _evictionCursor.add(pair);
                    if (_lifo) {
                        // Skip over the element we just added back
                        _evictionCursor.previous();
                    }
                }
                _numInternalProcessing--;
            }
        }
    }
private void ensureMinIdle() throws Exception {
        // this method isn't synchronized so the
        // calculateDeficit is done at the beginning
        // as a loop limit and a second time inside the loop
        // to stop when another thread already returned the
        // needed objects
        int objectDeficit = calculateDeficit(false);
        for ( int j = 0 ; j < objectDeficit && calculateDeficit(true) > 0 ; j++ ) {
            try {
                addObject();
            } finally {
                synchronized (this) {
                    _numInternalProcessing--;
                    allocate();
                }
            }
        }
    }

問題就出在上面的代碼當中,細心的小夥伴可能已經發現了問題:

if ((getMinEvictableIdleTimeMillis() > 0) &&
                    (idleTimeMilis > getMinEvictableIdleTimeMillis())) {
                removeObject = true;
            }

這代碼不管三七二十一,只要檢測到空閒時間過長,上去先把renoveObject變成true,先銷燬,然後再ensureMinIdle,創建新連接,問題也就是這裏了,這裏的池化概念有問題,可能開發的理念不同,應該先判斷是不是大於minpool,再判斷空閒時間。然後修改成大一點的空閒時間檢測,根據業務來決定,多大合適,2.x是沒有這個問題的,我就不貼代碼了,之後就是最上面的那個圖片了。

修改完之後就是比較平穩的曲線了,其實這個問題的原因還有一個,就是我們上層還有一層redis,redis的能夠承擔大多數的讀,寫入mysql的量也不大,在國內站很久以前也有這種情況,不過現在他們組國內站的機器都500臺,qps也很高了,基本上問題不大。上面就是我處理的過程,當然真正的處理過程沒有這麼輕鬆,其中的一些過程省略了,到此爲止。。。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章