网络编程答疑融合连环tcp/nio/bio/redis/redisson/lettuce/netty/dubbo

  • Post author:
  • Post category:其他


如果有不对的地方, 欢迎在评论区指正:

  1. bio

    1.1 请求-响应模型. 对于接收方, serverSocket.accept() 为每个请求(连接)安排一个线程

    1.2浪费(阻塞占比大): socket.getInputStream().read()调用是阻塞的, 实际情况对于常见的web应用, 大家都是长连接, 同一时刻, 阻塞在此在线程会不少.

    1.3 风险:线程很容易就上去满了

    1.4 对于.1.3风险, 改良: 可以使用线程池, socket封装为task, 往线程池提交任务. 忙不过来就排到任务队列去

    1.5 对于1.2 浪费: 没办法, 因为面向流的bio读数据的时候是阻塞的, 就算你同一个线程绑定多个连接, 总不能上一个连接read()不到数据的时候, 线程阻塞. 那这个线程上的其他连接都没戏了

  2. nio

    2.1 对于1.2浪费, 我们希望, 1个线程具有监听多个连接的能力, 当多个连接的任意一个具有可读数据的时候. 线程可以获取到数据. 这就是io多路复用

    这里有几点需要说明:

    2.2.1 1个线程监听m个连接(这个m是没有上限的. 你可能有疑惑, 那没有上限, 程序的上限在哪里呢, 这个在后文netty会说. 这个线程池中每个线程基本都均摊了m个不同的连接.) 这个就是多路复用

    2.2.2 我们想要达到这个样子, 我给你分配一块内存, 当连接有数据可读的时候, 内核要自动帮我读到这块内存中去, 这个块内存就叫缓冲区吧. (如果缓冲区读满了还是没有读完连接上传来的此轮数据, 没关系, 方正我线程是自旋去读数据的, 下一个循环依然会有这个连接). 内核完成这个操作就减少了用户态内核态之间的切换与数据拷贝, 程序员编程也简单了. 这个缓冲区就不像bio了, bio没有缓冲区, bio是面向流的 单向的. 我这个buffer可读可写 , 维护两个指针就行了, 时分复用一块内存. 区分为可读区可写区. 这样就可以给写操作用了.

    2.2.3 内核想要完成多路复用, 前提是系统提供的不阻塞读的系统调用. 如果连接没有数据读就立即返回. 当然, 这其中有基于遍历的 有基于事件回调的, epoll poll等实现的多路复用器

  3. 所以总结:

    在这里插入图片描述

  4. 连接是绑定到固定的线程上的好处

    4.1 1个线程监听m个连接, 连接是绑定到固定的线程上. 好处, 站在连接的角度, 这个连接上的数据处理是串行有序的, 是线程安全的, 是可以高效无锁的, 是方便编程的. 这也是netty优雅高效的设计之一(这个和是现在服务端还是客户端无关, 只要是netty, 这两码事, 不要老以为netty nio是服务端).

    进一步展开:

    4.11 tcp是有序的, 只要连接是绑定到固定的线程上, 连接的处理就是串行的. 也就是说, 即便请求方并发的通过同一个连接发了多个请求过来, 这几个请求是不会乱序的.

    4.12 这就解释了为什么 redisson/lettuce 同一个连接上发出的并发请求, 收到的响应依然是和请求同顺序的(redis单线程连接, 哪怕redis高版本的io是多线程的, 只要连接是绑定到固定的io线程上. 所以站在同一个连接上看, redis还是单线程的). redis通讯协议不支持加requestId啥的, 照样能高效(还减少了并发竞争与切换) 把请求和响应对应起来. 而且redis本身很快, 只要没有满命令, lettuce单个连接也是很强的

    4.13 和2.2.1相关, 谈不上局限吧. 拿接收方来说, 1个线程监听m个连接, 并且绑定了这几个连接

    既然io线程绑了多个连接, 自然要快点处理, 所以io线程不要挂上多余耗时的业务处理 不然把绑定上的其他连接rt增加了.

    所以良好的设计是, io线程后面再跟一个业务线程池, io线程负责读写解码心跳等, 线程数量就设定cpu核数或者2倍(因为没有阻塞的情况下, 最大并行就这么多). 业务线程就看服务器性能了, 也和业务耗时有关, dubbo默认200 (为啥不是cpu数, 别闹, 我们还是要提高并发的, 不能要客户干等不是), 而且这种设计也是解耦的, 业务耗时归业务

    4.14 业务线程池拿dubbo举例 如何让请求和响应对应呢?

    业务线程池如果还是连接会绑定在固定线程上的话, 站在连接角度的一切还是串行的. 只不过是在几个线程之间接力串行.

    但是业务线程池往往不会这么做, dubbo不是这样的, 因为每个可读连接都是一份急待处理的业务请求, 需要同优先度的处理, 而不是由于绑在同一个线程上而被迫等待上一个业务请求处理完才能处理. 所以需要一个请求id放在通信协议中. 响应报文也会带, 发送方收到响应后, 按缓存请求id和接下来要处理结果的对象 (一般封装的连接对象上是缓存一个map结构, 请求id为key, 上面挂一个future. 当请求方等待在future.get()的时候, 就可以被唤醒并取到结果了)

4.15 4.13说要加跟一个业务线程池, 那和bio相比强在哪里呢? bio渴望与所有连接数正相关, nio更渴望与同一时间并发的可读连接正相关(bio压根没可读事件等事件, 不阻塞读的话, 没法感知, nio能注册感兴趣的事件). bio是连接数维度的, nio是并发请求数维度的, 还是有一定百分比差距的. 当然如果你的业务就是所有连接都是大报文, 传大对象, 或者高io, 那nio的io线程反而就不行了, 因为nio是想省线程, 按之前说的io线程是cpu数. 不够用. 反而会 增加并发开销, 线程切换, 以及阻塞同线程下绑定的其他连接. 换言之, io报文处理要足够快, 这才是高性能. 大报文时. 解析一个报文, 可能要多轮几次多路复用器才能把

整个报文读或写出来, 这一下就让同一个线程上多个channel间的同一时刻都要待读数据需要处理概率增大了, 因为你一轮没有处理完, 处理不过来. 所以大报文下, netty性能要下降些. io线程要配置多点? 配置多点那就没意义了, 就是线程数量上需要涨几倍了, 还不如直接接业务线程池好了. (也不要什么扩展性, 高性能网络通讯框架, 因为高并发下报文大, 本来就高性能不起来嘛, io线程处理不过来, io压力变大了)

  1. tomcat vs netty (此段尚在研究验证中, 需要对比下springboot内嵌的tomcat和内嵌的netty实现的webserver)

    5.1 共同点: 大家都是reactor模型, 也就是说, 让少量的io线程去做事件监听, 要阻塞就是这几个io线程, 不要阻塞我的业务线程, 我的业务线程是要高吞吐率, 要干活的, 不能闲着. io线程只管提交封装成runbable对象过来就行了

    5.2 差异: netty希望io线程能编解码报文, 等报文准备好了, 再提交任务.(否则不全的报文数据就积压在buffer中, 不阻塞, 可以利用这个空档继续处理监听的其他连接编解码) , 提给业务线程(比如dubbo业务线程池)

    而tomcat是只要是可读, 就解码等操作都给业务线程了(读的过程是阻塞的(就算是不阻塞, 也是for循环读, 实际上就是阻塞的), 会一直读, 读到读完整个报文) https://www.cnblogs.com/sglx/p/15423671.html

    请求体是阻塞的, 请求行和头不是

    5.2 tomcat vs netty 我的理解

    5.2.1性能差异恐怕不大, 大报文下netty的性能差异可能差点, 因为少量的io线程可能读写多个连接编解码任务繁重. (所以并发高没事, 但是报文要编解码快, 所以netty以及基于他的框架比如dubbo 不适合大报文比如超大对象, 文件这种)高并发+小报文下可能表现更好一点吧, 相当于花了更少的线程, 干了同样的活.

    5.2.2 netty 更适合被二次开发, 比如服务端 加一个线程池就行了, 扩展性强

    5.2.3 tomcat 是专业web服务器, netty能搞jsp吗, websocket? 如何维护session? 主流web展示层技术兼不兼容? 如何下文件等把这里搞定, 那netty又变成和另一个tomcat了. 专物专用, 都不要贬低. 如果只是http场景不是复杂的情况下, 并且是在小数据的请求上,此时netty优于tomcat, 可以用netty实现http服务器. 因为: https://zhuanlan.zhihu.com/p/433868436

  2. nio是非阻塞吗

    是的, 不是降低阻塞, 是非阻塞, 因为没数据可读时可以立即返回. (返回你可以干点别的事, 然后再去轮询)

    如果你不想轮询, 那得aio了 (这个以后开一篇吧)

    但是要注意, 大报文请求的高并发, 如果一个线程绑定了多个连接, 串行处理下, 有等待问题.

  3. 客户端需要nio吗? 或者说客户端连接池需要nio吗?

    看情况:

    相比非阻塞, 不阻塞你, 你的线程多了一份自由选择的余地.

    情况1: 线程可以继续处理别的请求, 发往服务端, 前提是两个: 1. 你能把请求和响应对得起来 2. 你的协议本身是支持并发发请求的, 也就是非阻塞. (其实这两个隐隐是同一个意思, 也就是协议或者服务端起码要有能力让你把请求和响应对应起来)

    比如: lettuce 单个线程就挺好, 因为redis快. 请求响应也能对应, 这个在上文说过(这个和协议无关, 原因是站在同一连接的角度, redis是串行的, 线程安全的, 就简单说成是单线程吧)

    再比如: http2支持并发发请求并发响应(多路复用), 所以可以.但http1.0不能, 不支持并发发请求, http1.1也够呛, 因为不支持并发响应. (浏览器直接aio)

    再比如: dubbo客户端 tcp 也能并发发, dubbo 协议中有requestid来把请求和响应对得起来

    反正, 基本不会亏. 只要你不是天生就是个阻塞的业务场景, 出了阻塞在哪里, 没有啥别的事要做.

    情况2: 线程安全问题

    jedis(即单个连接对象)是线程不安全的, 在一个请求响应回合不要被多个线程占用. 不然就可能出现线程安全问题. 官方推荐使用 线程安全的池 即池化提高性能又安全(为什么安全呢, 客户端自己保证, 连接池租借出来, 然后使用, 用完在归还给池中. 用的途中, 不在池子里面, 别的线程借不到, 再还回去. 就是线程安全的. 时分复用, 这也是与享元模式的区别, 线程不安全, 享元往往是线程安全, 同时共享).

    即便你解决了安全问题, 还得解决请求响应对应问题(最终就变成下一个lettuce)

    情况3: 事务问题

    事务使用过程中, 是要独占这个连接的. 开启和结束. 别的提交不要进来, 进来就乱了.

    所以事务是独占连接, 相当于也是线程不安全的.

    情况4(还在研究): jdbc目前还是阻塞的(JDBC的设计者可能认为NIO对于此目的不必要,或者它会给API增加不必要的复杂性.), 没有采用nio. 需要使用真正实现了 NIO 的数据库客户端。目前有这些 NIO 的 JDBC 客户端,但是都不普及:

    Vert.x 客户端:https://vertx.io/docs/vertx-jdbc-client/java/

    r2jdbc 客户端:http://r2dbc.io/

    Jasync-sql 客户端:https://github.com/jasync-sql/jasync-sql

    为什么不必要呢? 只能说jdbc目前是这个样子吧, 要考虑事务, api易用, 并发, 请求响应对应, 不阻塞你能带来什么? 带来多给数据库来点并发? 你的下一步操作是否需要依赖这一步查询的结果, 如果是并且你也没别的事能干, 那就阻塞算了. 如果不是, 那还是有一些别的场景的

8.常见中间件的线程模型

服务端不一定是reactor模型的, 因为网络io不一定是大部分中间件的瓶颈, 也就是说, 短板不在那里, 补了用处有, 但不大.

reids是: https://www.cnblogs.com/wzh2010/p/15886804.html (redis是单线程浪费多核cpu, 得利用起来, 所以用来干io和其他事情)

mysql待确定源码: https://blog.csdn.net/weixin_30298733/article/details/113211281

tomcat是: https://blog.csdn.net/qq_26222859/article/details/46230095

rocketmq是:

  1. 总结: nio非阻塞就是花少量的io线程 换来 非阻塞和并发向同一连接请求, 所以省连接, 非阻塞(意味着当前线程可以干别的事)

    netty 线程安全 (连接安排在固定的线程上)

Lettuce是一个高性能的redis客户端,底层基于netty框架来管理连接,天然是非阻塞和线程安全的。

比起jedis需要为每个实例创建物理连接来保证线程安全,lettuce确实很优秀。



版权声明:本文为stevenxyy原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。