基于 lumen 的微服务架构实践

php框架

浏览数:952

2019-1-10

lumen

为速度而生的 Laravel 框架

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

任务目标

图片描述

因为业务需求,需要在内网服务B中获取到公网服务A中的数据,但是B服务并不能直接对接公网,于是需要开发一个relay 中转机来完成数据转存和交互。

任务列表

  • 环境准备 【done】
  • RSA数据加密 【done】
  • guzzle请求封装 【done】
  • 添加monolog日志【done】
  • 数据库migrate
  • Event和Listener的业务应用
  • Scheduler计划任务(基于crontab)
  • Jobs和Queue业务应用
  • 使用supervisor守护queue进程和java进程
  • 添加sentry来获取服务日志信息和实现邮件报警
  • jwt用户身份校验
  • .env 文件的配置
  • 可能的扩展 K8S docker
  • 性能并发测试

环境准备

  • 机器是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非对称加密。

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

<?php
namespace App\Lib\Oscar;
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
  • 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();

它在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

高可用问题思考

  • 数据传输量过大可能导致的问题

    • RSA加密失败
    • 请求超时
    • 数据库存储并发
    • 列队失败重试和堵塞
  • 数据操作日志监控和到达率监控

未完待续…..

原文地址:https://segmentfault.com/a/1190000017831939