概述
Web 应用开发完成并测试通过之后,接下来就需要进行部署和维护的工作。工作内容包括:记录程序产生的日志、处理程序发生的错误、部署程序及更新、数据备份和恢复等。
应用日志
Go 语言中自带的 log 包可以记录日志,但是它只是包含了简单的日志记录功能,如果想把应用日志保存到文件,然后又能够结合日志实现很多复杂的功能,需要使用第三方开发的一个日志系统,比如: seelog,它实现了很强大的日志功能。
seelog 介绍
seelog 是用 Go 语言实现的一个日志系统,它提供了一些简单的函数来实现复杂的日志分配、过滤和格式化。主要有如下特性:
- XML 的动态配置,可以不用重新编译程序而动态的加载配置信息
- 支持热更新,能够动态改变配置而不需要重启应用
- 支持多输出流,能够同时把日志输出到多种流中、例如文件流、网络流等
- 支持不同的日志输出:命令行输出、文件输出、缓存输出、log rotate、SMTP邮件
项目中使用示例:
package main
import log "github.com/cihub/seelog"
func main() {
defer log.Flush()
log.Info("Hello from Seelog!")
}
编译后运行如果出现了 1564713478889704315 [Info] Hello from Seelog!
,说明 seelog 日志系统已经成功安装并且可以正常运行了。
基于 seelog 的自定义日志处理
seelog 支持自定义日志处理,下面是基于它自定义的日志处理包的部分内容:
package logs
import (
"fmt"
"github.com/cihub/seelog"
)
var Logger seelog.LoggerInterface
func loadAppConfig() {
appConfig := `
<seelog minlevel="warn">
<outputs formatid="common">
<rollingfile type="size" filename="./data/logs/roll.log" maxsize="100000" maxrolls="5"/>
<filter levels="critical">
<file path="./data/logs/critical.log" formatid="critical"/>
<smtp formatid="criticalemail" senderaddress="xxx@gmail.com" sendername="ShortUrl API" hostname="smtp.xxx.com" hostport="587" username="mailusername" password="mailpassword">
<recipient address="xxx@gmail.com"/>
</smtp>
</filter>
</outputs>
<formats>
<format id="common" format="%Date/%Time [%LEV] %Msg%n" />
<format id="critical" format="%File %FullPath %Func %Msg%n" />
<format id="criticalemail" format="Critical error on our server!\n %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
</formats>
</seelog>
`
logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig))
if err != nil {
fmt.Println(err)
return
}
UseLogger(logger)
}
func init() {
DisableLog()
loadAppConfig()
}
// DisableLog disables all library log output
func DisableLog() {
Logger = seelog.Disabled
}
// UseLogger uses a specified seelog.LoggerInterface to output library log.
// Use this func if you are using Seelog logging system in your app.
func UseLogger(newLogger seelog.LoggerInterface) {
Logger = newLogger
}
上面主要实现了三个函数:
-
DisableLog
初始化全局变量 Logger 为 seelog 的禁用状态,主要为了防止 Logger 被多次初始化 -
loadAppConfig
根据配置文件初始化 seelog 的配置信息,这里把配置文件通过字符串读取设置好了,当然也可以通过读取 XML 文件。里面的配置说明如下:-
seelog
minlevel 参数可选,如果被配置高于或等于此级别的日志会被记录,同理 maxlevel。 -
outputs
输出信息的目的地,这里分成了两份数据,一份记录到 log rotate 文件里面。另一份设置了 filter,如果这个错误级别是 critical,那么将发送报警邮件。 -
formats
定义了各种日志的格式
-
-
UseLogger
设置当前的日志器为相应的日志处理
上面定义了一个自定义的日志处理包,下面就是使用示例:
package main
import (
"logs"
)
func main() {
addr := "127.0.0.1:8080"
logs.Logger.Info("Start server at:", addr)
err := "系统异常 500"
logs.Logger.Critical("Server err:", err)
}
编译后运行,在 /data/logs
目录下 critical.log 文件里日志记录:use.go /Users/play/goweb/src/dm/use.go main.main Server err:系统异常 500
,roll.log 文件里日志记录:2019-08-02/11:24:32 [CRT] Server err:系统异常 500
发生错误发送邮件:
<smtp formatid="criticalemail" senderaddress="xxx@gmail.com" sendername="ShortUrl API" hostname="smtp.xxx.com" hostport="587" username="mailusername" password="mailpassword">
<recipient address="xxx@gmail.com"/>
</smtp>
邮件的格式通过 criticalemail 配置,然后通过其他的配置发送邮件服务器的配置,通过 recipient 配置接收邮件的用户,如果有多个用户可以再添加一行。
要测试这个代码是否正常工作,可以在代码中增加类似下面的一个假消息
logs.Logger.Critical("test Critical message")
现在只要应用在线上记录一个 Critical 的信息,接收的邮箱就会收到一个 Email ,这样一旦线上的系统出现问题,你就能立马通过邮件获知,就能及时的进行处理。
使用应用日志
应用日志可以用做:数据分析、性能分析、用户行为分析、记录问题。
假如需要跟踪用户尝试登录系统的操作。这里会把成功与不成功的尝试都记录下来。记录成功的使用 "Info" 日志级别,而不成功的使用 "warn" 级别。如果想查找所有不成功的登录,可以利用 linux 的 grep 之类的命令工具,如下:
# cat /data/logs/roll.log | grep "failed login"
2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password
通过这种方式就可以很方便的查找相应的信息,这样有利于针对应用日志做一些统计和分析。另外还需要考虑日志的大小,对于一个高流量的 Web 应用来说,日志的增长是相当可怕的,所以在 seelog 的配置文件里面设置了 logrotate,这样就能保证日志文件不会因为不断变大而导致我们的磁盘空间不够引起问题。
网站错误处理
Web 应用一旦上线之后,日常运行中可能出现多种错误,具体如下所示:
-
数据库错误:
指与访问数据库服务器或数据相关的错误。例如,以下可能出现的一些数据库错误。-
连接错误:
这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在。 -
查询错误:
使用的 SQL 非法导致错误,这样的 SQL 错误如果程序经过严格的测试应该可以避免。 -
数据错误:
数据库中的约束冲突,例如一个唯一字段中插入一条重复主键的值就会报错,但是如果应用程序在上线之前经过了严格的测试也是可以避免这类问题。
-
-
应用运行时错误:
这类错误范围很广,涵盖了代码中出现的几乎所有错误。可能的应用错误的情况如下:-
文件系统和权限:
应用读取不存在的文件,或者读取没有权限的文件、或者写入一个不允许写入的文件,这些都会导致一个错误。应用读取的文件如果格式不正确也会报错,例如配置文件应该是 ini 的配置格式,而设置成了 json 格式就会报错。 -
第三方应用:
如果应用程序耦合了其他第三方接口程序,例如应用程序发表文章之后自动调用接发微博的接口,所以这个接口必须正常运行才能完成发表一篇文章的功能。
-
-
HTTP 错误:
这些错误是根据用户的请求出现的错误,最常见的就是 404 错误。虽然可能会出现很多不同的错误,但其中比较常见的错误还有 401 未授权错误(需要认证才能访问的资源)、403 禁止错误(不允许用户访问的资源)和 503 错误(程序内部出错)。 -
操作系统出错:
这类错误都是由于应用程序上的操作系统出现错误引起的,主要有操作系统的资源被分配完了,导致死机,还有操作系统的磁盘满了,导致无法写入,这样就会引起很多错误。 -
网络出错:
指两方面的错误,一方面是用户请求应用程序的时候出现网络断开,这样就导致连接中断,这种错误不会造成应用程序的崩溃,但是会影响用户访问的效果;另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败,这种需要对应用程序做有效的测试,能够避免这类问题出现的情况下程序崩溃。
错误处理的目标
在实现错误处理之前,必须明确错误处理想要达到的目标是什么,错误处理系统应该完成以下工作:
-
通知访问用户出现错误了:
不论出现的是一个系统错误还是用户错误,用户都应当知道 Web 应用出了问题,用户的这次请求无法正确的完成了。例如,对于用户的错误请求,显示一个统一的错误页面(404.html)。出现系统错误时,通过自定义的错误页面显示系统暂时不可用之类的错误页面(error.html)。 -
记录错误:
系统出现错误,一般就是调用函数的时候返回 err 不为 nil 的情况,可以使用前面小节介绍的日志系统记录到日志文件。如果是一些致命错误,则通过邮件通知系统管理员。一般 404 之类的错误不需要发送邮件,只需要记录到日志系统。 -
回滚当前的请求操作:
如果一个用户请求过程中出现了一个服务器错误,那么已完成的操作需要回滚。下面来看一个例子:一个系统将用户递交的表单保存到数据库,并将这个数据递交到一个第三方服务器,但是第三方服务器挂了,这就导致一个错误,那么先前存储到数据库的表单数据应该删除(应告知无效),而且应该通知用户系统出现错误了。 -
保证现有程序可运行可服务:
没有人能保证程序一定能够一直正常的运行着,万一哪一天程序崩溃了,那么需要记录错误,然后立刻让程序重新运行起来,让程序继续提供服务,然后再通知系统管理员,通过日志等找出问题。
如何处理错误
以下是处理不同的错误的示例:
- 通知用户出现错误:
通知用户在访问页面的时候可以有两种错误:404.html 和error.html,下面分别显示了错误页面的源码:
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>找不到页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>404!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
另一个源码:
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>系统错误页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>系统暂时不可用!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
404 的错误处理逻辑,如果是系统的错误也是类似的操作,同时看到在:
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayhelloName(w, r)
return
}
NotFound404(w, r)
return
}
func NotFound404(w http.ResponseWriter, r *http.Request) {
log.Error("页面找不到") //记录错误日志
t, _ = t.ParseFiles("tmpl/404.html", nil) //解析模板文件
ErrorInfo := "文件找不到" //获取当前用户信息
t.Execute(w, ErrorInfo) //执行模板的merger操作
}
func SystemError(w http.ResponseWriter, r *http.Request) {
log.Critical("系统错误") //系统错误触发了Critical,那么不仅会记录日志还会发送邮件
t, _ = t.ParseFiles("tmpl/error.html", nil) //解析模板文件
ErrorInfo := "系统暂时不可用" //获取当前用户信息
t.Execute(w, ErrorInfo) //执行模板的merger操作
}
如何处理异常
在很多其它语言中有 try...catch 关键词,用来捕获异常情况,但是其实很多错误都是可以预期发生的,而不需要异常处理,应该当做错误来处理,这也是为什么 Go 语言采用了函数返回错误的设计,这些函数不会 panic ,例如如果一个文件找不到,os.Open 返回一个错误,它不会 panic;如果你向一个中断的网络连接写数据,net.Conn 系列类型的 Write 函数返回一个错误,它们不会 panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败,因为设计者已经用返回错误清楚地表明了这一点。这就是上面所讲的可以预期发生的错误。
但是还有一种情况,有一些操作几乎不可能失败,而且在一些特定的情况下也没有办法返回错误,也无法继续执行,这样情况就应该 panic。举个例子:如果一个程序计算 x[j],但是 j 越界了,这部分代码就会导致 panic,像这样的一个不可预期严重错误就会引起 panic,在默认情况下它会杀掉进程,它允许一个正在运行这部分代码的 goroutine 从发生错误的 panic 中恢复运行,发生 panic 之后,这部分代码后面的函数和代码都不会继续执行,这是 Go 特意这样设计的,因为要区别于错误和异常,panic 其实就是异常处理。如下代码,我们期望通过 uid 来获取 User 中的 username 信息,但是如果 uid 越界了就会抛出异常,这个时候如果没有 recover 机制,进程就会被杀死,从而导致程序不可服务。因此为了程序的健壮性,在一些地方需要建立 recover 机制。
func GetUser(uid int) (username string) {
defer func() {
if x := recover(); x != nil {
username = ""
}
}()
username = User[uid]
return
}
上面介绍了错误和异常的区别,那么在开发程序的时候如何来设计呢?规则很简单:如果你定义的函数有可能失败,它就应该返回一个错误。当我调用其他 package 的函数时,如果这个函数实现的很好,我不需要担心它会 panic,除非有真正的异常情况发生,即使那样也不应该是我去处理它。而 panic 和 recover 是针对自己开发 package 里面实现的逻辑,针对一些特殊情况来设计。
应用部署
Go 的应用程序部署,可以利用第三方工具来管理,第三方的工具有很多,例如 Supervisord、upstart、daemontools 等。
Supervisord 介绍
Supervisord 是用 Python 实现的一款非常实用的进程管理工具。supervisord 会帮你把管理的应用程序转成 daemon 程序,而且可以方便的通过命令开启、关闭、重启等操作,而且它管理的进程一旦崩溃会自动重启,这样就可以保证程序执行中断后的情况下有自我修复的功能。
Supervisord 安装
Supervisord 可以通过 sudo easy_install supervisor
安装,当然也可以通过 Supervisord 官网下载后解压并转到源码所在的文件夹下执行 setup.py install
来安装。
使用 easy_install 必须安装 setuptools:
打开 http://pypi.python.org/pypi/setuptools#files
,根据你系统的 python 的版本下载相应的文件,然后执行 sh setuptoolsxxxx.egg,这样就可以使用 easy_install 命令来安装 Supervisord。
Supervisord 配置
Supervisord 默认的配置文件路径为 /etc/supervisord.conf,通过文本编辑器修改这个文件,下面是一个示例的配置文件:
;/etc/supervisord.conf
[unix_http_server]
file = /var/run/supervisord.sock
chmod = 0777
chown= root:root
[inet_http_server]
# Web管理界面设定
port=9001
username = admin
password = yourpassword
[supervisorctl]
; 必须和'unix_http_server'里面的设定匹配
serverurl = unix:///var/run/supervisord.sock
[supervisord]
logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
user=root ; (default is current user, required if root)
childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP)
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
; 管理的单个进程的配置,可以添加多个program
[program:blogdemon]
command=/data/blog/blogdemon
autostart = true
startsecs = 5
user = root
redirect_stderr = true
stdout_logfile = /var/log/supervisord/blogdemon.log
Supervisord 管理
Supervisord 安装完成后有两个可用的命令行 supervisor 和 supervisorctl,命令使用解释如下:
- supervisord,初始启动 Supervisord,启动、管理配置中设置的进程。
- supervisorctl stop programxxx,停止某一个进程 (programxxx),programxxx 为 [program:blogdemon] 里配置的值,这个示例就是 blogdemon。
- supervisorctl start programxxx,启动某个进程
- supervisorctl restart programxxx,重启某个进程
- supervisorctl stop all,停止全部进程,注:start、restart、stop 都不会载入最新的配置文件。
- supervisorctl reload,载入最新的配置文件,并按新的配置启动、管理所有进程。
备份和恢复
维护人员需要对生产服务器上的数据做好备份和恢复的工作,比如做好异地灾备,冷备热备和恢复数据的准备。
应用备份
在大多数集群环境下,Web 应用程序基本不需要备份,因为这个其实就是一个代码副本,在本地开发环境中,或者版本控制系统中已经保持这些代码。但是很多时候,一些开发的站点需要用户来上传文件,那么需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存,这样即使系统崩溃,只要文件还在云存储上,至少数据不会丢失。
如果没有采用云储存的情况下,可以使用一个文件同步工具 rsync:rsync 能够实现网站的备份,不同系统的文件的同步。rysnc 的官方网站:rsync.samba.org/ 。
MySQL 备份
MySQL 的备份有两种方式:热备份和冷备份,热备份目前主要是采用 master/slave 方式。冷备份就是数据有一定的延迟,但是可以保证该时间段之前的数据完整,例如有些时候可能误操作引起了数据的丢失,那么 master/slave 模式是无法找回丢失数据的,但是通过冷备份可以部分恢复数据。
MySQL 恢复
MySQL 备份分为热备份和冷备份,热备份主要的目的是为了能够实时的恢复,例如应用服务器出现了硬盘故障,那么可以通过修改配置文件把数据库的读取和写入改成 slave,这样就可以尽量少时间的中断服务。
但是有时候需要通过冷备份的 SQL 来进行数据恢复,既然有了数据库的备份,就可以通过命令导入:
mysql -u username -p databse < backup.sql
redis 备份
redis 是目前使用最多的 NoSQL,它的备份也分为两种:热备份和冷备份,redis 也支持 master/slave 模式,所以热备份可以通过这种方式实现。冷备份的方式:redis 其实会定时的把内存里面的缓存数据保存到数据库文件里面,备份只要备份相应的文件就可以,就是利用 rsync 备份到非本地机房就可以实现。
redis 恢复
redis 的恢复分为热备份恢复和冷备份恢复,热备份恢复的目的和方法同 MySQL 的恢复一样,只要修改应用的相应的数据库连接即可。
但是有时候需要根据冷备份来恢复数据,redis 的冷备份恢复其实就是只要把保存的数据库文件 copy 到 redis 的工作目录,然后启动 redis 就可以了,redis 在启动的时候会自动加载数据库文件到内存中,启动的速度根据数据库的文件大小来决定。