我们选用如下技术栈:
以下,是spring cloud使用Eureka作为注册中心的基本通信图
我们将简单搭建类似的应用拓扑:
访问 https://start.spring.io/,创建一个Spring引导项目,将Eureka Server作为依赖项:
在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/
和之前一样,在spring initializer上创建一个项目,包含spring-boot-web和eureka discovery依赖项。web用于提供restful API, eureka discovery用于往eureka server上注册服务。
实现一个 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
仍然在spring initializer上创建一个项目,这次,需要依赖Feign和Eureka discovery。Eureka discovery用户在服务中心获取提供服务的实例信息,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/
这次只针对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
的值不能相同,该属性用来区分实例。每个应用实例必须指定一个不重复的,能表明应用的名字。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#*
我们按顺序启动以下程序
注意,三个provider需要有不同的service_name,分别设为microservice-provider-1
,microservice-provider-2
,microservice-provider-3
。而consumer,我们将其service_name设为microservice-comsumer-1
在consumer上访问三次/user/get
, 即调用三次http://localhost:9003/user/get
。
因为是round robin算法,所以应该是轮询了后端三个provider,这时,APM的界面上可以看到所有的服务:
点击microservice-comsumer-1
,应该可以看到3个transaction:
可以看到,三个transaction,分别调用了三个后端服务:
点击后,可看到后端微服务的具体信息:
以及具体的sql语句和堆栈信息:
在这个以http restful API为基础的spring cloud框架中,我们可以看到elastic APM能够做到将有因果关系的微服务调用,即由多个微服务共同完成的事务作为监控目标,将事务中所有的服务统一到一个调用链当中,并且为我们展示了每个服务的耗时,和具体的SQL语句和堆栈等额外信息。
但是,该功能还在持续进化,尚不能服务所有的场景。目前仅支持以HTTP为基础的网络框架,并且,一部分框架还是1.4.0之后才支持的。
而对于异步框架,也是在1.4.0之后才支持。
而对于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