You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ti...@apache.org on 2022/01/12 15:12:41 UTC

[ignite] branch master updated: IGNITE-12464 : Metrics for the Java Services V3. (#9457)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new ed5fd0e  IGNITE-12464 : Metrics for the Java Services V3. (#9457)
ed5fd0e is described below

commit ed5fd0e0721e2e799987a200a279ab96eeb6dca9
Author: Vladimir Steshin <vl...@gmail.com>
AuthorDate: Wed Jan 12 18:12:03 2022 +0300

    IGNITE-12464 : Metrics for the Java Services V3. (#9457)
---
 .../ignite/snippets/services/ServiceExample.java   |   8 +-
 .../java/org/apache/ignite/IgniteServices.java     |   4 +-
 .../processors/service/GridServiceProxy.java       |  49 +++-
 .../processors/service/IgniteServiceProcessor.java |  90 +++++-
 .../service/LazyServiceConfiguration.java          |   1 +
 .../processors/service/ServiceContextImpl.java     |  35 ++-
 .../apache/ignite/internal/util/IgniteUtils.java   |  19 ++
 .../ignite/services/ServiceConfiguration.java      |  36 +++
 .../processors/service/GridServiceMetricsTest.java | 323 +++++++++++++++++++++
 .../GridServiceProcessorAbstractSelfTest.java      |  23 +-
 .../service/GridServiceProcessorProxySelfTest.java | 184 ++++++++----
 .../processors/service/inner/MyService.java        |   7 +
 .../testsuites/IgniteServiceGridTestSuite.java     |   2 +
 13 files changed, 691 insertions(+), 90 deletions(-)

diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
index d7cd753..a1658f1 100644
--- a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
@@ -50,11 +50,11 @@ public class ServiceExample {
 
         // Print the latest counter value from our counter service.
         System.out.println("Incremented value : " + counterService.get());
-        
+
         //tag::undeploy[]
         services.cancel("myCounterService");
         //end::undeploy[]
-        
+
         ignite.close();
     }
 
@@ -63,7 +63,7 @@ public class ServiceExample {
         //tag::deploy-with-cluster-group[]
         Ignite ignite = Ignition.start();
 
-        //deploy the service to the nodes that host the cache named "myCache" 
+        //deploy the service to the nodes that host the cache named "myCache"
         ignite.services(ignite.cluster().forCacheNodes("myCache"));
 
         //end::deploy-with-cluster-group[]
@@ -83,7 +83,7 @@ public class ServiceExample {
 
     @Test
     void affinityKey() {
-        
+
         //tag::deploy-by-key[]
         Ignite ignite = Ignition.start();
 
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteServices.java b/modules/core/src/main/java/org/apache/ignite/IgniteServices.java
index 769d322..d5acb38 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteServices.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteServices.java
@@ -589,7 +589,7 @@ public interface IgniteServices extends IgniteAsyncSupport {
      * @param sticky Whether or not Ignite should always contact the same remote
      *      service or try to load-balance between services.
      * @param <T> Service type.
-     * @return Either proxy over remote service or local service if it is deployed locally.
+     * @return Proxy over service.
      * @throws IgniteException If failed to create service proxy.
      */
     public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky) throws IgniteException;
@@ -606,7 +606,7 @@ public interface IgniteServices extends IgniteAsyncSupport {
      * @param timeout If greater than 0 created proxy will wait for service availability only specified time,
      *  and will limit remote service invocation time.
      * @param <T> Service type.
-     * @return Either proxy over remote service or local service if it is deployed locally.
+     * @return Proxy over service.
      * @throws IgniteException If failed to create service proxy.
      */
     public <T> T serviceProxy(String name, Class<? super T> svcItf, boolean sticky, long timeout)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java
index f2d4886..8f60b2a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProxy.java
@@ -32,6 +32,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
@@ -47,6 +48,7 @@ import org.apache.ignite.internal.binary.BinaryArray;
 import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
 import org.apache.ignite.internal.managers.communication.GridIoPolicy;
+import org.apache.ignite.internal.processors.metric.impl.HistogramMetricImpl;
 import org.apache.ignite.internal.processors.platform.PlatformNativeException;
 import org.apache.ignite.internal.processors.platform.services.PlatformService;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
@@ -205,8 +207,13 @@ public class GridServiceProxy<T> implements Serializable {
                         if (svcCtx != null) {
                             Service svc = svcCtx.service();
 
-                            if (svc != null)
-                                return callServiceLocally(svc, mtd, args, callAttrs);
+                            if (svc != null) {
+                                HistogramMetricImpl hist = svcCtx.isStatisticsEnabled() ?
+                                    svcCtx.metrics().findMetric(mtd.getName()) : null;
+
+                                return hist == null ? callServiceLocally(svc, mtd, args, callAttrs) :
+                                    measureCall(hist, () -> callServiceLocally(svc, mtd, args, callAttrs));
+                            }
                         }
                     }
                     else {
@@ -447,13 +454,32 @@ public class GridServiceProxy<T> implements Serializable {
     /**
      * @param mtd Method to invoke.
      */
-    String methodName(Method mtd) {
+    private static String methodName(Method mtd) {
         PlatformServiceMethod ann = mtd.getDeclaredAnnotation(PlatformServiceMethod.class);
 
         return ann == null ? mtd.getName() : ann.value();
     }
 
     /**
+     * Calls the target, measures and registers its duration.
+     *
+     * @param histogram Related metric.
+     * @param target    Target to call and measure.
+     */
+    private static <T> T measureCall(
+            HistogramMetricImpl histogram,
+            Callable<T> target
+    ) throws Exception {
+        long startTime = System.nanoTime();
+
+        try {
+            return target.call();
+        } finally {
+            histogram.value(System.nanoTime() - startTime);
+        }
+    }
+
+    /**
      * Invocation handler for service proxy.
      */
     private class ProxyInvocationHandler implements InvocationHandler {
@@ -538,17 +564,22 @@ public class GridServiceProxy<T> implements Serializable {
 
             Method mtd = ctx.method(key);
 
-            Object res;
+            HistogramMetricImpl hist = ctx.isStatisticsEnabled() ? ctx.metrics().findMetric(mtd.getName()) : null;
 
-            if (ctx.service() instanceof PlatformService && mtd == null)
-                res = callPlatformService((PlatformService)ctx.service());
-            else
-                res = callService(ctx.service(), mtd);
+            Object res = hist == null ? callService(ctx, mtd) : measureCall(hist, () -> callService(ctx, mtd));
 
             return U.marshal(ignite.configuration().getMarshaller(), res);
         }
 
         /** */
+        private Object callService(ServiceContextImpl svcCtx, Method mtd) throws Exception {
+            if (svcCtx.service() instanceof PlatformService && mtd == null)
+                return callPlatformService((PlatformService)svcCtx.service());
+            else
+                return callOrdinaryService(svcCtx.service(), mtd);
+        }
+
+        /** */
         private Object callPlatformService(PlatformService srv) {
             try {
                 return srv.invokeMethod(mtdName, false, true, args, callAttrs);
@@ -562,7 +593,7 @@ public class GridServiceProxy<T> implements Serializable {
         }
 
         /** */
-        private Object callService(Service srv, Method mtd) throws Exception {
+        private Object callOrdinaryService(Service srv, Method mtd) throws Exception {
             if (mtd == null)
                 throw new GridServiceMethodNotFoundException(svcName, mtdName, argTypes);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
index 055fc58..6d7bdb2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.service;
 
+import java.io.Externalizable;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -67,6 +69,7 @@ import org.apache.ignite.internal.processors.cache.ValidationOnNodeJoinUtils;
 import org.apache.ignite.internal.processors.cluster.ChangeGlobalStateMessage;
 import org.apache.ignite.internal.processors.cluster.DiscoveryDataClusterState;
 import org.apache.ignite.internal.processors.cluster.IgniteChangeGlobalStateSupport;
+import org.apache.ignite.internal.processors.metric.MetricRegistry;
 import org.apache.ignite.internal.processors.platform.services.PlatformService;
 import org.apache.ignite.internal.processors.security.OperationSecurityContext;
 import org.apache.ignite.internal.processors.security.SecurityContext;
@@ -93,17 +96,22 @@ import org.apache.ignite.spi.communication.CommunicationSpi;
 import org.apache.ignite.spi.discovery.DiscoveryDataBag;
 import org.apache.ignite.spi.discovery.DiscoverySpi;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
 import org.apache.ignite.spi.systemview.view.ServiceView;
 import org.apache.ignite.thread.IgniteThreadFactory;
 import org.apache.ignite.thread.OomExceptionHandler;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static org.apache.ignite.configuration.DeploymentMode.ISOLATED;
 import static org.apache.ignite.configuration.DeploymentMode.PRIVATE;
 import static org.apache.ignite.events.EventType.EVT_NODE_JOINED;
 import static org.apache.ignite.internal.GridComponent.DiscoveryDataExchangeType.SERVICE_PROC;
+import static org.apache.ignite.internal.processors.metric.impl.MetricUtils.metricName;
 import static org.apache.ignite.internal.processors.security.SecurityUtils.nodeSecurityContext;
+import static org.apache.ignite.internal.util.IgniteUtils.allInterfaces;
 import static org.apache.ignite.plugin.security.SecurityPermission.SERVICE_DEPLOY;
 
 /**
@@ -126,6 +134,21 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
     /** */
     public static final String SVCS_VIEW_DESC = "Services";
 
+    /** Base name domain for invocation metrics. */
+    private static final String SERVICE_METRIC_REGISTRY = "Services";
+
+    /** Description for the service method invocation metric. */
+    private static final String DESCRIPTION_OF_INVOCATION_METRIC_PREF = "Duration in milliseconds of ";
+
+    /** Default bounds of invocation histogram in nanoseconds. */
+    public static final long[] DEFAULT_INVOCATION_BOUNDS = new long[] {
+        NANOSECONDS.convert(1, MILLISECONDS),
+        NANOSECONDS.convert(10, MILLISECONDS),
+        NANOSECONDS.convert(50, MILLISECONDS),
+        NANOSECONDS.convert(200, MILLISECONDS),
+        NANOSECONDS.convert(1000, MILLISECONDS)
+    };
+
     /** Local service instances. */
     private final ConcurrentMap<IgniteUuid, Collection<ServiceContextImpl>> locServices = new ConcurrentHashMap<>();
 
@@ -316,6 +339,8 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
 
         deployedServices.clear();
 
+        ctx.metric().remove(SERVICE_METRIC_REGISTRY);
+
         locServices.values().stream().flatMap(Collection::stream).forEach(srvcCtx -> {
             cancel(srvcCtx);
 
@@ -1039,7 +1064,7 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
         if (hasLocalNode(prj)) {
             ServiceContextImpl ctx = serviceContext(name);
 
-            if (ctx != null) {
+            if (ctx != null && !ctx.isStatisticsEnabled()) {
                 Service srvc = ctx.service();
 
                 if (srvc != null && callAttrsProvider == null) {
@@ -1267,7 +1292,8 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
                         UUID.randomUUID(),
                         cacheName,
                         affKey,
-                        Executors.newSingleThreadExecutor(threadFactory));
+                        Executors.newSingleThreadExecutor(threadFactory),
+                        cfg.isStatisticsEnabled());
 
                     ctxs.add(srvcCtx);
 
@@ -1276,6 +1302,8 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
             }
         }
 
+        ReadOnlyMetricRegistry invocationMetrics = null;
+
         for (final ServiceContextImpl srvcCtx : toInit) {
             final Service srvc;
 
@@ -1302,6 +1330,13 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
                 log.info("Starting service instance [name=" + srvcCtx.name() + ", execId=" +
                     srvcCtx.executionId() + ']');
 
+            if (cfg.isStatisticsEnabled()) {
+                if (invocationMetrics == null)
+                    invocationMetrics = createServiceMetrics(srvcCtx);
+
+                srvcCtx.metrics(invocationMetrics);
+            }
+
             // Start service in its own thread.
             final ExecutorService exe = srvcCtx.executor();
 
@@ -1388,14 +1423,20 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
      * @param ctxs Contexts to cancel.
      * @param cancelCnt Number of contexts to cancel.
      */
-    private void cancel(Iterable<ServiceContextImpl> ctxs, int cancelCnt) {
+    private void cancel(Collection<ServiceContextImpl> ctxs, int cancelCnt) {
         for (Iterator<ServiceContextImpl> it = ctxs.iterator(); it.hasNext(); ) {
-            cancel(it.next());
+            ServiceContextImpl svcCtx = it.next();
+
+            cancel(svcCtx);
 
             it.remove();
 
-            if (--cancelCnt == 0)
+            if (--cancelCnt == 0) {
+                if (ctxs.isEmpty())
+                    ctx.metric().remove(serviceMetricRegistryName(svcCtx.name()));
+
                 break;
+            }
         }
     }
 
@@ -1966,4 +2007,43 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
 
         return null;
     }
+
+    /**
+     * Creates metrics registry for the invocation histograms.
+     *
+     * @param srvcCtx ServiceContext.
+     * @return Created metric registry.
+     */
+    private ReadOnlyMetricRegistry createServiceMetrics(ServiceContextImpl srvcCtx) {
+        MetricRegistry metricRegistry = ctx.metric().registry(serviceMetricRegistryName(srvcCtx.name()));
+
+        for (Class<?> itf : allInterfaces(srvcCtx.service().getClass())) {
+            for (Method mtd : itf.getMethods()) {
+                if (metricIgnored(mtd.getDeclaringClass()))
+                    continue;
+
+                metricRegistry.histogram(mtd.getName(), DEFAULT_INVOCATION_BOUNDS, DESCRIPTION_OF_INVOCATION_METRIC_PREF +
+                    '\'' + mtd.getName() + "()'");
+            }
+        }
+
+        return metricRegistry;
+    }
+
+    /**
+     * @return {@code True} if metrics should not be created for this class or interface.
+     */
+    private static boolean metricIgnored(Class<?> cls) {
+        return Service.class.equals(cls) || Externalizable.class.equals(cls) || PlatformService.class.equals(cls);
+    }
+
+    /**
+     * Gives proper name for service metric registry.
+     *
+     * @param srvcName Name of the service.
+     * @return registry name for service {@code srvcName}.
+     */
+    static String serviceMetricRegistryName(String srvcName) {
+        return metricName(SERVICE_METRIC_REGISTRY, srvcName);
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
index e0add26..7d34665 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/LazyServiceConfiguration.java
@@ -64,6 +64,7 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
         this.srvcBytes = srvcBytes;
         srvc = cfg.getService();
         srvcClsName = srvc.getClass().getName();
+        isStatisticsEnabled = cfg.isStatisticsEnabled();
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
index 38acfce..a5aa112 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceContextImpl.java
@@ -28,6 +28,7 @@ import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceCallContext;
 import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -60,6 +61,9 @@ public class ServiceContextImpl implements ServiceContext {
     /** Methods reflection cache. */
     private final ConcurrentMap<GridServiceMethodReflectKey, Method> mtds = new ConcurrentHashMap<>();
 
+    /** Invocation metrics. */
+    private ReadOnlyMetricRegistry metrics;
+
     /** Service. */
     @GridToStringExclude
     private volatile Service svc;
@@ -67,23 +71,29 @@ public class ServiceContextImpl implements ServiceContext {
     /** Cancelled flag. */
     private volatile boolean isCancelled;
 
+    /** Service statistics flag. */
+    private final boolean isStatisticsEnabled;
+
     /**
      * @param name Service name.
      * @param execId Execution ID.
      * @param cacheName Cache name.
      * @param affKey Affinity key.
      * @param exe Executor service.
+     * @param statisticsEnabled Service statistics flag.
      */
     ServiceContextImpl(String name,
         UUID execId,
         String cacheName,
         Object affKey,
-        ExecutorService exe) {
+        ExecutorService exe,
+        boolean statisticsEnabled) {
         this.name = name;
         this.execId = execId;
         this.cacheName = cacheName;
         this.affKey = affKey;
         this.exe = exe;
+        this.isStatisticsEnabled = statisticsEnabled;
     }
 
     /** {@inheritDoc} */
@@ -133,6 +143,29 @@ public class ServiceContextImpl implements ServiceContext {
     }
 
     /**
+     * @return Invocation metrics.
+     */
+    @Nullable ReadOnlyMetricRegistry metrics() {
+        return metrics;
+    }
+
+    /**
+     * Sets the invocation metrics.
+     *
+     * @return {@code this}.
+     */
+    ServiceContextImpl metrics(ReadOnlyMetricRegistry metrics) {
+        this.metrics = metrics;
+
+        return this;
+    }
+
+    /** @return {@code True} if statistics is enabled for this service. {@code False} otherwise. */
+    boolean isStatisticsEnabled() {
+        return isStatisticsEnabled;
+    }
+
+    /**
      * @param key Method key.
      * @return Method.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
index acacde4..89999a0 100755
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
@@ -6117,6 +6117,25 @@ public abstract class IgniteUtils {
     }
 
     /**
+     * Provides all interfaces of {@code cls} including inherited ones. Excludes duplicated ones in case of multiple
+     * inheritance.
+     *
+     * @param cls Class to search for interfaces.
+     * @return Collection of interfaces of {@code cls}.
+     */
+    public static Collection<Class<?>> allInterfaces(Class<?> cls) {
+        Set<Class<?>> interfaces = new HashSet<>();
+
+        while (cls != null) {
+            interfaces.addAll(Arrays.asList(cls.getInterfaces()));
+
+            cls = cls.getSuperclass();
+        }
+
+        return interfaces;
+    }
+
+    /**
      * Gets simple class name taking care of empty names.
      *
      * @param cls Class to get the name for.
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
index a517197..d4efb0a 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceConfiguration.java
@@ -17,8 +17,11 @@
 
 package org.apache.ignite.services;
 
+import java.io.Externalizable;
 import java.io.Serializable;
+import org.apache.ignite.IgniteServices;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.processors.service.IgniteServiceProcessor;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.lang.IgnitePredicate;
@@ -77,6 +80,9 @@ public class ServiceConfiguration implements Serializable {
     @GridToStringExclude
     protected IgnitePredicate<ClusterNode> nodeFilter;
 
+    /** Enables or disables service statistics. */
+    protected boolean isStatisticsEnabled;
+
     /**
      * Gets service name.
      * <p>
@@ -256,6 +262,36 @@ public class ServiceConfiguration implements Serializable {
         return this;
     }
 
+    /**
+     * Enables or disables statistics for the service. If enabled, durations of the service's methods invocations are
+     * measured (in milliseconds) and stored in histograms of metric registry
+     * {@link IgniteServiceProcessor#SERVICE_METRIC_REGISTRY} by service name.
+     * <p>
+     * <b>NOTE:</b> Statistics are collected only with service proxies obtaining by methods like
+     * {@link IgniteServices#serviceProxy(String, Class, boolean)} and won't work for direct referense of local
+     * services which you can get by, for example, {@link IgniteServices#service(String)}.
+     * <p>
+     * <b>NOTE:</b> Statistics are collected only for all service's interfaces except {@link Service} and
+     * {@link Externalizable} if implemented. Statistics are not collected for methods not declared in any interface.
+     *
+     * @param enabled If {@code true}, enables service statistics. Disables otherwise.
+     * @return {@code this} for chaining.
+     */
+    public ServiceConfiguration setStatisticsEnabled(boolean enabled) {
+        isStatisticsEnabled = enabled;
+
+        return this;
+    }
+
+    /**
+     * Tells wheter statistics for this service is enabled.
+     *
+     * @return {@code True}, if statistics for this service will be enabled. {@code False} otherwise.
+     */
+    public boolean isStatisticsEnabled() {
+        return isStatisticsEnabled;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (!equalsIgnoreNodeFilter(o))
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceMetricsTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceMetricsTest.java
new file mode 100644
index 0000000..9b036cf
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceMetricsTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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.ignite.internal.processors.service;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import com.google.common.collect.Iterables;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.metric.GridMetricManager;
+import org.apache.ignite.internal.processors.service.inner.MyService;
+import org.apache.ignite.internal.processors.service.inner.MyServiceFactory;
+import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.spi.metric.HistogramMetric;
+import org.apache.ignite.spi.metric.Metric;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
+import org.apache.ignite.spi.metric.jmx.JmxMetricExporterSpi;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.processors.service.IgniteServiceProcessor.serviceMetricRegistryName;
+
+/**
+ * Tests metrics of service invocations.
+ */
+public class GridServiceMetricsTest extends GridCommonAbstractTest {
+    /** Number of service invocations. */
+    private static final int INVOKE_CNT = 50;
+
+    /** Service name used in the tests. */
+    private static final String SRVC_NAME = "TestService";
+
+    /** Error message of created metrics. */
+    private static final String METRICS_MUST_NOT_BE_CREATED = "Service metric registry must not be created.";
+
+    /** Utility holder of current grid number. */
+    private int gridNum;
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        stopAllGrids();
+
+        super.afterTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        // JMX metrics exposition to see actual namings and placement of the metrics.
+        cfg.setMetricExporterSpi(new JmxMetricExporterSpi());
+
+        return cfg;
+    }
+
+    /** Checks service metrics are enabled / disabled properly. */
+    @Test
+    public void testServiceMetricsEnabledDisabled() throws Exception {
+        IgniteEx ignite = startGrid();
+
+        ServiceConfiguration srvcCfg = serviceCfg(MyServiceFactory.create(), 0, 1);
+
+        srvcCfg.setStatisticsEnabled(false);
+
+        ignite.services().deploy(srvcCfg);
+
+        assertNull(METRICS_MUST_NOT_BE_CREATED, findMetricRegistry(ignite.context().metric(), SRVC_NAME));
+
+        ignite.services().cancel(SRVC_NAME);
+
+        srvcCfg.setStatisticsEnabled(true);
+
+        ignite.services().deploy(srvcCfg);
+
+        assertNotNull("Service metric registry must be created.",
+            findMetricRegistry(ignite.context().metric(), SRVC_NAME));
+    }
+
+    /** Checks metric are created when service is deployed and removed when service is undeployed. */
+    @Test
+    public void testMetricsOnServiceDeployAndCancel() throws Exception {
+        List<IgniteEx> grids = startGrids(3, false);
+
+        // 2 services per node.
+        grids.get(0).services().deploy(serviceCfg(MyServiceFactory.create(), grids.size() * 2, 2));
+
+        awaitPartitionMapExchange();
+
+        int expectedCnt = Arrays.stream(MyService.class.getDeclaredMethods()).map(Method::getName).collect(
+            Collectors.toSet()).size();
+
+        // Make sure metrics are registered.
+        for (IgniteEx ignite : grids)
+            assertEquals(metricsCnt(ignite), expectedCnt);
+
+        grids.get(0).services().cancel(SRVC_NAME);
+
+        awaitPartitionMapExchange();
+
+        for (IgniteEx ignite : grids)
+            assertEquals(metricsCnt(ignite), 0);
+    }
+
+    /** Tests service metrics migrates correclty with the service redeployment. */
+    @Test
+    public void testRedeploy() throws Exception {
+        List<IgniteEx> grids = startGrids(3, false);
+
+        // 2 services per node.
+        grid(0).services().deploy(serviceCfg(MyServiceFactory.create(), 1, 0));
+
+        awaitPartitionMapExchange();
+
+        // Only same method metric count must persist across the cluster for the singleton.
+        int expectedCnt = Arrays.stream(MyService.class.getDeclaredMethods()).map(Method::getName).collect(
+            Collectors.toSet()).size();
+
+        // Only same method metric count must persist across the cluster for the singleton.
+        assertEquals("Only one metric registry can persist for one service instance", expectedCnt,
+            grids.stream().map(GridServiceMetricsTest::metricsCnt).mapToInt(Integer::intValue).sum());
+
+        for (int i = 0; i < grids.size(); ++i) {
+            if (metricsCnt(grid(i)) > 0) {
+                stopGrid(i);
+
+                awaitPartitionMapExchange();
+
+                break;
+            }
+        }
+
+        // Only same method metric count must persist across the cluster for the singleton.
+        assertEquals("Only one metric registry can persist for one service instance", expectedCnt,
+            G.allGrids().stream().map(grid -> metricsCnt((IgniteEx)grid)).mapToInt(Integer::intValue).sum());
+    }
+
+    /** Tests service metrics for single service instance. */
+    @Test
+    public void testServiceMetricsSingle() throws Throwable {
+        testServiceMetrics(1, 1, 1, 1);
+    }
+
+    /** Tests service metrics for multy service instance: one per server. */
+    @Test
+    public void testServiceMetricsMulty() throws Throwable {
+        testServiceMetrics(3, 3, 3, 1);
+    }
+
+    /** Tests service metrics for multy service instance: fewer that servers and clients. */
+    @Test
+    public void testServiceMetricsMultyFew() throws Throwable {
+        testServiceMetrics(4, 3, 2, 1);
+    }
+
+    /** Tests service metrics for multy service instance: serveral instances per node. */
+    @Test
+    public void testServiceMetricsMultyDuplicated() throws Throwable {
+        testServiceMetrics(3, 2, 3, 3);
+    }
+
+    /** Tests service metrics for multy service instance: serveral instances per node, total fewer that servers. */
+    @Test
+    public void testServiceMetricsMultyFewDuplicated() throws Throwable {
+        testServiceMetrics(5, 4, 3, 2);
+    }
+
+    /**
+     * Invokes service in various ways: from clients, servers, etc. Checks these calls reflect in the metrics.
+     *
+     * @param serverCnt Number of server nodes.
+     * @param clientCnt Number of client nodes.
+     * @param perClusterCnt Number of service instances per cluster.
+     * @param perNodeCnt Number of service instances per node.
+     */
+    private void testServiceMetrics(int serverCnt, int clientCnt, int perClusterCnt, int perNodeCnt) throws Throwable {
+        List<IgniteEx> servers = startGrids(serverCnt, false);
+
+        List<IgniteEx> clients = startGrids(clientCnt, true);
+
+        servers.get(0).services().deploy(serviceCfg(MyServiceFactory.create(), perClusterCnt, perNodeCnt));
+
+        awaitPartitionMapExchange();
+
+        List<MyService> serverStickyProxies = servers.stream()
+            .map(ignite -> (MyService)ignite.services().serviceProxy(SRVC_NAME, MyService.class, true))
+            .collect(Collectors.toList());
+
+        List<MyService> clientStickyProxies = clients.stream()
+            .map(ignite -> (MyService)ignite.services().serviceProxy(SRVC_NAME, MyService.class, true))
+            .collect(Collectors.toList());
+
+        long invokeCollector = 0;
+
+        // Call service through the server proxies.
+        for (int i = 0; i < INVOKE_CNT; ++i) {
+            // Call from server.
+            IgniteEx ignite = servers.get(i % servers.size());
+
+            callService4Times(ignite, serverStickyProxies.get(i % serverStickyProxies.size()));
+
+            // Call from client.
+            ignite = clients.get(i % clients.size());
+
+            callService4Times(ignite, clientStickyProxies.get(i % clientStickyProxies.size()));
+
+            invokeCollector += 8;
+        }
+
+        long invokesInMetrics = 0;
+
+        // Calculate and check invocations within the metrics.
+        for (IgniteEx ignite : servers) {
+            ReadOnlyMetricRegistry metrics = findMetricRegistry(ignite.context().metric(), SRVC_NAME);
+
+            // Metrics may not be deployed on this server node.
+            if (metrics == null)
+                continue;
+
+            for (Metric metric : metrics) {
+                if (metric instanceof HistogramMetric)
+                    invokesInMetrics += sumHistogramEntries((HistogramMetric)metric);
+            }
+        }
+
+        // Compare calls number and metrics number.
+        assertEquals("Calculated wrong service invocation number.", invokesInMetrics, invokeCollector);
+    }
+
+    /** Expose ignite-references of the nodes as list. */
+    private List<IgniteEx> startGrids(int cnt, boolean client) throws Exception {
+        List<IgniteEx> grids = new ArrayList<>(cnt);
+
+        for (int i = 0; i < cnt; ++i)
+            grids.add(client ? startClientGrid(gridNum++) : startGrid(gridNum++));
+
+        return grids;
+    }
+
+    /**
+     * Executes 2 calls for {@link MyService} though unsticky proxy and 2 calls to {@code extraSrvc}. Total 4 are
+     * suposed to present in the metrics.
+     *
+     * @param ignite Server or client node.
+     * @param extraSrvc Extra service instance or proxy to call.
+     */
+    private static void callService4Times(IgniteEx ignite, MyService extraSrvc) {
+        MyService srvc = ignite.services().serviceProxy(SRVC_NAME, MyService.class, false);
+
+        srvc.hello();
+
+        srvc.hello(1);
+
+        extraSrvc.hello();
+
+        extraSrvc.hello(2);
+    }
+
+    /** Provides test service configuration. */
+    private static ServiceConfiguration serviceCfg(Service srvc, int perClusterCnt, int perNodeCnt) {
+        ServiceConfiguration svcCfg = new ServiceConfiguration();
+
+        svcCfg.setName(SRVC_NAME);
+        svcCfg.setService(srvc);
+        svcCfg.setMaxPerNodeCount(perNodeCnt);
+        svcCfg.setTotalCount(perClusterCnt);
+        svcCfg.setStatisticsEnabled(true);
+
+        return svcCfg;
+    }
+
+    /** @return Number of metrics contained in metric registry of the test service. */
+    private static int metricsCnt(IgniteEx ignite) {
+        return Iterables.size(ignite.context().metric().registry(serviceMetricRegistryName(SRVC_NAME)));
+    }
+
+    /**
+     * Count total of histogram values.
+     *
+     * @param histogram Histogram to traverse.
+     * @return Sum of all entries of {@code histogram} buckets.
+     */
+    private static long sumHistogramEntries(HistogramMetric histogram) {
+        if (histogram == null)
+            return 0;
+
+        long sum = 0;
+
+        for (int i = 0; i < histogram.value().length; ++i)
+            sum += histogram.value()[i];
+
+        return sum;
+    }
+
+    /** @return Metric registry if it is found in {@code metricMgr} by name {@code srvcName}. Null otherwise. */
+    private static ReadOnlyMetricRegistry findMetricRegistry(GridMetricManager metricMgr, String srvcName) {
+        for (ReadOnlyMetricRegistry registry : metricMgr) {
+            if (registry.name().equals(serviceMetricRegistryName(srvcName)))
+                return registry;
+        }
+
+        return null;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java
index edb6338..b081aa6 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorAbstractSelfTest.java
@@ -42,6 +42,7 @@ import org.apache.ignite.resources.IgniteInstanceResource;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.services.ServiceDeploymentException;
 import org.apache.ignite.services.ServiceDescriptor;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
@@ -216,19 +217,27 @@ public abstract class GridServiceProcessorAbstractSelfTest extends GridCommonAbs
 
         IgniteFuture<?> fut2 = svcs2.future();
 
-        info("Deployed service: " + name);
-
-        fut1.get();
+        Exception err1 = null;
 
-        info("Finished waiting for service future: " + name);
+        try {
+            fut1.get();
+        }
+        catch (ServiceDeploymentException e) {
+            if (e.getMessage().contains("Failed to deploy some services."))
+                err1 = e;
+            else
+                throw new IllegalStateException("An unexpeted error caught while deploying service.", e);
+        }
 
         try {
             fut2.get();
 
-            fail("Failed to receive mismatching configuration exception.");
+            if (err1 == null)
+                fail("Failed to receive mismatching configuration exception.");
         }
-        catch (IgniteException e) {
-            info("Received mismatching configuration exception: " + e.getMessage());
+        catch (Exception e) {
+            if (!e.getMessage().contains("Failed to deploy some service"))
+                throw new IllegalStateException("An unexpeted error caught while concurrent deploying.", e);
         }
     }
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java
index 76e02bf..7555df2 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/GridServiceProcessorProxySelfTest.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.service;
 
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
@@ -26,6 +28,7 @@ import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.internal.util.typedef.PA;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceConfiguration;
 import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.junit.Test;
@@ -39,6 +42,13 @@ public class GridServiceProcessorProxySelfTest extends GridServiceProcessorAbstr
         return 4;
     }
 
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        grid(0).services().cancelAll();
+    }
+
     /**
      * @throws Exception If failed.
      */
@@ -240,64 +250,88 @@ public class GridServiceProcessorProxySelfTest extends GridServiceProcessorAbstr
     }
 
     /**
+     * Checks local service without the statistics.
+     *
      * @throws Exception If failed.
      */
     @Test
-    public void testLocalProxyInvocation() throws Exception {
-        final String name = "testLocalProxyInvocation";
-
-        final Ignite ignite = grid(0);
-
-        ignite.services().deployNodeSingleton(name, new MapServiceImpl<String, Integer>());
-
-        for (int i = 0; i < nodeCount(); i++) {
-            final int idx = i;
-
-            final AtomicReference<MapService<Integer, String>> ref = new AtomicReference<>();
-
-            //wait because after deployNodeSingleton we don't have guarantees what service was deploy.
-            boolean wait = GridTestUtils.waitForCondition(new PA() {
-                @Override public boolean apply() {
-                    MapService<Integer, String> svc = grid(idx)
-                        .services()
-                        .serviceProxy(name, MapService.class, false);
-
-                    ref.set(svc);
-
-                    return svc instanceof Service;
-                }
-            }, 2000);
+    public void testLocalProxyInvocationWithoutStat() throws Exception {
+        checkLocalProxy(false);
+    }
 
-            // Make sure service is a local instance.
-            assertTrue("Invalid service instance [srv=" + ref.get() + ", node=" + i + ']', wait);
+    /**
+     * Checks local service with the statistics enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testLocalProxyInvocationWithStat() throws Exception {
+        checkLocalProxy(true);
+    }
 
-            ref.get().put(i, Integer.toString(i));
-        }
+    /**
+     * Checks remote non-sticky proxy without the statistics.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteNotStickProxyInvocationWithoutStat() throws Exception {
+        checkRemoteProxy(false, false);
+    }
 
-        MapService<Integer, String> map = ignite.services().serviceProxy(name, MapService.class, false);
+    /**
+     * Checks remote non-sticky proxy with the statistics enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteNotStickyProxyInvocationWithStat() throws Exception {
+        checkRemoteProxy(true, false);
+    }
 
-        for (int i = 0; i < nodeCount(); i++)
-            assertEquals(1, map.size());
+    /**
+     * Checks remote sticky proxy without the statistics.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRemoteStickyProxyInvocationWithoutStat() throws Exception {
+        checkRemoteProxy(false, true);
     }
 
     /**
+     * Checks remote sticky proxy with the statistics enabled.
+     *
      * @throws Exception If failed.
      */
     @Test
-    public void testRemoteNotStickProxyInvocation() throws Exception {
-        final String name = "testRemoteNotStickProxyInvocation";
+    public void testRemoteStickyProxyInvocationWithStat() throws Exception {
+        checkRemoteProxy(true, true);
+    }
 
-        final Ignite ignite = grid(0);
+    /**
+     * Checks remote service proxy (node singleton) with or without the statistics.
+     *
+     * @param withStat If {@code true}, enables the service metrics, {@link ServiceConfiguration#setStatisticsEnabled(boolean)}.
+     * @param sticky If {@code true}, requests sticky proxy.
+     */
+    private void checkRemoteProxy(boolean withStat, boolean sticky) throws InterruptedException {
+        final String svcName = "remoteServiceTest";
+
+        deployNodeSingleton(svcName, withStat);
 
-        ignite.services().deployNodeSingleton(name, new MapServiceImpl<String, Integer>());
+        Ignite ignite = grid(0);
 
         // Get remote proxy.
         MapService<Integer, String> svc = ignite.services(ignite.cluster().forRemotes()).
-            serviceProxy(name, MapService.class, false);
+            serviceProxy(svcName, MapService.class, sticky);
 
-        // Make sure service is a local instance.
         assertFalse(svc instanceof Service);
 
+        assertTrue(Arrays.asList(svc.getClass().getInterfaces()).contains(MapService.class));
+
+        assertEquals(svc.size(), 0);
+
         for (int i = 0; i < nodeCount(); i++)
             svc.put(i, Integer.toString(i));
 
@@ -305,52 +339,78 @@ public class GridServiceProcessorProxySelfTest extends GridServiceProcessorAbstr
 
         for (ClusterNode n : ignite.cluster().forRemotes().nodes()) {
             MapService<Integer, String> map = ignite.services(ignite.cluster().forNode(n)).
-                serviceProxy(name, MapService.class, false);
+                serviceProxy(svcName, MapService.class, sticky);
 
-            // Make sure service is a local instance.
             assertFalse(map instanceof Service);
 
-            size += map.size();
+            assertTrue(Arrays.asList(svc.getClass().getInterfaces()).contains(MapService.class));
+
+            if (map.size() != 0)
+                size += map.size();
         }
 
         assertEquals(nodeCount(), size);
     }
 
     /**
-     * @throws Exception If failed.
+     * Checks local service (node singleton) with or without statistics.
+     *
+     * @param withStat If {@code true}, enables the service metrics, {@link ServiceConfiguration#setStatisticsEnabled(boolean)}.
      */
-    @Test
-    public void testRemoteStickyProxyInvocation() throws Exception {
-        final String name = "testRemoteStickyProxyInvocation";
-
-        final Ignite ignite = grid(0);
+    private void checkLocalProxy(boolean withStat) throws Exception {
+        final String svcName = "localProxyTest";
 
-        ignite.services().deployNodeSingleton(name, new MapServiceImpl<String, Integer>());
+        deployNodeSingleton(svcName, withStat);
 
-        // Get remote proxy.
-        MapService<Integer, String> svc = ignite.services(ignite.cluster().forRemotes()).
-            serviceProxy(name, MapService.class, true);
+        for (int i = 0; i < nodeCount(); i++) {
+            final int idx = i;
 
-        // Make sure service is a local instance.
-        assertFalse(svc instanceof Service);
+            final AtomicReference<MapService<Integer, String>> ref = new AtomicReference<>();
 
-        for (int i = 0; i < nodeCount(); i++)
-            svc.put(i, Integer.toString(i));
+            //wait because after deployNodeSingleton we don't have guarantees what service was deploy.
+            boolean wait = GridTestUtils.waitForCondition(new PA() {
+                @Override public boolean apply() {
+                    MapService<Integer, String> svc = grid(idx)
+                        .services()
+                        .serviceProxy(svcName, MapService.class, false);
 
-        int size = 0;
+                    ref.set(svc);
 
-        for (ClusterNode n : ignite.cluster().forRemotes().nodes()) {
-            MapService<Integer, String> map = ignite.services(ignite.cluster().forNode(n)).
-                serviceProxy(name, MapService.class, false);
+                    return (withStat ? Proxy.isProxyClass(svc.getClass()) : svc instanceof Service) &&
+                        Arrays.asList(svc.getClass().getInterfaces()).contains(MapService.class);
+                }
+            }, 2000);
 
             // Make sure service is a local instance.
-            assertFalse(map instanceof Service);
+            assertTrue("Invalid service instance [srv=" + ref.get() + ", node=" + i + ']', wait);
 
-            if (map.size() != 0)
-                size += map.size();
+            ref.get().put(i, Integer.toString(i));
         }
 
-        assertEquals(nodeCount(), size);
+        MapService<Integer, String> map = grid(0).services().serviceProxy(svcName, MapService.class, false);
+
+        for (int i = 0; i < nodeCount(); i++)
+            assertEquals(1, map.size());
+    }
+
+    /**
+     * Deploys {@link MapServiceImpl} service over the cluster as node singleton.
+     *
+     * @param svcName Service name
+     * @param withStat If {@code true}, enabled the serive metrics {@link ServiceConfiguration#setStatisticsEnabled(boolean)}.
+     */
+    private void deployNodeSingleton(String svcName, boolean withStat) throws InterruptedException {
+        ServiceConfiguration svcCfg = new ServiceConfiguration();
+
+        svcCfg.setName(svcName);
+        svcCfg.setMaxPerNodeCount(1);
+        svcCfg.setTotalCount(nodeCount());
+        svcCfg.setService(new MapServiceImpl<String, Integer>());
+        svcCfg.setStatisticsEnabled(withStat);
+
+        grid(0).services().deploy(svcCfg);
+
+        awaitPartitionMapExchange();
     }
 
     /**
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java
index 251b438..325f145 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/service/inner/MyService.java
@@ -32,6 +32,13 @@ public interface MyService extends Service {
     int hello();
 
     /**
+     * @return Given {@code helloValue}.
+     */
+    default int hello(int helloValue) {
+        return helloValue;
+    }
+
+    /**
      * hashCode() method with a dummy argument.
      *
      * @param dummy Argument.
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
index 64622ad..a1fe470 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteServiceGridTestSuite.java
@@ -25,6 +25,7 @@ import org.apache.ignite.internal.processors.service.GridServiceContinuousQueryR
 import org.apache.ignite.internal.processors.service.GridServiceDeployClusterReadOnlyModeTest;
 import org.apache.ignite.internal.processors.service.GridServiceDeploymentCompoundFutureSelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceDeploymentExceptionPropagationTest;
+import org.apache.ignite.internal.processors.service.GridServiceMetricsTest;
 import org.apache.ignite.internal.processors.service.GridServicePackagePrivateSelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceProcessorBatchDeploySelfTest;
 import org.apache.ignite.internal.processors.service.GridServiceProcessorMultiNodeConfigSelfTest;
@@ -121,6 +122,7 @@ import org.junit.runners.Suite;
     GridServiceDeployClusterReadOnlyModeTest.class,
     GridServiceClusterReadOnlyModeTest.class,
     IgniteServiceCallContextTest.class,
+    GridServiceMetricsTest.class
 })
 public class IgniteServiceGridTestSuite {
     /** */