清单:在产品中启动微服务之前必须做什么

本文简要介绍了我自己的经验以及我与同事们日夜不停地进行耙击事件的经验。 如果每个人都喜欢至少更准确地编写微服务,那么许多事件就永远不会发生。


不幸的是,一些低级的程序员认真地认为,内部带有某种命令的Dockerfile本身就是微服务,即使现在也可以部署。 码头工人在旋转,替补席上泥泞不堪。 这种方法充满了各种问题,包括性能下降,无法调试和拒绝服务,以及噩梦般的数据不一致。


如果您觉得是时候在Kubernetes / ECS /或其他任何版本中启动另一个应用程序了,那么我有一些反对意见。


也提供英文版本


我为自己制定了一套标准,以评估在生产中启动应用程序的准备情况。 此清单的某些要点不能应用于所有应用程序,而只能应用于特殊应用程序。 其他人通常适用于所有事物。 我确定您可以在评论中添加选项,也可以对其中的一些观点提出异议。


如果您的微服务不满足至少一项条件,我将不允许它在我理想的集群中,该集群建在地下2000米的地下掩体中,地下采暖和封闭的独立互联网供应系统。


走吧....


注意:项目的顺序无关紧要。 无论如何,对我来说。


自述文件简短说明


它在其存储库中的Readme.md开头处包含对自身的简短说明。

天哪,看起来很简单。 但是我发现该存储库没有多少丝毫解释为什么需要它,解决了什么任务等等。 无需谈论更复杂的事情,例如配置选项。


与监控系统集成


将指标发送到DataDog,NewRelic,Prometheus等。

分析资源消耗,内存泄漏,堆栈跟踪,服务相互依赖性,错误率-如果不了解所有这些(并且不仅是所有这些),就很难控制大型分布式应用程序中发生的事情。


警报已配置


该服务包括涵盖所有标准情况以及已知独特情况的警报。

指标不错,但是没有人会遵循。 因此,如果出现以下情况,我们将自动接听电话/推送/短信:


  • CPU /内存消耗已大大增加。
  • 流量增加/急剧下降。
  • 每秒处理的事务数在任何方向都发生了巨大变化。
  • 组装后工件的大小发生了巨大变化(exe,app,jar,...)。
  • 错误的百分比或其频率超过了允许的阈值。
  • 服务已停止发送指标(通常被忽略)。
  • 违反了某些预期事件的规律性(定时作业无效,并非所有事件都得到处理等)
  • ...

运行手册已创建


已经为服务创建了描述已知或预期的意外事件的文档。

  • 如何确保错误是内部的并且不依赖于第三方;
  • 是否取决于在哪里,写给谁和写什么;
  • 如何安全地重新启动它;
  • 如何从备份还原以及备份位于何处;
  • 创建了哪些特殊的仪表板/查询来监视此服务;
  • 该服务是否有自己的管理面板以及如何到达那里;
  • 是否有API / CLI,以及如何使用它来解决已知问题;
  • 等等。

各个组织之间的列表差异很大,但是至少应该有一些基本的内容。


所有日志均以STDOUT / STDERR格式编写


该服务不会在生产模式下创建任何日志文件,不会将其发送到任何外部服务,不包含用于日志轮换的任何冗余抽象等。

当应用程序创建日志文件时,这些日志是无用的。 您不会进入并行运行的5个容器中,希望捕获所需的错误(在这里,您正在哭泣 ...)。 重新启动容器将导致这些日志完全丢失。


如果应用程序将其自己的日志写入第三方系统(例如Logstash),则会创建无用的冗余。 相邻服务不知道如何执行此操作,因为 它有不同的框架吗? 你有动物园。


该应用程序将部分日志写入文件,将部分日志写入stdout,因为开发人员可以方便地在控制台中查看INFO,在文件中查看DEBUG? 通常这是最糟糕的选择。 没有人需要您需要了解和维护的复杂性以及完全冗余的代码和配置。


日志是杰森


每条日志行均以Json格式编写,并包含一组一致的字段

到目前为止,几乎每个人都以纯文本形式编写日志。 这是一场真正的灾难。 我很高兴永远不会知道Grok Patterns 。 有时我梦见他们,我冻结,努力不动,以免引起他们的注意。 只需尝试解析一次日志中的Java异常。


杰森很好,那是天堂赐予的火。 只需添加:


  • 根据RFC 3339的 毫秒时间戳;
  • 级别:信息,警告,错误,调试
  • user_id;
  • app_name
  • 和其他领域。

下载到任何合适的系统(例如,正确配置的ElasticSearch)并享受。 连接许多微服务的日志,并再次感受到什么是好的整体应用程序。


(并且您可以添加Request-Id并进行跟踪...)


详细级别的日志


应用程序必须支持至少具有两个操作模式的环境变量,例如LOG_LEVEL:ERRORS和DEBUG。

希望同一生态系统中的所有服务都支持相同的环境变量。 不是配置选项,不是命令行上的选项(尽管这是可逆的),但默认情况下会从环境中立即获取。 如果出现问题,您应该能够获得尽可能多的日志,如果一切正常,则应该能够获得尽可能少的日志。


固定依赖版本


包管理器的依赖关系是固定的,包括次要版本(例如,cool_framework = 2.5.3)。

当然,已经对此进行了很多讨论。 某些修复程序依赖于主要版本,希望只有次要的bug修复程序和安全修复程序将在次要版本中。 错了
每个依赖项中的每个更改都应反映在单独的提交中 。 以便在出现问题时可以将其取消。 用手很难控制吗? 有一些有用的机器人( 例如) ,可以跟踪更新并为您每个人创建请求请求。


Dockerized


存储库包含可用于生产的Dockerfile和docker-compose.yml

长期以来,Docker已成为许多公司的标准。 尽管有例外,但是即使您没有生产Docker,任何工程师也应该能够进行docker-compose,而不用考虑其他任何事情来获得用于本地验证的dev程序集。 并且系统管理员必须已使用必要版本的库,实用程序等使开发人员验证了程序集,在该程序中,应用程序至少应以某种方式工作以使其适合生产。


环境配置


从环境中读取所有重要的配置选项,并且环境的优先级高于配置文件(但低于启动时的命令行参数)。

没有人会想要阅读您的配置文件并研究其格式。 接受吧。


此处有更多详细信息: https : //12factor.net/config


准备和活力调查


包含适当的端点或cli命令,以测试是否准备在整个生命周期内启动和正常运行时服务于请求。

如果应用程序处理HTTP请求,则默认情况下它应具有两个接口:


  1. 为了验证该应用程序是活动的而不是冻结的,使用了“活动性”测试。 如果应用程序没有响应,它可能会被诸如Kubernetes之类的协调器自动停止,“ 但这并不准确 。” 实际上,杀死冻结的应用程序会导致多米诺骨牌效应,并永久性地影响您的服务。 但这不是开发人员的问题,只需执行此端点即可。


  2. 为了验证该应用程序不仅已启动,而且准备接受请求,将执行就绪测试。 如果应用程序已建立与数据库,排队系统等的连接,则应以200到400的状态响应(对于Kubernetes)。



资源限制


以一致的格式包含对内存,CPU,磁盘空间和任何其他可用资源的消耗的限制。

对于不同的组织和协调者,此项目的具体实施将有很大不同。 但是,必须对所有服务以单一格式设置这些限制,对于不同的环境(prod,dev,test等),这些限制必须不同,并且必须与应用程序代码一起不在存储库中


组装和交付是自动化的


已配置组织或项目中使用的CI / CD系统,并可以根据接受的工作流程将应用程序交付到所需的环境。

没有任何东西可以手动交付生产。


不管自动化项目的组装和交付有多么困难,都必须在该项目投入生产之前完成。 此项目包括Ansible / Chef食谱/ Salt / ...的组装和发布,移动设备应用程序的组装,操作系统的分支的组装,虚拟机映像的组装等。
无法自动化? 因此,您无法将其投放世界。 在您之后,没有人会收集它。


正常关机-正确关机


该应用程序可以处理SIGTERM和其他信号,并在处理完当前任务后系统地中断其工作。

这是非常重要的一点。 Docker进程变得孤立,并在无人看到的后台运行了几个月。 非事务操作在执行过程中中断,从而导致服务和数据库之间的数据不一致。 这会导致无法预见的错误,并且可能会非常非常昂贵。


如果您不控制任何依赖项并且不能保证您的代码将正确处理SIGTERM,请使用dumb-init之类的东西。


更多信息在这里:



定期检查数据库连接


应用程序不断对数据库执行ping操作,并自动响应任何请求的“连接丢失”异常,尝试自行还原数据库或正确完成其工作

我看到了很多情况(这不仅仅是言语转变),当为处理队列或事件而创建的服务因超时而失去连接并开始无休止地将错误倒入日志,将消息返回到队列,将它们发送到Dead Letter Queue或干脆不做它们的工作时。


水平缩放


随着负载的增加,足以运行更多的应用程序实例以确保处理所有请求或任务。

并非所有应用程序都可以水平扩展。 一个著名的例子是卡夫卡消费者 。 这并不一定很糟糕,但是如果某个特定应用程序无法启动两次,则所有相关方都需要事先了解这一点。 这些信息应该引起人们的注意,并尽可能保留在自述文件中。 通常,某些应用程序在任何情况下都不能并行启动,这给其支持带来了严重困难。


如果应用程序本身控制这些情况,或者为其编写一个包装程序以有效地监视“竞争者”,并且仅允许该进程开始或开始工作,直到另一个进程完成其工作,或者直到某个外部配置允许N个进程同时工作,则更好。


死信队列和错误消息恢复能力


如果服务侦听队列或响应事件,则更改消息的格式或内容不会导致其崩溃。 N次重复处理任务的失败尝试,然后将消息发送到Dead Letter Queue。

许多次,我看到无休止地重启消费者和生产线,这些消费者和生产线膨胀到如此之大的规模,以至于他们随后的处理花费了很多天。 任何队列侦听器都应准备好更改格式,消息本身的随机错误(例如,在json中键入数据)或由子代码处理消息时。 我什至遇到过这样的情况,一个非常流行的框架的标准RabbitMQ库不支持重试,尝试计数器等。


更糟的是,如果一条消息在失败的情况下被简单地销毁了。


每个进程的已处理消息和任务数量的限制


它支持环境变量,可以强制使用该变量来限制已处理任务的最大数量,之后该服务将正确关闭。

一切都在流动,一切都在变化,尤其是记忆。 内存消耗和OOM不断增长的图最终被杀死,这是现代kubernetic思维的常态。 原始测试的实现将为您省去检查所有这些内存泄漏的麻烦,这将使您的生活更加轻松。 我经常看到人们花费大量时间和精力(和金钱)来阻止这种流失,但是并不能保证您的同事的下一次提交不会使情况变得更糟。 如果应用程序可以在一周内存活-这是一个很好的指标。 然后让它自己结束并重新启动。 这比SIGKILL(关于SIGTERM,请参见上文)或“内存不足”异常要好。 几十年来,这个插头足以满足您的需求。


不使用第三方集成和按IP地址过滤


如果应用程序向允许从有限IP地址访问的第三方服务发出请求,则该服务将通过反向代理间接执行这些调用。

这是一种罕见的情况,但极为不愉快。 当一个微小的服务阻止更改集群或将整个基础架构移至另一个区域的可能性时,这非常不便。 如果您需要与不知道如何使用oAuth或VPN的人进行通信,请提前配置反向代理 。 不要在程序中实现此类外部集成的动态添加/删除,因为这样做会使您陷入唯一可用的运行时中。 最好立即自动执行这些过程来管理Nginx配置,并在您的应用程序中与他联系。


明显的HTTP用户代理


该服务用对任何API的所有请求的自定义标题替换User-agent头,并且该头包含有关服务本身及其版本的足够信息。

当您有100个不同的应用程序相互通信时,您会疯狂地在日志中看到诸如“ Go-http-client / 1.1”之类的内容以及Kubernetes容器的动态IP地址。 始终明确标识您的应用程序及其版本。


不违反许可证


它不包含过度限制应用程序的依赖项,也不是其他人代码的副本,依此类推。

这是不言而喻的案子,但碰巧看到即使是撰写NDA的律师也开始出现打ic。


不使用不受支持的依赖项


首次启动该服务时,它不包括已经过时的依赖项。

如果您进入项目的库不再受任何人支持,请寻找另一种方法来实现目标或开发库本身。


结论


我的清单上有一些针对特定技术或情况的非常具体的检查,但我只是忘了添加一些内容。 我相信您还会发现一些需要记住的东西。

Source: https://habr.com/ru/post/zh-CN438064/


All Articles