微服务架构与RPC框架讲解

服务端架构的演变

微服务定义

维基上对其定义为:一种软件开发技术 - 面向服务的体系结构(SOA)架构样式的一种变体,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通。每个服务都围绕着具体业务进行构建,并且能够独立地部署到生产环境、类生产环境等。

后端架构演变

单体架构

业务初期用户业务不复杂、访问量较小的时,我们将应用服务、数据库、文件服务器部署在一台服务器上。

读写分离

随着并发访问量变大,一台机器已经无法满足我们的需求了。一般数据库最开始出现使用率过高。这时我们通常采用数据缓存方式减少直接对数据库的访问,或者增加数据库服务器,少量服务器用于写入,多台服务器读取,从而在保证数据一致的同时减少数据库压力。

集群

随着并发访问量继续变大,一台服务器无法满足用户请求,这时集群概念就产生了。集群简单来说就是一群运行着相同程序的服务器。通常一台服务器接收用户请求并做反向代理,处理数据交由集群上其他节点解决。这样每台服务器都可能处理任何业务。理论上是可以完全解决并发问题的。

异步架构

在某些特定的业务,例如需花费较长时间才能完成的业务。我们不必实时处理此类任务,当用户提交请求后,系统会把任务添加到消息队列中,完成之后改变数据的状态,用户在刷新或接到通知后知道任务已经处理完毕。

面向服务的架构(SOA)

虽然我们可以无限制的扩展集群从而解决访问量过大的问题,但是由于集群维护会随着机器增多呈现指数型增长。于是架构师根据业务自身的特性将整个系统拆分成一个个的小的系统,例如专门用于登录验证,专门用于购物车,专门用于支付等,上层应用将这些小系统组合起来形成一个完整的系统。这个阶段应该叫做应用切分。当业务越来越复杂,小系统间的调用关系会乱成一团,不同团队间的协议也可能不同。ESB出现可以梳理各种应用系统的复杂关系,使调用关系变得清晰。

在服务化架构中,系统依然被拆分为多个应用,但与此同时, 应用与应用之间相互冗余的部分也被抽取出来了,作为一个服务单独存在。比如上面讲到的购物车系统和支付系统中查询订单的业务代码,被提取出来作为一个订单查询服务而单独存在。这时服务化又带来了新的痛点,服务之间相互依赖,一个服务既可以作为Provider对外提供功能,又需要作为Consumer来依靠其他服务的实现,所以需要进行服务的治理

微服务

公司急速发展,越来越多的业务需求被加入到系统中,系统已经变得极为庞大。 不同应用和数据之间互相依赖,逻辑纠缠不清,项目的部署进入了混沌状态,对于大型、依赖关系复杂的系统需要一个更为有力的架构。为了解决系统复杂性服务解耦的问题,这时候该微服务和领域驱动建模(DDD)出场了。

比如在APP上要实现一个下单成功的功能,总不能在APP客户端既要调用订单系统的服务去更新订单,又要调用购物车系统提供的服务去更新购物车吧。而是只需要一个API,调用成功就表示用户成功支付,订单已完成。而这一的一个个应用层之间使用的API,也可以被应用层看做一个个的服务,这就是微服务架构。其中用来对这些微服务中的API进行统一管理的模块通常被称为服务网关

微服务为什么需要容器化

  • 没有容器技术,微服务落地是非常困难的,仅仅是资源管理就会及其复杂,更别提调度和编排了。
  • 容器和微服务是紧密集合在一起的,因为微服务会将单体应用拆分成很多小的应用,因而运维和持续集成会工作量变大,而容器技术能很好的解决这个问题 ,使得服务可以切割得更小,成为支撑微服务很好的平台。
  • 容器只是工具,微服务只是一个架构,两者没任何关系,只是从最佳实践来说容器和微服务比较切合。
  • 看微服务规模大小,服务拆分2-5个并发小,做与不做容器都可以,大项目几十上百个服务,高并发的服务需要部署多个,使用容器就能快速部署。

总结:使用微服务架构会对运维工作增大,而容器技术可以极大减少运维工作,使得微服务和容器技术非常契合。

远程过程调用(RPC)

在微服务和服务化架构中,不同的服务之间想要互相通信就只能通过网络,而系统之所以被做成服务化,其中一条主要的原因就是因为访问量巨大,所以不同的功能模块之间的网络通信是一个极其频繁的事情。HTTP协议作为一个功能强大的网络通信协议,随之带来的问题是开销太大,用在微服务架构下过犹不及。因此,RPC通信协议产生了,协议简单随之而来的是性能优越。

简单来说:服务器A(rpc客户端),服务器B(rpc服务端,方法b),那么服务器A通过rpc直接调用服务器B的方法b的过程。
其实现步骤如下:

  • 首先A与B之间建立一个TCP连接。
  • 然后A把需要调用的方法名以及方法参数序列化成字节流发送出去。
  • B接受A发送过来的字节流,然后反序列化得到目标方法名、参数,接着执行相应的方法调用得到结果,B将结果返回。
  • A接受远程调用结果

RPC的优点:

  • 解耦。
  • 跨语言。
  • 自定义协议。

PRC的缺点:

  • 依赖网络。
  • 异常处理困难,超时问题。

代码实现

注意,此代码为测试代码,实际线上功能还需解决很多问题,例如并发功能等。

// 代码结构
// |-RpcServer.php
// |-RpcClient.php
// |-service
//    |-News.php

// RpcServer.php 内容

<?php
/**
 * Description: Rpc服务端
 */
class RpcServer {

    /**
     * @var array
     * Description: 此类的基本配置
     */
    private $params = [
        'host'  => '',  // ip地址,列出来的目的是为了友好看出来此变量中存储的信息
        'port'  => '', // 端口
        'path'  => '' // 服务目录
    ];

    /**
     * @var array
     * Description: 本类常用配置
     */
    private $config = [
        'real_path' => '',
        'max_size'  => 2048 // 最大接收数据大小
    ];

    /**
     * @var nul
     * Description:
     */
    private $server = null;

    /**
     * Rpc constructor.
     */
    public function __construct($params)
    {
        $this->check();
        $this->init($params);
    }

    /**
     * Description: 必要验证
     */
    private function check() {
        $this->serverPath();
    }

    /**
     * Description: 初始化必要参数
     */
    private function init($params) {
        // 将传递过来的参数初始化
        $this->params = $params;
        // 创建tcpsocket服务
        $this->createServer();
    }

    /**
     * Description: 创建tcpsocket服务
     */
    private function createServer() {
        $this->server = stream_socket_server("tcp://{$this->params['host']}:{$this->params['port']}", $errno,$errstr);
        if (!$this->server) exit([
            $errno,$errstr
        ]);
    }

    /**
     * Description: rpc服务目录
     */
    public function serverPath() {
        $path = $this->params['path'];
        $realPath = realpath(__DIR__ . $path);
        if ($realPath === false ||!file_exists($realPath)) {
            exit("{$path} error!");
        }
        $this->config['real_path'] = $realPath;
    }

    /**
     * Description: 返回当前对象
     */
    public static function instance($params) {
        return new RpcServer($params);
    }

    /**
     * Description: 运行
     */
    public function run() {
        while (true) {
            $read   = [$this->server];
            $write  = null;
            $except = null;
            if (stream_select($read, $write, $except, null) > 0) {
                foreach ($read as $_socket) {
                    $client = stream_socket_accept($_socket);
                    if ($client) {
                        echo "有新连接\n";
                        $buf = fread($client, $this->config['max_size']);
                        print_r('接收到的原始数据:'.$buf."\n");
                        // 自定义协议目的是拿到类方法和参数(可改成自己定义的)
                        $this->parseProtocol($buf,$class, $method,$params);
                        // 执行方法
                        $this->execMethod($client, $class, $method, $params);
                        //关闭客户端
                        fclose($client);
                        echo "关闭了连接\n";
                    }
                }

            }
            var_dump($read, $write, $except);

        }
    }

    /**
     * @param $class
     * @param $method
     * @param $params
     * Description: 执行方法
     */
    private function execMethod($client, $class, $method, $params) {
        if($class && $method) {
            // 首字母转为大写
            $class = ucfirst($class);
            $file = $this->params['path'] . '/' . $class . '.php';
            //判断文件是否存在,如果有,则引入文件
            if(file_exists($file)) {
                require_once $file;
                //实例化类,并调用客户端指定的方法
                $obj = new $class();
                //如果有参数,则传入指定参数
                if(!$params) {
                    $data = $obj->$method();
                } else {
                    $data = $obj->$method($params);
                }
                // 打包数据
                $this->packProtocol($data);
                //把运行后的结果返回给客户端
                fwrite($client, $data);
            }
        } else {
            fwrite($client, 'class or method error');
        }
    }

    /**
     * Description: 解析协议
     */
    private function parseProtocol($buf, &$class, &$method, &$params) {
        $buf = json_decode($buf, true);
        $class = $buf['class']??'test';
        $method = $buf['method']??'tuzisir1';
        $params = $buf['params']??'';
    }

    /**
     * @param $data
     * Description: 打包协议
     */
    private function packProtocol(&$data) {
        $data = json_encode($data, JSON_UNESCAPED_UNICODE);
    }

}

RpcServer::instance([
    'host'  => '127.0.0.1',
    'port'  => 8888,
    'path'  => './service'
])->run();

// RpcClient.php 内容

<?php
/**
 * Description: Rpc客户端
 */
class RpcClient {

    /**
     * @var array
     * Description: 调用的地址
     */
    private $urlInfo = array();

    /**
     * RpcClient constructor.
     */
    public function __construct($url)
    {
        $this->urlInfo = parse_url($url);
    }

    /**
     * Description: 返回当前对象
     */
    public static function instance($url) {
        return new RpcClient($url);
    }

    public function __call($name, $arguments)
    {
        // TODO: Implement __call() method.
        //创建一个客户端
        $client = stream_socket_client("tcp://{$this->urlInfo['host']}:{$this->urlInfo['port']}", $errno, $errstr);
        if (!$client) {
            exit("{$errno} : {$errstr} \n");
        }
        $data = [
            'class'  => basename($this->urlInfo['path']),
            'method' => $name,
            'params' => $arguments
        ];
        //向服务端发送我们自定义的协议数据
        fwrite($client, json_encode($data));
        //读取服务端传来的数据
        $data = fread($client, 2048);
        //关闭客户端
        fclose($client);
        return $data;
    }
}
$cli = new RpcClient('http://127.0.0.1:8888/news');
echo $cli->tuzisir1()."\n";
echo $cli->tuzisir2(array('name' => 'tuzisir', 'age' => 23));

// News.php 内容

<?php
class News {
    public function display($data)
    {
        return json_encode(['result'=>"News display(), title is {$data['title']}"]);
    }
    public function tuzisir1() {
        return '我是无参方法';
    }
    public function tuzisir2($params) {
        return $params;
    }
}

此处评论已关闭