# RPC-Project **Repository Path**: LuWangLin/RPC-Project ## Basic Information - **Project Name**: RPC-Project - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-07-03 - **Last Updated**: 2021-07-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 动态代理部分 > 使用jdk动态代理 > 使用借口调用服务提供方的服务时,会使用以下注解方式 ```java @RpcReference(group = "lwl", version = "1.0") HelloService helloService; ``` 实现spring中的`BeanPostProcessor`接口,实现扫描bean的时候根据注解上传递的参数创建相应的动态代理对象 ```java @Slf4j @Component public class SpringBeanPostProcessor implements BeanPostProcessor { .... @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class targetClass = bean.getClass(); Field[] declaredFields = targetClass.getDeclaredFields(); for (Field declaredField : declaredFields) { RpcReference rpcReference = declaredField.getAnnotation(RpcReference.class); if (rpcReference != null) { RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder() .group(rpcReference.group()) .version(rpcReference.version()).build(); RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceConfig); Object clientProxy = rpcClientProxy.getProxy(declaredField.getType()); declaredField.setAccessible(true); try { declaredField.set(bean, clientProxy); } catch (IllegalAccessException e) { e.printStackTrace(); } } } return bean; } } ``` 动态代理对象中保存了注解上的相应的服务信息,以及接口信息,而且代理对象中实现了相关的远程调用细节 `RpcClientProxy` 代理类实现了真正的远程调用细节 ```java @Override public Object invoke(Object proxy, Method method, Object[] args) { log.info("invoked method: [{}]", method.getName()); RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName()) .parameters(args) .interfaceName(method.getDeclaringClass().getName()) .paramTypes(method.getParameterTypes()) .requestId(UUID.randomUUID().toString()) .group(rpcServiceConfig.getGroup()) .version(rpcServiceConfig.getVersion()) .build(); RpcResponse rpcResponse = null; if (rpcRequestTransport instanceof NettyRpcClient) { CompletableFuture> completableFuture = (CompletableFuture>) rpcRequestTransport.sendRpcRequest(rpcRequest); rpcResponse = completableFuture.get(); } if (rpcRequestTransport instanceof SocketRpcClient) { rpcResponse = (RpcResponse) rpcRequestTransport.sendRpcRequest(rpcRequest); } this.check(rpcResponse, rpcRequest); return rpcResponse.getData(); } ``` # 负载均衡 ## [基于权重随机算法的 RandomLoadBalance](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#21-randomloadbalance) RandomLoadBalance 是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。比如,经过一万次选择后,服务器 A 被选中的次数大约为5000次,服务器 B 被选中的次数约为3000次,服务器 C 被选中的次数约为2000次。 以上就是 RandomLoadBalance 背后的算法思想,比较简单。下面开始分析源码。 ```java public class RandomLoadBalance extends AbstractLoadBalance { public static final String NAME = "random"; private final Random random = new Random(); @Override protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { int length = invokers.size(); int totalWeight = 0; boolean sameWeight = true; // 下面这个循环有两个作用,第一是计算总权重 totalWeight, // 第二是检测每个服务提供者的权重是否相同 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); // 累加权重 totalWeight += weight; // 检测当前服务提供者的权重与上一个服务提供者的权重是否相同, // 不相同的话,则将 sameWeight 置为 false。 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; } } // 下面的 if 分支主要用于获取随机数,并计算随机数落在哪个区间上 if (totalWeight > 0 && !sameWeight) { // 随机获取一个 [0, totalWeight) 区间内的数字 int offset = random.nextInt(totalWeight); // 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。 // 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。 // 第一次循环,offset - 5 = 2 > 0,即 offset > 5, // 表明其不会落在服务器 A 对应的区间上。 // 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8, // 表明其会落在服务器 B 对应的区间上 for (int i = 0; i < length; i++) { // 让随机值 offset 减去权重值 offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { // 返回相应的 Invoker return invokers.get(i); } } } // 如果所有服务提供者权重值相同,此时直接随机返回一个即可 return invokers.get(random.nextInt(length)); } } ``` RandomLoadBalance 的算法思想比较简单,在经过多次请求后,能够将调用请求按照权重值进行“均匀”分配。当然 RandomLoadBalance 也存在一定的缺点,当调用次数比较少时,Random 产生的随机数可能会比较集中,此时多数请求会落到同一台服务器上。这个缺点并不是很严重,多数情况下可以忽略。RandomLoadBalance 是一个简单,高效的负载均衡实现,因此 Dubbo 选择它作为缺省实现。 关于 RandomLoadBalance 就先到这了,接下来分析 LeastActiveLoadBalance。 ## [基于 hash 一致性的 ConsistentHashLoadBalance](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#23-consistenthashloadbalance) 一致性 hash 算法由麻省理工学院的 Karger 及其合作者于1997年提出的,算法提出之初是用于大规模缓存系统的负载均衡。它的工作过程是这样的,首先根据 ip 或者其他的信息为缓存节点生成一个 hash,并将这个 hash 投射到 [0, 2^32^ - 1] 的圆环上。当有查询或写入请求时,则为缓存项的 key 生成一个 hash 值。然后**查找第一个大于或等于该 hash 值的缓存节点**,并到这个节点中查询或写入缓存项。如果当前节点挂了,则在下一次查询或写入缓存时,为缓存项查找另一个大于其 hash 值的缓存节点即可。大致效果如下图所示,每个缓存节点在圆环上占据一个位置。如果缓存项的 key 的 hash 值小于缓存节点 hash 值,则到该缓存节点中存储或读取缓存项。比如下面绿色点对应的缓存项将会被存储到 cache-2 节点中。由于 cache-3 挂了,原本应该存到该节点中的缓存项最终会存储到 cache-4 节点中。 ![image.png](https://b3logfile.com/siyuan/1613457082108/assets/image-20210704201315-h8hhs4v.png) 下面来看看一致性 hash 在 Dubbo 中的应用。我们把上图的缓存节点替换成 Dubbo 的服务提供者,于是得到了下图: ![](https://dubbo.apache.org/imgs/dev/consistent-hash-invoker.jpg) 这里相同颜色的节点均属于同一个服务提供者,比如 Invoker1-1,Invoker1-2,……, Invoker1-160。这样做的目的是通过引入虚拟节点,让 Invoker 在圆环上分散开来,避免数据倾斜问题。所谓数据倾斜是指,由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到了少量请求的情况。比如: > ["hash环偏斜"](siyuan://blocks/20210704201641-3xd10s0) > ![](https://dubbo.apache.org/imgs/dev/consistent-hash-data-incline.jpg) 如上,由于 Invoker-1 和 Invoker-2 在圆环上分布不均,导致系统中75%的请求都会落到 Invoker-1 上,只有 25% 的请求会落到 Invoker-2 上。解决这个问题办法是引入虚拟节点,通过虚拟节点均衡各个节点的请求量。 ["hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?"](siyuan://blocks/20210704212830-vtxq6uc) 到这里背景知识就普及完了,接下来开始分析源码。我们先从 ConsistentHashLoadBalance 的 doSelect 方法开始看起,如下: ```java public class ConsistentHashLoadBalance extends AbstractLoadBalance { private final ConcurrentMap> selectors = new ConcurrentHashMap>(); @Override protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { String methodName = RpcUtils.getMethodName(invocation); String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName; // 获取 invokers 原始的 hashcode int identityHashCode = System.identityHashCode(invokers); ConsistentHashSelector selector = (ConsistentHashSelector) selectors.get(key); //一个selector对应一组服务,这个selector负责对这组相同的服务进行负载均衡调用 // 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,可能新增也可能减少了。 // 此时 selector.identityHashCode != identityHashCode 条件成立 if (selector == null || selector.identityHashCode != identityHashCode) { // 创建新的 ConsistentHashSelector selectors.put(key, new ConsistentHashSelector(invokers, methodName, identityHashCode)); selector = (ConsistentHashSelector) selectors.get(key); } // 调用 ConsistentHashSelector 的 select 方法选择 Invoker return selector.select(invocation); } private static final class ConsistentHashSelector {...} } ``` 如上,doSelect 方法主要做了一些前置工作,比如检测 invokers 列表是不是变动过,以及创建 ConsistentHashSelector。这些工作做完后,接下来开始调用 ConsistentHashSelector 的 select 方法执行负载均衡逻辑。在分析 select 方法之前,我们先来看一下一致性 hash 选择器 ConsistentHashSelector 的初始化过程,如下: ```java private static final class ConsistentHashSelector { // 使用 TreeMap 存储 Invoker 虚拟节点 private final TreeMap> virtualInvokers; private final int replicaNumber; private final int identityHashCode; private final int[] argumentIndex; ConsistentHashSelector(List> invokers, String methodName, int identityHashCode) { this.virtualInvokers = new TreeMap>(); this.identityHashCode = identityHashCode; URL url = invokers.get(0).getUrl(); // 获取虚拟节点数,默认为160,也就是每个服务节点对对应与hash环上的160个虚拟节点 this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160); // 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算 String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0")); argumentIndex = new int[index.length]; for (int i = 0; i < index.length; i++) { argumentIndex[i] = Integer.parseInt(index[i]); } for (Invoker invoker : invokers) { String address = invoker.getUrl().getAddress(); for (int i = 0; i < replicaNumber / 4; i++) { // 对 address + i 进行 md5 运算,得到一个长度为16的字节数组 byte[] digest = 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, h = 3 时过程同上 long m = hash(digest, h); // 将 hash 到 invoker 的映射关系存储到 virtualInvokers 中, // virtualInvokers 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构 virtualInvokers.put(m, invoker); } } } } } ``` ConsistentHashSelector 的构造方法执行了一系列的初始化逻辑,比如从配置中获取虚拟节点数以及参与 hash 计算的参数下标,默认情况下只使用第一个参数进行 hash。需要特别说明的是,ConsistentHashLoadBalance 的负载均衡逻辑只受参数值影响,具有相同参数值的请求将会被分配给同一个服务提供者。ConsistentHashLoadBalance 不 关系权重,因此使用时需要注意一下。 在获取虚拟节点数和参数下标配置后,接下来要做的事情是计算虚拟节点 hash 值,并将虚拟节点存储到 TreeMap 中。到此,ConsistentHashSelector 初始化工作就完成了。接下来,我们来看看 select 方法的逻辑。 ```java public Invoker select(Invocation invocation) { // 将参数转为 key String key = toKey(invocation.getArguments()); // 对参数 key 进行 md5 运算 byte[] digest = md5(key); // 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法, // 寻找合适的 Invoker return selectForKey(hash(digest, 0)); } private Invoker selectForKey(long hash) { // 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 Invoker //从map的尾部开始找,因为treemap是递增有序的,找出所有大于等于的集合中的第一个值 Map.Entry> entry = virtualInvokers.tailMap(hash, true).firstEntry(); // 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null, // 需要将 TreeMap 的头节点赋值给 entry if (entry == null) { entry = virtualInvokers.firstEntry(); } // 返回 Invoker return entry.getValue(); } ``` 如上,选择的过程相对比较简单了。首先是对参数进行 md5 以及 hash 运算,得到一个 hash 值。然后再拿这个值到 TreeMap 中查找目标 Invoker 即可。 到此关于 ConsistentHashLoadBalance 就分析完了。在阅读 ConsistentHashLoadBalance 源码之前,大家一定要先补充背景知识,不然很难看懂代码逻辑。 # 序列化 https://blog.csdn.net/ncuzengxiebo/article/details/83820821 kryo大体有三种序列化方法,每种方式都有其优势和劣势。 1,kryo.writeObject,这种方法只会序列化对象实例,而不会记录对象所属类的任何信息。优势是最节省空间,劣势是在反序列化时,需要提供类作为模板才能顺利反序列。反序列化时使用readObject。 2,直接kryo.writeClassAndObject,这种方法会先将对象所属类的全限定名序列化,然后再依次序列化对象实例的成员。优势是完全动态序列化,整个kryo周期都不需要提供类信息。反序列化时使用readClassAndObject 3,先注册,再kryo.writeClassAndObject,这种方式时最理想的,其结合了前两种优势,又有效规避了劣势,事先将需要序列化的类注册给kryo(此时类和唯一id绑定),之后使用writeClassAndObject序列化时,只会序列化注册id,而不会序列化类的全限定名了,这样大大节省了空间(通常只比writeObject多一个字节)。反序列化时使用readClassAndObject。注意序列化和反序列的kryo的注册信息应当保持一致。 # RPC 协议 **为了避免语义不一致的事情发生,我们就需要在发送请求的时候设定一个边界,然后在收到请求的时候按照这个设定的边界进行数据分割。这个边界语义的表达,就是我们所说的协议。** # 优化点 ## 使用CompletableFuture优化接受服务提供端返回结果 最开始的时候是通过 `AttributeMap` 绑定到Channel上实现的 > AttributeMap这是是绑定在Channel或者ChannelHandlerContext上的一个附件,相当于依附在这两个对象上的寄生虫一样,相当于附件一样。 > > 我们知道每一个ChannelHandlerContext都是ChannelHandler和ChannelPipeline之间连接的桥梁,每一个ChannelHandlerContext都有属于自己的上下文,也就说每一个ChannelHandlerContext上如果有AttributeMap都是绑定上下文的,也就说如果A的ChannelHandlerContext中的AttributeMap,B的ChannelHandlerContext是无法读取到的。 > > 但是Channel上的AttributeMap就是大家共享的,每一个ChannelHandler都能获取到。

链接:https://www.jianshu.com/p/95cc930d48bf
> ![image.png](https://b3logfile.com/siyuan/1613457082108/assets/image-20210705154810-659y9hz.png) > 这个缺点是每次发送完消息,还需要等待服务器那边返回消息才能往下执行,在这里阻塞等待服务器的消息 > 这种是实现的缺点是不清晰,而且你每次都要调用 `channel.closeFuture().sync();` 阻塞来手动等待请求返回。 ## **增加 Netty 心跳机制** : 保证客户端和服务端的连接不被断掉,避免重连。 在客户端的handler中加入了心跳机制,保证客户端和服务端的链接不被断掉,避免重连。 `com/luwanglin/remoting/transport/netty/client/NettyRpcClientHandler.java` ```java @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleState state = ((IdleStateEvent) evt).state(); if (state == IdleState.WRITER_IDLE) { log.info("write idle happen [{}]", ctx.channel().remoteAddress()); Channel channel = nettyRpcClient.getChannel((InetSocketAddress) ctx.channel().remoteAddress()); RpcMessage rpcMessage = new RpcMessage(); rpcMessage.setCodec(SerializationTypeEnum.PROTOSTUFF.getCode()); rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode()); //心跳请求包 rpcMessage.setMessageType(RpcConstants.HEARTBEAT_REQUEST_TYPE); rpcMessage.setData(RpcConstants.PING); channel.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } } else { super.userEventTriggered(ctx, evt); } } ``` # Dubbo SPI机制 ![Dubbo_SPI机制.png](https://b3logfile.com/siyuan/1613457082108/assets/Dubbo_SPI机制-20210705224743-084ulc0.png) 简化版的Dubbo SPI机制 ```java public final class ExtensionLoader { private static final String SERVICE_DIRECTORY = "META-INF/extensions/"; //保存所有的接口的对应的扩展类ExtensionLoader private static final Map, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap<>(); //key为扩展类对象,value为扩展类的实例对象 private static final Map, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(); private final Class type; /** * 保存接口的实现类对应的Holder对象,因为一个接口可能有多种实现类,一个holder对象保存一个接口实现类 */ private final Map> cachedInstances = new ConcurrentHashMap<>(); /** * 保存配置文中的键值对关系,value为相应的Class对象,一个holder对象中保存了对应接口的所有扩展类的类对象 */ private final Holder>> cachedClasses = new Holder<>(); private ExtensionLoader(Class type) { this.type = type; } @SuppressWarnings("unchecked") public static ExtensionLoader getExtensionLoader(Class type) { if (type == null) { throw new IllegalArgumentException("Extension type should not be null."); } if (!type.isInterface()) { throw new IllegalArgumentException("Extension type must be an interface."); } if (type.getAnnotation(SPI.class) == null) { throw new IllegalArgumentException("Extension type must be annotated by @SPI"); } // firstly get from cache, if not hit, create one ExtensionLoader extensionLoader = (ExtensionLoader) EXTENSION_LOADERS.get(type); if (extensionLoader == null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); extensionLoader = (ExtensionLoader) EXTENSION_LOADERS.get(type); } return extensionLoader; } public T getExtension(String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Extension name should not be null or empty."); } // firstly get from cache, if not hit, create one Holder holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<>()); holder = cachedInstances.get(name); } // create a singleton if no instance exists //单例模式中的双重检查方式 Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { instance = createExtension(name); holder.set(instance); } } } return (T) instance; } private T createExtension(String name) { // load all extension classes of type T from file and get specific one by name Class clazz = getExtensionClasses().get(name); if (clazz == null) { throw new RuntimeException("No such extension of name " + name); } T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { try { //因为前面getExtension调用当前方法时已经使用双重检查保证了并发下的安全 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } catch (Exception e) { log.error(e.getMessage()); } } return instance; } private Map> getExtensionClasses() { // get the loaded extension class from the cache Map> classes = cachedClasses.get(); // double check if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { //因为这个映射关系只加载一次,所以只用使用hashmap就足够了 classes = new HashMap<>(); // load all extensions from our extensions directory loadDirectory(classes); cachedClasses.set(classes); } } } return classes; } private void loadDirectory(Map> extensionClasses) { //根据同名文件加载所有的同名文件 String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName(); try { Enumeration urls; ClassLoader classLoader = ExtensionLoader.class.getClassLoader(); urls = classLoader.getResources(fileName); if (urls != null) { while (urls.hasMoreElements()) { URL resourceUrl = urls.nextElement(); loadResource(extensionClasses, classLoader, resourceUrl); } } } catch (IOException e) { log.error(e.getMessage()); } } private void loadResource(Map> extensionClasses, ClassLoader classLoader, URL resourceUrl) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) { String line; // read every line while ((line = reader.readLine()) != null) { // get index of comment final int ci = line.indexOf('#'); if (ci >= 0) { // string after # is comment so we ignore it line = line.substring(0, ci); } line = line.trim(); if (line.length() > 0) { try { final int ei = line.indexOf('='); String name = line.substring(0, ei).trim(); String clazzName = line.substring(ei + 1).trim(); // our SPI use key-value pair so both of them must not be empty if (name.length() > 0 && clazzName.length() > 0) { Class clazz = classLoader.loadClass(clazzName); extensionClasses.put(name, clazz); } } catch (ClassNotFoundException e) { log.error(e.getMessage()); } } } } catch (IOException e) { log.error(e.getMessage()); } } } ``` > 配置文件中的key-value键值对映射关系是在一次性加载的,而且Class对象也是一次性加载的,但是实例对象是在需要的时候才通过反射进行创建的 > # RPC调用流程 客户端发起调用,被代理对象拦截 ```java public Object invoke(Object proxy, Method method, Object[] args) { log.info("invoked method: [{}]", method.getName()); RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName()) .parameters(args) .interfaceName(method.getDeclaringClass().getName()) .paramTypes(method.getParameterTypes()) .requestId(UUID.randomUUID().toString()) .group(rpcServiceConfig.getGroup()) .version(rpcServiceConfig.getVersion()) .build(); RpcResponse rpcResponse = null; if (rpcRequestTransport instanceof NettyRpcClient) { CompletableFuture> completableFuture = (CompletableFuture>) rpcRequestTransport.sendRpcRequest(rpcRequest); rpcResponse = completableFuture.get(); } if (rpcRequestTransport instanceof SocketRpcClient) { rpcResponse = (RpcResponse) rpcRequestTransport.sendRpcRequest(rpcRequest); } this.check(rpcResponse, rpcRequest); return rpcResponse.getData(); } ``` 将接口信息,参数,参数类型,请求id,组,版本等信息进行封装到request对象中,然后调用sendRequest方法发送请求 通过zookeeper进行服务发现之后,调用相应的负载均衡算法选择服务提供方并返回其的地址 具体的发送信息交由netty进行编码之后发送,服务端收到消息之后进行解码,然后rpcrequest对象的信息进行处理,使用反射调用相关的处理方法返回结果 # 性能测试对比 使用Apache ab测试工具进行测试的,使用Netty 替换socket进行网络传输,对比测试如下: Netty ```java (base)  ~/ ab -c 1000 -n 10000 http://gpu2.luckforefforts.top:19999/testBenchmark This is ApacheBench, Version 2.3 <$Revision: 1807734 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking gpu2.luckforefforts.top (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: Server Hostname: gpu2.luckforefforts.top Server Port: 19999 Document Path: /testBenchmark Document Length: 25 bytes Concurrency Level: 1000 Time taken for tests: 1.919 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 1580000 bytes HTML transferred: 250000 bytes Requests per second: 5211.41 [#/sec] (mean) Time per request: 191.887 [ms] (mean) Time per request: 0.192 [ms] (mean, across all concurrent requests) Transfer rate: 804.10 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 1 14 103.7 3 1030 Processing: 15 169 55.4 158 385 Waiting: 15 169 55.3 158 384 Total: 27 183 116.6 161 1197 Percentage of the requests served within a certain time (ms) 50% 161 66% 198 75% 209 80% 215 90% 243 95% 252 98% 374 99% 1163 100% 1197 (longest request) ``` socket ```java (base)  ~/ ab -c 1000 -n 10000 http://gpu2.luckforefforts.top:19999/testBenchmark This is ApacheBench, Version 2.3 <$Revision: 1807734 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking gpu2.luckforefforts.top (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: Server Hostname: gpu2.luckforefforts.top Server Port: 19999 Document Path: /testBenchmark Document Length: 25 bytes Concurrency Level: 1000 Time taken for tests: 3.129 seconds Complete requests: 10000 Failed requests: 41 (Connect: 0, Receive: 0, Length: 41, Exceptions: 0) Non-2xx responses: 41 Total transferred: 1583034 bytes HTML transferred: 254182 bytes Requests per second: 3195.68 [#/sec] (mean) Time per request: 312.922 [ms] (mean) Time per request: 0.313 [ms] (mean, across all concurrent requests) Transfer rate: 494.03 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 1 20 127.2 3 1038 Processing: 7 116 169.1 86 3107 Waiting: 7 115 169.1 86 3107 Total: 22 135 213.5 89 3115 Percentage of the requests served within a certain time (ms) 50% 89 66% 94 75% 98 80% 102 90% 124 95% 293 98% 1120 99% 1140 100% 3115 (longest request) ``` 性能将近提升了60%