elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪

文章目录

    • 测试的基本架构
      • 技术栈
      • 测试框架逻辑拓扑
    • 微服务应用的搭建
      • 搭建Eureka注册服务器
      • Eureka服务器配置
      • 创建provider service
        • 创建 rest endpoint
      • 创建consumer
        • Feign配置
    • 使用apm java agent进行探测
    • 测试
      • 启动流程
      • 测试分布式追踪
    • 总结

微服务架构现在已经是各个互联网应用的标配,并且开始作为一个技术框架的基本要求,慢慢扩展到其他行业,比如金融,保险等面向大量用户的J2EE应用中。使用微服务架构,就意味着我们需要将原本高度耦合在同一个应用中的多个功能拆分出来,解耦为多个微服务应用,并且,每个微服务应用可单独编译,集成,部署。因此,每个微服务可能在生态系统中具有多个实例,并且数量和实例地址都是动态变化的。这里,就对APM的分布式追踪提出了具体的要求,即单笔业务,在微服务架构下,会经过多个步骤,是多个原子服务聚合后的结果,APM需要对整个链路上的各个服务的性能进行监控,并归纳到单个业务当中。下图,是一张互联网通用的架构图,其中每个环节都是微服务的核心部分,由此可见,分布式追踪已经成为APM必不可少的功能。
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第1张图片
在上一篇文章 elastic APM 深入测试 一 (无嵌套调用的分布式微服务监控) 中,我们已经初探了使用 传统的服务器端负载平衡的分布式模型的分布式追踪。在文章 Spring cloud 之ribbon与eureka – 发生在客户端的负载均衡中,我也解释了传统的服务器端负载平衡所具有的问题,在这篇博文中,我将选择spring cloud,作为基本的微服务架构来对elastic APM的distributed trace功能进行深入的测试。

测试的基本架构

技术栈

我们选用如下技术栈:

  • Java,IntelliJ,Maven作为开发环境
  • Spring-boot和Cloud作为应用程序框架
  • Eureka作为服务注册服务器
  • 使用OpenFeign作为服务发现和远程调用的客户端

测试框架逻辑拓扑

以下,是spring cloud使用Eureka作为注册中心的基本通信图
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第2张图片
我们将简单搭建类似的应用拓扑:

  • 创建一个Eureka server作为服务的注册和发现中心
  • 创建多个provider实例,注册到服务中心,并行的提供服务
  • 创建一个consumer应用,通过服务中心提供的信息,远程调用provider实例提供的服务

微服务应用的搭建

搭建Eureka注册服务器

访问 https://start.spring.io/,创建一个Spring引导项目,将Eureka Server作为依赖项:
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第3张图片

Eureka服务器配置

在spring boot应用程序类中添加@EnableEurekaServer注解,并在应用程序属性文件中添加以下配置。

package com.example.ribboneurekaserver;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
 
@SpringBootApplication
@EnableEurekaServer
public class RibbonEurekaServerApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(RibbonEurekaServerApplication.class, args);
    }
}

application.properties:

spring.application.name= ${springboot.app.name:eureka-serviceregistry}
server.port = ${server-port:8761}
eureka.instance.hostname= ${springboot.app.name:eureka-serviceregistry}
eureka.client.registerWithEureka= false
eureka.client.fetchRegistry= false
eureka.client.serviceUrl.defaultZone: http://${registry.host:localhost}:${server.port}/eureka/

创建provider service

和之前一样,在spring initializer上创建一个项目,包含spring-boot-web和eureka discovery依赖项。web用于提供restful API, eureka discovery用于往eureka server上注册服务。

创建 rest endpoint

实现一个 Rest Controller,提供rest endpoint,用于对数据库的操作:

package com.example.sqldemo.controller;

import com.example.sqldemo.entity.User;
import com.example.sqldemo.service.UserService;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@Controller
@EnableEurekaClient
@RequestMapping( "/user" )
public class UserController
{
    @Resource
    private UserService userService;

    @RequestMapping( "/showUser" )
    @ResponseBody
    public User toIndex( HttpServletRequest request, Model model )
    {
        try
        {
            int userId = Integer.parseInt( request.getParameter( "id" ) );
            User user = this.userService.getUserById( userId );
            return user;
        }
        catch( Exception e )
        {
            return this.userService.getUserById( 1 );
        }

    }

    @RequestMapping( "/addUser" )
    @ResponseBody
    public void addUser()
    {
        List<User> users = createRandomUsers();
        for( User user : users )
        {
            this.userService.addUser( user );
        }
    }

    @RequestMapping( "/addUsers" )
    @ResponseBody
    public void addUsers()
    {
        this.userService.addUsers( createRandomUsers() );
    }

    private List<User> createRandomUsers(){
        List<User> records = new ArrayList<>( );

        for( int i = 0; i < 100; i++ )
        {
            Random random=new Random();
            int length = 3 + random.nextInt( 3 );
            String randomName = getRandomString( length );
            String randomPassword = getRandomString( length );
            User user = new User();
            user.setUserName( randomName );
            user.setPassword( randomPassword );
            user.setAge( 5 + random.nextInt(50) );
            records.add( user );
        }
        return records;
    }
    private String getRandomString(int length){
        String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random=new Random();
        StringBuffer sb=new StringBuffer();
        for(int i=0;i<length;i++){
            int number=random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }
}

在以上controller中,提供了三个接口:

  • /user/showUser,用于读取数据库的用户信息
  • /user/addUser,往数据库中添加100个随机用户
  • /user/addUsers,以批处理的方式往数据库中添加100个随机用户
  • 同时有两个私有函数用于产生随机用户信息

设置application.properties:

spring.application.name=server
server.port = 9090
 
eureka.client.serviceUrl.defaultZone= http://${registry.host:localhost}:${registry.port:8761}/eureka/
eureka.client.healthcheck.enabled= true
eureka.instance.leaseRenewalIntervalInSeconds= 1
eureka.instance.leaseExpirationDurationInSeconds= 2

spring.datasource.url=jdbc:mysql://localhost:3306/lex
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
mybatis.type-aliases-package=com.example.sqldemo
mybatis.mapper-locations=classpath:mapper/*.xml

创建consumer

仍然在spring initializer上创建一个项目,这次,需要依赖Feign和Eureka discovery。Eureka discovery用户在服务中心获取提供服务的实例信息,Feign用于将远程调用本地化。

Feign配置

在应用程序类中,添加两个注释@EnableFeignClients@EnableDiscoveryClient以启用feign和Eureka客户端以进行服务注册与获取。

package com.example.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

//启用服务注册与发现
@EnableDiscoveryClient
//启用feign进行远程调用
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {

	public static void main(String[] args) {
		try{
			SpringApplication.run(ConsumerApplication.class, args);
		}catch( Exception e )
		{
			System.out.print( e.getMessage() );
		}
	}

}

创建一个接口,添加@FeignClient,并指定需要在Eureka服务中心中获取的服务名。Feign会自动通过discovery client,在Eureka服务中心获取提供该服务的所有实例的信息,并根据round robin算法,自动在客户端负载均衡地调用远程服务:

package com.example.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Created by lij021 on 2019/2/14.
 */
@FeignClient("dservice")
public interface UserRemote{
    //restful api 调用
    @GetMapping("user/showUser")
    public String getUser();
}

创建一个controller:

package com.example.consumer.controller;

import com.example.consumer.service.UserRemote;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

@Controller
@RequestMapping( "/user" )
public class ConsumerController
{

    @Autowired
    UserRemote userRemote;



    @RequestMapping( "/get" )
    @ResponseBody
    public String getUserWithEureka()
    {
        return userRemote.getUser();
    }

    @RequestMapping( "/get2" )
    @ResponseBody
    public String indexWithUrl()
    {
        RestTemplate restTemplate = new RestTemplate();
        String results = restTemplate.getForObject("http://localhost:9090/user/showUser", String.class);
        return results;
    }
}

这里,我提供了两个方法,一个是通过userRemote发现服务,并远程调用。一个是以硬编码的方式,调用远程rest服务。

application.properties:

spring.application.name=spring-cloud-consumer
#配置端口号
server.port=9003
#服务注册中心的配置内容,指定服务注册中心的Url
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

使用apm java agent进行探测

这次只针对java应用,java agent是通过-javaagent的选项,在java字节码上增加的代理功能,对代码无侵入。
在maven仓库上下载最新的java agent,我在写这篇文章时,最新的agent版本是v1.4.0。强烈建议大家使用最新的版本,因为在v1.4.0版本上,java的apm agent才开始支持OkHttp和HttpUrlConnection,而feign正是依赖于此,如果是用v1.3.0版本,会出现无法探测到远程调用的问题(此问题会在测试部分有解释)。
下载agent后,通过以下命令将java agent attach到被监测应用的JVM上:

java -javaagent:/path/to/elastic-apm-agent-.jar \
     -Delastic.apm.service_name=my-application \
     -Delastic.apm.server_url=http://localhost:8200 \
     -Delastic.apm.secret_token= \
     -Delastic.apm.application_packages=org.example \
     -jar my-application.jar

这里需注意:

  • elastic.apm.service_name的值不能相同,该属性用来区分实例。每个应用实例必须指定一个不重复的,能表明应用的名字。
  • 可在java agent所在的目录创建一个elasticapm.properties文件,将common值放到文件中进行统一管理,比如:
    • server_urls=http://localhost:8200
    • secret_token=nhkGtRqvIGmRVZEjCs
  • 其他比较重要的动态属性是elastic.apm.trace_methods,该属性用于指定哪些类,哪些方法需要归类为transaction或span。格式如下:
    • org.example.* (omitting the method is possible since 1.4.0)
    • org.example.*#* (before 1.4.0, you need to specify a method matcher)
    • org.example.MyClass#myMethod
    • org.example.MyClass#myMethod()
    • org.example.MyClass#myMethod(java.lang.String)
    • org.example.MyClass#myMe*od(java.lang.String, int)
    • private org.example.MyClass#myMe*od(java.lang.String, *)
    • * org.example.MyClas*#myMe*od(*.String, int[])
    • public org.example.services.*Service#*

测试

启动流程

我们按顺序启动以下程序

启动Eureka server
启动3个provider实例依次在端口9090,9091,9092
启动consumer实例

注意,三个provider需要有不同的service_name,分别设为microservice-provider-1microservice-provider-2microservice-provider-3。而consumer,我们将其service_name设为microservice-comsumer-1

测试分布式追踪

在consumer上访问三次/user/get, 即调用三次http://localhost:9003/user/get
因为是round robin算法,所以应该是轮询了后端三个provider,这时,APM的界面上可以看到所有的服务:
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第4张图片
点击microservice-comsumer-1,应该可以看到3个transaction:
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第5张图片
可以看到,三个transaction,分别调用了三个后端服务:
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第6张图片
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第7张图片
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第8张图片
点击后,可看到后端微服务的具体信息:
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第9张图片
以及具体的sql语句和堆栈信息:
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第10张图片

总结

在这个以http restful API为基础的spring cloud框架中,我们可以看到elastic APM能够做到将有因果关系的微服务调用,即由多个微服务共同完成的事务作为监控目标,将事务中所有的服务统一到一个调用链当中,并且为我们展示了每个服务的耗时,和具体的SQL语句和堆栈等额外信息。
但是,该功能还在持续进化,尚不能服务所有的场景。目前仅支持以HTTP为基础的网络框架,并且,一部分框架还是1.4.0之后才支持的。
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第11张图片
而对于异步框架,也是在1.4.0之后才支持。
elastic APM 深入测试 二 基于spring cloud微服务框架的分布式追踪_第12张图片
而对于dubbo这种在国内非常流行的微服务框架,由于它是用自有的RPC通信协议(当然,也可以配置为使用http协议),导致elastic APM无法自动对其进行监控,因此,限制elastic APM在微服务框架上的适用范围。

最后,再次提醒大家,一定要适用最新的java agent客户端,因为在v1.3.0上,尚不支持spring cloud,具体原因可见网址:https://discuss.elastic.co/t/apm-java-agent-do-not-support-microservice-framework-spring-cloud/168550

你可能感兴趣的:(Java,ELK,点火三周的Elastic,Stack专栏)