Java agent 使用

news2024/11/19 3:36:12

一、前言

于一个即将上线的应用来说,系统监控是必不可少的,为什么需要监控呢?应用是跑在服务器上的,应用在运行过程中会发生各自意想不到的问题,像大家熟知的OOM,mysql故障,服务器宕机,程序500等等,因此为了能够第一时间掌握应用运行过程中的各自异常状况,对于应用系统的监控是必要的,而在大多数情况下,应用自身的运行时异常占据了绝大的比例,因此合理的监控显得非常重要。

二、应用监控来源

设想一个场景,如果我们要监控一段程序的执行耗时,通常会有哪些做法呢?

  1. 方法前后添加起始,结束耗时;
  2. 使用aop技术跟踪方法的执行耗时;
  3. ......

上面谈到,在大多数情况下,应用自身的运行时异常占据了绝大的比例,所以在日常开发中,通过程序中日志埋点可以很好的帮助分析和定位问题,而说到底,还是监控代码段的执行,如果是一个简单的单体应用,怎么样都无所谓,毕竟简单嘛,但对于一个成熟的运行中的系统来说,尤其是那种分布式的应用平台,不管使用上面哪种技术,都涉及到应用内部程序的开发工作量,面临这种情况该怎么办呢?

是不是有一种技术可以在需要对既有代码做改动的前提下就能够监控代码运行的一些指标呢?

答案是肯定的,那就是接下来要讲的 java agent技术,也就是Java探针技术。

三、Java agent 是什么?

Java Agent技术,也称为Java代理、Java探针,它允许程序员利⽤其构建⼀个独⽴于应⽤程序的代理程序。

  • Java Agent 本质上就是一个 jar 包。
  • 对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。
  • 使用Java Agent可以实现在Java程序运行的过程中对其进行修改。

四、Java Agent 主要功能点

  • 在加载java文件前可以拦截字节码并做修改;
  • 在运行期间变更已加载的类的字节码;
  •  获取所有已经被加载过的类;
  • 获取所有已经被初始化过了的类;
  • 获取某个对象的大小;

以上这些功能,使得Java Agent在作为一个独立于Java应用程序的代理程序的同时,可以协助监测、运行甚至替换 JVM 上的程序。Agent的应用十分广泛,可用于实现Java IDE的调试功能、热部署功能、线上诊断⼯具和性能分析⼯具。

例如,百度网络攻击防护工具OpenRASP中就使用了Java Agent来对敏感函数进行插桩,以此实现攻击检测。大名鼎鼎的skywalking对于各类应用监测其底层实现也是基于Java Agent。

五、Java agent 加载过程

在开始编码之前,有必要对Java agent的原理做一些了解,我们知道,一个普通的java程序要运行起来,可以通过main函数运行,或者打成jar包之后指定启动类,说的再简单点,调用执行命令之后,jvm会通过类加载器加载class文件,然后运行起来;

 有了上面对于java agent的了解,java agent可以在加载java文件前拦截字节码并做修改的,执行过程如下图:

如果将两者结合起来看,更具体的过程如下图,Java agent的入场时机以及作用一目了然; 

六、Java agent 代理的两个入口函数

JVM启动支持加载agent代理,而agent代理本身就是一个JVM TI的客户端,其通过监听事件的方式获取Java应用运行状态,调用JVM TI提供的接口对应用进行控制。两个核心的入口函数如下:

// 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM
public static void premain(String agentArgs, Instrumentation inst);
 
// 用于JVM启动后,在运行时刻加载
public static void agentmain(String agentArgs, Instrumentation inst);

这两个入口函数分别对应于JVM TI专门提供了执行 字节码增强(bytecode instrumentation) 的两个接口,对这两个入口函数再做一些补充说明:

  • premain加载时刻增强(JVM 启动时加载),类字节码文件在JVM加载的时候进行增强;
  • agentMain动态增强(JVM 运行时加载),已经被JVM加载的class字节码文件,当被修改或更新时进行增强;

这两个接口都是从JDK 1.6开始支持,我们无需对上面JVM TI提供的两个接口规范了解太多,Java Agent和 Java Instrument类包 封装好了字节码增强的上述接口通信,JVM在加载agent时会先找函数1,如果没有发现函数1,则会寻找函数2;

七、Java agent 初体验

有了上面的基础储备后,接下来通过实际的代码演示来体验下Java agent的使用吧;

premain 使用演示一

1、变现一个类,提供一个premain的入口方法

import java.lang.instrument.Instrumentation;
 
public class MyPreMainAgent {
 
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("hello javaAgent");
    }
 
}

2、在工程的pom中添加premain打包插件

要注意的是<Premain-Class>标签中写的是上面premain所在类的完整路径

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.congge.agent.MyPreMainAgent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

3、将模块编译打成jar包

将这个jar包解压之后,里面有一个非常重要的文件

 打开该文件,内容如下,通过该文件,应用程序在使用java agent的jar包的时候才会正确加载premain对应的入口函数;

4、编写测试类,启动引用上述的jar

public class AgentTest {
 
    public static void main(String[] args) {
        new UserService().sayHello("hell agent");
    }
 
}

启动时在启动参数中指定上面的jar

然后运行测试类,可以看到jar包中的premain方法的打印输出结果,是在测试类的方法打印结果之前输出;

premain 使用演示二

使用javaagent监控运行时内存使用情况

1、Metric 监控指标类

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.Arrays;
import java.util.List;
 
 
public class Metric {
 
    private static final long MB = 1048576L;
 
    public static void printMemoryInfo() {
        MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
        MemoryUsage headMemory = memory.getHeapMemoryUsage();
 
        String info = String.format("\ninit: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
                headMemory.getInit() / MB + "MB",
                headMemory.getMax() / MB + "MB", headMemory.getUsed() / MB + "MB",
                headMemory.getCommitted() / MB + "MB",
                headMemory.getUsed() * 100 / headMemory.getCommitted() + "%"
 
        );
 
        System.out.print(info);
 
        MemoryUsage nonheadMemory = memory.getNonHeapMemoryUsage();
 
        info = String.format("init: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
                nonheadMemory.getInit() / MB + "MB",
                nonheadMemory.getMax() / MB + "MB", nonheadMemory.getUsed() / MB + "MB",
                nonheadMemory.getCommitted() / MB + "MB",
                nonheadMemory.getUsed() * 100 / nonheadMemory.getCommitted() + "%"
 
        );
        System.out.println(info);
    }
 
    public static void printGCInfo() {
        List<GarbageCollectorMXBean> garbages = ManagementFactory.getGarbageCollectorMXBeans();
        for (GarbageCollectorMXBean garbage : garbages) {
            String info = String.format("name: %s\t count:%s\t took:%s\t pool name:%s",
                    garbage.getName(),
                    garbage.getCollectionCount(),
                    garbage.getCollectionTime(),
                    Arrays.deepToString(garbage.getMemoryPoolNames()));
            System.out.println(info);
        }
    }
 
}

2、premain入口函数类

import java.lang.instrument.Instrumentation;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
 
public class MyJvmAgent {
 
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is an perform monitor agent.");
 
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Metric.printMemoryInfo();
                Metric.printGCInfo();
            }
        }, 0, 3000, TimeUnit.MILLISECONDS);
    }
 
}

3、测试类

import java.util.ArrayList;
import java.util.List;
 
public class AgentTest {
 
    public static void main(String[] args) throws Exception {
        System.out.println("hello javaAgent");
        boolean is = true;
        while (is) {
            List<Object> list = new ArrayList<>();
            list.add("hello agent");
        }
    }
 
}

将上面的MyJvmAgent 配置到pom中,打包,然后像上面的案例那样运行,观察输出结果

八、javassit 介绍与使用

在上面的java agent的类加载原理图中,其最终在编辑字节码文件时,具体的技术实现可以有多种,比如cglib,asm或javassist等;

javassit介绍

javassit是一个开源的分析、编辑和创建Java字节码的类库,主要优点是简单且快速,直接使用java编码的形式,不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成字节码,相对于asm技术,学习和使用成本要低很多;

javassit依赖

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>

javassit 案例使用

1、打印输出方法耗时

通常为了输出一个方法的执行耗时,最简单的办法就是在方法头和方法尾部获取当前的执行时间最后再相减,即可得到方法的执行时长;

如果是在生产系统中来做这个事情,一个工程中随便几十几百个类,方法更多,这样加起来就相当费事了,就可以考虑使用java agent的方式来做,下面来看看使用java agent的实现过程;

提供一个业务类

在该类中有一个普通的方法,为了模拟执行业务,休眠1秒

public class UserService {
 
    public void sayHello(String name){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayHello()");
    }
 
}

提供premain方法

import javassist.*;
 
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
 
public class AgentMain {
 
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        final ClassPool pool = new ClassPool();
        pool.appendSystemPath();
 
        //基于工具,在运行的时候修改class字节码,即动态插装
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                if (!"com/congge/agent/UserService".equals(className)) {
                    return null;
                }
                try {
 
                    CtClass ctClass = pool.get("com.congge.agent.UserService");
                    CtMethod sayHello = ctClass.getDeclaredMethod("sayHello");
 
                    // 打印方法耗时 ================
                    //FIXME 1、复制一个方法
                    CtMethod copy = CtNewMethod.copy(sayHello, ctClass, null);
                    copy.setName("sayHelloCopy");
                    ctClass.addMethod(copy);
 
                    //2、 改变原有的方法
                    sayHello.setBody("{long begin = System.currentTimeMillis();\n" +
                            "                    sayHelloCopy($1);\n" +
                            "                    System.out.println(System.currentTimeMillis()-begin);}");
 
                    //打印方法耗时 ================
 
                    //sayHello.insertBefore("System.out.println(System.currentTimeMillis());");
                    return ctClass.toBytecode();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }
 
}

在该方法中,需要说明的是,通过javassit的编码方式,可以通过下面这种方式将一段程序以字符串的形式动态植入,像其中的 $1 就属于javassit的解析参数的一种指令,有兴趣的同学可以深入学习javassit做进一步的了解;

测试类

public class AgentTest {
 
    public static void main(String[] args) {
        new UserService().sayHello("hell agent");
    }
 
}

然后像上面的操作那样,将premain所在的类配置到pom打包,然后运行测试类,控制台就可以输出本次方法的执行耗时;

2、打印输出方法耗时改进

上面的案例使用javassit实现了对某个方法的执行耗时的监控,现在进一步思考下,如果希望这个功能做的更加通用些,比如说在实际业务中,我们不仅仅是输出某个方法的执行耗时,而是输出某一个包下的类的方法执行耗时,或者某一些特定类的方法的执行耗时又该怎么做呢?接下来,基于上面的案例做进一步的优化改进,其目的是为了尽可能做到通用性;

改进premain方法

该方法的改进点涉及下面几点:

  • 加载java agent的jar包时,可由外部传递参数,即外部传入要执行方法耗时的规则;
  • 可以对类中的所有方法进行插桩;
  • 对有返回值和无返回值的方法均可适用;

import javassist.*;
 
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
 
public class AgentMain1 {
 
    public static void premain(String agentArgs, Instrumentation instrumentation) {
 
        final String config = agentArgs;
 
        final ClassPool pool = new ClassPool();
        pool.appendSystemPath();
 
        //基于工具,在运行的时候修改class字节码,即动态插装
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                if (className == null || !className.replaceAll("/", ".").startsWith(config)) {
                    return null;
                }
                try {
                    className = className.replaceAll("/", ".");
                    CtClass ctClass = pool.getCtClass(className);
                    for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
                        newMethod(ctMethod);
                    }
                    return ctClass.toBytecode();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }
 
    private static CtMethod newMethod(CtMethod oldMethod) throws CannotCompileException, NotFoundException {
        CtMethod copy = CtNewMethod.copy(oldMethod, oldMethod.getDeclaringClass(), null);
        copy.setName(oldMethod.getName() + "$agent");
        oldMethod.getDeclaringClass().addMethod(copy);
 
        if (oldMethod.getReturnType().equals(CtClass.voidType)) {
            oldMethod.setBody(String.format(voidSource, oldMethod.getName()));
        } else {
            oldMethod.setBody(String.format(source, oldMethod.getName()));
        }
        return copy;
    }
 
    final static String source = "{\n" +
            "long begin = System.currentTimeMillis();\n" +
            "        Object result;\n" +
            "        try {\n" +
            "            result = ($w)%s$agent($$);\n" +
            "        }finally {\n" +
            "            long end = System.currentTimeMillis();\n" +
            "            System.out.println(end-begin);\n" +
            "        }\n" +
            "        return ($r) result;\n" +
            "}\n";
 
 
    final static String voidSource = "{\n" +
            "long begin = System.currentTimeMillis();\n" +
            "        try {\n" +
            "            %s$agent($$);\n" +
            "        }finally {\n" +
            "            long end = System.currentTimeMillis();\n" +
            "            System.out.println(end-begin);\n" +
            "        }\n" +
            "}\n";
 
}

UserService 类再追加一个方法

public String sayHello1(String name){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayHello() " + name);
        return name;
    }

启动类

public class AgentTest {
 
    public static void main(String[] args) {
        new UserService().sayHello("hell agent");
        new UserService().sayHello1("hell agent");
    }
 
}

按照上面案例的步骤再次运行,不过这一次启动类中需要添加如下参数,表示以 com.congge.agent.User 这个为前置的类才会被监控执行方法的耗时;

-javaagent:E:\code-self\spi\java-agent\target\java-agent-1.0-SNAPSHOT.jar=com.congge.agent.User

输出控制台结果如下,

通过这种方式,就可以让agent的jar具备了更多的灵活性和一定的通用性,即只要符合匹配规则的类都可以适用这个jar来得到方法的执行耗时;

未完待续 ... 感谢您的观看!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106997.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot+Vue实现前后端分离的教务评教系统

文末获取源码 开发语言&#xff1a;Java 使用框架&#xff1a;spring boot 前端技术&#xff1a;JavaScript、Vue.js 、css3 开发工具&#xff1a;IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库&#xff1a;MySQL 5.7/8.0 数据库管理工具&#xff1a;phpstudy/Navicat JD…

将spark的数据保存到MySQL

文章目录前言环境的准备是必要的下载解压放置文件代码书写注意事项结束语前言 我们用spark对数据进行分析和提取数据后要对得到的数据进行保存接下来的内容是将数据保存到MySQL数据库中 环境的准备是必要的 下载 &#xff08;本小博主已经为看官大人准备好了下载地址点击下载…

【JVM实战系列】「监控调优体系」实战开发arthas-spring-boot-starter监控你的微服务是否健康

前提介绍 相信如果经历了我的上一篇Arthas的文章[【JVM实战系列】「监控调优体系」针对于Alibaba-Arthas的安装入门及基础使用开发实战指南]之后&#xff0c;相信你对Arthas的功能和使用应该有了一定的理解了。那么我们就要进行下一步的探索功能。 Arthas对于SpringBoot2的支…

无效回表谁的锅?存储引擎:这事儿不赖我

明确场景 要回答这个问题&#xff0c;我们一般分几步来走&#xff1a; 1.确认问题&#xff0c;对齐Sql语句&#xff1b; 2.解答问题本身&#xff0c;也就是时间复杂度分析&#xff1b; 3.针对本身提出这个场景&#xff0c;可能出现的性能瓶颈进行分析&#xff1b; 4.针对瓶…

vue+element模仿腾讯视频电影网站

一.布局设计 官方图例&#xff1a; demo效果&#xff1a; 顶部1&#xff1b;左侧菜单栏2&#xff1b;右侧内容展示区3&#xff1b; 关键点&#xff1a; 顶部固定&#xff0c;不随页面滚动而滚动&#xff1b;左侧可局部滚动显示更多菜单&#xff1b;右侧局部滚动&#xff…

拓扑梅尔智慧办公平台(Topomel Box) 3.0发布

今天&#xff0c;2022年12月21日&#xff0c;我很高兴地宣布&#xff1a;拓扑梅尔智慧办公平台(Topomel Box)的3.0版本正式发布。 下面&#xff0c;请允许我简单地介绍下新版本的一些基本情况。 新特性 1) 统一的文件管理 将所有你关心的文件都统一存放在同一个地方&#xff…

Linux 4.7内核syncookie的性能

虽然现在的内核都已经是4.11版本了&#xff0c;但本文依旧基于较老的内核版本旧事重提&#xff0c;就4.7版本的一个针对syncookie的一个优化书写一段吹捧与嘲讽。 自从4.4版本的Lockless TCP listener以来&#xff0c;针对TCP在大并发连接处理这块一直都没有更大的突破&#x…

RNA-seq 详细教程:注释(15)

学习内容 了解可用的基因组注释数据库和存储信息的不同类型比较和对比可用于基因组注释数据库的工具应用各种 R 包检索基因组注释基因组注释 对二代测序结果的分析需要将基因、转录本、蛋白质等与功能或调控信息相关联。为了对基因列表进行功能分析&#xff0c;我们通常需要获得…

哈希冲突概率计算及python仿真

目录 1. 前言 2. 生日问题 3. 哈希冲突问题 4. 简易python仿真 5. 从另一个角度看哈希冲突概率 1. 前言 Hash函数不是计算理论的中基本概念&#xff0c;计算理论中只有单向函数的说法。所谓的单向函数&#xff0c;是一个复杂的定义&#xff0c;严格的定义要参考理论或者密…

老板,明年我用Seata搞定分布式事务管理的规范化建设 | 中篇

辞旧迎新&#xff0c;22年要结束了&#xff0c;明年做什么想好了嘛&#xff1f;要不要用 Seata 搞定公司分布式事务管理的规范化建设&#xff1f; 欢迎关注微信公众号「架构染色」交流和学习 一、背景 在上一篇《明年用Seata搞定分布式事务管理的规范化建设 | 上篇》 中介绍了…

低成本、高效率!华为云桌面助力企业数字化转型

在云计算飞速发展的今天&#xff0c;传统办公设备体积大、能耗高、维护难、更新换代快等问题日益凸显&#xff0c;而基于云计算平台的虚拟办公系统逐渐被业界接受并得到广泛应用。其中&#xff0c;华为云桌面Workspace既满足了企业移动办公、远程办公、安全办公等要求&#xff…

恒业微晶冲刺创业板上市:计划募资8亿元,戴联平为实控人

12月20日&#xff0c;上海恒业微晶材料科技股份有限公司&#xff08;下称“恒业微晶”&#xff09;在深圳证券交易所创业板递交招股书。本次冲刺创业板上市&#xff0c;恒业微晶计划募资8亿元&#xff0c;将用于恒业新型分子筛项目。 据天眼查信息显示&#xff0c;恒业微晶成立…

Servlet中Cookie和Session技术

一、状态管理1.1 现有问题HTTP协议是无状态的&#xff0c;不能保存每次提交的信息如果用户发来一个新的请求&#xff0c;服务器无法知道它是否与上次的请求有联系对于那些需要多次提交数据才能完成的Web操作&#xff0c;比如登录来说&#xff0c;就有问题了。1.2 概念将浏览器与…

牛津大学最新 | LUMix:Mixup改进版,几行代码轻松涨点!

点击下方卡片&#xff0c;关注“自动驾驶之心”公众号ADAS巨卷干货&#xff0c;即可获取点击进入→自动驾驶之心【目标检测】技术交流群后台回复【LUMix】获取论文&#xff01;&#xff01;&#xff01;摘要当使用噪声样本和正则化技术进行训练时&#xff0c;现代深度网络可以更…

云端数据“上榜”了!

背景介绍随着全球特别是北美地区VNF网络应用渐渐地往云上迁移&#xff0c;云环境中更高的性能需求变得越来越迫切。作为一流数据处理中心部门&#xff0c;随着大势所趋&#xff0c;不仅仅专研于裸机的性能数据&#xff0c;也开始关注Intel平台在不同云环境中的性能表现。在DPDK…

外汇天眼:日本央行突然上调收益率目标上限,日元10分钟内涨超2%

12 月 20 日&#xff0c;日本央行公布利率决议&#xff0c;并在货币政策会议上宣布堪称“黑天鹅事件”的重大政策转变。日本央行意外地调整了收益率曲线控制计划&#xff0c;宣布将收益率目标上限从 0.25% 上调至 0.5% 左右&#xff0c;同时又将 1 至 3 月日本国债购买规模提高…

Java当中多态的理解

1. 什么是多态 同一操作&#xff0c;作用于不同的对象&#xff0c;可以有不同的解释&#xff0c;产生不同的执行结果&#xff0c;这就是多态性。 对应到 Java 里就是针对同一个类型的对象&#xff0c;执行同一个方法&#xff0c;会表现出不同的行为。 简单点说: 就是用基类…

<Linux进程信号>——《Linux》

本节重点&#xff1a; 1. 掌握Linux信号的基本概念 2. 掌握信号产生的一般方式 3. 理解信号递达和阻塞的概念&#xff0c;原理。 4. 掌握信号捕捉的一般方式。 5. 重新了解可重入函数的概念。 6. 了解竞态条件的情景和处理方式 7. 了解SIGCHLD信号&#xff0c; 重新编写信号处理…

面试官:Docker 有几种网络模式?5 年工作经验都表示答不上来。。

docker容器网络 Docker在安装后自动提供3种网络&#xff0c;可以使用docker network ls命令查看 [rootlocalhost ~]# docker network ls NETWORK ID NAME DRIVER SCOPE cd97bb997b84 bridge bridge l…

第1章 概述

第一章 概述 考试范围&#xff1a; 1.1-1.10 考试内容&#xff1a; 章节后的Review Terms&#xff08;名词基本都在课文中&#xff09; 考试题型&#xff1a; 综合题 Review Terms Database-management system (DBMS) &#xff1a;A collection of interrelated data and a …