建议你在将Golang API投入生成之前阅读此文,此文是基于真实的线上问题修复经历,如有巧合,纯属踩雷!
01 架构
我们在整个系统架构中使用了微服务模式。 有一个网关 API (我们称之为主 API )为我们的用户(移动和网络)提供 API。 它的角色类似于 API 网关,所以它的任务只处理来自用户的请求,然后调用所需的服务,并向用户构建响应。 此AP服务完全由 Golang 来编写。
我们已经为我们的主API挣扎了很长一段时间,这些 API 总是被关闭且总是长时间处于无法响应的状态,有时导致我们的 API 无法访问,服务也处于无法使用状态。
API监控仪表盘显示红色警报,老实说,当我们的 API监控仪表盘变成红色时,是一件非常非常危险的事情,会给我们的工程师带来压力、恐慌和崩溃。
我们的 CPU 和内存使用率也正在变得越来越高。 如果发生这种情况,我们只需无助的手动重新启动服务,然后等待它再次重新运行。
这个 bug 真的让我们很沮丧,因为我们没有任何关于这个 bug 的日志。 我们只知道响应时间很长。 Cpu 和内存使用量不断增加。 这就像一场噩梦。
阶段1: 使用定制的 http.Client
在开发这个服务时,我们真正学到的一件事是:“不要相信默认配置,切记”。我们使用一个内置的 http客户端,而不是使用默认的一个从 http 的包
client:=http.Client{} //default
我们根据需要添加一些配置。 因为我们需要重新连接,所以我们在参数中进行了一些配置,并控制了最大空闲可重用连接。
func main() {
keepAliveTimeout:= 600 * time.Second
timeout:= 2 * time.Second
defaultTransport := &http.Transport{
Dial: (&net.Dialer{
KeepAlive: keepAliveTimeout,}
).Dial,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,}client:= &http.Client{
Transport: defaultTransport,
Timeout: timeout,
}
}复制代码
我们从这个阶段学到的是: 如果我们想重用连接池到另一个服务,我们必须读取响应体并关闭它。
因为我们的主 API 只是调用另一个服务,我们犯了一个致命的错误。 我们的主 API 应该重用来自 http 的可用连接,所以无论发生什么,我们必须读取响应体,即使我们不需要它。 我们也必须关闭响应体。 这两种方法都用于避免服务器中的内存泄漏。
假如我们忘记在代码中关闭响应主体。 这些东西会给我们的生产带来巨大的灾难
解决方案是: 我们关闭响应主体并读取它,即使我们不需要数据。
func Func()error {
req, err:= http.NewRequest("GET","http://example.com?q=one",nil)
if err != nil {
return err
}
resp, err:= client.Do(req)
//=================================================
// CLOSE THE RESPONSE BODY
//=================================================
if resp != nil {
defer resp.Body.Close() // MUST CLOSED THIS
}
if err != nil {
return err
}
//=================================================
// READ THE BODY EVEN THE DATA IS NOT IMPORTANT
// THIS MUST TO DO, TO AVOID MEMORY LEAK WHEN REUSING HTTP
// CONNECTION
//=================================================
_, err = io.Copy(ioutil.Discard, resp.Body) // WE READ THE BODY
if err != nil {
return err
}
}复制代码
经过几个月稳定运行,这个错误没有再次发生。但 在2018年1月的第一个星期,我们的一个服务被我们的主要 API 调用, 宕机了。 由于某些原因,它不能被访问。
因此,当我们的内容服务关闭时,我们的主 API 将再次启动。 Api 仪表盘再次变红,API 响应时间变得越来越慢。 我们的 CPU 和内存使用率非常高,即使使用自动缩放。
同样,我们试图再次找到根本问题。 嗯,在重新运行内容服务之后,我们再次运行良好。
对于这种情况,我们很好奇,为什么会发生这种情况。 因为我们认为,我们已经在 http 中设置了超时截止时间。 所以正常来说这种情况,不可能再次发生。
在我们在代码中check潜在的问题时,我们发现了一些非常危险的代码。
type sampleChannel struct {
Data *Sample
Err error
}
func (u *usecase) GetSample(id int64, someparam string, anotherParam string) ([]*Sample, error) {
chanSample := make(chan sampleChannel, 3)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
chanSample <- u.getDataFromGoogle(id, anotherParam) // just example of function
}()
wg.Add(1)
go func() {
defer wg.Done()
chanSample <- u.getDataFromFacebook(id, anotherParam)
}()
wg.Add(1)
go func() {
defer wg.Done()
chanSample <- u.getDataFromTwitter(id, anotherParam)
}()
wg.Wait()
close(chanSample)
result := make([]*Sample, 0)
for sampleItem := range chanSample {
if sampleItem.Error != nil {
logrus.Error(sampleItem.Err)
}
if sampleItem.Data == nil {
continue
}
result = append(result, sampleItem.Data)
}
return result
}复制代码
超时控制
func (u *usecase) GetSample(id int64, someparam string, anotherParam string) ([]*Sample, error) {
chanSample := make(chan sampleChannel, 3)
defer close(chanSample)
go func() {
chanSample <- u.getDataFromGoogle(id, anotherParam) // just example of function
}()
go func() {
chanSample <- u.getDataFromFacebook(id, anotherParam)
}()
go func() {
chanSample <- u.getDataFromTwitter(id,anotherParam)
}()
result := make([]*feed.Feed, 0)
timeout := time.After(time.Second * 2)
for loop := 0; loop < 3; loop++ {
select {
case sampleItem := <-chanSample:
if sampleItem.Err != nil {
logrus.Error(sampleItem.Err)
continue
}
if feedItem.Data == nil {
continue
}
result = append(result,sampleItem.Data)
case <-timeout:
err := fmt.Errorf("Timeout to get sample id: %d. ", id)
result = make([]*sample, 0)
return result, err
}
}
return result, nil;
}复制代码
在第三阶段之后,我们的问题仍然没有完全解决。 我们的主 API 仍然消耗高 CPU 和内存。
这是因为,即使我们已经将 Internal Server Error 返回给 我们的用户,但是我们的 goroutine 仍然存在。 我们想要的是,如果我们已经返回响应,那么所有的资源也会被清除,没有例外。
我们发现了一些有趣的功能,我们还没有意识到在golang中可以使用context来帮助取消。 而不是利用时间。 在使用超时之后,我们转移到上下文。
背景。 有了这种新的方式,我们的服务更可靠了。然后,我们通过向相关的函数添加上下文,再次更改代码结构。
func (u *usecase) GetSample(c context.Context, id int64, someparam string, anotherParam string) ([]*Sample, error) {
if c== nil {
c= context.Background()
}
ctx, cancel := context.WithTimeout(c, time.Second * 2)
defer cancel()
chanSample := make(chan sampleChannel, 3)
defer close(chanSample)
go func() {
chanSample <- u.getDataFromGoogle(ctx, id, anotherParam) // just example of function
}()
go func() {
chanSample <- u.getDataFromFacebook(ctx, id, anotherParam)
}()
go func() {
chanSample <- u.getDataFromTwitter(ctx, id,anotherParam)
}()
result := make([]*feed.Feed, 0)
for loop := 0; loop < 3; loop++ {
select {
case sampleItem := <-chanSample:
if sampleItem.Err != nil {
continue
}
if feedItem.Data == nil {
continue
}
result = append(result,sampleItem.Data)
// ============================================================
// CATCH IF THE CONTEXT ALREADY EXCEEDED THE TIMEOUT
// FOR AVOID INCONSISTENT DATA, WE JUST SENT EMPTY ARRAY TO
// USER AND ERROR MESSAGE
// ============================================================
case <-ctx.Done(): // To get the notify signal that the context already exceeded the timeout
err := fmt.Errorf("Timeout to get sample id: %d. ", id)
result = make([]*sample, 0)
return result, err
}
}
return result, nil;
}复制代码
func ( u *usecase) getDataFromFacebook(ctx context.Context, id int64, param string) sampleChanel {
req, err := http.NewRequest("GET", "https://facebook.com", nil)
if err != nil {
return sampleChannel{
Err: err,
}
}
// ============================================================
// THEN WE PASS THE CONTEXT TO OUR REQUEST.
// THIS FEATURE CAN BE USED FROM GO 1.7
// ============================================================
if ctx != nil {
req = req.WithContext(ctx) // NOTICE THIS. WE ARE USING CONTEXT TO OUR HTTP CALL REQUEST
}
resp, err := u.httpClient.Do(req)
if err != nil {
return sampleChannel{
Err: err,
}
}
body, err := ioutils.ReadAll(resp.Body)
if err != nil {
return sampleChannel{
Err: err,
}
sample := new(Sample)
err := json.Unmarshall(body, &sample)
if err != nil {
return sampleChannle{
Err: err,
}
}
return sampleChannel{
Err: nil,
Data: sample,
}
}
}复制代码
经验教训:
1,不要在在生产中使用默认选项.
2,不要在在生产中使用默认选项. 如果您正在构建一个大的并发 api,千万不要使用默认选项
3,大量阅读,大量尝试,大量失败,大量收获
4,我们从这个经验中学到了很多,这种经验只有在真实的案例和真实的用户中才能获得。 我很高兴能参与修复这个漏洞
关注微信公众号
有疑问加站长微信联系(非本文作者)