基础软件“好用”指南:必须跨越这两道鸿沟!
2021-10-26
来源:CSDN
最近有一件事情让我印象特别深刻,作为引子和大家唠一唠:我们在内部做一些极端的流量回归仿真实验时,在 TiKV(TiDB 的分布式存储组件)上观测到了异常的 CPU 使用率,但是从我们的 Grafana Metrics、日志输出里面并没有看到异常,因此也一度困惑了好几天,最后靠一位老司机盲猜并结合 profiling 才找到真凶,真凶出现在谁都没有想到的地方:Debug 用的日志模块(澄清一下:目前这个 Bug 已经修复了,而且这个 Bug 的触发是在非常极端压力的场景下+日志级别全开才会出现,请各位用户放心)。
这篇文章并不是做 Bug 分析,我觉得更重要的是,找问题过程中我们使用的工具、老司机的思考过程。作为一个观察者,我看到年轻的同事看着老司机熟练地操作 perf 和在各种各样工具和界面中切换那种仰慕的眼神,我隐约觉得事情有点不对:这意味着这门手艺不能复制。
事后,我做了一些关于基础软件用户体验的调研,发现该领域的理论和资料确实挺少(大多数是 ToC 产品的研究,系统软件相关的大概只有 UNIX 哲学流派),而且缺乏系统化,依赖于作者个人「品味」,但是软件体验的好和坏显然存在,例如一个有经验的工程师看到一个命令行工具,敲几下就知道是否好用,是不是一个有「品味」的工具。
很多时候「品味」之所以被称为「品味」,就是因为说不清道不明,这固然是软件开发艺术性的一种体现,但是这也意味着它不可复制,不易被习得。我觉得这也不好,今天这篇以及可能接下来的几篇文章(虽然后几篇我还不知道写啥,但是先立个 Flag)会试着总结一下好的基础软件体验到底从哪里来。
作为第一篇,本文将围绕可观测性和可交互性两个比较重要的话题来谈。至于为什么把这两点放在一起聊,我先卖个关子,最后说。
可观测性
可观测性是什么?这可从我两年前发表的《我眼中的分布式系统可观测性》[1]一文中可见一斑,相同的内容我在这里就不赘述。随着在 TiDB 中对可观测性实践的深入,对这个话题有了更深的理解,为了更好的理解,我们首先先明确一个问题:当我们在聊可观测的时候,到底是谁在观测?
是谁在观测?
很多朋友可能会一愣,心想:这还用说,肯定是人,总不能是机器。没错,的确是人在观测,但就是这么一个浅显的道理往往会被软件设计者忽略,所以这两者的区别到底是什么?为什么强调人这个主体很重要?
要回答这个问题,需要清楚一个现实:人的短期工作记忆是很有限的。大量的心理学研究表明,人类工作记忆的容量大致只有 4,即在短期同时关注 4 项信息[2],再多的信息就要靠分模块的方式记忆,如我们快速记忆电话号码的方式,以 13800001111 为例,我们通常不是一个个数字背,而是形如:138-0000-1111 进行分组。
在了解人的心智模型的一些基础假设和带宽后,我想很多系统软件开发者大概不再会炫耀:我的软件有 1000 多个监控项!这不仅不是好事,反而让更多的信息破坏了短期记忆的形成,引入了更多的噪音,让使用者在信息的海洋里花很多时间找关键信息,以及不自觉的分类(我相信大脑的一个不自觉的后台任务就是对信息建索引和分类,注意这同样是消耗带宽的),所以第一个结论:软件应用一屏的界面里面最好只有 4 个关键信息。那么,接下来的一个问题是:哪些是关键信息?什么是噪音?
区分关键信息和噪音
这个问题没有标准答案。对于系统软件来说,我的经验是:跟着关键资源走。软件其实很简单,本质就是对硬件资源的使用和分配,讲究平衡的艺术。关键的硬件资源无非也就下面几个,对于下面每一个关键资源在某个采样时间段(单点没有太多意义),都可以通过一些简单的问题的询问,得到对系统运行状态的大致图景:
CPU:哪些线程在工作?这些线程都在干嘛?这些线程各自消耗了多少 CPU Time?
内存:当前内存中存储了哪些东西?这些东西的命中率情况?(通常我们更关注业务缓存)?
网络 I/O:QPS/TPS 有异常吗?当前主要的网络 I/O 是由什么请求发起的?带宽还够吗?请求延迟?长链接还是短链接(衡量 syscall 的开销)?
磁盘 I/O:磁盘在读写文件吗?读写哪些文件?大多数的读写是什么 Pattern?吞吐多大?一次 I/O 延迟多大?
关键日志:不是所有日志都有用,只有包含特定关键字的日志,人们才会关心。所以,有没有特定关键字的日志出现?
通过以上标准问题的灵魂拷问,必定可以对系统运行状态有一定的了解。
更进一步的关键是,这些系统的指标一定要和业务上下文联系在一起才能好用,举例说明,对于一个支持事务的数据库来说,假设我们看到 CPU 线程和 call stack,发现大量的 CPU 时间花在了 wait / sleep / idle 之类的事情上,同时也没有其他 I/O 资源瓶颈,此时,如果只看这些的数字可能会一脸懵,但是结合事务的冲突率来看可能柳岸花明,甚至能直接给出这些 lock 的等待时间都花在了哪些事务,甚至哪些行的冲突上,这对观测者是更有用的信息。
也并不是说其他的信息就没用,而是相当多的信息的价值是后验的,例如:绝大多数的 debug 日志,或者那些为了证实猜想的辅助信息,其实在解决未知问题时候几乎没有帮助,而且还需要观察者有大量的背景知识,这类信息最好的呈现方式还是折叠起来,眼不见为净的好。
如果打开 TiDB 的内部 Grafana 就会看到大量这样的指标,如 stall-conditions-changed-of-each-cf(虽然我知道这个指标的含义,但是我猜 TiDB 的用户里 99% 的人不知道),而且从名字里面我看到了写下这个名字的工程师内心的挣扎,他一定很想让其他人(或者自己)看懂这个名字指的是什么,但是比较遗憾,至少在我这里没有成功。
观察的下一步是什么?作出行动。
在做出行动之前想想,有行动的前提是什么?我们处理问题的行动大致会遵循下面模式(我自己总结的,但任何一本认知心理学的书都会有类似的概念):观察—>发现动机—>猜想—>验证猜想—>形成计划—>行动,然后再回到观察,反复循环。
这个里面人(或者是老司机的经验)体现比较重要地方是在从观察到猜想这个环节,至于观察的动机而言无非有两种:
1. 解决眼前的故障;
2. 规避潜在的风险(避免未来的故障)。
假设系统没有问题,也不太需要做出改变。 我觉得这两步之所以重要,是因为基本上其他环节都可以用自动化,唯独这两步很难,因为需要用到:人的知识/经验和直觉。
对于一个拥有好的可观测性的系统,通常都是能很好利用人直觉的高手,举个小的例子:当打开一个系统后台界面时,我们试着不去关注具体的文字信息,如果界面中的红色黄色的色块比较多,我们的直觉会告诉自己这个系统可能处于不太健康的状态,更进一步如果红色和黄色大致都聚集在屏幕的某个具体位置上,我们的注意力一定会聚焦到这个位置;如果一个界面上全是绿色,那应该是比较健康的状态。
怎么最大化利用人的直觉?或者说要引导到什么地方?我认为最好的点是:风险的预判。
人的直觉用在哪?风险的预判
此处需要利用一些先验知识。在聊这个话题之前,我想分享一个我之前听过的小故事,当年福特工厂里有个电机坏了,然后找了个老师傅,他听了听声音,看了看机器运转情况,最后用粉笔在电机上画了一条线,说这个地方的线圈多绕了多少多少圈,将信将疑的工人们照做,果然问题解决了,然后老师傅开了个 1 万美元的维修费(当时算是天价),福特的老板问他凭啥画一条线就收那么多钱,老师傅开了个账单:画线 1 美元,知道在哪画这条线 9999 美元。
故事的真假暂且不聊,假设是真的,我们可以看到直觉和经验,真的是能产生很多的价值,我当时听到这个故事的第一反应是,这个老师傅肯定这种情况见的多了(废话),而且这个问题一定是常见问题。
其实解决问题最难部分是通过观察(尤其是一些特征点)排除掉绝大多数不靠谱的方向,另外要相信常见故障的原因是会收敛的。这时一个具有良好可观测性系统的第一步就是能给使用者的直觉指引方向,这个方向就需要前人的知识来给出可能性最大的故障点以及相关的指标(例如 CPU 使用率等);第二步就是通过一些心理学小技巧把它展现出来。
下面以 TiDB 中即将会引入的一个小功能 TopSQL 加以佐证。这个功能说起来也很简单,我们发现很多用户故障都和少量的 SQL 相关,这类的 SQL 的特征是拥有和别的 SQL 有明显不同的 CPU footprint,但是每一条 SQL 的 footprint 独立看起来还挺正常的,所以 TopSQL 的功能就是回答:CPU 到底消耗了多少?在哪些 SQL 上?我试着不去解读下面这个截图,我猜聪明的你马上就能知道怎么用:
你的直觉会告诉你,后半段那段密集的绿色占比好像和其他有什么不一样,将整体的 CPU 使用率推高了,感觉有问题的样子,没错,这大概就是正确的方向,好的可视化能够利用人的直觉快速定位主要矛盾。
什么叫做“一个操作”?识别操作的真正的生命周期
刚才写第一点的时候想到还有一个经常被人忽略的关键资源:时间。本来想把时间放到关键资源那节里面,但是想了想放在这里可能更加合适。
稍微形而上一点来看,我们现在的计算机都是图灵机的实现,我小学就知道图灵完备语言的最小功能集合:读/写变量,分支,循环。用文学一点的说法是:所谓程序就是无数个轮回,大轮回嵌套着小轮回(循环),每个轮回中根据现状(变量)不断的做出选择(分支)。
我说到这里可能聪明的读者会猜到我想说什么:如果我们讨论可观测性脱离了周期,就毫无意义。而周期的定义又是灵活的,对于人而言,大周期显然是一辈子,小周期可以是一年一日,甚至周期可以不用时间跨度作为单位,比如一份工作的周期…
对于一个数据库软件而言,什么是一个合理的周期?是一条 SQL 的执行周期?还是一个事务从 Begin 到 Commit ?这里没有标准答案,但是我个人建议,周期越贴近终端用户的使用场景越实用。
譬如,在数据库中,选择单条 SQL 的执行作为周期不如选择事务的周期,事务周期不如应用程序一个请求全链路的周期。其实 TiDB 在很早就引入了 OpenTracing 来追踪一个 SQL 的执行周期内到底调用了哪些函数,花费多少时间,但最早只应用在了 TiDB 的 SQL 层内部(熟悉我们的朋友应该知道我们的 SQL 和存储是分离的),没有在存储层 TiKV 实现,所以就会出现一条 SQL 语句的执行过程往下追到 TiKV 就到了一个断头路;
后来我们实现了把 TraceID 和 SpanID 传到了 TiKV 内部这个功能才算初步可用,至少把一个周期的图景变得更加完整了,本来我们打算就止步于此,但是后来发生了一个小事情,某天一个客户说:为什么我的应用访问 TiDB 那么慢?然后我一看 TiDB 的监控,没有啊,SQL 到数据库这边基本都是毫秒就返回了,但是客户说:你看我这个请求也没干别的呀,两边怎么对不上?后来我们把 Tracer 加进来以后才知道客户这边的网络出了点问题。
这个案例提醒了我,如果能做到全链路的 Tracing,这里的全链路应该是从业务端请求开始计算,去看待生命周期才有意义。所以在此之后我们在 TiDB 里面通过拓展 Session Variable,能够支持用户将 OpenTracing 协议的 Tracer 信息通过 Session Varible 传入到 TiDB 的体系中,打通业务层和数据库层,能够真正实现的一个全生命周期的跟踪,这个功能也会在很近的未来的版本中和大家见面。
说了这么多,总结几点:
1. 时间也是重要资源。
2. 抓 Sample 也好,做 Trace 也好,选对周期很重要。
3. 周期越贴近业务的周期越有用。