谁控制过去,谁就控制未来 – 奥威尔《1984》

日志(log)系统

日志是系统的历史。

让我们将运行着的大型程序视为一个运转着的社会(小型程序可以视为社区),运转着的社会和运行着的程序,都是一种时间序列上的事件集合。

发生在社会里的这些事件,被记录下之后,我们称其为历史。而发生在系统里的这些事件,被记录下之后,我们称之为日志。

历史

那么我们先来谈谈历史吧。

在银河系旋臂荒凉的一角,一个称为地球的蓝色行星上(where),一种由猿猴进化而来的的生物(who)在某个时间段里(when)生活其中,做着一些在柏拉图看来非常无关紧要的事(what)(柏拉图说但凡是人的事,就没有一件真的非常重要)。但我们都知道彼之蜜糖 吾之砒霜,彼之砒霜也可能是吾之蜜糖,柏拉图不屑的这些人间事,在另一些看来却值得大书特书。他们使用一种编码(文字)将发生过的事件(event)记录在某种存储介质中(竹简或是纸张中)(storage)。由于社会里人数如是此之多,事件发生得是如此的密集,这些记录者(历史学家)不得不拣选其中的一些事来记录,而抛弃另外一些。最初的挑选也许是被迫无奈,毕竟你不可能事无巨细得记录所发生的一切,否则记录历史所需的物质材料将大于构建世界的物质材料。这点很好论证,在你记录这段历史时, 你在记录历史这件事本身也正在发生,这很像站在两面镜子之间的你看到的递归景象。如果事无巨细记录一切,光是记录你自己,就将耗尽所有的墨水。由于事件必须被拣选,于是你总是可以客观地记录历史的同时,挑挑拣拣,让历史呈现出你想要的样子。

熟悉数据的人们都知道,如果你有挑选样本的权力,为了得出某些特定结论,你其实不必在数据上造假 :)

事件

写上边的一段,倒不是为了调侃历史学家。我其实想说,对于一个程序员而言,如何记录日志,记录哪些事件,怎样安全地存储它,是至关重要的,这和历史之于现实社会一样重要,关于这点《1984》已经给我们论述过了。日志虽是对过往发生的事件的记录,但它影响到系统的未来。从日志中,你能看到系统的很多特性(可视化),你可以了解到使用系统的人的特质(数据分析),出了问题能够快速定位与修复(sentry)。

我们回到历史的比喻。在写作历史时,记录事件所用的纸和笔当然是重要的,但要成为一个好的历史学家,你大多的时间可不是在练字或选纸上,如何挑选以及如何记录所发生事件,才是你需要用心之处

对于构建程序/网站的日志系统也是一样,日志如何写入,如何存储,这些只是基本工作,这些工作并不怎么花时间,你大多的事件需要花在如何挑选事件,如何记录这些事件,之后如何分析处理它们。而之后如何使用它们,又反过来决定了你需要记录哪些事件,以及事件中的哪些信息

如何在一个程序语言/框架中写入和存储日志,这方面社区已经有大量的讨论,这个问题往往不必重复解决,很多最佳实践都在语言层(如Python)或者框架层(Django)做完了。稍加配置即可

构建日志系统,我们大量的事件需要花在考虑如何描述事件,以及哪些是我们关心的事件(这是一个业务相关的问题)。关于如何描述事件,尽管我们最终会用代码实现它,但这其实不是一个编程问题,一个事件无非是某些人(who)在某个时间段(when)在某些场合(where)做了某些事(what),至于他为何要做(why),就是数据挖掘需要做的事了。写作一篇新闻你常常也要考虑这些要素,所以,对事件的描述,本质上和编程无关。

功夫在诗外

日志中的事件

在日志这个话题下,技术社区聊到日志系统,关心的主要是系统的架构,用了什么组件,组件之间如何协同。至于如何描述日志,则谈的不多。对于一个初次使用日志的新手,弄懂怎么将日志文本写入系统并不难,此后的存储、备份、归并、分析也多有现成解决方案或最佳实践。工程师在日志记录这件事上能感受到极大的自由,就像你拿着笔在A4纸面前那种自由,似乎什么都能记下,但该记录些什么,又如何表述它们,往往令人不知所措

本文将论述日志系统的构建,以网站系统为例,其中记录的日志主要围绕与网站用户相关的事件,至于系统日志则不在讨论的范围内。我们也将论述以及如何让日志系统服务于其他系统。架构之外,如何描述日志将是这篇文章的重心所在

文中,我们将以open edx和我们在PaperWeekly中的构建的网站后端系统为例来说明。

这两个系统都基于Django(一个Python web框架)构建,但阅读本文,并不假设你具备Django的知识,当然如果你懂Django最好不过,一些细节可能对你有用,但不懂也不影响阅读。

也许你不需要构建自己的日志系统

在我们正式开始之前,需要说明的是: 也许你并不需要专门构建一个完整的日志系统。

这取决于你要干嘛。如果你关心网站的用户流量、Top10入口页面、Top10来源网站、访客停留时间…这些用户指标,google analyticsBaidu统计往往是更聪明的选择,花费一盏茶的事件,你就能将它集成到你的网站里。即便是一些自定义的事件跟踪,你也可以利用这些SDK插件来做

如果你想做的是网站监控,事件告警,诸如你想知道为何每天深夜,网站都会无故崩溃,是昼夜温差变化还是狼人来袭。那么Sentry是你最佳的选择,集成它也只需要一盏茶的功夫。

如果你还继续往下读,我们假设你有足够的理由构建日志系统了。

open edx中的日志系统

如果你意识到构建日志系统时,要将事件的描述也包括在内,那么当你看到open edx复杂的日志系统,你就不会那么吃惊。其中的复杂之处,多是对事件的描述,他们为此构建了多层的抽象,而对事件的描述必然涉及到大量的业务逻辑和概念,所以读起来是十分晦涩的。我并不建议深挖这一块。即便你的系统以open edx为主体,我也不建议去拓展和hack它的日志系统。

理由如下:

首先成本高昂,关于edx日志系统,我在一篇文章里有细述:explore Open edX tracking system

在架构层面,这里边并没有什么复杂性,无非是利用了Django的log机制(其实是对python logging模块的包装),繁琐的是层层的包装。我觉得去理解和hack这些包装是痛苦且不必要的。原因是日志在存储层面无非是一种文本序列,你可以分别/分布式地存储它们。如果最终的分析需要合并,有很多优秀的工具帮你归并日志(诸如Beats、Logstash之类)。日志的聚合是最后考虑的问题,而且你大可放心。

下边我们说说open edx中值得我们学习的地方,先说说open edx中对事件的描述。这部分是这块令人头疼的主要原因。而头疼的原因是这里牵扯到大量的业务逻辑和领域概念。

典型的事件如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
    "username": "wwj",
    "event_source": "server",
    "name": "edx.course.enrollment.deactivated",
    "referer": "http:\/\/localhost:8001\/container\/i4x:\/\/edX\/DemoX\/vertical\/69dedd38233a46fc89e4d7b5e8da1bf4?action=new",
    "accept_language": "en-US,en;q=0.8",
    "time": "2014-01-26T00:28:28.388782+00:00",
    "agent": "Mozilla\/5.0 (Windows NT 6.1; WOW64; Trident\/7.0; rv:11.0) like Gecko",
    "page": null
    "host": "courses.edx.org",
    "session": "a14j3ifhskngw0gfgn230g",
    "context": {
      "user_id": 9999999,
      "org_id": "edX",
      "course_id": "edX\/DemoX\/Demo_Course",
      "path": "\/change_enrollment",
    },
    "ip": "NN.NN.NNN.NNN",
    "event": {
      "course_id": "edX\/DemoX\/Demo_Course",
      "user_id": 9999999,
      "mode": "honor"
    },
    "event_type": "edx.course.enrollment.deactivated"
  }

这是一条相对简单的事件。但我们可以看到基于json的日志,表现力极强(因为支持嵌套结构),你可以轻松将你关心的信息,都组织在嵌套json结构中。但也别忘了扁平优于嵌套。这个例子里另一个具有参考价值的是对一些业务无关的信息,诸如username、event_source、referer这些都是通用的概念,这就是我们提到的事件描述相关的部分。

如果你对细节有更多的兴趣,可以参考我之前的文章:基于tracking logs的数据分析.如果你的兴趣在open edx本身,那么我之前和同事一起翻译的Tracking Logs 中的事件(翻译)你可能有兴趣。

open edx中的日志系统给我的影响主要是:你可以直接使用django的日志系统并选择json作为日志格式,此外我们就不必深入了。当然,如果你对open edx本身熟悉,过一遍它的事件类型,作为案例学习,看看它是如何选择事件信息(这需要里理解业务),也是很有益的

PaperWeekly中的日志系统

我们在PaperWeekly构建日志系统时,架构层面参考了open edx。我们日志系统同时服务于:推荐系统和数据可视化(对google analytics做些补充)

我们先做个概览:在PaperWeekly网站后台我们首先将事件pub出去,对此感兴趣的订阅者自行订阅它(典型订阅者包括消息系统和日志系统),每个订阅者都跑在一个task中(celery)

我们最初是在构建通知系统时,发现我们需要考虑如何来描述事件(诸如我们要通知用户A:你发表的文章B被用户C点赞了),为了描述这点,我们需要关心事件:用户C点赞了文章B, 而后在构建推荐系统时,发现我们同样需要关心这些事件。于是我们索性把这几块一并做了。利用pub/sub机制来解耦。

通知系统和日志系统很大程度是重叠的。核心都是事件,那么怎么描述事件就摆到桌面来了。

在关于通知系统的讨论中,这块被讨论得多些,我之前也专门整理了一篇文章:notification system思路、概念与实现,我们在paperweekly,所做的基本是对这篇文章提到的概念做了实现,这块@izuo做了大量出色的工作。 最近@izuo将这块的工作梳理在消息系统的概念、设计与实现

如何把你关心的事件结构化,一种好的策略是把它们描述为AVOT的结构(我们之所以需要AVOT是因为我们需要结构化事件,我们之所以需要结构化事件,是因为程序善于处理结构化数据,否则你就需要处理一堆脏数据了)。AVOT分别是:

  • Actor
  • Verb
  • Object
  • Target

让我们举例说明: 用户C将文章B放入收藏夹D中,让我们把对应关系写上:

  • Actor: 用户C
  • Verb: 放入
  • Object: 文章B
  • Target: 收藏夹D

这其中可能引起疑惑的是Object和Target的区别,两者都是名词,如何决定两个名词中哪个对应Object,哪个对应Target。就功能而言你当然可以随意(它们本质只是4个容器),但最好使用它们的语义,这样你不至于把自己搞乱了,target的意义你可以通过这个英文句子来理解:Erik is pinning Hawaii to his Places to Visit board.,在这里his Places to Visit board是Target。

至于前边我们提到的用户C点赞了文章B,我们把它视为省略了Target

你可以试试用AVOT去描述你所关心的事件,你会发现它具有极强的变现力!

实现细节

最后我们来讨论一下实现细节

PaperWeekly和open edx一样直接使用了Django的日志系统, 我们为用户日志,单独构建了一个handle,以区别与系统日志

当然,关于日志的这些细节无关紧要,你可以参考你正在使用的框架,文档里论述应该都很全面

1
2
3
4
5
6
7
8
9
        'track_log': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'maxBytes': 1024 * 1024 * 10,  # 10MB
            'backupCount': 100,  # 10MB * 100 # 1G
            'filename': os.path.join(TRACK_LOG_PATH,
                                     'track_log.log'), 
            'formatter': 'track_log'
        },

至于它的formatter则是:

1
2
3
        'track_log': {
            'format': '%(message)s'
        },

track_log formatter只忠实地记录打印message,而message则由python中的类去构建,这些消息类就是对我们前边谈到的AVOB的实践,你需要使用AVOB去具体描述你所关心的事件(通常与业务相关)

补充一些细节:我们前头说到日志系统主动去sub系统发布的事件,具体的实现利用了Django的signal机制。为了让这些记录不影响用户访问,我们将记录log的task放到了celery里

前端事件

如果你同时还关心发生在前端的事件,有两个办法解决,其一是使用google analytics这类前端分析工具,它允许你自定义事件。其二是在后端写一个通用接口,前端构建好事件(json数据),发送给后台,后台把它当作一般的事件数据,记录到日志中即可,最好加上日志来源这个属性:event_source,这点open edx考虑得很周到。

服务于其他系统

前边提到我们让日志系统服务于推荐系统和数据可视化

推荐系统会根据用户的行为日志计算出用户的的feed流,实时更新。这部分的实现细节是:我们将日志同步到一台独立的计算机器上,分布式地算出用户feed流之后,回推到网站后端->用户主页.

至于数据可视化我们则用了ELK来做,先用Logstash使json平坦化,然后根据业务需要,可视化这些数据。对于日志中不包含的数据,可以通过查询数据库得到,诸如日志中只要记录username,我们就可以还原出user对象(通过查询数据库)。这部分我们也是参考了open edx数据分析这块的的做法,这块的一个建议是,尽量不要再服务器运行高峰期去操作生产服务器。如果你的查询很少,可以直接查询生产服务器,如果查询多且耗性能的话,建议把数据库同步到计算节点上,做本地查询。

参考