【穩定性平臺】GOREPLAY流量錄製回放實戰

得物技術發表於2021-10-30

GoReplay 簡介

隨著應用程式的複雜度的增長,測試它所需要的工作量也呈指數級增長。 GoReplay 為我們提供了複用現有流量進行測試的簡單想法。GoReplay是一個用golang開發的簡單的流量錄製外掛,支援多種方式的過濾,限流放大,重寫等等特性。GoReplay 可以做到對程式碼完全無侵入性,也不需要更改你的生產基礎設施,並且與語言無關。它不是代理,而是直接監聽網路卡上的流量。
image.png
GoReplay 工作方式:listener server 捕獲流量,並將其傳送至 replay server 或者儲存至檔案,或者儲存到kafka。然後replay server 會將流量轉移至配置的地址

使用過程

需求:接到演算法側的需求,需要錄製真實的生產環境流量,並且隨時回放到任意環境。

由於演算法側部分場景為非Java語言編寫,現存的流量錄製平臺暫時無法支援,需要採用新的錄製元件來支撐壓測需求,遂選擇goreplay 。

GoReplay支援將錄製的資料儲存到本地檔案中,然後回放時從檔案中讀取。考慮到每次錄製回放時需要進行儲存及下發檔案的複雜度,我們期望使用更便捷的方式來管理資料。
GoReplay也是原生支援錄製資料儲存到kafka中的,但是在使用的時候,發現它有較大的限制;使用kafka儲存資料時,必須是流量錄製的同時進行流量回放,其架構圖如下
image.png
流程1-4 無法拆分,只能同時進行

這會顯得流量錄製回放功能很雞肋,我們需要錄製好的資料任意時刻重放,並且也要支援將一份錄製好的資料多次重放。既然它已經將流量資料儲存到了kafka,我們就可以考慮對GoReplay進行改造,以讓他支援我們的需求。

改造後的流量錄製回放架構圖:
image.png
圖中,1-2 與 3-5 階段是相互獨立的

也就是說,流量錄製過程與回放過程可以拆開。只需要在錄製開始與結束的時候記錄kafka的offset,就可以知道這個錄製任務包含了哪些資料,我們可以輕鬆的將每一段錄製資料,整理成錄製任務,然後在需要的時候進行流量回放。

改造與整合

kafka offset 支援改造

簡要過程:
原始碼中的 InputKafkaConfig 的定義

type InputKafkaConfig struct {
    producer sarama.AsyncProducer
    consumer sarama.Consumer
    Host     string `json:"input-kafka-host"`
    Topic    string `json:"input-kafka-topic"`
    UseJSON  bool   `json:"input-kafka-json-format"`
}

修改後的 InputKafkaConfig 的定義

type InputKafkaConfig struct {
    producer  sarama.AsyncProducer
    consumer  sarama.Consumer
    Host      string `json:"input-kafka-host"`
    Topic     string `json:"input-kafka-topic"`
    UseJSON   bool   `json:"input-kafka-json-format"`
    StartOffset    int64  `json:"input-kafka-offset"`
    EndOffset int64  `json:"input-kafka-end-offset"`
}

原始碼中,從kafka讀取資料的片段:
可以看到,它選取的offset 是 Newest

for index, partition := range partitions {
        consumer, err := con.ConsumePartition(config.Topic, partition, sarama.OffsetNewest)

        go func(consumer sarama.PartitionConsumer) {
            defer consumer.Close()

            for message := range consumer.Messages() {
                i.messages <- message
            }
        }(consumer)

    }

修改過後的從kafka讀資料的片段:

for index, partition := range partitions {
        consumer, err := con.ConsumePartition(config.Topic, partition, config.StartOffset)
        offsetEnd := config.EndOffset - 1

        go func(consumer sarama.PartitionConsumer) {
            defer consumer.Close()

            for message := range consumer.Messages() {
                // 比較訊息的offset, 當超過這一批資料的最大值的時候,關閉通道
                if offsetFlag && message.Offset > offsetEnd {
                    i.quit <- struct{}{}
                    break
                }
                i.messages <- message
            }
        }(consumer)
    }

此時,只要在啟動回放任務時,指定kafka offset的範圍。就可以達到我們想要的效果了。

整合到壓測平臺

通過頁面簡單的填寫選擇操作,然後生成啟動命令,來替代冗長的命令編寫

StringBuilder builder = new StringBuilder("nohup /opt/apps/gor/gor");
// 拼接引數 組合命令
builder.append(" --input-kafka-host ").append("'").append(kafkaServer).append("'");
builder.append(" --input-kafka-topic ").append("'").append(kafkaTopic).append("'");
builder.append(" --input-kafka-start-offset ").append(record.getStartOffset());
builder.append(" --input-kafka-end-offset ").append(record.getEndOffset());
builder.append(" --output-http ").append(replayDTO.getTargetAddress());
builder.append(" --exit-after ").append(replayDTO.getMonitorTimes()).append("s");
if (StringUtils.isNotBlank(replayDTO.getExtParam())) {
  builder.append(" ").append(replayDTO.getExtParam());
}
builder.append(" > /opt/apps/gor/replay.log 2>&1 &");
String completeParam = builder.toString();

壓測平臺通過 Java agent 暴露的介面來控制 GoReplay程式的啟停

String sourceAddress = replayDTO.getSourceAddress();
String[] split = sourceAddress.split(COMMA);
for (String ip : split) {
  String uri = String.format(HttpTrafficRecordServiceImpl.BASE_URL + "/gor/start", ip,                                                  HttpTrafficRecordServiceImpl.AGENT_PORT);
  // 重新建立物件
  GoreplayRequest request = new GoreplayRequest();
  request.setConfig(replayDTO.getCompleteParam());
  request.setType(0);
  try {
    restTemplate.postForObject(uri, request, String.class);
  } catch (RestClientException e) {
    LogUtil.error("start gor fail,please check it!", e);
    MSException.throwException("start gor fail,please check it!", e);
  }
}

相關文章