热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ETCD十三启动ETCDServer过程

etcdServer启动总览etcd服务端的启动包括两大块:etcdServer主进程,直接或者间接包含了raftNode、WAL、Snapshott


etcd Server 启动总览

etcd 服务端的启动包括两大块:


  • etcdServer 主进程,直接或者间接包含了 raftNode、WAL、Snapshotter 等多个核心组件,可以理解为一个容器;

  • 另一块则是 raftNode,对内部 Raft 协议实现的封装,暴露简单的接口,用来保证写事务的集群一致性。

etcd 可分为 Client 客户端层、API 网络接口层、etcd Raft 算法层、逻辑层和 etcd 存储层。如下图所示:

etcd 服务端对 EtcdServer 结构进行了抽象,其包含了 raftNode 属性,代表 Raft 集群中的一个节点。

etcd server 入口:

//etcdmain/main.go:25
func Main(args []string) {checkSupportArch()if len(args) > 1 {cmd := args[1]switch cmd {case "gateway", "grpc-proxy":if err := rootCmd.Execute(); err != nil {fmt.Fprint(os.Stderr, err)os.Exit(1)}return}}startEtcdOrProxyV2(args)
}

// 位于 etcdmain/etcd.go:52
func startEtcdOrProxyV2() {grpc.EnableTracing &#61; falsecfg :&#61; newConfig()defaultInitialCluster :&#61; cfg.ec.InitialCluster// 异常日志处理defaultHost, dhErr :&#61; (&cfg.ec).UpdateDefaultClusterFromName(defaultInitialCluster)var stopped <-chan struct{}var errc <-chan error// identifyDataDirOrDie 返回 data 目录的类型which :&#61; identifyDataDirOrDie(cfg.ec.GetLogger(), cfg.ec.Dir)if which !&#61; dirEmpty {switch which {// 以何种模式启动 etcdcase dirMember:stopped, errc, err &#61; startEtcd(&cfg.ec)case dirProxy:err &#61; startProxy(cfg)default:lg.Panic(..)}} else {shouldProxy :&#61; cfg.isProxy()if !shouldProxy {stopped, errc, err &#61; startEtcd(&cfg.ec)if derr, ok :&#61; err.(*etcdserver.DiscoveryError); ok && derr.Err &#61;&#61; v2discovery.ErrFullCluster {if cfg.shouldFallbackToProxy() {shouldProxy &#61; true}}}if shouldProxy {err &#61; startProxy(cfg)}}if err !&#61; nil {// ... 有省略// 异常日志记录}osutil.HandleInterrupts(lg)notifySystemd(lg)select {case lerr :&#61; <-errc:lg.Fatal("listener failed", zap.Error(lerr))case <-stopped:}osutil.Exit(0)
}

 根据上述实现&#xff0c;我们可以绘制出如下的 startEtcdOrProxyV2 调用流程图&#xff1a;

 

我们来具体解释一下上图中的每一个步骤。


  • cfg :&#61; newConfig()用于初始化配置&#xff0c;cfg.parse(os.Args[1:])&#xff0c;随后从第二个参数开始解析命令行输入参数。

  • setupLogging()&#xff0c;用于初始化日志配置。

  • identifyDataDirOrDie&#xff0c;判断 data 目录的类型&#xff0c;有 dirMember、dirProxy、dirEmpty&#xff0c;分别对应 etcd 目录、Proxy 目录和空目录。etcd 首先根据 data 目录的类型&#xff0c;判断启动 etcd 还是启动代理。如果是 dirEmpty&#xff0c;再根据命令行参数是否指定了 proxy 模式来判断。

  • startEtcd&#xff0c;核心的方法&#xff0c;用于启动 etcd&#xff0c;我们将在下文讲解这部分内容。

  • osutil.HandleInterrupts(lg) 注册信号&#xff0c;包括 SIGINT、SIGTERM&#xff0c;用来终止程序&#xff0c;并清理系统。

  • notifySystemd(lg)&#xff0c;初始化完成&#xff0c;监听对外的连接。

  • select()&#xff0c;监听 channel 上的数据流动&#xff0c;异常捕获与等待退出。

  • osutil.Exit()&#xff0c;接收到异常或退出的命令。

通过上述流程&#xff0c;我们可以看到 startEtcdOrProxyV2 的重点是 startEtcd。下面我们就来具体分析其启动的过程。


startEtcd 启动 etcd 服务

// startEtcd runs StartEtcd in addition to hooks needed for standalone etcd.
func startEtcd(cfg *embed.Config) (<-chan struct{}, <-chan error, error) {e, err :&#61; embed.StartEtcd(cfg)if err !&#61; nil {return nil, nil, err}osutil.RegisterInterruptHandler(e.Close)select {case <-e.Server.ReadyNotify(): // wait for e.Server to join the clustercase <-e.Server.StopNotify(): // publish aborted from &#39;ErrStopped&#39;}return e.Server.StopNotify(), e.Err(), nil
}

startEtcd启动 etcd 服务主要是通过调用StartEtcd方法&#xff0c;该方法的实现位于 embed 包&#xff0c;用于启动 etcd 服务器和 HTTP 处理程序&#xff0c;以进行客户端/服务器通信。

// 位于 embed/etcd.go:92
func StartEtcd(inCfg *Config) (e *Etcd, err error) {// 校验 etcd 配置if err &#61; inCfg.Validate(); err !&#61; nil {return nil, err}serving :&#61; false// 根据合法的配置&#xff0c;创建 etcd 实例e &#61; &Etcd{cfg: *inCfg, stopc: make(chan struct{})}cfg :&#61; &e.cfg// 为每个 peer 创建一个 peerListener(rafthttp.NewListener)&#xff0c;用于接收 peer 的消息if e.Peers, err &#61; configurePeerListeners(cfg); err !&#61; nil {return e, err}// 创建 client 的 listener(transport.NewKeepAliveListener) contexts 的 map&#xff0c;用于服务端处理客户端的请求if e.sctxs, err &#61; configureClientListeners(cfg); err !&#61; nil {return e, err}for _, sctx :&#61; range e.sctxs {e.Clients &#61; append(e.Clients, sctx.l)}// 创建 etcdServerif e.Server, err &#61; etcdserver.NewServer(srvcfg); err !&#61; nil {return e, err}e.Server.Start()// 在 rafthttp 启动之后&#xff0c;配置 peer Handlerif err &#61; e.servePeers(); err !&#61; nil {return e, err}// ...有删减return e, nil
}

根据上述代码&#xff0c;我们可以总结出如下的调用步骤&#xff1a;


  • inCfg.Validate()检查配置是否正确&#xff1b;

  • e &#61; &Etcd{cfg: *inCfg, stopc: make(chan struct{})}创建一个 etcd 实例&#xff1b;

  • configurePeerListeners 为每个 peer 创建一个 peerListener(rafthttp.NewListener)&#xff0c;用于接收 peer 的消息&#xff1b;

  • configureClientListeners 创建 client 的 listener(transport.NewKeepAliveListener)&#xff0c;用于服务端处理客户端的请求&#xff1b;

  • etcdserver.NewServer(srvcfg)创建一个 etcdServer 实例&#xff1b;

  • 启动etcdServer.Start()&#xff1b;

  • 配置 peer handler。

其中etcdserver.NewServer(srvcfg)etcdServer.Start()分别用于创建一个 etcdServer 实例和启动 etcd&#xff0c;下面我们就分别介绍一下这两个步骤。


 服务端初始化

服务端初始化涉及比较多的业务操作&#xff0c;包括 etcdServer 的创建、启动 backend、启动 raftNode 等&#xff0c;下面我们具体介绍这些操作。


NewServer 创建实例

NewServer 方法用于创建一个 etcdServer 实例&#xff0c;我们可以根据传递过来的配置创建一个新的 etcdServer&#xff0c;在 etcdServer 的生存期内&#xff0c;该配置被认为是静态的。

我们来总结一下 etcd Server 的初始化涉及的主要方法&#xff0c;如下内容&#xff1a;

NewServer() |-v2store.New() // 创建 store&#xff0c;根据给定的命名空间来创建初始目录|-wal.Exist() // 判断 wal 文件是否存在|-fileutil.TouchDirAll // 创建文件夹|-openBackend // 使用当前的 etcd db 返回一个 backend|-restartNode() // 已有 WAL&#xff0c;直接根据 SnapShot 启动&#xff0c;最常见的场景|-startNode() // 在没有 WAL 的情况下&#xff0c;新建一个节点 |-tr.Start // 启动 rafthttp|-time.NewTicker() 通过创建 &EtcdServer{} 结构体时新建 tick 时钟

需要注意的是&#xff0c;我们要在 kv 键值对重建之前恢复租期。当恢复 mvcc.KV 时&#xff0c;重新将 key 绑定到租约上。如果先恢复 mvcc.KV&#xff0c;它有可能在恢复之前将 key 绑定到错误的 lease。

另外就是最后的清理逻辑&#xff0c;在没有先关闭 kv 的情况下关闭 backend&#xff0c;可能导致恢复的压缩失败&#xff0c;并出现 TX 错误。


启动 backend

创建好 etcdServer 实例之后&#xff0c;另一个重要的操作便是启动 backend。backend 是 etcd 的存储支撑&#xff0c;openBackend调用当前的 db 返回一个 backend。openBackend方法的具体实现如下&#xff1a;

// 位于 etcdserver/backend.go:68
func openBackend(cfg ServerConfig) backend.Backend {// db 存储的路径fn :&#61; cfg.backendPath()now, beOpened :&#61; time.Now(), make(chan backend.Backend)go func() {// 单独协程启动 backendbeOpened <- newBackend(cfg)}()// 阻塞&#xff0c;等待 backend 启动&#xff0c;或者 10s 超时select {case be :&#61; <-beOpened:return becase <-time.After(10 * time.Second):// 超时&#xff0c;db 文件被占用)}return <-beOpened
}

可以看到&#xff0c;我们在openBackend的实现中首先创建一个 backend.Backend 类型的 chan&#xff0c;并使用单独的协程启动 backend&#xff0c;设置启动的超时时间为 10s。beOpened <- newBackend(cfg)主要用来配置 backend 启动参数&#xff0c;具体的实现则在 backend 包中。

etcd 底层的存储基于 boltdb&#xff0c;使用newBackend方法构建 boltdb 需要的参数&#xff0c;bolt.Open(bcfg.Path, 0600, bopts)在给定路径下创建并打开数据库&#xff0c;其中第二个参数为打开文件的权限。如果该文件不存在&#xff0c;将自动创建。传递 nil 参数将使 boltdb 使用默认选项打开数据库连接。


启动 Raft

NewServer的实现中&#xff0c;我们可以基于条件语句判断 Raft 的启动方式&#xff0c;具体实现如下&#xff1a;

switch {case !haveWAL && !cfg.NewCluster:// startNodecase !haveWAL && cfg.NewCluster:// startNodecase haveWAL:// restartAsStandaloneNode// restartNodedefault:return nil, fmt.Errorf("unsupported Bootstrap config")
}

haveWAL变量对应的表达式为wal.Exist(cfg.WALDir())&#xff0c;用来判断是否存在 WAL&#xff0c;cfg.NewCluster则对应 etcd 启动时的--initial-cluster-state&#xff0c;标识节点初始化方式&#xff0c;该配置默认为new&#xff0c;对应的变量 haveWAL 的值为 true。new 表示没有集群存在&#xff0c;所有成员以静态方式或 DNS 方式启动&#xff0c;创建新集群&#xff1b;existing 表示集群存在&#xff0c;节点将尝试加入集群。

在三种不同的条件下&#xff0c;raft 对应三种启动的方式&#xff0c;分别是&#xff1a;startNode、restartAsStandaloneNode 和 restartNode。下面我们将结合判断条件&#xff0c;具体介绍这三种启动方式。

 startNode

在如下的两种条件下&#xff0c;raft 将会调用 raft 中的startNode方法。

- !haveWAL && cfg.NewCluster
- !haveWAL && !cfg.NewCluster
- startNode(cfg, cl, cl.MemberIDs())
- startNode(cfg, cl, nil)
// startNode 的定义
func startNode(cfg ServerConfig, cl *membership.RaftCluster, ids []types.ID) (id types.ID, n raft.Node, s *raft.MemoryStorage, w *wal.WAL) ;

可以看到&#xff0c;这两个条件下都会调用 startNode 方法&#xff0c;只不过调用的参数有差异。在没有 WAL 日志&#xff0c;并且是新配置结点的场景下&#xff0c;需要传入集群的成员 ids&#xff0c;如果加入已有的集群则不需要。

我们以其中的一种 case&#xff0c;具体分析&#xff1a;

case !haveWAL && !cfg.NewCluster:// 加入现有集群时检查初始配置&#xff0c;如有问题则返回错误if err &#61; cfg.VerifyJoinExisting(); err !&#61; nil {return nil, err}// 使用提供的地址映射创建一个新 raft 集群cl, err &#61; membership.NewClusterFromURLsMap(cfg.Logger, cfg.InitialClusterToken, cfg.InitialPeerURLsMap)if err !&#61; nil {return nil, err}// GetClusterFromRemotePeers 采用一组表示 etcd peer 的 URL&#xff0c;并尝试通过访问其中一个 URL 上的成员端点来构造集群existingCluster, gerr :&#61; GetClusterFromRemotePeers(cfg.Logger, getRemotePeerURLs(cl, cfg.Name), prt)if gerr !&#61; nil {return nil, fmt.Errorf("cannot fetch cluster info from peer urls: %v", gerr)}if err &#61; membership.ValidateClusterAndAssignIDs(cfg.Logger, cl, existingCluster); err !&#61; nil {return nil, fmt.Errorf("error validating peerURLs %s: %v", existingCluster, err)}// 校验兼容性if !isCompatibleWithCluster(cfg.Logger, cl, cl.MemberByName(cfg.Name).ID, prt) {return nil, fmt.Errorf("incompatible with current running cluster")}remotes &#61; existingCluster.Members()cl.SetID(types.ID(0), existingCluster.ID())cl.SetStore(st)cl.SetBackend(be)// 启动 raft Nodeid, n, s, w &#61; startNode(cfg, cl, nil)cl.SetID(id, existingCluster.ID())

从上面的主流程来看&#xff0c;首先是做配置的校验&#xff0c;然后使用提供的地址映射创建一个新的 raft 集群&#xff0c;校验加入集群的兼容性&#xff0c;最后启动 raft Node。

StartNode 基于给定的配置和 raft 成员列表&#xff0c;返回一个新的节点&#xff0c;它将每个给定 peer 的 ConfChangeAddNode 条目附加到初始日志中。peers 的长度不能为零&#xff0c;如果长度为零将调用 RestartNode 方法。

RestartNode 与 StartNode 类似&#xff0c;但不包含 peers 列表&#xff0c;集群的当前成员关系将从存储中恢复。如果调用方存在状态机&#xff0c;则传入已应用到该状态机的最新一个日志索引值&#xff1b;否则直接使用零作为参数。

重启 raft Node

当已存在 WAL 文件时&#xff0c;raft Node 启动时首先需要检查响应文件夹的读写权限&#xff08;当集群初始化之后&#xff0c;discovery token 将不会生效&#xff09;&#xff1b;接着将会加载快照文件&#xff0c;并从 snapshot 恢复 backend 存储。

cfg.ForceNewCluster对应 etcd 配置中的--force-new-cluster&#xff0c;如果为 true&#xff0c;则会强制创建一个新的单成员集群&#xff1b;否则重新启动 raft Node。

restartAsStandaloneNode

--force-new-cluster配置为 true 时&#xff0c;则会调用 restartAsStandaloneNode&#xff0c;即强制创建一个新的单成员集群。该节点将会提交配置更新&#xff0c;强制删除集群中的所有成员&#xff0c;并添加自身作为集群的一个节点&#xff0c;同时我们需要将其备份设置进行还原。

restartAsStandaloneNode 的实现中&#xff0c;首先读取 WAL 文件&#xff0c;并且丢弃本地未提交的 entries。createConfigChangeEnts 创建一系列 Raft 条目&#xff08;即 EntryConfChange&#xff09;&#xff0c;用于从集群中删除一组给定的 ID。如果当前节点self出现在条目中&#xff0c;也不会被删除&#xff1b;如果self不在给定的 ID 内&#xff0c;它将创建一个 Raft 条目以添加给定的self默认成员&#xff0c;随后强制追加新提交的 entries 到现有的数据存储中。

最后就是设置一些状态&#xff0c;构造 raftNode 的配置&#xff0c;重启 raft Node。

restartNode

在已有 WAL 数据的情况中&#xff0c;除了restartAsStandaloneNode场景&#xff0c;当--force-new-cluster为默认的 false 时&#xff0c;直接重启 raftNode。这种操作相对来说比较简单&#xff0c;减少了丢弃本地未提交的 entries 以及强制追加新提交的 entries 的步骤。接下来要做的就是直接重启 raftNode 还原之前集群节点的状态&#xff0c;读取 WAL 和快照数据&#xff0c;最后启动并更新 raftStatus。


rafthttp 启动

分析完 raft Node 的启动&#xff0c;接下来我们看 rafthttp 的启动。Transport 实现了 Transporter 接口&#xff0c;它提供了将 raft 消息发送到 peer 并从 peer 接收 raft 消息的功能。我们需要调用 Handler 方法来获取处理程序&#xff0c;以处理从 peerURLs 接收到的请求。用户需要先调用 Start 才能调用其他功能&#xff0c;并在停止使用 Transport 时调用 Stop。

rafthttp 的启动过程中首先要构建 Transport&#xff0c;并将 m.PeerURLs 分别赋值到 Transport 中的 Remote 和 Peer 中&#xff0c;之后将 srv.r.transport 指向构建好的 Transport 即可。


启动 etcd 服务端

接下来就是 etcd 的真正启动了&#xff0c;我们来看主要调用步骤&#xff1a;

// 位于 embed/etcd.go:220
e.Server.Start()
// 接收 peer 消息
if err &#61; e.servePeers(); err !&#61; nil {
return e, err
}
// 接收客户端请求
if err &#61; e.serveClients(); err !&#61; nil {
return e, err
}
// 提供导出 metrics
if err &#61; e.serveMetrics(); err !&#61; nil {
return e, err
}
serving &#61; true

启动 etcd Server&#xff0c;包括三个主要的步骤&#xff1a;首先e.Server.Start初始化 Server 启动的必要信息&#xff1b;接着实现集群内部通讯&#xff1b;最后开始接收 peer 和客户端的请求&#xff0c;包括 range、put 等请求。

 e.Server.Start

在处理请求之前&#xff0c;Start方法初始化 Server 的必要信息&#xff0c;需要在DoProcess之前调用&#xff0c;且必须是非阻塞的&#xff0c;任何耗时的函数都必须在单独的协程中运行。Start方法的实现中还启动了多个 goroutine&#xff0c;这些协程用于选举时钟设置以及注册自身信息到服务器等异步操作。

集群内部通信

集群内部的通信主要由 Etcd.servePeers 实现&#xff0c;在 rafthttp.Transport 启动之后&#xff0c;配置集群成员的处理器。首先生成 http.Handler 来处理 etcd 集群成员的请求&#xff0c;并做一些配置校验。goroutine 读取 gRPC 请求&#xff0c;然后调用 srv.Handler 处理这些请求。srv.Serve总是返回非空的错误&#xff0c;当 Shutdown 或者 Close 时&#xff0c;返回的错误则是 ErrServerClosed。最后srv.Serve在独立协程启动对集群成员的监听。

处理客户端请求

Etcd.serveClients主要用来处理客户端请求&#xff0c;比如我们常见的 range、put 等请求。etcd 处理客户端的请求&#xff0c;每个客户端的请求对应一个 goroutine 协程&#xff0c;这也是 etcd 高性能的支撑&#xff0c;etcd Server 为每个监听的地址启动一个客户端服务协程&#xff0c;根据 v2、v3 版本进行不同的处理。在serveClients中&#xff0c;还设置了 gRPC 的属性&#xff0c;包括 GRPCKeepAliveMinTime 、GRPCKeepAliveInterval 以及 GRPCKeepAliveTimeout 等。


推荐阅读
  • 本文介绍了如何利用Shell脚本高效地部署MHA(MySQL High Availability)高可用集群。通过详细的脚本编写和配置示例,展示了自动化部署过程中的关键步骤和注意事项。该方法不仅简化了集群的部署流程,还提高了系统的稳定性和可用性。 ... [详细]
  • 本文详细介绍了MySQL数据库的基础语法与核心操作,涵盖从基础概念到具体应用的多个方面。首先,文章从基础知识入手,逐步深入到创建和修改数据表的操作。接着,详细讲解了如何进行数据的插入、更新与删除。在查询部分,不仅介绍了DISTINCT和LIMIT的使用方法,还探讨了排序、过滤和通配符的应用。此外,文章还涵盖了计算字段以及多种函数的使用,包括文本处理、日期和时间处理及数值处理等。通过这些内容,读者可以全面掌握MySQL数据库的核心操作技巧。 ... [详细]
  • 本文详细介绍了 InfluxDB、collectd 和 Grafana 的安装与配置流程。首先,按照启动顺序依次安装并配置 InfluxDB、collectd 和 Grafana。InfluxDB 作为时序数据库,用于存储时间序列数据;collectd 负责数据的采集与传输;Grafana 则用于数据的可视化展示。文中提供了 collectd 的官方文档链接,便于用户参考和进一步了解其配置选项。通过本指南,读者可以轻松搭建一个高效的数据监控系统。 ... [详细]
  • Unity与MySQL连接过程中出现的新挑战及解决方案探析 ... [详细]
  • 服务器部署中的安全策略实践与优化
    服务器部署中的安全策略实践与优化 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 在ElasticStack日志监控系统中,Logstash编码插件自5.0版本起进行了重大改进。插件被独立拆分为gem包,每个插件可以单独进行更新和维护,无需依赖Logstash的整体升级。这不仅提高了系统的灵活性和可维护性,还简化了插件的管理和部署过程。本文将详细介绍这些编码插件的功能、配置方法,并通过实际生产环境中的应用案例,展示其在日志处理和监控中的高效性和可靠性。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 本文详细介绍了一种利用 ESP8266 01S 模块构建 Web 服务器的成功实践方案。通过具体的代码示例和详细的步骤说明,帮助读者快速掌握该模块的使用方法。在疫情期间,作者重新审视并研究了这一未被充分利用的模块,最终成功实现了 Web 服务器的功能。本文不仅提供了完整的代码实现,还涵盖了调试过程中遇到的常见问题及其解决方法,为初学者提供了宝贵的参考。 ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • 思科IOS XE与ISE集成实现TACACS认证配置
    本文详细介绍了如何在思科IOS XE设备上配置TACACS认证,并通过ISE(Identity Services Engine)进行用户管理和授权。配置包括网络拓扑、设备设置和ISE端的具体步骤。 ... [详细]
  • MySQL Decimal 类型的最大值解析及其在数据处理中的应用艺术
    在关系型数据库中,表的设计与SQL语句的编写对性能的影响至关重要,甚至可占到90%以上。本文将重点探讨MySQL中Decimal类型的最大值及其在数据处理中的应用技巧,通过实例分析和优化建议,帮助读者深入理解并掌握这一重要知识点。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
author-avatar
魔豆从容_368
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有