lumen
为速度而生的 Laravel 框架
官网的介绍很简洁,而且 lumen 确实也很简单,我在调研了 lumen 相关组件(比如缓存,队列,校验,路由,中间件和最重要的容器)之后认为已经能够满足我目前这个微服务的需求了。
任务目标
因为业务需求,需要在内网服务B中获取到公网服务A中的数据,但是B服务并不能直接对接公网,于是需要开发一个relay 中转机来完成数据转存和交互。
任务列表
- 环境准备 【done】
- RSA数据加密 【done】
- guzzle请求封装 【done】
- 添加monolog日志【done】
- 数据库migrate【done】
- Event和Listener的业务应用 【done】
- Scheduler计划任务(基于crontab)【done】
- 使用Mail来发邮件
- Jobs和Queue业务应用
- 使用supervisor守护queue进程和java进程
- 添加sentry来获取服务日志信息和实现邮件报警
- jwt用户身份校验
- .env 文件的配置
- 可能的扩展 K8S docker
- 性能并发测试 【done】
环境准备
- 机器是centos6.8, 使用work用户, 安装 php(^7),mysql,nginx,redis
- yum 安装的同学可以试试 https://www.softwarecollectio...
-
安装composer
-
https://getcomposer.org/downl...
# 注意php的环境变量 php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php -r "if (hash_file('sha384', 'composer-setup.php') === '93b54496392c062774670ac18b134c3b3a95e5a5e5c8f1a9f115f203b75bf9a129d5daa8ba6a13e2cc8a1da0806388a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" php composer-setup.php php -r "unlink('composer-setup.php');" mv composer.phar /usr/local/bin/composer
-
-
安装lumen
- composer global require "laravel/lumen-installer"
- composer create-project --prefer-dist laravel/lumen YOURPROJECT
-
配置 .env
配置 Lumen 框架所有的配置信息都是存在 .env 文件中。一旦 Lumen 成功安装,你同时也要 配置本地环境。 应用程序密钥 在你安装完 Lumen 后,首先需要做的事情是设置一个随机字符串到应用程序密钥。通常这个密钥会有 32 字符长。 这个密钥可以被设置在 .env 配置文件中。如果你还没将 .env.example 文件重命名为 .env,那么你现在应该 去设置下。如果应用程序密钥没有被设置的话,你的用户 Session 和其它的加密数据都是不安全的!
-
配置nginx 和 php-fpm
-
配置nginx的server
server { listen 8080; server_name localhost; index index.php index.html index.htm; root /home/work/YOURPROJECT/public; error_page 404 /404.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { root /home/work/YOURPROJECT/public; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; #include fastcgi.conf; } }
- php-fpm的监听端口
- 推荐一篇文章:Nginx+Php-fpm运行原理详解
-
lumen 基础介绍
- lumen的入口文件是 public/index.php,在nginx配置文件中已有体现
-
初始化核心容器是 bootstrap/app.php 它做了几件非常重要的事情
- 加载了 composer的 autoload 自动加载
- 创建容器并可以选择开启 Facades 和 Eloquent (建议都开启,非常方便)
- Register Container Bindings:注册容器绑定 ExceptionHandler(后面monolog和sentry日志收集用到了) 和 ConsoleKernel(执行计划任务)
- Register Middleware:注册中间件,例如auth验证: $app->routeMiddleware(['auth' => AppHttpMiddlewareAuthenticate::class,]);
- 注册Service Providers
$app->register(App\Providers\AppServiceProvider::class); $app->register(App\Providers\AuthServiceProvider::class); $app->register(App\Providers\EventServiceProvider::class); 在AppServiceProvider 里还能一起注册多个provider // JWT $this->app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class); // redis $this->app->register(\Illuminate\Redis\RedisServiceProvider::class); // 方便IDE追踪代码的Helper,因为laravel使用了大量的魔术方法和call方法以至于,对IDE的支持并不友好,强烈推荐开发环境安装 $this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class); // sentry $this->app->register(\Sentry\SentryLaravel\SentryLumenServiceProvider::class);
- 加载route文件 routes/web.php
//localhost:8080/test 调用app/Http/Controllers/Controller.php的 test方法 $router->get("/test", ['uses' => "Controller@test"]); // 使用中间件进行用户校验 $router->group(['middleware' => 'auth:api'], function () use ($router) { $router->get('/auth/show', 'AuthController@getUser'); });
- 还可以添加其他初始化控制的handler,比如说这个 monolog日志等级和格式,以及集成sentry的config
$app->configureMonologUsing(function(Monolog\Logger $monoLog) use ($app){ // 设置processor的extra日志信息等级为WARNING以上,并且不展示Facade类的相关信息 $monoLog->pushProcessor(new \Monolog\Processor\IntrospectionProcessor(Monolog\Logger::WARNING, ['Facade'])); // monolog 日志发送到sentry $client = new Raven_Client(env('SENTRY_LARAVEL_DSN')); $handler = new Monolog\Handler\RavenHandler($client); $handler->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true, true)); $monoLog->pushHandler($handler); // 设置monolog 的日志处理handler return $monoLog->pushHandler( (new Monolog\Handler\RotatingFileHandler( env('APP_LOG_PATH') ?: storage_path('logs/lumen.log'), 90, env('APP_LOG_LEVEL') ?: Monolog\Logger::DEBUG) )->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true, true)) ); });
- 配置文件 config/ 和 .env 文件
- 其他目录文件用到时再具体说明
RSA数据加密
因为业务中包含部分敏感数据,所以,数据在传输过程中需要加密传输。选用了RSA非对称加密。
- 借鉴了 PHP 使用非对称加密算法(RSA)
- 但由于传输数据量较大,加密时会报错,所以采用了分段加密连接和分段解密
php使用openssl进行Rsa长数据加密(117)解密(128)
如果选择密钥是1024bit长的(openssl genrsa -out rsa_private_key.pem 1024),那么支持加密的明文长度字节最多只能是1024/8=128byte;
如果加密的padding填充方式选择的是OPENSSL_PKCS1_PADDING(这个要占用11个字节),那么明文长度最多只能就是128-11=117字节。如果超出,那么这些openssl加解密函数会返回false。
- 分享一个我的完成版的工具类
openssl genrsa -out rsa_private_key.pem 1024
//生成原始 RSA私钥文件
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key.pem
//将原始 RSA私钥转换为 pkcs8格式
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
使用tip
// 私钥加密则公钥解密,反之亦然
$data = \GuzzleHttp\json_encode($data);
$EncryptData = Rsa::privateEncrypt($data);
$data = Rsa::publicDecrypt($EncryptData);
guzzle使用
- 安装超简单 composer require guzzlehttp/guzzle:~6.0
- guzzle 支持PSR-7 http://docs.guzzlephp.org/en/...
- 官网的示例也很简单,发个post自定义参数的例子
use GuzzleHttp\Client;
$client = new Client();
// 发送 post 请求
$response = $client->request(
'POST', $this->queryUrl, [
'form_params' => [
'req' => $EncryptData
]
]);
$callback = $response->getBody()->getContents();
$callback = json_decode($callback, true);
- guzzle支持 异步请求
// Send an asynchronous request.
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org');
$promise = $client->sendAsync($request)->then(function ($response) {
echo 'I completed! ' . $response->getBody();
});
$promise->wait();
- 值的注意的是github上有一个很好玩的项目 https://github.com/kitetail/zttp
它在guzzle的基础上做了封装,采用链式调用
$response = Zttp::withHeaders(['Fancy' => 'Pants'])->post($url, [
'foo' => 'bar',
'baz' => 'qux',
]);
$response->json();
// => [
// 'whatever' => 'was returned',
// ];
$response->status();
// int
$response->isOk();
// true / false
#如果是guzzle 则需要更多的代码
$client = new Client();
$response = $client->request('POST', $url, [
'headers' => [
'Fancy' => 'Pants',
],
'form_params' => [
'foo' => 'bar',
'baz' => 'qux',
]
]);
json_decode($response->getBody());
monolog日志
- 在LaravelLumenApplication 中会初始化执行
/**
* Register container bindings for the application.
*
* @return void
*/
protected function registerLogBindings()
{
$this->singleton('Psr\Log\LoggerInterface', function () {
// monologConfigurator 我们在 bootstrap/app.php中已经初始化了
if ($this->monologConfigurator) {
return call_user_func($this->monologConfigurator, new Logger('lumen'));
} else {
// 这里new的 Logger 就是 Monolog 类
return new Logger('lumen', [$this->getMonologHandler()]);
}
});
}
- 因为monologConfigurator 我们在 bootstrap/app.php中已经初始化了,所以lumen实际实现的log类是 RotatingFileHandler(按日期分文件) 格式的log,里面还可以详细定义日志的格式,文件路径,日志等级等
- 中间有一段 sentry部分的代码,含义是添加一个monolog日志handler,在发生日志信息记录时,同步将日志信息发送给sentry的服务器,sentry服务器的接收地址在 .env的 SENTRY_LARAVEL_DSN 中记录
$app->configureMonologUsing(function(Monolog\Logger $monoLog) use ($app){
$monoLog->pushProcessor(new \Monolog\Processor\IntrospectionProcessor(Monolog\Logger::WARNING, ['Facade']));
// monolog 日志发送到sentry
$client = new Raven_Client(env('SENTRY_LARAVEL_DSN'));
$handler = new Monolog\Handler\RavenHandler($client);
$handler->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true, true));
$monoLog->pushHandler($handler);
return $monoLog->pushHandler(
(new Monolog\Handler\RotatingFileHandler(
env('APP_LOG_PATH') ?: storage_path('logs/lumen.log'),
90,
env('APP_LOG_LEVEL') ?: Monolog\Logger::DEBUG)
)->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true, true))
);
});
- 准备动作完成后使用方法就很简单了
use Illuminate\Support\Facades\Log;
Log::info(11);
// [2019-01-09 14:25:47] lumen.INFO: 11
Log::error('error info', $exception->getMessage());
数据库migrate
-
基本的使用就只有三步,详情请参考官网文档 数据库迁移
# 1 初始化迁移文件 php artisan make:migration create_Flights_table # 2 自定义表结构 class CreateFlightsTable extends Migration { public function up() { Schema::create('flights', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('airline'); $table->timestamps(); }); } } # 3 执行迁移,执行迁移的库是 .env 中配置好的 php artisan migrate
-
很推荐使用 migrate 来记录数据库,它的核心优势是:允许团队简单轻松的编辑并共享应用的数据库表结构
- 场景1:数据库迁移时,开发原本需要先从数据库导出表结构,然后在新的数据库上执行;现在只需要修改数据库连接参数,执行
php artisan migrate
就完成了 (线上同步配置文件可以使用分布式文件系统,比如Apollo) - 场景2:需要alert 字段或索引时,也只需要更新迁移文件然后执行更新,因为代码全程记录了所有数据库的修改记录,日后查看相关数据库信息时也更加方便(相当于把sql.log文件放在了php代码中管理)
- 场景1:数据库迁移时,开发原本需要先从数据库导出表结构,然后在新的数据库上执行;现在只需要修改数据库连接参数,执行
- 如果一个迁移文件执行后,内容做了修改,需要修改一下文件名称中的时间,不然执行不成功,因为在 migrations 表中已经记录该文件已同步完成的信息了
Event和Listener的业务应用
-
首先解决一个问题,为什么要使用Event+Listener 来处理业务?
- Event事件应当作为Hook来使用,实现的是代码结构的解耦,尤其是当一个业务模块需要同时关联多个业务模块时,Event+Listener 的工具可以通过解耦代码使代码的可维护性增加,并且可以避免重复代码的出现。
- 在Listener 中可以通过 implements ShouldQueue 这个接口来实现异步队列执行,从而优化接口性能
- 转载一篇有详细内容的文章 Laravel 中的 Event 和事件的概念
-
在初始化lumen后,代码中有Example示例 相关文件,更多内容可以查看官方文档
- AppEventsExampleEvent.php
- AppListenersExampleListener.php
- Appproviders/EventServiceProvider.php 配置触发关系
Scheduler计划任务
- scheduler 的使用使开发摆脱了一种不好的开发方式:在各种机器上跑茫茫多的脚本,时间一长这种模式几乎不可维护,一旦发生交接时更是特别容易遗漏机器和脚本。这种传统的“简单”方式,毫无疑问会造成相当多的麻烦。
- 现在 laravel 的 scheduler 提供了一种更易于使用和维护的计划任务方式。
过去,你可能需要在服务器上为每一个调度任务去创建 Cron 入口。但是这种方式很快就会变得不友好,因为这些任务调度不在源代码中,并且你每次都需要通过 SSH 链接登录到服务器中才能增加 Cron 入口。
Laravel 命令行调度器允许你在 Laravel 中对命令调度进行清晰流畅的定义。且使用这个任务调度器时,你只需要在你的服务器上创建单个 Cron 入口接口。你的任务调度在 app/Console/Kernel.php 的 schedule 方法中进行定义。
这个单一入口就是在crontab中添加一行
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
这个 Cron 为每分钟执行一次 Laravel 的命令行调度器。当 schedule:run 命令被执行的时候,Laravel 会根据你的调度执行预定的程序。
然后在 app/Console/Kernel.php 中定义任何你想要执行的命令,脚本,代码。
protected function schedule(Schedule $schedule)
{
// 调用一个闭包函数
$schedule->call(function () {
event(new GetData());
})->cron("0 */6 * * *");
// 调用 Artisan 命令
$schedule->command('emails:send --force')->daily();
// 调度 队列任务 分发任务到 "heartbeats" 队列...
$schedule->job(new Heartbeat, 'heartbeats')->everyMinute();
// 调用 Shell 命令
$schedule->exec('sh build.sh')->hourly();
// 甚至做闭包限制测试:如果给定的 Closure 返回结果为 true,只要没有其他约束条件阻止任务运行,任务就会一直执行下去
$schedule->command('emails:send')->daily()->when(function () {
return true;
});
// 规定任务只能在一台机器上执行
//为了说明任务应该在单个服务器上运行,在定义调度任务时使用 onOneServer 方法。第一个获取到任务的服务器会生成一个原子锁,用来防止其他服务器在同一时刻执行相同任务
->onOneServer();
// 任务输出到某个文件或发送到邮箱
->sendOutputTo($filePath);
->emailOutputTo($email);
}
- 还可以做一个安全的措施,本地备份数据库 Laravel定时任务备份数据库
使用Mail来发邮件
- 安装mail组件 composer require illuminate/mail
- 添加config文件 并在 bootstrap/app.php 中加载
"smtp",
'host' => "smtp.qq.com", // 根据你的邮件服务提供商来填
'port' => "465", // 同上
'encryption' => "ssl", // 同上 一般是tls或ssl
'username' => '[email protected]',
'password' => 'xxx', // 在qq邮箱中,这个密码是生成的校验码
'from' => [
'address' => '[email protected]',
'name' => 'xxx',
],
];
$app->configure('mail');
- 在 app/Providers/AppServiceProvider.php 或 bootstrap/app.php 中注服务
$this->app->register(\Illuminate\Mail\MailServiceProvider::class); //注册服务提供者
- mail的两个用法
// 发送文本
$text = '这里是测试';
Mail::raw($text, function($message) {
$message->to('[email protected]')->subject("test subject");
});
// 发送模板邮件, testMail 是模板的名字,创建在 resources/views/testMail.blade.php
Mail::send('testMail', ["data" => $data, "count" => $count], function ($message) {
$message->to(["[email protected]", "[email protected]"])
->cc(["[email protected]"])
->subject("test subject");
});
- 给个例子出来 resources/views/testMail.blade.php
simple data
数据
数据详情
数量
@foreach ($data as $key => $item)
{{ $key }}
@endforeach
data
{{ count($data) }}
@foreach ($diffCB as $item)
{{ $item }}
@endforeach
性能测试
- 开启opcache
- composer dump-autoload --optimize
不开启opcache
ab -c 100 -n 1000 localhost:8002/phpinfo
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx/1.10.2
Server Hostname: localhost
Server Port: 8002
Document Path: /test
Document Length: 92827 bytes
Concurrency Level: 100
Time taken for tests: 4.171 seconds
Complete requests: 1000
Failed requests: 140
(Connect: 0, Receive: 0, Length: 140, Exceptions: 0)
Write errors: 0
Total transferred: 92989847 bytes
HTML transferred: 92826847 bytes
Requests per second: 239.74 [#/sec] (mean)
Time per request: 417.113 [ms] (mean)
Time per request: 4.171 [ms] (mean, across all concurrent requests)
Transfer rate: 21771.20 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.8 0 4
Processing: 29 394 74.6 388 628
Waiting: 27 392 74.6 385 625
Total: 32 394 74.2 388 629
Percentage of the requests served within a certain time (ms)
50% 388
66% 407
75% 445
80% 451
90% 479
95% 517
98% 557
99% 570
100% 629 (longest request)
==开启opcache==
yum install php7.*-opcache (根据当前php版本做选择)
php -i | grep opcache.ini
修改 opcache.ini
// 大部分维持默认值,少部分值可以根据业务做调整
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=0
ab -c 100 -n 1000 localhost:8002/phpinfo
Benchmarking localhost (be patient)
; Enable Zend OPcache extension module
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx/1.10.2
Server Hostname: localhost
Server Port: 8002
Document Path: /test
Document Length: 93858 bytes
Concurrency Level: 100
Time taken for tests: 0.657 seconds
Complete requests: 1000
Failed requests: 298
(Connect: 0, Receive: 0, Length: 298, Exceptions: 0)
Write errors: 0
Total transferred: 94021119 bytes
HTML transferred: 93858119 bytes
Requests per second: 1522.02 [#/sec] (mean)
Time per request: 65.702 [ms] (mean)
Time per request: 0.657 [ms] (mean, across all concurrent requests)
Transfer rate: 139747.77 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 1.4 0 6
Processing: 15 61 15.8 54 119
Waiting: 10 61 15.9 54 119
Total: 19 61 15.9 54 121
Percentage of the requests served within a certain time (ms)
50% 54
66% 56
75% 62
80% 69
90% 89
95% 100
98% 108
99% 114
100% 121 (longest request)
可以看到并发大概提升了10倍,达到了1522qps(当然这是没有DB交互以及接口调用的简单输出响应测试),平均响应时间和数据传输速度提升了6-7倍。
-
在生产环境运行
composer dump-autoload --optimize
- composer autoload 慢的主要原因在于来自对 PSR-0 和 PSR-4 的支持,加载器得到一个类名时需要到文件系统里查找对应的类文件位置,这导致了很大的性能损耗,当然这在我们开发时还是有用的,这样我们添加的新的类文件就能即时生效。 但是在生产模式下,我们想要最快的找到这些类文件,并加载他们。
- composer dump-autoload --optimize 这个命令的本质是将 PSR-4/PSR-0 的规则转化为了 classmap 的规则, 因为 classmap 中包含了所有类名与类文件路径的对应关系,所以加载器不再需要到文件系统中查找文件了。可以从 classmap 中直接找到类文件的路径。
-
注意事项
- 建议开启 opcache , 这样会极大的加速类的加载。
- php5.5 以后的版本中默认自带了 opcache 。
- 这个命令并没有考虑到当在 classmap 中找不到目标类时的情况,当加载器找不到目标类时,仍旧会根据PSR-4/PSR-0 的规则去文件系统中查找
高可用问题思考
-
数据传输量过大可能导致的问题
- RSA加密失败
- 请求超时
- 数据库存储并发
- 列队失败重试和堵塞
- 数据操作日志监控和到达率监控
未完待续.....