PHP通过Memcached协议连接Netty, 调用Java服务

1.Memcached协议定义:

查看memached源码头文件:http://bazaar.launchpad.net/~tangent-trunk/libmemcached/1.2/view/head:/libmemcached/memcached/protocol_binary.h Memcached对操作类型定义了一个枚举类型,如下:

typedef enum {
        PROTOCOL_BINARY_CMD_GET = 0x00,
        PROTOCOL_BINARY_CMD_SET = 0x01,
        PROTOCOL_BINARY_CMD_ADD = 0x02,
        PROTOCOL_BINARY_CMD_REPLACE = 0x03,
        PROTOCOL_BINARY_CMD_DELETE = 0x04,
        PROTOCOL_BINARY_CMD_INCREMENT = 0x05,
        PROTOCOL_BINARY_CMD_DECREMENT = 0x06,
        PROTOCOL_BINARY_CMD_QUIT = 0x07,
        PROTOCOL_BINARY_CMD_FLUSH = 0x08,
        PROTOCOL_BINARY_CMD_GETQ = 0x09,
        PROTOCOL_BINARY_CMD_NOOP = 0x0a,
        PROTOCOL_BINARY_CMD_VERSION = 0x0b,
        PROTOCOL_BINARY_CMD_GETK = 0x0c,
        PROTOCOL_BINARY_CMD_GETKQ = 0x0d,
        PROTOCOL_BINARY_CMD_APPEND = 0x0e,
        PROTOCOL_BINARY_CMD_PREPEND = 0x0f,
        PROTOCOL_BINARY_CMD_STAT = 0x10,
        PROTOCOL_BINARY_CMD_SETQ = 0x11,
        PROTOCOL_BINARY_CMD_ADDQ = 0x12,
        PROTOCOL_BINARY_CMD_REPLACEQ = 0x13,
        PROTOCOL_BINARY_CMD_DELETEQ = 0x14,
        PROTOCOL_BINARY_CMD_INCREMENTQ = 0x15,
        PROTOCOL_BINARY_CMD_DECREMENTQ = 0x16,
        PROTOCOL_BINARY_CMD_QUITQ = 0x17,
        PROTOCOL_BINARY_CMD_FLUSHQ = 0x18,
        PROTOCOL_BINARY_CMD_APPENDQ = 0x19,
        PROTOCOL_BINARY_CMD_PREPENDQ = 0x1a,
        PROTOCOL_BINARY_CMD_VERBOSITY = 0x1b,
        PROTOCOL_BINARY_CMD_TOUCH = 0x1c,
        PROTOCOL_BINARY_CMD_GAT = 0x1d,
        PROTOCOL_BINARY_CMD_GATQ = 0x1e,
        PROTOCOL_BINARY_CMD_GATK = 0x23,
        PROTOCOL_BINARY_CMD_GATKQ = 0x24,

        PROTOCOL_BINARY_CMD_SASL_LIST_MECHS = 0x20,
        PROTOCOL_BINARY_CMD_SASL_AUTH = 0x21,
        PROTOCOL_BINARY_CMD_SASL_STEP = 0x22,
 
        PROTOCOL_BINARY_CMD_RGET      = 0x30,
        PROTOCOL_BINARY_CMD_RSET      = 0x31,
        PROTOCOL_BINARY_CMD_RSETQ     = 0x32,
        PROTOCOL_BINARY_CMD_RAPPEND   = 0x33,
        PROTOCOL_BINARY_CMD_RAPPENDQ  = 0x34,
        PROTOCOL_BINARY_CMD_RPREPEND  = 0x35,
        PROTOCOL_BINARY_CMD_RPREPENDQ = 0x36,
        PROTOCOL_BINARY_CMD_RDELETE   = 0x37,
        PROTOCOL_BINARY_CMD_RDELETEQ  = 0x38,
        PROTOCOL_BINARY_CMD_RINCR     = 0x39,
        PROTOCOL_BINARY_CMD_RINCRQ    = 0x3a,
        PROTOCOL_BINARY_CMD_RDECR     = 0x3b,
        PROTOCOL_BINARY_CMD_RDECRQ    = 0x3c,
   
        PROTOCOL_BINARY_CMD_SET_VBUCKET = 0x3d,
        PROTOCOL_BINARY_CMD_GET_VBUCKET = 0x3e,
        PROTOCOL_BINARY_CMD_DEL_VBUCKET = 0x3f,
      
        PROTOCOL_BINARY_CMD_TAP_CONNECT = 0x40,
        PROTOCOL_BINARY_CMD_TAP_MUTATION = 0x41,
        PROTOCOL_BINARY_CMD_TAP_DELETE = 0x42,
        PROTOCOL_BINARY_CMD_TAP_FLUSH = 0x43,
        PROTOCOL_BINARY_CMD_TAP_OPAQUE = 0x44,
        PROTOCOL_BINARY_CMD_TAP_VBUCKET_SET = 0x45,
        PROTOCOL_BINARY_CMD_TAP_CHECKPOINT_START = 0x46,
        PROTOCOL_BINARY_CMD_TAP_CHECKPOINT_END = 0x47,

        PROTOCOL_BINARY_CMD_LAST_RESERVED = 0xef,

 
        PROTOCOL_BINARY_CMD_SCRUB = 0xf0
    } protocol_binary_command;

memcached协议的实现见:https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped, 这里有详细的协议描述。

2 分析Memcached的Get操作实现:

php-memcached扩展连接memcached执行一次Get操作的完整数据包如下, 抓包工具可以分析, 非常清晰:

	0x0000:  4502 0051 44b2 4000 4006 0000 7f00 0001  E..QD.@.@.......
	0x0010:  7f00 0001 f624 2bcc 9791 7255 c792 0c06  .....$+...rU....
	0x0020:  8018 31d7 fe45 0000 0101 080a 7164 6249  ..1..E......qdbI
	0x0030:  7164 6249 800d 0005 0000 0000 0000 0005  qdbI............
	0x0040:  0001 0000 0000 0000 0000 0000 4865 6c6c  ............Hell
	0x0050:  6f 

	GetKQ

	0x0000:  4502 004c 8d28 4000 4006 0000 7f00 0001  E..L.(@.@.......
	0x0010:  7f00 0001 f624 2bcc 9791 7272 c792 0c06  .....$+...rr....
	0x0020:  8018 31d7 fe40 0000 0101 080a 7164 6249  [email protected]
	0x0030:  7164 6249 800a 0000 0000 0000 0000 0000  qdbI............
	0x0040:  0002 0000 0000 0000 0000 0000  

	No-op

	0x0000:  4502 005a f688 4000 4006 0000 7f00 0001  E..Z..@.@.......
	0x0010:  7f00 0001 2bcc f624 c792 0c06 9791 728a  ....+..$......r.
	0x0020:  8018 31d5 fe4e 0000 0101 080a 7164 6249  ..1..N......qdbI
	0x0030:  7164 6249 810d 0005 0400 0000 0000 000e  qdbI............
	0x0040:  0001 0000 0000 0000 0000 0001 0000 0000  ................
	0x0050:  4865 6c6c 6f57 6f72 6c64

	响应GetKQ


	0x0000:  4502 004c fc54 4000 4006 0000 7f00 0001  E..L.T@.@.......
	0x0010:  7f00 0001 2bcc f624 c792 0c2c 9791 728a  ....+..$...,..r.
	0x0020:  8018 31d5 fe40 0000 0101 080a 7164 6249  [email protected]
	0x0030:  7164 6249 810a 0000 0000 0000 0000 0000  qdbI............
	0x0040:  0002 0000 0000 0000 0000 0000            ............

	响应No-op

	0x0000:  4502 004c 5a00 4000 4006 0000 7f00 0001  E..LZ.@.@.......
	0x0010:  7f00 0001 f624 2bcc 9791 728a c792 0c44  .....$+...r....D
	0x0020:  8018 31d5 fe40 0000 0101 080a 7164 6249  [email protected]
	0x0030:  7164 6249 8007 0000 0000 0000 0000 0000  qdbI............
	0x0040:  0003 0000 0000 0000 0000 0000            ............

	Quit

	0x0000:  4502 004c ce6b 4000 4006 0000 7f00 0001  E..L.k@.@.......
	0x0010:  7f00 0001 2bcc f624 c792 0c44 9791 72a2  ....+..$...D..r.
	0x0020:  8018 31d4 fe40 0000 0101 080a 7164 6249  [email protected]
	0x0030:  7164 6249 8107 0000 0000 0000 0000 0000  qdbI............
	0x0040:  0003 0000 0000 0000 0000 0000

	响应 Quit

这是一次get('Hello'),响应‘World’的完整数据包, get操作的协议类型是GetKQ,执行GetKQ之后会紧急着一次No-op。这些数据包的内容在Netty中可以直接打印出来,方法:

System.out.println("msg:" + ByteBufUtil.hexDump(out.readBytes(out.readableBytes()))); 

3.基于UNIX Domain Socket的服务端实现

      这里不适应nio,因为本机UNIX Domain Socket实现性能最好。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。关键代码如下:

  EventLoopGroup bossGroup = null;
        EventLoopGroup workerGroup = null;
        if (Epoll.isAvailable()) {
            bossGroup = new EpollEventLoopGroup();
            workerGroup = new EpollEventLoopGroup();
        } else {
            bossGroup = new KQueueEventLoopGroup();
            workerGroup = new KQueueEventLoopGroup();
        }

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    //.channel(NioServerSocketChannel.class)
                    .channel(Epoll.isAvailable() ? EpollServerDomainSocketChannel.class : KQueueServerDomainSocketChannel.class)
                    .childHandler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ServerInitializer())
                    .option(ChannelOption.SO_BACKLOG, 4096)
                    .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                    //.childOption(ChannelOption.SO_KEEPALIVE, true)
                    //.childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);

            System.out.println("Server 启动了");

            SocketAddress s = new DomainSocketAddress("/tmp/netty.sock");
            ChannelFuture f = b.bind(s).sync();
            //ChannelFuture f = b.bind(port).sync(); 

            f.channel().closeFuture().sync();

实现memcached协议编解码, 关键代码如下:

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in,
            List out) {
        switch (state) {
            case Header:
                if (in.readableBytes() < 24) {
                    return;
                }
                magic = in.readByte();
                opCode = in.readByte();
                keyLength = in.readShort();
                extraLength = in.readByte();
                in.skipBytes(1);
                status = in.readShort();
                totalBodySize = in.readInt();
                id = in.readInt(); 
                cas = in.readLong();

                state = State.Body;
            case Body:
                if (in.readableBytes() < totalBodySize) {
                    return; 
                }
                int flags = 0,
                 expires = 0;
                int actualBodySize = totalBodySize;
                if (extraLength > 0) {
                    flags = in.readInt();
                    actualBodySize -= 4;
                }
                if (extraLength > 4) {
                    expires = in.readInt();
                    actualBodySize -= 4;
                }
                String key = "";
                if (keyLength > 0) {
                    ByteBuf keyBytes = in.readBytes(keyLength);
                    key = keyBytes.toString(CharsetUtil.UTF_8);
                    actualBodySize -= keyLength;
                }
                ByteBuf body = in.readBytes(actualBodySize);
                String data = body.toString(CharsetUtil.UTF_8);
                out.add(new MemcachedRequest(
                        magic,
                        opCode,
                        status,
                        id,
                        cas,
                        flags,
                        expires,
                        key,
                        data
                ));

                state = State.Header;
        }

    }

服务端我们只解析get,noop, quit几个协议的数据包,支持了get也就同时支持了getMulti, 其他的都忽略:

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MemcachedRequest s) throws Exception {
        Channel channel = ctx.channel();
        String ret;
        MemcachedResponse msg;
        log.info("channelRead0: {}", s.toString());

        if (s.opCode()== Opcode.GET && s.key().length() > 0) {
            ApiServiceHandler api = ApplicationContextProvider.getBean(ApiServiceHandler.class);
            ret = api.invoke(s.key());
            msg = new MemcachedResponse(Opcode.GET, s.key(), ret);
            log.info("writeAndFlush: {}", msg);
            channel.writeAndFlush(msg);
        } else if (s.opCode()== Opcode.NOOP) {
            msg = new MemcachedResponse(Opcode.NOOP, "", "");
            msg.setCas(0);
            msg.setHasExtras(false);
            log.info("writeAndFlush: {}", msg);
            channel.writeAndFlush(msg);
        } else if (s.opCode()== Opcode.QUIT) {
            //todo
        }


4.启动基于Springboot的Netty Server,调用Dubbo服务

这里我们当请求Hello的时候响应World, 作为测试, 请求其他内容的时候,我们调用一个dev环境的Dubbo服务findMemberInfo, 获取memberid为13的用户基本信息。写一个PHP测试程序测试:

setOptions([
    Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
    Memcached::OPT_TCP_NODELAY => true,
    Memcached::OPT_CONNECT_TIMEOUT => 100,
    Memcached::OPT_SEND_TIMEOUT => 1000,
    Memcached::OPT_RECV_TIMEOUT => 1000, 
    Memcached::OPT_BINARY_PROTOCOL=> true,
]);

if (count($m->getServerList()) <= 0) {
	//$m->addServers([['127.0.0.1', 6666, 100]]);
	$m->addServers([['/tmp/netty.sock',  0, 100]]);
	//$m->addServers([['127.0.0.1',  11212, 100]]);
}else{
	echo "getServerList:".count($m->getServerList()).PHP_EOL;
}

$time= mtime();
$key= getServiceKey('findMemberInfo', 36497515);
$key2= str_repeat("A", 250);
//echo $key.PHP_EOL;
//$d= $m->get("Hello");  //13
$d= $m->get($key);  //13
//$d= $m->getMulti(array('Hello', 'Hello2'));
cost();
var_dump($d);
//$d= $m->get($key);  //13
//$d= $m->set("Hello", "World");  //1
//$m->getStats();   //11
//$m->set("testKey", 1);  //1
//$m->set("testKey", 1000, 600);
//$m->delete("testKey");   //4
//$m->add("testKey", 1, 600);  //2
//$m->increment("testKey", 2, 1);  //5
//$m->decrement("testKey", 2);  //6
//$m->setMulti(['key1'=>10, 'key3'=>20, 'badkey'=>30], 600);  //1 3*3
//$m->getMulti(array('key1', 'key3', 'badkey'), $cas);   //13 3次


function getServiceKey($api, ...$params){
	$data= ['api'=> $api,  'params'=>$params, 'from'=> 'php56'];
	return json_encode($data);
}

function mtime(){
	list($t1, $t2) = explode(' ', microtime());
	return ((float)$t1 + (float)$t2);
}

function cost(){
	global $time;
	$t= mtime() - $time;
	echo $t.PHP_EOL;
	$time= mtime();
}


执行之后能正确返回数据,如下所示:

[root@yizhibo_posco_dev_10_10_20_53 test]# php test.php
0.0032110214233398
string(929) "{"baseInfo":{"appId":20,"avatar":"https://alcdn.img.xiaoka.tv/20170425/119/ac7/13/119ac79b4898fdf2f6778a92c02fabc0.jpg","birthday":"1493078400","check":1,"checkmobile":1,"constellation":"金牛座","country":86,"createip":169608009,"createtime":1493107256,"encrypt":"rfn0nj2x","enumber":0,"enumberend":0,"gender":1,"icon":"https://alcdn.img.xiaoka.tv/20170425/93b/894/13/93b89477d7c42994b45d16b1b9b1ff52.jpg","integral":0,"isAnnoy":0,"isenumber":0,"isxktv":1,"language":"","lastloginip":0,"lastlogintime":1516618190,"lat":39.999285,"lon":116.489316,"memberSource":0,"memberid":13,"mobile":"18300001111","mtype":0,"nickname":"可口可乐了","origin":3,"password":"2884e40d36d624081303cbd5d67024a4","province":"","status":0,"talentname":"普通","talenttype":0,"updatetime":1513494882},"weiboInfo":{"weibo_checked":0,"weibo_expiretime":"","weibo_isv":0,"weibo_nick":"","weibo_openid":"","weibo_refreshtoken":"","weibo_token":""}}" 

Netty端会打出日志,如下所示:

17:59:23.145 logback [epollEventLoopGroup-3-1] INFO  com.yxw.app1.netty.ServerHandler - channelRead0: MemcachedRequest(magic=-128, opCode=10, status=0, id=1376256, cas=0, flags=0, expires=0, key=, data=)
17:59:23.145 logback [epollEventLoopGroup-3-1] INFO  com.yxw.app1.netty.ServerHandler - writeAndFlush: MemcachedResponse(magic=129, opCode=10, key=, flags=0, expires=0, body=, id=0, cas=0, hasExtras=false)
17:59:23.185 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - writeAndFlush: MemcachedResponse(magic=129, opCode=13, key={"api":"findMemberInfo","params":[36497515],"from":"php56"}, flags=0, expires=0, body={"baseInfo":{"appId":20,"avatar":"https://alcdn.img.xiaoka.tv/20170425/119/ac7/13/119ac79b4898fdf2f6778a92c02fabc0.jpg","birthday":"1493078400","check":1,"checkmobile":1,"constellation":"金牛座","country":86,"createip":169608009,"createtime":1493107256,"encrypt":"rfn0nj2x","enumber":0,"enumberend":0,"gender":1,"icon":"https://alcdn.img.xiaoka.tv/20170425/93b/894/13/93b89477d7c42994b45d16b1b9b1ff52.jpg","integral":0,"isAnnoy":0,"isenumber":0,"isxktv":1,"language":"","lastloginip":0,"lastlogintime":1516618190,"lat":39.999285,"lon":116.489316,"memberSource":0,"memberid":13,"mobile":"18300001111","mtype":0,"nickname":"可口可乐了","origin":3,"password":"2884e40d36d624081303cbd5d67024a4","province":"","status":0,"talentname":"普通","talenttype":0,"updatetime":1513494882},"weiboInfo":{"weibo_checked":0,"weibo_expiretime":"","weibo_isv":0,"weibo_nick":"","weibo_openid":"","weibo_refreshtoken":"","weibo_token":""}}, id=0, cas=1, hasExtras=true)
17:59:23.185 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - channelRead0: MemcachedRequest(magic=-128, opCode=10, status=0, id=1376256, cas=0, flags=0, expires=0, key=, data=)
17:59:23.185 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - writeAndFlush: MemcachedResponse(magic=129, opCode=10, key=, flags=0, expires=0, body=, id=0, cas=0, hasExtras=false)  


5.测试PHP的Memcached扩展长连接性能,发现目前系统一个Bug

PHP是可以使用Memcached长连接模式的, 一个php-fpm一个连接,当请求次数大于php-fpm进程数量之后, 每个php-fpm进程就持有长连接了。但是PHP端得注意3个问题:

(1)要先判断if (count($m->getServerList()) <= 0)  再执行$m→addServers, 否则会往连接池一直添加连接, 会有什么后果, 没有详细测试。PHP手册有说明:

Important to not call ->addServers() every run -- only call it if no servers exist (check getServerList() ); otherwise, since addServers() does not check for dups, it will let you add the same server again and again and again, resultings in hundreds if not thousands of connections to the MC daemon


(2)setOptions操作要在if (count($m->getServerList()) <= 0) 代码块 的里面执行, 否则虽然连接是保持了,但是刷新页面的时候会先断开再重新连,长连接变短连接。这是一个很大坑。Java端日志可以证明这一点:

18:39:59.322 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - channelRead0: MemcachedRequest(magic=-128, opCode=7, status=0, id=1310720, cas=0, flags=0, expires=0, key=, data=)
channelInactive:null掉线
Client:null连接上
channelActive:null在线
18:39:59.323 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - channelRead0: MemcachedRequest(magic=-128, opCode=13, status=0, id=1376256, cas=0, flags=0, expires=0, key={"api":"findMemberInfo","params":[36497515],"from":"php56"}, data=)
18:39:59.324 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - writeAndFlush: MemcachedResponse(magic=129, opCode=13, key={"api":"findMemberInfo","params":[36497515],"from":"php56"}, flags=0, expires=0, body={"baseInfo":{"appId":20,"avatar":"https://alcdn.img.xiaoka.tv/20170425/119/ac7/13/119ac79b4898fdf2f6778a92c02fabc0.jpg","birthday":"1493078400","check":1,"checkmobile":1,"constellation":"金牛座","country":86,"createip":169608009,"createtime":1493107256,"encrypt":"rfn0nj2x","enumber":0,"enumberend":0,"gender":1,"icon":"https://alcdn.img.xiaoka.tv/20170425/93b/894/13/93b89477d7c42994b45d16b1b9b1ff52.jpg","integral":0,"isAnnoy":0,"isenumber":0,"isxktv":1,"language":"","lastloginip":0,"lastlogintime":1516618190,"lat":39.999285,"lon":116.489316,"memberSource":0,"memberid":13,"mobile":"18300001111","mtype":0,"nickname":"可口可乐了","origin":3,"password":"2884e40d36d624081303cbd5d67024a4","province":"","status":0,"talentname":"普通","talenttype":0,"updatetime":1513494882},"weiboInfo":{"weibo_checked":0,"weibo_expiretime":"","weibo_isv":0,"weibo_nick":"","weibo_openid":"","weibo_refreshtoken":"","weibo_token":""}}, id=0, cas=1, hasExtras=true)
18:39:59.325 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - channelRead0: MemcachedRequest(magic=-128, opCode=10, status=0, id=1441792, cas=0, flags=0, expires=0, key=, data=)
18:39:59.325 logback [epollEventLoopGroup-3-2] INFO  com.yxw.app1.netty.ServerHandler - writeAndFlush: MemcachedResponse(magic=129, opCode=10, key=, flags=0, expires=0, body=, id=0, cas=0, hasExtras=false)  

当setOptions/setOption 在if代码块的外面的时候, 每次请求都会设置这些参数, 设置参数导致本次请求Server端的时候PHP的memcached扩展会先发一个0x07的Quit协议的消息, 再连接, 然后GetKQ(0x0d=13), No-op(0x0a=10),放在里面的时候没有这个问题。 目前公司的framework框架的memcached缓存实现类就有这个问题, 虽然连接是保持了, 但是下一次连的时候因为要setOptions,会断开再重新连,和短连接无异,会损耗一定的性能。解决办法很简单, 将 setOptions 操作移动到if (count($m->getServerList()) <= 0) 代码块里面即可。

     以上问题如果要验证的话, 很简单:

  • 查php-fpm的进程数:ps aux|grep php-fpm|wc -l 
  • 发起http请求, 请求次数大于php-fpm进程数,再发起请求,抓包分析结果;

    ab -n 300 http://dev.yxw.xiaoka.tv/i.php?11112226666


  • 抓包匹配查找8007关键词的消息,如下所示:

            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........
            0x0020:  5018 0060 fe34 0000 8007 0000 0000 0000  P..`.4..........

    8007关键词的消息即为Quit消息。 为什么是8007呢? 0x80为请求端的magic number, 0x07为Quit请求。可以看到,每次刷新页面,会断开重连。

   经过测试,php通过memcached扩展调用一次dubbo服务findMemberInfo方法再返回,整个流程耗时大概在2毫秒。用ab简单压测一下, 比目前的rpc-agent性能略好。

(3)需要设置这些Option:

$memcached->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);
$memcached->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 2);
$memcached->setOption(Memcached::OPT_REMOVE_FAILED_SERVERS, true);
$memcached->setOption(Memcached::OPT_RETRY_TIMEOUT, 1);
$memcached->addServers($servers);

参考:http://php.net/manual/zh/memcached.addservers.php, 确保Socket连接断掉之后能够Remove掉,然后重新连。如果没有这些设置, 一旦Socket连接是断的, 只要不重启php-fpm, 还是会继续使用这些Socket连接, 那么就会引起异常。 重启php-fpm能解决问题。  

kill -USR2 `cat /opt/remi/php56/root/var/run/php-fpm/php-fpm.pid` 


6. 限制

使用这个Netty服务理论上可以支持PHP和Java之间的完美交互,实现RPC调用, 但是实际不是这样的, PHP的memcached扩展是限制250字节,libmemcached的key长度限制和memcached扩展一致, 直接看代码:

#define MEMC_OBJECT_KEY_MAX_LENGTH 250
 
#define MEMC_CHECK_KEY(intern, key)                                               \
	if (UNEXPECTED(ZSTR_LEN(key) == 0 ||                                          \
		ZSTR_LEN(key) > MEMC_OBJECT_KEY_MAX_LENGTH ||                             \
		(memcached_behavior_get(intern->memc, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL) \
				? !s_memc_valid_key_binary(key)                                   \
				: !s_memc_valid_key_ascii(key)                                    \
		))) {                                                                     \
		intern->rescode = MEMCACHED_BAD_KEY_PROVIDED;                             \
		RETURN_FALSE;                                                             \
	}

PHP可以用$key2= str_repeat("A", 251)测试, 长度大于250直接返回false。解决办法有这几个:

  • 修改php的memcached扩展重新编译以支持更大的长度比如10240;
  • 综合使用get和getMulti,上面实现的Netty server 也支持getMulti, 但是getMulti其实是串行的get, 只是减少了连接的开销;
  • 使用其他自定义协议,但是可能不会像Memcached+php-fpm这样能很好的使用php-fpm进程维持长连接。








你可能感兴趣的:(java,php)