
@jcutrer
今天,我们将讨论事件日志,定量指标以及监视所有这些内容,以提高团队对事件的响应率并减少目标系统的停机时间。
Erlang / OTP作为框架和构建分布式系统的思想为我们提供了规范的开发,工具和标准组件实现方法。 假设我们利用了OTP的潜力,并从原型一直生产到生产。 我们的Erlang项目在战斗服务器上感觉很棒,代码库在不断发展,出现了新的要求和功能,新人加入了团队,一切似乎都很好。 但是有时会出问题,技术问题再加上人为因素会导致事故。
由于不可能为所有可能的故障和问题案例埋下伏笔,或者在经济上不可行,因此有必要通过管理和软件解决方案减少发生故障时的系统停机时间。
在信息系统中,总是有发生各种性质的故障的可能性:
- 硬件故障和电源故障
- 网络故障:配置错误,固件曲线
- 逻辑错误:从算法编码错误到子系统和系统边界处出现的体系结构问题。
- 安全问题以及相关的攻击和黑客,包括内部欺诈。
我们立即区分责任:例如由zabbix组织的对基础设施的监视将负责计算设备和数据传输网络的运行。 关于安装和配置此类监视的文章很多,我们不再赘述。
从开发人员的角度来看,可访问性和质量问题在于早期发现错误和性能问题以及对它们的快速响应的层面。 这需要评估的方法和手段。 因此,让我们尝试导出量化指标,分析在项目开发和运营的不同阶段可以显着提高质量的指标。
组装系统
让我再次提醒您有关工程方法和测试在软件开发中的重要性。 Erlang / OTP同时提供两个测试框架:eunit和通用测试。
作为对代码库状态及其动态状态进行初始评估的指标,您可以使用成功和有问题的测试数,它们的执行时间以及测试中代码覆盖率的百分比。 这两个框架均允许将测试结果保存为Junit格式。
例如,对于rebar3和ct,将以下行添加到rebar.config中:
{cover_enabled, true}. {cover_export_enabled, true}. {ct_opts,[ {ct_hooks, [{cth_surefire, [{path, "report.xml"}]}]} ]}.
成功和失败测试的数量将使您能够构建趋势图:

看一下,您可以评估团队的动力和测试的回归。 例如,在Jenkins中,可以使用“测试结果分析器插件”来获得该图。
如果测试变成红色或开始运行很长时间,那么这些度量标准即使在组装和自动测试阶段也可以最终确定发行版。
应用指标
除了操作系统指标外,监视还应包括应用程序指标,例如每秒的视图数,付款数和其他关键指标。
在我的项目中,我使用${application}.${metrics_type}.${name}
类的模板来命名度量。 通过此命名,您可以获取表单的指标列表
messaging.systime_subs.messages.delivered = 1654 messaging.systime_subs.messages.proxied = 0 messaging.systime_subs.messages.published = 1655 messaging.systime_subs.messages.skipped = 3
也许指标越多,就越容易理解复杂系统中正在发生的事情。
Erlang VM指标
应该特别注意监视Erlang VM。 让它崩溃的思想很漂亮,正确使用OTP肯定会帮助提升Erlang VM内部应用程序的崩溃部分。 但是不要忘记Erlang VM本身,因为很难删除它,但是有可能。 所有选项均基于资源耗尽。 我们列出了主要的:
原子表溢出。
原子是标识符,其主要目的是提高代码的可读性。 一旦创建的原子将永久保留在Erlang VM实例的内存中,因为它们不会被垃圾收集器清除。 为什么会这样呢? 垃圾收集器在每个进程中分别使用该进程中的数据工作,而原子可以分布在许多进程的数据结构中。
默认情况下,可以创建1,048,576个原子。 在有关杀死Erlang VM的文章中,通常可以找到类似的内容。
[list_to_atom(integer_to_list(I)) || I <- lists:seq(erlang:system_info(atom_count), erlang:system_info(atom_limit))]
作为这种效果的例证。 似乎在实际系统中无法解决人为的问题,但是在某些情况下...例如,在外部API处理程序中,在解析请求时,使用binary_to_atom/2
代替binary_to_existing_atom/2
或list_to_atom/1
而不是list_to_existing_atom/1
。
以下参数应用于监视原子状态:
erlang:memory(atom_used)
-用于原子的内存量erlang:system_info(atom_count)
-在系统中创建的原子数。 与erlang:system_info(atom_limit)
,可以计算出原子利用率。
工艺泄漏。
我想马上说,当process_limit(+ P达到erl)参数erlang vm不会崩溃,但是它进入紧急状态,例如,很可能无法连接到它。 最终,分配给泄漏的进程时,可用内存不足将导致erlang vm崩溃。
erlang:system_info(process_count)
-当前活动进程的数量。 与erlang:system_info(process_limit)
,可以计算进程利用率。erlang:memory(processes)
-为进程分配的内存erlang:memory(processes_used)
-用于进程的内存。
邮箱进程溢出。
此类问题的一个典型示例是,发送方进程将消息发送到接收方进程而无需等待确认,而receive
方进程中的接收由于丢失或不正确的模式而忽略了所有这些消息。 结果,邮件会累积在邮箱中。 尽管erlang具有在处理程序无法处理处理的情况下降低发送方速度的机制,但是无论如何,在可用内存耗尽后,vm会崩溃。
要了解邮箱溢出是否存在问题,etop将提供帮助。
$ erl -name etop@host -hidden -s etop -s erlang halt -output text -node dest@host -setcookie some_cookie -tracing off -sort msg_q -interval 1 -lines 25

作为连续监控的指标,您可以采用问题处理的数量。 要识别它们,可以使用以下功能:
top_msq_q()-> [{P, RN, L, IC, ST} || P <- processes(), { _, L } <- [ process_info(P, message_queue_len) ], L >= 1000, [{_, RN}, {_, IC}, {_, ST}] <- [process_info(P, [registered_name, initial_call, current_stacktrace]) ] ].
另外,可以记录此列表,然后在接收到来自监视的通知时,可以简化问题分析。
二进制泄漏。
大堆(超过64个字节)的二进制文件的内存在常规堆中分配。 分配的块具有一个参考计数器,该计数器显示可以访问它的进程数。 重置计数器后,将进行清洁。 最简单的系统,但正如他们所说,存在细微差别。 原则上,进程可能会在堆上生成大量垃圾,以致系统没有足够的内存来执行清理。
erlang:memory(binary)
充当监视指标,显示分配给二进制文件的内存。
因此,可以找出导致vm崩溃的原因,但除此之外,最好监视直接或间接影响应用程序正常运行的重要参数:
- ETS:
erlang:memory(ets)
使用的erlang:memory(ets)
。 - 编译模块的内存:
erlang:memory(code)
。
如果您的解决方案不使用动态代码编译,则可以排除此选项。
我还要提到erlydtl。 如果动态编译模板,则编译会创建一个加载到vm内存中的梁。 它还可能导致内存泄漏。 - 系统内存:
erlang:memory(system)
。 显示erlang运行时内存消耗。 - 消耗的总内存:
erlang:memory(total)
。 这是进程和运行时消耗的内存量。 - 有关减少量的信息:
erlang:statistics(reductions)
。 - 准备执行的进程和端口数:
erlang:statistics(run_queue)
。 - vm:
erlang:statistics(runtime)
实例的正常运行时间使您可以了解是否在没有日志分析的情况下进行了重新启动。 - 网络活动:
erlang:statistics(io)
。
提交指标到zabbix
我们将创建一个包含应用程序指标和erlang vm指标的文件,我们将每N秒更新一次。 对于每个erlang节点,指标文件必须包含在其上运行的应用程序的指标以及erlang vm实例的指标。 结果应该是这样的:
messaging.systime_subs.messages.delivered = 1654 messaging.systime_subs.messages.proxied = 0 messaging.systime_subs.messages.published = 1655 messaging.systime_subs.messages.skipped = 3 …. erlang.io.input = 2205723664 erlang.io.output = 1665529234 erlang.memory.binary = 1911136 erlang.memory.ets = 1642416 erlang.memory.processes = 23596432 erlang.memory.processes_used = 23598864 erlang.memory.system = 50883752 erlang.memory.total = 74446048 erlang.processes.count = 402 erlang.processes.run_queue = 0 erlang.reductions = 148412771 ....
使用zabbix_sender
我们将把这个文件发送到zabbix,在那里图形表示以及创建自动化和通知触发器的功能已经可用。
现在,我们已经在监视系统中建立了度量标准,并在其基础上创建了自动化触发器和通知事件,我们有机会通过对所有功能偏离正常状态的危险提前做出反应来避免发生事故。
集中收集原木
当项目中有1-2台服务器时,您可能仍然可以不使用中央日志收集而生活,但是,一旦出现具有许多服务器,群集和环境的分布式系统,就必须解决收集日志和方便查看日志的问题。
要在项目中写入日志,我使用啤酒。 从原型到生产的过程中,项目通常会经历以下收集日志的阶段:
- 最简单的日志记录,输出到本地文件,甚至输出到stdout(lager_file_backend)
- 使用syslogd集中收集日志,并将日志自动发送到收集器。 对于这样的方案, lager_syslog是合适的。
该方案的主要缺点是,您需要转到日志收集服务器,找到具有必要日志的文件,并以某种方式过滤事件以查找调试所需的事件。 - 集中收集日志,并将其存储在存储库中,并能够按记录过滤和搜索。
关于可以使用后者的缺点,优点和数量指标,我们将讨论一个具体实现的方式lager_clickhouse
,我将在大多数正在开发的项目中使用它。 关于lager_clickhouse
的几句话。 这是用于将事件保存到Clickhouse的大型后端。 目前,这是一个内部项目,但是有计划使其公开。 在开发lager_clickhouse时,我不得不绕过clickhouse的某些功能,例如,使用事件缓冲以避免在clickhouse中频繁发出请求。 通过稳定的操作和良好的性能,付出了不小的努力。
保存到资源库的方法的主要缺点是附加实体-Clickhouse,以及需要开发用于将事件保存到其中的代码以及用于分析和搜索事件的用户界面的需求。 另外,对于某些项目,使用tcp发送日志可能至关重要。
但是在我看来,利弊胜过所有可能的弊端。
lua脚本的使用是处理紧急事件的自动化主题的进一步发展。 任何开发人员或管理员都可以编写脚本来处理日志和指标。 脚本带来了灵活性,并允许您创建个性化的自动化脚本和通知。
总结
要了解系统中发生的过程并调查事件,拥有定量指标和事件日志以及分析它们的便捷工具至关重要。 我们收集的有关该系统的信息越多,就越容易分析其行为并纠正问题,即使在出现问题的阶段也是如此。 如果我们的措施不起作用,我们将随时掌握事件的时间表和详细日志。
您如何在Erlang / Elixir上运行解决方案,以及在生产中遇到了哪些有趣的案例?