当前位置: 首页 > news >正文

企业做网站推广产品需要多少钱什么是同ip网站

企业做网站推广产品需要多少钱,什么是同ip网站,一个服务器可以做两个网站吗,嘉兴信息发布终端多少钱一台背景 传统的大型单体系统随着业务体量的增大已经很难满足市场对技术的需求#xff0c;通过对将整块业务系统拆分为多个互联依赖的子系统并针对子系统进行独立优化#xff0c;能够有效提升整个系统的吞吐量。在进行系统拆分之后#xff0c;完整的业务事务逻辑所对应的功能会…背景 传统的大型单体系统随着业务体量的增大已经很难满足市场对技术的需求通过对将整块业务系统拆分为多个互联依赖的子系统并针对子系统进行独立优化能够有效提升整个系统的吞吐量。在进行系统拆分之后完整的业务事务逻辑所对应的功能会部署在多个子系统上此时用户的一次点击请求会触发若干子系统之间的相互功能调用如何分析一次用户请求所触发的多次跨系统的调用过程、如何定位存在响应问题的调用链路等等问题是链路追踪技术所要解决的问题。 举一个网络搜索的示例来说明这样一个链路监控系统需要解决的一些挑战。当用户在搜索引擎中输入一个关键词后一个前端服务可能会将这次查询分发给数百个查询服务每个查询服务在其自己的索引中进行搜索。该查询还可以被发送到许多其他子系统这些子系统可以处理敏感词汇、检查拼写、用户画像分析或寻找特定领域的结果包括图像、视频、新闻等。所有这些服务的结果有选择地组合在一起最终展示在搜索结果页面中我们将这个模型称为一次完整的搜索过程。 在这样一次搜索过程中总共可能需要数千台机器和许多不同的服务来处理一个通用搜索查询。此外在网络搜索场景中用户的体验和延迟紧密相关一次搜索延时可能是由于任何子系统的性能不佳造成的。开发人员仅考虑延迟可能知道整个系统存在问题但却无法猜测哪个服务有问题也无法猜测其行为不良的原因。首先开发人员可能无法准确知道正在使用哪些服务随时都可能加入新服务和修改部分服务以增加用户可见的功能并改进性能和安全性等其他方面其次开发人员不可能是庞大系统中每个内部微服务的专家每一个微服务可能有不同团队构建和维护另外服务和机器可以由许多不同的客户端同时共享因此性能问题可能是由于另一个应用的行为引起。 Dapper简介 在分布式链路追踪方面Google早在2010年针对其内部的分布式链路跟踪系统Dapper发表了相关论文对分布式链路跟踪技术进行了介绍强烈推荐阅读。其中提出了两个基本要求。第一拥有广泛的覆盖面。针对庞大的分布式系统其中每个服务都需要被监控系统覆盖即使是整个系统的一小部分没有被监控到该链路追踪系统也可能是不可靠的。第二提供持续的监控服务。对于链路监控系统需要7*24小时持续保障业务系统的健康运行保证任何时刻都可以及时发现系统出现的问题并且通常情况下很多问题是难以复现的。根据这两个基本要求分布式链路监控系统的有如下几个设计目标 应用级透明 链路监控组件应该以基础通用组件的方式提供给用户以提高稳定性应用开发者不需要关心它们。对于Java语言来说方法可以说是调用的最小单位想要实现对调用链的监控埋点势必对方法进行增强。Java中对方法增强的方式有很多比如直接硬编码、动态代理、字节码增强等等。应用级透明其实是一个比较相对的概念透明度越高意味着难度越大对于不同的场景可以采用不同的方式。 低开销 低开销是链路监控系统最重要的关注点分布式系统对于资源和性能的要求本身就很苛刻因此监控组件必须对原服务的影响足够小将对业务主链路的影响降到最低。链路监控组件对于资源的消耗主除了体现在增强方法的消耗上其次还有网络传输和数据存储的消耗因为对于链路监控系统来说想要监控一次请求势必会产生出请求本身外的额外数据并且在请求过程中这些额外的数据不仅会暂时保存在内存中在分布式场景中还会伴随着该请求从上游服务传输至下游服务这就要求产生的额外数据尽可能地少并且在伴随请求进行网络传输的时候只保留少量必要的数据。 扩展性和开放性 无论是何种软件系统可扩展性和开放性都是衡量其质量优劣的重要标准。对于链路监控系统这样的基础服务系统来说上游业务系统对于链路监控系统来说是透明的在一个规模较大的企业中一个基础服务系统往往会承载成千上万个上游业务系统。每个业务系统由不同的团队和开发人员负责虽然使用的框架和中间件在同一个企业中有大致的规范和要求但是在各方面还是存在差异的。因此作为一个基础设施链路监控系统需要具有非常好的可扩展性除了对企业中常用中间件和框架的支撑外还要能够方便开发人员针对特殊的业务场景进行定制化的开发。 数据模型 OpenTracing规范 Dapper将请求按照三个维度划分为Trace、Segment、Span三种模型该模型已经形成了OpenTracing规范。OpenTracing是为了描述分布式系统中事务的语义而与特定下游跟踪或监控系统的具体实现细节无关因此描述这些事务不应受到任何特定后端数据展示或者处理的影响。大的概念就不多介绍了重点看一下Trace、Segment、Span这三种模型到底是什么。 Trace 表示一整条调用链包括跨进程、跨线程的所有Segment的集合。 Segment 表示一个进程JVM或线程内的所有操作的集合即包含若干个Span。 Span 表示一个具体的操作。Span在不同的实现里可能有不同的划分方式这里介绍一个比较容易理解的定义方式 Entry Span入栈Span。Segment的入口一个Segment有且仅有一个Entry Span比如HTTP或者RPC的入口或者MQ消费端的入口等。Local Span通常用于记录一个本地方法的调用。Exit Span出栈Span。Segment的出口一个Segment可以有若干个Exit Span比如HTTP或者RPC的出口MQ生产端或者DB、Cache的调用等。 按照上面的模型定义一次用户请求的调用链路图如下所示 唯一id 每个请求有唯一的id还是很必要的那么在海量的请求下如何保证id的唯一性并且能够包含请求的信息Eagleeye的traceId设计如下 根据这个id我们可以知道这个请求在2022-10-18 10:10:40发出被11.15.148.83机器上进程号为14031的Nginx对应标识位e接收到。其中的四位原子递增数从0-9999目的是为了防止单机并发造成traceId碰撞。 关系描述 将请求划分为Trace、Segment、Span三个层次的模型后如何描述他们之间的关系 从【OpenTracing规范】一节的调用链路图中可以看出Trace、Segment可以作为整个调用链路中的逻辑结构而Span才是真正串联起整个链路的单元系统可以通过若干个Span串联起整个调用链路。 在Java中方法是以入栈、出栈的形式进行调用那么系统在记录Span的时候就可以通过模拟出栈、入栈的动作来记录Span的调用顺序不难发现最终一个链路中的所有Span呈现树形关系那么如何描述这棵Span树Eagleeye中的设计很巧妙EagleEye设计了RpcId来区别同一个调用链下多个网络调用的顺序和嵌套层次。 如下图所示 RpcId用0.X1.X2.X3.....Xi来表示根节点的RpcId固定从0开始id的位数.的数量表示了Span在这棵树中的层级Id最后一位表示了Span在这一层级中的顺序。那么给定同一个Trace中的所有RpcId便可以很容易还原出一个完成的调用链 - 0- 0.1- 0.1.1- 0.1.2- 0.1.2.1- 0.2- 0.2.1- 0.3- 0.3.1- 0.3.1.1- 0.3.2 跨进程传输 再进一步在整个调用链的收集过程中不可能将整个Trace信息随着请求携带到下个应用中为了将跨进程传输的trace信息减少到最小每个应用Segment中的数据一定是分段收集的这样在Eagleeye的实现下跨Segment的过程只需要携带traceId和rpcid两个简短的信息即可。在服务端收集数据时数据自然也是分段到达服务端的但由于种种原因分段数据可能存在乱序和丢失的情况 如上图所示收集到一个Trace的数据后通过rpcid即可还原出一棵调用树当出现某个Segment数据缺失时可以用第一个子节点替代。 数据埋点 如何进行方法增强埋点是分布式链路追系统的关键因素在Dapper提出的要求中可以看出方法增强同时要满足应用级透明和低开销这两个要求。之前我们提到应用级透明其实是一个比较相对的概念透明度越高意味着难度越大对于不同的场景可以采用不同的方式。本文我们介绍阿里的Eagleye和开源的SkyWalking来比较两种埋点方式的优劣。 编码 阿里Eagleeye的埋点方式是直接编码的方式通过中间件预留的扩展点实现。但是按照我们通常的理解来说编码对于Dapper提出的扩展性和开放性似乎并不友好那为什Eagleye么要采用这样的方式个人认为有以下几点 阿里有中间件的使用规范不是想用什么就用什么因此对于埋点的覆盖范围是有限的阿里有给力的中间件团队专门负责中间件的维护中间件的埋点对于上层应用来说也是应用级透明的对于埋点的覆盖是全面的阿里应用有接入Eagleye监控系统的要求因此对于可插拔的诉求并没有非常强烈。 从上面几点来说编码方式的埋点完全可以满足Eagleye的需要并且直接编码的方式在维护、性能消耗方面也是非常有优势的。 字节码增强 相比于EagleyeSkyWalking这样开源的分布式链路监控系统在开源环境下就没有这么好做了。开源环境下面临的问题其实和阿里集团内部的环境正好相反 开源环境下每个开发者使用的中间件可能都不一样想用什么就用什么因此对于埋点的覆盖范围几乎是无限的开源环境下各种中间件都由不同组织或个人进行维护甚至开发者还可以进行二次开发不可能说服他们在代码中加入链路监控的埋点开源环境下并不一定要接入链路监控体系大多数个人开发者由于资源有限或其他原因没有接入链路监控系统的需求。 从上面几点来说编码方式的埋点肯定是无法满足SkyWalking的需求的。针对这样的情况Skywalking采用如下的开发模式 Skywalking提供了核心的字节码增强能力和相关的扩展接口对于系统中使用到的中间件可以使用官方或社区提供的插件打包后植入应用进行埋点如果没有的话甚至可以自己开发插件实现埋点。Skywalking采用字节码增强的方式进行埋点下面简单介绍字节码增强的相关知识和Skywalking的相关实现。 对Java应用实现字节码增强的方式有Attach和Javaagent两种本文做一个简单的介绍。 Attach Attach是一种相对动态的方式在阿尔萨斯Arthas这样的诊断系统中广泛使用利用JVM提供的Attach API可以实现一个JVM对另一个运行中的JVM的通信。用一个具体的场景举例我们要实现Attach JVM对一个运行中JVM的监控。如下图所示 Attach JVM利用Attach API获取目标JVM的实例底层会通过socketFile建立两个JVM间的通信Attach JVM指定目标JVM需要挂载的agent.jar包挂载成功后会执行agent包中的agentmain方法此时就可以对目标JVM中类的字节码进行修改Attach JVM通过Socket向目标JVM发送命令目标JVM收到后会进行响应以达到监控的目的。 虽然Attach可以灵活地对正在运行中的JVM进行字节码修改但在修改时也会受到一些限制比如不能增减父类、不能增加接口、不能调整字段等。 Javaagent Javaagent大家应该相对熟悉他的启动方式是在启动命令中加入javaagent参数指定需要挂载的agent java-javaagent:/path/agent.jarkey1value1,key2value2-jarmyJar.jar Javaagent在IDE的Debug模式、链路监控系统等场景中广泛使用。它的核心是在目标JVM执行main方法前执行agent的premain方法以插入前置逻辑 目标JVM通过javaagent参数启动后找到指定的agent执行agent的premain方法agent中通过JVM暴露的接口添加一个Transformer顾名思义它可以Transform字节码目标JVM在类加载的时候会触发JVM内置的事件回调Transformer以实现字节码的增强。 和Attach方式相比Javaagent只能在main方法之前执行。但是在修改字节码时较为灵活甚至可以修改JDK的核心类库。 字节码增强类库 Java提供了很多字节码增强类库比如大家耳熟能详的cglib、Javassist原生的Jdk Proxy还有底层的ASM等。在2014年一款名为Byte Buddy的字节码增强类库横空出世并在2015年获得Dukes Choice award。Byte Buddy兼顾高性能、易用、功能强大3个方面下面是摘自其官网的一张常见字节码增强类库性能比较图单位: 纳秒 基线Byte BuddycglibJavassistJdk Proxy简单类创建0.003±0.001142.772±1.390515.174±26.753193.733±4.43070.712±0.645接口实现0.004±0.0011126.364±10.328960.527±11.7881070.766±59.8651060.766±12.231方法调用0.002±0.0010.002±0.0010.003±0.0010.011±0.0010.008±0.001类型扩展0.004±0.001885.983±7.9011632.730±52.737683.478±6.735-父类方法调用0.004±0.0010.004±0.0010.021±0.0010.025±0.001- 上图中的对比项我们可以大致分为两个方面生成快速代码方法调用、父类方法调用和快速生成代码简单类创建、接口实现、类型扩展我们理所应当要优先选择前者。从数据可以看出Byte Buddy在纳秒级的精度下在方法调用和父类方法调用上和基线基本没有差距而位于其后的是cglib。 Byte Buddy和cglib有较为出色的性能得益于它们底层都是基于ASM构建如果将ASM也加入对比那么它的性能一定是最高的。但是用过ASM的同学虽然不一定能感受到它的高性能但一定能感受到它噩梦般的开发体验 mv.visitFieldInsn(GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;); mv.visitLdcInsn(begin of sayhello().); mv.visitMethodInsn(INVOKEVIRTUAL, java/io/PrintStream, println, (Ljava/lang/String;)V, false); Skywalking案例分析 介绍了这么多下面结合Skywalking中使用Byte Buddy的案例和大家一起体验下字节码增强的开发过程其中只简单介绍相关主流程代码各种细节就不介绍了。Skywalking为开发者提供了简单易用的插件接口对于开发者来说不需要知道怎么增强方法的字节码只需要关心以下几点 要增强哪个类的哪个方法 Skywalking提供了ClassMatch支持各种类、方法的匹配方式。包括类名、前缀、正则、注解等方式的匹配除此之外还提供了与、或、非逻辑链接以支持用户通过各种方式精确定位到一个具体的方法。我们看一个插件中的代码 这段逻辑表示需要增强不带annotation1注解并且带有annotaion2注解或annotaion3注解的方法的字节码。ClassMatch通过Builder模式提供用户流式编程的方式最终Skywalking会将用户提供的一串ClassMatch构建出一个内部使用的类匹配逻辑。 需要添加/修改什么逻辑 知道了需要增强哪个类的哪个方法那下一步就是如何增强。Java中的方法可以分为静态方法、实例方法和构造方法三类方法Skywalking对于这三种方法的增强逻辑为用户提供了不同的扩展点 实例方法构造方法静态方法前置支持不支持支持后置支持支持支持异常时支持不支持支持参数重写支持不支持支持其他扩展让原Class实现EnhancedInstance接口并且添加一个名为_$EnhancedClassField_ws的字段可通过getSkyWalkingDynamicField和setSkyWalkingDynamicField方法进行访问。 以实例方法为例Skywalking提供了如下实例方法拦截器 public interface InstanceMethodsAroundInterceptor {// 方法执行前置扩展点void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class?[] argumentsTypes,MethodInterceptResult result) throws Throwable;// 方法执行后置扩展点Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class?[] argumentsTypes,Object ret) throws Throwable;// 方法抛出异常时扩展点void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,Class?[] argumentsTypes, Throwable t); } 开发者通过实现该接口即可对一个实例方法进行逻辑扩展字节码增强。方法参数列表中的第一个类型为EnhancedInstance的参数其实就是当前对象thisSkywalking中所有实例方法或构造方法被增强的类都会实现EnhancedInstance接口。 假设我们有一个Controller里面只有一个sayHello方法返回Hello经过Skywalking增强后反编译一下它被增强后的字节码文件 可以看到 Skywalking在其中插入了一个名为_$EnhancedClassField_ws的字段开发者在某些场合可以合理利用该字段存储一些信息。比如存储Spring MVC中Controller的跟路径或者Jedis、HttpClient链接中对端信息等。原来的syHello方法名被修改了但仍保存下来并且新生成了一个增强后的sayHello方法静态代码块里将经过字节码增强后的sayHello方法存入缓存字段。 增强的前置条件是什么 在某些时候并不是只要引入了对应插件就一定会对相关的代码进行字节码增强。比如我们想对Spring MVC的Controller进行埋点我们使用的是Spring 4.x版本但是插件却是 5.x 版本的如果直接对源码进行增强可能会因为版本的差别带来意料之外的问题。Skywalking提供了一种witness机制简单来说就是当我们的代码中存在指定的类或方式时当前插件才会进行字节码增强。比如Spring 4.x版本中需要witness这两个类 如果粒度不够还可以对方法进行witness。比如Elastic Search 6.x版本中witness了这个方法 意思就是SearchHits类中必须有名为getTotalHits、参数列表为空并且返回long的方法。 除了上面的扩展点外Skywalking还支持对jdk核心类库的字节码增强比如对Callable和Runnable进行增强已支持异步模式下的埋点透传。这就需要和BootstrapClassLoader打交道了Skywalking帮我们完成了这些复杂的逻辑。Skywalking Agent部分整体的模型如下图所示 左侧SPI部分是Skywalking暴露的插件规范接口开发者根据这些接口实现插件。右侧Core部分负责加载插件并且利用Byte Buddy提供的字节码增强逻辑对应用中指定类和方法的字节码进行增强。 源码分析 介绍了Skywalking的插件模型后下面从Javaagent的入口premain开始介绍下主要的流程 public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {// 1. 加载所有的Skywalking插件 // 插件实现了Skywalking接口规范包括需要增强哪个类、需要怎么增强这两个要素final PluginFinder new PluginFinder(new PluginBootstrap().loadPlugins());// 2. 构建ByteBuddy// ByteBuddy提供了流式的API来指定ByteBuddy类库的行为用于各种配置final ByteBuddy byteBuddy new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));// 3. 构建AgentBuilder实例需要忽略的类AgentBuilder agentBuilder new AgentBuilder.Default(byteBuddy).ignore(nameStartsWith(net.bytebuddy.).or(nameStartsWith(org.slf4j.)).or(nameStartsWith(org.groovy.)).or(nameContains(javassist)).or(nameContains(.asm.)).or(nameContains(.reflectasm.)).or(nameStartsWith(sun.reflect)).or(nameStartsWith(org.apache.skywalking.).and(not(nameStartsWith(org.apache.skywalking.apm.toolkit.))))// 忽略Java中的【Synthetic】// Synthetic指所有存在于字节码文件中但是不存在于源代码中的【构造】即JVM帮我们生产的东西// 比如内部类指向外部类实例的this$0字段外部类访问内部类的私有变量时JDK帮我们生产的方法等等// JDK11后引入了NBAC机制引入了新的嵌套类组织方式不再生成Synthetic方法.or(ElementMatchers.isSynthetic()));// 4. 处理JDK9的module特性解决跨模块类访问的问题// 5. 将AgentBuilder插桩在Instrumentation上agentBuilder// 根据插件的内容构建需要增强类的匹配器.type(pluginFinder.buildMatch())// Transformer就是字节码增强逻辑的主要入口.transform(new Transformer(pluginFinder))// Retransform模式保留被修改的字节码// Redifine模式覆盖被修改的字节码.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)// 一些监听器.with(new RedefinitionListener()).with(new Listener())// 将AgentBuilder插桩在JVM提供的Instrumentation上.installOn(instrumentation); } 上面的流程主要做了两件事 从指定的目录加载所有插件到内存中构建Byte Buddy核心的AgentBuilder插桩到JVM的Instrumentation API上包括需要增强哪些类以及核心的增强逻辑Transformer。 private static class Transformer implements AgentBuilder.Transformer {private PluginFinder pluginFinder;Transformer(PluginFinder pluginFinder) {this.pluginFinder pluginFinder;}/*** 这个方法在类加载的过程中会由JVM调用Byte Buddy做了封装* param builder 原始类的字节码构建器* param typeDescription 类描述信息* param classLoader 这个类的类加载器* param module jdk9中模块信息* return 修改后的类的字节码构建器*/Overridepublic DynamicType.Builder? transform(final DynamicType.Builder? builder,final TypeDescription typeDescription,final ClassLoader classLoader,final JavaModule module) {LoadedLibraryCollector.registerURLClassLoader(classLoader);// 根据类信息找到针对这个类进行字节码增强的插件可能有多个ListAbstractClassEnhancePluginDefine pluginDefines pluginFinder.find(typeDescription);if (pluginDefines.size() 0) {DynamicType.Builder? newBuilder builder;EnhanceContext context new EnhanceContext();for (AbstractClassEnhancePluginDefine define : pluginDefines) {// 调用插件的define方法得到新的字节码DynamicType.Builder? possibleNewBuilder define.define(typeDescription, newBuilder, classLoader, context);if (possibleNewBuilder ! null) {newBuilder possibleNewBuilder;}}// 返回增强后的字节码给JVM完成字节码增强return newBuilder;}return builder;} } JVM在类加载的时候会触发JVM内置事件回调Transformer传入原始类的字节码、类加载器等信息从而实现对字节码的增强。其中的AbstractClassEnhancePluginDefine就是一个插件的抽象。 public abstract class AbstractClassEnhancePluginDefine {public DynamicType.Builder? define(TypeDescription typeDescription, DynamicType.Builder? builder,ClassLoader classLoader, EnhanceContext context) throws PluginException {// witness机制WitnessFinder finder WitnessFinder.INSTANCE;//通过类加载器找witness类没有就直接返回不进行字节码的改造String[] witnessClasses witnessClasses();if (witnessClasses ! null) {for (String witnessClass : witnessClasses) {if (!finder.exist(witnessClass, classLoader)) {return null;}}}//通过类加载器找witness方法没有就直接返回不进行字节码的改造ListWitnessMethod witnessMethods witnessMethods();if (!CollectionUtil.isEmpty(witnessMethods)) {for (WitnessMethod witnessMethod : witnessMethods) {if (!finder.exist(witnessMethod, classLoader)) {return null;}}}// enhance开始修改字节码DynamicType.Builder? newClassBuilder this.enhance(typeDescription, builder, classLoader, context);// 修改完成返回新的字节码context.initializationStageCompleted();return newClassBuilder;}protected DynamicType.Builder? enhance(TypeDescription typeDescription, DynamicType.Builder? newClassBuilder,ClassLoader classLoader, EnhanceContext context) throws PluginException {// 增强静态方法newClassBuilder this.enhanceClass(typeDescription, newClassBuilder, classLoader);// 增强实例方法 构造方法newClassBuilder this.enhanceInstance(typeDescription, newClassBuilder, classLoader, context);return newClassBuilder;} } 通过witness机制检测满足条件后对静态方法、实例方法和构造方法进行字节码增强。我们以实例方法和构造方法为例 public abstract class ClassEnhancePluginDefine extends AbstractClassEnhancePluginDefine {protected DynamicType.Builder? enhanceInstance(TypeDescription typeDescription,DynamicType.Builder? newClassBuilder, ClassLoader classLoader,EnhanceContext context) throws PluginException {// 获取插件定义的构造方法拦截点ConstructorInterceptPointConstructorInterceptPoint[] constructorInterceptPoints getConstructorsInterceptPoints();// 获取插件定义的实例方法拦截点InstanceMethodsInterceptPointInstanceMethodsInterceptPoint[] instanceMethodsInterceptPoints getInstanceMethodsInterceptPoints();String enhanceOriginClassName typeDescription.getTypeName();// 非空校验boolean existedConstructorInterceptPoint false;if (constructorInterceptPoints ! null constructorInterceptPoints.length 0) {existedConstructorInterceptPoint true;}boolean existedMethodsInterceptPoints false;if (instanceMethodsInterceptPoints ! null instanceMethodsInterceptPoints.length 0) {existedMethodsInterceptPoints true;}if (!existedConstructorInterceptPoint !existedMethodsInterceptPoints) {return newClassBuilder;}// 这里就是之前提到的让类实现EnhancedInstance接口并添加_$EnhancedClassField_ws字段if (!typeDescription.isAssignableTo(EnhancedInstance.class)) {if (!context.isObjectExtended()) {// Object类型、private volatie修饰符、提供方法进行访问newClassBuilder newClassBuilder.defineField(_$EnhancedClassField_ws, Object.class, ACC_PRIVATE | ACC_VOLATILE).implement(EnhancedInstance.class).intercept(FieldAccessor.ofField(_$EnhancedClassField_ws));context.extendObjectCompleted();}}// 构造方法增强if (existedConstructorInterceptPoint) {for (ConstructorInterceptPoint constructorInterceptPoint : constructorInterceptPoints) {// jdk核心类if (isBootstrapInstrumentation()) {newClassBuilder newClassBuilder.constructor(constructorInterceptPoint.getConstructorMatcher()).intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.withDefaultConfiguration().to(BootstrapInstrumentBoost.forInternalDelegateClass(constructorInterceptPoint// 非jdk核心类 .getConstructorInterceptor()))));} else {// 找到对应的构造方法并通过插件自定义的InstanceConstructorInterceptor进行增强newClassBuilder newClassBuilder.constructor(constructorInterceptPoint.getConstructorMatcher()).intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.withDefaultConfiguration().to(new ConstructorInter(constructorInterceptPoint.getConstructorInterceptor(), classLoader))));}}}// 实例方法增强if (existedMethodsInterceptPoints) {for (InstanceMethodsInterceptPoint instanceMethodsInterceptPoint : instanceMethodsInterceptPoints) {// 找到插件自定义的实例方法拦截器InstanceMethodsAroundInterceptorString interceptor instanceMethodsInterceptPoint.getMethodsInterceptor();// 这里在插件自定义的匹配条件上加了一个【不为静态方法】的条件ElementMatcher.JunctionMethodDescription junction not(isStatic()).and(instanceMethodsInterceptPoint.getMethodsMatcher());// 需要重写入参if (instanceMethodsInterceptPoint.isOverrideArgs()) {// jdk核心类if (isBootstrapInstrumentation()) {newClassBuilder newClassBuilder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(OverrideCallable.class)).to(BootstrapInstrumentBoost.forInternalDelegateClass(interceptor)));// 非jdk核心类 } else {newClassBuilder newClassBuilder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(OverrideCallable.class)).to(new InstMethodsInterWithOverrideArgs(interceptor, classLoader)));}// 不需要重写入参} else {// jdk核心类 if (isBootstrapInstrumentation()) {newClassBuilder newClassBuilder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().to(BootstrapInstrumentBoost.forInternalDelegateClass(interceptor)));// 非jdk核心类 } else {// 找到对应的实例方法并通过插件自定义的InstanceMethodsAroundInterceptor进行增强newClassBuilder newClassBuilder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().to(new InstMethodsInter(interceptor, classLoader)));}}}}return newClassBuilder;} } 根据是否要重写入参、是否是核心类走到不同的逻辑分支大致的增强逻辑大差不差就是根据用户自定义的插件找到需要增强的方法和增强逻辑利用Byte Buddy类库进行增强。 用户通过方法拦截器实现增强逻辑但是它是面向用户的并不能直接用来进行字节码增强Skywalking加了一个中间层来连接用户逻辑和Byte Buddy类库。上述代码中的XXXInter便是中间层比如针对实例方法的InstMethodsInter InstMethodsInter封装用户自定义的逻辑并且对接ByteBuddy的核心类库当执行到被字节码增强的方法时会执行InstMethodsInter的intercept方法可以和上面反编译被增强后类的字节码文件进行对比 public class InstMethodsInter {private static final ILog LOGGER LogManager.getLogger(InstMethodsInter.class);// 用户在插件中定义的实例方法拦截器private InstanceMethodsAroundInterceptor interceptor;public InstMethodsInter(String instanceMethodsAroundInterceptorClassName, ClassLoader classLoader) {try {// 加载用户在插件中定义的实例方法拦截器interceptor InterceptorInstanceLoader.load(instanceMethodsAroundInterceptorClassName, classLoader);} catch (Throwable t) {throw new PluginException(Cant create InstanceMethodsAroundInterceptor., t);}}/*** 当执行被增强方法时会执行该intercept方法** param obj 实例对象this* param allArguments 方法入参* param method 参数描述* param zuper 原方法调用的句柄* param method 被增强后的方法的引用 * return 方法返回值*/RuntimeTypepublic Object intercept(This Object obj, AllArguments Object[] allArguments, SuperCall Callable? zuper,Origin Method method) throws Throwable {EnhancedInstance targetObject (EnhancedInstance) obj;MethodInterceptResult result new MethodInterceptResult();try {// 拦截器前置逻辑interceptor.beforeMethod(targetObject, method, allArguments, method.getParameterTypes(), result);} catch (Throwable t) {LOGGER.error(t, class[{}] before method[{}] intercept failure, obj.getClass(), method.getName());}Object ret null;try {// 是否中断方法执行if (!result.isContinue()) {ret result._ret();} else {// 执行原方法ret zuper.call();// 为什么不能走method.invoke因为method已经是被增强后方法调用就死循环了// 可以回到之前的字节码文件查看原因看一下该intercept执行的时机}} catch (Throwable t) {try {// 拦截器异常时逻辑interceptor.handleMethodException(targetObject, method, allArguments, method.getParameterTypes(), t);} catch (Throwable t2) {LOGGER.error(t2, class[{}] handle method[{}] exception failure, obj.getClass(), method.getName());}throw t;} finally {try {// 拦截器后置逻辑ret interceptor.afterMethod(targetObject, method, allArguments, method.getParameterTypes(), ret);} catch (Throwable t) {LOGGER.error(t, class[{}] after method[{}] intercept failure, obj.getClass(), method.getName());}}return ret;} } 上述逻辑其实就是下图中红框中的逻辑 Byte Buddy提供了声明式方式通过几个注解就可以实现字节码增强逻辑。 数据收集 下一步就是将收集到的Trace数据发送到服务端。为了将对主链路的影响降到最小一般都采用先存本地、再异步采集的方式。Skywalking和Eagleeye的实现有所不同我们分别介绍 存储 Eagleeye 鹰眼采用并发环形队列存储Trace数据如下图所示 环形队列在很多日志框架的异步写入过程中很常见其中主要包括读指针take指向队列中的最后一条数据写指针put指向队列中下一个数据将存放的位置并且支持原子读、写数据。take和put指针朝一个时钟方向移动当生产数据的速度超过消费速度时会出现put指针“追上”take指针的情况套圈此时根据不同的策略可以丢弃即将写入的数据或将老数据覆盖。 Skywalking Skywalking在实现上有所区别采用分区的QueueBuffer存储Trace数据多个消费线程通过Driver平均分配到各个QueueBuffer上进行数据消费 QueueBuffer有两种实现除了基于JDK的阻塞队列外还有一种普通数组原子下标的方式。Skywalking对于这两种实现有不同的使用场景基于JDK阻塞队列的实现用在服务端而普通数组原子下标的方式用在Agent端因为后者更加轻量性能更高。对于后者这里介绍一下其中比较有趣的地方。 有趣的原子下标 普通的Oject数组是无法支持并发的但只要保证每个线程获取下标的过程是原子的即可保证数组的线程安全。这需要保证 多线程获取的下标是依次递增的从0开始到数组容量-1当某个线程获取的下标超过数组容量需要从0开始重新获取。 这其实并不难实现通过一个原子数和取模操作一行代码就能完成上面的两个功能。但我们看Skywalking是如何实现这个功能的 // 提供原子下标的类 public class AtomicRangeInteger {// JDK提供的原子数组private AtomicIntegerArray values;// 固定值15private static final int VALUE_OFFSET 15;// 数组开始下标固定为0private int startValue;// 数组最后一个元素的下标固定为数组的最大长度-1private int endValue;public AtomicRangeInteger(int startValue, int maxValue) {// 创建一个长度为31的原子数组this.values new AtomicIntegerArray(31);// 将第15位设置为初始值0this.values.set(VALUE_OFFSET, startValue);this.startValue startValue;this.endValue maxValue - 1;}// 核心方法获取数组的下一个下标public final int getAndIncrement() {int next;do {// 原子递增next this.values.incrementAndGet(VALUE_OFFSET);// 如果超过了数组范围CAS重制到0if (next endValue this.values.compareAndSet(VALUE_OFFSET, next, startValue)) {return endValue;}} while (next endValue);return next - 1;} } Skywalking用了一个长度固定为31的JDK原子数组的固定第15位进行相关原子操作JDK8中的原子数组利用Unsafe通过偏移量直接对数组中的元素进行内存操作那为什么要这么做呢我们先将其称为V1版本再来看看V2版本这是Skywalking早期版本使用的代码 public class AtomicRangeInteger {private AtomicInteger value;private int startValue;private int endValue;public AtomicRangeInteger(int startValue, int maxValue) {this.value new AtomicInteger(startValue);this.startValue startValue;this.endValue maxValue - 1;}public final int getAndIncrement() {int current;int next;do {// 获取当前下标current this.value.get();// 如果超过最大范围则从0开始next current this.endValue ? this.startValue : current 1;// CAS更新下标失败则循环重试} while (!this.value.compareAndSet(current, next));return current;} } 肉眼可见这段V2版本的代码逻辑不如V1版本因为在V2中获取当前值和CAS更新这两个步骤是分开的并不具备原子性因此并发冲突的可能性更高从而导致循环次数增加而使用JDK提供的incrementAndGet方法效率更高。再看下V3版本 public class AtomicRangeInteger extends Number implements Serializable {// 用原子整型替代V1版本的原子数组private AtomicInteger value;private int startValue;private int endValue;public AtomicRangeInteger(int startValue, int maxValue) {this.value new AtomicInteger(startValue);this.startValue startValue;this.endValue maxValue - 1;}public final int getAndIncrement() {int next;do {next this.value.incrementAndGet();if (next endValue this.value.compareAndSet(next, startValue)) {return endValue;}}while (next endValue);return next - 1;} } 这个版本唯一的区别就是使用AtomicInteger代替原来的AtomicIntegerArray的第15位。还有最后一个最简单的V4版本通过一个原子数和取模操作完成 public class AtomicRangeInteger {private AtomicLong value;private int mask;public AtomicRangeInteger(int startValue, int maxValue) {this.value new AtomicLong(startValue);this.mask maxValue - 1;}public final int getAndIncrement() {return (int)(value.incrementAndGet() % mask);} } 通过Benchmark压测数据来看看这几个版本的性能有什么差别固定128线程3轮预热、5轮正式每轮10s。 Skywalking官方数据数组大小100 版本得分描述V145832615.061 ± 2987464.163 ops/s原子数组第15位操作V213496720.554 ± 240134.803 ops/s老版本V339201251.850 ± 1005866.969 ops/s原子整数代替原子数组第15位 自己在mac上测试的数据数组大小100 版本得分描述V137368086.272 ± 2702764.084 ops/s原子数组第15位操作V28066661.954 ± 1165851.129 ops/s老版本V326124150.437 ± 684039.516 ops/s原子整数代替原子数组第15位V451063216.834 ± 7775168.064 ops/s原子数取模 自己在mac上测试的数据数组大小128 版本得分描述V129452469.035 ± 1853738.513 ops/s原子数组第15位操作V27998178.059 ± 148894.535 ops/s老版本V339011356.081 ± 3603737.004 ops/s原子整数代替原子数组第15位V461012525.493 ± 6054137.447 ops/s原子数取模 Skywalking官方显示通过原子数组的固定第15位操作的V1版本表现最好而在我自己本机环境测试中V3版本通过原子整数代替的方式和V1版本有高有低而原子数取模的性能是最高的。个人猜测Skywalking通过原子数组的固定第15位操作是为了进行缓存填充测试结果和环境有比较大的关系而不使用原子数取模的原因是原子数的大小会无限递增。 传输 最后一步就是数据的传输如下图所示 Skywalking提供了GRPC和Kafka两种数据传输方式而鹰眼则先将数据存入本地日志中再通过agent将数据采集到服务端。和Skywalking相比用户可以直接在机器上查看trace日志而Skywalking提供了日志插件以提供可插拔的本地trace存储功能。 从整体上来看Skywalking采取了埋点和中间件代码分离的方式在某种意义上实现了应用级透明但是在后期维护的过程中中间件版本的升级需要配合插件版本的升级在维护方面带来了一些问题。而Eagleeye编码方式的埋点由中间件团队维护对于上层的应用也是透明的更加适合阿里集团内部的环境。原文链接 本文为阿里云原创内容未经允许不得转载。
http://wiki.neutronadmin.com/news/146365/

相关文章:

  • 衡水网站建设费用c盘优化大师
  • 保健品网站可以做网站用花生壳怎么做网站的服务器
  • 安徽seo网站推广定制微信网站
  • 聊城定制型网站开发984网站建设项目
  • 网站设计深圳联系电话?wordpress显示当前时间
  • 开发公司融资专干笔试百度搜索优化平台
  • 扁平化设计网站建设商铺装修找谁
  • 网站建设主要用什么软件利用网站宣传 两学一做
  • 哪里做网站最便宜网站建设与管理复习知识点
  • 资金盘做网站WordPress底部固定导航
  • 寻花问柳专注做男人喜爱的网站手机定制软件
  • 外贸仿牌网站被封的后果外贸网站怎么做才好
  • 网站建设完成之后要索取哪些电商网站建设求职定位
  • 临沂手机网站建设长沙网页制作开发公司
  • 短视频网站如何做推广阿里云网站商城建设
  • 网站建设的一些背景图片wordpress重定向地址不匹配
  • 烟台网站建设公司地址鹰潭市网站建设
  • 如何看网站的浏览量推广普通话演讲稿
  • 江苏省电力建设质量监督中心站网站揭阳网站制作服务
  • 高级网站开发工程师考试题视频营销网站
  • 南宁公司网站建设果洛wap网站建设
  • 东海建设局网站科技九洲君
  • 做单页网站价格网站欢迎页源码
  • 西宁做网站seo安岳网站建设
  • 如何租用服务器做网站关于公司建网站
  • 威海房地产网站建设沙河网络推广
  • 网站建设的行业网站游戏入口
  • 公司名称 网站域名 关联app开发公司哪家好 上海
  • 在哪个网站做图片视频带音乐电商设计学什么
  • 能力天空的网站建设优劣势不需要登录的网页小游戏