Netflix Mantis簡介 - Baeldung

banq發表於2020-10-24

Mantis是一個用於構建流處理應用程式(作業)的平臺。它提供了一種簡便的方法來管理作業的部署和生命週期。此外,它有助於這些作業之間的資源分配,發現和通訊。
因此,開發人員可以始終專注於實際的業務邏輯,同時始終獲得強大且可擴充套件的平臺的支援,以執行其高容量,低延遲,無阻塞的應用程式。
螳螂的工作包括三個不同的部分:
  • source,負責從外部源檢索所述資料
  • 一個或多個階段stages,負責處理傳入的事件流
  • sink收集處理過的資料

現在讓我們探索它們。
加入 mantis-runtime  jackson-databind 依賴:

<dependency>
    <groupId>io.mantisrx</groupId>
    <artifactId>mantis-runtime</artifactId>
</dependency>
 
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

現在,為了設定工作的資料來源,讓我們實現Mantis Source介面:

public class RandomLogSource implements Source<String> {
 
    @Override
    public Observable<Observable<String>> call(Context context, Index index) {
        return Observable.just(
          Observable
            .interval(250, TimeUnit.MILLISECONDS)
            .map(this::createRandomLogEvent));
    }
 
    private String createRandomLogEvent(Long tick) {
        // generate a random log entry string
        ...
    }
 
}

如我們所見,它只是每秒多次生成隨機日誌條目。

 

建立一個Mantis作業
該作業只是從我們的RandomLogSource收集日誌事件。稍後,我們將新增組和聚合轉換以獲得更復雜和有趣的結果。
首先,讓我們建立一個LogEvent實體:

public class LogEvent implements JsonType {
    private Long index;
    private String level;
    private String message;
 
    // ...
}

然後,讓我們新增我們的TransformLogStage。可實現ScalarComputation介面並拆分日誌條目以構建LogEvent。此外,它還會過濾出任何錯誤的格式化字串:

public class TransformLogStage implements ScalarComputation<String, LogEvent> {
 
    @Override
    public Observable<LogEvent> call(Context context, Observable<String> logEntry) {
        return logEntry
          .map(log -> log.split("#"))
          .filter(parts -> parts.length == 3)
          .map(LogEvent::new);
    }
 
}

 

執行作業

public class LogCollectingJob extends MantisJobProvider<LogEvent> {
 
    @Override
    public Job<LogEvent> getJobInstance() {
        return MantisJob
          .source(new RandomLogSource())
          .stage(new TransformLogStage(), new ScalarToScalar.Config<>())
          .sink(Sinks.eagerSubscribe(Sinks.sse(LogEvent::toJsonString)))
          .metadata(new Metadata.Builder().build())
          .create();
    }
 
}

它擴充套件了MantisJobProvider。首先,它從我們的RandomLogSource獲取資料,並將TransformLogStage應用於獲取的資料。最後,它將處理後的資料傳送到內建的接收器,該接收器熱切地透過SSE訂閱和傳遞資料。
現在,讓我們將作業配置為在啟動時本地執行:

@SpringBootApplication
public class MantisApplication implements CommandLineRunner {
 
    // ...
 
    @Override
    public void run(String... args) {
        LocalJobExecutorNetworked.execute(new LogCollectingJob().getJobInstance());
    }
}

讓我們執行該應用程式。我們將看到類似以下的日誌訊息:

Serving modern HTTP SSE server sink on port: 86XX


現在讓我們使用curl連線到接收器:

$ curl localhost:86XX
data: {"index":86,"level":"WARN","message":"login attempt"}
data: {"index":87,"level":"ERROR","message":"user created"}
data: {"index":88,"level":"INFO","message":"user created"}
data: {"index":89,"level":"INFO","message":"login attempt"}
data: {"index":90,"level":"INFO","message":"user created"}
data: {"index":91,"level":"ERROR","message":"user created"}
data: {"index":92,"level":"WARN","message":"login attempt"}
data: {"index":93,"level":"INFO","message":"user created"}
...


 

配置接收器sink
到目前為止,我們已經使用內建接收器來收集處理過的資料。讓我們看看是否可以透過提供自定義接收器為我們的場景增加更多的靈活性。
例如,如果我們想按訊息過濾日誌怎麼辦?
讓我們建立一個實現Sink <LogEvent>介面的LogSink:

public class LogSink implements Sink<LogEvent> {
    @Override
    public void call(Context context, PortRequest portRequest, Observable<LogEvent> logEventObservable) {
        SelfDocumentingSink<LogEvent> sink = new ServerSentEventsSink.Builder<LogEvent>()
          .withEncoder(LogEvent::toJsonString)
          .withPredicate(filterByLogMessage())
          .build();
        logEventObservable.subscribe();
        sink.call(context, portRequest, logEventObservable);
    }
    private Predicate<LogEvent> filterByLogMessage() {
        return new Predicate<>("filter by message",
          parameters -> {
            if (parameters != null && parameters.containsKey("filter")) {
                return logEvent -> logEvent.getMessage().contains(parameters.get("filter").get(0));
            }
            return logEvent -> true;
        });
    }
}

在此接收器實現中,我們配置了一個謂詞,該謂詞使用filter引數僅檢索包含filter引數中設定的文字的日誌:

$ curl localhost:8874?filter=login
data: {"index":93,"level":"ERROR","message":"login attempt"}
data: {"index":95,"level":"INFO","message":"login attempt"}
data: {"index":97,"level":"ERROR","message":"login attempt"}
...

注意Mantis還提供了一種強大的查詢語言MQL,可用於以SQL方式查詢,轉換和分析流資料。
 

階段Stage連結
現在,讓我們假設有興趣知道在給定的時間間隔內有多少個ERROR,WARN或INFO日誌條目。為此,我們將在工作中再增加兩個階段並將它們連結在一起。

  • 分組

首先,讓我們建立一個GroupLogStage。
此階段是ToGroupComputation實現,該實現從現有TransformLogStage接收LogEvent流資料。之後,它將按日誌級別對條目進行分組並將其傳送到下一個階段:

public class GroupLogStage implements ToGroupComputation<LogEvent, String, LogEvent> {
 
    @Override
    public Observable<MantisGroup<String, LogEvent>> call(Context context, Observable<LogEvent> logEvent) {
        return logEvent.map(log -> new MantisGroup<>(log.getLevel(), log));
    }
 
    public static ScalarToGroup.Config<LogEvent, String, LogEvent> config(){
        return new ScalarToGroup.Config<LogEvent, String, LogEvent>()
          .description("Group event data by level")
          .codec(JacksonCodecs.pojo(LogEvent.class))
          .concurrentInput();
    }
    
}

我們還透過提供描述,用於序列化輸出的編解碼器,建立了一個自定義階段配置,並允許透過使用  parallelInput()同時執行此階段的呼叫方法。
需要注意的一件事是,此階段是水平可伸縮的。意味著我們可以根據需要執行該階段的多個例項。還值得一提的是,當在Mantis叢集中部署時,此階段會將資料傳送到下一階段,以便屬於特定組的所有事件將落入下一階段的同一工作人員。
  • 彙總

在繼續進行下一步之前,我們首先新增一個LogAggregate實體:

public class LogAggregate implements JsonType {
 
    private final Integer count;
    private final String level;
 
}

現在,讓我們建立鏈中的最後一個階段。
此階段實現GroupToScalarComputation並將日誌組流轉換為標量LogAggregate。透過計算每種型別的日誌在流中出現多少次來完成此操作。此外,它還有一個LogAggregationDuration引數,可用於控制聚合視窗的大小:

public class CountLogStage implements GroupToScalarComputation<String, LogEvent, LogAggregate> {
 
    private int duration;
 
    @Override
    public void init(Context context) {
        duration = (int)context.getParameters().get("LogAggregationDuration", 1000);
    }
 
    @Override
    public Observable<LogAggregate> call(Context context, Observable<MantisGroup<String, LogEvent>> mantisGroup) {
        return mantisGroup
          .window(duration, TimeUnit.MILLISECONDS)
          .flatMap(o -> o.groupBy(MantisGroup::getKeyValue)
            .flatMap(group -> group.reduce(0, (count, value) ->  count = count + 1)
              .map((count) -> new LogAggregate(count, group.getKey()))
            ));
    }
 
    public static GroupToScalar.Config<String, LogEvent, LogAggregate> config(){
        return new GroupToScalar.Config<String, LogEvent, LogAggregate>()
          .description("sum events for a log level")
          .codec(JacksonCodecs.pojo(LogAggregate.class))
          .withParameters(getParameters());
    }
 
    public static List<ParameterDefinition<?>> getParameters() {
        List<ParameterDefinition<?>> params = new ArrayList<>();
 
        params.add(new IntParameter()
          .name("LogAggregationDuration")
          .description("window size for aggregation in milliseconds")
          .validator(Validators.range(100, 10000))
          .defaultValue(5000)
          .build());
 
        return params;
    }
    
}

  • 配置並執行作業

public class LogAggregationJob extends MantisJobProvider<LogAggregate> {
 
    @Override
    public Job<LogAggregate> getJobInstance() {
 
        return MantisJob
          .source(new RandomLogSource())
          .stage(new TransformLogStage(), TransformLogStage.stageConfig())
          .stage(new GroupLogStage(), GroupLogStage.config())
          .stage(new CountLogStage(), CountLogStage.config())
          .sink(Sinks.eagerSubscribe(Sinks.sse(LogAggregate::toJsonString)))
          .metadata(new Metadata.Builder().build())
          .create();
    }
}

一旦執行該應用程式並執行我們的新作業,就可以看到每隔幾秒鐘檢索一次的日誌計數:

$ curl localhost:8133
data: {"count":3,"level":"ERROR"}
data: {"count":13,"level":"INFO"}
data: {"count":4,"level":"WARN"}
 
data: {"count":8,"level":"ERROR"}
data: {"count":5,"level":"INFO"}
data: {"count":7,"level":"WARN"}
...



綜上所述,在本文​​中,我們已經瞭解了Netflix Mantis是什麼以及它的用途。此外,我們研究了主要概念,使用它們來構建作業,並探索了針對不同場景的自定義配置。
與往常一樣,完整的程式碼可以在GitHub上找到