【ETCD】【源码阅读】ETCD启动流程源码解读

启动流程的图如下:

1、主函数入口

ETCD 启动的入口在 etcd/server/main.go 文件中。

go 复制代码
package main

import (
	"os"

	"go.etcd.io/etcd/server/v3/etcdmain"
)

func main() {
	etcdmain.Main(os.Args)
}

这里调用了 etcdmain.Main(),这是 ETCD 的实际启动逻辑。

2、etcdmain.Main(os.Args)详解

代码源文件:etcd/server/v3/etcdmain.main.go

go 复制代码
func Main(args []string) {
	// 1. 检查系统架构支持
	checkSupportArch()

	if len(args) > 1 { // // 2. 检查命令行参数
		cmd := args[1]
		switch cmd {
		case "gateway", "grpc-proxy": // // 3. 判断是否运行网关或 gRPC 代理模式
			if err := rootCmd.Execute(); err != nil {
				fmt.Fprint(os.Stderr, err)
				os.Exit(1)
			}
			return
		}
	}

	startEtcdOrProxyV2(args) // // 4. 启动普通 ETCD 节点或代理节点
}
2.1.checkSupportArch()
  • 功能:检查当前运行的系统架构是否被 ETCD 支持。
  • 目的:确保程序在不支持的架构上不会运行,例如特定 ARM 版本可能不被支持。
2.2. 检查命令行参数:
  • args 是启动时传递的命令行参数,args[0] 通常是可执行文件名,args[1] 是实际命令。
2.3. 启动不同模式:
  • 如果命令为 "gateway""grpc-proxy",表示要运行 ETCD 的网关或 gRPC 代理模式。
  • 执行 rootCmd.Execute(),启动对应的命令逻辑。
  • rootCmd 是 Cobra 框架定义的根命令,包含所有子命令和配置。
2.4. 启动普通节点或代理节点:
  • 如果没有匹配到特殊命令,调用 startEtcdOrProxyV2(args)

  • 功能:根据配置和参数,决定是启动标准 ETCD 节点还是运行代理模式。

    ​ 接下来可以深入分析 startEtcdOrProxyV2 函数,该函数负责实际启动 ETCD 实例或代理模式。它会加载配置、初始化组件,并启动 Raft、MVCC 等核心模块。

3、startEtcdOrProxyV2(args)详解

go 复制代码
func startEtcdOrProxyV2(args []string) {
	// 禁用 gRPC 跟踪,优化性能
	grpc.EnableTracing = false

	// 创建一个新的配置对象
	cfg := newConfig()
	// 保存初始集群配置,方便后续处理
	defaultInitialCluster := cfg.ec.InitialCluster

	// 解析传入的命令行参数,将配置赋值给 cfg
	err := cfg.parse(args[1:])
	// 初始化日志记录器,确保所有输出都写入日志。
	lg := cfg.ec.GetLogger()
	// If we failed to parse the whole configuration, print the error using
	// preferably the resolved logger from the config,
	// but if does not exists, create a new temporary logger.
	if lg == nil {
		var zapError error
		// use this logger
		lg, zapError = logutil.CreateDefaultZapLogger(zap.InfoLevel)
		if zapError != nil {
			fmt.Printf("error creating zap logger %v", zapError)
			os.Exit(1)
		}
	}
	lg.Info("Running: ", zap.Strings("args", args))
	if err != nil {
		lg.Warn("failed to verify flags", zap.Error(err))
		switch {
		case errorspkg.Is(err, embed.ErrUnsetAdvertiseClientURLsFlag):
			lg.Warn("advertise client URLs are not set", zap.Error(err))
		}
		os.Exit(1)
	}

	cfg.ec.SetupGlobalLoggers()

	defer func() {
		logger := cfg.ec.GetLogger()
		if logger != nil {
			logger.Sync()
		}
	}()

	// 解析默认集群地址
	defaultHost, dhErr := (&cfg.ec).UpdateDefaultClusterFromName(defaultInitialCluster)
	if defaultHost != "" {
		lg.Info(
			"detected default host for advertise",
			zap.String("host", defaultHost),
		)
	}
	if dhErr != nil {
		lg.Info("failed to detect default host", zap.Error(dhErr))
	}

	// 设置数据目录 如果未指定数据目录,使用默认目录
	if cfg.ec.Dir == "" {
		cfg.ec.Dir = fmt.Sprintf("%v.etcd", cfg.ec.Name)
		lg.Warn(
			"'data-dir' was empty; using default",
			zap.String("data-dir", cfg.ec.Dir),
		)
	}

	var stopped <-chan struct{}
	var errc <-chan error

	// 判断数据目录类型并启动
	which := identifyDataDirOrDie(cfg.ec.GetLogger(), cfg.ec.Dir)
	if which != dirEmpty {
		lg.Info(
			"server has already been initialized",
			zap.String("data-dir", cfg.ec.Dir),
			zap.String("dir-type", string(which)),
		)
		switch which {
        // 启动etcd
		case dirMember:
			stopped, errc, err = startEtcd(&cfg.ec) // 启动 ETCD 服务
		case dirProxy:
			lg.Panic("v2 http proxy has already been deprecated in 3.6", zap.String("dir-type", string(which)))
		default:
			lg.Panic(
				"unknown directory type",
				zap.String("dir-type", string(which)),
			)
		}
	} else {
		lg.Info(
			"Initialize and start etcd server",
			zap.String("data-dir", cfg.ec.Dir),
			zap.String("dir-type", string(which)),
		)
		stopped, errc, err = startEtcd(&cfg.ec)
	}

	// 处理启动错误
	if err != nil {
	   // ... 省略部分代码
		lg.Fatal("discovery failed", zap.Error(err))
	}

	// 信号处理与退出
	osutil.HandleInterrupts(lg)

	// At this point, the initialization of etcd is done.
	// The listeners are listening on the TCP ports and ready
	// for accepting connections. The etcd instance should be
	// joined with the cluster and ready to serve incoming
	// connections.
	notifySystemd(lg)

	select {
	case lerr := <-errc:
		//2 fatal out on listener errors
		lg.Fatal("listener failed", zap.Error(lerr))
	case <-stopped:
	}

	osutil.Exit(0)
}
3.1. 关键调用

startEtcd(&cfg.ec):这是实际启动 ETCD 服务的函数,负责初始化和启动所有必要组件。

identifyDataDirOrDie():检查数据目录类型,决定是启动服务还是退出。

4、startEtcd(&cfg.ec)详解

go 复制代码
// startEtcd runs StartEtcd in addition to hooks needed for standalone etcd.
func startEtcd(cfg *embed.Config) (<-chan struct{}, <-chan error, error) {
	// 是启动 ETCD 服务的核心函数。它负责初始化 ETCD 实例、注册中断信号处理,
	// 并等待服务成功启动或异常停止。
	// 最后,返回相关的停止和错误通道
	e, err := embed.StartEtcd(cfg)

	// 启动失败,立即返回错误
	if err != nil {
		return nil, nil, err
	}
	// 注册中断信号处理
	osutil.RegisterInterruptHandler(e.Close)

	// 等待服务状态
	select {
	case <-e.Server.ReadyNotify(): // wait for e.Server to join the cluster
	case <-e.Server.StopNotify(): // publish aborted from 'ErrStopped'
	}
	return e.Server.StopNotify(), e.Err(), nil
}
4.1 embed.StartEtcd(cfg)

根据传入的配置 cfg,启动一个嵌入式 ETCD 实例。

返回值 eembed.Etcd 实例,封装了 ETCD 服务的所有组件,包括 Raft 节点、存储引擎、gRPC 和 HTTP 接口。

错误处理:如果启动失败,立即返回错误。

4.2 RegisterInterruptHandler
  • 作用 :监听系统中断信号(如 SIGINTSIGTERM),确保在接收到中断信号时,调用 e.Close() 优雅地关闭 ETCD 服务。
  • e.Close:关闭所有资源(网络连接、存储、Raft 等)。

5、embed.StartEtcd(cfg)详解

go 复制代码
// StartEtcd launches the etcd server and HTTP handlers for client/server communication.
// The returned Etcd.Server is not guaranteed to have joined the cluster. Wait
// on the Etcd.Server.ReadyNotify() channel to know when it completes and is ready for use.
func StartEtcd(inCfg *Config) (e *Etcd, err error) {

	// 1.配置验证
	if err = inCfg.Validate(); err != nil {
		return nil, err
	}
	serving := false
	// 初始化 ETCD 实例
	e = &Etcd{cfg: *inCfg, stopc: make(chan struct{})}
	cfg := &e.cfg

	// 如果服务未成功启动,则关闭所有资源和监听器
	defer func() {
		if e == nil || err == nil {
			return
		}
		if !serving {
			// errored before starting gRPC server for serveCtx.serversC
			for _, sctx := range e.sctxs {
				close(sctx.serversC)
			}
		}
		e.Close()
		e = nil
	}()

	if !cfg.SocketOpts.Empty() {
		cfg.logger.Info(
			"configuring socket options",
			zap.Bool("reuse-address", cfg.SocketOpts.ReuseAddress),
			zap.Bool("reuse-port", cfg.SocketOpts.ReusePort),
		)
	}
	e.cfg.logger.Info(
		"configuring peer listeners",
		zap.Strings("listen-peer-urls", e.cfg.getListenPeerURLs()),
	)

	// 配置 Peer(节点间通信)监听器
	if e.Peers, err = configurePeerListeners(cfg); err != nil {
		return e, err
	}

	e.cfg.logger.Info(
		"configuring client listeners",
		zap.Strings("listen-client-urls", e.cfg.getListenClientURLs()),
	)

	// 配置 Client(客户端通信)监听器
	if e.sctxs, err = configureClientListeners(cfg); err != nil {
		return e, err
	}

	for _, sctx := range e.sctxs {
		e.Clients = append(e.Clients, sctx.l)
	}

	var (
		urlsmap types.URLsMap
		token   string
	)
	// 初始化集群信息
	memberInitialized := true
	if !isMemberInitialized(cfg) {
		memberInitialized = false
		urlsmap, token, err = cfg.PeerURLsMapAndToken("etcd")
		if err != nil {
			return e, fmt.Errorf("error setting up initial cluster: %w", err)
		}
	}

	// AutoCompactionRetention defaults to "0" if not set.
	if len(cfg.AutoCompactionRetention) == 0 {
		cfg.AutoCompactionRetention = "0"
	}

	// 设置存储和压缩选项
	autoCompactionRetention, err := parseCompactionRetention(cfg.AutoCompactionMode, cfg.AutoCompactionRetention)
	if err != nil {
		return e, err
	}

	backendFreelistType := parseBackendFreelistType(cfg.BackendFreelistType)

	srvcfg := config.ServerConfig{
		Name:                                     cfg.Name,
		ClientURLs:                               cfg.AdvertiseClientUrls,
		PeerURLs:                                 cfg.AdvertisePeerUrls,
		DataDir:                                  cfg.Dir,
		DedicatedWALDir:                          cfg.WalDir,
		SnapshotCount:                            cfg.SnapshotCount,
		SnapshotCatchUpEntries:                   cfg.SnapshotCatchUpEntries,
		MaxSnapFiles:                             cfg.MaxSnapFiles,
		MaxWALFiles:                              cfg.MaxWalFiles,
		InitialPeerURLsMap:                       urlsmap,
		InitialClusterToken:                      token,
		DiscoveryURL:                             cfg.Durl,
		DiscoveryProxy:                           cfg.Dproxy,
		DiscoveryCfg:                             cfg.DiscoveryCfg,
		NewCluster:                               cfg.IsNewCluster(),
		PeerTLSInfo:                              cfg.PeerTLSInfo,
		TickMs:                                   cfg.TickMs,
		ElectionTicks:                            cfg.ElectionTicks(),
		InitialElectionTickAdvance:               cfg.InitialElectionTickAdvance,
		AutoCompactionRetention:                  autoCompactionRetention,
		AutoCompactionMode:                       cfg.AutoCompactionMode,
		QuotaBackendBytes:                        cfg.QuotaBackendBytes,
		BackendBatchLimit:                        cfg.BackendBatchLimit,
		BackendFreelistType:                      backendFreelistType,
		BackendBatchInterval:                     cfg.BackendBatchInterval,
		MaxTxnOps:                                cfg.MaxTxnOps,
		MaxRequestBytes:                          cfg.MaxRequestBytes,
		MaxConcurrentStreams:                     cfg.MaxConcurrentStreams,
		SocketOpts:                               cfg.SocketOpts,
		StrictReconfigCheck:                      cfg.StrictReconfigCheck,
		ClientCertAuthEnabled:                    cfg.ClientTLSInfo.ClientCertAuth,
		AuthToken:                                cfg.AuthToken,
		BcryptCost:                               cfg.BcryptCost,
		TokenTTL:                                 cfg.AuthTokenTTL,
		CORS:                                     cfg.CORS,
		HostWhitelist:                            cfg.HostWhitelist,
		CorruptCheckTime:                         cfg.ExperimentalCorruptCheckTime,
		CompactHashCheckEnabled:                  cfg.ExperimentalCompactHashCheckEnabled,
		CompactHashCheckTime:                     cfg.ExperimentalCompactHashCheckTime,
		PreVote:                                  cfg.PreVote,
		Logger:                                   cfg.logger,
		ForceNewCluster:                          cfg.ForceNewCluster,
		EnableGRPCGateway:                        cfg.EnableGRPCGateway,
		ExperimentalEnableDistributedTracing:     cfg.ExperimentalEnableDistributedTracing,
		UnsafeNoFsync:                            cfg.UnsafeNoFsync,
		EnableLeaseCheckpoint:                    cfg.ExperimentalEnableLeaseCheckpoint,
		LeaseCheckpointPersist:                   cfg.ExperimentalEnableLeaseCheckpointPersist,
		CompactionBatchLimit:                     cfg.ExperimentalCompactionBatchLimit,
		CompactionSleepInterval:                  cfg.ExperimentalCompactionSleepInterval,
		WatchProgressNotifyInterval:              cfg.ExperimentalWatchProgressNotifyInterval,
		DowngradeCheckTime:                       cfg.ExperimentalDowngradeCheckTime,
		WarningApplyDuration:                     cfg.ExperimentalWarningApplyDuration,
		WarningUnaryRequestDuration:              cfg.WarningUnaryRequestDuration,
		ExperimentalMemoryMlock:                  cfg.ExperimentalMemoryMlock,
		ExperimentalTxnModeWriteWithSharedBuffer: cfg.ExperimentalTxnModeWriteWithSharedBuffer,
		ExperimentalBootstrapDefragThresholdMegabytes: cfg.ExperimentalBootstrapDefragThresholdMegabytes,
		ExperimentalMaxLearners:                       cfg.ExperimentalMaxLearners,
		V2Deprecation:                                 cfg.V2DeprecationEffective(),
		ExperimentalLocalAddress:                      cfg.InferLocalAddr(),
		ServerFeatureGate:                             cfg.ServerFeatureGate,
	}

	if srvcfg.ExperimentalEnableDistributedTracing {
		tctx := context.Background()
		tracingExporter, terr := newTracingExporter(tctx, cfg)
		if terr != nil {
			return e, terr
		}
		e.tracingExporterShutdown = func() {
			tracingExporter.Close(tctx)
		}
		srvcfg.ExperimentalTracerOptions = tracingExporter.opts

		e.cfg.logger.Info(
			"distributed tracing setup enabled",
		)
	}

	srvcfg.PeerTLSInfo.LocalAddr = srvcfg.ExperimentalLocalAddress

	print(e.cfg.logger, *cfg, srvcfg, memberInitialized)

	// 创建服务实例
	if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
		return e, err
	}

	// buffer channel so goroutines on closed connections won't wait forever
	e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs))

	// newly started member ("memberInitialized==false")
	// does not need corruption check
	if memberInitialized && srvcfg.ServerFeatureGate.Enabled(features.InitialCorruptCheck) {
		if err = e.Server.CorruptionChecker().InitialCheck(); err != nil {
			// set "EtcdServer" to nil, so that it does not block on "EtcdServer.Close()"
			// (nothing to close since rafthttp transports have not been started)

			e.cfg.logger.Error("checkInitialHashKV failed", zap.Error(err))
			e.Server.Cleanup()
			e.Server = nil
			return e, err
		}
	}

	// 启动服务
	e.Server.Start()

	e.servePeers()

	e.serveClients()

	// 用于暴露 ETCD 的 监控指标
	if err = e.serveMetrics(); err != nil {
		return e, err
	}

	e.cfg.logger.Info(
		"now serving peer/client/metrics",
		zap.String("local-member-id", e.Server.MemberID().String()),
		zap.Strings("initial-advertise-peer-urls", e.cfg.getAdvertisePeerURLs()),
		zap.Strings("listen-peer-urls", e.cfg.getListenPeerURLs()),
		zap.Strings("advertise-client-urls", e.cfg.getAdvertiseClientURLs()),
		zap.Strings("listen-client-urls", e.cfg.getListenClientURLs()),
		zap.Strings("listen-metrics-urls", e.cfg.getMetricsURLs()),
	)
	serving = true
	return e, nil
}
5.1. 配置验证

验证配置是否合法。例如,检查必填项是否缺失

5.2. 初始化 ETCD 实例

创建一个新的 ETCD 实例,并初始化用于控制服务停止的通道 stopc

5.3.延迟清理(defer 块)

在函数退出时,确保资源被正确释放。如果服务未成功启动,则关闭所有资源和监听器。

5.4. 配置网络监听器
配置 Peer(节点间通信)监听器

为节点间通信设置监听器,方便集群内部通过 Raft 协议进行通信。

配置 Client(客户端通信)监听器:

为客户端请求设置监听器,处理外部对 ETCD 的访问。

5.5.初始化集群信息

如果当前节点是新成员,则生成初始集群信息,包括节点 URL 和集群令牌。

5.6.设置存储和压缩选项:

设置数据压缩和存储的保留策略,以控制存储空间占用。

5.7.创建服务实例

基于配置创建 Raft 节点和存储服务。

5.8.启动服务
go 复制代码
e.Server.Start()
e.servePeers()
e.serveClients()

启动 Raft 节点,监听节点间和客户端的请求。

5.9 记录启动完成日志并返回结果
  • 记录服务启动成功的信息,包括节点 ID 和监听的 URL。
  • 返回已启动的 ETCD 实例
相关推荐
nongcunqq1 小时前
abap 操作 excel
java·数据库·excel
rain bye bye2 小时前
calibre LVS 跑不起来 就将setup 的LVS Option connect下的 connect all nets by name 打开。
服务器·数据库·lvs
阿里云大数据AI技术3 小时前
云栖实录|MaxCompute全新升级:AI时代的原生数据仓库
大数据·数据库·云原生
不剪发的Tony老师4 小时前
Valentina Studio:一款跨平台的数据库管理工具
数据库·sql
weixin_307779134 小时前
在 Microsoft Azure 上部署 ClickHouse 数据仓库:托管服务与自行部署的全面指南
开发语言·数据库·数据仓库·云计算·azure
六元七角八分4 小时前
pom.xml
xml·数据库
虚行4 小时前
Mysql 数据同步中间件 对比
数据库·mysql·中间件
奥尔特星云大使4 小时前
mysql读写分离中间件Atlas安装部署及使用
数据库·mysql·中间件·读写分离·atlas
牛马baby5 小时前
【mysql】in 用到索引了吗?
数据库·mysql·in
杀气丶5 小时前
L2JBR - 修复数据库编码为UTF8
数据库·sql·oracle