PHP浮点数

<?php
    $f = 0.58;
    var_dump(intval($f * 100)); //为啥输出57

为啥输出是57啊? PHP的bug么?

我相信有很多的同学有过这样的疑问, 因为光问我类似问题的人就很多, 更不用说bugs.php.net上经常有人问…

要搞明白这个原因, 首先我们要知道浮点数的表示:

浮点数, 以64位的长度为例, 会采用1位符号位(E), 11指数位(Q), 52位尾数(M)表示(一共64位).

符号位:最高位表示数据的正负,0表示正数,1表示负数。

指数位:表示数据以2为底的幂,指数采用偏移码表示

尾数:表示数据小数点后的有效数字.

这里的关键点就在于, 小数在二进制的表示, 关于小数如何用二进制表示, 大家可以百度一下, 我这里就不再赘述, 我们关键的要了解, 0.58 对于二进制表示来说, 是无限长的值(下面的数字省掉了隐含的1)..

0.58的二进制表示基本上(52位)是: 0010100011110101110000101000111101011100001010001111
0.57的二进制表示基本上(52位)是: 0010001111010111000010100011110101110000101000111101

而两者的二进制, 如果只是通过这52位计算的话,分别是:

0.58 -> 0.57999999999999996
0.57 -> 0.56999999999999995

0.58 * 100 = 57.999999999

那你intval一下, 自然就是57了….

可见, 这个问题的关键点就是: “你看似有穷的小数, 在计算机的二进制表示里却是无穷的”

so, 不要再以为这是PHP的bug了, 这就是这样的…..

PHP中使用strlen和mb_strlen, iconv_strlen的区别

在PHP中,我们常常需要处理字符串长度。比如说,有些情况下需要把过长的字符串截短一点,然后存入数据库或者文件。

今天的程序中,出现了一个bug,很简单的问题,postgresql说无法存储字符串,说是无效的UTF-8字符。很好奇,数据库到程序包括页面都是UTF-8,为什么会出现这个问题。仔细读了程序,原来有一个地方是使用的strlen获取字符串长度,而字符串是中文。根据这个strlen获取了长度后,又继续通过substr进行了字符串截取。这样肯定会有问题,因为里面的字符都是中文的,至少应该处理一下.

那么,可以这么讲,在php的中文编程中,基本上可以先不使用strlen这个方法了,直接使用mb_strlen不可以了。因为mb_strlen能够处理多语言字符,当然你要安装php-mbstring.

在使用mb_strlen的时候,如果不确定运行时是否为utf-8,可以用 mb_internal_encoding(“UTF-8″);来处理。不过用mb_strlen第二个参加添加utf-8也可以。

对于iconv_strlen,我用得不多,不过我觉得这个方法和mb_strlen是一样的,只要设置的字符集没问题。

反正,别用strlen了,除非是判断纯拉丁字符。

解决php内存泄露问题

网站每天大概有60万ip/300万pv的访问,网站产品很复杂,代码结构差,开发工程师来来去去,代码只能只读了。突然有一天开始频繁出现php-fpm进程耗光内存和cpu占有率飙升,前端频繁出现504错误

php-fpm进程耗光内存 这个就是传说中的内存泄露,所谓内存泄露,是指进程在运行过程中,内存占用率逐步上升而不释放,导致系统可用内存越来越少的情况

严格上说,这个也不算致命错误,“内存泄露”只对长期运行的程序有威胁,对单一任务的执行脚本不需要担心

最简单的处理方式,是定时重启进程。php-fpm的配置信息里面有个max_request,就定义一个fastcgi进程处理完多少个请求之后退出这样系统可用释放掉内存,但是如果内存占用率增长速度非常快,频繁重启进程,就会影响服务的稳定性,所以这个问题必须正面解决

内存泄露排查非常困难

  • 因为代码规模非常大,想靠做code review的方式来查基本上不可能
  • php并非运行在虚拟机上,没有什么官方的monitor(类似java hprof,JVM Monitor等)
  • 在互联网上搜索,找不到任何答案

探索解决问题

  1. 使用 valgrind 调试php-cgi进程

    Valgrind 是一个linux常用的程序的内存调试和代码剖析,对调试C/C++程序的内存泄露很有帮助,它的机制是在系统alloc/free等函数调用上加计数。用 valgrind 调试php-cgi,要求php-cgi 是debug版本,实践证明行不通:

    1. php-cgi debug 版本放在线上根本跑不起来,性能太差
    2. php程序的内存泄露,是由于一些循环引用,或者gc的逻辑错误,valgrind无法探测,它适合去检查php解释器是否有内存泄露问题
  2. php解释器(Zend core)自带有检查内存泄露的机制

    php解释器的核心代码叫做(Zend Core) 在用valgrind 调试php-cgi进程,我查看了php-cgi的代码,发现zend core 实现了内存泄露的自我检查 但是 同上原因,php-cgi debug 跑不起来,也无法得到调试信息

  3. FreeBSD 的 DTrace

    DTrace是freebsd 系统支持的核心调试器,可以在各个系统函数调用上加计数点,twitter曾经用过。这个方法最后没有使用 有如下原因:

    1. 需要找一台服务器安装freebsd,并部署到线上、或者模拟负载,非常繁琐

    2. 我仔细研究了DTrace的文档,发现这个可以认为是增强的 valgrind,也不能解决我们的问题

这3种方法都不行,陷入困境.但是换个角度思考:虽然解决php程序内存泄露没有方便的工具,但是 web 程序是按请求切分的,一个http请求,对应的php进程执行一个php文件

一个自然的想法是,记录 每次 http请求处理前后php进程的内存占用率之差,然后对结果排序,就能找出,让进程内存增加可能性最大的 文件 ,这个文件导致内存泄露的可能性最大

计算进程内存占用率有两种方式

php内置函数 memory_get_usage

  1. 这个函数是 Zend Core里面一个计数器,是zend core认为的内存使用量,但是php内存泄露有可能是zend core逻辑错误导致的,所以memory_get_usage不一定可靠

  2. linux 系统文件 /proc/{$pid}/status 会记录某个进程的运行状态,里面的 VmRSS 字段记录了该进程使用的常驻物理内存(Residence),这个就是该进程实际占用的物理内存了,用这个数据比较靠谱,在程序里面提取这个值也很容易

找到思路,就可以开始动手写程序

直接修改了php-cgi的源代码,在main.c里面处理每个fastcgi请求前后分别加计数代码,输出日志到log文件,重新编译上线

运行30分钟之后,执行

cat short.log| awk '{print $3 "\t" $7 "\t" $6 "\t" $4$5}' |sort -r -n |head -n 100

很容易找到最可能出现内存泄露的代码文件,然后进一步排查,重构代码,这就很简单了:能不加载的文件就不加载,大数组用完之后赶紧unset ….

更好的办法

后来,我才发现其实不需要去修改php的源代码,php.ini配置文件里面有两个配置项: auto_append_file,auto_prepend_file,可以在请求前后注入代码 ….

web程序做性能优化也是这个思路,但是要简单很多,无需写代码,在nginx log里面加上$request_time ,用awk/sort 处理一下就可以找出瓶颈。

PHP编码规范:日志接口

本文档用来描述日志类库的通用接口。

主要目标是让类库获得一个Psr\Log\LoggerInterface对象并且使用一个简单通用的方式来写日志。有自定义需求的框架和CMS可以根据情况扩展这个接口,但应当保持和该文档的兼容性,这将确保使用第三方库和应用能统一的写应用日志。

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

关键词实现者在这个文档被解释为:在日志相关的库和框架实现LoggerInterface接口的人。用这些实现的人都被称作用户

1. 规范

1.1 基础

  • LoggerInterface暴露八个接口用来记录八个等级(debug, info, notice, warning, error, critical, alert, emergency)的日志。

  • 第九个方法是log,接受日志等级作为第一个参数。用一个日志等级常量来调用这个方法的结果必须和调用具体等级方法的一致。如果具体的实现不知道传入的不按规范的等级来调用这个方法必须抛出一个Psr\Log\InvalidArgumentException。用户不应自定义一个当前不支持的未知等级。

1.2 消息

  • 每个方法都接受字符串,或者有__toString方法的对象作为消息。实现者可以对传入的对象有特殊的处理。如果不是,实现者必须将它转换成字符串。

  • 消息可以包含可以被上下文数组的数值替换的占位符。

    占位符名字必须和上下文数组键名对应。

    占位符名字必须使用使用一对花括号为分隔。在占位符和分隔符之间不能有任何空格。

    占位符名字应该A-Za-z0-9,下划线_和句号.。其它的字符作为以后占位符规范的保留。

    实现者可以使用占位符来实现不同的转义和翻译日志成文。用户在不知道上下文数据是什么的时候不应提前转义占位符。

下面提供一个占位符替换的例子,仅作为参考:

function interpolate($message, array $context = array())
{
    // build a replacement array with braces around the context keys
    $replace = array();
    foreach ($context as $key => $val) {
        $replace['{' . $key . '}'] = $val;
    }
    // interpolate replacement values into the message and return
    return strtr($message, $replace);
}
// a message with brace-delimited placeholder names
$message = "User {username} created";

// a context array of placeholder names => replacement values
$context = array('username' => 'bolivar');

// echoes "Username bolivar created"
echo interpolate($message, $context);

1.3 上下文

  • 每个方法接受一个数组作为上下文数据,用来存储不适合在字符串中填充的信息。数组可以包括任何东西。实现者必须确保他们对上下文数据足够的掌控。在上下文中一个给定值不可抛出一个异常,也不可产生任何PHP错误,警告或者提醒。

  • 如果在上下文中传入了一个异常对象,它必须以exception作为键名。记录异常轨迹是通用的模式,如果日志底层支持这样也是可以被允许的。实现者在使用它之前必须验证exception的键值是不是一个异常对象,因为它可以允许是任何东西。

1.4 助手类和接口

  • Psr\Log\AbstractLogger类让你非常简单的实现和扩展LoggerInterface接口以实现通用的log方法。其他八个方法将会把消息和上下文转发给它。

  • 类似的,使用Psr\Log\LoggerTrait只需要你实现通用的log方法。记住traits不能实现接口前,你依然需要implement LoggerInterface

  • Psr\Log\NullLogger是和接口一个提供的。它可以为使用接口的用户提供一个后备的“黑洞”。如果上下文数据非常重要,这不失为一个记录日志更好的办法。

  • Psr\Log\LoggerAwareInterface只有一个setLogger(LoggerInterface $logger)方法可以用来随意设置一个日志记录器。

  • Psr\Log\LoggerAwareTraittrait可以更简单的实现等价于接口。通过它可以访问到$this->logger

  • Psr\Log\LogLevel类拥有八个等级的常量。

2. 包

作为psr/log 的一部分,提供接口和相关异常类的一些描述以及一些测试单元用来验证你的实现。

3. Psr\Log\LoggerInterface

<?php

namespace Psr\Log;

/**
* Describes a logger instance
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data, the only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
    /**
    * System is unusable.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function emergency($message, array $context = array());

    /**
    * Action must be taken immediately.
    *
    * Example: Entire website down, database unavailable, etc. This should
    * trigger the SMS alerts and wake you up.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function alert($message, array $context = array());

    /**
    * Critical conditions.
    *
    * Example: Application component unavailable, unexpected exception.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function critical($message, array $context = array());

    /**
    * Runtime errors that do not require immediate action but should typically
    * be logged and monitored.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function error($message, array $context = array());

    /**
    * Exceptional occurrences that are not errors.
    *
    * Example: Use of deprecated APIs, poor use of an API, undesirable things
    * that are not necessarily wrong.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function warning($message, array $context = array());

    /**
    * Normal but significant events.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function notice($message, array $context = array());

    /**
    * Interesting events.
    *
    * Example: User logs in, SQL logs.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function info($message, array $context = array());

    /**
    * Detailed debug information.
    *
    * @param string $message
    * @param array $context
    * @return null
    */
    public function debug($message, array $context = array());

    /**
    * Logs with an arbitrary level.
    *
    * @param mixed $level
    * @param string $message
    * @param array $context
    * @return null
    */
    public function log($level, $message, array $context = array());
}

4. Psr\Log\LoggerAwareInterface

<?php

namespace Psr\Log;

/**
* Describes a logger-aware instance
*/
interface LoggerAwareInterface
{
    /**
    * Sets a logger instance on the object
    *
    * @param LoggerInterface $logger
    * @return null
    */
    public function setLogger(LoggerInterface $logger);
}

5. Psr\Log\LogLevel

<?php

namespace Psr\Log;

/**
* Describes log levels
*/
class LogLevel
{
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}

PHP编码规范:基本代码规范

本节标准包含了成为标准代码所需要的基本元素,以确保高级技术特性可以在PHP代码中共享。

RFC 2119中的特性关键词”必须”(MUST),“不可”(MUST NOT),“必要”(REQUIRED),“将会”(SHALL),“不会”(SHALL NOT),“应当”(SHOULD),“不应”(SHOULD NOT),“推荐”(RECOMMENDED),“可以”(MAY)和“可选”(OPTIONAL)在这文档中将被用来描述。

1. 大纲

  • 文件必须使用 <?php<?= 标签。

  • 文件必须使用不带BOM的UTF-8代码文件。

  • 文件应当声明符号(类,函数,常量等…)或者引起副作用(例如:生成输出,修改.ini配置等),但不能同时存在。

  • 命名空间和类名必须遵守 PSR-0

  • 类名必须使用骆驼式StudlyCaps写法 (译者注:驼峰式的一种变种,后文将直接用StudlyCaps表示)。

  • 类名常量必须使用全大写和下划线分隔符。

  • 方法名必须使用驼峰式cameCase写法(译者注:后文将直接用camelCase表示)。

2. 文件

2.1. PHP标签

PHP代码必须使用长标签<?php ?>或者短输出式<?= ?>标签;它不可使用其他的标签变种。

2.2. 字符编码

PHP代码必须只使用不带BOM的UTF-8。

2.3. 副作用

一个文件应当声明新符号 (类名,函数名,常量等)并且不产生副作用,或者应当执行有边缘影响的逻辑,但不能同时使用。

短语”副作用”意思是不直接执行逻辑的类,函数,常量等 仅包括文件

“副作用”包含但不局限于:生成输出,明确使用requireinclude,连接外部服务,修改ini配置,触发错误和异常,修改全局或者静态变量,读取或修改文件等等

下面是一个例子文件同时包含声明和副作用 即避免的例子:

<?php   
// side effect: change ini settings
ini_set('error_reporting', E_ALL);

// side effect: loads a file
include "file.php";

// side effect: generates output
echo "<html>\n";

// declaration
function foo()
{
    // function body
}

下面这个例子仅仅包含声明并且没有副作用; 即需要提倡的例子:

<?php
// declaration
function foo()
{
    // function body
}

// conditional declaration is *not* a side effect
if (! function_exists('bar')) {
    function bar()
    {
        // function body
    }
}

3. 命名空间和类名

命名空间和类名必须遵守 PSR-0: http://www.zzphp.net/?p=278.

这意味着每个类只能是一个文件本身,并且至少有一个层级的命名空间:顶级的组织名称。

类名必须使用骆驼式StudlyCaps写法

代码必须使用PHP5.3及以后编写正式的命名空间 例子:

<?php
// PHP 5.3 and later:
namespace Vendor\Model;

class Foo
{
}

代码使用5.2.x及之前编写应当使用Vendor_作为前缀的伪命名空间作为类

<?php
// PHP 5.2.x and earlier:
class Vendor_Model_Foo
{
}

4. 类常量,属性和方法

术语“类”指所有的类,接口和特性(traits)

4.1. 常量

类常量必须使用全大写,分隔符使用下划线作为声明。 例子:

<?php
namespace Vendor\Model;

class Foo
{
    const VERSION = '1.0';
    const DATE_APPROVED = '2012-06-01';
}

4.2. 属性

本手册有意避免推荐使用$StulyCaps$cameCase或者unser_score作为属性名字

不管名称约定是不是在一个应当接受的合理范围。这个范围可能是组织,包,类,方法。

4.3. 方法

方法名必须用cameCase()写法

PHP编码规范:自动加载器特性强制性要求

下面描述了关于自动加载器特性强制性要求:

强制性

  • 一个完全标准的命名空间必须要有一下的格式结构\<Vendor Name>\(<Namespace>\)*<Class Name>
  • 命名空间必须有一个顶级的组织名称 (“Vendor Name”).
  • 命名空间中可以根据情况决定使用多少个子空间
  • 命名空间中的分隔符当从文件系统加载的时候将被映射为 DIRECTORY_SEPARATOR
  • 命名空间中的类名中的_没有特殊含义,也将被作为DIRECTORY_SEPARATOR对待.
  • 命名空间中的类名在从文件系统加载时文件名都需要以.php结尾
  • 组织名,空间名,类名都可以随意选择使用大小写英文字符

示例

  • \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
  • \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php
  • \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php
  • \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php

命名空间和类名中的下划线

  • \namespace\package\Class_Name => /path/to/project/lib/vendor/namespace/package/Class/Name.php
  • \namespace\package_name\Class_Name => /path/to/project/lib/vendor/namespace/package_name/Class/Name.php

以上是我们为实现无痛的自动加载特性设定的最低标准。你可以按照此标准实现一个SplClassLoader在PHP 5.3中去加载类。

实例

下面是一个函数实例简单展示如何使用上面建议的标准进行自动加载

<?php

function autoload($className)
{
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strrpos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    require $fileName;
}

SplClassLoader实现

下面的gist是一个SplClassLoader实例可以按照上面建议的自动加载特性来加载类。这也是我们当前推荐在PHP5.3中按照上述标准加载类的方式