Go底层原理与工程化实践 (李乐) (z-library.sk, 1lib.sk, z-lib.sk) (2)

Author: 李乐

GO

全书分为2篇: 1.第1篇详细介绍Go语言高性能优势是如何实现的,包括经典的GMP调度模型,Go语言调度器的实现,垃圾回收,以及如何基于管道、锁等并发编程。 2.第二篇主要是项目实战,手把手带领读者从0开始搭建高性能、高稳定的Go服务。以及在面对线上问题时,如何调试、分析、解决。 通过学习本书,读者对Go语言的核心——高并发会有一个深刻的认识,具备一定的Go并发编程经验,能够独立完成高性能、高稳定Go服务的架构设计,并且能够基于一些工具进行Go线上问题分析与性能调优。

📄 File Format: PDF
💾 File Size: 8.1 MB
17
Views
0
Downloads
0.00
Total Donations

📄 Text Preview (First 20 pages)

ℹ️

Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

📄 Page 1
(This page has no text content)
📄 Page 2
目 录 版权信息 内容简介 作者简介 前言 第1章 为什么要了解Go底层 1.1 Go服务怎么出现502状态码了 1.2 Go服务为什么没响应了 1.3 Uber如何通过GC调优节约7万个内核 1.4 Go语言进阶路线 1.5 本章小结 第2章 Go语言并发模型 2.1 GMP调度模型 2.2 协程管理 2.3 调度器 2.4 本章小结 第3章 调度器触发时机 3.1 网络I/O 3.2 管道 3.3 定时器 3.4 系统调用 3.5 本章小结 第4章 Go语言并发编程 4.1 什么是并发问题 4.2 CSP并发模型 4.3 基于锁的协程同步 4.4 如何并发操作map 4.5 并发控制sync.WaitGroup 4.6 并发对象池sync.Pool 4.7 如何实现单例模式 4.8 并发检测 4.9 本章小结
📄 Page 3
第5章 GC原理、调度与调优 5.1 内存管理 5.2 三色标记与写屏障 5.3 标记与清理 5.4 GC调度与GC调优 5.5 本章小结 第6章 手把手教你搭建Go项目 6.1 Go项目架构设计 6.2 Web框架Gin 6.3 日志与全链路追踪 6.4 访问数据库 6.5 HTTP调用 6.6 单元测试 6.7 本章小结 第7章 高性能Go服务开发 7.1 分库分表 7.2 使用Redis缓存 7.3 使用本地缓存 7.4 资源复用 7.5 其他 7.6 本章小结 第8章 高可用Go服务开发 8.1 可用性定义与高可用三板斧 8.2 流量治理组件Sentinel 8.3 Go服务监控 8.4 其他 8.5 本章小结 第9章 Go语言微服务入门 9.1 Go语言RPC标准库 9.2 微服务框架Kitex 9.3 本章小结 第10章 实现Go服务平滑升级 10.1 服务升级导致502状态码
📄 Page 4
10.2 Go语言信号处理框架 10.3 Go服务平滑退出 10.4 基于gracehttp的Go服务平滑升级 10.5 本章小结 第11章 Go服务调试 11.1 Go程序分析利器pprof 11.2 性能分析工具Trace 11.3 使用dlv调试Go程序 11.4 本章小结 第12章 线上服务实战 12.1 两种导致502状态码的情况 12.2 意想不到的并发问题 12.3 HTTP服务假死问题 12.4 HTTP客户端引发的问题 12.5 类型不匹配导致的线上问题 12.6 其他问题 12.7 本章小结
📄 Page 5
版权信息 书名:Go底层原理与工程化实践 作者:李乐 陈雷 出版社:机械工业出版社 出版时间:2024-06-26 ISBN:9787111758266 ·
📄 Page 6
内容简介 本书由知名企业Go语言开发专家撰写,他们的项目经验极为丰富,为读者分享了不可多得的实 践技巧。本书从底层原理与工程化实践两个维度讲解:先基于工程化视角为读者详细剖析了Go 语言的核心原理;之后,从0到1带领读者实现一个高性能、高可用的Go服务,并在此过程中分 享了10余个线上典型问题的解决思路。阅读本书,读者能利用多种框架完成Go语言大型项目开 发,同时让项目具有良好的性能与稳定性。 全书共12章,从逻辑上分为两篇。 第一篇(第1~5章) 详细介绍Go语言的高性能优势是如何实现的,包括经典的GMP调度模型、 调度器的实现、垃圾回收原理,以及如何基于管道、锁等实现并发编程。 第二篇(第6~12章) 主要是7个项目实战,带领读者从零开始搭建高性能、高可用的GO服务, 并让读者知道面对线上问题时如何调试、分析、解决。通过学习本书,读者对Go语言的核心 ——高并发会有深刻的认识,积累一定的Go并发编程经验,能够独立完成高性能、高可用Go服 务的架构设计,并且能够基于一些工具进行Go线上问题分析与性能调优。
📄 Page 7
作者简介 李乐 Golang开发专家,先后就职于滴滴出行、好未来,西安电子科技大学硕士,拥有多年互联网研发 经验,合著有《Redis5设计与源码分析》。 陈雷
📄 Page 8
希望学产研负责人,清华大学本硕与北京邮电大学硕士,曾在百度、腾讯和滴滴等公司工作, 拥有15年产品研发经验,合著有《PHP7底层设计与源码实现》。
📄 Page 9
前言 为什么要写这本书 Go语言是目前非常受欢迎的语言之一,具有入门快、性能高、开发效率高等特点,越来越多的 互联网企业开始使用Go语言。云原生体系中的Kubernetes、Docker等开源项目也都是基于Go语言 开发的,因此想要深入研究云原生技术,就必须精通Go语言。 但是,大多数Go开发者往往只是做一些简单的增删改查,始终徘徊在Go初级阶段。想要进一步 掌握Go语言,最快的方法就是原理与实战相结合,这也是本书写作的初衷。 本书首先对Go语言的核心原理进行详细介绍,其次分享大量的Go项目实战经验与技巧,希望读 者能够从中有所收获。 本书特色 ❑ 提供Go高并发特性与并发编程常用技巧 :本书对Go语言的核心原理,包括并发模型、调 度器、垃圾回收、并发编程等进行了详细介绍。 ❑ 应对大部分业务场景 :本书在讲解项目实战时,并没有停留在简单的增删改查层面,而是 分享了很多Go项目开发经验,包括常用开源框架的使用技巧与核心原理、高性能与高可用的Go 服务开发,以及Go服务平滑升级等。 ❑ 给出线上问题处理方案 :本书列举了10多个线上典型问题并讲解了这些问题的解决思路, 这些问题都是Go语言初学者经常遇到的,相信读者能够从中吸取经验,总结出一套属于自己的 方法论。 ❑ 引入“问题-思考-探索”模式 :本书在讲解Go语言核心原理以及项目实战时,尽可能从问题 出发,引导读者主动思考、主动探索,以便读者对所学知识有更深入的理解。 读者对象 ❑ Go开发工程师 ❑ 对Go语言感兴趣的读者 如何阅读本书 本书共12章。 第1章 先以3个具体的案例为切入点,说明深入学习Go语言的必要性,然后从原理与实战两个维 度分享Go语言进阶路线。 第2章 详细介绍经典的GMP调度模型,其中重点介绍了协程以及调度器的实现原理,以便读者 掌握Go并发模型。 第3章 主要介绍网络I/O、管道、定时器以及系统调用是如何触发调度的。 第4章 详细介绍Go语言的几种常见多协程同步方案,包括基于管道的协程同步、基于锁的协程 同步,以及非常有用的并发检测工具race。 第5章 重点讲解Go语言GC(垃圾回收)的实现原理,包括三色标记法与写屏障技术、标记过程 与清理过程、GC调度与GC调优。
📄 Page 10
第6章 首先以经典的商城项目为例,带领读者从0到1搭建一个完整的Go项目。其次重点介绍常 用开源框架的使用技巧,包括Web框架Gin、日志框架Zap与全链路追踪、数据库访问框架 Gorm、HTTP客户端框架go-resty、单元测试等。 第7章 详细介绍高性能Go服务开发的常用方案,包括分库分表、缓存,以及资源复用、异步化 处理、无锁编程等。 第8章 详细介绍高可用Go服务开发的常用方案,包括流量治理、Go服务监控、超时控制以及错 误处理。 第9章 首先介绍微服务架构的基本概念,其次选取目前比较流行的一款微服务框架Kitex,讲解 它是如何实现微服务架构以及服务治理的。 第10章 首先介绍为什么Go服务升级会导致502状态码,其次讲解如何基于开源框架gracehttp实现 Go服务平滑升级。 第11章 首先讲解使用Go语言性能分析利器pprof、Trace分析Go服务性能指标的技巧,其次介绍 Go语言专用的调试工具dlv。 第12章 列举10多个线上问题并讲解这些问题的解决思路,包括Go服务502问题、并发问题、 HTTP服务假死问题等,希望读者在以后的开发工作中能够避免类似的问题发生,或者在遇到类 似的线上问题时能够从容应对。 读者可以根据自己的兴趣及需要,选择阅读相关章节。 勘误和支持 由于水平有限,再加上编写时间仓促,书中难免会出现不准确的地方,恳请读者批评指正。如 果读者有更多的宝贵意见,欢迎访问https://segmentfault.com/u/lishuo0进行专题讨论,我们会尽量 在线上为读者提供解答。同时,读者也可以通过邮箱lieshuoa@163.com联系我们。期待得到读者 的反馈,让我们在技术之路上互勉共进。 李乐
📄 Page 11
第1章 为什么要了解Go底层 Go语言是由谷歌开发的,它不仅具有静态强类型和并发性的特点,还集成了垃圾回收功能,能 够在降低代码复杂性的同时,不损失应用程序的性能。Go语言是目前非常受欢迎的语言之一, 简单高效,越来越多的企业和开发者开始使用Go语言。Go语言入门较快,但进阶较困难,原因 在于:一方面,大多数Go开发者通常只涉及简单的增删改查,很少接触到Go语言的并发编程、 性能调优和线上调试等工作;另一方面,大多数人可能缺乏主动学习的动力和勇气,或者不知 道如何进一步学习Go语言。本章通过三个具体实例,包括Go语言502问题、Go服务假死问题和 Go语言GC调优问题,说明了解Go语言底层的重要性。最后,笔者还总结了Go语言的进阶路 线,希望能帮助到每一位Go开发者。
📄 Page 12
1.1 Go服务怎么出现502状态码了 服务端开发最常见的问题可能就是HTTP状态码异常了,常见的异常状态码包括404 Not Found、 502 Bad Gateway、504 Gateway Time-out等。异常状态码可能是链路层问题,也可能是服务本身出 现问题。本节主要介绍为什么Go服务超时会导致502状态码,以及如何基于context实现超时控制 逻辑。 1.1.1 服务超时为什么导致502状态码 根据HTTP状态码的定义,服务超时的状态码不应该是504吗?为什么我们却说Go服务超时会导致 502状态码呢?很简单,我们可以模拟Go服务超时的情况,测试一下这时的状态码到底是502还是 504。另外,为了保证测试结果的准确性,服务的访问链路尽量与线上环境保持一致,这里假设 访问链路是客户端→网关→Go服务,也就是说除了Go服务之外还需要搭建网关服务。 目前大部分企业都是基于Nginx搭建网关,而Nginx的编译安装过程比较复杂,所以我们选择通过 Docker方式部署并启动Nginx服务。部署方式如下: 参考上面的说明,docker run命令用于运行容器。容器中Nginx服务的默认配置文件位于/etc/nginx 目录下,因此我们使用-v参数将本地配置文件映射到容器中的指定目录。其中,nginx.conf是 Nginx的主配置文件(参考Nginx的默认配置模板);conf.d目录下包含所有虚拟服务的配 置;/var/log/nginx是Nginx服务日志文件的存放目录。 我们的目的是测试Go服务超时情况,其对应的Nginx虚拟服务配置如下所示: 另外,别忘了在主配置文件nginx.conf中引入conf.d目录下所有的虚拟服务配置,引入方式如下:
📄 Page 13
参考上面的配置文件,网关Nginx会将所有请求转发到x.x.x.x:8080,即Go服务监听端口必须是 8080。Go语言的标准库还是比较完善的,基于net/http库只需要几行代码就能创建并启动一个 HTTP服务,代码如下: 参考上面的代码,WriteTimeout用于设置HTTP请求的超时时间,函数http.HandleFunc用于注册请 求处理方法。注意,我们通过函数time.Sleep使协程休眠5s,模拟请求处理超时情况。编译并运行 上面的Go程序,通过curl命令发起HTTP请求,结果如下: 参考上面的运行结果,客户端收到的是502状态码,并且请求处理耗时为5.049s。这里有两个问 题。 1)为什么Go服务超时返回的是502状态码? 2)明明我们设置的请求超时时间为3s,为什么实际的请求处理耗时却是5.049s? 502状态码的含义是Bad Gateway,看上去好像是网关错误,其实不是。实际上,502状态码通常 是由网关Nginx与上游服务之间的TCP连接异常导致的,或者是上游服务直接返回了502状态码。 怎么判断是哪种情况呢?只需查看Nginx的错误日志。如果是网关Nginx与上游服务之间的TCP连 接异常导致的502状态码,Nginx一定会记录错误日志。比如针对上面的502状态码问题,查看 Nginx的错误日志,如下所示:
📄 Page 14
从上面的日志可以清楚地看到,该502状态码产生的原因是,当网关等待从上游服务读取响应头 时,上游服务过早地关闭了连接。也就是说,Go服务在检测到请求超时后,直接关闭了TCP连 接,所以才导致网关返回了502状态码。如果你对此还有疑惑的话,可以通过tcpdump工具抓包验 证一下,抓包结果如下所示: 参考上面的抓包结果,20点48分56秒,网关Nginx与Go服务建立了TCP连接,然后将HTTP请求转 发到Go服务。20点49分01秒,Go服务关闭了该TCP连接。也就是说,Go服务在5s超时后才关闭了 连接。抓包结果与模拟验证的结果一致。 还有一个问题,请求超时时间为3s,为什么5s后Go服务才关闭连接呢?这就需要了解 WriteTimeout超时功能的实现逻辑了。 参考图1-1,当Go服务接收到客户端请求时,会根据WriteTimeout添加定时器。当处理时间超时 后,定时器会设置Go服务与客户端的连接状态为已超时(注意只是设置一个标志位),所以即使 请求处理时间已超过WriteTimeout,Go服务依然还在默默地处理请求。当Go服务处理完该HTTP 请求,准备向客户端返回数据时,检测到与客户端的连接为已超时状态,于是便关闭了与客户端 的TCP连接,从而导致网关返回了502状态码。 1.1.2 基于context的超时控制 由1.1.1小节的介绍可知,Go服务超时(请求处理时间超过WriteTimeout配置)会导致502状态码。 幸运的是,这种情况比较容易排查。但是Go服务超时返回502状态码合理吗? 假设网关是基于Nginx搭建的,需要说明的是Nginx有一个功能叫作被动健康检查。这是什么意思 呢?就是当Nginx向上游服务转发请求时,如果出现一些异常情况,比如超时、上游节点返回 502/504等错误,或者Nginx与上游节点的TCP连接异常等情况,Nginx会标记该上游节点为异常。 当某个上游节点在一段时间内的失败次数达到配置阈值时,Nginx会将该节点临时摘除,也就是 说后续不会再向该节点转发请求。思考一下:是否会出现所有上游服务节点都被临时摘除的情况 呢?
📄 Page 15
图1-1 WriteTimeout超时功能的实现逻辑 我们可以测试一下上面描述的场景。被动健康检查要求上游服务包含多个节点,所以再添加一个 上游服务节点,配置如下: 注意,如果你在同一台机器上启动这两个Go服务,还需要修改另一个Go服务的监听端口号,同 时记得重新启动Nginx容器。接下来将使用ab压测工具来模拟大量并发请求。ab工具的使用方法如 下: 查看Nginx访问日志,你会发现存在大量502状态码的请求,对应的Nginx错误日志如下:
📄 Page 16
从上面的日志可以清楚地看到,该502状态码产生的原因是,当Nginx选择上游节点建立连接时, 发现没有“存活”节点,也就是说所有的上游节点都被临时“摘除”了。这时候,Nginx不会再向任何 节点转发请求,会直接向客户端返回502状态码。 笔者曾经遇到过这样的线上问题:由于一个非核心接口处理超时,短时间内出现了大量502状态 码,网关Nginx因此认为所有的上游节点都不可用,导致其他核心接口也出现了大量502状态码, 甚至影响到了业务功能。那有什么办法能避免呢?办法是当Go服务超时时,依然向客户端返回 200状态码,超时错误信息可以通过数据标识返回给客户端。 Go语言为我们提供了一个非常有用的标准库——context(上下文),它可用于在整个请求上下文 传递数据以及辅助实现超时控制逻辑。那么如何通过context改造Go服务,既能实现超时控制逻 辑,又能保证超时后依然向客户端返回200状态码?可参考下面的代码: 参考上面的代码,context.WithTimeout返回一个带有超时功能的上下文,这里我们设置的超时时 间是3s,也就是说3s后ctx.Done()方法返回的管道数据可读。请求处理逻辑由子协程实现,处理完 成之后将返回结果写入管道c。select是Go语言关键字,可以监听多个管道的读写事件。如果子协 程首先处理完成请求,管道c可读,便会向客户端正常返回数据。如果上下文先超时,ctx.Done()
📄 Page 17
方法返回的管道数据可读,便会向客户端返回超时错误。编译并运行上面的Go程序,同时通过 curl命令发起HTTP请求,结果如下: 可以看到,虽然处理该HTTP请求需要耗时5s,但是当上下文3s超时后,Go服务就立即向客户端 返回数据,并且状态码是200。也就是说,基于context实现超时控制逻辑,一方面可以快速地向 客户端返回数据(即使是超时错误),另一方面可以避免502状态码引起的一些隐患。
📄 Page 18
1.2 Go服务为什么没响应了 不知道你有没有遇到过这种情况:Go服务看起来正在运行,但是大量HTTP请求却没有任何响 应,甚至查不到任何业务日志。我们通常称这一现象为Go服务假死。造成Go服务假死的原因有 很多种,比如死锁。也就是说,请求处理协程抢占了一把已被其他协程占有并且永远不会释放的 锁时,所有的请求处理协程都将被无限期阻塞,Go服务当然也就不会有任何响应了。本小节将为 大家演示Go服务假死的现象,并且讲解如何排查Go服务假死问题。 1.2.1 谁阻塞了协程 假设有这样一个业务场景:某个接口的业务逻辑非常复杂,但可以分为核心逻辑和非核心逻辑, 而且非核心逻辑的复杂度较高,执行时间较长。在这种情况下,通常会选择异步处理非核心逻 辑。也就是说,在处理完核心流程后,请求处理协程会将数据写入队列(例如管道),然后立即 返回,其他协程再从队列中获取数据并进行处理。 Go语言基于协程与管道实现上述功能的代码如下:
📄 Page 19
参考上面的代码,管道queue代表异步队列,函数initAsyncQueue用于初始化队列以及异步协程。 请求处理协程的主要逻辑可以分为三部分:处理请求、将数据写入队列、向客户端返回结果。异 步协程的主要逻辑是循环从队列获取数据,并执行一些非核心逻辑。 上述程序有什么风险吗?想想执行非核心逻辑耗时较长的情况,也就是说从队列读取数据的速度 较慢,但是恰好请求访问量又较大,也就是说请求处理协程向队列写入数据的速度较快。这时候 队列(管道)中的数据可能会出现堆积现象,甚至在极端情况下队列(管道)的容量会满,这样 一来请求处理协程再向队列(管道)写入数据就会被阻塞。再考虑另外一种情况,如果异步协程 因为某些原因异常退出了,也就是说没有协程从队列读取数据,那么队列(管道)的容量很快就 会满了,这时候请求处理协程同样会被阻塞。 我们可以模拟一下第一种情况,编译并运行上面的程序,随后通过ab压测工具模拟大量的并发请 求,命令如下: 压测的同时,可以通过curl命令发起HTTP请求,命令如下: 随着时间的流逝,你会发现curl命令没有任何响应,也就是说Go服务没有返回响应数据,看起来 像是Go服务假死了。当然,上面的Go程序比较简单,你可能很容易就能分析出为什么Go服务假 死了。但是,实际的Go程序往往非常复杂,很难通过代码分析出是什么原因导致的Go服务假 死。这时候怎么办呢?其实Go语言本身就提供了工具pprof,它可以采集Go程序的运行时数据, 比如协程栈,这样Go服务阻塞在哪里就一目了然了。
📄 Page 20
当然,采集Go程序的运行时数据是需要耗费一些资源(时间、内存、CPU等)的,所以需要我们 手动引入一些代码来开启pprof功能,代码如下所示: 开启pprof功能之后,就可以通过指定接口采集Go程序的运行时数据了。接下来就是基于pprof排 查前面的Go服务假死问题了,分析Go服务协程栈如下: pprof采集的部分Go程序的运行时数据可读性不太好,所以Go语言还提供了工具来帮助我们分析 Go程序的运行时数据,使用方式如上面的示例所示。参考上面的输出结果,有100个请求处理协 程(与ab压测工具并发请求数100有关)因为管道的写操作而阻塞。当然,生产环境的访问量通 常比较大,所以阻塞的协程数一般更多。另外,从协程栈也可以看到写管道的函数是 main.main.func2,也就是main包的main函数中的第二个匿名函数。 最后总结一下,Go服务假死通常是请求处理协程因某些原因阻塞了,所以这时候只需要通过pprof 分析协程栈往往就能确定阻塞的原因。 1.2.2 写管道可以不阻塞协程吗 参考1.2.1小节的例子,由于异步协程执行非核心逻辑的耗时较长,因此当请求访问量较大时,队 列(管道)中的数据可能会积压,极端情况下甚至会阻塞请求处理协程的写入操作。那怎么办 呢?能不能创建多个异步协程来处理数据呢?当然可以,这样可以在一定程度上避免数据的积 压。但是,仍然无法完全避免此问题,也就是说仍然有可能会阻塞请求处理协程的写入操作。这 是肯定不能接受的,还有其他办法吗?比如管道的写入操作能不能不阻塞用户协程呢? 在Go语言中,可以通过select+default的组合实现管道的非阻塞式操作,我们基于这种方案改造一 下1.2.1小节中请求处理协程向队列写入数据的逻辑,如下所示:
The above is a preview of the first 20 pages. Register to read the complete e-book.

💝 Support Author

0.00
Total Amount (¥)
0
Donation Count

Login to support the author

Login Now

Recommended for You

Loading recommended books...
Failed to load, please try again later
Back to List