ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

亿级用户IM系统 之 DPVS动态权重(二、根据后端真实情况负载均衡)

亿级用户IM系统 之 DPVS动态权重(二、根据后端真实情况负载均衡) 引子那天看到一个年薪百万的工作某通信公司招亿级用户的IM系统的总负责人其中一项工作是对接入网关进行负载均衡优化。今天我按AI给出的方案总结一下一起讨论一下亿级IM的构架。前文我们讨论了机房的网络结构以及DPVS的使用模式但是由于每个连接上真实业务有区别而造成个别后端服务负载过大的情况目录一、DPVS默认负载均衡转发规则及其局限性1.1 默认调度算法1.2 在亿级IM系统中的局限性二、动态调度算法与通信机制2.1 整体架构设计2.2 关键设计要点三、内核参数绑定与全链路调优3.1. 核心资源隔离最关键的一步3.2. 网络与DPDK层调优3.3. 应用与监控层调优3.4 CPU资源隔离与绑定核心策略 总结一、DPVS默认负载均衡转发规则及其局限性在深入优化之前首先要理解DPVS原生调度机制的工作原理与不足这决定了我们为何要进行改造。1.1 默认调度算法DPVS原生支持多种调度算法这些算法继承自LVSLinux Virtual Server兼容LVS的调度策略。这些算法主要分为静态调度算法和动态调度算法两大类。下面是这些算法的归纳表格方便对比算法类别算法名称英文缩写核心原理与特点适用场景静态调度算法轮询调度RR按顺序将请求依次分配给每台后端服务器循环往复实现简单。适用于后端服务器硬件配置和处理能力完全一致的场景。加权轮询调度WRR为每台服务器配置不同的权重权重高的服务器分配更多请求是RR的增强版。适用于后端服务器性能存在差异的异构集群。目标地址散列调度DH根据请求的目标IP地址进行哈希计算将来自同一目标IP的请求始终分配给同一台服务器。适用于需要将特定服务请求固定到特定服务器的场景。源地址散列调度SH根据请求的源IP地址进行哈希计算将来自同一源IP的请求始终分配给同一台服务器。适用于需要实现会话保持Session Sticky的场景确保来自同一客户端的请求由同一台服务器处理。动态调度算法最小连接调度LC将新请求动态分配给当前活动连接数最少的后端服务器。适用于长连接场景如WebSocket、数据库连接能更动态地适应负载变化。加权最小连接调度WLC在LC算法基础上引入权重连接分配比例与服务器的权重和当前连接数综合相关是更精细的LC。是当前应用最广泛的动态调度算法既考虑了服务器性能差异又兼顾了实时负载。基于局部性的最少连接调度LBLC针对Cache集群设计尽量将相同目标IP的请求调度到同一台服务器以提高缓存命中率。当该服务器负载过高时再选择连接数最少的其他服务器。主要用于Web Cache、数据库Cache等需要高缓存命中率的场景。带复制的基于局部性最少连接LBLCRLBLC的升级版它会维护一个目标IP到“一组”服务器而不是一台的映射。当该组内的服务器都负载过高时会动态地从集群中选一台新服务器加入该组。同样是针对Cache集群的优化提供了比LBLC更高的可用性。最短期望延迟调度SED基于WLC算法改进通过公式(活动连接数1)*256/权重计算出一个期望值选择期望值最小的服务器。在高并发场景下能更高效地找出处理新请求最快的服务器。最少队列调度NQSED的增强版在计算期望值时如果某台服务器的活动连接数为0则无需计算直接分配避免了SED可能出现的饥饿现象。适用于对响应速度有极致要求且存在空闲服务器的场景。注LC最小连接和WLC加权最小连接是动态调度算法中最核心、最常用的两种。DPVS原生支持与LVS兼容的多种调度算法在IM场景中最常用的是加权轮询WRR, Weighted Round Robin工作机制管理员为每台后端网关服务器RS配置一个静态权重如ipvsadm -a -t ... -r ... -w 100DPVS按照权重比例轮流转发新连接。权重越高分配的新连接越多。设计假设WRR假设所有RS的硬件配置相同且每个请求的处理开销是均匀的。1.2 在亿级IM系统中的局限性然而IM业务的请求特征打破了WRR的假设主要体现在以下方面问题现象后果业务请求异构文本消息CPU密集、图片/文件上传IO密集、长轮询内存连接数对不同资源的消耗差异巨大。静态权重无法应对实时负载变化导致部分RS资源耗尽而其他RS仍空闲形成热点。长连接优先级不均高权重RS会积累大量长连接即使其应用层处理能力已饱和DPVS仍会向其转发新请求。用户体验下降且需要额外机制处理RS优雅下线时长连接迁移问题。后端故障依赖外部探测DPVS本身仅支持简单的端口存活检测无法感知应用层卡顿或假死状态。请求被转发到“半死不活”的RS导致超时堆积和业务失败。结论要在亿级规模下实现精细化流量治理必须在DPVS的调度体系中引入动态反馈机制使其能够感知后端RS的实时负载。二、动态调度算法与通信机制为了解决静态权重无法感知负载的问题我们需要建立一套“采集-通信-决策”的闭环系统。在某些应用中我们可能希望按哈希将某些用户映射到同一台机器上但是IM的连接负载可能差别非常大这里应该将IM网关设计为无状态的应用而服务本身使用redis方式上报自己的真实负载或者提供rpc服务方式供查询负载。简单的说我们这里假设IM网关服务将自己的连接数CPU负载等综合情况计算得到一个1-99的负载值。DPVS服务器上外挂一个agent1程序动态刷新到各个IM网关的负载值使用共享内存的方式传递给DPVS。有效权重 静态权重 × (100 - 负载分数) / 1002.1 整体架构设计采用“外挂Agent 共享内存 无锁通信”的高性能架构将控制面与数据面分离采集层Agent每台RS上的轻量级Agent采集CPU、内存、连接数、平均延迟等指标。通信层共享内存为避免网络IO如Redis、HTTP API对DPVS Worker核心造成阻塞我们采用POSIX共享内存shm_open 无锁环形队列进行通信实现纳秒级数据交换。决策层DPVS调度器Worker核心直接从共享内存读取RS的实时负载分数参与调度计算。2.2 关键设计要点数据结构DPVS将获得的后端的服务的IP写到一个地址数组这个数组在agent一侧只读而另外agent一侧写入的包含agent的pid进程存活检测、heartbeat超时保护、score数组1-99的负载分数这些参数在dpvs一侧只读。同步机制Agent以固定频率如1秒计算分数并写入共享内存并使用版本号seq确保DPVS读到的数据一致通过kill(pid, 0)定期检测Agent进程存活若超时则自动降级为静态WRR。性能保障严格遵循DPVSPer-Core无锁化原则Worker核心绝不在转发路径上加锁或发起远程RPC/Redis调用所有动态数据均来自本地共享内存。数据结构定义如下#ifndef __SHM_DPVS_H__ #define __SHM_DPVS_H__ ​ #include stdint.h #include unistd.h ​ #define MAX_GATEWAY_NUM 100 #define HEARTBEAT_TIMEOUT 5 // 5秒超时 #define SHM_FILE_PATH /dpvs_agent_shm ​ struct shm_gateway_table { // 1. 状态标志健康监测 pid_t agent_pid; // Agent的PID uint64_t agent_heartbeat; // Agent最后心跳时间戳秒 uint8_t agent_ready; // Agent是否就绪 (1就绪, 0未就绪) uint8_t dpvs_ready; // DPVS是否就绪 (1就绪, 0未就绪) uint8_t reserved[6]; // 对齐填充 ​ // 2. 数据有效性标志版本号 uint64_t config_seq; // 配置版本号DPVS写入后1 uint64_t score_seq; // 分数版本号Agent写入后1 ​ // 3. DPVS写入后端地址数据 uint32_t count; // 网关总数 struct { uint32_t ip; uint16_t port; } configs[MAX_GATEWAY_NUM]; // 4. agent写入实际负载分数 int scores[MAX_GATEWAY_NUM]; // 负载分数 0~100, -1无效 }; ​ #endif此方案已在业界被验证为成熟的高性能动态调度模式。三、内核参数绑定与全链路调优要让DPVS发挥真正实力调优的核心思路是“资源隔离”与“内核卸载”把DPVS的Worker核心从一切干扰中解放出来。参考携程等大厂的实践以及DPVS社区的优化指南主要有以下几个层面的工作3.1. 核心资源隔离最关键的一步这是保证DPVS稳定高性能的基础。你需要从Linux内核手中把Worker核心“抢”过来交给DPVS独占。内核启动参数隔离在系统的GRUB启动参数里加上isolcpus[cpu-list]把你打算分配给DPVS的CPU核心从Linux调度器中隔离出去。这样系统进程和中断就不会再往这些核上跑了。避开“第一核”不要把CPU 0分配给DPVS Worker它默认要处理大量系统中断和内核线程。建议从CPU 1或更后面的核心开始分配物理核心和它的超线程兄弟不要同时作为Worker使用。关闭超线程(HT)如果条件允许在BIOS里关闭超线程。在DPVS这种CPU密集型场景下关掉HT有时比开着性能更稳定。必要时使用nohz_full将隔离的核心进一步配置为nohz_full让它们尽可能少地响应时钟中断减少“打盹”对转发性能的影响。做完这步你的Worker核心就成了“专车专用”性能会非常稳定。3.2. 网络与DPDK层调优网卡队列与CPU绑定确保网卡的多队列RSS与DPVS的Worker核心一一对应。这样入向流量才能均匀散列到各个核心避免单核成为瓶颈。使用大页内存(1GB)在GRUB启动参数中加入default_hugepagesz1G hugepagesz1G hugepages[数量]并确保有足够的内存比如8GB以上。大页能显著降低TLB miss提升内存访问效率。多核模式支持高核数服务器如果你的服务器核心数超过64个需要修改源码中的CONFIG_DPVS_MAX_LCORE宏定义并重新编译否则DPVS只会看到一部分核心。正确加载KNI模块如果启用了KNI功能加载rte_kni.ko时务必加上carrieron参数否则KNI虚拟网卡可能无法正常工作。前文提及某些版本需要打DPDK补丁3.3. 应用与监控层调优关闭Debug日志与抓包生产环境务必关闭所有Debug级别日志和抓包功能这些功能对性能影响极大。监控大循环与丢包通过dpip link show -s监控网卡是否存在imiss丢包或通过CONFIG_RECORD_BIG_LOOP监控Worker核心是否存在处理延迟Big Loop。出现这些问题往往是前面的隔离没做到位。避免资源争抢禁止在DPVS所在机器上运行其他CPU密集型任务。否则会导致Worker核心无法及时处理消息出现ipvsadm命令超时等问题。另外如果DPVS的CPU占用过高确实可能“饿死”Agent进程尤其是当Agent没有被精心部署时。这是一个非常现实的生产环境问题。3.4 CPU资源隔离与绑定核心策略基于我们讨论的“抢核心”思想资源分配需兼顾DPVS、Agent、RPC/Redis等辅助组件这里假设服务器有16个物理核把0核给系统用最后一个核给外挂用CPU核心分配用途绑定方式说明核心0系统管理保留供Linux内核、sshd、系统中断使用不做任何负载均衡业务。核心1-14DPVS Workerisolcpus1-14通过内核启动参数物理隔离专用于DPVS数据包转发彻底排除其他进程干扰。核心15Agent 控制面taskset -c 15 ./agent 绑定Agent进程、ipvsadm命令、以及FRR路由协议栈等控制面组件。独立核心Redis / RPC服务独立核心若Agent需上报数据或配置下发建议将Redis或RPC服务进程也绑定到独立核心避免竞争。可以设置新增的agent1在代码中强制绑定最后一个核#include sched.h // 需要添加这个头文件 #include sys/sysinfo.h // 如果需要额外检测也可以 ​ /** taskset -c 15 ./agent * 将当前进程绑定到最后一个可用的 CPU 核心上 * return 0 成功, -1 失败 */ int bind_to_last_cpu(void) { int num_cpus sysconf(_SC_NPROCESSORS_ONLN); if (num_cpus 0) { fprintf(stderr, ⚠️ Failed to get CPU count, skip binding\n); return -1; } ​ int last_cpu num_cpus - 1; cpu_set_t mask; CPU_ZERO(mask); CPU_SET(last_cpu, mask); ​ if (sched_setaffinity(0, sizeof(mask), mask) ! 0) { perror(⚠️ sched_setaffinity failed); return -1; } ​ printf(✅ Agent bound to CPU %d (total %d cores)\n, last_cpu, num_cpus); return 0; } ​ // 在 main() 函数最开头调用 int main() { // 第一步绑核 bind_to_last_cpu(); // ← 添加这一行 ​ signal(SIGINT, sigint_handler); // ... 你原有的代码 ... } ​ //# 在另一个终端查询 //$ taskset -cp 3146 //pid 3146s current affinity list: 15 总结亿级DPVS的优化本质上是流量调度算法动态感知、进程间高速通信共享内存与系统资源硬隔离内核调优三者的深度结合。方案严格遵循数据面与控制面分离、Per-Core无锁化的原则确保系统在面对海量连接和复杂业务时能实现自动负载均衡与极致性能。该架构已在多家互联网大厂的IM系统中得到生产环境验证。但是IM网关的负载均衡需要考虑的远不只这些下文再说。未完待续
返回列表