建议你在将Golang API投入生成之前阅读此文,此文是基于真实的线上问题修复经历,如有巧合,纯属踩雷!
01 架构
我们在整个系统架构中使用了微服务模式。 有一个网关 API (我们称之为主 API )为我们的用户(移动和网络)提供 API。 它的角色类似于 API 网关,所以它的任务只处理来自用户的请求,然后调用所需的服务,并向用户构建响应。 此AP服务完全由 Golang 来编写。
![](https://static.studygolang.com/190829/a812dd39db9270fe517b0a52ca4c8e3e.png)
我们已经为我们的主API挣扎了很长一段时间,这些 API 总是被关闭且总是长时间处于无法响应的状态,有时导致我们的 API 无法访问,服务也处于无法使用状态。
API监控仪表盘显示红色警报,老实说,当我们的 API监控仪表盘变成红色时,是一件非常非常危险的事情,会给我们的工程师带来压力、恐慌和崩溃。
我们的 CPU 和内存使用率也正在变得越来越高。 如果发生这种情况,我们只需无助的手动重新启动服务,然后等待它再次重新运行。
![](https://static.studygolang.com/190829/35fb8bfcca9c61c17cef1cfae399c844.jpg)
![](https://static.studygolang.com/190829/7d41f9993c5beaf0e379b2ec6ecfbbda.png)
这个 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,我们从这个经验中学到了很多,这种经验只有在真实的案例和真实的用户中才能获得。 我很高兴能参与修复这个漏洞
关注微信公众号
有疑问加站长微信联系(非本文作者)
![](https://static.golangjob.cn/static/img/footer.png?imageView2/2/w/280)