主頁 > 資料庫 > G1垃圾回收引數調優及MySQL虛參考造成GC時間過長分析

G1垃圾回收引數調優及MySQL虛參考造成GC時間過長分析

2023-06-29 09:04:41 資料庫

1. 背景

我方有一應用,偶爾會出現GC時間過長(間隔約4小時),導致性能波動的問題(介面最長需要耗時3秒以上),經排查為G1垃圾回收器引數配置不當 疊加 MySQL 鏈接超過閑置時間回收,產生大量的虛參考,導致G1在執行老年代混合GC,標記階段耗時過長導致,以下為對此問題的分析及問題總結,

此外,此應用因為使用redis快取為資料庫快取一段時間的熱點資料,導致業務起量創建資料庫鏈接后,會很容易被閑置,容易被資料庫連接池判定為閑置過長而清理,

應用背景

JDK1.8 , mysql-connector-java-5.1.30, commons-dbcp-1.4, spring-4.3.20.RELEASE

硬體:8核16GB;

JVM啟動引數概要:


-Xms9984m -Xmx9984m -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=512m -XX:MaxDirectMemorySize=512m -XX:ParallelGCThreads=8 -XX:CICompilerCount=4 -XX:ConcGCThreads=4 -server -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1HeapRegionSize=32M -XX:SurvivorRatio=10 -XX:MaxTenuringThreshold=5 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1ReservePercent=20 -XX:G1MixedGCLiveThresholdPercent=80 -XX:MaxGCPauseMillis=100 -XX:+ExplicitGCInvokesConcurrent -XX:+PrintGCDetails -XX:+PrintReferenceGC -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:/export/Logs/app/tomcat7-gc.log

dbcp配置:


maxActive=20
initialSize=10
maxIdle=10
minIdle=5
minEvictableIdleTimeMillis=180000
timeBetweenEvictionRunsMillis=20000
validationQuery=select 1

配置說明:

  • maxActive=20: 連接池中最多能夠同時存在的連接數,配置為20,
  • `initialSize=10: 資料源初始化時創建的連接數,配置為10,
  • maxIdle=10: 連接池中最大空閑連接數,也就是說當連接池中的連接數超過10個時,多余的連接將會被釋放,配置為10,
  • minIdle=5: 連接池中最小空閑連接數,也就是說當連接池中的連接數少于5個時,會自動添加新的連接到連接池中,配置為5,
  • `minEvictableIdleTimeMillis=180000: 連接在連接池中最小空閑時間,這里配置為3分鐘,表示連接池中的連接在空閑3分鐘之后將會被移除,避免長時間占用資源,
  • timeBetweenEvictionRunsMillis=20000: 連接池中維護執行緒的運行時間間隔,單位毫秒,這里配置為20秒,表示連接池中會每隔20秒檢查連接池中的連接是否空閑過長時間并且需要關閉,
  • validationQuery=select 1: 驗證連接是否有效的SQL陳述句,這里使用了一個簡單的SELECT 1查詢,

關鍵詞

java G1GC , G1引數調優,G1 STW 耗時過長,com.mysql.jdbc.NonRegisteringDriver,ConnectionPhantomReference,PhantomReference, GC ref-proc spent too much time ,GC remark,Finalize Marking

2. 問題排查

可用率報警

日志查詢

查詢本地日志,找到下游timeout的介面日志, 并找到相關IP為:11.#.#.201

查看jvm監控

這里有個問題,因為系統工具ump對jvm 的監控,只關注young gc & full gc, 遺漏了G1的混合GC, 導致直接通過系統監控未及時發現GC過長的問題,到后面下載了gc日志,最終分析得到了 GC時間超過3.7秒的監控結果,這也是問題的根源,

3. 問題分析

GC日志分析

2023-06-03T14:40:31.391+0800: 184748.113: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.1017154 secs]
   [Parallel Time: 70.3 ms, GC Workers: 6]
      [GC Worker Start (ms): Min: 184748113.5, Avg: 184748113.6, Max: 184748113.6, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 1.9, Avg: 2.1, Max: 2.2, Diff: 0.3, Sum: 12.3]
      [Update RS (ms): Min: 9.7, Avg: 9.9, Max: 10.4, Diff: 0.7, Sum: 59.6]
         [Processed Buffers: Min: 12, Avg: 39.5, Max: 84, Diff: 72, Sum: 237]
      [Scan RS (ms): Min: 0.1, Avg: 0.7, Max: 1.2, Diff: 1.1, Sum: 4.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 56.9, Avg: 57.5, Max: 57.7, Diff: 0.8, Sum: 344.8]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 6]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 70.1, Avg: 70.1, Max: 70.2, Diff: 0.1, Sum: 420.9]
      [GC Worker End (ms): Min: 184748183.7, Avg: 184748183.7, Max: 184748183.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.4 ms]
   [Other: 31.0 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 30.1 ms]
      [Ref Enq: 0.1 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.1 ms]
   [Eden: 1760.0M(1760.0M)->0.0B(2080.0M) Survivors: 192.0M->224.0M Heap: 6521.7M(9984.0M)->4912.0M(9984.0M)]
Heap after GC invocations=408 (full 0):
 garbage-first heap   total 10223616K, used 5029872K [0x0000000570000000, 0x00000005720009c0, 0x00000007e0000000)
  region size 32768K, 7 young (229376K), 7 survivors (229376K)
 Metaspace       used 112870K, capacity 115300K, committed 115968K, reserved 1150976K
  class space    used 12713K, capacity 13380K, committed 13568K, reserved 1048576K
}
 [Times: user=0.45 sys=0.01, real=0.10 secs] 
2023-06-03T14:40:31.493+0800: 184748.215: [GC concurrent-root-region-scan-start]
2023-06-03T14:40:31.528+0800: 184748.251: [GC concurrent-root-region-scan-end, 0.0359052 secs]
2023-06-03T14:40:31.528+0800: 184748.251: [GC concurrent-mark-start]
2023-06-03T14:40:31.623+0800: 184748.345: [GC concurrent-mark-end, 0.0942951 secs]
2023-06-03T14:40:31.624+0800: 184748.347: [GC remark 2023-06-03T14:40:31.624+0800: 184748.347: [Finalize Marking, 0.0003013 secs] 2023-06-03T14:40:31.625+0800: 184748.347: [GC ref-proc, 3.7471488 secs] 2023-06-03T14:40:35.372+0800: 184752.094: [Unloading, 0.0254883 secs], 3.7778434 secs]
 [Times: user=3.88 sys=0.05, real=3.77 secs] 
2023-06-03T14:40:35.404+0800: 184752.127: [GC cleanup 4943M->4879M(9984M), 0.0025357 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
2023-06-03T14:40:35.407+0800: 184752.129: [GC concurrent-cleanup-start]
2023-06-03T14:40:35.407+0800: 184752.130: [GC concurrent-cleanup-end, 0.0000777 secs]
{Heap before GC invocations=409 (full 0):
 garbage-first heap   total 10223616K, used 6930416K [0x0000000570000000, 0x00000005720009c0, 0x00000007e0000000)
  region size 32768K, 67 young (2195456K), 7 survivors (229376K)
 Metaspace       used 112870K, capacity 115300K, committed 115968K, reserved 1150976K
  class space    used 12713K, capacity 13380K, committed 13568K, reserved 1048576K

根據日志分析,該G1垃圾回收器共進行了一次youngGC 和 一次并發標記,

young GC在14:40:31.391開始,暫停時間為0.1017154秒,是 young collection 和 initial-mark 階段,并且6個 GC 作業執行緒并行執行了70.3毫秒,GC 程序中,有多個階段的時間記錄和詳細資訊,堆大小從6521M減少到4912M,

并發標記在14:40:31.493開始,并涉及并發掃描、標記和重標記等階段,在清理階段結束后,堆大小從4943M減少到4879M,

因此,該 G1 垃圾回收器在這段時間STW(Stop-The-World)的總時間為 0.10 + 3.77 = 3.87 秒,

Gceasy.io 分析的結果:

疑問1:youngGC后堆記憶體回收不明顯?

后面會追溯到MySQL的鏈接池虛參考,本次GC只能先將鏈接實體GC掉, 而用于追蹤connection實體回收的虛參考,會在下次GC才能被回收,

疑問2:G1的MaxGCPauseMillis是否嚴格遵守,為什么我配置了-XX:MaxGCPauseMillis=100 本次gc時間還是達到了3秒以上?

G1 的 -XX:MaxGCPauseMillis 引數表示 G1 GC 的最大 STW 時間,即如果單次 GC 暫停時間超過該值,則 G1 會盡可能地調整其行為以達到該目標,但是請注意,該引數是一個指導性的引數,不能保證絕對精度,

在實際應用中,由于應用程式和系統的負載情況、堆記憶體大小等因素的影響,GC 的發生和 STW 時間都可能會出現一定程度的波動和不確定性,因此,單次 GC 暫停時間超過 -XX:MaxGCPauseMillis 配置值是有可能發生的,而且這種情況在高負載或堆記憶體使用較高時更容易出現,

疑問3:監控中為啥出現了多次mixed GC, 而且間隔還如此的短

-XX:G1MixedGCCountTarget,默認為8,這個引數標識最后的混合回收階段會執行8次,一次只回收掉一部分的Region,然后系統繼續運行,過一小段時間之后,又再次進行混合回收,重復8次,執行這種間斷的混合回收,就可以把每次的混合回收時間控制在我們需要的停頓時間之內了,同時達到垃圾清理的效果,清理了 7 次就已經滿足了回收效果,所以沒有繼續 mixed GC,

以上是為了:MaxGCPauseMillis 停頓時間控制在期望范圍內,所以會出現多次間隔很短連續的mixed GC.

dump分析

通過 MAT 工具分析發現,第一個可能的問題就是:com.mysql.jdbc.NonRegisteringDriver 占用比較大的記憶體,如下圖:

The class com.mysql.jdbc.NonRegisteringDriver, loaded by org.apache.catalina.loader.ParallelWebappClassLoader @ 0x73e8b9a00, occupies 857,532,208 (88.67%) bytes. The memory is accumulated in one instance of java.util.concurrent.ConcurrentHashMap$Node[], loaded by , which occupies 857,529,112 (88.67%) bytes.

Keywords

com.mysql.jdbc.NonRegisteringDriver
org.apache.catalina.loader.ParallelWebappClassLoader @ 0x73e8b9a00
java.util.concurrent.ConcurrentHashMap$Node[]

·然后看大物件串列,NonRegisteringDriver 物件確實占記憶體比較多,其中成員變數connectionPhantomRefs 占記憶體最多,里面存的是資料庫連接的虛參考,其型別是 ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference>,占比居然有88%之多,

增加JVM引數,列印詳細物件回收掃描日志

增加JVM引數: -XX:+PrintReferenceGC, 列印各種參考物件的詳細回收時間:

PrintReferenceGC是JVM提供的一個引數,用于在進行垃圾回收時列印參考處理相關資訊,當啟用該引數后,JVM會在每次進行垃圾回收時將一些有關參考處理(Reference Processing)的資訊輸出到標準輸出或指定的日志檔案中,

具體來說,PrintReferenceGC引數可幫助開發人員和系統管理員更好地了解應用程式中各種型別參考被處理的情況,包括Soft Reference、Weak Reference、Phantom Reference等,這些資訊對于診斷記憶體泄漏和調整垃圾回收器性能非常有用,

列印的詳細的gc日志如下:

2023-06-04T10:28:52.886+0800: 24397.548: [GC concurrent-root-region-scan-start]
2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-root-region-scan-end, 0.0545027 secs]
2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-mark-start]
2023-06-04T10:28:53.198+0800: 24397.859: [GC concurrent-mark-end, 0.2565503 secs]
2023-06-04T10:28:53.199+0800: 24397.860: [GC remark 2023-06-04T10:28:53.199+0800: 24397.860: [Finalize Marking, 0.0004169 secs] 2023-06-04T10:28:53.199+0800: 24397.861: [GC ref-proc2023-06-04T10:28:53.199+0800: 24397.861: [SoftReference, 9247 refs, 0.0035753 secs]2023-06-04T10:28:53.203+0800: 24397.864: [WeakReference, 963 refs, 0.0003121 secs]2023-06-04T10:28:53.203+0800: 24397.865: [FinalReference, 60971 refs, 0.0693649 secs]2023-06-04T10:28:53.273+0800: 24397.934: [PhantomReference, 49828 refs, 20 refs, 4.5339260 secs]2023-06-04T10:28:57.807+0800: 24402.468: [JNI Weak Reference, 0.0000755 secs], 4.6213645 secs] 2023-06-04T10:28:57.821+0800: 24402.482: [Unloading, 0.0332897 secs], 4.6620392 secs]
 [Times: user=4.60 sys=0.31, real=4.67 secs] 
2023-06-04T10:28:57.863+0800: 24402.524: [GC cleanup 4850M->4850M(9984M), 0.0031413 secs]
 [Times: user=0.01 sys=0.01, real=0.00 secs] 
{Heap before GC invocations=68 (full 0):
 garbage-first heap   total 10223616K, used 7883923K [0x0000000570000000, 0x00000005720009c0, 0x00000007e0000000)
  region size 32768K, 98 young (3211264K), 8 survivors (262144K)
 Metaspace       used 111742K, capacity 114304K, committed 114944K, reserved 1150976K
  class space    used 12694K, capacity 13362K, committed 13568K, reserved 1048576K

翻譯日志內容如下:

2023-06-04T10:28:52.886+0800: 24397.548: [GC concurrent-root-region-scan-start]:開始掃描并發根區域,
2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-root-region-scan-end, 0.0545027 secs]:并發根區域掃描結束,持續時間為0.0545027秒,
2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-mark-start]:開始并發標記程序,
2023-06-04T10:28:53.198+0800: 24397.859: [GC concurrent-mark-end, 0.2565503 secs]:并發標記程序結束,持續時間為0.2565503秒,
2023-06-04T10:28:53.199+0800: 24397.860: [GC remark]: G1執行remark階段,
2023-06-04T10:28:53.199+0800: 24397.860: [Finalize Marking, 0.0004169 secs]:標記finalize佇列中待處理物件,持續時間為0.0004169秒,
2023-06-04T10:28:53.199+0800: 24397.861: [GC ref-proc]: 進行參考處理,
2023-06-04T10:28:53.199+0800: 24397.861: [SoftReference, 9247 refs, 0.0035753 secs]:處理軟參考,持續時間為0.0035753秒,
2023-06-04T10:28:53.203+0800: 24397.864: [WeakReference, 963 refs, 0.0003121 secs]:處理弱參考,持續時間為0.0003121秒,
2023-06-04T10:28:53.203+0800: 24397.865: [FinalReference, 60971 refs, 0.0693649 secs]:處理虛參考,持續時間為0.0693649秒,
2023-06-04T10:28:53.273+0800: 24397.934: [PhantomReference, 49828 refs, 20 refs, 4.5339260 secs]:處理final reference中的phantom參考,持續時間為4.5339260秒,
2023-06-04T10:28:57.807+0800: 24402.468: [JNI Weak Reference, 0.0000755 secs]:處理JNI weak參考,持續時間為0.0000755秒,
2023-06-04T10:28:57.821+0800: 24402.482: [Unloading, 0.0332897 secs]:卸載無用的類,持續時間為0.0332897秒,
[Times: user=4.60 sys=0.31, real=4.67 secs]:垃圾回收的時間資訊,user表示用戶態CPU時間、sys表示內核態CPU時間、real表示實際運行時間,
2023-06-04T10:28:57.863+0800: 24402.524: [GC cleanup 4850M->4850M(9984M), 0.0031413 secs]:執行cleanup操作,將堆大小從4850M調整為4850M,持續時間為0.0031413秒,

其中PhantomReference耗時最長:

2023-06-04T10:28:53.273+0800: 24397.934: [PhantomReference, 49828 refs, 20 refs, 4.5339260 secs]:處理final reference中的phantom參考,持續時間為4.5339260秒,

原始碼追蹤

MYSQL driver相關

注意,本原始碼基于當前依賴的mysql-connector-java-5.1.30.jar, 各版本的代碼有一定差異性質;

涉及MySQL創建連接池的地方為:com.mysql.jdbc.NonRegisteringDriver#connect方法:

  public Connection connect(String url, Properties info) throws SQLException {
      //...省略部分代碼

        Properties props = null;
        if ((props = this.parseURL(url, info)) == null) {
            return null;
        } else if (!"1".equals(props.getProperty("NUM_HOSTS"))) {
            return this.connectFailover(url, info);
        } else {
            try {
                //  獲取連接主要在這里
                com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(this.host(props), this.port(props), props, this.database(props), url);
                return newConn;
            } catch (SQLException var6) {
                throw var6;
            } catch (Exception var7) {
                SQLException sqlEx = SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + var7.toString() + Messages.getString("NonRegisteringDriver.18"), "08001", (ExceptionInterceptor)null);
                sqlEx.initCause(var7);
                throw sqlEx;
            }
        }
    }

com.mysql.jdbc.ConnectionImpl構造器比較長,在完成引數構建后,在其結尾處,發現它呼叫了NonRegisteringDriver.trackConnection(this)

public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
    ......
    NonRegisteringDriver.trackConnection(this);
}

其中追蹤任務的注冊如下:這段MySQL driver原始碼的作用是實作對MySQL資料庫連接的跟蹤,

 public class NonRegisteringDriver {
 
   //省略部分代碼...


   // 連接虛參考指向map的容器宣告
  protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap();


  // 將連接放入追蹤容器中
   protected static void trackConnection(com.mysql.jdbc.Connection newConn) {
        ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl)newConn, refQueue);
        connectionPhantomRefs.put(phantomRef, phantomRef);
    }
    
   
     //省略部分代碼...
   }

第一行代碼宣告了一個名為connectionPhantomRefsConcurrentHashMap容器,該容器用于存盤ConnectionPhantomReference實體,

第二個方法trackConnection的作用是將新連接添加到connectionPhantomRefs映射中,它接受一個com.mysql.jdbc.Connection物件作為引數,創建一個新的ConnectionPhantomReference實體,并使用它和參考佇列(refQueue)將其添加到connectionPhantomRefs映射中,

總的來說,這兩個代碼片段旨在通過使用虛參考來實作跟蹤連接到MySQL資料庫的機制,虛參考用于跟蹤已被JVM垃圾回收的物件,允許程式在物件從記憶體中洗掉后執行特定任務,

連接虛參考為靜態內部類

這段代碼是 MySQL 驅動程式的一部分,用于清理底層網路資源并確保在 MySQL 連接物件被垃圾回收時釋放這些資源,

    static class ConnectionPhantomReference extends PhantomReference<ConnectionImpl> {
        private NetworkResources io;

        ConnectionPhantomReference(ConnectionImpl connectionImpl, ReferenceQueue<ConnectionImpl> q) {
            super(connectionImpl, q);

            try {
                this.io = connectionImpl.getIO().getNetworkResources();
            } catch (SQLException var4) {
            }

        }

        void cleanup() {
            if (this.io != null) {
                try {
                    this.io.forceClose();
                } finally {
                    this.io = null;
                }
            }

        }
    }

這段 MySQL driver 原始碼中的 ConnectionPhantomReference 類是一個繼承自 PhantomReference<ConnectionImpl> 的靜態內部類,其中 ConnectionImpl 是 MySQL 連接的實作類,該類的作用是在連接物件被垃圾回收時清理底層網路資源,

ConnectionPhantomReference 建構式接受 ConnectionImpl 物件和一個參考佇列作為引數,并呼叫父類 PhantomReference 的建構式來創建虛參考,它還通過呼叫 ConnectionImpl 的 getIO().getNetworkResources() 方法獲取與連接關聯的網路資源,如果獲取失敗,則不會拋出例外,

cleanup() 方法用于在連接物件被垃圾回收后清理網路資源,它檢查 io 屬性是否為空,如果不為空,則呼叫 forceClose() 方法來強制關閉底層網路資源,最終將 io 屬性設定為 null,整個程序確保連接物件被垃圾回收時,底層網路資源也被正確地釋放,

MySQL為什么要使用虛參考來解決IO資源回收問題?

MySQL 使用虛參考來解決 IO 資源回收問題,主要是因為 JDBC 連接物件在關閉連接時無法保證其底層網路資源會被立即釋放,這可能會導致底層網路資源長時間占用,最終導致應用程式出現性能下降或者資源耗盡的情況,

使用虛參考的好處在于,它允許程式在物件從記憶體中洗掉后執行特定任務, MySQL 驅動程式利用 Java 提供的參考佇列機制,將 JDBC 連接物件的虛參考加入到佇列中,一旦連接物件被垃圾回收,JVM 會將它加入到參考佇列中等待進一步處理,此時,MySQL 驅動程式通過監視參考佇列并清理底層網路資源,確保這些資源在連接物件被垃圾回收時被正確地釋放,從而避免了底層網路資源長時間占用的問題,

使用虛參考的優點在于可以實作更精細的記憶體管理和資源回收,提高應用程式的可靠性和性能,

那MySQL是怎樣執行最終的IO資源回收的呢,是使用了定時任務還是異步守護執行緒?

MySQL 在執行最終的 IO 資源回收時,使用了異步守護執行緒來清理底層網路資源,

MySQL 驅動程式將 JDBC 連接物件的虛參考加入到參考佇列中后,會創建一個專門的守護執行緒來監視該參考佇列,當連接物件被垃圾回收并加入到參考佇列中后,守護執行緒會從參考佇列中獲取該連接物件的虛參考,并在后臺執行緒中執行與連接相關的底層網路資源清理作業,

具體來說,守護執行緒在每次迭代中從參考佇列中獲取一組虛參考,并呼叫虛參考的 cleanup() 方法來清理底層網路資源,如果成功清理,則將其從映射表中洗掉;否則,保留虛參考以便下一次迭代,

由于清理操作是在后臺執行緒中進行的,因此不會阻塞應用程式的主執行緒,同時,使用異步守護執行緒還可以避免定時任務可能帶來的性能開銷和不必要的 CPU 使用率,

MySQL 使用異步守護執行緒來執行 IO 資源回收作業,這種方式可以確保底層網路資源的及時釋放,并且不會對應用程式的性能產生負面影響,

以下是 MySQL 驅動程式中執行 IO 資源回收的守護執行緒的相關代碼:

public class NonRegisteringDriver implements java.sql.Driver {
 
//省略代碼...

//在MySQL driver中守護執行緒創建及啟動...
static {
		AbandonedConnectionCleanupThread referenceThread = new AbandonedConnectionCleanupThread();
		referenceThread.setDaemon(true);
		referenceThread.start();
	}
  
  //省略代碼...
}

public class AbandonedConnectionCleanupThread extends Thread {
	private static boolean running = true;
	private static Thread threadRef = null;

	public AbandonedConnectionCleanupThread() {
		super("Abandoned connection cleanup thread");
	}

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

	public static void shutdown() throws InterruptedException {
		running = false;
		if (threadRef != null) {
			threadRef.interrupt();
			threadRef.join();
			threadRef = null;
		}
	}

}

這段代碼是 MySQL 驅動程式中的一個類 AbandonedConnectionCleanupThread,該類繼承自 Java 的 Thread 類,其作用是定期清理連接物件的底層網路資源,防止出現資源泄露和記憶體溢位等問題,在該類的 run() 方法中,回圈檢查是否有虛參考需要清理,如果存在,則呼叫 cleanup() 方法來清理虛參考所關聯的底層網路資源,并從 NonRegisteringDriver.connectionPhantomRefs 映射中洗掉該虛參考,

在 MySQL 驅動程式中,connectionPhantomRefs 映射表的鍵和值都是 ConnectionPhantomReference 型別的物件,即使用同一個物件作為鍵和值,

這樣做的主要原因是,在虛參考被加入到參考佇列中時,需要從映射表中洗掉對應的虛參考,如果將虛參考物件作為鍵,那么在洗掉虛參考時,需要遍歷整個映射表才能找到該虛參考物件并將其洗掉,這會造成不必要的性能開銷,

相反,如果將虛參考物件作為值,那么只需要使用該虛參考物件自身來洗掉映射表中的條目,就可以提高洗掉效率,在 trackConnection() 方法中,創建一個新的 ConnectionPhantomReference 物件,并將其作為鍵和值存盤到 connectionPhantomRefs 映射表中,這樣,在虛參考被加入到參考佇列中時,只需從映射表中洗掉該虛參考物件自身即可,無需遍歷整個映射表,

總之,使用同一個物件作為映射表的鍵和值,可以更有效地洗掉映射表中的條目,提高性能和效率,

PS: java.lang.ref.ReferenceQueue#remove(long) 介紹

java.lang.ref.ReferenceQueue#remove(long) 方法的作用是從佇列中移除已經被回收的 Reference 物件,若佇列中沒有任何元素,則該方法會一直等待直到有元素加入或者經過指定時間后回傳 null,

該方法的引數為超時時間,單位為毫秒,如果在指定的超時時間內沒有元素加入佇列,則該方法回傳 null,如果超時時間為 0,則該方法立即回傳,并且不會等待任何元素加入佇列,

通常情況下,程式不需要呼叫這個方法,Java 虛擬機會自動呼叫該方法來處理已經被回收的物件,只有在特定的情況下,如手動管理記憶體時才需要手動呼叫該方法,

這里使用java.lang.ref.ReferenceQueue主要作用是在Connection被垃圾回收器回收后, 能通過remove(timeout) 獲取已經被回收的參考佇列,然后進行后置處理,

推斷

因為目前在NonRegisteringDriver的靜態成員變數:connectionPhantomRefs, 有幾萬個物件, 證明在這段時間積累了大量的資料庫鏈接connection實體進入以下生命周期:

創建 --> 閑置 ---> 回收;

疑點1:

因為前面連接閑置時間為db.minEvictableIdleTimeMillis=180000(3分鐘), 可能是造成問題的關鍵,正常的MySQL鏈接閑置時間默認為8小時,

疑點2:

此外,因為該應用有多個資料源,且部分資料進行了分庫,導致和資料庫的連接數較多,而且訪問請求也不均勻,這就導致了高峰期創建大量資料庫連接池,低峰期又被回收,使com.mysql.jdbc.NonRegisteringDriver#connectionPhantomRefs 中的物件越來越多,而且大部分進入老年代,需要fullGC或者G1 的mixedGC才能進行回收,

4. 解決方案

方案1:調整JVM引數

雖然問題的根源是MySQL的鏈接失效,造成虛參考在gc的remark階段耗時較長,但是我們的G1引數配置也存在問題:其中引數1的調整是本次減少GC時間的關鍵,

引數1:ParallelRefProcEnabled (并行參考處理開啟)

經過核查應用確認為G1GC 引數配置不當,未開啟ParallelRefProcEnabled(并行參考處理)JDK8需要手動開啟,JDK9+已默認開啟, 導致G1 Final Remark階段使用單執行緒標記(G1混合GC的時候,STW約為2~3秒);

解決方案:

開啟ParallelRefProcEnabled(并行參考處理)功能后,ParallelGCThreads=8 啟用8執行緒標記;

引數2:ParallelGCThreads (并行 GC 執行緒數)

ParallelGCThreads引數是一個用于指定G1垃圾回收器并行垃圾收集執行緒數的引數,與其他Java垃圾回收器不同,G1垃圾回收器可以同時執行多個垃圾回收執行緒來加速垃圾回收速度,

在Java 8中,默認情況下,G1垃圾回收器的ParallelGCThreads引數設定為與CPU核心數量相同的值,最大為8,如果您的機器有更多的CPU核心,則可以通過手動調整該引數來提高并行垃圾回收的效率,例如,如果您的機器有8個核心,則可以將ParallelGCThreads設定為8或更高的值,

需要注意的是,過高的ParallelGCThreads值可能會導致處理器爭用和背景關系切換,從而影回應用程式的性能,因此,在進行調整時,應根據機器規格和實際負載進行測驗和優化,以找到最佳引數配置,建議保持默認值,并在實測結果反饋之后再進行適當的調整,

另外,還可以使用-XX:+UseDynamicNumberOfGCThreads引數讓G1自動適配并行執行緒數,這樣,執行緒數量會根據系統負載自動增減,從而獲得更好的性能

解決方案:

機器為8核,最開始只配置了6C,故而將并行GC執行緒數量增加到8,或者不不配置,默認為核心數8;

引數3:G1MixedGCLiveThresholdPercent (G1混合垃圾回收器存活閾值百分比)

G1MixedGCLiveThresholdPercent是一個用于控制G1垃圾回收器混合垃圾回收的引數,它決定混合垃圾回收器在何時執行,這個值指某一個標記為old region的區域中存活物件百分達到此閾值后,會被G1 mixed GC回收處理;

如果將該值設定得太低,則可能導致頻繁的混合垃圾回收,從而影回應用程式的性能;如果將其設定得太高,則可能會導致老年代中的垃圾增加,造成堆記憶體占用過高,單次STW延長,

默認值: 85%;

解決方案:

因為前期設定為80, 本次調整為60 , 雖然增加混合GC觸發的頻率,但是因為此時物件更少,需要消耗的時間也變短;

引數4: MaxTenuringThreshold (物件晉升到老年代的最大年齡)

MaxTenuringThreshold是一個用于控制物件在Survivor區域中存活的時間閾值的引數,它指定了物件在經過多少次Minor GC后可以晉升到老年代,具體來說,如果某個物件在Survivor區域中存活了足夠長的時間(即經過足夠多次的Minor GC),則會被移動到老年代中,

在Java 8中,默認情況下,MaxTenuringThreshold引數設定為15,即物件必須在Survivor區域中存活15次Minor GC才能晉升到老年代,如果將其設定為0,則表示所有的物件都可以在第一次Minor GC時晉升到老年代;如果將其設定為較大的值,則表示只有存活時間比較長的物件才能晉升到老年代,

需要注意的是,如果將MaxTenuringThreshold引數設定得過小,則可能導致頻繁的晉升和回收操作,從而影回應用程式的性能;如果將其設定得過大,則可能會導致Survivor區域中的空間不足,從而強制進行Full GC操作,同樣也會影回應用程式的性能,

建議在實際應用程式場景中進行測驗和優化,以找到最佳的MaxTenuringThreshold值,通常,該值應該設定在8-15之間,并且應根據應用程式的記憶體使用情況和GC日志進行調整,

解決方案:

因為前期設定為5, 本次調整為15 ,或者不需要配置,默認為15;

調整后的JVM引數

-Xms9984m  -Xmx9984m  -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=512m  -XX:MaxDirectMemorySize=512m  -XX:ParallelGCThreads=8  -XX:CICompilerCount=4  -XX:ConcGCThreads=4 -server -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:SurvivorRatio=10 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1ReservePercent=20 -XX:G1MixedGCLiveThresholdPercent=60 -XX:MaxGCPauseMillis=100 -XX:+ExplicitGCInvokesConcurrent -XX:+ParallelRefProcEnabled -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintReferenceGC  -XX:+PrintHeapAtGC -Xloggc:/export/Logs/app/tomcat7-gc.log 

方案2: 優化MySQL的虛參考問題

調整連接池引數

在諸多連接池中,發現一個引數配置錯誤,有一個資料庫的db.minEvictableIdleTimeMillis=180000(3分鐘),適當調大連接的閑置時間到30Min;

將DBCP連接池引數minIdle調小,減少閑置執行緒的數量(在整個連接都不活躍的情況下),也可以起到一定作用;

但是這個方式,治標不治本, 因為虛參考還在一直產生,

優化MySQL driver虛參考

A. 反射暴力清理

虛參考往往做為一種兜底策略,避免用戶忘記釋放資源,引發記憶體泄露(在未使用連接池的如java初學者手擼的資料庫鏈接管理代碼往往是有用的),我們使用DBCP或其它池化技術已經會嚴謹處理資源的釋放,可以不采用兜底策略,直接洗掉中 connectionPhantomRefs 中的虛參考,使物件不可達,在 GC 時直接回收,從而減少 PhantomReference 的處理時間,

// 每兩小時清理 connectionPhantomRefs,減少對 mixed GC 的影響
SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> {
  try {
    Field connectionPhantomRefs = NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
    connectionPhantomRefs.setAccessible(true);
    Map map = (Map) connectionPhantomRefs.get(NonRegisteringDriver.class);
    if (map.size() > 50) {
      map.clear();
    }
  } catch (Exception e) {
    log.error("connectionPhantomRefs clear error!", e);
  }
}, 2, 2, TimeUnit.HOURS);

以上代碼通過反射定時清理掉NonRegisteringDriver靜態變數connectionPhantomRefs,即信任DBCP在鏈接回收的時候,會自動嚴謹處理資源釋放,MySQL driver自帶的安全防護全部定時清空,相當于禁用此功能,

特別說明:  A方案無安全性問題,可放心采納;

B. 升級MySQL jdbc driver到8.0.22+,開啟disableAbandonedConnectionCleanup

Oracle應該是接收了大量開發人員的反饋,意識到了此功能可能造成GC嚴重過長的問題,在高版本中已經可以通過配置選擇性關閉此非必要拖尾功能, mysql-connector-java 版本(8.0.22+)的代碼對資料庫連接的虛參考有新的處理方式,其增加了開關,可以手動關閉此功能,

其版本8.0.22介紹增加此引數即是為了解決JVM 虛參考相關問題,但是默認是未啟用,需要手動開啟:

https://dev.mysql.com/doc/relnotes/connector-j/8.0/en/news-8-0-22.html

When using Connector/J, the AbandonedConnectionCleanupThread thread can now be disabled completely by setting the new system property com.mysql.cj.disableAbandonedConnectionCleanup to true when configuring the JVM. The feature is for well-behaving applications that always close all connections they create. Thanks to Andrey Turbanov for contributing to the new feature. (Bug #30304764, Bug #96870)

? 有了這個配置,就可以在啟動引數上設定屬性:

java -jar app.jar -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true

? 或者在代碼里設定屬性:

System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");

當 com.mysql.cj.disableAbandonedConnectionCleanup=true 時,生成資料庫連接時就不會生成虛參考,對 GC 就沒有任何影響了,

是否升級MySQL的驅動版本,根據實際情況取舍,以上A/B方案都能解決該問題,

總結

G1引數調優總結

  1. ParallelGCThreads 主要為并發標記相關執行緒,其本身處理時候也會STW, 最好全部占用CPU內核,如機器為8核,則該值最好設定為8;
  2. ConcGCThreads 為并發標記執行緒數量,其標記的時候,業務執行緒可以不STW, 可以使用部分核心,剩余部分核心用于跑業務執行緒;我們這里配置為CPU核心數量的一半4;
  3. MaxGCPauseMillis 只是一個期望值(GC會盡可能保證),并不能嚴格遵守,尤其是在標記階段,會受到堆記憶體物件數量的影響,故而不能太過依賴此值覺得STW的實際時間;如我方配置的100ms, 實際出現3~4秒的機會也比較多;
  4. ParallelRefProcEnabled 如果你搜索的各種資料提醒你這個開關是默認開啟的,那你要特別注意了; JDK8版本此功能是默認關閉的,在JDK9+之后才是默認開啟,故而JDK8需要手動開啟,保險起見,無論什么版本都配置該值;
  5. G1垃圾回收器基本上不會出現fullGC, 在youngGC 和 mixedGC(mixedGC頻次也不高)就完成了垃圾回收,如果出現了fullGC, 反而應該排查是否有例外,
  6. 本文未對G1垃圾回收器之程序及原理進行詳細分析,請參考其它文章進行理解;

附錄:Oracle官方JDK8 G1引數調優官方檔案

MySQL虛參考問題總結

  1. MySQL 鏈接驅動如果在8.0.22版本以下,只能通過反射主動清理此非必要拖尾功能(一般成熟的資料庫連接池功能都能安全釋放鏈接相關資源),
  2. MySQL驅動8.0.22+版本, 可以通過配置手動關閉此功能;

結果說明

經過以上調整后(MySQL的虛參考問題一并解決后),gc時間能較好的控制在100ms以內,平均耗時26ms, mixed gc頻率大幅度減少, 主要為youngGC,

參考資料

線上問題分析系列:資料庫連接池記憶體泄漏問題的分析和解決方案

com.mysql.jdbc.NonRegisteringDriver 記憶體泄漏

神燈-線上問題處理案例1:出乎意料的資料庫連接池

G1收集器詳解及調優

infoq-Tips for Tuning the Garbage First Garbage Collector

Java Garbage Collection handbook

PPPHUANG-JVM 優化踩坑記

PPPHUANG- MySQL 驅動中虛參考 GC 耗時優化與原始碼分析

作者:京東零售 張世彬

來源:京東云開發者社區

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/556269.html

標籤:其他

上一篇:資料交換不失控:華為云EDS,讓你的資料你做主

下一篇:返回列表

標籤雲
其他(161831) Python(38259) JavaScript(25516) Java(18273) C(15238) 區塊鏈(8273) C#(7972) AI(7469) 爪哇(7425) MySQL(7273) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5875) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4607) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2438) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1985) HtmlCss(1976) 功能(1967) Web開發(1951) C++(1942) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1881) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • G1垃圾回收引數調優及MySQL虛參考造成GC時間過長分析

    我方有一應用,偶爾會出現GC時間過長(間隔約4小時),導致性能波動的問題(介面最長需要耗時3秒以上)。經排查為G1垃圾回收器引數配置不當 疊加 MySQL 鏈接超過閑置時間回收,產生大量的虛參考,導致G1在執行老年代混合GC,標記階段耗時過長導致。以下為對此問題的分析及問題總結。 ......

    uj5u.com 2023-06-29 09:04:41 more
  • 資料交換不失控:華為云EDS,讓你的資料你做主

    摘要:隨著企業資料空間在內部的成功實踐,2022年,華為正式推出云服務產品——華為云交換資料空間EDS(Exchange Data Space),秉持“你的資料你做主”的宗旨,以保護企業資料主權為基礎,促進企業資料高效流通,實作資料價值最大化。 本文分享自華為云社區《資料交換不失控——華為云EDS, ......

    uj5u.com 2023-06-29 09:04:21 more
  • MongoDB(二)

    # MongoDB概念決議 ## 1、MongoDB vs SQL ![](https://img2023.cnblogs.com/blog/2987571/202306/2987571-20230628132104929-1652408975.png) ## 2、資料庫 * 一個MongoDB可以 ......

    uj5u.com 2023-06-29 09:04:04 more
  • 跑得更快!華為云GaussDB以出色的性能守護“ERP的心臟”

    摘要:GaussDB已經全面支撐起MetaERP,在包括庫存服務在內的9大核心模塊中穩定運行,端到端業務效率得到10倍提升。 本文分享自華為云社區《跑得更快!華為云GaussDB以出色的性能守護“ERP的心臟”》,作者:GaussDB 資料庫。 ERP作為華為企業經營最核心的系統,伴隨著華為20多年 ......

    uj5u.com 2023-06-29 09:03:32 more
  • 資料交換不失控:華為云EDS,讓你的資料你做主

    摘要:隨著企業資料空間在內部的成功實踐,2022年,華為正式推出云服務產品——華為云交換資料空間EDS(Exchange Data Space),秉持“你的資料你做主”的宗旨,以保護企業資料主權為基礎,促進企業資料高效流通,實作資料價值最大化。 本文分享自華為云社區《資料交換不失控——華為云EDS, ......

    uj5u.com 2023-06-29 09:02:22 more
  • MongoDB(二)

    # MongoDB概念決議 ## 1、MongoDB vs SQL ![](https://img2023.cnblogs.com/blog/2987571/202306/2987571-20230628132104929-1652408975.png) ## 2、資料庫 * 一個MongoDB可以 ......

    uj5u.com 2023-06-29 09:00:55 more
  • 1、MySQL操作命令學習1

    一、對資料庫及表的基礎操作 1、連接資料庫服務器 mysql -hlocalhost -uroot -p123456 2、2.退出服務器 exit 3、查看所有的資料庫 show databases; 4、創建一個資料庫 create database java; 5、洗掉資料庫 drop data ......

    uj5u.com 2023-06-29 09:00:28 more
  • G1垃圾回收引數調優及MySQL虛參考造成GC時間過長分析

    我方有一應用,偶爾會出現GC時間過長(間隔約4小時),導致性能波動的問題(介面最長需要耗時3秒以上)。經排查為G1垃圾回收器引數配置不當 疊加 MySQL 鏈接超過閑置時間回收,產生大量的虛參考,導致G1在執行老年代混合GC,標記階段耗時過長導致。以下為對此問題的分析及問題總結。 ......

    uj5u.com 2023-06-29 09:00:24 more
  • Spark編程基礎

    # Scala撰寫Spark的WorkCount ## 創建一個Maven專案 在pom.xml中添加依賴和插件 ```XML 8 8 UTF-8 3.2.3 2.12.15 org.scala-lang scala-library ${scala.version} org.apache.spark ......

    uj5u.com 2023-06-29 09:00:06 more
  • 圖文結合帶你搞懂GreatSQL體系架構

    **往期系列回顧** - [圖文結合系列之帶你搞懂MySQL日志系列](http://mp.weixin.qq.com/s?__biz=MzkzMTIzMDgwMg==&mid=2247496981&idx=1&sn=ec496da6e52e19ee505483a15fb54f6b&chksm=c2 ......

    uj5u.com 2023-06-28 10:20:55 more