由一個簡單需求到Linux環境下的syslog、unix domain socket


 

 

  本文記錄了因為一個簡單的日志需求,繼而對linux環境下syslog、rsyslog、unix domain socket的學習。本文關注使用層面,並不涉及rsyslog的實現原理,感興趣的讀者可以參考rsyslog官網。另外,本文實驗的環境實在debian8,如果是其他linux發行版本或者debian的其他版本,可能會稍微有些差異。

需求:

  工作中有一個在Linux(debian8)環境下運行的服務器程序,用python語言實現,代碼中有不同優先級的日志需要記錄,開發的時候都是使用python的logging模塊輸出到文件,示例代碼如下:

  

 1 import logging, os
 2 
 3 logger = None
 4 def get_logger():
 5     global logger
 6     if not logger:
 7         logger = logging.getLogger('ServerLog')
 8         logger.setLevel(logging.INFO)
 9         filehandler = logging.FileHandler(os.environ['HOME'] + '/Server.log', encoding='utf8')
10         filehandler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
11         logger.addHandler(filehandler)
12     return logger
13 
14 def some_func():
15     get_logger().info("call some_func")
16 
17 if __name__ == '__main__':
18     some_func()

 

  運行上面這段代碼,就會在home目錄下面產生一個server.log文件。

  后來數據分析的部門說他們希望能夠實時拿到一部分日志,他們有一台專門處理日志的服務器,那么怎么把日志發給他們呢?筆者之前並沒有相關經驗,數據分析部門的同事說,這種需求他們都是找運維人員幫忙。運維同事給出的方案很簡單:產品把日志寫到syslog,然后他們負責把帶有某些關鍵字的日志轉發給數據分析部門,在運維同事的指導下,把代碼改成了這樣:

 1 import logging
 2 import logging.handlers
 3 
 4 logger = None
 5 def get_logger():
 6     global logger
 7     if not logger:
 8         logger = logging.getLogger('ServerLog')
 9         logger.setLevel(logging.INFO)
10 
11         sys_handler = logging.handlers.SysLogHandler('/dev/log', facility=logging.handlers.SysLogHandler.LOG_LOCAL0)
12         syslog_tag = 'ServerLog'
13         sys_handler.setFormatter(logging.Formatter(syslog_tag + ":%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
14 
15         logger.addHandler(sys_handler)
16     return logger
17 
18 def some_func():
19     get_logger().info("call some_func")
20 
21 if __name__ == '__main__':
22     some_func()

 

  上面的代碼修改了日志的輸出形式,直觀的感受就是從文件server.log 到了 /dev/log,但/dev/log對應的是SysLogHandler,並不是FileHandler,所以肯定不是一個普通的文件。此時,我有兩個疑問:第一,這里我並沒有將日志輸出到home目錄下的Server.log文件,但是程序運行的時候生成了這么一個文件;第二,怎么講日志發送到數據分析部門的服務器。

  不懂就問:

  Q:新的代碼下怎么生成Server.log文件,日志內容又是怎么轉發到數據分析部門的服務器?

  A:  這個是/etc/init.d/rsyslog這個后台程序根據/etc/rsyslog.conf 這個配置文件 將日志輸出到不同的文件,包括網絡文件,即其他服務器。看/etc/rsyslog.conf這個配置就明白了。

  Q:OK,那python代碼將文件輸出到/dev/log跟 rsyslog又是什么關系呢?

  A:python的sysloghandler會將日志發送到rsyslog,他們之間使用unix domain socket通信,具體看logging模塊的源碼就知道了

 

unix domain socket:

  按照上面的對話的意思,python程序先將日志發送給rsyslog這個程序,然后rsyslog再處理收到的日志數據,所以先看logging代碼:

  SysLogHandler這個類在logging.handlers.py, 核心代碼如下:

 1     def __init__(self, address=('localhost', SYSLOG_UDP_PORT),
 2                  facility=LOG_USER, socktype=socket.SOCK_DGRAM):
 3         """
 4         Initialize a handler.
 5 
 6         If address is specified as a string, a UNIX socket is used. To log to a
 7         local syslogd, "SysLogHandler(address="/dev/log")" can be used.
 8         If facility is not specified, LOG_USER is used.
 9         """
10         logging.Handler.__init__(self)
11 
12         self.address = address
13         self.facility = facility
14         self.socktype = socktype
15 
16         if isinstance(address, basestring):
17             self.unixsocket = 1
18             self._connect_unixsocket(address)
19         else:
20             self.unixsocket = 0
21             self.socket = socket.socket(socket.AF_INET, socktype)
22             if socktype == socket.SOCK_STREAM:
23                 self.socket.connect(address)
24         self.formatter = None
25 
26     def _connect_unixsocket(self, address):
27         self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
28         # syslog may require either DGRAM or STREAM sockets
29         try:
30             self.socket.connect(address)
31         except socket.error:
32             self.socket.close()
33             self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
34             self.socket.connect(address)

  在__init__.doc里面寫得很清楚,如果address是一個字符串(默認值是一個tuple),那么會建立一個unix socket(unix domain socket)。如果address為“/dev/log”(正如我們之前的python代碼),那么輸出到本機的syslogd程序。另外,在第27行 self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) socket.socket的第一個參數family 的值為AF_UNIX,而不是我們經常使用的AF_INET(IPV4)或者AF_INET6(IPV6)。那么什么是unix domain socket呢?

  

  unix domain socket是進程間通信(IPC:inter-process communication)的一種方式,其他還有管道、命名管道、消息隊列、共享內存、socket之類的。unix domain socket與平常使用的socket(狹義的internet socket)有什么區別呢,那就是unix domain socket只能在同一台主機上的進程之間通信,普通的socket也可以通過'localhost'來在同一台主機通信,那么unix domain socket有哪些優勢呢?

  第一:不需要經過網絡協議棧

  第二:不需要打包拆包、計算校驗和、維護序號和應答等

  所以,優勢就是性能好,一個字,快。

 

  下面用一個簡單的服務器客戶端例子來看看unix domain socket的使用方法與過程:

  服務器:uds_server.py

 1 ADDR = '/tmp/uds_tmp'
 2 
 3 import socket, os
 4 
 5 def main():
 6     try:
 7         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 8         if os.path.exists(ADDR):
 9             os.unlink(ADDR)
10         sock.bind(ADDR)
11         sock.listen(5)
12         while True:
13                 connection, address = sock.accept()
14                 print "Data : %s" % connection.recv(1024);
15                 connection.send("hello uds client")
16                 connection.close()
17     finally:
18         sock.close()
19 
20 if __name__ == '__main__':
21         main()

 

  客戶端:uds_client.py

 1 ADDR = '/tmp/uds_tmp'
 2 
 3 import socket
 4 
 5 def main():
 6         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 7         sock.connect(ADDR)
 8         sock.send('hello unix domain socket server')
 9         print 'client recieve', sock.recv(1024)
10         sock.close()
11 
12 if __name__ == '__main__':
13         main()

 

  首先:運行服務器 python uds_server.py,這個時候在/tmp 目錄下產生了文件,用ls查看詳細信息如下:

  

  可以看到,文件類型(第一個字段)為s,代表socket文件。(PS: 如果進程間用命令管道通信,也是利用中間文件,ls顯示的文件類型為p)

  運行客戶端 python uds_client.py,在客戶端和服務器端都有相應的輸出,使用方法與普通socket沒有什么大的差異。

 

日志轉發流程:

  在了解了unix domain socket這個概念之后,下面就比較簡單了,首先是/dev/log這個文件,我們用ls來查看這個文件的信息

  

  可以看到這個文件是一個符號鏈接文件,真實的文件是/run/systemd/journal/dev-log, 那么再來查看這個文件

  

  ok,是一個socket文件,復合預期,按照之前的unix domain socket的例子,rsyslog也應該咋這個文件上監聽,我們來看看

  

  lsof fd可以列出所有使用了這個文件(linux下文件的概念比較寬泛)的進程,事實上我們看到只有systemd和systemd-j兩個不明所以的進程。那么直接看看rsyslog使用的unix domain socket吧

  

      

  額,可以看到rsyslogd使用的socket domain socket是/run/systemd/journal/syslog,並不是/run/systemd/journal/dev-log,這兩個文件在同一個目錄下,那么再來看看還有哪些進程使用了/run/systemd/journal/syslog。

   

  so,systemd和rsyslogd都使用了這個文件,感覺像是應用進程(e.g. 上面的python程序)將日志通過/run/systemd/journal/dev-log(/dev/log背后真正的文件)發送到systemd, 然后systemd 再將日志通過/run/systemd/journal/syslog發送到rsyslogd,是不是這樣呢,google了一下,發現了這篇文章understand-logging-in-linux,確實是這么一個過程:

  

systemd has a single monolithic log management program, systemd-journald. This runs as a service managed by systemd.

  • It reads /dev/kmsg for kernel log data.
  • It reads /dev/log (a symbolic link to /run/systemd/journal/dev-log) for application log data from the GNU C library's syslog() function.
  • It listens on the AF_LOCAL stream socket at /run/systemd/journal/stdout for log data coming from systemd-managed services.
  • It listens on the AF_LOCAL datagram socket at /run/systemd/journal/socket for log data coming from programs that speak the systemd-specific journal protocol (i.e. sd_journal_sendv() et al.).
  • It mixes these all together.
  • It writes to a set of system-wide and per-user journal files, in /run/log/journal/ or /var/log/journal/.
  • If it can connect (as a client) to an AF_LOCAL datagram socket at /run/systemd/journal/syslogit writes journal data there, if forwarding to syslog is configured.

  

  ok,到現在為止,我們知道了應用程序的日志是怎么轉發到rsyslog,那么rsyslog怎么處理接收到的日志,秘密就在/etc/rsyslog.conf, 在打開這個配置文件之前,我們先看看rsyslog官網的簡單描述:

   RSYSLOG is the rocket-fast system for log processing.

   原來R是rocket-fast的意思!火箭一般快!官網聲稱每秒可以處理百萬級別的日志。rsyslogd在部分linux環境是默認的syslogd程序(至少在筆者的機器上),d是daemon的意思,后台進程。系統啟動的時候就會啟動該進程來處理日志(包括操作系統自身和用戶進程的日志)。打開修改過的/etc/rsyslog.conf, 接下來就是見證奇跡的時刻

   

  原來一舉一動都在監控之中。這個文件是系統提供的,直接在這個文件上做修改顯然不是明智之舉。如上圖紅色部分,可以再rysyslog.d文件夾下增加自己的配置文件,定制日志過濾規則。那么看看的rsyslog.d文件夾下新增的tmp.conf

 1 $FileOwner USERNAME
 2 $FileGroup USERNAME
 3 $FileCreateMode 0644
 4 $DirCreateMode 0755
 5 $Umask 0022
 6 $template serverLog,"/home/USERNAME/Server.log"
 7 $template LogFormat,"%msg%\n"
 8 if $syslogfacility-text == 'local0' and $syslogtag contains 'ServerLog' then -?serverLog;LogFormat
 9 
10 #if $syslogfacility-text == 'local0' and $syslogtag contains 'ServerLog' then @someip:port
11 & stop

  再來回顧一下對應的應用代碼:

 1 import logging
 2 import logging.handlers
 3 
 4 logger = None
 5 def get_logger():
 6     global logger
 7     if not logger:
 8         logger = logging.getLogger('ServerLog')
 9         logger.setLevel(logging.INFO)
10 
11         sys_handler = logging.handlers.SysLogHandler('/dev/log', facility=logging.handlers.SysLogHandler.LOG_LOCAL0)
12         syslog_tag = 'ServerLog'
13         sys_handler.setFormatter(logging.Formatter(syslog_tag + ":%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
14 
15         logger.addHandler(sys_handler)
16     return logger
17 
18 def some_func():
19     get_logger().info("call some_func")
20 
21 if __name__ == '__main__':
22     some_func()

  注意:配置文件需要與應用代碼配合,比如代碼中第11行 facility=logging.handlers.SysLogHandler.LOG_LOCAL0 與 配置中 $syslogfacility-text == 'local0' 相對應;代碼第12行 syslog_tag = 'ServerLog' 與 配置文件 $syslogtag contains 'ServerLog' 對應。關於python代碼中syslogtag的設置,參考了stackoverflow上的這個問答

  當我們修改了配置時候需要通過命令 /etc/init.d/rsyslog restart 來重啟rsyslogd,重啟之后再運行之前的python文件,就可以了。

   

 發送到遠端服務器:

  上面的tmp.conf文件注釋掉了第10行,這一行的作用是將滿足條件的日志發送到指定的其他機器上,IP:Port用來指定接受日志的遠端rsyslogd程序。默認情況下rsyslogd在514端口監聽。假設我需要給局域網內10.240.10.10發送syslog,第10行改成這樣就行了:

if $syslogfacility-text == 'local0' and $syslogtag contains 'ServerLog' then @10.240.10.10

  那么10.240.10.10主要開啟rsyslogd的遠程監聽,並指定遠端日志的輸出規則,for example:

  

  這個配置,讓rsyslogd使用UDP和TCP協議同時在514端口上監聽,並將非本機的日志輸出到對應遠端主機名的文件。注意,以上修改 都需要重啟rsyslogd才能生效。

總結:

  日志從應用程序到最終的日志文件(或者遠程服務器)的流程如下:

  

  

references:

inter-process communication

unix domain socket

understand-logging-in-linux

在 Linux 上配置一個 syslog 服務器


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM