我们的微服务目前都是在服务器上部署的,也是基于 Docker 来部署的。
运维部门基于 K8s 自研了一套容器云管理平台,平台名称叫做 Ares,我们也开始准备将微服务迁移到这平台上,降低虚拟机或实体机服务器运维成本,提高服务器资源利用效率。
希腊神话中为战争而生的神,奥林匹斯十二神之一,被视为尚武精神的化身。看起来很牛逼的样子!
微服务框架使用了流行的 Spring Cloud 框架。
框架技术组件如下:
网关 Zuul 层使用了 Ribbon 做负载均衡、Hystrix 做限流熔断。
后端微服务使用了阿里巴巴开源的 Sentinel 做限流熔断。
由于当时服务器的配置不同,比如有低配置的虚拟机,还有高配置的物理机服务器。
所以呢,我们基于当时的服务器配置现状,基于 Ribbon 自行扩展了按照权重的负载均衡策略,对 Eureka 注册中心管理界面做了一点改造,能够支持动态对每台服务器变更权重。
因为本文的问题跟 Eureka 注册中心有关,对 Eureka 架构做个介绍下。
Eureka 注册中心简易架构图:
上图简要描述了 Eureka 的基本架构,由3个角色组成:
1)Eureka Server
提供服务注册、发现、健康检查。
2)Service Provider
服务提供方,
将自身服务注册到 Eureka,从而使服务消费方能够找到,
我们将容器可以作为服务提供者,会注册到 Eureka。
3)Service Consumer
服务消费方,
从Eureka获取注册服务列表,从而能够消费服务
我们可以将 Zuul 网关作为服务消费者。
考虑到使用的 Spring Cloud 框架,结合运维提供的容器平台。
制定容器化部署架构如下:
选择镜像及版本、CPU、内存配置、配置健康检查、日志收集、Pod 副本数量。提交创建容器。
容器启动时,申请 SLB VIP,作为服务注册 IP,向 Eureka 上发起注册。
域名请求,DNS 解析经过 GSLB(全局软负载均衡)负载到 Zuul 网关,Zuul 网关从 Eureka 注册中心拉取服务注册表,通过 Ribbon 负载均衡,从本地服务注册列表中,选择其中一台 Server,发起 Http 调用。
容器内注册的是 SLB VIP(软负载均衡),这个 SLB 通过内部的 Nginx 负载均衡机制,轮询到后端的容器的多个 Pod IP 上,Pod IP 正是我们部署的微服务业务。
当时我们对接口压测时,发现使用 K8s 内部的 Service IP 存在性能瓶颈,该问题还在研究中。后来运维内部商榷,使用 SLB 来达到负载均衡的效果。
另外说明一点:
运维基于 K8s 自研的这套容器平台,网络层面做了重新架设和优化,打通了各个机房的网络。
这样做给我们的架构部署带来了好处:
前期目标仅为了迁移微服务业务,考虑到稳定性等因素,正式上线的Zuul网关和Eureka 注册中心部署在 K8s 集群外,微服务业务部署在容器内,因网络可通,容器启动后申请的 VIP,可以直接注册到 Eureka 上。
仿真环境(预上线环境)是直接将Eureka注册中心,也部署在了容器平台中,接下来会说下,因此导致的一些问题,以及解决该问题的方式。
容器测试阶段结束,由于运维调整为了 SLB VIP,将以前的应用(一个应用下包含多个 Pod 容器)都删除掉,我们重新搭建一套仿真环境用于,上线前的性能测试环境。
但是当我们部署完 Eureka 后,发现以前删除掉的应用VIP 也注册上来了,而且这个 VIP 网络是不通的,无法访问的。
Eureka 管理控制台示意图:
telnet 命令测试:
telnet 10.11.195.197 80 Trying 10.11.195.197... telnet: connect to address 10.11.195.197: Network is unreachable telnet: Unable to connect to remote host
结果提示 10.11.195.197 这个 VIP,网络是不可达的。
起初,跟运维老哥请教,经过容器内排查后,也暂时没有太多眉目,确定是这个 VIP,已经下线了,网络也不通。
按照这个推测,是不太可能注册到 Eureka 上来的。
开始考虑到以为是 Eureka 机制是不是有问题,但仔细用「屁股」猜想论思考一下,结合 Eureka 框架底层原理来看,是不应该出现这个情况。
根据 Eureka 续约机制,一定是有哪个「哥们」在默默给这个服务 IP 发续约( 向注册中心发送心跳
)。
我们在 Eureka Server 服务端,也有监听各个动作的机制,如注册服务、续约服务、下线服务,根据日志看,也的确是有这个服务 IP 一直在发送续约动作。
续约监听代码:
@EventListener public void listen(EurekaInstanceRenewedEvent event) { InstanceInfo instanceInfo = event.getInstanceInfo(); if (instanceInfo != null) { logger.info("renew ...." + instanceInfo.getInstanceId()); } else { logger.info("renew ....instanceInfo is null"); } }
既然引出了上述问题,当然不能放任不管,一定要一探究竟。
这种问题你若不理他,早晚会搞出点别的事情来的。
Eureka 服务端已经收到了注册和一直续约的请求,说明一定是有哪个服务一直在偷偷发送心跳。
到底是谁干的啊?
运维老哥暂时比较忙,看来只能先查找网络链路,抓取网络数据包看看到底是怎么回事了。
网络工具一般常用的就是 tcpdump、Wireshark。
Wireshark 小故事:
大概发生在 10 几年前,主导 Ethereal(应该听说过吧)的大佬跳槽了,然后这个商标就不能继续使用了,但是这个工具在当时来说人气很旺,后来大佬就将项目更名为 Wireshark 了。
服务器上命令行的抓包程序 tethereal 更名为了 tshark。
容器镜像中默认是不会自带这些工具的。镜像中 Linux 操作系统使用的是 CentOS,通过自带的 yum 源安装网络工具包,比较方便。
安装 wireshark:
yum install -y wireshark
安装 tcpdump:
yum install -y tcpdump
这里我们使用的是 Wireshark 工具,简单介绍下这个工具:
如果你要看到全部网络数据包,直接执行tshark命令即可。
1)获得 tshark 命令帮助
tshark --help
2)tshark 抓包模式参数一览
3)tshark 命令实战使用
打印源目标 Host 及 Http 协议信息:
tshark -s 512 -i eth0 -n -f 'tcp dst port 80' -t ad -R 'http.host and http.request.uri' -T fields -e "frame.time" -e "ip.src" -e "http.host" -e "http.request.method" -e "http.request.uri" | tr -d ' '
-i 捕获 eth0 网卡;
-n 禁止所有地址名字解析(默认为允许所有)
-t 设置解码结果的时间格式。
"ad"表示带日期的绝对时间,"a"表示不带日期的绝对时间,"r"表示从第一个包到现在的相对时间,“d”表示两个相邻包之间的增量时间(delta)
-R 设置读取(显示)过滤表达式(read filter expression)
-T -e 输出指定的字段
执行结果:
来段文本:
[root@mas-manager-eureka-es1-66cb79bfb7-snmxm manager]# tshark -n -t a -R http.request -T fields -e "frame.time" -e "ip.src" -e "http.host" -e "http.request.method" -e "http.request.uri" | grep 10.11 Running as user "root" and group "root". This could be dangerous. Capturing on eth0 Sep 27, 2019 00:22:05.174770971 10.124.12.169 10.124.14.4 PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569490783397 Sep 27, 2019 00:22:13.814821143 10.124.11.125 10.124.14.4 PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569407741389 Sep 27, 2019 00:22:15.180243816 10.124.11.123 10.124.14.4 PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569490783397
通过抓包,根据问题 IP 过滤得到的结果,我们看到了
/eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569490783397
说明是有 IP 在上报,上报来源 IP 就是第二列的 IP。
将这个信息提供给运维同学,根据来源 IP 去继续查找线索。
但是,发现第二列 IP 并不是实际的服务器节点 IP,查不到。因为这些 IP 都是主机上的虚拟 IP,每次上报来源 IP 不同,所以还要反向查找实际归属的主机节点。
实际是 SLB 节点上虚拟 IP,因为会负载到它所属主机节点,这台主机上默认只能支撑最大 65535 个 TCP 连接,所以为了单机能支撑更高的 TCP 连接数,会虚拟出来很多个 IP。假设有 10 个虚拟 IP,每个虚拟 IP 支撑 65535 个 TCP 连接,这台主机总共可以支撑 10 * 65535 = 60万以上的连接数了。
K8s 有多套集群,每个集群中有很多台主机节点,茫茫主机池中怎么去查找这些虚拟 IP 呢,
其实我们的目的是为了找到,谁往注册中心发送请求了,还是可以继续通过抓取网络数据包来定位这个问题。
这些网络数据包一定会经过 K8s 集群里的某一台节点,一台一台去找,很麻烦, 编写简单脚本抓包查找
:
#!/bin/bash tcpdump -i any host 10.124.14.4 -n -s 0 -X -l | grep 10.11.195>/tmp/1.txt & sleep 20s kill -2 %1 cat /tmp/1.txt
网络抓包结果:
截取了关键的抓包信息:
11:41:56.598204 IP 10.110.157.81.54078 > 10.124.14.4.http: Flags [P.], seq 273:622, ack 218, win 245, options [nop,nop,TS val 3348483954 ecr 1420800289], length 349: HTTP: PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569394834392 HTTP/1.1
这里的 10.110.157.81.54078
就是主机节点 IP,目标地址 10.124.14.4
就是容器内的 Eureka 注册中心地址。
发送的请求是 /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569394834392
这个请求地址上就带了 10.11.195.197 这个网络不可达的 IP 地址。
到这里,其实我们已经基本定位到了,一定是从 K8s 集群容器内发出来了。
根据这个有价值的节点信息,连接到 K8s 集群内,查找该节点上部署的容器。
查找 K8s 集群内 Pod 命令行:
kubectl get pod --all-namespaces -o wide |grep 10.110.157.81
部署在改节点上的容器:
运维根据 Eureka 上名称大概猜测一下,终于找到这个「罪魁祸首」的容器了。
进入容器内,查看配置 SVIP (Eureka上的注册IP)就是 10.11.195.197 这个IP。
将这个问题容器彻底关闭后,没有再继续发送续约请求,Eureka 注册中心上过了一段时间摘掉了 IP。
大家可能有疑问,这么繁琐,为啥不直接到 K8s 集群内去找,因为 K8s 集群内目前已有业务在运行着,集群内有几百个容器在跑着。当时运维一起测试时,容器名称都是自定义的,所以不是很好查找。
咱们经过这个过程的排查,确认了这个 Eureka 注册中心上的地址虽然不通,但是一直是有容器在上报,而上报的「ServerId」指向的 10.11.195.197:80 地址。
我们也可以结合底层源码了解下。
Eureka 续约时序图:
接口实现方式跟注册服务类似,更新自身状态后,会同步到其他集群节点。
PeerAwareInstanceRegistryImpl 类的 renew 方法会调用到 AbstractInstaceRegistry 抽象实例注册类的 renew 方法。
AbstractInstaceRegistry#renew 方法源码:
会根据服务 id 从注册表中获取 Lease 对象,如果不为空,则完成续约,更新 lastUpdateTimestamp 字段。
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
是一个 ConcurrentHashMap 结构,Key 就是应用名称,Value 也是一个 Map 结构。
Map 结构中的 Key 是注册ID(IP + 端口),Value 是 Lease 服务续约对象,里面包含了动作类型,最后上报(心跳)更新时间戳等等信息。
容器服务作为 Eureka Client,每隔一定时间间隔(默认60秒)向注册中心发起一次续约。
Eureka Server 会定时检测服务实例心跳是否正常,如果间隔一定时间(90秒),还没有来续约,就会将这个服务从注册中心摘除掉。
总结上述分析过程,一图胜千言:
重要的不是结果,而是这个过程,希望你也能享受这个过程。
Wireshark 使用文档:
https://www.wireshark.org/docs/man-pages/tshark.html
Netflix Eureka 源代码:
https://github.com/netflix/eureka
欢迎关注我的公众号,更多精彩文章,与你一同成长,扫码二维码关注~