【经验】微服务日志的七种最佳实践

微服务架构是一种全新的应用结构,它能够帮助您通过松耦合的系统,开发、测试、部署和发布彼此相互独立的各种服务。因此微服务背后的理念是:将大型系统分解成多个独立的小部分。

通常情况下,每个服务都能通过 HTTP 的端点与其他服务交互。它们在隐藏技术栈细节的同时,会暴露自己的契约 (contract) 给其对应的消费者 (consumer) 角色。例如:服务 A 可以在调用服务 B 的同时,也去调用服务 C,而只要整个请求链是完整的,那么服务 A 就能够对发起请求的客户端做出响应。

微服务架构能够给我们的系统带来很多方面的好处,其主要能力包括:使用不同的技术栈、独立地进行部署、一次只解决一个小问题等。但是,由于它在通信和管理上的复杂性,一般使用微服务的成本会比较高。而且在一个或多个服务出现问题时,微服务会变得更加复杂。如果没有掌握良好的、且有意义的日志的话,你都无法回答诸如:哪个服务、为什么、和在什么情况下失败了等问题。

老实说,我本人最憎恨那些由于糟糕的日志策略,所导致的一些“未知”的系统错误。下面我们和您分享一些,自己在与微服务打交道时总结出的七种最佳实践。

1. 用唯一性 ID 来关联各个请求

请回想一下我们上面提到的服务 A、B、C 之间的请求调用链。在实践中,我们应当给每一个调用分配一个唯一性的 ID,以便标识出每一个请求。

设想您正在记录每个服务的访问与错误日​​志。如果您发现在服务 B 上有错误,那么您就能知道该错误是来自于服务 A、还是服务 C。

如果错误信息足够详细的话,您也许不必去重现错误。但是多数情况并非如此,您必须通过正确的方式,将各个服务 (如服务 B) 中的所有请求进行错误重现。因此,如果您发现了某个与之相关联的请求,那么您只需要在日志中寻找出它所对应的 ID 便可。

随后,您可以顺藤摸瓜地从系统中将那些主要请求的某个部分,从服务的全量日志中截取出来。接着,您就可以知道是哪项服务的主请求花费了最多的时间。其可能性包括:可能是某项服务使用到了缓存、或是某项服务不止一次调用了其他服务、以及其他有趣的细节。

2. 在响应中包含唯一性 ID

微服务的用户可能会不止一次地碰到同一个错误。面对这样的情况,您应该乘机对客户端可能接收到的响应进行编码,以便它能够将一个唯一性的 ID,连同与该错误相关的任何其他有用的信息都传递出来。当然,这个唯一性的 ID 完全可以和我们在上面所提到的相关请求保持一致。

因此,在响应的有效载荷中包含与请求相关的唯一性 ID,将有助于您和您的客户更迅速地发现各类问题。同时,您也可以获悉请求日期、时间和其他细节上的参数,以便您能够更好地理解自己所碰到的问题。另外,您还可以将请求的 ID,添加到诸如“请联系服务管理员,并报告该问题。”之类的常见补充性错误信息之中,以便深入了解到底是什么原因引起该错误,进而防止它在未来再次发生。

3. 发送日志到集中的位置

在此,让我们假设您已经对各种有用的日志信息进行了分类。下一步,我们就需要将各类日志发送到一个集中化的位置。

试想一下:如果您每次都需要登录到各个相互单独的服务器上,以来读取不同的日志信息,那么您将不得不花费更多的时间去试图关联这些问题。这远不如您登录到某一个位置,并一站式地访问到所有的日志,以定位问题。

此外,您的系统通常会随着时间的推移,而变得日趋复杂,而各项微服务的数量也会节节攀升。同时,您的各种服务可能会分处不同的服务器或提供商,这都会让形势变得更为复杂。

因此,集中式存放日志正在成为业界的常规方法,特别是当您的服务工作在云端、容器、或其他混合环境之中,而某些服务器可能会在无任何通知的情况下下线的时候。例如,在出现异常错误,或是内存的消耗水平已经达到 100% 时,某些容器就会被终止运行。

您可以在服务器中断之前,通过设置代理,每五分钟推 / 拉一次日志,来解决此类问题。您也可以在服务器上配置一个 cronjob(定时任务)、sidecar container、或是一个与其他进程共享的文件位置,来集中化各种日志。为了避免日志被篡改,您还可以自行构建一套解决方案。

可见,将所有服务的日志都集中到一处,会有助于您更容易、且有效地定位各种关联问题。

4. 结构化您的日志数据

在具体实践中,我们很难为所有的日志数据预先定义好格式。有些日志可能需要比其他日志更多的字段,相反这些字段可能会对那些不需要的日志来说不但多余、而且浪费字节数。

微服务架构是通过使用不同的技术堆栈,来解决此类问题的。不过,这会影响每个服务的日志格式。例如:某一个服务可能是用逗号来分隔不同的字段,而其他日志则使用的是管道或命名空间。

上述方法显然比较复杂。因此,我们可以通过将自己的日志数据构建成一套标准的格式,如:JavaScript Object Notation(JSON),来简化解析日志的过程。JSON 允许您拥有多层次的数据。在必要的时候,您可以在单个日志的事件中获取更多的语义信息。

同时,此法也使得对于特定日志格式的解析更加直接。通过对数据采取结构化,就算您的日志里有各种不同的字段,其格式也会变得更加标准。籍此,您也可以在集中化的位置上创建各种搜索,例如:检索包含有 500 条及以上,“HTTP_CODE”字段的日志信息。可以说,使用结构化的日志方式既能让您的微服务日志实现标准化,又不失灵活性。

5. 为每个请求添加上下文

通常情况下,如果系统能够提供足够的信息,那么我们就能够更好地了解针对某个问题的上下文请求,更快地发现该问题的根本原因。不过,给各种日志添加上下文,也会在代码层面上产生一些重复性的工作,因为在您所需要的许多日志事件中,已经包含了诸如日期和时间等通用数据信息。因此在我们的代码中,应当只记录那些重要的消息、并涉及到一些特定的领域,以使得日志看起来简单明了。

您可能会想到各种五花八门的数据需要被记录,但是让我们通过如下的列表,来告诉您哪些才是真正需要记录的具体特定领域吧。

  • 日期和时间。当然,如果能够保证读取日志的人都在同一时区的话,您大可不必一律采用 UTC(世界标准时间) 的格式。
  • 堆栈错误。您可以将异常对象作为参数传递给自己的日志库。
  • 服务的名称或代码,这样您就可以根据微服务来区分不同的日志。
  • 发生错误的函数、类或文件名,这样您就省去了跟踪问题出处的时间。
  • 与外部服务交互的各种名称,例如:您可以获悉是哪个进程在调用数据库时出现了问题。
  • 服务器和客户端请求的 IP 地址。这些信息将有助于发现那些不健康的服务器、或识别出 [DDoS 类攻击]
  • 应用程序的用户代理,以便您能判断是哪些浏览器或用户碰到了问题。
  • 通过 HTTP 代码来获取错误的更多语义。这些代码将有助于创建各类警报。

可见,为每个请求添加上下文,能够节省您对系统进行排障的时间。

6. 将日志存储到本地

将日志存储到本地,似乎听起来和我们前面说的“发送日志到集中的位置”有些矛盾,其实则不然。最初我是将各种日志,直接通过 HTTP 请求的方式发送到别处的。但是我屡次发现这些流量传输占用掉了我大量的出站带宽,以至于影响到了其他更为重要的微服务调用。

因此,我们需要对日志的外发和本地存储有所取舍。最终,我之所以选择了本地存储,是因为这样有助于从应用程序中分离日志、并减少上下文的切换。针对数据库,您可以采取将应用程序与其日志区分不同存储卷的方式。

例如:亚马逊的 AWS就一个选项,用户可以使用一种称为Elastic File System(EFS) 的服务,去挂载某个卷。其功能类似于网络附属存储 (network-attached storage,NAS)。那么,您可以按需辗转到另一台服务器上,挂载相同容量的卷,然后将各种日志转发到那个集中的位置上。

简单说来,我们可以使用 Docker 容器,来实现将所有应用程序的日志都发送到相同的位置。然后汇总、过滤和转发这些日志的存储库,到其他进程或服务那里。

7. 记录重要且有意义的数据,有备无患

如果您是刚开始接触微服务的日志问题,那么上述最佳实践可能会对你比较“无感”。但是,只要您足够细心,在持续使用了微服务一段时间之后,您就可以通过对现有日志信息和方式的评估,逐渐摸索出哪些才是您可以用来发现和解决奇怪问题的有用信息。

同时,在记录和积累了足够多的日志数据之后,您还可以伺机采用自动化的警报方式,以节约您通过读取大量日志来定位问题的时间。当然,自动化警报也能够帮助您以一种积极主动方式,限制各种错误向所有用户处蔓延。

总之,集中化日志信息,是微服务错误分析的必备手段。而为日志添加足够多的上下文信息,则能够更好地分辨出那些是有用的日志,那些是无用的信息。