作者:屿山、十眠

微服务系统架构中,服务之间的依靠关系扑朔迷离,我们往往会运用负载均衡组件协作注册中心来结束服务间的感知。而这种感知行为需求调用方、负载均衡组件、注册中心、被调用方互相协作才干够结束,在呈现问题时我们又或许很难承认是哪一部分的问题,在常规场景中,注册中心会有对应的控制台能够查看,而调用方、负载均衡组件、被调用方处则需求我们手动增加日志打印语句偏重启运用才干得到相关的信息,而有些组件又难以找到适宜的方位增加我们日志代码,使得这类问题的排查效率低下。

负载均衡原理剖析

我们以 Spring Cloud 运用为例剖析一下,微服务负载均衡到底是怎样一回事?

本文的 demo 包含 log-demo-spring-cloud-zuul、log-demo-spring-cloud-a、log-demo-spring-cloud-b、log-demo-spring-cloud-c 四个运用,选用最简单的 Spring Cloud 规范用法依次调用,能够直接在项目上查看源码:

github.com/aliyun/alib…

以 Spring Cloud 常用的客户端负载均衡组件 Ribbon 作为示例,其作业原理如下图所示。

从负载均衡到路由,微服务使用现场一键到位

Ribbon 坐落客户端一侧,通过服务注册中心(本文中为 Nacos)获取到一份服务端供给的可用服务列表。随后,在客户端发送央求时通过负载均衡算法选择一个服务端实例再进行拜访,以到达负载均衡的目的。在这个进程中为了感知服务注册中心的可用服务列表的变化,Ribbon 会在结构 com.netflix.loadbalancer.DynamicServerListLoadBalancer 时,发动一个守时线程去循环调用 com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers 方法更新自己持有的可用服务列表。

  @VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
      //从注册中心获取可用服务列表
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);
            if (filter != null) {
        //根据加载的过滤器过滤地址
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
    //更新可用服务列表
        updateAllServerList(servers);
    }

通过代码能够发现,updateAllServerList(servers)方法的参数 servers 就是更新后可用服务列表,不过为了保证取得真实的现场,我们跟着调用链继续往下。

  protected void updateAllServerList(List<T> ls) {
        // other threads might be doing this - in which case, we pass
        if (serverListUpdateInProgress.compareAndSet(false, true)) {
            try {
                for (T s : ls) {
                    s.setAlive(true); // set so that clients can start using these
                                      // servers right away instead
                                      // of having to wait out the ping cycle.
                }
                setServersList(ls);
                super.forceQuickPing();
            } finally {
                serverListUpdateInProgress.set(false);
            }
        }
    }

能够看到只要一个线程能够调用 setServersList(ls)方法去更新可用服务列表,之后的调用链还有一些处理逻辑。

com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateAllServerList
  -> com.netflix.loadbalancer.DynamicServerListLoadBalancer#setServersList
  -> com.netflix.loadbalancer.DynamicServerListLoadBalancer#setServerListForZones
  -> com.netflix.loadbalancer.LoadBalancerStats#updateZoneServerMapping

其间 updateZoneServerMapping 方法的参数 Map<String, List> map 底子等同于本次更新动作终究所更新的可用服务列表。也就是说,只要能打印出这个方法的参数,我们就能够知道每次更新可用服务列表的结果,这就能够帮忙我们了解调用方以及负载均衡组件在这个场景下的真结束场。

无侵入的微服务观察才干

我们是否能够供给一种才干,我们能够动态的在任意代码的要害方位动态地打印需求的日志,观测到任何一部分当下的真结束场,然后辅佐我们排查问题。考虑到散布式微服务运用的复杂度,这种才干需求以下一些特色:

  • 散布式特性:满意在散布式场景下,即使是复杂微服务系统架构下,该才干需求打通微服务链路、日志调整、流量条件匹配,上下游联动等一系列散布式场景下的才干。

  • 无侵入特性:无需重启运用,动态增强与卸载,能够动态增强整个运用或许是任意节点。

  • 无缺的现场保存才干:能够将抓取到的现场上下文等信息,自动保存至远端的日志系统中。

  • 活络的规则配 :能够活络匹配任意流量,增强任意方法点位,能够活络控制所需的保存上下文内容。

根据以上考虑,我们供给了无侵入的微服务观察才干,能够灵敏帮忙我们处理微服务场景下的复杂问题的定位与确诊,能够更好地为我们的办理供给思路与帮忙,助力于企业构建无缺的微服务办理系统。

观察 loadbalancer 恢复服务发现第一现场

下面来看看怎样处理微服务负载均衡才干?

将凭仗微服务观察才干,在我们寻找到的方位上保存打印政策方法包含入参的现场。我们首要选择 log-demo-spring-cloud-a 运用,在接口列表处选择自定义埋点,并且填入我们所承认的政策类和政策方法。

政策类: com.netflix.loadbalancer.LoadBalancerStats 政策方法: updateZoneServerMapping(java.util.Map)

从负载均衡到路由,微服务使用现场一键到位

由于在这个场景下不需求过滤条件,流量过滤条件部分坚持默许关闭即可。在打印内容部分,由于我们所重视的内容是该方法的入参,因此勾选通用分类中的央求参数,其余选项能够根据需求勾选。

从负载均衡到路由,微服务使用现场一键到位

政策实例根据实际需求选择全部或是指定实例,终究打开规则并点击承认。

查询服务发现结果

在结束上述配备后,我们能够在对应的 SLS 的 LogStore 中查看搜集的日志,其结构大致如下所示。

appName:log-demo-spring-cloud-a
destinationEndpoint:
end:1662541729796
endpoint:10.0.0.24
hostname:log-demo-spring-cloud-a-58b8b7ccc9-gnmsv
interface:com.netflix.loadbalancer.LoadBalancerStats:updateZoneServerMapping(java.util.Map)
ip:10.0.0.24
parameters:[{"unknown":[{"alive":true,"host":"10.0.0.125","hostPort":"10.0.0.125:20002","id":"10.0.0.125:20002","instance":{"clusterName":"DEFAULT","enabled":true,"ephemeral":true,"healthy":true,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000,"instanceId":"10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B","ip":"10.0.0.125","ipDeleteTimeout":30000,"metadata":{"__micro.service.app.id__":"hkhon1po62@622bd5a9ab6ab48","preserved.register.source":"SPRING_CLOUD"},"port":20002,"serviceName":"DEFAULT_GROUP@@sc-B","weight":1.0},"metaInfo":{"appName":"DEFAULT_GROUP@@sc-B","instanceId":"10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B"},"metadata":{"$ref":"$[0].unknown[0].instance.metadata"},"port":20002,"readyToServe":true,"zone":"UNKNOWN"},{"alive":true,"host":"10.0.0.52","hostPort":"10.0.0.52:20002","id":"10.0.0.52:20002","instance":{"clusterName":"DEFAULT","enabled":true,"ephemeral":true,"healthy":true,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000,"instanceId":"10.0.0.52#200...翻开
parentSpanID:-1
ruleName:[237]
serviceType:DYNAMIC
spanID:4096
start:1662541729795
success:true
tag:_base
traceID:ea1a00001816625413997651001d0001
userId:1784327288677274

parameters 部分是被包装成 JSON 格局的入参,将其格局化后能够看到这就是我们想要获取的可用服务列表。

[
    {
        "unknown": [
            {
                "alive": true,
                "host": "10.0.0.125",
                "hostPort": "10.0.0.125:20002",
                "id": "10.0.0.125:20002",
                "instance": {
                    "clusterName": "DEFAULT",
                    "enabled": true,
                    "ephemeral": true,
                    "healthy": true,
                    "instanceHeartBeatInterval": 5000,
                    "instanceHeartBeatTimeOut": 15000,
                    "instanceId": "10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B",
                    "ip": "10.0.0.125",
                    "ipDeleteTimeout": 30000,
                    "metadata": {
                        "__micro.service.app.id__": "hkhon1po62@622bd5a9ab6ab48",
                        "preserved.register.source": "SPRING_CLOUD"
                    },
                    "port": 20002,
                    "serviceName": "DEFAULT_GROUP@@sc-B",
                    "weight": 1.0
                },
                "metaInfo": {
                    "appName": "DEFAULT_GROUP@@sc-B",
                    "instanceId": "10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B"
                },
                "metadata": {
                    "$ref": "$[0].unknown[0].instance.metadata"
                },
                "port": 20002,
                "readyToServe": true,
                "zone": "UNKNOWN"
            },
            {
                "alive": true,
                "host": "10.0.0.52",
                "hostPort": "10.0.0.52:20002",
                "id": "10.0.0.52:20002",
                "instance": {
                    "clusterName": "DEFAULT",
                    "enabled": true,
                    "ephemeral": true,
                    "healthy": true,
                    "instanceHeartBeatInterval": 5000,
                    "instanceHeartBeatTimeOut": 15000,
                    "instanceId": "10.0.0.52#20002#DEFAULT#DEFAULT_GROUP@@sc-B",
                    "ip": "10.0.0.52",
                    "ipDeleteTimeout": 30000,
                    "metadata": {
                        "__micro.service.app.id__": "hkhon1po62@622bd5a9ab6ab48",
                        "preserved.register.source": "SPRING_CLOUD",
                        "__micro.service.env__": "[{"desc":"k8s-pod-label","priority":100,"tag":"gray","type":"tag"}]"
                    },
                    "port": 20002,
                    "serviceName": "DEFAULT_GROUP@@sc-B",
                    "weight": 1.0
                },
                "metaInfo": {
                    "appName": "DEFAULT_GROUP@@sc-B",
                    "instanceId": "10.0.0.52#20002#DEFAULT#DEFAULT_GROUP@@sc-B"
                },
                "metadata": {
                    "$ref": "$[0].unknown[1].instance.metadata"
                },
                "port": 20002,
                "readyToServe": true,
                "zone": "UNKNOWN"
            }
        ]
    }
]

为了证明所获取的是真实的现场,我们通过容器控制台,将该运用所调用的被调用方,伸缩至 3 个节点。在结束扩容后,查看日志发现,可用服务列表按照预期由本来的 2 个实例变更为 3 个实例。

一键处理全链路灰度流量逃逸问题

有时某个功用发版依靠多个服务一同升级上线。我们希望能够对这些服务的新版别一同进行小流量灰度验证,这就是微服务架构中特有的全链路灰度场景,通过构建从网关到整个后端服务的环境阻隔来对多个不同版其他服务进行灰度验证。在发布进程中,我们只需布置服务的灰度版别,流量在调用链路上流转时,由流经的网关、各个中间件以及各个微服务来辨认灰度流量,并动态转发至对应服务的灰度版别。如下图:

从负载均衡到路由,微服务使用现场一键到位

上图能够很好展现这种计划的作用,我们用不同的颜色来表示不同版其他灰度流量,能够看出无论是微服务网关仍是微服务本身都需求辨认流量,根据办理规则做出动态决策。当服务版别产生变化时,这个调用链路的转发也会实时改动。比较于利用机器搭建的灰度环境,这种计划不只能够节约许多的机器本钱和运维人力,并且能够帮忙开发者实时快速的对线上流量进行精细化的全链路控制。

在我们出产环境运用全链路灰度的进程中,我们常常会遇到一些问题:

  • 我们配备全链路灰度的流量流向是否符合预期,我们的流量是否按照我们配备的灰度规则进行匹配。

  • 我们灰度的流量呈现了许多的慢调用、失常,我该怎样承认是我们新版别代码的业务问题仍是由于我们在流量灰度进程中考虑不全导致的系统问题,怎样快速定位问题,然后结束高效的迭代。

  • 在我们规划灰度系统的进程中,我们需求考虑怎样对我们的灰度流量进行打标,有些时分在进口运用、微服务接口处或许难以找到适宜的流量特征(参数、headers 等带着的具备业务语义的标识),在这样的场景下我们怎样便利地对我们的流量进行打标。

根据以上一些列的问题,也是我们在支撑云上客户落地全链路灰度的进程中不断碰到的问题。微服务观察才干也就是我们在这个进程中笼统规划出来的一个才干。针对上诉的问题,我们的微服务观察才干都能够很好地处理。

观察灰度流量,流量逃逸问题无所遁形

关于灰度流量,我们在运用全链路灰度中往往会重视以下三个问题:

  • 我们配备全链路灰度的流量流向是否符合预期,有没有流量打到了非灰度运用
  • 符合灰度规则的流量是否被打上了对应的灰度标签
  • 不符合灰度规则的流量是否存在被误打上灰度标签的情况

由于假如产生灰度流量没能按照预期调度,或许非灰度流量被差错地调度到灰度运用上的情况,不只会影响灰度功用的测验,甚至会影响非灰度运用的正常运行。

为了答复上述的三个问题,我们需求观测灰度流量在系统中真实的标签和途径的才干。为此我们能够增加自定义流量规则,在配备规则的流量过滤条件部分选中对应的全链路灰度标签,并在打印内容中对应我们配备的灰度规则勾选央求参数、Headers 等信息,全部具有该灰度标签的流量的日志会被自动搜集并且打印。

从负载均衡到路由,微服务使用现场一键到位

通过在控制台查询搜集的日志中的参数、Headers 等信息能够判别流量匹配是否正确,不符合灰度规则的流量是否存在被误打上灰度标签的情况。而通过查询灰度流量日志中的 appName 信息能够判别其所通过的运用是否全部都是灰度版别,然后判别全链路灰度流量的途径是否符合预期。

关于是否有匹配的流量没有被打上标签这个问题,我们能够去除标签的流量过滤条件,然后搜集全部的流量,并且通过在控制台对参数等信息的筛选,查询是否有符合条件的流量没有被打上灰度标签。

定位灰度流量的问题

在全链路灰度中,由于全系统统中运行着灰度运用和非灰度运用,相较于日常场景愈加复杂。在灰度流量呈现慢调用或许失常时,快速定位的难度也会更大,而微服务观察的动态打印日志才干能够加快这一进程。

在灰度开端前,假如我们对或许会呈现的问题没有很好的预期,能够先行在进口运用处配备较粗粒度的日志规则,便利我们查询到问题的呈现。

创立规则时选中进口运用,在政策接口列表中,我们能够增加全部的 web、rpc 接口,也能够只增加我们所重视的。随后在流量过滤条件中选中对应的灰度标签、打开慢调用并输入慢调用阈值(在此处慢调用能够是相对原版别较慢的调用而非肯定意义上的慢)或是打开失常,随后在打印内容中选中定位所需的信息,比方央求参数、差错信息、调用仓库等。

从负载均衡到路由,微服务使用现场一键到位

由于该规则运用于进口运用,我们需求翻开后续链路日志的开关,以打印该流量途径上的全部日志。

从负载均衡到路由,微服务使用现场一键到位

在打开规则后,我们能够点击该规则对应的大盘,来查询所搜集的日志。

符合过滤条件的央求和它后续链路的日志都会被搜集,我们能够在央求列表中选择查看某一央求的链路概略,然后发现呈现问题的具体方位。关于某些问题也能够看到其失常仓库,然后承认产生失常的方法调用和导致该失常的或许的内部方法,随后我们能够通过配备自定义流量规则,在政策接口中选中自定义埋点,并输入这些嫌疑方法。

从负载均衡到路由,微服务使用现场一键到位

在打印内容中能够选择央求参数,返回值等信息辅佐判别。在打开规则后便能够打印这些方法的日志,然后判别问题产生的原因。

总结

本文根据常见的服务调用场景,以 Ribbon 负载均衡组件为例,展现了微服务观察才干能够在要害的方位为我们恢复与记载丰厚的现场信息,使得原有的黑盒场景能够便捷直观地被观测到,在微服务架构下,相似的不方便观测的重要场景还有非常多,都能够凭仗微服务观察才干来监测或是在失常时辅佐排查。一同,全链路灰度是微服务办理中比较重要的一个场景,我们在落地全链路灰度的进程中最让人头大的两个问题就是流量路由不生效以及流量逃逸,我们凭仗于微服务观察才干能够快速定位与处理全链路灰度相关的问题。

MSE 微服务观察才干我们还在持续地打磨与完善,旨在帮忙我们更好地办理我们的微服务运用,助力于云上帮忙企业构建无缺的微服务系统。欢迎我们尝鲜与领会~