草庐IT

golang 应用自升级

千里之行,始于足下 2023-03-28 原文

概要

最近遇到一个需求,golang应用部署在远程机器,远程机器在内网,部署之后不方便再次登录此远程机器去升级。

因此,需要golang应用自动检查是否需要升级,如果需要升级,则下载二进制后自升级。

自升级库

golang自升级的库有好几个,比较之后决定采用: https://github.com/jpillora/overseer
此库不是最全面的,但是实现原理和提供的接口比较简单,代码量也不大,便于定制。

overseer 库简介

overseer 将升级的程序启动在主协程上,真正完成功能的部分作为 Program(这个可以当做实际程序的 main 函数)运行。
其中最重要的2个部分是 **Config **和 Fetcher

Config

overseer 通过 Config 结构提供了一些参数来控制自更新。

// Config defines overseer's run-time configuration
type Config struct {
	//Required will prevent overseer from fallback to running
	//running the program in the main process on failure.
	Required bool
	//Program's main function
	Program func(state State)
	//Program's zero-downtime socket listening address (set this or Addresses)
	Address string
	//Program's zero-downtime socket listening addresses (set this or Address)
	Addresses []string
	//RestartSignal will manually trigger a graceful restart. Defaults to SIGUSR2.
	RestartSignal os.Signal
	//TerminateTimeout controls how long overseer should
	//wait for the program to terminate itself. After this
	//timeout, overseer will issue a SIGKILL.
	TerminateTimeout time.Duration
	//MinFetchInterval defines the smallest duration between Fetch()s.
	//This helps to prevent unwieldy fetch.Interfaces from hogging
	//too many resources. Defaults to 1 second.
	MinFetchInterval time.Duration
	//PreUpgrade runs after a binary has been retrieved, user defined checks
	//can be run here and returning an error will cancel the upgrade.
	PreUpgrade func(tempBinaryPath string) error
	//Debug enables all [overseer] logs.
	Debug bool
	//NoWarn disables warning [overseer] logs.
	NoWarn bool
	//NoRestart disables all restarts, this option essentially converts
	//the RestartSignal into a "ShutdownSignal".
	NoRestart bool
	//NoRestartAfterFetch disables automatic restarts after each upgrade.
	//Though manual restarts using the RestartSignal can still be performed.
	NoRestartAfterFetch bool
	//Fetcher will be used to fetch binaries.
	Fetcher fetcher.Interface
}

一般用不到这么多参数,核心的是:

  • Program
  • Fetcher

常用有:

  • Address
  • Addresses
  • MinFetchInterval
  • PreUpgrade

Fetcher

除了 Config,overseer 中另一个重要的接口就是 Fetcher。
Fetcher 接口定义了程序如何初始化和更新

package fetcher

import "io"

// Interface defines the required fetcher functions
type Interface interface {
	//Init should perform validation on fields. For
	//example, ensure the appropriate URLs or keys
	//are defined or ensure there is connectivity
	//to the appropriate web service.
	Init() error
	//Fetch should check if there is an updated
	//binary to fetch, and then stream it back the
	//form of an io.Reader. If io.Reader is nil,
	//then it is assumed there are no updates. Fetch
	//will be run repeatedly and forever. It is up the
	//implementation to throttle the fetch frequency.
	Fetch() (io.Reader, error)
}

overseer 只带了几个实现好了的 Fetcher,可以满足大部分需求,也可以自己继承 Fetcher 接口实现自己的 Fetcher。

简单的自升级示例

演示自动升级,我们需要编译2个版本的程序。

示例如下:

package main

import (
	"fmt"
	"time"

	"github.com/jpillora/overseer"
	"github.com/jpillora/overseer/fetcher"
)

const version = "v0.1"

// 控制自升级
func main() {
	overseer.Run(overseer.Config{
		Program:          actualMain,
		TerminateTimeout: 10 * time.Second,
		Fetcher: &fetcher.HTTP{
			URL:      "http://localhost:9000/selfupgrade",
			Interval: 1 * time.Second,
		},
		PreUpgrade: preUpgrade,
	})
	// mainWithSelfUpdate()
}

// 升级前的动作,参数是下载的程序的临时位置,如果返回 error,则不升级
func preUpgrade(tempBinaryPath string) error {
	fmt.Printf("download binary path: %s\n", tempBinaryPath)
	return nil
}

// 这里一般写是实际的业务,此示例是不断打印 version
func actualMain(state overseer.State) {
	for {
		fmt.Printf("%s: current version: %s\n", time.Now().Format("2006-01-02 15:04:05"), version)
		time.Sleep(3 * time.Second)
	}
}

上面的程序编译后启动。

$ go build -o selfupgrade

$ ./selfupgrade 
2022-05-21 00:46:52: current version: v0.1
2022-05-21 00:46:55: current version: v0.1
2022-05-21 00:46:58: current version: v0.1
2022-05-21 00:47:01: current version: v0.1
2022-05-21 00:47:04: current version: v0.1

启动之后开始不断的打印版本号(间隔3秒)。不要停止此程序。

然后我们修改 version,并且将 actualMain 中的间隔修改为5秒。

const version = "v0.2"  // v0.1 => v0.2

// 。。。 省略。。。

// 这里一般写是实际的业务,此示例是不断打印 version
func actualMain(state overseer.State) {
	for {
		fmt.Printf("%s: current version: %s\n", time.Now().Format("2006-01-02 15:04:05"), version)
		time.Sleep(5 * time.Second)
	}
}

修改之后,再编译一个版本到 ~/tmp 目录(如果不存在提前创建)。
然后启动一个文件服务,我用python自带的方法启动了一个服务,服务端口对应代码中的升级URL("http://localhost:9000/selfupgrade"

$ go build -o ~/tmp/selfupgrade 
$ cd ~/tmp
$ python -m http.server 9000

过一会儿之后,就能看到之前启动程序已经更新。
更新之后版本号变成 v0.2,时间间隔变成了5秒

2022-05-21 01:27:22: current version: v0.1
2022-05-21 01:27:25: current version: v0.1
download binary path: /tmp/overseer-5c0865554eb0f83a
2022-05-21 01:27:28: current version: v0.1
2022-05-21 01:27:31: current version: v0.1
2022-05-21 01:27:34: current version: v0.1
2022-05-21 01:27:37: current version: v0.1
2022-05-21 01:27:37: current version: v0.2
2022-05-21 01:27:42: current version: v0.2
2022-05-21 01:27:47: current version: v0.2

Web服务自升级示例

web服务与之类似,比如:

func actualMainServer(state overseer.State) {
	http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "%s: current version: %s\n", time.Now().Format("2006-01-02 15:04:05"), version)
	}))
	http.ListenAndServe(":8000", nil)
}

将上面函数替换 overseer.Config 的Program即可。

通过观察进程的变化,可以看出升级之后就是将子进程重启,主进程没变。

升级前:

$ ps -ef | ag self
wangyub+    8058    4443  1 09:58 pts/12   00:00:00 ./selfupgrade
wangyub+    8067    8058  0 09:58 pts/12   00:00:00 ./selfupgrade
wangyub+    8130    3548  0 09:59 pts/11   00:00:00 ag self

升级后:

$ ps -ef | ag self
wangyub+    8058    4443  0 09:58 pts/12   00:00:00 ./selfupgrade
wangyub+    8196    8058  0 09:59 pts/12   00:00:00 ./selfupgrade
wangyub+    8266    3548  0 09:59 pts/11   00:00:00 ag self

上面的写法,会导致端口的服务中断一会儿,如果要保持端口持续畅通,可以用官方示例中的写法。

overseer.Run(overseer.Config{
		// 。。。省略。。。
		Address:          ":8000",  // 服务的端口
	})

实际的server中使用 state 中的 Listener。

func actualMainServer(state overseer.State) {
	http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "%s: current version: %s\n", time.Now().Format("2006-01-02 15:04:05"), version)
	}))
	http.Serve(state.Listener, nil)  // 这里使用 state 中的 Listener,也就是 Config中的 Address
}

总结

总的来说,overseer 满足了自升级的各种需求。
但是自带的Fetcher功能比较简单,比如HTTP的Fetcher,升级的过程可能只有一个URL还不够,还有更加复杂的版本检查和比较。
实际场景下可能需要定制一个适合自己应用的Fetcher。

有关golang 应用自升级的更多相关文章

  1. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  4. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  5. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  6. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  7. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

  8. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  9. ruby - 在不使用 RVM 的情况下在 Mac 上卸载和升级 Ruby - 2

    我最近决定从我的系统中卸载RVM。在thispage提出的一些论点说服我:实际上,我的决定是,我根本不想担心Ruby的多个版本。我只想使用1.9.2-p290版本而不用担心其他任何事情。但是,当我在我的Mac上运行ruby--version时,它告诉我我的版本是1.8.7。我四处寻找如何简单地从我的Mac上卸载这个Ruby,但奇怪的是我没有找到任何东西。似乎唯一想卸载Ruby的人运行linux,而使用Mac的每个人都推荐RVM。如何从我的Mac上卸载Ruby1.8.7?我想升级到1.9.2-p290版本,并且我希望我的系统上只有一个版本。 最佳答案

  10. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

随机推荐