php laravel微服务架构,基于 lumen 的微服务架构实践

lumen

为速度而生的 Laravel 框架

官网的介绍很简洁,而且 lumen 确实也很简单,我在调研了 lumen 相关组件(比如缓存,队列,校验,路由,中间件和最重要的容器)之后认为已经能够满足我目前这个微服务的需求了。

任务目标

bVbmYho?w=1205&h=561

因为业务需求,需要在内网服务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

安装composer

# 注意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的监听端口

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非对称加密。

如果选择密钥是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

namespace App\Lib;

class Rsa

{

private static $PRIVATE_KEY =

'-----BEGIN RSA PRIVATE KEY-----

xxxxxxxxxxxxx完整复制过来xxxxxxxxxxxxxxxxxxx

-----END RSA PRIVATE KEY-----';

private static $PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----

xxxxxxxxxxxxx完整复制过来xxxxxxxxxxxxxxxxxxx

-----END PUBLIC KEY-----';

/**

* 获取私钥

* @return bool|resource

*/

private static function getPrivateKey()

{

$privateKey = self::$PRIVATE_KEY;

return openssl_pkey_get_private($privateKey);

}

/**

* 获取公钥

* @return bool|resource

*/

private static function getPublicKey()

{

$publicKey = self::$PUBLIC_KEY;

return openssl_pkey_get_public($publicKey);

}

/**

* 私钥加密

* @param string $data

* @return null|string

*/

public static function privateEncrypt($data = '')

{

if (!is_string($data)) {

return null;

}

$EncryptStr = '';

foreach (str_split($data, 117) as $chunk) {

openssl_private_encrypt($chunk, $encryptData, self::getPrivateKey());

$EncryptStr .= $encryptData;

}

return base64_encode($EncryptStr);

}

/**

* 公钥加密

* @param string $data

* @return null|string

*/

public static function publicEncrypt($data = '')

{

if (!is_string($data)) {

return null;

}

return openssl_public_encrypt($data,$encrypted,self::getPublicKey()) ? base64_encode($encrypted) : null;

}

/**

* 私钥解密

* @param string $encrypted

* @return null

*/

public static function privateDecrypt($encrypted = '')

{

$DecryptStr = '';

foreach (str_split(base64_decode($encrypted), 128) as $chunk) {

openssl_private_decrypt($chunk, $decryptData, self::getPrivateKey());

$DecryptStr .= $decryptData;

}

return $DecryptStr;

}

/**

* 公钥解密

* @param string $encrypted

* @return null

*/

public static function publicDecrypt($encrypted = '')

{

if (!is_string($encrypted)) {

return null;

}

return (openssl_public_decrypt(base64_decode($encrypted), $decrypted, self::getPublicKey())) ? $decrypted : null;

}

}

使用tip

// 私钥加密则公钥解密,反之亦然

$data = \GuzzleHttp\json_encode($data);

$EncryptData = Rsa::privateEncrypt($data);

$data = Rsa::publicDecrypt($EncryptData);

guzzle使用

安装超简单 composer require guzzlehttp/guzzle:~6.0

官网的示例也很简单,发个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();

它在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代码中管理)

如果一个迁移文件执行后,内容做了修改,需要修改一下文件名称中的时间,不然执行不成功,因为在 migrations 表中已经记录该文件已同步完成的信息了

Event和Listener的业务应用

首先解决一个问题,为什么要使用Event+Listener 来处理业务?

Event事件应当作为Hook来使用,实现的是代码结构的解耦,尤其是当一个业务模块需要同时关联多个业务模块时,Event+Listener 的工具可以通过解耦代码使代码的可维护性增加,并且可以避免重复代码的出现。

在Listener 中可以通过 implements ShouldQueue 这个接口来实现异步队列执行,从而优化接口性能

在初始化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 中加载

//qq邮件

return [

'driver' => "smtp",

'host' => "smtp.qq.com", // 根据你的邮件服务提供商来填

'port' => "465", // 同上

'encryption' => "ssl", // 同上 一般是tls或ssl

'username' => 'xxx@qq.com',

'password' => 'xxx', // 在qq邮箱中,这个密码是生成的校验码

'from' => [

'address' => 'xxx@qq.com',

'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('xxx@qiyi.com')->subject("test subject");

});

// 发送模板邮件, testMail 是模板的名字,创建在 resources/views/testMail.blade.php

Mail::send('testMail', ["data" => $data, "count" => $count], function ($message) {

$message->to(["xxx@qiyi.com", "xxx@qiyi.com"])

->cc(["liguopu@qiyi.com"])

->subject("test subject");

});

给个例子出来 resources/views/testMail.blade.php

simple data

/* gridtable */

table.gridtable {

font-family: verdana,arial,sans-serif;

font-size:14px;

color:#333333;

border-width: 1px;

border-color: #666666;

border-collapse: collapse;

}

table.gridtable th {

border-width: 1px;

padding: 5px;

border-style: solid;

border-color: #666666;

background-color: #dedede;

}

table.gridtable td {

border-width: 1px;

padding: 5px;

border-style: solid;

border-color: #666666;

background-color: #ffffff;

}

数据

数据详情数量

@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加密失败

请求超时

数据库存储并发

列队失败重试和堵塞

数据操作日志监控和到达率监控

未完待续.....

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值