This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new 37efd1f  Add e2e cluster tests (#3016)
37efd1f is described below

commit 37efd1f95a9ae980bf9dc2124fdab364b12d7293
Author: kezhenxu94 <[email protected]>
AuthorDate: Sun Jul 14 23:25:41 2019 +0800

    Add e2e cluster tests (#3016)
    
    * Add e2e cluster tests
---
 Jenkinsfile-E2E                                    |  15 +-
 apm-dist/bin/oapServiceNoInit.sh                   |   2 +-
 .../org/apache/skywalking/e2e/AbstractQuery.java   |  14 +-
 .../apache/skywalking/e2e/SimpleQueryClient.java   |   4 +
 .../skywalking/e2e/metrics/MetricsQuery.java       |  19 ++
 .../skywalking/e2e/service/ServiceMatcher.java     |  14 +-
 .../skywalking/e2e/service/ServicesMatcher.java    |  24 +-
 .../java/org/apache/skywalking/e2e/topo/Call.java  |  10 +
 .../apache/skywalking/e2e/topo/CallMatcher.java    |  10 +
 .../java/org/apache/skywalking/e2e/topo/Node.java  |  10 +
 .../apache/skywalking/e2e/topo/NodeMatcher.java    |  10 +
 .../apache/skywalking/e2e/topo/TopoMatcher.java    |  46 ++-
 .../e2e/trace/{TracesData.java => Span.java}       |  41 ++-
 .../apache/skywalking/e2e/trace/SpanMatcher.java   | 120 +++++++
 .../e2e/trace/{TracesData.java => Tag.java}        |  32 +-
 .../org/apache/skywalking/e2e/trace/Trace.java     |   6 +
 .../apache/skywalking/e2e/trace/TraceMatcher.java  |  36 +++
 .../apache/skywalking/e2e/trace/TracesData.java    |  32 +-
 .../apache/skywalking/e2e/trace/TracesMatcher.java |  29 ++
 .../apache/skywalking/e2e/trace/TracesQuery.java   |   5 +
 test/e2e/e2e-cluster/consumer/pom.xml              |  56 ++++
 .../e2e/cluster/Service1Application.java}          |  31 +-
 .../skywalking/e2e/cluster/TestController.java     |  50 +++
 .../org/apache/skywalking/e2e/cluster/User.java}   |  38 ++-
 .../consumer/src/main/resources/application.yml    |  22 ++
 test/e2e/e2e-cluster/pom.xml                       |  38 +++
 test/e2e/e2e-cluster/provider/pom.xml              |  69 ++++
 .../e2e/cluster/Service0Application.java}          |  33 +-
 .../skywalking/e2e/cluster/TestController.java}    |  55 ++--
 .../org/apache/skywalking/e2e/cluster/User.java}   |  47 ++-
 .../apache/skywalking/e2e/cluster/UserRepo.java}   |  28 +-
 .../provider/src/main/resources/application.yml    |  35 +++
 test/e2e/e2e-cluster/test-runner/pom.xml           | 202 ++++++++++++
 .../test-runner/src/docker/clusterize.awk          |  92 ++++++
 .../test-runner/src/docker/rc.d/rc0-prepare.sh     |  32 ++
 .../test-runner/src/docker/rc.d/rc1-startup.sh     |  73 +++++
 .../skywalking/e2e/ClusterVerificationITCase.java  | 348 +++++++++++++++++++++
 ...ing.e2e.ClusterVerificationITCase.endpoints.yml |  25 ++
 ...ing.e2e.ClusterVerificationITCase.instances.yml |  34 ++
 ...king.e2e.ClusterVerificationITCase.services.yml |  28 ++
 ...ywalking.e2e.ClusterVerificationITCase.topo.yml |  39 +++
 ...alking.e2e.ClusterVerificationITCase.traces.yml |  25 ++
 test/e2e/e2e-single-service/pom.xml                |   2 +-
 .../skywalking/e2e/SampleVerificationITCase.java   |  97 +++---
 test/e2e/pom.xml                                   |  21 ++
 45 files changed, 1709 insertions(+), 290 deletions(-)

diff --git a/Jenkinsfile-E2E b/Jenkinsfile-E2E
index d43016e..2845776 100644
--- a/Jenkinsfile-E2E
+++ b/Jenkinsfile-E2E
@@ -42,8 +42,18 @@ pipeline {
         }
 
         stage('Run End-to-End Tests') {
-            steps {
-                sh './mvnw -Dbuild.id=${BUILD_ID} -f test/e2e/pom.xml clean 
verify'
+            parallel {
+                stage('Run Single Node Tests') {
+                    steps {
+                        sh './mvnw -DskipSurefire=false -Dbuild.id=${BUILD_ID} 
-f test/e2e/pom.xml -pl e2e-single-service -am verify'
+                    }
+                }
+
+                stage('Run Cluster Tests (ES/ZK)') {
+                    steps {
+                        sh './mvnw -Dbuild.id=${BUILD_ID} -f test/e2e/pom.xml 
-pl e2e-cluster/test-runner -am verify'
+                    }
+                }
             }
         }
     }
@@ -54,6 +64,7 @@ pipeline {
             // we need to clean up when there are containers started by the 
e2e tests
             sh 'docker ps'
             sh 'docker ps | grep -e "skywalking-e2e-container-${BUILD_ID}" | 
awk \'{print $1}\' | xargs --no-run-if-empty docker stop'
+            sh 'docker ps | grep -e "skywalking-e2e-container-${BUILD_ID}" | 
awk \'{print $1}\' | xargs --no-run-if-empty docker rm'
             deleteDir()
         }
     }
diff --git a/apm-dist/bin/oapServiceNoInit.sh b/apm-dist/bin/oapServiceNoInit.sh
index c522e7b..5b80f72 100644
--- a/apm-dist/bin/oapServiceNoInit.sh
+++ b/apm-dist/bin/oapServiceNoInit.sh
@@ -20,7 +20,7 @@ PRG="$0"
 PRGDIR=`dirname "$PRG"`
 [ -z "$OAP_HOME" ] && OAP_HOME=`cd "$PRGDIR/.." >/dev/null; pwd`
 
-OAP_LOG_DIR="${OAP_HOME}/logs"
+OAP_LOG_DIR=${OAP_LOG_DIR:-"${OAP_HOME}/logs"}
 JAVA_OPTS=" -Xms256M -Xmx512M"
 
 if [ ! -d "${OAP_HOME}/logs" ]; then
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/AbstractQuery.java 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/AbstractQuery.java
index 3bb8e47..2b38b9e 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/AbstractQuery.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/AbstractQuery.java
@@ -38,8 +38,8 @@ public abstract class AbstractQuery<T extends 
AbstractQuery<?>> {
             return start;
         }
         return "SECOND".equals(step())
-            ? 
LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).format(TIME_FORMATTER)
-            : 
LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).format(MINUTE_TIME_FORMATTER);
+            ? 
LocalDateTime.now(ZoneOffset.UTC).minusMinutes(15).format(TIME_FORMATTER)
+            : 
LocalDateTime.now(ZoneOffset.UTC).minusMinutes(15).format(MINUTE_TIME_FORMATTER);
     }
 
     public T start(String start) {
@@ -87,4 +87,14 @@ public abstract class AbstractQuery<T extends 
AbstractQuery<?>> {
         this.step = step;
         return (T) this;
     }
+
+    public T stepByMinute() {
+        this.step = "MINUTE";
+        return (T) this;
+    }
+
+    public T stepBySecond() {
+        this.step = "SECOND";
+        return (T) this;
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/SimpleQueryClient.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/SimpleQueryClient.java
index 1a12c79..1cff898 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/SimpleQueryClient.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/SimpleQueryClient.java
@@ -57,6 +57,10 @@ public class SimpleQueryClient {
 
     private final String endpointUrl;
 
+    public SimpleQueryClient(String host, String port) {
+        this("http://"; + host + ":" + port + "/graphql");
+    }
+
     public SimpleQueryClient(String endpointUrl) {
         this.endpointUrl = endpointUrl;
     }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/metrics/MetricsQuery.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/metrics/MetricsQuery.java
index edcbce1..5256c8c 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/metrics/MetricsQuery.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/metrics/MetricsQuery.java
@@ -29,16 +29,35 @@ public class MetricsQuery extends 
AbstractQuery<MetricsQuery> {
     public static String SERVICE_P90 = "service_p90";
     public static String SERVICE_P75 = "service_p75";
     public static String SERVICE_P50 = "service_p50";
+    public static String[] ALL_SERVICE_METRICS = {
+        SERVICE_P99,
+        SERVICE_P95,
+        SERVICE_P90,
+        SERVICE_P75,
+        SERVICE_P50
+    };
 
     public static String ENDPOINT_P99 = "endpoint_p99";
     public static String ENDPOINT_P95 = "endpoint_p95";
     public static String ENDPOINT_P90 = "endpoint_p90";
     public static String ENDPOINT_P75 = "endpoint_p75";
     public static String ENDPOINT_P50 = "endpoint_p50";
+    public static String[] ALL_ENDPOINT_METRICS = {
+        ENDPOINT_P99,
+        ENDPOINT_P95,
+        ENDPOINT_P90,
+        ENDPOINT_P75,
+        ENDPOINT_P50
+    };
 
     public static String SERVICE_INSTANCE_RESP_TIME = 
"service_instance_resp_time";
     public static String SERVICE_INSTANCE_CPM = "service_instance_cpm";
     public static String SERVICE_INSTANCE_SLA = "service_instance_sla";
+    public static String[] ALL_INSTANCE_METRICS = {
+        SERVICE_INSTANCE_RESP_TIME,
+        SERVICE_INSTANCE_CPM,
+        SERVICE_INSTANCE_SLA
+    };
 
     private String id;
     private String metricsName;
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServiceMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServiceMatcher.java
index 00fcdc6..8aec478 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServiceMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServiceMatcher.java
@@ -44,15 +44,15 @@ public class ServiceMatcher extends 
AbstractMatcher<Service> {
     }
 
     private void verifyKey(Service service) {
-        final String expected = this.getKey();
+        final String expected = getKey();
         final String actual = service.getKey();
 
         doVerify(expected, actual);
     }
 
     private void verifyLabel(Service service) {
-        final String expected = this.getLabel();
-        final String actual = String.valueOf(service.getLabel());
+        final String expected = getLabel();
+        final String actual = service.getLabel();
 
         doVerify(expected, actual);
     }
@@ -72,4 +72,12 @@ public class ServiceMatcher extends AbstractMatcher<Service> 
{
     public void setLabel(String label) {
         this.label = label;
     }
+
+    @Override
+    public String toString() {
+        return "ServiceMatcher{" +
+            "key='" + key + '\'' +
+            ", label='" + label + '\'' +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServicesMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServicesMatcher.java
index 8b33d7f..54dccfe 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServicesMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/service/ServicesMatcher.java
@@ -22,6 +22,7 @@ import java.util.LinkedList;
 import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
 
 /**
  * @author kezhenxu94
@@ -44,10 +45,25 @@ public class ServicesMatcher {
     public void verify(final List<Service> services) {
         assertThat(services).hasSameSizeAs(this.getServices());
 
-        int size = this.getServices().size();
-
-        for (int i = 0; i < size; i++) {
-            this.getServices().get(i).verify(services.get(i));
+        for (int i = 0; i < getServices().size(); i++) {
+            boolean matched = false;
+            for (Service service : services) {
+                try {
+                    this.getServices().get(i).verify(service);
+                    matched = true;
+                } catch (Throwable ignored) {
+                }
+            }
+            if (!matched) {
+                fail("Expected: %s\nActual: %s", getServices(), services);
+            }
         }
     }
+
+    @Override
+    public String toString() {
+        return "ServicesMatcher{" +
+            "services=" + services +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Call.java 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Call.java
index ef39714..e2104d8 100644
--- a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Call.java
+++ b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Call.java
@@ -64,4 +64,14 @@ public class Call {
         this.target = target;
         return this;
     }
+
+    @Override
+    public String toString() {
+        return "Call{" +
+            "id='" + id + '\'' +
+            ", source='" + source + '\'' +
+            ", detectPoints=" + detectPoints +
+            ", target='" + target + '\'' +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/CallMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/CallMatcher.java
index e69b0eb..1b73e90 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/CallMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/CallMatcher.java
@@ -101,4 +101,14 @@ public class CallMatcher extends AbstractMatcher<Call> {
     public void setTarget(String target) {
         this.target = target;
     }
+
+    @Override
+    public String toString() {
+        return "CallMatcher{" +
+            "id='" + id + '\'' +
+            ", source='" + source + '\'' +
+            ", detectPoints=" + detectPoints +
+            ", target='" + target + '\'' +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
index bb796af..413894a 100644
--- a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
+++ b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
@@ -62,4 +62,14 @@ public class Node {
         isReal = real;
         return this;
     }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+            "id='" + id + '\'' +
+            ", name='" + name + '\'' +
+            ", type='" + type + '\'' +
+            ", isReal='" + isReal + '\'' +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/NodeMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/NodeMatcher.java
index b4f264b..89cc07e 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/NodeMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/NodeMatcher.java
@@ -93,4 +93,14 @@ public class NodeMatcher extends AbstractMatcher<Node> {
     public void setIsReal(String isReal) {
         this.isReal = isReal;
     }
+
+    @Override
+    public String toString() {
+        return "NodeMatcher{" +
+            "id='" + id + '\'' +
+            ", name='" + name + '\'' +
+            ", type='" + type + '\'' +
+            ", isReal='" + isReal + '\'' +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/TopoMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/TopoMatcher.java
index aa64b04..9d7ec35 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/TopoMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/TopoMatcher.java
@@ -23,7 +23,7 @@ import org.apache.skywalking.e2e.verification.AbstractMatcher;
 import java.util.List;
 import java.util.Objects;
 
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
 
 /**
  * A simple matcher to verify the given {@code Service} is expected
@@ -47,22 +47,34 @@ public class TopoMatcher extends AbstractMatcher<TopoData> {
     }
 
     private void verifyNodes(TopoData topoData) {
-        assertThat(topoData.getNodes()).hasSameSizeAs(getNodes());
-
-        int size = getNodes().size();
-
-        for (int i = 0; i < size; i++) {
-            getNodes().get(i).verify(topoData.getNodes().get(i));
+        for (int i = 0; i < getNodes().size(); i++) {
+            boolean matched = false;
+            for (int j = 0; j < topoData.getNodes().size(); j++) {
+                try {
+                    getNodes().get(i).verify(topoData.getNodes().get(j));
+                    matched = true;
+                } catch (Throwable ignored) {
+                }
+            }
+            if (!matched) {
+                fail("Expected: %s\nActual: %s", getNodes(), 
topoData.getNodes());
+            }
         }
     }
 
     private void verifyCalls(TopoData topoData) {
-        assertThat(topoData.getCalls()).hasSameSizeAs(getCalls());
-
-        int size = getCalls().size();
-
-        for (int i = 0; i < size; i++) {
-            getCalls().get(i).verify(topoData.getCalls().get(i));
+        for (int i = 0; i < getCalls().size(); i++) {
+            boolean matched = false;
+            for (int j = 0; j < topoData.getCalls().size(); j++) {
+                try {
+                    getCalls().get(i).verify(topoData.getCalls().get(j));
+                    matched = true;
+                } catch (Throwable ignored) {
+                }
+            }
+            if (!matched) {
+                fail("Expected: %s\nActual: %s", getCalls(), 
topoData.getCalls());
+            }
         }
     }
 
@@ -81,4 +93,12 @@ public class TopoMatcher extends AbstractMatcher<TopoData> {
     public void setCalls(List<CallMatcher> calls) {
         this.calls = calls;
     }
+
+    @Override
+    public String toString() {
+        return "TopoMatcher{" +
+            "nodes=" + nodes +
+            ", calls=" + calls +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Span.java
similarity index 67%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
copy to 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Span.java
index aa5b49a..98ebe66 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Span.java
@@ -18,32 +18,27 @@
 
 package org.apache.skywalking.e2e.trace;
 
+import lombok.Data;
+
 import java.util.List;
 
 /**
  * @author kezhenxu94
  */
-public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
-
-    public List<Trace> getData() {
-      return data;
-    }
-
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
-    }
-  }
-
-  private Traces traces;
-
-  public Traces getTraces() {
-    return traces;
-  }
-
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
+@Data
+public class Span {
+    private String traceId;
+    private String segmentId;
+    private int spanId;
+    private int parentSpanId;
+    private String serviceCode;
+    private long startTime;
+    private long endTime;
+    private String endpointName;
+    private String type;
+    private String peer;
+    private String component;
+    private boolean isError;
+    private String layer;
+    private List<String> tags;
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/SpanMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/SpanMatcher.java
new file mode 100644
index 0000000..09491f2
--- /dev/null
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/SpanMatcher.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.e2e.trace;
+
+import com.google.common.base.Strings;
+import lombok.Data;
+import lombok.ToString;
+import org.apache.skywalking.e2e.verification.AbstractMatcher;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author kezhenxu94
+ */
+@Data
+@ToString(callSuper = true)
+public class SpanMatcher extends AbstractMatcher<Span> {
+    private String traceId;
+    private String segmentId;
+    private String spanId;
+    private String parentSpanId;
+    private String serviceCode;
+    private String startTime;
+    private String endTime;
+    private String endpointName;
+    private String type;
+    private String peer;
+    private String component;
+    private String isError;
+    private String layer;
+    private List<String> tags;
+
+    @Override
+    public void verify(final Span span) {
+        if (Objects.nonNull(traceId)) {
+            String expected = this.getTraceId();
+            String actual = span.getTraceId();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(segmentId)) {
+            String expected = this.getSegmentId();
+            String actual = span.getSegmentId();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(spanId)) {
+            String expected = String.valueOf(this.getSpanId());
+            String actual = String.valueOf(span.getSpanId());
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(parentSpanId)) {
+            String expected = String.valueOf(this.getParentSpanId());
+            String actual = String.valueOf(span.getParentSpanId());
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(serviceCode)) {
+            String expected = this.getServiceCode();
+            String actual = span.getServiceCode();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(startTime)) {
+            String expected = String.valueOf(this.getStartTime());
+            String actual = String.valueOf(span.getStartTime());
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(endTime)) {
+            String expected = String.valueOf(this.getEndTime());
+            String actual = String.valueOf(span.getEndTime());
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(endpointName)) {
+            String expected = this.getEndpointName();
+            String actual = span.getEndpointName();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(type)) {
+            String expected = this.getType();
+            String actual = span.getType();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(peer)) {
+            String expected = this.getPeer();
+            String actual = span.getPeer();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(component)) {
+            String expected = this.getComponent();
+            String actual = span.getComponent();
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(isError)) {
+            String expected = 
Strings.nullToEmpty(String.valueOf(this.getIsError()));
+            String actual = 
Strings.nullToEmpty(String.valueOf(span.isError()));
+            doVerify(expected, actual);
+        }
+        if (Objects.nonNull(layer)) {
+            String expected = this.getLayer();
+            String actual = span.getLayer();
+            doVerify(expected, actual);
+        }
+
+    }
+
+}
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Tag.java
similarity index 68%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
copy to test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Tag.java
index aa5b49a..a55cdef 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Tag.java
@@ -18,32 +18,26 @@
 
 package org.apache.skywalking.e2e.trace;
 
-import java.util.List;
-
 /**
  * @author kezhenxu94
  */
-public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
+public class Tag {
+    private String key;
+    private String value;
 
-    public List<Trace> getData() {
-      return data;
+    public String getKey() {
+        return key;
     }
 
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
+    public void setKey(final String key) {
+        this.key = key;
     }
-  }
-
-  private Traces traces;
 
-  public Traces getTraces() {
-    return traces;
-  }
+    public String getValue() {
+        return value;
+    }
 
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
+    public void setValue(final String value) {
+        this.value = value;
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Trace.java 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Trace.java
index 6296d79..913e280 100644
--- a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Trace.java
+++ b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/Trace.java
@@ -31,10 +31,12 @@ public class Trace {
     private String start;
     private boolean isError;
     private final List<String> traceIds;
+    private final List<Span> spans;
 
     public Trace() {
         this.endpointNames = new ArrayList<>();
         this.traceIds = new ArrayList<>();
+        this.spans = new ArrayList<>();
     }
 
     public String getKey() {
@@ -81,6 +83,10 @@ public class Trace {
         return traceIds;
     }
 
+    public List<Span> getSpans() {
+        return spans;
+    }
+
     @Override
     public String toString() {
         return "Trace{" +
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TraceMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TraceMatcher.java
index 30a537e..87d7b76 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TraceMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TraceMatcher.java
@@ -39,6 +39,7 @@ public class TraceMatcher extends AbstractMatcher<Trace> {
     private String start;
     private String isError;
     private List<String> traceIds;
+    private List<SpanMatcher> spans;
 
     @Override
     public void verify(final Trace trace) {
@@ -65,6 +66,10 @@ public class TraceMatcher extends AbstractMatcher<Trace> {
         if (Objects.nonNull(getTraceIds())) {
             verifyTraceIds(trace);
         }
+
+        if (Objects.nonNull(getSpans())) {
+            verifySpans(trace);
+        }
     }
 
     private void verifyKey(Trace trace) {
@@ -121,6 +126,16 @@ public class TraceMatcher extends AbstractMatcher<Trace> {
         }
     }
 
+    private void verifySpans(Trace trace) {
+        assertThat(trace.getSpans()).hasSameSizeAs(getSpans());
+
+        int size = getSpans().size();
+
+        for (int i = 0; i < size; i++) {
+            getSpans().get(i).verify(trace.getSpans().get(i));
+        }
+    }
+
     public String getKey() {
         return key;
     }
@@ -168,4 +183,25 @@ public class TraceMatcher extends AbstractMatcher<Trace> {
     public List<String> getTraceIds() {
         return traceIds != null ? traceIds : new ArrayList<>();
     }
+
+    public List<SpanMatcher> getSpans() {
+        return spans;
+    }
+
+    public void setSpans(final List<SpanMatcher> spans) {
+        this.spans = spans;
+    }
+
+    @Override
+    public String toString() {
+        return "TraceMatcher{" +
+            "key='" + key + '\'' +
+            ", endpointNames=" + endpointNames +
+            ", duration='" + duration + '\'' +
+            ", start='" + start + '\'' +
+            ", isError='" + isError + '\'' +
+            ", traceIds=" + traceIds +
+            ", spans=" + spans +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
index aa5b49a..2832bd2 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
@@ -24,26 +24,26 @@ import java.util.List;
  * @author kezhenxu94
  */
 public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
+    public static class Traces {
+        private List<Trace> data;
 
-    public List<Trace> getData() {
-      return data;
-    }
+        public List<Trace> getData() {
+            return data;
+        }
 
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
+        public Traces setData(List<Trace> data) {
+            this.data = data;
+            return this;
+        }
     }
-  }
 
-  private Traces traces;
+    private Traces traces;
 
-  public Traces getTraces() {
-    return traces;
-  }
+    public Traces getTraces() {
+        return traces;
+    }
 
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
+    public void setTraces(final Traces traces) {
+        this.traces = traces;
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesMatcher.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesMatcher.java
index cade9cc..7d48b59 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesMatcher.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesMatcher.java
@@ -22,6 +22,7 @@ import java.util.LinkedList;
 import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
 
 /**
  * @author kezhenxu94
@@ -50,4 +51,32 @@ public class TracesMatcher {
             this.traces.get(i).verify(traces.get(i));
         }
     }
+
+    /**
+     * Verify the traces in a loose manner
+     *
+     * @param traces
+     */
+    public void verifyLoosely(final List<Trace> traces) {
+        for (int i = 0; i < getTraces().size(); i++) {
+            boolean matched = false;
+            for (int j = 0; j < traces.size(); j++) {
+                try {
+                    getTraces().get(i).verify(traces.get(j));
+                    matched = true;
+                } catch (Throwable ignored) {
+                }
+            }
+            if (!matched) {
+                fail("Expected: %s\n Actual: %s", getTraces(), traces);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "TracesMatcher{" +
+            "traces=" + traces +
+            '}';
+    }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesQuery.java
 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesQuery.java
index 22b2455..410586a 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesQuery.java
+++ 
b/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesQuery.java
@@ -85,6 +85,11 @@ public class TracesQuery extends AbstractQuery<TracesQuery> {
         return this;
     }
 
+    public TracesQuery orderByStartTime() {
+        this.queryOrder = "BY_START_TIME";
+        return this;
+    }
+
     public TracesQuery pageSize(int pageSize) {
         this.pageSize = String.valueOf(pageSize);
         return this;
diff --git a/test/e2e/e2e-cluster/consumer/pom.xml 
b/test/e2e/e2e-cluster/consumer/pom.xml
new file mode 100644
index 0000000..efaa53c
--- /dev/null
+++ b/test/e2e/e2e-cluster/consumer/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <parent>
+        <artifactId>e2e-cluster</artifactId>
+        <groupId>org.apache.skywalking</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+
+    <artifactId>consumer</artifactId>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+                <configuration>
+                    <executable>true</executable>
+                    <addResources>true</addResources>
+                    <excludeDevtools>true</excludeDevtools>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 
b/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/Service1Application.java
similarity index 64%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
copy to 
test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/Service1Application.java
index aa5b49a..7ba83aa 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ 
b/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/Service1Application.java
@@ -16,34 +16,17 @@
  *
  */
 
-package org.apache.skywalking.e2e.trace;
+package org.apache.skywalking.e2e.cluster;
 
-import java.util.List;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 /**
  * @author kezhenxu94
  */
-public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
-
-    public List<Trace> getData() {
-      return data;
-    }
-
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
+@SpringBootApplication
+public class Service1Application {
+    public static void main(String[] args) {
+        SpringApplication.run(Service1Application.class, args);
     }
-  }
-
-  private Traces traces;
-
-  public Traces getTraces() {
-    return traces;
-  }
-
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
 }
diff --git 
a/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/TestController.java
 
b/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/TestController.java
new file mode 100644
index 0000000..c59f3d3
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/TestController.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.e2e.cluster;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author kezhenxu94
+ */
+@RestController
+@RequestMapping("/e2e")
+public class TestController {
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @GetMapping("/health-check")
+    public String hello() {
+        return "healthy";
+    }
+
+    @PostMapping("/users")
+    public User createAuthor(@RequestBody final User user) throws 
InterruptedException {
+        Thread.sleep(1000L);
+        final ResponseEntity<User> response = restTemplate.postForEntity(
+            "http://localhost:9090/e2e/users";, user, User.class
+        );
+        return response.getBody();
+    }
+}
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 
b/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/User.java
similarity index 65%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
copy to 
test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/User.java
index aa5b49a..f62d1e7 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ 
b/test/e2e/e2e-cluster/consumer/src/main/java/org/apache/skywalking/e2e/cluster/User.java
@@ -16,34 +16,32 @@
  *
  */
 
-package org.apache.skywalking.e2e.trace;
-
-import java.util.List;
+package org.apache.skywalking.e2e.cluster;
 
 /**
  * @author kezhenxu94
  */
-public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
-
-    public List<Trace> getData() {
-      return data;
+public class User {
+    public User() {
     }
 
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
+    private Long id;
+
+    private String name;
+
+    public Long getId() {
+        return id;
     }
-  }
 
-  private Traces traces;
+    public void setId(final Long id) {
+        this.id = id;
+    }
 
-  public Traces getTraces() {
-    return traces;
-  }
+    public String getName() {
+        return name;
+    }
 
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
+    public void setName(final String name) {
+        this.name = name;
+    }
 }
diff --git a/test/e2e/e2e-cluster/consumer/src/main/resources/application.yml 
b/test/e2e/e2e-cluster/consumer/src/main/resources/application.yml
new file mode 100644
index 0000000..757e7cb
--- /dev/null
+++ b/test/e2e/e2e-cluster/consumer/src/main/resources/application.yml
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+server:
+  port: 9091
+
+spring:
+  main:
+    banner-mode: 'off'
diff --git a/test/e2e/e2e-cluster/pom.xml b/test/e2e/e2e-cluster/pom.xml
new file mode 100644
index 0000000..757863d
--- /dev/null
+++ b/test/e2e/e2e-cluster/pom.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <parent>
+        <artifactId>apache-skywalking-e2e</artifactId>
+        <groupId>org.apache.skywalking</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>e2e-cluster</artifactId>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>provider</module>
+        <module>consumer</module>
+        <module>test-runner</module>
+    </modules>
+</project>
diff --git a/test/e2e/e2e-cluster/provider/pom.xml 
b/test/e2e/e2e-cluster/provider/pom.xml
new file mode 100644
index 0000000..804e24c
--- /dev/null
+++ b/test/e2e/e2e-cluster/provider/pom.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <parent>
+        <artifactId>e2e-cluster</artifactId>
+        <groupId>org.apache.skywalking</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+
+    <artifactId>provider</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+            <version>${spring.boot.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>${h2.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+                <configuration>
+                    <executable>true</executable>
+                    <addResources>true</addResources>
+                    <excludeDevtools>true</excludeDevtools>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/Service0Application.java
similarity index 64%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
copy to 
test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/Service0Application.java
index aa5b49a..791eab1 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/Service0Application.java
@@ -16,34 +16,19 @@
  *
  */
 
-package org.apache.skywalking.e2e.trace;
+package org.apache.skywalking.e2e.cluster;
 
-import java.util.List;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
 /**
  * @author kezhenxu94
  */
-public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
-
-    public List<Trace> getData() {
-      return data;
-    }
-
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
+@EnableJpaRepositories
+@SpringBootApplication
+public class Service0Application {
+    public static void main(String[] args) {
+        SpringApplication.run(Service0Application.class, args);
     }
-  }
-
-  private Traces traces;
-
-  public Traces getTraces() {
-    return traces;
-  }
-
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/TestController.java
similarity index 53%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
copy to 
test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/TestController.java
index bb796af..3907648 100644
--- a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
+++ 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/TestController.java
@@ -16,50 +16,33 @@
  *
  */
 
-package org.apache.skywalking.e2e.topo;
+package org.apache.skywalking.e2e.cluster;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
 /**
  * @author kezhenxu94
  */
-public class Node {
-    private String id;
-    private String name;
-    private String type;
-    private String isReal;
-
-    public String getId() {
-        return id;
-    }
-
-    public Node setId(String id) {
-        this.id = id;
-        return this;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public Node setName(String name) {
-        this.name = name;
-        return this;
-    }
-
-    public String getType() {
-        return type;
-    }
+@RestController
+@RequestMapping("/e2e")
+public class TestController {
+    private final UserRepo userRepo;
 
-    public Node setType(String type) {
-        this.type = type;
-        return this;
+    public TestController(final UserRepo userRepo) {
+        this.userRepo = userRepo;
     }
 
-    public String getReal() {
-        return isReal;
+    @GetMapping("/health-check")
+    public String hello() {
+        return "healthy";
     }
 
-    public Node setIsReal(String real) {
-        isReal = real;
-        return this;
+    @PostMapping("/users")
+    public User createAuthor(@RequestBody final User user) {
+        return userRepo.save(user);
     }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/User.java
similarity index 64%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
copy to 
test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/User.java
index bb796af..043127f 100644
--- a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/topo/Node.java
+++ 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/User.java
@@ -16,50 +16,41 @@
  *
  */
 
-package org.apache.skywalking.e2e.topo;
+package org.apache.skywalking.e2e.cluster;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
 
 /**
  * @author kezhenxu94
  */
-public class Node {
-    private String id;
+@Entity
+public class User {
+    public User() {
+    }
+
+    @Id
+    @GeneratedValue
+    private Long id;
+
+    @Column
     private String name;
-    private String type;
-    private String isReal;
 
-    public String getId() {
+    public Long getId() {
         return id;
     }
 
-    public Node setId(String id) {
+    public void setId(final Long id) {
         this.id = id;
-        return this;
     }
 
     public String getName() {
         return name;
     }
 
-    public Node setName(String name) {
+    public void setName(final String name) {
         this.name = name;
-        return this;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    public Node setType(String type) {
-        this.type = type;
-        return this;
-    }
-
-    public String getReal() {
-        return isReal;
-    }
-
-    public Node setIsReal(String real) {
-        isReal = real;
-        return this;
     }
 }
diff --git 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/UserRepo.java
similarity index 64%
copy from 
test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
copy to 
test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/UserRepo.java
index aa5b49a..f798f63 100644
--- 
a/test/e2e/e2e-base/src/main/java/org/apache/skywalking/e2e/trace/TracesData.java
+++ 
b/test/e2e/e2e-cluster/provider/src/main/java/org/apache/skywalking/e2e/cluster/UserRepo.java
@@ -16,34 +16,12 @@
  *
  */
 
-package org.apache.skywalking.e2e.trace;
+package org.apache.skywalking.e2e.cluster;
 
-import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
 
 /**
  * @author kezhenxu94
  */
-public class TracesData {
-  public static class Traces {
-    private List<Trace> data;
-
-    public List<Trace> getData() {
-      return data;
-    }
-
-    public Traces setData(List<Trace> data) {
-      this.data = data;
-      return this;
-    }
-  }
-
-  private Traces traces;
-
-  public Traces getTraces() {
-    return traces;
-  }
-
-  public void setTraces(final Traces traces) {
-    this.traces = traces;
-  }
+public interface UserRepo extends JpaRepository<User, Long> {
 }
diff --git a/test/e2e/e2e-cluster/provider/src/main/resources/application.yml 
b/test/e2e/e2e-cluster/provider/src/main/resources/application.yml
new file mode 100644
index 0000000..ef3ed01
--- /dev/null
+++ b/test/e2e/e2e-cluster/provider/src/main/resources/application.yml
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+server:
+  port: 9090
+
+spring:
+  main:
+    banner-mode: 'off'
+  datasource:
+    url: jdbc:h2:mem:testdb
+    driver-class-name: org.h2.Driver
+    data-username: sa
+    password: sa
+    platform: org.hibernate.dialect.H2Dialect
+  jpa:
+    generate-ddl: true
+    hibernate:
+      ddl-auto: create-drop
+    properties:
+      hibernate.format_sql: true
+    show-sql: true
diff --git a/test/e2e/e2e-cluster/test-runner/pom.xml 
b/test/e2e/e2e-cluster/test-runner/pom.xml
new file mode 100644
index 0000000..c799dc5
--- /dev/null
+++ b/test/e2e/e2e-cluster/test-runner/pom.xml
@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <parent>
+        <artifactId>e2e-cluster</artifactId>
+        <groupId>org.apache.skywalking</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>test-runner</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>e2e-base</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>provider</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>consumer</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+    </dependencies>
+
+    <properties>
+        <service0.name>provider</service0.name>
+        <service1.name>consumer</service1.name>
+        <e2e.container.version>1.1</e2e.container.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>io.fabric8</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <configuration>
+                    <containerNamePattern>%a-%t-%i</containerNamePattern>
+                    <imagePullPolicy>Always</imagePullPolicy>
+                    <images>
+                        <image>
+                            
<name>elastic/elasticsearch:${elasticsearch.version}</name>
+                            
<alias>skywalking-e2e-container-${build.id}-elasticsearch</alias>
+                            <run>
+                                <ports>
+                                    <port>es.port:9200</port>
+                                </ports>
+                                <wait>
+                                    <http>
+                                        <url>http://localhost:${es.port}</url>
+                                        <method>GET</method>
+                                        <status>200</status>
+                                    </http>
+                                    <time>120000</time>
+                                </wait>
+                                <env>
+                                    
<discovery.type>single-node</discovery.type>
+                                </env>
+                            </run>
+                        </image>
+                        <image>
+                            <name>zookeeper:${zookeeper.image.version}</name>
+                            
<alias>skywalking-e2e-container-${build.id}-zookeeper</alias>
+                            <run>
+                                <ports>
+                                    <port>zk.port:2181</port>
+                                </ports>
+                                <wait>
+                                    <log>binding to port</log>
+                                    <time>30000</time>
+                                </wait>
+                            </run>
+                        </image>
+                        <image>
+                            
<name>skyapm/e2e-container:${e2e.container.version}</name>
+                            <alias>skywalking-e2e-container-${build.id}</alias>
+                            <run>
+                                <env>
+                                    <MODE>cluster</MODE>
+                                    <SW_STORAGE_ES_CLUSTER_NODES>
+                                        
skywalking-e2e-container-${build.id}-elasticsearch:9200
+                                    </SW_STORAGE_ES_CLUSTER_NODES>
+                                    <SW_CLUSTER_ZK_HOST_PORT>
+                                        
skywalking-e2e-container-${build.id}-zookeeper:2181
+                                    </SW_CLUSTER_ZK_HOST_PORT>
+
+                                    <INSTRUMENTED_SERVICE_1>
+                                        ${service0.name}-${project.version}.jar
+                                    </INSTRUMENTED_SERVICE_1>
+                                    <INSTRUMENTED_SERVICE_1_OPTS>
+                                        
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800
+                                        -DSW_AGENT_NAME=${service0.name}
+                                    </INSTRUMENTED_SERVICE_1_OPTS>
+                                    <INSTRUMENTED_SERVICE_1_ARGS>
+                                        --server.port=9090
+                                    </INSTRUMENTED_SERVICE_1_ARGS>
+
+                                    <INSTRUMENTED_SERVICE_2>
+                                        ${service1.name}-${project.version}.jar
+                                    </INSTRUMENTED_SERVICE_2>
+                                    <INSTRUMENTED_SERVICE_2_OPTS>
+                                        
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11801
+                                        -DSW_AGENT_NAME=${service1.name}
+                                    </INSTRUMENTED_SERVICE_2_OPTS>
+                                    <INSTRUMENTED_SERVICE_2_ARGS>
+                                        --server.port=9091
+                                    </INSTRUMENTED_SERVICE_2_ARGS>
+                                </env>
+                                <dependsOn>
+                                    
<container>skywalking-e2e-container-${build.id}-elasticsearch</container>
+                                    
<container>skywalking-e2e-container-${build.id}-zookeeper</container>
+                                </dependsOn>
+                                <ports>
+                                    <port>+webapp.host:webapp.port:8081</port>
+                                    
<port>+service.host:service.port:9091</port>
+                                </ports>
+                                <links>
+                                    
<link>skywalking-e2e-container-${build.id}-elasticsearch</link>
+                                    
<link>skywalking-e2e-container-${build.id}-zookeeper</link>
+                                </links>
+                                <volumes>
+                                    <bind>
+                                        <volume>
+                                            
../../../../dist/apache-skywalking-apm-bin:/sw
+                                        </volume>
+                                        <volume>
+                                            
../${service0.name}/target/${service0.name}-${project.version}.jar:/home/${service0.name}-${project.version}.jar
+                                        </volume>
+                                        <volume>
+                                            
../${service1.name}/target/${service1.name}-${project.version}.jar:/home/${service1.name}-${project.version}.jar
+                                        </volume>
+                                        <volume>
+                                            
${project.basedir}/src/docker/rc.d:/rc.d:ro
+                                        </volume>
+                                        <volume>
+                                            
${project.basedir}/src/docker/clusterize.awk:/clusterize.awk
+                                        </volume>
+                                    </bind>
+                                </volumes>
+                                <wait>
+                                    <log>SkyWalking e2e container is ready for 
tests</log>
+                                    <time>2400000</time>
+                                </wait>
+                            </run>
+                        </image>
+                    </images>
+                </configuration>
+            </plugin>
+
+            <!-- set the system properties that can be used in test codes -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <configuration>
+                    <systemPropertyVariables>
+                        <sw.webapp.host>${webapp.host}</sw.webapp.host>
+                        <sw.webapp.port>${webapp.port}</sw.webapp.port>
+                        <service.host>${service.host}</service.host>
+                        <service.port>${service.port}</service.port>
+                    </systemPropertyVariables>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/test/e2e/e2e-cluster/test-runner/src/docker/clusterize.awk 
b/test/e2e/e2e-cluster/test-runner/src/docker/clusterize.awk
new file mode 100644
index 0000000..870e43b
--- /dev/null
+++ b/test/e2e/e2e-cluster/test-runner/src/docker/clusterize.awk
@@ -0,0 +1,92 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#!/usr/bin/awk -f
+
+BEGIN {
+    in_cluster_section=0;
+    in_cluster_zk_section=0;
+
+    in_storage_section=0;
+    in_storage_es_section=0;
+    in_storage_h2_section=0;
+}
+
+{
+    if (in_cluster_section == 0) {
+        in_cluster_section=$0 ~ /^cluster:$/
+    } else {
+        in_cluster_section=$0 ~ /^(#|\s{2})/
+    }
+    if (in_storage_section == 0) {
+        in_storage_section=$0 ~ /^storage:$/
+    } else {
+        in_storage_section=$0 ~ /^(#|\s{2})/
+    }
+
+    if (in_cluster_section == 1) {
+        # in the cluster: section now
+        # disable standalone module
+        if ($0 ~ /^  standalone:$/) {
+            print "#" $0
+        } else {
+            if (in_cluster_zk_section == 0) {
+                in_cluster_zk_section=$0 ~ /^#?\s+zookeeper:$/
+            } else {
+                in_cluster_zk_section=$0 ~ /^(#\s{4}|\s{2})/
+            }
+            if (in_cluster_zk_section == 1) {
+                # in the cluster.zookeeper section now
+                # uncomment zk config
+                gsub("^#", "", $0)
+                print
+            } else {
+                print
+            }
+        }
+    } else if (in_storage_section == 1) {
+        # in the storage: section now
+        # disable h2 module
+        if (in_storage_es_section == 0) {
+            in_storage_es_section=$0 ~ /^#?\s+elasticsearch:$/
+        } else {
+            in_storage_es_section=$0 ~ /^#?\s{4}/
+        }
+        if (in_storage_h2_section == 0) {
+            in_storage_h2_section=$0 ~ /^#?\s+h2:$/
+        } else {
+            in_storage_h2_section=$0 ~ /^#?\s{4}/
+        }
+        if (in_storage_es_section == 1) {
+            # in the storage.elasticsearch section now
+            # uncomment es config
+            gsub("^#", "", $0)
+            print
+        } else if (in_storage_h2_section == 1) {
+            # comment out h2 config
+            if ($0 !~ /^#/) {
+                print "#" $0
+            } else {
+                print
+            }
+        } else {
+            print
+        }
+    } else {
+        print
+    }
+}
+
diff --git a/test/e2e/e2e-cluster/test-runner/src/docker/rc.d/rc0-prepare.sh 
b/test/e2e/e2e-cluster/test-runner/src/docker/rc.d/rc0-prepare.sh
new file mode 100755
index 0000000..1723c9b
--- /dev/null
+++ b/test/e2e/e2e-cluster/test-runner/src/docker/rc.d/rc0-prepare.sh
@@ -0,0 +1,32 @@
+# Licensed to the SkyAPM under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#!/usr/bin/env bash
+
+if test "${MODE}" = "cluster"; then
+    original_wd=$(pwd)
+
+    # substitute application.yml to be capable of cluster mode
+    cd ${SW_HOME}/config \
+        && awk -f /clusterize.awk application.yml > clusterized_app.yml \
+        && mv clusterized_app.yml application.yml
+
+    cd ${SW_HOME}/webapp \
+        && awk '/^\s+listOfServers/ {gsub("127.0.0.1:12800", 
"127.0.0.1:12800,127.0.0.1:12801", $0)} {print}' webapp.yml > 
clusterized_webapp.yml \
+        && mv clusterized_webapp.yml webapp.yml
+
+    cd ${original_wd}
+fi
diff --git a/test/e2e/e2e-cluster/test-runner/src/docker/rc.d/rc1-startup.sh 
b/test/e2e/e2e-cluster/test-runner/src/docker/rc.d/rc1-startup.sh
new file mode 100755
index 0000000..9e95aec
--- /dev/null
+++ b/test/e2e/e2e-cluster/test-runner/src/docker/rc.d/rc1-startup.sh
@@ -0,0 +1,73 @@
+# Licensed to the SkyAPM under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#!/usr/bin/env bash
+
+echo 'starting OAP server...' \
+    && SW_STORAGE_ES_BULK_ACTIONS=1 \
+    && SW_STORAGE_ES_FLUSH_INTERVAL=1 \
+    && start_oap 'init'
+
+echo 'starting Web app...' \
+    && start_webapp '0.0.0.0' 8081
+
+if test "${MODE}" = "cluster"; then
+    # start another OAP server in a different port
+    echo 'starting OAP server...' \
+        && SW_CORE_GRPC_PORT=11801 \
+        && SW_CORE_REST_PORT=12801 \
+        && SW_STORAGE_ES_BULK_ACTIONS=1 \
+        && SW_STORAGE_ES_FLUSH_INTERVAL=1 \
+        && start_oap 'no-init'
+
+    # start another WebApp server in a different port
+    echo 'starting Web app...' \
+        && start_webapp '0.0.0.0' 8082
+fi
+
+echo 'starting instrumented services...' && start_instrumented_services
+
+check_tcp 127.0.0.1 \
+          9090 \
+          60 \
+          10 \
+          "waiting for the instrumented service 0 to be ready"
+
+if [[ $? -ne 0 ]]; then
+    echo "instrumented service 0 failed to start in 30 * 10 seconds: "
+    cat ${SERVICE_LOG}/*
+    exit 1
+fi
+
+check_tcp 127.0.0.1 \
+          9091 \
+          60 \
+          10 \
+          "waiting for the instrumented service 1 to be ready"
+
+if [[ $? -ne 0 ]]; then
+    echo "instrumented service 1 failed to start in 24 * 10 seconds: "
+    cat ${SERVICE_LOG}/*
+    exit 1
+fi
+
+echo "SkyWalking e2e container is ready for tests"
+
+tail -f ${OAP_LOG_DIR}/* \
+        ${WEBAPP_LOG_DIR}/* \
+        ${SERVICE_LOG}/* \
+        ${ES_HOME}/logs/elasticsearch.log \
+        ${ES_HOME}/logs/stdout.log
diff --git 
a/test/e2e/e2e-cluster/test-runner/src/test/java/org/apache/skywalking/e2e/ClusterVerificationITCase.java
 
b/test/e2e/e2e-cluster/test-runner/src/test/java/org/apache/skywalking/e2e/ClusterVerificationITCase.java
new file mode 100644
index 0000000..eb45ef0
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/test-runner/src/test/java/org/apache/skywalking/e2e/ClusterVerificationITCase.java
@@ -0,0 +1,348 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.e2e;
+
+import org.apache.skywalking.e2e.metrics.AtLeastOneOfMetricsMatcher;
+import org.apache.skywalking.e2e.metrics.Metrics;
+import org.apache.skywalking.e2e.metrics.MetricsQuery;
+import org.apache.skywalking.e2e.metrics.MetricsValueMatcher;
+import org.apache.skywalking.e2e.service.Service;
+import org.apache.skywalking.e2e.service.ServicesMatcher;
+import org.apache.skywalking.e2e.service.ServicesQuery;
+import org.apache.skywalking.e2e.service.endpoint.Endpoint;
+import org.apache.skywalking.e2e.service.endpoint.EndpointQuery;
+import org.apache.skywalking.e2e.service.endpoint.Endpoints;
+import org.apache.skywalking.e2e.service.endpoint.EndpointsMatcher;
+import org.apache.skywalking.e2e.service.instance.Instance;
+import org.apache.skywalking.e2e.service.instance.Instances;
+import org.apache.skywalking.e2e.service.instance.InstancesMatcher;
+import org.apache.skywalking.e2e.service.instance.InstancesQuery;
+import org.apache.skywalking.e2e.topo.TopoData;
+import org.apache.skywalking.e2e.topo.TopoMatcher;
+import org.apache.skywalking.e2e.topo.TopoQuery;
+import org.apache.skywalking.e2e.trace.Trace;
+import org.apache.skywalking.e2e.trace.TracesMatcher;
+import org.apache.skywalking.e2e.trace.TracesQuery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.web.client.RestTemplate;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.InputStream;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.ALL_ENDPOINT_METRICS;
+import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.ALL_INSTANCE_METRICS;
+import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.ALL_SERVICE_METRICS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author kezhenxu94
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+public class ClusterVerificationITCase {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(ClusterVerificationITCase.class);
+
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final Yaml yaml = new Yaml();
+
+    private SimpleQueryClient queryClient;
+    private String instrumentedServiceUrl;
+    private long retryInterval = TimeUnit.SECONDS.toMillis(30);
+
+    @Before
+    public void setUp() {
+        final String swWebappHost = System.getProperty("sw.webapp.host", 
"127.0.0.1");
+        final String swWebappPort = System.getProperty("sw.webapp.port", 
"32791");
+        final String instrumentedServiceHost = 
System.getProperty("service.host", "127.0.0.1");
+        final String instrumentedServicePort = 
System.getProperty("service.port", "32790");
+        queryClient = new SimpleQueryClient(swWebappHost, swWebappPort);
+        instrumentedServiceUrl = "http://"; + instrumentedServiceHost + ":" + 
instrumentedServicePort;
+    }
+
+    @Test(timeout = 1200000)
+    @DirtiesContext
+    public void verify() throws Exception {
+        LocalDateTime startTime = LocalDateTime.now(ZoneOffset.UTC);
+
+        final Map<String, String> user = new HashMap<>();
+        user.put("name", "SkyWalking");
+        List<Service> services = Collections.emptyList();
+        while (services.size() < 2) {
+            try {
+                restTemplate.postForEntity(
+                    instrumentedServiceUrl + "/e2e/users",
+                    user,
+                    String.class
+                );
+                services = queryClient.services(
+                    new ServicesQuery()
+                        .start(startTime)
+                        .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+                );
+            } catch (Throwable ignored) {
+            }
+        }
+
+        final ResponseEntity<String> responseEntity = 
restTemplate.postForEntity(
+            instrumentedServiceUrl + "/e2e/users",
+            user,
+            String.class
+        );
+        LOGGER.info("responseEntity: {}, {}", responseEntity.getStatusCode(), 
responseEntity.getBody());
+        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+        verifyTraces(startTime);
+
+        verifyServices(startTime);
+
+        verifyTopo(startTime);
+    }
+
+    private void verifyTopo(LocalDateTime minutesAgo) throws Exception {
+        final TopoData topoData = queryClient.topo(
+            new TopoQuery()
+                .stepByMinute()
+                .start(minutesAgo)
+                .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+        );
+
+        InputStream expectedInputStream =
+            new 
ClassPathResource("expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.topo.yml").getInputStream();
+
+        final TopoMatcher topoMatcher = yaml.loadAs(expectedInputStream, 
TopoMatcher.class);
+        topoMatcher.verify(topoData);
+    }
+
+    private void verifyServices(LocalDateTime minutesAgo) throws Exception {
+        List<Service> services = queryClient.services(
+            new ServicesQuery()
+                .start(minutesAgo)
+                .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+        );
+        while (services.isEmpty()) {
+            LOGGER.warn("services is null, will retry to query");
+            services = queryClient.services(
+                new ServicesQuery()
+                    .start(minutesAgo)
+                    .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+            );
+            Thread.sleep(retryInterval);
+        }
+
+        InputStream expectedInputStream =
+            new 
ClassPathResource("expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.services.yml").getInputStream();
+
+        final ServicesMatcher servicesMatcher = 
yaml.loadAs(expectedInputStream, ServicesMatcher.class);
+        servicesMatcher.verify(services);
+
+        for (Service service : services) {
+            LOGGER.info("verifying service instances: {}", service);
+
+            verifyServiceMetrics(service, minutesAgo);
+
+            Instances instances = verifyServiceInstances(minutesAgo, service);
+
+            verifyInstancesMetrics(instances, minutesAgo);
+
+            Endpoints endpoints = verifyServiceEndpoints(minutesAgo, service);
+
+            verifyEndpointsMetrics(endpoints, minutesAgo);
+        }
+    }
+
+    private Instances verifyServiceInstances(LocalDateTime minutesAgo, Service 
service) throws Exception {
+        Instances instances = queryClient.instances(
+            new InstancesQuery()
+                .serviceId(service.getKey())
+                .start(minutesAgo)
+                .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+        );
+        while (instances == null) {
+            LOGGER.warn("instances is null, will retry to query");
+            instances = queryClient.instances(
+                new InstancesQuery()
+                    .serviceId(service.getKey())
+                    .start(minutesAgo)
+                    .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+            );
+            Thread.sleep(retryInterval);
+        }
+        InputStream expectedInputStream =
+            new 
ClassPathResource("expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.instances.yml").getInputStream();
+        final InstancesMatcher instancesMatcher = 
yaml.loadAs(expectedInputStream, InstancesMatcher.class);
+        instancesMatcher.verify(instances);
+        return instances;
+    }
+
+    private Endpoints verifyServiceEndpoints(LocalDateTime minutesAgo, Service 
service) throws Exception {
+        Endpoints endpoints = queryClient.endpoints(
+            new EndpointQuery().serviceId(service.getKey())
+        );
+        while (endpoints == null) {
+            LOGGER.warn("endpoints is null, will retry to query");
+            endpoints = queryClient.endpoints(
+                new EndpointQuery().serviceId(service.getKey())
+            );
+            Thread.sleep(retryInterval);
+        }
+        InputStream expectedInputStream =
+            new 
ClassPathResource("expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.endpoints.yml").getInputStream();
+        final EndpointsMatcher endpointsMatcher = 
yaml.loadAs(expectedInputStream, EndpointsMatcher.class);
+        endpointsMatcher.verify(endpoints);
+        return endpoints;
+    }
+
+    private void verifyInstancesMetrics(Instances instances, final 
LocalDateTime minutesAgo) throws Exception {
+        for (Instance instance : instances.getInstances()) {
+            for (String metricsName : ALL_INSTANCE_METRICS) {
+                LOGGER.info("verifying service instance response time: {}", 
instance);
+
+                boolean matched = false;
+                while (!matched) {
+                    LOGGER.warn("instanceRespTime is null, will retry to 
query");
+                    Metrics instanceRespTime = queryClient.metrics(
+                            new MetricsQuery()
+                                .stepByMinute()
+                                .metricsName(metricsName)
+                                .start(minutesAgo)
+                                
.end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+                                .id(instance.getKey())
+                        );
+                    AtLeastOneOfMetricsMatcher instanceRespTimeMatcher = new 
AtLeastOneOfMetricsMatcher();
+                    MetricsValueMatcher greaterThanZero = new 
MetricsValueMatcher();
+                    greaterThanZero.setValue("gt 0");
+                    instanceRespTimeMatcher.setValue(greaterThanZero);
+                    try {
+                        instanceRespTimeMatcher.verify(instanceRespTime);
+                        matched = true;
+                    } catch (Throwable ignored) {
+                        Thread.sleep(retryInterval);
+                    }
+                    LOGGER.info("{}: {}", metricsName, instanceRespTime);
+                }
+            }
+        }
+    }
+
+    private void verifyEndpointsMetrics(Endpoints endpoints, final 
LocalDateTime minutesAgo) throws Exception {
+        for (Endpoint endpoint : endpoints.getEndpoints()) {
+            if (!endpoint.getLabel().equals("/e2e/users")) {
+                continue;
+            }
+            for (String metricName : ALL_ENDPOINT_METRICS) {
+                LOGGER.info("verifying endpoint {}, metrics: {}", endpoint, 
metricName);
+
+                boolean matched = false;
+                while (!matched) {
+                    LOGGER.warn("serviceMetrics is null, will retry to query");
+                    Metrics metrics = queryClient.metrics(
+                        new MetricsQuery()
+                            .stepByMinute()
+                            .metricsName(metricName)
+                            .start(minutesAgo)
+                            
.end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+                            .id(endpoint.getKey())
+                    );
+                    AtLeastOneOfMetricsMatcher instanceRespTimeMatcher = new 
AtLeastOneOfMetricsMatcher();
+                    MetricsValueMatcher greaterThanZero = new 
MetricsValueMatcher();
+                    greaterThanZero.setValue("gt 0");
+                    instanceRespTimeMatcher.setValue(greaterThanZero);
+                    try {
+                        instanceRespTimeMatcher.verify(metrics);
+                        matched = true;
+                    } catch (Throwable ignored) {
+                        Thread.sleep(retryInterval);
+                    }
+                    LOGGER.info("metrics: {}", metrics);
+                }
+            }
+        }
+    }
+
+    private void verifyServiceMetrics(Service service, final LocalDateTime 
minutesAgo) throws Exception {
+        for (String metricName : ALL_SERVICE_METRICS) {
+            LOGGER.info("verifying service {}, metrics: {}", service, 
metricName);
+
+            boolean matched = false;
+            while (!matched) {
+                Metrics serviceMetrics = queryClient.metrics(
+                    new MetricsQuery()
+                        .stepByMinute()
+                        .metricsName(metricName)
+                        .start(minutesAgo)
+                        .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+                        .id(service.getKey())
+                );
+                AtLeastOneOfMetricsMatcher instanceRespTimeMatcher = new 
AtLeastOneOfMetricsMatcher();
+                MetricsValueMatcher greaterThanZero = new 
MetricsValueMatcher();
+                greaterThanZero.setValue("gt 0");
+                instanceRespTimeMatcher.setValue(greaterThanZero);
+                try {
+                    instanceRespTimeMatcher.verify(serviceMetrics);
+                    matched = true;
+                } catch (Throwable ignored) {
+                    Thread.sleep(retryInterval);
+                }
+                LOGGER.info("serviceMetrics: {}", serviceMetrics);
+            }
+        }
+    }
+
+    private void verifyTraces(LocalDateTime minutesAgo) throws Exception {
+        List<Trace> traces = queryClient.traces(
+            new TracesQuery()
+                .stepBySecond()
+                .start(minutesAgo)
+                .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+                .orderByStartTime()
+        );
+        while (traces.isEmpty()) {
+            LOGGER.warn("traces is empty, will retry to query");
+            traces = queryClient.traces(
+                new TracesQuery()
+                    .stepBySecond()
+                    .start(minutesAgo)
+                    .end(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(1))
+                    .orderByStartTime()
+            );
+            Thread.sleep(retryInterval);
+        }
+
+        InputStream expectedInputStream =
+            new 
ClassPathResource("expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.traces.yml").getInputStream();
+
+        final TracesMatcher tracesMatcher = yaml.loadAs(expectedInputStream, 
TracesMatcher.class);
+        tracesMatcher.verifyLoosely(traces);
+    }
+}
diff --git 
a/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.endpoints.yml
 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.endpoints.yml
new file mode 100644
index 0000000..3ddf42e
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.endpoints.yml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 1 health-check by docker-maven-plugin
+# 1 drop table if exists, because we have `ddl-auto: create-drop`
+# 1 drop sequence
+# 1 create sequence
+# 1 create table statement
+
+endpoints:
+  - key: not null
+    label: /e2e/users
diff --git 
a/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.instances.yml
 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.instances.yml
new file mode 100644
index 0000000..1c48fc4
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.instances.yml
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 1 health-check by docker-maven-plugin
+# 1 drop table if exists, because we have `ddl-auto: create-drop`
+# 1 drop sequence
+# 1 create sequence
+# 1 create table statement
+
+instances:
+  - key: gt 0
+    label: not null
+    attributes:
+      - name: os_name
+        value: not null
+      - name: host_name
+        value: not null
+      - name: process_no
+        value: gt 0
+      - name: ipv4s
+        value: not null
diff --git 
a/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.services.yml
 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.services.yml
new file mode 100644
index 0000000..f0fc184
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.services.yml
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 1 health-check by docker-maven-plugin
+# 1 drop table if exists, because we have `ddl-auto: create-drop`
+# 1 drop sequence
+# 1 create sequence
+# 1 create table statement
+
+services:
+  - key: not null
+    label: provider
+
+  - key: not null
+    label: consumer
diff --git 
a/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.topo.yml
 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.topo.yml
new file mode 100644
index 0000000..9f74cfc
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.topo.yml
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+nodes:
+  - id: not null
+    name: User
+    type: USER
+  - id: not null
+    name: provider
+    type: Tomcat
+  - id: not null
+    name: consumer
+    type: Tomcat
+  - id: not null
+    name: "localhost:-1"
+    type: H2
+calls:
+  - id: not null
+    source: not null
+    target: not null
+  - id: not null
+    source: not null
+    target: not null
+  - id: not null
+    source: not null
+    target: not null
diff --git 
a/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.traces.yml
 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.traces.yml
new file mode 100644
index 0000000..4542e5d
--- /dev/null
+++ 
b/test/e2e/e2e-cluster/test-runner/src/test/resources/expected-data/org.apache.skywalking.e2e.ClusterVerificationITCase.traces.yml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+traces:
+  - key: not null
+    endpointNames:
+      - /e2e/users
+    duration: ge 0
+    start: gt 0
+    isError: false
+    traceIds:
+      - not null
diff --git a/test/e2e/e2e-single-service/pom.xml 
b/test/e2e/e2e-single-service/pom.xml
index 866ef62..32e1d6d 100644
--- a/test/e2e/e2e-single-service/pom.xml
+++ b/test/e2e/e2e-single-service/pom.xml
@@ -72,7 +72,7 @@
                 <groupId>io.fabric8</groupId>
                 <artifactId>docker-maven-plugin</artifactId>
                 <configuration>
-                    <containerNamePattern>%a-%t</containerNamePattern>
+                    <containerNamePattern>%a-%t-%i</containerNamePattern>
                     <images>
                         <image>
                             
<name>skyapm/e2e-container:${e2e.container.version}</name>
diff --git 
a/test/e2e/e2e-single-service/src/test/java/org/apache/skywalking/e2e/SampleVerificationITCase.java
 
b/test/e2e/e2e-single-service/src/test/java/org/apache/skywalking/e2e/SampleVerificationITCase.java
index cb04459..883ba14 100644
--- 
a/test/e2e/e2e-single-service/src/test/java/org/apache/skywalking/e2e/SampleVerificationITCase.java
+++ 
b/test/e2e/e2e-single-service/src/test/java/org/apache/skywalking/e2e/SampleVerificationITCase.java
@@ -59,19 +59,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.ENDPOINT_P50;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.ENDPOINT_P75;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.ENDPOINT_P90;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.ENDPOINT_P95;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.ENDPOINT_P99;
-import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_INSTANCE_CPM;
-import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_INSTANCE_RESP_TIME;
-import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_INSTANCE_SLA;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_P50;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_P75;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_P90;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_P95;
-import static org.apache.skywalking.e2e.metrics.MetricsQuery.SERVICE_P99;
+import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.ALL_ENDPOINT_METRICS;
+import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.ALL_INSTANCE_METRICS;
+import static 
org.apache.skywalking.e2e.metrics.MetricsQuery.ALL_SERVICE_METRICS;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
@@ -82,6 +72,8 @@ public class SampleVerificationITCase {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(SampleVerificationITCase.class);
 
     private final RestTemplate restTemplate = new RestTemplate();
+    private final int retryTimes = 5;
+    private final int retryInterval = 30;
 
     private SimpleQueryClient queryClient;
     private String instrumentedServiceUrl;
@@ -90,11 +82,10 @@ public class SampleVerificationITCase {
     public void setUp() {
         final String swWebappHost = System.getProperty("sw.webapp.host", 
"127.0.0.1");
         final String swWebappPort = System.getProperty("sw.webapp.port", 
"32783");
-        final String instrumentedServiceHost0 = 
System.getProperty("client.host", "127.0.0.1");
-        final String instrumentedServicePort0 = 
System.getProperty("client.port", "32782");
-        final String queryClientUrl = "http://"; + swWebappHost + ":" + 
swWebappPort + "/graphql";
-        queryClient = new SimpleQueryClient(queryClientUrl);
-        instrumentedServiceUrl = "http://"; + instrumentedServiceHost0 + ":" + 
instrumentedServicePort0;
+        final String instrumentedServiceHost = 
System.getProperty("client.host", "127.0.0.1");
+        final String instrumentedServicePort = 
System.getProperty("client.port", "32782");
+        queryClient = new SimpleQueryClient(swWebappHost, swWebappPort);
+        instrumentedServiceUrl = "http://"; + instrumentedServiceHost + ":" + 
instrumentedServicePort;
     }
 
     @Test
@@ -111,13 +102,29 @@ public class SampleVerificationITCase {
         );
         assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
 
-        Thread.sleep(5000);
-
-        verifyTraces(minutesAgo);
+        doRetryableVerification(() -> {
+            try {
+                verifyTraces(minutesAgo);
+            } catch (Exception e) {
+                LOGGER.warn(e.getMessage(), e);
+            }
+        });
 
-        verifyServices(minutesAgo);
+        doRetryableVerification(() -> {
+            try {
+                verifyServices(minutesAgo);
+            } catch (Exception e) {
+                LOGGER.warn(e.getMessage(), e);
+            }
+        });
 
-        verifyTopo(minutesAgo);
+        doRetryableVerification(() -> {
+            try {
+                verifyTopo(minutesAgo);
+            } catch (Exception e) {
+                LOGGER.warn(e.getMessage(), e);
+            }
+        });
     }
 
     private void verifyTopo(LocalDateTime minutesAgo) throws Exception {
@@ -125,7 +132,7 @@ public class SampleVerificationITCase {
 
         final TopoData topoData = queryClient.topo(
             new TopoQuery()
-                .step("MINUTE")
+                .stepByMinute()
                 .start(minutesAgo.minusDays(1))
                 .end(now)
         );
@@ -194,17 +201,12 @@ public class SampleVerificationITCase {
     }
 
     private void verifyInstancesMetrics(Instances instances) throws Exception {
-        final String[] instanceMetricsNames = new String[] {
-            SERVICE_INSTANCE_RESP_TIME,
-            SERVICE_INSTANCE_CPM,
-            SERVICE_INSTANCE_SLA
-        };
         for (Instance instance : instances.getInstances()) {
-            for (String metricsName : instanceMetricsNames) {
+            for (String metricsName : ALL_INSTANCE_METRICS) {
                 LOGGER.info("verifying service instance response time: {}", 
instance);
                 final Metrics instanceRespTime = queryClient.metrics(
                     new MetricsQuery()
-                        .step("MINUTE")
+                        .stepByMinute()
                         .metricsName(metricsName)
                         .id(instance.getKey())
                 );
@@ -219,22 +221,15 @@ public class SampleVerificationITCase {
     }
 
     private void verifyEndpointsMetrics(Endpoints endpoints) throws Exception {
-        final String[] endpointMetricsNames = {
-            ENDPOINT_P99,
-            ENDPOINT_P95,
-            ENDPOINT_P90,
-            ENDPOINT_P75,
-            ENDPOINT_P50
-        };
         for (Endpoint endpoint : endpoints.getEndpoints()) {
             if (!endpoint.getLabel().equals("/e2e/users")) {
                 continue;
             }
-            for (String metricName : endpointMetricsNames) {
+            for (String metricName : ALL_ENDPOINT_METRICS) {
                 LOGGER.info("verifying endpoint {}, metrics: {}", endpoint, 
metricName);
                 final Metrics metrics = queryClient.metrics(
                     new MetricsQuery()
-                        .step("MINUTE")
+                        .stepByMinute()
                         .metricsName(metricName)
                         .id(endpoint.getKey())
                 );
@@ -249,18 +244,11 @@ public class SampleVerificationITCase {
     }
 
     private void verifyServiceMetrics(Service service) throws Exception {
-        final String[] serviceMetrics = {
-            SERVICE_P99,
-            SERVICE_P95,
-            SERVICE_P90,
-            SERVICE_P75,
-            SERVICE_P50
-        };
-        for (String metricName : serviceMetrics) {
+        for (String metricName : ALL_SERVICE_METRICS) {
             LOGGER.info("verifying service {}, metrics: {}", service, 
metricName);
             final Metrics instanceRespTime = queryClient.metrics(
                 new MetricsQuery()
-                    .step("MINUTE")
+                    .stepByMinute()
                     .metricsName(metricName)
                     .id(service.getKey())
             );
@@ -289,4 +277,15 @@ public class SampleVerificationITCase {
         final TracesMatcher tracesMatcher = new 
Yaml().loadAs(expectedInputStream, TracesMatcher.class);
         tracesMatcher.verify(traces);
     }
+
+    private void doRetryableVerification(Runnable runnable) throws 
InterruptedException {
+        for (int i = 0; i < retryTimes; i++) {
+            try {
+                runnable.run();
+                break;
+            } catch (Throwable ignored) {
+                Thread.sleep(retryInterval);
+            }
+        }
+    }
 }
diff --git a/test/e2e/pom.xml b/test/e2e/pom.xml
index f914a73..ae5dcf4 100644
--- a/test/e2e/pom.xml
+++ b/test/e2e/pom.xml
@@ -34,6 +34,7 @@
     <modules>
         <module>e2e-base</module>
         <module>e2e-single-service</module>
+        <module>e2e-cluster</module>
     </modules>
 
     <properties>
@@ -51,6 +52,9 @@
         <snake.version>1.23</snake.version>
         <gson.version>2.8.5</gson.version>
         <h2.version>1.4.199</h2.version>
+        <elasticsearch.version>6.3.2</elasticsearch.version>
+        <zookeeper.image.version>3.5</zookeeper.image.version>
+        <lombok.version>1.18.4</lombok.version>
 
         <!-- build.id is an available environment variable in Jenkins to
              distinguish the different build jobs, once Jenkins job is aborted,
@@ -64,6 +68,9 @@
         <maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
         <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
         <docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>
+        <surefire.version>2.12.4</surefire.version>
+
+        <skipSurefire>true</skipSurefire>
     </properties>
 
     <dependencies>
@@ -92,6 +99,12 @@
             <artifactId>guava</artifactId>
             <version>${guava.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${lombok.version}</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
     <build>
@@ -152,6 +165,14 @@
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${surefire.version}</version>
+                <configuration>
+                    <skip>${skipSurefire}</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-failsafe-plugin</artifactId>
                 <version>${maven-failsafe-plugin.version}</version>
                 <executions>

Reply via email to