远程 tail 日志文件

2015年10月10日

很多时候开发人员都会有查看服务器日志的需求,而且很多时候是在上线或者出现错误情况下, 很大可能都会执行的如下命令:

tail -f iam.log
tail -f iam.log | grep Exception
tail -1000 iam.log
tail -1000 iam.log | grep Error
balala ...

但是很多公司登陆服务器的步骤十分繁琐(为了安全, 你懂得), 比如要先登陆到跳板机, 然后通过跳板机才能登陆到相应的服务器。跳板机的登陆又有双因子验证之类, 比如结合手机上的验证码。 这种情况下想要查看日志会非常麻烦,特别是比较紧急的情况,登陆到服务器得费一番周折。因此我们需要一种简单的查看日志的方式,比如不用登陆服务器就可以在本地执行 tail 命令,或者有 web 页面可以让我们查看相应的日志。基于这样的需求调研了几种方案,以下是一些方案的实现和优缺点介绍。(前面一部分是准备自己造轮子, 后来准备采用现成的开源方案, 发现需求不满足之后又回到了自己造轮子。)


1. nc & http event stream

    echo -e 'HTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *\nContent-type: text/event-stream\n' && tail -f ./iam.log | sed -e 's/^/data: /;s/$/\\n/' | nc -l 1234

打开浏览器,输入 http://127.0.0.1:1234/, 如果 iam.log 一直有内容在写入的话, 就可以看到新添加内容源源不断地显示在浏览器中了。那么这一行命令的原理是什么呢?这里我们首先需要了解 nc 命令和 HTTP 中 Content-type: text/event-stream

nc - The nc (or netcat) utility is used for just about anything under the sun involving TCP or UDP. It can open TCP connections, send UDP packets, listen on arbitrary TCP and UDP ports, do port scanning, and deal with both IPv4 and IPv6. Unlike telnet(1), nc scripts nicely, and separates error messages onto standard error instead of sending them to standard output, as telnet(1) does with some.

简单来说 nc 是一款短小精悍的网络工具,可通过 TCP 或 UDP 协议传输读写数据。上面的命令将管道之前命令的输出通过 1234 端口发送出去,在发送的 TCP 数据的前面添加了 HTTP 协议相关的东西,所以这里可以等价于一个简单的 http 服务,同时在 HTTP 头中声明了 Content-type: text/event-stream。

Server-Sent Events - One Way Messaging.
A server-sent event is when a web page automatically gets updates from a server. This was also possible before, but the web page would have to ask if any updates were available. With server-sent events, the updates come automatically.

在 HTTP 中设置了 Content-type: text/event-stream 之后, 客户端会源源不断的从服务器端获取新的数据。上面的命令采用了这种方式,相当于将 tail -f ./iam.log 的输出源源不断的发送到客户端,就实现浏览器能够获取 tail 的内容目的。 但是这种方式的问题在于其是一次性的,就是只能接受一次连接请求,之后便会失效, 这个和 nc 的实现相关, 它只会等待第一次 TCP 连接, 并把数据发送给它。


2. Cherrypy & subprocess & http event stream

为了避免 nc 命令带来的缺陷, 可以采用其他的 http server 来代替 nc, 下面是使用 cherrypy 的方式, 在请求过来之后通过 subprocess 打开 tail 命令, 并将 tail 的标准输出返回给客户端即可(pip install cherrypy and subprocess 先)。

    import cherrypy
    import subprocess
    import sys

    class Tail:

      @cherrypy.expose
      def index(self):
        cherrypy.response.headers['Content-Type'] = 'text/event-stream'

        def stream():
          f = subprocess.Popen(
            "exec tail -F %s" %(sys.argv[1]),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=True)
          while True:
            line = f.stdout.readline()
            yield line

        return stream()

      index._cp_config = {'response.stream': True}

    cherrypy.quickstart(Tail())

注意 tail -F 能够在文件改名之后继续监控同名的文件,对于支持logratote的日志文件很必要。执行命令 python tail.py iam.log,并在浏览器输入 http://127.0.0.1:8080/ 即可。这里解决了 nc 命令只能访问一次的问题, 但是发现访问了几次之后系统中会滞留很多 tail -f iam.log 进程, 这是因为 http 连接在断开之后, 开启的子进程并没有终止,所在在 event-stream 断开之后需要关闭相应的子进程,修改之后的代码如下:

    import cherrypy
    import subprocess
    import sys
    import os
    import signal

    class Tail:

      @cherrypy.expose
      def index(self):
        cherrypy.response.headers['Content-Type'] = 'text/event-stream'

        def stream():
          try: 
            f = subprocess.Popen(
              "tail -F %s" %(sys.argv[1]),
              stdout=subprocess.PIPE,
              stderr=subprocess.PIPE,
              shell=True)
            while True:
              line = f.stdout.readline()
              yield line
          finally:
            try:
              os.kill(f.pid, signal.SIGTERM)
            except Exception as e:
              print str(e)

        return stream()

      index._cp_config = {'response.stream': True}

    cherrypy.quickstart(Tail())

注意这里采用 利用 event-stream 的方式实现比较简单直接, 可以添加在任何普通的 http 服务中, 不用添加任何新的协议支持。同时这里还可以实现其他命令输出的远程显示,只需要将相应的命令替换掉即可。


3. websocketd

由于远程查看日志应该是一个比较普遍的需求,应该有许多开源的方案可供选择, 第一个找到的就是 Websocketed:

websocketd is a small command-line tool that will wrap an existing command-line interface program, and allow it to be accessed via a WebSocket.

其采用 pipeline + websocket 的方式,将相关的 sehll 命令的输出通过 websocket 的方式传递给客户端。 Websocketed 基于 Go 语言编写, 具体可以参见其 github 主页 https://github.com/joewalnes/websocketd。 对应到远程 tail 日志的需求, 执行如下命令即可:

websocket --port=8080 `tail -F iam.log` 

我们可能需要查看很多台机器的很多日志, 所以需要在各个机器上开多个端口, 每个端口对应一个日志文件。同时需要编写一个前端页面接受 websocket 请求, 同时将这些日志的请求汇总到同一个页面以方便查看, 由于这款工具不是一个特别为远程 tail 日志定制的,所以还需要一定的开发工作才能满足我们的需求。另外由于需要执行的命令在启动的时候已经固定了,这样不够灵活,比如需要执行 tail -100 iam.log 就是一个问题。


4. log.io & rtail

log.io 是一个浏览器中的实时日志监控工具,其采用 node 编写, 通过 socket.io 实现日志更新的及时推送, 同时以流的形式来组织多个日志文件, 重要的是界面也很炫酷, 可参见 demo 。 log.io 包含两个组件 log-server 和 log-harvester:

log.io-server: 负责多个日志流的整合和前端显示。

log.io-harvester: 负责监控日志文件的变化, 将新的内容发往 log.io-server。

程序的数据流图如下:

                                                                +------------+
                                                          +------>|   client1  |
  +--------------------+                                  |       +------------+
  | log.io-harvester1  |logs/TCP                          |                     
  |/opt/logs/access.log|----+                             |                     
  |/opt/logs/error.log |    |                             |logs/socket.io       
  +--------------------+    |     +----------------+      |                     
                            |     |                |      |                     
                            +---->| log.io-server  |------+                     
                            |     |                |      |                     
  +--------------------+    |     +----------------+      |                     
  | log.io-harvester2  |    |                             |logs/socket.io       
  |/opt/logs/access.log|----+                             |                     
  |/opt/logs/error.log |logs/TCP                          |                     
  +--------------------+                                  |       +------------+
                                                          +------>|   clien2   |
                                                                  +------------+

log.io-harvester 监控日志文件的变化, 并将新的日志通过 TCP 的方式源源不断的发给 log.io-server, log.io-server 将接收到的日志发送到连接到服务器的客户端。 这里存在一个问题就是当日志文件量比较大的时候, log.io-server 的带宽会成为瓶颈, 因为所有的日志更新都在不断的发送给它。 另一方面因为日志发往同一个 log.io-server, 为了区分每一行日志的来源, 在日志发往 log.io-server 之前,需要在每一行日志前面加上相应的标识,这样需要针对日志做行切分,在日志量大的时候也会带来 log.io-harvester 的计算压力。在实践的过程中我们也出现了某个大量产生的日志的文件发生了拥堵情况,导致内存占用变大,同时日志有严重的延迟现象。由于远程查看日志的需求频率并不是那么高, 但是为此我们需要付出的是 7 * 24 小时的计算资源和带宽资源,这带来了严重的资源浪费,同时还会有日志延迟现象的发生。另外还存在和 websocketd 同样的缺点,就是查看日志的方式不够灵活, 只能查看打开浏览器之后产生的日志,不能够完成 tail -100 iam.log 的功能。

rtail 是 log.io 的一个改进版本:

The rtail approach is very simple:

  1. pipe something into rtail using UNIX I/O redirection [2]

  2. broadcast every line using UDP

  3. rtail-server, if listening, will dispatch the stream into your browser, using socket.io.

主要的改进为两个方面:

  1. 采用 pipeline 的方式收集命令的输出,而不是像 log.io 一样通过监控日志文件的变化。可以采用 tail -F 的方式来完成 log.io 提供的功能, 实现简单并且提供了更多的可能性。
  2. 采用更加高效的 UDP 来代替 TCP。

由于 rtail 和 log.io 的原理基本相同, 所以上文提到的资源浪费和无法实现 tail -n 的无法实现的缺点依旧存在。


5. new rtail

在看过了一些实现方案之后,发现大部分实现原理都是类似于简单的 websocket 或者 比较完备的 log.io 方案,对于我来说好像都不太能够满足需求, 我们期望最好实现和登陆到服务器之后查看日志几乎等价的功能, 就是说最好需要支持 tail 所有功能, 当然如果还支持 grep 或者 awk 便是极好的。基于这样的需求准备开始自己造一个轮子,目标是至少支持 tail 命令所有的选项。方案确定为采用 socket.io 的方式, 因为其采用 node 实现比较简单,同时为了灵活性放弃了在浏览器查看的想法, 转而决定采用命令行的方式, 不仅仅是因为码农比较习惯命令行的方式,更重要的是在命令行中我们能够方便的使用 grep, awk, sed 等命令帮助我们查找或者分析日志。 实现的方案如下 (取名也是 rtail,是在已经开始写代码之后才发现已经有一个叫 rtail 的东东了, 那么就叫它 new-rtail 吧):

                                      logs/socket.io                                      
             +---------------------------------------------------------------------------+
             |                                                           +------------+  |
             |                                                   +------>|   rtail1   |<-+
  +--------------------+  config                                 |       +------------+   
  |    rtail-slave1    |/socket.io                               |                        
  |/opt/logs/access.log|--------+                                |                        
  |/opt/logs/error.log |        |                                |config/socket.io        
  +--------------------+        |        +----------------+      |                        
                                |        |                |      |                        
                                +------->| rtail-server   |------+                        
                                |        |                |      |                        
  +--------------------+        |        +----------------+      |                        
  |    rtail-slave2    |        |                                |                        
  |/opt/logs/access.log|--------+                                |config/socket.io        
  |/opt/logs/error.log |  config                                 |                        
  +--------------------+/socket.io                               |       +------------+   
             |                                                   +------>|   rtail2   |<-+
             |                                                           +------------+  |
             +---------------------------------------------------------------------------+
                                         logs/socket.io                                   

new-rtail 的三个组件介绍:

  • rtail-slave : 主要是负责将相关的 tail 执行结果发送到 rtail 客户端。 rtail-slave 在启动之后将自身的信息注册到 rtail-server, 包括获取日志的地址以及所有支持的日志文件。同时每间隔一段时间将注册信息发送到 rtail-server, 以便 rtail-server 确定其还活着。rtail-slave 接受 rtail 客户端发送过来的 tail 请求, 通过 subprocess 的方式执行相关的命令, 并将输出结果返回给客户端。因为利用的是系统本身支持的 tail 命令,所以 new-rtail 很容易就支持了 tail 的所有选项(oh,yeah!)。

  • rtail-server : 负责管理所有的 rtail-slave, 主要保存可使用的 rtail-slave 的信息。rtail-server 接受 rtail-salve 不断的发送注册信息, 保存到缓存并设置过期时间,如果 rtail-slave 在过期之前没有再次发来注册信息, 则判定这个 rtail-slave 不可用, 从配置文件中删除。另一方面 rtail-server 接受 rtail 客户端的询问请求, 比如支持的 host 和日志文件等信息。

  • rtail : 获取 tail 结果的客户端。 首先询问 rtail-server 相关的地址信息,然后连接到相应的 rtail-slave 获取日志文件的 tail 信息。

上述实现中 rtail-server 只是保存当前存活的 rtail-slave 的信息, 而不是日志文件的中转, 这样减轻了 rtail-server 的压力, 避免其成为瓶颈。 另一方面 rtail-slave 只有在 rtail 客户端发起请求的时候才真正获取结果并发送到客户端,这样避免了像 log.io 无谓的资源消耗。同时 new-rtail 支持所有的 tail 命令选项,同时还在命令行中还可以通过管道使用 grep,awk,sed 等命令。 本地运行命令如下:

rtail rc-log01 -f tatooine-warn.log | grep Exception

使用也比较方便, 仅仅比在本地 tail 命令多了一个 host 的选项, 其他的用法完全一致。实现代码和详细使用说明见 mzeroo/rtail(由于对 node 不是很熟悉,代码质量一般般哈, 继续完善吧)。 这里的实现存在一个问题, 比如我们执行 rtail rc-log01 -10000000 iam.log | grep "Some special" | head -100 的时候, 这时 rtail 会从服务器取回来 10000000 行日志文件之后在查找,这会造成带宽的浪费。 改进的方案是在服务器端支持 grep 命令和 head 命令,然后将计算好的结果传回到客户端即可,期望改进之后执行的方式如: rtail rc-log01 -10000000 iam.log -grep "Some special" -head -100 (目前还没有特别大的需求,后续可改进)。

参考

[1] http://www.cnblogs.com/wenbiao/p/3375811.html

[2] http://www.w3schools.com/html/html5_serversentevents.asp

[3] http://www.html5rocks.com/en/tutorials/eventsource/basics/

[4] https://github.com/joewalnes/websocketd