本文分享自华为云社区《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在微服务注册中心的应用的更多相关文章

  1. 如何优化Spring Cloud微服务注册中心架构?

    作者: 石杉的架构笔记 1.再回顾:什么是服务注册中心? 先回顾一下什么叫做服务注册中心? 顾名思义,假设你有一个分布式系统,里面包含了多个服务,部署在不同的机器上,然后这些不同机器上的服务之间要互相 ...

  2. Nacos作为微服务注册中心,爱不释手的感觉

    我觉得Nacos用起来还不错 在使用SpringCloud做分布式微服务架构时,注册中心是必不可少的一个组件.目前可以用的主要有:Eureka.Consul.Zookeeper.今天,我们就来说一下A ...

  3. 【十次方微服务后台开发】Day02:加密与JWT鉴权、微服务注册中心、配置中心、熔断器、网关、消息总线、部署与持续集成、容器管理与监控Rancher、influxDB、grafana

    一.密码加密与微服务鉴权JWT 1.BCrypt密码加密 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用B ...

  4. 详解ElasticAPM实现微服务的链路追踪(NET)

    前言 Elastic APM实现链路追踪,首先要引用开源的APMAgent(APM代理),然后将监控的信息发送到APMServer,然后在转存入ElasticSearch,最后有Kibana展示:具体 ...

  5. 并发系列5-大白话聊聊Java并发面试问题之微服务注册中心的读写锁优化【石杉的架构笔记】

  6. Eureka 微服务注册中心搭建

    本机IP为  192.168.1.102 1.   新建Maven项目   eureka 2.   pom.xml <project xmlns="http://maven.apach ...

  7. 基于ZooKeeper的服务注册中心

    本文介绍基于ZooKeeper的Dubbo服务注册中心的原理. 1.ZooKeeper中的节点 ZooKeeper是一个树形结构的目录服务,支持变更推送,因此非常适合作为Dubbo服务的注册中心. 注 ...

  8. lms框架服务注册中心

    服务注册中心原理 在分布式系统里的注册中心.原理是将部署服务的机器地址记录到注册中心,服务消费者在有需求的时候,只需要查询注册中心,输入提供的服务名,就可以得到地址,从而发起调用. 在微服务架构下,主 ...

  9. 服务注册中心之ZooKeeper系列(一)

    一.服务注册中心介绍 分布式服务框架部署在多台不同的机器上.例如服务A是订单相关的处理服务,服务B是订单的客户的相关信息服务.此时有个需求需要在服务A中获取订单客户的信息.如下图: 此时就面临以下几个 ...

  10. 微服务-注册与发现-zookeeper bydasn

    目录 一.微服务注册的概述 二.zookeeper2.1 zookeeper安装启动2.2 zookeeper集群搭建2.3 zkcli操作 一.微服务注册概述 在微服务中,有这么一个东西叫服务注册表 ...

随机推荐

  1. PHP -pop魔术方法

    PHP魔术方法: PHP提供了一系列的魔术方法,这些魔术方法为编程提供了很多便利,在 PHP 中的作用是非常重要的.PHP 中的魔术方法通常以__(两个下划线)开始,可以在要使用时灵活调用. 例题 [ ...

  2. 轻松掌握组件启动之MongoDB:快速入门、Linux安装和Docker配置指南

    引言 我们将继续深入研究组件启动专题.在之前的文章中,我们已经详细介绍了Redis的各种配置使用方法,为读者提供了全面的指导.然而,今天我们将转向另一个备受关注的数据库--MongoDB.MongoD ...

  3. 小景的Dba之路--如何导出0记录表以及数据泵的使用

    小景最近在系统压测相关的工作,其中涉及了数据备份导出的操作.今天的问题是:exp命令不会导出0记录表,那么我们探讨下如何导出0记录表以及数据泵的使用. 首先,我们先刨析一下问题现象及原因: 在 Ora ...

  4. 新手面对安卓6.0以上的版本时出现一个关于文件权限检测的问题,报错为:“无法解析符号 'checkSelfPermission'”,解决办法

    [[注意]:这只是笔者在遇到这个问题时的解决方法,如果对您毫无帮助,请自寻他法!!!] 面对新手:在简单做一个音乐播放程序时,如果面对安卓6.0以上的版本,就会出现一个关于文件权限检测的问题,报错为: ...

  5. Unity Yaml文本标量处理

    在做脱离unity处理unity的yaml文档的工具(prefab.material等) unity使用的yaml是YAML的语法子集,主要难点在处理文本标量上,如果用工具修改以后和unity生成的格 ...

  6. undefined reference to vtable for问题解决(QT)

    主要在运行时出现 原因是在自定义类使用信号与槽,在创建文件时,未继承QObject类并且没有添加Q_OBJECT: 解决: 在需要的类中,添加Q_OBJECT,继承QObject类. 然后使用QTCr ...

  7. Unity3D 选择焦点切换

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  8. mysql练习案例(实操)

    最近想要在回去复习mysql语句,就在网上找了一些案例练习,起初找得都是零零散散的,后面参考这篇博客做出了一个实操案例.Eric_Squirrel:mysql学生表经典案例50题. 首先是建表,我用的 ...

  9. golang在win10安装、环境配置 和 goland开发工具golang配置 及Terminal的git配置

    前言 本人在使用goland软件开发go时,对于goland软件配置网上资料少,为了方便自己遗忘.也为了希望和我一样的小白能够更好的使用,所以就写下这篇博客,废话不多说开搞. 一.查看自己电脑系统版本 ...

  10. AtCoder Beginner Contest 329 (ABC329)

    A. Spread 不说了,代码. B. Next 不说了,代码. C. Count xxx Description 给定一个长度为 \(N\) 的字符串 \(S\),求 \(S\) 中非空连续,并且 ...