#6 etcd 的应用

2021-12-09

简介

etcd 是 CoreOS 的一个子项目,KV 数据库,原是设计用于存储集群共享配置数据,基于 Apache 协议开源。
PS: CoreOS 2018 年被 RedHat 收购,然后更名为 Container Linux, 后来和 RedHat 的 Project Atomic 合并,形成 Fedora CoreOS 项目。

本质是一个 KV 存储,从这个角度看和 Redis 很像。但:

Item etcd Redis
性能 高 非常高
存储方式 磁盘 + 内存缓存 内存
持久化 boltdb AOF + rdb
数据类型 string 丰富
API gRPC RESP
一致性 Raft 复制

PS:性能:更高 QPS + 更低延迟
PS:RESP:REdis Serialization Protocol(TCP 文本协议)

作用

本质就是一个轻量级的 KV 存储系统,由于其高并发、支持分布式(强一致性)、支持版本控制、支持实时更新通知等特点,所以常用于:

  • 服务发现:帮助分布式系统中的服务相互找到并建立连接。
  • 配置管理:集中管理和动态更新分布式系统的配置信息。
  • 分布式锁:确保在分布式环境中,同一时间只有一个客户端能获取锁,避免资源冲突。

概念

  1. Raft 算法

    Raft 是一种用于管理复制日志的一致性算法,节点之间通过复制日志来达成一致状态。
    当客户端发起写请求时,领导者(Leader)节点接收请求并将其记录到日志中,接着把日志条目复制到其他跟随者(Follower)节点。
    只有当多数节点都成功复制了该日志条目后,该条目才会被提交,数据更新才会被应用。

  2. 角色相关概念

    • Node(节点)/Member(成员):指 etcd 集群中的单个服务器实例,每个节点都有一个唯一标识,存储着部分或全部的数据副本,并且参与集群的一致性协议。
    • Leader(领导者):负责处理客户端写请求和协调日志复制的节点。同一时刻集群中只有一个领导者,它会接收客户端的写操作,并将这些操作广播给其他跟随者节点。
    • Follower(跟随者):跟随领导者的指令,接收并复制领导者发送的日志条目。跟随者不直接处理客户端的写请求,只响应领导者的请求。
    • Candidate(候选人):在 Raft 算法的选举过程中,当跟随者在一定时间内没有收到领导者的心跳信息时,它会转变为候选人状态。
      候选人会发起新一轮的选举,向其他节点请求投票。如果获得多数节点的投票,候选人就会成为新的领导者。
    • Peer(对等节点):指集群中地位平等的其他节点,节点之间相互通信以实现数据复制、选举等功能。
  3. Term(任期)

    逻辑时间概念,用于划分不同的选举周期,确保选举的正确性和防止过期的领导者重新掌权,同时在日志复制和消息传递中也起到重要的同步和协调作用。
    每个任期从一次选举开始,有一个唯一的递增编号。
    在一个任期内,最多只能有一个领导者。
    如果选举失败或领导者故障,会开启一个新的任期。

结构

image
image

和 Redis 对比

  1. 两者都是 key-value 类型存储
  2. Redis 是 C 写的,而 etcd 是 Go 写的
  3. Redis 支持多种数据类型,而 etcd 不支持
  4. Redis 支持 Lua 编程,而 etcd 不支持
  5. Redis 支持简单的权限控制,而 etcd 不支持
  6. Redis 采用 RESP 私有协议,而 etcd 采用 GRPC 或 HTTP (JSON)
  7. Redis 支持复制集的方式同步数据,而 etcd 则是通过 Raft 实现强一致性
  8. Redis 通过 RDB (快照) / AOF (增量) 的方式持久化

部署 (Ubuntu)

sudo apt install -y etcd

etcd --version

sudo systemctl start etcd
sudo systemctl status etcd

部署 (Docker)

docker-compose.yml:

version: "3"

networks:
    etcd_net:
        driver: bridge
        ipam:
            driver: default
            config:
                - subnet: ${NETWORK_CONFIG_SUBNET}

services:
    etcd-0:
        networks:
            etcd_net:
                ipv4_address: ${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}
        image: quay.io/coreos/etcd:latest
        ports:
            - ${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}:4001:4001
            - ${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}:2380:2380
            - ${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}:2379:2379
        hostname: etcd-0
        environment:
            - GOMAXPROCS=2
        command: >-
            /usr/local/bin/etcd
            -name etcd-0
            -advertise-client-urls http://etcd-0:2379,http://etcd-0:4001
            -listen-client-urls http://${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}:2379,http://${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}:4001
            -initial-advertise-peer-urls http://etcd-0:2380
            -listen-peer-urls http://${ETCD_01_NETWORKS_ETCD_NET_ADDRESS}:2380
            -initial-cluster-token etcd-cluster
            -initial-cluster etcd-0=http://etcd-0:2380,etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380
            -initial-cluster-state new
    etcd-1:
        networks:
            etcd_net:
                ipv4_address: ${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}
        image: quay.io/coreos/etcd:latest
        ports:
            - ${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}:4001:4001
            - ${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}:2380:2380
            - ${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}:2379:2379
        hostname: etcd-1
        environment:
            - GOMAXPROCS=2
        command: >-
            /usr/local/bin/etcd
            -name etcd-1
            -advertise-client-urls http://etcd-1:2379,http://etcd-1:4001
            -listen-client-urls http://${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}:2379,http://${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}:4001
            -initial-advertise-peer-urls http://etcd-1:2380
            -listen-peer-urls http://${ETCD_02_NETWORKS_ETCD_NET_ADDRESS}:2380
            -initial-cluster-token etcd-cluster
            -initial-cluster etcd-0=http://etcd-0:2380,etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380
            -initial-cluster-state new
    etcd-2:
        networks:
            etcd_net:
                ipv4_address: ${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}
        image: quay.io/coreos/etcd:latest
        ports:
            - ${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}:4001:4001
            - ${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}:2380:2380
            - ${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}:2379:2379
        hostname: etcd-2
        environment:
            - GOMAXPROCS=2
        command: >-
            /usr/local/bin/etcd
            -name etcd-2
            -advertise-client-urls http://etcd-2:2379,http://etcd-2:4001
            -listen-client-urls http://${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}:2379,http://${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}:4001
            -initial-advertise-peer-urls http://etcd-2:2380
            -listen-peer-urls http://${ETCD_03_NETWORKS_ETCD_NET_ADDRESS}:2380
            -initial-cluster-token etcd-cluster
            -initial-cluster etcd-0=http://etcd-0:2380,etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380
            -initial-cluster-state new

etcdctl

etcdctl - A simple command line client for etcd.

ENDPOINTS=ip:port,ip:port,ip:port

# set
etcdctl --endpoints=$ENDPOINTS put test1 111
etcdctl --endpoints=$ENDPOINTS put test2 222
etcdctl --endpoints=$ENDPOINTS put test3 333

# get
etcdctl --endpoints=$ENDPOINTS get test1
etcdctl --endpoints=$ENDPOINTS get test1 --write-out="json"
etcdctl --endpoints=$ENDPOINTS get test --prefix

# delete
etcdctl --endpoints=$ENDPOINTS del test1
etcdctl --endpoints=$ENDPOINTS del test --prefix

# 集群信息
etcdctl --write-out=table --endpoints=$ENDPOINTS endpoint status
etcdctl --write-out=table --endpoints=$ENDPOINTS member list

示例 (Golang)

go get go.etcd.io/etcd/client/v3
package main

import (
    "context"
    "fmt"
    "time"

    etcd "go.etcd.io/etcd/client/v3"
)

func main() {
    cli, err := etcd.New(etcd.Config{
        Endpoints:   []string{"192.168.31.204:2379"},
        DialTimeout: 3 * time.Second,
    })
    if err != nil {
        fmt.Printf("etcd ConnectError: %v\n", err)
        return
    }
    defer cli.Close()

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    resp, err := cli.Get(ctx, "test1")
    cancel()
    if err != nil {
        fmt.Printf("etcd GetError: %v\n", err)
    }

    for _, kv := range resp.Kvs {
        fmt.Printf("etcd KeyVlue: %s, %s\n", kv.Key, kv.Value)
    }
}

示例 (Python)

python3 -m pip install -u etcd3
import etcd3
etcd = etcd3.client(host='etcd-host-01', port=2379)

testkey = '/key'
testval = '1234'

etcd.put(testkey, testval)

v = etcd.get(testkey)
print(v)
assert v == testval

etcd.delete(testkey)

参考资料与拓展阅读

#3 Go 微服务生态

2021-06-30

micro

曾经最优秀的,最有前途的 Go 微服务框架。

但是后来发生了一些事...

框架的所有者是 Micro 公司,在 v3 的时候(2020 年下半年),他们决定更改项目定位。

公司运营一个云平台,叫做 m3o,专门提供 micro 项目托管,概念叫做 Micro as a Service。

新版本 micro/micro shields.io 目标由微服务框架改成云原生平台,并且协议由 Apache License 2.0 变更成了 Polyform Shield License(我查了一下,这个协议大意是说如果使用者和所有者发生竞争,则授权取消)。

原项目 microhq/go-micro 进行归档,不再维护。
后来项目转给了他们公司 CTO 账户下 asim/go-micro shields.io,继续维护。
再后来项目更名为 nitro (asim/nitro, gonitro/nitro),还搞了官网啥的。
再后来又放弃了,退回 asim/go-micro。
乱!

云原生就云原生,原有项目没有精力维护也没什么关系,做出这些决定一会儿一变,真是作得一批。
新协议更加是令人无语,杜绝竞争这一条就是堵住了所有商业使用的可能性(据说 nitro 时期也曾一度变更为 Polyform 协议)。

总之,我觉得这个框架要被玩死。

如果像我一样刚接触 Go 微服务,我觉得可以跳过 micro,或者仅用于学习参考。

gokit

A standard library for microservices.
https://github.com/go-kit/kit shields.io

它的定位和 micro 不一样,micro 是一个大而全的微服务框架,而 gokit 是一些在微服务中有用的工具,用于开发自己的框架。

国产项目

kratos

go-kratos 是一个基于 Go 语言的轻量级微服务开发框架,由 Bilibili 交给开源社区维护,采用 MIT 开源协议。项目致力于提供云原生应用开发所需的基础能力,帮助开发团队快速构建结构清晰、易于测试和维护的后端服务。

核心功能包括:HTTP/gRPC 双协议支持,通过 Protocol Buffers(Protobuf)定义 API 并生成代码;提供中间件机制,支持日志、认证、参数校验、链路追踪和指标监控;支持可插拔的配置管理、服务注册与发现,以及统一的错误处理机制。框架不强制绑定数据库、ORM 或特定基础设施,便于按需集成现有技术栈。

go-kratos 适合构建微服务、内部 API 服务及大型后端系统。项目还提供 kratos-layout 参考模板,包含分层架构、依赖注入和代码生成示例,可作为新项目的脚手架。需要注意的是,Kratos 主要解决服务端基础架构问题,并不直接提供完整的用户中心、权限管理或管理后台。

项目地址:github.com/go-kratos/kratos

Kratos 的设计核心是:框架提供统一接口和基础机制,具体基础设施通过标准库、独立 Go Module 或外部服务接入。它不是开箱即用的完整业务后台,也不会强制绑定数据库、缓存或注册中心。

功能模块 核心技术 / 依赖 实现方案
HTTP 服务 net/http、Kratos HTTP Transport 封装 HTTP Server、路由、请求上下文和中间件
gRPC 服务 google.golang.org/grpc 基于 gRPC Server、Interceptor 和 Protobuf 生成代码
API 定义与代码生成 Protobuf、protoc、protoc-gen-go、Kratos CLI 先定义 .proto,再生成请求结构、客户端及服务端接口
依赖注入 Google Wire(脚手架常用) 在启动阶段组装 Repository、Service、Transport 等对象
日志 Kratos Log 接口、Go log/slog 统一日志接口,具体输出后端可以替换
请求校验 Protobuf 校验规则、Validate Middleware 根据请求结构执行参数校验,失败时返回统一错误
身份认证 JWT Middleware(Contrib) 校验 Token、提取身份信息;用户登录和 Token 签发由业务实现
授权 Casbin -
权限控制 Middleware、业务授权逻辑 在接口层进行访问控制;复杂 RBAC 需要自行集成授权组件
链路追踪 OpenTelemetry 通过 Middleware 传播 Trace Context,采集跨服务调用链
指标监控 OpenTelemetry Metrics、Prometheus 等 采集请求数量、耗时、错误率等指标,并接入监控系统
配置管理 Kratos Config、YAML 等 统一加载配置;远程配置通过独立适配模块(Apollo、etcd、Nacos、Consul)接入
服务注册与发现 registry 接口、etcd / Consul / Nacos 等 实现注册、服务发现和实例变更监听
限流与熔断 Kratos Middleware 服务端限流、客户端熔断;分布式限流通常需要额外实现或集成
错误处理 Kratos Errors、HTTP/gRPC 状态映射 统一错误码和错误信息,映射为协议对应的响应
编码与序列化 JSON Codec、Protobuf 统一请求和响应编码,可扩展其他 Codec
数据持久化 自选 SQL 驱动、数据库或 ORM Kratos 不内置业务 Repository,应用自行实现数据访问层
  • Transport 层:HTTP / gRPC · Middleware · API 校验
  • Service 层:业务逻辑 · 认证授权 · 错误处理
  • Data / Repository 层:数据库 · 缓存 · 外部 API · 消息队列
  • 基础设施:配置、日志、指标、链路追踪、服务发现

业务层依赖抽象接口,基础设施层负责具体实现,启动入口负责依赖组装:

type UserRepository interface {
    Create(ctx context.Context, name string) error
}

// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----

// 用户业务逻辑
type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) CreateUser(
    ctx context.Context, name string,
) error {
    return s.repo.Create(ctx, name)
}

// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----

type MySQLUserRepository struct {
    db *sql.DB
}

func (r *MySQLUserRepository) Create(
    ctx context.Context, name string,
) error {
    _, err := r.db.ExecContext(
        ctx,
        "INSERT INTO users (name) VALUES (?)",
        name,
    )
    return err
}

Jupiter

注意:这个项目 2024 年开始就不怎么活跃了。

Jupiter 是斗鱼开源的 Go 微服务治理框架,项目地址:douyu/jupiter。它不仅提供 HTTP、gRPC 等服务开发能力,更注重微服务在生产环境中的运行、监控与治理,适用于中大型后端系统和分布式业务平台。

Jupiter 的核心功能包括:服务通信与治理,支持 HTTP、gRPC、服务注册发现、负载均衡、限流、超时和重试等机制;可观测性,提供统一日志、指标采集、分布式链路追踪和错误处理能力;配置管理,支持配置驱动的组件初始化与运行时管理;开发工具链,提供项目脚手架、标准化项目模板和开发调试工具,降低项目搭建与维护成本。

其设计理念是以应用为中心,对常用第三方组件进行统一封装,减少基础设施差异对业务代码的影响,并将监控、配置和治理能力整合到应用运行时中。

适用场景: 需要统一技术栈、完善可观测性和服务治理能力的 Go 微服务体系。相比 Gin、Echo 等侧重 HTTP 请求处理的 Web 框架,Jupiter 的定位更偏向企业级微服务基础设施。代价是引入的组件和治理体系更多,轻量级单体应用未必需要完整采用。

go-zero

其作者在知乎上比较活跃:万俊峰Kevin, Title 是晓黑板 CTO。

go-zero 是一个基于 Go 语言开发的开源微服务框架,旨在帮助开发者快速构建高性能、高可用、易于维护的分布式系统。它提供 API 网关、RPC 服务、服务发现、负载均衡、限流熔断、超时控制、缓存、链路追踪等能力,覆盖微服务从开发到运行的主要环节。

go-zero 的核心特点是 API 定义驱动开发:开发者可以通过 .api 文件描述 HTTP 接口,并使用 goctl 自动生成路由、控制器、逻辑层及相关代码;对于 RPC 服务,则可通过 Protocol Buffers 定义接口并生成代码,减少重复劳动,统一项目结构。框架内置服务治理机制,能够降低分布式系统的开发与运维复杂度。

在架构设计上,go-zero 强调分层组织、依赖隔离和工程规范,适合构建用户服务、订单系统及其他业务微服务。相比轻量级 Web 框架 Gin,go-zero 提供了更完整的微服务基础设施;但对于简单的单体应用,其整体约束和学习成本也相对更高。

参考资料与拓展阅读

#1 云原生

2021-01-23

网络上各种定义,都说是引用自某个大公司或者知名组织,看得人眼花缭乱、云里雾里。
我还是坚持我一贯的态度,大部分名词都是不同利益团体炒作出来的营销概念,世界不会因这些概念而美好半分,我们应该透过现象看本质。