使用场景
监控汇总
目前正在用的一个场景,针对某一台机器上的错误进行汇总并报警,咱们把一分钟以内的相同报警合并成一条,用共享内存来暂存,很是实用且高效。
PHP SESSION
若是你是单机的服务,且又启用了session,那么能够把session换成共享内存的来存储,会比文件要快上很多,这里还要强调是单机,这是最大的软肋,但就功能上来说没有memcache方便。
什么是共享内存
共享内存是一种在同一台机器的不一样进程(应用程序)之间交换数据的方式。一个进程可建立一个可供其余进程访问的内存段,并赋予它相应的权限。每一个内存段拥有一个唯一的ID,咱们一般称之为shmid,这个ID指向一个物理内存区域,其余进程可经过此ID来操做这块内存, 包扩读取、写入以及删除。
共享内存的使用是一种在进程之间交换数据的快速方法,主要由于在建立内存段以后传递数据,不会涉及内核。这种方法经常称为进程间通讯 (IPC)。其余 IPC 方法包括管道、消息队列、RPC 和套接字。
PHP 中几种常见的共享内存使用方式
APC 能够缓存 PHP 的 opcode 提升应用的性能,能够在同个 PHP-FPM 进程池的进程间共享数据,经常使用功能以下:
- apc_store
- apc_fetch
- apc_add
- apc_delete
- apcinc apcdec
- apc_cas
- apcclearcache
- apcsmainfo
Shmop Unix 系统共享内存使用接口经常使用功能:
- shmop_open
- shmop_close
- shmop_read
- shmop_write
- shmop_delete
ipcs -m 查看本机共享内存的状态和统计。
ipcrm -m shmid 或 ipcrm -M shmkey 清除共享内存中的数据。
SystemV Shm经常使用功能:
- ftok
- shm_attach
- shm_detach
- shmputvar
- shmgetvar
- shmremovevar
使用共享内存须要考虑操做的原子性和锁、并行和互斥。
sem 信号量相关函数:
* semget * semremove * semacquire * semrelease
PHP 提供的 IPC 机制。
- --enable-shmop 共享内存,只能按字节操做
- --enable-sysvsem 信号量
- --enable-sysvshm 共享内存,和 shmop 的差异是提供的操做函数不一样,支持 key、value操做
- --enable-sysvmsg 消息队列
本文主讲
如何使用 PHP shmop 建立和操做共享内存段,使用它们存储可供其余应用程序使用的数据。
1. 建立内存段
共享内存函数相似于文件操做函数,但无需处理一个流,您将处理一个共享内存访问 ID。第一个示例就是 shmopopen 函数,它容许您打开一个现有的内存段或建立一个新内存段。此函数很是相似于经典的 fopen 函数,后者打开用于文件操做的流,返回一个资源供其余但愿读取或写入该打开的流的函数使用。让咱们看看 shmopopen的用法:
<?php $key = ftok(__FILE__, 'h'); $mode = 'c'; $permissions = 0644; $size = 1024; $shmid = shmop_open($key, $mode, $permissions, $size); ?>
第一个参数($key):
系统创建IPC通信 (消息队列、信号量和共享内存) 时必须指定一个key值。一般状况下,该key值经过ftok函数获得, * *key是一个咱们逻辑上表示共享内存段的标识。不一样进程只要选择同一个Key值就能够共享同一段存储段。
第二个参数($mode):
访问模式,它相似于fopen的访问模式,有如下几种
- 模式 “a”,它容许您访问只读内存段
- 模式 “w”,它容许您访问可读写的内存段
- 模式 “c”,它建立一个新内存段,或者若是该内存段已存在,尝试打开它进行读写 *模式 “n”,它建立一个新内存段,若是该内存段已存在,则会失败,返回 false,并伴随有warning: unable to attach or create shared memory segment
第三个参数($permissions):
内存段的权限。您必须在这里提供一个八进制值,它相似于UNIX操做系统文件和目录的操做权限。
第四个参数($size):
内存段大小,以字节为单位。在写入一个内存段以前,您必须在它之上分配适当的字节数。
返回结果:
此函数返回一个 ID 编号,其余函数可以使用该 ID 编号操做该共享内存段。这个 ID 是共享内存访问 ID,与系统 ID 不一样,它以参数的形式传递。请注意不要混淆这二者。若是失败,shmop_open 将返回 FALSE。
shmop_open成功后,使用ipcs -m, 能够查看到刚刚建立的内存段,注意 申请的内存段有严格的权限,好比用root用户申请的,普通用户就无权访问
2. 向内存段写入数据
使用 shmop_write 函数向共享内存块写入数据。此函数的使用很简单,它仅接受 3 个参数,以下所示。
<?php //这里shmid能够延用上一段代码返回的shmid $shmid = shmop_open(ftok(__FILE__,'h'), 'c', 0644, 1024); shmop_write($shmid, "Hello World!", 0); ?>
这个函数相似于 fwrite 函数, 在这里有三个参数。 * 第一个参数(shmid):是shmopopen返回的ID,它识别您操做的共享内存块。∗第二个参数(shmid):是shmopopen返回的ID,它识别您操做的共享内存块。∗第二个参数(data):是您但愿存储的数据。 * 第三个参数($offset):是您但愿开始写入的位置。默认状况下,咱们始终使用 0 来表示开始写入的位置。
返回结果:此函数在失败时会返回 FALSE,在成功时会返回写入的字节数。
3. 从内存段读取数据
从共享内存段读取数据很简单。您只须要一个打开的内存段和 shmop_read 函数,它接受三个参数,以下所示:
<?php $shmid = shmop_open(ftok(\__FILE_\_,'h'), 'c', 0644, 1024); shmop_write($shmid, "Hello World\!", 0); var_dump(shmop_read($shmid, 0, 11)); ?>
- 第一个参数($shmid):是 shmop_open 返回的 ID,它识别您操做的共享内存块。
- 第二个参数($start):是您但愿从内存段读取的位置,这个参数能够始终为0, 表示数据的开头
- 第三个参数(count):是您希望读取的字节数。一般情况下我们用shmopsize(count):是您但愿读取的字节数。通常状况下咱们用shmopsize(shmid),以便完整的读取它。
4. 删除内存段
shmop_delete 该函数只接收一个参数,以下所示:
<?php $shmid = shmop_open(ftok(\__FILE_\_,'h'), 'c', 0644, 1024); shmop_delete($shmid); ?>
其实这个函数不会实际删除该内存段。它将该内存段标记为删除状态,由于共享内存段在有其余进程正在使用它时没法被删除。shmop_delete 函数将该内存段标记为删除,阻止任何其余进程打开它。要删除它,咱们须要关闭该内存段。
5. 关闭内存段
打开一个共享内存段会 “附加” 到它。附加该内存段以后,咱们可在其中进行读取和写入,但完成操做后,咱们必须从它解除。
<?php $shmid = shmop_open(ftok(\__FILE_\_,'h'), 'c', 0644, 1024); shmop_write($shmid, "Hello World\!", 0); shmop_delete($shmid); shmop_close($shmid); ?>
共享内存的原子操做 - 信号控制
针对共享内存的写操做自己不是原子性的,那么当咱们大量并发进行读写的时候,怎么保证原子性呢,这里要引入信号量进行控制。
PHP 也提供了内置扩展 sysvsem ,其实咱们在看sysvsem 提供的一系列sem*的方法的时候,就会想到,这和上面提到的shmop*有什么区别呢,咱们来看官房文档中的这一个解释:PHP already had a shared memory extension (sysvshm) written by Christian Cartus cartus@atrior.de, unfortunately this extension was designed with PHP only in mind and offers high level features which are extremely bothersome for basic SHM we had in mind.
也就是说:sysvshm 扩展提供的方法在存储以前对用户的数据进行serialize处理,这里就致使这个存储的数据是没法与其它语言共享的,这一系列方法是php only的方法。
引入信号控制以后的示例:
<?php $key = ftok(_FILE_, 'h') $mode = "c"; $permissions = 0755; $size = 1024; // 内存段的大小,单位是字节 $semid = sem_get($key); # 请求信号控制权 if (sem_acquire($semid)) { $shmid = shmop_open($key, 'c', 0644, 1024); # 读取并写入数据 shmop_write($shmid, '13800138000', 0); # 关闭内存块 shmop_close($shmid); # 释放信号 sem_release($semid); }
共享内存的操做是很是快的,在本地想要模拟实现写入冲突是很是困难的,可是本地想模拟实现写入冲突其实是很是难的(考虑到计算机的执行速度)。在本地测试中,使用 for 循环操做时若是不使用shmop_close 关闭资源会出现没法打开共享内存的错误警告。这应该是由于正在共享内存被上一次操做占用中尚未释放致使。
共享内存,memcache,文件的读写速度对比。
如下是同时读写1k的数据读写100000次的时间对比:
读(s) | 写(s) | |
memcache | 7.8 | 8.11 |
file | 2.6 | 3.2 |
shm | 0.1 | 0.07 |
<?php
//$key = ftok(_FILE_, 'h'); //$key = ftok('/tmp/', 'h'); $key = 0xff4; $permissions = 0755; $size = 1024; // 内存段的大小,单位是字节 $semid = sem_get($key); # 请求信号控制权 if (sem_acquire($semid)) { $content = file_get_contents(__FILE__); $shmid = shmop_open($key, 'c', 0644, strlen($content)); # 读取并写入数据 shmop_write($shmid, $content, 0); # 关闭内存块 shmop_close($shmid); # 释放信号 sem_release($semid); }
<?php /** * @param $fn */ function profile(callable $fn) { $t = microtime(1); $i = 0; while ($i < 999) { $fn(); ++$i; } var_dump('takes ', microtime(1) - $t); } $fnIO = function (){ file_get_contents(__DIR__."/chmop_create.php"); }; $fn = function (){ //$key = ftok('chmop', 'h'); //$key = ftok('/tmp/', 'h'); $key = 0xff4; $permissions = 0755; $size = 1024; // 内存段的大小,单位是字节 $semid = sem_get($key); # 请求信号控制权 if (sem_acquire($semid)) { $shmid = shmop_open($key, 'a', 0644, 1024); # 读取并写入数据 $out = shmop_read($shmid, 0, shmop_size($shmid)); # 关闭内存块 // var_dump($out); // echo $out; shmop_close($shmid); # 释放信号 sem_release($semid); } }; profile($fn); profile($fnIO);
这是Mac Pro 2014上的结果:
这是ali ECS的结果 :
可见,现代磁盘优化比我们老观念中的性能要好些了~
但利用内存依然是个不错的玩法~