• 如何实现零宕机的配置热加载


    对于高可用的服务,为了保证服务可用性,更新配置时必然不能直接停止服务,可以使用配置热加载来避免服务暂停,不需要重启服务。

    配置的热加载可以分为两个场景,手动更新与自动更新。

    手动更新

    对于一些临时调试,服务数量不多的情况下,可以进行手动更新配置。需要实现两点,如何触发更新,以及接受到更新后如何操作。

    触发更新的手段很多,常见的有

    • 通过命令行,例如nginx -s reload
    • 通过信号,通常是SIGHUP,比如sshd、Prometheus等,其实Nginx的热加载内部也是调用SIGHUP信号
    • HTTP接口,例如Prometheus也支持HTTP的方式通过curl -X POST :9090/-/reload可以重新加载配置
    • RPC接口,类似HTTP

    接受到配置更新通知后,需要程序内部来重新加载配置,类似初始化过程,但要注意运行时可以要加锁来保证线程安全。

    自动更新

    自动更新是建立手动更新的基础上,首先服务要提供手动更新的方法,其次可以通过服务本身或者外部进程来自动调用配置更新接口,外部程序可以使用SideCar的形式与服务绑定。

    自动加载配置的关键是如何感知配置变化,要考虑到单机环境与分布式环境。

    单机环境

    Linux提供了inotify接口,可以用来监听文件或者目录的增上改查事件。我们可以使用inotify来监听配置变化,如果有更新则调用更新接口来实现热加载。其他平台也提供了类似的接口。

    在Golang中fsnotify提供了跨平台的文件监听接口,可以方便的监听文件,使用方式如下:

        watcher, _ := fsnotify.NewWatcher()
        defer watcher.Close()
    
        // 监听目录或者文件
        watcher.Add("/tmp")
    
        go func() {
            for {
                // 获取监听事件
                select {
                case event, ok := <-watcher.Events:
                    if !ok {
                        return
                    }
                    log.Println("event:", event)
                    if event.Has(fsnotify.Write) {
                        log.Println("modified file:", event.Name)
                        // 进行更新操作
                    }
                case err, ok := <-watcher.Errors:
                    if !ok {
                        return
                    }
                    log.Println("error:", err)
                }
            }
        }()
    
    • 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

    分布式环境

    在分布式环境中实现配置热更新,需要能够感知配置(本地或者远端),对于本地配置需要平台配合将远端配置同步到本地(比如kubernetes会同步ConfigMap到Pod中),然后按照单机环境的方式来监听文件变化。

    对于远端配置,需要依赖额外的分布式配置中心,比如Apollo、etcd、ZooKeeper等。以etcd为例,etcd提供了watch接口,可以监听对应配置的变化

    // 获取watch Channel
    ch := client.Watch(d.watchContext, d.Prefix, clientv3.WithPrefix())
    
    // 处理事件
    for {
    		select {
    		case wr, ok := <-ch:
    			if !ok {
    				return fmt.Errorf("watch closed")
    			}
    			if wr.Err() != nil {
    				return wr.Err()
    			}
    			for _, ev := range wr.Events {
    				key, val := string(ev.Kv.Key), string(ev.Kv.Value)
    				switch ev.Type {
    				case mvccpb.PUT:
    					// 更新处理逻辑
                        // 1. 对比配置是否变化
                        // 2. 变化了更新内存中的配置
    				case mvccpb.DELETE:
    					// 删除处理逻辑
    				}
    			}
    		}
    	}
    
    • 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

    为了实现配置更新通知,通常有两种方式,Pull与Push。

    • Pull就是客户端轮询,定期查询配置是否更新,这种方式实现简单,对服务器压力小,但时效性低
    • Push由服务端实现,通过维护一个长连接,实时推送数据,这种方式时效性高,但逻辑更复杂,连接过多会影响服务端性能。目前etcd v3版本是通过HTTP2来实现实时数据推送

    总结

    本文主要总结实现配置热更新的多种方式,手动更新可以通过Socket、信号等进程间通信手段来通知服务,自动更新可以通过inotify来感知配置变化,在分布式环境中就需要配合分布式配置中心来进行热更新。

    Explore more in https://qingwave.github.io

  • 相关阅读:
    C++ 循环截取字符串
    virtio代码分析(一)-qemu部分
    Swift学习内容精选(二)
    操作系统学习笔记7 | 进程同步与合作
    你好Avalonia框架
    家政小程序开发,引领家庭服务新时代的科技革命
    Windows11 wsl2编译Android14 使用ASfP Debug windows上启动的模拟器
    【牛客刷题日记】— Javascript 通关秘籍(2)
    FastAPI Python照片打马赛克API
    《Unix 网络编程》13:守护进程和 inetd 超级服务器
  • 原文地址:https://blog.csdn.net/u012986012/article/details/127125344