前言
作为阿里云底层提供的基础设施, 内部的物理网络和许多网络产品在数据平面给客户的可操作性并不高, 从一定程度上来说是个黑盒. 当然, 在传统的 IDC 环境, 业务和物理网络之间也存在同样的隔阂. 所以在遇到业务卡顿, 延迟, 不通等问题的时候, 很容易怀疑到网络. 因此如何抽丝拨茧, 找到正确的方向对症下药才能够真正的解决问题. 毕竟 "真相只有一个".
在进行问题排查和处理的时候, 难度最高的场景就是极度偶发, 复现频率极低的问题. 尤其在网络排查的领域, 通常为了性能和控制资源消耗, 不会将每一个数据包的情况都一一记录下来, 对于一次偶发的应用层记录的超时, 网络层通常没有明确的对应此次应用层调用的包交互记录, 因此排查起来非常困难.
在这次的案例中, 我们通过一个客户端查询 Redis 集群偶发超时的小案例, 来说明一些诊断思路, 排查手段, 进而引出一些在网络方面提高业务稳定性的最佳实践.
问题环境
这次的问题是一个交互性 web 应用中的一个子模块, 主要进行 Redis 查询, 可以简单将其理解为视频弹幕网站中 "查询弹幕" 的小模块. 这个模块的拓扑非常简单:
在上面的拓扑中, 客户使用 ECS 构建了一个 Redis 集群, 前面用 Codis 实现了一层 Redis Proxy (为了通用性, 后面均用 Redis proxy 来描述), 并将这组 Redis proxy 挂载在一个 SLB 后, 通过 SLB 的单一入口提供服务.
问题现象
客户的主要问题是访问其自建 Redis 系统的客户端会不定期出现超时报错, 尽管总体概率不高, 但是报错概率高于其业务运行在原有机房的水平. 超时报错主要有两种情况:
一般情况下超时数量与业务量呈正相关, 非业务高峰期但是 SLB,ECS 的资源使用率均较低.
会存在突发性的大量超时.
诊断思路
作为问题排查的第一步, 首先要了解到问题本身所处的上下文和环境. 在平时诊断问题收集信息的时候, 为了有条理的, 全面的收集信息, 笔者将需要收集的信息分为两种类型: 资源因素和环境因素.
资源因素: 即发生问题的系统的拓扑. 比如涉及的各种应用程序, 主机, 转发设备, 链路资源等, 并且要充分理解这些资源组建在拓扑中起到的作用.
环境因素: 即描述这个问题所需要的信息, 比如报错日志, 问题发生时间, 频率, 应用层设置的超时时间等等.
了解资源因素和环境因素后, 可以将问题的定义明确为: 在哪些资源上发生了什么样的问题, 然后根据该定义收集与问题相关的信息, 并在解读和分析的时候通过数据排除所有的不可能, 这样才能进行高效和准确的问题排查.
在本案例中, 资源因素已经在上文的拓扑中阐述, 问题所涉及的环境因素包括: 客户设置的是 50ms 超时, 在客户端的报错是 read timeout(代表排除了 tcp 的三次握手超时), 报错频率为非业务高峰期一个小时 10 个左右, 业务高峰期 1 小时上百个. 但是偶尔 (一周内偶尔发生一次到两次) 无论业务高峰还是非业务高峰都会有较多的, 上百次的 read timeout 和 connect timeout. 客户已经排查过 Redis, 其查询时间每次耗时不超过 10ms, 而 Redis proxy 没有记录转发的日志.
排查方法
因为所有可用的日志仅能定位到这个系统的两端(客户端, Redis), 需要收集进一步的信息. 面对这种超时类的问题, 最直接, 有效的办法就是进行抓包. 而该问题发生的频率不高, 整个系统流量也比较大, 一直开着抓包很容易将磁盘撑满, 因此需要使用循环抓包:
tcpdump -i <接口 | any> -C <每文件大小> -W <文件个数> -w <保存文件名> 抓包过滤条件
该命令的意思是针对某个接口, 按照过滤条件进行抓包, 保存指定文件名前缀的文件下, 最多占用每文件大小 * 文件个数 的磁盘空间并循环覆盖. 开启循环抓包后, 等待客户端报错再次出现, 即可抓到现场的包交互过程.
抓包的结果文件可以使用 wireshark 打开, 但是使用循环抓包抓到的数据包文件较大, 数量较多, 可以使用下面的小技巧进行快速的过滤:
- // 在安装了 wireshark 的电脑上都会有 capinfos 和 tshark 两个命令, 以笔者使用的 macOS 为例
- ~$ capinfos -a -e *cap // 使用 capinfos 查看抓包文件的其实时间和结束时间, 选取包含报错时间 +- 超时时间的文件, 其他文件就不需要了
- File name: colasoft_packets.cap
- Packet size limit: inferred: 66 bytes - 1518 bytes (range)
- First packet time: 2019-06-12 09:00:00.005519934
- Last packet time: 2019-06-12 09:59:59.998942048
- File name: colasoft_packets1.cap
- Packet size limit: inferred: 60 bytes - 1518 bytes (range)
- First packet time: 2019-06-12 09:00:00.003709451
- Last packet time: 2019-06-12 09:59:59.983532957
- // 如果依然有较多文件, 则可以使用 tshark 命令进行筛选. 比如报错中提到 Redis 查询一个 key 超时, 则可以用以下脚本找到这次查询请求具体在哪个文件中:
- ~$ for f in ./*; do echo $f; tshark -r $f 'tcp.payload contains"keyname"'; done
找到对应的请求之后, 再用 wireshark 打开该文件, 找到对应数据包, 跟踪对应流来找到五元组和整个流的上下文交互.
在本案例中, 通过对比客户端, Redis proxy 和 Redis 三层的抓包, 发现客户端发出请求到收到响应的时间的确在问题发生时话费了 100 多 ms, 而这部分耗时主要发生在 Redis 将响应返回给 Redis proxy 的丢包重传导致. 整体时序示意图如下:
对于从抓包中观察到的丢包现象, 在通过阿里云内部监控确定了物理链路的确不存在丢包的情况下, 我们发现 Redis proxy 所在的 ECS 上, 虚拟化层面的后端驱动在向前端驱动送包的时候, 前后端队列的丢包计数的增长趋势和业务超时的频率有相同趋势, 进一步排查发现客户 ECS 操作系统内的网卡多队列没有开启, 导致只有一个 CPU 处理网卡中断, 而当流量有瞬间突增的时候, CPU 来不及处理网卡中断导致前后端队列堆积, 队列溢出后导致丢包.
为了解决这个问题, 我们建议客户将网卡多队列开启, 并将不同网卡队列中断的 CPU 亲和性绑定在不同的 CPU 上. 对于阿里云 ECS, 可以使用的网卡队列是和实例规格绑定的, 具体可以参考 ECS 实例规格文档. 简单的开启网卡队列并使用 irqbalance 自动调度网卡队列中断 CPU 亲和性的方法可以参考阿里云官方文档.
本案例中客户开启网卡多队列并开启 irqbalance 服务之后, 每小时都出现的访问超时问题已经解决, 但是还是会有每隔几天会出现的突发性大量超时. 经过汇聚客户的报错信息和阿里云底层的网络监控, 我们最终确认这种每隔几天就会出现的突发性大量超时是因为阿里云底层的跨可用区间链路抖动导致的.
阿里云的每一个可用区可以理解为是一个机房, 而不同可用区之间可以互为同城灾备关系. 为了确保可用区之间不会故障扩散, 不同可用区机房需要保持一定物理机距离, 并通过同城传输光缆将所有可用区相互连接实现可用区之间的互访.
连接可用区之间的同城传输光缆的可靠性远低于机房内部跳纤, 且经常容易受到道路施工, 质量劣化的影响, 导致链路中断. 为了保证业务连续性, 阿里云提供了充足的冗余链路并使用传输倒换, 路由切换等技术确保部分跨可用区链路时故障可以自动收敛, 但是在倒换过程中产生的丢包却无法完全避免. 根据阿里云底层监控, 当一条跨可用区链路中断时, 通常会导致持续 3-5 秒的 1% 左右的丢包(具体需要看中断链路数量占总链路数量的占比), 而反映在业务上, 则有可能造成时延敏感业务接近 1 分钟的部分超时报错. 因此在本案例中造成了突增的超时报错.
假如客户使用资源时, 可用区分布非常散乱, 则会造成可用区间链路抖动对业务影响的频率升高. 比如客户的客户端 ECS 分布在多个可用区(A,B),SLB 在可用区 C ,Redis proxy 和 Redis 在可用区 D,E, 则 A 到 C,B 到 C,C 到 D,D 到 E 的跨可用区链路抖动都会影响到整个系统.
最佳实践
通过这个案例我们可以总结出关于主机网络和网络部署方面两个最佳实践:
主机网络方面: 打开网卡多队列并将网卡软中断打散以获取最佳的网络性能. 总体来讲, 为了获得稳定的网络性能, 有以下通用建议:
使用 VPC 实例: 除了网络租户隔离, 支持专线, VPN 网关等好处外, VPC 环境在底层转发能力上也比经典网络实例有大幅提高, 最新一代实例均基于 VPC 环境实现, 因此也提供了更强的网络转发性能.
使用独享型实例: 独享型实例采用严格的资源隔离技术, 确保虚拟机不会收到 "吵闹的邻居" 影响.
打开网卡多队列并绑定网卡软中断处理 CPU 亲和性: 对于不同网卡队列使用不同 CPU 进行处理, 提高网络处理性能
将网卡多个队列分别绑定到某几个专用 CPU 上, 而其他进程绑定到其他 CPU 上, 让网卡软中断处理使用专门的 CPU: 适用于纯转发类, 对网络性能要求极高的服务.
- // 绑定网卡软中断的方法:
- //1. 首先看 cat /proc/interrupts | grep virtio, 在阿里云提供的标准操作系统中, virtio0 是网卡队列
- ~$cat /proc/interrupts | grep virtio
- //omit outputs
- 31: 310437168 0 0 0 PCI-MSI-edge virtio0-input.0
- 32: 346644209 0 0 0 PCI-MSI-edge virtio0-output.0
- // 将第一列的中断号记录下来, 修改下面的文件绑定 CPU 亲和性
- echo <CPU affinity mask> /proc/irq/{irq number}/smp_affinity
- // 具体 CPU affinity mask 可以参考 manpage https://linux.die.net/man/1/taskset, 这里不再说明.
物理网络方面: 建议从业务容忍度和时延敏感度进行权衡来选择业务的部署.
从业务容忍度的角度来说, 如果 tcp 协议中发生了丢包, 那么最坏情况下需要等待 RTO 超时才能够重传(tail drop 场景, 其他场景有 fast retrans 机制), 而 RTO 超时的最小取值在 kernel 中的定义为 200HZ, 即 200ms. 对于内网互访或者同城互访这种时延较低的场景, 可以理解为一次丢包的最坏情况就是 200ms 的重传, 因此对于关键业务, 至少将请求超时设置在 200ms 以上, 让 tcp 有一次重传的机会. 而对于非关键业务, 一次查询是否返回数据并不关键的, 则可以将请求超时设置的更小以便保护整个系统. 因此业务容忍有两个方面: 业务可以容忍错误, 或者业务可以容忍重传.
从时延敏感度的角度来说, 为了确保时延敏感业务尽量少的受跨可用区链路影响, 建议尽量将时延敏感业务的整个业务调用都在一个可用区内完成. 而不同的可用区之间尽管提供相同服务, 但是之间不会经常跨可用区调用. 比如 Web server 层调用提供缓存服务的 Redis 在本可用区完成, 只有缓存没有命中的少量情况, 才有可能跨可用区查询数据库, 甚至使用只读实例等其他技术, 将跨可用区链路抖动的影响降至最低.
结束语
通过上面的案例和最佳实践的说明, 可以看到 "权衡" 在业务系统架构涉及和部署当中是无处不在的. 所谓优化就是在给定的环境下, 为了实现业务目标, 将资源倾斜到最需要的地方. 另一方面, 对于许多客户在上云前的系统架构中, 受限与机房成本, 位置等原因, 可能没有使用过多机房的组网场景, 而云计算对这些客户带来的除了基础设施跨代升级的便利之外, 还提供了天然的容灾能力, 而业务系统的架构和部署也需要增加因容灾所带来的组网场景的复杂化.
来源: https://yq.aliyun.com/articles/705634