周记 8

  hexo太不好用了 各种 出错 就在这里写自己的周记吧!

# PHP的一些问题

## PHP-FPM 运行模式

php-fpm的进程数可以根据设置分为动态和静态。

* 静态:直接开启指定数量的php-fpm进程,不再增加或者减少;


* 动态:开始的时候开启一定数量的php-fpm进程,当请求量变大的时候,动态的增加php-fpm进程数到上限,当空闲的时候自动释放空闲的进程数到一个下限。

这里先说一下涉及到这个的几个参数吧,他们分别是pm、pm.max_children、pm.start_servers、pm.min_spare_servers和pm.max_spare_servers。

pm表示使用那种方式,有两个值可以选择,就是static(静态)或者dynamic(动态)。在更老一些的版本中,dynamic被称作apache-like。这个要注意看配置文件给出的说明了。

下面4个参数的意思分别为:

* pm.max_children:静态方式下开启的php-fpm进程数量。
* pm.start_servers:动态方式下的起始php-fpm进程数量。
* pm.min_spare_servers:动态方式下的最小php-fpm进程数量。
* pm.max_spare_servers:动态方式下的最大php-fpm进程数量。

那么,对于我们的服务器,选择哪种执行方式比较好呢?事实上,跟Apache一样,我们运行的PHP程序在执行完成后,或多或少会有内存泄露的问题。这也是为什么开始的时候一个php-fpm进程只占用3M左右内存,运行一段时间后就会上升到20-30M的原因了。所以,动态方式因为会结束掉多余 的进程,可以回收释放一些内存,所以推荐在内存较少的服务器或者VPS上使用。具体最大数量根据 内存/20M 得到。比如说512M的VPS,建议pm.max_spare_servers设置为20。至于pm.min_spare_servers,则建议根据服务器的负载情况来设置,比较合适的值在5~10之间。

然后对于比较大内存的服务器来说,设置为静态的话会提高效率。因为频繁开关php-fpm进程也会有时滞,所以内存够大的情况下开静态效果会更好。数量也可以根据 内存/30M 得到。比如说2GB内存的服务器,可以设置为50;4GB内存可以设置为100等。

Nginx不支持对外部程序的直接调用或者解析,所有的外部程序(包括PHP)必须通过FastCGI接口来调用。FastCGI接口在Linux下是socket(这个socket可以是文件socket,也可以是ip socket)。为了调用CGI程序,还需要一个FastCGI的wrapper(wrapper可以理解为用于启动另一个程序的程序),这个wrapper绑定在某个固定socket上,如端口或者文件socket。当Nginx将CGI请求发送给这个socket的时候,通过FastCGI接口,wrapper接收到请求,然后派生出一个新的线程,这个线程调用解释器或者外部程序处理脚本并读取返回数据;接着,wrapper再将返回的数据通过FastCGI接口,沿着固定的socket传递给Nginx;最后,Nginx将返回的数据发送给客户端。


总结一下 fpm运行是多线程模型 分为静态和动态两种模式

静态更加适合内存良好的机器

动态更适合内存小的机器

静态的分配原理是 机器内存 / 30

动态的分配原则是 机器内存 / 20

动态能动态的开闭worker 可以避免一些 内存泄漏问题~

## OPcache

PHP(本文所述案例PHP版本均为7.1.3)作为一门动态脚本语言,其在zend虚拟机执行过程为:读入脚本程序字符串,经由词法分析器将其转换为单词符号,接着语法分析器从中发现语法结构后生成抽象语法树,再经静态编译器生成opcode,最后经解释器模拟机器指令来执行每一条opcode。

在上述整个环节中,生成的opcode可以应用编译优化技术如死代码删除、条件常量传播、函数内联等各种优化来精简opcode,达到提高代码的执行性能的目的。

PHP扩展opcache,针对生成的opcode基于共享内存支持了缓存优化。在此基础上又加入了opcode的静态编译优化。这里所述优化通常采用优化器(Optimizer)来管理,编译原理中,一般用优化遍(Opt pass)来描述每一个优化。

整体上说,优化遍分两种:

一种是分析pass,是提供数据流、控制流分析信息为转换pass提供辅助信息;

一种是转换pass,它会改变生成代码,包括增删指令、改变替换指令、调整指令顺序等,通常每一个pass前后可dump出生成代码的变化。

本文基于编译原理,结合opcache扩展提供的优化器,以PHP编译基本单位op_array、PHP执行最小单位opcode为出发点。介绍编译优化技术在Zend虚拟机中的应用,梳理各个优化遍是如何一步步优化opcode来提高代码执行性能的。最后结合PHP语言虚拟机执行给出几点展望。

OPcache 缓存的最小粒度 我认为是 单个PHP 文件

使用下列推荐设置来获得较好的性能:


opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.save_comments=0

你也可以禁用 opcache.save_comments 并且启用 opcache.enable_file_override。 需要提醒的是,在生产环境中使用上述配置之前,必须经过严格测试。 因为上述配置存在一个已知问题,它会引发一些框架和应用的异常, 尤其是在存在文档使用了备注注解的时候。


### 静态编译 解释执行 即时编译

静态编译(static compilation),也称事前编译(ahead-of-time compilation),简称AOT。即把源代码编译成目标代码,执行时在支持目标代码的平台上运行。

动态编译(dynamic compilation),相对于静态编译而言,指”在运行时进行编译”。通常情况下采用解释器(interpreter)编译执行,它是指一条一条的解释执行源语言。

JIT编译(just-in-time compilation),即即时编译,狭义指某段代码即将第一次被执行时进行编译,而后则不用编译直接执行,它为动态编译的一种特例。 PHP8 HHVM


### OP_ARRAY

类似于C语言的栈帧(stack frame)概念,即一个运行程序的基本单位(一帧),一般为一次函数调用的基本单位。此处,一个函数或方法、整个PHP脚本文件、传给eval表示PHP代码的字符串都会被编译成一个op_array。

实现上op_array为一个包含程序运行基本单位的所有信息的结构体,当然opcode数组为该结构最为重要的字段,不过除此之外还包含变量类型、注释信息、异常捕获信息、跳转信息等。

## OPCODE

解释器执行(ZendVM)过程即是执行一个基本单位op_array内的最小优化opcode,按顺序遍历执行,执行当前opcode,会预取下一条opcode,直到最后一个RETRUN这个特殊的opcode返回退出。

这里的opcode某种程度也类似于静态编译器里的中间表示(类似于LLVM IR),通常也采用三地址码的形式,即包含一个操作符,两个操作数及一个运算结果。其中两个操作数均包含类型信息。此处类型信息有五种,分别为:

编译变量(Compiled Variable,简称CV),编译时变量即为php脚本中定义的变量。

内部可重用变量(VAR),供ZendVM使用的临时变量,可与其它opcode共用。

内部不可重用变量(TMP_VAR),供ZendVM使用的临时变量,不可与其它opcode共用。

常量(CONST),只读常量,值不可被更改。

无用变量(UNUSED)。由于opcode采用三地址码,不是每一个opcode均有操作数字段,缺省时用该变量补齐字段。

类型信息与操作符一起,供执行器匹配选择特定已编译好的C函数库模板,模拟生成机器指令来执行。

参考资料:https://tech.youzan.com/understanding-opcode-optimization-in-php/

## PHP 脚本执行过程

PHP -> lex -> tokens -> parse -> simple 表达式 -> compile -> opcode -> zendvm -> exec

1)Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)。

2)Parsing, 将Tokens转换成简单而有意义的表达式。

3)Compilation, 将表达式编译成Opocdes。

4)Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

## PHP内存模型与执行模型

内存管理一般会包括以下内容:

是否有足够的内存供我们的程序使用;

如何从足够可用的内存中获取部分内存;

对于使用后的内存,是否可以将其销毁并将其重新分配给其它程序使用。

PHP底层对内存的管理, 围绕着小块内存列表(free_buckets)、 大块内存列表(large_free_buckets)和剩余内存列表(rest_buckets)三个列表来分层进行的。 ZendMM向系统进行的内存申请,并不是有需要时向系统即时申请,而是由ZendMM的最底层(heap层)先向系统申请一大块的内存,通过对上面三种列表的填充,建立一个类似于内存池的管理机制。 在程序运行需要使用内存的时候,ZendMM会在内存池中分配相应的内存供使用。这样做的好处是避免了PHP向系统频繁的内存申请操作

PHP对内存的分配,是结合PHP的用途来设计的,PHP一般用于web应用程序的数据支持,单个脚本的运行周期一般比较短(最多达到秒级),内存大块整块的申请,自主进行小块的分配, 没有进行比较复杂的不相临地址的空闲内存合并,而是集中再次向系统请求。 这样做的好处就是运行速度会更快,缺点是随着程序的运行时间的变长,内存的使用情况会“越来越多”(PHP5.2及更早版本)。 所以PHP5.3之前的版本并不适合做为守护进程长期运行。

https://www.cnblogs.com/phpworld/p/5916560.html

PHP从下到上是一个4层体系:

Zend引擎:Zend整体用纯C实现,是PHP的内核部分,它将PHP代码翻译(词法、语法解析等一系列编译过程)为可执行opcode的处理并实现相应的处理方法、实现了基本的数据结构(如hashtable、oo)、内存分配及管理、提供了相应的api方法供外部调用,是一切的核心,所有的外围功能均围绕Zend实现。

Extensions:围绕着Zend引擎,extensions通过组件式的方式提供各种基础服务,我们常见的各种内置函数(如array系列)、标准库等都是通过extension来实现,用户也可以根据需要实现自己的extension以达到功能扩展、性能优化等目的(如贴吧正在使用的PHP中间层、富文本解析就是extension的典型应用)。

Sapi:Sapi全称是Server Application Programming Interface,也就是服务端应用编程接口,Sapi通过一系列钩子函数,使得PHP可以和外围交互数据,这是PHP非常优雅和成功的一个设计,通过sapi成功的将PHP本身和上层应用解耦隔离,PHP可以不再考虑如何针对不同应用进行兼容,而应用本身也可以针对自己的特点实现不同的处理方式。

上层应用:这就是我们平时编写的PHP程序,通过不同的sapi方式得到各种各样的应用模式,如通过webserver实现web应用、在命令行下以脚本方式运行等等。

如果PHP是一辆车,那么车的框架就是PHP本身,Zend是车的引擎(发动机),Ext下面的各种组件就是车的轮子,Sapi可以看做是公路,车可以跑在不同类型的公路上,而一次PHP程序的执行就是汽车跑在公路上。因此,我们需要:性能优异的引擎+合适的车轮+正确的跑道。

PHP53的 gc 改进

http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html

是因为一个zval在一个时刻只能表示一种类型的变量。可以看到_zvalue_value中只有5个字段,但是PHP中算上NULL有8种数据类型,那么PHP内部是如何用5个字段表示8种类型呢?这算是PHP设计比较巧妙的一个地方,它通过复用字段达到了减少字段的目的。例如,在PHP内部布尔型、整型及资源(只要存储资源的标识符即可)都是通过lval字段存储的;dval用于存储浮点型;str存储字符串;ht存储数组(注意PHP中的数组其实是哈希表);而obj存储对象类型;如果所有字段全部置为0或NULL则表示PHP中的NULL,这样就达到了用5个字段存储8种类型的值。

在PHP只用于做动态页面脚本时,这种泄露也许不是很要紧,因为动态页面脚本的生命周期很短,PHP会保证当脚本执行完毕后,释放其所有资源。但是PHP发展到目前已经不仅仅用作动态页面脚本这么简单,如果将PHP用在生命周期较长的场景中,例如自动化测试脚本或deamon进程,那么经过多次循环后积累下来的内存泄露可能就会很严重。这并不是我在耸人听闻,我曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互。

PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

首先PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,这个数量默认是10,000,如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。

由上文我们可以知道,一个zval如果有引用,要么被全局符号表中的符号引用,要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根(root)。这里我们暂且不讨论PHP是如何发现这些可能根的,这是个很复杂的问题,总之PHP有办法发现这些可能根zval并将它们投入根缓冲区。

当根缓冲区满额时,PHP就会执行垃圾回收,此回收算法如下:

1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。

2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。

3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。

如果不能完全理解也没有关系,只需记住PHP5.3的垃圾回收算法有以下几点特性:

1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。

2、可以解决循环引用问题。

3、可以总将内存泄露保持在一个阈值以下。

## PHP cli 的交互模式

php -a

## PHP-FPM restart reload


https://www.cnblogs.com/GaZeon/p/5421906.html

启动php-fpm:
/usr/local/php/sbin/php-fpm
 
php 5.3.3 以后的php-fpm 不再支持 php-fpm 以前具有的 /usr/local/php/sbin/php-fpm (start|stop|reload)等命令,所以不要再看这种老掉牙的命令了,需要使用信号控制:
master进程可以理解以下信号
INT, TERM 立刻终止
QUIT 平滑终止
USR1 重新打开日志文件
USR2 平滑重载所有worker进程并重新载入配置和二进制模块


reload --重新加载,reload会重新加载配置文件,Nginx服务不会中断。而且reload时会测试conf语法等,如果出错会rollback用上一次正确配置文件保持正常运行。
restart --重启(先stop后start),会重启Nginx服务。这个重启会造成服务一瞬间的中断,如果配置文件出错会导致服务启动失败,那就是更长时间的服务中断了。

因此 reload 更加平滑 对线上业务影响更小

php-fpm master 进程可以理解一下以下的信号:

   INT, TERM 立刻终止
   QUIT 平滑终止 
   USR1 重新打开日志文件
   USR2 平滑重载所有worker进程并重新载入配置和二进制模块

关闭php-fpm:

kill -INT `cat /usr/local/php/var/run/php-fpm.pid`

平滑重启php-fpm:

kill -USR2 `cat /usr/local/php/var/run/php-fpm.pid`


kill usr2 会 保证使用原来的配置文件


## 常用PHP configure

```
/configure --prefix=/usr/local/Cellar/php54 --with-mysql=/usr/local/Cellar/mysql/5.7.22/ --with-mysqli=/usr/local/Cellar/mysql/5.7.22/bin/mysql_config --enable-bcmath --enable-mbstring --enable-sockets --enable-magic-quotes --enable-fpm --enable-fastcgi --enable-pcntl --with-mcrypt --with-mhash --with-gmp --with-openssl --enable-zip --enable-ftp --with-bz2 --with-pdo-mysql=/usr/local/Cellar/mysql/5.7.22/ --with-curl
```

# Others

## base58

比特币用的加密方式

相比Base64,Base58不使用数字"0",字母大写"O",字母大写"I",和字母小写"l",以及"+"和"/"符号。

设计Base58主要的目的是:

* 避免混淆。在某些字体下,数字0和字母大写O,以及字母大写I和字母小写l会非常相似。

* 不使用"+"和"/"的原因是非字母或数字的字符串作为帐号较难被接受。

* 没有标点符号,通常不会被从中间分行。

* 大部分的软件支持双击选择整个字符串。

但是这个base58的计算量比base64的计算量多了很多。因为58不是2的整数倍,需要不断用除法去计算。

感觉 base58 计算方式是有bug的

大多数都很低了

## Linux 进程状态

Linux 进程状态

```
 D    不可中断     Uninterruptible sleep (usually IO)
 
    R    正在运行,或在队列中的进程
   
    S    处于休眠状态
   
    T    停止或被追踪
   
    Z    僵尸进程
   
    W    进入内存交换(从内核2.6开始无效)
   
    X    死掉的进程


    <    高优先级
   
    N    低优先级
   
    L    有些页被锁进内存
   
    s    包含子进程
   
    +    位于后台的进程组;
 
    l    多线程,克隆线程  multi-threaded (using CLONE_THREAD, like NPTL pthreads do)


```

## 常见限流算法

令牌桶算法

1)存放固定令牌的桶,生产令牌的速率固定 

2)当令牌达到上限时候,产生的令牌被丢弃或拒绝 

3)n个请求过来,拿n个令牌,若令牌不足,则请求被决绝或等待

传统

如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内

如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter

漏桶算法

1)桶容量固定,固定速录流出 

2)桶是空的,不流出 

3)以任意速率流入桶,若超过桶容量,被丢弃


滑动窗口

滑动窗口,又称rolling window。为了解决这个问题,我们引入了滑动窗口算法。如果学过TCP网络协议的话,那么一定对滑动窗口这个名词不会陌生。下面这张图,很好地解释了滑动窗口算法:

## Redis Lua

脚本的安全性
当将 Lua 脚本复制到附属节点, 或者将 Lua 脚本写入 AOF 文件时, Redis 需要解决这样一个问题: 如果一段 Lua 脚本带有随机性质或副作用, 那么当这段脚本在附属节点运行时, 或者从 AOF 文件载入重新运行时, 它得到的结果可能和之前运行的结果完全不同。

三、使用Lua脚本的好处

1、减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数。

2、原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。

3、代码复用:客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本来完成相同的逻辑。

4、速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。

5、可以移植:只要是有ANSI C 编译器的平台都可以编译,你可以看到它可以在几乎所有的平台上运行:从 Windows 到Linux,同样Mac平台也没问题, 再到移动平台、游戏主机,甚至浏览器也可以完美使用 (翻译成JavaScript).

6、源码小巧:20000行C代码,可以编译进182K的可执行文件,加载快,运行快。

Redis Lua解释器加载以下Lua库:

```
      

1、base lib.
2、table lib.
3、string lib.
4、math lib.
5、struct lib.
6、cjson lib.
7、cmsgpack lib.
8、bitop lib.
9、redis.sha1hex function.
10、redis.breakpoint和redis.debug 函数在Redis Lua调试器的上下文中。
      
```

## Redis哨兵

redis 哨兵模式

http://redis.majunwei.com/topics/sentinel.html

Redis-Sentinel是官方推荐的高可用解决方案,当redis在做master-slave的高可用方案时,假如master宕机了,redis本身(以及其很多客户端)都没有实现自动进行主备切换,而redis-sentinel本身也是独立运行的进程,可以部署在其他与redis集群可通讯的机器中监控redis集群。

它的主要功能有一下几点

1、不时地监控redis是否按照预期良好地运行;

2、如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);

3、能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。

4、哨兵为客户端提供服务发现,客户端链接哨兵,哨兵提供当前master的地址然后提供服务,如果出现切换,也就是master挂了,哨兵会提供客户端一个新地址。

哨兵(sentinel)本身也是支持集群的

很显然,单个哨兵会存在自己挂掉而无法监控整个集群的问题,所以哨兵也是支持集群的,我们通常用三台哨兵机器来监控一组redis集群。

具体测试结构 1 m 2slave 3 哨兵

模拟多实例的话 可以使用 多port的形式进行实现


redis考虑的参数:
repl-diskless-sync 无硬盘复制功能 2.8 实验性 对应硬盘较慢的 slave 防止对redis 带来性能损失

repl-disable-tcp-nodelay

在slave和master同步后(发送psync/sync),后续的同步是否设置成TCP_NODELAY

假如设置成yes,则redis会合并小的TCP包从而节省带宽,但会增加同步延迟(40ms),造成master与slave数据不一致

假如设置成no,则redis master会立即发送同步数据,没有延迟
前者关注性能,后者关注一致性

append_only:
持久化 方式

redis支持两种持久化方式,一种是 Snapshot(RDB)<二进制文件> 也是默认方式,另一种是Append only file(AOF)的方式。

Client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。 Stop the World

应用方链接的时候 应该链接 哨兵端口?

但是如果哨兵端口失效如何?


部署哨兵之前需要了解的基本事情

1. 一个健壮的部署至少需要三个哨兵实例。

2. 三个哨兵实例应该放置在客户使用独立方式确认故障的计算机或虚拟机中。例如不同的物理机或不同可用区域的虚拟机。 (异地多活)

3. sentinel + Redis实例不保证在故障期间保留确认的写入,因为Redis使用异步复制。然而有方式部署哨兵使丢失数据限制在特定时刻,虽然有更安全的方式部署它。(可能在故障期间 写入会失效)


4. 你的客户端要支持哨兵,流行的客户端都支持哨兵,但不是全部。


5. 没有HA设置是安全的,如果你不经常的在开发环境测试,在生产环境他们会更好。你可能会有一个明显的错误配置只是当太晚的时候。


6. Sentinel,Docker,或者其他形式的网络地址交换或端口映射需要加倍小心:Docker执行端口重新映射,破坏Sentinel自动发现其他的哨兵进程和master的slave列表。稍后在这个文档里检查关于Sentinel和Docker的部分,了解更多信息。

                         
测试故障转移

这时候我们部署的sentinel已经准备好了测试。我们只需要kill掉master检查配置是否改变。所以只需要做如下的事情:

redis-cli -p 6379 DEBUG sleep 30

这个命令将使master休眠30秒不能访问。这主要是模仿master由于一些原因挂掉。

Slaves优先级

Redis实例有一个salve-priority的配置参数。这个信息暴露在Redis slave的 INFO 输出里,Sentinel使用它以便于在可用的salves中获取slave进行故障转移:

* 如果Slave优先级设置为0.slave永远不会晋升为master。


* 优先级数字越低越被Sentinel优先选择。


例如如果有一个slave S1在当前master的同一个数据中心,并且另一个salve S2在另一个数据中心,可以设置S1的优先级为10,S2的优先级为100,所以如果master故障并且S1和S2都可用,S1将会被优先选择。
了解关于slave选择的更多信息,请参考 本文档的 slave选择和优先级部分。

猜你喜欢

转载自www.cnblogs.com/xiaoerli520/p/9545433.html