目录

一、负载均衡

二、负载均衡算法

1、静态负载均衡

2、动态负载均衡

三、Dubbo负载均衡的四种算法

1、基于权重随机算法RandomLoadBalance

2、基于最少活跃数算法LeastActiveLoadBalance

3、基于一致性hash算法ConsistentHashLoadBalance

4、基于加权轮询算法RoundRobinLoadBalance


一、负载均衡

负载均衡简单的说就是对流量进行重新分配,避免单一机器直接被较为集中的流量击穿,或者避免部分机器由于没有合理分配流量导致空闲。

负载均衡分位硬件负载均衡和软件负载均衡,这里主要对软件负载均衡进行一个记录。

软件方面的产品比较流行的有LVS、Nginx、HaProxy等。

二、负载均衡算法

负载均衡算法分位静态负载均衡和动态负载均衡。

1、静态负载均衡

①轮询法

轮询是将客户端的请求轮流分配到每一个节点上,当有5台机器的时候,来5个请求,每台机器都将得到一次请求,并不会关心服务器实际的连接数和当前的系统负载。

②随机法

随机法顾名思义就是客户端请求会随机选取一台服务器进行访问,当客户端调用服务器的次数增多的时候,每台机器获得的请求数是差不多的,也就是平均分配了。

③源地址哈希法

源地址哈希是获取客户端的ip地址,通过哈希算法得出一个值,通过该值对后端服务器列表大小进行取模,返回的结果就是客户端要访问的服务器的序号。

通过上述介绍,可以知道,同一个ip地址的请求,都会打在后端同一台机器上进行访问,这个也可以用于进行本地session、本地缓存的场景。

但是该方法无法保证高可用,如果后端一个节点出现故障,会导致改节点上的客户端无法使用;并且如果某个用户是热点用户,巨大的流量都会打在同一台机器上,导致流量分布不均衡。

④加权轮询法

不同的后端服务器配置可能不同,因此能承受的流量也不同,使用加权法,可以让配置高的机器得到更大的权重,处理更多的请求。降低配置低的机器的请求。

2、动态负载均衡

动态负载均衡需要根据之前的结果进行运算后才确定的算法,需要保存之前的结果和将结果进行计算,理论上动态算法比静态算法在调度过程中更消耗资源。

①最小连接数

根据后端服务器当前的连接情况,动态的选择当前连接数最少的一个节点处理当前请求,这个可以提高整个集群的运转效率,但是也提高了复杂度,在每次调用的连接和断开时都需要进行计数。

②最快响应速度法

根据请求的响应时间,动态调整每个节点的权重,将响应速度更快的机器分配更多的权重,响应速度慢的分配更少的权重。同时提高了复杂度,每次调用都要计算请求的响应时间。

三、Dubbo负载均衡的四种算法

微服务的Dubbo中,是怎么实现的呢?我们通过源码来看一下:

Dubbo中实现负载均衡有个基类:AbstractLoadBalance:

public abstract class AbstractLoadBalance implements LoadBalance {public AbstractLoadBalance() {}public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {if (invokers != null && !invokers.isEmpty()) {// 如果 invokers 列表中仅有一个 Invoker,直接返回即可,无需进行负载均衡// 否则调用 doSelect 方法进行负载均衡,该方法为抽象方法,由子类实现return invokers.size() == 1 ? (Invoker)invokers.get(0) : this.doSelect(invokers, url, invocation);} else {return null;}}protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> var1, URL var2, Invocation var3);// 服务提供者权重计算逻辑protected int getWeight(Invoker<?> invoker, Invocation invocation) {// 获取 weight 配置值int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100);if (weight > 0) {// 获取服务提供者启动时间戳long timestamp = invoker.getUrl().getParameter("remote.timestamp", 0L);if (timestamp > 0L) {    // 计算服务提供者运行时长int uptime = (int)(System.currentTimeMillis() - timestamp);// 获取服务预热时间,默认为10分钟int warmup = invoker.getUrl().getParameter("warmup", 600000);// 如果服务运行时间小于预热时间,则重新计算服务权重,即降权if (uptime > 0 && uptime < warmup) {// 重新计算服务权重weight = calculateWarmupWeight(uptime, warmup, weight);}}}return weight;}static int calculateWarmupWeight(int uptime, int warmup, int weight) {// 计算权重,下面代码逻辑上形似于 (uptime / warmup) * weight。// 随着服务运行时间 uptime 增大,权重计算值 ww 会慢慢接近配置值 weightint ww = (int)((float)uptime / ((float)warmup / (float)weight));return ww < 1 ? 1 : (ww > weight ? weight : ww);}
}

可以看到下面有四种实现算法:

1、ConsistentHashLoadBalance:基于一致性hash算法

2、LeastActiveLoadBalance:基于最少活跃数算法

3、RandomLoadBalance:基于权重随机算法

4、RoundRobinLoadBalance:基于加权轮询算法

1、基于权重随机算法RandomLoadBalance

这个算法是Dubbo的默认负载均衡算法,基于权重进行分配,如果没有进行权重分配,那么每个分配到比列是一样的,基本可以认为是轮询算法。 RandomLoadBalance算法在经过多次请求后,能够将调用请求按照权重值进行均匀分配。

一起看一下代码:

public class RandomLoadBalance extends AbstractLoadBalance {public static final String NAME = "random";private final Random random = new Random();public RandomLoadBalance() {}protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {// 后端服务器节点是invokers,获取节点大小int length = invokers.size();// 累加权重int totalWeight = 0;// 权重标识boolean sameWeight = true;int offset;int i;// 计算总权重 totalWeight// 检测每个服务提供者的权重是否相同,若不相同,则将 sameWeight 置为 falsefor(offset = 0; offset < length; ++offset) {i = this.getWeight((Invoker)invokers.get(offset), invocation);totalWeight += i;// 检测当前服务提供者的权重与上一个服务提供者的权重是否相同,// 不相同的话,则将 sameWeight 置为 false。if (sameWeight && offset > 0 && i != this.getWeight((Invoker)invokers.get(offset - 1), invocation)) {sameWeight = false;}}// 进行随机数落在哪一个权重区间的判断if (totalWeight > 0 && !sameWeight) {// 基于总的权重数计算出随机数offset = this.random.nextInt(totalWeight);// 编辑后端服务节点,看随机数是否落在节点之内,如果是被选中// 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。for(i = 0; i < length; ++i) {// 让随机值 offset 减去权重值offset -= this.getWeight((Invoker)invokers.get(i), invocation);if (offset < 0) {// 返回相应的 Invokerreturn (Invoker)invokers.get(i);}}}// 如果权重相等,就按照直接的随机算法return (Invoker)invokers.get(this.random.nextInt(length));}
}

2、基于最少活跃数算法LeastActiveLoadBalance

这个算法会判断机器目前的连接数,连接数少的就会接收更多的请求进行处理。但默认是认为机器越好,处理速度越快。则连接数越少,但如果实际机器性能不好,这个算法就比较不那么好了。

public class LeastActiveLoadBalance extends AbstractLoadBalance {public static final String NAME = "leastactive";private final Random random = new Random();public LeastActiveLoadBalance() {}protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {int length = invokers.size();// 最小的活跃数int leastActive = -1;// 具有相同,最小的活跃数int leastCount = 0;// leastIndexs 用于记录具有相同“最小活跃数”的 Invoker 在 invokers 列表中的下标信息int[] leastIndexs = new int[length];int totalWeight = 0;// 第一个最小活跃数的 Invoker 权重值,用于与其他具有相同最小活跃数的 Invoker 的权重进行对比,// 以检测是否所有具有相同最小活跃数的 Invoker 的权重均相int firstWeight = 0;boolean sameWeight = true;int offsetWeight;int leastIndex;   // 遍历每一个invoker,找出最小的连接数for(offsetWeight = 0; offsetWeight < length; ++offsetWeight) {Invoker<T> invoker = (Invoker)invokers.get(offsetWeight);// 获取活跃数leastIndex = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();// 获取权重int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100);// 判断该活跃数是不是比已经取到的最小活跃数还要小if (leastActive != -1 && leastIndex >= leastActive) {// 如果目前的活跃数与最小活跃数相等,则进行权重的判断if (leastIndex == leastActive) {// 当前 Invoker 的活跃数 active 与最小活跃数 leastActive 相同leastIndexs[leastCount++] = offsetWeight;// 累加权重totalWeight += weight;// 检测当前 Invoker 的权重与 firstWeight 是否相等,// 不相等则将 sameWeight 置为 falseif (sameWeight && offsetWeight > 0 && weight != firstWeight) {sameWeight = false;}}} else {// 使用当前活跃数 active 更新最小活跃数 leastActiveleastActive = leastIndex;// 更新 leastCount 为 1leastCount = 1;// 记录当前下标值到 leastIndexs 中leastIndexs[0] = offsetWeight;totalWeight = weight;firstWeight = weight;sameWeight = true;}}// 如果最小活跃数只有一个,则返回if (leastCount == 1) {return (Invoker)invokers.get(leastIndexs[0]);} else {// 如果最小活跃数有多个,进行权重的判断,算法与RoandomLoadBalance一致if (!sameWeight && totalWeight > 0) {// 随机获取一个 [0, totalWeight) 之间的数字offsetWeight = this.random.nextInt(totalWeight);// 循环让随机数减去具有最小活跃数的 Invoker 的权重值,// 当 offset 小于等于0时,返回相应的 Invokerfor(int i = 0; i < leastCount; ++i) {leastIndex = leastIndexs[i];offsetWeight -= this.getWeight((Invoker)invokers.get(leastIndex), invocation);if (offsetWeight <= 0) {return (Invoker)invokers.get(leastIndex);}}}// 如果权重也是相等的,随机返回一个return (Invoker)invokers.get(leastIndexs[this.random.nextInt(leastCount)]);}}
}

这里简单说一下LeastActiveLoadBalance的算法逻辑:

①遍历invokers列表,寻找活跃数最小的Invoker,如果有多个相同的最小活跃数,则记录下来

②在记录下来的数据中,比较权重值是否相等

③如果只有一个Invoker具有最小活跃数,则直接返回即可

④如果有多个Invoker具有最小活跃数,并且权重不一致,则使用RoandomLoadBalance算法处理

⑤如果有多个Invoker具有最小活跃数,但权重相等,则随机返回一个即可

3、基于一致性hash算法ConsistentHashLoadBalance

关于一致性hash算法的一些介绍,可以参考:

五分钟看懂一致性哈希算法 - 掘金

简单的说就是定义一组hash是在0-2^32-1之间,然后每个服务器计算出一个hash值,定位在0-2^32-1上面,然后每个请求对象也会计算出一个hash值,然后找到第一个比对象hash值大的服务器的hash值,然后该对象就缓存在该服务器上。如果找不到比该对象大的服务器的hash值,则缓存到第一个服务器上,即hash值最小的服务器上。

如果服务器较少,可以引入虚拟节点,比如两个服务器A、B,则引入虚拟节点A1、A2、A3、B1、B2、B3,此时就有6个hash值,然后A1、A2、A3对应A服务器,B1、B2、B3对应B服务器,然后再按照上面的方式找到对应的缓存服务器即可。

看一下源码:

public class ConsistentHashLoadBalance extends AbstractLoadBalance {private final ConcurrentMap<String, ConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors = new ConcurrentHashMap();public ConsistentHashLoadBalance() {}protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName();// 获取 invokers 原始的 hashcodeint identityHashCode = System.identityHashCode(invokers);ConsistentHashLoadBalance.ConsistentHashSelector<T> selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key);// 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,可能新增也可能减少了。// 此时 selector.identityHashCode != identityHashCode 条件成立if (selector == null || selector.identityHashCode != identityHashCode) {// 创建新的 ConsistentHashSelectorthis.selectors.put(key, new ConsistentHashLoadBalance.ConsistentHashSelector(invokers, invocation.getMethodName(), identityHashCode));selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key);}// 调用 ConsistentHashSelector 的 select 方法选择 Invokerreturn selector.select(invocation);}private static final class ConsistentHashSelector<T> {private final TreeMap<Long, Invoker<T>> virtualInvokers = new TreeMap();private final int replicaNumber;private final int identityHashCode;private final int[] argumentIndex;ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {this.identityHashCode = identityHashCode;URL url = ((Invoker)invokers.get(0)).getUrl();// 获取虚拟节点数,默认为160this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);// 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));this.argumentIndex = new int[index.length];for(int i = 0; i < index.length; ++i) {this.argumentIndex[i] = Integer.parseInt(index[i]);}Iterator i$ = invokers.iterator();while(i$.hasNext()) {Invoker<T> invoker = (Invoker)i$.next();String address = invoker.getUrl().getAddress();for(int i = 0; i < this.replicaNumber / 4; ++i) {// 对 address + i 进行 md5 运算,得到一个长度为16的字节数组byte[] digest = this.md5(address + i);// 对 digest 部分字节进行4次 hash 运算,得到四个不同的 long 型正整数for(int h = 0; h < 4; ++h) {// h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算// h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算// h = 2,取 digest 中下标为 8 ~ 11 的4个字节进行位运算 // h = 3 时过程同上long m = this.hash(digest, h);// 将 hash 到 invoker 的映射关系存储到 virtualInvokers 中,// virtualInvokers 中的元素要有序,因此选用 TreeMap 作为存储结构this.virtualInvokers.put(m, invoker);}}}}public Invoker<T> select(Invocation invocation) {// 将参数转为 keyString key = this.toKey(invocation.getArguments());// 对参数 key 进行 md5 运算byte[] digest = this.md5(key);// 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法,// 寻找合适的 Invokerreturn this.selectForKey(this.hash(digest, 0));}private String toKey(Object[] args) {StringBuilder buf = new StringBuilder();int[] arr$ = this.argumentIndex;int len$ = arr$.length;for(int i$ = 0; i$ < len$; ++i$) {int i = arr$[i$];if (i >= 0 && i < args.length) {buf.append(args[i]);}}return buf.toString();}private Invoker<T> selectForKey(long hash) {// 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 InvokerEntry<Long, Invoker<T>> entry = this.virtualInvokers.tailMap(hash, true).firstEntry();// 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,// 需要将 TreeMap 的头结点赋值给 entryif (entry == null) {entry = this.virtualInvokers.firstEntry();}// 返回 Invokerreturn (Invoker)entry.getValue();}private long hash(byte[] digest, int number) {return ((long)(digest[3 + number * 4] & 255) << 24 | (long)(digest[2 + number * 4] & 255) << 16 | (long)(digest[1 + number * 4] & 255) << 8 | (long)(digest[number * 4] & 255)) & 4294967295L;}private byte[] md5(String value) {MessageDigest md5;try {md5 = MessageDigest.getInstance("MD5");} catch (NoSuchAlgorithmException var6) {throw new IllegalStateException(var6.getMessage(), var6);}md5.reset();byte[] bytes;try {bytes = value.getBytes("UTF-8");} catch (UnsupportedEncodingException var5) {throw new IllegalStateException(var5.getMessage(), var5);}md5.update(bytes);return md5.digest();}}
}

4、基于加权轮询算法RoundRobinLoadBalance

对于轮询而言,假设我们有三台服务器A、B、C,我们将收到的第一个请求给A,第二个给B、第三个给C、第四个给A、第五个给B......这种方式就是轮询,这里是默认每台机器的性能相近,但实际上并不会这样,因此有了加权,也就是调控每台服务器被调用的比列。比如给A、B、C分别设置权重5:3:2,则10次请求中5次分配给A,3次分配给B,2次分配给C。

源码如下:

public class RoundRobinLoadBalance extends AbstractLoadBalance {public static final String NAME = "roundrobin";private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap();public RoundRobinLoadBalance() {}protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {// key = 全限定类名 + "." + 方法名String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName();int length = invokers.size();// 最大权重int maxWeight = 0;// 最小权重int minWeight = 2147483647;LinkedHashMap<Invoker<T>, RoundRobinLoadBalance.IntegerWrapper> invokerToWeightMap = new LinkedHashMap();// 权重总和int weightSum = 0;int currentSequence;// 查找最大和最小权重,计算权重总和等for(int i = 0; i < length; ++i) {currentSequence = this.getWeight((Invoker)invokers.get(i), invocation);// 获取最大和最小权重maxWeight = Math.max(maxWeight, currentSequence);minWeight = Math.min(minWeight, currentSequence);if (currentSequence > 0) {// 将 currentSequence封装到 IntegerWrapper 中invokerToWeightMap.put(invokers.get(i), new RoundRobinLoadBalance.IntegerWrapper(currentSequence));// 累加权重weightSum += currentSequence;}}// 查找AtomicPositiveInteger对应的实例,如果为null就创建一个实例AtomicPositiveInteger sequence = (AtomicPositiveInteger)this.sequences.get(key);if (sequence == null) {this.sequences.putIfAbsent(key, new AtomicPositiveInteger());sequence = (AtomicPositiveInteger)this.sequences.get(key);}// 获取当前的调用编号currentSequence = sequence.getAndIncrement();// 如果 最小权重 < 最大权重,表明服务提供者之间的权重是不相等的if (maxWeight > 0 && minWeight < maxWeight) {// 使用调用编号对权重总和进行取余操作int mod = currentSequence % weightSum;// 进行 maxWeight 次遍历for(int i = 0; i < maxWeight; ++i) {// 对invokerToWeightMap进行迭代遍历Iterator i$ = invokerToWeightMap.entrySet().iterator();while(i$.hasNext()) {Entry<Invoker<T>, RoundRobinLoadBalance.IntegerWrapper> each = (Entry)i$.next();// 获取 InvokerInvoker<T> k = (Invoker)each.getKey();// 获取权重包装类 IntegerWrapperRoundRobinLoadBalance.IntegerWrapper v = (RoundRobinLoadBalance.IntegerWrapper)each.getValue();// 如果 mod = 0,且权重大于0,此时返回相应的 Invokerif (mod == 0 && v.getValue() > 0) {return k;}// mod != 0,且权重大于0,此时对权重和 mod 分别进行自减操作if (v.getValue() > 0) {v.decrement();--mod;}}}}// 返回Invokerreturn (Invoker)invokers.get(currentSequence % length);}private static final class IntegerWrapper {private int value;public IntegerWrapper(int value) {this.value = value;}public int getValue() {return this.value;}public void setValue(int value) {this.value = value;}public void decrement() {--this.value;}}
}

微服务的几种负载均衡算法相关推荐

  1. Java实现5种负载均衡算法

    Java实现5种负载均衡算法 1. 轮询算法 import com.google.common.collect.Lists;import java.util.List; import java.uti ...

  2. 【云原生微服务八】Ribbon负载均衡策略之WeightedResponseTimeRule源码剖析(响应时间加权)

    文章目录 一.前言 二.WeightedResponseTimeRule 1.计算权重? 1)如何更新权重? 2)如何计算权重? 3)例证权重的计算 2.权重的使用 1)权重区间问题? 一.前言 前置 ...

  3. Dubbo内置4种负载均衡算法(详解)

    1.1 什么是负载均衡 在实际开发中,一个服务基本都是集群模式的,也就是多个功能相同的项目在运行,这样才能承受更高的并发,这时一个请求到这个服务,就需要确定访问哪一个服务器 Dubbo框架内部支持负载 ...

  4. 五分钟让你搞懂Nginx负载均衡原理及四种负载均衡算法

    前言 今天这篇文章介绍了负载均衡的原理以及对应的四种负载均衡算法,当然还有对应的指令及实战,欢迎品尝.有不同意见的朋友可以评论区留言! 负载均衡 所谓负载均衡,就是 Nginx 把请求均匀的分摊给上游 ...

  5. 微服务SpringCloud中的负载均衡,你都会么?

    1.什么是负载均衡 首先我们来看看维基百科对负载均衡的说明: 负载平衡(Load balancing)是一种计算机技术,用来在多个计算机(计算机集群).网络连接.CPU.磁盘驱动器或其他资源中分配负载 ...

  6. ribbon的7种负载均衡算法和替换方法

    一,ribbon核心组件IRule自带的7中负载均衡算法 1,轮询 com.netflix.loadbalancer.RoundRobinRule 2,随机 com.netflix.loadbalan ...

  7. SpringCloud Hoxton版微服务-RestTempalte + @LoadBlanced 实现负载均衡

    RestTempalte + @LoadBlanced 实现负载均衡 一.服务提供者注册 二.服务消费者调用 1.编写服务消费者 2.RestTemplate中开启负载均衡支持 3.启动服务测试 总结 ...

  8. 微服务网关-Gateway-LoadBalancerClient实现负载均衡讲解

    LoadBalancerClient 路由过滤器(客户端负载均衡) 上面的路由配置每次都会将请求给指定的URL处理,但如果在以后生产环境,并发量较大的时候,我们需要根据服务的名称判断来做负载均衡操作, ...

  9. 常见的几种负载均衡算法

    1.轮询 将所有请求,依次分发到每台服务器上,适合服务器硬件相同的场景. 优点:服务器请求数目相同: 缺点:服务器压力不一样,不适合服务器配置不同的情况: 2.随机 请求随机分配到各台服务器上. 优点 ...

最新文章

  1. Linux集群系统Heartbeat
  2. 设计模式复习-建造者模式
  3. Shell 扩展的分类
  4. tensorflow2.0五种机器学习算法对中文文本分类
  5. 黑盒测试的常见测试方法
  6. 使用Wireshark抓包分析TCP协议
  7. 日常记账后,图表查看账目类别
  8. 微信小程序生成推广二维码
  9. Qt+OpenCV在arm板上运行实现思路
  10. matlab的shading,matlab colormap,caxis,shading,hsv,pcolor, alpha
  11. 凹凸贴图(Bump Map)实现原理以及与法线贴图(Normal Map)的区别
  12. Win10开机取消微软登录密码
  13. 【安全算法之概述】一文带你简要了解常见常用的安全算法(RT-Thread技术论坛优秀文章)
  14. sparksql查询_筛选_过滤
  15. java常见的命名规则
  16. Rebus渲染农场分析
  17. C语言中出现UB现象 undefined behaviour.
  18. 基于java的华容道小游戏
  19. gms认证流程_GMS认证流程及周期 智能手机和平板GMS授权
  20. 利用浏览器的油猴插件下载网页视频

热门文章

  1. mac 查看 keystore文件MD5
  2. 基于单片机的直流电机转速控制设计(电路+程序)
  3. 彻底了解toString和valueOf区别
  4. 高达光之翼月光蝶效果制作
  5. ironpython安装_安装 IronPython
  6. 电商API接口-电商OMS不可或缺的一块 调用代码展示
  7. Metro UI CSS 学习笔记之 基础组件
  8. Path环境变量的配置
  9. 朗新科技林海潮:企业应用的云上架构演变
  10. (Linux)make编译用法简述