Sentinel學習(六) —— 控制檯和客戶端通信原理

控制檯

控制檯主要的處理類是 FlowControllerV1

@RestController
@RequestMapping(value = "/v1/flow")
public class FlowControllerV1 {

    private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class);

    @Autowired
    private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;

    @Autowired
    private SentinelApiClient sentinelApiClient;

    @GetMapping("/rules")
    @AuthAction(PrivilegeType.READ_RULE)
    public Result<List<FlowRuleEntity>> apiQueryMachineRules(@RequestParam String app,
                                                             @RequestParam String ip,
                                                             @RequestParam Integer port) {

        if (StringUtil.isEmpty(app)) {
            return Result.ofFail(-1, "app can't be null or empty");
        }
        if (StringUtil.isEmpty(ip)) {
            return Result.ofFail(-1, "ip can't be null or empty");
        }
        if (port == null) {
            return Result.ofFail(-1, "port can't be null");
        }
        try {
            List<FlowRuleEntity> rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port);
            rules = repository.saveAll(rules);
            return Result.ofSuccess(rules);
        } catch (Throwable throwable) {
            logger.error("Error when querying flow rules", throwable);
            return Result.ofThrowable(-1, throwable);
        }
    }

    private <R> Result<R> checkEntityInternal(FlowRuleEntity entity) {
        if (StringUtil.isBlank(entity.getApp())) {
            return Result.ofFail(-1, "app can't be null or empty");
        }
        if (StringUtil.isBlank(entity.getIp())) {
            return Result.ofFail(-1, "ip can't be null or empty");
        }
        if (entity.getPort() == null) {
            return Result.ofFail(-1, "port can't be null");
        }
        if (StringUtil.isBlank(entity.getLimitApp())) {
            return Result.ofFail(-1, "limitApp can't be null or empty");
        }
        if (StringUtil.isBlank(entity.getResource())) {
            return Result.ofFail(-1, "resource can't be null or empty");
        }
        if (entity.getGrade() == null) {
            return Result.ofFail(-1, "grade can't be null");
        }
        if (entity.getGrade() != 0 && entity.getGrade() != 1) {
            return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got");
        }
        if (entity.getCount() == null || entity.getCount() < 0) {
            return Result.ofFail(-1, "count should be at lease zero");
        }
        if (entity.getStrategy() == null) {
            return Result.ofFail(-1, "strategy can't be null");
        }
        if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) {
            return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
        }
        if (entity.getControlBehavior() == null) {
            return Result.ofFail(-1, "controlBehavior can't be null");
        }
        int controlBehavior = entity.getControlBehavior();
        if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) {
            return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
        }
        if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) {
            return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
        }
        if (entity.isClusterMode() && entity.getClusterConfig() == null) {
            return Result.ofFail(-1, "cluster config should be valid");
        }
        return null;
    }

    @PostMapping("/rule")
    @AuthAction(PrivilegeType.WRITE_RULE)
    public Result<FlowRuleEntity> apiAddFlowRule(@RequestBody FlowRuleEntity entity) {
        Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
        if (checkResult != null) {
            return checkResult;
        }
        entity.setId(null);
        Date date = new Date();
        entity.setGmtCreate(date);
        entity.setGmtModified(date);
        entity.setLimitApp(entity.getLimitApp().trim());
        entity.setResource(entity.getResource().trim());
        try {
            entity = repository.save(entity);

            publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS);
            return Result.ofSuccess(entity);
        } catch (Throwable t) {
            Throwable e = t instanceof ExecutionException ? t.getCause() : t;
            logger.error("Failed to add new flow rule, app={}, ip={}", entity.getApp(), entity.getIp(), e);
            return Result.ofFail(-1, e.getMessage());
        }
    }

    @PutMapping("/save.json")
    @AuthAction(PrivilegeType.WRITE_RULE)
    public Result<FlowRuleEntity> apiUpdateFlowRule(Long id, String app,
                                                  String limitApp, String resource, Integer grade,
                                                  Double count, Integer strategy, String refResource,
                                                  Integer controlBehavior, Integer warmUpPeriodSec,
                                                  Integer maxQueueingTimeMs) {
        if (id == null) {
            return Result.ofFail(-1, "id can't be null");
        }
        FlowRuleEntity entity = repository.findById(id);
        if (entity == null) {
            return Result.ofFail(-1, "id " + id + " dose not exist");
        }
        if (StringUtil.isNotBlank(app)) {
            entity.setApp(app.trim());
        }
        if (StringUtil.isNotBlank(limitApp)) {
            entity.setLimitApp(limitApp.trim());
        }
        if (StringUtil.isNotBlank(resource)) {
            entity.setResource(resource.trim());
        }
        if (grade != null) {
            if (grade != 0 && grade != 1) {
                return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got");
            }
            entity.setGrade(grade);
        }
        if (count != null) {
            entity.setCount(count);
        }
        if (strategy != null) {
            if (strategy != 0 && strategy != 1 && strategy != 2) {
                return Result.ofFail(-1, "strategy must be in [0, 1, 2], but " + strategy + " got");
            }
            entity.setStrategy(strategy);
            if (strategy != 0) {
                if (StringUtil.isBlank(refResource)) {
                    return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
                }
                entity.setRefResource(refResource.trim());
            }
        }
        if (controlBehavior != null) {
            if (controlBehavior != 0 && controlBehavior != 1 && controlBehavior != 2) {
                return Result.ofFail(-1, "controlBehavior must be in [0, 1, 2], but " + controlBehavior + " got");
            }
            if (controlBehavior == 1 && warmUpPeriodSec == null) {
                return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
            }
            if (controlBehavior == 2 && maxQueueingTimeMs == null) {
                return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
            }
            entity.setControlBehavior(controlBehavior);
            if (warmUpPeriodSec != null) {
                entity.setWarmUpPeriodSec(warmUpPeriodSec);
            }
            if (maxQueueingTimeMs != null) {
                entity.setMaxQueueingTimeMs(maxQueueingTimeMs);
            }
        }
        Date date = new Date();
        entity.setGmtModified(date);
        try {
            entity = repository.save(entity);
            if (entity == null) {
                return Result.ofFail(-1, "save entity fail: null");
            }

            publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get(5000, TimeUnit.MILLISECONDS);
            return Result.ofSuccess(entity);
        } catch (Throwable t) {
            Throwable e = t instanceof ExecutionException ? t.getCause() : t;
            logger.error("Error when updating flow rules, app={}, ip={}, ruleId={}", entity.getApp(),
                entity.getIp(), id, e);
            return Result.ofFail(-1, e.getMessage());
        }
    }

    @DeleteMapping("/delete.json")
    @AuthAction(PrivilegeType.WRITE_RULE)
    public Result<Long> apiDeleteFlowRule(Long id) {

        if (id == null) {
            return Result.ofFail(-1, "id can't be null");
        }
        FlowRuleEntity oldEntity = repository.findById(id);
        if (oldEntity == null) {
            return Result.ofSuccess(null);
        }

        try {
            repository.delete(id);
        } catch (Exception e) {
            return Result.ofFail(-1, e.getMessage());
        }
        try {
            publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get(5000, TimeUnit.MILLISECONDS);
            return Result.ofSuccess(id);
        } catch (Throwable t) {
            Throwable e = t instanceof ExecutionException ? t.getCause() : t;
            logger.error("Error when deleting flow rules, app={}, ip={}, id={}", oldEntity.getApp(),
                oldEntity.getIp(), id, e);
            return Result.ofFail(-1, e.getMessage());
        }
    }

    private CompletableFuture<Void> publishRules(String app, String ip, Integer port) {
        List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
        return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules);
    }
}

從上面可以看出來,dashboard 是通過一個叫 SentinelApiClient 的類去指定的 ip 和 port 處獲取數據的。這個 ip 和 port 是前端頁面直接提交給後端的,而前端頁面又是通過 /app/{app}/machines.json 接口獲取機器列表的。

連接 dashboard

sentinel-core 在初始化的時候,通過 JVM 參數中指定的 dashboard 的 ip 和 port,會主動向 dashboard 發起連接的請求,該請求是通過 HeartbeatSender 接口以心跳的方式發送的,並將自己的 ip 和 port 告知 dashboard。這裏 sentinel-core 上報給 dashboard 的端口是 sentinel 對外暴露的自己的 CommandCenter 的端口。

HeartbeatSender 有兩個實現類,一個是通過 http,另一個是通過 netty,我們看 http 的實現類:

SimpleHttpHeartbeatSender.java

private final HeartbeatMessage heartBeat = new HeartbeatMessage();
private final SimpleHttpClient httpClient = new SimpleHttpClient();
@Override
public boolean sendHeartbeat() throws Exception {
    if (TransportConfig.getRuntimePort() <= 0) {
        RecordLog.info("[SimpleHttpHeartbeatSender] Runtime port not initialized, won't send heartbeat");
        return false;
    }
    InetSocketAddress addr = getAvailableAddress();
    if (addr == null) {
        return false;
    }
    SimpleHttpRequest request = new SimpleHttpRequest(addr, HEARTBEAT_PATH);
    request.setParams(heartBeat.generateCurrentMessage());
    try {
        SimpleHttpResponse response = httpClient.post(request);
        if (response.getStatusCode() == OK_STATUS) {
            return true;
        }
    } catch (Exception e) {
        RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addr + " : ", e);
    }
    return false;
}

通過一個 HttpClient 向 dashboard 發送了自己的信息,包括 ip port 和版本號等信息。

其中 consoleHost 和 consolePort 的值就是從 JVM 參數 csp.sentinel.dashboard.server 中獲取的。

dashboard 在接收到 sentinel-core 的連接之後,就會與 sentinel-core 建立連接,並將 sentinel-core 上報的 ip 和 port 的信息包裝成一個 MachineInfo 對象,然後通過 SimpleMachineDiscovery 將該對象保存在一個 map 中,如下圖所示:

在這裏插入圖片描述

請求數據

當 dashboard 有了具體的 sentinel-core 實例的 ip 和 port 之後,就可以去請求所需要的數據了。

讓我們再回到最開始的地方,我在頁面上查詢某一臺機器的限流的規則時,是將該機器的 ip 和 port 以及 appName 都傳給了服務端,服務端通過這些信息去具體的遠程實例中請求所需的數據,拿到數據後再封裝成 dashboard 所需的格式返回給前端頁面進行展示。

具體請求限流規則列表的代碼在 SentinelApiClient 中,如下所示:

SentinelApiClient.java

public List<FlowRuleEntity> fetchFlowRuleOfMachine(String app, String ip, int port) {
    String url = "http://" + ip + ":" + port + "/" + GET_RULES_PATH + "?type=" + FLOW_RULE_TYPE;
    String body = httpGetContent(url);
    logger.info("FlowRule Body:{}", body);
    List<FlowRule> rules = RuleUtils.parseFlowRule(body);
    if (rules != null) {
        return rules.stream().map(rule -> FlowRuleEntity.fromFlowRule(app, ip, port, rule))
            .collect(Collectors.toList());
    } else {
        return null;
    }
}

可以看到也是通過一個 httpClient 請求的數據,然後再對結果進行轉換,具體請求的過程是在 httpGetContent 方法中進行的,我們看下該方法,如下所示:

private String httpGetContent(String url) {
    final HttpGet httpGet = new HttpGet(url);
    final CountDownLatch latch = new CountDownLatch(1);
    final AtomicReference<String> reference = new AtomicReference<>();
    httpClient.execute(httpGet, new FutureCallback<HttpResponse>() {
        @Override
        public void completed(final HttpResponse response) {
            try {
                reference.set(getBody(response));
            } catch (Exception e) {
                logger.info("httpGetContent " + url + " error:", e);
            } finally {
                latch.countDown();
            }
        }
        @Override
        public void failed(final Exception ex) {
            latch.countDown();
            logger.info("httpGetContent " + url + " failed:", ex);
        }
        @Override
        public void cancelled() {
            latch.countDown();
        }
    });
    try {
        latch.await(5, TimeUnit.SECONDS);
    } catch (Exception e) {
        logger.info("wait http client error:", e);
    }
    return reference.get();
}

從代碼中可以看到,是通過一個異步的 httpClient 再結合 CountDownLatch 等待 5 秒的超時時間去獲取結果的。

客戶端

sentinel-core 在啓動的時候,執行了一個 InitExecutor.init 的方法,該方法會觸發所有 InitFunc 實現類的 init 方法。

InitExecutor#doInit

public static void doInit() {
    //InitExecutor只會初始化一次,並且初始化失敗會退出
    if (!initialized.compareAndSet(false, true)) {
        return;
    }
    try {
        //通過spi加載InitFunc子類
        ServiceLoader<InitFunc> loader = ServiceLoader.load(InitFunc.class);
        List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
        for (InitFunc initFunc : loader) {
            RecordLog.info("[InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName());
            //給所有的initFunc排序,按@InitOrder從小到大進行排序
            //然後封裝成OrderWrapper對象
            insertSorted(initList, initFunc);
        }
        for (OrderWrapper w : initList) {
            w.func.init();
            RecordLog.info(String.format("[InitExecutor] Executing %s with order %d",
                w.func.getClass().getCanonicalName(), w.order));
        }
    } catch (Exception ex) {
        RecordLog.warn("[InitExecutor] WARN: Initialization failed", ex);
        ex.printStackTrace();
    } catch (Error error) {
        RecordLog.warn("[InitExecutor] ERROR: Initialization failed with fatal error", error);
        error.printStackTrace();
    }
}

因爲這裏我們引入了sentinel-transport-simple-http模塊,所以使用spi加載InitFunc的子類的時候會新加載兩個子類實例,分別是:CommandCenterInitFunc、HeartbeatSenderInitFunc。然後會遍歷loader,根據@InitOrder的大小進行排序,並封裝成OrderWrapper放入到initList中。

所以initList裏面的對象順序是:

  • CommandCenterInitFunc
  • HeartbeatSenderInitFunc
  • MetricCallbackInit

然後遍歷initList依次調用init方法。

CommandCenterInitFunc

CommandCenterInitFunc 則會啓動一個 CommandCenter 對外提供 sentinel-core 的數據服務,而這些數據服務是通過一個一個的 CommandHandler 來提供的,如下圖所示:
在這裏插入圖片描述
CommandCenterInitFunc#init

public void init() throws Exception {
    //獲取commandCenter對象
    CommandCenter commandCenter = CommandCenterProvider.getCommandCenter();

    if (commandCenter == null) {
        RecordLog.warn("[CommandCenterInitFunc] Cannot resolve CommandCenter");
        return;
    }
    //調用SimpleHttpCommandCenter的beforeStart方法
    //用來設置CommandHandler的實現類
    commandCenter.beforeStart();
    commandCenter.start();
    RecordLog.info("[CommandCenterInit] Starting command center: "
            + commandCenter.getClass().getCanonicalName());
}

這個方法裏面的所有操作都是針對CommandCenter來進行的,所以我們先來看看CommandCenterProvider這個類。

CommandCenterProvider

static {
    //初始化commandCenter對象
    resolveInstance();
}

private static void resolveInstance() {
    //獲取SpiOrder更大的子類實現類
    CommandCenter resolveCommandCenter = SpiLoader.loadHighestPriorityInstance(CommandCenter.class);

    if (resolveCommandCenter == null) {
        RecordLog.warn("[CommandCenterProvider] WARN: No existing CommandCenter found");
    } else {
        commandCenter = resolveCommandCenter;
        RecordLog.info("[CommandCenterProvider] CommandCenter resolved: " + resolveCommandCenter.getClass()
            .getCanonicalName());
    }
}

CommandCenterProvider會在首次初始化的時候調用resolveInstance方法。在resolveInstance方法裏面會調用SpiLoader.loadHighestPriorityInstance來獲取CommandCenter,這裏獲取的是SimpleHttpCommandCenter這個實例,loadHighestPriorityInstance方法具體的實現非常簡單,我就不去分析了。然後將commandCenter賦值SimpleHttpCommandCenter實例。

所以CommandCenterProvider.getCommandCenter()方法返回的是SimpleHttpCommandCenter實例。然後調用SimpleHttpCommandCenter的beforeStart方法。

SimpleHttpCommandCenter#beforeStart

public void beforeStart() throws Exception {
    // Register handlers
    //調用CommandHandlerProvider的namedHandlers方法
    //獲取CommandHandler的spi中設置的實現類
    Map<String, CommandHandler> handlers = CommandHandlerProvider.getInstance().namedHandlers();
    //將handlers中的數據設置到handlerMap中
    registerCommands(handlers);
}

這個方法首先會調用CommandHandlerProvider的namedHandlers中獲取所有的CommandHandler實現類。

CommandHandlerProvider#namedHandlers

private final ServiceLoader<CommandHandler> serviceLoader = ServiceLoader.load(CommandHandler.class);

public Map<String, CommandHandler> namedHandlers() {
    Map<String, CommandHandler> map = new HashMap<String, CommandHandler>();
    for (CommandHandler handler : serviceLoader) {
        //獲取實現類CommandMapping註解的name屬性
        String name = parseCommandName(handler);
        if (!StringUtil.isEmpty(name)) {
            map.put(name, handler);
        }
    }
    return map;
}

這個類會通過spi先加載CommandHandler的實現類,然後將實現類按註解上面的name屬性放入到map裏面去。CommandHandler的實現類是用來和控制檯進行交互的處理類,負責處理。

這也是策略模式的一種應用,根據map裏面的不同策略來做不同的處理,例如SendMetricCommandHandler是用來統計調用信息然後發送給控制檯用的,ModifyRulesCommandHandler是用來做實時修改限流策略的處理的等等。

然後我們再回到CommandCenterInitFunc中,繼續往下走,調用commandCenter.start()方法。

SimpleHttpCommandCenter#start

public void start() throws Exception {
    //獲取當前機器的cpu線程數
    int nThreads = Runtime.getRuntime().availableProcessors();
    //創建一個cpu線程數大小的固定線程池,用來做業務線程池用
    this.bizExecutor = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
        new ArrayBlockingQueue<Runnable>(10),
        new NamedThreadFactory("sentinel-command-center-service-executor"),
        new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                CommandCenterLog.info("EventTask rejected");
                throw new RejectedExecutionException();
            }
        });

    Runnable serverInitTask = new Runnable() {
        int port;

        {
            try {
                //獲取port
                port = Integer.parseInt(TransportConfig.getPort());
            } catch (Exception e) {
                port = DEFAULT_PORT;
            }
        }

        @Override
        public void run() {
            boolean success = false;
            //創建一個ServerSocket
            ServerSocket serverSocket = getServerSocketFromBasePort(port);

            if (serverSocket != null) {
                CommandCenterLog.info("[CommandCenter] Begin listening at port " + serverSocket.getLocalPort());
                socketReference = serverSocket;
                executor.submit(new ServerThread(serverSocket));
                success = true;
                port = serverSocket.getLocalPort();
            } else {
                CommandCenterLog.info("[CommandCenter] chooses port fail, http command center will not work");
            }

            if (!success) {
                port = PORT_UNINITIALIZED;
            }

            TransportConfig.setRuntimePort(port);
            //關閉線程池
            executor.shutdown();
        }

    };

    new Thread(serverInitTask).start();
}
  1. 這個方法會創建一個固定大小的業務線程池
  2. 創建一個serverInitTask,裏面負責建立serverSocket然後用executor去創建一個ServerThread異步執行serverSocket
  3. executor用完之後會在serverInitTask裏面調用executor的shutdown方法去關閉線程池

其中executor是一個單線程的線程池:

private ExecutorService executor = Executors.newSingleThreadExecutor(
    new NamedThreadFactory("sentinel-command-center-executor"));

ServerThread是SimpleHttpCommandCenter的內部類:

public void run() {
    while (true) {
        Socket socket = null;
        try {
              //建立連接
            socket = this.serverSocket.accept();
              //默認的超時時間是3s
            setSocketSoTimeout(socket);
            HttpEventTask eventTask = new HttpEventTask(socket);
            //使用業務線程異步處理
            bizExecutor.submit(eventTask);
        } catch (Exception e) {
            CommandCenterLog.info("Server error", e);
            if (socket != null) {
                try {
                    socket.close();
                } catch (Exception e1) {
                    CommandCenterLog.info("Error when closing an opened socket", e1);
                }
            }
            try {
                // In case of infinite log.
                Thread.sleep(10);
            } catch (InterruptedException e1) {
                // Indicates the task should stop.
                break;
            }
        }
    }
}

run方法會使用構造器傳入的serverSocket建立連接後設置超時時間,封裝成HttpEventTask類,然後使用上面創建的bizExecutor異步執行任務。

HttpEventTask是Runnable的實現類,所以調用bizExecutor的submit的時候會調用其中的run方法使用socket與控制檯進行交互。

HttpEventTask#run

public void run() {
          ....
        // Validate the target command.
        //獲取commandName
        String commandName = HttpCommandUtils.getTarget(request);
        if (StringUtil.isBlank(commandName)) {
            badRequest(printWriter, "Invalid command");
            return;
        }
        // Find the matching command handler.
        //根據commandName獲取處理器名字
        CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
        if (commandHandler != null) {
            //調用處理器結果,然後返回給控制檯
            CommandResponse<?> response = commandHandler.handle(request);
            handleResponse(response, printWriter, outputStream);
        }  
          ....
    } catch (Throwable e) {
        ....
    } finally {
        ....
    }
}

HttpEventTask的run方法很長,但是很多都是有關輸入輸出流的,我們不關心,所以省略。只需要知道會把request請求最後轉換成一個控制檯發過來的指令,然後通過SimpleHttpCommandCenter調用getHandler得到處理器,然後處理數據就行了。

所以這個整個的處理流程就是:
在這裏插入圖片描述
通過這樣的一個處理流程,然後實現了類似reactor的一個處理流程。
在這裏插入圖片描述
SimpleHttpCommandCenter#getHandler

public static CommandHandler getHandler(String commandName) {
    return handlerMap.get(commandName);
}

handlerMap裏面的數據是通過前面我們分析的調用beforeStart方法設置進來的。

然後通過commandName獲取對應的控制檯,例如:控制檯發送過來metric指令,那麼就會對應的調用SendMetricCommandHandler的handle方法來處理控制檯的指令。

HeartbeatSenderInitFunc

HeartbeatSenderInitFunc主要是用來做心跳線程使用的,定期的和控制檯進行心跳連接。

HeartbeatSenderInitFunc#init

public void init() {
    //獲取HeartbeatSender的實現類
    HeartbeatSender sender = HeartbeatSenderProvider.getHeartbeatSender();
    if (sender == null) {
        RecordLog.warn("[HeartbeatSenderInitFunc] WARN: No HeartbeatSender loaded");
        return;
    }
    //創建一個corepoolsize爲2,maximumPoolSize爲最大的線程池
    initSchedulerIfNeeded();
    //獲取心跳間隔時間,默認10s
    long interval = retrieveInterval(sender);
    //設置間隔心跳時間
    setIntervalIfNotExists(interval);
    //開啓一個定時任務,每隔interval時間發送一個心跳
    scheduleHeartbeatTask(sender, interval);
}
  1. 首先會調用HeartbeatSenderProvider.getHeartbeatSender方法,裏面會根據spi創建實例,返回一個SimpleHttpHeartbeatSender實例。
  2. 調用initSchedulerIfNeeded方法創建一個corepoolsize爲2的線程池
  3. 獲取心跳間隔時間,如果沒有設置,那麼是10s
  4. 調用scheduleHeartbeatTask方法開啓一個定時線程調用。

我們來看看scheduleHeartbeatTask方法:

HeartbeatSenderInitFunc#scheduleHeartbeatTask

private void scheduleHeartbeatTask(/*@NonNull*/ final HeartbeatSender sender, /*@Valid*/ long interval) {
    pool.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
                sender.sendHeartbeat();
            } catch (Throwable e) {
                RecordLog.warn("[HeartbeatSender] Send heartbeat error", e);
            }
        }
    }, 5000, interval, TimeUnit.MILLISECONDS);
    RecordLog.info("[HeartbeatSenderInit] HeartbeatSender started: "
        + sender.getClass().getCanonicalName());
}

默認的情況,創建的這個定時任務會每隔10s調用一次SimpleHttpHeartbeatSender的sendHeartbeat方法。

SimpleHttpHeartbeatSender#sendHeartbeat

public boolean sendHeartbeat() throws Exception {
    if (TransportConfig.getRuntimePort() <= 0) {
        RecordLog.info("[SimpleHttpHeartbeatSender] Runtime port not initialized, won't send heartbeat");
        return false;
    }
    //獲取控制檯的ip和端口等信息
    InetSocketAddress addr = getAvailableAddress();
    if (addr == null) {
        return false;
    }
    //設置http調用的ip和端口,還有訪問的url
    SimpleHttpRequest request = new SimpleHttpRequest(addr, HEARTBEAT_PATH);
    //獲取版本號,端口等信息
    request.setParams(heartBeat.generateCurrentMessage());
    try {
        //發送post請求
        SimpleHttpResponse response = httpClient.post(request);
        if (response.getStatusCode() == OK_STATUS) {
            return true;
        }
    } catch (Exception e) {
        RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addr + " : ", e);
    }
    return false;
}

這個心跳檢測的方法就寫的很簡單了,通過Dcsp.sentinel.dashboard.server預先設置好的ip和端口號發送post請求到控制檯,然後檢測是否返回200,如果是則說明控制檯正常,否則進行異常處理。

總結

現在我們已經知道了 dashboard 是如何獲取到實時數據的了,具體的流程如下所示:

  1. 首先 sentinel-core 向 dashboard 發送心跳包

  2. dashboard 將 sentinel-core 的機器信息保存在內存中

  3. dashboard 根據 sentinel-core 的機器信息通過 httpClient 獲取實時的數據

  4. sentinel-core 接收到請求之後,會找到具體的 CommandHandler 來處理

  5. sentinel-core 將處理好的結果返回給 dashboard

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