详解ZooKeeper在微服务注册中心的应用
本文分享自华为云社区《SpringCloud ZooKeeper 详解,以及与Go、Rust等非Java服务的集成》,作者: 张俭。
ZooKeeper,是一个开源的分布式协调服务,不仅支持分布式选举、任务分配,还可以用于微服务的注册中心和配置中心。本文,我们将深入探讨ZooKeeper用做微服务注册中心的场景。
ZooKeeper中的服务注册路径
SpringCloud ZooKeeper遵循特定的路径结构进行服务注册
/services/${spring.application.name}/${serviceId}
示例:
/services/provider-service/d87a3891-1173-45a0-bdfa-a1b60c71ef4e
/services和/${spring.application.name}是ZooKeeper中的永久节点,/${serviceId}是临时节点,当服务下线时,ZooKeeper会自动删除该节点。
注:当微服务的最后一个实例下线时,SpringCloud ZooKeeper框架会删除/${spring.application.name}节点。
ZooKeeper中的服务注册数据
下面是一个典型的服务注册内容示例:
{
"name":"provider-service",
"id":"d87a3891-1173-45a0-bdfa-a1b60c71ef4e",
"address":"192.168.0.105",
"port":8080,
"sslPort":null,
"payload":{
"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
"id":"provider-service",
"name":"provider-service",
"metadata":{
"instance_status":"UP"
}
},
"registrationTimeUTC":1695401004882,
"serviceType":"DYNAMIC",
"uriSpec":{
"parts":[
{
"value":"scheme",
"variable":true
},
{
"value":"://",
"variable":false
},
{
"value":"address",
"variable":true
},
{
"value":":",
"variable":false
},
{
"value":"port",
"variable":true
}
]
}
}
其中,address、port和uriSpec是最核心的数据。uriSpec中的parts区分了哪些内容是可变的,哪些是固定的。
SpringCloud 服务使用OpenFeign互相调用
一旦两个微服务都注册到了ZooKeeper,那么它们就可以通过OpenFeign互相调用了。简单的示例如下
服务提供者
创建SpringBoot项目
创建SpringBoot项目,并添加spring-cloud-starter-zookeeper-discovery和spring-boot-starter-web依赖。
配置application.yaml
spring: application: name: provider-service cloud: zookeeper: connect-string: localhost:2181 server: port: 8082
注册到ZooKeeper
在启动类上添加@EnableDiscoveryClient注解。
创建一个简单的REST接口
@RestController
public class ProviderController {
@GetMapping("/hello")
public String hello() {
return "Hello from Provider Service!";
}
}
服务消费者
创建SpringBoot项目
创建SpringBoot项目,并添加spring-cloud-starter-zookeeper-discovery、spring-cloud-starter-openfeign和spring-boot-starter-web依赖。
配置application.yaml
spring: application: name: consumer-service cloud: zookeeper: connect-string: localhost:2181 server: port: 8081
注册到ZooKeeper
在启动类上添加@EnableDiscoveryClient注解。
创建一个REST接口,通过OpenFeign调用服务提供者
@RestController
public class ConsumerController {
@Autowired
private ProviderClient providerClient;
@GetMapping("/getHello")
public String getHello() {
return providerClient.hello();
}
}
运行效果
curl localhost:8081/getHello -i HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 28 Date: Wed, 18 Oct 2023 02:40:57 GMT Hello from Provider Service!
非Java服务在SpringCloud ZooKeeper中注册
可能有些读者乍一看觉得有点奇怪,为什么要在SpringCloud ZooKeeper中注册非Java服务呢?没有这个应用场景。
当然,这样的场景比较少,常见于大部分项目都是用SpringCloud开发,但有少部分项目因为种种原因,不得不使用其他语言开发,比如Go、Rust等。这时候,我们就需要在SpringCloud ZooKeeper中注册非Java服务了。
对于非JVM语言开发的服务,只需确保它们提供了Rest/HTTP接口并正确地注册到ZooKeeper,就可以被SpringCloud的Feign客户端所调用。
Go服务在SpringCloud ZooKeeper
example代码组织:
├── consumer │ └── consumer.go ├── go.mod ├── go.sum └── provider └── provider.go
Go服务提供者在SpringCloud ZooKeeper
注:该代码的质量为demo级别,实际生产环境需要更加严谨的代码,如重连机制、超时机制、更优秀的服务ID生成算法等。
package main
import (
"fmt"
"log"
"net/http"
"time"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/samuel/go-zookeeper/zk"
)
const (
zkServers = "localhost:2181" // Zookeeper服务器地址
)
func main() {
// 初始化gin框架
r := gin.Default()
// 添加一个简单的hello接口
r.GET("/hello", func(c *gin.Context) {
c.String(http.StatusOK, "Hello from Go service!")
})
// 注册服务到zookeeper
registerToZookeeper()
// 启动gin服务器
r.Run(":8080")
}
func registerToZookeeper() {
conn, _, err := zk.Connect([]string{zkServers}, time.Second*5)
if err != nil {
panic(err)
}
// 检查并创建父级路径
ensurePathExists(conn, "/services")
ensurePathExists(conn, "/services/provider-service")
// 构建注册的数据
data, _ := json.Marshal(map[string]interface{}{
"name": "provider-service",
"address": "127.0.0.1",
"port": 8080,
"sslPort": nil,
"payload": map[string]interface{}{"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance", "id": "provider-service", "name": "provider-service", "metadata": map[string]string{"instance_status": "UP"}},
"serviceType": "DYNAMIC",
"uriSpec": map[string]interface{}{
"parts": []map[string]interface{}{
{"value": "scheme", "variable": true},
{"value": "://", "variable": false},
{"value": "address", "variable": true},
{"value": ":", "variable": false},
{"value": "port", "variable": true},
},
},
})
// 在zookeeper中注册服务
path := "/services/provider-service/" + generateServiceId()
_, err = conn.Create(path, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
if err != nil {
log.Fatalf("register service error: %s", err)
} else {
log.Println(path)
}
}
func ensurePathExists(conn *zk.Conn, path string) {
exists, _, err := conn.Exists(path)
if err != nil {
log.Fatalf("check path error: %s", err)
}
if !exists {
_, err := conn.Create(path, []byte{}, 0, zk.WorldACL(zk.PermAll))
if err != nil {
log.Fatalf("create path error: %s", err)
}
}
}
func generateServiceId() string {
// 这里简化为使用当前时间生成ID,实际生产环境可能需要更复杂的算法
return fmt.Sprintf("%d", time.Now().UnixNano())
}
调用效果
curl localhost:8081/getHello -i HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 28 Date: Wed, 18 Oct 2023 02:43:52 GMT Hello from Go Service!
Go服务消费者在SpringCloud ZooKeeper
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/samuel/go-zookeeper/zk"
)
const (
zkServers = "localhost:2181" // Zookeeper服务器地址
)
var conn *zk.Conn
func main() {
// 初始化ZooKeeper连接
initializeZookeeper()
// 获取服务信息
serviceInfo := getServiceInfo("/services/provider-service")
fmt.Println("Fetched service info:", serviceInfo)
port := int(serviceInfo["port"].(float64))
resp, err := http.Get(fmt.Sprintf("http://%s:%d/hello", serviceInfo["address"], port))
if err != nil {
panic(err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
func initializeZookeeper() {
var err error
conn, _, err = zk.Connect([]string{zkServers}, time.Second*5)
if err != nil {
log.Fatalf("Failed to connect to ZooKeeper: %s", err)
}
}
func getServiceInfo(path string) map[string]interface{} {
children, _, err := conn.Children(path)
if err != nil {
log.Fatalf("Failed to get children of %s: %s", path, err)
}
if len(children) == 0 {
log.Fatalf("No services found under %s", path)
}
// 这里只获取第一个服务节点的信息作为示例,实际上可以根据负载均衡策略选择一个服务节点
data, _, err := conn.Get(fmt.Sprintf("%s/%s", path, children[0]))
if err != nil {
log.Fatalf("Failed to get data of %s: %s", children[0], err)
}
var serviceInfo map[string]interface{}
if err := json.Unmarshal(data, &serviceInfo); err != nil {
log.Fatalf("Failed to unmarshal data: %s", err)
}
return serviceInfo
}
Rust服务在SpringCloud ZooKeeper
example代码组织:
├── Cargo.lock ├── Cargo.toml └── src └── bin ├── consumer.rs └── provider.rs
Rust服务提供者在SpringCloud ZooKeeper
use std::collections::HashMap;
use std::time::Duration;
use serde_json::Value;
use warp::Filter;
use zookeeper::{Acl, CreateMode, WatchedEvent, Watcher, ZooKeeper};
static ZK_SERVERS: &str = "localhost:2181";
static mut ZK_CONN: Option<ZooKeeper> = None;
struct LoggingWatcher;
impl Watcher for LoggingWatcher {
fn handle(&self, e: WatchedEvent) {
println!("WatchedEvent: {:?}", e);
}
}
#[tokio::main]
async fn main() {
let hello = warp::path!("hello").map(|| warp::reply::html("Hello from Rust service!"));
register_to_zookeeper().await;
warp::serve(hello).run(([127, 0, 0, 1], 8083)).await;
}
async fn register_to_zookeeper() {
unsafe {
ZK_CONN = Some(ZooKeeper::connect(ZK_SERVERS, Duration::from_secs(5), LoggingWatcher).unwrap());
let zk = ZK_CONN.as_ref().unwrap();
let path = "/services/provider-service";
if zk.exists(path, false).unwrap().is_none() {
zk.create(path, vec![], Acl::open_unsafe().clone(), CreateMode::Persistent).unwrap();
}
let service_data = get_service_data();
let service_path = format!("{}/{}", path, generate_service_id());
zk.create(&service_path, service_data, Acl::open_unsafe().clone(), CreateMode::Ephemeral).unwrap();
}
}
fn get_service_data() -> Vec<u8> {
let mut data: HashMap<&str, Value> = HashMap::new();
data.insert("name", serde_json::Value::String("provider-service".to_string()));
data.insert("address", serde_json::Value::String("127.0.0.1".to_string()));
data.insert("port", serde_json::Value::Number(8083.into()));
serde_json::to_vec(&data).unwrap()
}
fn generate_service_id() -> String {
format!("{}", chrono::Utc::now().timestamp_nanos())
}
Rust服务消费者在SpringCloud ZooKeeper
use std::collections::HashMap;
use std::time::Duration;
use zookeeper::{WatchedEvent, Watcher, ZooKeeper};
use reqwest;
use serde_json::Value;
static ZK_SERVERS: &str = "localhost:2181";
struct LoggingWatcher;
impl Watcher for LoggingWatcher {
fn handle(&self, e: WatchedEvent) {
println!("WatchedEvent: {:?}", e);
}
}
#[tokio::main]
async fn main() {
let provider_data = fetch_provider_data_from_zookeeper().await;
let response = request_provider(&provider_data).await;
println!("Response from provider: {}", response);
}
async fn fetch_provider_data_from_zookeeper() -> HashMap<String, Value> {
let zk = ZooKeeper::connect(ZK_SERVERS, Duration::from_secs(5), LoggingWatcher).unwrap();
let children = zk.get_children("/services/provider-service", false).unwrap();
if children.is_empty() {
panic!("No provider services found!");
}
// For simplicity, we just take the first child (i.e., service instance).
// In a real-world scenario, load balancing strategies would determine which service instance to use.
let data = zk.get_data(&format!("/services/provider-service/{}", children[0]), false).unwrap();
serde_json::from_slice(&data.0).unwrap()
}
async fn request_provider(provider_data: &HashMap<String, Value>) -> String {
let address = provider_data.get("address").unwrap().as_str().unwrap();
let port = provider_data.get("port").unwrap().as_i64().unwrap();
let url = format!("http://{}:{}/hello", address, port);
let response = reqwest::get(&url).await.unwrap();
response.text().await.unwrap()
}
详解ZooKeeper在微服务注册中心的应用的更多相关文章
- 如何优化Spring Cloud微服务注册中心架构?
作者: 石杉的架构笔记 1.再回顾:什么是服务注册中心? 先回顾一下什么叫做服务注册中心? 顾名思义,假设你有一个分布式系统,里面包含了多个服务,部署在不同的机器上,然后这些不同机器上的服务之间要互相 ...
- Nacos作为微服务注册中心,爱不释手的感觉
我觉得Nacos用起来还不错 在使用SpringCloud做分布式微服务架构时,注册中心是必不可少的一个组件.目前可以用的主要有:Eureka.Consul.Zookeeper.今天,我们就来说一下A ...
- 【十次方微服务后台开发】Day02:加密与JWT鉴权、微服务注册中心、配置中心、熔断器、网关、消息总线、部署与持续集成、容器管理与监控Rancher、influxDB、grafana
一.密码加密与微服务鉴权JWT 1.BCrypt密码加密 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用B ...
- 详解ElasticAPM实现微服务的链路追踪(NET)
前言 Elastic APM实现链路追踪,首先要引用开源的APMAgent(APM代理),然后将监控的信息发送到APMServer,然后在转存入ElasticSearch,最后有Kibana展示:具体 ...
- 并发系列5-大白话聊聊Java并发面试问题之微服务注册中心的读写锁优化【石杉的架构笔记】
- Eureka 微服务注册中心搭建
本机IP为 192.168.1.102 1. 新建Maven项目 eureka 2. pom.xml <project xmlns="http://maven.apach ...
- 基于ZooKeeper的服务注册中心
本文介绍基于ZooKeeper的Dubbo服务注册中心的原理. 1.ZooKeeper中的节点 ZooKeeper是一个树形结构的目录服务,支持变更推送,因此非常适合作为Dubbo服务的注册中心. 注 ...
- lms框架服务注册中心
服务注册中心原理 在分布式系统里的注册中心.原理是将部署服务的机器地址记录到注册中心,服务消费者在有需求的时候,只需要查询注册中心,输入提供的服务名,就可以得到地址,从而发起调用. 在微服务架构下,主 ...
- 服务注册中心之ZooKeeper系列(一)
一.服务注册中心介绍 分布式服务框架部署在多台不同的机器上.例如服务A是订单相关的处理服务,服务B是订单的客户的相关信息服务.此时有个需求需要在服务A中获取订单客户的信息.如下图: 此时就面临以下几个 ...
- 微服务-注册与发现-zookeeper bydasn
目录 一.微服务注册的概述 二.zookeeper2.1 zookeeper安装启动2.2 zookeeper集群搭建2.3 zkcli操作 一.微服务注册概述 在微服务中,有这么一个东西叫服务注册表 ...
随机推荐
- 探索Redis与MySQL的双写问题
本文已收录至GitHub,推荐阅读 Java随想录 微信公众号:Java随想录 原创不易,注重版权.转载请注明原作者和原文链接 目录 双写一致问题 缓存读写策略 Cache-Aside Pattern ...
- Go 方法集合与选择receiver类型
Go 方法集合与选择receiver类型 目录 Go 方法集合与选择receiver类型 一.receiver 参数类型对 Go 方法的影响 二.选择 receiver 参数类型原则 2.1 选择 r ...
- go 中的循环依赖
什么是循环依赖 Go 中的循环依赖是指两个或多个包之间相互引用,形成了一个循环依赖关系.这种情况下,包 A 依赖包 B,同时包 B 也依赖包 A,导致两个包之间无法明确地确定编译顺序,从而可能引发编译 ...
- 开发现代化的.NetCore控制台程序:(4)使用GithubAction自动构建以及发布nuget包
前言 上一篇文章介绍了将 nuget 包发布到 Github Packages 上. 本文更进一步,使用 GitHub Action 搭建 CI/CD 流水线,进行 nuget 的自动构建和发布. G ...
- Halcon、HDevelop快速入门
HDevelop基础一 HDevelop概述 HDevelop是一款机器视觉的集成开发环境.下面将对HDevelop的界面内容做一下简单的介绍. 界面介绍 打开HDevelop,将看到以下画面. ...
- 可怕!.Net 8正式发布了,.Net野心确实不小!
随着三天.NET Conf 2023的会议结束了,.Net 8正式发布了. .Net 8是官方号称有史以来性能最快的一个版本了. .Net 8 增加了数以千计的性能.稳定性和安全性改进,以及平台和工具 ...
- MIT实验警示:人类或需要人工智能辅助才能理解复杂逻辑
麻省理工实验揭示人类的天赋缺陷 麻省理工学院林肯实验室(MIT Lincoln Laboratory)的一项研究表明,尽管形式规范具有数学上的精确性,但人类并不一定能对其进行解释.换句话说就是,人类在 ...
- C/S结构用户界面设计
C/S结构用户界面设计 [实验编号] 10003809547j 图形用户界面设计 [实验学时] 8学时 [实验环境] l 所需硬件环境为微机: l 所需软件环境为Microsoft Visual S ...
- 【python】Tkinter学习
定义 python自带的可编辑的GUI界面,是一个图像窗口. Tkinter是使用python进行窗口视窗设计的模块. 标签-按钮 2.1.Lable&Button标签和按钮 定义window ...
- 【结对作业】第一周 | 学习体会day02
今天我们想要实现线路的查询 发现了几个错误 1 <%-- 下拉表单的命名使用错误,导致无法接收前端数值--%> 首先我们很少使用下拉表单,之前用的也忘了,然后格式出现了错误 2 遇到typ ...