源码分析 RocketMQ DLedger(多副本) 之日志复制-下篇

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 源码分析 RocketMQ DLedger(多副本) 之日志复制-下篇

3、EntryHandler 详解


EntryHandler 同样是一个线程,当节点状态为从节点时激活。


3.1 核心类图



3a4bce11a27ffaacf41ad40e670ecd49.png

其核心属性如下:


  • long lastCheckFastForwardTimeMs
    上一次检查主服务器是否有 push 消息的时间戳。
  • ConcurrentMap>> writeRequestMap
    append 请求处理队列。
  • BlockingQueue>> compareOrTruncateRequests
    COMMIT、COMPARE、TRUNCATE 相关请求


3.2 handlePush


从上文得知,主节点会主动向从节点传播日志,从节点会通过网络接受到请求数据进行处理,其调用链如图所示:

28f53ec9c6d35821297f180327aaa777.jpg


最终会调用 EntryHandler 的 handlePush 方法。


EntryHandler#handlePush

1public CompletableFuture<PushEntryResponse> handlePush(PushEntryRequest request) throws Exception {
 2    //The timeout should smaller than the remoting layer's request timeout
 3    CompletableFuture<PushEntryResponse> future = new TimeoutFuture<>(1000);      // @1
 4    switch (request.getType()) {
 5        case APPEND:                                                                                                          // @2
 6            PreConditions.check(request.getEntry() != null, DLedgerResponseCode.UNEXPECTED_ARGUMENT);
 7            long index = request.getEntry().getIndex();
 8            Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> old = writeRequestMap.putIfAbsent(index, new Pair<>(request, future));
 9            if (old != null) {
10                logger.warn("[MONITOR]The index {} has already existed with {} and curr is {}", index, old.getKey().baseInfo(), request.baseInfo());
11                future.complete(buildResponse(request, DLedgerResponseCode.REPEATED_PUSH.getCode()));
12            }
13            break;
14        case COMMIT:                                                                                                           // @3
15            compareOrTruncateRequests.put(new Pair<>(request, future));
16            break;
17        case COMPARE:
18        case TRUNCATE:                                                                                                     // @4
19            PreConditions.check(request.getEntry() != null, DLedgerResponseCode.UNEXPECTED_ARGUMENT);
20            writeRequestMap.clear();
21            compareOrTruncateRequests.put(new Pair<>(request, future));
22            break;
23        default:
24            logger.error("[BUG]Unknown type {} from {}", request.getType(), request.baseInfo());
25            future.complete(buildResponse(request, DLedgerResponseCode.UNEXPECTED_ARGUMENT.getCode()));
26            break;
27    }
28    return future;
29}

从几点处理主节点的 push 请求,其实现关键点如下。


代码@1:首先构建一个响应结果Future,默认超时时间 1s。


代码@2:如果是 APPEND 请求,放入到 writeRequestMap 集合中,如果已存在该数据结构,说明主节点重复推送,构建返回结果,其状态码为 REPEATED_PUSH。放入到 writeRequestMap 中,由 doWork 方法定时去处理待写入的请求。


代码@3:如果是提交请求, 将请求存入 compareOrTruncateRequests 请求处理中,由 doWork 方法异步处理。


代码@4:如果是 COMPARE 或 TRUNCATE 请求,将待写入队列 writeRequestMap  清空,并将请求放入 compareOrTruncateRequests 请求队列中,由 doWork 方法异步处理。


接下来,我们重点来分析 doWork 方法的实现。


3.3 doWork 方法详解


EntryHandler#doWork

1public void doWork() {
 2    try {
 3        if (!memberState.isFollower()) {     // @1
 4            waitForRunning(1);
 5            return;
 6        }
 7        if (compareOrTruncateRequests.peek() != null) {    // @2
 8            Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = compareOrTruncateRequests.poll();
 9            PreConditions.check(pair != null, DLedgerResponseCode.UNKNOWN);
10            switch (pair.getKey().getType()) {
11                case TRUNCATE:
12                    handleDoTruncate(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());
13                    break;
14                case COMPARE:
15                    handleDoCompare(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());
16                    break;
17                case COMMIT:
18                    handleDoCommit(pair.getKey().getCommitIndex(), pair.getKey(), pair.getValue());
19                    break;
20                default:
21                    break;
22            }
23        } else { // @3
24            long nextIndex = dLedgerStore.getLedgerEndIndex() + 1;
25            Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = writeRequestMap.remove(nextIndex);
26            if (pair == null) {
27                checkAbnormalFuture(dLedgerStore.getLedgerEndIndex());
28                waitForRunning(1);
29                return;
30            }
31            PushEntryRequest request = pair.getKey();
32            handleDoAppend(nextIndex, request, pair.getValue());
33        }
34    } catch (Throwable t) {
35        DLedgerEntryPusher.logger.error("Error in {}", getName(), t);
36        DLedgerUtils.sleep(100);
37    }
38}

代码@1:如果当前节点的状态不是从节点,则跳出。


代码@2:如果 compareOrTruncateRequests 队列不为空,说明有COMMIT、COMPARE、TRUNCATE 等请求,这类请求优先处理。值得注意的是这里使用是 peek、poll 等非阻塞方法,然后根据请求的类型,调用对应的方法。稍后详细介绍。


代码@3:如果只有 append 类请求,则根据当前节点最大的消息序号,尝试从 writeRequestMap 容器中,获取下一个消息复制请求(ledgerEndIndex + 1) 为 key 去查找。如果不为空,则执行 doAppend 请求,如果为空,则调用 checkAbnormalFuture 来处理异常情况。


接下来我们来重点分析各个处理细节。


3.3.1 handleDoCommit


处理提交请求,其处理比较简单,就是调用 DLedgerStore 的 updateCommittedIndex 更新其已提交偏移量,故我们还是具体看一下DLedgerStore 的 updateCommittedIndex 方法。


DLedgerMmapFileStore#updateCommittedIndex

1public void updateCommittedIndex(long term, long newCommittedIndex) {   // @1
 2    if (newCommittedIndex == -1
 3            || ledgerEndIndex == -1
 4            || term < memberState.currTerm()
 5            || newCommittedIndex == this.committedIndex) {                               // @2
 6            return;
 7    }
 8    if (newCommittedIndex < this.committedIndex
 9            || newCommittedIndex < this.ledgerBeginIndex) {                             // @3
10        logger.warn("[MONITOR]Skip update committed index for new={} < old={} or new={} < beginIndex={}", newCommittedIndex, this.committedIndex, newCommittedIndex, this.ledgerBeginIndex);
11        return;
12    }
13    long endIndex = ledgerEndIndex;
14    if (newCommittedIndex > endIndex) {                                                       // @4
15            //If the node fall behind too much, the committedIndex will be larger than enIndex.
16        newCommittedIndex = endIndex;
17    }
18    DLedgerEntry dLedgerEntry = get(newCommittedIndex);                        // @5                
19    PreConditions.check(dLedgerEntry != null, DLedgerResponseCode.DISK_ERROR);
20    this.committedIndex = newCommittedIndex;
21    this.committedPos = dLedgerEntry.getPos() + dLedgerEntry.getSize();     // @6
22}

代码@1:首先介绍一下方法的参数:


  • long term
    主节点当前的投票轮次。
  • long newCommittedIndex:
    主节点发送日志复制请求时的已提交日志序号。


代码@2:如果待更新提交序号为 -1 或 投票轮次小于从节点的投票轮次或主节点投票轮次等于从节点的已提交序号,则直接忽略本次提交动作。


代码@3:如果主节点的已提交日志序号小于从节点的已提交日志序号或待提交序号小于当前节点的最小有效日志序号,则输出警告日志[MONITOR],并忽略本次提交动作。


代码@4:如果从节点落后主节点太多,则重置 提交索引为从节点当前最大有效日志序号。


代码@5:尝试根据待提交序号从从节点查找数据,如果数据不存在,则抛出 DISK_ERROR 错误。


代码@6:更新 commitedIndex、committedPos 两个指针,DledgerStore会定时将已提交指针刷入 checkpoint 文件,达到持久化 commitedIndex 指针的目的。


3.3.2 handleDoCompare


处理主节点发送过来的 COMPARE 请求,其实现也比较简单,最终调用 buildResponse 方法构造响应结果。


EntryHandler#buildResponse

1private PushEntryResponse buildResponse(PushEntryRequest request, int code) {
 2    PushEntryResponse response = new PushEntryResponse();
 3    response.setGroup(request.getGroup());
 4    response.setCode(code);
 5    response.setTerm(request.getTerm());
 6    if (request.getType() != PushEntryRequest.Type.COMMIT) {
 7        response.setIndex(request.getEntry().getIndex());
 8    }
 9    response.setBeginIndex(dLedgerStore.getLedgerBeginIndex());
10    response.setEndIndex(dLedgerStore.getLedgerEndIndex());
11    return response;
12}

主要也是返回当前从几点的 ledgerBeginIndex、ledgerEndIndex 以及投票轮次,供主节点进行判断比较。


3.3.3 handleDoTruncate


handleDoTruncate 方法实现比较简单,删除从节点上 truncateIndex 日志序号之后的所有日志,具体调用dLedgerStore 的 truncate 方法,由于其存储与 RocketMQ 的存储设计基本类似故本文就不在详细介绍,简单介绍其实现要点:根据日志序号,去定位到日志文件,如果命中具体的文件,则修改相应的读写指针、刷盘指针等,并将所在在物理文件之后的所有文件删除。大家如有兴趣,可以查阅笔者的《RocketMQ技术内幕》第4章:RocketMQ 存储相关内容。


3.3.4 handleDoAppend


1private void handleDoAppend(long writeIndex, PushEntryRequest request,
 2    CompletableFuture<PushEntryResponse> future) {
 3    try {
 4        PreConditions.check(writeIndex == request.getEntry().getIndex(), DLedgerResponseCode.INCONSISTENT_STATE);
 5        DLedgerEntry entry = dLedgerStore.appendAsFollower(request.getEntry(), request.getTerm(), request.getLeaderId());
 6        PreConditions.check(entry.getIndex() == writeIndex, DLedgerResponseCode.INCONSISTENT_STATE);
 7        future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));
 8        dLedgerStore.updateCommittedIndex(request.getTerm(), request.getCommitIndex());
 9    } catch (Throwable t) {
10        logger.error("[HandleDoWrite] writeIndex={}", writeIndex, t);
11        future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
12    }
13}

其实现也比较简单,调用DLedgerStore 的 appendAsFollower 方法进行日志的追加,与appendAsLeader 在日志存储部分相同,只是从节点无需再转发日志。


3.3.5 checkAbnormalFuture


该方法是本节的重点,doWork 的从服务器存储的最大有效日志序号(ledgerEndIndex) + 1 序号,尝试从待写请求中获取不到对应的请求时调用,这种情况也很常见,例如主节点并么有将最新的数据 PUSH 给从节点。接下来我们详细来看看该方法的实现细节。


EntryHandler#checkAbnormalFuture

1if (DLedgerUtils.elapsed(lastCheckFastForwardTimeMs) < 1000) {
2    return;
3}
4lastCheckFastForwardTimeMs  = System.currentTimeMillis();
5if (writeRequestMap.isEmpty()) {
6    return;
7}

Step1:如果上一次检查的时间距现在不到1s,则跳出;如果当前没有积压的append请求,同样跳出,因为可以同样明确的判断出主节点还未推送日志。


EntryHandler#checkAbnormalFuture

1for (Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair : writeRequestMap.values()) {
 2    long index = pair.getKey().getEntry().getIndex();             // @1
 3    //Fall behind
 4    if (index <= endIndex) {                                                   // @2
 5        try {
 6            DLedgerEntry local = dLedgerStore.get(index);
 7            PreConditions.check(pair.getKey().getEntry().equals(local), DLedgerResponseCode.INCONSISTENT_STATE);
 8            pair.getValue().complete(buildResponse(pair.getKey(), DLedgerResponseCode.SUCCESS.getCode()));
 9            logger.warn("[PushFallBehind]The leader pushed an entry index={} smaller than current ledgerEndIndex={}, maybe the last ack is missed", index, endIndex);
10        } catch (Throwable t) {
11            logger.error("[PushFallBehind]The leader pushed an entry index={} smaller than current ledgerEndIndex={}, maybe the last ack is missed", index, endIndex, t);
12            pair.getValue().complete(buildResponse(pair.getKey(), DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
13        }
14        writeRequestMap.remove(index);
15        continue;
16    }
17    //Just OK
18    if (index ==  endIndex + 1) {    // @3
19        //The next entry is coming, just return
20        return;
21    }
22    //Fast forward
23    TimeoutFuture<PushEntryResponse> future  = (TimeoutFuture<PushEntryResponse>) pair.getValue();    // @4
24    if (!future.isTimeOut()) {
25        continue;
26    }
27    if (index < minFastForwardIndex) {                                                                                                                // @5
28        minFastForwardIndex = index;
29    }
30}

Step2:遍历当前待写入的日志追加请求(主服务器推送过来的日志复制请求),找到需要快速快进的的索引。其关键实现点如下:


  • 代码@1:首先获取待写入日志的序号。
  • 代码@2:如果待写入的日志序号小于从节点已追加的日志(endIndex),并且日志的确已存储在从节点,则返回成功,并输出警告日志【PushFallBehind】,继续监测下一条待写入日志。
  • 代码@3:如果待写入 index 等于 endIndex + 1,则结束循环,因为下一条日志消息已经在待写入队列中,即将写入。
  • 代码@4:如果待写入 index 大于 endIndex + 1,并且未超时,则直接检查下一条待写入日志。
  • 代码@5:如果待写入 index 大于 endIndex + 1,并且已经超时,则记录该索引,使用 minFastForwardIndex 存储。


EntryHandler#checkAbnormalFuture

1if (minFastForwardIndex == Long.MAX_VALUE) {
2    return;
3}
4Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = writeRequestMap.get(minFastForwardIndex);
5if (pair == null) {
6    return;
7}

Step3:如果未找到需要快速失败的日志序号或 writeRequestMap 中未找到其请求,则直接结束检测。


EntryHandler#checkAbnormalFuture

1logger.warn("[PushFastForward] ledgerEndIndex={} entryIndex={}", endIndex, minFastForwardIndex);
2pair.getValue().complete(buildResponse(pair.getKey(), DLedgerResponseCode.INCONSISTENT_STATE.getCode()));

Step4:则向主节点报告从节点已经与主节点发生了数据不一致,从节点并没有写入序号 minFastForwardIndex 的日志。如果主节点收到此种响应,将会停止日志转发,转而向各个从节点发送 COMPARE 请求,从而使数据恢复一致。


行为至此,已经详细介绍了主服务器向从服务器发送请求,从服务做出响应,那接下来就来看一下,服务端收到响应结果后的处理,我们要知道主节点会向它所有的从节点传播日志,主节点需要在指定时间内收到超过集群一半节点的确认,才能认为日志写入成功,那我们接下来看一下其实现过程。


4、QuorumAckChecker


日志复制投票器,一个日志写请求只有得到集群内的的大多数节点的响应,日志才会被提交。


4.1 类图



bde391dc11dd58a252eaeab16bfee3ac.png

其核心属性如下:


  • long lastPrintWatermarkTimeMs
    上次打印水位线的时间戳,单位为毫秒。
  • long lastCheckLeakTimeMs
    上次检测泄漏的时间戳,单位为毫秒。
  • long lastQuorumIndex
    已投票仲裁的日志序号。


4.2 doWork 详解


QuorumAckChecker#doWork

1if (DLedgerUtils.elapsed(lastPrintWatermarkTimeMs) > 3000) {    
2    logger.info("[{}][{}] term={} ledgerBegin={} ledgerEnd={} committed={} watermarks={}",
3            memberState.getSelfId(), memberState.getRole(), memberState.currTerm(), dLedgerStore.getLedgerBeginIndex(), dLedgerStore.getLedgerEndIndex(), dLedgerStore.getCommittedIndex(), JSON.toJSONString(peerWaterMarksByTerm));
4    lastPrintWatermarkTimeMs = System.currentTimeMillis();
5}

Step1:如果离上一次打印 watermak 的时间超过3s,则打印一下当前的 term、ledgerBegin、ledgerEnd、committed、peerWaterMarksByTerm 这些数据日志。


QuorumAckChecker#doWork

1if (!memberState.isLeader()) {   // @2
2    waitForRunning(1);
3    return;
4}

Step2:如果当前节点不是主节点,直接返回,不作为。


QuorumAckChecker#doWork

1if (pendingAppendResponsesByTerm.size() > 1) {   // @1
 2    for (Long term : pendingAppendResponsesByTerm.keySet()) {
 3        if (term == currTerm) {
 4            continue;
 5        }
 6        for (Map.Entry<Long, TimeoutFuture<AppendEntryResponse>> futureEntry : pendingAppendResponsesByTerm.get(term).entrySet()) {
 7            AppendEntryResponse response = new AppendEntryResponse();
 8            response.setGroup(memberState.getGroup());
 9            response.setIndex(futureEntry.getKey());
10            response.setCode(DLedgerResponseCode.TERM_CHANGED.getCode());
11            response.setLeaderId(memberState.getLeaderId());
12            logger.info("[TermChange] Will clear the pending response index={} for term changed from {} to {}", futureEntry.getKey(), term, currTerm);
13            futureEntry.getValue().complete(response);
14        }
15        pendingAppendResponsesByTerm.remove(term);
16    }
17}
18if (peerWaterMarksByTerm.size() > 1) {
19    for (Long term : peerWaterMarksByTerm.keySet()) {
20        if (term == currTerm) {
21            continue;
22        }
23        logger.info("[TermChange] Will clear the watermarks for term changed from {} to {}", term, currTerm);
24        peerWaterMarksByTerm.remove(term);
25    }
26}

Step3:清理pendingAppendResponsesByTerm、peerWaterMarksByTerm 中本次投票轮次的数据,避免一些不必要的内存使用。

1Map<String, Long> peerWaterMarks = peerWaterMarksByTerm.get(currTerm);
 2long quorumIndex = -1;
 3for (Long index : peerWaterMarks.values()) {  // @1
 4    int num = 0;
 5    for (Long another : peerWaterMarks.values()) {  // @2
 6        if (another >= index) {
 7            num++;
 8        }
 9    }
10    if (memberState.isQuorum(num) && index > quorumIndex) {  // @3
11        quorumIndex = index;
12    }
13}
14dLedgerStore.updateCommittedIndex(currTerm, quorumIndex);  // @4

Step4:根据各个从节点反馈的进度,进行仲裁,确定已提交序号。为了加深对这段代码的理解,再来啰嗦一下 peerWaterMarks 的作用,存储的是各个从节点当前已成功追加的日志序号。例如一个三节点的 DLedger 集群,peerWaterMarks 数据存储大概如下:

1{
2“dledger_group_01_0” : 100,
3"dledger_group_01_1" : 101,
4}

其中 dledger_group_01_0 为从节点1的ID,当前已复制的序号为 100,而 dledger_group_01_1 为节点2的ID,当前已复制的序号为 101。再加上主节点,如何确定可提交序号呢?


  • 代码@1:首先遍历 peerWaterMarks 的 value 集合,即上述示例中的 {100, 101},用临时变量 index 来表示待投票的日志序号,需要集群内超过半数的节点的已复制序号超过该值,则该日志能被确认提交。


  • 代码@2:遍历 peerWaterMarks 中的所有已提交序号,与当前值进行比较,如果节点的已提交序号大于等于待投票的日志序号(index),num 加一,表示投赞成票。


  • 代码@3:对 index 进行仲裁,如果超过半数 并且 index 大于 quorumIndex,更新 quorumIndex 的值为 index。quorumIndex 经过遍历的,得出当前最大的可提交日志序号。


  • 代码@4:更新 committedIndex 索引,方便 DLedgerStore 定时将 committedIndex 写入 checkpoint 中。
1ConcurrentMap<Long, TimeoutFuture<AppendEntryResponse>> responses = pendingAppendResponsesByTerm.get(currTerm);
 2boolean needCheck = false;
 3int ackNum = 0;
 4if (quorumIndex >= 0) {
 5    for (Long i = quorumIndex; i >= 0; i--) {  // @1
 6        try {
 7            CompletableFuture<AppendEntryResponse> future = responses.remove(i);   // @2
 8            if (future == null) {                                                                                              // @3
 9                needCheck = lastQuorumIndex != -1 && lastQuorumIndex != quorumIndex && i != lastQuorumIndex;
10                break;
11            } else if (!future.isDone()) {                                                                                // @4
12                AppendEntryResponse response = new AppendEntryResponse();
13                response.setGroup(memberState.getGroup());
14                response.setTerm(currTerm);
15                response.setIndex(i);
16                response.setLeaderId(memberState.getSelfId());
17                response.setPos(((AppendFuture) future).getPos());
18                future.complete(response);
19            }
20            ackNum++;                                                                                                      // @5
21        } catch (Throwable t) {
22            logger.error("Error in ack to index={} term={}", i, currTerm, t);
23        }
24    }
25}

Step5:处理 quorumIndex 之前的挂起请求,需要发送响应到客户端,其实现步骤:


  • 代码@1:从 quorumIndex 开始处理,没处理一条,该序号减一,直到大于0或主动退出,请看后面的退出逻辑。
  • 代码@2:responses 中移除该日志条目的挂起请求。
  • 代码@3:如果未找到挂起请求,说明前面挂起的请求已经全部处理完毕,准备退出,退出之前再 设置 needCheck 的值,其依据如下(三个条件必须同时满足):
  • 最后一次仲裁的日志序号不等于-1
  • 并且最后一次不等于本次新仲裁的日志序号
  • 最后一次仲裁的日志序号不等于最后一次仲裁的日志。正常情况一下,条件一、条件二通常为true,但这一条大概率会返回false。
  • 代码@4:向客户端返回结果。
  • 代码@5:ackNum,表示本次确认的数量。
1if (ackNum == 0) {
 2    for (long i = quorumIndex + 1; i < Integer.MAX_VALUE; i++) {
 3        TimeoutFuture<AppendEntryResponse> future = responses.get(i);
 4        if (future == null) {
 5            break;
 6        } else if (future.isTimeOut()) {
 7            AppendEntryResponse response = new AppendEntryResponse();
 8            response.setGroup(memberState.getGroup());
 9            response.setCode(DLedgerResponseCode.WAIT_QUORUM_ACK_TIMEOUT.getCode());
10            response.setTerm(currTerm);
11            response.setIndex(i);
12            response.setLeaderId(memberState.getSelfId());
13            future.complete(response);
14        } else {
15            break;
16        }
17    }
18    waitForRunning(1);
19}

Step6:如果本次确认的个数为0,则尝试去判断超过该仲裁序号的请求,是否已经超时,如果已超时,则返回超时响应结果。

1if (DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000 || needCheck) {
 2    updatePeerWaterMark(currTerm, memberState.getSelfId(), dLedgerStore.getLedgerEndIndex());
 3    for (Map.Entry<Long, TimeoutFuture<AppendEntryResponse>> futureEntry : responses.entrySet()) {
 4        if (futureEntry.getKey() < quorumIndex) {
 5            AppendEntryResponse response = new AppendEntryResponse();
 6            response.setGroup(memberState.getGroup());
 7            response.setTerm(currTerm);
 8            response.setIndex(futureEntry.getKey());
 9            response.setLeaderId(memberState.getSelfId());
10            response.setPos(((AppendFuture) futureEntry.getValue()).getPos());
11            futureEntry.getValue().complete(response);
12            responses.remove(futureEntry.getKey());
13        }
14    }
15    lastCheckLeakTimeMs = System.currentTimeMillis();
16}

Step7:检查是否发送泄漏。其判断泄漏的依据是如果挂起的请求的日志序号小于已提交的序号,则移除。


Step8:一次日志仲裁就结束了,最后更新 lastQuorumIndex 为本次仲裁的的新的提交值。


关于 DLedger 的日志复制部分就介绍到这里了。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
消息中间件 JSON 数据格式
查看rabbitmq日志,Rabbitmq Trace日志
查看rabbitmq日志,Rabbitmq Trace日志
495 2
|
消息中间件 存储 Shell
Docker部署RabbitMQ配置日志映射本地文件
Docker部署RabbitMQ配置日志映射本地文件
380 0
|
消息中间件 RocketMQ
rocketMq错误日志所在位置
rocketMq错误日志所在位置
184 0
|
6月前
|
消息中间件 开发工具 RocketMQ
消息队列 MQ产品使用合集之如何关闭客户端的日志记录
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
7月前
|
消息中间件 存储 RocketMQ
RocketMQ源码分析之事务消息实现原理下篇-消息服务器Broker提交回滚事务实现原理
RocketMQ源码分析之事务消息实现原理下篇-消息服务器Broker提交回滚事务实现原理
|
4月前
|
消息中间件 Java 调度
"解锁RabbitMQ云版:揭秘电商巨头、日志大师、任务狂人的秘密武器,你的系统升级就差这一步!"
【8月更文挑战第14天】在分布式与微服务架构中,RabbitMQ云版本作为消息队列服务,助力系统间解耦与异步通信。通过三个场景展示其实用性:1) 订单处理系统中,利用RabbitMQ实现跨服务流程的解耦;2) 日志收集与分析,异步发送日志至中央系统,保障业务流畅;3) 任务调度,处理耗时任务避免阻塞主线程。这些应用充分展现了RabbitMQ云版本的强大功能和灵活性。
38 0
|
7月前
|
消息中间件 Java RocketMQ
修改rocketmq的日志文件位置
修改rocketmq的日志文件位置
167 0
修改rocketmq的日志文件位置
|
7月前
|
消息中间件 存储 数据库
RocketMQ如何实现日志收集
RocketMQ如何实现日志收集
272 0
|
消息中间件 算法 Java
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!
778 1
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!