写点什么

netty 系列之: 在 netty 中实现线程和 CPU 绑定

作者:程序那些事
  • 2022 年 5 月 18 日
  • 本文字数:3520 字

    阅读完需:约 12 分钟

netty系列之:在netty中实现线程和CPU绑定

简介

之前我们介绍了一个非常优秀的细粒度控制 JAVA 线程的库:java thread affinity。使用这个库你可以将线程绑定到特定的 CPU 或者 CPU 核上,通过减少线程在 CPU 之间的切换,从而提升线程执行的效率。


虽然 netty 已经够优秀了,但是谁不想更加优秀一点呢?于是一个想法产生了,那就是能不能把 affinity 库用在 netty 中呢?


答案是肯定的,一起来看看吧。

引入 affinity

affinity 是以 jar 包的形式提供出去的,目前最新的正式版本是 3.20.0,所以我们需要这样引入:


<!-- https://mvnrepository.com/artifact/net.openhft/affinity --><dependency>    <groupId>net.openhft</groupId>    <artifactId>affinity</artifactId>    <version>3.20.0</version></dependency>
复制代码


引入 affinity 之后,会在项目的依赖库中添加一个 affinity 的 lib 包,这样我们就可以在 netty 中愉快的使用 affinity 了。

AffinityThreadFactory

有了 affinity,怎么把 affinity 引入到 netty 中呢?


我们知道 affinity 是用来控制线程的,也就是说 affinity 是跟线程有关的。而 netty 中跟线程有关的就是 EventLoopGroup,先看一下 netty 中 EventLoopGroup 的基本用法,这里以 NioEventLoopGroup 为例,NioEventLoopGroup 有很多构造函数的参数,其中一种是传入一个 ThreadFactory:


    public NioEventLoopGroup(ThreadFactory threadFactory) {        this(0, threadFactory, SelectorProvider.provider());    }
复制代码


这个构造函数表示 NioEventLoopGroup 中使用的线程都是由 threadFactory 创建而来的。这样以来我们就找到了 netty 和 affinity 的对应关系。只需要构造 affinity 的 ThreadFactory 即可。


刚好 affinity 中有一个 AffinityThreadFactory 类,专门用来创建 affinity 对应的线程。


接下来我们来详细了解一下 AffinityThreadFactory。


AffinityThreadFactory 可以根据提供的不同 AffinityStrategy 来创建对应的线程。


AffinityStrategy 表示的是线程之间的关系。在 affinity 中,有 5 种线程关系,分别是:


    SAME_CORE - 线程会运行在同一个CPU core中。    SAME_SOCKET - 线程会运行在同一个CPU socket中,但是不在同一个core上。    DIFFERENT_SOCKET - 线程会运行在不同的socket中。    DIFFERENT_CORE - 线程会运行在不同的core上。    ANY - 只要是可用的CPU资源都可以。
复制代码


这些关系是通过 AffinityStrategy 中的 matches 方法来实现的:


boolean matches(int cpuId, int cpuId2);
复制代码


matches 传入两个参数,分别是传入的两个 cpuId。我们以 SAME_CORE 为例来看看这个 mathes 方法到底是怎么工作的:


    SAME_CORE {        @Override        public boolean matches(int cpuId, int cpuId2) {            CpuLayout cpuLayout = AffinityLock.cpuLayout();            return cpuLayout.socketId(cpuId) == cpuLayout.socketId(cpuId2) &&                    cpuLayout.coreId(cpuId) == cpuLayout.coreId(cpuId2);        }    }
复制代码


可以看到它的逻辑是先获取当前 CPU 的 layout,CpuLayout 中包含了 cpu 个数,sockets 个数,每个 sockets 的 cpu 核数等基本信息。并且提供了三个方法根据给定的 cpuId 返回对应的 socket、core 和 thread 信息:


    int socketId(int cpuId);
int coreId(int cpuId);
int threadId(int cpuId);
复制代码


matches 方法就是根据传入的 cpuId 找到对应的 socket,core 信息进行比较,从而生成了 5 中不同的策略。


先看一下 AffinityThreadFactory 的构造函数:


    public AffinityThreadFactory(String name, boolean daemon, @NotNull AffinityStrategy... strategies) {        this.name = name;        this.daemon = daemon;        this.strategies = strategies.length == 0 ? new AffinityStrategy[]{AffinityStrategies.ANY} : strategies;    }
复制代码


可以传入 thread 的 name 前缀,和是否是守护线程,最后如果 strategies 不传的话,默认使用的是 AffinityStrategies.ANY 策略,也就是说为线程分配任何可以绑定的 CPU。


接下来看下这个 ThreadFactory 是怎么创建新线程的:


public synchronized Thread newThread(@NotNull final Runnable r) {        String name2 = id <= 1 ? name : (name + '-' + id);        id++;        Thread t = new Thread(new Runnable() {            @Override            public void run() {                try (AffinityLock ignored = acquireLockBasedOnLast()) {                    r.run();                }            }        }, name2);        t.setDaemon(daemon);        return t;    }
private synchronized AffinityLock acquireLockBasedOnLast() { AffinityLock al = lastAffinityLock == null ? AffinityLock.acquireLock() : lastAffinityLock.acquireLock(strategies); if (al.cpuId() >= 0) lastAffinityLock = al; return al; }
复制代码


从上面的代码可以看出,创建的新线程会以传入的 name 为前缀,后面添加 1,2,3,4 这种后缀。并且根据传入的是否是守护线程的标记,将调用对应线程的 setDaemon 方法。


重点是 Thread 内部运行的 Runnable 内容,在 run 方法内部,首先调用 acquireLockBasedOnLast 方法获取 lock,在获得 lock 的前提下运行对应的线程方法,这样就会将当前运行的 Thread 和 CPU 进行绑定。


从 acquireLockBasedOnLast 方法中,我们可以看出 AffinityLock 实际上是一个链式结构,每次请求的时候都调用的是 lastAffinityLock 的 acquireLock 方法,如果获取到 lock,则将 lastAffinityLock 进行替换,用来进行下一个 lock 的获取。


有了 AffinityThreadFactory,我们只需要在 netty 的使用中传入 AffinityThreadFactory 即可。

在 netty 中使用 AffinityThreadFactory

上面讲到了要在 netty 中使用 affinity,可以将 AffinityThreadFactory 传入 EventLoopGroup 中。对于 netty server 来说可以有两个 EventLoopGroup,分别是 acceptorGroup 和 workerGroup,在下面的例子中我们将 AffinityThreadFactory 传入 workerGroup,这样后续 work 中分配的线程都会遵循 AffinityThreadFactory 中配置的 AffinityStrategies 策略,来获得对应的 CPU:


//建立两个EventloopGroup用来处理连接和消息        EventLoopGroup acceptorGroup = new NioEventLoopGroup(acceptorThreads);        //创建AffinityThreadFactory        ThreadFactory threadFactory = new AffinityThreadFactory("affinityWorker", AffinityStrategies.DIFFERENT_CORE,AffinityStrategies.DIFFERENT_SOCKET,AffinityStrategies.ANY);        //将AffinityThreadFactory加入workerGroup        EventLoopGroup workerGroup = new NioEventLoopGroup(workerThreads,threadFactory);        try {            ServerBootstrap b = new ServerBootstrap();            b.group(acceptorGroup, workerGroup)                    .channel(NioServerSocketChannel.class)                    .childHandler(new ChannelInitializer<SocketChannel>() {                        @Override                        public void initChannel(SocketChannel ch) throws Exception {                            ch.pipeline().addLast(new AffinityServerHandler());                        }                    })                    .option(ChannelOption.SO_BACKLOG, 128)                    .childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口并开始接收连接 ChannelFuture f = b.bind(port).sync();
// 等待server socket关闭 f.channel().closeFuture().sync(); } finally { //关闭group workerGroup.shutdownGracefully(); acceptorGroup.shutdownGracefully(); }
复制代码


为了获取更好的性能,Affinity 还可以对 CPU 进行隔离,被隔离的 CPU 只允许执行本应用的线程,从而获得更好的性能。


要使用这个特性需要用到 linux 的 isolcpus。这个功能主要是将一个或多个 CPU 独立出来,用来执行特定的 Affinity 任务。


isolcpus 命令后面可以接 CPU 的 ID,或者可以修改/boot/grub/grub.conf 文件,添加要隔离的 CPU 信息如下:


isolcpus=3,4,5
复制代码

总结

affinity 可以对线程进行极致管控,对性能要求严格的朋友可以试试,但是在使用过程中需要选择合适的 AffinityStrategies,否则可能会得不到想要的结果。


本文的例子可以参考:learn-netty4


更多内容请参考 http://www.flydean.com/51-netty-thread-affinity/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 22 小时前阅读数: 27
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:在netty中实现线程和CPU绑定_Java_程序那些事_InfoQ写作社区