先描述一下問題,多個服務器實現的負載均衡,每個服務器存儲在自己的硬盤裏。但是現在需要對日誌做統一的分析,在多個服務器上統計就麻煩了。思路是把日誌統一到一臺日誌服務器上,再統一做統計分析。怎麼統一到一臺服務器上,說實話沒有特別好的思路,最後嘗試了log4j的SocketAppender。查了不少網絡資源,都說的有些不明瞭,還是得親自嘗試之後才見分曉。
1、客戶端的配置:
客戶端的配置比較簡單,只需要告訴log4j需要監聽哪個遠程服務器的哪個端口即可。直接在log4j.properties裏直接配置就好。
- log4j.appender.logs=org.apache.log4j.DailyRollingFileAppender
- log4j.appender.logs.File = /data/logs/request/logs.log
- log4j.appender.logs.layout = org.apache.log4j.PatternLayout
- log4j.appender.logs.layout.ConversionPattern=%d [%t] - %m%n
- log4j.appender.logs.DatePattern='.'yyyy-MM-dd'.log'
- log4j.appender.socket=org.apache.log4j.net.SocketAppender
- log4j.appender.socket.RemoteHost=172.16.2.152
- log4j.appender.socket.Port=4560
- log4j.appender.socket.LocationInfo=true
- #下面這兩句感覺沒用
- log4j.appender.socket.layout=org.apache.log4j.PatternLayout
- log4j.appender.socket.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%t%m%n
- #將日誌寫入本地和遠程日誌服務器
- log4j.logger.com.test.core.filter =DEBUG,socket,logs
2、日誌服務器的配置:
日誌服務器需要單獨啓動一個java進程,接收客戶端給自己發送的socket請求。Log4j提供了org.apache.log4j.net.SocketServer類,直接運行其main函數就行了(當然也可以自己寫啦)。
java -cp /log4jsocket/serverConfig/log4j-1.2.16.jarorg.apache.log4j.net.SocketServer 4560 /log4jsocket/log4jserver.properties /log4jsocket/clientConfig
/log4jsocket/serverConfig/log4j-1.2.16.jar是log4j jar包存放的位置,org.apache.log4j.net.SocketServer需要三個參數:
1)4560 是監聽的端口號
2)/log4jsocket/log4jserver.properties 是記錄日誌服務器的日誌的配置文件
3)/log4jsocket/clientConfig 是客戶端配置文件所在的目錄(注意是目錄)。
着重說一下org.apache.log4j.net.SocketServer的第三個參數,這個文件夾下配置的是各個客戶端的日誌的配置。配置文件以.lcf結尾,文件名可以用客戶端的IP命名,log4j會自己找發送請求的客戶端IP對應的那個配置文件,如172.16.2.46服務器發送的socket請求會尋找172.16.2.46.lcf配置文件,並根據配置將日誌寫入對應的文件。
- #注意logger後面的值要與client的值相同
- log4j.logger.com.test.core.filter=DEBUG,localLogs
- log4j.appender.localLogs=org.apache.log4j.DailyRollingFileAppender
- log4j.appender.localLogs.File=/data/logs/request/172.16.2.46/logs.log
- log4j.appender.localLogs.layout=org.apache.log4j.PatternLayout
- log4j.appender.localLogs.layout.ConversionPattern=%d [%t] - %m%n
- log4j.appender.localLogs.DatePattern='.'yyyy-MM-dd'.log'
這樣做的好處是可以根據不同客戶端,將日誌寫入不同的文件夾下的。
其實,配置過程就這麼簡單,但是當你這麼做之後,你會發現運行org.apache.log4j.net.SocketServer後,客戶端向日志服務器發送請求時,會報找不到.lcf文件的錯誤,得不到想要的結果。原因出在org.apache.log4j.net.SocketServer代碼中的一個小bug。
- <span style="font-size:12px;">LoggerRepository configureHierarchy(InetAddress inetAddress)
- {
- cat.info("Locating configuration file for " + inetAddress);
- String s = inetAddress.toString();
- int i = s.indexOf("/");
- if (i == -1) {
- cat.warn("Could not parse the inetAddress [" + inetAddress + "]. Using default hierarchy.");
- return genericHierarchy();
- }
- String key = s.substring(0,i);
- File configFile = new File(this.dir, key + CONFIG_FILE_EXT);
- if (configFile.exists()) {
- Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG));
- this.hierarchyMap.put(inetAddress, h);
- new PropertyConfigurator().doConfigure(configFile.getAbsolutePath(), h);
- return h;
- }
- cat.warn("Could not find config file [" + configFile + "].");
- return genericHierarchy();
- }</span>
String key = s.substring(0, i);換成String key = s.substring(i+1);就好了。這段代碼是解析IP地址,然後尋找對應IP命名的.lcf配置文件;如果找不到,則解析默認的generic.lcf。由於截取的錯誤,導致找不到172.16.2.46.lcf,文件夾下又沒有generic.lcf,所以會拋異常。
org.apache.log4j.net.SocketServer代碼中的另外一個bug是,只能接收來自一臺客戶端的日誌請求,一旦客戶端停止運行,SocketServer也將關閉。查看代碼:
- public static void main(String[] argv)
- {
- if (argv.length == 3)
- init(argv[0], argv[1], argv[2]);
- else
- usage("Wrong number of arguments.");
- try
- {
- cat.info("Listening on port " + port);
- ServerSocket serverSocket = new ServerSocket(port);
- cat.info("Waiting to accept a new client.");
- Socket socket = serverSocket.accept();
- InetAddress inetAddress = socket.getInetAddress();
- cat.info("Connected to client at " + inetAddress);
- LoggerRepository h = (LoggerRepository)server.hierarchyMap.get(inetAddress);
- if (h == null) {
- h = server.configureHierarchy(inetAddress);
- }
- cat.info("Starting new socket node.");
- new Thread(new SocketNode(socket, h)).start();
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
- }
問題出在只建立了一個socket連接就不在accept了,加上while循環問題就解決了。
- ServerSocket serverSocket = new ServerSocket(port);
- while(true){
- cat.info("Waiting to accept a new client.");
- Socket socket = serverSocket.accept();
- InetAddress inetAddress = socket.getInetAddress();
- cat.info("Connected to client at " + inetAddress);
- LoggerRepository h = (LoggerRepository)server.hierarchyMap.get(inetAddress);
- if (h == null) {
- h = server.configureHierarchy(inetAddress);
- }
- cat.info("Starting new socket node.");
- new Thread(new SocketNode(socket, h)).start();
- }
好了。Log4j的配置到此結束。
最後一個問題,日誌服務器是linux,需要有一個統一的start、shutdown命令來啓動和關閉org.apache.log4j.net.SocketServer。那就需要些shell命令了,下面這段代碼參考了http://www.cnblogs.com/baibaluo/archive/2011/08/31/2160934.html
catalina.sh
- <span style="font-size:12px;">#!/bin/bash
- #端口
- LISTEN_PORT=4560
- #服務端log4j配置文件
- SERVER_CONFIG=/log4jsocket/server.properties
- #客戶端的配置
- CLIENT_CONFIG_DIR=/log4jsocket/clientConfig
- #Java程序所在的目錄(classes的上一級目錄)
- APP_HOME=/opt/log4jsocket/serverConfig
- #需要啓動的Java主程序(main方法類)
- APP_MAINCLASS=org.apache.log4j.net.SocketServer
- #拼湊完整的classpath參數,包括指定lib目錄下所有的jar
- CLASSPATH=$APP_HOME
- for i in "$APP_HOME"/*.jar; do
- CLASSPATH="$CLASSPATH":"$i"
- done
- #JDK所在路徑
- JAVA_HOME="/opt/jdk1.6.0_30"
- #執行程序啓動所使用的系統用戶,考慮到安全,推薦不使用root帳號
- RUNNING_USER=root
- #java虛擬機啓動參數
- JAVA_OPTS="-ms512m -mx512m -Xmn256m -Djava.awt.headless=true -XX:MaxPermSize=128m"
- #初始化psid變量(全局)
- psid=0
- checkpid() {
- javaps=`$JAVA_HOME/bin/jps -l | grep $APP_MAINCLASS`
- if [ -n "$javaps" ]; then
- psid=`echo $javaps | awk '{print $1}'`
- else
- psid=0
- fi
- }
- start() {
- checkpid
- if [ $psid -ne 0 ]; then
- echo "================================"
- echo "warn: $APP_MAINCLASS already started! (pid=$psid)"
- echo "================================"
- else
- echo -n "Starting $APP_MAINCLASS ..."
- JAVA_CMD="nohup $JAVA_HOME/bin/java -classpath $CLASSPATH $APP_MAINCLASS $LISTEN_PORT $SERVER_CONFIG $CLIENT_CONFIG_DIR >/dev/null 2>&1 &"
- su - $RUNNING_USER -c "$JAVA_CMD"
- checkpid
- if [ $psid -ne 0 ]; then
- echo "(pid=$psid) [OK]"
- else
- echo "[Failed]"
- fi
- fi
- }
- stop() {
- checkpid
- if [ $psid -ne 0 ]; then
- echo -n "Stopping $APP_MAINCLASS ...(pid=$psid) "
- su - $RUNNING_USER -c "kill -9 $psid"
- if [ $? -eq 0 ]; then
- echo "[OK]"
- else
- echo "[Failed]"
- fi
- checkpid
- if [ $psid -ne 0 ]; then
- stop
- fi
- else
- echo "================================"
- echo "warn: $APP_MAINCLASS is not running"
- echo "================================"
- fi
- }
- status() {
- checkpid
- if [ $psid -ne 0 ]; then
- echo "$APP_MAINCLASS is running! (pid=$psid)"
- else
- echo "$APP_MAINCLASS is not running"
- fi
- }
- info() {
- echo "System Information:"
- echo "****************************"
- echo `head -n 1 /etc/issue`
- echo `uname -a`
- echo
- echo "JAVA_HOME=$JAVA_HOME"
- echo `$JAVA_HOME/bin/java -version`
- echo
- echo "APP_HOME=$APP_HOME"
- echo "APP_MAINCLASS=$APP_MAINCLASS"
- echo "****************************"
- }
- case "$1" in
- 'start')
- start
- ;;
- 'stop')
- stop
- ;;
- 'restart')
- stop
- start
- ;;
- 'status')
- status
- ;;
- 'info')
- info
- ;;
- *)
- echo "Usage: $0 {start|stop|restart|status|info}"
- exit 0
- esac
- </span>
- <span style="font-size:12px;">#!/bin/sh
- EXECUTABLE=/log4jsocket/catalina.sh
- exec "$EXECUTABLE" start "$@"</span>
- <span style="font-size:12px;">EXECUTABLE=/log4jsocket/catalina.sh
- exec "$EXECUTABLE" stop "$@"</span>