jetcd实战之一:极速体验

2021-12-07 08:18:56 浏览数 (1)

关于jetcd

  • jetcd是etcd v3版本的官方java客户端工具,java项目通过该库可以对etcd执行各种操作,当前最新发布版本是0.5.0
  • jetcd官方github:https://github.com/etcd-io/jetcd
  • etcd在线api文档:https://etcd.io/docs/next/learning/api/

关于jetcd实战系列

《jetcd实战系列》是欣宸新的原创系列,旨在与大家一起学习如何用jetcd操作etcd,除了基本增删改查,还会涉及到version、监听、租约等etcd特有功能;

系列文章链接

  1. jetcd实战之一:极速体验
  2. jetcd实战之二:基本操作
  3. jetcd实战之三:进阶操作(事务、监听、租约)

本篇概览

作为《jetcd实战系列》的开篇,主要是为整个系列做准备工作,包括以下内容:

  1. 梳理实战涉及到的应用和库的版本信息;
  2. 基于docker-compose部署etcd集群;
  3. 新建gradle工程,作为整个实战系列的父工程;
  4. 编写helloworld应用,验证jetcd可以正常访问etcd集群;

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):

名称

链接

备注

项目主页

https://github.com/zq2599/blog_demos

该项目在GitHub上的主页

git仓库地址(https)

https://github.com/zq2599/blog_demos.git

该项目源码的仓库地址,https协议

git仓库地址(ssh)

git@github.com:zq2599/blog_demos.git

该项目源码的仓库地址,ssh协议

  • 这个git项目中有多个文件夹,kubebuilder相关的应用在jetcd-tutorials文件夹下,如下图红框所示:
  • jetcd-tutorials文件夹下有多个子项目,本篇的是helloworld:

版本信息

实战系列所用的etcd是部署在docker环境下的三个实例组建的集群:

  1. etcd:3.4.7
  2. docker:20.10.5(Community)
  3. docker-compose:1.28.5
  4. jetcd:0.5.0
  5. jdk:1.8.0_271
  6. springboot:2.4.4
  7. IDEA:2020.2.3 (Ultimate Edition)
  8. gradle:6.8.3
  9. 电脑操作系统:macOS Big Sur 11.2.3

部署集群

  • 确认docker和docker-compose已正常运行
  • 新建docker-compose.yml文件:
代码语言:javascript复制
version: '3'
services:
  etcd1:
    image: "quay.io/coreos/etcd:v3.4.7"
    entrypoint: /usr/local/bin/etcd
    command:
      - '--name=etcd1'
      - '--data-dir=/etcd_data'
      - '--initial-advertise-peer-urls=http://etcd1:2380'
      - '--listen-peer-urls=http://0.0.0.0:2380'
      - '--listen-client-urls=http://0.0.0.0:2379'
      - '--advertise-client-urls=http://etcd1:2379'
      - '--initial-cluster-token=etcd-cluster'
      - '--heartbeat-interval=250'
      - '--election-timeout=1250'
      - '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
      - '--initial-cluster-state=new'
    ports:
      - 2379:2379
    volumes:
      - ./store/etcd1/data:/etcd_data
  etcd2:
    image: "quay.io/coreos/etcd:v3.4.7"
    entrypoint: /usr/local/bin/etcd
    command:
      - '--name=etcd2'
      - '--data-dir=/etcd_data'
      - '--initial-advertise-peer-urls=http://etcd2:2380'
      - '--listen-peer-urls=http://0.0.0.0:2380'
      - '--listen-client-urls=http://0.0.0.0:2379'
      - '--advertise-client-urls=http://etcd2:2379'
      - '--initial-cluster-token=etcd-cluster'
      - '--heartbeat-interval=250'
      - '--election-timeout=1250'
      - '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
      - '--initial-cluster-state=new'
    ports:
      - 2380:2379
    volumes:
      - ./store/etcd2/data:/etcd_data
  etcd3:
    image: "quay.io/coreos/etcd:v3.4.7"
    entrypoint: /usr/local/bin/etcd
    command:
      - '--name=etcd3'
      - '--data-dir=/etcd_data'
      - '--initial-advertise-peer-urls=http://etcd3:2380'
      - '--listen-peer-urls=http://0.0.0.0:2380'
      - '--listen-client-urls=http://0.0.0.0:2379'
      - '--advertise-client-urls=http://etcd3:2379'
      - '--initial-cluster-token=etcd-cluster'
      - '--heartbeat-interval=250'
      - '--election-timeout=1250'
      - '--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380'
      - '--initial-cluster-state=new'
    ports:
      - 2381:2379
    volumes:
      - ./store/etcd3/data:/etcd_data
  • 从上述脚本可见,宿主机的2379、2380、2381三个端口被用来映射三个etcd容器的2379端口;
  • 执行命令docker-compose up -d,如下,可见三个容器都已经创建,名称分别是28_etcd1_1、28_etcd2_1、28_etcd3_1,容器名和当前目录名有关:
代码语言:javascript复制
zhaoqin@zhaoqindeMacBook-Pro-2 28 % docker-compose up -d
Creating network "28_default" with the default driver
Creating 28_etcd2_1 ... done
Creating 28_etcd3_1 ... done
Creating 28_etcd1_1 ... done
  • etcd集群已启动成功,来试试基本操作命令是否正常,执行以下命令新建一个键值对,键是/aaa/foo,值是111:
代码语言:javascript复制
docker exec 28_etcd1_1 /usr/local/bin/etcdctl put /aaa/foo 111
  • 查看命令如下(我这换了个容器):
代码语言:javascript复制
docker exec 28_etcd2_1 /usr/local/bin/etcdctl get /aaa/foo -w fields
  • 得到的是完整的结果,除了键值还有Revision、ModRevision、Version、Lease等字段:
代码语言:javascript复制
"ClusterID" : 10316109323310759371
"MemberID" : 15168875803774599630
"Revision" : 2
"RaftTerm" : 2
"Key" : "/aaa/foo"
"CreateRevision" : 2
"ModRevision" : 2
"Version" : 1
"Value" : "111"
"Lease" : 0
"More" : false
"Count" : 1

新建gradle工程,作为整个实战系列的父工程

  • 接下来新建一个gradle工程,整个实战系列都是在此父工程下开发;
  • 新建gradle工程名为jetcd-tutorials,其build.gradle内容如下:
代码语言:javascript复制
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter

// gradle自身会用到的相关设置
buildscript {
    // 仓库
    repositories {
        // 本地
        mavenLocal()
        // 如果有私服就在此配置,如果没有请注释掉
        maven {
            url 'http://192.168.50.43:8081/repository/aliyun-proxy/'
        }
        // 阿里云
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public/'
        }
        // 中央仓库
        mavenCentral()
        // grandle插件
        maven {
            url 'https://plugins.gradle.org/m2/'
        }
    }

    // 子模块会用到的变量
    ext {
        springBootVersion = '2.4.4'
    }
}

// 插件
plugins {
    id 'java'
    id 'java-library'
    // 有这个声明,子模块可以使用org.springframework.boot插件而无需指定版本,但是apply=false表示当前模块不使用此插件
    id 'org.springframework.boot' version "${springBootVersion}" apply false
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

// gradle wrapper指定版本
wrapper {
    gradleVersion = '6.8.3'
}

// 取当前时间
def buildTimeAndDate = OffsetDateTime.now()

// 根据时间生成字符串变量
ext {
    projectVersion = project.version
    buildDate = DateTimeFormatter.ISO_LOCAL_DATE.format(buildTimeAndDate)
    buildTime = DateTimeFormatter.ofPattern('HH:mm:ss.SSSZ').format(buildTimeAndDate)
}


// 针对所有project的配置,包含根项目
allprojects {
    group 'com.bolingcavalry'
    version '1.0-SNAPSHOT'

    apply plugin: 'java'
    apply plugin: 'idea'
    apply plugin: 'io.spring.dependency-management'

    // 编译相关参数
    compileJava {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
        options.encoding = 'UTF-8'
        options.compilerArgs =  [
                '-Xlint:all', '-Xlint:-processing'
        ]
    }

    // Copy LICENSE
    tasks.withType(Jar) {
        from(project.rootDir) {
            include 'LICENSE'
            into 'META-INF'
        }
    }

    // 生成jar文件时,MANIFEST.MF的内容如下
    jar {
        manifest {
            attributes(
                    'Created-By': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})".toString(),
                    'Built-By': 'travis',
                    'Build-Date': buildDate,
                    'Build-Time': buildTime,
                    'Built-OS': "${System.properties['os.name']}",
                    'Specification-Title': project.name,
                    'Specification-Version': project.version,
                    'Specification-Vendor': 'Will Zhao',
                    'Implementation-Title': project.name,
                    'Implementation-Version': project.version,
                    'Implementation-Vendor': 'Will Zhao'
            )
        }
    }

    // 仓库
    repositories {
        // 本地
        mavenLocal()
        // 如果有私服就在此配置,如果没有请注释掉
        maven {
            url 'http://192.168.50.43:8081/repository/aliyun-proxy/'
        }
        // 阿里云
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public/'
        }
        // 中央仓库
        mavenCentral()
        // grandle插件
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
}

// 类似maven的dependencyManagement,这里将所有jar的版本指定好,子模块在依赖时可以不用指定版本
allprojects { project ->
    buildscript {
        dependencyManagement {
            imports {
                mavenBom "org.springframework.boot:spring-boot-starter-parent:${springBootVersion}"
                mavenBom "org.junit:junit-bom:5.7.0"
            }

            dependencies {
                dependency 'org.projectlombok:lombok:1.16.16'
                dependency 'org.apache.commons:commons-lang3:3.11'
                dependency 'commons-collections:commons-collections:3.2.2'
                dependency 'io.etcd:jetcd-core:0.5.0'
                dependency 'org.slf4j:slf4j-log4j12:1.7.30'
            }
        }
    }
}

// 坐标信息
group 'com.bolingcavalry'
version '1.0-SNAPSHOT'
  • 现在根模块已经建好,后面整个系列的代码都会写在这个根模块下面;

编写helloworld应用

  • 接下来写个helloworld应用验证jetcd能不能操作etcd集群;
  • 在根模块下面新建名为helloworld的gradle子模块,其build.gradle内容如下:
代码语言:javascript复制
plugins {
    id 'java'
}

// 子模块自己的依赖
dependencies {
    implementation 'io.etcd:jetcd-core'
    implementation 'org.projectlombok:lombok'
    // annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.slf4j:slf4j-log4j12'
    testImplementation('org.junit.jupiter:junit-jupiter')
}

test {
    useJUnitPlatform()
}

group 'com.bolingcavalry'
version '1.0-SNAPSHOT'
  1. 新增HelloWorld.java,代码很简单,getKVClient方法用来生成客户端实例,put方法向etcd写入键值对,get方法去etcd查询指定键的值:
代码语言:javascript复制
package com.bolingcavalry;

import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static com.google.common.base.Charsets.UTF_8;

@Slf4j
public class HelloWorld {

    /**
     * 新建key-value客户端实例
     * @return
     */
    private KV getKVClient(){
        String endpoints = "http://192.168.50.239:2379,http://192.168.50.239:2380,http://192.168.50.239:2381";
        Client client = Client.builder().endpoints(endpoints.split(",")).build();
        return client.getKVClient();
    }

    /**
     * 将字符串转为客户端所需的ByteSequence实例
     * @param val
     * @return
     */
    private static ByteSequence bytesOf(String val) {
        return ByteSequence.from(val, UTF_8);
    }

    /**
     * 查询指定键对应的值
     * @param key
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public String get(String key) throws ExecutionException, InterruptedException{
        log.info("start get, key [{}]", key);
        GetResponse response = getKVClient().get(bytesOf(key)).get();

        if (response.getKvs().isEmpty()) {
            log.error("empty value of key [{}]", key);
            return null;
        }

        String value = response.getKvs().get(0).getValue().toString(UTF_8);
        log.info("finish get, key [{}], value [{}]", key, value);
        return value;
    }

    /**
     * 创建键值对
     * @param key
     * @param value
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public PutResponse put(String key, String value) throws ExecutionException, InterruptedException {
        log.info("start put, key [{}], value [{}]", key, value);
        return getKVClient().put(bytesOf(key), bytesOf(value)).get();
    }
}
  1. 接下来咱们写个单元测试类来验证上述代码是否有效:
代码语言:javascript复制
package com.bolingcavalry;

import io.etcd.jetcd.kv.PutResponse;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class HelloWorldTest {
    // 用与测试的键
    private static final String KEY = "/abc/foo-"   System.currentTimeMillis();

    // 用于测试的值
    private static final String VALUE = "/abc/foo";

    @org.junit.jupiter.api.Test
    @Order(2)
    void get() throws ExecutionException, InterruptedException {
        String getResult = new HelloWorld().get(KEY);
        assertEquals(VALUE, getResult);
    }

    @Test
    @Order(1)
    void put() throws ExecutionException, InterruptedException {
        PutResponse putResponse = new HelloWorld().put(KEY, VALUE);
        assertNotNull(putResponse);
        assertNotNull(putResponse.getHeader());
    }
}
  1. 代码写完后按照下图红框指示执行单元测试,可见jetcd操作etcd成功:
  • 至此,《jetcd实战》系列的开篇就完成了,咱们搭建好了etcd集群,还初步体验过jetcd的基本功能,接下来的章节会从基本操作开始,由浅入深的学习jetcd;

0 人点赞