socket选项 SO_REUSEPORT

miffa 发布于 2015/03/24 17:21

字数 3383
阅读 6076
收藏 6

前言

本篇用于记录学习SO_REUSEPORT的笔记和心得,末尾还会提供一个bindp小工具也能为已有的程序享受这个新的特性。

当前Linux网络应用程序问题

运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:

  1. 单线程listen/accept,多个工作线程接收任务分发,虽CPU的工作负载不再是问题,但会存在:

    • 单线程listener,在处理高速率海量连接时,一样会成为瓶颈

    • CPU缓存行丢失套接字结构(socket structure)现象严重

  2. 所有工作线程都accept()在同一个服务器套接字上呢,一样存在问题:

    • 多线程访问server socket锁竞争严重

    • 高负载下,线程之间处理不均衡,有时高达3:1不均衡比例

    • 导致CPU缓存行跳跃(cache line bouncing)

    • 在繁忙CPU上存在较大延迟

上面模型虽然可以做到线程和CPU核绑定,但都会存在:

  • 单一listener工作线程在高速的连接接入处理时会成为瓶颈

  • 缓存行跳跃

  • 很难做到CPU之间的负载均衡

  • 随着核数的扩展,性能并没有随着提升

比如HTTP CPS(Connection Per Second)吞吐量并没有随着CPU核数增加呈现线性增长:

Linux kernel 3.9带来了SO_REUSEPORT特性,可以解决以上大部分问题。

SO_REUSEPORT解决了什么问题

linux man文档中一段文字描述其作用:

The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.

SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:

  • 允许多个套接字 bind()/listen() 同一个TCP/UDP端口

    • 每一个线程拥有自己的服务器套接字

    • 在服务器套接字上没有了锁的竞争

  • 内核层面实现负载均衡

  • 安全层面,监听同一个端口的套接字只能位于同一个用户下面

其核心的实现主要有三点:

  • 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport。

  • 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口

  • 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。

代码分析,可以参考引用资料 [多个进程绑定相同端口的实现分析[Google Patch]]。

CPU之间平衡处理,水平扩展

以前通过fork形式创建多个子进程,现在有了SO_REUSEPORT,可以不用通过fork的形式,让多进程监听同一个端口,各个进程中accept socket fd不一样,有新连接建立时,内核只会唤醒一个进程来accept,并且保证唤醒的均衡性。

模型简单,维护方便了,进程的管理和应用逻辑解耦,进程的管理水平扩展权限下放给程序员/管理员,可以根据实际进行控制进程启动/关闭,增加了灵活性。

这带来了一个较为微观的水平扩展思路,线程多少是否合适,状态是否存在共享,降低单个进程的资源依赖,针对无状态的服务器架构最为适合了。

新特性测试或多个版本共存

可以很方便的测试新特性,同一个程序,不同版本同时运行中,根据运行结果决定新老版本更迭与否。

针对对客户端而言,表面上感受不到其变动,因为这些工作完全在服务器端进行。

服务器无缝重启/切换

想法是,我们迭代了一版本,需要部署到线上,为之启动一个新的进程后,稍后关闭旧版本进程程序,服务一直在运行中不间断,需要平衡过度。这就像Erlang语言层面所提供的热更新一样。

想法不错,但是实际操作起来,就不是那么平滑了,还好有一个hubtime开源工具,原理为SIGHUP信号处理器+SO_REUSEPORT+LD_RELOAD,可以帮助我们轻松做到,有需要的同学可以检出试用一下。

SO_REUSEPORT已知问题

SO_REUSEPORT根据数据包的四元组{src ip, src port, dst ip, dst port}和当前绑定同一个端口的服务器套接字数量进行数据包分发。若服务器套接字数量产生变化,内核会把本该上一个服务器套接字所处理的客户端连接所发送的数据包(比如三次握手期间的半连接,以及已经完成握手但在队列中排队的连接)分发到其它的服务器套接字上面,可能会导致客户端请求失败,一般可以使用:

  • 使用固定的服务器套接字数量,不要在负载繁忙期间轻易变化

  • 允许多个服务器套接字共享TCP请求表(Tcp request table)

  • 不使用四元组作为Hash值进行选择本地套接字处理,挑选隶属于同一个CPU的套接字

与RFS/RPS/XPS-mq协作,可以获得进一步的性能:

  • 服务器线程绑定到CPUs

  • RPS分发TCP SYN包到对应CPU核上

  • TCP连接被已绑定到CPU上的线程accept()

  • XPS-mq(Transmit Packet Steering for multiqueue),传输队列和CPU绑定,发送数据

  • RFS/RPS保证同一个连接后续数据包都会被分发到同一个CPU上

  • 网卡接收队列已经绑定到CPU,则RFS/RPS则无须设置

  • 需要注意硬件支持与否

目的嘛,数据包的软硬中断、接收、处理等在一个CPU核上,并行化处理,尽可能做到资源利用最大化。

SO_REUSEPORT不是一贴万能膏药

虽然SO_REUSEPORT解决了多个进程共同绑定/监听同一端口的问题,但根据新浪林晓峰同学测试结果来看,在多核扩展层面也未能够做到理想的线性扩展:

可以参考Fastsocket在其基础之上的改进,链接地址

支持SO_REUSEPORT的Tengine

淘宝的Tengine已经支持了SO_REUSEPORT特性,在其测试报告中,有一个简单测试,可以看出来相对比SO_REUSEPORT所带来的性能提升:

使用SO_REUSEPORT以后,最明显的效果是在压力下不容易出现丢请求的情况,CPU均衡性平稳。

Java支持否?

JDK 1.6语言层面不支持,至于以后的版本,由于暂时没有使用到,不多说。

Netty 3/4版本默认都不支持SO_REUSEPORT特性,但Netty 4.0.19以及之后版本才真正提供了JNI方式单独包装的epoll native transport版本(在Linux系统下运行),可以配置类似于SO_REUSEPORT等(JAVA NIIO没有提供)选项,这部分是在io.netty.channel.epoll.EpollChannelOption中定义(在线代码部分)。

在linux环境下使用epoll native transport,可以获得内核层面网络堆栈增强的红利,如何使用可参考Native transports文档。

使用epoll native transport倒也简单,类名稍作替换:

NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel

比如写一个PING-PONG应用服务器程序,类似代码:

public void run() throws Exception {
    EventLoopGroup bossGroup = new EpollEventLoopGroup();
    EventLoopGroup workerGroup = new EpollEventLoopGroup();
    try {
        ServerBootstrap b = new ServerBootstrap();
        ChannelFuture f = b
                .group(bossGroup, workerGroup)
                .channel(EpollServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch)
                            throws Exception {
                        ch.pipeline().addLast(
                                new StringDecoder(CharsetUtil.UTF_8),
                                new StringEncoder(CharsetUtil.UTF_8),
                                new PingPongServerHandler());
                    }
                }).option(ChannelOption.SO_REUSEADDR, true)
                .option(EpollChannelOption.SO_REUSEPORT, true)
                .childOption(ChannelOption.SO_KEEPALIVE, true).bind(port)
                .sync();
        f.channel().closeFuture().sync();
    } finally {
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
    }
}

若不要这么折腾,还想让以往Java/Netty应用程序在不做任何改动的前提下顺利在Linux kernel >= 3.9下同样享受到SO_REUSEPORT带来的好处,不妨尝试一下bindp,更为经济,这一部分下面会讲到。

bindp,为已有应用添加SO_REUSEPORT特性

以前所写bindp小程序,可以为已有程序绑定指定的IP地址和端口,一方面可以省去硬编码,另一方面也为测试提供了一些方便。

另外,为了让以前没有硬编码SO_REUSEPORT的应用程序可以在Linux内核3.9以及之后Linux系统上也能够得到内核增强支持,稍做修改,添加支持。

但要求如下:

  1. Linux内核(>= 3.9)支持SO_REUSEPORT特性

  2. 需要配置REUSE_PORT=1

不满足以上条件,此特性将无法生效。

使用示范:

REUSE_PORT=1 BIND_PORT=9999 LD_PRELOAD=./libbindp.so java -server -jar pingpongserver.jar &

当然,你可以根据需要运行命令多次,多个进程监听同一个端口,单机进程水平扩展。

使用示范

使用python脚本快速构建一个小的示范原型,两个进程,都监听同一个端口10000,客户端请求返回不同内容,仅供娱乐。

server_v1.py,简单PING-PONG:

# -*- coding:UTF-8 -*-

import socket
import os PORT = 10000
BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', PORT))
s.listen(1) while True:
    conn, addr = s.accept()
    data = conn.recv(PORT)
    conn.send('Connected to server[%s] from client[%s]\n' % (os.getpid(), addr))
    conn.close() s.close()

server_v2.py,输出当前时间:

# -*- coding:UTF-8 -*-

import socket
import time
import os PORT = 10000
BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', PORT))
s.listen(1) while True:
    conn, addr = s.accept()
    data = conn.recv(PORT)
    conn.send('server[%s] time %s\n' % (os.getpid(), time.ctime()))
    conn.close() s.close()

借助于bindp运行两个版本的程序:

REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v1.py &
REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v2.py &

模拟客户端请求10次:

for i in {1..10};do echo "hello" | nc 127.0.0.1 10000;done

看看结果吧:

Connected to server[3139] from client[('127.0.0.1', 48858)]
server[3140] time Thu Feb 12 16:39:12 2015
server[3140] time Thu Feb 12 16:39:12 2015
server[3140] time Thu Feb 12 16:39:12 2015
Connected to server[3139] from client[('127.0.0.1', 48862)]
server[3140] time Thu Feb 12 16:39:12 2015
Connected to server[3139] from client[('127.0.0.1', 48864)]
server[3140] time Thu Feb 12 16:39:12 2015
Connected to server[3139] from client[('127.0.0.1', 48866)]
Connected to server[3139] from client[('127.0.0.1', 48867)]

可以看出来,CPU分配很均衡,各自分配50%的请求量。

嗯,虽是小玩具,有些意思 :))

SO_REUSADDR VS SO_REUSEPORT

因为能力有限,还是有很多东西(SO_REUSEADDR和SO_REUSEPORT的区别等)没有能够在一篇文字中表达清楚,作为补遗,也方便以后自己回过头来复习。

两者不是一码事,没有可比性。有时也会被其搞晕,自己总结的不好,推荐StackOverflow的Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ?资料,总结的很全面。

简单来说:

  • 设置了SO_REUSADDR的应用可以避免TCP 的 TIME_WAIT 状态 时间过长无法复用端口,尤其表现在应用程序关闭-重启交替的瞬间

  • SO_REUSEPORT更强大,隶属于同一个用户(防止端口劫持)的多个进程/线程共享一个端口,同时在内核层面替上层应用做数据包进程/线程的处理均衡

若有困惑,推荐两者都设置,不会有冲突。

Netty多线程使用SO_REUSEPORT

上一篇讲到SO_REUSEPORT,多个程绑定同一个端口,可以根据需要控制进程的数量。这里讲讲基于Netty 4.0.25+Epoll navtie transport在单个进程内多个线程绑定同一个端口的情况,也是比较实用的。

TCP服务器,同一个进程多线程绑定同一个端口

这是一个PING-PONG示范应用:

     public void run() throws Exception {
            final EventLoopGroup bossGroup = new EpollEventLoopGroup();
            final EventLoopGroup workerGroup = new EpollEventLoopGroup();
            ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)
                     .channel(EpollServerSocketChannel. class)
                     .childHandler( new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(
                                            new StringDecoder(CharsetUtil.UTF_8 ),
                                            new StringEncoder(CharsetUtil.UTF_8 ),
                                            new PingPongServerHandler());
                           }
                     }).option(ChannelOption. SO_REUSEADDR, true)
                     .option(EpollChannelOption. SO_REUSEPORT, true)
                     .childOption(ChannelOption. SO_KEEPALIVE, true);             int workerThreads = Runtime.getRuntime().availableProcessors();
           ChannelFuture future;
            //new  thread            for ( int i = 0; i < workerThreads; ++i) {
                future = b.bind( port).await();
                 if (!future.isSuccess())
                      throw new Exception(String. format("fail to bind on port = %d.",
                                 port), future.cause());
           }
           Runtime. getRuntime().addShutdownHook (new Thread(){
                 @Override
                 public void run(){
                     workerGroup.shutdownGracefully();
                     bossGroup.shutdownGracefully();
                }
           });
     }

打成jar包,在CentOS 7下面运行,检查同一个端口所打开的文件句柄。

# lsof -i:8000
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    3515 root   42u  IPv6  29040      0t0  TCP *:irdmi (LISTEN)
java    3515 root   43u  IPv6  29087      0t0  TCP *:irdmi (LISTEN)
java    3515 root   44u  IPv6  29088      0t0  TCP *:irdmi (LISTEN)
java    3515 root   45u  IPv6  29089      0t0  TCP *:irdmi (LISTEN)

同一进程,但打开的文件句柄是不一样的。

UDP服务器,多个线程绑同一个端口

/**
 * UDP谚语服务器,单进程多线程绑定同一端口示范
 */
public final class QuoteOfTheMomentServer {        private static final int PORT = Integer.parseInt(System. getProperty("port" ,
                   "9000" ));        public static void main(String[] args) throws Exception {
             final EventLoopGroup group = new EpollEventLoopGroup();             Bootstrap b = new Bootstrap();
            b.group(group).channel(EpollDatagramChannel. class)
                        .option(EpollChannelOption. SO_REUSEPORT, true )
                        .handler( new QuoteOfTheMomentServerHandler());             int workerThreads = Runtime.getRuntime().availableProcessors();
             for (int i = 0; i < workerThreads; ++i) {
                  ChannelFuture future = b.bind( PORT).await();
                   if (!future.isSuccess())
                         throw new Exception(String.format ("Fail to bind on port = %d.",
                                     PORT), future.cause());
            }             Runtime. getRuntime().addShutdownHook(new Thread() {
                   @Override
                   public void run() {
                        group.shutdownGracefully();
                  }
            });
      }
}
} @Sharable
class QuoteOfTheMomentServerHandler extends
            SimpleChannelInboundHandler<DatagramPacket> {        private static final String[] quotes = {
                   "Where there is love there is life." ,
                   "First they ignore you, then they laugh at you, then they fight you, then you win.",
                   "Be the change you want to see in the world." ,
                   "The weak can never forgive. Forgiveness is the attribute of the strong.", };        private static String nextQuote() {
             int quoteId = ThreadLocalRandom.current().nextInt( quotes .length );
             return quotes [quoteId];
      }        @Override
       public void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet)
                   throws Exception {
             if ("QOTM?" .equals(packet.content().toString(CharsetUtil. UTF_8))) {
                  ctx.write( new DatagramPacket(Unpooled.copiedBuffer( "QOTM: "
                              + nextQuote(), CharsetUtil. UTF_8), packet.sender()));
            }
      }        @Override
       public void channelReadComplete(ChannelHandlerContext ctx) {
            ctx.flush();
      }        @Override
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
      }
}

同样也要检测一下端口文件句柄打开情况:

# lsof -i:9000
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    3181 root   26u  IPv6  27188      0t0  UDP *:cslistener
java    3181 root   27u  IPv6  27217      0t0  UDP *:cslistener
java    3181 root   28u  IPv6  27218      0t0  UDP *:cslistener
java    3181 root   29u  IPv6  27219      0t0  UDP *:cslistener

小结

以上为Netty+SO_REUSEPORT多线程绑定同一端口的一些情况,是为记载。

Netty之ChannelOption的各种参数之EpollChannelOption.SO_REUSEPORT的更多相关文章

  1. Netty之ChannelOption的各种参数

    ChannelOption.SO_BACKLOG, 1024 BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最 ...

  2. Netty 中ChannelOption的含义以及使用的场景

    Netty 中ChannelOption的含义以及使用的场景 转自:http://www.cnblogs.com/googlemeoften/p/6082785.html 1.ChannelOptio ...

  3. Netty之ChannelOption

    一.概述 最近在写一个分布式服务框架,打算用netty框架做底层网络通信,关于netty的学习可以参考如下资料: http://blog.csdn.net/column/details/enjoyne ...

  4. Netty:option和childOption参数设置说明

    Channel配置参数 (1).通用参数 CONNECT_TIMEOUT_MILLIS :   Netty参数,连接超时毫秒数,默认值30000毫秒即30秒. MAX_MESSAGES_PER_REA ...

  5. Linux下Netty实现高性能UDP服务(SO_REUSEPORT)

    参考: https://www.jianshu.com/p/61df929aa98b SO_REUSEPORT学习笔记:http://www.blogjava.net/yongboy/archive/ ...

  6. 谈谈如何使用Netty开发实现高性能的RPC服务器

    RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协议.说的再直白一点,就是客户端在不必知道 ...

  7. Netty实现高性能RPC服务器优化篇之消息序列化

    在本人写的前一篇文章中,谈及有关如何利用Netty开发实现,高性能RPC服务器的一些设计思路.设计原理,以及具体的实现方案(具体参见:谈谈如何使用Netty开发实现高性能的RPC服务器).在文章的最后 ...

  8. Java Netty 4.x 用户指南

    问题 今天,我们使用通用的应用程序或者类库来实现互相通讯,比如,我们经常使用一个 HTTP 客户端库来从 web 服务器上获取信息,或者通过 web 服务来执行一个远程的调用. 然而,有时候一个通用的 ...

  9. 基于Netty的私有协议栈的开发

    基于Netty的私有协议栈的开发 书是人类进步的阶梯,每读一本书都使自己得以提升,以前看书都是看了就看了,当时感觉受益匪浅,时间一长就又还回到书本了!所以说,好记性不如烂笔头,以后每次看完一本书都写一 ...

随机推荐

  1. JDBC API阐述

    JDBC API JDBC API 是一系列的接口,它使得应用程序能够进行数据库联接,执行SQL语句,并且得到返回结果. Driver 接口 Java.sql.Driver 接口是所有 JDBC 驱动 ...

  2. Mybatis进阶使用-一级缓存与二级缓存

    简介 缓存是一般的ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力.跟Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口. 一级缓存 ...

  3. python open函数初习

    open("路径","打开方式")  打开方式:'r'只读模式,‘w’写模式,‘a’追加模式 ‘b’二进制模式,‘+’读/写模式.例: fh=open(&quo ...

  4. Mac上如果看不到.git目录的解决方法

    Mac OS X上,如果需要查看.git目录下的隐藏文件,操作很简单: 做法是:打开一个Terminal终端窗口,输入: defaults write com.apple.finder AppleSh ...

  5. Azure Logic App 入门(一)

    一,引言 前两天看一个azure相关的题,接触到一个叫 “Azure Logic App” 的服务,刚好,今天抽空学习以下,顺便结合它做一篇入门的分析文章. 首先,我们得对它有个大概的认识,了解以下A ...

  6. Kubernetes K8S之资源控制器StatefulSets详解

    Kubernetes的资源控制器StatefulSet详解与示例 主机配置规划 服务器名称(hostname) 系统版本 配置 内网IP 外网IP(模拟) k8s-master CentOS7.7 2 ...

  7. 企业网站SEO如何选择关键词

    http://www.wocaoseo.com/thread-17-1-1.html       企业网站的关键词应该如何去选择?有很多的企业老板在网上某某企业在网上做了一个网站,一天盈利多少后,觉得 ...

  8. 写Seo网站标题应该注意什么

    http://www.wocaoseo.com/thread-11-1-1.html 最近看了群里一些朋友讨论关于网站优化标题应该注意哪些?各种说法五花八门,好的seo优化标题是可以给网站带来不错的流 ...

  9. Htmlcss学习笔记1——html基础

    Hyper text markup language 超文本标签语言.不是一种编程语言,而是一种标记语言标记语言是一套标记标签 开发工具 chrome subline vscode photoshop ...

  10. Python | 详解Python中的协程,为什么说它的底层是生成器?

    今天是Python专题的第26篇文章,我们来聊聊Python当中的协程. 我们曾经在golang关于goroutine的文章当中简单介绍过协程的概念,我们再来简单review一下.协程又称为是微线程, ...