深入理解監控系統——CAT Server端源碼解析(消息消費\報表處理\展示)

相關文章:
[分佈式監控CAT] Server端源碼解析——初始化
[分佈式監控CAT] Client端源碼解析
[分佈式監控CAT] Server端源碼解析——消息消費\報表處理

前言

**Server端 **
(Cat-consumer 用於實時分析從客戶端提供的數據\Cat-home 作爲用戶給用戶提供展示的控制端
,並且Cat-home做展示時,通過對Cat-Consumer的調用獲取其他節點的數據,將所有數據彙總展示)

consumer、home以及路由中心都是部署在一起的,每個服務端節點都可以充當任何一個角色

**Client端 **
(Cat-client 提供給業務以及中間層埋點的底層SDK)


上文說到了CAT-Server的啓動初始化。
接着我們要分析一下CAT-Server如何接受各個客戶端上報(TCP長連接)的消息,以及如何消費、解析、存儲等等
先來看一下CAT整體的架構圖:
這裏寫圖片描述

消費、解析

com.dianping.cat.analysis.TcpSocketReceiver

在上一篇文章中說過了服務端的啓動,在CAT-Server啓動時會啓動Netty的Nio 多線程Reactor模塊來接收客戶端的請求:

一個Accept線程池(Main Reactor Thread Pool )用來處理連接操作(通常還可以在這各Accept中加入權限驗證、名單過濾邏輯);

接着Accept連接成功的socket請求被轉發到 專門處理IO操作的線程池(Sub Reactor Thread Pool ,實現異步);在這裏做了消息的解碼處理;

再接着,解碼處理後,將消息發送到每個報表解析器內置的內存隊列中。消息將被異步分發給各個解析器單獨處理(不存在數據競爭)。

消息的接受是在這個類TcpSocketReceiver.java完成的:

    // 在CatHomeModule啓動時被調用
	public void init() {
		try {
			startServer(m_port);
		} catch (Throwable e) {
			m_logger.error(e.getMessage(), e);
		}
	}
	/**
     * 啓動一個netty服務端
     * @param port
     * @throws InterruptedException
     */
	public synchronized void startServer(int port) throws InterruptedException {
		boolean linux = getOSMatches("Linux") || getOSMatches("LINUX");
		int threads = 24;
		ServerBootstrap bootstrap = new ServerBootstrap();
		//linux走epoll的事件驅動模型
		m_bossGroup = linux ? new EpollEventLoopGroup(threads) : new NioEventLoopGroup(threads);//用來做爲接受請求的線程池 master線程
		m_workerGroup = linux ? new EpollEventLoopGroup(threads) : new NioEventLoopGroup(threads);//用來做爲處理請求的線程池 slave線程
		bootstrap.group(m_bossGroup, m_workerGroup);
		bootstrap.channel(linux ? EpollServerSocketChannel.class : NioServerSocketChannel.class);

		bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {//channel初始化設置
			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
				ChannelPipeline pipeline = ch.pipeline();

				pipeline.addLast("decode", new MessageDecoder());//增加消息解碼器
			}
		});
        // 設置channel的參數
		bootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
		bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
		bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
		bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

		try {
			m_future = bootstrap.bind(port).sync();//綁定監聽端口,並同步等待啓動完成
			m_logger.info("start netty server!");
		} catch (Exception e) {
			m_logger.error("Started Netty Server Failed:" + port, e);
		}
	}

啓動netty,對每個客戶端上報的消息都會做解碼處理,從字節流轉換爲消息樹MessageTree tree,接着交給DefaultMessageHandler處理。

public class DefaultMessageHandler extends ContainerHolder implements MessageHandler, LogEnabled {
	
    /*
     * MessageConsumer按每個period(整小時一個period)組合了多個解析器,用來解析生產多個報表(如:Transaction、
     * Event、Problem等等)。一個解析器對象-一個有界隊列-一個整小時時間組合了一個PeriodTask,輪詢的處理這個有界隊列中的消息
     */
    @Inject
	private MessageConsumer m_consumer;

	private Logger m_logger;

	@Override
	public void enableLogging(Logger logger) {
		m_logger = logger;
	}

	@Override
	public void handle(MessageTree tree) {
		if (m_consumer == null) {
			m_consumer = lookup(MessageConsumer.class);//從容器中加載MessageConsumer實例
		}

		try {
			m_consumer.consume(tree);//消息消費
		} catch (Throwable e) {
			m_logger.error("Error when consuming message in " + m_consumer + "! tree: " + tree, e);
		}
	}
}

OMS設計是按照每小時去彙總數據,爲什麼要使用一個小時的粒度呢?
這個是一個trade-off,實時內存數據處理的複雜度與內存的開銷方面的折中方案。
在這個小時結束後將生成的Transaction\Event\Problean報表存入Mysql、File(機器根目錄俠)。然而爲了實時性,當前小時的報表是保存在內存中的。

PeriodManager 用來管理 OMS單位小時內的各種類型的解析器,包括將上報的客戶端數據派發給不同的解析器(這種派發可以理解爲訂閱\發佈)。每個解析器,將收到的消息存入內置隊列,並且用單獨的線程去獲取消息並處理。

接下來我們繼續看代碼:

com.dianping.cat.analysis.PeriodManager

public class PeriodManager implements Task {
	 public void init() {
        long startTime = m_strategy.next(System.currentTimeMillis());//當前小時的起始時間

        startPeriod(startTime);
    }
	
    @Override
    public void run() {
   // 1s檢查一下當前小時的Period對象是否需要創建(一般都是新的小時需要創建一個Period代表當前小時)
        while (m_active) {
            try {
                long now = System.currentTimeMillis();
                //value>0表示當前小時的Period不存在,需要創建一個
                //如果當前線小時的Period存在,那麼Value==0
                long value = m_strategy.next(now);
                if (value > 0) {
                    startPeriod(value);
                } else if (value < 0) {
                    //  //當這個小時結束後,會異步的調用endPeriod(..),將過期的Period對象移除,help GC
                    Threads.forGroup("cat").start(new EndTaskThread(-value));
                }
            } catch (Throwable e) {
                Cat.logError(e);
            }

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
    //當這個小時結束後,會異步的調用這個方法,將過期的Period對象移除,help GC
    private void endPeriod(long startTime) {
        int len = m_periods.size();

        for (int i = 0; i < len; i++) {
            Period period = m_periods.get(i);

            if (period.isIn(startTime)) {
                period.finish();
                m_periods.remove(i);
                break;
            }
        }
    }

......
}	


消息消費是由MessageConsumer的實現類RealtimeConsumer處理:

com…RealtimeConsumer.consume(MessageTree tree)

    @Override
    public void consume(MessageTree tree) {
        String domain = tree.getDomain();
        String ip = tree.getIpAddress();

        if (!m_blackListManager.isBlack(domain, ip)) {// 全局黑名單 按domain-ip
            long timestamp = tree.getMessage().getTimestamp();
            //PeriodManager用來管理、啓動periodTask,可以理解爲每小時的解析器。
            Period period = m_periodManager.findPeriod(timestamp);//根據消息產生的時間,查找這個小時所屬的對應Period

            if (period != null) {
                period.distribute(tree);//將解碼後的tree消息依次分發給所有類型解析器
            } else {
                m_serverStateManager.addNetworkTimeError(1);
            }
        } else {
            m_black++;

            if (m_black % CatConstants.SUCCESS_COUNT == 0) {
                Cat.logEvent("Discard", domain);
            }
        }
    }

分發消息給各個解析器(類似向訂閱者發佈消息)
void com.dianping.cat.analysis.Period.distribute(MessageTree tree)

    /**
     * 將解碼後的tree消息依次分發給所有類型解析器
     * @param tree
     */
    public void distribute(MessageTree tree) {
        m_serverStateManager.addMessageTotal(tree.getDomain(), 1);// 根據domain,統計消息量
        boolean success = true;
        String domain = tree.getDomain();

        for (Entry<String, List<PeriodTask>> entry : m_tasks.entrySet()) {
            List<PeriodTask> tasks = entry.getValue();//某種類型報表的解析器
            int length = tasks.size();
            int index = 0;
            boolean manyTasks = length > 1;

            if (manyTasks) {
                index = Math.abs(domain.hashCode()) % length;//hashCode的絕對值 % 長度 =0~length-1之間的任一個數
            }
            PeriodTask task = tasks.get(index);
            boolean enqueue = task.enqueue(tree);//注意:這裏會把同一個消息依依放入各個報表解析中的隊列中

            if (enqueue == false) {
                if (manyTasks) {
                    task = tasks.get((index + 1) % length);
                    enqueue = task.enqueue(tree);//放入隊列,異步消費

                    if (enqueue == false) {
                        success = false;
                    }
                } else {
                    success = false;
                }
            }
        }

        if (!success) {
            m_serverStateManager.addMessageTotalLoss(tree.getDomain(), 1);
        }
    }

**PeriodTask **
每個periodTask對應一個線程,m_analyzer對應解析器處理m_queue中的消息

public class PeriodTask implements Task, LogEnabled {
	@Override
	public void run() {//每個periodTask對應一個線程,m_analyzer對應解析器處理m_queue中的消息
		try {
			m_analyzer.analyze(m_queue);
		} catch (Exception e) {
			Cat.logError(e);
		}
	}

AbstractMessageAnalyzer
這裏寫圖片描述

	@Override
    public void analyze(MessageQueue queue) {// 解析器在當前小時內自旋,不停從隊列中拿取消息,然後處理
        while (!isTimeout() && isActive()) {// timeOut:當前時間>小時的開始時間+一小時+三分鐘;
                                            // isActive默認爲true,調用shutdown後爲false
            MessageTree tree = queue.poll();// 非阻塞式獲取消息

            if (tree != null) {
                try {
                    process(tree);// 解析器實現類 override
                } catch (Throwable e) {
                    m_errors++;

                    if (m_errors == 1 || m_errors % 10000 == 0) {
                        Cat.logError(e);
                    }
                }
            }
        }
        // 如果當前解析器以及超時,那麼處理完對應隊列內的消息後返回。
        while (true) {
            MessageTree tree = queue.poll();

            if (tree != null) {
                try {
                    process(tree);
                } catch (Throwable e) {
                    m_errors++;

                    if (m_errors == 1 || m_errors % 10000 == 0) {
                        Cat.logError(e);
                    }
                }
            } else {
                break;
            }
        }
    }

這裏寫圖片描述

所以我們可以看到:
消息發送到服務端,服務端解碼爲 MessageTree準備消費。期間存在一個demon線程,1s檢查一下當前小時的Period對象是否需要創建(一般都是新的小時需要創建一個Period代表當前小時)。

如果當前小時的Period存在,那麼我們的MessageTree會被分發給各個PeriodTask,這裏其實就是把消息發送到每個PeriodTask中的內存隊列裏,然後每個Task異步去消費。就是通過使用Queue實現瞭解耦與延遲異步消費。

每個PeriodTask持有MessageAnalyzer analyzer(Transaction\Event\Problean…每種報表都對應一個解析器的實現類)、MessageQueue queue對象,PeriodTask會不停地解析被分發進來的MessageTree,形成這個解析器所代表的報表。

當前時間進入下個小時,會創建一個新的當前小時的Period,並且異步的remove之前的Period。

注意,這裏有個比較坑的地方是,作者沒有使用線程池,每小時各個解析器的線程並沒有池化,而是直接銷燬後再次創建!

展示

對於實時報表,直接通過HTTP請求分發到相應消費機,待結果返回後聚合展示(對分區數據做聚合);歷史報表則直接取數據庫並展示。

存儲

存儲主要分成兩類:一個是 報表(Transaction、Event、Problem…),一個是logview,也是就是原始的MessageTree。

所有原始消息會先存儲在本地文件系統,然後上傳到HDFS中保存;而對於報表,因其遠比原始日誌小,則以K/V的方式保存在MySQL中。

報表存儲:在每個小時結束後,將內存中的各個XML報表 保存到Mysql、File(\data\appdatas\cat\bucket\report…)中。

這裏寫圖片描述

logView的保存有後臺線程(默認20個,Daemon Thread [cat-Message-Gzip-n])輪詢處理:會間隔一段時間後從消息隊列中拿取MessageTree,並進行編碼壓縮,保存到\data\appdatas\cat\bucket\dump\年月\日\domain-ip1-ip2-ipn目錄下。

com.dianping.cat.consumer.dump.LocalMessageBucketManager.MessageGzip.run()

	@Override
		public void run() {
			try {
				while (true) {
					MessageItem item = m_messageQueue.poll(5, TimeUnit.MILLISECONDS);

					if (item != null) {
						m_count++;
						if (m_count % (10000) == 0) {
							gzipMessageWithMonitor(item);//數量達到10000的整數倍,通過上報埋點記錄監控一下
						} else {
							gzipMessage(item);
						}
					}
				}
			} catch (InterruptedException e) {
				// ignore it
			}
		}


private void gzipMessage(MessageItem item) {
			try {
				MessageId id = item.getMessageId();
				String name = id.getDomain() + '-' + id.getIpAddress() + '-' + m_localIp;
				String path = m_pathBuilder.getLogviewPath(new Date(id.getTimestamp()), name);
				LocalMessageBucket bucket = m_buckets.get(path);

				if (bucket == null) {
					synchronized (m_buckets) {
						bucket = m_buckets.get(path);
						if (bucket == null) {
							bucket = (LocalMessageBucket) lookup(MessageBucket.class, LocalMessageBucket.ID);
							bucket.setBaseDir(m_baseDir);
							bucket.initialize(path);

							m_buckets.put(path, bucket);
						}
					}
				}

				DefaultMessageTree tree = (DefaultMessageTree) item.getTree();
				ByteBuf buf = tree.getBuffer();
				MessageBlock block = bucket.storeMessage(buf, id);

				if (block != null) {
					if (!m_messageBlocks.offer(block)) {
						m_serverStateManager.addBlockLoss(1);
						Cat.logEvent("DumpError", tree.getDomain());
					}
				}
			} catch (Throwable e) {
				Cat.logError(e);
			}
		}

	public MessageBlock storeMessage(final ByteBuf buf, final MessageId id) throws IOException {
		synchronized (this) {
			int size = buf.readableBytes();

			m_dirty.set(true);
			m_lastAccessTime = System.currentTimeMillis();
			m_blockSize += size;
			m_block.addIndex(id.getIndex(), size);
			buf.getBytes(0, m_out, size); // write buffer and compress it

			if (m_blockSize >= MAX_BLOCK_SIZE) {
				return flushBlock();
			} else {
				return null;
			}
		}
	}

logView的文件存儲設計

這裏寫圖片描述

接下來,會介紹CAT中出現的一些經典的設計、算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章