0%

初始化 vagrant box

1
2
3
4
mkdir playbooks
cd playbooks
vagrant init ubuntu/trusty14.04
vagrant up

连接到 vagrant 虚拟机

使用 vagrant ssh 命令可以登录到刚刚创建的 ubuntu14.04 虚拟机中。

这种方法可以让我们直接与 shell 交互,但是 Ansible 使用标准 SSH 客户端连接到虚拟机,而不是使用 vagrant ssh 命令。

如下操作告诉 Vagrant ssh 输出 SSH 连接的详细信息:

1
vagrant ssh-config

输出如下:

Host default HostName 127.0.0.1 User vagrant Port 2201 UserKnownHostsFile /dev/null StrictHostKeyChecking no PasswordAuthentication no IdentityFile /Users/ruby/Documents/ubuntu14.04/.vagrant/machines/default/virtualbox/private_key IdentitiesOnly yes LogLevel FATAL

最重要的部分是:

1
2
3
4
HostName 127.0.0.1
User vagrant
Port 2201
IdentityFile /Users/ruby/Documents/ubuntu14.04/.vagrant/machines/default/virtualbox/private_key

Vagrant 1.7 版本改变了它处理 SSH 私钥的行为。从 1.7 版本开始,Vagrant 为每台机器都创建了以一个私钥。之前的版本都使用相同的私钥,该私钥存放在 ~/.vagrant.d/insecure_private_key

基于这些信息,下面确认一下你是否可以从命令行发起到虚拟机的 SSH 会话:

1
ssh vagrant@127.0.0.1 -p 2201 -i /Users/ruby/Documents/ubuntu14.04/.vagrant/machines/default/virtualbox/private_key

注意:端口必须和上面的一致

将测试服务器的信息配置在 Ansible 中

Ansible 只能管理那些它明确了解的服务器。你只需要在 inventory 文件中指定服务器的信息就可以将这些信息提供给 Ansible。

每台服务器都需要一个名字以便 Ansible 来识别。可以使用服务器的 hostname,或者给服务器起一个别名并传递一些参数。这些参数告诉 Ansible 如何连接到服务器。

我们将给刚刚创建的 vagrant 服务器起一个别名:testserver。

在 playbooks 目录下创建一个 hosts 文件。这个文件将充当 inventory 文件。

如果你正在使用 Vagrant 机器作为你的测试服务器,hosts 文件应该和下面的内容很像:

1
testserver ansible_host=127.0.0.1 ansible_port=2201 ansible_user=vagrant ansible_private_key_file=/Users/ruby/Documents/ubuntu14.04/.vagrant/machines/default/virtualbox/private_key

这里我们看到一个使用 Vagrant 的缺点:不得不明确地传入额外参数来告诉 Ansible 如何连接。在一般情况下,我们不需要这些补充信息。

1
ansible testserver -i hosts -m ping

testserver | SUCCESS => {
"changed": false,
"ping": "pong"
}

如果命令没有执行成功,可以添加 -vvvv 参数来查看这个错误的更多信息。

我们可以看到模块执行成功。输出中的 changed:false 部分告诉我们模块的执行没有改变服务器的状态。输出中的 "ping": "pong" 是正确的模块定义的输出。

ping 模块除了检查 Ansible 是否可以打开到服务器的 SSH 会话外并不做任何其他的事情。它就是用来检测你是否能连接到服务器的实用工具。

使用 ansible.cfg 文件来简化配置

在上例中,我们不得不在 inventory 文件中输入很多内容来告诉 Ansible 关于我们测试服务器的信息。幸运的是,Ansible 有许多方法来让你定义各种变量。

这样,就不需要把那些信息都堆在一个地方了。ansible.cfg 文件可以设定一些默认值,这样就不需要将同样的内容键入很多遍了。

我们应该把 ansible.cfg 放到哪里

Ansible 按照如下位置和顺序来查找 ansible.cfg 文件:

  • ANSIBLE_CONFIG 环境变量所指定的文件

  • ./ansible.cfg (当前目录下的 ansible.cfg)

  • ~/.ansible.cfg (主目录下的 .ansible.cfg)

  • /etc/ansible/ansible.cfg

ansible.cfg

1
2
3
4
5
[defaults]
inventory = hosts
remote_user = vagrant
private_key_file = .vagrant/machines/default/virtualbox/private_key
host_key_checking = False

上面的范例配置关闭了主机密钥检查,这样在处理 Vagrant 机器时会很方便。

否则,每次销毁并重新创建一台 Vagrant 机器之后,都需要编辑 ~/.ssh/known_hosts 文件。但是,关闭主机密钥检查在通过网络连接其他主机时会成为安全风险。

有了我们设定的默认值,你就不需要在 inventory 文件中明确配置 SSH 文件密钥了。它将简化为:

1
testserver ansible_host=127.0.0.1 ansible_port=2201

还可以在执行 ansible 命令时去掉 -i 参数:

1
ansible testserver -m ping 

查看服务器上的运行时间:

1
ansible testserver -m command -a uptime

command 模块非常有用,ansible 将它设为默认模块,所以我们可以这样简化操作:

1
ansible testserver -a uptime

如果命令中包含空格,需要使用引号将命令括起来,这样 shell 才会将整个字符串作为一个参数传递给 ansible。如:

1
ansible testserver -a "tail -f /var/log/dmesg"

如果需要使用 root 来执行,需要传入参数 -b 告诉 Ansible 使用 sudo 以 root 权限来执行。

例如,访问 /var/log/syslog 需要使用 root 权限:

1
ansible testserver -b -a "tail /var/log/syslog"

可以看到,Ansible 在运行的时候也会写入 syslog。

在使用 ansible 命令行工具的时候,并不仅限于 ping 和 command 模块。可以使用任何你喜欢的模块。例如,可以像这样在 Ubuntu 上安装 Nginx:

1
ansible testserver -b -m apt -a name=Nginx

如果安装 Nginx 失败,可能需要更新一下软件包列表。告诉 Ansible 在安装软件包之前执行等同于 apt-get update 的操作,需要将参数从 name=Nginx 变更为 name=Nginx update_cache=yes。

可以按照如下操作重启 Nginx:

1
ansible testserver -b -m service -a "name=nginx state=restarted"

因为只有 root 才可以安装 Nginx 软件包和重启服务,所以需要添加 -b 参数来使用 sudo

mac 下安装 ansible

1
brew install ansible

添加配置文件

1
2
sudo mkdir /etc/ansible
touch /etc/ansible/hosts

ssh 公钥加到服务器

1
ssh-keygen -t RSA

cat ~/.ssh/id_rsa.pub 的内容复制到服务器对应 user home 的 authorized_keys 里面

修改 ~/.ssh/config

添加下面的内容

1
2
3
4
5
Host vagrant
HostName vagrant
User vagrant
Port 22
IdentityFile /Users/ruby/.ssh/id_rsa

Host 自定义的一个名称

HostName 服务器域名或者 ip

vagrant 是本机 /etc/hosts 已经配置了的 hosts

IdentityFile 是私钥的路径,和加到服务器的公钥是同一对

修改 /etc/ansible/hosts 配置

添加

1
2
[test-vagrant]
vagrant

test-vagrant 是分组名 (我们可以对某一组服务器执行某个命令)

vagrant 是 Host

不使用 .ssh/config 的 hosts 文件配置写法:在 hosts 里面每一行指定 hostname、user 等信息,如

1
2
3
testserver ansible_host=127.0.0.1 ansible_port=2222\
ansible_user=vagrant\
ansible_private_key_file=.vagrant/machines/default/virtualbox/private_key

测试

1
ansible vagrant -m command -a "pwd"

➜ ~ ansible vagrant -m command -a "pwd"
vagrant | CHANGED | rc=0 >>
/home/vagrant

弃用(还是使用 openresty 吧)

安装 LuaJIT

1
2
3
wget -c http://luajit.org/download/LuaJIT-2.0.5.tar.gz
tar -xzvf LuaJIT-2.0.5.tar.gz
make install PREFIX=/usr/local/LuaJIT

然后把下面这一行加到 ~/.bash_profile 或 ~/.zshrc

1
export LD_LIBRARY_PATH=/usr/local/LuaJIT/lib:$LD_LIBRARY_PATH

下载 nginx

1
2
3
nginx -v  # 假设是 1.11.5
wget http://nginx.org/download/nginx-1.11.5.tar.gz
tar -xzvf nginx-1.11.5.tar.gz

下载 nginx lua 模块

lua-nginx-module

1
2
wget https://github.com/openresty/lua-nginx-module/archive/v0.10.15.tar.gz
tar -xzvf v0.10.15.tar.gz

编译 nginx lua 模块

1
2
cd nginx-1.11.5
./configure ...(这里是 nginx -V 里的 configure 选项) --add-dynamic-module=../lua-nginx-module-0.10.15

编译完成会在 nginx-1.11.5/objs 下面出现 ngx_http_lua_module.so,这个就是编译生成的 lua 模块

修改 nginx 配置

在 nginx.conf 开头加上下面这一行:

1
load_module /root/nginx-1.11.3/objs/ngx_http_lua_module.so;

当然这里的路径需要根据实际情况修改。

测试

1
nginx -t

知识点

  • nginx -V 可以查看编译时的选项

Lumen 使用 dingo/api 的第一步便是 $app->register(Dingo\Api\Provider\LumenServiceProvider::class);,所以就从这个文件说起吧。

这个文件的父类是 \Dingo\Api\Provider\DingoServiceProvider,我们可以在这个文件中发现其注册了很多的东西到 App 容器里面,其中也有很多单例的对象等等等等,

但是,这些都不重要,重要的是 ::boot 方法里面的处理:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Boot the service provider.
*
* @return void
*/
public function boot()
{
parent::boot();

// 1. 加载 dingo/api 配置
$this->app->configure('api');

// 2. 获取 app 容器反射对象
$reflection = new ReflectionClass($this->app);

// 3. 拿出 app 容器中定义的中间件, 传递进 \Dingo\Api\Http\Middleware\Request 中间件中
$this->app[Request::class]->mergeMiddlewares(
$this->gatherAppMiddleware($reflection)
);

// 4. 把 \Dingo\Api\Http\Middleware\Request 中间件加到 app 中间件处理链起始位置,
// 这样一来,请求会先经过 \Dingo\Api\Http\Middleware\Request 中间件处理,然后才是我们使用 `app()->middleware` 定义的中间件。
$this->addRequestMiddlewareToBeginning($reflection);

// Because Lumen sets the route resolver at a very weird point we're going to
// have to use reflection whenever the request instance is rebound to
// set the route resolver to get the current route.
$this->app->rebinding(IlluminateRequest::class, function ($app, $request) {
$request->setRouteResolver(function () use ($app) {
$reflection = new ReflectionClass($app);

$property = $reflection->getProperty('currentRoute');
$property->setAccessible(true);

return $property->getValue($app);
});
});

$this->app->routeMiddleware([
'api.auth' => Auth::class,
'api.throttle' => RateLimit::class,
'api.controllers' => PrepareController::class,
]);
}

到此为止,我们已经知道,我们的请求会先经过 \Dingo\Api\Http\Middleware\Request 中间件处理,这是很关键的一步,接下来,我们看看这个中间件做了什么:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
try {
// 验证是否是 dingo 请求(前缀匹配上了 API_PREFIX 或者域名匹配上 API_DOMAIN),若不是会直接传递给下一个中间件
// (详情: \Dingo\Api\Provider\HttpServiceProvider::registerHttpValidation)
// 若是:
// 1. dingo 接管异常处理
// 2. 转换 \Illuminate\Http\Request 为 \Dingo\Api\Http\Request
// 3. 重新绑定 app 容器的 request 对象为 Dingo Request, 然后把 $request 传递给其他 app 容器中定义的中间件处理
// 4. 异常的时候返回异常响应对象
if ($this->validator->validateRequest($request)) {
$this->app->singleton(LaravelExceptionHandler::class, function ($app) {
return $app[ExceptionHandler::class];
});

$request = $this->app->make(RequestContract::class)->createFromIlluminate($request);

$this->events->fire(new RequestWasMatched($request, $this->app));

// 这个方法效果是所有中间件处理完之后才会执行控制器里面对应的方法
return $this->sendRequestThroughRouter($request);
}
} catch (Exception $exception) {
$this->exception->report($exception);

return $this->exception->handle($exception);
}

return $next($request);
}

最后一步,也是很关键的一步:

在所有中间件处理完之后,会把 $request 传递给 \Dingo\Api\Routing\Router::dispatch 处理,

处理完成后,使用 \Dingo\Api\Routing\Router::prepareResponse 处理控制器方法返回的响应结果

这是 dingo/api transformer include 操作实际执行的地方,

也许我们都曾经有过这样的疑惑:include 到底是在哪里进行数据库查询的? 其实是在这个方法里面执行的,具体细节有兴趣的执行研究。

这也算是 dingo 的一个特性了,但是 include 有太多坏处了,例如 N+1,但是也还是有解决方法的,不用 include... 还有嵌套 include 导致代码难以维护等等

原因: 项目重度依赖 Dingo, 直接放弃 Dingo 的代价太大

有兴趣了解原理的可以看 Dingo api 处理请求机制

各个依赖版本

  • laravel-s(3.5.8)

  • swoole 4.3.3

  • php-7.1.14

  • Lumen 5.5.38

  • Dingo/api 2.0.0-alpha2.2

步骤

修改 Laravel.php 的 handleDynamic 方法,如下

这一步需要另外 copy 一份 Laravel.php 出来,假设放在 App\Swoole\Laravel.php

App.php

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
... // 其他内容和原文件一致

public function handleDynamic(IlluminateRequest $request)
{
ob_start();

if ($this->isLumen()) {
// dingo router 路由处理
$response = app('Dingo\Api\Http\Middleware\Request')->handle($request, function ($request) {
return $request;
});

// 不是 dingo router 里面定义的路由
if ($response instanceof \Illuminate\Http\Request) {
$response = $this->app->dispatch($response);
}

if ($response instanceof SymfonyResponse) {
$content = $response->getContent();
} else {
$content = (string)$response;
}

if ($response instanceof \Symfony\Component\HttpFoundation\Response) {
$this->reflectionApp->callTerminableMiddleware($response);
}
} else {
$response = $this->kernel->handle($request);
$content = $response->getContent();
$this->kernel->terminate($request, $response);
}

// prefer content in response, secondly ob
if (!($response instanceof StreamedResponse) && strlen($content) === 0 && ob_get_length() > 0) {
$response->setContent(ob_get_contents());
}

ob_end_clean();

return $response;
}

... // 其他内容和原文件一致

原来的处理方式: $response = $this->app->dispatch($request); 这个处理方式会只使用 Lumen 的 app() 来处理 HTTP 请求,第一次请求正常,但是后续请求没什么意外是无法正常处理的。

新的处理方式: 使用 app('Dingo\Api\Http\Middleware\Request') 来处理 HTTP 请求,这是正常情况下 Dingo 处理请求的第一个经过的中间件,如果请求之后返回的还是一个 Request,而不是 Response,说明这不是使用 app('api.router') 定义的路由,退化为使用 $response = $this->app->dispatch($request); 处理该请求。

自动加载处理

然后在 bin/laravels.php $basePath 后面加上:

1
require_once $basePath . '/app/Swoole/Laravel.php';

目的: 自动加载机制在加载 Laravel 类的时候加载的是自定义的 Laravel.php,而不是原来的 Laravel.php 这样我们的修改就起效了。

Dingo 控制器在请求结束清理

添加文件 app/Cleaners/ControllerCleaner.php

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
<?php

namespace App\Cleaners;

use Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface;
use Illuminate\Container\Container;

class ControllerCleaner implements CleanerInterface
{
public function clean(Container $app, Container $snapshot)
{
$ref = new \ReflectionObject($app);
$property = $ref->getProperty('instances');
$property->setAccessible(true);
$instances = $property->getValue($app);

foreach ($instances as $key => $instance) {
if ($instance instanceof \Laravel\Lumen\Routing\Controller) {
$instance = null;
app()->offsetUnset($key);
}
}
}
}

然后在 config/laravels.php 中的 cleaners 中加上该 Cleaner:

1
2
3
'cleaners' => [
App\Cleaners\ControllerCleaner::class, // 单例控制器清除
],

Dingo 中间件处理

新增文件 App.php

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
<?php

namespace App\Cleaners;

use Dingo\Api\Http\Middleware\Request;
use Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface;
use Illuminate\Container\Container;

class MiddlewareCleaner implements CleanerInterface
{
public function clean(Container $app, Container $snapshot)
{
$reflect = new \ReflectionObject($snapshot);
$property = $reflect->getProperty('middleware');
$property->setAccessible(true);

$middleware = $property->getValue($snapshot);

// 移除 Dingo\Api\Http\Middleware\Request,防止死循环
$middleware = array_values(array_diff($middleware, [Request::class]));

app(Request::class)->setMiddlewares($middleware);
}
}

然后在 config/laravels.php 的 cleaners 中加上该 cleaner:

1
2
3
'cleaners' => [
App\Cleaners\MiddlewareCleaner::class, // 单例控制器清除
],

目的: 这个命名不是很准确地说明它的作用,其实这个文件的作用是把被 dingo 清除的中间件还原回来,这样后续的请求才会有第一次请求的中间件。

(dingo 会在其处理的周期内通过反射把 app 容器内的中间件清除,会导致后续请求没有经过这些中间件。)

这里说的中间件是定义在 app() App 容器中的中间件:

bootstrap/app.php

1
2
3
$app->middleware([
XxxMiddleware::class,
]);

php-fpm 环境下,由于每个请求相互之间不会相互影响,因此不会有问题,而在 laravel-s 环境下,请求结束之后,请求 context 不会被完全销毁,有可能会对后续请求造成影响。