本文导读;
微服务技术架构选型介绍k8s 容器化部署架构方案Eureka 注册中心问题场景问题解决手段及原理剖析阅读本文建议先了解;
注册中心基本原理K8s;Kuberneters;基本概念我们的微服务目前都是在服务器上部署的;也是基于 docker 来部署的。
运维部门基于 K8s 自研了一套容器云管理平台;平台名称叫做 Ares;我们也开始准备将微服务迁移到这平台上;降低虚拟机或实体机服务器运维成本;提高服务器资源利用效率。
Ares;阿瑞斯;战神;
希腊神话中为战争而生的神;奥林匹斯十二神之一;被视为尚武精神的化身。看起来很牛逼的样子;
微服务框架使用了流行的 Spring Cloud 框架。
框架技术组件如下;
注册中心选择的是 Eureka网关使用的 Zuul配置中心使用的 Apollo熔断限流使用了 Sentienl Hystrix网关 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 框架;结合运维提供的容器平台。
制定容器化部署架构如下;
从容器创建到可访问流程;
1;创建容器
选择镜像及版本、CPU、内存配置、配置健康检查、日志收集、Pod 副本数量。提交创建容器。
2;服务注册
容器启动时;申请 SLB VIP;作为服务注册 IP;向 Eureka 上发起注册。
3;网关发起请求
域名请求;DNS 解析经过 GSLB;全局软负载均衡;负载到 Zuul 网关;Zuul 网关从 Eureka 注册中心拉取服务注册表;通过 Ribbon 负载均衡;从本地服务注册列表中;选择其中一台 Server;发起 Http 调用。
4;容器提供服务
容器内注册的是 SLB VIP;软负载均衡;;这个 SLB 通过内部的 nginx 负载均衡机制;轮询到后端的容器的多个 Pod IP 上;Pod IP 正是我们部署的微服务业务。
为什么要使用SLB VIP呢?
当时我们对接口压测时;发现使用 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 不同;所以还要反向查找实际归属的主机节点。
为什么要有虚拟 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 字段。
Eureka 注册表的数据结构;
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
欢迎关注我的公众号;更多精彩文章;与你一同成长;扫码二维码关注~
迅捷CAD编辑器怎么使用VIP功能?-迅捷CAD编辑器VIP功能使用教程攻略