在有些程序人写完了他们的 Go 应用之后,这总会成为一个大问题——“我刚写的这个 Go 应用,当它崩溃的时候我要怎么重启?”,因为你没法用 go run main.go 或者 ./main 这样的命令让它持续运行,并且当程序崩溃的时候能够重启。
一个普通使用的好办法是使用 Docker。但是,设置 Docker 以及为容器配置你的应用需要花费时间,当你的程序需要和 MySQL、Redis 这样的服务器/进程交互时更是如此。对于一个大型或长期项目来说,毋庸置疑这是一个正确的选择。但是如果在你手上的是个小应用,你想要快速部署并且实时地服务器上查看状态,那么你可能需要考虑别的选择。
另一个选择就是在你的 Linux 服务器上创建一个守护进程,然后让它作为一个服务运行,但是这需要花费一些额外的工夫。而且,如果你并不具备 Linux 系统和服务相关的知识的话,这就不是一件简单的事情了。所以,这里有一个最简单的解决方案——使用 Supervisor[1] 来部署你的 Go 应用,然后它会为你处理好其余的工作。它是一个能够帮你监控你的应用程序并在其崩溃时进行重启的工具。
本文是 Go语言中文网组织的 GCTT 翻译,发布在 Go语言中文网公众号,转载请联系我们授权。
安装
安装 Supervisor 相当简单,在 Ubuntu 上这条命令就会在你的系统上安装 Supervisor。
sudo apt install supervisor
然后你需要将 Supervisor 添加到系统的用户组中:
sudo addgroup --system supervisor
现在,在创建 Supervisor 的配置文件之前,我们先写一个简单的 Go 程序。这个程序将会读取 .env 文件中的配置项,然后和 MySQL 数据库进行交互。代码如下:
(为了方便演示,我们会让代码简单些)
```
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
type User struct {
Email string `json:"email"`
Password string `json:"password"`
}
var db *sql.DB
func init() {
var err error
err = godotenv.Load()
if err != nil {
log.Println("Error readin .env: ", err)
os.Exit(1)
}
dbUserName := os.Getenv("DB_USERNAME")
dbPassword := os.Getenv("DB_PASSWORD")
dbNAME := os.Getenv("DB_NAME")
dsn := dbUserName + ":" + dbPassword + "@/" + dbNAME
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Println(err)
os.Exit(1)
}
}
func main() {
r := mux.NewRouter()
r.Use(middleware)
r.HandleFunc("/", rootHandler)
r.HandleFunc("/user", createUserHandler).Methods("POST")
fmt.Println("Listening on :8070")
if err := http.ListenAndServe(":8070", r); err != nil {
// 退出程序
log.Println("Failed starting server ", err)
os.Exit(1)
}
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is root handler")
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
user := &User{}
err := json.NewDecoder(r.Body).Decode(user)
// 对请求响应 JSON 数据
// 在实际应用中你可能想要创建一个进行错误处理的函数
if err != nil {
// 我们也可以这么做
// errREsp := `"error": "Invalid input", "status": 400`
// w.Header().Set("Content-Type", "application/json")
// w.WriteHeader(400)
// w.Write([]byte(errREsp))
// 然而我们会让服务器崩溃
log.Fatal(err)
return
}
// 在实际应用中必须对密码进行哈希,可以使用 bcrypt 算法
_, err = db.Exec("INSERT INTO users(email, password) VALUES(?,?)", user.Email, user.Password)
if err != nil {
log.Println(err)
// 简单起见,发送明文字符串
// 创建一个有效的 JSON 响应
errREsp := `"error": "Internal error", "status": 500` // 返回 500 状态码,因为这是我们而非用户的问题
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.Write([]byte(errREsp))
return
}
}
// 一个简单的中间件,只用来记录请求的 URI
var middleware = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestPath := r.URL.Path
log.Println(requestPath)
next.ServeHTTP(w, r) // 在中间件调用链中进行处理!
})
}
```
现在,如果我们想要用 Supervisor 来运行这个程序,我们需要构建程序的二进制文件。同时在项目的根目录下创建一个 .env 文件 —— 如果你想把配置文件和项目放在一起的话,在这个文件中写上 MySQL 数据库需要的变量。
将这个仓库克隆到你想要运行的服务器上。确保你遵循了 Go 目录路径的惯例:
```
$ Go build .
```
Go 的这个命令最终会创建一个以项目根目录命名的二进制文件,所以如果项目的根目录是 myapp,那么文件的名称就是 myapp。
现在,在服务器上创建 Supervisor 的配置文件 /etc/supervisor/conf.d。
```
#/etc/supervisor/conf.d/myapp.conf
[program:myapp]
directory=/root/gocode/src/github.com/monirz/myapp
command=/root/gocode/src/github.com/monirz/myapp/myapp
autostart=true
autorestart=true
stderr_logfile=/var/log/myapp.err
stdout_logfile=/var/log/myapp.log
environment=CODENATION_ENV=prod
environment=GOPATH="/root/gocode"
```
这里的 directory 和 command 变量很重要。directory 变量应该设置为项目的根目录,因为程序将会尝试在 directory 指定的路径下读取 .env 文件或是其他需要的配置文件。autorestart 变量设置为 true,这样当程序崩溃时就会重启。
现在通过下面的命令重新加载 Supervisor:
```
$ sudo supervisorctl reload
```
来检查下它的状态。
```
$ sudo supervisorctl status
```
一切都正确配置的话,你应该会看到类似下面的输出内容:
```
myapp RUNNING pid 2023, uptime 0:00:03
```
我们名为 myapp 的 Go 服务端程序正在后台运行。
现在向我们刚写的 API 发起一些请求。首先检查 rootHandler 是否正在工作。然后向 /user 结点发送一个包含无效 JSON 格式数据的请求。这应当会让服务器崩溃。但是服务器上没有存储任何日志,不是吗?因为我们还没有实现日志功能?
等等,Supervisor 实际上已经为我们处理了日志。如果你到 /var/log 目录下查看 myapp.log 文件,你就会看到它记录着已经向服务器发起过的请求的 URI 路径。
```
$ cat /var/log/myapp.log
```
错误日志也是如此。好了,我们的服务器程序已经运行了——崩溃的话会重启,还会记录每个请求和错误信息。我觉得我们应该是在大约 5 分钟以内做完了这些事吧?(大概是吧,谁在乎呢。)但关键是,用 Supervisor 来部署和监控你的 Go 应用程序时十分简单的。
- go入门
- go基础
- go语言介绍
- go语言主要特性
- Golang内置类型和函数
- init函数和main函数
- 下划线
- iota
- 字符串
- 数据类型:数组与切片
- 数据类型:byte、rune与字符串
- 变量的5种创建方式
- 数据类型:字典
- 指针
- 数据类型:指针
- 类型断言
- 流程控制:defer延迟执行
- defer陷进
- 异常机制:panic和recover
- go函数
- go方法
- go依赖管理
- 轻松搞懂goroot与gopath区别
- 使用go module导入本地包的方法教程详解
- 读取用户的输入
- 文件读写
- 文件拷贝
- 从命令行读取参数
- JSON 数据格式
- 4 种常见JSON 格式数据解码
- XML 数据格式
- 用 Gob 传输数据
- Go 中的密码学
- 学习资料建议
- 深入结构体
- 测试
- 单元测试
- 常用标准库
- fmt
- time
- flag
- log
- IO操作
- 文件读取
- strconv
- template
- http
- context
- json
- 从文件中反序列化json对象
- xml
- go proxy 设置
- 面向对象
- 结构体
- struct能不能比较
- 接口
- make和new的区别
- go进阶
- Slice底层实现
- 闭包与递归
- 空接口
- 反射
- 接口中的“坑”
- 反射三定律
- 结构体里的tag
- 并发编程
- 初识Go 协程:goroutine
- go协程:管道
- 任务和master-锁实现和通道实现
- 惰性生成器的实现
- runtime包
- Goroutine池
- 定时器
- 并发安全和锁
- Sync
- 原子操作(atomic包)
- GMP 原理与调度
- 爬虫案例
- 邮件发送
- Godoc 安装与使用
- test
- 如何测试
- 基准测试
- 数组与切片
- 结构体,方法和接口
- Map实现原理
- 自定义error
- 网络编程
- socket编程
- 互联网协议
- tcp 服务器
- tcp编程
- UDP编程
- TCP黏包
- http编程
- websocket编程
- 设计模式
- 设置模式6大原则
- 创建型模式
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
- 创建者模式
- 原型模式
- 单例模式
- 结构性模式
- 外观模式
- 适配器模式
- 代理模式
- 组合模式
- 享元模式
- 装饰模式
- 桥模式
- 行为型模式
- 中介者模式
- 观察者模式
- 命令模式
- 迭代器模式
- 模板方法模式
- 策略模式
- 状态模式
- 备忘录模式
- 解释器模式
- 职责链模式
- 访问者
- rpc
- Golang内存分配逃逸分析
- 面试题汇总
- 信号量的原理与使用
- 如何让在强制转换类型时不发生内存拷贝
- Go 如何利用 Linux 内核的负载均衡能力
- 性能优化:Go Ballast 让内存控制更加丝滑
- unsafe包详解
- go实战
- Go语言中编码规范
- json如何转为struct对象
- cobra
- 通过go mod模式创建cobra项目
- gorm
- gocache
- zap日志库
- echart
- web技术
- niugo
- context回调实现原理
- 认证与授权
- oauth2.0的4种实现方式
- IRIS
- 安装
- 入门
- 自定义http错误
- 基本HTTP API
- 中间件
- session
- websocket
- mvc
- cookie使用
- Casbin
- CORS跨域资源共享
- csrf防御
- jwt
- 限制HTTP请求次数的中间件Tollbooth
- 文件服务
- 基础使用
- 文件下载
- hero依赖注入与结构体转化
- hero基础
- 网络教程
- gin
- viper
- 在 5 分钟之内部署一个 Go 应用(Supervisor )
- go如何正常go get导入包
- 杂项
- 开源许可证
- 算法
- 洗牌算法
- 经典算法
- 基排序
- 冒泡排序
- 选择排序算法
- 二叉树
- 堆排序
- 快速排序
- 二分查找
- 图算法
- 有向图结构实现
- 拓扑排序
- 一致性hash算法
- 前缀树(字典树)
- 算法实现
- 斐波拉契
- 加密算法
- 简单可逆加密
- DH密钥交换(Diffie–Hellman key exchange)算法
- 代码实现
- Polybius密码(棋盘密码
- xor加密算法
- go应用
- 调试
- 构建并运行
- 包别名
- 类型转换
- error错误的2种处理方式
- 使用defer实现代码追踪
- 计算函数执行时间
- 通过内存缓存来提升性能
- make和new
- 关闭的channel可以读取数据吗
- 如何优雅的关闭channel
- channel应用场景
- map相关问题
- Go 面向包的设计和架构分层
- 设计模式实战
- 模板模式
- 责任链模式
- 组合模式实战
- 观察者模式实战
- 状态模式实战
- 区块链
- 构建一个区块链 -- Part 1: 基本原型
- 构建一个区块链 -- Part 2: 工作量证明
- 构建一个区块链 -- Part 3:持久化和命令行接口
- 从0到精通
- go常用命令
- 获取命令行参数
- http服务
- 基础
- struct 5种实例化
- md5
- Go Protobuf入门
