负载均衡原理及实现

article/2025/10/10 11:49:01

什么是负载均衡?

负载均衡( LoadBalance ),顾名思义就是把任务压力进行平衡的分摊到集群中各个操作单元(或机器)上,使得避免集群中部分机器压力过大而部分机器过于空闲。经过负载均衡,使得每个机器获取适合自己的处理能力负载。


负载均衡分类

负载均衡可分为硬件负载均衡和软件负载均衡。

硬件负载均衡比如F5、NetScaler等价格比较昂贵,一般开发中很难接触到。但软件负载均衡还是很容易接触到的,比如大家常见的Nginx、LVS等。

软件负载均衡又可以分为四层负载均衡和七层负载均衡。

所谓四层负载均衡就是在网络层基于IP+端口的负载均衡。也就是通过报文中的目标地址和端口,在加上负载均衡设备的选择方式从而选择最终对应的服务器。

所谓七层负载均衡就是在四层的基础上在应用层根据用户的http请求头和URL等信息将请求转发到特定的服务器上。LVS(Linux Virtual Server)为四层负载均衡,Nginx可以做四层也可以做七层。

负载均衡常见算法

对于软件中实现的负载均衡,常见的负载均衡算法有轮询(Round Robin)法、随机(Random)法、加权轮询(Weight Round Robin)法、加权随机(Weight Random)法、源地址哈希(Hash)法、一致性哈希(Consistent Hash)法等。


1、轮询(Round Robin)法

将接收到的请求按照顺序依次分配给各个服务器对外提供服务。

代码如下:

#include <stdio.h>int position = 0; 
int totalServer = 4;
pthread_mutex_t mut;//互斥锁char * serverInfo[]=
{"192.168.1.1","192.168.1.2","192.168.1.3","192.168.1.4",};char * getServerUrl()
{char * ret = NULL; pthread_mutex_lock(&mut);if(position >= totalServer )position = 0;position++;ret = serverInfo[position];pthread_mutex_unlock(&mut);return ret;
}

轮询就是对各个服务节点依次顺序遍历,当请求的次数 position 达到最大服务节点数量时,重新从0开始。

其实可以把服务节点列表想象成一个圆中的各个圆点,当访问请求次数到达最大值时也就循环是一周,然后在从0开始。

优点:对于各个服务节点对外提供服务的次数绝对均衡。

缺点:由于使用了 position 获取服务节点,因此对 position 的修改要做到互斥,所以对 性能会造成一定的影响,同时对于低承载能力的服务节点不友好,因为它要和高承载能力的服务节点接受同样的压力。


2、随机(Random)法

根据后端服务节点的个数,从中随机选择其中一个节点对外提供服务。

代码如下:

char * getServerUrl()
{int i = rand() % totalServer;return serverInfo[i];    
}

优点: 算法实现简单,当请求的次数增大时,各个服务节点处理的请求数量近似于均衡。

缺点: 和轮训算法一样,对于低承载能力的服务节点不友好。

3、加权轮询(Weight Round Robin)法

上述的轮询法和随机法存在一个共同的缺点,就是各个服务器节点负载相对均衡,但是当各个服务器节点载能力不同时,会对低承载能力的服务器节点压力过大。

正是由于这个缺点,后续有了加权轮询法、加权随机法,该算法的实现就是给不同配置的服务器节点,配置不同的权重值,后续分配请求时,对权重高的服务器节点,多分配点,对权重低的服务器节点,少分配点。

如下实现了一个加权轮询算法:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define IP_LEN  22typedef struct {int weight;int cur_weight;//哪个server当权权重最大,就分配个谁char ip[IP_LEN];
}serverInfo;int getWeightSum(int *weight, int size)
{int i = 0;int total = 0;for(i = 0; i < size; i++){total += weight[i];}return total;
}serverInfo * initServer(char **ips, int *weight, int size)
{int i = 0;serverInfo * servers = calloc(size+1, sizeof(serverInfo));for(i = 0; i < size; i++){servers[i].weight = weight[i];memcpy(servers[i].ip, ips[i], IP_LEN);servers[i].cur_weight = 0;}return servers;
}/**该算法参看nginx中平滑的加权轮询算法,该算法会保证生成的序列比较均匀而普通的加权轮询算法在某种权重下会出现不均匀的序列
*/
int getServerUrlIndex(serverInfo *s, int size)
{int i=0;int index = 0; //记录当前权重最大的server索引int total = 0; //记录权重总和for(i = 0; i < size; i++){s[i].cur_weight += s[i].weight;total += s[i].weight;//记录权重最大的索引节点if(s[index].cur_weight < s[i].cur_weight){index = i;}}//把当前权重最大的减去权重总和,这时候当前权重会减小,所以下次分配就不一定还是该server了s[index].cur_weight -=total;//返回当前权重最大的server索引return index;   
}void getServerUrl(serverInfo *s, int *weight, int size)
{int i = 0;int index = 0;int total = getWeightSum(weight, size);for(i=0; i<total; i++){index = getServerUrlIndex(s, size);printf("get server is %s => %d\n", s[index].ip, s[index].weight);}}int main()
{int i = 0;int weight[] = {5,2,3};char *ips[] = {"192.168.1.1", "192.168.2.2", "192.168.3.3"};int size = sizeof(weight)/sizeof(int);serverInfo *s = initServer(ips, weight, size);for(i = 0; i < size; i++){printf("server init is %s => %d\n", s[i].ip, s[i].weight);}printf("getServerUrl:\n");getServerUrl(s, weight, size);return 0;}//得到结果如下:
server init is 192.168.1.1 => 5
server init is 192.168.2.2 => 2
server init is 192.168.3.3 => 3
getServerUrl:
get server is 192.168.1.1 => 5
get server is 192.168.3.3 => 3
get server is 192.168.2.2 => 2
get server is 192.168.1.1 => 5
get server is 192.168.1.1 => 5
get server is 192.168.3.3 => 3
get server is 192.168.1.1 => 5
get server is 192.168.2.2 => 2
get server is 192.168.3.3 => 3
get server is 192.168.1.1 => 5

优点:加权轮询能够使用权重解决不同承载能力的服务器节点。

缺点: 需要使用一定的机制解决某些情况下生成的序列是不均匀的问题。

4、源地址哈希(Hash)法

根据客户端的 ip 经过 hash 获取一个 hash 值,然后根据该 hash 值和服务器节点总数进行取模运算,取得对应的服务器节点。

char * getServerUrl()
{int i = hashcode(clientIP) % totalServer;return serverInfo[i];    
}

优点 : 当服务器列表不变时,只要请求源地址不变,总会请求到相同的目标服务器,这样就可以在客户端-服务器之间构建一个有状态 session。

缺点 : 当有大量的获取客户端时,会造成部分服务器过于繁忙,导致负载不均衡。另外,当某个服务器节点挂掉后,后续 hash 到该节点的所有请求都会得不到响应。

5、一致性哈希(Consistent Hash)法

传统的hash算法一般就是 key 和节点总数进行 mod 运算 mod(key, n),但这样会带来一个问题,就是当扩容或者缩容时,他们的映射关系就变成了mod (key, n+1) 或 mod (key, n-1),这会导致所有的 key 都会出现新的映射关系,因此很多 key 就会出现映射失败。

为了解决普通 hash 这种扩展能力和容错能力差的问题,就出现了一致性 hash(Consistent Hashing)的概念,采用一致性 hash 时,当出现扩容或者缩容时,影响的 key 只有少数部分,对全局影响不大。

一致性 hash 采用对每个服务节点进行计算 hash值,该hash值的范围在(02³²-1)中,我们把这些数字收尾相连构成一个范围在(02³²-1)的 hash 环中,然后把各个服务器节点经过 hash 运算得到对应的值,然后散列到 hash 环中,如下图,

在这里插入图片描述

当请求到来时,根据请求的唯一标志,比如请求的客户 IP 等经过 hash 运算得到一个 hash 值,然后根据该值从 hash 环上以顺时针的方向查找,得到一个离自己最近的服务器节点,该服务器节点也就是该请求对应的服务器。如下图,客户端请求经过 hash,从 hash 环中顺时针找到 node2 服务节点。
在这里插入图片描述

经过上述介绍我们可以发现一个问题,也就是当服务节点较少时,会出现数据倾斜问题,也就失去了平衡性,如下图,我们会发现很多都分配给了node2节点了。

在这里插入图片描述

为了防止这种数据倾斜,我们采用虚拟节点来保证平衡性。虚拟机节点其实就是服务器节点的复制,我们把一个服务器节点映射多个虚拟节点,当请求经过 hash运算后从hash 环上以顺时针的方向查找到虚拟机节点时,我们就把虚拟节点对应的实际服务节点作为服务器。虚拟节点如下图
在这里插入图片描述

比如客户端请求到来,根据客户端的 key1 经过 hash 计算映射到 hash 环中如下图,从该点顺时针找到了服务器节点 3 的虚拟节点,因此把服务器节点 3 作为请求对应的服务器。
在这里插入图片描述

当某个服务节点宕机(比如服务节点2),那么请求对象最终落到了服务节点 3,对于整个系统的影响也就是 key1 和 node2 之间的范围。如下图,
在这里插入图片描述

新增服务节点同理,当 key1 和 node3 之间新增了一个服务节点2,那么 key1 经过 hash 查找服务节点时会找到服务节点 2。


一致性hash的实现

本次分析一下开源项目 ketama 中的一致性 hash 算法的实现。

在 ketama 中为了平衡性也采用了虚拟机节点,同时为了不同的服务节点的承载 能力也使用了权重,因此权重高的,相应的虚拟机节点也就相应的多,服务器节点也就处理的请求多。

ketama 中使用如下结构记录服务节点信息。
typedef struct
{unsigned int point;  //圆环上的点char ip[22]; //实际服务节点ip
} mcs;// 服务器信息,主要记录服务器的ip地址和权重值
typedef struct
{char addr[22]; //服务器ip地址unsigned long memory; // 权重值
} serverinfo;// 以下数据结构就是continuum环上的结点,环上的每个点其实代表了一个ip地址,该结构把点和ip地址一一对应起来。
typedef struct
{int numpoints; //在环上的点的总数同时也是 array数组元素的总个数void* modtime; // 更改时间void* array; // mcs数组 该数组时以mcs中point从小到大排过序的,为了方便从环上找响应的服务节点
} continuum;

一致性hash环的创建

/*
一致性hash环的创建
该函数是创建continuum的核心函数,它先从配置文件中读取集群服务器ip和端口,以及权重信息。
创建continuum环,并把这些服务器信息和环上的数组下标对应起来。
*/
static int
ketama_create_continuum( key_t key, char* filename )
{...unsigned int numservers = 0; // 该变量来记录共从配置文件中共读取了多少个服务器unsigned long memory; // 该变量是配置文件中所有服务器权重值的总和serverinfo* slist; // 从配置文件中读取到的服务器信息,包括ip地址,端口,权重值// 从配置文件filename中读取服务器信息,把服务器总数保存到变量numservers中,把所有服务器的权重值保存到memory中slist = read_server_definitions( filename, &numservers, &memory );/*以下代码开始构建continuum环平均每台服务器要在这个环上布160个点,这个数组的元素个数就是服务器个数*160。具体多少个点,需要根据事情的服务器权重值进行计算得到。为什么要选择160个点呢?主要是通过md5计算出来的是16个整数,把这个整数分成4等分,每份是4位整数。而每进行一次hash计算,我们可以获得4个点。*/mcs continuum[ numservers * 160 ];unsigned int i, k, cont = 0;for( i = 0; i < numservers; i++ ) // 遍历所有服务器开始在环上部点{float pct = (float)slist[i].memory / (float)memory; // 计算服务器i在所有服务器权重的占比/* 由于计算一次可以得到4个点,所有对每一台机器来说,总的计算只需要计算40*numservers次。按权重占比进行划分,就是以下的计算得到的次数*/unsigned int ks = floorf( pct * 40.0 * (float)numservers );//ks是每个ip地址对应的总点数for( k = 0; k < ks; k++ ) // 计算出总次数,每次可以得到4个点{/* 40 hashes, 4 numbers per hash = 160 points per server */char ss[30];unsigned char digest[16];// 通过计算hash值来得到下标值,该hash值是字符串:"-n",其中的n是通过权重计算出来的该主机应该部点的总数/4。sprintf( ss, "%s-%d", slist[i].addr, k );ketama_md5_digest( ss, digest ); // 计算其字符串的md5值,该值计算出来后是一个unsigned char [16]的数组,也就是可以保存16个字节int h;for( h = 0; h < 4; h++ )// 共有16个字节,可以处理4次,得到4个点的值{/*把计算出来的连续4位的数字,进行移位。把第一个数字移到一个整数的最高8位,后面的一次移动次高8位,后面一次补零,这样就得到了一个32位的整数值。*/continuum[cont].point = ( digest[3+h*4] << 24 )| ( digest[2+h*4] << 16 )| ( digest[1+h*4] <<  8 )|   digest[h*4];// 复制对应的ip地址到该点上memcpy( continuum[cont].ip, slist[i].addr, 22 );cont++;}}}free( slist );/*Sorts in ascending order of "point" 以下代码对计算出来的环上点的值进行排序,方便进行查找这里要注意:排序是按照point的值(计算出来的整数值)进行的,也就是说原来的数组下标顺序被打乱了。*//* Sorts in ascending order of "point" */qsort( (void*) &continuum, cont, sizeof( mcs ), (compfn)ketama_compare );/*到这里算法的实现就结束了,环上的点(0^32整数范围内)都已经建立起来,每个点都是0到2^32的一个整数和ip地址的结构。这样查找的时候,只是需要hash(key),并在环上找到对应的数的位置,取得该节点的ip地址即可。*/...return 1;
}

hash环的构建也很简单:

1、从配置文件中读取服务器信息列表,获取总的权重值。

2、创建numservers * 160 个point点也就是虚拟机节点,然后根据权重进行hash运算分配这些point点,权重高的分配的越多。同时对这些point点进行从小到大排序,为何后续查找方便。


在环上查找元素

//计算key的hash值的实现方法
unsigned int ketama_hashi( char* inString ) 
{unsigned char digest[16];// 对key的值做md5计算,得到一个有16个元素的unsigned char数组ketama_md5_digest( inString, digest ); //取数组中的前4个字符,并移位,得到一个unsigned int的hash值return (unsigned int)(( digest[3] << 24 )| ( digest[2] << 16 )| ( digest[1] <<  8 )|   digest[0] );
}//在环上查找相应的结点
mcs* ketama_get_server( char* key, ketama_continuum cont )
{unsigned int h = ketama_hashi( key ); // 计算key的hash值,并保存到变量h中int highp = cont->numpoints; // 该变量cont->numpoints是总的数组埋点数mcs (*mcsarr)[cont->numpoints] = cont->array; // 数组结点的值int lowp = 0, midp;unsigned int midval, midval1;// divide and conquer array search to find server with next biggest// point after what this key hashes towhile ( 1 ){// 从数组的中间位置开始找// 注意此时的数组是按照point的值排好序了midp = (int)( ( lowp+highp ) / 2 );// 若中间位置等于最大点数,直接绕回到0位置if ( midp == cont->numpoints )return &( (*mcsarr)[0] ); // if at the end, roll back to zeroth// 取的中间位置的point值midval = (*mcsarr)[midp].point;/* 获取 midp 上限和下限, 若midp 为0 则midval1 为0 ,否则取midp-1的point,也即是获取 h 在如下范围内 (*mcsarr)[midp-1].point <  h <  (*mcsarr)[midp]*/midval1 = midp == 0 ? 0 : (*mcsarr)[midp-1].point;// 把h的值和取的两个值point值进行比较,若在这两个point值之间说明h值应该放在较大的那个point值的下标对应的ip地址上if ( h <= midval && h > midval1 )return &( (*mcsarr)[midp] );if ( midval < h ) // 否则继续二分查找lowp = midp + 1;elsehighp = midp - 1;if ( lowp > highp ) // 若没有找到,直接返回0位置的值,这种情况应该很少return &( (*mcsarr)[0] );}
}

查找服务器节点如下:

1、根据请求的key进行hash得到一个整数。

2、根据该整数从环上查找到一个比该整数大最近虚拟机节点,获取到的服务器节点就是要找的服务器。


以上就是hash一致性算法的实现。

优点:具有较好的扩展性和容错性,同时保证了数据的平衡性。

缺点:当某个客户端发送很多请求,后端hash到同一个服务器节点,会导致服务器负载过大,也即是没有解决热点问题。

总结

  • 负载均衡就是通过一定方式把客户端的请求分配到相应的服务节点上,保证各个服务器节点能够在自己的承载范围内提供服务。

  • 负载均衡分为硬件负载均衡和软件负载均衡,一般接触到最多的是软件负载均衡,软件负载均衡有不同的负载均衡算法,各个负载均衡算法有各自的优缺点,可以根据适当的场景采用对应的算法。




负载均衡原理及实现

作者:Linux码农,来源:公众号「Linux码农



喜欢本文的朋友,欢迎关注公众号 Linux码农,获取更多干货
在这里插入图片描述


http://chatgpt.dhexx.cn/article/J9QV6UEg.shtml

相关文章

Android 获取手机已安装的应用列表(适配)

权限 <uses-permissionandroid:name"android.permission.QUERY_ALL_PACKAGES"tools:ignore"QueryAllPackagesPermission" /> 加上权限才能获取到所有的应用 获取代码 public static boolean hasApplication(Context context, String packageName) …

如何在没有微软商店的情况下在Windows 10上安装应用程序

如何在没有微软商店的情况下在Windows 10上安装应用程序 通过微软商店&#xff0c;你可以轻松地在Windows 10设备上安装应用程序&#xff0c;就像使用Google Play或AppleStore一样。IT部门经常从设备上删除微软商店&#xff0c;或设置组策略禁用Windows更新&#xff0c;以防止…

“打开文件所在位置“失败,提示“该文件没有与之关联的应用来执行操作。请安装应用,若已经安装应用...““

问题点:装了某个软件,卸载后出现"打开文件所在位置"失败,出现以下提示: 该文件没有与之关联的应用来执行操作。请安装应用&#xff0c;若已经安装应用&#xff0c;请在“默认应用设置"页面中创建关联 解决办法: 打开注册表,找到HKEY_CLASSES_ROOT\Folder\shell,…

Win 11 打开未知文件/打开方式 该文件没有与之关联的应用来执行该操作。请安装应用,若已经安装应用,请在“默认应用设置”页面中创建关联。

问题 鼠标右键选中文件 - 打开方式&#xff0c;或者选择其他应用 弹窗提示&#xff1a; 该文件没有与之关联的应用来执行该操作。请安装应用&#xff0c;若已经安装应用&#xff0c;请在“默认应用设置”页面中创建关联。 打开注册表&#xff0c;查看是否存在以下路径 HKEY_C…

该文件没有与之关联的应用来执行该操作。请安装应用,若已经安装应用,请在“默认应用设置“页面中创建关联

在任意文件上鼠标右键&#xff0c;选择打开方式或者选择其他应用时报错“该文件没有与之关联的应用来执行该操作。请安装应用&#xff0c;若已经安装应用&#xff0c;请在"默认应用设置"页面中创建关联”&#xff0c;错误现象如下&#xff1a; 打开注册表&#xf…

安装软件程序

包管理基础 在深入了解Linux软件包管理之前&#xff0c;本章将先介绍一些基础知识。各种主流Linux发行版都采用了某种形式的包管理系统来控制软件和库的安装。 PMS利用一个数据库来记录各种相关内容&#xff1a;  Linux系统上已安装了什么软件包&#xff1b;  每个包安装了…

android 获取已安装应用(App)列表

效果图 获取手机上已安装应用列表&#xff0c;将获取到的信息用集合返回&#xff0c;可以自己定义是否过滤系统应用&#xff0c;Appinfo是一个实体类&#xff0c;包含应用的名称 包名 图标icon等等 /*** 获取手机已安装应用列表* param ctx* param isFilterSystem 是否过滤系统…

应用程序安装流程

https://maoao530.github.io/2017/01/18/package-install/ 本文介绍APK的安装流程。 一、安装流程图 APK安装流程&#xff0c;总体可以下图流程&#xff0c;用ProcessOn画的&#xff0c;凑合看&#xff1a; 从上图我们可以看到apk安装到最后都会调用到这个flow&#xff1a; PM…

app 安装方式

APK的安装方式主要有以下几种&#xff1a; 通过adb命令安装&#xff1a;adb 命令包括adb push/install用户下载的Apk&#xff0c;通过系统安装器PackageInstaller安装该Apk。PackageInstaller是系统内置的应用程序&#xff0c;用于安装和卸载应用程序。系统开机时安装系统应用…

安卓手机怎样安装apk应用

以华为手机为例 首先手机要关闭纯净模式&#xff0c;方法&#xff1a; 设置–系统和更新–纯净模式–关闭 然后下载/接收apk文件 &#xff08;如果是从浏览器下载的&#xff0c;可以直接安装&#xff0c;如果是从其他渠道下载&#xff0c;则安装方法如下&#xff09; 打开手机…

大体了解Android应用安装过程及原理

Android应用安装的流程及路径&#xff1a; 应用安装涉及到如下几个目录&#xff1a; system/app 系统自带的应用程序&#xff0c;无法删除 data/app 用户程序安装的目录&#xff0c;有删除权限。安装时把apk文件复制到此目录 data/data …

[笔记] 应用安装过程梳理

一、概述 1、四种应用安装场景 (1)系统自安装:每次开机启动时,都会预安装系统自带的APK文件,没有安装界面; (2)应用市场安装:从小米应用商店(com.xiaomi.market)、Google Play(com.android.vending)等,下载 APK 后,自动安装; (3)adb命令安装:使用 adb 工具…

如何在win10+VS2017环境下新建一个简单的WDF示例程序

上一课我们在win10系统VS2017开发环境下搭建了WDK驱动程序开发环境的搭建&#xff0c;详见我的博客【如何在win10VS2017环境下安装USB驱动开发套件WDK】&#xff0c;今天我们来尝试建立一个最简单的KDM示例工程。 提前说明&#xff1a;本博客所建立的示例工程的源码&#xff0…

WDF驱动安装方法

WDF驱动的安装方法: 编译好的SYS文件、INF文件、另外还需要一个WdfCoInstaller01009.dll文件&#xff0c;对应的WDK版本是7600.16385.1这个版本&#xff0c;其中WdfCoInstaller01009.dll文件位于“WDK安装目录” \redist\wdf\x86下,这个动态链接库文件是WDF驱动框架在安装时所必…

WRF模式

随着生态文明建设和“碳中和”战略的持续推进&#xff0c;我国及全球气候变化及应对是政府、科学界及商业界关注的焦点。气候是多个领域&#xff08;生态、水资源、风资源及碳中和等问题&#xff09;的主要驱动因素&#xff0c;合理认知气候变化有利于解释生态环境变化机理及过…

WDF VIOLATION 你的电脑遇到问题,需要重新启动

文章目录 问题描述问题原因解决方法 【写在前面】作为一名还未毕业的研究僧&#xff0c;天天泡实验室是常有的事。学校的电脑是惠普的。我在上面装了Win10的系统。不久之前&#xff0c;电脑重启后竟然蓝屏了&#xff0c;无法进入系统&#xff0c;蓝屏上显示的是WDF VIOLATION 你…

WDS服务

二&#xff1a;WDS服务 预启动执行环境&#xff08;Preboot eXecution Environment&#xff0c;PXE&#xff09;也被称为预执行环境&#xff0c;提供了一种使用网络接口&#xff08;Network Interface&#xff09;启动计算机的机制。这种机制让计算机的启动可以不依赖本地数据存…

基于WDF驱动模型使用WFP框架进行网络层数据修改

一、WDF驱动模型介绍&#xff1a; WDF驱动模型是微软推出的驱动程序开发环境&#xff0c;是Vista及其以后OS的驱动模型。在visia之前win2000之后用的是WDM驱动模型。WDF是以WDM为基础进行了建模和封装&#xff0c;降低了开发难度。WDF将驱动程序与操作系统内核之间进行了分离&a…

从零开始学习Windows WDF驱动程序开发

从零开始学习Windows WDF驱动程序开发 ▼ 目录&#xff1a; 1. 第一部分 编译安装测试一个简单的WDF驱动程序 1.1 编译安装测试 1.2 Windows驱动开发就是要开发出INF文件和SYS文件 1.3 简单介绍下编译脚本 1.4 Windows应用程序如何访问一个设备 2. 第二部分 了解WDF框架并且开发…

亲手解剖WDF驱动

亲手解剖WDF驱动 创建一个WDF驱动(non-pnp) WDF驱动其实是微软公司提供的一套驱动开发的框架。 有了这个框架之后&#xff0c;开发驱动会简单一些。WDF本身是从WDM基础上封装而成的。WDF里面封装了很多对象&#xff0c;如WDFDRIVER等。如果要学习使用WDF来开发驱动&#xff0c;…