聊一聊可观测性

不知为何,可观测性这个词突然就火起来,各个大厂都甚至纷纷成立可观测团队了。那么可观测性究竟是个啥呢?它凭什么能火起来呢?它与传统监控有什么区别呢,是否又是新瓶装旧酒? 说实话,虽然我很久之前就听说过可观测性这个词,不过一直持怀疑态度,认为不过是炒概念。直到最近,读了《Observability Engineering》这本书,让我打开新视界,可观测性并非空穴来风,其确实有它的价值,而且相当大。 什么是可观测性 可观测性(Observability)这个单词最开始是出现于数学领域,表示度量一个系统内部状态由其外部输出的信息中推断出来的程度。Wikipedia上的定义有点抽象,举个汽车的例子: 一辆汽车可以说是一个系统,当其外部输出信息只有油箱油量、当前车速时,我们能推断出车辆预计还能行驶多少公里 而当其外部输出信息还包括发动机温度、轮胎磨损程度时,我们还能推断出发动机和轮胎的工作情况是否需要维护,如果需要维护那么能行驶的距离显然会比之前预估的行驶举例少,显然此时这辆汽车的可观测性程度比之更高了 而对于一个软件系统,它所能暴露的外部信息常见的有指标、日志、链路追踪事件,显然我们可以通一些工具来分析它的外部信息从而推断系统的内部状态。因此这里,软件系统暴露的信息越详细,分析工具越强劲,内部状态就能推断地越准确与详细,那么该软件系统的可观测性就越好。 不过,此时你可能有疑问:这些指标日志啥的不都是些的现成东西吗?那是不是可以说,本质上,传统意义的监控就是可观测性呢?这里的答案是否定的,且听我下文解释。 与传统监控的区别 传统意义上的监控(Monitoring)是一种行为,这种行为分析系统的内部状态。而可观测性正如上文中描述的一样,是一种系统的性质,其他性质有健壮性、可测试性等 在传统监控的场景下,我们收集、存储并分析指标数据。我们制作监控大盘并设置各种告警,当发生异常时,告警触发,收到告警后,我们根据告警的内容以及相关的监控图表,来推断出异常发生的原因,并由此做出相应的处理(或增加资源、或修复Bug)。 但是这里有一个很大前提,就是告警策略必须预先设置,而如何设置又完全取决于经验与直觉。换句话说,通过监控我们只能检测一些已知的潜在风险,例如机器的负载、CPU使用率、磁盘使用率等。而对于一个未知或复杂的系统,当它发生异常时,我们往往只能束手无策,或者通过一些线索去猜测可能的原因并验证,如果猜错了那又得重复上述过程,非常的浪费时间。 而在可观测性的场景下,系统中植入了各种各样的代码和工具,并提供了非常丰富的可观测的数据(metric, logs, traces等各种数据),通过这些数据并结合合适的工具,我们能够很快地排查出问题的根因所在。举个例子,同样是一个未知的系统,当发现某个接口很慢时,我们可以通过链路追踪工具找到瓶颈点,通过瓶颈点再分析当时的系统资源使用率,饱和度,负载情况以及应用日志等,从而很快地定位出根因(资源问题?代码问题?第三方服务问题?等等) 流行的原因 近10年IT相关行业发生了天翻地覆的变化。IT技术也是日新月异,尤其是微服务架构、分布式以及云原生的高速发展,以及各种敏捷开发思想深入人心。 如今的软件系统已经与10年前的大不相同了,复杂度、灵活度、变化度等都大幅提升。而由此带来的问题就是,系统稳定性保障变得越来越困难,尤其是问题根因的定位 例如,对于一些复杂问题,有时候花费数个月都无法定位,最后的选择往往都是推倒重来 因此单靠传统的监控已经无法满足当下软件系统的观测需求,传统监控只能解决那些"known unknown/known"类的问题,而无法应对"unknown unknown"类的问题,而这类问题在如今的架构下要多得多。 Known unknown/known:指的是你熟悉的已知或未知的软件系统,这种系统可预测,因此我们可以预先设置各种告警 Unknown unknown:指的是你不熟悉的未知系统,这种系统完全未知,只能通过可观测性工具来探测 三大支柱是可观测性吗 一说到可观测性,可能最先联想的就是“三大支柱(the three pillars)”,即logs, metrics以及traces(如下图所示)。很多人(包括我)经常以为它们就是所谓的可观测性,毕竟很多PAAS平台和厂商就是这么宣传的,但这并不完全正确。 是的,没错,三大支柱确实是可观测性体系里不可或缺的条件。但这并不代表我暴露了这些数据,我的软件系统就具备了可观测性,同样也不能代表我收集分析了这些数据,我就实现了一个可观测性工具系统。 首先,可观测性需要的数据并非只有这三者,它还可以是用户的反馈信息、系统profiling信息等各种统计、事件信息;其次暴露的数据的维度、基数以及数据间的关联度等等都会影响系统的可观测性;而一个可观测性工具的搭建除了收集这些统计、时间信息,还包括数据传输与处理,数据存储以及交互的易用性等,此外涉及到的数据隔离,容量规划,低成本且高性能等问题也是十分棘手的。 不过,话虽如此,“三大支柱”虽不能等同于可观测性,但它们是你迈向可观测性的第一步:) 总结 综上所述,如果你的应用非常简单,比如一个单体应用,那么传统监控也足够满足需求了。但是一旦切换为微服务架构,甚至完全云原生化的开发方式时,此时软件系统的复杂度就成指数级增加了,而此时可观测性就显得异常重要。 相信你都经过,一个软件系统随着业务的发展会变得越来越复杂,到最后每个人都只能往上面堆功能,而对老代码甚至不敢改动一行,最终软件系统就会变成人们口中的“屎山”,而后面的人的唯一选择只能是推到重来。 而如果可观测性一开始就在架构考虑中,那么无论我们的的系统变化多大,多复杂,我们都能对其了如指掌并快速定位问题根因,此外还能提前发现到系统架构的不合理之处并及时调整。 参考 Observability Engineering https://www.splunk.com/en_us/data-insider/what-is-observability.html https://en.wikipedia.org/wiki/Observability https://www.dynatrace.com/news/blog/what-is-observability-2/

七月 11, 2022 · 1 分钟 · erenming

使用Hugo部署GithubPages

记录一下使用Hugo生成静态博客,并通过githubPages自动化部署的过程。 这里,我的目标是: 使用blog-source作为原始的内容仓库,<your-name>.github.io作为实际的githubPages仓库 通过github Action将两者串联起来,原始内容提交变更时,自动触发内容生成并发布 这样的好处是,可以将blog-source作为私有仓库,并能直接以<your-name>.github.io作为URL。且通过github action实现CICD,解放双手实现自动化。这里我画了一张图,便于理解: Hugo 安装Hugo,然后初始化 # macOS install hugo brew install hugo # create site project hugo new site blog-source 选择你中意的主题并安装 cd blog-source git init # add paperMod as theme git submodule add https://github.com/adityatelange/hugo-PaperMod themes/paperMod 添加文章并启动demo hugo new posts/my-first-post.md # start demo for preview hugo server -D 创建一个额外的仓库,这里我创建一个名为blog-source的仓库并作为刚才创建的blog-source的远端仓库 cd blog-source git init git remote add origin <your-remove-git> GithubPages 创建一个githubPages仓库,名称必须是<your-name>.github.io。DOC Connection 创建sshKey: ssh-keygen -t rsa -b 4096 -C "$(git config user....

五月 29, 2022 · 1 分钟 · erenming

重启博客之路

不知为何,写博客总是断断续续,距离上次更新已过去快一年了,直至如今面试碰壁方才后悔莫及。 事实上,写博客对于对于知识的小伙理解非常有好处,毕竟要让别人听得懂,首先自己得更懂才行嘛。因为,通常学习一项新知识,通常需要理论学习-实践-总结输出三个阶段,而我往往只完成了第一阶段便草草了事,无法对知识有更深入的理解。因此,我打算重启我的博客之路,持续学习持续输出。 之前博客用过Hexo,也用过博客园等等,最近看到hugo的paperMod主题非常讨喜,因此打算彻底切换到hugo+paperMod,也算起个好头吧。之前的文章也会从博客园迁移到hugo上,不过后续两边也会尽量同步更新。 然后,也打算支持中英文双语,主要是为了提升自己的英文水平,以便能和世界上的程序员更好地交流。不过目前主打还是中文,母语写起来还是方便点,英文会挑选文章进行编写翻译,同时英文文章也会同步发布在Medium上。 此外,为了降低断更的概率、提高文章质量,我在这里给自己立一个flag,即每月至少更新一篇文章,文章长度适宜、做到通俗易懂、绝不模棱两可故作高深

五月 26, 2022 · 1 分钟 · erenming

浅析Go内存分配器的实现

为什么需要内存分配器? 总说周知,内存作为一种相对稀缺的资源,在操作系统中以虚拟内存的形式来作为一种内存抽象提供给进程,这里可以简单地把它看做一个连续的地址集合{0, 1, 2, ..., M},由栈空间、堆空间、代码片、数据片等地址空间段组合而成,如下图所示(出自CS:APP3e, Bryant and O’Hallaron的第9章第9节) 这里我们重点关注Heap(堆),堆是一块动态的虚拟内存地址空间。在C语言中,我们通常使用malloc来申请内存以及使用free来释放内存,也许你想问,这样不就足够了吗?但是,这种手动的内存管理会带来很多问题,比如: 给程序员带来额外的心智负担,必须得及时释放掉不再使用的内存空间,否则就很容易出现内存泄露 随着内存的不断申请与释放,会产生大量的内存碎片,这将大大降低内存的利用率 因此,正确高效地管理内存空间是非常有必要的,常见的技术实现有Sequential allocation, Free-List allocation等。那么,在Go中,内存是如何被管理的呢? 注:此为Go1.13.6的实现逻辑,随版本更替某些细节会有些许不同 实现原理 Go的内存分配器是基于TCMalloc设计的,因此我建议你先行查阅,这将有利于理解接下来的内容。 大量工程经验证明,程序中的小对象占了绝大部分,且生命周期都较为短暂。因此,Go将内存划分为各种类别(Class),并各自形成Free-List。相较于单一的Free-List分配器,分类后主要有以下优点: 其一方面减少不必要的搜索时间,因为对象只需要在其所属类别的空闲链表中搜索即可 另一方面减少了内存碎片化,同一类别的空闲链表,每个对象分配的空间都是一样大小(不足则补齐),因此该链表除非无空闲空间,否则总能分配空间,避免了内存碎片 那么,Go内存分配器具体是如何实现的呢?接下来,我将以自顶向下的方式,从宏观到微观,层层拨开她的神秘面纱。 数据结构 首先,介绍Go内存分配中相关的数据结构。其总体概览图如下所示: heapArena 在操作系统中,我们一般把堆看做是一块连续的虚拟内存空间。 Go将其划分为数个相同大小的连续空间块,称之arena,其中,heapArena则作为arena空间的管理单元,其结构如下所示: type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan ... } bitmap: 表示arena区域中的哪些地址保存了对象,哪些地址保存了指针 spans: 表示arena区域中的哪些操作系统页(8K)属于哪些mspan mheap 然后,则是核心角色mheap了,它是Go内存管理中的核心数据结构,作为全局唯一变量,其结构如下所示: type mheap struct { free mTreap ... allspans []*mspan ... arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena ... central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } } free: 使用树堆的结构来保存各种类别的空闲mspan allspans: 用以记录了分配过了的mspan arenas: 表示其覆盖的所有arena区域,通过虚拟内存地址计算得到下标索引 central: 表示其覆盖的所有mcentral,一共134个,对应67个类别 mcentral 而mcentral充当mspan的中心管理员,负责管理某一类别的mspan,其结构如下:...

六月 15, 2021 · 1 分钟 · erenming

Golang内存优化实践指南

最近做了许多有关Go内存优化的工作,总结了一些定位、调优方面的套路和经验,于是,想通过这篇文章与大家分享讨论。 发现问题 性能优化领域有一条总所周知的铁律,即:不要过早地优化。编写一个程序,首先应该保证其功能的正确性,以及诸如设计是否合理、需求等是否满足,过早地优化只会引入不必要的复杂度以及设计不合理等各种问题。 那么何时才能开始优化呢?一句话,问题出现时。诸如程序出现频繁OOM,CPU使用率异常偏高等情况。如今,在这微服务盛行的时代,公司内部都会拥有一套或简单或复杂的监控系统,当系统给你发出相关告警时,你就要开始重视起来了。 问题定位 1. 查看内存曲线 首先,当程序发生OOM时,首先应该查看程序的内存使用量曲线,可以通过现有监控系统查看,或者prometheus之类的开源工具。 曲线一般都是呈上升趋势,比如goroutine泄露的曲线一般是使用量缓慢上升直至OOM,而内存分配不合理往往时在高负载时快速攀升以致OOM。 2. 问题复现 这块是可选项,但是最好能保证复现。如果能在本地或debug环境复现问题,这将非常有利于我们反复进行测试和验证。 3. 使用pprof定位 Go官方工具提供了pporf来专门用以性能问题定位,首先得在程序中开启pprof收集功能,这里假定问题程序已开启pprof。(对这块不够了解的同学,建议通过这两篇文章(1, 2)学习下pprof工具的基本用法) 接下来,我们复现问题场景,并及时获取heap和groutine的采样信息。 获取heap信息: curl http://loalhost:6060/debug/pprof/heap -o h1.out 获取groutine信息:curl http://loalhost:6060/debug/pprof/goroutine -o g1.out 这里你可能想问,这样就够了吗? 当然不是,只获取一份样本信息是不够的。内存使用量是不断变化的(通常是上升),因此我们需要的也是期间heap、gourtine信息的变化信息,而非瞬时值。一般来说,我们需要一份正常情况下的样本信息,一份或多份内存升高期间的样本信息。 数据收集完毕后,我们按照如下3个方面来排查定位。 排查goroutine泄露 使用命令go tool pprof --base g1.out g2.out ,比较goroutine信息来判断是否有goroutine激增的情况。 进入交互界面后,输入top命令,查看期间goroutine的变化。 同时可执行go tool pprof --base g2.out g3.out来验证。我之前写了的一篇实战文章,记录了goroutine泄露的排查过程。 排查内存使用量 使用命令go tool pprof --base h1.out h2.out,比较当前堆内存的使用量信息来判断内存使用量。 进入交互界面后,输入top命令,查看期间堆内存使用量的变化。 排查内存分配量 当上述排查方向都没发现问题时,那就要查看期间是否有大量的内存申请了,以至于GC都来不及回收。使用命令go tool pprof --alloc_space --base h1.out h2.out,通过比较前后内存分配量来判断是否有分配不合理的现象。 进入交互界面后,输入top命令,查看期间堆内存分配量的变化。 一般来说,通过上述3个方面的排查,我们基本就能定位出究竟是哪方面的问题导致内存激增了。我们可以通过web命令,更为直观地查看问题函数(方法)的完整调用链。 问题优化 定位到问题根因后,接下来就是优化阶段了。这个阶段需要对Go本身足够熟悉,还得对问题程序的业务逻辑有所了解。 我梳理了一些常见的优化手段,仅供参考。实际场景还是得实际分析。 goroutine泄露 这种问题还是比较好修复的,需要显式地保证goroutine能正确退出,而非以一些自以为的假设来保证。例如,通过传递context.Context对象来显式退出 go func(ctx context.Context) { for { select { case <-ctx....

一月 9, 2021 · 1 分钟 · erenming