前言
我是Prometheus和Grafana的忠实粉丝。 作为谷歌前SRE,我学会了注重优质的监控,这种结合让我在过去的一年中一直处于不败之地。 我一直在用它们来监控我的个人服务器(包括了黑盒和白盒监控)、用它们监控Euskal Encounter 外部和内部基础事件,用它们为客户做的专业事等等。 Prometheus编写自定义的导出程序,使得监视自己的数据变得非常简单,而且很有可能你会找到一个在你项目之外的导出服务已经在为你工作了。 例如,我们使用sql_exporter为Encounter事件为突发事件现场参数创建了一个非常不错的仪表板。
因为把node_exporter放到任意一台电脑上很容易,而且有一个基本的系统级度量(CPU,内存,网络,磁盘,文件系统的使用等)Prometheus实例,我想,为什么不监视我的笔记本? 我有一台Clevo“游戏”笔记本电脑,它可以作为我的主要工作站,大部分都是我假装在家里用桌面电脑,但是我出门还经常带上它,参加像Chaos通信大会这样的大型活动。 由于我已经有一个笔记本和Prometheus的服务器之间的VPN,所以我只需要启动prometheus-node_exporter,调出服务,并指向我的Prometheus实例。 这会自动为其配置警报,这意味着每当我打开太多Chrome选项卡并耗尽我的32GB内存时,我的手机将发出巨大的噪音。 完美。
出现了一个小麻烦
不过,设置完之后一个小时,我的手机上确实出现了一个页面:我新添加的目标是无法访问的。 所显而易见的是 SSH 可以进入笔记本电脑,但是node_exporter已经崩溃。
fatal error: unexpected signal during runtime execution [signal SIGSEGV: segmentation violation code=0x1 addr=0xc41ffc7fff pc=0x41439e] goroutine 2395 [running]: runtime.throw(0xae6fb8, 0x2a) /usr/lib64/go/src/runtime/panic.go:605 +0x95 fp=0xc4203e8be8 sp=0xc4203e8bc8 pc=0x42c815 runtime.sigpanic() /usr/lib64/go/src/runtime/signal_unix.go:351 +0x2b8 fp=0xc4203e8c38 sp=0xc4203e8be8 pc=0x443318 runtime.heapBitsSetType(0xc4204b6fc0, 0x30, 0x30, 0xc420304058) /usr/lib64/go/src/runtime/mbitmap.go:1224 +0x26e fp=0xc4203e8c90 sp=0xc4203e8c38 pc=0x41439e runtime.mallocgc(0x30, 0xc420304058, 0x1, 0x1) /usr/lib64/go/src/runtime/malloc.go:741 +0x546 fp=0xc4203e8d38 sp=0xc4203e8c90 pc=0x411876 runtime.newobject(0xa717e0, 0xc42032f430) /usr/lib64/go/src/runtime/malloc.go:840 +0x38 fp=0xc4203e8d68 sp=0xc4203e8d38 pc=0x411d68 github.com/prometheus/node_exporter/vendor/github.com/prometheus/client_golang/prometheus.NewConstMetric(0xc42018e460, 0x2, 0x3ff0000000000000, 0xc42032f430, 0x1, 0x1, 0x10, 0x9f9dc0, 0x8a0601, 0xc42032f430) /var/tmp/portage/net-analyzer/prometheus-node_exporter-0.15.0/work/prometheus-
像许多Prometheus组件一样,node_exporter是用Go编写的。 Go是一种相对安全的语言,如果你愿意的话,它可以让你有足够的自由度,而且它没有像Rust那样强有力的安全保证,但是这在Go下不小心会引发错误。 更重要的是,node_exporter是一个相对简单的Go应用程序,主要是纯Go的依赖关系。 因此,这是一个有趣的崩溃。 特别是因为崩溃是在mallocgc里面的,但是一般情况下它不会崩溃。
重启几次之后,事情变得更有趣了:
2017/11/07 06:32:49 http: panic serving 172.20.0.1:38504: runtime error: growslice: cap out of range goroutine 41 [running]: net/http.(*conn).serve.func1(0xc4201cdd60) /usr/lib64/go/src/net/http/server.go:1697 +0xd0 panic(0xa24f20, 0xb41190) /usr/lib64/go/src/runtime/panic.go:491 +0x283 fmt.(*buffer).WriteString(...) /usr/lib64/go/src/fmt/print.go:82 fmt.(*fmt).padString(0xc42053a040, 0xc4204e6800, 0xc4204e6850) /usr/lib64/go/src/fmt/format.go:110 +0x110 fmt.(*fmt).fmt_s(0xc42053a040, 0xc4204e6800, 0xc4204e6850) /usr/lib64/go/src/fmt/format.go:328 +0x61 fmt.(*pp).fmtString(0xc42053a000, 0xc4204e6800, 0xc4204e6850, 0xc400000073) /usr/lib64/go/src/fmt/print.go:433 +0x197 fmt.(*pp).printArg(0xc42053a000, 0x9f4700, 0xc42041c290, 0x73) /usr/lib64/go/src/fmt/print.go:664 +0x7b5 fmt.(*pp).doPrintf(0xc42053a000, 0xae7c2d, 0x2c, 0xc420475670, 0x2, 0x2) /usr/lib64/go/src/fmt/print.go:996 +0x15a fmt.Sprintf(0xae7c2d, 0x2c, 0xc420475670, 0x2, 0x2, 0x10, 0x9f4700) /usr/lib64/go/src/fmt/print.go:196 +0x66 fmt.Errorf(0xae7c2d, 0x2c, 0xc420475670, 0x2, 0x2, 0xc420410301, 0xc420410300) /usr/lib64/go/src/fmt/print.go:205 +0x5a
但是 Sprintf却崩了。
runtime: pointer 0xc4203e2fb0 to unallocated span idx=0x1f1 span.base()=0xc4203dc000 span.limit=0xc4203e6000 span.state=3 runtime: found in object at *(0xc420382a80+0x80) object=0xc420382a80 k=0x62101c1 s.base()=0xc420382000 s.limit=0xc420383f80 s.spanclass=42 s.elemsize=384 s.state=_MSpanInUse <snip> fatal error: found bad pointer in Go heap (incorrect use of unsafe or cgo?) runtime stack: runtime.throw(0xaee4fe, 0x3e) /usr/lib64/go/src/runtime/panic.go:605 +0x95 fp=0x7f0f19ffab90 sp=0x7f0f19ffab70 pc=0x42c815 runtime.heapBitsForObject(0xc4203e2fb0, 0xc420382a80, 0x80, 0xc41ffd8a33, 0xc400000000, 0x7f0f400ac560, 0xc420031260, 0x11) /usr/lib64/go/src/runtime/mbitmap.go:425 +0x489 fp=0x7f0f19ffabe8 sp=0x7f0f19ffab90 pc=0x4137c9 runtime.scanobject(0xc420382a80, 0xc420031260) /usr/lib64/go/src/runtime/mgcmark.go:1187 +0x25d fp=0x7f0f19ffac90 sp=0x7f0f19ffabe8 pc=0x41ebed runtime.gcDrain(0xc420031260, 0x5) /usr/lib64/go/src/runtime/mgcmark.go:943 +0x1ea fp=0x7f0f19fface0 sp=0x7f0f19ffac90 pc=0x41e42a runtime.gcBgMarkWorker.func2() /usr/lib64/go/src/runtime/mgc.go:1773 +0x80 fp=0x7f0f19ffad20 sp=0x7f0f19fface0 pc=0x4580b0 runtime.systemstack(0xc420436ab8) /usr/lib64/go/src/runtime/asm_amd64.s:344 +0x79 fp=0x7f0f19ffad28 sp=0x7f0f19ffad20 pc=0x45a469 runtime.mstart() /usr/lib64/go/src/runtime/proc.go:1125 fp=0x7f0f19ffad30 sp=0x7f0f19ffad28 pc=0x430fe0
现在垃圾处理机制偶然间又发现了一个问题。
在这点上,得出两个很明显结论:要么有一个严重的硬件问题,要么在二进制文件中存在一个混乱的内存损坏错误。 我最初认为前者是不太可能的,因为这台机器的负载工作非常混杂,没有迹象表明可以追溯到硬件(我有平衡分配软件内存资源,而不会随机分配)。 由于类似于node_exporter的Go二进制文件是静态链接的,并且不依赖于任何其他库,所以我可以下载正式版本的二进制文件,然后尝试去做,这可以将大部分剩余的系统因素导致的出错情况排除掉。 然而,当我这样做的时候,我还是崩溃了。
unexpected fault address 0x0 fatal error: fault [signal SIGSEGV: segmentation violation code=0x80 addr=0x0 pc=0x76b998] goroutine 13 [running]: runtime.throw(0xabfb11, 0x5) /usr/local/go/src/runtime/panic.go:605 +0x95 fp=0xc420060c40 sp=0xc420060c20 pc=0x42c725 runtime.sigpanic() /usr/local/go/src/runtime/signal_unix.go:374 +0x227 fp=0xc420060c90 sp=0xc420060c40 pc=0x443197 github.com/prometheus/node_exporter/vendor/github.com/prometheus/client_model/go.(*LabelPair).GetName(...) /go/src/github.com/prometheus/node_exporter/vendor/github.com/prometheus/client_model/go/metrics.pb.go:85 github.com/prometheus/node_exporter/vendor/github.com/prometheus/client_golang/prometheus.(*Desc).String(0xc4203ae010, 0xaea9d0, 0xc42045c000) /go/src/github.com/prometheus/node_exporter/vendor/github.com/prometheus/clien
又一次出现完全不同的崩溃。 在这一点上,我偶然碰到了一个确实存在依赖node_exporter包或其依赖其他包的问题,所以我在GitHub上提出了一个问题。 有可能开发者以前见过这个问题? 如果他们有任何想法,那么可以引起他们的注意并快速解决问题。
长时间的打转和绕弯
毫不奇怪,第一个猜测是一个硬件问题。 这不无道理:毕竟,我只是在一台特定的机器上碰到问题。 我所有的其他机器都可以的运行node_exporter。 虽然在这个主机上我没有连接其他不稳定的硬件,但是我也没有其他的解释来说明这台机器的特别之处能够导致node_exporter崩溃。 Memtest86+运行从来不会崩溃,所以我安装了一个。
如下图所示:
哎呦! 糟糕的内存。 那么,更具体一点,内存有坏块。 通过测试运行一个完整的过程后,我得到的只是一个坏的位,再加上测试7中的一些错误肯定了我的想法(它移动周围的内存块,所以可以放大一个微小的错误)。
进一步的测试表明,在SMP模式下的Memtest86 + test#5可以快速检测到错误,但通常不会在第一遍检测到。 错误总是在相同的地址相同的位。 这表明这个问题是一个微小的坏块或泄漏的内存单元。 特别是随温度变差的一种。 这是非常合乎逻辑的:更高的温度会增加内存单元泄漏几率,并且因此更有可能的是,稍微有点边缘的内存单元实际上会导致位翻转。
从这个角度来看,这是274,877,906,944中的一个坏位。这实际上是一个很好的错误率!但是硬盘和闪存的错误率要高得多 - 只是这些设备在出厂时标有坏块,在用户不知情的情况下被调换出去,然后可以将新发现的弱块标记为坏块,并将其重新定位到备用区。内存没有这样的奢侈,所以一个坏位会永远存在。
唉,这是不可能是我陷入node_exporter困境的原因。那个应用程序使用的RAM很少,所以它碰到坏位的机会(反复的碰到)是非常低的。这种问题在很大程度上并不明显,可能会导致某些图形中的像素错误,在某些文本中出现单个字母翻转,破坏一些指令很可能导致无法运行,也许也有很小概率是当某些实际重要的东西确实落在了坏点上。尽管如此,它确实会导致程序长期运行的可靠性问题,这就是服务器和其他可靠设备必须使用ECC RAM(ECC是一种能够实现纠错的技术)才能纠正这种错误的原因。
我使用温度设置在130°C的热风枪一次性预热了两个模块(其他两个模块在后盖下,因为我的笔记本电脑总共有四个SODIMM插槽)。运行了一下模块指令,我发现额外三个只能在高温下检测到弱点,他们分布在我的三个RAM上。
我还发现,即使我交换了模块,错误的位置仍然保持大致一致:地址的最高位保持不变。这是因为内存条是交错分布的:数据遍布在四个内存条上,但并不是每个内存条被分配为连续可用地址空间的四分之一。这很方便,因为我可以屏蔽一个足够大的RAM区域,以覆盖每个错误位的所有可能的地址,而不必担心将来可能会交换内存条并且弄脏屏蔽。我发现屏蔽连续的128KiB区域应该能覆盖每个给定坏点的所有可能的地址排列,但是,为了更好的解决,我将它们四舍五入到1MiB。这给了我三个1MiB对齐的块来掩盖它(其中一个掩盖了两个坏点,总共四个我想要掩蔽的坏点):
评论删除后,数据将无法恢复
评论(0)