GoReplay 簡介
隨著應用程式的複雜度的增長,測試它所需要的工作量也呈指數級增長。 GoReplay 為我們提供了複用現有流量進行測試的簡單想法。GoReplay是一個用golang開發的簡單的流量錄製外掛,支援多種方式的過濾,限流放大,重寫等等特性。GoReplay 可以做到對程式碼完全無侵入性,也不需要更改你的生產基礎設施,並且與語言無關。它不是代理,而是直接監聽網路卡上的流量。
GoReplay 工作方式:listener server 捕獲流量,並將其傳送至 replay server 或者儲存至檔案,或者儲存到kafka。然後replay server 會將流量轉移至配置的地址
使用過程
需求:接到演算法側的需求,需要錄製真實的生產環境流量,並且隨時回放到任意環境。
由於演算法側部分場景為非Java語言編寫,現存的流量錄製平臺暫時無法支援,需要採用新的錄製元件來支撐壓測需求,遂選擇goreplay 。
GoReplay支援將錄製的資料儲存到本地檔案中,然後回放時從檔案中讀取。考慮到每次錄製回放時需要進行儲存及下發檔案的複雜度,我們期望使用更便捷的方式來管理資料。
GoReplay也是原生支援錄製資料儲存到kafka中的,但是在使用的時候,發現它有較大的限制;使用kafka儲存資料時,必須是流量錄製的同時進行流量回放,其架構圖如下
流程1-4 無法拆分,只能同時進行
這會顯得流量錄製回放功能很雞肋,我們需要錄製好的資料任意時刻重放,並且也要支援將一份錄製好的資料多次重放。既然它已經將流量資料儲存到了kafka,我們就可以考慮對GoReplay進行改造,以讓他支援我們的需求。
改造後的流量錄製回放架構圖:
圖中,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);
}
}