无路在配置docker的PHP环境或者是在服务器上使用pecl安装PHP扩展时,都会遇到连接pecl.php.net连接不上的问题,导致安装包无法下载.

可以通过增加代理的方式实现翻墙下载:

Docker环境配置如下:

1
2
RUN pear config-set http_proxy http://123.56.177.141:6000 \	
RUN pecl install redis

这需要将代理设置和pecl安装扩展在同一layer执行才能生效,具体可到docker了解layer

http_proxy 需要自己去通过搭建ss或者部署frp到可联通的服务器。

配置 commit.template

如果把此项指定为你系统上的一个文件,当你提交的时候, Git 会默认使用该文件定义的内容。 例如:你创建了一个模板文件$HOME/.gitmessage.txt,它看起来像这样:

1
2
3
4
5
subject line

what happened

[ticket: X]

设置commit.template,当运行git commit时, Git 会在你的编辑器中显示以上的内容, 设置commit.template如下:

1
2
$ git config --global commit.template $HOME/.gitmessage.txt
$ git commit

社区规范

https://www.conventionalcommits.org/en/v1.0.0-beta.2/

通用模板:

1
2
3
4
5
<type>[optional scope]: <description>

[optional body]

[optional footer]
types
  • fix: 修复代码提交
  • feat: 引入新的功能提交
  • chore,docs,style,refactor,perf,test,improvement
scope

限定当前提交类型所涉及的范围:feat(parser): add ability to parse arrays.

Examples

提交信息含有描述和break change

1
2
3
feat: allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for extending other config files

不含提交信息正文

1
docs: correct spelling of CHANGELOG

包含范围信息

1
feat(lang): added polish language

修复bug

1
2
3
4
5
fix: minor typos in code

see the issue for details on the typos fixed

fixes issue #12

安装kakfa

1
2
3
$ wget http://mirrors.hust.edu.cn/apache/kafka/2.0.0/kafka_2.11-2.0.0.tgz
$ tar zxvf kafka_2.11-2.0.0.tgz
$ cd kafka_2.11-2.0.0/

启动zookeeper,如果使用的是单独kafka包,则包含了zookeeper

1
$ bin/zookeeper-server-start.sh config/zookeeper.properties

如果默认的端口2181被占用,则需要修改config/zookeeper.properties 来更改默认端口号.

启动kafka

1
$ bin/kafka-server-start.sh config/server.properties

创建一个topic - test

1
$ bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

查看创建的topic

1
$ bin/kafka-topics.sh --list --zookeeper localhost:2181

除去手动创建topic之外还可以通过配置broker自动创建当topic不存在时。

发送消息

1
$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test

消费消息

1
$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning

模拟多broker集群

复制多分配置文件:

1
2
$ cp config/server.properties config/server-1.properties
$ cp config/server.properties config/server-2.properties

修改两份配置文件,如下:

1
2
3
4
5
6
7
8
9
config/server-1.properties:
broker.id=1
listeners=PLAINTEXT://:9093
log.dirs=/tmp/kafka-logs-1

config/server-2.properties:
broker.id=2
listeners=PLAINTEXT://:9094
log.dirs=/tmp/kafka-logs-2

broker.id 需要在整个集群中是唯一的。

启动broker

1
2
$ bin/kafka-server-start.sh config/server-1.properties &
$ bin/kafka-server-start.sh config/server-2.properties &

创建相应topic

1
$ bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic

查看各个broker情况

1
2
3
> bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-replicated-topic
Topic:my-replicated-topic PartitionCount:1 ReplicationFactor:3 Configs:
Topic: my-replicated-topic Partition: 0 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0

使用场景

机器A:10.9.193.137 无法访问外网,但能与有外网访问权限的机器连接
机器B:10.9.197.104 可以外网联通,并和机器A联通

目的

使机器A可以访问外网(git, composer, etc…)

实施步骤

frp 部署宿主机器:机器B

启动frps

1
2
3
4
5
6
7
8
//frps.ini
[common]
bind_port = 7000

#dashboard
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = admin

启动frp server

1
./frps -c ./frps.ini >> frps.log 2>&1 &

启动client(frpc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//frpc.ini
[common]
server_addr = 127.0.0.1
server_port = 7000

[test_static_file]
type = tcp
remote_port = 6001
plugin = static_file
# 要对外暴露的文件目录
plugin_local_path = /home/work/light/files
# 访问 url 中会被去除的前缀,保留的内容即为要访问的文件路径
plugin_strip_prefix = static
plugin_http_user = abc
plugin_http_passwd = abc

[socks5]
type = tcp
remote_port = 6002
plugin = socks5

[http_proxy]
type = tcp
remote_port = 6000
plugin = http_proxy

启动frp client

1
./frpc -c ./frpc.ini >> frpc.log 2>&1 &

这样就在机器B上分别部署了三个服务:

  • 静态文件服务器
  • socks5
  • http_proxy

机器A上使用http_proxy或者socks5联通外网

1
2
3
4
5
6
export http_proxy="http://username:password@10.9.197.104:6000"
composer:
export HTTP_PROXY="http://10.9.197.104:6000"
composer install
curl:
curl --socks5 socks5://10.9.197.104:6002 https://www.baidu.com
1
2
3
4
5
6
7
yum:

vim /etc/yum.conf

proxy=http://10.9.197.104:6000
proxy_username=username
proxy_password=password

使用场景:

本地开发,需要提供外网访问(微信、微信小程序调试)

物料:

* 一台可以外网访问的机器Server
* 可用域名 Host

实施:

将 Host DNS解析到Server,如果希望使用通配符则可以添加解析为:

1
*.demo.lzuer.net -> ServerIP

frps部署到宿主机器:Server, 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[common]
bind_port = 7000
token = &UJM8ik,

#log
log_file = ./logs/frps.log
log_level = info
log_max_days = 1

#dashboard
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = admin
#虚拟端口, 用于nginx代理
vhost_http_port = 60000
#子域名,配置后,客户端可以随便指定二级域名使用(方便多人)
subdomain_host = demo.lzuer.net

启动frps:

1
./frps -c ./frps.ini &

配置nginx代理

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name *.demo.lzuer.net;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:60000;
}
}

部署frpc到本地开发环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[common]
server_addr = 123.56.177.147
server_port = 7000
token = &UJM8ik,

[web_qx]
type = http
subdomain = qxmugen
host_header_rewrite = qxmugen.io
local_ip = qxmugen.io
local_port = 80
remote_port = 60000

[web_lab]
type = http
subdomain = lab
host_header_rewrite = f.start.lab.io
local_ip = f.start.lab.io
local_port = 80
remote_port = 60000
subdomain 即为配置的子域名, 例如设置为 qxmugen, 则实际外网地址为 qxmugen.demo.lzuer.net
local_ip 配置为对应的本地 host_name, 因为本地开发环境使用vagrant并通过配置hosts的形式访问

如果不配置此项frp默认会解析到 127.0.0.1

启动frpc之后看到frps打印如下日志表示部署成功:

1
2
3
4
5
2018/05/04 10:29:28 [I] [service.go:262] client login info: ip [123.125.250.166:27629] version [0.17.0] hostname [] os [windows] arch [amd64]
2018/05/04 10:29:28 [I] [proxy.go:270] [805811e4fd83dce4] [web_qx] http proxy listen for host [qxmugen.demo.lzuer.net] location []
2018/05/04 10:29:28 [I] [control.go:327] [805811e4fd83dce4] new proxy [web_qx] success
2018/05/04 10:29:28 [I] [proxy.go:270] [805811e4fd83dce4] [web_lab] http proxy listen for host [lab.demo.lzuer.net] location []
2018/05/04 10:29:28 [I] [control.go:327] [805811e4fd83dce4] new proxy [web_lab] success

设计示意图

当使用cURL请求服务时,会执行 :

1
$response = curl_exec($ch);

通过对request/response的打印可以获取以下信息:

Request:

1
2
3
4
5
6
POST /item/save HTTP/1.1
Host: services.mydomain.com
Accept: */*
Content-Length: 429
Expect: 100-continue
Content-Type: multipart/form-data

Response:

1
2
3
4
5
6
7
8
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Date: Fri, 06 Jul 2012 08:37:01 GMT
Server: Apache
Vary: Accept-Encoding,User-Agent
Content-Length: 256
Content-Type: application/json; charset=utf-8

通常我们在对response进行header和body的拆分的时候由于多返回了一段header,则会导致解析响应body的时候出现问题。

常见的浏览器不会发送Expect消息头,但是其他类型的客户端如cURL默认会这么做。

Expect 是一个请求消息头,包含一个一个期望条件,表示服务器只有在满足此期望条件的情况下才能完成请求处理。

目前HTTP规范只规定了一个期望条件,即Expect:100-continue,对此服务器可以做以下回应:

  • 100如果消息头中的期望条件可以得到满足,请求可以顺利进行
  • 417(Exceptation Failed)如果服务器不能满足期望条件的话,也可以返回人任意其他(4xx)错误码

Expect: 100-continue即通知接收方客户端要发送一个体积可能很大的消息体,期望收到状态码为100(Continue)的临时回复。

服务器通过返回一个状态码为100(Continue)的回复告知客户端可以继续发送消息体。

所以希望解决上面的问题,我们可以主动发送一个空的Expect请求头即可:

1
2
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Expect:']);
$response = curl_exec($ch);

场景

任何系统的优化都会采用解耦异步的方式,将某些耗时的场景异步话,从而提升处理速度优化用户体验。例如在常见的场景:发送注册邮件,更加复杂的场景:朋友圈。

朋友圈场景中,需要处理更加复杂的业务逻辑,简单的,当用户发送一条朋友圈时,需要更新数据库「这里如果采用缓存的方式,还需要进行同步等一系列操作」,更新所有好友关系的信息流等等一系列操作。

这时就需要进行异步话,在朋友圈场景中,当前用户发出朋友圈,对于用户自己应该是最快能看到「这里常采用的方式是在APP端进行展示,异步请求服务端进行存储」,剩下的工作可以异步来完成。

实现思路

  • 队列采用beanstalkd
  • supervisor管理消费者

当用户发送朋友圈时,将tweet ID推送到tube ,通过消费进程来实时消费该tube 中的消息,完成异步话。

问题

当用户量少时,上面的方案还能正常运行。当用户量增加,朋友之间的关系更加复杂后,每条消息的处理都会非常耗时,从而大大增加了延时。

多并行处理

采用常用分表策略,将tweet发送到不同的tube,多开进程来处理。例如,可以根据tweet_id%5的方式,将所有的tweet根据ID取模来分配到不同的tube「还可以更多」.

这样,消费进行可以根据规则,多开进程来分别消耗。

根据上面的思路,使用supervisor来管理,设置进程数为5 .

1
2
3
4
5
6
7
8
9
10
11
[program:pub_tweet]
command=php yii queue/tweet %(process_num)01d
directory=/home/wwwroooot/welfare
process_name=%(program_name)s_sharding_%(process_num)s
numprocs=5
numprocs_start=0
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stdout_logfile_maxbytes=10MB
redirect_stderr=true
autostart=true
autorestart=true

在supervisor中查看进程状态如下:

1
2
3
4
5
pub_tweet:pub_tweet_sharding_0       RUNNING   pid 22847, uptime 0:00:03
pub_tweet:pub_tweet_sharding_1 RUNNING pid 22848, uptime 0:00:03
pub_tweet:pub_tweet_sharding_2 RUNNING pid 22849, uptime 0:00:03
pub_tweet:pub_tweet_sharding_3 RUNNING pid 22850, uptime 0:00:03
pub_tweet:pub_tweet_sharding_4 RUNNING pid 22851, uptime 0:00:03

ps -ef | grep tweet

1
2
3
4
5
root     22847 22846  0 09:32 ?        00:00:00 php yii queue/tweet 0
root 22848 22846 0 09:32 ? 00:00:00 php yii queue/tweet 1
root 22849 22846 0 09:32 ? 00:00:00 php yii queue/tweet 2
root 22850 22846 0 09:32 ? 00:00:00 php yii queue/tweet 3
root 22851 22846 0 09:32 ? 00:00:00 php yii queue/tweet 4

可以看到已经有5个进程跑起来

那么在消费程序中就可以根据传入的当前进程num,来分别从不同的tube取出消息处理。

大致如下:

1
2
3
4
5
6
7
8
public function actionTweet($shared)
{
$tube = sprint('tweet_pub_%s', $shared);
$pheanstalk->watch($tute);
while($job = $pheanstalk->reverse()) {
...
}
}

什么是wrk

wrk是一个用来做HTTP benchmark测试的工具。可以产生显著的压力。

为什么是wrk

相比于Apache ab功能更为强大,可以使用lua脚本来支持更为复杂的测试场景,例如PUT请求等。

在对于Restful架构的API接口来说,测试起来更加便捷。

获取和安装

源码地址: https://github.com/wg/wrk

1
2
3
4
5
6

$ git clone https://github.com/wg/wrk.git
$ cd wrk
$ make all
$ cp wrk /usr/bin/wrk
$ wrk -h

使用方法

基本使用方法

1
$ wrk -t 2 -c 50 -d 1 --latency http://bj.58.com

使用两个线程保持50个连接请求1秒钟,并打印延迟统计信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
Running 1s test @ http://bj.58.com
2 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 148.87ms 180.61ms 927.67ms 85.14%
Req/Sec 192.47 99.38 400.00 63.16%
Latency Distribution
50% 65.21ms
75% 204.23ms
90% 355.24ms
99% 835.83ms
367 requests in 1.01s, 37.14MB read
Requests/sec: 364.46
Transfer/sec: 36.88MB

主要关注的点有:

  • Latency 请求处理时延,上面测试结果平均延时时间为148.87ms

  • Requests/sec 每秒处理的请求数(QPS)

上面的结果可以看到(Latency Distribution): 50%的请求延时在65.21ms

当然,上面的请求时间只用了1s,理论上请求的时间越长得结果更加准确

使用进阶

模拟POST请求

新建 post.lua

1
2
3
4
wrk.method = "POST"
wrk.body = "foo=bar&baz=qux"
wrk.headers["Content-Type"] = "application/x-www-form-urlencode"
wrk.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"

请求:

1
$ wrk -t 4 -c 100 -d 30s --timeout 10 --script=post.lua --latency http://127.0.0.1/post.php

通过在脚本中记录日志的方式可以看到$_POST数据和$_SERVER中的字段为lua脚本中设置的值.

更为复杂的场景

wrk提供了几个hook函数,可以用lua来编写一些更为复杂场景下的测试需求。

参考资料:

在使用 vagrant 开发过程中, 突然遇到磁盘被沾满, 导致数据库无法写入.

1
2
3
4
5
6
7
8
文件系统                 容量  已用  可用 已用% 挂载点
/dev/mapper/centos-root 39G 39G 136K 100% /
devtmpfs 911M 0 911M 0% /dev
tmpfs 921M 0 921M 0% /dev/shm
tmpfs 921M 8.4M 912M 1% /run
tmpfs 921M 0 921M 0% /sys/fs/cgroup
/dev/sda1 497M 125M 373M 25% /boot
tmpfs 185M 0 185M 0% /run/user/1000

有足足的39G为啥就满了呢?

通过 sudo du -h -d 1 在home查看发现:

1
2
3
4
32G     ./tmp
3.6G ./var
2.3G ./usr
1.9G ./home

/tmp 文件足足占了32G了, 可想而知磁盘空间沾满的罪魁祸首在这儿.

进入文件查看, 里面大部分是 cachegrind.out.* 文件, 有的单个文件就达到了26个G.

经过查询, 才知道这些文件是Xdebug产生的文件. 好吧, 既然知道了这些文件的产生以及用途, 删掉就好了!

1
$ sudo find /tmp/  -name 'cachegrind*' -exec rm '{}' \;

再来看下磁盘占用:

1
2
3
4
5
6
7
8
文件系统                 容量  已用  可用 已用% 挂载点
/dev/mapper/centos-root 39G 6.9G 32G 18% /
devtmpfs 911M 0 911M 0% /dev
tmpfs 921M 0 921M 0% /dev/shm
tmpfs 921M 8.4M 912M 1% /run
tmpfs 921M 0 921M 0% /sys/fs/cgroup
/dev/sda1 497M 125M 373M 25% /boot
tmpfs 185M 0 185M 0% /run/user/1000

最后可以将清理命令加入crontab来定时执行清理.

–EOF–

在全站升级Https的过程中需要对websocket服务进行https化。

之前, 使用workerman提供服务, 并采用暴露端口的方式供系统通过IP形式访问。

采用Https后, 采用的方案是使用nginx做一层反向代理, 这样可以取消之前websocket暴露外网的端口.

踩坑

配置

为websocket配置特定的后缀, 有nginx进行反向代理:

1
2
3
location = /socket.io/ {
proxy_pass http://127.0.0.1:2120;
}

127.0.0.1:2120 为 workerman 启动的websocket地址.

测试效果

WTF , 结果并没有按照预期的完全无误. 在控制台中得到如下错误提示:

1
2
3
wss://.../socket.io/?EIO=2&transport=websocket&sid=p3af7ZNfvogtq6tAAAG0'

failed: Error during WebSocket handshake: Unexpected response code: 400.

然而, 开始并没有去查看导致这个错误的具体原因是什么, 而是直接进行了测试, 测试的结果是系统能够正常运行.

出坑

当然, 仅仅能够正常运行并不能够证明什么, 事实上, 错误仍然存在. 之后google了一番, 在 socket.io 库的

issue里找到了解决方案:

https://github.com/socketio/socket.io/issues/1942#issuecomment-82352072

尝试:

1
2
3
4
5
6
7
location = /socket.io/ {
proxy_pass http://127.0.0.1:2120;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

再次测试, 错误消失!

  1. proxy_http_version 1.1;

The first line tells Nginx to use HTTP/1.1 when communicating to the Node backend, which is required for WebSockets.

告诉nginx使用 HTTP/1.1 协议和后端的websocket服务通信

  1. proxy_set_header Upgrade $http_upgrade & proxy_set_header Connection "upgrade"

The next two tell Nginx to respond to the Upgrade request which is initiated over HTTP by the browser when it wants to use a WebSocket.

这两个告诉Nginx应对升级请求发起HTTP上的浏览器当它想要使用一个WebSocket。

参考: https://chrislea.com/2013/02/23/proxying-websockets-with-nginx/

–eof–