我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。

为什么 Node.js 的异步 IO 具有非常好的性能

Node.js 的卖点是「异步单线程」,虽然主流 Web 后端编程语言中,对异步编程有很好支持的语言并不少,但只有 Node.js 丧心病狂地将所有 IO 强制异步进行。Python 和 Ruby 也有这样的框架,但因为在实际使用中会不可避免地用到含有同步代码的库,因此没能成长起来,而在 Node.js 之前,JavaScript 的服务器端编程几乎是空白,所以 Node.js 才得以建立起了一个所有 IO 均为异步的代码库。

大部分 Web 应用的瓶颈都在 IO, 即读写磁盘,读写网络,读写数据库。使用怎样的策略等待这段时间,就成了改善性能的关键点。

  • PHP 的策略:多进程运行,直接原地等待 IO 完成。缺点:多个进程会消耗多份内存,进程间难以共享数据。
  • C/C++ 通常的策略:多线程运行,程序自己维护锁的状态。缺点:开发成本高,容易出错,不易调试。
  • Python(Tornado): 多个请求在单个进程中轮流执行,遇到 IO 时切换到另一个请求。缺点:对于单个请求而言,依然没有最高效地利用时间。

何谓「最高效地利用时间」?比如现在有两个不相关的数据库查询,在 PHP 中通常会先执行一个,执行完成后再执行第二个(总时间是 a + b). 显然这不是最高效的,应该同时执行两个查询,时间是 max(a, b).

Python 和其他支持多线程的语言的问题就在于,在语言层面,程序员很难告诉虚拟机,应当将两个操作同时执行,即使有办法,也相当麻烦,大多数人懒得去用(也不值得去用)。而因为 Node.js 丧心病狂地强制所有 IO 异步执行,Node.js 的程序员也可以说是轻车熟路,配合一些改善代码可读性库(promise, async), 可以很轻松地让不相干的操作并行执行。

上面讲了异步 IO 的实现,那么异步 IO 的优势究竟体现在哪里呢。实际上异步 IO 并不能神奇地减轻服务器的压力,该加服务器还是一样要加服务器,只不过异步 IO 会减少单个请求的时间,去掉单个请求中那些无意义的等待时间。所以单位时间内处理的请求没有变化,但每个请求的处理时间却减少了。从这个角度,服务器也节约了一些资源——即维持每个请求的连接消耗的内存。

Ubuntu 14.04 VPS 部署 PHP 环境及 WordPress

软件及版本选择

  • Ubuntu 14.04

    Ubuntu 是目前用户数量数一数二的发行版,背后有大土豪维护,可以说是轻量级用户的最佳选择。而 14.04 是目前最新的 LTS 版本,目前已经发布了半年了,基本是目前支持最好的版本。

  • Nginx

    Nginx 是一个轻量级的,配置灵活,擅长并发的 Web 服务器。

  • PHP-FPM

    PHP-FPM 是目前官方推荐的最佳的运行模式。

  • MariaDB

    MySQL 的替代品,毕竟目前 MySQL 的创始人已经不建议我们使用 MySQL 了。

基本配置

通常当你创建了一台 VPS, 你会得到一个 IP 和一个 root 密码,所以,先用 ssh 登上你的服务器:

ssh root@106.186.21.33
# 如果有警告输入 yes 来确认,然后输入你的 root 密码

配置一下公钥登录,省着每次登录都要输入密码,非常建议像我一样把公钥上传到一个公开的地址,这样只要一条命令就可以设置好:

mkdir ~/.ssh; curl 'https://raw.githubusercontent.com/jysperm/meta/master/Key/JyAir.pub' >> ~/.ssh/authorized_keys; chmod -R 700 ~/.ssh;

然后更新一下软件包列表,升级现有软件包:

apt-get update
apt-get upgrade

修改一下主机名,最好改成一个确实可以访问到这台服务器的域名:

vi /etc/hostname
vi /etc/hosts

安装软件包

apt-get install nginx postfix php5-fpm mariadb-server memcached
apt-get install php-pear php5-mysql php5-curl php5-gd php5-mcrypt php5-memcache
apt-get install python make screen git wget zip unzip iftop vim curl htop iptraf nethogs
  • nginx: Web 服务器
  • postfix: SMTP 服务器,用来支持从本地发送邮件
  • php5-fpm: PHP 进程管理器,及 PHP 解释器
  • mariadb-server: 类 MySQL 数据库
  • memcached: 基于内存的缓存,很多程序会用到
  • php-pear: PHP 的包管理器
  • php5-mysql: PHP MySQL 数据库驱动
  • php5-curl: 一个 HTTP 协议库
  • php5-gd: 一个图像处理库
  • php5-mcrypt: 一个加密算法库
  • php5-memcache: Memcached 驱动
  • python: 一个常用的脚本语言解释器
  • make: 一个常用的构建工具
  • screen: 一个常用的 Shell 会话管理工具
  • git: 一个常用的版本控制工具
  • wget, curl: 常用的文件下载工具
  • zip, unzip: ZIP 压缩和解压工具
  • iftop, iptraf, nethogs: 常用的流量监控工具
  • vim: 一个常用的编辑器
  • htop: 一个常用的进程监控工具

安装 WordPress

新建一个普通用户,并切换到该用户

adduser wordpress
su wordpress
cd ~

下载 WordPress, 请自行到官网查看最新版本的下载地址:

wget http://cn.wordpress.org/wordpress-3.9-zh_CN.zip

解压文件:

unzip wordpress-*.zip

设置文件权限:

chmod -R 750 wordpress

删除安装包:

rm wordpress-*.zip

回到 root:

exit

配置 PHP-FPM

为 WordPress 创建一个进程池:

vi /etc/php5/fpm/pool.d/wordpress.conf

这是一份很典型的配置文件,通过监听 Unix Socket 来提供服务,动态调节进程数,最高 10 个进程,最低 3 个进程:

[wordpress]

user = wordpress
group = wordpress

listen = /home/wordpress/phpfpm.sock

listen.owner = wordpress
listen.group = wordpress
listen.mode = 0660

pm = dynamic
pm.max_children = 10
pm.min_spare_servers = 3
pm.max_spare_servers = 5

slowlog = /home/wordpress/phpfpm.slowlog
request_slowlog_timeout = 5s
request_terminate_timeout = 15s

php_admin_value[error_log] = /home/wordpress/phpfpm_error.log
php_admin_flag[log_errors] = On

配置 Nginx

删掉 Nginx 的默认站点:

rm /etc/nginx/sites-enabled/default

新建一个站点:

vi /etc/nginx/sites-enabled/wordpress

这份配置文件已将请求重写到 index.php, 可以直接在 WordPress 中使用「固定链接」功能:

server {
    listen 80;
    server_name jysperm.me;

    root /home/wordpress/wordpress;
    index index.html index.php;
    autoindex off;

    location / {
        try_files $uri $uri/ /index.php;
    }

    location ~ \.php$ {
        fastcgi_pass unix:///home/wordpress/phpfpm.sock;
        include fastcgi_params;
        fastcgi_index index.php;
    }
}

如果你希望把其他所有域名都跳转到你的站点,可以添加这么一段:

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;
    rewrite ^/(.*)$ http://jysperm.me permanent;
}

然后我们需要修正 Nginx 和 PHP-FPM 配合的一个 Bug:

vi /etc/nginx/fastcgi_params

fastcgi_param SCRIPT_FILENAME 开头的行改为:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

为 Nginx 添加读取 WordPress 文件的权限:

usermod -G wordpress -a www-data

配置 MySQL

进入 MySQL 控制台:

mysql -p
# 需要输入你的 MySQL root 密码

# 创建数据库
CREATE DATABASE `wordpress`;

# 为 WordPress 新建用户
CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'password';

# 授予权限
GRANT ALL PRIVILEGES ON  `wordpress` . * TO  'wordpress'@'localhost';

# 退出
QUIT

重启

好了,已经配置完成了,我们直接重启服务器即可,这样所有服务都会重启并使用新的配置:

reboot

为什么我无法访问一个网站

这是从我开始经营 RP 主机 以来,被问及的最多的一个问题。

本文将以诊断「为什么我无法访问一个网站」的方式,讲述 Web 服务的大致工作流程。

目录

  • URL
  • DNS
  • TCP/IP 连接
  • 加载资源
  • 渲染

URL

通常来讲,所谓访问一个网站,我们输入给浏览器的是类似于这样的一个地址:

http://jysperm.me/note/1519

用术语来讲,这是一个 URI, 一个 URI 有两个部分,一是协议,二是定位符。
比如「mailto:jysperm@gmail.com」这是一个邮箱的标识符,其中「mailto」是协议,「jysperm@gmail.com」是定位符。

再比如「urn:isbn:9787115281487」这是一本书(HTTP 权威指南)的标识符。

用来表示网页的 URI 有一个比较特殊的名字叫 URL, 它大致分成这几个部分:

  • http - 协议
  • jysperm.me - 主机名
  • /note/1519 - 路径

除此之外还可能会有类似于「?id=1519」的查询字符串。

这样的一个 URL, 便可以定位到一个网页。

DNS

首先我们要找到为我们提供服务的服务器,即 URL 中的主机名,在这里是 jysperm.me.

TCP/IP 网络中通过 IP 地址来唯一地定位一台主机,更确切地说,每一块网卡,会有一个唯一的 IP 地址。

IP 地址形如 117.121.25.186, 它本身是存在一定结构性的,比如 117.121.25.186 和 117.121.25.187,就有非常大的可能性两者的物理距离很近。

这样的结构性方便于计算机之间互相联络,但不便于人类记忆,于是我们用由字母组成的域名来表示主机。

我们通过一个叫 DNS 的系统,来将域名翻译成 IP 地址,DNS 系统本身说简单也简单,说复杂也复杂,在这里我们只讲述如何使用,而不研究其原理。

如果浏览器给你的提示是类似于「无法找到 jysperm.me」或者「无法连接到 jysperm.me」,那么有很大可能性是 DNS 查找失败或者出错。

我们可以使用 nslookup 这个命令来手动进行 DNS 查询

> nslookup jysperm.me
Server: 8.8.8.8
Address:    8.8.8.8#53

Non-authoritative answer:
Name:   jysperm.me
Address: 117.121.25.186

可以看到,我们向 8.8.8.8(Google 的免费公众 DNS), 查询 jysperm.me 这个域名,它返回给我们的结果是 117.121.25.186.

这里是 8.8.8.8 是系统的默认 DNS 服务器,我们还可以自己来指定 DNS 查询服务器:

> nslookup jysperm.me 114.114.114.114
Server: 114.114.114.114
Address:    114.114.114.114#53

Non-authoritative answer:
Name:   jysperm.me
Address: 117.121.25.186

这里使用了国内的 114.114.114.114 进行查询,返回的结果是一样的。

在某些网络封锁比较严重的国家,DNS 查询的结果可能会被篡改,因为 DNS 本身几乎没有任何安全措施,所以查询到的结果可能并不是可靠的。

TCP/IP 连接

知道了服务器的 IP 地址,下一步是建立 TCP 连接。这一步最常遇到的问题有:

  • 无法连接到服务器/连接超时
  • 连接被拒绝
  • 连接被重置

首先 IP 层面,要向一台主机发送数据,这个主机必须是「可达的」,换句话说就是对方必须在线。

最简单的,我们可以用 ping 命令验证一个主机是否在线:

> ping 117.121.25.186
PING 117.121.25.186 (117.121.25.186): 56 data bytes
64 bytes from 117.121.25.186: icmp_seq=0 ttl=128 time=138.651 ms
64 bytes from 117.121.25.186: icmp_seq=1 ttl=128 time=71.345 ms
64 bytes from 117.121.25.186: icmp_seq=2 ttl=128 time=286.960 ms

ping 使用的是 ICMP ECHO 指令,就好比你跟对方打招呼,问它是否在线,如果对方回应了,就说明它在线。但是,并非所有服务器都会回应 ICMP ECHO.

如果发现 ping 命令没有回应,类似于下面的情况:

> ping 59.24.3.173
PING 59.24.3.173 (59.24.3.173): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2

那么就说明你和服务器直接没有建立连通的连接,但是,连接是在那里断开的呢?这时我们可以用 traceroute 来探测数据包所走过的路径:

> traceroute www.qq.com
traceroute to www.qq.com (180.96.86.192), 64 hops max, 52 byte packets
 1  * * *
 2  100.64.0.1 (100.64.0.1)  10.224 ms  5.379 ms  6.794 ms
 3  218.4.12.229 (218.4.12.229)  8.342 ms  6.817 ms  8.169 ms
 4  218.94.165.2 (218.94.165.2)  8.128 ms  8.259 ms  7.173 ms
 5  61.155.133.121 (61.155.133.121)  11.152 ms  9.288 ms  9.990 ms
 6  202.102.69.254 (202.102.69.254)  11.464 ms  12.463 ms  11.027 ms
 7  180.96.51.94 (180.96.51.94)  13.674 ms  13.192 ms  12.364 ms
 8  180.96.48.202 (180.96.48.202)  13.564 ms  11.213 ms  13.945 ms
 9  * * *

traceroute 依赖于 ICMP, 并非所有服务器都开启了 ICMP, 因此会有一些路径点被显示为星号。

我们再来看另一个不可达的例子:

> traceroute twitter.com
traceroute to twitter.com (59.24.3.173), 64 hops max, 52 byte packets
 1  * * *
 2  100.64.0.1 (100.64.0.1)  4.694 ms  4.502 ms  8.044 ms
 3  218.4.12.237 (218.4.12.237)  8.874 ms  5.177 ms  8.133 ms
 4  218.4.13.49 (218.4.13.49)  15.205 ms  11.400 ms  12.011 ms
 5  202.97.55.41 (202.97.55.41)  14.011 ms  15.794 ms  16.518 ms
 6  202.97.50.254 (202.97.50.254)  12.207 ms  12.614 ms  13.165 ms
 7  202.97.35.78 (202.97.35.78)  12.246 ms
    202.97.35.22 (202.97.35.22)  13.337 ms
    202.97.34.126 (202.97.34.126)  13.701 ms
 8  202.97.60.33 (202.97.60.33)  58.292 ms  12.432 ms  18.093 ms
12  * * *

可以看到,最后一个可以追踪到的路径点是 202.97.60.33, 通过一些 IP 库可以查到,这个地址属于「广东省广州市 互联网交换中心」,可以打个电话问问他们那边出了什么情况。

加载资源

访问一个网页会发起不止一个请求,除了网页本身还会加载一些例如样式,图片,脚本之类的资源,甚至很多时候还会加载来自其他网站的资源,通过 Chrome 的开发人员工具中的「Network」选项卡,可以看到加载一个页面所发起的所有请求。

这是一个正常的网站, 所有的资源都加载成功了:

正常网站的加载情况

再看下面这张图,其中一个资源没有加载完成,导致整个页面都无法完成,这时我们就可以通过上面的方法单独排查这一个请求。

没有加载完全的情况

通过浏览器来检查一个资源究竟能不能下载在一些复杂情况下并不可靠,因为浏览器会为你的请求加上大量的参数,而且在得到资源后,还会进行渲染。

因此为了「纯粹」地下载一个资源,我们可以用 curl:

> curl http://pomotodo.com -I
HTTP/1.1 302 Moved Temporarily
Server: nginx
Date: Fri, 01 Aug 2014 03:51:35 GMT
Content-Type: text/html
Content-Length: 154
Connection: keep-alive
Location: https://pomotodo.com/

可以看到 http://pomotodo.com 并非像我们看到的那样显示了一个页面,而是发起了一个到 SSL 版本地址的重定向,SSL 可以保证传输的私密性,目前国内只有很小一部分有节操的网站在使用。

渲染

等到所有资源都加载完了,但是页面还是没有内容,或者乱七八糟怎么办?

在正确地得到所有资源后,浏览器会执行网站提供的脚本,来对资源进行加工,以便像网站制作者期望的那样来展现页面。

在这个过程中,脚本需要使用浏览器提供的 API 来进行渲染,而不同的浏览器所支持的 API 是有差异的,因此一些制作不是很精良的网站会在不同的浏览器中呈现不同的效果,甚至直接出错。

在此建议大家选择一个现代而强大的浏览器,比如 Chrome, 这样可以保证浏览器支持网页所需要的功能。

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2025-02-10.