多线程下的fork及进程的写时复制导致的性能问题

名词解释

  1. PHP vs HHVM: PHP指的是php.net(Zend)实现的PHP,而HHVM指的是Facebook开源的PHP实现
  2. PHP-FPM: (PHP Fastcgi Process Manager) 一个PHP Sapi实现,目前的主流的Web应用使用的方式。基于多进程的模型
  3. HHVM: AdminServer 这是HHVM中为了更好的运维和定位问题实现的一个HTTP操作接口,可以实时的获取和操作HHVM内部状态, 这对于我们是一个非常便利的接口,比如可以打印出内部的队列长度(fpm中也有类似接口,不过灵活性差很多)

多线程下fork()/exec()出现的性能问题

贴吧目前使用的HHVM来运行PHP程序,HHVM采用的是多线程模型, 以前我们使用的是PHP-FPM,PHP-FPM采用的是多进程的模型。 我们通过一个我们上线遇到的问题来看看Linux的写时复制和多线程相关的问题。

上周我们迁移一个服务HHVM运行环境时发现上线后CPU占用飚的非常高,经过分析发现程序时间消耗的最多时间的地方是:fork(), 这个有点奇怪,fork的时候怎么会花那么长时间呢?经过分析发现我们有个基础库的实现中使用了PHP的exec()函数来启动shell程序做字符处理,exec的实现和bash类似,先fork()一个进程然后通过exec系统调用执行相应的命令, 这没有什么不对,对不对。但是为什么在HHVM下会导致CPU利用率过高,而PHP中没事呢? 先看一些基本的背景:

关于写时复制

在Linux中要启动一个新进程的方式通常是:先调用fork()函数fork出一个新的进程,然后在 新的进程中调用exec()函数来启动新的程序从而达到启动新程序的目的,比如采用下面的代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int start_prog(char *prog, char* args[])
{
  pid_t pid = fork(); // 创建子进程
  if (pid < 0) return -1;
  if (pid == 0) { // 子进程
    if (execv(prog, args) < 0) return -1; // 加载新的程序
  } else {
    return 1;
  }
}

int main()
{
  char* args[] = {NULL};
  return start_prog("/bin/ls", args);
}

我们知道进程间的内存地址空间是隔离的,fork()系统调用的结果是生成一个新的子进程,为了保证隔离性, 早期的UNIX采用在fork()将父进程的地址空间完整的复制一份。这个操作非常的耗时。 为了提高效率现代的Unix及Linux采用了一种称为写时复制的技术,其实也就是一种延迟操作的做法, 子进程和父进程在fork()时并不马上复制,而是暂时共享内存空间,随后只要父进程或者子进程试图写共享的内存就会产生一个异常, 这时内核才把内存空间进程复制,比如我们在Shell中启动一个程序时随后就会启动新的程序,启动后的程序将会覆盖旧的内存空间, 如果提前就复制了,那么这个复制操作其实是白做了,为此系统将这个操作优化为写时复制。

写时复制,发生写时才复制内存地址空间

如果马上进行exec加载新的程序,那么复制地址就没必要了

解决方案

从前面的介绍我们知道,启动新程序的时候利用了写实复制的技术避免不必要的消耗。对于HHVM来说, 我们使用的是多线程,每个线下都在并行的执行,也就是说,在某个线程在执行fork()的是时候 还会有其他线程在处理任务,由于线程间是共享进程空间的,那时不可避免的可能会写内存。这就触发了 操作系统的写实复制,导致大量的内存复制操作(其实也是没必要的),这会导致资源占用急剧上涨。

说到这里你可能会说:你看,多线程模式不太好吧。HHVM的实现是不是有问题?其实对于PHP也会有类似的问题的, 如果你使用的是PHP的多线程模型(现在应该很少的人使用)。

这个问题,HHVM使用了一个比较巧妙的方式来解决。

HHVM的思路是这样的,既然多线程下写实复制容易出俩捣乱,那么就让fork发生在非多线程的进程中,让他不可能发生空间复制。

  1. HHVM启动时先预先启动N个(可配置)代理进程,在父进程和这些代理进程之前预先开启管道方便通信
  2. 有需要启动子进程的时候,通过管道选择一个没有正在fork()进程的代理进程,将执行信息通过管道发给代理进程
  3. 代理进程根据要执行的程序fork()一个新的子进程并执行相应的命令,然后将执行完成的信息通过管道写回主进程。

从上面可以知道,因为代理进程每个进程都只有一个线程不会存在多线程写的问题。 HHVM中将它称为轻进程

解决方案

使用了HHVM的轻进程后,CPU直接就降了下来,我们虽然可是使用这个方案解决这个问题,不过我们还是将 exec()调用改成了PHP原生文件读取操作,一来exec()函数的成本相对较高,二来使用shell不利于可移植性, 虽然我们不太可能使用Windows,不过这样的耦合是没必要的。

总结

  1. 多线程vs多进程。其实这两个模式没有绝对的好坏,就要需要什么,多进程的好处是进程隔离,程序出现问题 也能保证服务不整体crash掉,但是多进程带来的问题是进程间通信的成本,多线程也有多好处,比如HHVM中的 AdminServer,队列的管理等等,如果不是多线程,JIT,实例管理,Debug都会非常的复杂。
  2. 对于高并发的程序建议少用不用exec()/system()等函数,除了内存占用,进程数也可能会变成资源瓶颈, 其实和其他思路类似,尽量在用户态把事情做了。
  3. 多线程下fork()还有其他的问题,我们的场景是fork() 然后马上执行新的程序,如果你是真的要fork, 这可能会遇到更多的问题,比如锁等等,请参考http://rachelbythebay.com/w/2014/08/16/forkenv/

参考资料

  1. 《Understanding Linux Kernel 3rd》
  2. HHVM AdminServer: https://github.com/facebook/hhvm/blob/master/hphp/doc/command.admin_server
  3. PHP-FPM status page: https://www.centos.bz/2013/09/enable-php-fpm-status-page/
  4. HHVM: http://hhvm.com/

好的API设计

最近在重构公司的一个交互中间件,在重新设计API及总体架构的时候思考了许多, 不禁萌发了一个疑问,什么样的API才算是一个设计良好的API呢?

参考了许多的资料,做一下总结。主要来自这个keynote

什么是API

我们只要是在进行编程我们就需要不停的设计API,

API简单来讲可以是一个调用的函数,一个接口。抽象来说:接口是一个内聚系统暴漏给外部的一切信息。 包含但不限于:

  • 调用方式:比如通过lib库或者http接口等
  • 调用约定:比如lib的函数签名或者HTTP的参数,http method或者头信息,长短链接等等。
  • 依赖关系:比如接口的调用需要涉及到第三方或者其他的准备工作等等。

设计良好API的特点

这里探讨的API均为系统边界的API设计,而对于内部API来说不在探讨范围之内。

变动困难

API就像一个人一样,我们和一个API打交道的时候需要了解这个人的特性偏好等, 有的人很好相处,而有的人让人很头疼,尤其是你不得不和他打交道的时候。

和人一样,如果你不得不和他打交道,要改变他的秉性是很痛苦的,人的“本性难移”, API也一样,一旦发布了,要改变的成本就很大很大。

好的API应该具有:

  1. 易于学习
  2. 即使没有文档也易于使用
  3. 不易误用:这一点很重要,要减少使用者的心智负担
  4. 使用API的代码易于维护。这个有点不一样,不是API本身易于维护,而是API的友好度。 比如接口功能单一,使用简单,使用者的代码也会相应的更易于维护
  5. 易于满足需求:API的完备性和正交性。能够容易的满足需求,完备性保证功能完整, 正交性保证接口的简洁性,不需要为所有的需求提供接口,而是由用户去组合。
  6. 易于扩展

怎么样设计良好的API

基本原则:

  1. 专一:一个API的功能应该是单一的,需要能够很容易的解释和理解,也就会更好用。
  2. 尽可能的小:小有很多的优势,易于理解和维护。
  3. 尽量少的外部依赖:减少使用者的成本
  4. 设计不被实现影响:不要暴漏实现细节给用户
  5. 竟可能少的暴露,不止是内部细节,对于不必要的接口尽量不要发布,比如使用不多的功能, 可以暂时不暴露接口。
  6. 良好的命名:尽量做到自描述。
  7. 完善的文档
  8. 考虑性能

其他具体的方法还是参考后面参考资料的内容吧。

参考资料

  • http://mattgemmell.com/api-design/
  • http://lcsd05.cs.tamu.edu/slides/keynote.pdf 虽然是Java相关,但是道理是相通的
  • http://ishare.iask.sina.com.cn/f/61717785.html 《API Design for C++》
  • http://qt-project.org/wiki/API-Design-Principles
  • http://www4.in.tum.de/~blanchet/api-design.pdf (前面的链接里提到的)
Comments

2013年终总结及2014规划

2013 结束了,迎来了新的一年。年复一年,我们在前行,虽然匆忙,也是时候停下来想一想了,回顾一下过去的一年都有哪些收获,规划一下来年。

2013年回顾:

找了一下去年的总结,发现被自己删掉了,因为不完整。原来计划2013年完成TiPHP的实现。这个计划没有做完。学习@胖胖,不找借口,是没有足够的时间。

这一年:

  1. 生活
    1. 情感上终于跟随自己的内心做自己了,年末的三个月是我所经历过最完美的时光。
    2. 运动上,坚持了半年多每周的羽毛球,还有跑步,身体没有以前好,不过后面3个多月运动的少了。
  2. 工作
    1. 代码量不足,相对以前代码的编写量不足,14年会努力提高代码量和代码质量。
    2. 收获
      • 私有云平台的设计实现,对在线业务的运维和支持体系有了新的理解。除了基础本身基础设施的重要性非常大。是每个公司都要考虑的问题。
      • LAMP技术的投入。主要是在性能优化上,做了一些业务的优化,对优化技术上也学到了不少。
      • 6月份参加 PHPConf 分享了贴吧的LAMP架构,第一次参加技术会议,学习到不少:
        • 首先得感谢胖胖 @phppan 的引荐,一直没有想过要主动参加什么 会议。
        • 准备PPT的过程 很感谢定坤的支持,立强和洪波专业细致的指导,让我的讲稿如此的出色,同事也要感谢 所有贴吧同事给的意见。
      • 职业上有了提升,仍然是需要感谢定坤,立强和洪波,你们给了我极大的帮助,很感谢定坤的认可,感谢立强的帮助,以及专业靠普通的洪波。
  3. 自我提升
    • 业余项目
      • php-leveldb, 写完后维护的少了。主要是自己的需求其实不多。所以放着了。
      • php开源:对PHP本身的贡献开始变慢了。是观察力不够,没有特别好的想法。
      • hhvm:提交了4个小的补丁
      • TIPI:精力赶不上,更新很慢,有很多热心读者发信来问,表示歉意。
    • 读书,主要有:
      • 《Critical Thinking》
      • 《Think fast and slow》
      • 《编译原理》
        • 学习了cousera 上的编译原理课程,收获很大,对语言的设计实现上又了较全面的理解。
      • 《编译器设计》
      • 《伊豆的舞女》

总结来说: 2013年是比较充实的一年,情感上有很大的收获,工作上很感谢有leader的认可,感谢有很好的技术氛围, 有非常有经验的同事,让我能够不停的成长。

2014年规划和愿望:

2014年是我一个新的开始,主要专注在技术的深度和个人影响力的提升上。 同时要提升生活品质,要学会感恩,要学会经营,全力的投入,无论和感情还是工作上。

不用担心没有方向,前进了,方向自然会出现;努力了,自然会有收获。不瞻前顾后。

  1. 生活
    • Be together with my girl.
    • 完成memory的设计和制作
    • 去厦门
    • 坚持每周至少运动6小时,提高身体素质
    • 认识更多的朋友,学习他们优良的品质和能力
  2. 技术
    • 学习数据库的实现细节和优化
    • 投入精力学习HHVM,参与开发,每个月至少提交一个补丁
    • 研读lua的实现并完成学习笔记
    • 完成TiPHP项目
  3. 工作
    • 完成访问中间件的重构
    • 在性能优化及LAMP技术上有产出
    • 在架构及影响力上有进一步的提升
  4. 自我提升
    • 课程学习:至少完整的学习3门cousera上的课程。
    • 读书:
      • 技术类:至少4本。精读。
        • 《哥德尔,艾舍尔,巴赫》买来好久没有读,开始读完以后发现非常有意思。
      • 非技术类:至少4本。
  5. 新年愿望:
    • 母亲身体健康,快乐。
    • 我的宝贝能感受到她渴望的快乐和充实,心想事成。
    • 大侄子夏飞今天的高考能有个好成绩,我的第一个侄子,很聪明,希望他能沉着应对,我相信他能做最好的自己。
    • 二侄子夏伟能突破自己,性格中内向的部分能更阳光,希望他能感受到世界的美好, 我要尽自己的力量让他快乐的度过有些艰难的过程。
    • 三侄子夏俊能尽快进入状态
    • 小宝宝们健康

以前还会有一些物质上的愿望,比如相机,MacBook之类的,刚才绞尽脑汁都没有想到自己想要 什么样具体的礼物。还是以前那样:我只在乎物质带来的精神收益,不靠虚浮的物质来满足内心的需要。

Comments

有关无开销异常

在编写程序中我们无时不刻的在处理异常,异常处理

Comments

Nginx中location的匹配和rewrite

最近在线上进行nginx规则的调整的时候遇到一个问题,发现在location匹配时候可能会踩到的一个坑。

location在匹配规则的时候匹配的是归一化之后的URL,比如多个斜杠或者URL中带”.”, “..”的都会被 归一化。

而在内部rewrite的时候新的URL地址是不会再次被归一化的。这种不一致如果没有留意可能会遇到问题。

比如:

````
if ($request_uri ~ "/api") {
    rewrite (.*) /newapi/$1;    # 斜杠多余了
}

location /newapi/api {
    set $testapi 1;
}

location /newapi {
    # ...   
}
````

对于上面的配置中,rewrite的时候不小心多写了个斜杠,对于这个配置, 如果用地址:/api访问的话 /newapi/api 这个location是不能被匹配的。 而用地址/newapi//api直接访问是可以匹配到/newapi/api这个location的。

本质上是因为用户直接访问的URL会先归一化处理,而rewrite之后是不会处理的。

具体见文档: http://nginx.org/en/docs/http/ngx_http_core_module.html

Comments

[译文]可伸缩Go调度器设计文档

原文: Scalable Go Scheduler Design Doc https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit?pli=1

TODO

Comments

Go学习笔记:空标示符-

开始学习下Go语言,为了强化记忆开始记一些学习笔记。

Go语言是一门很简单的语言,它为我们做了很多的决定,比如很多在其他语言中 不推荐使用的编码风格在Go是不允许的。比如: 变量或者包声明或导入后没有使用是无法编译通过的。 它的编译只有Fatal没有Warning,这对于代码质量是很有好处的。在其他语言中 最佳实践也是编写warning-free的代码。Go把这个最佳实践放到了语言级别。

Go中的空标示符(blank indentifier): _

刚开始在看到Go中的空标示符是以为它只是一种约定,因为下划线看起来比那么的 显眼,而普通变量又不太可能只使用一个下划线来命名,在其他语言中我们不想使用 一个变量的话很简单忽略之就可以了,而如果对一个函数的返回值不感兴趣的话, 不对返回值赋值即可。

先记录下结论:空标示符不是一个普通变量或标示符,而是一个特殊的标示符, 对于这种类型的标示符绑定表达式时不进行真正的绑定。

这是什么意思呢?也就说比如将一个值赋值给空操作符是不会进行值绑定的。

blank indentifier - test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
  _ "io"  // 如果不重名名包为_ 而在代码中没有使用这个包会编译不通过
          // 这样导入一个包是有副作用的,导入一个包后会执行包的init()方法,
          // 如果只是为了避免编译不通过而绑定到_是不推荐的做法。
    "fmt"
)

func getMulti() (int, int) {
  return 3, 4
}

func main() {
  _ = 20       // 绑定没有作用,不会报错
  // _ := 10  // 编译不通过,因为表达式左边没有一个有效的新的标示符
              // no new variables on left side of :=

  x, _ := getMulti()

  fmt.Printf("%d\n", x)
  // fmt.Printf("%d\n", _)   这样是编译不通过的,因为_并不能被赋值
  //                         编译 "cannot use _ as value"
}

总结

空标示符不是普通标示符,是一个语言级别的标示符,通常用来:

  1. 显式的忽略函数或其他多值赋值表达式中的某些的返回值,多值表达式通常有:

    • 函数的多个返回值
    • range循环中的key-value值
    • 多值赋值,比如: x, y, z := 10, 20, 30, 不过这种情况比较小
  2. 或者导入包不使用包而只利用包的初始化函数的副作用。但是不推荐用这种方式 来绕过因为包未被使用而编译不通过的问题

这里只是做一个笔记,其实Go的语言规范中写的还是很详细的。对于一些有疑惑的地方 一翻手册就会发现答案。这也是一门语言小的好处,歧义会非常少。

参考资料

  1. Golang 官方 http://golang.org
  2. Golang 语言规范 http://golang.org/ref/specGo的语言规范非常小,所有语言语法规范 都在这里可以找到。
  3. 《The Way to Go》这本书是目前比较全面的一本,在看完Go的手册之后推荐阅读
Comments

怎么样追查PHP的Bug、Segment Fault(core Dump):方法,工具等

Comments

LevelDB扩展发布V0.1版本

很开心,第一个提交的PHP扩展已经在PECL官方发布了,这是一个Google LevelDB的PHP 封装,主要用于对LevelDB的访问,目前已经实现了LevelDB最具价值的一些特性:迭代器,快照等。

LevelDB数据的设计是只能单进程访问的(多线程没有问题),所以通常这个扩展不合适作为普通的Web应用数据存储, 可以作为离线的数据存储用,或者只是方便读取现有leveldb的数据。

如果有需要可以前去 http://pecl.php.net/package/leveldb 下载。基本的使用说明在http://reeze.cn/php-leveldb/ 详细的API文档由勤劳高效的胖胖http://www.phppan.com/编写,不过还没有发布。

同时,还有了@php.net的马甲一枚: reeze(at)php.net。MARK一下。

Comments

怎么样获取PHP函数默认参数的常量名

好久没有更新了,发篇占位文:如果某个函数的默认参数是个常量,那么怎么样获取这个参数的常量名称?见代码:

1
2
3
4
5
<?php

function new_blog($title = DEFAULT_TITLE) {
  // blahblah
}

在上面的代码中,怎么样获取函数new_blog函数的参数$title所对应的默认值常量名: DEFAULT_TITLE。这个问题和以前我曾写过的一篇 关于如何获取变量名称的博文相似。

这个问题,在PHP5.4.6之前基本上没有解决方法了,因为函数定义是编译时的信息,在PHP运行时是获取不到的。 当然这里说的无法实现是指的使用官方PHP版本时没法搞定。

在PHP中类似的需求,一般都可以使用PHP的反射扩展。

PHP的反射(Reflection)

反射是PHP5中提供的用于获取或操作PHP内部信息的标准扩展,可能写应用代码的用户使用的较少一些, 编写框架或者平台性的系统会使用到。

比如你的框架需要实现一种插件机制,而你可能需要利用反射来获取类或者函数的元信息。 这里就不对Reflection的使用做过多的介绍了,详细信息见官方文档: http://cn.php.net/manual/en/book.reflection.php

新的函数ReflectionParameter::getDefaultValueConstantName()

不过在PHP5.4.6之前,Reflection是没有实现该功能的。这个需求其实来自PHPUnit的作者Sebastian Bergmann。 因为这个需求在Reflection模块来说是一个缺失,不属于大功能的升级,所以直接进入了目前的最新分支PHP-5.4。 同时这个功能在PHP-5.4.6中可用了。https://github.com/php/php-src/blob/PHP-5.4.6/NEWS#L41

实现代码见:https://github.com/php/php-src/commit/13a9555342a4156a6150818234639b49a596ccd6, 这个方法目前没有使用说明,不过看名字应该也能明白。不过可以参考测试用例

这个提交给ReflectionParameter类增加了两个函数:

  1. ReflectionParameter::isDefaultValueConstant() 用于判断函数的这个参数是否是常量默认参数
  2. ReflectionParameter::getDefaultValueConstantName() 用于获取这个常量默认参数的参数名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
define("CONST_TEST_1", "const1");

function ReflectionParameterTest($test1=array(), $test2 = CONST_TEST_1) {
  echo $test;
}
$reflect = new ReflectionFunction('ReflectionParameterTest');
foreach($reflect->getParameters() as $param) {
  if($param->getName() == 'test1') {
      var_dump($param->isDefaultValueConstant());
  }
  if($param->getName() == 'test2') {
      var_dump($param->isDefaultValueConstant());
  }
  if($param->isDefaultValueAvailable() && $param->isDefaultValueConstant()) {
      var_dump($param->getDefaultValueConstantName());
  }
}

class Foo2 {
  const bar = 'Foo2::bar';
}

class Foo {
  const bar = 'Foo::bar';

  public function baz($param1 = self::bar, $param2=Foo2::bar, $param3=CONST_TEST_1) {
  }
}

$method = new ReflectionMethod('Foo', 'baz');
$params = $method->getParameters();

foreach ($params as $param) {
    if ($param->isDefaultValueConstant()) {
        var_dump($param->getDefaultValueConstantName());
    }
}
?>
// 运行结果
bool(false)
bool(true)
string(12) "CONST_TEST_1"
string(9) "self::bar"
string(9) "Foo2::bar"
string(12) "CONST_TEST_1"