目录
在上一篇,详细分享了skywalking的搭建和使用,以及如何在springboot和dubbo服务中集成skywalking的详细流程。在微服务治理中,springcloud也是技术选型中的一个成熟的解决方案,而且相对dubbo来说,springcloud涉及到的微服务组件更多,调用链路可能更复杂,本文将详细介绍下如何在springcloud中集成skywalking。
本文springcloud微服务模块需要依赖的外部模块如下:
服务模块如下:
像mysql,redis的搭建相信很多同学都非常熟悉了,这里就不再赘述了,快速介绍下nacos的单机搭建流程,nacos下载地址:git下载地址
也可以直接在这里下载,nacos 1.4.2安装包?
1、解压安装包
tar -zxvf nacos-server-1.4.2.tar.gz
2、进入到bin目录使用脚本启动
sh startup.sh -m standalone
服务正常启动后,可以在浏览器访问nacos的ui界面:http://IP:8848/nacos
默认登录账号和密码:nacos/nacos?
最外层添加如下依赖
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<dubbo.version>3.1.5</dubbo.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<!-- mybatis-plus 版本 -->
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<druid.version>1.1.17</druid.version>
<mybatis-boot.version>2.2.2</mybatis-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.23</version>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.14.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
模块结构如下
创建一个数据库,并提前准备一张测试使用的表
CREATE TABLE `t_user` (
`id` varchar(32) NOT NULL,
`name` varchar(32) DEFAULT NULL,
`email` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
添加一条测试使用的数据
INSERT INTO `db-base`.`t_user`(`id`, `name`, `email`) VALUES ('001', 'jerry', 'jerry@163.com');
用户模块的服务将会使用nacos作为注册中心,所以需要添加nacos的依赖,同时,后面的服务调用需要走统一网关,因此gateway的依赖不可少
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--nacos服务发现客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-boot.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
server:
port: 9002
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: nacos的地址:8848
profiles:
active: dev # 环境标识
datasource:
url: jdbc:mysql://数据库连接地址:3306/db-base
driverClassName: com.mysql.jdbc.Driver
username: root
password: 123456
redis:
host: localhost
port: 6379
mybatis:
mapper-locations: classpath:mapper/*.xml
#目的是为了省略resultType里的代码量
type-aliases-package: com.congge.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
接口中将会使用redis作为缓存,需要自定义对缓存的序列化
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public FastJson2JsonRedisSerializer<Object> fastJson2JsonRedisSerializer() {
return new FastJson2JsonRedisSerializer<>(Object.class);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(fastJson2JsonRedisSerializer());
redisTemplate.setHashValueSerializer(fastJson2JsonRedisSerializer());
return redisTemplate;
}
}
自定义序列化类
import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private Class<T> clazz;
public FastJson2JsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t).getBytes(DEFAULT_CHARSET);
}
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
}
在这段代码中,添加了一个根据ID查询用户详情的方法,第一次未查到将会走数据库,然后放入缓存,以后相同的请求再过来的时候,如果缓存中有数据将会走缓存
import com.alibaba.fastjson2.JSON;
import com.congge.entity.User;
import com.congge.mapper.UserMapper;
import com.congge.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
@Override
public User getById(String id) {
log.info("[用户服务] 基于 id 查询用户信息:{}", id);
String key = "sw:users:" + id;
Object json = redisTemplate.opsForValue().get(key);
if (json != null) {
log.info("[用户服务] redis 中查询到用户信息:key={}, json={}", key, json);
return JSON.parseObject(json.toString(), User.class);
}
User user = userMapper.getById(id);
if (user != null) {
log.info("[用户服务] redis 中不存在,从数据库查到数据并缓存:{}", user);
redisTemplate.opsForValue().set(key, user, 2, TimeUnit.HOURS);
return user;
}
log.warn("[用户服务] 基于 id 查询用户失败,用户不存在:{}", id);
return null;
}
}
添加一个接口类
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//http:localhost:9002/user/getById?id=001
@GetMapping("/getById")
public User getById(@RequestParam String id) {
return userService.getById(id);
}
}
@MapperScan("com.congge.mapper")
@SpringBootApplication
public class UserApp {
public static void main(String[] args) {
SpringApplication.run(UserApp.class, args);
}
}
启动用户模块的服务,然后调用查询用户的接口,可以正常查到数据库的数据
同时nacos的服务列表中,也能看到当前注册上去的用户服务信息
到这里,用户服务就基本整合完成
模块结构如下
订单模块中,将会通过feign的方式调用user模块的服务,所以这里增加了feign的依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos服务发现客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
server:
port: 9003
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: nacos的地位:8848
order模块中对user模块的接口调用,通过下面的接口定义,然后注入到需要调用的类中即可
import com.congge.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user-service",path = "/user")
public interface UserFeignService {
@GetMapping(value = "/getById")
User getById(@RequestParam("id") String id);
}
@RequestMapping("/order")
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
//localhost:9003/order/getById?id=001
@GetMapping("/getById")
public Object getById(@RequestParam String id) {
return orderService.getById(id);
}
}
接口实现
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserFeignService userFeignService;
@Override
public Map getById(String id) {
log.info("[订单服务] 基于 id 查询订单详情:{}", id);
Map map = new HashMap();
Order order = new Order();
order.setOrderId("0002");
order.setProductId("0001");
order.setProductName("小米手机");
map.put("order",order);
User user = userFeignService.getById("001");
map.put("user",user);
return map;
}
}
@EnableFeignClients
@SpringBootApplication
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class, args);
}
}
启动order模块的服务,然后调用查询订单的接口,可以看到期望的返回结果
同时在nacos的服务列表中存在两个服务信息
上面分别完成了两个服务模块的搭建,测试,以及相互之间的调用,但是并没有通过网关,接下来将服务接口的调用通过网关接入
<dependencies>
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
网关模块的搭建,主要是配置文件中涉及的路由规则的配置,需要搞清楚规则配置中的各项含义,否则调用的时候容易出错
server:
port: 9001
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: nacos的地址:8848
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
routes:
- id: us_route
uri: lb://user-service
predicates:
- Path=/us/**
filters:
- StripPrefix=1
- id: order_route
uri: lb://order-service
predicates:
- Path=/os/**
filters:
- StripPrefix=1
# profiles:
# active: dev # 环境标识
@SpringBootApplication
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class, args);
}
}
启动网关模块的服务,然后在nacos中可以看到网关的服务信息也注册进来了
用户查询接口测试
如果通过网关调用,可以调用接口:localhost:9001/us/user/getById?id=001
订单查询接口测试
如果通过网关调用,可以调用接口:localhost:9001/os/order/getById?id=001
通过上面的步骤,我们完成了springcloud的微服务模块的搭建,和调用效果的测试,接下来,将微服务接入到SkyWalking中,看看SkyWalking是否能够追踪到微服务调用的完整链路信息
对gateway,user,order三个模块,在服务启动时分别添加如下启动参数
gateway模块
-javaagent:E:\code-self\skywalking-agent\skywalking-agent.jar -DSW_AGENT_NAME=service-gateway -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=IP服务地址:11800
user模块
-javaagent:E:\code-self\skywalking-agent\skywalking-agent.jar -DSW_AGENT_NAME=service-user -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=IP服务地址:11800
order模块
-javaagent:E:\code-self\skywalking-agent\skywalking-agent.jar -DSW_AGENT_NAME=service-order -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=IP服务地址:11800
先分别启动user和order模块,然后调用查询order的服务接口
调用成功后,等待监控数据上报到Skywalking,然后去Skywalking观察调用链路的信息
拓扑图展现
从拓扑图上可以看到该接口调用的完整链路信息
Trace的信息展现
调用链路的信息就更加的完整了,正好是获取订单详情接口的完整链路,包括最终获取用户走的是redis缓存
按照上面同样的方式,启动网关服务,然后通过网关调用获取订单详情的接口
接口调用成功,此时再去Skywalking监控界面检查调用的拓扑信息,奇怪的是,在调用链路中,并没有显示调用的起始点是网关,这是怎么回事呢?
默认情况下,oap服务并不识别gateway作为服务链路的入口,如果需要支持,可以在下载的Agent的包目录下,找到optional-plugins目录下的gateway的插件包,然后拷贝到plugins目录中
注意,拷贝的jar包版本要与你工程中的包版本对应起来,拷贝完成后,重新重启几个模块的服务
再次调用接口
调用成功后,从Skywalking的拓扑信息,以及Trace链路监控信息来看,此时网关就作为入口能够正常显示出来了
在生产环境中,随着部署的微服务增多,微服务中引用的中间件的增多,一个接口从开始到响应结果,中间的调用链路可能非常复杂,如果不借助外部的可视化工具进行协助排查,这将是一个非常耗时耗力的过程,所以在此情况下,Skywalking在全链路监控这一块提供了一个非常好的选择,本篇到此结束,感谢观看。