Swoole slowlog 乱码修复

先说结果:fork 了 swoole 的源码,修复之后打了个 v1.10.7 的版本。 发现问题 由于各种原因项目在使用 swoole 的 v1.x 远古版本跑 TCP 服务,基础环境如下。 CentOS 6.9 PHP 5.5.38 然后遇到了程序偶发性超时情况严重。怀疑是版本bug,首先将 swoole 升级到了当前环境可用的最高版本 v1.10.6,问题依然没有解决。但是这个版本已经有了慢日志功能,将慢日志功能开起来,模拟程序阻塞超时,试试看功能可用否。 tcp_server.php <?php class tcpTest { static public function aa() { self::bb(); } static public function bb() { sleep(3); } } class Server { static private $serv = null; private function __construct() { $serv = new \swoole_server("0.0.0.0", 9577); $serv->set([ 'reactor_num' => 2, 'worker_num' => 8, 'task_worker_num' => 0, 'dispatch_mode' => 2, 'daemonize' => false, 'tcp_fastopen' => true, 'request_slowlog_timeout' => 2, 'request_slowlog_file' => '/tmp/swoole_slow.log', 'trace_event_worker' => true, ]); $serv->on('Start', array($this, 'onStart')); $serv->on('Connect', array($this, 'onConnect')); $serv->on('Receive', array($this, 'onReceive')); $serv->on('Close', array($this, 'onClose')); $serv->start(); } public function onStart($serv) { echo __METHOD__ . PHP_EOL; } public function onConnect($serv, $fd, $from_id) { echo __METHOD__ . " worker_id:{$serv->worker_id} work_pid:{$serv->worker_pid} fd:{$fd} from_id:{$from_id}" . PHP_EOL; } public function onReceive($serv, $fd, $from_id, $data) { $fdinfo = $serv->connection_info($fd,$from_id,true); echo __METHOD__ . " ip:{$fdinfo['remote_ip']} worker_id:{$serv->worker_id} work_pid:{$serv->worker_pid} fd:{$fd} from_id:{$from_id} data:{$data}" . PHP_EOL; \tcpTest::aa(); $res_data = ['time' => date('Y-m-d H:i:s')]; $serv->send($fd, json_encode($res_data)); } public function onClose($serv, $fd, $from_id) { echo __METHOD__ . " worker_id:{$serv->worker_id} work_pid:{$serv->worker_pid} fd:{$fd} from_id:{$from_id}" . PHP_EOL . PHP_EOL; } static public function inst() { if (!(self::$serv instanceof self)) { self::$serv = new self; } return self::$serv; } } $res = \Server::inst(); tcp_client.php ...

May 11, 2020 · 3 min · 433 words · Nick

年前线上问题总结

Intro 在家中除了带娃、看书也无事可做。把节前值班遇到的问题总结下。 除夕那天我们公司依然正常上班,很多同事都已经提前回家了,在的人也不多,中午吃完饭没什么事就都走了。突然领导发来贺电以为什么好事,居然线上出问题了。前端同事反馈后端无响应,这个反馈也是没谁了,问了下具体调用的后端url,某个同事的 php 项目,没辙硬上吧。找运维查了下对应的机器有四台,先上 kibana 看下 nginx access 日志有大量请求404,error 日志没问题。再看 php error 日志有响应很慢的请求,应该是进来了,再看下 php 慢日志看调用栈应该是走到了一个 redis 类。这时候运维还在先让运维看下 redis 有问题没。我接着追代码,看代码发现这老哥没用框架定义的 redis 实例化类,自己整了个,真难受啊,一行行看 redis 配置从哪取的八成是这 redis 的问题。这时候运维反馈线上 redis 没问题。我接着撸代码,终于看到调的配置名了,去项目下 grep 下。这一搜发现这个配置有测试环境的,有预发环境的,还有个默认的配置,线上配置文件没有,不用说线上环境肯定用的默认的配置,默认的配的是测试环境的 redis。然而测试环境因为春节放假都关机了。 问题查清楚了,加上了线上环境的 redis 配置。 但是这个 bug 太tm智障了,全是问题。 生产环境 调 测试环境 生产环境和测试环境要实现网络隔离,配置文件方式还是有点老,整个配置中心。 自行实现 redis 类 框架已经提供了 redis 类,还要自行实现,直接影响排查效率。屏蔽了 redis 链接不上,然后给前端扔了个 404。没有规矩不成方圆要多 code review 啊。 另 最近新型冠状病毒肆虐异常,各地陆续启动公共卫生事件一级响应,春节假期国家也延长到了正月初十。北京这边有的村不让从外回京的人进村,要外隔离,也是没谁了。

January 30, 2020 · 1 min · 55 words · Nick

Golang 与 PHP 的 json 序列化问题

Intro 最近在做 Golang 与 PHP 的 RPC 实现。因 PHP 业务端已上线稳定,Golang 方面则需要完全兼容。其中使用了 json 序列化,发现区别还是很大的,见下面代码。 $ php -a php > echo json_encode("<test我爱中国>"); "<test\u6211\u7231\u4e2d\u56fd>" package main import ( "encoding/json" "fmt" ) func main() { st := "<test我爱中国>" res, err := json.Marshal(st) if err != nil { fmt.Println("json err:", err.Error()) } fmt.Printf("json is: %s", string(res)) // json is: "\u003ctest我爱中国\u003e" } PHP 默认的 json_encode() 函数会把多字节字符转成 \uXXXX 当然通过设置 JSON_UNESCAPED_UNICODE 可以解决这个问题。这里不动 PHP 代码。 Golang 这里用 json 包的 Marshal 方法实现序列化,对多字节字符是不进行处理的。但是这个方法出于安全考虑会将"<", “>”, “&“这三个字符转成 \uXXXX 形式。这还不是最魔幻的,这个方法没有可选参数进行设置。 ...

December 23, 2019 · 3 min · 467 words · Nick

容器服务化方向的一些探索

本文参考使用Docker打造自己的云平台编写 本文基于 Docker Swarm Mode 实现容器化,虽然目前 k8s 更火一些,但实在是太重了,以后再折腾。 使用 traefik 来实现反向代理、负载均衡,traefik 还自带了服务发现、后端断路器、健康检查等,相当于是自带服务发现的 nginx。当然它还支持其他的容器编排工具如,服务发现工具如 Consul。 使用 Portainer 来管理 Docker 容器,可以兼容 Docker Swarm 模式。 Docker 的安装就不说了。装完初始化 Swarm 模式。 $ docker swarm init 先设置一下环境变量。 $ export DOCKER_DEV_PATH=/usr/local/src/docker-dev 日志文件都会统一存到 ${DOCKER_DEV_PATH}/logs。 traefik 新建一个编排文件 traefik.yml。 version: '3.3' services: reverse-proxy: image: traefik:1.7-alpine command: --web --docker --docker.domain=cloud-labs.io --docker.watch --docker.swarmmode=true --loglevel=INFO --accesslog --accesslog.filepath=/logs/access.log --traefiklog --traefiklog.filepath=/logs/traefik.log deploy: mode: replicated replicas: 1 labels: - traefik.enable=true - traefik.backend=traefik - traefik.frontend.rule=Host:monitor.cloud-labs.io - traefik.port=8080 - traefik.docker.network=traefik_proxy networks: - proxy ports: - 8081:80 volumes: - /var/run/docker.sock:/var/run/docker.sock - ${DOCKER_DEV_PATH}/logs/traefik:/logs - /dev/null:/traefik.toml networks: proxy: 简单解释下 labels 中配置的含义 ...

August 28, 2019 · 2 min · 361 words · Nick

PHP7 数组排序函数源码解析

今天来看看经常使用的数组排序函数如 sort, rsort, asort, arsort, ksort, krsort 。话不多说直接找 sort 函数吧。 在 php7.3 源码中搜索 PHP_FUNCTION(sort) 可以搜到如下 其中 .h 文件是C语言的头文件,直接打开 .c 文件。 sort 函数如下,其中我加了一点注释。 PHP_FUNCTION(sort) { zval *array; zend_long sort_type = PHP_SORT_REGULAR; // 默认的排序规则 compare_func_t cmp; // 这里开始接请求参数 ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_ARRAY_EX(array, 0, 1) Z_PARAM_OPTIONAL Z_PARAM_LONG(sort_type) ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); // 根据排序规则获取使用的排序函数 cmp = php_get_data_compare_func(sort_type, 0); // 进行排序 if (zend_hash_sort(Z_ARRVAL_P(array), cmp, 1) == FAILURE) { RETURN_FALSE; } RETURN_TRUE; } 不但 rsort, asort, arsort, ksort, krsort 这些函数在 array.c 文件中,PHP数组相关的也都在其中。 先说下 rsort, asort, arsort, ksort, krsort 函数内容与 sort 只有细微的差别。 ksort、krsort 是根据键排序所以排序规则获取排序函数用的是 php_get_key_compare_func 参数与 php_get_data_compare_func 是一样的。 php_get_data_compare_func、php_get_key_compare_func 函数第二个参数意思是是否降序排列,rsort、arsort、krsort 第二个参数都是1。 进行排序时 zend_hash_sort(Z_ARRVAL_P(array), cmp, 1) 第三个参数意思是是否重新排列索引, sort、rsort 传的都是1。 做个表格看下 ...

May 22, 2019 · 5 min · 935 words · Nick

编写Go的TCP服务来替代PHP的Swoole

背景说明 目前公司采用微服务架构,主要开发语言为PHP,通过Swoole开启TCP服务供业务端调用。通过公司内部编写的PHP扩展封装客户端调用逻辑。 需求 暂定使用Go语言开发新的业务,并提供TCP服务。其中老的PHP项目要通过原有的客户端扩展实现无修改调用。 解决方案 通过阅读客户端扩展源码了解调用逻辑。编写简单的测试如下。 <?php $_client = new \swoole_client(SWOOLE_SOCK_TCP | SWOOLE_KEEP); $_client->set([ 'open_length_check' => true, 'package_length_type' => 'N', 'package_length_offset' => 0, 'package_body_offset' => 4, 'package_max_length' => 24657920, ]); if (false == $_client->connect("127.0.0.1", 8880)) { printf("err_msg: %s err_code: %s" . PHP_EOL, var_export($_client->errMsg, true), var_export($_client->errCode, true)); } // 随便测试个请求参数 $data = [ 'api' => 'getUserInfo', 'params' => [ 'user_id' => 123 ] ]; $data = json_encode($data); $data = gzcompress($data, 9); $_client->send(pack("N", strlen($data)) . $data); $res = $_client->recv(); $end = getTime(); $data = json_decode($res, true); 其中前4个字节是head,表示body长度,采用二进制大端字节序编码。body先进行json编码再进行了zlib压缩。这都是编写Go的TCP服务时需要处理的。 写个简单的Go TCP服务试试,先不考虑过多的错误边界处理。 package main import ( "bytes" "compress/zlib" "encoding/binary" "fmt" "io" "net" ) func main() { ln, err := net.Listen("tcp", ":8880") if err != nil { fmt.Printf("%s", err) } for { conn, err := ln.Accept() if err != nil { fmt.Printf("accept err:%s", err) } go handleConnection(conn) } } func handleConnection(conn net.Conn) { fmt.Println("on conn") var err error headLen := 4 head := make([]byte, headLen) if _, err = conn.Read(head); err != nil { fmt.Println(err.Error()) return } // 解码大端字节序获取body长度 bodyLen := binary.BigEndian.Uint32(head) allBody := make([]byte, 0) readLen := 0 for bodyLen > 0 { body := make([]byte, bodyLen) readLen, err = conn.Read(body) if err != nil { fmt.Println(err.Error()) return } bodyLen = bodyLen - uint32(readLen) allBody = append(allBody, body[:readLen]...) } // 解压zlib压缩的数据 RFC 1950 b := bytes.NewReader(allBody) uncompressRead, err := zlib.NewReader(b) if err != nil { fmt.Printf("uncompress data err:%s", err) } var uncompressData bytes.Buffer io.Copy(&uncompressData, uncompressRead) // 解出的json字符串 fmt.Printf("Received:%s", uncompressData.Bytes()) // 路由调用实际业务逻辑处理 ... // conn.Write() conn.Close() return } 运行Go的TCP服务,跑一个PHP请求测试。 ...

April 30, 2019 · 2 min · 247 words · Nick

PHP5下的Error错误处理及问题定位

背景说明 来说说当PHP出现E_ERROR级别致命的运行时错误的问题定位方法。例如像Fatal error: Allowed memory size of内存溢出这种。当出现这种错误时会导致程序直接退出,PHP的error log中会记录一条错误日志说明报错的具体文件和代码行数,其它的任何信息都没有了。如果是PHP7的话还可以像捕获异常一样捕获错误,PHP5的话就不行了。 一般想到的方法就是看看报错的具体代码,如果报错文件是CommonReturn.class.php像下面这个样子。 <?php /** * 公共返回封装 * Class CommonReturn */ class CommonReturn { /** * 打包函数 * @param $params * @param int $status * * @return mixed */ static public function packData($params, $status = 0) { $res['status'] = $status; $res['data'] = json_encode($params); return $res; } } 其中json_encode那一行报错了,然后你查了下packData这个方法,有很多项目的类中都有调用,这时要怎么定位问题呢? 场景复现 好,首先我们复现下场景。假如实际调用的程序bug.php如下 <?php require_once './CommonReturn.class.php'; $res = ini_set('memory_limit', '1m'); $res = []; $char = str_repeat('x', 999); for ($i = 0; $i < 900 ; $i++) { $res[] = $char; } $get_pack = CommonReturn::packData($res); // something else 运行bug.php PHP错误日志中会记录 ...

January 8, 2019 · 2 min · 381 words · Nick