9 月 16 日,以“云服务时代如何打造高可用的服务架构”为主题的“云片技术开放日”在上海市浦东区中兴智慧园举行,又拍云系统开发工程师张超受邀在活动上做了《又拍云日志服务架构设计和实践》的分享。

1.png

张超,负责又拍云 CDN 平台相关组件的更新及维护。Github ID: tokers,热爱编程,活跃于 OpenResty 社区和 Nginx 邮件列表,曾为 ngx_lua 贡献源码,在 nginx 、nginx lua、CDN 性能优化、日志优化方面有较为深入的研究。平时混迹开源软件社区之中,专注于服务端技术的研究。

以下是分享全文:


又拍云作为一家 CDN 服务提供商,每天产生 50T 左右的日志,数量庞大、种类繁多,不论是公司内部,还是客户,对日志都有需求,而且需求都不尽相同。

所以,我首先谈一谈,为什么又拍云需要一个强大的日志系统,主要有三个原因:

首先是为用户提供日志归档的服务,日志根据不同的服务名可提供给用户下载,包括在又拍云后台上可以看到的服务,比如用户想了解自己的服务今天访问热门的是哪些资源等。

其次是提供近乎实时、多维度的日志分析,包括问题排查,并对接到内部报警系统,这点对又拍云平台的稳定性来说非常重要,比如某个用户的源站网络出现问题,我们可以通过监控系统和内部的服务,将日志给到报警系统实现报警,从而可以通知用户。

第三是日志离线分析功能,可以进行复杂的数据模型计算分析,提供包括细化到每一个省份、城市,定期生成的报表,用户可以了解自己的服务在各个城市的访问情况。

日志服务系统概览

2.png

△ 日志服务系统

又拍云的日志都是通过 CDN 边缘节点的服务产生,服务基于 ngx_lua。ngx_lua 是 OpenResty 的一个核心组件,它将 Lua VM 嵌入到 Nginx 当中,极大地扩展了 Nginx 的能力。

这里介绍一下又拍云 CDN 代理层的服务全部是基于 OpenResty / Nginx 开发的,我们在 Nginx 的优化和二次开发方面投入了大量的精力。

这些日志在边缘节点进行一定的缓冲后,通过公网上传到数据中心的日志服务代理层。因为 CDN 节点分布在全国和海外一些地区,如果每一个机房都到数据中心建传输网络,这个成本比较大,所以这里都是走公网传输。

数据中心日志代理这层使用的是基于 OpenResty / Nginx 构建的一个服务,这层主要是由 LVS 进行四层的负载均衡,所有日志都会均摊到这个集群中的每台机器。这层还会根据日志类型,比如是某个定制的大客户的日志、内部人员使用的日志等,将日志进行一致性哈希,保存到不同的 Kafka topic 。

日志会在 Kafka 集群里面进行暂存。在 Kafka 后面连接着成百上千个日志消费者(包括公司部门和客户),不同的消费者会消费不同的 Kafka topic,它会根据不同的需求,将日志转换成用户所要的格式。

潜在的两个问题

在日志服务过程中,可能会遇到些许问题,这里仅列举了两个问题,当然不止这两点,也存在一些其他值得关注的问题。

问题一:上传问题——由于 CDN 的特点,CDN 的边缘机房部署在全国各地和海外,通过公网上传日志如果遇到部分网络不理想的情况,就会上传失败、丢失。

问题二:日志消费赶不上生产的问题——上传成功,都保存在 Kafka ,但是日志消费者消费能力有限的时候,会导致 Kafka 队列发生堆积。

机房掉电也是很严重的问题。不过又拍云的 Kafka 集群所在的机器装备了双电源,保证它至少在 99.9999% 的情况下,不会出现掉电影响服务。

下面我针对这两个问题,聊一聊又拍云的解决方法。

日志上报方案:解决日志上传问题

我这里说的“日志上报”,指的是又拍云针对 Nginx 日志格式的上报方案。

第一种是通过脚本周期性地向 Nginx 发送信号进行日志切割,然后把日志归档上传。这种方式有个弊端,Nginx 是一个基于事件循环来展开工作的,它的敌人就是阻塞,Nginx access.log 目前写磁盘采用的是同步的方式,而且经过了文件系统,极有可能哪个文件系统或者磁盘抽风,整个进程就卡住,导致这个进程内的所有请求一段时间内会处在“饥饿”的状态,影响到它的工作。这种方案看起来显然不是特别完美。

第二种是利用 Nginx 在 1.7.1 版本所支持的直接将产生的 access.log 发到外部的 syslog 组件的方案,这个方案存在一个缺点是如果发送失败日志就会直接丢失,所以也不是特别符合我们的需求。

考虑到又拍云的服务所产生的日志数据相对比较复杂,直接用 Nginx 的 access.log 是不能很好地描述这些日志数据,因此我们采用了以下的方案:

  • CDN 边缘机房引入单独的 log agent 服务,由它对接数据中心的日志代理层,即我们的 CDN 边缘服务不是直接发到日志代理层的;
  • 在 log agent 内置 disk queue;
  • CDN 边缘服务引入基于 Nginx 所写的日志发送方案: lua-resty-logger-socket,将日志发送到本机的 log agnet (127.0.0.1)。

lua-resty-logger-socket (https://github.com/cloudflare/lua-resty-logger-socket)是 cloudflare 针对日志发送的开源方案。该库有一个核心的内存 buffer,它会把上传所提交来的日志数据全部暂存起来,buffer 的内容只会在达到一定大小(flush_limit)之后才将数据冲刷出去,如果发送失败,内容不会被丢弃;如果缓冲的数据大小超过一个硬性值(drop_limit),那么当前的日志会被丢弃。

这个库是基于 nginx_lua 的 Cosocket 技术,Cosocket 通过利用 Lua coroutine 的 yield / resume 特性,完美结合了 Nginx 的事件框架,因此是百分百非阻塞的,这避免了 access.log 写磁盘带来的阻塞问题,和又拍云的 CDN 服务的软件架构非常契合。

接下来日志就到了边缘的 log agent 组件,我们内部称之为“logger”,接收 CDN 边缘服务所发来的日志并进行转发。其实它做的事情很简单,周期性地把日志转发出去,如果网络质量相对比较理想,这些日志只会被短暂地缓冲在内存当中。而如果网络出现异常,内存里的日志会越来越多,当超过某个阈值的时候,日志会自动加入到 disk queue ,这是一个基于文件系统的先进先出队列,当需要转发日志的时,会优先处理在内存和磁盘中排队的日志数据。

如果网络一直很差,日志一直堆积,甚至本机磁盘都不够放,那这个问题就太严重了。这种情况下因为毕竟磁盘容量有限,所幸我们有完善的报警机制,基本上只要出现问题我们立刻就会发现,然后运维和研发会去介入并解决问题。

通过这套机制保证日志即使在网络情况不理想的时候也不会被丢弃。如此,就解决了上面的第一个问题:公网网络不理想的情况下,日志上传不上去,出现丢失的问题,CDN 边缘机房日志上报可靠性得到了保障。

自研 Morgans 系统:让“日志消费”跟上“日志生产”

又拍云的日志种类繁多,包括内部使用、客户使用或者通用的等,需求也不尽相同,所以需要一个通用的日志消费者(包括需要调用日志的公司部门和客户)框架,避免重复劳动,满足不同的需求,基于此,又拍云自研了 Morgans 项目 。

又拍云没有采用开源的日志解决方案,有几个具体的原因:

  • Logstash 使用了 JRuby 作为插件编写语言,而 JRuby 对资源的消耗比较重;
  • Logstash 本身存在不少缺陷,容易踩坑;
  • 某些业务场景十分复杂,例如需要适配客户的 FTP 服务器;或者某些客户的服务器需要先进行鉴权;又或者日志需要进行分类和打包;
  • Mozilla Heka 项目多年未更新,许多问题也得不到修复;
  • 自行设计能够更加贴近业务需求,更加轻量,即使出错也能够快速定位解决。

目前 Morgans 系统已经接管了又拍云所有日志定制和内部监控数据的处理,消耗的 CPU 、内存均比较少,稳定且基本无害。

容器化:解决日志生产和消费的思路

如果日志到 Kafka 这一层没有问题,但日志消费者不给力,这会直接导致了日志数据无法及时传递到下一层,破坏了整个系统近实时的传输转发能力。如何增强消费能力?消费者在设计上需要支持水平可扩容,才能从容应对日志量突增的情况,避免服务可用性降低。

如果新增一种消费者程序或者扩容时,需要提前申请物理机、人工推送服务上线、部署对应的监控报警程序等,全部弄完可能一天的时间已经过去,但是问题还是没有解决,整个操作周期很长,如果是紧急的情况,可能会带来不少的损失。

相比之下,使用容器云平台的优势就非常明显:

  • 资源池化,需要多少 CPU 、内存就配置多少;
  • 拥有故障转移能力,弹性伸缩;
  • 运维成本降低,只需要在网页上配置一些东西,整个服务就布上去了;
  • 自带监控报警,目前对接到了 Slack,如果服务挂了,会推送消息告知什么原因使容器的程序挂掉了。

3.png

上图是在又拍云容器云平台上创建一个服务,首先创建一个组和相应的名字,重要的是填入容器镜像的下载地址,包括启动命令、需要多少 CPU、多少内存、磁盘等。配置完成后,服务就起来了,可以说运维成本相对比较低。

4.png

上图是日志消费者服务的部分运行监控图,第一个是 CPU 监控图,平均每个容器占用的 CPU 变化;第二个是内存的监控图,可以看到内存使用非常平稳,如此可以了解大概是怎么样利用我们的服务资源。

模块设计:沿袭自 Nginx 的设计哲学

Nginx 的设计哲学是一些皆模块。Morgans 项目也遵循了这个设计哲学,内部也有四个模块:

  • Input module:从 Kafka 或其他日志源消费原始格式的日志
  • Filter module:根据需求转换成目标类型的日志
  • Output module:根据需求转发或者暂存到目标服务
  • Core module:消费者框架代码

Morgans 的模块设计借鉴了 Nginx ,Nginx 的模块设计已经相当漂亮,可以自己注册一些配置指令,如果配置想提供给用户,就注册这样的配置项。Morgans 也如法炮制,每个模块拥有自己的配置结构,可以注册配置指令,注册各类功能的钩子函数。这些钩子函数在不同的阶段发挥各自的作用,这种用法可能会颠覆了大家对 golang 语言使用的看法。

5.png

如上图,是 Morgans 前端的一个模块结构体,名字、配置项、类型,包括 PostParse 处理到某条配置文件指令时,某个模块的某个配置指令钩子会被调用,从而正确解析到该配置;配置文件解析完毕后,模块的 PostParse 钩子会被调用;准备开始工作时,模块的 InitProcess 钩子会被调用等。

如果编译过 Nginx,一定知道 Nginx 的目录下有一个 auto 子目录,其目录下的 configure 脚本是用来定制其构建配置的。通过它可以选择编译哪些模块,打开某些功能等。与 Nginx 类似,Morgans 每个模块都会有一个 config ,可以让他设置几个变量,包括名字、模块类型、钩子等,通过只编译需要的模块,还可以减小编译得到的二进制文件大小。

灵活的配置文件设计

Morgans 拥有灵活的配置文件设计,首先指令式的配置方式,简洁而不失描述能力。

其次,Morgans 支持从外部的 key-value 存储中加载配置。当我们在容器云平台配置消费者时,需要给他们提供镜像,包含了消费者二进制和全部配置,如果某个时刻我们得去更新配置,那么就需要重新构建镜像并上传,镜像在 CI /CD 中构建相对比较慢,而如果它能够支持外部读取内存,那就会快很多(无需重新构建镜像,只需要重启服务即可),目前我们使用满配置的 hashicorp 公司开源的 consul,把它作为一个 key-value 的存储,我们某个时刻修改了配置,在容器云平台找到对应的服务,重启一下就能生效。

第三是结合消费者框架内置的变量系统,实现了变量插值功能。

6.png

上图是变量插值的示例,进行 input::kafka 的实例 mykafka ,定义一些需要的设计,比如 topic mytopic 数据如何分割、定义换行符来区分、配置多少个 broker 、消费者组是什么,还有 offset 的管理等。

下面定义了一个 filter ,比如有一个 common 的过滤模块,随便给个名字,后面是 一个 hdfs output 模块,包括文件的 replication 属性、hdfs namenode 的地址、user等。在 filepath 里面, $hostname 、 $mylog_timestamp 称之为变量。整个 filepath 的配置利用到了变量插值的技术,即取出配置里的源字符串,转换为中间形态,使用的时候去解析,得到目标字符串。

Morgans 内置了一套简易的运行时变量系统,和 Nginx 的类似,这套变量系统关联到每个日志对象,每个模块可以自行注册变量,以及变量被使用时需要调用的钩子,钩子则可以关联到一个日志对象的上下文。使用分成两步,第一步会处理源字符串当中每个变量和常量并解析得 到 ComplexValue 对象,第二步是发生在需要使用时,调用这项变量和常量的 handler 从而拼接得到经过解释后的完整字符串。这样做的好处是什么?比如有很多用户他们都存到了 HDFS 里,通过这个方法,一份配置文件可以适应几个、几十个、几百个用户,一个二进制就能够解决这样的问题。(https://github.com/tokers/corgi