首页 > 其他分享 >pfinder实现原理揭秘

pfinder实现原理揭秘

时间:2024-06-03 15:23:08浏览次数:20  
标签:插件 字节 class pfinder public 原理 揭秘 加载

1.引言

在现代软件开发过程中,性能优化和故障排查是保证应用稳定运行的关键任务之一。Java作为一种广泛使用的编程语言,其生态中涌现出了许多优秀的监控和诊断工具,诸如:SkyWalking、Zipkin等,它们帮助开发者和运维人员深入了解应用的运行状态,快速定位和解决问题。在京东内部,则使用的是自研的pfinder。

本文旨在深入探讨pfinder的核心原理和架构设计,揭示它是如何实现应用全链路监控的。我们将从pfinder的基本概念和功能开始讲起,逐步深入到其具体实现机制。

1.pfinder概述

2.1.pfinder简介

PFinder (problem finder) 是UMP团队打造的新一代APM(应用性能追踪)系统,集调用链追踪、应用拓扑、多维监控于一身,无需修改代码,只需要在启动文件增加 2 行脚本,便可实现接入。接入后便会对应用提供可观测能力,目前支持京东主流的中间件,包括:jimdb,jmq,jsf,以及一些常用的开源组件:tomcat、http client,mysql,es等。

2.2.pfinder功能

PFinder 除了具备 ump 现有功能的基础上,增加了以下重磅功能:

多维监控: 支持按多个维度统计监控指标,按机房、按分组、按JSF别名、按调用方,各种维度随心组合查看

自动埋点: 自动对 SpringMVC,JSF,MySQL,JMQ 等常用中间件进行性能埋点,无需改动代码,接入即可观测

应用拓扑: 自动梳理服务的上下游和中间件的依赖拓扑

调用链追踪: 基于请求的跨服务调用追踪,助你快速分析性能瓶颈

自动故障分析: 通过AI算法自动分析调用拓扑上所有服务的监控数据,自动判断故障根因

流量录制回放: 通过录制线上流量,回放至待特定环境(测试、预发),对比回放与录制时产生的差异,帮助用户补全业务场景、完善测试用例

跨单元逃逸流量监控: 支持 JSF 跨单元流量、逃逸流量监控,单元化应用运行状态一目了然

2.3.APM类组件对比

  Zipkin Pinpoint SkyWalking CAT pfinder
贡献者 Twitter 韩国公司 华为 美团 京东
实现方式 拦截请求,发送 http/mq 数据到 zipkin 服务 字节码注入 字节码注入 代理埋点(拦截器、注解、过滤器) 字节码注入
接入方式 基于 linkerd/sleuth,引入配置即可 javaagent 字节码 javaagent 字节码 代码侵入 javaagent 字节码
agent 到 collector 传输协议 http、MQ thrift gRPC http/tcp JMTP
OpenTracing 支持   支持   支持
粒度 接口级 方法级 方法级 代码级 方法级
全局调用统计   支持 支持 支持 支持
traceid 查询 支持   支持   支持
告警   支持 支持 支持 支持
JVM 监控   支持 支持 支持 支持

更重要的一点是:pfinder对京东内部自研组件提供了支持,比如:jsf、jmq、jimdb

1.pfinder背后的秘密

既然pfinder是基于字节码增强实现的,那么讲到pfinder,字节码增强技术自然也是无法避开的话题。这里我将字节码增强技术分两点来说,也是我认为实现字节码增强需要解决的两个关键点:

1.字节码是为了机器设计的,而非人类,字节码可读性极差、修改门槛极高,那么我们如何修改字节码呢?

2.修改后的字节码如何注入运行时JVM中呢?

欲攻善其事,必先利其器,所以下面我们围绕着这两个问题进行展开,当然,对这方面知识已经有所掌握的同学可忽略。

3.1.字节码修改

字节码修改成熟的框架已经很多了,诸如:ASM、javassist、bytebuddy、bytekit,下面我们用这几个字节码修改框架实现一个相同的功能,来对比下这几个框架使用上的区别。现在我们通过字节码修改来实现下面的功能:

 


 

1.ASM实现
   @Override
        public void visitCode() {
            super.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("start");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                //方法在返回之前,打印"end"
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("end");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            mv.visitInsn(opcode);
        }
1.javassist实现
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("com.ggc.javassist.HelloWord");
        CtMethod m = cc.getDeclaredMethod("printHelloWord");
        m.insertBefore("{ System.out.println(\"start\"); }");
        m.insertAfter("{ System.out.println(\"end\"); }");
        Class c = cc.toClass();
        cc.writeFile("/Users/gonghanglin/workspace/workspace_me/bytecode_enhance/bytecode_enhance_javassist/target/classes/com/ggc/javassist");
        HelloWord h = (HelloWord)c.newInstance();
        h.printHelloWord();
1.bytebuddy实现
    // 使用ByteBuddy动态生成一个新的HelloWord类
        Class<?> dynamicType = new ByteBuddy()
                .subclass(HelloWord.class) // 指定要修改的类
                .method(ElementMatchers.named("printHelloWord")) // 指定要拦截的方法名
                .intercept(MethodDelegation.to(LoggingInterceptor.class)) // 指定拦截器
                .make()
                .load(HelloWord.class.getClassLoader()) // 加载生成的类
                .getLoaded();

        // 创建动态生成类的实例,并调用方法
        HelloWord dynamicService = (HelloWord) dynamicType.newInstance();
        dynamicService.printHelloWord();
public class LoggingInterceptor {
    @RuntimeType
    public static Object intercept(@AllArguments Object[] allArguments, @Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        // 打印start
        System.out.println("start");
        try {
            // 调用原方法
            Object result = callable.call();
            // 打印end
            System.out.println("end");
            return result;
        } catch (Exception e) {
            System.out.println("exception end");
            throw e;
        }
    }
}
1.bytekit实现
 // Parse the defined Interceptor class and related annotations
        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
        List<InterceptorProcessor> processors = interceptorClassParser.parse(HelloWorldInterceptor.class);
        // load bytecode
        ClassNode classNode = AsmUtils.loadClass(HelloWord.class);
        // Enhanced process of loaded bytecodes
        for (MethodNode methodNode : classNode.methods) {
            MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
            for (InterceptorProcessor interceptor : processors) {
                interceptor.process(methodProcessor);
            }
        }
public class HelloWorldInterceptor {
    @AtEnter(inline = true)
    public static void atEnter() {
        System.out.println("start");
    }

    @AtExit(inline = true)
    public static void atEit() {
        System.out.println("end");
    }
}
特性 ASM Javassist ByteBuddy ByteKit
性能 ASM的性能最高,因为它直接操作字节码,没有中间环节 劣于ASM 介于javassist和ASM之间 介于javassist和ASM之间
易用性 需精通字节码,学习成本高,不支持debug Java语法进行开发,但是采用的硬编码形式开发,不支持debug 比Javassist更高级,更符文Java开发习惯,可以对增强代码进行断点调试 比Javassist更高级,更符文Java开发习惯,可以对增强代码进行断点调试
功能 直接操作字节码,功能最为强大。 功能相对完备 功能相对完备 功能相对完备,对比ByteBuddy,ByteKit能防止重复增强

3.2.字节码注入

相信大家经常使用idea去debug我们写的代码,我们是否想过debug是如何实现的呢?暂时先卖个关子。

1.JVMTIAgent

JVM在设计之初就考虑到了对JVM运行时内存、线程等指标的监控和分析和代码debug功能的实现,基于这两点,早在JDK5之前,JVM规范就定义了JVMPI(JVM分析接口)和JVMDI(JVM调试接口),JDK5之后,这两个规范就合并成为了JVMTI(JVM工具接口)。JVMTI其实是一种JVM规范,每个JVM厂商都有不同的实现,另外,JVMTI接口需使用C语言开发,以动态链接的形式加载并运行。

JVMTI接口
接口 功能
Agent_OnLoad(JavaVM *vm, char *options, void *reserved); agent在启动时加载的情况下,也就是在vm参数里通过-agentlib来指定,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。
Agent_OnAttach(JavaVM* vm, char* options, void* reserved); agent是attach到目标进程上,然后给对应的目标进程发送load命令来加载agent,在加载过程中就会调用Agent_OnAttach函数。
Agent_OnUnload(JavaVM *vm); 在agent卸载的时候调用

其实idea的debug功能便是借助JVMTI实现的,具体说是利用了jre内置的jdwp agent来实现的。我们在idea中debug程序时,控制台命令如下:

 


 

这里agentlib参数就是用来跟要加载的agent的名字,比如这里的jdwp(不过这不是动态库的名字,而JVM是会做一些名称上的扩展,比如在MACOS下会去找libjdwp.dylib的动态库进行加载,也就是在名字的基础上加前缀lib,再加后缀.dylib)。

1.instrument

上面说到JVMTIAgent基于C语言开发,以动态链接的形式加载并运行,这对java开发者不太友好。在JDK5之后,JDK开始提供java.lang.instrument.Instrumentation接口,让开发者可以使用Java语言编写Agent。其实,instrument也是基于JVMTI实现的,在MACOS下instrument动态库名为libinstrument.dylib。

instrument主要方法
方法 功能
void addTransformer(ClassFileTransformer transformer) 添加一个字节码转换器,用来修改加载类的字节码
Class[] getAllLoadedClasses() 返回当前JVM中加载的所有的类的数组
Class[] getInitiatedClasses(ClassLoader loader) 返回指定的类加载器中的所有的类的数据
void redefineClasses(ClassDefinition... definitions) 用给定的类的字节码数组替换指定的类的字节码文件,也就是重新定义指定的类
void retransformClasses(Class<?>... classes) 指定一系列的Class对象,被指定的类都会重新变回去(去掉附加的字节码)
1.instrument和ByteBuddy实现javaagent打印方法耗时

1.agent包MANIFEST.MF配置(maven插件)

<archive>
   <manifestEntries>
       // 指定premain()的所在方法
       <Agent-CLass>com.ggc.agent.GhlAgent</Agent-CLass>
       <Premain-Class>com.ggc.agent.GhlAgent</Premain-Class>
       <Can-Redefine-Classes>true</Can-Redefine-Classes>
       <Can-Retransform-Classes>true</Can-Retransform-Classes>
   </manifestEntries>
</archive>

2.agen主类

public class GhlAgent {
    public static Logger log = LoggerFactory.getLogger(GhlAgent.class);

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        log.info("agentmain方法");
        boot(instrumentation);
    }
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        log.info("premain方法");
        boot(instrumentation);
    }
    private static void boot(Instrumentation instrumentation) {
        //创建一个代理增强对象
        new AgentBuilder.Default().type(ElementMatchers.nameStartsWith("com.jd.aviation.performance.service.impl"))//拦截指定的类
                .transform((builder, typeDescription, classLoader, javaModule) ->
                        builder.method(ElementMatchers.isMethod().and(ElementMatchers.isPublic())
                                ).intercept(MethodDelegation.to(TimingInterceptor.class))
                ).installOn(instrumentation);
    }
}

3.拦截器

public class TimingInterceptor {
    public static Logger log = LoggerFactory.getLogger(TimingInterceptor.class);
    @RuntimeType
    public static Object intercept(@SuperCall Callable<?> callable) throws Exception {
        long start = System.currentTimeMillis();
        try {
            // 原方法调用
            return callable.call();
        } finally {
            long end = System.currentTimeMillis();
            log.info("Method call took {} ms",(end - start));
        }
    }
}

4.效果

 


 

1.pfinder实现原理

4.1.pfinder应用架构

 


 

1.pfinder agent启动时首先加载META-INF/pfinder/service.addon和META-INF/pfinder/plugin.addon配置文件中的服务和插件。2.根据加载的插件做字节码增强。3.使用JMTP将服务和插件产生的数据(trace、指标等)进行上报。

4.2.pfinder插件增强代码解析

1.service加载

 


 

创建SimplePFinderServiceLoader实例,在profilerBootstrap.boot(serviceLoaders)方法中加载配置文件中的service。

 


 

使用创建的SimplePFinderServiceLoader实例加载service,并返回一个service工厂的迭代器。

 


 

真正的加载走的是AddonLoader中的load方法。service加载完成后,继续看bootService方法:

 


 

bootService中完成创建service实例、注册service、初始化service,service的加载至此就完成了。

1.plugin加载&字节码增强

在介绍插件加载前,我们先了解下插件的包含了哪些信息。

 


 

增强拦截器:这个类里面放了具体的增强逻辑

增强点类型:增强时根据不同类型走不同逻辑

增强类/方法匹配器:用于匹配需要增强的类/方法

InterceptPoint是个数组,增强点可以配置多个。

plugin的加载和字节码增强发生在初始化service过程中,具体地说发生在com.jd.pfinder.profiler.service.impl.PluginRegistrar这个service初始化的过程中了。

 protected boolean doInitialize(ProfilerContext profilerContext) {
     AgentEnvService agentEnvService = (AgentEnvService)profilerContext.getService(AgentEnvService.class);
     Instrumentation instrumentation = agentEnvService.instrumentation();
     if (instrumentation == null) {
       LOGGER.info("Instrumentation missing, PFinder PluginRegistrar enhance ignored!");
       return false;
     }
     this.pluginLoaders = profilerContext.getAllService(PluginLoader.class);
     this.enhanceHandler = new EnhancePluginHandler(profilerContext);
     ElementMatcher.Junction<TypeDescription> typeMatcherChain = null;
     for (PluginLoader pluginLoader : this.pluginLoaders) {
       pluginLoader.loadPlugins(profilerContext);

       for (ElementMatcher.Junction<TypeDescription> typeMatcher : (Iterable<ElementMatcher.Junction<TypeDescription>>)pluginLoader.typeMatchers()) {
         if (typeMatcherChain == null) {
           typeMatcherChain = typeMatcher; continue;
         }
         typeMatcherChain = typeMatcherChain.or((ElementMatcher)typeMatcher);
       }
     }
     if (typeMatcherChain == null) {
       LOGGER.warn("no any enhance-point. pfinder enhance will be ignore.");
       return false;
     }
     ConfigurationService configurationService = (ConfigurationService)profilerContext.getService(ConfigurationService.class);
     String enhanceExcludePolicy =(String)configurationService.get(ConfigKey.PLUGIN_ENHANCE_EXCLUDE);LoadedClassSummaryHandler loadedClassSummaryHandler =null;if(((Boolean)configurationService.get(ConfigKey.LOADED_CLASSES_SUMMARY_ENABLED,Boolean.valueOf(false))).booleanValue()){
       loadedClassSummaryHandler =newLoadedClassSummaryHandler.DefaultImpl(configurationService,((ScheduledService)profilerContext.getService(ScheduledService.class)).getDefault());}(newAgentBuilder.Default()).with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).with(AgentBuilder.RedefinitionStrategy.REDEFINITION).with(newAgentBuilder.RedefinitionStrategy.Listener(){publicvoidonBatch(int index,List<Class<?>> batch,List<Class<?>> types){}publicIterable<? extends List<Class<?>>>onError(int index,List<Class<?>> batch,Throwable throwable,List<Class<?>> types){returnCollections.emptyList();}publicvoidonComplete(int amount,List<Class<?>> types,Map<List<Class<?>>, Throwable> failures){for(Map.Entry<List<Class<?>>, Throwable> entry : failures.entrySet()){for(Class<?> aClass : entry.getKey()){PluginRegistrar.LOGGER.warn("Redefine class: {} failure! ignored!",newObject[]{ aClass.getName(), entry.getValue()});}}}}).ignore((ElementMatcher)ElementMatchers.nameStartsWith("org.groovy.").or((ElementMatcher)ElementMatchers.nameStartsWith("jdk.nashorn.")).or((ElementMatcher)ElementMatchers.nameStartsWith("javax.script.")).or((ElementMatcher)ElementMatchers.nameContains("javassist")).or((ElementMatcher)ElementMatchers.nameContains(".asm.")).or((ElementMatcher)ElementMatchers.nameContains("$EnhancerBySpringCGLIB$")).or((ElementMatcher)ElementMatchers.nameStartsWith("sun.reflect")).or((ElementMatcher)ElementMatchers.nameStartsWith("org.apache.jasper")).or((ElementMatcher)pfinderIgnoreMather()).or((ElementMatcher)Matchers.forPatternLine(enhanceExcludePolicy)).or((ElementMatcher)ElementMatchers.isSynthetic())).type((ElementMatcher)typeMatcherChain).transform(this).with(newListener(loadedClassSummaryHandler)).installOn(instrumentation);returntrue;}

第8行,先从上下文中取出注册的PluginLoader(插件加载器),第12行遍历插件加载器加载插件,插件加载逻辑其实和service一样,使用的都是AddonLoader中的load方法。插件加载完成之后被插件加载器持有,第14-19行则收集插件中增强类的匹配器,用于AgentBuilder的创建。AgentBuilder的创建标志着字节码增强的开始,具体的逻辑在transform的实例方法中。

 


 

transform方法中遍历插件,enhance方法中对各个插件做增强。

 


 

enhance方法中遍历各个插件的增强点数组走enhanceInterceptPoint方法做增强。

 


 

enhanceInterceptPoint方法中根据增强点类型做增强。

 


 

上图是以Advice方式增强实例方法,传递了interceptorFieldAppender和methodCacheFieldAppender两个参数,并使用AdviceMethodEnhanceInvoker访问并修改待增强的类和方法。AdviceMethodEnhanceInvoker中有onMethodEnter、onMethodExit两个方法,分别表示进入方法后和退出方法前。

 


 

 


 

AdviceMethodEnhanceInvoker中onMethodEnter、onMethodExit两个方法还会调用插件中配置interceptor对应的onMethodEnter、onMethodExit、onException方法,至此插件字节码增强就结束了。

1.我的思考

5.1.多线程traceId丢失问题

pfinder目前已经将traceId放到了MDC中,我们通过在日志配置文件中添加[%X{PFTID}]便能在日志中打印traceId。但是我们知道MDC使用的是ThreadLocal去保存的traceId,在跨线程时会出现线程丢失的情况。pfinder在这方面做了字节码增强,无论使用线程池还是@Async,都不会存在traceId丢失的问题。

 public class TracingRunnable
   implements PfinderWrappedRunnable
 {
   private final Runnable origin;
   private final TracingSnapshot<?> snapshot;
   private final Component component;
   private final String operationName;
   private final String interceptorName;
   private final InterceptorClassLoader interceptorClassLoader;

   public TracingRunnable(Runnable origin, TracingSnapshot<?> snapshot, Component component, String operationName, String interceptorName, InterceptorClassLoader interceptorClassLoader) {
     this.origin = origin;
     this.snapshot = snapshot;
     this.component = component;
     this.operationName = operationName;
     this.interceptorClassLoader = interceptorClassLoader;
     this.interceptorName = interceptorName;
   }
   public void run() {
     TracingContext tracingContext = ContextManager.tracingContext();
     if (tracingContext.isTracing() && tracingContext.traceId().equals(this.snapshot.getTraceId())) {
       this.origin.run();
       return;
     }LowLevelAroundTracingContext context =SpringAsyncTracingContext.create(this.operationName,this.interceptorName,this.snapshot,this.interceptorClassLoader,this.component);
     context.onMethodEnter();try{this.origin.run();}catch(RuntimeException ex){
       context.onException(ex);throw ex;}finally{
       context.onMethodExit();}}publicRunnablegetOrigin(){returnthis.origin;}publicStringtoString(){return"TracingRunnable{origin="+this.origin +", snapshot="+this.snapshot +", component="+this.component +", operationName='"+this.operationName +'\''+'}';}}

拿线程池执行Runnable任务来说,pfinder通过TracingRunnable包装我们的Runnable的实现,利用构造函数将主线程的traceId通过snapshot参数传给TracingRunnable,在run方法中将参数snapshot放到上下文中,最后从上下文中取出放到子线程的MDC中,从而实现traceId跨线程传递。

5.2.热部署

既然javaagent能做字节码增强,也能实现热部署,此外, pfinder客户端和服务端通过jmtp有命令的交互,可以通过服务端向agent发送命令来实现类搜索、反编译、热更新等功能,笔者基于这一想法粗略实现了一个在线热部署的功能,具体如下:

类搜索:

 


 

反编译:

 


 

热更新:

 


 

上述只是笔者做的一个简单的实现,还有很多不足的地方:

1.对于Spring XML、MyBatis XML的支持。

2.Instrumentation的局限性:由于jvm基于安全考虑,不允许改类结构,比如新增字段,新增方法和修改类的父类等。想要突破这种局限,就需要使用Dcevm(Java Hostspot的补丁)了。

欢迎有兴趣的同学一起学习交流。

标签:插件,字节,class,pfinder,public,原理,揭秘,加载
From: https://www.cnblogs.com/Jcloud/p/18228963

相关文章

  • BOSHIDA AC/DC电源模块的工作原理是什么?
    BOSHIDAAC/DC电源模块的工作原理是什么?AC/DC电源模块是将交流电转换为直流电的电子设备。它在电子设备中起着至关重要的作用,如电脑、手机、家用电器等设备都需要使用AC/DC电源模块来提供稳定的直流电源。 AC/DC电源模块的工作原理可以分为几个关键步骤:1.输入滤波:交流电经......
  • 计算机网络基础-VRRP原理与配置
    目录一、了解VRRP1、VRRP的基本概述2、VRRP的作用二、VRRP的基本原理1、VRRP的基本结构图2、设备类型(Master,Backup) 3、VRRP抢占功能3.1:抢占模式3.2、非抢占模式4、VRRP设备的优先级5、VRRP工作原理三、VRRP的基本配置3.1、配置主设备与备用设备​ 3.2、结果......
  • x264 参考帧管理原理:i_poc 变量
    POCH.264中的POC(PictureOrderCount)用于表示解码帧的显示顺序。当视频码流中存在B帧时,解码顺序和显示顺序可能不一致,因此需要根据POC来重新排列视频帧的显示顺序,以避免跳帧或画面不连贯的问题。具体来说,POC的作用包括:重排显示顺序:POC确保即使在存在B帧的情况下,视频帧......
  • 揭秘成功加盟招商背后的营销策略:如何让你的品牌脱颖而出?
    作为一名手工酸奶品牌的创始人,目前全国也复制了100多家门店,我来分享下我是如何做招商加盟,让品牌脱颖而出!一、成功加盟招商的典型营销策略分析1、线上线下渠道的有效整合:线上渠道:利用自媒体渠道、搜索引擎优化(SEO)、内容营销等方式,提高品牌曝光度和吸引力。还可以通过官方网站......
  • 前沿科技:揭秘未来十年的技术趋势
    前沿科技:揭秘未来十年的技术趋势在过去的几十年中,科技的进步以惊人的速度推进,彻底改变了我们的生活方式和社会结构。展望未来十年,几项关键技术将继续塑造我们的世界。从人工智能的深入发展到生物技术的突破,再到可持续能源的革新,这些前沿技术将引领我们走向一个更加智能和高......
  • 论文AI率太高怎么办?AI降重方法大揭秘
    如何有效降低AIGC论文的重复率,也就是我们说的aigc如何降重?AIGC疑似度过高确实是个比较愁人的问题。如果你用AI帮忙写了论文,就一定要在交稿之前做一下AIGC降重的检查。一般来说,如果论文的AIGC超过30%,很可能会被判定为AI代写,从而无法参加答辩,影响毕业。那么如何降低AIGC的疑似度......
  • 云计算【第一阶段(8)】vrrp的工作原理
    一、VRRP介绍VRRP(VirtualRouterRedundancyProtocol,虚拟路由器冗余协议)提供了局域网上的设备备份机制VRRP是一种容错协议,在提高可靠性的同时,简化了主机的配置。在具有多播或广播能力的局域网(如以太网)中,借助VRRP能在某台设备出现故障时仍然提供高可靠的缺省链路,有效避免一链......
  • 数据保护技巧揭秘:为导出文件添加防护密码的实用指南
    一、前言当涉及到敏感数据的导出和共享时,数据安全是至关重要的。在现代数字化时代,保护个人和机密信息免受未经授权的访问和窃取是每个组织和个人的首要任务之一。在这种背景下,葡萄城的纯前端表格控件SpreadJS提供的加密功能为用户提供了一种强大的工具,可以轻松地将导出的Excel......
  • Shell阶段10 awk工作原理, 内部变量, 正则/比较/条件/逻辑表达式, 判断语句, 循环语
    AWK什么是awkawk是一个编程语言主要作用:对文本和数据的处理awk处理数据的流程1.扫描文件内容,从上到下进行扫描,按照行进行处理2.寻找匹配到的内容,进行读取到特定的模式中,进行行处理3.行满足指定模式动作,则输出到屏幕上面,不满足丢弃4.接着读取下一行继续处理,接着循环,直......
  • 挂箱圈地震啦!CS2挂箱产业链全揭秘!
    CS2挂箱圈地震啦!挂箱产业链全揭秘!#挂箱子教学#cs挂箱子#csgo挂箱子挂箱圈地震啦!CS2挂箱产业链全揭秘!什么是CS2挂箱子?他们是怎么赚钱的?这节视频为大家揭秘。CS2挂箱子,首先你要拥有一个CS2的优先账号,然后使用这个账号在CS2的官方服务器中进行游玩,完成比赛的时候就......