pfinder实现原理揭秘

京东云开发者
• 阅读 268

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,下面我们用这几个字节码修改框架实现一个相同的功能,来对比下这几个框架使用上的区别。现在我们通过字节码修改来实现下面的功能:



pfinder实现原理揭秘



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程序时,控制台命令如下:



pfinder实现原理揭秘



这里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.效果



pfinder实现原理揭秘



1.pfinder实现原理

4.1.pfinder应用架构



pfinder实现原理揭秘



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

4.2.pfinder插件增强代码解析

1.service加载



pfinder实现原理揭秘



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



pfinder实现原理揭秘



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



pfinder实现原理揭秘



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



pfinder实现原理揭秘



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

1.plugin加载&字节码增强

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



pfinder实现原理揭秘



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

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

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

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 = new LoadedClassSummaryHandler.DefaultImpl(configurationService, ((ScheduledService)profilerContext.getService(ScheduledService.class)).getDefault());
     }

     (new AgentBuilder.Default())

       .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
       .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
       .with(new AgentBuilder.RedefinitionStrategy.Listener()
         {
           public void onBatch(int index, List<Class<?>> batch, List<Class<?>> types) {}
           public Iterable<? extends List<Class<?>>> onError(int index, List<Class<?>> batch, Throwable throwable, List<Class<?>> types) {
             return Collections.emptyList();
           }
           public void onComplete(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!", new Object[] { 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(new Listener(loadedClassSummaryHandler))
       .installOn(instrumentation);
     return true;
   }

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



pfinder实现原理揭秘



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



pfinder实现原理揭秘



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



pfinder实现原理揭秘



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



pfinder实现原理揭秘



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



pfinder实现原理揭秘





pfinder实现原理揭秘



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();
     }
   }
   public Runnable getOrigin() {
     return this.origin;
   }
   public String toString() {
     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发送命令来实现类搜索、反编译、热更新等功能,笔者基于这一想法粗略实现了一个在线热部署的功能,具体如下:

类搜索:



pfinder实现原理揭秘



反编译:



pfinder实现原理揭秘



热更新:



pfinder实现原理揭秘



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

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

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

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

点赞
收藏
评论区
推荐文章
Stella981 Stella981
3年前
SkyWalking链路监控(一):SkyWalking快速搭建
简介当分布式系统服务比较多,特别是微服务,出现故障就很难排查。所以需要借助APM系统进行排查(ApplicationPerformanceManagement,即应用性能管理),SkyWalking是APM系统的一种,类似的产品还有CAT、Zipkin、Pinpoint。SkyWalking和Pinpoint相比其他系统,做到了无侵入性
Wesley13 Wesley13
3年前
Java基础知识强化(用于自我巩固)以及审查
1\.Java和JDK的关系JDK(JavaDevelopmentKit)Java开发工具包,它包括:编译器、Java运行环境(JRE,JavaRuntimeEnvironment)、JVM(Java虚拟机)监控和诊断工具等Java则表示一种开发语言。2. Java程序是怎么执行的?
Stella981 Stella981
3年前
JVM 常用命令行工具
本文部分摘自《深入理解Java虚拟机第三版》基础故障处理工具Java开发人员肯定都知道JDK的bin目录下有许多小工具,这些小工具除了用于编译和运行Java程序外,打包、部署、签名、调试、监控、运维等各种场景都可能会见到它们的影子本文主要介绍的是用于监视虚拟机运行状态和进行故障处理的工具,根
API 小达人 API 小达人
1年前
实用干货丨Eolink Apikit 配置和告警规则的各种用法
API在运行过程中可能会遇到各种异常情况,如响应时间过长、调用频率过高、请求参数错误等,这些异常会对系统的稳定性和性能产生严重影响。因此,对API进行异常监控和告警是非常必要的。本文将介绍EolinkApikit中使用的告警规则,帮助开发者和运维人员更好地监控和管理API。
陈哥聊测试 陈哥聊测试
9个月前
提升系统管理:监控和可观察性在DevOps中的作用
在不断发展的DevOps世界中,深入了解系统行为、诊断问题和提高整体性能的能力是首要任务之一。监控和可观察性是促进这一过程的两个关键概念,为系统的健康和性能提供了宝贵的可见性。虽然这些术语经常可以互换使用,但它们代表着理解和管理复杂系统的不同方法。在本文中
爱学it学无止境 爱学it学无止境
4个月前
看动画,轻松学习23种C++设计模式完结无密
C设计模式深度解析:提升代码质量与可维护性的关键在C软件开发中,设计模式作为一种经过验证的软件开发方法,被广泛用于解决常见的设计问题,提高代码的可读性、可维护性和可扩展性。本文将深入探讨C中几种常用的设计模式,分析其原理、应用场景及实现方式,以
京东云开发者 京东云开发者
3个月前
企业业务前端监控实践
监控的背景和意义在现代前端开发中,接入监控系统是一个很重要的环节,它可以帮助开发者、产品、运营了解应用的性能表现,用户的实际体验以及潜在的错误和问题,从而进一步优化用户体验,帮助产品升级迭代。背景•应用复杂性增加:随着单页应用(SPA)和渐进式网页应用(P
云监控的发展历程与未来展望
云监控能够实时监测云端资源的可用性、性能和操作。通过收集和分析关键监控数据,可以快速发现云端系统和应用的性能瓶颈和潜在问题,并作出响应以确保其正常运行。因此,云监控是保证企业云端业务平稳运行的关键组成部分。
不是海碗 不是海碗
1年前
一口气整理三种不同二维码生成的Java 接入代码
引言二维码已经成为现代社会中广泛应用的一种工具,它们具有快速、可靠和高容量的信息传递能力。通过扫描二维码,用户可以轻松获取网址、产品信息、支付链接等各种信息。本文将介绍二维码生成器API作为一种工具,并探讨其功能和用法,包括普通二维码生成、带图片的艺术二维
子桓 子桓
1年前
中文支持m1 JProfiler 13 下载 JProfiler 13 安装教程
JProfiler13是一款用于Java应用程序性能分析和调优的强大工具。它提供了丰富的功能和工具,帮助开发者深入了解和优化Java应用程序的性能,并发现和解决潜在的性能问题。以下是JProfiler13的一些主要功能和特点:1.实时性能分析:JProfi