热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

无人值守的自动dump(二)

之前在这篇无人值守(一)[1]简单介绍了我们针对线

之前在这篇无人值守(一)[1]简单介绍了我们针对线上抖动问题定位的工具的设计思路,思路很简单,技术含量很低,是个人都可以想得到,但是它确实帮我们查到了很多很难定位的问题。

在本篇里,我们重点讲一讲这个工具[2]在生产环境帮我们发现了哪些问题。

OOM 类问题

RPC decode 未做防御性编程,导致 OOM

应用侧的编解码可能是非官方实现(如 node 之类的无官方 SDK 的项目),在一些私有协议 decode 工程中会读诸如 list len 之类的字段,如果外部编码实现有问题,发生了字节错位,就可能会读出一个很大的值。

非标准 app ----encode-------> 我们的应用 decode -----> Boom!

decoder 实现都是需要考虑这种情况的,类似这样[3]。如果对请求方的数据完全信任,碰到对方的 bug 或者恶意攻击,可能导致自己的服务 OOM。

在线上实际发现了一例内存瞬间飚升的 case,收到报警后,我们可以看到:

1: 1330208768 [1: 1330208768] @ 0x11b1df3 0x11b1bcb 0x119664e 0x11b1695 0x1196f77 0x11a956a 0x11a86c7 0x1196724 0x11b1695 0x11b1c29 0x119664e 0x11b1695 0x11b1c29 0x119664e 0x11b1695 0x11b1c29 0x119664e 0x11bb360 0x168f143 0x179c2fc 0x1799b70 0x179acd6 0x16d3306 0x16d1088 0xf59386 0xf59474 0xf54e5f 0xf54987 0xf539f1 0xf6043a 0xcd8c0d 0x49b481
....下面是表示栈内容的,这不重要

1: 1330208768 [1: 1330208768]
表示 inuse_objects : inuse_space [alloc_objects : alloc_space]
,这里可以看到一个对象就直接用掉了 1GB 内存,显然不是正常情况,查看代码后,发现有未进行大小判断而完全信任用户输入数据包的 decode 代码。

修复起来很简单,像前面提到的 async-h1
一样加个判断就可以了。

tls 开启后线上进程占用内存上涨,直至 OOM

线上需要做全链路加密,所以有开启 tls 的需求,但开启之后发现内存一路暴涨,直至 OOM,工具可以打印如下堆栈:

heap profile: 1460: 27614136 [45557: 1080481472] @ heap/1048576
727: 23822336 [730: 23920640] @ 0xc56b96 0xc591e8 0xc58e68 0xc59ed1 0xdd55ff 0xde15b8 0xde13ef 0xde09e9 0xde050c 0x13bfa13 0x13bf475 0x14c33d0 0x14c49f8 0x14cb398 0x14bffab 0x14cdf78 0xddcf90 0x45eda1
# 0xc56b95  *****mtls/crypto/tls.(*block).reserve+0x75   *****mtls/crypto/tls/conn.go:475

查阅老版本的 Go 代码,发现其 TLS 的 write buffer 会随着写出的数据包大小增加而逐渐扩容,其扩容逻辑比较简单:

func (b *block) reserve(n int) {
 if cap(b.data) >= n {
  return
 }
 m := cap(b.data)
 if m == 0 {
  m = 1024
 }
 for m < n {
  m *= 2
 }
 data := make([]byte, len(b.data), m)
 copy(data, b.data)
 b.data = data
}

初始为 1024,后续不够用每次扩容为两倍。但是阅读 tls 的代码后得知,这个写出的数据包大小最大实际上只有 16KB + 额外的一个小 header 大小左右,但老版本的实现会导致比较多的空间浪费,因为最终会扩容到 32KB。

这段比较浪费空间的逻辑在 Go1.12 之后已经进行了优化:

func sliceForAppend(in []byte, n int) (head, tail []byte) {
 if total := len(in) + n; cap(in) >= total {
  head = in[:total]
 } else {
  head = make([]byte, total)
  copy(head, in)
 }
 tail = head[len(in):]
 return
}

变成了需要多少,分配多少的朴实逻辑。所以会比老版本在这个问题上有不少缓解,不过在我们的场景下,新版本的代码依然没法满足需求,所以还需要进一步优化,这就是后话了。

goroutine 暴涨类问题

本地 app GC hang 死,导致 goroutine 卡 channel send

在我们的程序中有一段和本地进程通信的逻辑,write goroutine 会向一个 channel 中写数据,按常理来讲,同物理机的两个进程通过网络通信成本比较低,类似下面的代码按说不太可能出问题:


concurrently:
taskChan <- task

consumer:
for task := range taskChan {
    // 憋一些 task 一起写
    localConnection.write(task 们)
}


看起来问题不大,但是线上经常都有和这个 channel send 相关的抖动,我们通过工具拿到的现场:

2020-11-03 08:00:05,950 [ERROR] [diag.goroutine] [diagnose] pprof goroutine, config_min : 3000, config_diff : 25, config_abs : 200000,  previous : [41402 44257 47247 50085 52795 55509 29762 32575 35451 38460], current : 55509, profile : goroutine profile: total 55513
40844 @ 0x46daaf 0x4433ab 0x443381 0x443165 0xf551f7 0x12fd2e7 0x12fc94f 0x13f41d5 0x13fc45f 0xf43ee4 0xcd8c0d 0x49b481
#    ****channel.Send 这是个假的栈,你理解意思就行了


当前憋了 5w 个 goroutine,有 4w 个卡在 channel send 上,这个 channel 的对面还是一条本地连接,令人难以接受。

但是要考虑到,线上的业务系统是 Java,Java 发生 FGÇ 的时候可不是闹着玩的。对往本地连接的 write buffer 写数据一定不会卡的假设是有问题的。

既然出问题了,说明在这里对我们的程序进行保护是必要的,修改起来也很简单,给 channel send 加一个超时就可以了。

应用逻辑死锁,导致连接不可用,大量 goroutine 阻塞在 lock 上

大多数网络程序里,我们需要在发送应用层心跳,以保证在一些异常情况(比如拔网线)下,能够把那些无效连接从连接池里剔除掉。

对于我们的场景来说,客户端向外创建的连接,如果一直没有请求,那么每隔一段时间会向外发送一个应用心跳请求,如果心跳连续失败(超时) N 次,那么将该连接进行关闭。

在这个场景下会涉及到两把锁:

  • 对连接进行操作的锁 conn lock
  • 记录心跳请求的 request map lock

心跳成功的流程:收到心跳响应包,获取 conn lock -> 获取 request map lock
心跳失败的流程:timer 超时,获取 request map lock -> 需要关闭连接 -> 获取 conn lock

可以看出来,心跳的成功和失败流程并发时,获取锁的流程符合死锁的一般定义:持有锁、非抢占、循环等待。

这个 bug 比较难触发,因为心跳失败要失败 N 次才会关闭连接,而正好在最后一次发生了心跳成功和失败并发才会触发上述的死锁,线上可以通过 goroutine 短时间的上涨发现这个问题,goroutine 的现场也是可以看得到的。简单分析就可以发现这个死锁问题(因为后续的流程都会卡在其中一把锁上)。

知道原因解决起来就不麻烦了,涉及到一些具体的业务逻辑,这里就不赘述了。

CPU 尖刺问题

应用逻辑导致死循环问题

国际化业务涉及到冬夏令时的切换,从夏令时切换到冬令时,会将时钟向前拔一个月,但天级日志轮转时,会根据轮转前的时间计算 24 小时后的时间,并按与 24:00 的差值来进行 time.Sleep,这时会发现整个应用的 CPU 飚高。自动采样结果发现一直在循环计算时间和重命名文件。

list 一下相关的函数,能很快地发现执行死循环的代码位置。这里就不截真实代码了,随便举个例子:

        .          .     23:func cpuex(wr http.ResponseWriter, req *http.Request) {
         .          .     24: go func() {
    17.73s     19.37s     25:  for {
         .          .     26:  }
         .          .     27: }()
         .          .     28:}

参考资料

[1]

无人值守(上): https://xargin.com/autodumper-for-go/

[2]

工具: https://github.com/mosn/holmes

[3]

这样: https://github.com/http-rs/async-h1/blob/main/src/server/decode.rs#L41





推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • OpenMap教程4 – 图层概述
    本文介绍了OpenMap教程4中关于地图图层的内容,包括将ShapeLayer添加到MapBean中的方法,OpenMap支持的图层类型以及使用BufferedLayer创建图像的MapBean。此外,还介绍了Layer背景标志的作用和OMGraphicHandlerLayer的基础层类。 ... [详细]
  • 使用freemaker生成Java代码的步骤及示例代码
    本文介绍了使用freemaker这个jar包生成Java代码的步骤,通过提前编辑好的模板,可以避免写重复代码。首先需要在springboot的pom.xml文件中加入freemaker的依赖包。然后编写模板,定义要生成的Java类的属性和方法。最后编写生成代码的类,通过加载模板文件和数据模型,生成Java代码文件。本文提供了示例代码,并展示了文件目录结构。 ... [详细]
  • 本文介绍了在PostgreSQL中批量导入数据时的优化方法。包括使用unlogged表、删除重建索引、删除重建外键、禁用触发器、使用COPY方法、批量插入等。同时还提到了一些参数优化的注意事项,如设置effective_cache_size、shared_buffer等,并强调了在导入大量数据后使用analyze命令重新收集统计信息的重要性。 ... [详细]
  • 流数据流和IO流的使用及应用
    本文介绍了流数据流和IO流的基本概念和用法,包括输入流、输出流、字节流、字符流、缓冲区等。同时还介绍了异常处理和常用的流类,如FileReader、FileWriter、FileInputStream、FileOutputStream、OutputStreamWriter、InputStreamReader、BufferedReader、BufferedWriter等。此外,还介绍了系统流和标准流的使用。 ... [详细]
  • 本文介绍了如何通过维持两个堆来获取一个数据流中的中位数。通过使用最大堆和最小堆,分别保存数据流中较小的一半和较大的一半数值,可以保证两个堆的大小差距为1或0。如果数据流中的数量为奇数,则中位数为较大堆的最大值;如果数量为偶数,则中位数为较大堆的最大值和较小堆的最小值的平均值。可以使用优先队列来实现堆的功能。本文还提供了相应的Java代码实现。 ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • 生产环境下JVM调优参数的设置实例
     正文前先来一波福利推荐: 福利一:百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。福利二 ... [详细]
  • Python中程序员的面试题有哪些
    小编给大家分享一下Python中程序员的面试题有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有 ... [详细]
author-avatar
此女我很爱_484
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有