第 9 页的文章列表

rabbitmq遇到的一次tcp半打开的问题

问题描述

业务中有一个测试服务器,里面运行了好几个任务队列。但是自从将任务队列从redis迁移到rabbitmq上后,一直会在运行一段时间后停止运行。一般这种情况下,要么是进程退出了,要么是连接断开了。但是检查后发现,进程是正常运行的,并且通过netstat发现,连接也一直存在。如果是连接断开,我的代码中也做了断线重连的机制。

问题排查

一开始以为是php-amqplib这个库的问题,就去找它的issue,看看有没有类似的问题。这里没有找到类似的bug。 然后去复现了一个最小的demo,这个问题一直都是出现在运行相当长的时间之后。于是决定对测试服务器上已经出现问题的代码进行抓包。新push的任务,任务队列的进程就是接收不到,但是netstat结果,任务队列到rabbit服务的连接又确实是存在的。 没有办法就去随便翻翻《UNIX网络编程》的TCP章节,然后想到TCP的全双工特性。全双工特性就是说**在一个给定的连接上应用可以在任何时候在进出两个方向上既发送数据又接收数据。建立一个全双工连接后,需要的话可以把它转换成一个单工连接。**于是用netstat检查rabbit服务的连接,发现是没有到任务队列的连接的。所以问题就是出现在这里。**但是仔细思考一下,这里的问题并不能用全双工特性去解释,因为全双工转成单工是tcp的一个特性,但是在这个问题中,应该是一个异常情况。全双工是需要双端协商的,而我这里的问题应该是: **服务器关闭了连接,但是任务队列却没有收到关闭的Fin报文,**很多时候被称为半打开

问题解决

解决这个问题很简单,启用php-amqplib的心跳包机制即可。

更多的思考

1.半连接,半打开,半关闭(以下用A,B代表连接的两端) 半连接:出现在tcp的三次握手阶段。A发送syn,B响应ack,syn后,此时处于半连接状态。如果A不发送ack,B将会一直为这个半连接分配一段内存空间。因此可以使用这个特点对B进行半连接攻击 半打开(half-open):A断开连接但是却没有发送Fin报文,导致B不知道。在维基百科上半打开和半连接是相同的。 半关闭:在关闭的4次挥手阶段,A端发送Fin,B端ack但是不发送Fin。 半连接、半关闭都是正常出现的情况。半打开则是不正常的状态,**一个Unix进程无论自愿地(调用exit或是从main函数中返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。**也就是说,只有当服务器断电等这种非正常关闭的情况下才会出现半连接,否则对端都应该收到Fin报文,然后关闭连接。 1.什么情况导致了半打开? 服务器断电这类情况肯定是没有出现的,所以一定是其他地方有问题导致了这个情况。因为这个问题只在测试服务器上出现,生产服务器上并没有。所以有点难推测。 2.双向连接(bidirectional)和全双工(full-duplex) 在我的理解中,双向连接指的是A端确认了到B端的连接,B端也确认了到A端的连接。全双工则指可以同时发送和接收,互不干扰。 3.心跳机制是如何避免这种情况的? 心跳机制一般都是隔一段时间主动发送一个消息给对端来确认连接是否存活。如果连接丢失,则必然不会收到对端的响应。这样在响应超时后重新发起连接即可。 其实tcp也有一个keep-alive机制。与心跳包作用类似,但是一是检查的周期长,二是一旦启用,机器上所有的连接都会启用这个机制,导致资源浪费。

参考

TCP half-open 半连接、半打开、半关闭

分类:工作标签:#rabbitmq #tcp半打开

go 内存模型

简介

go的内存模型旨在说明:一个协程中对变量v的写入产生的值可以保证被另一个协程中的对变量v的读取观察到。

Happens Before

在一个协程内,读写操作必须按照程序指定的顺序进行。在一个协程内,编译器和处理器可能对读写操作重写排序,但是这个排序的前提是:在当前协程内,不会改变程序的执行行为。但是这个重新排序是不保证其他协程观测到执行顺序是不改变的。比如在协程1中a=1;b=2,但在其他协程的感知中,可能b比a先更新值。 我们这里定义Happens Before(在...之前发生),如果事件e1在事件e2之前发生,那么我们就可以说e2在e1之后发生。如果e1既不在e2之前发生,也不在e2之后发生。那么e1和e2就是同时发生的(并发)。 在一个协程内,Happens Before的顺序就是程序表达的那样。 如果下面两点可以保证,就说明对变量v的读取r允许观察到对变量v的写入w:

  • r不是在w之前发生
  • 在w之后并且r之前没有其他的对v的写入w’

为了保证变量v的读取r观察到v的特定写入w,并且保证w是唯一允许被r观察到的。也就是说,r保证能观察到w。需要做到下面两点:

  • w在r之前发生
  • 其他的对v的写入w’要么发生在w之前,要么发生在w之后

下面两点比上面两点要求更为严格。它保证了没有其他的写入w’和w、r同时发生。 在一个协程内,因为没有并发,所以这两种定义是一致的:读取r可以观察到写入w对变量v最近一次的写入。但是当多个协程同时访问同一个共享变量时,就必须使用同步事件来建立Happens Before语义来保证读取r可以观察到指定的写入w。 在内存模型中,对变量v以0值初始化是一次写入。 对于大于单机器字节的读取和写入,可以看做是对多个单机器字节的乱序操作。

同步

初始化

程序初始化是在单协程内运行的,但是这个协程可能创建其他的协程。它们是并发的。 如果包p引入了包q,则q的初始化函数会在p的初始化函数之前运行。 main.main函数在所有的初始化函数之后运行。

协程创建

go关键字创建协程发生在协程运行之前。

协程销毁

协程的销毁不保证在程序中的任何事件发生之前。比如:

var a string

func hello() {
    go func() { a = "hello" }()
    print(a)
}

这个赋值没有跟随任何同步事件,所以它不保证被其他协程观察到。事实上,激进的编译器会删除整个go语句。 如果需要,可以使用同步原语比如管道通信来建立一个相关的执行顺序。

管道通信

在go的协程中,管道通信是非常重要的一个同步方法。通常发送方和接受方在两个不同的协程中,利用发送和接收这两个有序的动作来进行同步。

1.在有缓冲的管道中,发送一定发生在接收完成前。(A send on a channel happens before the corresponding receive from that channel completes.

例如:

var c = make(chan int, 10)
var a string

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print(a)
}

a = "hello, world"一定在c<-0之前发生,c<-0一定在<-c之前发生,<-c一定在print(a)之前发生。这样就能保证a = "hello, world"print(a)之前发生。则保证可以打印出hello, world

2.管道的关闭一定发生在从管道中接收值之前。

因此上面的例子将<-c替换成close(c)也是可以的。

3.在无缓冲管道中,接收一定发生在发送完成前。(The closing of a channel happens before a receive that returns a zero value because the channel is closed.)

例如下面的例子将发送和接收语句互换了位置。

var c = make(chan int)
var a string

func f() {
    a = "hello, world"
    <-c
}
func main() {
    go f()
    c <- 0
    print(a)
}

如果上述例子中管道是有缓冲的(e.g., c = make(chan int, 1)) ,就无法保证一定能打印出hello,world

4.在容量为C的管道中,第k个接收发生在k+C个发送完成之前。(The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.)

第4点推广了第一点的规则。这里其实有一点绕,举个例子:第1个接收发生在1+C个发送完成之前。首先思考:第一个接收能否保证在0+C个发送完成之前?答案是不能。因为管道有C个容量的缓冲,C个发送语句发送完成前,完全可以不调用接收语句。那第1个接收发生在1+C个发送完成之前如何保证,我们知道,当管道缓冲满了之后,就无法向管道中发送,发送语句会阻塞。因此必须在接收之后发送语句才能继续执行。 第四点规则使得计数信号量可以由缓冲管道建模:管道中的数量对应了当前并发量,管道的容量对应了最大并发量。发送语句占用一个信用量,接收语句释放一个信号量。这是限制并发量的一个惯用手段。 下面的例子限制了最大并发量为3:

var limit = make(chan int, 3)

func main() {
    for _, w := range work {
        go func(w func()) {
            limit <- 1
            w()
            <-limit
        }(w)
    }
    select{}
}

同样的,我们也可以用lock和once来实现happens before语义

不正确的同步方式

即使读r可以观察到同时发生的写w的值,但这并不意味这在r之后发生的读r’可以观察到在w之前发生的写w’。例如:

var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}

这段程序可能打印出2、0 这种现象使得一些常见的方式失效。比如双重锁定检查

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func doprint() {
    if !done {
        once.Do(setup)
    }
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

这段程序不能保证print(a)时能够观察到a的值一定是hello, world。 同样的,还有一种循环等待的写法也可能有问题,例如:

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    print(a)
}

这段程序也不保证print(a)一定能打印出内容,甚至更坏的情况下无法观察到done发生 了改变,因此程序会死循环下去。 还有一种衍生版本的写法也会有问题

type T struct {
    msg string
}

var g *T

func setup() {
    t := new(T)
    t.msg = "hello, world"
    g = t
}

func main() {
    go setup()
    for g == nil {
    }
    print(g.msg)
}

即使main协程观察到了g被赋值,也不一定能观察到g.msg有值。

参考

The Go Memory Model(原文)

分类:go标签:#go内存模型

用c写php扩展的笔记

编写php扩展的步骤:

1.使用php-src中ext文件夹中的ext_skel生成项目框架 2.编辑config.m4,将其中三句话前面的dnl删除,改成下面这样。

PHP_ARG_WITH(md2pic, for md2pic support,
Make sure that the comment is aligned:
[  --with-md2pic             Include md2pic support])

3.执行phpize 4.执行./configure 5.使用make编译 6.使用make install安装扩展 7.将扩展加入php.ini中 8.使用php -m检查扩展是否正常加载

关于config.m4

config.m4相当于一个构建系统,在php扩展的开发中,我的理解就是它可以用来配置lib,include,flags等编译时的属性以及其他的一些功能。这里给出一个配置了其他的lib和include信息的config.m4文件

dnl $Id$
dnl config.m4 for extension md2pic

dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

PHP_ARG_WITH(md2pic, for md2pic support,
Make sure that the comment is aligned:
[  --with-md2pic             Include md2pic support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(md2pic, whether to enable md2pic support,
dnl Make sure that the comment is aligned:
dnl [  --enable-md2pic           Enable md2pic support])

if test "$PHP_MD2PIC" != "no"; then
  dnl Write more examples of tests here...

  dnl # --with-md2pic -> check with-path
  dnl SEARCH_PATH="/usr/local /usr"     # you might want to change this
  dnl SEARCH_FOR="/include/md2pic.h"  # you most likely want to change this
  dnl if test -r $PHP_MD2PIC/$SEARCH_FOR; then # path given as parameter
  dnl   MD2PIC_DIR=$PHP_MD2PIC
  dnl else # search default path list
  dnl   AC_MSG_CHECKING([for md2pic files in default path])
  dnl   for i in $SEARCH_PATH ; do
  dnl     if test -r $i/$SEARCH_FOR; then
  dnl       MD2PIC_DIR=$i
  dnl       AC_MSG_RESULT(found in $i)
  dnl     fi
  dnl   done
  dnl fi
  dnl
  dnl if test -z "$MD2PIC_DIR"; then
  dnl   AC_MSG_RESULT([not found])
  dnl   AC_MSG_ERROR([Please reinstall the md2pic distribution])
  dnl fi

  dnl # --with-md2pic -> add include path

  PHP_ADD_INCLUDE(src/libMultiMarkdown/include)

  LIBNAME=gd # you may want to change this
  LIBSYMBOL=gdImageCreate # you most likely want to change this 

  PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
  [
    PHP_ADD_LIBRARY_WITH_PATH(gd,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(curl,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(png,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(z,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(jpeg,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(freetype,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(m,"/usr/lib", MD2PIC_SHARED_LIBADD)
    AC_DEFINE(HAVE_MD2PICLIB,1,[ ])
  ],[
    AC_MSG_ERROR([wrong md2pic lib version or lib not found])
  ],[

  ])


  dnl
  PHP_SUBST(MD2PIC_SHARED_LIBADD)
  PHP_NEW_EXTENSION(md2pic, [md2pic.c \
  src/libMultiMarkdown/aho-corasick.c \
  src/libMultiMarkdown/beamer.c \
  src/libMultiMarkdown/char.c \
  src/libMultiMarkdown/critic_markup.c \
  src/libMultiMarkdown/d_string.c \
  src/libMultiMarkdown/epub.c \
  src/libMultiMarkdown/file.c \
  src/libMultiMarkdown/html.c \
  src/libMultiMarkdown/latex.c \
  src/libMultiMarkdown/lexer.c \
  src/libMultiMarkdown/memoir.c \
  src/libMultiMarkdown/miniz.c \
  src/libMultiMarkdown/mmd.c \
  src/libMultiMarkdown/object_pool.c \
  src/libMultiMarkdown/opendocument-content.c \
  src/libMultiMarkdown/opendocument.c \
  src/libMultiMarkdown/scanners.c \
  src/libMultiMarkdown/stack.c \
  src/libMultiMarkdown/textbundle.c \
  src/libMultiMarkdown/token_pairs.c \
  src/libMultiMarkdown/token.c \
  src/libMultiMarkdown/transclude.c \
  src/libMultiMarkdown/rng.c \
  src/libMultiMarkdown/uuid.c \
  src/libMultiMarkdown/writer.c \
  src/libMultiMarkdown/zip.c \
  src/libMultiMarkdown/parser.c \
  src/libMultiMarkdown/pic.c], $ext_shared,, [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 ] )
fi

编写php扩展的资料

我这里主要参考的是 php内核剖析这本书。 php的扩展其实也可以用c++开发。这里有一个很好的项目php-x,并且开发扩展也要容易很多。

分类:linux, php标签:#php_extentsion

HTTP协议中的缓存控制

一、总览

HTTP协议中有以下的头部字段和缓存相关(很多内容都是复制的MDN的文档)

字段名 请求头包含 响应头包含 含义 出现的协议版本
Cache-Control 是否缓存、缓存时间、缓存验证等 HTTP/1.1
Pragma 只有一种用法:Pragma: no-cache。在响应头中没有规定 HTTP/1.0
Vary 它决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复 HTTP/1.1
If-Match 在请求方法为 GET 和 HEAD 的情况下,服务器仅在请求的资源满足此首部列出的 ETag 之一时才会返回资源。而对于 PUT 或其他非安全方法来说,只有在满足条件的情况下才可以将资源上传 HTTP/1.1
If-None-Match 对于 GET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为 200 。对于其他方法来说,当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候,才会对请求进行相应的处理。 HTTP/1.1
If-Modified-Since 服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为200。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的304响应,而在 Last-Modified 首部中会带有上次修改时间。不同于If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。 HTTP/1.1
If-Unmodified-Since 只有当资源在指定的时间之后没有进行过修改的情况下,服务器才会返回请求的资源,或是接受 POST 或其他 non-safe 方法的请求。如果所请求的资源在指定的时间之后发生了修改,那么会返回 412 (Precondition Failed) 错误。 HTTP/1.1
ETag TagHTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖(“空中碰撞”) HTTP/1.1
Expires Expires 响应头包含日期/时间, 即在此时候之后,响应过期 HTTP/1.1
Last-Modified 包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。 HTTP/1.1
Date 消息生成的时间 HTTP/1.1
If-Range If-Range HTTP 请求头字段用来使得 Range 头字段在一定条件下起作用:当字段值中的条件得到满足时,Range 头字段才会起作用,同时服务器回复206 部分内容状态码,以及Range 头字段请求的相应部分;如果字段值中的条件没有得到满足,服务器将会返回 200 OK 状态码,并返回完整的请求资源。 HTTP/1.1

二、详细说明

一眼看上去,缓存相关的字段确实有很多。但是实际上,稍微理一理思路即可。 上面所有的字段都在围绕着3个点:

  • 1.是否要缓存
  • 2.缓存多久
  • 3.缓存是否有效

2.1 是否要缓存

一个HTTP的客户端(包括浏览器,以及CDN等缓存代理)如何知道当前的请求是否要缓存呢? 在Cache-Control中,有下列取值来决定是否缓存:

public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。
private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。
no-store:缓存不应存储有关客户端请求或服务器响应的任何内容。

2.2 缓存多久

源服务器上的内容可能随时发生变化,那么如何知道什么时候去检查缓存是否更新了呢?HTTP协议中有以下字段规定了一个缓存的有效期。 Cache-Control

max-age={seconds}:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
s-maxage={seconds}:覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。
max-stale[={seconds}]:表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。
min-fresh={seconds}:表示客户端希望在指定的时间内获取最新的响应。

Expire

Expires: {http-date}: 表示在http-date之后,这个缓存就过期了。如果http-date是一个无效的时间值,则代表已过期。如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age" 指令,那么 Expires 头会被忽略。

Date和Last-Modified

如果在`Cache-Control`和`Expire`都没有返回的情况下,也可以通过`Date`头和`Last-Modified`头去计算缓存的有效期。缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10。

2.3 缓存是否有效

源服务器上的内容可能随时发生变化, 那么HTTP客户端如果知道自己缓存的内容是否有效呢?这里要分几种情况:

  • 服务器的响应中有Cache-Control:must-revalidate头:当前缓存在有效时间内,此时缓存默认就是有效的。当前缓存过了有效时间,则会向服务器验证缓存是否过期。如果服务返回304,则代表缓存没有过期。
  • 服务器的响应中有Cache-Control: no-cachePragma:no-cache头:则表示每一次都要从服务器验证缓存是否过期。no-cache的优先级是要大于Pragma的

注:no-cache和must-revalidate的区别

在RFC7234中说到: “must-revalidate” 响应头指令表示一旦该响应过期,这个缓存在向源服务器成功验证之前禁止使用。在任何情况下,一个缓存都必须遵循“must-revalidate”指令;特殊情况下,如果源服务器无法连接,必须生成504(Getway Timeout)响应。 “no-cache”响应头指令表示缓存在向源服务器成功验证之前禁止使用(注:不论缓存是否过期)。如果“no-cache”指令指明了一或多个字段,缓存可以被用来响应之后的请求。但是,在没有和源服务器进行验证的情况下,任何“no-cache”中列出的字段都禁止在之后的响应中被发送。这使得源服务器可以阻止某些字段被重复使用,但是仍然可以缓存响应的其他部分。 “no-cache”中的字段不仅仅限于http1.1协议中列举出来的字段。字段名是大小写不敏感的。使用双引号包围。 因此个人认为,在某些时候max-age=0;must-revalidate 可以等同于 no-cache。

那么这个验证机制是什么样的?也分几种情况

  • 根据文件指纹ETag:在服务器返回了一个文件的ETag的情况下,HTTP客户端可以根据If-None-MatchIf-Match来向服务器验证当前缓存是否过期。
  • 根据文件修改时间:在服务器返回了Last-Modified的情况下,HTTP客户端可以根据If-Unmodified-SinceIf-Modified-Since来向服务器验证当前缓存是否过期。
  • 一个比较特殊的If-RangeIf-Range通常出现在分段请求当中,用来分段请求的资源主体是否发生了变化。它的值既可以是etag,也可以是GMT时间戳。

注:因为Last-Modified精确到秒,在精确度上比ETag低,所以应该以ETag为主。 2.4 关于vary字段 上面说了三个点,但是没有涉及到vary字段。vary和缓存并不是直接相关的。取一段MDN的说明:

Vary 是一个HTTP响应头部信息,它决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复。它被服务器用来表明在 content negotiation algorithm(内容协商算法)中选择一个资源代表的时候应该使用哪些头部信息(headers). 在响应状态码为 304 Not Modified 的响应中,也要设置 Vary 首部,而且要与相应的 200 OK 响应设置得一模一样。

举个例子,如果服务器返回的网页是分手机版和电脑版的,一般我们会根据user-agent来判断浏览器是手机浏览器还是电脑上的浏览器。假设有一个中间代理的请求:

手机用户1请求index.html---------->中间代理------------>源服务器
电脑用户1请求index.html---------->中间代理------------>源服务器

在电脑用户1请求index.html时,中间代理会向原服务器请求还是直接使用本地缓存的副本呢? 如果原服务器在第一次请求时响应头中有vary:user-agent则会重新请求。因为两次请求的user-agent是不同的,因此缓存不能被重复使用。但是如果没有指定则使用本地缓存作为响应。


参考文档

HTTP缓存控制小结 HTTP 协议中 Vary 的一些研究 http://www.cnblogs.com/chyingp/p/no-cache-vs-must-revalidate.html HTTP缓存 HTTP Caching | MDN Cache-Control | MDN Pragma | MDN Vary | MDN If-Match | MDN If-None-Match | MDN If-Modified-Since | MDN If-Unmodified-Since | MDN ETag | MDN Expires | MDN Last-Modified | MDN If-Range | MDN

分类:网络协议标签:#HTTP协议 #缓存控制

从php-fpm解析FastCGI协议

这是一篇类似于开发笔记的文章,从php-fpm与nginx的tcp请求中,去理解FastCGI协议,因此不会详细的阐述FastCGI协议到底是什么样的。

从一段抓包说起

"No.","Time","Source","Destination","Protocol","Length","Info"
"431","22.975896289","127.0.0.1","127.0.0.1","TCP","76","55928  >  9000 [SYN] Seq=0 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=0 WS=128"
"432","22.975910047","127.0.0.1","127.0.0.1","TCP","76","9000  >  55928 [SYN, ACK] Seq=0 Ack=1 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=1732184618 WS=128"
"433","22.975920352","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [ACK] Seq=1 Ack=1 Win=43776 Len=0 TSval=1732184618 TSecr=1732184618"
"434","22.975948796","127.0.0.1","127.0.0.1","TCP","1356","55928  >  9000 [PSH, ACK] Seq=1 Ack=1 Win=43776 Len=1288 TSval=1732184618 TSecr=1732184618"
"435","22.975953739","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [ACK] Seq=1 Ack=1289 Win=174720 Len=0 TSval=1732184618 TSecr=1732184618"
"452","23.068068706","127.0.0.1","127.0.0.1","TCP","660","9000  >  55928 [PSH, ACK] Seq=1 Ack=1289 Win=174720 Len=592 TSval=1732184710 TSecr=1732184618"
"453","23.068076923","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [ACK] Seq=1289 Ack=593 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710"
"454","23.068097717","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [FIN, ACK] Seq=593 Ack=1289 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710"
"455","23.068153021","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [FIN, ACK] Seq=1289 Ack=594 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710"
"456","23.068163150","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [ACK] Seq=594 Ack=1290 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710"

为了抓这段包,需要将php-fpm中的监听地址改成tcp socket。注意:tcp socket的性能远远低于unix socket。 可以看到,这里面除了tcp的握手和断开以及应答部分,有PSH标志的是FastCGI的具体协议内容。可以看到nginx给php-fpm发送了一段数据,之后php-fpm进行响应。FastCGI协议就是这样简单的使用tcp协议,使得Web Server可以转发HTTP请求到FastCGI应用程序上,具体的协议内容可以参考FastCGI规范中文翻译

php-fpm在单次请求结束后,会主动断开连接,而在FastCGI协议中,明确说明单次连接是可以复用的。

https://stackoverflow.com/questions/43280573/whether-the-connection-between-php-fpm-and-nginx-by-fast-cgi-are-persistent-kee 这个链接中有关于nginx和php-fpm连接释放的相关说明。 web server 可以将关闭权限委托给php-fpm,这样php-fpm在每次请求结束后就会关闭。 将关闭权限委托给php-fpm的好处就是不会因为连接的占用导致子进程不释放。但是不断的建立和断开连接也会影响性能。

当前php-fpm和nginx的主动断开连接是否会影响性能

会影响性能,但是并不推荐保持连接。 如果希望php-fpm不主动关闭连接,可以使用以下设置:

Syntax: fastcgi_keep_conn on | off;
Default:    
fastcgi_keep_conn off;
Context:    http, server, location
This directive appeared in version 1.1.4.

记得在upstream中使用keepalive选项

upstream backend {
    server 127.0.0.1:9000
    keepalive 20
}

但是缺点也很明显,如果用户请求一直和nginx保持连接,那么nginx也不会释放该与php-fpm的连接。这样会一直占用php-fpm的子进程不释放。当达到php-fpm的最大子进程时,就会拒绝其他的请求。 同时需要注意的是,如果nginx和php-fpm都在本地,不断的重新建立连接的影响是很小的。因此并不推荐将fastcgi_keep_conn选项打开。 这里是一些压测数据(pm.max_children 设置为20,这里只使用20个并发): 测试命令

ab -k -n 100000 -c 20 http://localhost/php/index.php

在主动断开FastCGI连接的情况下:

Server Software:        nginx/1.13.3
Server Hostname:        localhost
Server Port:            80

Document Path:          /php/index.php
Document Length:        60 bytes

Concurrency Level:      20
Time taken for tests:   28.334 seconds
Complete requests:      100000
Failed requests:        0
Keep-Alive requests:    0
Total transferred:      22900000 bytes
HTML transferred:       6000000 bytes
Requests per second:    3529.39 [#/sec] (mean)
Time per request:       5.667 [ms] (mean)
Time per request:       0.283 [ms] (mean, across all concurrent requests)
Transfer rate:          789.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0      14
Processing:     1    5   2.4      5     212
Waiting:        0    5   2.4      5     212
Total:          1    6   2.5      5     212

在不断开连接的情况下: 测试一直没法正常完成,部分请求会超时。 因此FastCGI是没有必要保持连接的,这会大大降低并发度。

benchmark 压测,请求直接超时退出

ab -k -c 100 -n 10000 "http://localhost/php/index.php" php-fpm有一个子进程数量的限制,在并发过高时,没有办法为每一个请求分配一个子进程,导致请求一直在等待,直至超时退出。

unix socket和tcp socket的区别

unix socket相对于tcp socket来说,性能会提升很多。 unix socket虽然也有个socket,但是和网络一点关系都没有。unix socket是进程间的通信。

unix socket不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

实现FastCGI协议时,tcp连接中读到EOF代表了什么

在写代码过程中,tcp连接读到了EOF。从表面上来看,是读到了流的结束。但也意味着对端至少关闭了写通道。这是因为php-fpm读到了它无法理解的请求,因此直接关闭了连接。

在开发过程中,遇到了php-fpm进程不释放的问题

在BeginRequestRecord中,将flags置为1,这样与fastcgi的连接会一直保持。但是我在tcp连接中读到EOF时,却没有释放这个连接。因此这个连接会占用一个php-fpm子进程不会释放。只要手动释放这个连接即可,或者将flags设为0。

参考

FastCGI协议分析 Nginx支持PHP的PATHINFO模式配置分析 Linux下的IPC-UNIX Domain Socket

分类:php标签:#php-fpm #fastcgi

FastCGI 规范中文翻译

原文地址:https://fastcgi-archives.github.io/FastCGI\_Specification.html

1.简介

FastCGI 是一种对 CGI 的开放扩展,在不改变 Web 服务的前提下,为所有的网络应用程序提供了很高的性能。 这个规范的目的很小:从应用程序角度来看,指定了一个 FastCGI 应用程序和一个支持 FaseCGI的 Web 服务之间的接口。许多 Web 服务的特性和 FastCGI相关,例如,应用程序管理工具,与Web服务器接口的应用程序无关,此处不再赘述。 这个规范适用于Unix(更确切的说,适用于支持 Berkeley Sockets 的 POSIX 系统)。规范的大部分是一个简单的通信协议,它独立于字节序,并将扩展到其他系统。 我们将通过比较 FastCGI 和常规的 CGI/1.1 的 Unix 实现来介绍它。 FastCGI 是被设计用于支持常驻内存的应用程序进程,例如,应用程序服务。常规的 CGI/1.1 的 Unix 实现的主要不同之处在于,CGI 会创建一个应用程序进程,响应一个请求之后就会退出。 FastCGI进程的初始状态比CGI / 1.1进程的初始状态更简洁,因为 FastCGI 进程在初始化时没有开始与任何事物连接。它没有常规的打开标准输入(stdin)、输出(stdout)和错误(stderr)流,并且它不会通过环境变量接受大量信息。在一个 FastCGI 进程中,关键的初始状态是监听一个 socket,这个socket会接收来自 Web 服务器的连接。 一个 FastCGI 进程在它监听的 socket 上接收一个连接时,进程会执行一个简单的协议去接收和发送数据。这个协议主要有两个目的。第一,在多个独立的 FastCGI 请求中,这个协议复用一个传输连接。这支持那些使用了事件驱动或多线程编程技术来处理并发请求的应用程序。第二,对于每一个请求,这个协议在每个传输方向上都提供了多个独立的数据流。这样,例如,stdout 和 stderr 数据都通过单个传输连接从应用程序传递到Web服务器,而不是像 CGI/1.1 那样需要单独的管道。 一个 FastCGI 应用程序扮演了明确定义的角色之一。我们最熟悉的是响应器角色,应用程序从一个 HTTP 请求中接收所有的信息,之后生成一个 HTTP 响应;这正是 CGI/1.1 程序所扮演的角色。第二个角色是认证器,应用程序从一个 HTTP 请求中接收所有的信息,之后生成一个认证通过/不通过的决定。第三个角色是过滤器,应用程序从一个 HTTP 请求中接收所有的信息,加上一个 Web 服务器中存储的额外的文件数据流,然后生成一个“过滤的”版本的数据流作为 HTTP 响应。这个框架是可扩展的,因此更多的 FastCGI 角色可以在以后定义。 在本说明书的其余部分中,术语“ FastCGI 应用程序”,“应用程序进程”或“应用程序服务器”在不会引起混淆的情况下缩写为“应用程序”

2.初始处理状态

2.1 参数列表

默认情况下,Web 服务器创建一个包含单个元素的参数列表,应用程序的名字会被当作可执行文件路径名的最后一部分。Web 服务器可能提供了一种方法来指明一个不同的应用程序名称,或者一个更详细的参数列表。 注意,由 Web 服务器执行的文件可能是一个解释性脚本(一个文本文件,以#!开头),这种情况下,应用程序参数的构建如在execve联机帮助页中所述那样。

2.2 文件描述符

Web 服务器在应用程序开始执行时打开单个文件描述符FCGI_LISTENSOCK_FILENO。这个描述符指向由 Web 服务器创建的监听的socket。 FCGI_LISTENSOCK_FILENO 等价于 STDIN_FILENO 。标准的描述符 STDOUT_FILENO 和 STDERR_FILENO 在应用程序开始执行时被关闭。判断一个应用程序是被 CGI 还是 FastCGI 调用的可靠方法是:调用 getpeername(FCGI_LISTENSOCK_FILENO),返回 -1 并将errno设置为ENOTCONN的就是 FastCGI 程序。 Web服务器选择可靠的传输,Unix流管道(AF_UNIX)或TCP/IP(AF_INET),隐含在FCGI_LISTENSOCK_FILENO套接字的内部状态中。

2.3 环境变量

Web 服务器可以使用环境变量去传递参数给应用程序。这个规范定义了一个这样的变量:FCGI_WEB_SERVER_ADDRS。我们期待随着规范的演变,会有更多的变量会被传递。Web 服务器可以提供一种方法去绑定其他的环境变量,比如 PATH 变量。

2.4 其他状态

Web 服务器可以提供一种方法去指明一个应用程序的初始处理状态的其他部分,比如优先级,用户 ID,用户组 ID,根目录,以及进程的工作目录。

3.协议基础

3.1 符号

我们使用 C 语言符号去定义协议信息的格式。所有的结构体元素都使用 unsigned char 类型定义,并安排使ISO C编译器以常规方式将它们排列,没有填充。在结构体中,第一个字节会被第一个传输,第二个会被第二个传输,以此类推。 我们使用两个公约来概括我们的定义。 第一,当两个相邻的结构体组件名称相同时,除了后缀“B1”和“B0”,这意味着这两个组件可以被视为单个数字,计算为B1<<8 + B0。 第二,我们扩展 C 的结构体,允许以下的形式

struct {
    unsigned char mumbleLengthB1;
    unsigned char mumbleLengthB0;
    ... /* other stuff */
    unsigned char mumbleData[mumbleLength];
};

这代表着一个变长的结构体,它的长度是由前面的组件的值决定的。

3.2 接受传输连接

一个 FastCGI 应用程序在由文件描述符 FCGI_LISTENSOCK_FILENO 引用的 socket 上调用 accept() 去接收一个新的传输连接。如果 accept() 成功了,FCGI_WEB_SERVER_ADDRS 环境变量被绑定,应用程序立即执行以下的特殊操作:

  • FCGI_WEB_SERVER_ADDRS: 这个值是 Web 服务器的有效的 ip 地址列表。
  • 如果 FCGI_WEB_SERVER_ADDRS 绑定了,应用程序检查新连接的对等 IP 地址是否在列表中。如果检查失败了(包括连接没有使用 TCP/IP 这种可能性),应用程序关掉连接来响应。
    • FCGI_WEB_SERVER_ADDRS 是由英文逗号分割的 IP 地址列表。每一个 IP 地址是由点号分割的4个0~255内的数字组成。例如:FCGI_WEB_SERVER_ADDRS=199.170.183.28,199.170.183.71 。

应用程序可以接收多个并发传输连接,但是它不一定需要这样做。

3.3 记录

应用程序使用一个简单的协议从 Web 服务器获取请求并执行。协议的具体内容视应用程序的角色而定,但是一般来说,Web 服务器首先发送参数和其他数据到应用程序,之后应用程序发送结果数据给 Web 服务器,最终应用程序告诉 Web 服务器请求处理已经结束。 所有通过传输连接的数据都是在 FastCGI 记录(records)里的。FastCGI 记录完成两件事。第一,记录在多个独立的请求之间复用传输连接。这种复用支持使用事件驱动模型或多线程技术来处理并发请求的应用程序。第二,在同一个请求中,记录提供了在不同方向上多个独立的数据流。这样,stdout 和 stderr 可以使用同一个传输连接来传输,而不是需要不同的连接。

        typedef struct {
            unsigned char version;
            unsigned char type;
            unsigned char requestIdB1;
            unsigned char requestIdB0;
            unsigned char contentLengthB1;
            unsigned char contentLengthB0;
            unsigned char paddingLength;
            unsigned char reserved;
            unsigned char contentData[contentLength];
            unsigned char paddingData[paddingLength];
        } FCGI_Record;

一个 FastCGI 记录包含一个定长的前缀,以及变长的内容和填充字节。一条记录包含7个部分:

  • 版本号(version):指定 FastCGI 协议的版本号。这个规范文档的版本号是 FCGI_VERSION_1。
  • 类型(type):指定这条记录的类型。例如,记录的功能函数。具体的记录类型和功能函数在之后的章节有详细介绍。
  • 请求ID(requestId):指定这条记录属于哪个 FastCGI 请求。
  • 内容长度(contentLength):在contentData部分存储的字节数。
  • 填充长度(paddingLength):在paddingData部分存储的字节数。
  • 内容数据(contentData):在0到65535字节之间的数据,根据记录类型进行解释。
  • 填充数据(paddingData):0到255个字节的数据,被忽略。

我们使用宽松的C struct初始化语法来指定常量FastCGI记录。我们省略了版本号部分,忽略填充部分,并将requestId视为一个数字。因此 {FCGI_END_REQUEST, 1, {FCGI_REQUEST_COMPLETE,0} 是一个 type == FCGI_END_REQUEST, requestId == 1, and contentData == {FCGI_REQUEST_COMPLETE,0} 的记录。

Padding

协议允许发送者填充发送的记录,然后要求接收者解释 paddingLength,跳过 paddingData。Padding 允许发送者保持数据对齐,达到更高效的数据处理。使用X窗口系统协议的经验显示了这种对齐的性能优势。 我们推荐记录的长度是8字节的整数倍。一个 FastCGI 的固定长度部分正好是8个字节。

处理请求ID

Web服务器重用 FastCGI 的请求ID;在一个给定的传输连接上,应用程序追踪每个请求 ID 的当前状态。当应用程序收到一条记录{FCGI_BEGIN_REQUEST, R, …},一个请求 ID R 置为活跃状态。当应用程序发送一条记录 {FCGI_END_REQUEST, R, …} 给 Web 服务器时,请求 ID R置为非活跃状态。 当请求 ID R 是非活跃的,应用程序会忽略所有的 requestId R 的记录,除了如上所述的 FCGI_BEGIN_REQUEST 记录。 Web 服务器试图保持 FastCGI 请求 ID 是一个很小的数字。这样应用程序就可以使用一个很短的数组来追踪请求 ID 的状态,而不是一个长的数组或是一个哈希表。应用程序可以选择在同一时间仅仅接收一条请求。这样应用程序可以简单的根据当前连接请求 ID 来检查 requestId。

记录类型

有两种阐述 FastCGI 记录类型的方法。 第一个区别是管理记录和应用程序记录。管理记录包含非特定于任何 Web 服务器请求的信息,例如有关应用程序的协议功能的信息。应用程序记录包含有关requestId组件标识的特定请求的信息。 第二个区别是离散记录和流记录。离散记录本身包含有意义的数据单元。流记录是流的一部分,例如,一系列的0或更多的非空记录(length != 0),之后紧跟着一个空记录(length 0)。流记录的 contentData 部分是一连串的字节组成。这个字节序列就是流的值。因此流的值是独立于它包含多少条记录,以及它的字节在非空记录中如何划分。 这两点解释是不相关的。在当前版本的 FastCGI 协议定义的记录类型中,所有的管理记录类型都是离散的记录类型,几乎所有的应用程序记录类型都是流记录类型。但是有三个应用程序记录类型是离散的,也不能保证在之后的版本中,一个管理记录类型是流式的。

3.4 键值对

在这些角色中,FastCGI 应用程序需要读写边长值的不同数字。因此采用一个标准格式去编码一个键值对是有用的。 FastCGI 发送的键值对格式:键长,值长,键,值。小于等于127字节可以用一个字节编码,大于127字节的用4个字节编码:

typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair14;

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair44;

第一个字节的高位表示长度的编码。高位是0表示一个字节编码,高位是1表示4个字节编码。 这样的键值对格式允许发送方发送二进制数据,使得接受者可以立即分配正确大小的存储空间,即使是很大的值。

3.5 关闭传输连接

Web 服务器控制传输连接的生命周期。Web 服务器可以在没有活跃请求时关闭连接。或者 Web 服务器可以将关闭权限委托给应用程序(请参阅FCGI_BEGIN_REQUEST)。在这种情况下,应用程序在指定的请求之后关闭连接。 这种灵活设计可以包容不同的应用程序风格。简单的应用程序一次只处理一个请求,每个请求都会建立一个连接。更复杂的应用将会处理并发请求,一个和多个传输连接,会长时间保持传输连接。 通过在完成写入响应时关闭传输连接,简单的应用程序可以显着提升性能。Web服务器需要控制长期连接的连接生存期。 当应用程序关闭连接或发现连接已关闭时,应用程序将启动新连接。

4.管理记录类型

4.1 FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT

Web 服务器可以查询应用程序中的特定变量。服务器通常会在应用程序启动时执行查询,以便自动化系统配置的某些方面。 应用程序接受一个查询,比如{FCGI_GET_VALUES, 0, …}。FCGI_GET_VALUES 记录的 contentData 部分包含一系列的具有空值的键值对。 应用程序通过发送一个带有值的记录{FCGI_GET_VALUES_RESULT, 0, …}来响应。如果应用程序不理解在查询中的某个变量名,它会从响应中忽略该名称。FCGI_GET_VALUES 被设计成允许一个开放结束集合的变量。初始集合变量提供信息去帮助服务器操作应用,以及连接管理:

  • FCGI_MAX_CONNS:应用程序接收的并发传输连接的最大值。比如,1或10。
  • FCGI_MAX_REQS:应用程序接收的并发请求的最大值。比如1或50。
  • FCGI_MPXS_CONNS:如果应用程序不复用连接,这个值是0(例如,一个请求一个连接)。否则是1。

4.2 FCGI_UNKNOWN_TYPE

管理记录类型集可能会在此协议的未来版本中增长。为了提供这种演变,该协议包括 FCGI_UNKNOWN_TYPE 管理记录。当应用程序收到其类型T不理解的管理记录时,应用程序将使用{FCGI_UNKNOWN_TYPE,0,{T}}进行响应。 FCGI_UNKNOWN_TYPE记录的contentData部分具有以下形式:

typedef struct {
    unsigned char type;    
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

类型组件是无法识别的管理记录的类型。

5.应用的记录类型

5.1 FCGI_BEGIN_REQUEST, FCGI_GET_VALUES_RESULT

Web服务发送一个 FCGI_BEGIN_REQUEST 记录来开始一个请求。 一个 FCGI_BEGIN_REQUEST 记录的 contentData 部分有以下形式:

typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

角色组件设置Web服务器期望应用程序扮演的角色。当前定义的角色是:

  • FCGI_RESPONDER
  • FCGI_AUTHORIZER
  • FCGI_FILTER

角色定义具体在第六章描述。 flags部分包含一个控制连接关闭的位:

  • flags & FCGI_KEEP_CONN: 如果是0,应用程序在响应请求后关闭连接。如果不是0,应用程序在响应请求后不关闭连接;Web 服务器保持对连接的管理权限。

5.2 键值对流:FCGI_PARAMS, FCGI_RESULTS

FCGI_PARAMS

是一种流记录类型,用于从Web服务器向应用程序发送键值对。名称 - 值对一个接一个地沿着流向下发送,没有指定的顺序。

5.3 字节流:FCGI_STDIN, FCGI_DATA, FCGI_STDOUT, FCGI_STDERR

FCGI_STDIN

是一种流记录类型,用于从Web服务器向应用程序发送任意数据。 FCGI_DATA是第二个流记录类型,用于向应用程序发送其他数据。 FCGI_STDOUT和FCGI_STDERR是流记录类型,用于分别从应用程序向Web服务器发送任意数据和错误数据。

5.4 FCGI_ABORT_REQUEST

Web服务器发送 FCGI_ABORT_REQUEST 记录以中止请求。收到{FCGI_ABORT_REQUEST,R}后,应用程序会尽快响应{FCGI_END_REQUEST,R,{FCGI_REQUEST_COMPLETE,appStatus}}。这确实是来自应用程序的响应,而不是来自FastCGI库的低级别确认。 当HTTP客户端关闭其传输连接而来自客户端FastCGI请求正运行到一半时,Web服务器将中止FastCGI请求。这种情况似乎不太可能,大多数FastCGI请求的响应时间都很短,如果客户端速度很慢,Web服务器会提供输出缓冲。但FastCGI应用程序可能与其他系统通信有延迟或正执行服务器推送。 当Web服务器未通过传输连接复用请求时,Web服务器可以通过关闭请求的传输连接来中止请求。但是对于多路复用的请求,关闭传输连接会导致中止连接上的所有请求,这是一种令人遗憾的结果。

5.5 FCGI_END_REQUEST

应用程序发送FCGI_END_REQUEST记录以终止请求,既可能因为应用程序已处理请求,也可能应用程序已拒绝该请求。 FCGI_END_REQUEST记录的contentData部分具有以下形式:

typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;

appStatus组件是应用程序级状态代码。每个角色都在文档上记录了它对appStatus的使用。 protocolStatus组件是协议级状态代码;可能的protocolStatus值是:

  • FCGI_REQUEST_COMPLETE:正常的请求结束。
  • FCGI_CANT_MPX_CONN:拒绝新请求。当Web服务器通过一个连接将并发请求发送到旨在每个连接一次处理一个请求的应用程序时,就会发生这种情况。
  • FCGI_OVERLOADED:拒绝新请求。当应用程序耗尽某些资源时会发生这种情况,例如:数据库连接。
  • FCGI_UNKNOWN_ROLE:拒绝新请求。当Web服务器指定了应用程序未知的角色时,会发生这种情况。

6.角色

6.1 角色协议

角色协议仅包括具有应用程序记录类型的记录。它们都使用流传输几乎所有的数据。 为了使协议可靠并简化应用程序编程,角色协议被设计使用几乎连续的编组(nearly sequential marshalling.)。具有严格连续编组(strictly sequential marshalling)的协议中,应用程序接收其第一个输入,然后是第二个输入,等等。直接所有数据接受完成。类似地,应用程序发送它的第一个输出,然后发送它的第二个输出,直到它发送它们全部。输入不相互交错,输出不相互交错。 连续编组规则对某些FastCGI角色限制太多。因为 CGI 程序没有时间上的限制,可以同时使用 stdout和stderr。因此角色协议使用FCGI_STDOUT 和FCGI_STDERR来允许这两个流交错。 所有角色协议都使用FCGI_STDERR流,就像在传统应用程序编程中使用stderr一样:以可理解的方式报告应用程序级错误。使用FCGI_STDERR流始终是可选的。如果应用程序没有要报告的错误,它将不发送FCGI_STDERR记录或一个零长度FCGI_STDERR记录。 当角色协议要求传输FCGI_STDERR以外的流时,即使流是空的,也总是传输至少一个流类型的记录 再次为了可靠的协议和简化的应用程序编程,角色协议被设计成几乎连续的编组(nearly sequential marshalling.)。在真正的请求-响应协议中,应用程序在发送其第一个输出记录之前接收其所有输入记录。请求-响应协议不允许流水线操作。 请求-响应规则对某些FastCGI角色限制太多;毕竟,在开始写stdout之前,CGI程序不限制读取所有stdin。因此一些角色协议允许这种特定的可能性。首先,应用程序接收除最终流输入之外的所有输入。当应用程序开始接收最终流输入时,它可以开始写入其输出。 当角色协议使用FCGI_PARAMS传输文本值时,例如CGI程序从环境变量中获取的值,值的长度不包括终止空字节,且值本身不包含空字节。需要提供environ(7)格式键值对的应用程序必须在键和值之间插入等号,并在值后附加空字节。 角色协议不支持CGI的非解析头功能。FastCGI应用程序使用 Status 和Location CGI头设置响应状态。

6.2 响应器

一个响应器角色的FastCGI应用程序与CGI / 1.1程序具有相同的目的:它接收与HTTP请求关联的所有信息并生成HTTP响应。 下面将解释响应器如何模拟CGI/1.1的每个元素:

  • 响应器应用程序通过FCGI_PARAMS从Web服务器接收CGI/1.1环境变量。
  • 接下来,响应器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前,应用程序从该流接收最多CONTENT_LENGTH个字节。 (仅当HTTP客户端无法提供它们时,应用程序才会收到少于CONTENT_LENGTH个字节,例如因为客户端崩溃了。)
  • 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器,通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些,而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前,完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前,结束读取FCGI_STDIN。
  • 发送所有stdout和stderr数据后,响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE,将appStatus组件设置状态代码后,CGI程序通过exit系统调用返回。

响应者执行更新,例如实现POST方法时,应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH进行比较,如果两个数字不相等则中止更新。

6.3 授权器

授权器FastCGI应用程序接收与HTTP请求相关的所有信息,并生成授权/未授权的决策。在授权决策的情况下,授权者还可以将键值对与HTTP请求相关联;在做出未经授权的决定时,授权器会向HTTP客户端发送完整的响应。 由于CGI / 1.1定义了一种表示与HTTP请求相关的信息的完美方法,因此授权器使用相同的表示:

  • 授权器应用程序通过FCGI_PARAMS流从Web服务器接收HTTP请求信息,与响应器的格式相同。Web服务器不发送CONTENT_LENGTH,PATH_INFO,PATH_TRANSLATED和SCRIPT_NAME头。
  • 授权器应用程序以与Responder相同的方式发送stdout和stderr数据。CGI/1.1响应状态指明了请求的 处置方式。如果应用程序发送状态200(OK),则Web服务器允许访问。根据其配置,Web服务器可以继续进行其他访问检查,包括对其他授权器的请求。

授权器应用程序的200响应可能包括名称以Variable-为前缀的标头。这些头将应用程序中的键值对传递给Web服务器。例如,响应头:

Variable-AUTH_METHOD: database lookup

使用名称AUTH-METHOD传输值“database lookup”。服务器将这些键值对与HTTP请求相关联,并将它们包含在处理HTTP请求时执行的后续CGI或FastCGI请求中。当应用程序提供200响应时,服务器会忽略名称不带Variable-前缀的响应头,并忽略任何响应内容。 对于除“200”(OK)以外的授权器响应状态值,Web服务器拒绝访问并将响应状态,标头和内容发送回HTTP客户端。

6.4 过滤器

过滤器FastCGI应用程序接收与HTTP请求相关的所有信息,以及来自存储在Web服务器上的文件的额外数据流,并生成数据流的“过滤”版本作为HTTP响应。

过滤器的功能类似于将数据文件作为参数的响应器程序。区别在于使用过滤器,数据文件和过滤器本身都可以使用Web服务器的访问控制机制进行访问控制,将数据文件名称作为参数的响应程序必须对数据文件执行自己的访问控制检查。

过滤器采取的步骤类似于响应者的步骤。服务器首先向Filter提供环境变量,然后是标准输入(通常是POST数据),最后是数据文件输入:

- 与响应器一样,过滤器应用程序通过FCGI_PARAMS从Web服务器接收键值对。过滤器应用程序接收两个专属的变量:FCGI_DATA_LAST_MOD和FCGI_DATA_LENGTH。
- 接下来,过滤器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前,应用程序从该流接收最多CONTENT_LENGTH个字节。(仅当HTTP客户端无法提供它们时,应用程序才会收到少于CONTENT_LENGTH个字节,例如因为客户端崩溃了。)

- 接下来,过滤器应用程序通过FCGI_DATA从Web服务器接收文件数据。该文件的最后修改时间(表示为1970年1月1日UTC以来的整数秒)为FCGI_DATA_LAST_MOD;应用程序可以查阅此变量并从缓存中进行响应而无需读取文件数据。在接收流结束指示之前,应用程序从该流中读取最多FCGI_DATA_LENGTH个字节。 - 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器,通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些,而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前,完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前,结束读取FCGI_DATA。 - 发送所有stdout和stderr数据后,响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE,将appStatus组件设置状态代码后,CGI程序通过exit系统调用返回。 过滤器应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH和FCGI_DATA上的FCGI_DATA_LENGTH进行比较。如果数字不匹配且过滤器是一次查询,过滤器响应应提供数据丢失的指示。如果数字不匹配且过滤器是一次更新,则过滤器应中止更新。

7.错误

FastCGI应用程序以零状态退出,表示它是故意终止的,例如为了执行原始形式的垃圾收集。FastCGI应用程序以非零状态退出,表示它崩溃了。Web服务器或其他应用程序管理器如何响应以零或非零状态退出的应用程序超出了本规范的范围。 Web服务器可以通过发送SIGTERM来请求FastCGI应用程序退出。如果应用程序忽略SIGTERM,则Web服务器可以使用SIGKILL。 astCGI应用程序使用FCGI_STDERR流和FCGI_END_REQUEST记录的appStatus部分报告应用程序级错误。在许多情况下,将通过FCGI_STDOUT流直接向用户报告错误。 Unix上,应用程序向syslog报告较低级别的错误,包括FastCGI协议错误和FastCGI环境变量中的语法错误。根据错误的严重程度,应用程序可以继续或以非零状态退出。

8.类型和常量

/*
 * Listening socket file number
 */
#define FCGI_LISTENSOCK_FILENO 0

typedef struct {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} FCGI_Header;

/*
 * Number of bytes in a FCGI_Header.  Future versions of the protocol
 * will not reduce this number.
 */
#define FCGI_HEADER_LEN  8

/*
 * Value for version component of FCGI_Header
 */
#define FCGI_VERSION_1           1

/*
 * Values for type component of FCGI_Header
 */
#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

/*
 * Value for requestId component of FCGI_Header
 */
#define FCGI_NULL_REQUEST_ID     0

typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

typedef struct {
    FCGI_Header header;
    FCGI_BeginRequestBody body;
} FCGI_BeginRequestRecord;

/*
 * Mask for flags component of FCGI_BeginRequestBody
 */
#define FCGI_KEEP_CONN  1

/*
 * Values for role component of FCGI_BeginRequestBody
 */
#define FCGI_RESPONDER  1
#define FCGI_AUTHORIZER 2
#define FCGI_FILTER     3

typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;

typedef struct {
    FCGI_Header header;
    FCGI_EndRequestBody body;
} FCGI_EndRequestRecord;

/*
 * Values for protocolStatus component of FCGI_EndRequestBody
 */
#define FCGI_REQUEST_COMPLETE 0
#define FCGI_CANT_MPX_CONN    1
#define FCGI_OVERLOADED       2
#define FCGI_UNKNOWN_ROLE     3

/*
 * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records
 */
#define FCGI_MAX_CONNS  "FCGI_MAX_CONNS"
#define FCGI_MAX_REQS   "FCGI_MAX_REQS"
#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS"

typedef struct {
    unsigned char type;    
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

typedef struct {
    FCGI_Header header;
    FCGI_UnknownTypeBody body;
} FCGI_UnknownTypeRecord;

9.参考文献

The WWW Common Gateway Interface at W3C

A.表:记录类型的属性

下表列出了所有记录类型,并指出了每种记录的属性:

  • WS->App: 此类记录只能由Web服务器发送到应用程序。其他类型的记录只能由应用程序发送到Web服务器。
  • management: 此类型的记录包含不是特定于Web服务器请求的信息,并使用的的请求ID。其他类型的记录包含特定于请求的信息,不能使用空的请求ID。
  • stream: 此类型的记录形成一个流,由具有空contentData的记录终止。其他类型的记录是离散的;每个都带有一个有意义的数据单元。
                               WS->App   management  stream

        FCGI_GET_VALUES           x          x
        FCGI_GET_VALUES_RESULT               x
        FCGI_UNKNOWN_TYPE                    x

        FCGI_BEGIN_REQUEST        x
        FCGI_ABORT_REQUEST        x
        FCGI_END_REQUEST
        FCGI_PARAMS               x                    x
        FCGI_STDIN                x                    x
        FCGI_DATA                 x                    x
        FCGI_STDOUT                                    x 
        FCGI_STDERR                                    x     

B. 典型的协议消息流

示例的其他符号约定:

  • 流记录的contentData(FCGI_PARAMS,FCGI_STDIN,FCGI_STDOUT和FCGI_STDERR)表示为字符串。以“…”结尾的字符串太长而无法显示,因此仅显示前缀。
  • 发送到Web服务器的消息相对于从Web服务器接收的消息缩进。
  • 消息按应用程序所经历的时间顺序显示。
  1. 一个没有stdin数据的简单请求,以及一个成功的响应:

(注:\013\016这里是8进制编码。这里所有的记录都省略了Version和PaddingData,因此类似于FCGI_BEGIN_REQUEST代表Type,1代表RequestId)

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

2.与示例1类似,但这次使用stdin上的数据。 Web服务器选择使用比以前更多的FCGI_PARAMS记录发送参数:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SER"}
{FCGI_PARAMS,          1, "VER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, "quantity=100&item=3047936"}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

3.与示例1类似,但这次应用程序检测到错误。应用程序将消息记录到stderr,将页面返回给客户端,并将非零退出状态返回给Web服务器。应用程序选择使用更多FCGI_STDOUT记录发送页面:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<ht"}
    {FCGI_STDERR,      1, "config error: missing SI_UID\n"}
    {FCGI_STDOUT,      1, "ml>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_STDERR,      1, ""}
    {FCGI_END_REQUEST, 1, {938, FCGI_REQUEST_COMPLETE}}

4.示例1的两个实例,复用到单个连接上。第一个请求比第二个请求更难,因此应用程序不按顺序完成请求:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_BEGIN_REQUEST,   2, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          2, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n"}

{FCGI_PARAMS,          2, ""}
{FCGI_STDIN,           2, ""}

    {FCGI_STDOUT,      2, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      2, ""}
    {FCGI_END_REQUEST, 2, {0, FCGI_REQUEST_COMPLETE}}
    {FCGI_STDOUT,      1, "<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
分类:FastCGI协议

redis使用总结

1.redis的使用场景

项目中存在多种redis的使用场景

1.1 场景一:缓存(key,value)

缓存是一个非常常见的场景,在项目中,可以将mysql中的一部分数据查询的结果缓存到redis中,以此来获取更快的查询速度。 以用户信息缓存为例,我的做法如下:

  • 1.设定用户信息缓存的规则,比如userinfo128代表用户id为128的用户信息缓存。
  • 2.在query语句执行时,先检查userinfo128是否存在,如果存在,直接取出结果返回,否则执行sql语句进行查询,并将查询结果缓存。
  • 3.在update和delete语句执行时,删除userinfo128的缓存,这样下一次query时就会自动更新缓存了。

1.2 timeline(sorted set)

timeline非常经典的场景就是微博,朋友圈这种,每个用户都能在自己的timeline上获取到按时间排序的其他用户发布的动态。这里有以前总结过的一篇文章:朋友圈式的TIMELINE设计方案。 其实这个 timeline 我一开始的实现是用 list 去做的,但是用list会存在一个问题:因为推送动态可能同时发生,导致不是严格的按照时间排序。

1.3 推送用户集合(set)

这个功能其实就相当于维持一个用户的粉丝列表。这个列表是一个会经常发生变化的集合,并且在项目中是根据用户的关系链计算出来的,单次的查询会消耗很多的时间,因此做成一个集合,在需要推送动态之类的内容时,直接从redis的集合中查询,会节约很多的时间。

1.4 任务队列(list)

整个项目中很多的操作都是异步的,比如发短信,发邮件,推送用户动态等等,使用redis作为任务队列是很简单的,使用它的list结构,然后使用lpush/rpop对,一边push进任务,另一边有一个单独的后台进程pop出任务进行执行。 当然lpush/rpop并不是很好的一个选择,更好的选择是lpush/brpop,使用阻塞版本的pop指令,可以减少很多不必要的轮询。 在使用这样的任务队列时,还需要考虑到一个问题,如果在取出一个任务时进程崩溃,那么这个任务就彻底的丢失了。因此还可以使用 rpoplpush 或者阻塞版本的 brpoplpush ,取出一个任务的同时备份到另一个队列。如果执行成功的话就再lrem掉这个备份即可。关于队列的更详细的使用在第7大点有更详细的说明。 当然, Redis其实并不推荐作为任务队列的实现,如果需要的话,可以尝试使用Redis作者的另一个项目:disque,或者是kafka。  

1.5 计数器(hash)

计数器我认为也算是 redis 一个常用的功能了,我认为原因有以下:

  • 1.很多场景下的计数功能都是一个非常高频的操作,使用 redis 会拥有极高的性能。
  • 2.redis支持原子性的自增(incre)操作,不用担心CAS(check and set)的问题。
  • 3.传统数据库,如mysql,如果是MyISAM,单次的更新会带来表锁,如果是InnoDB,则带来行锁,影响并发度。

计数器的使用很简单,直接对某个key做 incr 操作,或者对某个 hash 的 key 做 hincrby 操作即可。  

2.php在使用redis时,多个数据库切换的困扰

项目中使用的是phpredis这个扩展,在使用pconnect保持redis长连接时,所有对redis的操作会共用同一个redis连接。这就导致:多个进程同时使用一个redis连接,并且多个进程使用的数据库不同时导致错误。比如下方的操作:

// 进程1做以下操作
$redis->select(0);
$redis->set("key1", "val1");

// 进程2做以下操作
$redis->select(1);
$redis-set("key2", "val2");

但是在redis的server端,所做的操作可能如下:

select(0)
select(1)
set("key2", "val2")
set("key1", "val1")

这样就导致key1的存储错误 所以我必须在所有这样的操作中,使用MULTI/EXEC对去解决这个问题。以上代码变成:

// 进程1做以下操作
$redis->multi();
$redis->select(0);
$redis->set("key1", "val1");
$result->exec();

// 进程2做以下操作
$redis->multi();
$redis->select(1);
$redis-set("key2", "val2");
$result->exec();

事实上,使用 redis 时,同时使用多个数据库并不推荐。因为在 redis 集群中是不支持 select 命令的。

3.redis多个数据库之间的切换,对性能有影响吗?

在探讨这个问题之前,摘录官网上对 select 命令的说明:

Since the currently selected database is a property of the connection, clients should track the currently selected database and re-select it on reconnection. While there is no command in order to query the selected database in the current connection, the CLIENT LIST output shows, for each client, the currently selected database.

大致意思可以翻译为:

因为当前选中的数据库是连接的一个属性,每个客户端连接都跟踪记录了当前选中的数据库,在重新连接时会重新选择数据库。虽然没有命令是为了查询当前连接选中的数据库,但是 CLIENT LIST 的输出会显示,每个客户端当前选中的是哪个数据库。

因此 select 操作只是修改了当前连接的属性。

4.redis 有 16 个数据库,目的是什么,最佳的使用方式是什么?为什么 redis 集群不支持 select?

同样的摘录一段官网的介绍

Redis different selectable databases are a form of namespacing: all the databases are anyway persisted together in the same RDB / AOF file. However different databases can have keys having the same name, and there are commands available like FLUSHDB, SWAPDB or RANDOMKEY that work on specific databases.   In practical terms, Redis databases should mainly used in order to, if needed, separate different keys belonging to the same application, and not in order to use a single Redis instance for multiple unrelated applications. When using Redis Cluster, the SELECT command cannot be used, since Redis Cluster only supports database zero. In the case of Redis Cluster, having multiple databases would be useless, and a worthless source of complexity, because anyway commands operating atomically on a single database would not be possible with the Redis Cluster design and goals.

大意如下:

Redis 多个不同的可选择的数据库是命名空间的一个表现形式:所有的数据库都会在同一个 RDB/AOF 文件中进行持久化。当然不同的数据库可以拥有同样的名字的键,同样的也有一些类似 FLUSHDB, SWAPDB 或 RANDOMKEY 这样的命名专门在数据库上工作的。 实际上,Redis 数据库应该主要用来分离属于一个应用的不同的键,而不是为了使用一个单独的 Redis 实例服务于多个不相关的应用。 当使用 Redis 集群时,SELECT 命名就不能使用了,因为 Redis 集群仅仅支持数据库0。在 Redis 集群的案例中,拥有多个数据库是无用的,是一种毫无价值的复杂性的来源,因为 Redis 集群的设计和目标是不可能支持 SELECT 命令的。

这里解释了为什么 Redis 被设计为有多个数据库,是为了分离同一个应用中不同的键而设计的,但是不能在多个不相关的应用中使用同一个 Redis 实例。并且还要注意在 Redis 集群中无法使用 SELECT ,因此在项目中还是不用为好。

5.redis是单进程的,如何理解?

我在第一次看到这句话时,是很不理解的。对于这种应用,不可能只有一个进程在工作啊。但是在深入了解之后,明白了这里的单进程指的是:处理 Redis 命令是单进程的。 也就是说,同一时间,在并发和并行的层面上来说,都只有一个 Redis 命令被执行。这样设计的理由我的理解有以下:

  • Redis是内存数据库,所有的操作耗时都视CPU的运行速度而定,IO不可能是瓶颈,并行/并发处理带来的意义不大。
  • 并行/并发会增加应用的复杂度

6.redis中使用队列的问题

在1.4小节中我提到了使用Redis作为任务队列的场景。在使用时遇到了程序运行一段时间之后,无法使用brpop获取数据的问题,并且这个程序的连接依然是存活的。 于是我用 CLIENT LIST 查看当前的连接客户端。发现服务器中有大量连接,但是很多连接的 idle 特别长,明显是很久以前的连接,这些连接我可以肯定是已经断开的。经过检查之后,发现 Redis.conf 中的 tcp-keepalive 项我设置为0了,设置为0就不会检查连接是否存活,从而导致连接一直存在。以前将 tcp-keepalive 设置为60, 那这跟 brpop 无法从 Redis 中获取数据有什么关系呢?以下是个人的猜想时间。 这要从Redis的block模型说起。Redis的网络连接是epoll模型的,所以是一个异步的io,肯定不会block一个连接。那么Redis server为了实现这样的block操作,会维持一个内部的哈希表,这个哈希表保存了哪个key上阻塞了哪些客户端。如下图所示: 此处输入图片的描述 如果此时list key1中被push进了一个值,key1就被置为ready状态,然后从链表头部取出client2,将值传给它。可能在我自己的测试中,有大量的已经断开连接客户端阻塞在key1上,但是因为tcp-keepalive为0,没有被及时清除。导致以上的结果。(目前的水平只能这么解释了,虽然还有很多地方说不通)

分类:php, redis标签:#redis #php #任务队列

ABNF格式说明

一、简介

ABNF全称是Augmented Backus-Naur Form,广泛用于很多的互联网文档说明中。主要作用就是以简洁的字符串来描述某些规范。使用了ABNF的标准说明有:电子邮件的标准说明[RFC733]和之后的[RFC822],HTTP1.1协议的[RFC7230]。因此,要想阅读这些文档,必须了解ABNF的格式。ABNF在RFC5234中进行了详细的说明。

二、规则定义

2.1 规则命名

ABNF中规则的命名是大小写不敏感的,由字母开头,后面跟上字母、数字或连字符

2.2 规则格式

一个规则是如下格式定义的:

name = elements crlf

name指的是规则名,elements是一个或多个规则名,或者是终端字符,crlf也就是我们常说的\r\n

2.3 终端值

一个规则被解释成一个字符串。每个字符都是一个非负的数字(比如ASCII码中a对应十进制的97)。终端值就是这些数字。目前定义了以下几种进制:

b     = binary          ;二进制
d     = decimal         ;十进制
x     = hexadecimal     ;十六进制

因此:

CR = %d13
CR = %x0D

使用“.“号来分割字符

CRLF = %d13.10

2.4 额外的编码

根据编码不同,所显示的值可能也不同。比如7-bit的US-ASCII和16-bit的unicode编码,结果是截然不同的。目前7-bit的US-ASCII编码是最常用的。

三、 运算符

3.1 连接: Rule1 Rule2

连接的意思就是值一个规则可能由其他规则连接而成。比如

foo = %x61 ; a
bar = %x62 ; b
mumble = foo bar foo

因此规则mumnle = aba

3.2 选择: Rule1 / Rule2

选择就是多选一的意思。比如

rule = foo / bar

那么rule是foo或者bar都接受的

3.3 扩展的选择: Rule1 =/ Rule2

ruleset = rule1 / rule2
ruleset =/ rule3
ruleset =/ rule4 / rule5

那么ruleset最终为

ruleset = rule1 / rule2 / rule3 / rule4 / rule5

3.4 范围选择: %c##-##

DIGIT = %x30-39

等价于

DIGIT = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"

3.5 序列组: (Rule1 Rule2)

序列组主要是为了阅读上的方便

elem (foo / bar) blat

等价于

(elem foo blat) or (elem bar blat)

elem foo / bar blat

等价于

(elem foo) or (bar blat)

3.6 变量重复: *Rule

完整的格式为:<a>*<b>element <a><b>是可选的数字值,代表最少a个,最多b个 因此:

*<element> 0到任意多个
1*<element> 至少1个
3*3<element> 只能是3个
1*2<element> 1到2个

3.7 指定的重复: nRule

n<element>等价于n*n<element>

3.8 可选的序列: [Rule]

[Rule]代表这个规则可有可无。因此[foo bar]等价于*1[foo bar]

3.9 注释: ;Comment

使用;来表示注释

3.10 运算符优先级

运算符优先级从上往下排序如下:

规则名, 单值, 终端值
注释
范围取值
重复
组, 可选
连接
选择

四、使用ABNF定义ABNF

rulelist = 1*( rule / (*c-wsp c-nl) )

rule = rulename defined-as elements c-nl
; continues if next line starts
; with white space

rulename = ALPHA *(ALPHA / DIGIT / "-")

defined-as = *c-wsp ("=" / "=/") *c-wsp
; basic rules definition and
; incremental alternatives

elements = alternation *c-wsp

c-wsp = WSP / (c-nl WSP)

c-nl = comment / CRLF
; comment or newline

comment = ";" *(WSP / VCHAR) CRLF

alternation = concatenation
*(*c-wsp "/" *c-wsp concatenation)

concatenation = repetition *(1*c-wsp repetition)

repetition = [repeat] element

repeat = 1*DIGIT / (*DIGIT "*" *DIGIT)

element = rulename / group / option /
char-val / num-val / prose-val

group = "(" *c-wsp alternation *c-wsp ")"

option = "[" *c-wsp alternation *c-wsp "]"

char-val = DQUOTE *(%x20-21 / %x23-7E) DQUOTE
; quoted string of SP and VCHAR
; without DQUOTE

num-val = "%" (bin-val / dec-val / hex-val)

bin-val = "b" 1*BIT
[ 1*("." 1*BIT) / ("-" 1*BIT) ]
; series of concatenated bit values
; or single ONEOF range

dec-val = "d" 1*DIGIT
[ 1*("." 1*DIGIT) / ("-" 1*DIGIT) ]

hex-val = "x" 1*HEXDIG
[ 1*("." 1*HEXDIG) / ("-" 1*HEXDIG) ]

prose-val = "<" *(%x20-3D / %x3F-7E) ">"
; bracketed string of SP and VCHAR
; without angles
; prose description, to be used as
; last resort

附录:核心规则

ALPHA = %x41-5A / %x61-7A ; A-Z / a-z

BIT = "0" / "1"

CHAR = %x01-7F
; any 7-bit US-ASCII character,
; excluding NUL

CR = %x0D
; carriage return

CRLF = CR LF
; Internet standard newline

CTL = %x00-1F / %x7F
; controls

DIGIT = %x30-39
; 0-9

DQUOTE = %x22
; " (Double Quote)

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"

HTAB = %x09
; horizontal tab

LF = %x0A
; linefeed

LWSP = *(WSP / CRLF WSP)
; Use of this linear-white-space rule
; permits lines containing only white
; space that are no longer legal in
; mail headers and have caused
; interoperability problems in other
; contexts.
; Do not use when defining mail
; headers and use with caution in
; other contexts.

OCTET = %x00-FF
; 8 bits of data

SP = %x20

VCHAR = %x21-7E
; visible (printing) characters

WSP = SP / HTAB
; white space

RFC5234地址:https://tools.ietf.org/html/rfc5234

分类:工作, 计算机标签:#ABNF #RFC5234

Canvas性能优化小结

简介

H5中引入了对canvas的支持,使得网页的表达能力更加丰富了。程序员可以通过canvas来绘制复杂的图形,甚至是游戏。因为工作中的需求,需要使用canvas在网页上绘制家谱。具体的算法可以看之前的一篇总结:树的可视化以及家谱绘制的算法 。当家谱中的数据量变大之后,整个绘制过程会变的很卡(800左右人物的家谱1000次绘制居然需要25s左右)。但是在做完优化后,1000次绘制只需要0.15s左右。 家谱示例 目前的家谱绘制共有两部分。一个是家谱的主体,用户可以通过鼠标、滚轮去任意的浏览;一个是左上角的小地图功能,帮助用户了解到当前浏览的位置,小地图中的有色方块就是用户整个屏幕显示的内容。

优化措施一:缓存

缓存是在绝大多数系统中经常用到的提升性能的方法,典型的以空间换时间。在canvas中当然也可以使用缓存。在家谱的每一次绘制中,都要遍历所有的人物,然后计算他们的位置大小,然后绘制到界面上,然后还要遍历所有的路径再次进行绘制。而使用缓存的目的就是避免这一部分的计算。因此,在任何复杂计算后的绘制,都能使用缓存来提升性能。 在canvas中有这样一个方法,CanvasRenderingContext2D.drawImage(image, dx, dy)image参数代表要绘制的图片源,不仅仅可以是一个image对象,还可以是一个canvas对象。所以如果我们将一个复杂的图形作为canvas对象缓存起来,然后直接使用drawImage方法去绘制到画面上,就能减少很多的重复计算,来提升性能。 例如以下的伪代码

function paintPerson() {
    // paint 
}

function paintFamilyTree() {
    for(var i = 0; i < 10000; i++) {
        paintPerson()
    }
}

// 用户移动鼠标就需要重绘族谱
dom.addEventListener("mousemove", function() {
    paintFamilyTree()
}

用户每一次移动鼠标,都需要10000次的循环去画。所以我们使用下面的方法,使得只需要计算一次

function paintPerson() {
    // paint 
}

function paintFamilyTree() {
    for(var i = 0; i < 10000; i++) {
        paintPerson()
    }
}

var canvasObj = cache(paintFamilyTree) // 缓存这次绘制的结果。

dom.addEventListener("mousemove", function() {
    ctx.drawImage(canvasObj,0,0)
}

那么如何去缓存这个 canvas 对象呢?可以使用document.createElement('canvas')来创建一个不在页面上显示的 canvas 对象,一般也称它为离屏画布,这里用变量 offScreenCanvas 来称呼。然后将绘制的图形画在这个 canvas 上,之后调用 ctx.drawImage(offScreenCanvas,0,0) 即可。 在创建这个离屏画布的时候,我们也要设置合适的宽高,这样也有助于提升性能。 可以参考这个例子来感受一下性能差距: 离屏画布缓存来提升页面性能 例子代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>离屏canvas实例</title>
        <style>
            .main-wrapper {
                width: 800px;
                margin: 10px auto;
            }
        </style>
    </head>

    <body onload="init()">
        <div class="main-wrapper">
            <canvas width="800" height="600" style="border: 1px solid #ccc" id="canvas">
                你的浏览器不支持canvas
            </canvas>
            <br><br><br><br><br><br><br><br><br>
            <div id="result"></div>
            <br>
            渲染次数:<input type="number" id="times" value="1000">,共耗时(ms)<span id="time_used">0</span><br>
            <button onclick="doTest(false,false)">不使用离屏canvas</button>
            <button onclick="doTest(true,false)">使用离屏canvas</button>
            <button onclick="doTest(true,true)">使用离屏canvas并设置正确的高度</button>
        </div>
        <script>
            /**
             * 离屏缓存,num为缓存canvas的数量
             */
            var OffScreenCache = function (num) {
                this.canvases = [];
                for (i = 0; i < num; i++) {
                    this.canvases.push(document.createElement("canvas"));
                }
            }
            OffScreenCache.prototype = {
                pop: function() {
                    return this.canvases.pop();
                },
                push: function(canvas) {
                    this.canvases.push(canvas);
                },
                destroy: function() {
                    this.canvases = null;
                }
            }
            var Ball = function (color) {
                this.radius = 50;
                this.lineWidth = 4;
                this.cache = null;
                this.color = color;
            }
            Ball.prototype = {
                paint: function(ctx, x, y) {
                    ctx.save();
                    ctx.lineWidth = this.lineWidth;
                    ctx.strokeStyle = this.color;
                    for (i = 1; i < this.radius; i+= this.lineWidth) {
                        ctx.beginPath();
                        ctx.arc(x, y, i, 0, Math.PI*2,true); // 绘制
                        ctx.stroke();
                    }
                    ctx.restore();
                },
                useCache: function (cacheCanvas, autoSet) {
                    if (autoSet) {
                        cacheCanvas.width = this.radius * 2;
                        cacheCanvas.height = this.radius * 2;
                    }
                    cacheCtx = cacheCanvas.getContext('2d')
                    this.paint(cacheCtx, cacheCanvas.width / 2, cacheCanvas.height / 2)
                    this.cache = cacheCanvas
                }
            }
            /******************** 下面是执行代码 **************************/
            var g_canvas, g_ctx;
            var g_width = 800;
            var g_height = 600;
            function init() {
                g_canvas = document.getElementById("canvas");
                g_ctx = g_canvas.getContext('2d');
            }
            function getRandomPos() {
                var x = Math.random() * g_width
                var y = Math.random() * g_height
                return {x:x, y:y}
            }
            function showResult(ballCount) {
                var dom = document.getElementById("result");
                dom.innerHTML = "";
                var str = "";
                for (var key of Object.keys(ballCount)) {
                    str += "<span>" + key + "ball:" + ballCount[key] + "</span>    "
                }
                dom.innerHTML = str;
            }
            function doTest(useCache, autoSet) {
                g_ctx.clearRect(0, 0, g_width, g_height)
                var startTime = Date.now();
                var times = document.getElementById("times").value
                var colorArray = ['red', 'blue', 'green', 'black', 'pink']   // 共绘制5种颜色
                var ballCount = {};
                colorArray.forEach(function(color) {
                    ballCount[color] = 0;
                })
                if (useCache) {
                    var colorBall = [];
                    var cacheCanvases = new OffScreenCache(colorArray.length);
                    for (var i = 0; i < colorArray.length; i++) {
                        var ball = new Ball(colorArray[i]);
                        var cacheCanvas = cacheCanvases.pop();
                        ball.useCache(cacheCanvas, autoSet)
                        colorBall.push(ball)
                    }
                    for (var i = 0; i < times; i++) {
                        ball = colorBall[i % 5]
                        ballCount[ball.color]++;
                        var pos = getRandomPos()
                        g_ctx.drawImage(ball.cache, pos.x, pos.y)       // 开始画
                    }
                } else {
                    for (var i = 0; i < times; i++) {
                        var color = colorArray[i % 5];
                        var ball = new Ball(color)
                        var pos = getRandomPos()
                        ball.paint(g_ctx, pos.x, pos.y)               // 开始画
                        ballCount[color]++;
                    }
                }
                var endTime = Date.now();
                document.getElementById("time_used").innerText = endTime - startTime;
                showResult(ballCount)
            }
        </script>
    </body>
</html>

优化措施二:分层

在上述的族谱图片中,左上角的小地图其实是有两个canvas重叠而成。底层的canvas绘制族谱的缩略图,上层的canvas绘制小方框。这样在小方框移动时,只需清空上层canvas的局部画布重绘小方框即可,不需要重绘底层的缩略图。

分类:工作, 前端标签:#html5 #canvas

接口请求速率(接口防刷)限制方案

1.场景

当前业务中接口防刷的场景有:

  • 1.短信/邮件接口。这个接口不做好防刷策略,损失是很大的。
  • 2.涉及到安全问题的接口调用。比如注册接口,登录接口等等。过于频繁的请求可能代表着用户的批量注册,用户密码的暴力破解(需要考虑到可能是用户忘记密码了)等等。

2.要求

  • 1.与当前业务系统解耦合
  • 2.可以很方便的对不同的接口设置不同的请求频率限制
  • 3.具有较好的性能。在大并发的情况下,需要有正确的结果

3.方案提出

3.1 token bucket(令牌桶)

token bucket算法的如下图: token_bucket 描述如下: 1.假设有一个桶,不断的向桶中投放token,并且我们也可以从桶中取出token。 2.投放token的速率是恒定的r1,桶满了token数量则不会再增长。 3.取出的速率是随机的r2。如果桶是空的,则无法取出token。 4.所有的请求都必须从桶中取出token才是有效的。否则该请求会被拒绝。 举个例子,假设桶的容量是10,每秒放2个token进去。

  • 如果现在每秒2个请求,那么所有这样的请求都是没有问题的,都会拿到token。
  • 如果现在每秒4个请求,10 + 2x = 4x。也就是x=5秒后请求就没法及时拿到token,被丢弃。

总结出以下特点: - 如果请求速率过快,到一段时间后会受到限制 - 允许突发请求

3.2 redis数据库

使用redis数据库完全是因为方便以及速度。只要注意这其中存在的CAS问题即可

4.具体实现

首先,为了解决redis中检查和存储数据存在的CAS问题,我们需要使用lua脚本。因此,对于token bucket的主要实现是用lua脚本实现,然后通过php调用。

local function mysplit(inputstr, sep)
    if sep == nil then
            sep = "%s"
    end
    local t={} ; 
    local i=1
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            t[i] = str
            i = i + 1
    end
    return t
end

local res = redis.pcall('get',KEYS[1])

if (res == false) then
    -- bucket不存在,初始化为capacity - 1
    local str = string.format("%d %d",ARGV[1] - 1,ARGV[2])  
    return redis.pcall('set',KEYS[1],str)
end

local arr_str = mysplit(res, " ")

local num = arr_str[1]
local timestamp = arr_str[2]

-- 根据时间戳来计算num,这里一秒2个令牌
num = num + (ARGV[2] - timestamp) * ARGV[3]
-- 不超过10个令牌
if num > 10 then
    num = 10
end

if (tonumber(num) > 0) then
    -- 拿到token并减一
    local str = string.format("%d %d",num - 1,ARGV[2])
    return redis.pcall('set', KEYS[1], str)
else 
    -- 没有拿到token
    return false
end
<?php

$sha = '2ff7ad2d1b49da8430e5adc8675e';

/**
 * 初始化lua脚本
 */
function initScript($redis) {
    global $sha;
    //不存在脚本,load进去
    $script = file_get_contents('check_and_set.lua');
    $sha = $redis->script('load', $script);
    if($redis->getLastError() !== NULL){
        echo "出错了:".$redis->getLastError().'\n';
    }
    echo "初始化的脚本sha:".$sha.PHP_EOL;
}

function getTokenFromBucket($redis, $bucket) {
    global $sha;
    $capacity = 10;
    $time = time();
    $inputRate = 2;     //一秒2个token
    $params = array($bucket, $capacity, $time, $inputRate);
    $result = $redis->evalSha($sha, $params, 1);
    if($redis->getLastError() !== NULL){
        echo "出错了:".$redis->getLastError().'\n';
    }
    return $result;
}

$redis = new Redis();
$redis->pconnect("127.0.0.1");

initScript($redis);

$start = microtime(true) * 1000;

while(true) {
    $result =  getTokenFromBucket($redis, 'bucket1');
    if (!$result) {
        break;
    } else {
        echo time()."--拿到令牌".PHP_EOL;
        usleep(250000);    //每秒请求4次,10 + 2x = 4x, x = 5。5秒左右无法拿到令牌
    }
}

$end = microtime(true) * 1000;

echo "共耗时:".($end - $start)."毫秒".PHP_EOL.PHP_EOL;

$start = microtime(true) * 1000;

while(true) {
    $result =  getTokenFromBucket($redis, 'bucket2');
    if (!$result) {
        break;
    } else {
        echo "拿到令牌".PHP_EOL;
        usleep(500000);    //每秒请求2次,永远可以拿到令牌
    }
}

$end = microtime(true) * 1000;

echo "共耗时:".($end - $start)."毫秒".PHP_EOL.PHP_EOL;

代码中主要部分概括如下:

  • 检查redis中是否存在桶bucket1
  • 如果不存在,那么默认的容量是10,将bucket1设为9 timestamp(timestamp为当前的时间戳)。返回true
  • 如果存在,则获取该bucket1的值,解析出实际token数量为num,以及上一次存储时时间戳为pre。如果rate为token的生成速率,那么现在的token数量应该是:num = num + (time() - pre) * rate。num最大为默认容量10。如果num大于0,则减一后重新设置,返回true。否则返回false。

代码的运行结果如下: 运行结果演示

5.扩展知识

与token bucket相似的算法还有leaky bucket(漏桶)。leaky bucket 就像是 token bucket的镜像一样。leaky bucket 一般用于Traffic shaping和Traffic policing等问题,与下面的两张图对应。 leaky bucket leaky bucket 可以描述如下:

  • 1.有一个桶,其上方水龙头以变化的速率r1向里面滴水,桶底的水龙头则以恒定的速率向下漏水
  • 2.水桶装满后,上方水龙头的滴水就会溢出
  • 3.水桶空后,下方水龙头则不会滴水

这里举一个异步任务处理(任务队列的容量是有限的)的例子。

  • 上方水龙头相当于任务投递系统,下方水龙头相当于任务处理系统。
  • 任务投递的速率是变化的,因为不同时间段的系统负载可能不同。
  • 任务处理系统的速率是恒定的,因为机器的处理能力是恒定的。
  • 当任务投递的速率大于处理速率,填充满任务队列后,后面的任务都会被丢弃。

可以看出leaky bucket可以将不规则的抖动的请求序列变成规则的平滑的请求序列。leaky bucket通常有两个版本:作为一个评判的仪器(meter)或者是生成一个合法请求队列(queue)。 虽然在使用中leaky bucket和token bucket是不同的,但实际上他们是同一种思路。leaky bucket的变化的输入相当于token bucket的变化的输出,leaky bucket的恒定的输出相当于token bucket的恒定的输出。

参考文献

token bucket leaky bucket

分类:工作, redis标签:#接口防刷 #token_bucket