Tony Bai · Go 语言第一课
Tony Bai
资深架构师,tonybai.com 博主
21492 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
开篇词 (1讲)
结束语 (1讲)
Tony Bai · Go 语言第一课
15
15
1.0x
00:00/00:00
登录|注册

38|成果优化:怎么实现一个TCP服务器?(下)

埋入度量数据项到服务端处理流程
定义度量数据项
配置Grafana
配置Prometheus
配置docker-compose.yml
使用Prometheus+Grafana组合
使用sync.Pool重用对象
减少堆内存对象分配
使用bufio.Reader和bufio.Writer
降低net.Conn的Read和Write频率
在服务端埋入度量数据采集点
建立观测设施
尝试进一步提升服务端性能
寻找服务端程序的其他可优化点
优化措施:带缓冲的网络I/O、重用内存对象
性能剖析工具:pprof
观测设施的建立:Prometheus+Grafana
四步循环方法:建立性能基准、性能剖析、代码优化、与性能基准比较
确定优化效果
比较优化前后的性能数据
压测服务端程序
重用内存对象
带缓存的网络I/O
识别瓶颈点
采集性能剖析数据
启动pprof性能剖析支持
导入 net/http/pprof
基于度量指标为程序建立图形化的性能基准
编写Go原生提供的性能基准测试(benchmark test)
与基准比较,确定优化效果
代码优化
性能剖析
建立性能基准
思考题
小结
性能基准比较
代码优化
尝试用pprof剖析
建立性能基准
Go程序优化的基本套路
成果优化:怎么实现一个TCP服务器?

该思维导图由 AI 生成,仅供参考

你好,我是 Tony Bai。
在上一讲中,我们初步实现了一个基于 TCP 的自定义应用层协议的通信服务端。对于一个常驻内存的服务端而言,更高的性能以及更低的资源消耗,始终是后端开发人员的追求。同时,更高性能的服务程序,也意味着在处理相同数量访问请求的前提下,我们使用的机器数量更少,这可是为公司节省真金白银的有效策略。
而且,Go 语言最初设计时就被定位为“系统级编程语言”,这说明高性能也一直是 Go 核心团队的目标之一。很多来自动态类型语言的开发者转到 Go 语言,几乎都有着性能方面的考量。
所以,在实战篇的最后一讲,我们就结合上一讲实现的自定义应用层协议的通信服务端,看看优化 Go 程序使用的常用工具与套路,给你引引路。

Go 程序优化的基本套路

Go 程序的优化,也有着固定的套路可循,这里我将它整理成了这张示意图:
这张图也不难理解,我简单解释一下。
首先我们要建立性能基准。要想对程序实施优化,我们首先要有一个初始“参照物”,这样我们才能在执行优化措施后,检验优化措施是否有效,所以这是优化循环的第一步。
第二步是性能剖析。要想优化程序,我们首先要找到可能影响程序性能的“瓶颈点”,这一步的任务,就是通过各种工具和方法找到这些“瓶颈点”。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文介绍了如何通过Go语言实现一个TCP服务器,并围绕Go程序的优化套路展开讨论。作者首先介绍了Go程序优化的基本套路,包括建立性能基准、性能剖析、代码优化和与基准比较确定优化效果。文章详细讲解了如何建立性能基准,包括通过编写Go原生提供的性能基准测试用例和基于度量指标为程序建立图形化的性能基准。作者建议使用Prometheus+Grafana的组合来建立性能指标观测设施,并提供了在Linux主机上安装这些工具的步骤。文章还介绍了如何在服务端埋入度量数据采集点,并在Grafana中建立对应的仪表板来展示这些度量数据项。最后,文章给出了docker-compose.yml文件的配置和执行步骤,并提供了一幅示意图来帮助读者更直观地了解整个观测设施中各个工具之间的关系。整体而言,本文内容涵盖了TCP服务器的实现和Go程序优化的基本套路,对于后端开发人员具有一定的参考价值。文章通过介绍使用pprof工具进行性能剖析和代码优化的实例,为读者提供了实用的技术指导,帮助他们更好地理解和应用Go语言进行性能优化。文章还详细介绍了如何使用sync.Pool进行堆内存对象的重用,以减少GC的压力,进而提高程序性能。通过实例分析和优化,读者可以深入了解Go语言性能优化的方法和技巧。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Tony Bai · Go 语言第一课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(23)

  • 最新
  • 精选
  • 多选参数
    原本以为最后的实战课只是写一个稍微大点的项目。结果,恰恰相反,老师使用一个极小的项目,带着走了一遍 Go 开发和调优的过程,这种方法论或者思想的传授,真的比单纯写代码可以学到更多,是在其他书籍或专栏所没有的,很喜欢这种方式!感谢老师! 简单总结下,最后实战课核心的内容: 1. Go 开发的流程结合自己的几个月的经历,总结下:(1)首先是明确问题或需求,之后根据问题或需求,提炼出需要用到的技术点,比如 socket 技术;(2)之后,去调研相关的技术或相关项目;(3)最后写设计方案;(4)根据设计方案,实现代码。(PS:这个过程可以循环迭代) 2. Go 程序优化的过程:(1)首先是明确衡量指标,比如某个函数的执行时间、程序在一定时间内可以处理的请求量、接受的数据包数量等等(并获取首次的指标情况);(2)之后,使用 pprof 获取 CPU、内存的使用情况,并分析使用情况,得到整个程序的瓶颈所在;(3)最后,根据分析得到的结果,优化源码;(4)优化源码后,再次测试衡量指标,根据新获取的指标情况,决定是否继续分析和优化。 另外,可以继续优化点的应该就是与 Submit 使用相同的地方,比如 submitAck。

    作者回复: 👍

    2022-05-05
    6
  • 罗杰
    什么时候能和老师一样有如此丰富的优化经验呢,讲的非常精彩。

    作者回复: 👍

    2022-01-28
    2
    6
  • 菠萝吹雪—Code
    精彩的优化过程!感谢老师这个专栏,很喜欢这种讲解知识点的方式!第一遍结束,第二遍开始记笔记分析每一行代码,第三遍再过下老师的两本书,应该算是完全入门go了

    作者回复: 书也买了啊,哈哈,嫡系了🤝

    2022-09-14归属地:北京
    3
  • 骚动
    mac下的配置参考: 1. mac不能用host,需要用ports端口映射 version: "3.2" services: prometheus: container_name: prometheus image: prom/prometheus:latest ports: - "9090:9090" volumes: - ./conf/tcp-server-prometheus.yml:/etc/prometheus/prometheus.yml - /etc/localtime:/etc/localtime restart: on-failure grafana: container_name: grafana image: grafana/grafana:latest restart: on-failure ports: - "3000:3000" volumes: - /etc/localtime:/etc/localtime - ./data/grafana:/var/lib/grafana node_exporter: image: quay.io/prometheus/node-exporter:latest restart: always container_name: node_exporter command: - '--path.rootfs=/host' ports: - "9100:9100" volumes: - /Users/zouqiang/Documents/docker/monitor/data/node_exporter 2. grafana上配置prometheus数据源时,url: http://本机ip:端口 , 不要用http://localhost:9090这种方式,因为容器间的localhost不是同一个localhost, 用主机名直接指定即可

    作者回复: 👍

    2022-08-19归属地:北京
    2
    3
  • @%初%@
    老师,有个问题,sync.pool也就是内存池那块,请求过来之后,我看到是直接放到内存池,没有置空数据,那么在内存池获取的时候,会不会出现数据错乱的问题呢?

    作者回复: 好问题。使用sync.Pool时,复用内存对象当前数据可能会造成数据错乱的情况。因此如何处理这种问题,要看pool中的对象是什么以及如何用? 像我们这里,pool中对象是一个Submit的指针,而submit的结构如下: type Submit struct { ID string Payload []byte } 这个结构在每次decode时都会被全量覆盖: func (s *Submit) Decode(pktBody []byte) error { s.ID = string(pktBody[:8]) s.Payload = pktBody[8:] return nil } 也就是说虽然put时没有重置,但取出后重置了。 换成其他pool中对象和其他场景,的确需要考虑是否需要重置对象,以及是在put时还是get时进行重置。

    2022-06-19
    3
  • bufio.Reader.Read 方法内部,每次从 net.Conn 尝试读取其内部缓存大小的数据,而不是用户传入的希望读取的数据大小 -----------这里有个疑问,server端for 循环里不是每次都是重新读取到的conn传过来的数据吗,也就是每次client端发送过来的payload都是要有一次必要的读取,为什么会减少读取的次数呢?

    作者回复: 是减少socket read系统调用的次数,也就是说增加buf read前,每次read,无论read 1个字节,还是read 100个字节,都要执行一次sys call。而增加buf read后的read会看buf中是否有足够的数据,如果有数据满足read需求,就不会执行syscall去读取底层的socket数据。当然之所以能做这么是因为bufio.Reader.Read每次从底层socket读取的数据并不是上层传入的大小,默认值为defaultBufSize ,即4096。

    2022-04-14
    2
    3
  • Geek_25f93f
    老师,看网上有种说法。池化这种事情,Java很早很早之前也做过 现在都不怎么提了,go的编译器太弱了?

    作者回复: "管它黑猫白猫,能抓住老鼠就是好猫",至少目前sync.Pool能在一定场景下帮助我们提升性能。 go与java虽然都是gc语言,但由于类型系统的差异,gc所面对的环境也不同,有些事情不能以强弱来论。比如java gc做分代,而go gc没有分代,不是go gc不想做分代,而是经过实测分代对go gc的提升有限,因为go的许多生命周期很短的临时对象经过逃逸分析后是分配在栈上的,完全不需要在堆上分配。 当然和java比,go在gc方面的打磨时间还远远不够。这反过来说也是好事,目前go gc已经很不错了,未来只能更强大。作为Go应用开发者,我们将来可以做到“躺赢”:)。

    2022-06-26
    2
  • Mew151
    老师,想问一下, tcp-server-demo2-with-pprof 的代码,我在我的 Mac 上分别启动 server 和 client ,过了一段时间后 server 报如下错误(打印是我加了些详细日志之后的): metrics server start ok(*:8889) server start ok(on *:8888) [2022-10-13 13:41:54.562] io.ReadFull: read tcp 127.0.0.1:8888->127.0.0.1:49689: read: operation timed out totalLen is 28, n is 20 [2022-10-13 13:41:54.562] handleConn: frame decode error: read tcp 127.0.0.1:8888->127.0.0.1:49689: read: operation timed out [2022-10-13 13:41:55.545] io.ReadFull: read tcp 127.0.0.1:8888->127.0.0.1:49754: read: operation timed out totalLen is 32, n is 15 [2022-10-13 13:41:55.545] handleConn: frame decode error: read tcp 127.0.0.1:8888->127.0.0.1:49754: read: operation timed out [2022-10-13 13:41:55.590] io.ReadFull: read tcp 127.0.0.1:8888->127.0.0.1:49729: read: operation timed out totalLen is 31, n is 26 ... 请问这是什么原因呢?我看程序里也没有设置 SetReadDeadline 的地方,为什么会报读超时呢

    作者回复: 在linux上跑一个小时,也没有出现问题。以前还真没在mac上跑过,我抽时间调查一下。

    2022-10-13归属地:北京
    3
    1
  • Geek_25f93f
    老师,这个frame.decode解包过程为什么不会出现粘包的现象啊?按理说收到这个frame部分也可能出现半包的情况啊,为什么我不管怎么试 在服务端加sleep时间 或者 加client的发送包体长度和协程数量,也没办法进入 if n != int(totalLen-4) 这个判断分支呀

    作者回复: 主要是因为io.ReadFull这个函数,可以查看一下它的manual。

    2022-06-27
    3
    1
  • 木木
    我这里Frame的Decode函数在读totalLen的时候会有小概率读到一个错误的数值,请问有人知道这是为什么吗?

    作者回复: 可以试着将你收到的所有数据按字节逐个dump出来。可以使用hex.Dump这个函数。 btw同问:有遇到过这种错误情况的么?

    2022-03-22
    2
    1
收起评论
显示
设置
留言
23
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部