抽丝剥茧 分布式服务框架设计 实战落地篇
1、概述
前面我们已经设计了一款分布式服务框架 Cheese,文章通过一些图文的形式给大家描绘出了Cheese的骨架和脉络,相信大家脑海里肯定还有一张这样的图
今天我们就将上面的架构图落地成可运行的程序,打造出一款简洁优雅的分布式服务框架
2、项目环境
2.1、前置要求
1、在此之前需要准备好Zookeeper环境,关于Zookeeper环境的搭建大家可以参考之前的文章
Zookeeper 快速入门到实战
Zookeeper实战 集群环境部署
2、另外最好还需要具备Apache Curator 的基本使用的能力,同样的可以参考前面的文章
Zookeeper客户端工具 Apache Curator 最佳实践
2.2、相关的依赖
SpringBoot | 3.2.9 |
Zookeeper | 3.9.1 |
Apache Curator | 5.7.0 |
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.wcan</groupId>
<artifactId>cheese</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.7.0</version> <!-- 请检查最新版本 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
2.3、 搭建项目
我们新建一个项目 cheese 项目结构如下图所示
其中最外层我们设计3个接口,分别对应架构图中的服务发现、服务注册、远程调用这三个核心功能,接下来我们详细的看看 如何落地
3、ApplicationListener开发
我们先来开发项目中的全局监听器,他的功能就是在程序启动后找到需要对外暴露的服务然后交给RegistServer组件,这里我们使用Spring的ApplicationListener 去实现,我们将这部分代码放在event包下,相关代码实现如下:
@Component
public class ServerLister implements ApplicationListener<ApplicationReadyEvent> {
private final RegisterEvent registerEvent;
private final CheeseConfig cheeseConfig;
public ServerLister(RegisterEvent registerEvent,CheeseConfig cheeseConfig) {
this.registerEvent = registerEvent;
this.cheeseConfig = cheeseConfig;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
System.out.println("所有的 Controller 和它们的请求映射:");
Map<String, Object> serverMap = new HashMap<String, Object>();
Map<String, Object> controllers = event.getApplicationContext().getBeansWithAnnotation(Controller.class);
String serverPackage = cheeseConfig.getServerPackage();
if(null == serverPackage || "".equals(serverPackage))
return;
controllers.forEach((key, value) -> {
if (value.toString().contains(serverPackage)) {
serverMap.put(key.toString(), value.getClass().getName());
}
});
registerEvent.registerServer(serverMap);
}
}
我们实现ApplicationListener接口,然后在onApplicationEvent中实现对应的逻辑,需要注意的是我们还需要获取到本机的ip和端口,因此我们还需要一个Environment 对象,这里我们在新建一个RegisterEvent组件,目的是组合RegistServer组件,相关代码如下
@Component
public class RegisterEvent {
private Environment environment;
private RegistServer registerServer;
public RegisterEvent(Environment environment, RegistServer registerServer){
this.environment = environment;
this.registerServer = registerServer;
}
private String getServerAddress() {
String ip = "无法获取本机 IP 地址";
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
String port = environment.getProperty("server.port", "8080");
return ip + ":" + port;
}
public void registerServer(Map<String, Object> serverMap) {
String ip = getServerAddress();
for (Map.Entry<String, Object> entry : serverMap.entrySet()) {
Object value = entry.getValue();
String serverName = "/" + value.toString();
try {
registerServer.register(serverName, ip);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
我们通过Environment 对象可以获取到服务所在的主机以及端口,然后ServerLister将需要注册的服务传递过来后就可以一起交给RegisterServer 组件了。此时项目结构是这样的
4、服务注册与服务发现
4.1、服务注册功能开发
我们先在最外层的 定义 RegistServer 接口,该接口里先声明一个方法。
public interface RegistServer {
public void register(String serviceName, String serviceAddress) throws Exception;
}
接着我们在 server 包下新建一个实现类 HttpRegistServer 。这块代码的职责是负责注册事件。
@Service
public class HttpRegistServer implements RegistServer {
private CuratorExecute curatorExecute;
public HttpRegistServer(CuratorExecute curatorExecute) {
this.curatorExecute = curatorExecute;
}
@Override
public void register(String serviceName, String serviceAddress)throws Exception {
curatorExecute.setServerData(serviceName, serviceAddress);
}
}
4.2、服务发现功能开发
同样的 DiscoveryServer 组件 相关的代码如下
public interface DiscoveryServer {
public List<String> discovery() throws Exception;
public String getSingleServer(String serviceName) throws Exception;
}
@Service
public class HttpDiscoveryServer implements DiscoveryServer {
private CuratorExecute curatorExecute;
public HttpDiscoveryServer(CuratorExecute curatorExecute) {
this.curatorExecute = curatorExecute;
}
@Override
public List<String> discovery() throws Exception {
List<String> list =new ArrayList<String>();
String serverData = curatorExecute.getServerData();
list.add(serverData);
return list;
}
@Override
public String getSingleServer(String serviceName) throws Exception {
return curatorExecute.getServerData(serviceName);
}
}
服务发现组件中我们能定义了2个方法,分别是拉取服务列表和根据服务名拉取指定的服务。这部分完成后项目结构如下
5、CuratorExecute 组件开发
CuratorExecute 组件主要是和Zookeeper交互的,并且需要承载 HttpRegistServer 和HttpDiscoveryServer 实现服务注册和服务发现的基础能力 。我们在execute包下新建CuratorExecute类。相关代码如下
@Service
public class CuratorExecute {
private CheeseConfig cheeseConfig;
private CuratorFramework curatorFramework;
public CuratorExecute(CheeseConfig cheeseConfig) {
this.cheeseConfig = cheeseConfig;
RetryPolicy retryPolicy = new ExponentialBackoffRetry(cheeseConfig.getBaseSleepTimeMs(), cheeseConfig.getMaxRetries());
curatorFramework = CuratorFrameworkFactory.builder()
.connectString(cheeseConfig.getZkUrl())
.sessionTimeoutMs(cheeseConfig.getTimeout())
.retryPolicy(retryPolicy)
.build();
//开启连接
curatorFramework.start();
}
public CuratorFramework getCuratorFramework() {
return curatorFramework;
}
public String getServerData() throws Exception{
return getServerData(cheeseConfig.getNodePath());
}
public String getServerData(String path) throws Exception {
GetDataBuilder data = this.curatorFramework.getData();
if (!path.startsWith("/"))
path = "/" + path;
byte[] bytes = data.forPath(path);
return new String(bytes);
}
public void setServerData(String path, String data) throws Exception {
if (curatorFramework.checkExists().forPath(path) == null) {
curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
}
curatorFramework.setData().forPath(path, data.getBytes());
}
public void close() {
if (curatorFramework != null) {
curatorFramework.close();
}
}
}
6、RemoteServer开发
最后还需要一个能够实现远程调用的组件,也就是架构图中的 RemoteServer组件,
接口设计两个方法,一个用于根据服务名远程调用,另外一个是带参数的。关于返回值这里分别使用了String 和Map ,大家可以自己定义合适的返回类型。
public interface RemoteServer {
String execute(String serviceName) throws Exception;
Map<String, Object> execute(String serviceName, String params) throws Exception;
}
接着我们将它的实现类放在execute包下,相关代码如下
@Service
public class HttpRemoteExecute implements RemoteServer {
private DiscoveryServer discoveryServer;
private RestTemplate restTemplate;
public HttpRemoteExecute(DiscoveryServer discoveryServer,RestTemplate restTemplate){
this.discoveryServer = discoveryServer;
this.restTemplate = restTemplate;
}
@Override
public String execute(String params) throws Exception {
return null;
}
@Override
public Map<String, Object> execute(String serviceName, String params) throws Exception {
System.out.println("HttpRemoteExecute execute");
String[] serviceNames = serviceName.split("#");
String serverIp = discoveryServer.getSingleServer(serviceNames[0]);
String url = PROTOCOL_HTTP+serverIp+"/"+serviceNames[1];
// 发送 GET 请求并获取响应
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
// 输出响应状态码和响应体
System.out.println("Status Code: " + response.getStatusCode());
System.out.println("Response Body: " + response.getBody());
String body = response.getBody();
Map<String, Object> map = new HashMap<String, Object>();
map.put("data", body);
return map;
}
}
另外,我们还需要配置一个RestTemplate 组件到容器里
@Configuration
public class ServiceConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
7、关于项目配置的设计与实现
最后我们需要设计一个配置类,用来实现动态管理Zookeeper的配置以及cheese 运行时需要的一些配置。我们将这块功能 设计成 一个 CheeseConfig 类,他的功能是 动态的维护一些配置信息。比如 Zookeeper的链接串,超时时间以及默认节点的路径和尝试次数等等
@Service
public class CheeseConfig {
@Value("${zkUrl}")
private String zkUrl;
@Value("${timeout:3000}")
private int timeout;
@Value("${zkPort:2181}")
private String zkPort;
@Value("${nodePath}")
private String nodePath;
@Value("${nodeValue:tom and jerry}")
private String nodeValue;
@Value("${baseSleepTimeMs:1000}")
private int baseSleepTimeMs;
@Value("${maxRetries:3}")
private int maxRetries;
@Value("${serverPackage:''}")
private String serverPackage;
//getter setter .......
}
我们在common包下再新建一个常量类
public class Constant {
public static final String PROTOCOL_HTTP = "http://";
}
8、项目发布
cheese 的目前的代码就这么多了,最后我们来看看整个项目的结构吧
我们通过idea工具就可以完成打包的工作了
9、cheese 的使用
9.1、服务提供者 tom-store
完成了开发后我们可以来测试一下,我们先来新建一个项目 ,项目结构如下
项目依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.tom</groupId>
<artifactId>tom-store</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.wcan</groupId>
<artifactId>cheese</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
我们编写一个简单的Controller 模拟Tom 返回给Jerry 的消息
@RestController
public class CheeseController {
@RequestMapping("/getCheese")
public Map<String, Object> getCheese(){
Map<String, Object> map = new HashMap<String, Object>();
map.put("cheese", "A piece of cheese on the small table in the room");
map.put("msg","我正在约会 不要打扰我");
return map;
}
}
主启动类需要注意,我们要扫描cheese框架的类
@SpringBootApplication
@ComponentScan(basePackages = {"org.tom","org.wcan.cheese"})
public class TomStoreApplication {
public static void main(String[] args) {
SpringApplication.run(TomStoreApplication.class, args);
}
}
最后我们需要配置zk
spring.application.name=tom-store
server.port=9000
zkUrl=192.168.200.100:9002,192.168.200.100:9003,192.168.200.100:9004
zookeeperBasePath=/tom
nodePath=/config
timeout=5000
serverPackage=org.tom
9.2、服务提供者测试
我们启动 tom-store 后 查看Zookeeper的控制台
我们发现 tom-store 的 ip和地址已经注册上去了。
同样的我们访问 http://192.168.200.1:9000/getCheese
此致服务注册的功能已经初步的实现了,接下来我们试试 服务发现和服务远程调用的功能
9.3、服务消费者 jerry-store
新建一个项目,取名 jerry-store
controller 代码如下:
@RestController
public class CheeseController {
@Autowired
private CheeseService cheeseService;
@RequestMapping("/getCheese")
public Object getCheese() throws Exception {
Map<String, Object> cheese = cheeseService.getCheese();
return cheese.get("data");
}
}
service 代码如下
public interface CheeseService {
public Map<String, Object> getCheese() throws Exception;
}
@Service
public class CheeseServiceImpl implements CheeseService{
@Autowired
private RemoteServer remoteServer;
@Value("${serviceName}")
private String serviceName;
@Override
public Map<String, Object> getCheese() throws Exception {
Map<String, Object> objectMap = remoteServer.execute(serviceName, "");
return objectMap;
}
}
我们再这里直接使用cheese 提供的RemoteServer 组件进行远程调用
最后添加配置
spring.application.name=jerry-store
server.port=8000
zkUrl=zkUrl=192.168.200.100:9002,192.168.200.100:9003,192.168.200.100:9004
zookeeperBasePath=/jerry
nodePath=/config
timeout=5000
serverPackage=org.jerry
##配置接口编号
serviceName=org.tom.controller.CheeseController#getCheese
这里我们需要配置一个接口编号,因为Jerry需要知道 tom 那个接口是处理奶酪的接口,可以去看看上一篇文章 理论设计篇描述的场景。
9.4、服务发现和远程调用测试
启动 jerry-store ,浏览器访问 http://192.168.200.1:8000/getCheese
到这里Jerry 就知道了 房间里的小桌子上有一块奶酪了。
10、总结和展望
我们完成了 cheese 的设计和开发,Jerry 通过它找到了想要吃的奶酪,但是你肯定会存在一些疑问。比如 Cheese 每次都会先去Zookeeper 查一下Tom的位置在进行调用,这样Tom和Jerry 之间的沟通效率会很低,并且如果多个服务这么玩 zookeeper 就会有很大访问压力。关于这些问题后面我们会进行一步一步的优化。
cheese 是一个粗糙的开始,也是一个最好的开始,我们后面一起加油