文章目录

一、和Istio的融合问题

1.1 Istio的mTLS默认开启导致无法访问K8S服务

在Istio 1.4.x版本,Mesh Policy 默认开启全局mTLS(values.global.mtls.enable=true),如果服务网格没有定义DestinationRule,那么就会使用mTLS。

如果此时网格内的服务想要访问外部的K8S服务(比如数据库等,没有进入网格的K8S服务),istio会以为它是网格内的,仍然以mTLS建立连接,导致对端服务无法响应。因此,需要显式地设置Istio的DestinationRule,禁用掉mTLS:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: mysql-1
  namespace: base-service
spec:
  host: mysql-1.base-service.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE

1.2 Istio iptables拦截导致性能问题

裸机上面PET测试性能挺好,放到K8S+Istio了之后,性能下降了很多,主要是因为开启了sidecar(Envoy)。

这个版本的Istio用Envoy修改ptables做流量拦截(注意是Istio-Proxy,不是Kube-Proxy),iptables是规则列表,每次都是O(n)遍历,随着服务和Pod的增多,时间会越来越长。

这种拦截尤其对长连接的消耗大,于是最终我们把连接节点放在了服务网格之外,放弃sidecar。目前考虑的是开始转用ipvs(通过Hash,很快)。

参考文章:

《理解 Istio Service Mesh 中 Envoy Sidecar 代理的路由转发》

《kube-proxy 模式对比:iptables 还是 IPVS?》

1.3 Istio 网格外无法访问网格内的服务

把连接节点放到网格外后,发现无法访问网格内的服务,其实也是mTLS导致的。Istio proxy(也就是Envoy)默认全局开启mTLS,不接受除mTLS以外的普通流量,网格外的服务没有Istio proxy,无法建立连接。

一种方式是将全局mTLS改为false(values.global.mtls.enable=false),这样mTLS和plain text流量都能进来。

还有一种方式是Istio提供的Policy CRD(CustomResouceDefinitions)配置服务的mTLS,将其设置为PERMISSIVE策略:

apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
  name: your-service-in-service-mesh-policy
  namespace: your-service-in-service-mesh-namespace
spec:
  targets:
  - name: your-service-in-service-mesh
  peers:
  - mtls:
      mode: PERMISSIVE
  • Strict:只接收TLS流量(默认)
  • Permissive:可以接收普通流量,也可以接收TLS流量
  • Disable:只接收普通流量,禁止使用TLS

1.4 应用监听Pod的IP导致Istio的流量进不来

最开始是纯K8S部署,我参考EMQ的DockerEntrypoint脚本文件编写DockerFile,能够成功获取到容器IP并用Netty绑定监听:

LOCAL_IP=$(hostname -i |grep -E -oh '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])'|head -n 1)

然而上了Istio之后,发现完全不行了,没有流量进来,服务器也没有任何异常。最终发现是因为Istio-proxy最终会将流量导向127.0.0.1,所以容器的业务必须监听“0.0.0.0”而不是容器的IP,否则流量导不到业务里去…

这里产生了新的问题,就是我有一些特殊需求,需要注册连接节点的信息,之前是利用容器IP去注册,现在由于监听端口都是0.0.0.0,无法区分,因此需要将绑定IP和注册IP分开,“Netty绑定IP=0.0.0.0”、“注册IP=容器IP”。

即使这样还是有问题,就是如果K8S有多个集群(比如多个区域),且互通,那么IP地址段必须不能是一样的,否则Istio+K8S路由会出现问题,好在现在多个集群的IP地址段互不相同…

1.5 Istio适配总结

最近发现有个“腾讯云原生”的博客园博客,这篇文章《istio 常见的 10 个异常》几乎把上面的坑都盘了一遍,今天9月份发布的,要是早点发,我们也不至于一个坑一个坑地跳…

另外我们现在用了这么久,其实也只用到了Istio中的1个功能,其他功能还在逐步试探,“熔断”和“金丝雀发布”是下一步要做的。

二、长连接稳定性和连接量问题

长连接最重要的就是两个指标:

  • 稳定性:客户端连多长时间不会断线
  • 连接量:指定CPU和内存的机器,能够连到多少的长连接?

网上的那些性能测试看起来十分玄乎,比如千万级长连接等等,他们都是把性能打满了的,硬往上怼出来的连接量,极限性能测试。

这里给出两个实际生产中的较好的标准:

  • 稳定性:在客户端网络稳定的情况下,1周不断线
  • 连接量:2核8G的机器,在正常流量的情况下,维持15W~20W的连接量,保持60%左右性能剩余(为什么我忘记了,后面再写)

实际上我们目前还没有达到这个标准,还在努力,先说一说我们做了哪些事情。

2.1 Linux参数优化

对Linux参数进行优化,网上已经有很多,比如EMQ官方的系统调优,给出了详细的参数。最重要的一个参数是“文件描述符”。

Linux把一切都看成文件,进程打开或创建新文件时,内核会向进程返回一个文件描述符,它是当前进程中最小可用的文件描述符。在Linux中一般会使用socket创建TCP连接,每个socket都会占用一个文件描述符,也就是说,每条TCP连接至少会占用1个描述符(有的TCP连接在执行了fork之类的情况下会占用多个文件描述符)。默认情况下,一个进程只能打开1024个(且有3个会被stdin/stdout/stderr占用)文件描述符,因此我们需要修改它的默认值,如EMQ的那样:

ulimit -n 1048576
注意:最大连接数和服务器的端口号是没有关系的,别想着只能连65535-1024个长连接。TCP是4元组标识的,客户端IP、客户端端口、服务端IP、服务端端口,服务器基本都是只监听1个端口(当然MQTT服务器可能监听2个或4个,分别用于TCP/WS/TCP-SSL/WS-SSL),所以TCP数量的限制在客户端,最多就是客户端IP数量×客户端端口数量那么多个,远远超出服务器性能承载量。

Linux在裸机上调参很好处理,但在K8S上,由于这些参数都是“机器”级别的,并不是Docker中的容器级别,这些参数的调整会影响到其他部署在该Node上的全部Pod,因此我们线上只调整了文件描述符中1个Linux参数。

之后的思路就是,好像K8S可以指定某些Pod只部署在某些机器上,因此相当于给连接节点专门开了一个机器集群,但是K8S一般都会选用很大的机器作为Node,这里可能就要考虑选用小机器了。做完这个之后,就可以只针对连接点调整所有Linux参数。

另外,这个openFiles我们用prometheus+kibana采集了,作为监控指标,非常方便:

2.2 负载均衡器ELB的各种问题

(1)ELB的默认心跳时长60s

在最初的瞎搞阶段,我们发现部署到K8S之后,客户端总是断线。之前我在裸机上测试功能和性能都OK的。排查了很久K8S的问题,最终我想计时断线时间看看规律,果然发现一个现象:固定在60秒断线。于是同事突然想起来ELB有自己的心跳时间,默认是60秒,而我们的MQTT的keepalive时间比这个长,如果客户端不发包仅靠心跳维持长连接,必然会在60秒被ELB切断。将ELB的心跳长之后,不会再有类似的情况发生。

(2)SSL终结问题

我们没有踩这个坑,直接通过经验规避了。因为SSL的加解密比较消耗CPU,因此不应该在连接节点上进行SSL加解密,而是在ELB直接终结了。EMQ也是这样做的《生产部署》,这是业界常见的经验。

(3)稳定性和连接量问题

刚上线发现不太稳定。先是走了一些弯路,最后实在不行还是只能通过抓包(tcpdump)+wireshark来观察和分析,发现有的情况下是连接之后没有发包,心跳丢失断开,这个是客户端的问题。有的情况是连续发送三个PingResp之后,丢失心跳断开,这个看起来像是服务器的问题,或者是服务器->ELB->客户端的链路上某个组件问题,但最早的时候观察Prometheus,并没有发现服务器有问题,于是便搁置了。后来同事解决了该问题,参见:《长连接波动队列暴增与连接量限制排查》

(4)ELB的连接数均匀分发

相比于短链接而言,长连接是有状态的,ELB的分发一般是轮询的,均匀分发到各个后端服务器,在刚开始倒是没问题。但是服务器是可能会宕机的,如果服务器宕机了,情况就变得不均匀了,举个例子:

  • 刚开始的时候:A服务器0,B服务器0;
  • 当客户端连上之后:A服务器100,B服务器100
  • 当A服务器宕机之后:A服务器0,B服务器200
  • A服务器恢复,继续均匀分发50个连接:A服务器50,B服务器250

比较好的方式是,后端服务器提供一个指标作为权重(比如连接量),然后ELB根据这个权重去分发连接请求,但目前并没有这样的机制,这也是目前我们无法解决的问题。

2.3 客户端的指数回退

在某些情况下,比如一片区域停电,会导致客户端集体上线,为了规避类似的这种情况对服务器造成的冲击,最好和设备端商量一个退避连接方式,例如常见的指数回退策略。

2.4 中间链路对长连接的限制

线上观察到很多客户端仍然60秒断开,但我们的ELB参数已经优化好了。向大佬们请教后,发现长连接的中间设备,也许会设置一些特殊的规则,比如60秒不发包就断开等等,一般出现在跨区连接上面较多。因此如果有多区域的需求,最好是进行多区域部署,这样让长连接尽量是本地连接的。

2.5 优雅停机

在需要滚动更新的时候,如果一下断开整个节点的长连接,很可能会引发大量客户端迅速重连,如果此时没有提前准备好新的节点,或者某个旧节点本身压力已经非常大量的情况下,很可能发生接连的宕机的雪崩情况。因此,正常断开长连接的姿势是,先将节点从ELB移除出去(此时长连接仍然是维持的),然后执行脚本分批间隔踢掉客户端,缓慢而稳定地刷掉连接。目前这些经验只在EC2上实践过,在K8S上如何执行这个方案还需要时间实践

我们已经实践过了,在K8S上面,不需要动ELB,只需要先启动3个以上新实例来接收流量,然后简单地修改掉selector的标签,让新连接的流量导入到新实例上面去,然后再执行旧连接的踢连接脚本即可。我们发现当量上去之后,真的不能直接停掉长连接节点。另外,如果旧连接踢得太快,由于MQTT有遗嘱等设计,而且踢掉连接是会有清理的动作的,相关中间件、数据库等压力会瞬间上涨(主要是CPU),必须要控制连接速度。

三、其他问题

3.1 TCP换腿

客户端并不会总是连接上同一个节点,因此每次客户端掉线重连时,必须要及时踢掉前一个TCP连接,避免等待TCP的心跳超时(或者等待MQTT协议的心跳超时)才执行断开操作。这里还有很多的隐藏问题需要思考:

(1)如果两条TCP在同一个机器上,如果是根据IP踢旧连接,如何区分两条连接

(2)如果客户端同时发起两次TCP连接(实际上生产中经常发生…),如何保证并发安全。

(3)如果两条TCP在不同机器上,踢的过程中肯定有时延,同时维护两条TCP,会不会对服务器性能有影响。

(4)如果前一条TCP没能踢成功,会不会对业务有影响。

(5)如果前一条TCP在断开的时候发出MQTT的遗嘱,会不会对现有业务有影响。

(6)新的连接是加分布锁禁止新连接,还是并行新连接、剔除旧连接

3.2 内存泄漏

使用Netty一定要注意内存泄漏问题,参考《Netty内存泄漏排查》


转载请注明出处http://www.bewindoweb.com/302.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠