C10K问题是代表一万个并发处理连接的术语。为此,我们经常需要更改已创建的网络套接字的设置以及Linux内核的默认设置,监视 TCP发送/接收缓冲区和队列的使用, 尤其是将我们的应用程序调整为合适的选项来解决这个问题。
在今天的文章中,我将讨论如果我们要构建可处理数千个连接的可伸缩应用程序,则需要遵循一些通用原则。如果您想从应用程序和底层系统中获得一些见识,我将参考Netty Framework,TCP和Socket内部以及一些有用的工具。
原则1:确保您的应用适合C10K问题
如上所述,当我们需要在最少的上下文切换和低内存占用的情况下尽可能多地利用CPU时,则需要使进程中的线程数非常接近于给定专用处理器的数量。
请牢记这一点,唯一可能的解决方案是选择一些非阻塞业务逻辑或具有很高CPU / IO处理时间比例(但是已经很危险)的业务逻辑。
有时,在您的应用程序堆栈中识别此行为不是很容易,将需要重新排列应用程序/代码,添加其他外部队列(RabbitMQ)或主题(Kafka),使用分布式系统,缓冲任务并能够从中拆分非阻塞代码。
但是,根据我的经验,由于以下原因,值得重写我的代码并使之更加不受阻塞:
我将我的应用程序分为两个不同的应用程序,即使它们共享相同的“域”,它们也很可能不会共享相同的部署和设计策略(例如,应用程序的一部分是可以使用线程池实现的REST端点,基于HTTP的服务器,第二部分是队列/主题的使用者,该队列/主题使用非阻塞驱动程序将某些内容写入DB。
我能够以不同的方式缩放这两个部分的实例数,因为负载/ CPU /内存很可能完全不同。
使用适当的工具:
我们保持线程数量尽可能少。不要忘记不仅检查服务器线程,还检查应用程序的其他部分:队列/主题使用者,DB驱动程序设置,日志记录设置(使用异步微批处理)。始终进行线程转储dump,以查看在您的应用程序中创建了哪些线程以及创建了多少线程(不要忘记使其在负载下进行,否则您的线程池将不会被完全初始化,其中很多都是延迟创建线程的)。我总是从线程池中为我的自定义线程命名(找到受害者并调试代码要容易得多)。
请注意,如果堵塞发生在对其他服务的HTTP / DB调用,我们可以使用反应式客户端,该客户端自动为传入的响应注册回调。考虑使用更适合服务2服务通信的协议,例如RSocket。
检查您的应用程序中包含的线程数是否一直很少。它指的是您的应用程序是否具有有限的线程池,并且能够承受给定的负载。
如果您的应用程序具有多个处理流,请始终验证其中哪些正在阻塞以及哪些是非阻塞。如果阻塞流的数量很大,那么您几乎肯定需要使用不同线程(来自预定义线程池)处理每个请求。在这种情况下,请考虑将基于线程池的HTTP Server与工作程序一起使用,在该服务器上,所有请求都与一个非常大的线程池放在不同的线程上以提高吞吐量。
原理2:缓存连接,而不是线程
该原理与HTTP Server编程模型的主题紧密相关 。主要思想不是将连接绑定到单个线程,而是使用一些库,这些库支持稍微复杂但更有效的读取TCP方法。
这并不意味着TCP连接绝对是免费的。最关键的部分是 TCP握手。因此,您应该始终使用持久连接(如设置Nginx的keep-alive)。如果仅将一个TCP连接用于发送一条消息,则将支付8个TCP段的开销(连接和关闭连接= 7个段)。
接受新的TCP连接
如果我们处在无法使用持久连接的情况下,那么很可能在很短的时间内就会产生大量已创建的连接。必须将这些已创建的连接排队,并等待接受我们的应用程序。
在上图中,我们可以看到积压了SYN和LISTEN。在 SYN Backlog中, 我们可以找到仅等待使用TCP Handshake进行确认的连接。但是,在LISTEN Backlog列表中, 我们已经完全初始化了连接,即使使用仅等待应用程序接受的TCP发送/接收缓冲区也是如此。请阅读SYN Flood DDoS攻击。
如果我们承受着很大的负担,并且有很多传入连接,那么实际上存在一个问题,负责接受连接的应用程序线程可能很繁忙:对已经连接的客户端执行IO。
new ServerBootstrap()
.channel(EpollServerSocketChannel.class)
.group(bossEventLoopGroup, workerEventLoopGroup)
.localAddress(8080)
.childOption(ChannelOption.SO_SND