blogjava-凯发k8网页登录

blogjava-凯发k8网页登录http://www.blogjava.net/yongboy/category/54837.html记录工作/学习的点点滴滴。zh-cnmon, 01 jun 2015 03:42:48 gmtmon, 01 jun 2015 03:42:48 gmt60so_reuseport学习笔记补遗http://www.blogjava.net/yongboy/archive/2015/02/25/423037.htmlnieyongnieyongwed, 25 feb 2015 14:23:00 gmthttp://www.blogjava.net/yongboy/archive/2015/02/25/423037.htmlhttp://www.blogjava.net/yongboy/comments/423037.htmlhttp://www.blogjava.net/yongboy/archive/2015/02/25/423037.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/423037.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/423037.html

前言

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

so_reusaddr vs so_reuseport

两者不是一码事,没有可比性。有时也会被其搞晕,自己总结的不好,推荐stackoverflow的资料,总结的很全面。

简单来说:

  • 设置了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;
            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多线程绑定同一端口的一些情况,是为记载。



nieyong 2015-02-25 22:23
]]>
so_reuseport学习笔记http://www.blogjava.net/yongboy/archive/2015/02/12/422893.htmlnieyongnieyongthu, 12 feb 2015 08:50:00 gmthttp://www.blogjava.net/yongboy/archive/2015/02/12/422893.htmlhttp://www.blogjava.net/yongboy/comments/422893.htmlhttp://www.blogjava.net/yongboy/archive/2015/02/12/422893.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/422893.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422893.html

前言

本篇用于记录学习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语言层面所提供的热更新一样。

想法不错,但是实际操作起来,就不是那么平滑了,还好有一个开源工具,原理为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,可以获得内核层面网络堆栈增强的红利,如何使用可参考文档。

使用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() {
                    @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,为已有应用添加so_reuseport特性

以前所写小程序,可以为已有程序绑定指定的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%的请求量。

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

bindp的使用方法

更多使用说明,请参考。

参考资料

  • 《so_reuseport: scaling techniques for servers with high connection rates》ppt


nieyong 2015-02-12 16:50
]]>
fastsocket学习笔记之小结篇http://www.blogjava.net/yongboy/archive/2015/02/05/422760.htmlnieyongnieyongthu, 05 feb 2015 07:21:00 gmthttp://www.blogjava.net/yongboy/archive/2015/02/05/422760.htmlhttp://www.blogjava.net/yongboy/comments/422760.htmlhttp://www.blogjava.net/yongboy/archive/2015/02/05/422760.html#feedback3http://www.blogjava.net/yongboy/comments/commentrss/422760.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422760.html

前言

前面啰啰嗦嗦的几篇文字,各个方面介绍了fastsocket,盲人摸象一般,能力有限,还得继续深入学习不是。这不,到了该小结收尾的时候了。

缘起,内核已经成为瓶颈

使用linux作为服务器,在请求量很小的时候,是不用担心其性能。但在海量的数据请求下,linux内核在tcp/ip网络处理方面,已经成为瓶颈。比如新浪在某台haproxy服务器上取样,90%的cpu时间被内核占用,应用程序只能够分配到较少的cpu时钟周期的资源。

经过haproxy系统详尽分析后,发现大部分cpu资源消耗在kernel里,并且在多核平台下,kernel在网络协议栈处理过程中存在着大量同步开销。

同时在多核上进行测试,http cps(connection per second)吞吐量并没有随着cpu核数增加呈现线性增长:


内核3.9之前的linux tcp调用


  • kernel 3.9之前的tcp socket实现
  • bind系统调用会将socket和port进行绑定,并加入全局tcp_hashinfo的bhash链表中
  • 所有bind调用都会查询这个bhash链表,如果port被占用,内核会导致bind失败
  • listen则是根据用户设置的队列大小预先为tcp连接分配内存空间
  • 一个应用在同一个port上只能listen一次,那么也就只有一个队列来保存已经建立的连接
  • nginx在listen之后会fork处多个worker,每个worker会继承listen的socket,每个worker会创建一个epoll fd,并将listen fd和accept的新连接的fd加入epoll fd
  • 但是一旦新的连接到来,多个nginx worker只能排队accept连接进行处理
  • 对于大量的短连接,accept显然成为了一个瓶颈

linux网络堆栈所存在问题

  • tcp处理&多核

    • 一个完整的tcp连接,中断发生在一个cpu核上,但应用数据处理可能会在另外一个核上
    • 不同cpu核心处理,带来了锁竞争和cpu cache miss(波动不平衡)
    • 多个进程监听一个tcp套接字,共享一个listen queue队列
    • 用于连接管理全局哈希表格,存在资源竞争
    • epoll io模型多进程对accept等待,惊群现象

  • linux vfs的同步损耗严重

    • socket被vfs管理
    • vfs对文件节点inode和目录dentry有同步需求
    • socket只需要在内存中存在即可,非严格意义上文件系统,不需要inode和dentry
    • 代码层面略过不必须的常规锁,但又保持了足够的兼容性

fastsocket所作改进

  1. tcp单个连接完整处理做到了cpu本地化,避免了资源竞争
  2. 保持完整bsd socket api

cpu之间不共享数据,并行化各自独立处理tcp连接,也是其高效的主要原因。其架构图可以看出其改进:


fastsocket架构图可以很清晰说明其大致结构,内核态和用户态通过ioctl函数传输。记得netmap在重写网卡驱动里面通过ioctl函数直接透传到用户态中,其更为高效,但没有完整的tcp/ip网络堆栈支持嘛。

fastsocket的tcp调用图

  • 多个进程可以同时listen在同一个port上
  • 动态链接库libfsocket.so拦截socket、bind、listen等系统调用并进入这个链接库进行处理
  • 对于listen系统调用,fastsocket会记录下这个fd,当应用通过epoll将这个fd加入到epoll fdset中时,libfsocket.so会通过ioctl为该进程clone listen fd关联的socket、sock、file的系统资源
  • 内核模块将clone的socket再次调用bind和listen
  • bind系统调用检测到另外一个进程绑定到已经被绑定的port时,会进行相关检查
  • 通过检查sock将会被记录到port相关联的一个链表中,通过该链表可以知道所有bind同一个port的sock
  • 而sock是关联到fd的,进程则持有fd,那么所有的资源就已经关联到一起
  • 新的进程再次调用listen系统调用的时候,fastsocket内核会再次为其关联的sock分配accept队列
  • 结果是多个进程也就拥有了多个accept队列,可避免cpu cache miss
  • fastsocket提供将每个listen和accept的进程绑定到用户指定的cpu核
  • 如果用户未指定,fastsocket将会为该进程默认绑定一个空闲的cpu核

fastsocket短连接性能

在新浪测试中,在24核的安装有centos 6.5的服务器上,借助于fastsocket,nginx和haproxy每秒处理连接数指标(connection/second)性能很惊人,分别增加290%和620%。这也证明了,fastsocket带来了tcp连接快速处理的能力。 除此之外,借助于硬件特性:

  • 借助于intel超级线程,可以获得另外20%的性能增长
  • haproxy代理服务器借助于网卡flow-director特性支持,吞吐量可增加15%

fastsocket v1.0正式版从2014年3月份开始已经在新浪生产环境中使用,用作代理服务器,因此大家可以考虑是否可以采用。针对1.0版本,以下环境较为收益:

  • 服务器至少不少于8个cpu核心
  • 短连接被大量使用
  • cpu周期大部分消耗在网络软中断和套接字系统调用上
  • 应用程序使用基于epoll的非阻塞io
  • 应用程序使用多个进程单独接受连接

多线程嘛,就得需要参考示范应用所提供实践建议了。

nginx测试服务器配置

  • nginx工作进程数量设置成cpu核数个
  • http keep-alive特性被禁用
  • 测试端http_load从nginx获取64字节静态文件,并发量为500*cpu核数
  • 启用内存缓存静态文件访问,用于排除磁盘影响
  • 务必禁用accept_mutex(多核访问accept产生锁竞争,另fastsocket内核模块为其去除了锁竞争)

从下表测试图片中,可以看到:

  1. fastsocket在24核服务器达到了475k connection/second,获得了21倍的提升
  2. centos 6.5在cpu核数增长到12核时并没有呈现线性增长势头,反而在24核时下降到159k cps
  3. linux kernel 3.13在24核时获得了近乎两倍于centos 6.5的吞吐量,283k cps,但在12核后呈现出扩展性瓶颈

haproxy重要配置

  • 工作进程数量等同于cpu核数个
  • 需要启用rfd(receive flow deliver)
  • http keep-alive需要禁用
  • 测试端http_load并发量为500*cpu核数
  • 后端服务器响应外围64个字节的消息

测试结果中:

  • fastsocket呈现出了惊人的扩展性能
  • 24核,linux kernel 3.13成绩为139k cps
  • 24核,centos 6.5借助fastsocket,获得了370k cps的吞吐量

实际部署环境的成绩

8核服务器线上环境运行了24小时的成绩,图a展示了部署fastsocket之前cpu利用率,图b为部署了fastsocekt之后的cpu利用率。 fastsocket带来的收益:

  • 每个cpu核心负载均衡
  • 平均cpu利用率降低10%
  • haproxy处理能力增长85%


其实吧,这一块期待新浪公布更多的数据。

长连接的支持正在开发中

长连接支持,还是需要等一等的。但是要支持什么类型长连接?百万级别应用服务器类型,还是redis,可能是后者。虽然目前正做,但目前没有时间表,但目前所做特性总结如下:

  1. 网络堆栈的定制
    • skb-pool,每一cpu核对应一个预分配skb pool,替换内核缓冲区kernel slab
      • percore skb pool
      • 合并skb头部和数据
      • 本地pool和重复循环使用的pool(flow-director)
    • fast-epoll
      • 多进程之间tcp连接共享变得稀少
      • 在file结构体中保存epoll entry,用以节省调用epoll_ctl时红黑树查询的开销
  2. 跨层的设计
    • direct-tcp,数据包隶属于已建立套接字会直接跳过路由过程
      • 记录tcp套接字的输入路由信息(record input route information in tcp socket)
      • 直接查找网络套接字在进入网络堆栈之前(lookup socket directly before network stack)
      • 从套接字读取输入路由信息(read input route information from socket)
      • 标记数据包被路有过(mark the packet as routed)
    • receive-cpu-selection 类似于rfs,但更轻巧、精准与快速
      • 把当前cpu核id编码到套接字中(application marks current cpu id in the socket)
      • 直接查询套接字在进入网络堆栈之前(lookup socket directly before network stack)
      • 读取套接字中包含的cpu核,然后发送给它(read cpu id from socket and deliver accordingly)
    • rps-framework 数据包在进入网络堆栈之前,让开发者在内核模块之外定制数据包投递规则,扩充rps功能

redis测试结果

测试环境:

  • cpu: intel e5 2640 v2 (6 core) * 2
  • nic: intel x520

redis配置选项:

  • tcp持久连接
  • 8个redis实例,绑定不同端口
  • 使用到8个cpu核心,并且绑定cpu核

测试结果:

  • 仅开启rss:20%的吞吐量增加
  • 启用网卡flow-director特性:45%吞吐量增加

但需要注意:

  • 仅为实验测试阶段
  • 为v1.0补充,nginx和haproxy同样会收益

fastsocket v1.1

v1.1版本要增加长连接的支持,那么类似于redis的服务器应用程序就很受益了,因为没有具体的时间表,只能够慢慢等待了。

以后一些优化措施

  1. 在上下文切换时,避免拷贝操作,zero-copy
  2. 中断机制完善,减少中断
  3. 支持批量提交,降低系统函数调用
  4. 提交到linux kernel主分支上去
  5. hugetlb/hugepage等

fastsocket和mtcp等简单对比

说是对比,其实是我从mtcp论文中摘取出来,增加了fastsocket一栏,可以看出人们一直努力的脚步。

types accept queue conn. locality socket api event handling packet i/o application mod- ification kernel modification
psio ,
dpdk ,
pf ring ,
netmap
no tcp stack batched no interface for transport layer no
(nic driver)
linux-2.6 shared none bsd socket syscalls per packet transparent no
linux-3.9 per-core none bsd socket syscalls per packet add option so reuseport no
affinity-accept per-core yes bsd socket syscalls per packet transparent yes
megapipe per-core yes lwsocket batched syscalls per packet event model to completion i/o yes
flexsc,vos shared none bsd socket batched syscalls per packet change to use new api yes
mtcp per-core yes user-level socket batched function calls batched socket api to mtcp api no
(nic driver)
fastsocket per-core yes bsd socket ioctl kernel calls per packet transparent no

有一个大致的印象,也方便对比,但这只能是一个暂时的摘要而已,人类对性能的渴求总是朝着更好的方向发展着。

部署尝试

怎么说呢,fastsocket是为大家耳熟能详服务器程序nginx,haproxy等而开发的。但若应用环境为大量的短连接,并且是小文件类型请求,不需要强制支持keep-alive特性(短连接要的是快速请求-相应,然后关闭),那么管理员可以尝试一下fastsocket,至于部署策略,选择性部署几台作为实验看看结果。

小结

本系列到此算是告一段落啦。以后呢,自然是希望fastsocket尽快发布对长连接的支持,还有更高性能的提升咯 :))

资源引用



nieyong 2015-02-05 15:21
]]>
fastsocket学习笔记之内核篇http://www.blogjava.net/yongboy/archive/2015/02/04/422732.htmlnieyongnieyongwed, 04 feb 2015 06:22:00 gmthttp://www.blogjava.net/yongboy/archive/2015/02/04/422732.htmlhttp://www.blogjava.net/yongboy/comments/422732.htmlhttp://www.blogjava.net/yongboy/archive/2015/02/04/422732.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/422732.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422732.html

前言

前面分析fastsocket慢慢凑成了几篇烂文字,要把一件事情坚持做下来,有时味同爵蜡,但既然选择了,也得硬着头皮做下去。闲话少说,文归正文。本文接自上篇内核模块篇,继续记录学习fastsocket内核的笔记内容。

fastsocket建立在so_reuseport支持基础上

linux kernel 3.9包含tcp/udp支持多进程、多线程绑定同一个ip和端口的特性,即so_reuseport;在内核层面同时也让线程/进程之间各自独享socket,避免cpu核之间以锁资源争夺accept queue的调用。在fastsocket/kernel/net/sock.h定义sock_common结构时,可以看到其身影:

unsigned char          skc_reuse:4;
unsigned char          skc_reuseport:4;

在多个socket.h文件中(比如fastsocket/kernel/include/asm/socket.h),定义了so_reusesort的变量值:

#define so_reuseport     15

在fastsocket/kernel/net/core/sock.c的sock_setsockopt和sock_getsockopt函数中,都有so_reuseport的身影:

sock_setsockopt函数中:

case so_reuseaddr:
  sk->sk_reuse = valbool;
  break;
case so_reuseport:
  sk->sk_reuseport = valbool;
  break;

sock_getsockopt函数体中:

case so_reuseaddr:
  v.val = sk->sk_reuse;
  break;
case so_reuseport:
  v.val = sk->sk_reuseport;
  break;

so_reuseport特性支持之前的事件驱动驱动服务器资源竞争:

之后呢,可以看做是并行的了:

fastsocket没有重复发明轮子,在so_reuseport基础上进行进一步的优化等。

嗯,后面准备写一个动态链接库小程序,打算让以前的没有硬编码so_reuseport的程序也能够在linux kernel >= 3.9系统上享受真正的端口重用的新特性的支持。

fastsocket架构图


下面按照其架构图所示内核层面从上到下一一列出。

虚拟文件系统vfs的改进

因为linux kernel vfs的同步损耗严重

  • vfs对文件节点inode和目录dentry有同步需求
  • 但socket只需要在内存中存在即可,非严格意义上文件系统,其不需要路径,不需要为inode和dentry加锁
  • 代码层面略过不必须的常规锁,但又保持了足够的兼容性

提交记录:

a209dfc vfs: dont chain pipe/anon/socket on superblock s_inodes list
4b93688 fs: improve scalability of pseudo filesystems

对vfs的改进,在所提升的性能中占有超过60%的比例,效果非常明显:

local listen table

对于多核多接收队列来说,linux原生的协议栈只能listen在一个socket上面,并且所有完成三次握手还没来得及被应用accept的套接字都会放入其附带的accept队列中,accept系统调用必须串行的从队列取出,当并发量较大时多核竞争,这将成为性能瓶颈,影响建立连接处理速度。

local listen table,fastsocket为每一个cpu核克隆监听套接字,并保存到其本地表中,cpu核之间不会存在accept的竞争关系。下面为引用描述内容:

  • 每个core有一个listen socket table。应用程序建立连接的时候,执行过程会调用local_listen()函数,有两个参数,一个是socket fd,一个是core number. new socket从原始的listen socket(global)拷贝到per-core local socket table. 这些对于应用程序来说都是透明的,提供给应用程序的socketfd是抽象过的,隐藏了底层的实现。
  • 当一个tcp syn到达本机,kernel首先去local listen table中找匹配的listen socket,如果找到,就通过网卡rss传递这个socket到一个core,否则就去global listen table中找。
  • 容错方面,当进程崩溃的话,local listen socket会被关闭,进入的连接将会被引导到global listen socket, 这样的话,别的process可以处理这些连接。由于local listen socket和global listen socket共享fd,所以kernel将会把新的connet通知到相应的process。
  • 如果应用程序进程使用accept()系统调用,那么处理过程是首先去global listen table中查找和操作(因为是读操作,没有使用锁),如果没有找到,那么去core的local table中查找。如果找到,就返回给应用程序。由于listen的时候把socket绑定到了一个core,所以查找的时候也去这个core的local table中查找。
  • epoll兼容性,如果应用程序使用epoll_ctl()系统调用,来把一个listen socket添加到epoll set中,那么local的listen socket和global的listen socket都被epoll监控。事件发生的时候,epoll_wait()系统调用会返回listen socket,accept()系统调用就会处理这个socket。这样就保证了epoll实现的兼容性。

使用流程图概括上面所述:

local established table

linux内核使用一个全局的hash表以及锁操作来维护establised sockets(被用来跟踪连接的sockets)。fastsocket 想法是把全局table分散到per-core table,当一个core需要访问socket的时候,只在隶属于自己的table中搜索,因此不需要锁操纵,也不存在资源竞争。由fastsocket建立的socket本地local established table中,其他的regular sockets保存在global的table中。core首先去自己的local table中查找(不需要锁),然后去global中查找。

receive flow deliver

默认情况下,应用程序主动发包的时候,发出去的包是通过正在执行本进程的那个cpu 核(系统分配的)来完成的;而接收数据包的时cpu 核是由前面提到的rss或rps来传递。这样一来,连接可能由不同的两个cpu核来完成。连接应该在本地化处理。rfs和intel网卡的flowdirector可以从软件和硬件上缓解这种情况,但是不完备。

rfd(receive flow deliver)主要的思想是cpu核数主动发起连接的时候可以把cpu core的标识和连接的source port编码到一起。cpu cores和ports的关系由一个关系集合来决定【cores,ports】, 对于一个port,有唯一的一个core与之对应。当一个core来建立connection的时候,rfd随机选择一个跟当前core匹配的port。接收包的时候,rfd负责决定这个包应该让哪一个core来处理,如果当前core不是被选中的cpu core,那么就deliver到选中的cpu core。

一般来说,rfd对代理程序收益比较大,单纯的web服务器可以选择禁用。

小结

以上参考了大量的外部资料进行整理而成,进而可以获得一个较为整体的fastsocket内核架构印象。

fastsocket的努力,在单个tcp连接的管理从网卡触发的硬中断、软中断、三次握手、数据传输、四次挥手等完整的过程在完整在一个cpu核上进行处理,从而实现了每一个cpu核心tcp资源本地化,这样为多核水平扩展打好了基础,减少全局资源竞争,平行化处理连接,同时降低文件锁的副作用,做到了极为高效的短连接处理方案,不得不赞啊。

引用资料:

  • fastsocket ppt


nieyong 2015-02-04 14:22
]]>
fastsocket学习笔记之模块篇http://www.blogjava.net/yongboy/archive/2015/02/03/422694.htmlnieyongnieyongtue, 03 feb 2015 05:26:00 gmthttp://www.blogjava.net/yongboy/archive/2015/02/03/422694.htmlhttp://www.blogjava.net/yongboy/comments/422694.htmlhttp://www.blogjava.net/yongboy/archive/2015/02/03/422694.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/422694.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422694.html

前言

本篇学习fastsocket内核模块fastsocket.so,作为用户态libfsocket.so的内核态的支持,处理ioctl传递到/dev/fastsocket的数据,非常核心和基础。嗯,还是先翻译,随后挟带些点评进来。

模块介绍

fastsocket内核模块 (fastsocket.ko) 提供若干特性,并各自具有开启和关闭等丰富选项可配置。

vfs 优化

centos 6.5带来的内核锁竞争处处可见,导致无论如何优化tcp/ip网络堆栈都不能够带来很好的性能扩展。比较严重锁竞争例子,inode_lockdcache_lock,针对套接字文件系统sockfs而言,并不是必须。fastsocket通过在vfs初始化结构时提供fastpath快速路径用以解决此项问题,已经向代号为香草(vanilla)的内核提交了两处修改:

a209dfc vfs: dont chain pipe/anon/socket on superblock s_inodes list
4b93688 fs: improve scalability of pseudo filesystems

此项修改没有提供选项可供配置,因此所有fastsocket创建的套接字sockets都会强制经由fastpath传输。

内核模块参数

enable_listen_spawn

fastsocket为每个cpu创建了一个本地socket监听表(local listen table),应用程序可以决定在一个特定cpu内核上处理某个新的连接,具体就是通过拷贝原始监听套接字socket,然后插入到本地套接字socket监听表中。当新建连接在某cpu处理时,系统内核尝试匹配本地socket监听表,匹配成功会插入到本地accept队列中。稍后,cpu会从本地accept队列中获取进行处理。

这种方式每一个网络软中断都会有隶属于自己本地套接字队列当新的连接进来时可以压入,每一个进程从本地队列中弹出连接进行处理。当进程和cpu进行绑定,一旦有网卡接口决定投递到某个cpu内核上,那么包括硬中断、软中断、系统调用以及用户进程,都会有这个cpu全程负责。好处就是客户端请求连接在没有锁的竞争环境下分散到各个cpu上被动处理本地连接。

本特性更适合以下情况:

  • 尽可能多的网卡rx接收队列和cpu核数
  • 应用程序工作进程被静态绑定到每一个cpu上

第一种情况下,rps可以在网卡接收队列小于cpu核数时被使用。第二种方案可以满足两个方面:

  • 应用程序在启动时自己绑定工作进程和cpu亲和性
  • 允许fastsocket自动为工作进程绑定cpu亲和性

因此,enable_listen_spawn具有三个值可供配置:

  • enable_listen_spawn=0: 彻底禁止
  • enable_listen_spawn=1: 启用,但要求应用程序自己绑定cpu
  • enable_listen_spawn=2 (默认值): 启用此特性,允许fastsocket为每一个工作进程绑定到cpu上

enable_fast_epoll

一旦开启,需要为文件结构额外添加一字段用以保存文件与epitem的映射关系,这样可省去在epoll_ctl方法被调用时从epoll红黑树查找epitem的开销。

虽然此项优化有所修改epoll语义,但带来了套接字性能提升。开启的前提是一个套接字只允许添加到一个epoll实例中,但不包括监听套接字。默认值为true可以适用于绝大多数应用程序,若你的程序不满足条件就得需要禁用了。

enable_fast_epoll 为布尔型boolean选项:

  • enable_fast_epoll=0: 禁用fast-epoll
  • enable_fast_epoll=1 (默认值): 开启fast-epoll

enable_receive_flow_deliver

rfd(receive flow deliver)会把为新建连接分配的cpu id封装到其连接的端口号中,而不是随机选择新创建的主动连接的源端口进行分配到cpu上。

当应用从活动连接收到数据包rfd解码时,会从目的地端口上解析出对应的cpu内核id,继而转发给对应的cpu内核。再加上listen_spawn,保证了一个连接cpu处理的完全本地化。

enable_receive_flow是一个布尔型选项:

  • enable_receive_flow=0 (默认值): 禁用rfd
  • enable_receive_flow=1: 启用rfd

注意事项:

  • 当启用时,在当前的实现,rfd完全覆盖rps策略,并使得rps无效。若使用rps,请禁用此特性
  • 由于rfd只会对诸如代理应用程序有利,我们建议在web服务器上禁用此特性

以上,翻译完毕。

源码简单梳理

fastsocket的内核模块相对路径为fastsocket/module/,除了readme.md外,就是两个软连接文件了:

  • fastsocket.c ../kernel/net/fastsocket/fastsocket.c 真实环境下不存在这个文件,可能是程序bug
  • fastsocket.h ../kernel/net/fastsocket/fastsocket.h 有对应头文件存在

换种说法,fastsocket内核模块真正路径为fastsocket/kernel/net/fastsocket,具体文件列表为:

  • kconfig
  • makefile
  • fastsocket.h 定义内核模块所使用到变量和方法
  • fastsocket_core.c 负责方法实现,供fastsocket_api.c调用
  • fastsocket_api.c 内核模块加载/卸载等操作,处理前端动态链接库经由ioctl传递的数据

fastsocket_api.c实现内核模块接口,在源码里面注册了好多文档暂时没有公开的可配置项目:

int enable_fastsocket_debug = 3;
/* fastsocket feature switches */
int enable_listen_spawn = 2;
int enable_receive_flow_deliver;
int enable_fast_epoll = 1;
int enable_skb_pool;
int enable_rps_framework;
int enable_receive_cpu_selection = 0;
int enable_direct_tcp = 0;
int enable_socket_pool_size = 0;
module_param(enable_fastsocket_debug,int, 0);
module_param(enable_listen_spawn, int, 0);
module_param(enable_receive_flow_deliver, int, 0);
module_param(enable_fast_epoll, int, 0);
module_param(enable_direct_tcp, int, 0);
module_param(enable_skb_pool, int, 0);
module_param(enable_receive_cpu_selection, int, 0);
module_param(enable_socket_pool_size, int, 0);
module_parm_desc(enable_fastsocket_debug, " debug level [default: 3]" );
module_parm_desc(enable_listen_spawn, " control listen-spawn: 0 = disabled, 1 = process affinity required, 2 = autoset process affinity[default]");
module_parm_desc(enable_receive_flow_deliver, " control receive-flow-deliver: 0 = disabled[default], 1 = enabled");
module_parm_desc(enable_fast_epoll, " control fast-epoll: 0 = disabled, 1 = enabled[default]");
module_parm_desc(enable_direct_tcp, " control direct-tcp: 0 = disbale[default], 1 = enabled");
module_parm_desc(enable_skb_pool, " control skb-pool: 0 = disbale[default], 1 = receive skb pool, 2 = send skb pool,  3 = both skb pool");
module_parm_desc(enable_receive_cpu_selection, " control rcs: 0 = disabled[default], 1 = enabled");
module_parm_desc(enable_socket_pool_size, "control socket pool size: 0 = disabled[default], other are the pool size");

接收用户态的libfsocket.so通过ioctl传递过来的数据,根据命令进行数据分发:

static long fastsocket_ioctl(struct file *filp, unsigned int cmd, unsigned long __user u_arg)
{
     struct fsocket_ioctl_arg k_arg;
     if (copy_from_user(&k_arg, (struct fsocket_ioctl_arg *)u_arg, sizeof(k_arg))) {
          eprintk_limit(err, "copy ioctl parameter from user space to kernel failed\n");
          return -efault;
     }
     switch (cmd) {
     case fsocket_ioc_socket:
          return fastsocket_socket(&k_arg);
     case fsocket_ioc_listen:
          return fastsocket_listen(&k_arg);
     case fsocket_ioc_spawn_listen:
          return fastsocket_spawn_listen(&k_arg);
     case fsocket_ioc_accept:
          return fastsocket_accept(&k_arg);
     case fsocket_ioc_close:
          return fastsocket_close(&k_arg);
     case fsocket_ioc_shutdown_listen:
          return fastsocket_shutdown_listen(&k_arg);
     //case fsocket_ioc_epoll_ctl:
     //     return fastsocket_epoll_ctl((struct fsocket_ioctl_arg *)arg);
     default:
          eprintk_limit(err, "ioctl [%d] operation not support\n", cmd);
          break;
     }
     return -einval;
}

fastsocket/library/libsocket.h头文件定义的fsocket_ioc_* 操作状态码就能够一一对应的上。 ioctl传输数据从用户态->内核态,需要经过一次拷贝过程(copy_from_user),然后根据cmd命令进行功能路由。

libfsocket.so如何与fastsocket内核模块交互

通过指定的设备通道/dev/fastsocket进行交互:

  1. fastsocket内核模块注册要监听的通道设备名称为/dev/fastsocket
  2. libfsocket打开/dev/fastsocket设备获得文件句柄,开始ioctl数据传递

小结

简单梳理了fastsocket内核模块,但一样有很多的点没有涉及,后面可能会在fastsocket内核篇中再次梳理一下。



nieyong 2015-02-03 13:26
]]>
fastsocket学习笔记之动态链接库篇http://www.blogjava.net/yongboy/archive/2015/02/02/422658.htmlnieyongnieyongmon, 02 feb 2015 06:16:00 gmthttp://www.blogjava.net/yongboy/archive/2015/02/02/422658.htmlhttp://www.blogjava.net/yongboy/comments/422658.htmlhttp://www.blogjava.net/yongboy/archive/2015/02/02/422658.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/422658.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422658.html

前言

本篇为fastsocket的动态链接库学习笔记,对应源码目录为 fastsocket/library,先翻译readme.md文件内容,后面添加上个人学习心得。

介绍

动态链接库libfsocket.so,为已有应用程序提供加速服务,具有可维护性和兼容性。

  • 可维护性:fastsocket优化在于重新实现套接字的系统调用从而达到linux内核网络堆栈效率的提高。而应用程序是不用修改这些系统调用,借助于fastsocket就可以达到加速的目的。fastsocket在内核模块提供了一个新的ioctl接口,供上层应用程序调用。
  • 兼容性:若让应用程序必须修改其代码以适应新的系统调用接口,在现实世界中这很麻烦也不可行。借助于libfsocket拦截系统调用并提供新的接口进行替换系统调用,同时fastsocket提供了与bsd socket完全兼容的调用接口,这使得应用程序在无需更改任何代码的情况下,可直接使用fastsocket,获得网络加速的效果。

编译

很简单,进入目录之后,执行make命令编译即可:

cd fastsocket/library
make

最后在当前目录下生成libfsocket.so文件。

用法

很简单的说,借助于ld_preload加载libfsocket.so,启动应用程序,以nginx为例:

ld_preload=/your_path/fastsocket/library/libfsocket.so nginx

若回滚,就简单了,直接启动nginx就行:

nginx

注意事项:

  • 确保fastsocket.ko内核模块已经加载成功
  • 只对启动时以预加载libfsocket.so的上层应用程序有效果

内部构件

fastsocket拦截网络套接字的常规系统调用,并使用ioctl接口取代之。

若不依赖于libfsocket.so,上层应用程序要想使用fastsocket percore-listen-table的特点,应用程序需要在父流程forking之后,以及提前做事件循环(event loop)处理,应用工作进程需要手动调用listen_spawn函数,复制全局的监听套接字并插入到本地监听表中。

libfsocket.so为上层应用程序做了listien_spawn的工作,用以保持应用程序的代码不变,方法如下:

  • libfsocket.so跟踪所有需要监听的套接字文件句柄
  • libfsocket.so拦截了epoll_ctl系统调用
  • 当监听到应用程序调用epoll_ctl添加监听套接字文件句柄到epoll时,libfsocket.so会调用listen_spawn方法

不是所有应用程序都适合本方案,但nginx、haproxy、lighttpd与之配合就工作得相当不错。因此当你在其他应用程序中想使用percore-listen-table特性时,请务必小心测试了,确保是否合适。

ok,翻译完毕。

源码一览

fastsocket/library用于构建libfsocket.so动态链接库,主要组成:

  • makefile 编译脚本
  • libsocket.h 头文件,定义变量、结构等
  • libsocket.c 动态链接库实现

libsocket.h

定义了ioctl(为input/output control缩写)函数和伪设备(/dev/fastsocket)交换数据所使用到的几个命令:

#define ioc_id 0xf5
#define fsocket_ioc_socket _io(ioc_id, 0x01)
#define fsocket_ioc_listen _io(ioc_id, 0x02)
#define fsocket_ioc_accept _io(ioc_id, 0x03)
#define fsocket_ioc_close _io(ioc_id, 0x04)
//#define fsocket_ioc_epoll_ctl _io(ioc_id, 0x05)
#define fsocket_ioc_spawn_listen _io(ioc_id, 0x06)
#define fsocket_ioc_shutdown_listen _io(ioc_id, 0x07)

紧接着定义了需要在用户态和内核态通过ioctl进行交互的结构:

struct fsocket_ioctl_arg {
     u32 fd;
     u32 backlog;
     union ops_arg {
          struct socket_accept_op_t {
               void *sockaddr;
               int *sockaddr_len;
               int flags;
          }accept_op;
          struct spawn_op_t {
               int cpu;
          }spawn_op;
          struct io_op_t {
               char *buf;
               u32 buf_len;
          }io_op;
          struct socket_op_t {
               u32 family;
               u32 type;
               u32 protocol;
          }socket_op;
          struct shutdown_op_t {
               int how;
          }shutdown_op;
          struct epoll_op_t {
               u32 epoll_fd;
               u32 size;
               u32 ep_ctl_cmd;
               u32 time_out;
               struct epoll_event *ev;
          }epoll_op;
     }op;
};

这样看来,ioctl函数原型调用为:

 ioctl(/dev/fastsocket设备文件句柄, fsocket_ioc_具体宏命令, fsocket_ioctl_arg结构指针)

现在大致能够弄清楚了内核态和用户态之间通过ioctl传递结构化的数据的方式了。

libsocket.c 简要分析

连接内核模块已经注册好的设备管道/dev/fastsocket,获取到文件描述符,同时做些cpu进程绑定的工作

#define init_fdset_num     65536
......
__attribute__((constructor))
void fastsocket_init(void)
{
     int ret = 0;
     int i;
     cpu_set_t cmask;
     ret = open("/dev/fastsocket", o_rdonly); // 建立fastsocket通道
     if (ret < 0) {
          fsocket_err("open fastsocket channel failed, please check\n");
          /* just exit for safty*/
          exit(-1);
     }
     fsocket_channel_fd = ret;
     fsocket_fd_set = calloc(init_fdset_num, sizeof(int));
     if (!fsocket_fd_set) {
          fsocket_err("allocate memory for listen fd set failed\n");
          exit(-1);
     }
     fsocket_fd_num = init_fdset_num; // 值为65535
     cpu_zero(&cmask);
     for (i = 0; i < get_cpus(); i  )
          cpu_set(i, &cmask);
     ret = sched_setaffinity(0, get_cpus(), &cmask);
     if (ret < 0) {
          fsocket_err("clear process cpu affinity failed\n");
          exit(-1);
     }
     return;
}

主观上,仅仅是为了短连接而设置的,定义的fastsocket文件句柄数组大小为65535,针对类似于web server、http api等环境足够了,针对百万级别的长连接服务器环境就不适合了。

socket/listen/accept/close/shutdown/epoll_ctl等函数,通过dlsym方式替换已有套接字系统函数等,具体的交互过程使用ioctl替代一些系统调用。

除了重写socket/listen/accept/close/shutdown等套接字接口,同时也对epoll_ctl方法动了手术(江湖传言cpu多核多进程的epoll服务器存在惊群现象),更好利用多核:

int epoll_ctl(int efd, int cmd, int fd, struct epoll_event *ev)
{
     static int (*real_epoll_ctl)(int, int, int, struct epoll_event *) = null;
     int ret;
     struct fsocket_ioctl_arg arg;
     if (fsocket_channel_fd >= 0) {
          arg.fd = fd;
          arg.op.spawn_op.cpu = -1;
          /* "automatically" do the spawn */
          if (fsocket_fd_set[fd] && cmd == epoll_ctl_add) {
               ret = ioctl(fsocket_channel_fd, fsocket_ioc_spawn_listen, &arg);
               if (ret < 0) {
                    fsocket_err("fsocket: spawn failed!\n");
               }
          }
     }
     if (!real_epoll_ctl)
          real_epoll_ctl = dlsym(rtld_next, "epoll_ctl");
     ret = real_epoll_ctl(efd, cmd, fd, ev);
     return ret;
}

因为定义了作用于内部的静态变量real_epoll_ctl,只有在第一次加载的时候才会被赋值,real_epoll_ctl = dlsym(rtld_next, "epoll_ctl"),后面调用时通过ioctl把fsocket_ioctl_arg传递到内核模块中去。

其它socket/listen/accept/close/shutdown等套接字接口,流程类似。

小结

以上简单翻译、粗略分析用户态fastsocket动态链接库大致情况,若要起作用,需要和内核态fastsocket进行交互、传递数据才能够作用的很好。



nieyong 2015-02-02 14:16
]]>
fastsocket学习笔记之网卡设置篇http://www.blogjava.net/yongboy/archive/2015/01/30/422592.htmlnieyongnieyongfri, 30 jan 2015 08:49:00 gmthttp://www.blogjava.net/yongboy/archive/2015/01/30/422592.htmlhttp://www.blogjava.net/yongboy/comments/422592.htmlhttp://www.blogjava.net/yongboy/archive/2015/01/30/422592.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/422592.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422592.html

前言

前面编译安装好了包含有fastsocket的内核模块,以及fastsocket的动态链接库libfsocket.so,下面其实就可以设置网卡了。

下面为一些名词解释,上下文中需要使用到:

  • rx:接收队列
  • tx:发送队列

本文网卡设置笔记内容,大部分来自于fastsocket源码相对路径fastsocket/scripts/;老规矩,先翻译。

网卡设置篇翻译原文

介绍

nic.sh脚本负责网卡配置以尽可能的最大化受益于fastsocket带来的问题。给定一个网卡接口, 它调整接口的各种特性以及一些系统配置。

相关配置

中断和cpu的亲和性

每个网卡硬件队列及其关联中断绑定到不同的cpu核心。若硬件队列数大于cpu核数,队列需要配置成循环round-robin方式, irqbalance服务需要被禁用以防其更改配置。

中断阀速率

nic.sh脚本通过ethtool命令设置每秒中断数上限,防止中断风暴。两个rx中断间隔设置成至少333us,约3000个中断每秒。

rps

为每个cpu核心与不同的网卡硬件队列之间建立一一映射对应关系,这样cpu核心就可以很均匀地处理网络数据包。当网卡硬件队列小于cpu内核数,nic.sh脚本利用rps (receive packet steering)软件方式平衡进入流量负载,这样cpu和硬件队列不存在对应关系。rps机制可以让进入的数据包自由分发到任一cpu核上。

网卡接收产生的中断可以均衡分配到对应cpu上。

xps

xps (transmit packet steering) 建立cpu内核和tx发送队列映射对应关系,掌控出站数据包。系统有n个cpu核心,脚本会设置xps至少存在n个tx队列在网卡接口上,这样就可以建立cpu内核和tx队列1对1的映射关系。

网卡传送数据产生的中断一样可以均很分配到cpu上,避免单个cpu核心过于繁忙。

iptables

压测时,防火墙iptables的规则会占用更多的cpu周期,有所降低网络堆栈性能。因此nic.sh脚本若检测到iptables后台运行中会直接输出报警信息,提示关闭之。

nic.sh脚本脚本分析

经过验证好用的intel和博通系列千兆和万兆网卡列表:

# igb
"intel corporation 82576 gigabit network connection (rev 01)"
"intel corporation i350 gigabit network connection (rev 01)"
# ixgbe
"intel corporation 82599eb 10-gigabit sfi/sfp  network connection (rev 01)"
"intel corporation 82599es 10-gigabit sfi/sfp  network connection (rev 01)"
# tg3
"broadcom corporation netxtreme bcm5720 gigabit ethernet pcie"
"broadcom corporation netxtreme bcm5761 gigabit ethernet pcie (rev 10)"
# bnx2
"broadcom corporation netxtreme ii bcm5708 gigabit ethernet (rev 12)"
"broadcom corporation netxtreme ii bcm5709 gigabit ethernet (rev 20)"

若当前服务器没有以上网卡,会警告一下,无碍。

这里把一些常规性的cpu、网卡驱动、网络队列情况检查单独抽取出来,重温好多已经遗忘的命令,有改变,这样写较简单嘛,便于以后使用:

  • 直接查看cpu核数:grep -c processor /proc/cpuinfo
  • 查看网卡软接收队列数:ls /sys/class/net/eth0/queues | grep -c rx
  • 查看网卡软发生队列数:ls /sys/class/net/eth0/queues | grep -c tx
  • 查看当前网卡硬件队列数:egrep -c eth0 /proc/interrupts
  • 查看网卡名称和版本号:lspci | grep ethernet | sed "s/ethernet controller: //g"
  • 查看网卡驱动名称:ethtool -i eth0 | grep driver

脚本先是获取cpu、网卡等信息,接着设置中断单位秒内吞吐量: ethtool -c eth0 rx-usecs 333 > /dev/null 2>&1

启用xps,充分借助网卡发送队列,提升网卡发送吞吐量,是有条件限制的,发送队列数要大于cpu核数:

if [[ $tx_queues -ge $cores ]]; then
    for i in $(seq 0 $((cores-1))); do
     cpuid_to_mask $((i%cores)) | xargs -i echo {} > /sys/class/net/$iface/queues/tx-$i/xps_cpus
    done
    info_msg "    xps enabled"
fi

接着判断是否可以启用prs,省去手动设置的麻烦,但启用rps前提是cpu核数与网卡硬件队列不相等:

if [[ ! $hw_queues == $cores ]]; then
    for i in /sys/class/net/$iface/queues/rx-*; do
     printf "%x\n" $((2**cores-1)) | xargs -i echo {} > $i/rps_cpus;
    done
    info_msg "    rps enabled"
else
    for i in /sys/class/net/$iface/queues/rx-*; do
     echo 0 > $i/rps_cpus;
    done
    info_msg "    rps disabled"
fi

若没有使用fastsocket,单纯借助于rps,会带来处理中断的cpu和处理当前数据包的cpu不是同一个,自然会造成cpu cache miss(cpu缓存丢失),造成少许的性能影响,为了避免这种情况,人们会依赖于rfs(receive flow steering)。

使用了fastsocket后,就不用这么麻烦了。

irqbalance和fastsocket有冲突,会强制禁用:

if ps aux | grep irqbalance | grep -v grep; then
    info_msg "disable irqbalance..."
    # xxx do we have a more moderate way to do this?
    killall irqbalance > /dev/null 2>&1
fi

脚本也包含了设置中断和cpu的亲和性:

i=0
intr_list $iface $driver | while read irq; do
    cpuid_to_mask $((i%cores)) | xargs -i echo {} > /proc/irq/$irq/smp_affinity
    i=$((i 1))
done

若iptables服务存在,会友善建议禁用会好一些,毕竟会带来性能损耗。文件打开句柄不大于1024,脚本同样会提醒,怎么设置文件打开句柄,可以参考以前博文。

linux系统网络堆栈的常规扩展优化措施

针对不使用fastsocket的服务器,当前比较流行的针对网卡的网络堆栈性能扩展、优化措施,一般会使用到rss、rps、rfs、xfs等方式,以便充分利用cpu多核和硬件网卡等自身性能,达到并行/并发处理的目的。下面总结一个表格,可以凑合看一下。


rss
(receive side scaling)
rps
(receive packet steering)
rfs
(receive flow steering)
accelerated rfs
(accelerated receive flow steering)
xps
(transmit packet steering)
解决问题 网卡和驱动支持 软件方式实现rss 数据包产生的中断和应用处理在同一个cpu上 基于rfs硬件加速的负载平衡机制 智能选择网卡多队列的队列快速发包
内核支持 2.6.36开始引入,需要硬件支持 2.6.35 2.6.35 2.6.35 2.6.38
建议 网卡队列数和物理核数一直 至此多队列的网卡若rss已经配置了,则不需要rps了 需要rps_sock_flow_entries和rps_flow_cnt属性 需要网卡设备和驱动都支持加速。并且要求ntuple过滤已经通过ethtool启用 单传输队列的网卡无效,若队列比cpu少,共享指定队列的cpu最好是与处理传输硬中断的cpu共享缓存的cpu
fastsocket 网卡特性 改进版rps,性能提升 源码包含,文档没有涉及 文档没有涉及 要求发送队列数要大于cpu核数
传送方向 网卡接收 内核接收 cpu接收处理 加速并接收 网卡发送数据

更具体优化措施,可以参考文档:。

另,若网卡支持flow director filters特性(这里有一个非常有趣的动画介绍,,值得一看),那么可以结合fastsocket一起加速。比如,在其所作redis长连接测试中,启用flow-director特性要比禁用可以带来25%的性能提升。

自然软硬结合,可以做的更好一些嘛。

延伸阅读:

小结

以上记录了学习fastsocket的网卡设置脚本方面笔记。

不过呢,nic.sh脚本,值得收藏,无论使不使用fastsocket,对线上服务器网卡调优都是不错选择哦。



nieyong 2015-01-30 16:49
]]>
fastsocket学习笔记之安装篇http://www.blogjava.net/yongboy/archive/2015/01/30/422579.htmlnieyongnieyongfri, 30 jan 2015 05:14:00 gmthttp://www.blogjava.net/yongboy/archive/2015/01/30/422579.htmlhttp://www.blogjava.net/yongboy/comments/422579.htmlhttp://www.blogjava.net/yongboy/archive/2015/01/30/422579.html#feedback2http://www.blogjava.net/yongboy/comments/commentrss/422579.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422579.html

前言

运行环境为centos 6.5系统,默认内核为2.6.32-431.el6.x86_64,下面所有编译安装操作是以root用户权限进行操作。

编译安装fastsocket内核

第一步需要下载代码,当然这是废话了,下载到/opt目录下:

 git clone https://github.com/fastos/fastsocket.git

编译安装

下载之后,需要进入其目录中:

 cd fastsocket/kernel

因为是涉及到内核嘛,编译之前需要做一些参数选项配置,使用make config会累死人的,好几千个选项参数需要你一一配置,大部分时间,默认配置就挺好的:

 make defconfig

然后嘛,编译内核的节奏:

 make

内核编译相当耗费时间,至少20分钟时间。之后紧接着是编译所需的内核模块,fastsocket模块:

 make modules_install

编译完成之后,最后一条输出,会看到:

depmod 2.6.32-431.17.1.el6.fastsocket

fastsocket内核模块编译好之后,需要安装内核:

 make install

上面命令其实执行shell脚本进行安装:

sh /opt/fastsocket/kernel/arch/x86/boot/install.sh 2.6.32-431.17.1.el6.fastsocket arch/x86/boot/bzimage \ system.map "/boot"

基本上,fastsocket内核模块已经构建安装完毕了,但需要告知linux系统在下次启动的时候切换到新编译的、包含有fastsocket模块的内核。

配置启动时需要切换的内核

这部分需要在/etc/grup.conf中配置,现在看一下其文件内容:

default=1
timeout=5
splashimage=(hd0,0)/grub/splash.xpm.gz
hiddenmenu
title centos (2.6.32-431.17.1.el6.fastsocket)
        root (hd0,0)
        kernel /vmlinuz-2.6.32-431.17.1.el6.fastsocket ro root=/dev/mapper/vg_centos6-lv_root rd_no_luks rd_no_md rd_lvm_lv=vg_centos6/lv_swap crashkernel=auto lang=zh_cn.utf-8 rd_lvm_lv=vg_centos6/lv_root  keyboardtype=pc keytable=us rd_no_dm rhgb quiet
        initrd /initramfs-2.6.32-431.17.1.el6.fastsocket.img
title centos (2.6.32-431.el6.x86_64)
        root (hd0,0)
        kernel /vmlinuz-2.6.32-431.el6.x86_64 ro root=/dev/mapper/vg_centos6-lv_root rd_no_luks rd_no_md rd_lvm_lv=vg_centos6/lv_swap crashkernel=auto lang=zh_cn.utf-8 rd_lvm_lv=vg_centos6/lv_root  keyboardtype=pc keytable=us rd_no_dm rhgb quiet
        initrd /initramfs-2.6.32-431.el6.x86_64.img

defautl=1,表示目前系统选择的以原先内核作作为启动项,原先位于第二个root (hd0,0)后面,需要切换到新的内核下面,需要修改default=0,保存后,reboot重启系统,使之生效。

检测生效

系统重启后,需要加载fastsocket模块到系统运行中去,下面以默认选项参数方式加载:

modprobe fastsocket

加载之后,列出当前系统所加载模块列表,检查是否成功

lsmod | grep fastsocket

若能看到类似输出信息,表示ok:

fastsocket 39766 0

开始构建libfastsocket.so链接库文件

上面内核模块安装好之后,可以构建fastsocket的动态链接库文件了:

cd /opt/fastsocket/library/
make

可能会收到一些警告信息,无碍:

gcc -g -shared -ldl -fpic libsocket.c -o libfsocket.so -wall
libsocket.c: 在函数‘fastsocket_init’中:
libsocket.c:59: 警告:隐式声明函数‘open’
libsocket.c: 在函数‘fastsocket_expand_fdset’中:
libsocket.c:109: 警告:隐式声明函数‘ioctl’
libsocket.c: 在函数‘accept’中:
libsocket.c:186: 警告:对指针赋值时目标与指针符号不一致
libsocket.c: 在函数‘accept4’中:
libsocket.c:214: 警告:对指针赋值时目标与指针符号不一致

最后,可以看到gcc编译之后生成的libfsocket.so库文件,说明编译成功。

小结

ok,编译安装到此结束,后面就是如何使用fastsocket的示范程序进行测试了。



nieyong 2015-01-30 13:14
]]>
fastsocket学习笔记之示范应用篇http://www.blogjava.net/yongboy/archive/2015/01/29/422550.htmlnieyongnieyongthu, 29 jan 2015 09:16:00 gmthttp://www.blogjava.net/yongboy/archive/2015/01/29/422550.htmlhttp://www.blogjava.net/yongboy/comments/422550.htmlhttp://www.blogjava.net/yongboy/archive/2015/01/29/422550.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/422550.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422550.html

前言

上篇介绍了如何构建安装fastsocket内核模块,下面将基于fastsocket/demo/readme.md文件翻译整理而成。

嗯,下面进入翻译篇。

介绍

示范为一个简单tcp server服务器程序,用于基准测试和剖析liunx内核网络堆栈性能表现,当然也是为了演示fastsocket可扩展和其性能改进。

示范应用基于epoll模型和非阻塞性io,处理网络连接,但只有在多核的模式下才能够工作得很好:程序的每一个进程被绑定到cpu的不同核,起始于cpu core 0,各自独立处理客户端连接请求。

示范程序具有两种工作模式:

  • 服务器模式:任何请求都会直接返回http 200 ok
  • 代理模式:服务器接收到客户端请求,转发给后端服务器,同时转发后端响应给客户端。

这是一个简单傻瓜形式的tcp server,仅仅用于测试使用,使用时要求客户端和服务器端只能够携带一个packet包大小的数据,否则程序会处理不了。

构建

以下面方式进行构建:

cd demo && make

用法

最简单方式以默认配置无参数形式运行:

./server

参数如下:

  • -w worker_num: 定义进程数.
    • 默认值为当前可用cpu核心数个进程.
  • -c start_core: 指定进程绑定cpu核的开始索引值
    • 默认值为 0.
  • -o log_file: 定义日志文件名称
    • 默认值为 ./demo.log
  • -a listen_address: 指定监听地址,[ip:port]字符串组合形式,支持添加多个地址
    • 默认值为 0.0.0.0:80
  • -x backend_address: 启动代理模式,需要填写[ip:port]组合形式地址,支持多个代理地址
    • 默认不开启
  • -v: 启用详细统计数据输出
    • 默认为禁用
  • -d: 启动debug调试模式,调试信息被写入日志文件中
    • 默认禁用
  • -k: 启用http keepalive机制,当前只能够工作在服务器模式下
    • 默认被禁用

实例

在运行之前,需要注意两点:

  • 为了跑满cpu,需要确保客户端和后端服务器都不应该成为瓶颈,两种可行方案:
    • 提供足够多机器用以充当客户端和后端服务器角色
    • 或在一台机器上充当客户端和后端服务器,使用fastsocket(推荐方案,较为节省服务器)
  • 正确配置网卡,若不知道如何做,可以参考源码中script目录

服务器模式示范

服务器模式至少需要两台主机:

  • 主机a作为客户端产生http请求
  • 主机b为web服务器

设定每台主机cpu 12核,网络大概设置如下:

 --------------------     --------------------
 |       host a       |     |      host b        |
 |                    |     |                    |
 |    10.0.0.1/24     |-----|    10.0.0.2/24     |
 |                    |     |                    |
 --------------------     --------------------

下面是运行两台主机的步骤:

主机b

  • web服务器模式单独运行,开启12个工作进程,和cpu核心数一致:

    ./server -w 12 -a 10.0.0.2:80

  • 或者测试借助于fastsocket所带来的性能

    ld_preload=../library/libfsocket.so ./server -w 12 -a 10.0.0.2:80

主机a

  • 运行apache ab程序作为请求者
    ab -n 1000000 -c 100 http://10.0.0.2:80/
  • 单个apache ab程序不能够体现服务器负载能力,多个ab实例同时并发运行可能会好很多,开12个实例和cpu核心数一致: n=12; for i in $(seq 1 $n); do ab -n 1000000 -c 100 http://10.0.0.2:80/ > /dev/null 2>&1; done

代理模式示范

代理模式下,需要三台机器:

  • 主机a作为客户端产生http请求
  • 主机b作为代理角色
  • 主机c则需要后端服务器

设定每台机器cpu内核数12,网络结构如下:

 --------------------     --------------------     --------------------
 |       host a       |     |       host b       |     |       host c       |
 |                    |     |                    |     |                    |
 |    10.0.0.1/24     |     |    10.0.0.2/24     |     |     10.0.0.3/24    |
 -------------------     -------------------     -------------------
           |                          |                           |
 -----------------------------------------------------------------------
 |                                 switch                                   |
 --------------------------------------------------------------------------

下面为具体的运行步骤:

主机b

  • 为代理服务器启动12个进程
    ./server -w 12 -a 10.0.0.2:80 -x 10.0.0.3:80
  • 或者以fastsocket方式启动 ld_preload=../library/libsocket.so ./server -w 12 -a 10.0.0.2:80 -x 10.0.0.3:80

主机c

  • 理论上任何web服务器都可以充当后端服务器,这里充分利用示范程序好了:
    ./server -w 12 -a 10.0.0.3:80

主机a

  • 作为客户端请求生成器,同样启动12个apache ab实例:
    n=12; for i in $(seq 1 $n); do ab -n 1000000 -c 100 http://10.0.0.2:80/ > /dev/null 2>&1; done

动手实践

以上翻译完毕,下面将是根据上面内容进行动手测试描述吧。

安装apache ab命令

检查一下包含apache ab命令的软件包:

yum provides /usr/bin/ab

可以看到类似于如下字样:

httpd-tools-2.2.15-39.el6.centos.x86_64 : tools for use with the apache http server

安装它就可以了

yum install httpd-tools

虚拟机测试

windows 7专业版跑vmware workstation 10.04虚拟机,两个centos 6.5系统,配置一致,2g内存,8个cpu逻辑处理器核心。

客户端安装apache ab命令测试,跑8个实例: for i in $(seq 1 8); do ab -n 10000 -c 100 http://192.168.192.16:80/ > /dev/null 2>&1; done

服务器端,分别记录:
/opt/fast/server -w 8 ld_preload=../library/libfsocket.so ./server -w 8

服务器模式对比

两组数据对比:

运行方式 处理消耗时间(秒) 处理总数 平均每秒处理数 最大值
单独运行 34s 80270 2361 2674
加载fasocket 28s 80399 2871 2964

代理模式数据

测试方式如上,三台服务器(测试端 代理端 服务器端)配置一样。第一次代理单独启动,第二次代理预加载fastsocket方式。

运行方式 处理消耗时间(秒) 处理总数 平均每秒处理数 最大值
第一次测试后端 44s 80189 1822 2150
第一次测试代理 44s 80189 1822 2152
第二次测试后端 42s 80051 1906 2188
第二次测试代理 42s 80051 1906 2167

备注:虚拟机上数据,不代表真实服务器上数据,仅供参考。

虽然基于虚拟机,测试环境受限,但一样可以看到基于fastsocket服务器模型,处理性能有所提升:总体处理时间,每秒平均处理数,以及处理上限等。

关于ld_preload注意事项

动态链接预先加载ld_preload虽是利器,但不是万能药,ld_preload遇到下面情况会失效:

  • 静态链接使用gcc -static参数把libc.so.6静态链入执行程序中
  • 设置执行文件的suid权限,可能也会导致ld_preload失效(如:chmod 4755 daemon)

情况很复杂,小心为上。

小结

学习并测试了fastsocket的源码示范部分,前后对比可以看到fastsocket带来了处理性能的提升。



nieyong 2015-01-29 17:16
]]>
fastsocket学习笔记之开篇http://www.blogjava.net/yongboy/archive/2015/01/29/422536.htmlnieyongnieyongthu, 29 jan 2015 06:11:00 gmthttp://www.blogjava.net/yongboy/archive/2015/01/29/422536.htmlhttp://www.blogjava.net/yongboy/comments/422536.htmlhttp://www.blogjava.net/yongboy/archive/2015/01/29/422536.html#feedback3http://www.blogjava.net/yongboy/comments/commentrss/422536.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/422536.html

前言

以前在infoq上看到fastsocket的宣传,明白了fastsocket是什么:

  • 高度可扩展的socket
  • 是linux内核层面的底层网络实现
  • 在多核机器上可实现极佳性能,24核以内的性能增长呈线性,远超过默认内核在12核以上的机器就会出现性能下降的情况
  • 非常容易使用和维护,应用代码无需变更
  • 针对kernel-2.6.32-431.17.1.el6/centos-6.5的实现
  • 已经在新浪的生产环境部署
  • 由新浪的操作系统团队发起
  • 清华大学操作系统实验室、intel、哲思自由软件社区(zeuux)对该项目均有支持
  • 开源协议为gplv2

    总之很吸引人,从内核层面进行优化tcp/ip网络堆栈,上层网络应用程序不用做修改,就可以得到处理性能的提升,很赞!

fastsocket学习笔记目录

近期有点小空闲,开始对fastsocket进行关注,虽然资料不多,但也记录了几篇连续的学习笔记。大部分笔记,思路主要是优先翻译官方文档,紧接着会夹带些个人一些学习笔记。

fastsocket项目地址是:,其wiki和代码是本系列笔记主要来源。一开始想进一步全面认知fastsocket,发现无从下手,只能从侧面开始一一旁敲侧击,逐渐加深。本系列笔记根据其源码目录结构划分特性,分开记录学习:

  • ,对应demo目录
  • ,对应scripts目录
  • ,对应library目录
  • ,对应module目录,实际上是kernel/net/fastsocket目录
  • ,对应kernel目录,也是内核模块篇

怎么说呢,能力有限,若发现问题/纰漏,请帮忙及时指正,不胜感激。

其它

代码贡献者,除了之外,目前提交最为频繁的是同学,其博客地址为,也是一位牛人。

优秀的开源项目,总是可以吸引到最优秀的开发者。



nieyong 2015-01-29 14:11
]]>
从网络游戏中学习如何处理延迟http://www.blogjava.net/yongboy/archive/2014/12/23/421672.htmlnieyongnieyongtue, 23 dec 2014 02:02:00 gmthttp://www.blogjava.net/yongboy/archive/2014/12/23/421672.htmlhttp://www.blogjava.net/yongboy/comments/421672.htmlhttp://www.blogjava.net/yongboy/archive/2014/12/23/421672.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/421672.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/421672.html前言

网络延迟是客观存在的,但网络游戏行业已经积累了大量优质经验,使用一些策略、技术手段在客户端消除/隐藏掉延迟带来的不便,以尽可能的掩盖实际存在的延时,同时实现实时渲染,将用户带入快速的交互式实时游戏中,体验完美的互动娱乐中。

这样处理结果,稍高延迟的玩家也不会因为网络不是那么好,也能够很和谐的与其它网络参差不及玩家一起游戏中。

虽然延时决定了实时游戏的最低反应时间,但最重要的是客户端看起来要流畅。第一人称设计游戏(fps)可巧妙的化解与规避,最终在适合普遍用户网络环境中(200ms),实现实时快速互动游戏。

嗯,下面就是近期脑补结果。

网游p2p & cs结构

早先网游使用p2p网络拓扑在玩家之间进行交换数据通信。但p2p模型引起的高延迟在fps游戏中无法被很好掩盖,所有玩家的延迟取决于当前玩家中延迟最烂的那个。好比木桶理论,低延迟网络好的玩家会被高延迟坏网络的玩家拖累。最终结果导致,所有玩家都不太开心了。但在局域网环境下,不会感觉到延迟带来的问题。另,游戏逻辑大部分都集中在客户端了,很难避免作弊行为。

c/s结构网游:

  • c/s结构在服务器端跑所有的游戏逻辑和输入响应,客户端只需要渲染以及把自己需要一些状态同步下来,把用户输入发给服务器端,然后显示结果就可以了
  • c/s结构网游最大优点就是把延迟从玩家之间最卡玩家的延迟改变为玩家和服务器连接的延迟,结果就是客户端在带宽上的要求也低了不少,因为只需要把输入发给服务器端就以及接收服务器响应就够了
  • c/s结构网游虽然转移了网络延迟矛盾点,但现实网络环境一样会带来较高的网络延迟。客户端每执行一次操作,都需要等待服务器端命令,那会用户操作会造成操纵卡顿现象。如何解决呢,客户端一般采用预测和插值等方式在渲染层隐藏网络延迟

客户端预测和插值

服务器可以允许某些情况下客户端本地即时执行移动操作,这种方法可以称为客户端预测。

比如游戏中键盘控制角色行走,这个时候可以在很小的时间段(时间很短,比如1-3秒)内预测用户行动轨迹(方向 加速度,角色行走结果),这部分的命令客户端会全部发送到服务器端校验正确与否(避免瞬间转移等外挂)。但客户端预测有时也不是百分百准确,需要服务器进行纠正(所谓服务器就是上帝,the sever is the man!)。纠正结果可能就是游戏角色行走轨迹和客户端预测轨迹有所偏差,客户端可以使用插值方式(粗略来讲,就是角色在两点之间移动渲染的方式)渲染游戏角色在游戏世界中的位置转移平滑一些,避免游戏角色从一个位置瞬间拉回到另一个位置,让人有些莫名其妙。

插值,有人也称之为路径补偿,都是一回事。插值的方法会涉及到很多数学公式,线性插值、三次线性插值等,比如这篇文章所讲到的。

小结:客户端预测,服务器端纠正,客户端采用插值方式微调。

针对交互的一群玩家,网络好坏层次不齐,游戏的一些操作效果可能需要”延迟补偿“策略进行

延迟补偿

延迟补偿是游戏服务器端执行的一种策略,处理用户命令回退到客户端发送命令的准确时间(延迟导致),根据客户端的具体情况进行修正,以牺牲游戏在伤害判定方面的真实感来弥补攻击行为等方面真实感,本质上是一种折中选择。

主要注意,延迟补偿不是发生在客户端。

关于延迟补偿的一个例子:

  1. 在fps游戏中,玩家a在10.5秒时向目标对象玩家b射击并且击中,射击信息被打包发送(网络延迟100毫秒),服务器于10.6秒收到,此时玩家b可能已跑到另外一个位置。
  2. 若服务器仅仅基于接收时刻(10.6秒)进行判断,那么玩家b没有收到伤害,或许可能会击中玩家b后面紧跟的玩家c(100ms后玩家c完全由可能已处于玩家a的射击目标位置)
  3. 为了弥补由于延迟造成的问题,服务器端需要引入“延迟补偿”策略用于修正因延迟造成错乱假象
  4. 服务器计算执行设计命令时间,然后找出当前世界10.5秒时刻玩家信息,根据射击算法模拟得出是否命中判断,以达到尽可能精确

若游戏延迟补偿被禁用,那么就会有许多玩家抱怨自己明明打中了对方却没有造成任何伤害。。

有所得,有所失:但这对低延时玩家貌似有些不公平,移动速度快,可能已经跑到角落里并且已蹲在一个箱子后面隐藏起来时被对手击中的错觉(子弹无视掩体,玩家隔着墙被射击),确实有些不乐意。

延迟补偿,网络高延迟的玩家有利,低延迟的玩家优势可能会被降低(低延迟玩家利益受损),但对维护游戏世界的平衡还是有利的。

对时&阀值

客户端和服务器需要对时,互相知道彼此延迟情况,比如云风定义的某个步骤:

客户端发送一个本地时间量给服务器,服务收到包后,夹带一个服务器时间返回给客户端。当客户端收到这个包后,可以估算出包在路程上经过的时间。同时把本地新时间夹带进去,再次发送给服务器。服务器也可以进一步的了解响应时间。

c/s两端通过类似步骤进行计算彼此延时/时差,同时会对实时同步设置一个阀值,比如对延迟低于10ms(0.01秒)的交互认为是即时同步发生,不会认为是延迟。

udp或tcp

不同类型的游戏会钟爱不同的协议呢,不一而足:

  • 客户端间歇性的发起无状态的查询,并且偶尔发生延迟是可以容忍,那么使用http/https吧
  • 客户端和服务器都可以独立发包,偶尔发生延迟可以容忍(比如:在线的纸牌游戏,许多mmo类的游戏),那么使用tcp长连接吧
  • 客户端和服务器都可以独立发包,而且无法忍受延迟(比如:大多数的多人fps动作类游戏quake、cs等,以及一些mmo类游戏),那么使用udp吧

tcp会认定丢包是因为本地带宽不足导致(本地带宽不足是丢包的一部分原因),但国内isp可能会在自身机房网络拥挤时丢弃数据包,这时候可能需要快速发包争抢通道,而非tcp窗口收缩,udp没有tcp窗口收缩的负担,可以很容易做到这一点。

要求实时性放在第一位的fps游戏(eg:quake,cs),广域网一般采用udp,因可容许有丢失数据包存在(另客户端若等待一段时间中间丢包,可以通过插值等手段忽略掉),一旦检测到可以快速发送,另不涉及到重发的时候udp比tcp要快一点嘛。但会在udp应用层面有所增加协议控制,比如ack等。

很多时候协议混用,比如mmo客户端也许首先使用http去获取上一次的更新内容, 重要信息如角色获得的物品和经验需要通过tcp传输,而周围人物的动向、npc移动、技能动画指令等则可以使用udp传输,虽然可能丢包,但影响不大。

小结

网游通过客户端预测、插值和服务器端延迟补贴等,化解/消除用户端网络延迟造成的停顿。我们虽然可能没有机会接触游戏开发,学习跨界的优良经验和实践,说不准会对当前工作某些业务点的处理有所启发呢。

本集由韩国宇航局赞助播出:我们要去远方看看,还有什么是我们的思密达。 ------ 《万万没想到》王大锤



nieyong 2014-12-23 10:02
]]>
随手记之android网络调试简要记录http://www.blogjava.net/yongboy/archive/2014/11/20/420371.htmlnieyongnieyongthu, 20 nov 2014 14:05:00 gmthttp://www.blogjava.net/yongboy/archive/2014/11/20/420371.htmlhttp://www.blogjava.net/yongboy/comments/420371.htmlhttp://www.blogjava.net/yongboy/archive/2014/11/20/420371.html#feedback2http://www.blogjava.net/yongboy/comments/commentrss/420371.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/420371.html最近一段时间,移动2g/3g客户端连接成功率不高,着实让人头疼。

说是android网络调试,其实也不过是在被root后android系统操作,使用adb shell执行一些常规的终端命令,检测2g/3g/4g/wifi网络等,进而确定一些因网络等导致的问题而已。但adb shell默认没有几个支持的命令,比如 cat, tcpdump,这些都是最基本的必备命令,也不支持。对于想要查看网络请求有几次跳转,不借助些外力,确实是件很不可能的事情。

基本将会包含如下内容:

  • 如何安装需要的linux终端命令tcpdump,mtr
  • 调试2g/3g等网络连通,域名请求跳转
  • 请求丢包情况

android终端扩展神器opkg

说它是神器,一点都不夸张。homepage((http://dan.drown.org/android/))上开篇明义:

unix command-line programs ported to run on android. this project uses opkg, which handles downloading and installing packages and their dependencies (like yum or apt). source for all packages are available.

作者dan (http://blog.dan.drown.org/)为我们移植到android平台,并且还为我们编译好相当多的常用程序,具体支持列表,可从changelog(http://dan.drown.org/android/)中找到,这里不再累述。

十分难得,由衷感谢。

下载opkg包

预先把依赖下载到本地:

http://dan.drown.org/android/system/xbin/busybox
http://dan.drown.org/android/opkg.tar.gz

安装opkg

设安装到android手机的 /data/local 目录,那么首先需要确保这个目录具有可读写权限。

记得要使用su命令切换到root管理员账户,操作、权限才不会受阻。

adb shell chmod 777 /data/local

拷贝opkg到/data/local目录

adb push busybox /data/local
adb push opkg.tar.gz /data/local

adb shell进去之后,开始编译安装:

cd /data/local
chmod 777 busybox
./busybox tar zxvf opkg.tar.gz

设置环境变量:

export path=$path:/data/local/bin

执行更新、安装准备

opkg update
opkg install opkg
opkg list # 可以查看可以支持安装的终端应用程序(命令)

话说,opkg可以应用于各种嵌入式环境中,超强的说。

安装linux终端应用/命令

可以一口气安装几个试试:

opkg install mtr curl tcpdump cat

当然,你也可以一个一个安装。

安装好之后呢,就是直接运行应用/命令了,测试baidu.com域名解析、丢包情况。

mtr -r baidu.com host: localhost loss% snt last
avg best wrst stdev
1.|-- ??? 100.0 10 0.0 0.0 0.0 0.0 0.0
2.|-- 192.168.61.1 0.0% 10 504.3 635.0 339.3 1024. 238.7
3.|-- 192.168.63.138 0.0% 10 392.9 588.7 298.5 847.7 220.3
4.|-- 221.130.39.106 0.0% 10 340.9 557.3 257.4 823.5 211.7
5.|-- 221.179.159.45 10.0% 10 649.6 631.4 332.6 821.4 165.0
6.|-- 111.13.14.6 10.0% 10 561.9 551.3 268.2 777.0 170.0
7.|-- 111.13.0.162 10.0% 10 510.6 570.6 385.5 767.6 116.6
8.|-- 111.13.1.14 10.0% 10 775.4 565.2 377.7 775.4 130.9
9.|-- 111.13.2.130 10.0% 10 707.2 564.6 381.1 887.3 173.4

嗯,通过mtr确实很容易就看出,网络跳数,每一个节点丢包率。这样就能很容易找到在移动2g/3g网络连接超时比较严重的问题所在。下面就是希望运维的同学尽快处理好,避免再次出现由联通机房再次跳转到移动机房问题。

非常感谢陈杰同学推荐的比ping traceroute还要好用命令mtr。一旦拥有,不会放手!

移动2g/3g下网络抓包

要想抓取2g/3g网络下数据包,必须安装一个tcpdump命令:

opkg install tcpdump

opkg很贴心的会把所依赖的libpcap也都一并安装上,完全不用担心版本问题!

tcpdump -i any -p -vv -s 0 -w /sdcard/capture.pcap

下面就是一气呵成的导出,使用wireshark进行分析了。

adb pull /sdcard/tmp1.pcap c:/tmp

其它有利于诊断网络的app

不习惯使用终端诊断网络,可以直接使用现成的app。

  1. 第一名 fing,大名如雷贯耳,跨android、ios平台,dns、ping等不在话下,居家生活之必备
  2. 第二名嘛,暂时还没有发现呢
  3. shark for root,也不错,android平台推荐
  4. 网速测试,可以看到当前网络的延迟等,也不错

有更好的app推荐,欢迎推荐一二。

小结

  1. 希望可以给遇到同样问题的同学一些帮助
  2. 记录下来便于以后索引


nieyong 2014-11-20 22:05
]]>
为什么批量请求要尽可能的合并操作http://www.blogjava.net/yongboy/archive/2014/11/09/419829.htmlnieyongnieyongsun, 09 nov 2014 14:08:00 gmthttp://www.blogjava.net/yongboy/archive/2014/11/09/419829.htmlhttp://www.blogjava.net/yongboy/comments/419829.htmlhttp://www.blogjava.net/yongboy/archive/2014/11/09/419829.html#feedback16http://www.blogjava.net/yongboy/comments/commentrss/419829.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/419829.html前言

线上情况:

  1. 线上redis集群,多个twemproxy代理(nutcracker),lvs dr路由均衡调度
  2. 客户端使用jedis操作redis集群,一个程序进程实例使用原先1024个工作线程处理请求,若干个进程实例
  3. 一天超过22亿次请求,网络一般情况下,一天超过上万个连接失败异常
  4. 运维同学告知,lvs压力较大

改进工作:

  1. 工作线程由原先1024改用16个
  2. 每个线程每次最多操作1000个redis命令批量提交

实际效果:

  1. 一天不到一亿次的请求量
  2. lvs压力大减
  3. cpu压力降低到原先1/3以下
  4. 单个请求抽样调研平均减少1-90毫秒时间(尤其是跨机房处理)

redis支持批量提交

原生支持批量操作方式

一般命令前缀若添加上m字符串,表示支持多个、批量命令提交了。

显式的...

mset key value [key value ...]
msetnx key value [key value ...]
hmget key field [field ...]
hmset key field value [field value ...]

一般方式的...

hdel key field [field ...]
srem key member [member ...]
rpush key value [value ...]
......

更多,请参考:

pipeline管道方式

官方文档:

  1. redis client把所有命令一起打包发送到redis server,然后阻塞等待处理结果
  2. redis server必须在处理完所有命令前先缓存起所有命令的处理结果
  3. 打包的命令越多,缓存消耗内存也越多
  4. 不是打包的命令越多越好
  5. 实际环境需要根据命令执行时间等各种因素选择合并命令的个数,以及测试效果等

java队列支持

一般业务、接入前端请求量过大,生产者速度过快,这时候使用队列暂时缓存会比较好一些,消费者直接直接从队列获取任务,通过队列让生产者和消费者进行分离这也是业界普通采用的方式。

监控队列

有的时候,若可以监控一下队列消费情况,可以监控一下,就很直观。同事为队列添加了一个监控线程,清晰明了了解队列消费情况。

示范

示范使用了redis pipeline,线程池,准备数据,生产者-消费者队列,队列监控等,消费完毕,程序关闭。

/**
 * 以下测试在jedis 2.6下测试通过
 * 
 * @author nieyong
 * 
 */
public class testjedispipeline {
    private static final int num = 512;
    private static final int max = 1000000; // 100w
    private static jedispool redispool;
    private static final executorservice pool = executors.newcachedthreadpool();
    protected static final blockingqueue queue = new arrayblockingqueue(
            max); // 100w
    private static boolean finished = false;
    static {
        jedispoolconfig config = new jedispoolconfig();
        config.setmaxactive(64);
        config.setmaxidle(64);
        try {
            redispool = new jedispool(config, "192.168.192.8", 6379, 10000,
                    null, 0);
        } catch (exception e) {
            system.err.println("init msg redis factory error! "   e.tostring());
        }
    }
    public static void main(string[] args) throws interruptedexception {
        system.out.println("prepare test data 100w");
        preparetestdata();
        system.out.println("prepare test data done!");
        // 生产者,模拟请求100w次
        pool.execute(new runnable() {
            @override
            public void run() {
                for (int i = 0; i < max; i  ) {
                    if (i % 3 == 0) {
                        queue.offer("del_key key_"   i);
                    } else {
                        queue.offer("get_key key_"   i);
                    }
                }
            }
        });
        // cpu核数*2 个工作者线程
        int threadnum = 2 * runtime.getruntime().availableprocessors();
        for (int i = 0; i < threadnum; i  )
            pool.execute(new consumertask());
        pool.execute(new monitortask());
        thread.sleep(10 * 1000);// 10sec
        system.out.println("going to shutdown server ...");
        setfinished(true);
        pool.shutdown();
        pool.awaittermination(1, timeunit.milliseconds);
        system.out.println("colse!");
    }
    private static void preparetestdata() {
        jedis redis = redispool.getresource();
        pipeline pipeline = redis.pipelined();
        for (int i = 0; i < max; i  ) {
            pipeline.set("key_"   i, (i * 2   1)   "");
            if (i % (num * 2) == 0) {
                pipeline.sync();
            }
        }
        pipeline.sync();
        redispool.returnresource(redis);
    }
    // queue monitor,生产者-消费队列监控
    private static class monitortask implements runnable {
        @override
        public void run() {
            while (!thread.interrupted() && !isfinished()) {
                system.out.println("queue.size = "   queue.size());
                try {
                    thread.sleep(500); // 0.5 second
                } catch (interruptedexception e) {
                    break;
                }
            }
        }
    }
    // consumer,消费者
    private static class consumertask implements runnable {
        @override
        public void run() {
            while (!thread.interrupted() && !isfinished()) {
                if (queue.isempty()) {
                    try {
                        thread.sleep(100);
                    } catch (interruptedexception e) {
                    }
                    continue;
                }
                list tasks = new arraylist(num);
                queue.drainto(tasks, num);
                if (tasks.isempty()) {
                    continue;
                }
                jedis jedis = redispool.getresource();
                pipeline pipeline = jedis.pipelined();
                try {
                    list> resultlist = new arraylist>(
                            tasks.size());
                    list waitdeletelist = new arraylist(
                            tasks.size());
                    for (string task : tasks) {
                        string key = task.split(" ")[1];
                        if (task.startswith("get_key")) {
                            resultlist.add(pipeline.get(key));
                            waitdeletelist.add(key);
                        } else if (task.startswith("del_key")) {
                            pipeline.del(key);
                        }
                    }
                    pipeline.sync();
                    // 处理返回列表
                    for (int i = 0; i < resultlist.size(); i  ) {
                        resultlist.get(i).get();
                        // handle value here ...
                        // system.out.println("get value "   value);
                    }
                    // 读取完毕,直接删除之
                    for (string key : waitdeletelist) {
                        pipeline.del(key);
                    }
                    pipeline.sync();
                } catch (exception e) {
                    redispool.returnbrokenresource(jedis);
                } finally {
                    redispool.returnresource(jedis);
                }
            }
        }
    }
    private static boolean isfinished(){
        return finished;
    }
    private static void setfinished(boolean bool){
        finished = bool;
    }
}

代码作为示范。若线上则需要处理一些异常等。

小结

若能够批量请求进行合并操作,自然可以节省很多的网络带宽、cpu等资源。有类似问题的同学,不妨考虑一下。



nieyong 2014-11-09 22:08 发表评论
]]>
随手记之linux 2.6.32内核syn flooding警告信息http://www.blogjava.net/yongboy/archive/2014/08/20/417165.htmlnieyongnieyongwed, 20 aug 2014 12:43:00 gmthttp://www.blogjava.net/yongboy/archive/2014/08/20/417165.htmlhttp://www.blogjava.net/yongboy/comments/417165.htmlhttp://www.blogjava.net/yongboy/archive/2014/08/20/417165.html#feedback3http://www.blogjava.net/yongboy/comments/commentrss/417165.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/417165.html前言

新申请的服务器内核为2.6.32,原先的tcp server直接在新内核的linxu服务器上运行,运行dmesg命令,可以看到大量的syn flooding警告:

possible syn flooding on port 8080. sending cookies.

原先的2.6.18内核的参数在2.6.32内核版本情况下,简单调整"net.ipv4.tcp_max_syn_backlog"已经没有作用。

怎么办,只能再次阅读2.6.32源码,以下即是。

最后小结处有直接结论,心急的你可以直接阅读总结好了。

linux内核2.6.32有关backlog值分析

net/socket.c:

syscall_define2(listen, int, fd, int, backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned)backlog > somaxconn)
            backlog = somaxconn;
        err = security_socket_listen(sock, backlog);
        if (!err)
            err = sock->ops->listen(sock, backlog);
        fput_light(sock->file, fput_needed);
    }
    return err;
}

net/ipv4/af_inet.c:

/*
 *  move a socket into listening state.
 */
int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err;
    lock_sock(sk);
    err = -einval;
    if (sock->state != ss_unconnected || sock->type != sock_stream)
        goto out;
    old_state = sk->sk_state;
    if (!((1 << old_state) & (tcpf_close | tcpf_listen)))
        goto out;
    /* really, if the socket is already in listen state
     * we can only allow the backlog to be adjusted.
     */
    if (old_state != tcp_listen) {
        err = inet_csk_listen_start(sk, backlog);
        if (err)
            goto out;
    }
    sk->sk_max_ack_backlog = backlog;
    err = 0;
out:
    release_sock(sk);
    return err;
}

inet_listen调用inet_csk_listen_start函数,所传入的backlog参数改头换面,变成了不可修改的常量nr_table_entries了。

net/ipv4/inet_connection_sock.c:

int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
    struct inet_sock *inet = inet_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
    if (rc != 0)
        return rc;
    sk->sk_max_ack_backlog = 0;
    sk->sk_ack_backlog = 0;
    inet_csk_delack_init(sk);
    /* there is race window here: we announce ourselves listening,
     * but this transition is still not validated by get_port().
     * it is ok, because this socket enters to hash table only
     * after validation is complete.
     */
    sk->sk_state = tcp_listen;
    if (!sk->sk_prot->get_port(sk, inet->num)) {
        inet->sport = htons(inet->num);
        sk_dst_reset(sk);
        sk->sk_prot->hash(sk);
        return 0;
    }
    sk->sk_state = tcp_close;
    __reqsk_queue_destroy(&icsk->icsk_accept_queue);
    return -eaddrinuse;
}

下面处理的是tcp syn_recv状态的连接,处于握手阶段,也可以说是半连接时,等待着连接方第三次握手。

/*
 * maximum number of syn_recv sockets in queue per listen socket.
 * one syn_recv socket costs about 80bytes on a 32bit machine.
 * it would be better to replace it with a global counter for all sockets
 * but then some measure against one socket starving all other sockets
 * would be needed.
 *
 * it was 128 by default. experiments with real servers show, that
 * it is absolutely not enough even at 100conn/sec. 256 cures most
 * of problems. this value is adjusted to 128 for very small machines
 * (<=32mb of memory) and to 1024 on normal or better ones (>=256mb).
 * note : dont forget somaxconn that may limit backlog too.
 */
int reqsk_queue_alloc(struct request_sock_queue *queue,
              unsigned int nr_table_entries)
{
    size_t lopt_size = sizeof(struct listen_sock);
    struct listen_sock *lopt;
    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = max_t(u32, nr_table_entries, 8);
    nr_table_entries = roundup_pow_of_two(nr_table_entries   1);
    lopt_size  = nr_table_entries * sizeof(struct request_sock *); 
    if (lopt_size > page_size)
        lopt = __vmalloc(lopt_size,
            gfp_kernel | __gfp_highmem | __gfp_zero,
            page_kernel);
    else
        lopt = kzalloc(lopt_size, gfp_kernel);
    if (lopt == null)
        return -enomem;
    for (lopt->max_qlen_log = 3;
         (1 << lopt->max_qlen_log) < nr_table_entries;
         lopt->max_qlen_log  );
    get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
    rwlock_init(&queue->syn_wait_lock);
    queue->rskq_accept_head = null;
    lopt->nr_table_entries = nr_table_entries;
    write_lock_bh(&queue->syn_wait_lock);
    queue->listen_opt = lopt;
    write_unlock_bh(&queue->syn_wait_lock);
    return 0;
}

关键要看nr_table_entries变量,在reqsk_queue_alloc函数中nr_table_entries变成了无符号变量,可修改的,变化受限。

比如实际内核参数值为:

net.ipv4.tcp_max_syn_backlog = 65535

所传入的backlog(不大于net.core.somaxconn = 65535)为8102,那么

// 取listen函数的backlog和sysctl_max_syn_backlog最小值,结果为8102
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
// 取nr_table_entries和8进行比较的最大值,结果为8102
nr_table_entries = max_t(u32, nr_table_entries, 8);
// 可看做 nr_table_entries*2,结果为8102*2=16204
nr_table_entries = roundup_pow_of_two(nr_table_entries   1);
计算结果,max_qlen_log = 14

2.6.18内核中max_qlen_log的计算方法

for (lopt->max_qlen_log = 6;
     (1 << lopt->max_qlen_log) < sysctl_max_syn_backlog;
     lopt->max_qlen_log  );
  1. 很显然,sysctl_max_syn_backlog参与了运算,sysctl_max_syn_backlog值很大的话会导致max_qlen_log值相对比也很大
  2. 若sysctl_max_syn_backlog=65535,那么max_qlen_log=16
  3. 2.6.18内核中半连接长度为2^16=65536

作为listen_sock结构定义了需要处理的处理半连接的队列元素个数为nr_table_entries,此例中为16204长度。

/** struct listen_sock - listen state
 *
 * @max_qlen_log - log_2 of maximal queued syns/requests
 */
struct listen_sock {
    u8          max_qlen_log;
    /* 3 bytes hole, try to use */
    int         qlen;
    int         qlen_young;
    int         clock_hand;
    u32         hash_rnd;
    u32         nr_table_entries;
    struct request_sock *syn_table[0];
};

经描述而知,2^max_qlen_log = 半连接队列长度qlen值。

再回头看看报告syn flooding的函数:

net/ipv4/tcp_ipv4.c

#ifdef config_syn_cookies
static void syn_flood_warning(struct sk_buff *skb)
{
    static unsigned long warntime;
    if (time_after(jiffies, (warntime   hz * 60))) {
        warntime = jiffies;
        printk(kern_info
               "possible syn flooding on port %d. sending cookies.\n",
               ntohs(tcp_hdr(skb)->dest));
    }
}
#endif

被调用的处,已精简若干代码:

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
......
#ifdef config_syn_cookies
    int want_cookie = 0;
#else
#define want_cookie 0 /* argh, why doesn't gcc optimize this :( */
#endif
    ......
    /* tw buckets are converted to open requests without
     * limitations, they conserve resources and peer is
     * evidently real one.
     */
     // 判断半连接队列是否已满 && !0
    if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef config_syn_cookies
        if (sysctl_tcp_syncookies) {
            want_cookie = 1;
        } else
#endif
        goto drop;
    }
    /* accept backlog is full. if we have already queued enough
     * of warm entries in syn queue, drop request. it is better than
     * clogging syn queue with openreqs with exponentially increasing
     * timeout.
     */
    if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
        goto drop;
    req = inet_reqsk_alloc(&tcp_request_sock_ops);
    if (!req)
        goto drop;
    ......
    if (!want_cookie)
        tcp_ecn_create_request(req, tcp_hdr(skb));
    if (want_cookie) {
#ifdef config_syn_cookies
        syn_flood_warning(skb);
        req->cookie_ts = tmp_opt.tstamp_ok;
#endif
        isn = cookie_v4_init_sequence(sk, skb, &req->mss);
    } else if (!isn) {
        ......
    }       
    ......
}

判断半连接队列已满的函数很关键,可以看看运算法则:

include/net/inet_connection_sock.h:

static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
    return reqsk_queue_is_full(&inet_csk(sk)->icsk_accept_queue);
}

include/net/rquest_sock.h:

static inline int reqsk_queue_is_full(const struct request_sock_queue *queue)
{
    // 向右移位max_qlen_log个单位
    return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
}

返回1,自然表示半连接队列已满。

以上仅仅是分析了半连接队列已满的判断条件,总之应用程序所传入的backlog很关键,如值太小,很容易得到1.

若 somaxconn = 128,sysctl_max_syn_backlog = 4096,backlog = 511 则最终 nr_table_entries = 256,max_qlen_log = 8。那么超过256个半连接的队列,257 >> 8 = 1,队列已满。

如何设置backlog,还得需要结合具体应用程序,需要为其调用listen方法赋值。

netty backlog处理

tcp server使用netty 3.7 版本,版本较低,在处理backlog,若我们不手动指定backlog值,jdk 1.6默认为50。

有证如下: java.net.serversocket:

public void bind(socketaddress endpoint, int backlog) throws ioexception {
    if (isclosed())
        throw new socketexception("socket is closed");
    if (!oldimpl && isbound())
        throw new socketexception("already bound");
    if (endpoint == null)
        endpoint = new inetsocketaddress(0);
    if (!(endpoint instanceof inetsocketaddress))
        throw new illegalargumentexception("unsupported address type");
    inetsocketaddress epoint = (inetsocketaddress) endpoint;
    if (epoint.isunresolved())
        throw new socketexception("unresolved address");
    if (backlog < 1)
      backlog = 50;
    try {
        securitymanager security = system.getsecuritymanager();
        if (security != null)
        security.checklisten(epoint.getport());
        getimpl().bind(epoint.getaddress(), epoint.getport());
        getimpl().listen(backlog);
        bound = true;
    } catch(securityexception e) {
        bound = false;
        throw e;
    } catch(ioexception e) {
        bound = false;
        throw e;
    }
}

netty中,处理backlog的地方:

org/jboss/netty/channel/socket/defaultserversocketchannelconfig.java:

@override
public boolean setoption(string key, object value) {
    if (super.setoption(key, value)) {
        return true;
    }
    if ("receivebuffersize".equals(key)) {
        setreceivebuffersize(conversionutil.toint(value));
    } else if ("reuseaddress".equals(key)) {
        setreuseaddress(conversionutil.toboolean(value));
    } else if ("backlog".equals(key)) {
        setbacklog(conversionutil.toint(value));
    } else {
        return false;
    }
    return true;
}

既然需要我们手动指定backlog值,那么可以这样做:

bootstrap.setoption("backlog", 8102); // 设置大一些没有关系,系统内核会自动与net.core.somaxconn相比较,取最低值

相对比netty 4.0,有些不智能,可参考:

小结

在linux内核2.6.32,若在没有遭受到syn flooding攻击的情况下,可以适当调整:

sysctl -w net.core.somaxconn=32768

sysctl -w net.ipv4.tcp_max_syn_backlog=65535

sysctl -p

另千万别忘记修改tcp server的listen接口所传入的backlog值,若不设置或者过小,都会有可能造成syn flooding的警告信息。开始不妨设置成1024,然后观察一段时间根据实际情况需要再慢慢往上调。

无论你如何设置,最终backlog值范围为:

backlog <= net.core.somaxconn

半连接队列长度约为:

半连接队列长度 ≈ 2 * min(backlog, net.ipv4.tcpmax_syn_backlog)

另,若出现syn flooding时,此时tcp syn_recv数量表示半连接队列已经满,可以查看一下:

ss -ant | awk 'nr>1 {  s[$1]} end {for(k in s) print k,s[k]}'

感谢运维书坤小伙提供的比较好用查看命令。



nieyong 2014-08-20 20:43
]]>
随手记之linux内核syn flooding警告信息http://www.blogjava.net/yongboy/archive/2014/08/06/416647.htmlnieyongnieyongwed, 06 aug 2014 13:57:00 gmthttp://www.blogjava.net/yongboy/archive/2014/08/06/416647.htmlhttp://www.blogjava.net/yongboy/comments/416647.htmlhttp://www.blogjava.net/yongboy/archive/2014/08/06/416647.html#feedback5http://www.blogjava.net/yongboy/comments/commentrss/416647.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/416647.html前言

最近线上服务器,dmesg会给出一些警告信息:

possible syn flooding on port 8080. sending cookies.

初看以为是受到dos拒绝性攻击,但仔细一分析,一天量也就是在1000多条左右,感觉上属于正常可接受范围。

下面需要找出来源,以及原因,以下内容基于linux 2.6.18内核。

警告输出源头

net/ipv4/tcp_ipv4.c:

#ifdef config_syn_cookies
static void syn_flood_warning(struct sk_buff *skb)
{
    static unsigned long warntime; // 第一次加载初始化为零,后续warntime = jiffies
    if (time_after(jiffies, (warntime   hz * 60))) {
        warntime = jiffies;
        printk(kern_info
           "possible syn flooding on port %d. sending cookies.\n",
           ntohs(skb->h.th->dest));
    }
}
#endif

很显然,config_syn_cookies在linux系统编译时,已被设置true。

time_after宏定义:

#define time_after(a,b)     \
    (typecheck(unsigned long, a) && \
     typecheck(unsigned long, b) && \
     ((long)(b) - (long)(a) < 0))

两个无符号的时间比较,确定先后顺序。

jiffies真身:

# define jiffies    raid6_jiffies()
#define hz 1000
......
static inline uint32_t raid6_jiffies(void)
{
    struct timeval tv;
    gettimeofday(&tv, null);
    return tv.tv_sec*1000   tv.tv_usec/1000; // 秒*1000   微秒/1000
}

回过头来,再看看syn_flood_warning函数:

static void syn_flood_warning(struct sk_buff *skb)
{
    static unsigned long warntime; // 第一次加载初始化为零,后续warntime = jiffies
    if (time_after(jiffies, (warntime   hz * 60))) {
        warntime = jiffies;
        printk(kern_info
           "possible syn flooding on port %d. sending cookies.\n",
           ntohs(skb->h.th->dest));
    }
}

warntime为static类型,第一次调用时被初始化为零,下次调用就是上次的jiffies值了,前后间隔值超过hz*60就不会输出警告信息了。

有关time_after和jiffies,分享几篇文章:

http://wenku.baidu.com/view/c75658d480eb6294dd886c4e.html

http://www.360doc.com/content/11/1201/09/1317564_168810003.shtml

 

警告输出需要满足的条件

注意观察want_cookie=1时的条件。

net/ipv4/tcp_ipv4.c:

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
    struct inet_request_sock *ireq;
    struct tcp_options_received tmp_opt;
    struct request_sock *req;
    __u32 saddr = skb->nh.iph->saddr;
    __u32 daddr = skb->nh.iph->daddr;
    __u32 isn = tcp_skb_cb(skb)->when; // when在tcp_v4_rcv()中会被置为0
    struct dst_entry *dst = null;
#ifdef config_syn_cookies
    int want_cookie = 0;
#else
#define want_cookie 0 /* argh, why doesn't gcc optimize this :( */
#endif
    /* never answer to syns send to broadcast or multicast */
    if (((struct rtable *)skb->dst)->rt_flags &
    (rtcf_broadcast | rtcf_multicast))
        goto drop;
    /* tw buckets are converted to open requests without
     * limitations, they conserve resources and peer is
     * evidently real one.
     */
    // if(判断半连接队列已满 && !0)
    if (inet_csk_reqsk_queue_is_full(sk) && !isn) { 
#ifdef config_syn_cookies
        if (sysctl_tcp_syncookies) { // net.ipv4.tcp_syncookies = 1
            want_cookie = 1;
        } else
#endif
        goto drop;
    }
    /* accept backlog is full. if we have already queued enough
     * of warm entries in syn queue, drop request. it is better than
     * clogging syn queue with openreqs with exponentially increasing
     * timeout.
     */
    // if(连接队列是否已满 && 半连接队列中还有未重传ack半连接数字 > 1) 
    if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
        goto drop;
    ......
    tcp_openreq_init(req, &tmp_opt, skb);
    ireq = inet_rsk(req);
    ireq->loc_addr = daddr;
    ireq->rmt_addr = saddr;
    ireq->opt = tcp_v4_save_options(sk, skb);
    if (!want_cookie)
        tcp_ecn_create_request(req, skb->h.th);
    if (want_cookie) { // 半连接队列已满会触发
#ifdef config_syn_cookies
        syn_flood_warning(skb);
#endif
        isn = cookie_v4_init_sequence(sk, skb, &req->mss);
    } else if (!isn) {
        ......
    }
    /* kill the following clause, if you dislike this way. */
    // net.ipv4.tcp_syncookies未设置情况下,sysctl_max_syn_backlog发生的作用
    else if (!sysctl_tcp_syncookies &&
             (sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
              (sysctl_max_syn_backlog >> 2)) &&
             (!peer || !peer->tcp_ts_stamp) &&
             (!dst || !dst_metric(dst, rtax_rtt))) {
            /* without syncookies last quarter of
             * backlog is filled with destinations,
             * proven to be alive.
             * it means that we continue to communicate
             * to destinations, already remembered
             * to the moment of synflood.
             */
            limit_netdebug(kern_debug "tcp: drop open "
                   "request from %u.%u.%u.%u/%u\n",
                   nipquad(saddr),
                   ntohs(skb->h.th->source));
            dst_release(dst);
            goto drop_and_free;
        }
        isn = tcp_v4_init_sequence(sk, skb);
    }
    tcp_rsk(req)->snt_isn = isn;
    if (tcp_v4_send_synack(sk, req, dst))
        goto drop_and_free;
    if (want_cookie) {
        reqsk_free(req);
    } else {
        inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init);
    }
    return 0;
drop_and_free:
    reqsk_free(req);
drop:
    return 0;
}

小结

总之,如系统出现:

possible syn flooding on port 8080. sending cookies.

若量不大,是在提醒你需要关心一下sysctl_max_syn_backlog其值是否过低:

sysctl -a | grep 'max_syn_backlog'

不妨成倍增加一下

sysctl -w net.ipv4.tcp_max_syn_backlog=8192

sysctl -p

若进程无法做到重新加载,那就需要重启应用,以适应新的内核参数。进而持续观察一段时间。

貌似tcp_max_syn_backlog参数其完整作用域还没有理解完整,下次有时间再写吧。



nieyong 2014-08-06 21:57
]]>
随手记之linux内核backlog笔记http://www.blogjava.net/yongboy/archive/2014/07/30/416373.htmlnieyongnieyongwed, 30 jul 2014 09:22:00 gmthttp://www.blogjava.net/yongboy/archive/2014/07/30/416373.htmlhttp://www.blogjava.net/yongboy/comments/416373.htmlhttp://www.blogjava.net/yongboy/archive/2014/07/30/416373.html#feedback5http://www.blogjava.net/yongboy/comments/commentrss/416373.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/416373.html零。前言

有些东西总是很容易遗忘,一时记得了,过两天就真正还给周公了。零零碎碎的不如一并记下来,以后可以直接拿过来查询即可。

以下内容基于linux 2.6.18内核。

一。listen方法传入的backlog参数,net.core.somaxconn

这个参数具体意义,先看看linux socket的listen解释

man listen

   #include 
   int listen(int sockfd, int backlog);

int类型的backlog参数,listen方法的backlog意义为,已经完成三次握手、已经成功建立连接的套接字将要进入队列的长度。

一般我们自己定义设定backlog值,若我们设置的backlog值大于net.core.somaxconn值,将被置为net.core.somaxconn值大小。若不想直接硬性指定,跟随系统设定,则需要读取/proc/sys/net/core/somaxconn。

net\socket.c :

/*
 *  perform a listen. basically, we allow the protocol to do anything
 *  necessary for a listen, and if that works, we mark the socket as
 *  ready for listening.
 */
int sysctl_somaxconn = somaxconn;
asmlinkage long sys_listen(int fd, int backlog)
{
    struct socket *sock;
    int err, fput_needed;
    if ((sock = sockfd_lookup_light(fd, &err, &fput_needed)) != null) {
        if ((unsigned) backlog > sysctl_somaxconn)
            backlog = sysctl_somaxconn;
        err = security_socket_listen(sock, backlog);
        if (!err)
            err = sock->ops->listen(sock, backlog);
        fput_light(sock->file, fput_needed);
    }
    return err;
}

比如经常使用的netty(4.0)框架,在linux下启动时,会直接读取/proc/sys/net/core/somaxconn值然后作为listen的backlog参数进行调用linux系统的listen进行初始化等。

int somaxconn = 3072;
bufferedreader in = null;
try {
    in = new bufferedreader(new filereader("/proc/sys/net/core/somaxconn"));
    somaxconn = integer.parseint(in.readline());
    logger.debug("/proc/sys/net/core/somaxconn: {}", somaxconn);
} catch (exception e) {
    // failed to get somaxconn
} finally {
    if (in != null) {
        try {
            in.close();
        } catch (exception e) {
            // ignored.
        }
    }
}
somaxconn = somaxconn;
......
private volatile int backlog = netutil.somaxconn;

一般稍微增大net.core.somaxconn值就显得很有必要。

设置其值方法:

sysctl -w net.core.somaxconn=65535

较大内存的linux,65535数值一般就可以了。

若让其生效,sysctl -p 即可,然后重启你的server应用即可。

二。网卡设备将请求放入队列的长度,netdev_max_backlog

内核代码中sysctl.c文件解释:

number of unprocessed input packets before kernel starts dropping them, default 300

我所理解的含义,每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的最大数目,一旦超过将被丢弃。

所起作用处,net/core/dev.c:

int netif_rx(struct sk_buff *skb)
{
    struct softnet_data *queue;
    unsigned long flags;
    /* if netpoll wants it, pretend we never saw it */
    if (netpoll_rx(skb))
        return net_rx_drop;
    if (!skb->tstamp.off_sec)
        net_timestamp(skb);
    /*
     * the code is rearranged so that the path is the most
     * short when cpu is congested, but is still operating.
     */
    local_irq_save(flags);
    queue = &__get_cpu_var(softnet_data);
    __get_cpu_var(netdev_rx_stat).total  ;
    if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
        if (queue->input_pkt_queue.qlen) {
enqueue:
            dev_hold(skb->dev);
            __skb_queue_tail(&queue->input_pkt_queue, skb);
            local_irq_restore(flags);
            return net_rx_success;
        }
        netif_rx_schedule(&queue->backlog_dev);
        goto enqueue;
    }
    __get_cpu_var(netdev_rx_stat).dropped  ;
    local_irq_restore(flags);
    kfree_skb(skb);
    return net_rx_drop;
}

以上代码看一下,大概会明白netdev_max_backlog会在什么时候起作用。



nieyong 2014-07-30 17:22
]]>
linux服务器端口的那些事http://www.blogjava.net/yongboy/archive/2014/06/28/415240.htmlnieyongnieyongsat, 28 jun 2014 06:15:00 gmthttp://www.blogjava.net/yongboy/archive/2014/06/28/415240.htmlhttp://www.blogjava.net/yongboy/comments/415240.htmlhttp://www.blogjava.net/yongboy/archive/2014/06/28/415240.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/415240.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/415240.html前言

公司内技术分享文档,不涉及公司内部技术等,可以拿出来分享一下。

演示文档

访问地址:

有些粗糙,有些点可能未表达清楚,您若发现谬误之处,欢迎及时指出。



nieyong 2014-06-28 14:15
]]>
《让网页加载快一些》培训演示文档http://www.blogjava.net/yongboy/archive/2013/06/28/401054.htmlnieyongnieyongfri, 28 jun 2013 08:56:00 gmthttp://www.blogjava.net/yongboy/archive/2013/06/28/401054.htmlhttp://www.blogjava.net/yongboy/comments/401054.htmlhttp://www.blogjava.net/yongboy/archive/2013/06/28/401054.html#feedback2http://www.blogjava.net/yongboy/comments/commentrss/401054.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/401054.html《让网页加载快一些》,这篇ppt演示文档,目的在于扩大视野用(没有涉及到深度),便于在处理网页性能优化时,为一些同事提供一些处理思路,避免到处撞墙。

目标:
如何让一个页面加载快一些,这是主题
页面每经过一个环节,都会简单涉及
覆盖面广(前前后后都有),但蜻蜓点水
可能会增加些视野(目的也就达到了)
前期不要做优化,但需要做规划!


豆丁地址:


nieyong 2013-06-28 16:56
]]>
基于erlang otp构建一个tcp服务器http://www.blogjava.net/yongboy/archive/2012/10/24/390185.htmlnieyongnieyongwed, 24 oct 2012 10:14:00 gmthttp://www.blogjava.net/yongboy/archive/2012/10/24/390185.htmlhttp://www.blogjava.net/yongboy/comments/390185.htmlhttp://www.blogjava.net/yongboy/archive/2012/10/24/390185.html#feedback1http://www.blogjava.net/yongboy/comments/commentrss/390185.htmlhttp://www.blogjava.net/yongboy/services/trackbacks/390185.html套接字模式

主动模式(选项{active, true})一般让人很喜欢,非阻塞消息接收,但在系统无法应对超大流量请求时,客户端发送的数据快过服务器可以处理的速度,那么系统就可能会造成消息缓冲区被塞满,可能出现持续繁忙的流量的极端情况下,系统因请求而溢出,虚拟机造成内存不足的风险而崩溃。

使用被动模式(选项{active, false})的套接字,底层的tcp缓冲区可用于抑制请求,并拒绝客户端的消息,在接收数据的地方都会调用gen_tcp:recv,造成阻塞(单进程模式下就只能消极等待某一个具体的客户端套接字,很危险)。需要注意的是,操作系统可能还会做一些缓存允许客户端机器继续发送少量数据,然后才会将其阻塞,此时erlang尚未调用recv函数。

混合型模式(半阻塞),使用选项{active, once}打开,主动仅针对一个消息,在控制进程发送完一个数据消息后,必须显示调用inet:setopts(socket, [{active, once}])重新激活以便接受下一个消息(在此之前,系统处于阻塞状态)。可见,混合型模式综合了主动模式和被动模式的两者优势,可实现流量控制,防止服务器被过多消息淹没。

以下tcp server代码,都是建立在混合型模式(半阻塞)基础上。

prim_inet相关说明

prim_inet没有官方文档,可以认为是对底层socket的直接包装。淘宝说,这是otp内部实现的细节 是针对erlang库开发者的private module,底层模块,不推荐使用。但在示范中演示了prim_inet操作socket异步特性。

设计模式

一般来说,需要一个单独进程进行客户端套接字监听,每一个子进程进行处理来自具体客户端的socket请求。

在示范中,子进程使用gen_fsm处理,很巧妙的结合状态机和消息事件,值得学习。

在文章中,作者也是使用此模式,但子进程不符合otp规范,因此个人认为不是一个很好的实践模式。

simple_one_for_one

简易的一对一监督进程,用来创建一组动态子进程。对于需要并发处理多个请求的服务器较为合适。比如socket 服务端接受新的客户端连接请求以后,需要动态创建一个新的socket连接处理子进程。若遵守otp原则,那就是子监督进程。

tcp server实现 

基于标准api简单实现

也是基于{active, once}模式,但阻塞的等待下一个客户端连接的任务被抛给了子监督进程。

看一下入口tcp_server_app吧

读取端口,然后启动主监督进程(此时还不会监听处理客户端socket请求),紧接着启动子监督进程,开始处理来自客户端的socket的连接。

监督进程tcp_server_sup也很简单:

需要注意的是,只有调用start_child函数时,才真正调用tcp_server_handler:start_link([lsock])函数。

tcp_server_handler的代码也不复杂:

代码很精巧,有些小技巧在里面。子监督进程调用start_link函数,init会返回{ok, #state{lsock = socket}, 0}. 数字0代表了timeout数值,意味着gen_server马上调用handle_info(timeout, #state{lsock = lsock} = state)函数,执行客户端socket监听,阻塞于此,但不会影响在此模式下其它函数的调用。直到有客户端进来,然后启动一个新的子监督进程tcp_server_handler,当前子监督进程解除阻塞。

 

基于prim_inet实现

这个实现师从于non-blocking tcp server using otp principles一文,但子进程改为了gen_server实现。

看一看入口,很简单的:

监督进程代码:

策略不一样,one_for_one包括了一个监听进程tcp_listener,还包含了一个tcp_client_sup进程树(simple_one_for_one策略)

tcp_listener单独一个进程用于监听来自客户端socket的连接:

很显然,接收客户端的连接之后,转交给tcp_client_handler模块进行处理:

和标准api对比一下,可以感受到异步io的好处。

小结

通过不同的模式,简单实现一个基于erlang otp的tcp服务器,也是学习总结,不至于忘记。

您若有更好的建议,欢迎告知,谢谢。

参考资料

  1. 《erlang程序设计》
  2. 《erlang/otp并发编程实战》


nieyong 2012-10-24 18:14
]]>
网站地图