PHP自动加载

前言

在PHP里引用类,通常使用include,include_once,require,require_once这个几个函数引入PHP文件。但如果你有一堆classes需要引入呢?

自动加载

在PHP5.3以后,为了解决批量加载文件的问题,__autoload函数就出现了。__autoload是标准的SPL扩展函数,当调用一个不存在的类,会尝试从该函数进行加载。这也就是所谓的延迟加载。

简单的代码实现如下:

<?php
// php/src/Autoload/AutoCase.php
class AutoCase
{
    public static function run()
    {
        echo "run by __autoload." . PHP_EOL;
    }
}

测试用例:

<?php
// php/tests/autoload/case1.php
function __autoload($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/AutoCase.php';
}
AutoCase::run();

执行测试用例:

php php/tests/autoload/case1.php
# 输出
run by __autoload.

内部实现

实际上__autoload函数的内部实现是spl_autoload_register。
代码实现如下:

<?php
// php/src/Autoload/SplCase.php
class AutoCase
{
    public static function run()
    {
        echo "run by spl_autoload_register." . PHP_EOL;
    }
}

测试用例:

<?php
// php/tests/autoload/case2.php
spl_autoload_register(function($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/SplCase.php';
});
AutoCase::run();

执行测试用例:

php php/tests/autoload/case2.php
# 输出
run by spl_autoload_register.

那如果两个函数同时存在呢?会出现什么情况呢?

测试用例如下:

<?php
// php/tests/autoload/case3.php
function __autoload($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/AutoCase.php';
}
spl_autoload_register(function($class)
{
    $dir = dirname(dirname(__DIR__));
    include_once $dir . '/src/Autoload/SplCase.php';
});
AutoCase::run();

执行测试用例:

php php/tests/autoload/case3.php
# 输出
run by spl_autoload_register.

事实上,spl_autoload_register函数默认会覆盖__autoload加载规则。

为什么要使用spl_autoload_register

首先,PHP官方在7.2已经把__autoload 变为DEPRECATED,也就是废弃的状态,后续该函数肯定会被删除的。
其次,spl_autoload_register函数可以有多个自动加载函数,更加灵活地加载自定义类。而__autoload 只能定义一次,只能有一个加载函数。
最后,spl_autoload_register与__autoload 同时存在的话,spl_autoload_register函数默认会覆盖__autoload加载规则。

spl_autoload_register应用

spl_autoload_register常用于多个类的加载,在composer里,composer install 后会生成一个vendor目录,有一个autoload.php。源代码如下:

<?php
// vendor/autoload.php
// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInitc8bbd05443dfc4a7281619cb2b14f876::getLoader();

实际上的加载规则在autoload_real.php和ClassLoader.php

<?php
// vendor/composer/autoload_real.php
<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInitc8bbd05443dfc4a7281619cb2b14f876
{
    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }
    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }
                  spl_autoload_register(array('ComposerAutoloaderInitc8bbd05443dfc4a7281619cb2b14f876', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        // bala bala
        $loader->register(true);
    }
}

class ClassLoader
{
   /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
    }
}

总结

我们对比了PHP的spl_autoload_register与__autoload 加载的实现。在后续的版本中,__autoload 实现是应该要废弃的。spl_autoload_register与__autoload 同时存在的话,spl_autoload_register函数默认会覆盖__autoload加载规则。我把上面的用例相关的代码在GitHub版本库,你可以随时查看。

Swoole必知必会

前言

PHP这门语言从诞生到现在差不多20年了,一直是作为Web领域快速开发的首选语言。PHP是一个很简单的语言,日常场景是这样的,从数据库取出数据,对模板进行渲染并直接输出到Web浏览器。但作为一个有追求的PHP开发者,想要真正学习后端架构技术的话,Swoole可能是你不得不了解的PHP扩展。

什么是Swoole

在Swoole的官方网站是这样来定义Swoole的。

Swoole是一个使用事件驱动、异步、非阻塞IO模型的高性能网络框架,具有可扩展性和高效率。它使PHP开发人员能够用PHP编程语言编写高性能、可伸缩、并发的TCP、UDP、Unix套接字、HTTP、WebSocket服务,而不需要太多关于非阻塞I/O编程和低级Linux内核的知识。Swoole比较适合用于服务器端开发,支持类似Go语言的协程,可以使用完全同步的代码实现自动协程调度。

从上面的定义,可以看出,其实学习Swoole就是学习后端编程。我们来看下Swoole编程与传统的PHP开发有什么区别。

运行模式

作为LNMP组件中的开发语言,PHP最常见的运行模式是Fast-CGI模式。PHP-FPM是运行FastCGI协议上的进程管理器,管理的主要是PHP处理CGI协议的请求进程,带有进程池的管理以及常驻进程的服务,高效的处理请求,提升请求并发量。

Swoole是运行在PHP-CLI模式下。PHP-CLI是CGI协议的简化版本,适合开发SHELL应用,脚本开发。

网络编程

PHP最常使用的请求协议是Http+Json,用户的请求Nginx服务器,Nginx将用户的请求变量转发给Fast-CGI模块,Fast-CGI将处理完的请求结果返回给Nginx服务,Nginx收到返回后发送结果给Web浏览器。

Swoole支持的协议会比较多。其中TCP/UDP协议可以应用于游戏服务器端开发,WebSocket协议常用于实时消息推送,比如聊天室等。Http Server可以用于高性能的Web服务,API服务等。甚至还有基于Swoole的微服务框架,比如Swoft。

底层知识

PHP开发者几乎很少去关注进程/线程方面的问题,因为通常情况下,都被Nginx和PHP-FPM已经处理了。

Swoole编程,就需要关注底层的实现:

  1. 需要学习异步编程模式
  2. 什么是多进程?什么是多线程?如何实现常驻进程?
  3. 如何使用类Go语言的协程,来提高程序的性能?

为什么你应该学习Swoole

高性能

学习Swoole并不需要重新学习一门语言,之前学习到PHP基础知识,也依然有效。你可以很容易采用Swoole搭建高性能的Http服务,WebSocket服务,甚至是TCP服务。Swoole提供异步的Redis,Http,WebSocket客户端,异步任务等。

协程

Swoole最新的版本拥有高可用性的内置协程,你可以采用同步的代码实现异步的高性能。你可以将协程理解为轻量级的线程,可以非常容易地在进程中创建成千上万个协程。

后端编程

最后一个,也是我觉得重要的一个原因。Swoole可能是PHP开发者切入到后端编程最简单的方式。如果你想要实现一个常驻进程,高性能的后端服务,Swoole可能是当前PHP开发工程师唯一的选择。

从长远来看,开发语言只是程序员实现的工具而已,真正解决问题是后端架构技术。作为一个PHP业务开发工程师,所能实现的价值是有上限的。而选择后端编程,你所能达到的上限几乎是没有。 后端编程的方向有很多,比如偏底层方向的操作系统,文件系统等,偏架构方向的分布式系统架构、微服务、DevOps、Cloud Native等,还有偏数据方向的大数据、机器学习、人工智能……

最后,我总结下学习Swoole编程需要掌握的知识:

  1. Linux下进程和线程的概念,进程/线程切换是如何调度的,进程间通信的基本知识
  2. Socket编程知识,基本操作如accept/connect、send/recv等,接受/发送缓冲区,阻塞/非阻塞等概念。
  3. IO复用,如select/epoll事件循环
  4. 熟悉TCP/UDP/WebSocket等协议
  5. Linux下调试命令

总结

我从什么是Swoole,为什么你应该学习Swoole,怎么学习Swoole等这几个方面总结了自己学习Swoole的方法论。学习新的知识是为了自己能有所成长,一个合格的工程师,应该时刻保持学习的心态,勇敢的尝试新的技术。

Etcd必知必会

前言

在前面的k8s的学习中,我们知道etcd存储了整个集群的状态信息,集群状态是非常重要的信息,在分布式环境下要能容忍失败,而且还不能挂。那么etcd是怎么做到的呢?

什么是etcd

我们来看下官方是怎么定义etcd的。

etcd是一个强一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器群访问的数据。 它优雅地处理网络分区期间的领导者选举,并且可以容忍机器故障,即使在领导者节点中也是如此。

我们看到了好几个关键字:分布式,键值,网络分区,机器故障,领导选举,尝试分别对这些名词进行解释下。

分布式,那么在架构上etcd部署应该多台服务器,集群部署一般由3-7个节点组成。

键值,类似于标准文件系统的数据存储。

网络分区,大多数分布式系统都分布在多个子网络,而区间通信可能会失败,就变成了网络分区。

机器故障,也就是集群中的节点,会因为各种原因失败,无法访问。

领导选举,节点挂掉了,领导者直接踢掉这台服务器即可,那么领导者也挂掉了呢,这时候就重新选择新的领导者了。

实现原理

etcd使用grpc作为消息传递的协议。对于不支持grpc的语言,提供Http Restful代理,该代理将Http/Json请求转换为grpc消息。

etcd采用Raft协议解决多个节点数据一致性。无论是领导节点还是从节点挂掉了,etcd都可以继续提供服务。

etcd底层存储在v2版本只是一个纯内存的实现,数据并没有存储在磁盘上,v3版本的实例采用bolt实现了数据的持久化。

Raft

etcd最核心的部分应该就raft协议。在etcd中,最常见的有2种角色,也就是Leader和Follower。Candidate角色在选举的时候开始出现。

这3个角色的规则如下:

Followers:

  1. 响应Leader 和 Candidate 发出的RPC请求
  2. 如果选举超时,没有从当前的Leader收到AppendEntries RPC请求或者没有收到节点给予的候选人投票,那么就把自己变为Candidate

Candidates:

  1. 成为候选人后,开始选举:
    增加当前的任期
    自己投票
    重置选举计时器
    发送equestVote RPC请求到所有其他服务器
  2. 如果从大多数服务器收到投票,那么就成为Leader
  3. 如果从Leader收到AppendEntries RPC请求,那么就变为Follower
  4. 如果选举超时,等待150-300ms重新发起选举

Leader:

  1. 选举期间:发送初始化空的AppendEntries RPC 心跳请求到每一个服务器,在idle空闲期间重复这个操作,防止选举超时。
  2. 如果从客户端收到,追加日志到本地日志,在进入状态机后响应请求。
  3. 如果Follower的最后日志索引(last log index) 大于等于 下一个索引(nextIndex),发送AppendEntries RPC请求,下一个日志从nextIndex开始。如果成功,为Follower更新 nextIndex和matchIndex。如果追加日志因不一致失败,递减nextIndex 并重试。

下面我们尝试从3个case出发,了解raft协议是如何实现数据一致性的。

第一次选举

使用的Raft协议的etcd集群在启动节点时,都是Follower的角色。集群中的Follower节点长时间没有收到Leader的心跳请求,就会进行预选举或者选举流程。

节点首先把自己变为Candidate角色,并给自己投票,然后对其他节点发起投票请求。当前节点接受到大于51%的节点投票,就会成为Leader。

节点失败

部分节点失败,并不会影响etcd服务正常的运行,客户端依然可以正常读写数据。

Leader并不需要等待全部的Follower写成功,只需要大部分,也就是超过半数即可。

失败的节点,重新加入集群,必须通过Leader维护的nextIndex索引,同步复制最新的日志条目。

Leader失败

Follower在指定时间内,如果没有收到AppendEntries RPC请求,那么就会发起投票,选举出新的Leader。

日志一致性

选举出Leader后,采用定时器同步日志到Follower节点。Raft维护着日志匹配的特性:

  1. 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
  2. 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。

Raft采用以下2个办法来保证日志的一致性:

  1. 选举限制。一个候选人,必须包含最新的日志列表,也就是说它的日志是在当前集群中最新的。Raft通过对比最后一条日志的索引值和任期号,决定日志的是否为最新。如果两份日志的任期不一致,那么任期号较大的日志为最新。如果两份日志任期号相同,那么日志索引值较大的为最新。
  2. 提交之前任期内的日志条目。Leader在当前任期里的日志通过计算副本可以提交。一旦当前任期的日志以这种方式被提交,因为日志是匹配的,那么之前的日志也一定会被提交。

总结

在这篇文章里,我们理解了etcd的定义,实现原理以及Raft协议的实现。当然,文章是从设计原理以及中文版的论文来尝试理解Raft协议,毕竟没有阅读源码来着直接,如果可能,阅读源码肯定是最快了解Raft的协议实现的方法了。

参考链接

  1. 寻找一种易于理解的一致性算法