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/03/28 13:52:50 UTC

[ignite] branch ignite-2.13 updated: IGNITE-15650 : Introduce statistics for platform services (#9768)

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

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


The following commit(s) were added to refs/heads/ignite-2.13 by this push:
     new 16b329c  IGNITE-15650 : Introduce statistics for platform services (#9768)
16b329c is described below

commit 16b329c0c0de56e16b6d9c16d5f47c9bbfc5540f
Author: Vladimir Steshin <vl...@gmail.com>
AuthorDate: Mon Mar 28 13:23:35 2022 +0300

    IGNITE-15650 : Introduce statistics for platform services (#9768)
---
 .../services/PlatformServiceConfiguration.java     |  52 ++++++
 .../platform/services/PlatformServices.java        |  14 +-
 .../processors/service/GridServiceProxy.java       |  18 +-
 .../processors/service/IgniteServiceProcessor.java |  36 +++-
 .../service/LazyServiceConfiguration.java          |  16 ++
 .../processors/service/GridServiceMetricsTest.java |   2 +-
 .../ignite/platform/PlatformDeployServiceTask.java | 122 +++++++++++-
 .../Services/IJavaService.cs                       |  12 ++
 .../Services/ServicesAsyncWrapper.cs               |   3 +-
 .../Services/ServicesTest.cs                       | 206 +++++++++++++++++++--
 .../Apache.Ignite.Core/Impl/Services/Services.cs   |  16 --
 .../Apache.Ignite.Core/Services/IServices.cs       |  41 ++--
 .../Services/ServiceConfiguration.cs               |  33 ++++
 13 files changed, 496 insertions(+), 75 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServiceConfiguration.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServiceConfiguration.java
new file mode 100644
index 0000000..b77f06f
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServiceConfiguration.java
@@ -0,0 +1,52 @@
+/*
+ * 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.platform.services;
+
+import org.apache.ignite.services.ServiceConfiguration;
+
+/**
+ * Extended service configuration. Keeps known method names of service to build proper service statistics.
+ */
+public class PlatformServiceConfiguration extends ServiceConfiguration {
+    /** */
+    private static final long serialVersionUID = 1L;
+
+    /** Known method names of platform service. */
+    private String[] mtdNames;
+
+    /**
+     * Constr.
+     */
+    PlatformServiceConfiguration() {
+        mtdNames(null);
+    }
+
+    /**
+     * @return Known method names of platform service.
+     */
+    public String[] mtdNames() {
+        return mtdNames;
+    }
+
+    /**
+     * Sets known method names of platform service.
+     */
+    void mtdNames(String[] mtdNames) {
+        this.mtdNames = mtdNames;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java
index 9178acc..06ec10d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/services/PlatformServices.java
@@ -484,8 +484,8 @@ public class PlatformServices extends PlatformAbstractTarget {
      * @param reader Binary reader,
      * @return Service configuration.
      */
-    @NotNull private ServiceConfiguration dotnetConfiguration(BinaryRawReaderEx reader) {
-        ServiceConfiguration cfg = new ServiceConfiguration();
+    @NotNull private PlatformServiceConfiguration dotnetConfiguration(BinaryRawReaderEx reader) {
+        PlatformServiceConfiguration cfg = new PlatformServiceConfiguration();
 
         cfg.setName(reader.readString());
         cfg.setService(new PlatformDotNetServiceImpl(reader.readObjectDetached(), platformCtx, srvKeepBinary));
@@ -499,6 +499,11 @@ public class PlatformServices extends PlatformAbstractTarget {
         if (filter != null)
             cfg.setNodeFilter(platformCtx.createClusterNodeFilter(filter));
 
+        cfg.setStatisticsEnabled(reader.readBoolean());
+
+        if (cfg.isStatisticsEnabled())
+            cfg.mtdNames(reader.readStringArray());
+
         return cfg;
     }
 
@@ -513,9 +518,8 @@ public class PlatformServices extends PlatformAbstractTarget {
 
         List<ServiceConfiguration> cfgs = new ArrayList<>(numServices);
 
-        for (int i = 0; i < numServices; i++) {
+        for (int i = 0; i < numServices; i++)
             cfgs.add(dotnetConfiguration(reader));
-        }
 
         return cfgs;
     }
@@ -822,5 +826,7 @@ public class PlatformServices extends PlatformAbstractTarget {
         if (svcCfg.getNodeFilter() instanceof PlatformClusterNodeFilterImpl)
             dotnetFilter = ((PlatformClusterNodeFilterImpl)svcCfg.getNodeFilter()).getInternalPredicate();
         w.writeObjectDetached(dotnetFilter);
+
+        w.writeBoolean(svcCfg.isStatisticsEnabled());
     }
 }
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 8f60b2a..39e7122 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
@@ -209,7 +209,7 @@ public class GridServiceProxy<T> implements Serializable {
 
                             if (svc != null) {
                                 HistogramMetricImpl hist = svcCtx.isStatisticsEnabled() ?
-                                    svcCtx.metrics().findMetric(mtd.getName()) : null;
+                                    invocationHistogramm(svcCtx, mtd.getName(), args) : null;
 
                                 return hist == null ? callServiceLocally(svc, mtd, args, callAttrs) :
                                     measureCall(hist, () -> callServiceLocally(svc, mtd, args, callAttrs));
@@ -564,7 +564,7 @@ public class GridServiceProxy<T> implements Serializable {
 
             Method mtd = ctx.method(key);
 
-            HistogramMetricImpl hist = ctx.isStatisticsEnabled() ? ctx.metrics().findMetric(mtd.getName()) : null;
+            HistogramMetricImpl hist = ctx.isStatisticsEnabled() ? invocationHistogramm(ctx, mtdName, args) : null;
 
             Object res = hist == null ? callService(ctx, mtd) : measureCall(hist, () -> callService(ctx, mtd));
 
@@ -630,6 +630,20 @@ public class GridServiceProxy<T> implements Serializable {
     }
 
     /**
+     * @return Invocation histogramm for the method.
+     */
+    private static HistogramMetricImpl invocationHistogramm(ServiceContextImpl ctx, String mtdName, Object[] args) {
+        if (ctx.service() instanceof PlatformService) {
+            assert args.length > 0 && args[0] instanceof String;
+            assert ctx.metrics() != null;
+
+            return ctx.metrics().findMetric((String)args[0]);
+        }
+        else
+            return ctx.metrics().findMetric(mtdName);
+    }
+
+    /**
      * Exception class that wraps an exception thrown by the service implementation.
      */
     private static class ServiceProxyException extends RuntimeException {
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 edafbf4..5d5fb3d 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
@@ -71,6 +71,7 @@ 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.platform.services.PlatformServiceConfiguration;
 import org.apache.ignite.internal.processors.security.OperationSecurityContext;
 import org.apache.ignite.internal.processors.security.SecurityContext;
 import org.apache.ignite.internal.util.future.GridCompoundFuture;
@@ -554,6 +555,9 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
         ensure(c.getService() != null, "getService() != null", c.getService());
         ensure(c.getTotalCount() > 0 || c.getMaxPerNodeCount() > 0,
             "c.getTotalCount() > 0 || c.getMaxPerNodeCount() > 0", null);
+        ensure(!c.isStatisticsEnabled() || !(c.getService() instanceof PlatformService) ||
+            c instanceof PlatformServiceConfiguration, "The service is a platform service and has statistics" +
+            "enabled. Service configuration must be PlatformServiceConfiguration.", null);
     }
 
     /**
@@ -664,7 +668,10 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
                 try {
                     byte[] srvcBytes = U.marshal(marsh, cfg.getService());
 
-                    cfgsCp.add(new LazyServiceConfiguration(cfg, srvcBytes));
+                    String[] knownSvcMdtNames = cfg instanceof PlatformServiceConfiguration ?
+                        ((PlatformServiceConfiguration)cfg).mtdNames() : null;
+
+                    cfgsCp.add(new LazyServiceConfiguration(cfg, srvcBytes).platformMtdNames(knownSvcMdtNames));
                 }
                 catch (Exception e) {
                     U.error(log, "Failed to marshal service with configured marshaller " +
@@ -1302,7 +1309,7 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
 
             if (cfg.isStatisticsEnabled()) {
                 if (invocationMetrics == null)
-                    invocationMetrics = createServiceMetrics(srvcCtx);
+                    invocationMetrics = createServiceMetrics(srvcCtx, cfg);
 
                 srvcCtx.metrics(invocationMetrics);
             }
@@ -1982,18 +1989,27 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
      * Creates metrics registry for the invocation histograms.
      *
      * @param srvcCtx ServiceContext.
+     * @param cfg Service configuration.
      * @return Created metric registry.
      */
-    private ReadOnlyMetricRegistry createServiceMetrics(ServiceContextImpl srvcCtx) {
+    private ReadOnlyMetricRegistry createServiceMetrics(ServiceContextImpl srvcCtx, ServiceConfiguration cfg) {
         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;
+        if (cfg instanceof LazyServiceConfiguration && ((LazyServiceConfiguration)cfg).platformMtdNames() != null) {
+            for (String definedMtdName : ((LazyServiceConfiguration)cfg).platformMtdNames()) {
+                metricRegistry.histogram(definedMtdName, DEFAULT_INVOCATION_BOUNDS,
+                    DESCRIPTION_OF_INVOCATION_METRIC_PREF + '\'' + definedMtdName + "()'");
+            }
+        }
+        else {
+            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() + "()'");
+                    metricRegistry.histogram(mtd.getName(), DEFAULT_INVOCATION_BOUNDS,
+                        DESCRIPTION_OF_INVOCATION_METRIC_PREF + '\'' + mtd.getName() + "()'");
+                }
             }
         }
 
@@ -2013,7 +2029,7 @@ public class IgniteServiceProcessor extends GridProcessorAdapter implements Igni
      * @param srvcName Name of the service.
      * @return registry name for service {@code srvcName}.
      */
-    static String serviceMetricRegistryName(String srvcName) {
+    public 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 7d34665..d0d4df1 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
@@ -40,6 +40,10 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
     /** */
     private byte[] srvcBytes;
 
+    /** Names of platform service methods to build service statistics. */
+    @GridToStringExclude
+    private String[] platformMtdNames;
+
     /**
      * Default constructor.
      */
@@ -120,6 +124,18 @@ public class LazyServiceConfiguration extends ServiceConfiguration {
         return true;
     }
 
+    /** */
+    LazyServiceConfiguration platformMtdNames(String[] platformMtdNames) {
+        this.platformMtdNames = platformMtdNames;
+
+        return this;
+    }
+
+    /** @return Names of known service methods. */
+    String[] platformMtdNames() {
+        return platformMtdNames;
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         String svcCls = srvc == null ? "" : srvc.getClass().getSimpleName();
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
index 9b036cf..ddbbc49 100644
--- 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
@@ -299,7 +299,7 @@ public class GridServiceMetricsTest extends GridCommonAbstractTest {
      * @param histogram Histogram to traverse.
      * @return Sum of all entries of {@code histogram} buckets.
      */
-    private static long sumHistogramEntries(HistogramMetric histogram) {
+    public static long sumHistogramEntries(HistogramMetric histogram) {
         if (histogram == null)
             return 0;
 
diff --git a/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java b/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java
index 0436d4c..275d905 100644
--- a/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java
+++ b/modules/core/src/test/java/org/apache/ignite/platform/PlatformDeployServiceTask.java
@@ -30,6 +30,8 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteException;
@@ -39,7 +41,10 @@ import org.apache.ignite.compute.ComputeJob;
 import org.apache.ignite.compute.ComputeJobAdapter;
 import org.apache.ignite.compute.ComputeJobResult;
 import org.apache.ignite.compute.ComputeTaskAdapter;
+import org.apache.ignite.compute.ComputeTaskSplitAdapter;
+import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.binary.BinaryArray;
+import org.apache.ignite.internal.util.lang.IgnitePair;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.platform.model.ACL;
@@ -55,11 +60,17 @@ import org.apache.ignite.platform.model.User;
 import org.apache.ignite.platform.model.Value;
 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.spi.metric.HistogramMetric;
+import org.apache.ignite.spi.metric.Metric;
+import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import static java.util.Calendar.JANUARY;
+import static org.apache.ignite.internal.processors.service.GridServiceMetricsTest.sumHistogramEntries;
+import static org.apache.ignite.internal.processors.service.IgniteServiceProcessor.serviceMetricRegistryName;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -102,7 +113,14 @@ public class PlatformDeployServiceTask extends ComputeTaskAdapter<String, Object
 
         /** {@inheritDoc} */
         @Override public Object execute() throws IgniteException {
-            ignite.services().deployNodeSingleton(serviceName, new PlatformTestService());
+            ServiceConfiguration svcCfg = new ServiceConfiguration();
+
+            svcCfg.setStatisticsEnabled(true);
+            svcCfg.setName(serviceName);
+            svcCfg.setMaxPerNodeCount(1);
+            svcCfg.setService(new PlatformTestService());
+
+            ignite.services().deploy(svcCfg);
 
             return null;
         }
@@ -111,10 +129,10 @@ public class PlatformDeployServiceTask extends ComputeTaskAdapter<String, Object
     /**
      * Test service.
      */
-    public static class PlatformTestService implements Service {
+    public static class PlatformTestService implements Service, PlatformHelperService {
         /** */
         @IgniteInstanceResource
-        private Ignite ignite;
+        private IgniteEx ignite;
 
         /** */
         private boolean isCancelled;
@@ -671,10 +689,96 @@ public class PlatformDeployServiceTask extends ComputeTaskAdapter<String, Object
             }
         }
 
+        /** {@inheritDoc} */
+        @Override public int testNumberOfInvocations(String svcName, String histName) {
+            return ignite.compute().execute(new CountServiceMetricsTask(), new IgnitePair<>(svcName, histName));
+        }
+
         /** */
         public Object contextAttribute(String name) {
             return svcCtx.currentCallContext().attribute(name);
         }
+
+        /**
+         * Calculates number of registered values among the service statistics. Can process all service metrics or
+         * certain named one.
+         */
+        private static class CountServiceMetricsTask extends ComputeTaskSplitAdapter<IgnitePair<String>, Integer> {
+            /** {@inheritDoc} */
+            @Override public Integer reduce(List<ComputeJobResult> results) throws IgniteException {
+                int cnt = 0;
+
+                for (ComputeJobResult res : results) {
+                    if (res.isCancelled()) {
+                        throw new IgniteException("Unable to count invocations in service metrics. Job was canceled " +
+                            "on node [" + res.getNode() + "].");
+                    }
+
+                    if (res.getException() != null) {
+                        throw new IgniteException("Unable to count invocations in service metrics. Job failed on " +
+                            "node [" + res.getNode() + "]: " + res.getException().getMessage(), res.getException());
+                    }
+
+                    if (res.getData() == null)
+                        continue;
+
+                    cnt += (int)res.getData();
+                }
+
+                return cnt;
+            }
+
+            /** {@inheritDoc} */
+            @Override protected Collection<? extends ComputeJob> split(int gridSize,
+                IgnitePair<String> arg) throws IgniteException {
+                return Stream.generate(() -> new CountServiceMetricsLocallyJob(arg.get1(), arg.get2())).limit(gridSize).
+                    collect(Collectors.toList());
+            }
+
+            /** Summs invocation of service methods by service statistics on certain node. */
+            private static class CountServiceMetricsLocallyJob extends ComputeJobAdapter {
+                /** Service name. */
+                private final String svcName;
+
+                /** Name of the histogramm. If {@code null}, every histogram in the service metric is processed. */
+                @Nullable private final String histName;
+
+                /** */
+                @IgniteInstanceResource
+                private IgniteEx ignite;
+
+                /**
+                 * @param svcName  Service name.
+                 * @param histName Name of the histogramm. If {@code null}, every histogram in the service metric is
+                 *                 processed.
+                 */
+                private CountServiceMetricsLocallyJob(String svcName, @Nullable String histName) {
+                    this.svcName = svcName;
+                    this.histName = histName;
+                }
+
+                /** {@inheritDoc} */
+                @Override public Integer execute() throws IgniteException {
+                    ReadOnlyMetricRegistry metrics = ignite.context().metric().registry(
+                        serviceMetricRegistryName(svcName));
+
+                    if (histName != null && !histName.isEmpty()) {
+                        HistogramMetric hist = metrics.findMetric(histName);
+
+                        return hist == null ? 0 : (int)sumHistogramEntries(hist);
+                    }
+
+                    int cnt = 0;
+
+                    for (Metric metric : metrics) {
+                        if (metric instanceof HistogramMetric)
+                            cnt += sumHistogramEntries((HistogramMetric)metric);
+                    }
+
+                    return cnt;
+                }
+            }
+        }
     }
 
     /** */
@@ -700,4 +804,16 @@ public class PlatformDeployServiceTask extends ComputeTaskAdapter<String, Object
             super(msg);
         }
     }
+
+    /**
+     * Platform helper service.
+     */
+    private interface PlatformHelperService {
+        /**
+         * Calculates number of registered values among the service statistics.
+         *
+         * @return Number of registered values among the service statistics.
+         */
+        int testNumberOfInvocations(String svcName, String histName);
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs
index 12fd52d..c1f7591 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/IJavaService.cs
@@ -216,4 +216,16 @@ namespace Apache.Ignite.Core.Tests.Services
         /** */
         object contextAttribute(string name);
     }
+
+    /// <summary>
+    /// Interface for the methods that are available only on Java side.
+    /// </summary>
+    [SuppressMessage("ReSharper", "InconsistentNaming")]
+    public interface IJavaOnlyService : IJavaService
+    {
+        /// <summary>
+        /// Returns number of measured by service metrics invocations of all service's methods or of its certain method.
+        /// </summary>
+        int testNumberOfInvocations(string svcName, string histName = null);
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesAsyncWrapper.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesAsyncWrapper.cs
index 6b834bf..42fbb3f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesAsyncWrapper.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesAsyncWrapper.cs
@@ -169,17 +169,18 @@ namespace Apache.Ignite.Core.Tests.Services
             return _services.GetServiceDescriptors();
         }
 
+#pragma warning disable 618 
         /** <inheritDoc /> */
         public T GetService<T>(string name)
         {
             return _services.GetService<T>(name);
         }
-
         /** <inheritDoc /> */
         public ICollection<T> GetServices<T>(string name)
         {
             return _services.GetServices<T>(name);
         }
+#pragma warning restore 618
 
         /** <inheritDoc /> */
         public T GetServiceProxy<T>(string name) where T : class
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs
index 798a98e..a89a539 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Services/ServicesTest.cs
@@ -41,6 +41,7 @@ namespace Apache.Ignite.Core.Tests.Services
     /// <summary>
     /// Services tests.
     /// </summary>
+#pragma warning disable 618
     public class ServicesTest
     {
         /** */
@@ -410,10 +411,6 @@ namespace Apache.Ignite.Core.Tests.Services
 
             Assert.AreEqual(43, svc.TestOverload(2, ServicesTypeAutoResolveTest.Param));
 
-            // Check local scenario (proxy should not be created for local instance)
-            Assert.IsTrue(ReferenceEquals(Grid2.GetServices().GetService<ITestIgniteService>(SvcName),
-                Grid2.GetServices().GetServiceProxy<ITestIgniteService>(SvcName)));
-
             // Check sticky = false: call multiple times, check that different nodes get invoked
             var invokedIds = Enumerable.Range(1, 100).Select(x => prx.NodeId).Distinct().ToList();
             Assert.AreEqual(2, invokedIds.Count);
@@ -490,10 +487,111 @@ namespace Apache.Ignite.Core.Tests.Services
             // Make sure there is an instance on grid1.
             var svcInst = Grid1.GetServices().GetService<ITestIgniteService>(SvcName);
             Assert.IsNotNull(svcInst);
+        }
+        
+        /// <summary>
+        /// Tests statistics of pure Java service. Call the service itself.
+        /// </summary>
+        [Test]
+        public void TestJavaServiceStatistics()
+        {
+            // Java service itself.
+            var helperSvc = Grid1.GetServices().GetServiceProxy<IJavaOnlyService>(_javaSvcName, false);
+            
+            // Check metrics of pure java service. There were no invocations yet.
+            Assert.AreEqual(0, helperSvc.testNumberOfInvocations(_javaSvcName));
+            // Now we did 1 invocation of pure Java service just before.
+            Assert.AreEqual(1, helperSvc.testNumberOfInvocations(_javaSvcName));
+            // In total we did 2 calls by now.
+            Assert.AreEqual(2, helperSvc.testNumberOfInvocations(_javaSvcName));
+        }
+
+        /// <summary>
+        /// Tests statistics of a platform service from client/remote node.
+        /// </summary>
+        [Test]
+        public void TestStatisticsRemote()
+        {
+            DoTestMetrics(Grid1.GetServices(), _client.GetServices(), null, false);
+
+            DoTestMetrics(_client.GetServices(), _client.GetServices(), null, false);
+        }
+        
+        /// <summary>
+        /// Tests statistics of a platform service from client/remote node using a call context.
+        /// </summary>
+        [Test]
+        public void TestStatisticsRemoteWithCallCtx()
+        {
+            DoTestMetrics(Grid1.GetServices(), _client.GetServices(), callContext(), false);
+
+            DoTestMetrics(_client.GetServices(), _client.GetServices(), callContext(), false);
+        }
+
+        /// <summary>
+        /// Tests statistics of a dynamically-proxied platform service from client/remote node.
+        /// </summary>
+        [Test]
+        public void TestStatisticsRemoteDynamically()
+        {
+            DoTestMetrics(Grid1.GetServices(), _client.GetServices(), null, true);
+
+            DoTestMetrics(_client.GetServices(), _client.GetServices(), null, true);
+        }
+        
+        /// <summary>
+        /// Tests statistics of a dynamically-proxied platform service from client/remote node using a call context.
+        /// </summary>
+        [Test]
+        public void TestStatisticsRemoteDynamicallyWithCallContext()
+        {
+            DoTestMetrics(Grid1.GetServices(), _client.GetServices(), callContext(), true);
+
+            DoTestMetrics(_client.GetServices(), _client.GetServices(), callContext(), true);
+        }
+
+        /// <summary>
+        /// Tests statistics of a platform service from server/local node.
+        /// </summary>
+        [Test]
+        public void TestStatisticsLocal()
+        {
+            DoTestMetrics(Grid1.GetServices(), Grid1.GetServices(), null, false);
+
+            DoTestMetrics(_client.GetServices(), Grid1.GetServices(), null, false);
+        }
+        
+        /// <summary>
+        /// Tests statistics of a platform service from server/local node using the call context.
+        /// </summary>
+        [Test]
+        public void TestStatisticsLocalWithCallContext()
+        {
+            DoTestMetrics(Grid1.GetServices(), Grid1.GetServices(), callContext(), false);
 
-            // Get dynamic proxy that simply wraps the service instance.
-            var prx = Grid1.GetServices().GetDynamicServiceProxy(SvcName);
-            Assert.AreSame(prx, svcInst);
+            DoTestMetrics(_client.GetServices(), Grid1.GetServices(), callContext(), false);
+        }
+
+        /// <summary>
+        /// Tests statistics of a dynamically-proxied platform service from server/local node.
+        /// </summary>
+        [Test]
+        public void TestStatisticsLocalDynamic()
+        {
+            DoTestMetrics(Grid1.GetServices(), Grid1.GetServices(), null, true);
+
+            DoTestMetrics(_client.GetServices(), Grid1.GetServices(), null, true);
+        }
+        
+        /// <summary>
+        /// Tests statistics of a dynamically-proxied platform service from server/local node using a call context.
+        /// </summary>
+        [Test]
+        public void TestStatisticsLocalDynamicWithCallContext()
+        {
+            DoTestMetrics(Grid1.GetServices(), Grid1.GetServices(), callContext(), true);
+
+            DoTestMetrics(_client.GetServices(), Grid1.GetServices(), callContext(), true);
         }
 
         /// <summary>
@@ -1287,7 +1385,7 @@ namespace Apache.Ignite.Core.Tests.Services
         }
 
         /// <summary>
-        /// Tets binary methods in services.
+        /// Tests binary methods in services.
         /// </summary>
         private void DoTestBinary(IJavaService svc, IJavaService binSvc, bool isPlatform)
         {
@@ -1331,6 +1429,88 @@ namespace Apache.Ignite.Core.Tests.Services
         }
 
         /// <summary>
+        /// Tests platform service statistics.
+        /// </summary>
+        private void DoTestMetrics(IServices producer, IServices consumer, IServiceCallContext callCtx, bool dyn)
+        {
+            var cfg = new ServiceConfiguration
+            {
+                Name = "TestMetricsSrv",
+                MaxPerNodeCount = 1,
+                TotalCount = 3,
+                Service = new PlatformTestService(),
+            };
+
+            producer.Deploy(cfg);
+
+            var svc = dyn
+                ? consumer.GetDynamicServiceProxy(cfg.Name, false, callCtx)
+                : consumer.GetServiceProxy<IJavaService>(cfg.Name, false, callCtx);
+
+            // Subject service, calculates invocations.
+            var helperSvc = producer.GetServiceProxy<IJavaOnlyService>(_javaSvcName, false);
+            
+            // Do 4 invocations.
+            Assert.AreEqual(3, dyn ? svc.testOverload(1, 2) : ((IJavaService)svc).testOverload(1, 2));
+            Assert.AreEqual(2, dyn ? svc.test(1) : ((IJavaService)svc).test(1));
+            Assert.AreEqual(true, dyn ? svc.test(false) : ((IJavaService)svc).test(false));
+            Assert.AreEqual(null, dyn ? svc.testNull(null) : ((IJavaService)svc).testNull(null));
+
+            // Service stats. is not enabled.
+            Assert.AreEqual(0, helperSvc.testNumberOfInvocations(cfg.Name));
+
+            producer.Cancel(cfg.Name);
+
+            AssertNoService(cfg.Name);
+            
+            // Redeploy service with enabled stats.
+            cfg.StatisticsEnabled = true;
+            cfg.Service = new PlatformTestService();
+            producer.Deploy(cfg);
+            
+            svc = dyn
+                ? consumer.GetDynamicServiceProxy(cfg.Name, false, callCtx)
+                : consumer.GetServiceProxy<IJavaService>(cfg.Name, false, callCtx);
+            
+            // Service metrics exists but holds no values.
+            Assert.AreEqual(0, helperSvc.testNumberOfInvocations(cfg.Name));
+
+            // One invocation.
+            Assert.AreEqual(2, dyn ? svc.test(1) : ((IJavaService)svc).test(1));
+            
+            // There should be just one certain and one total invocation.
+            Assert.AreEqual(1, helperSvc.testNumberOfInvocations(cfg.Name, "test"));
+            Assert.AreEqual(1, helperSvc.testNumberOfInvocations(cfg.Name));
+            
+            // Do 4 more invocations.
+            Assert.AreEqual(3, dyn ? svc.testOverload(1, 2) : ((IJavaService)svc).testOverload(1, 2));
+            Assert.AreEqual(2, dyn ? svc.test(1) : ((IJavaService)svc).test(1));
+            Assert.AreEqual(true, dyn ? svc.test(false) : ((IJavaService)svc).test(false));
+            Assert.AreEqual(null, dyn ? svc.testNull(null) : ((IJavaService)svc).testNull(null));
+            
+            // We did 3 invocations of method named 'test(...)' in total.
+            Assert.AreEqual(3, helperSvc.testNumberOfInvocations(cfg.Name, "test"));
+            
+            // We did 1 invocations of method named 'testOverload(...)' in total.
+            Assert.AreEqual(1, helperSvc.testNumberOfInvocations(cfg.Name, "testOverload"));
+            
+            Assert.AreEqual(1, helperSvc.testNumberOfInvocations(cfg.Name, "testNull"));
+            
+            // We did 5 total invocations.
+            Assert.AreEqual(5, helperSvc.testNumberOfInvocations(cfg.Name));
+            
+            // Check side methods are not measured. We still have only 5 invocations.
+            Assert.AreEqual("Apache.Ignite.Core.Tests.Services.PlatformTestService", svc.ToString());
+            Assert.AreEqual(5, helperSvc.testNumberOfInvocations(cfg.Name));
+            // 'ToString' must not be measured. Like Java service metrics, it's not declared as a service interface.
+            Assert.AreEqual(0, helperSvc.testNumberOfInvocations(cfg.Name, "ToString"));
+
+            // Undeploy again.
+            producer.Cancel(cfg.Name);
+            AssertNoService(cfg.Name);
+        }
+
+        /// <summary>
         /// Tests the footer setting.
         /// </summary>
         [Test]
@@ -1964,14 +2144,4 @@ namespace Apache.Ignite.Core.Tests.Services
         }
 #endif
     }
-
-    /// <summary> Tests with UseBinaryArray = true. </summary>
-    public class ServicesTestBinaryArrays : ServicesTest
-    {
-        /** */
-        public ServicesTestBinaryArrays() : base(true)
-        {
-            // No-op.
-        }
-    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs
index eeb4b96..1d986f4 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Services/Services.cs
@@ -378,13 +378,6 @@ namespace Apache.Ignite.Core.Impl.Services
             IgniteArgumentCheck.Ensure(typeof(T).IsInterface, "T", 
                 "Service proxy type should be an interface: " + typeof(T));
 
-            T locInst;
-
-            // In local scenario try to return service instance itself instead of a proxy
-            // Get as object because proxy interface may be different from real interface
-            if (callCtx == null && (locInst = GetService<object>(name) as T) != null)
-                return locInst;
-
             var javaProxy = DoOutOpObject(OpServiceProxy, w =>
             {
                 w.WriteString(name);
@@ -415,15 +408,6 @@ namespace Apache.Ignite.Core.Impl.Services
         {
             IgniteArgumentCheck.NotNullOrEmpty(name, "name");
 
-            // In local scenario try to return service instance itself instead of a proxy
-            if (callCtx == null)
-            {
-                var locInst = GetService<object>(name);
-
-                if (locInst != null)
-                    return locInst;
-            }
-
             var javaProxy = DoOutOpObject(OpServiceProxy, w =>
             {
                 w.WriteString(name);
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Services/IServices.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Services/IServices.cs
index 569712e..07a36ec 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Services/IServices.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Services/IServices.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Core.Services
 {
+    using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
     using System.Threading.Tasks;
@@ -240,6 +241,8 @@ namespace Apache.Ignite.Core.Services
         /// <typeparam name="T">Service type.</typeparam>
         /// <param name="name">Service name.</param>
         /// <returns>Deployed service with specified name.</returns>
+        [Obsolete("Corrupts the service statistics. Use the proxies like GetServiceProxy() or " +
+                  "GetDynamicServiceProxy() instead.")]
         T GetService<T>(string name);
 
         /// <summary>
@@ -248,33 +251,33 @@ namespace Apache.Ignite.Core.Services
         /// <typeparam name="T">Service type.</typeparam>
         /// <param name="name">Service name.</param>
         /// <returns>All deployed services with specified name.</returns>
+        [Obsolete("Corrupts the service statistics. Use the proxies like GetServiceProxy() or " +
+                  "GetDynamicServiceProxy() instead.")]
         ICollection<T> GetServices<T>(string name);
 
         /// <summary>
-        /// Gets a remote handle on the service. If service is available locally,
-        /// then local instance is returned, otherwise, a remote proxy is dynamically
-        /// created and provided for the specified service.
+        /// Gets a handle on remote or local service. The proxy is dynamically created and provided for the specified
+        /// service.
         /// </summary>
         /// <typeparam name="T">Service type.</typeparam>
         /// <param name="name">Service name.</param>
-        /// <returns>Either proxy over remote service or local service if it is deployed locally.</returns>
+        /// <returns>Proxy over service.</returns>
         T GetServiceProxy<T>(string name) where T : class;
 
         /// <summary>
-        /// Gets a remote handle on the service. If service is available locally,
-        /// then local instance is returned, otherwise, a remote proxy is dynamically
-        /// created and provided for the specified service.
+        /// Gets a handle on remote or local service. The proxy is dynamically created and provided for the specified
+        /// service.
         /// </summary>
         /// <typeparam name="T">Service type.</typeparam>
         /// <param name="name">Service name.</param>
         /// <param name="sticky">Whether or not Ignite should always contact the same remote
         /// service or try to load-balance between services.</param>
-        /// <returns>Either proxy over remote service or local service if it is deployed locally.</returns>
+        /// <returns>Proxy over service.</returns>
         T GetServiceProxy<T>(string name, bool sticky) where T : class;
 
         /// <summary>
-        /// Gets a remote handle on the service with the specified caller context.
-        /// The proxy is dynamically created and provided for the specified service.
+        /// Gets a handle on remote or local service with the specified caller context. The proxy is dynamically
+        /// created and provided for the specified service.
         /// </summary>
         /// <typeparam name="T">Service type.</typeparam>
         /// <param name="name">Service name.</param>
@@ -287,21 +290,19 @@ namespace Apache.Ignite.Core.Services
         T GetServiceProxy<T>(string name, bool sticky, IServiceCallContext callCtx) where T : class;
 
         /// <summary>
-        /// Gets a remote handle on the service as a dynamic object. If service is available locally,
-        /// then local instance is returned, otherwise, a remote proxy is dynamically
-        /// created and provided for the specified service.
+        /// Gets a handle on remote or local service as a dynamic object. The proxy is dynamically created and provided
+        /// for the specified service.
         /// <para />
         /// This method utilizes <c>dynamic</c> feature of the language and does not require any
         /// service interfaces or classes. Java services can be accessed as well as .NET services.
         /// </summary>
         /// <param name="name">Service name.</param>
-        /// <returns>Either proxy over remote service or local service if it is deployed locally.</returns>
+        /// <returns>Proxy over service.</returns>
         dynamic GetDynamicServiceProxy(string name);
 
         /// <summary>
-        /// Gets a remote handle on the service as a dynamic object. If service is available locally,
-        /// then local instance is returned, otherwise, a remote proxy is dynamically
-        /// created and provided for the specified service.
+        /// Gets a handle on remote or local service as a dynamic object. The proxy is dynamically created and provided
+        /// for the specified service.
         /// <para />
         /// This method utilizes <c>dynamic</c> feature of the language and does not require any
         /// service interfaces or classes. Java services can be accessed as well as .NET services.
@@ -309,12 +310,12 @@ namespace Apache.Ignite.Core.Services
         /// <param name="name">Service name.</param>
         /// <param name="sticky">Whether or not Ignite should always contact the same remote
         /// service or try to load-balance between services.</param>
-        /// <returns>Either proxy over remote service or local service if it is deployed locally.</returns>
+        /// <returns>Proxy over service.</returns>
         dynamic GetDynamicServiceProxy(string name, bool sticky);
         
         /// <summary>
-        /// Gets a remote handle on the service with the specified caller context.
-        /// The proxy is dynamically created and provided for the specified service.
+        /// Gets a handle on remote or local service as a dynamic object with the specified caller context. The proxy
+        /// is dynamically created and provided for the specified service.
         /// <para />
         /// This method utilizes <c>dynamic</c> feature of the language and does not require any
         /// service interfaces or classes. Java services can be accessed as well as .NET services.
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Services/ServiceConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Services/ServiceConfiguration.cs
index a7b9e7f..9753b98 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Services/ServiceConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Services/ServiceConfiguration.cs
@@ -20,6 +20,8 @@ namespace Apache.Ignite.Core.Services
     using System;
     using System.Diagnostics;
     using System.Diagnostics.CodeAnalysis;
+    using System.Linq;
+    using System.Reflection;
     using Apache.Ignite.Core.Binary;
     using Apache.Ignite.Core.Cluster;
 
@@ -63,6 +65,12 @@ namespace Apache.Ignite.Core.Services
         /// Gets or sets node filter used to filter nodes on which the service will be deployed.
         /// </summary>
         public IClusterNodeFilter NodeFilter { get; set; }
+        
+        /// <summary>
+        /// Enables or disables service statistics.
+        /// NOTE: Service statistics work only via service proxies. <see cref="IServices.GetServiceProxy{T}(string)"/>
+        /// </summary>
+        public bool StatisticsEnabled { get; set; }
 
         /// <summary>
         /// Serializes the Service configuration using IBinaryRawWriter
@@ -83,6 +91,29 @@ namespace Apache.Ignite.Core.Services
                 w.WriteObject(NodeFilter);
             else
                 w.WriteObject<object>(null);
+
+            w.WriteBoolean(StatisticsEnabled);
+
+            WriteExtraDescription(w);
+        }
+
+        /// <summary>
+        /// Provides extra info about platform service to avoid on-demand creation of service statistics on any
+        /// out-of-interface calls or things like 'ToString()'.
+        /// </summary>
+        private void WriteExtraDescription(IBinaryRawWriter writer)
+        {
+            if (StatisticsEnabled)
+            {
+                // Methods names of user interfaces of the service.
+                var mtdNames = Service.GetType().GetInterfaces()
+                    // No need to measure methods of these interface.
+                    .Where(t => t != typeof(IService))
+                    .SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |
+                                                  BindingFlags.Public ).Select(mtd => mtd.Name)).Distinct();
+
+                writer.WriteStringArray(mtdNames.ToArray());
+            }
         }
 
         /// <summary>
@@ -125,6 +156,8 @@ namespace Apache.Ignite.Core.Services
             {
                 // Ignore exceptions in user deserealization code.
             }
+
+            StatisticsEnabled = r.ReadBoolean();
         }
     }
 }
\ No newline at end of file