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方法来或者是异步的工厂方法来实现。

背景

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__

Ignite Java 客户端最佳实践

背景

本文总结了在使用Apache Ignite(Ignite2.0)的Java客户端时,需要注意的一些问题,以及一些最佳实践。值得一提的是 Ignite的Java客户端有一些跟直觉上不太一样的地方,需要注意下。

客户端相关

Ignite客户端有两处跟直觉上相差较大:

  • Ignite客户端连接没有默认超时时间,如果连接不上,有概率会导致创建客户端一直阻塞,所以一定要设置timeout参数
  • Ignite客户端默认不会重连,更不用说无限重连了。并且Ignite客户端重连的实现方式是预先计算出所有重连的时间戳,然后在这些时间戳到达时重连,由于要预先计算出重连的时间戳存入数组,这也就意味着不能无限重连。如果您的应用程序需要无限重连(在云原生环境下,这是非常常见的场景),那么您需要自己实现重连逻辑。

ClientConfiguration里的重要参数

ClientConfiguration timeout

控制连接超时的参数,单位是毫秒。必须设置!如果不设置,有概率会导致创建客户端一直阻塞。

SQL相关

SQL查询典型用法

1
2
3
SqlFieldsQuery query = new SqlFieldsQuery("SELECT 42").setTimeout(5, TimeUnit.SECONDS);
FieldsQueryCursor<List<?>> cursor = igniteClient.query(query))
List<List<?>> result = cursor.getAll();

注意:Ignite query出来的cursor如果自己通过iterator遍历则必须要close,否则会导致内存泄漏。

Query相关参数

SqlFieldsQuery timeout

SqlQuery的超时时间,必须设置。默认是0,表示永不超时。如果不设置,有概率会导致查询一直阻塞。

0%