undo表空间

undo tablespace 中包含undo log,是一种独立的表空间。在mysql启动时会默认创建两个undo表空间,且系统要求起码有两个活跃的undo表空间以支持自动truncate功能。(这样可以保证一个离线truncate另一个继续提供服务)。undo表空间文件创建在–innodb_undo_derectory变量指定的位置,默认在data目录下,文件名undo_N格式。每个文件的初始大小基于–innodb_page_size参数(默认16K), 默认大小为10M。

添加undo表空间

8.0.14版本以前,可以在启动时设置–innodb_undo_tablespaces来指定创建的undo表空间,后续版本废弃改参数,创建可以使用

create undo tablespace tablespace_name add datafile "filename";

使用该语法时,指定的filename可以是绝对路径,但是该路径必须是innodb可以识别的,–innodb_directoried变量定义了这些路径,而且不管该变量如何显示定义,都会包含三个默认路径,分别是–innodb_undo_directory, –data_dir,–innodb_data_home_dir,mysql启动时会在这些路径里扫描寻找undo tablespaces。
如果没有指定绝对路径,那么undo tablespace会被创建在innodb_undo_directory下,如果该变量为空,会创建在mysql的data(/var/lib/mysql)目录下.

删除undo表空间

create undo tablespace语法创建的undo表空间可以使用drop undo tablespace语法来删除(默认创建的表空间不可用),在删除前必须保证该空间是empty状态。
在清空一个表空间前必须把它的状态设置为inactive,确保该表空间的rollback segment的undo segment不会分配给事务使用。

alter undo tablespace tablespace_name set inactive;

设置后会等待已经使用该表空间的事务提交,待所有的事务提交后,purge线程开始truncate表空间至初始大小,这时候表空间的状态为empty,即可以被drop。

 drop undo tablespace tablespace_name;

可以在INNODB_TABLESPACES 表里查询到每个表空间的状态,包括undo表空间

 SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES 
 WHERE NAME LIKE tablepace_name;

默认的undo表空间不可以被drop但是可以设置成inactive状态,但是必须保证起码有两个活跃的表空间以支持表空间的自动truncate,否则会报错。

移动undo表空间

用create undo tablespace语法创建的undo表空间文件可以移动到任何innndb可以识别的路径,mysql在启动时会找到他们,但是默认创建的undo表空间不可移动,如果默认undo表空间被移动了,那么必须在启动时更改–innodb_undo_directory参数为移动后的路径。

清除undo表空间

一个表空间文件默认最大为1G,由–innodb_max_undo_log_size参数控制,innodb提供了自动和手动两种方式清除undo log
自动清除由purge线程(垃圾回收线程,可能有多个,后台独立运行,参数–innodb_purge_threads指定)完成,需要开启–innodb_undo_log_truncate参数,默认是开启的。

由于自动truncate必须保证有两个活动的undo表空间,那么手动就必须有三个获得undo表空间,因为可能有一个正在truncate而处于offline。
两种方式清除过程如上所述,等undo表空间的状态时empty后,状态再设置成active

对于自动清除可以使用–innodb_purge_rseg_truncate_frequency参数来提高清除频率,默认是purge被触发(undo tablespace大小超过–innodb_max_undo_log_size参数)了128次后,执行一次truncate,可以设置更小的值加速truncate。

MVCC多版本并发控制

mvcc可以保证RR隔离级别下一致性读(consistent read)的问题,也解决了读写冲突的问题。

读写冲突的来源

没有mvcc控制的情况下,假如我们的场景是读远大于写的场景,那么事务A对某读频繁的表id为1的行进行更新,根据两阶段锁协议,那么该行是会加一个X锁,且没有释放,这样其他事务在读取的时候会被锁上,导致并发能力降低。

而mvcc允许行被加锁的情况下,读取旧版本数据。

一致性读

innodb的一致性读通过一致性读视图及undo log实现。

Undo Log

数据库中的每一行都保存这不同的版本,旧版本保存在undo log中。

当一行数据被创建时,会有一些额外的数据,如下图(其中rowid只会在没有主键情况下生成,一般某个表没有指定主键innodb会选择第一个唯一索引作为主键,如果在没有会隐式的生成一列rowid):

当改行数据被修改时,

为该行记录生成undo log, 新纪录的db_roll_pt指针指向该条记录。

undo logs会有一个单独的purge线程负责清除,保证undo logs不会无限增长。

undo logs会有一些参数控制:

innodb_undo_directory:将undo log存储位置独立出来,默认位置为安装目录。

inndb_undo_tablespace:mysql初始化时分配的表空间个数,默认为128,之后会在data目录下创建制定个数的undo文件,文件名以undo_000,undo_001递增。(8.0.14版本后不再支持,具体undo tablespace的介绍,看详情)

(之所以mysql可以把undo log存储独立出来,原因是可以把日志部署到ssd上,提高事务的并发读。)

undo log都是保存在undo log segment中,undo log segment 保存在rollback segment中。

每个回滚段包含1024个undo log segment, 而mysql最大支持128个回滚段(innodb_rollback_segments参数可以显示控制)。因为每个事务在读写undo log时都要独占一块undo log segment,所以理论上最大支持1024 * 128个并发事务操作。

 

一致性读的实现

RR级别下,数据可见性的规则就是,事务启动前已经提交的事务的更改可见,事务启动后的事务提交不可见。(begin/start transaction并不是一个事务的起点,在该指令后的第一个操作标的语句才标识着事务启动。)

每个事务在启动时会获得一个递增的事务id,记trx_id,并且会创建一个read-view的struct,该结构体包含一个当前所有活动事务的id集合(包括当前事务),一个最小活动事务id和一个最大活动事务id,之前提到过,每一行数据都有一个额外的db_trx_id, 读取到该行时,如果该db_trx_id与当前事务id不一致,就开始读取undo log,然后利用undo log和活动事务集合判断出可以读取的值,判断逻辑图如下:

以上概念称为一致性读,与之对应的有一个”当前读”, 即:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)

举个例子:

k初始为1。

那么B再去查询时结果为3,这就是当前读。

缓冲区溢出攻击

输入的大小超出了分配的缓冲区内存大小,导致溢出。

char * string[5];
gets(string);  // 输入helloworld

程序相关的数据一般保存在内存的以下区域:

  1.  数据段:静态存储区(比如字符串字面量)和全局变量
  2. 代码段:程序机器码
  3. 栈:函数栈帧,一个函数栈帧包括参数,返回地址(函数返回后cpu执行的下一条指令的地址),局部变量等
  4. 堆:程序运行时动态申请的内存数据

当调用函数时,逻辑堆栈帧被压入栈中,堆栈帧包括函数的参数、返回地址、EBP(EBP是当前函数的存取指针,即存储或者读取数时的指针基地址,可以看成一个标准的函数起始代码)和局部变量(如果函数有局部变量)。程序执行结束后,局部变量的内容将会消失,但是不会被清除。
当函数返回时,逻辑堆栈帧从栈中被弹出,然后弹出EBP,恢复堆栈到调用函数时的地址,最后弹出返回地址到EIP(寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。),从而继续运行程序。所以假如有下图的情况:

由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:

  1. 淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。

       2. 淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。

       3. 淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!

       4. 淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。

       5. 淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。

而缓冲区溢出攻击就是利用了第三点,改变了cpu要执行的下一条指令的地址。

但是从实现上来说很难,因为没法预估返回地址在栈区的什么位置,也就无法确定溢出多少字节才能覆盖到返回地址。

PDO的深入理解

pdo可以通过预处理语句和参数绑定防注入,那么是不是就是绝对安全呢,根据之前的开发中踩的坑以及读到的一些相关资料,做一些总结。

 

本地预处理和模拟预处理

在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES,作用是打开模拟预处理(true)或者关闭(false)。手册描述

模拟预处理是防止某些数据库不支持预处理,比如sqllite,这样pdo会在本地进行预处理模拟。然后会对绑定的参数进行转义处理,最后将拼接好的sql语句发送给mysql服务器进行处理。也就是预处理工作并不是发生在数据库层面。与数据库的交互只有一次,而本地预处理只是将sql模板发送给数据库,数据库完成预处理操作,包括解析,编译,生成执行计划,之后pdo再将参数发给数据完成数据绑定,换句话说,为什么native prepare statment(数据库层面的prepare)可以防止sql注入,是因为sql模板已经交由数据库解析完成,sql语意确定,那么所有的参数都会被视作字符串参数,而不是sql语句或者表达式。

如果我们使用mysql,建议关闭模拟预处理。(某些低版本mysql不支持预处理除外)。

另外注意,即使mysql版本支持预处理,某些语句mysql无法支持prepare,那么pdo在处理时还是会使用模拟预处理。这样可能会存在注入的风险。

However, be aware that PDO will silently fallback to emulating statements that MySQL can’t prepare natively,参考这篇回答

 

字符集设置的正确性

还是上面那片问答,高票答主给了一个例子

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

为什么这个例子可以完成注入,答主也给了解释,原因就是上面所说的,pdo会进行模拟预处理,这样会在本地进行转义,具体转义可能类似于mysql_real_escape_string()(PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除)或者PDO::quote(),此类函数和addslashes这种函数区别在于,它会读取当前pdo驱动设置的字符集,而set names gbk并没有设置这个值,至于为什么,看鸟哥这篇文章,所以该函数在转义时还是认为pdo驱动的字符集是latin或者是utf8,于是它给x27转义,结果就变成了xbf5c27(x27对应ascii的\),该而数据库服务器的字符集被设置成gbk,于是xbf5c27被解释成

縗'

于是完成了注入,这也就是宽字节注入,经典的5c问题。

所以除了关闭模拟预处理之外,设置字符集也很重要,除了set names utf8,还需要设置pdo驱动的字符集,防止出现fallback成模拟预处理的情况,php5.6.3版本以上可以在初始化pdo时设置,如下

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', $user, $password);

或者mysqli_character_set_name函数 。

所以说最好的做法是关闭模拟预处理和设置正确的字符集。

 

另外补充几点:
1. MySQL客户端服务端针对预处理语句和非预处理语句定义了不同的数据传输协议。预处理语句使用所谓的二进制传输协议,MySQL服务端以二进制发送数据,发送之前不会被序列化。客户端不会仅接收字符串,相反的,他们将获取二进制数据并且尝试去转换值到合适的PHP数据类型。

2. 虽然预处理语句可以帮助我们防止sql注入,但是前提是我们必须正确的使用,例如下面代码就没用了:

$sql = 'select * from tab where name=' . $name . ' and password = ? limit 1';
$pdo->prepared($sql);
$pdo->execute([$password]);

这种需要保证所有的传入参数都先使用占位符代替。

 

参考:

https://www.v2ex.com/t/362646

https://github.com/wuzehv/blog/issues/32

http://zhangxugg-163-com.iteye.com/blog/1835721

服务容器-iocContainer

最近研究了一下服务容器的概念。网上也找了很多资料,也动手敲了不少代码,总结一下自己的看法。

服务容器应该不能说是一种技术,而是一种设计思想,主要解决了我们在代码中类依赖的问题。看个例子:

class Drive
{
    public $car;
    public function __construct(Car $car)
    {
         $this->car = $car;
    }

    public function do()
    {
         echo "drive ";
         $this->car->do();
    }
}
class People
{
    public $drive;
    public function __construct(Drive $drive)
    {
         $this->drive = $drive;
    }

    public function do()
    {
         echo "people ";
         $this->dirve->do();
    }
}
class Car { 
       public function do() { 
           echo "car";
       } 
} 

如代码所示,我们想要得到一个people类,必须依赖drive类,而drive类必须依赖car类,实际代码中可能比这个要复杂的多,如果有一种方法,我们直接传入people类名,就能返回people对象,多方便。这就是容器,容器可以帮助我们完成类依赖的自动处理和依赖注入等很多复杂的工作,对外则只需要很简单的调用。

这里有一个依赖注入的概念,对应的还有一个控制反转的概念,可以看看这篇博文介绍。

依赖注入

依赖注入和控制反转是对同一件事情的不同描述,它们描述的角度不同。依赖注入是从应用程序的角度在描述,应用程序依赖容
器创建并注入它所需要的外部资源。而控制反转是从容器的角度在描述,容器控制应用程序,由容器反向的向应用程序注入应用
程序所需要的外部资源。

那怎么实现一个容器呢,容器的核心就是帮我们理清一个类的依赖关系,比如上面的people类依赖drive类,那么我们怎么知道这种关系呢。其实php提供类很强大的反射机制,里面包括了很多对象,关于怎么使用,可以看这篇文章

反射和依赖注入

动手实现了一个简单的容器类,关于代码的解释可以看这篇文章

这里面有很多概念的东西,感觉写不出来,必须要多多敲它几百行代码方可破。

利用github-Webhooks自动部署代码

现在市面上大部分的仓库工具都提供webhooks功能,当触发某些特定的事件时,仓库会向指定地址发送一个http请求,利用此功能可以自动拉取代码完成部署。

当你在github上新建一个仓库,在setting里可以看到webhooks设置,我们设置好需要发送请求的地址以及触发事件,默认是push事件,不清楚的话,设置里有文档,如果不需要很复杂的功能,简单看一下就行了。

当然,请求会有个验签的动作(可选),建议选上,为了安全考虑。代码很简单,文档里有示例,php代码可以这么写:

$headers = getallheaders();
$payloadBody = file_get_contents("php://input");
$signature = $headers["X-Hub-Signature"];
$secretToken = getenv("SECRET_TOKEN");
$verifyHashHex = "sha1=" . hash_hmac("sha1", $payloadBody, $secretToken);
hash_equals($signature, $verifyHashHex);

验证成功后,执行:

popen("cd /path/to/rep && git pull --rebase", "r");

整个流程就结束了,在webhooks设置里能看到每次仓库发的请求。

下面写写我在完成整个过程中遇到的问题,总结一下,如果有朋友看到了这篇文章,能帮到你就再好不过。

  1. 调试代码,可以先写一个demo,利用exec函数,将命令执行结果输出,可以帮助我们排查问题
    echo getenv("HOME");exec("whoami", $o);
    print_r( $o );
    exec("git pull --rebase 2>&1", $o1);
    print_r( $o1 );

    如代码所示,先输出执行用户,再将错误信息也输出到标准输出。

  2. 用户主目录修改,如果你用的apache,并且没有该过配置文件的User指令,那么上面whoami命令输出的结果就是daemon用户,而该用户的主目录是/sbin,后面我们ssh公钥设置不方便,所以建议修改/etc/passwd,将主目录修改。可以在/home下新建daemon目录。
  3. 上面说到ssh设置,如果我们的remote是ssh方式访问,那么daemon主目录下新建下公钥吧,ssh-keygen,新建.ssh目录,另外一点要将该目录的权限设置成其他人不可访问,也就是说其他人不要给权限,否则后面拉取代码会出问题。
  4. .ssh下手动添加know_hosts文件,因为第一次拉取会有交互,必须你输入yes才会生成know_hosts文件,代码会卡住。
  5. HOME变量,如1,输出HOME变量,如果是/root,那么
    putenv("HOME=/home/daemon/");
  6. “error: unable to unlink old index.html(???)”,出现该错误还是权限问题,daemo用户对代码目录没有wx权限,也是就是不能删除文件。赋予即可。
  7. “insufficient permission for adding an object to repository database .git/objects”, objects目录及里面的文件可能没有权限写入。
    chmod -R g+w .git/objects

     

我在整个过程中遇到了很多问题,不过总算是完成了,记录一下。

使用Tideways监控php代码

最近研究了php性能调控相关工具,推荐一个tideways,还有一个xhgui界面化分析工具,帮助我们监控代码的运行。

关于怎么搭建,这里有一篇文章讲的很详细。

一些文章的补充:

1.  如果tideways安装失败,可以用以下方法安装:

wget https://github.com/tideways/php-xhprof-extension/archive/v4.1.5.tar.gz -O php-xhprof-extension-4.1.5.tar.gz
tar xzf php-xhprof-extension-4.1.5.tar.gz 
cd php-xhprof-extension-4.1.5 
phpize  
./configure --with-php-config=/php-config-path
make && make install 

2. 文章中给的是nginx 的配置,关于apache,php-value的写法如下(包含在httpd-vhost.conf或者.htaccess中):

<IfModule php7_module>
    php-value auto_prepend_file "/xhgui/path/to/external/header.php"
</IfModule>

3. install.php里会安装composer, 如果你已经安装了composer,可以手动执行 composer install 代替,如果失败,可以尝试执行 composer update

搭建好之后运行的效果如下: