基於JavaAgent的Mock,流量回放,耗時分析,全鏈路監控(實現中)系統-簡介篇

一. 背景

今天無心寫代碼,整理下文章看。應用對於第三方的依賴較多,由於第三方接口測試環境可靠性不高,容易導致測試人員測試堵塞;需要特定場景的數據,但是依賴相對複雜,僞造數據的成本較高等情況,對於接口,數據庫,redis等Mock的需求還是比較大的。目前公司內部不同部門有多套Mock方案,但是都沒有擺脫對代碼的侵入,可擴展性不高。基於目前大部分服務已經是Java技術棧的前提情況下,通過JavaAgent修改字節碼的方式達到Mock的目的的條件逐漸成熟,雖然該方案開發入門較高,但從可維護,推廣簡易,成本效益等角度看是值得嘗試的。

二. 一些提示以及注意事項

  1. 必須基於JVM技術棧
  2. 目前實現SOA接口的錄製,Mock,以及方法級別的耗時分析。
  3. Soa 通過DUBBO,自研SOA的方式提供。
  4. 數據庫查詢,redis查詢等JAVA API相對穩定(暫未實現,架構同理)
  5. 鏈路監控(架構同理)

三. 方案架構

  1. 架構

整體架構在目標應用層面主要劃分爲三部分,

  • Javaagent部分,主要Transform應用代碼,加載Plugin邏輯
  • Plugin,負責數據收集,發送給消息隊列,同步應用信息等
  • 應用,通過反射的方式調用Plugin的Hook,利用Javaagent將相關邏輯編織進應用code space。
    服務端,主要是用來數據管理,用戶管理,ceph管理和其他操作的Portal
    服務端和客戶端通過long-poll http,消息隊列方式進行通信。後面計劃自定義Transport層,通過TCP協議傳輸來提高通信效率和減少資源的利用

在這裏插入圖片描述
3. classloader 方案

Classloader的實現,通過自定義Classloader來實現與應用代碼空間的隔離,保證對應用不產生污染。

在這裏插入圖片描述

  1. 擴展類庫加載

在項目進行Transform的時候初始化自定義類加載,嘗試Attach路徑下的PluginJar。

在這裏插入圖片描述
SPI 核心探測實現

public class SPI {

    private static final Logger LOG = LoggerFactory.getLogger(SPI.class);

    private static final String SPI_BASE_PATH = "META-INF/snake/services";

    public static List<String> detect(JarFile jarFile, String name) {
        JarEntry jarEntry = jarFile.getJarEntry(SPI_BASE_PATH + "/" + name);
        if (jarEntry != null) {
            try (InputStream is = jarFile.getInputStream(jarEntry)) {
                List<String> entries = new LinkedList<>();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
                String readLine = null;
                while ((readLine = bufferedReader.readLine()) != null) {
                    if (readLine.length() > 5) {
                        entries.add(readLine.trim());
                    }
                }
                return entries;
            } catch (IOException e) {
                LOG.error(e.getMessage(), e);
                return Collections.emptyList();
            }
        }
        return Collections.emptyList();
    }
}

PluginAttacher核心實現

/**
 * @author [email protected]
 * @date 2020/5/6
 * MTEAT-INF/snake/service/xxxx.xxx.Pulgin
 */
public class PluginAttacher {

    private static final Logger LOG = LoggerFactory.getLogger(PluginAttacher.class);

    private final LinkedList<File> paths = new LinkedList<>();

    private final ReentrantLock jarScanLock = new ReentrantLock();

    private final ClassLoader classLoader;

    private static final AtomicBoolean ATTACHED = new AtomicBoolean(false);

    public PluginAttacher(final String path, ClassLoader classLoader) {
        this.paths.add(new File(path));
        this.classLoader = classLoader;
    }

    public void attemptAttach() {
        if (ATTACHED.get()) {
            return;
        }
        ATTACHED.set(true);
        load().forEach(Plugin::attach);
    }

    private List<Plugin> load() {
        List<Jar> jars = allJars();
        List<String> detected = new LinkedList<>();
        for (Jar jar : jars) {
            detected.addAll(SPI.detect(jar.jarFile, Plugin.class.getName()));
        }
        return detected.stream().map(d -> {
            try {
                Class<?> klass = classLoader.loadClass(d);
                Constructor<Plugin> constructor = (Constructor<Plugin>) klass.getConstructor();
                return constructor.newInstance();
            } catch (ClassNotFoundException | NoSuchMethodException
                    | InstantiationException | IllegalAccessException
                    | InvocationTargetException e) {
                LOG.error(e.getMessage(), e);
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());

    }


    private List<Jar> allJars() {
        final LinkedList<Jar> allJars = new LinkedList<>();
        jarScanLock.lock();

        for (File path : paths) {
            if (path.exists() && path.isDirectory()) {
                String[] jarNames = path.list((dir, name) -> name.endsWith(".jar"));
                if (jarNames != null) {
                    for (String jarName : jarNames) {
                        File jarFile = new File(path, jarName);
                        try {
                            Jar jar = new Jar(new JarFile(jarFile), jarFile);
                            allJars.add(jar);
                        } catch (IOException ex) {
                            LOG.error(ex.getMessage(), ex);
                        }
                    }
                }
            }
        }
        jarScanLock.unlock();
        return allJars;
    }

    private class Jar {
        private JarFile jarFile;

        private File sourceFile;

        public Jar(JarFile jarFile, File sourceFile) {
            this.jarFile = jarFile;
            this.sourceFile = sourceFile;
        }
    }

}

五. 當前功能支持

  1. 基於JavaAgent,對開發無感,無侵入式Mock
  2. 支持自研Soa服務,DUBBO服務的SOA接口Mock。
  3. 支持接口返回報文的錄製,可支持一次訪問記錄的鏈路日誌回放
  4. 服務方法調用鏈以及方法耗時記錄

六. 後續

  • 這篇文章主要講下整個系統的大體架構和一些想法,具體實現方案和實現技術在後續文章詳細介紹
  • 暫時想到的一些點,JavaAgent技術,字節碼注入技術(ASM,Javassist,Bytebuddy),Pulsar相關介紹,SOA-Dubbo簡要介紹,方法耗時收集,Mock技術方案,全鏈路監控等。
  • 有關Java,Golang,中間件技術愛好者可以發郵件或者留言來一起探討一下(e-mail:[email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章