在将Golang API投入生产之前,您必须阅读此内容。 根据我们在Kurio的真实故事,我们会为每个版本而苦苦挣扎,因为我们没有以正确的方式来做。
laptop on fire taken from google image search
几周前,我们在Kurio只是在主要服务中修复了我们怪异而未发现的错误e u P Z E r Q l O。 我们尝O = Q试了许多调试和修复它R S { % u的方法。 该错误与业务逻辑无关。 因为它已经投产了几个星期。 但是我们总是通过自动缩放机制来节省; y p a [ @ f C费用,因此就像运行良好一样。
直到后来,我们才弄清楚了,这是因为我们的代码做得不好。
架构
Fyi,我们在架构中使用了微服务模式。 我们有一个网关API(我们称之为主API)P 6 @ p v x,可为用户(移动和网络)提供API。 由于其作用类似于API网关,因此它的任务仅处理来自用户的请求,然后调用所需的服u i P e g务,并建立对用户的响应。 这个主要的API,C t , s b Q完全用Golang编写。 选择golang的原因是另一个我不会在这里讲的故事。
如果绘制在图片中,我们的系统将更像这样。
Kur^ u k j a _ X I Fio architecture
问题
我们与主要API的斗争已经很长时间了,主要API一直崩溃,并且对我们的移动应用程序的响应很长,有时甚至导致无法访问我们的API。 我们的API仪表板监视器只是变成红色了-老实说,当我们的API仪表板监视器变成红色时,这是一件危险的事,并给我们带来压力,恐慌和疯狂,使工程师。
其他是,我们的CPU和内存使用1 n x 2 3率越来越高。 如果发生这种情况,我们只需手动重新启动V C q s e [ r !它,然后等待它再次运行即可。
Our API response time up-to 86 secon~ V ~ C M f + } 3ds for a single request.
graph our APID Z [ o response time, and doing restart manually for safety.
这个错误确实使我们感到沮丧,因为我们没有任何日志专门说明此错误。 我们只是有( K R v ` J D ^这7 b H e + c d么长的响应时` / ] l $ l I v -间。 CPU和内存使用率增加。 就像一场噩梦。
阶段1:使用自定义的http.Client
开发此P z t :服务时,我们了解到并且真正了解到的一件事是,不要. * @ N # D q v相信默认配置。
我们使用自定义的httpG K c m / ^ M y a.Client,而不使用http包中的默认值,
client:=ht[ J 4 a } v l /tp.Client{} //default
我们根据需要添加一些配置。 因为我们C ? % 6 5 7 i F需要重用连接,所以我们在传$ T Z P ] g输和控制max-idle可重用连接中进行一些配置。
keepAliveTimeQ 0 [ C 8 (out:= 600 * timeB $ R B .Second
timeout:= 2 * time.Second
defaultTransport := &http.Transport{
Dial: (&amE ^ 7 j #p;net.Dialer{
KeepAlive: keepAliveTimeout,}
).Dial,
MaxIdleConns: 100,
Mz E C ` G 0 / 9 daxIdleConnsPerHost: 100,
}
client:= &a- v ) U . b [ omp;http.Client{
TrD S } L M [ansport: defaultTransport,
Timeout: timeout,
}
此配置可以帮助我们减少用于调用另一个服务的最长时间。
阶段2:避免未公开R Z j M P ] X回复的内容引起内存泄漏
我们从该阶段中学到的是:如果要重用连接池到另一个服务,则必须读取响应主体并将其关闭。
因为我们的主要API只是调用另一个服务,所以我们犯了一个致命错误。 我们的主要API假设要重用来自http.Client的可用w w A A d连接,因此无论发生什么情况,我们都必须阅读响应正文,即使我们不需要它。 而且,我们必须关闭响应主体。 两者都用于避免服务器中的内存泄漏。
我们忘记在代码中关闭响应主体。 这些事情可能给我们的生产造成巨大的灾难。
解决方案是:我们f _ ! S V W 4 b Z关闭响应主体并读取它,即使我们不需要数据也是如此。
req, err:= httpu 6 0 N.Ne_ ? g O 6 ~wRequest(\"GET\",\"http://example.com?q=one\",nil)
if err != nil {
return err
}
respA J h a, err:= client.Do(req)
//======v 9 3 Y==========i - f #=================================
// CLOSE THE RESPONSE BODY
//=================================================
if resp != nil {
defer resp.Body.d t OClose() // MUST CLOSED THIS
}
if err !T K ? w , m= nil {
return err
}
//=================================================
// READ THE BODY EVEN THE DATA IS NOT IMPORTANT
// THIS M# ) l @UST TO DO, TO AVOID MEMORY LEAK WHEN REUSING HTT5 i p uP
// CONNECTION
//===============r N c & F + Q========d + X I y $===h B b v f a=======================
_, err = io.Copy(ioutil.Discard, resp.Ba & : ! ;odyK & 0 9) // WE READ THE BODY
if erU 1 G S T , sr != nil {
return err
}
我们在这里阅读了一篇很棒的文章后,对此进行q % T了修复: / 11/21 /调整http客户端库8 S c p K n g以进行负载测试/
第1阶段和[ 1 e &第2阶段,并在自动扩展成功的帮助下减少了此错误。 好吧,老实说,自去年以V B , B来三个月都没有发生这种情况:2017年。
阶段3:Golang Channel 中T a s x ) o的超时控制
在运行几个月后,此错误再次发生。 在2018年1月的第一周,主要API调用的一项服务让我们说:已关闭。 由于某些原因l ) Q r j N w,无法访问它。
因此,当我们的内容服务关闭时,我们的主要API会再次触发。 API仪表板再0 V Z - , u . _次变红,g @ m b 3 :API响应时间越来越长。 即使使用自动缩放,我们的CPU和内存使用$ l o * w b d率也会很高。
同样,我们试图再次找到根本问题。 好了,在重新运行内容服务之后,我们再次运行良好。
对于这种情况,我们很好奇,为什么会9 V } M这样。 因为我们认为,我们已经在http.Client中设置了超时期限,所以在这种情况下,这永远不会发生。
在我们的代码中搜索潜在的问题,然后我们找到了一些危险的代码。
为了更简单U ; a V 1 @ !,代码看起来O B X & 6 ! y j更像这样* ps:此函数只是一个示例,但与@ $ ; % @ 7我们的模式相似
type sampleChannel struct{
Dk 2 { _ Y 8 Pata *Sample
Err error
}
func (? a g Q Pu *usecase) GetSample(id; i e int64, someparam string, anotherParam string) ([]*Sample, error) {
chanSJ A t u }ample := make(chan sampleChannel, 3)
wg :4 v o H R 9= sync.WaitGroW K 2 Qup{}
wg.Add(1)
go func() {
defer wg.Done()
chanE B JSaE O k U e J Imple <- u.getDataFromGoogle(id, anotherParam) // just example of function
}()
wg.Add(1)
go fum [ 1nc() {
defer wg.Done()
chanSample <- u.getDataFro! ? 0 E v t 4mFacn : g u v i ` zebook(id, anotherParam)
}()
wg.Add(1)
goY z z _ func() {
defer wg.Done()
chanSample <- u.getDataFromTwitter(id,anotherParam)
}()
wg.Wait()
close(chanSample)
result := mJ ; nake([]*SamI w ` L 0plY ) 2 s W o He, 0)
for sampleItem := range chanSample {
if sampleItem.Error != nil {
lT - eogrus.Error(sampleItem.Err)
}
if sampleItem.Data == nil {
continue
}
resU 2 ; K 8ult = appt O A p Gend(result, sampleItem.Data)
}
returnP t P C F : r o result
}
如果我们看上面的代码,那没有错。 但是此函数是访问最多的函数,并且在我们的主要API中具有最重的调用。 因为此函数将执行3个具有巨大处理能力的API调用。
为了改g [ d h m 8善这一点,我们使用通道上的超时控G ; / d & D A制进行了新的处理。 因为使用上述样式代码(使用WaitGroup的代码将等待直到所有过程完成),我们必须等待所有API调用都必须完成,这样我们才能处j ^ C [ 0 + Z理并将响应返回给用户。
这是我们的重大错误之一。 当我们的一项服: J : ] x务死亡时,此代码可能会造成巨大的灾难。 因为将要等待很长的( q ? 4 ] q c时间才能恢复服务。 当然,使用5K通话,这是一场灾难。
首次尝试解决方案:
我们通过添加超时来对其进行修改。 因此Z 1 q d Q,我们的用户不会等待那么长时间,他们只会收到内部服务3 U J N 0 [ k r I器错误。
func (u *usecase) GetSample(id int64,6 m n P H someparam string, anotherParam string) ([]*Sample, error) {
chanSample := make(chan sampleChannel, 3)
defer cloz m %s% 0 V g $e(chanSample)
go func() {
chanSample <- u.getDataFromGoogle(id, a# s Nno) + ; h $ BtherParam) // just example of function
}()
go func() {
chanSample <- u.getDataFromFacebook(id, anotherParam)
}h v & ! y ; c()
go func() {
chanSample <- u.getz G R D JDataFromTwitter(id,anotherParam)
}()Z h C z 9 A K D ?
result := makeu n I v 8 v k d s([]*feed.Feed, 0)
timeout := time.After(time.Second * 2)
for loop := 0; loom [ }p < 3; loop++ {
select {
case sampleItemO r & j := <-R { echanSample:
ifM . I u N v k I sampleItem.Err != nil {
logrus.Error(sampleItem.Err)
continue
}( { &
if feedItem.Data == nil {
continue
}
result = append(result,sampleu P ^ItemL ^ B N ? : ? o.Data)
caU Y R M ] (se <-timeout:
err := fmt.Errorf(\"Timeout to ge8 u ^t sample id: %d. \", id)
result = make([]*sample, 0)
return result, err
}
}
return result, nil;
}
阶段4:使用上下文进行超时控制
完成第3阶段后,我们的问题仍然没有完全解决。 我们的主要API仍然消耗大量CPU和内存。
发生这种情况是因为,即使我们已经将内部服务器错误返回给我们的用户,但goroutine仍然存在。 我们想要的是w H S | x z U,如果我们已经返回了响应,那么所有资源也将被清除,没有异常,包括在后台运行的goroutine和API调用。
稍后阅读本文后:http://dahernan.github.io/2015/02/04/cont{ 4 S u i ^ 0 k Next-and-cancellation-of-goroutines/
我们在golang中发现了一些我@ - 2们尚未意识到的有趣功能。 那是使用上下文来帮助取消例程。
而不是使用时间。在使用超时之后,我们转到context.Context。 有了这种新方法,我们的服务将更加可靠。
然后,我们通过向相关功能添加上下文来再次更改代码结构。
func (u *use_ r & [ d 8 ( qcase) GetSample(c context.Context, id int64, someparam string, anotherParam string) ([]*Sample, erro3 N 2 t }r) {
if c== nil {
c= context.Background()
}
ctx, cancel := context.WithTimeout(c, time.Second * 2)) : g T % s
defer cancel()
chanSample := makn m & a s [ { ke(chan sampleChannel, 3)
defer close(chanSample)
go func() {
chanSam= [ / h 1ple &la w v b | / Mt;- u.getDataFromGoogle(ctx, io I ]d, anotherParam) // just example of function
}()
go func() {
chanSample <- u.getDataFB j ] Z P R M VromFacebook(ctx, id, anotherParam)
}()
go func() {
chanSample <- u.getDataFromTwitter(ctx,} + m Q * 9 ] 5 id,anotherParam)
}()
resultX l B r 5 C F := make([]*feeZ ` C K 7d.Feed, 0)
for loop := 0; loop < 3; loop++ {
select {
case sampleItem := <-chanSample:
if samd _ 3 ~ h n , @pleItem.Err != nil {Q h S M N
continue
}
if feedItem.Data ==^ @ h 1 nil {
continue
}
result = append(result,sampleItem.Data)
// =========================C t k r ) M g ~ o===================================
// CATCH IF THE CONTEXT ALREADY EXCEEDED THE TIMEOUT
// FOR AVOIDw % I D [ y INCONSISTENT DATA, WE JUST SENT EMPTY ARRAY TO
// USER Aa w g D h o c xND ERROR MESSAGE
// ============================================================
case <-ctx.Done(): // To get the notify signal that the conV E R S dtext already excP r K r jeeded the timeout
err := fmt.Errorf(\"Timeout to geI j U ot sample id: %d. \", id)
result = make([]*sample, 0)
return result, err
}
}
return result, nil;a f o L
}
因此,我们将Contet U % u e $ D -xt用于代码中的每个goroutine调用。 这有助于我们释放内存并取消goroutine调用。
此外,为了获得更多控制和可靠性,我们还将上下$ = H文传递给我们的HTTP请求。
func ( u *uR 0 b ?secase) getDataFromFacebook(ctx contD & :ext.Context, id int64, parA z H # 2 z Qam string) sampleChanel{
req,e( D b z g h qrr := http.NewRequest(\"GET\",\"httpF C K $ [ E (s:! o 9 { , t//facebook.com\",nil)
if erv h G q ` ` D Xr != nil {
returns $ s sampleChannel{
Err: err,
}
}
// ======d - U m F====? t F % ;==o ] [================================================
// THEN WE PASS THE CONTEXT TO OUR REQUEST.
// THIS FEATURE CAN BE USED FROM GO 1.7
// ==============================================l D U x==============
if ctx != nil {
req = req.WithContext(ctx) // NOTICE TH= B [ 4 n c 0 2 WIS. WE ARE USING CONTE* , r _ nXT 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:nx a ^ a T Til,
Da% R + 1 rta:sample,
}
}
通过所有这些设H ^ K | * + W 3 G置和超时控制,我们的系统更加安全和可控。
学过的知识:
从未在生产中使用默认选项,也从未使用过默认选项。M Z x a O y 如果您要构建较A A 2 z B C d x j大的并发A,请不要使用默认选项。
阅读很多,尝试很多,失败{ 0 y . w ? 2很多,收获很多。我们从这种经验中学到了很多,这种经验只有在实际案例和实际用户中才能获得。 在修复此错误时,我很高兴能参与其中。
*最后更新时间:2018年1月1( A ! K8日:修复了一些错字
(本文翻译自Iman Tumorang的文章《Avoiding Memory Leak in Golang API》,参考:httpH O Q 0 Ps://m! K 5 - 1 : i 1edium.com/hackernoon/avoidinU 9 =g-M S $ P @memory-leak-in-golang-api-1^ 5 p d l z843ef45fca8)