微服务架构下的服务关联图 - 杨尚川的个人页面 - 开源中国

在微服务架构下,服务之间的关系是非常复杂的,是一个典型的有向有环图,在一个中等规模的项目中,一般会有100多个服务,而大型项目中,则会有数百个服务。

假设我们有如下6个服务:

每个服务都指定了自己依赖的服务:

AaaSvc:

BbbSvc:

CccSvc:

DddSvc:

EeeSvc:

FffSvc:

我们如何把如上6个服务中跟服务AaaSvc相关的服务可视化呢?如下图所示:

要完成这样的服务关联图,需要如下几个步骤:

1、遍历指定项目下的所有服务,构造两个map,serviceToServiceMap 和 reverseServiceToServiceMap,存储所有服务的直接依赖和反向直接依赖。


    public static void runService(String projectId,
                            Map<String, Set<String>> serviceToServiceMap,
                            Map<String, Set<String>> reverseServiceToServiceMap){
        if(! (serviceToServiceMap instanceof ConcurrentHashMap) ){
            throw new RuntimeException("参数serviceToServiceMap必须是ConcurrentHashMap的实例");
        }
        if(! (reverseServiceToServiceMap instanceof ConcurrentHashMap) ){
            throw new RuntimeException("参数reverseServiceToServiceMap必须是ConcurrentHashMap的实例");
        }
        MetaServiceRepository metaServiceRepository = SpringContextUtils.getBean("metaServiceRepository");
        List<MetaService> services = metaServiceRepository.findByProjectId(projectId);
        services.parallelStream().filter(item->!item.getName().contains("Deprecate")).forEach(item->{
            List<DependencyService> dependencyServices = item.constructDependencyServices();
            String key = item.getName()+"("+item.getDescription()+")";
            if(dependencyServices != null){
                dependencyServices.parallelStream().filter(dep->!dep.getName().contains("Deprecate")).forEach(dependencyService->{
                    String value = dependencyService.getName()+"("+dependencyService.getDescription()+")";
                    serviceToServiceMap.putIfAbsent(key, Collections.newSetFromMap(new ConcurrentHashMap<>()));
                    serviceToServiceMap.get(key).add(value);
                    reverseServiceToServiceMap.putIfAbsent(value, Collections.newSetFromMap(new ConcurrentHashMap<>()));
                    reverseServiceToServiceMap.get(value).add(key);
                });
            }
        });
    }

2、以服务AaaSvc为入口,利用直接依赖和反向直接依赖,构造服务依赖图和反向服务依赖图。

String name = metaService.getName()+"("+metaService.getDescription()+")";

Set<String> set = serviceToServiceMap.get(name);
ServiceDependencyGraph serviceDependencyGraph = new ServiceDependencyGraph(new HashMap<>(), name, set, serviceToServiceMap);

set = reverseServiceToServiceMap.get(name);
ServiceDependencyGraph reverseServiceDependencyGraph = new ServiceDependencyGraph(new HashMap<>(), name, set, reverseServiceToServiceMap);
import org.apache.commons.collections4.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 服务依赖图
 * @date 2017-09-2
 * @author 杨尚川
 */
public class ServiceDependencyGraph {
    private String name;
    private List<ServiceDependencyGraph> children = new ArrayList<>();

    public ServiceDependencyGraph(Map<String, AtomicInteger> stopServiceNames, String name, Set<String> set, Map<String, Set<String>> serviceToServiceMap){
        this.name = name;
        if(CollectionUtils.isNotEmpty(set)) {
            for (String item : set) {
                String key = name+"_"+item;
                stopServiceNames.putIfAbsent(key, new AtomicInteger());
                stopServiceNames.get(key).incrementAndGet();
                if(stopServiceNames.get(key).get()<10) {
                    Set<String> sub = serviceToServiceMap.get(item);
                    ServiceDependencyGraph serviceDependencyGraph = new ServiceDependencyGraph(stopServiceNames, item, sub, serviceToServiceMap);
                    children.add(serviceDependencyGraph);
                }
            }
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<ServiceDependencyGraph> getChildren() {
        return children;
    }
}

3、利用服务依赖图和反向服务依赖图,构造带有点和边描述信息的JSON结构:

List<Map<String, String>> nodes = new ArrayList<>();

addNode(serviceDependencyGraph, nodes);
addNode(reverseServiceDependencyGraph, nodes);

List<Map<String, Object>> edges = new ArrayList<>();

addEdge(reverseServiceToServiceMap, serviceDependencyGraph, edges, false);
addEdge(reverseServiceToServiceMap, reverseServiceDependencyGraph, edges, true);

Map<String, Object> graph = new HashMap<>();
graph.put("edges", edges);
graph.put("nodes", nodes);

private void addNode(String node, List<Map<String, String>> nodes){
    for(Map<String, String> item : nodes){
        if(node.equals(item.get("id"))){
            return;
        }
    }
    Map<String, String> nodeMap = new HashMap<>();
    nodeMap.put("id", node);
    nodeMap.put("name", node);
    nodes.add(nodeMap);
}

private void addNode(ServiceDependencyGraph serviceDependencyGraph, List<Map<String, String>> nodes){
    if(serviceDependencyGraph == null){
        return;
    }
    String node = serviceDependencyGraph.getName();
    addNode(node, nodes);
    if(serviceDependencyGraph.getChildren() != null){
        serviceDependencyGraph.getChildren().forEach(item->addNode(item, nodes));
    }
}

private void addEdge(Map<String, Set<String>> reverseServiceToServiceMap, ServiceDependencyGraph serviceDependencyGraph, List<Map<String, Object>> edges, boolean reverse){
    if(serviceDependencyGraph == null){
        return;
    }
    String source = serviceDependencyGraph.getName();
    serviceDependencyGraph.getChildren().forEach(target -> {
        boolean duplicate = false;
        Map<String, Object> map = new HashMap<>();
        if(reverse){
            String id = target.getName()+"-->"+source;
            for(Map<String, Object> item : edges){
                if(id.equals(item.get("id"))){
                    duplicate = true;
                }
            }
            map.put("id", id);
            map.put("target", source);
            map.put("source", target.getName());
            map.put("directed", true);
            map.put("source_score", reverseServiceToServiceMap.get(target.getName()) == null ? 0 : reverseServiceToServiceMap.get(target.getName()).size());
            map.put("target_score", reverseServiceToServiceMap.get(source) == null ? 0 : reverseServiceToServiceMap.get(source).size());
        }else {
            String id = source+"-->"+target.getName();
            for(Map<String, Object> item : edges){
                if(id.equals(item.get("id"))){
                    duplicate = true;
                }
            }
            map.put("id", id);
            map.put("source", source);
            map.put("target", target.getName());
            map.put("directed", true);
            map.put("source_score", reverseServiceToServiceMap.get(source) == null ? 0 : reverseServiceToServiceMap.get(source).size());
            map.put("target_score", reverseServiceToServiceMap.get(target.getName()) == null ? 0 : reverseServiceToServiceMap.get(target.getName()).size());
        }
        if(!duplicate) {
            edges.add(map);
        }
        addEdge(reverseServiceToServiceMap, target, edges, reverse);
    });
}

生成的JSON结构如下所示:

{
    "nodes":[
        {
            "globalWeight":4,
            "name":"AaaSvc(服务Aaa)"
        },
        {
            "globalWeight":4,
            "name":"CccSvc(服务Ccc)"
        },
        {
            "globalWeight":5,
            "name":"DddSvc(服务Ddd)"
        },
        {
            "globalWeight":4,
            "name":"EeeSvc(服务Eee)"
        },
        {
            "globalWeight":4,
            "name":"FffSvc(服务Fff)"
        },
        {
            "globalWeight":3,
            "name":"BbbSvc(服务Bbb)"
        }
    ],
    "edges":[
        {
            "distance":8,
            "source":0,
            "target":1
        },
        {
            "distance":8,
            "source":1,
            "target":0
        },
        {
            "distance":9,
            "source":0,
            "target":2
        },
        {
            "distance":9,
            "source":2,
            "target":3
        },
        {
            "distance":9,
            "source":3,
            "target":2
        },
        {
            "distance":9,
            "source":2,
            "target":4
        },
        {
            "distance":8,
            "source":4,
            "target":3
        },
        {
            "distance":8,
            "source":3,
            "target":4
        },
        {
            "distance":9,
            "source":4,
            "target":2
        },
        {
            "distance":7,
            "source":0,
            "target":5
        },
        {
            "distance":7,
            "source":5,
            "target":1
        },
        {
            "distance":7,
            "source":1,
            "target":5
        }
    ]
}

4、使用d3-force对如上的JSON进行展示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>测试项目_testDemo -- 服务Aaa_AaaSvc -- 服务关联图</title>
    <style>
        .node {
            stroke: #fff;
            stroke-width: 1.5px;
        }
        .node-active{
            stroke: #555;
            stroke-width: 1.5px;
        }
        .link {
            stroke: #555;
            stroke-opacity: .3;
        }
        .link-active {
            stroke-opacity: 1;
            stroke-width: 1.5px;
        }
        .overlay {
            fill: none;
            pointer-events: all;
        }
        #map{
            height:100%;
        }

        #ex1Slider .slider-selection {
            background: #BABABA;
        }

        #ex2Slider .slider-selection {
            background: #BABABA;
        }

        #ex3Slider .slider-selection {
            background: #BABABA;
        }

        #ex4Slider .slider-selection {
            background: #BABABA;
        }

        #ex5Slider .slider-selection {
            background: #BABABA;
        }

    </style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/10.2.0/css/bootstrap-slider.min.css"/>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script src="https://code.jquery.com/jquery-2.2.4.min.js" charset="utf-8"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/10.2.0/bootstrap-slider.min.js" charset="utf-8"></script>
    <script type="text/javascript">
        var gravity = 0.03;
        var linkDistanceMultiFactor = 10;
        var linkDistance = 100;
        var circleRadiusMultiFactor = 2;
        var circleRadius = 4;
        var arrowhead = 10;

        var graph;

        function load(graph){
            console.log("loading ...... ");
            console.log("gravity: "+gravity);
            console.log("linkDistanceMultiFactor: "+linkDistanceMultiFactor);
            console.log("linkDistance: "+linkDistance);
            console.log("circleRadiusMultiFactor: "+circleRadiusMultiFactor);
            console.log("circleRadius: "+circleRadius);


            var margin = {top: -5, right: -5, bottom: -5, left: -5};
            var width = $(window).width() - margin.left - margin.right,
                height = $(window).height() - margin.top - margin.bottom;

            var color = d3.scale.category10();

            var force = d3.layout.force()
                .charge(-200)
                .linkDistance(function(d) {return (d.source.weight+d.target.weight)*linkDistanceMultiFactor+linkDistance;})
                .size([width + margin.left + margin.right, height + margin.top + margin.bottom])
                .charge([-500])
                .theta(0.1)
                .gravity(gravity);

            var zoom = d3.behavior.zoom()
                .scaleExtent([0.3, 10])
                .on("zoom", zoomed);

            var drag = d3.behavior.drag()
                .origin(function(d) { return d; })
                .on("dragstart", dragstarted)
                .on("drag", dragged)
                .on("dragend", dragended);

            var svg = d3.select("#map").append("svg")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
                .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.right + ")")
                .call(zoom);

            var rect = svg.append("rect")
                .attr("width", width)
                .attr("height", height)
                .style("fill", "none")
                .style("pointer-events", "all");

            var container = svg.append("g");

            force
                .nodes(graph.nodes)
                .links(graph.edges)
                .start();

            var link = container.append("g")
                .attr("class", "links")
                .selectAll(".link")
                .data(force.links())
                .enter()
                .append("line")
                .attr("class", "link")
                .attr('marker-end', function(d,i) { return d.rpm === 0 ? '' : 'url(#arrowhead)'})
                .style("stroke-width", function(d) { return Math.sqrt(d.value); });

            var node = container.append("g")
                .attr("class", "nodes")
                .selectAll(".node")
                .data(force.nodes())
                .enter().append("g")
                .attr("class", "node")
                .attr("cx", function(d) { return d.x; })
                .attr("cy", function(d) { return d.y; })
                .call(drag);

            var nodeLabel = container.selectAll(".nodelabel")
                .data(force.nodes())
                .enter()
                .append("text")
                .style("pointer-events", "none")
                .attr({"x":function(d){return d.x;},
                    "y":function(d){return d.y;},
                    "class":"nodelabel",
                    "stroke":"#666"})
                .text(function(d){return d.name;});

            var linkPath = container.selectAll(".linkpath")
                .data(force.links())
                .enter()
                .append('path')
                .attr({'d': function(d) {return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y},
                    'class':'linkpath',
                    'fill-opacity':0,
                    'stroke-opacity':0,
                    'fill':'blue',
                    'stroke':'red',
                    'id':function(d,i) {return 'linkpath'+i}})
                .style("pointer-events", "none");

            var linkLabel = container.selectAll(".linklabel")
                .data(force.links())
                .enter()
                .append('text')
                .style("pointer-events", "none")
                .attr({'class':'linklabel',
                    'id':function(d,i){return 'linklabel'+i},
                    'dx':90,
                    'dy':-5,
                    'font-size':12,
                    'fill':'#666'});

            linkLabel.append('textPath')
                .attr('xlink:href',function(d,i) {return '#linkpath'+i})
                .style("pointer-events", "none")
                .text(function(d,i){ return d.rpm > 0 ? d.rpm + ' req/min' : ""; });

            container.append('defs').append('marker')
                .attr({'id':'arrowhead',
                    'viewBox':'-0 -5 10 10',
                    'refX':25,
                    'refY':0,
                    'orient':'auto',
                    'markerWidth':arrowhead,
                    'markerHeight':arrowhead,
                    'xoverflow':'visible'})
                .append('svg:path')
                .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
                .attr('fill', '#000')
                .attr('stroke','#000');

            node.append("circle")
                .attr("r", function(d) { return d.weight*circleRadiusMultiFactor + circleRadius; })
                .style("fill", function(d,i) { return "#e7ba52"; });

            force.on("tick", function() {
                link.attr("x1", function(d) { return d.source.x; })
                    .attr("y1", function(d) { return d.source.y; })
                    .attr("x2", function(d) { return d.target.x; })
                    .attr("y2", function(d) { return d.target.y; });

                node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

                nodeLabel.attr("x", function(d) { return d.x; })
                    .attr("y", function(d) { return d.y; });

                linkPath.attr('d', function(d) {
                    var path='M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y;
                    return path;
                });

                linkLabel.attr('transform',function(d){
                    if (d.target.x < d.source.x) {
                        bbox = this.getBBox();
                        rx = bbox.x+bbox.width/2;
                        ry = bbox.y+bbox.height/2;
                        return 'rotate(180 ' + rx + ' ' + ry + ')';
                    }
                    else {
                        return 'rotate(0)';
                    }
                });
            });

            var linkedByIndex = {};
            force.links().forEach(function(d) {
                linkedByIndex[d.source.index + "," + d.target.index] = 1;
            });

            function isConnected(a, b) {
                return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index];
            }

            node.on("mouseover", function(d) {
                node.classed("node-active", function(o) {
                    thisOpacity = isConnected(d, o) ? true : false;
                    this.setAttribute('fill-opacity', thisOpacity);
                    return thisOpacity;
                });

                link.classed("link-active", function(o) {
                    return o.source === d || o.target === d ? true : false;
                });

                d3.select(this).classed("node-active", true);
                d3.select(this).select("circle").transition()
                    .duration(750)
                    .attr("r", function(d) { return (d.weight*circleRadiusMultiFactor + circleRadius)*1.5; });
            })
                .on("mouseout", function(d){
                    node.classed("node-active", false);
                    link.classed("link-active", false);

                    d3.select(this).select("circle").transition()
                        .duration(750)
                        .attr("r", function(d) { return d.weight*circleRadiusMultiFactor + circleRadius; });
                });

            function dottype(d) {
                d.x = +d.x;
                d.y = +d.y;
                return d;
            }

            function zoomed() {
                container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
            }

            function dragstarted(d) {
                d3.event.sourceEvent.stopPropagation();
                d3.select(this).classed("dragging", true);
                force.start();
            }

            function dragged(d) {
                d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
            }

            function dragended(d) {
                d3.select(this).classed("dragging", false);
            }
        }


        $(function() {
            $('#ex1').slider({
                tooltip: 'hide'
            }).on('slideStop', function (e) {
                //获取新值
                gravity = e.value/100;
                load(graph);
            });
            $('#ex2').slider({
                tooltip: 'hide'
            }).on('slideStop', function (e) {
                //获取新值
                linkDistance = e.value;
                load(graph);
            });
            $('#ex3').slider({
                tooltip: 'hide'
            }).on('slideStop', function (e) {
                //获取新值
                linkDistanceMultiFactor = e.value;
                load(graph);
            });
            $('#ex4').slider({
                tooltip: 'hide'
            }).on('slideStop', function (e) {
                //获取新值
                circleRadius = e.value;
                load(graph);
            });
            $('#ex5').slider({
                tooltip: 'hide'
            }).on('slideStop', function (e) {
                //获取新值
                circleRadiusMultiFactor = e.value;
                load(graph);
            });
            $('#ex6').slider({
                tooltip: 'hide'
            }).on('slideStop', function (e) {
                //获取新值
                arrowhead = e.value;
                load(graph);
            });
            graph = JSON.parse('{"nodes":[{"globalWeight":4,"name":"AaaSvc(服务Aaa)"},{"globalWeight":4,"name":"CccSvc(服务Ccc)"},{"globalWeight":5,"name":"DddSvc(服务Ddd)"},{"globalWeight":4,"name":"EeeSvc(服务Eee)"},{"globalWeight":4,"name":"FffSvc(服务Fff)"},{"globalWeight":3,"name":"BbbSvc(服务Bbb)"}],"edges":[{"distance":8,"source":0,"target":1},{"distance":8,"source":1,"target":0},{"distance":9,"source":0,"target":2},{"distance":9,"source":2,"target":3},{"distance":9,"source":3,"target":2},{"distance":9,"source":2,"target":4},{"distance":8,"source":4,"target":3},{"distance":8,"source":3,"target":4},{"distance":9,"source":4,"target":2},{"distance":7,"source":0,"target":5},{"distance":7,"source":5,"target":1},{"distance":7,"source":1,"target":5}]}');
            load(graph);
        });

    </script>
</head>
<body>

<h3>测试项目_testDemo -- 服务Aaa_AaaSvc -- 服务关联图</h3>
<table>
    <tr>
        <td width="50"></td>
        <td>
            <span class="item">
                重力调节
                <input id="ex1" type="text" data-slider-id="ex1Slider" class="span2" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="0.03"/>
            </span>
        </td>
        <td width="50"></td>
        <td>
            <span class="item">
                最短边长
                <input id="ex2" type="text" data-slider-id="ex2Slider" class="span2" data-slider-min="0" data-slider-max="250" data-slider-step="1" data-slider-value="100"/>
            </span>
        </td>
        <td width="50"></td>
        <td>
            <span class="item">
                边长放大倍数
                <input id="ex3" type="text" data-slider-id="ex3Slider" class="span2" data-slider-min="0" data-slider-max="50" data-slider-step="1" data-slider-value="10"/>
            </span>
        </td>
        <td width="50"></td>
        <td>
            <span class="item">
                最短半径
                <input id="ex4" type="text" data-slider-id="ex4Slider" class="span2" data-slider-min="0" data-slider-max="10" data-slider-step="1" data-slider-value="4"/>
            </span>
        </td>
        <td width="50"></td>
        <td>
            <span class="item">
                半径放大倍数
                <input id="ex5" type="text" data-slider-id="ex5Slider" class="span2" data-slider-min="0" data-slider-max="5" data-slider-step="1" data-slider-value="2"/>
            </span>
        </td>
        <td width="50"></td>
        <td>
            <span class="item">
                箭头大小
                <input id="ex6" type="text" data-slider-id="ex5Slider" class="span2" data-slider-min="10" data-slider-max="30" data-slider-step="1" data-slider-value="10"/>
            </span>
        </td>
    </tr>
</table>
<div id="map"></div>
</body>
</html>

最终运行的效果如下:


原网址: 访问
创建于: 2018-10-13 16:09:35
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论