Go#

Go标准库#

timeout#

1
2
3
client := http.Client{
Timeout: timeout,
}

connection timeout#

1
2
3
4
5
6
7
client := http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: timeout,
}).Dial,
},
}

Java#

标准库(jdk17+)#

timeout#

1
2
3
4
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://example.com"))
.timeout(Duration.ofSeconds(10))
.build();

connectionTimeout#

1
2
3
HttpClient.Builder builder = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.version(HttpClient.Version.HTTP_1_1);

Reactor Netty#

timeout#

1
HttpClient client = HttpClient.create().responseTimeout(Duration.ofSeconds(10));

connectionTimeout#

1
HttpClient client = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

多语言SDK设计的常见问题#

日志打印的设计策略#

在SDK的关键节点,比如初始化完成、连接建立或者连接断开,都可以打印日志。如果是PerRequest的日志,一般默认不会打印INFO级别的日志。

SDK应该避免仅仅打印错误日志然后忽略异常;相反,它应该提供机制让调用者能够捕获并处理异常信息。这种做法有助于保持错误处理的透明性,并允许调用者根据需要采取适当的响应措施。正如David J. Wheeler所说”Put the control in the hands of those who know how to handle the information, not those who know how to manage the computers, because encapsulated details will eventually leak out.”把控制权放到那些知道如何处理信息的人手中,而不是放在那些知道如何管理计算机的人手中,因为封装的细节最终都会暴露。

是否需要使用显式的start/connect方法?#

像go这样的语言,一般来说不太在意特定的时间内,某个协程是否处于阻塞等待连接的状态。而在java这样的语言,特别是在采用响应式编程模型的场景下,通常需要通过异步操作来管理连接的建立。这可以通过显式的start/connect方法来或者是异步的工厂方法来实现。

背景#

由于Influxdb 1.X的客户端已经基本处于维护状态,同时openGemini仍在不断发展中,为了能够更好地支持openGemini,如支持对接多个服务端地址、支持对接Apache
Arrow Flight协议等,社区决定开发属于openGemini自己的客户端SDK。

客户端SDK规划功能#

  • 支持对接多个服务端地址
  • 支持对接Apache Arrow Flight协议
  • 支持Sql查询、结构化查询、写入、批量写入等,详见下文UML图
  • 默认超时,连接超时10秒,读写超时30秒

本文的方法假定编程语言不支持重载,如编程语言支持重载,可以对方法名进行一些优化调整。

Client constructor params design#

tls相关配置可以参考TLS配置参数设计

classDiagram
    class OpenGeminiClient {
        + List~Address~ addresses
        + AuthConfig authConfig // nullable, if null, means no auth
        + BatchConfig batchConfig // nullable, if null, means batch is disabled
        + timeout
        + connectTimeout
        + bool gzipEnabled
        + bool tlsEnabled
        + TlsConfig tlsConfig // language specific
        + void close()
    }

    class Address {
        + String host
        + int Port // in rust, it is u16
    }

    class AuthConfig {
        + AuthType authType // enum None, Password, Token
        + String username
        + String password
        + String token
    }

    class BatchConfig {
        + Duration batchInterval // must be greater than 0
        + int batchSize // must be greater than 0
    }

    OpenGeminiClient "1" *-- "many" Address: contains
    OpenGeminiClient *-- AuthConfig: contains
    OpenGeminiClient *-- BatchConfig: contains

Database & RetentionPolicy management design#

classDiagram
    class OpenGeminiClient {
        + void CreateDatabase(String database)
        + void CreateDatabaseWithRp(String database, rpConfig RpConfig)
        + String[] ShowDatabases()
        + void DropDatabase(String database)
        + void CreateRetentionPolicy(database string, rpConfig RpConfig, isDefault bool)
        + RetentionPolicy[] ShowRetentionPolicies(database string)
        + void DropRetentionPolicy(database, retentionPolicy string)
    }
    class RpConfig {
        + String Name
        + String Duration
        + String ShardGroupDuration
        + String IndexDuration
    }

Write point design#

classDiagram
    class OpenGeminiClient {
        + WritePoint(String database, Point point)
        + WriteBatchPoints(String database, BatchPoints batchPoints)
    }
    class BatchPoints {
        + List~Point~ points
        + AddPoint(Point)
    }

    class Point {
        + String measurement
        + Precision precision // enum, second, millisecond, microsecond, nanosecond, default is nanosecond
        + Time time // language specific
        + Map~String, String~ tags
        + Map~String, Object~ fields
        + AddTag(string, string) // init container if null
        + AddField(string, int) // init container if null
        + AddField(string, string) // init container if null
        + AddField(string, float) // init container if null
        + AddField(string, bool) // init container if null
        + SetTime(timestamp)
        + SetPrecision(type)
        + SetMeasurement(name)
    }

    BatchPoints "1" *-- "many" Point: contains

Sql-like query design#

classDiagram
    class Query {
        + String database
        + String retentionPolicy
        + String command
    }
classDiagram
    class QueryResult {
        + List~SeriesResult~ results
        + String error
    }
    class SeriesResult {
        + List~Series~ series // Series is an uncountable noun.
        + String error
    }
    class Series {
        + String name
        + Map~String, String~ tags
        + List~String~ columns
        + List~List~ values
    }
    QueryResult "1" *-- "0..*" SeriesResult: contains
    SeriesResult "1" *-- "0..*" Series: contains

Ping design#

classDiagram
    class OpenGeminiClient {
        + void ping(int index) // index selects one from multiple servers
    }

Inner Http client design#

使用类似InnerHttpClient的设计,将鉴权、负载均衡、重试等逻辑封装在内部,对client提供简单的接口。增强模块化和代码清晰度。

classDiagram
    class InnerHttpClient {
        + void executeHttpGetByIdx(int idx, ...) // specify server index
        + void executeHttpRequestByIdx(int idx, String method, ...) // specify server index
        + void executeHttpGet(String method, ...) // load balance
        + void executeHttpRequest(String method, ...) // load balance
        - void executeHttpRequestInner(String url, String method, ...) // inner method
    }
graph TD
    executeHttpGetByIdx --> executeHttpRequestByIdx
    executeHttpRequestByIdx --> executeHttpRequestInner
    executeHttpGet --> executeHttpRequest
    executeHttpRequest --> executeHttpRequestInner

Error handling#

Error message#

Scene1 http request failed#

1
$operation request failed, error: $error_details

Scene2 http response code is not 200~300#

1
$operation error resp, code: $code, body: $body

Scene3 other error#

1
2
3
$operation failed, error: $error_details
# example:
writePoint failed, unmarshall response body error: json: cannot unmarshal number ...

背景#

TLS(Transport Layer Security)是一种安全协议,用于在两个通信应用程序之间提供保密性和数据完整性。TLS是SSL(Secure Sockets Layer)的继任者。

不同的编程语言处理TLS配置的方式各有千秋, 本文针对TLS配置参数的设计进行探讨。

代码配置中,建议使用反映状态的参数名。

通用参数#

  • tlsEnable: 是否启用TLS

Go#

推荐使用方式一

方式一:

  • tlsConfig *tls.Config: Go标准库的内置TLS结构体

方式二:

由于Go不支持加密的私钥文件,推荐使用文件内容,而不是文件路径,避免敏感信息泄露。

  • tlsCertContent []byte: 证书文件内容
  • tlsPrivateKeyContent []byte: 私钥文件内容
  • tlsMinVersion uint16: TLS最低版本
  • tlsMaxVersion uint16: TLS最高版本
  • tlsCipherSuites []uint16: TLS加密套件列表

Java#

Java的TLS参数基本上都是基于keystore和truststore来配置的。一般常见设计如下参数:

  • keyStorePath: keystore文件路径
  • keyStorePassword: keystore密码
  • trustStorePath: truststore文件路径
  • trustStorePassword: truststore密码
  • tlsVerificationDisabled: 是否禁用TLS校验
  • tlsHostnameVerificationDisabled: 是否禁用TLS主机名校验,仅部分框架支持。
  • tlsVersions: TLS版本列表
  • tlsCipherSuites: TLS加密套件列表

JavaScript#

JavaScript可以使用标准库里的tls.SecureContextOptions

Kotlin#

kotlin的Tls与Java相同:

  • keyStorePath: keystore文件路径
  • keyStorePassword: keystore密码
  • trustStorePath: truststore文件路径
  • trustStorePassword: truststore密码
  • tlsVerificationDisabled: 是否禁用TLS校验
  • tlsHostnameVerificationDisabled: 是否禁用TLS主机名校验,仅部分框架支持。
  • tlsVersions: TLS版本列表
  • tlsCipherSuites: TLS加密套件列表

Python#

推荐使用方式一

方式一

  • ssl.SSLContext: Python标准库的内置TLS结构体

方式二

Python可以使用文件路径以及加密的私钥文件。

  • tlsCertPath: 证书文件路径
  • tlsPrivateKeyPath: 私钥文件路径
  • tlsPrivateKeyPassword: 私钥密码
  • tlsMinVersion: TLS最低版本
  • tlsMaxVersion: TLS最高版本
  • tlsCipherSuites: TLS加密套件列表

Rust#

由于常见的Rust TLS实现不支持加密的私钥文件,推荐使用文件内容,而不是文件路径,避免敏感信息泄露。 一般常见如下设计参数:

  • tls_cert_content Vec: 证书内容
  • tsl_private_key_content Vec: 私钥内容
  • tls_versions: TLS版本列表
  • tls_cipher_suites: TLS加密套件列表
  • tls_verification_disabled: 是否禁用TLS校验

根据Python项目的需求和特性,可以为Python的Http SDK项目选择以下命名方式:

  • xxx-client-python:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。
  • xxx-http-client-python:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-python:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

由于Python的调用方式通常是模块名.类名.方法名

TypeScript的调用方式通常是

1
2
import { ClassName } from 'moduleName';
const object = new ClassName();

根据TypeScript项目的需求和特性,可以为TypeScript的Http SDK项目选择以下命名方式:

  • xxx-client-ts:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。在npm可以注册为”xxx”。
  • xxx-http-client-ts:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-ts:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

根据Go项目的需求和特性,可以为Go的Http SDK项目选择以下命名方式:

  • xxx-client-go:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。
  • xxx-http-client-go:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-go:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

由于Go语言的调用方式是包名.结构体名.方法名,所以在设计SDK时,需要考虑包名、结构体名、方法名的设计。

以xxx业务为例,假设业务名为xxx,推荐包名也为xxx,结构体名为Client

目录布局可以是这样子的:

1
2
3
xxx-client-go/
|-- xxx/
| |-- client.go

Java Http SDK设计#

根据Java项目的需求和特性,可以为Java的Http SDK项目选择以下命名方式:

  • xxx-client-java:如果这个项目只有Http SDK,没有其他协议的SDK,推荐使用这个命名方式。
  • xxx-http-client-java:当存在其他协议的SDK时,可以使用这个命名方式,以区分不同协议的SDK。
  • xxx-admin-java:当项目使用其他协议作为数据通道,使用HTTP协议作为管理通道时,可以使用这个命名方式。

maven模块设计#

maven module命名可以叫xxx-client或者xxx-http-client,这通常取决于你的项目是否有其他协议的client,如果没有,那么推荐直接使用xxx-client。

假设包名前缀为com.xxx,module视图如下:

1
2
3
4
5
6
xxx-client-java(maven artifactId: xxx-client-parent)/
|-- xxx-client-api(接口定义,包名com.xxx.client.api,jdk8+)
|-- xxx-client-common/core(核心实现,包名com.xxx.client.common,jdk8+)
|-- xxx-client-jdk(基于jdk http client的实现,包名com.xxx.client.jdk,jdk17+)
|-- xxx-client-okhttp(基于okhttp的实现,包名com.xxx.client.okhttp,jdk8+)
|-- xxx-client-reactor(基于reactor-netty的实现,包名com.xxx.client.reactor,jdk8+)

依赖关系图:

graph TD
api[xxx-client-api]
common[xxx-client-common]
jdk[xxx-client-jdk]
okhttp[xxx-client-okhttp]
reactor[xxx-client-reactor]

common --> api

jdk --> common
okhttp --> common
reactor --> common

ZooKeeper,是一个开源的分布式协调服务,不仅支持分布式选举、任务分配,还可以用于微服务的注册中心和配置中心。本文,我们将深入探讨ZooKeeper用做微服务注册中心的场景。

ZooKeeper中的服务注册路径#

SpringCloud ZooKeeper遵循特定的路径结构进行服务注册

1
/services/${spring.application.name}/${serviceId}

示例:

1
/services/provider-service/d87a3891-1173-45a0-bdfa-a1b60c71ef4e

/services/${spring.application.name}是ZooKeeper中的永久节点,/${serviceId}是临时节点,当服务下线时,ZooKeeper会自动删除该节点。

注:当微服务的最后一个实例下线时,SpringCloud ZooKeeper框架会删除/${spring.application.name}节点。

ZooKeeper中的服务注册数据#

下面是一个典型的服务注册内容示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"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-discoveryspring-boot-starter-web依赖。

配置application.yaml#

1
2
3
4
5
6
7
8
9
spring:
application:
name: provider-service
cloud:
zookeeper:
connect-string: localhost:2181

server:
port: 8082

注册到ZooKeeper#

在启动类上添加@EnableDiscoveryClient注解。

创建一个简单的REST接口#

1
2
3
4
5
6
7
@RestController
public class ProviderController {
@GetMapping("/hello")
public String hello() {
return "Hello from Provider Service!";
}
}

服务消费者#

创建SpringBoot项目#

创建SpringBoot项目,并添加spring-cloud-starter-zookeeper-discoveryspring-cloud-starter-openfeignspring-boot-starter-web依赖。

配置application.yaml#

1
2
3
4
5
6
7
8
9
spring:
application:
name: consumer-service
cloud:
zookeeper:
connect-string: localhost:2181

server:
port: 8081

注册到ZooKeeper#

在启动类上添加@EnableDiscoveryClient注解。

创建一个REST接口,通过OpenFeign调用服务提供者#

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ConsumerController {

@Autowired
private ProviderClient providerClient;

@GetMapping("/getHello")
public String getHello() {
return providerClient.hello();
}
}

运行效果#

1
2
3
4
5
6
7
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代码组织:

1
2
3
4
5
6
├── consumer
│ └── consumer.go
├── go.mod
├── go.sum
└── provider
└── provider.go

Go服务提供者在SpringCloud ZooKeeper#

注:该代码的质量为demo级别,实际生产环境需要更加严谨的代码,如重连机制、超时机制、更优秀的服务ID生成算法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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())
}

调用效果

1
2
3
4
5
6
7
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#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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代码组织:

1
2
3
4
5
6
├── Cargo.lock
├── Cargo.toml
└── src
└── bin
├── consumer.rs
└── provider.rs

Rust服务提供者在SpringCloud ZooKeeper#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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()
}

为什么需要自解压的可执行文件#

大部分软件的安装包是一个压缩包,用户需要自己解压,然后再执行安装脚本。常见的两种格式是tar.gzzip。常见的解压执行脚本如下

tar.gz#

1
2
3
4
5
#!/bin/bash

tar -zxvf xxx.tar.gz
cd xxx
./install.sh

zip#

1
2
3
4
5
#!/bin/bash

unzip xxx.zip
cd xxx
./install.sh

在有些场景下,为了方便分发、安装,我们需要将多个文件和目录打包并与一个启动脚本结合。这样子就可以实现一键安装,而不需要用户自己解压文件,然后再执行启动脚本。

核心原理是,通过固定分隔符分隔脚本和压缩包部分,脚本通过分隔符将压缩包部分提取出来,然后解压,执行安装脚本,脚本不会超过固定分隔符。解压可以通过临时文件(zip)或流式解压(tar.gz)的方式实现。

创建包含zip压缩包的自解压可执行文件#

构造一个zip压缩包#

1
2
3
echo "hello zip" > temp.txt
zip -r temp.zip temp.txt
rm -f temp.txt

构造可执行文件 self_extracting.sh#

以使用__ARCHIVE_BELOW__做分隔符为例,self_extracting.sh里面内容:

推荐把临时文件放在内存文件路径下,这样子可以避免磁盘IO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
CURRENT_DIR="$(dirname "$0")"

ARCHIVE_START_LINE=$(awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' $0)

tail -n+$ARCHIVE_START_LINE $0 > /tmp/temp.zip
unzip /tmp/temp.zip" -d "$CURRENT_DIR"
rm "$CURRENT_DIR/temp.zip"

# replace the following line with your own code
cat temp.txt

exit 0

__ARCHIVE_BELOW__

将zip文件追加到self_extracting.sh文件的尾部

1
2
cat temp.zip >> self_extracting.sh
chmod +x self_extracting.sh

创建包含tar.gz压缩包的自解压可执行文件#

构造一个tar.gz压缩包#

1
2
3
echo "hello tar.gz" > temp.txt
tar -czf temp.tar.gz temp.txt
rm -f temp.txt

构造可执行文件 self_extracting.sh#

以使用__ARCHIVE_BELOW__做分隔符为例,self_extracting.sh里面内容:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
CURRENT_DIR="$(dirname "$0")"

ARCHIVE_START_LINE=$(awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' $0)
tail -n+$ARCHIVE_START_LINE $0 | tar xz -C "$CURRENT_DIR"

# replace the following line with your own code
cat temp.txt

exit 0

__ARCHIVE_BELOW__
0%