博客性能优化:续

去年写过一篇文章《博客性能优化》。 网站搬迁之后,需要重新优化一下。下面的测试结果都来自 webpagetest

优化之前

对字体开启压缩
按照 FONT MIME TYPES IN NGINX 这篇文章里说的,在 Nginx 的 mime.types 文件中加入下面几行:

font/ttf                      ttf;
font/opentype                 otf;
application/font-woff         woff;
application/vnd.ms-fontobject eot;

nginx.conf 的 gzip 类型中添加

gzip_types [...] font/ttf font/opentype application/vnd.ms-fontobject image/svg+xml;

除了 woff 文件,其它的字体都可以压缩。woff 本身就是压缩过的。

让客户端缓存静态文件
之前静态文件的配置很简单:

location /static {
    alias /home/laike9m/static;
}

完全没有客户端缓存。现在加上了:

location /static {
    alias /home/laike9m/static;
    etag on; 
    expires max;
    add_header Pragma public;
    add_header Cache-Control "public";
    access_log off;
}

/media 作同样配置。
特别地,我希望 css 文件不要缓存那么久,因为我经常修改 css,希望第二天就能看到效果。

location ~* ^/static/(.+\.css)$ {
    alias /home/laike9m/static/$1;
    etag on; 
    expires 1d;
    add_header Pragma public;
    add_header Cache-Control "public";
    access_log off;
}

这个 alias 写不对很容易就 404 了。还有就是注意要把 css 的这个 location 放在 /static 之前,因为 Nginx 在正则匹配的时候是 sequential 的,即放在前面的会先匹配,而不是像前缀匹配取最长的那个。

效果

似乎加载时间反而变长了。。。不过缓存对初次加载本来就没啥用。

开启 HTTP/2
如果不是因为想尝试 HTTP/2,我也不会写这篇文章了。Nginx 从 1.9.5 版开始正式支持 HTTP/2

首先要重新编译 Nginx,在 configure 时加入 --with-http_v2_module
然后安装 OpenSSL 1.0.2。官方的说法是“This is required to support the ALPN extension to TLS that our HTTP/2 implementation uses.”。

然后就发现网站没法访问了_(:3」∠)_,出现错误 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY
找到了两篇相关文章:
Guide to Deploying Diffie-Hellman for TLS
Security/Server Side TLS
某论坛的一个回复里说

To use HTTP/2, you need to make sure cipher ECDHE-RSA-AES128-GCM-SHA256 is the first one in your cipher configuration.

首先按第一篇文章说的生成一个新的 "Diffie-Hellman group",作为 ssl_dhparam 的值。

openssl dhparam -out dhparams.pem 2048

然后复制了一个以 ECDHE-RSA-AES128-GCM-SHA256 为起始的值作为 ssl_ciphers,网站就可以工作了。

效果

HTTP/2 的 Multipexing 在这张图里体现得很明显,不会开多个连接去下载静态资源了。然而速度上似乎并没有提升。

可能的后续优化
Nginx Configuration Snippets
本博客 Nginx 配置之性能篇

另外,webpagetest 给我的 First Byte Time 这一项评分是 B。官方的解释是:

The First Byte time is the time from when the user started navigating to the page until the first bit of the server response arrived. The bulk of this time is usually referred to the "back-end time" and is the amount of time the server spent building the page for the user.

评分标准

The target time is the time needed for the DNS, socket and SSL negotiations + 100ms. A single letter grade will be deducted for every 100ms beyond the target.


这一项没有达标的原因大概是后端现在什么缓存都没开。其实原来是有用 Memcached 缓存全站,但是不够令人满意。 因为我在发布文章之后的一段时间可能会编辑文章,而加缓存之后总是要很慢才能生效。目前还在寻求更好的方法。 理想状况是最新的一篇文章永远不缓存,或者是更新文章之后让缓存失效。这些都需要更精细的代码层面的控制。

这篇文章之后应该还会更新。

2015.10.20
添加 OCSP Stapling
引用 JerryQu 的说法

浏览器可能会在建立 TLS 连接时在线验证证书有效性,从而阻塞 TLS 握手,拖慢整体速度。OCSP stapling 是一种优化措施,服务端通过它可以在证书链中封装证书颁发机构的 OCSP(Online Certificate Status Protocol)响应,从而让浏览器跳过在线查询。服务端获取 OCSP 一方面更快(因为服务端一般有更好的网络环境),另一方面可以更好地缓存。

nginx.conf 添加上这三行就可以了:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /home/laike9m/files/laike9m_com/laike9m_com.chain.crt;

ssl_trusted_certificate 所需要的 pem 文件其实就是 chained crt(问了 gogetssl 员工得知)。

2015.10.30
启用 Session Ticket

ssl_session_tickets  on;

Session Ticket 和 Session Cache 一样,也是简化 TLS 握手的方案。我不是很清楚在服务器都支持的情况下浏览器会选择哪一种。

修改 ssl_buffer_size
关于 ssl_buffer_sizeNginx 文档是这么解释的:

Sets the size of the buffer used for sending data.

By default, the buffer size is 16k, which corresponds to minimal overhead when sending big responses. To minimize Time To First Byte it may be beneficial to use smaller values, for example:

ssl_buffer_size 4k;

然而到底设成多少比较好,网上找不到相关的说明/测试。在 Is TLS fast yet 里 Ilya Grigorik 把这个值设成了 1400:

ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

然后我也学着设成了 1400。不严谨的测试结果表明,TTFB 确实减小了,但是总的 download time 增大了一点。

开启 tcp_nopush + tcp_nodelay + sendfile

sendfile        on;
tcp_nopush      on;
tcp_nodelay     on;

这篇文章详细讲了为什么要这么配置。简单来说就是 sendfile on 启用了 sendfile(2) 这个系统调用,相比 read()/write() 有诸多优点;tcp_nopushtcp_nodelay 结合起来保证充分利用 MTU 的同时还能尽快地把数据包发出去而不等 0.2s。

2015.11.1
压缩 png/jpg
神器 tinypng.com

减小 favicon.ico 的大小
原来的 favicon.ico 竟然有 30KB。上网一查才知道,ico 其实就是多层未压缩的 bmp。本来想用 Photoshop 编辑,但是居然打不开。安了一个插件之后能打开了,然而编辑功能简直糟糕,多图层都没法显示。好在找到一枚神器 IcoFX,花钱买了正版。
按照这篇文章讲的,删掉了 64*64,24 * 24 两个图层。保留 32 * 32 是因为 Disqus 的站点缩略图可能要用。颜色也没有换成 4bit,仍然保留 8bit 256 色,毕竟已经减到 3KB 了。

2015.12.5
参照 sendfile 官方文档加上了 aio on

aio can be used to pre-load data for sendfile()

然后我无意之中发现,sendfile ongzip on 无法同时起作用,来自官方的《Tuning NGINX for Performance》 一文:

Note, however, that because data copied with sendfile() bypasses user space, it is not subject to the regular NGINX processing chain and filters that change content, such as gzip. When a configuration context includes both the sendfile directive and directives that activate a content-changing filter, NGINX automatically disables sendfile for that context.

因为 sendfile on 使用的是系统调用 sendfile(2),实现了 zero-copy,文件内根本就不会被拷贝到用户态(“bypasses user space”),因此 Nginx 无法进行压缩处理。

上网查了一下,唯一既能利用 zero-copy 又能利用 gzip 压缩的方案是启用 gzip_static。这个功能并非开箱即用,而是要求使用者先把文件压缩好,这样 Nginx 才能找到,如果没找到,就 fall back 回普通的 gzip。因为比较麻烦,这次先不弄了。计划是添加一个 Django command 实现原来指定 gzip 压缩的那些格式的文件。几个可能有用的链接:
Gzip per request vs static gzip 比较两种 gzip 方式
Nginx Performance Tuning 对于 gzip_static 讲得比官方文档清楚
Nginx中gzip_static模块的使用 使用 gzip_static 的一些注意事项

还有把 gzip_comp_level 从 6 改成了 9,提高压缩的程度,代价是更消耗 CPU,不过感觉 CPU 用的不多,所以无所谓。

博客迁移记录

之前在《决定转投 DigitalOcean》这篇文章中说了一下决定迁移网站的原因。 趁十一假期终于完成了博客的迁移工作,在此记录。

  1. Dump PostgreSQL data

    pg_dump -U laike9m -f dump.sql database1
    

    生成 425KB 的 dump.sql, 下载到本地,scp 到 DO machine。安装 PostgreSQL:

    sudo apt-get update
    sudo apt-get install postgresql postgresql-contrib
    

    创建用户和数据库,参考 how-to-install-and-use-postgresql-on-ubuntu-14-04。 还要给用户 laike9m 数据库的管理员权限。先切到 postgres 用户,然后参考 error-must-be-owner-of-language-plpgsql 给 laike9m 管理员权限。

  2. Load data

    psql -U laike9m database1 < dump.sql
    
  3. 配置虚拟环境
    安装 virtualenvwrapper,mkvirtualenv, 安装依赖。在安装之前先

    sudo apt-get install libpq-dev
    sudo apt-get install python3-dev
    

    因为要安装 psycopg2。

  4. 下载并修改代码
    在家目录创建 static, media 文件夹作为 STATIC_ROOTMEDIA_ROOT。 clone 代码,clone media 文件夹,按照新路径修改 settings.py, 下载 media 文件。设定密码环境变量(实际上这个用不到了)。
    之前的 Django 版本是 1.5,这次我想趁迁移升级成 1.8。参照官方的教程, 先把 south 从 INSTALLED_APPS 中去掉,然后在 css3two_blog 文件夹中

    mkdir migrations
    cd migrations
    touch __init__.py
    

    最后在根目录执行 python manage.py makemigrationspython manage.py migrate --fake-initial

    (blog)laike9m@laike9m1:~/Envs/blog/My_Blog$ python manage.py makemigrations
    Migrations for 'css3two_blog':
      0001_initial.py:
        - Create model BlogPost
        - Create model BlogPostImage
    (blog)laike9m@laike9m1:~/Envs/blog/My_Blog$ python manage.py migrate --fake-initial
    Operations to perform:
      Synchronize unmigrated apps: staticfiles, admindocs, messages, mytemplatetags, contact_form
      Apply all migrations: css3two_blog, contenttypes, taggit, admin, auth, sites, sessions
    Synchronizing apps without migrations:
      Creating tables...
        Running deferred SQL...
      Installing custom SQL...
    Running migrations:
      Rendering model states... DONE
      Applying contenttypes.0001_initial... FAKED
      Applying auth.0001_initial... FAKED
      Applying admin.0001_initial... FAKED
      Applying contenttypes.0002_remove_content_type_name... OK
      Applying auth.0002_alter_permission_name_max_length... OK
      Applying auth.0003_alter_user_email_max_length... OK
      Applying auth.0004_alter_user_username_opts... OK
      Applying auth.0005_alter_user_last_login_null... OK
      Applying auth.0006_require_contenttypes_0002... OK
      Applying taggit.0001_initial... FAKED
      Applying taggit.0002_auto_20150616_2121... OK
      Applying css3two_blog.0001_initial... FAKED
      Applying sessions.0001_initial... FAKED
      Applying sites.0001_initial... FAKED
    

    最后拷贝静态文件

    python manage.py collectstatic
    

    runserver 启动测试服务器,通过 ip 访问网站并执行各种操作,均没有出现问题。
    这里不得不赞一下 Django,原先以为 1.5 升级 1.8 将遇到各种问题,尤其是跨过了 1.7 这个内置 south 的重大改进版本,没想到居然顺利完成了。

  5. 安装配置 Nginx, uWSGI
    Nginx 我比较熟了,但是每次遇到 uWSGI 都很麻烦,这次也是。花了很长时间去解决 这个问题。后来发现在文档里已经有说明。
    uWSGI 配置文件 uwsgi.ini 如下

    [uwsgi]
    chdir           = /home/laike9m/Envs/blog/My_Blog
    module          = my_blog.wsgi:application
    home            = /home/laike9m/Envs/blog/
    master          = true
    processes       = 3
    socket          = /tmp/uwsgi.sock
    chmod-socket    = 777
    vacuum          = true
    logto           = /home/laike9m/uwsgi.log
    

    Nginx 配置在这里
    原本想直接开个 screen 来运行 uwsgi,后来发现用 upstart 更好。 下面是 uWSGI 的配置文件 /etc/init/uwsgi.conf:

    env DJANGO_DB_PASSWORD=
    env EMAIL_HOST_PASSWORD=
    env LC_ALL=en_US.UTF-8
    env LANG=en_US.UTF-8
    
    start on runlevel [2345]
    stop on runlevel [!2345]
    
    setuid laike9m
    setgid www-data
    
    exec /usr/local/bin/uwsgi --ini /home/laike9m/Envs/blog/My_Blog/uwsgi.ini
    

    这样之后用 sudo service uwsgi start/stop/restart 就能操作 uWSGI 了

  6. 添加 HTTPS
    证书还是用之前的,主要是修改 Nginx 配置。对 80 端口的访问要自动跳转。
    这里只贴关键部分的配置。

    server {
        listen 80;
        server_name  laike9m.com laike9m.com;
        return 301 https://$server_name$request_uri;
    }
    
    server {
        ...
        listen 443 ssl;
        server_name  laike9m.com laike9m.com;
        ssl_certificate /home/laike9m/files/laike9m_com/laike9m_com.crt;
        ssl_certificate_key /home/laike9m/files/laike9m_com/key;
        ...
    }
    


    基本就是这样了。

一点关于 iter 的历史

本文是另两篇文章的预备。想必很多人都知道 iterator 这个概念,也知道 Python 里有一个 __iter__ 方法。 这里想讲一些关于 iterator 的历史。

Iterator 这个概念在很多编程语言里都有,Python 引入 iterator 是在 2.2 版本,也就是 2001 年的时候。 Iterator 是用来干嘛的,维基百科已经写得很清楚:

In computer programming, an iterator is an object that enables a programmer to traverse a container, particularly lists.

那么问题来了,在引入 iterator 之前,Python 是怎么遍历,比如说,list 或者 dict 的呢?

先来看 list。实际上是依赖于 __getitem__ 方法。估计大部分人对这个方法并不熟悉,其实很简单:

>>> [1,2,3].__getitem__(1)
2

给一个 index,返回相应的元素,相当于调用 [1,2,3][1]
于是,只要定义了这一个方法,我们就可以遍历自定义的类了:

class MyClass:
    def __getitem__(self, index):
        if index > 3:
            raise IndexError("that's enough!")

        return index

for name in MyClass():
    print(name)

输出 shell 0 1 2 3 看见 IndexErrorfor in 就知道遍历到头了,所以并不会报错。实际上 2.2 之前的 for in 都是调用 __getitem__

再看 dict。在 2.2 之前,我们并不能 for x in some_dict 因为 dict 并没有定义 __getitem__。 想遍历 dict 的话只能这样: python for key in some_dict.keys(): print(some_dict[key]) 显然这样性能不会好,因为要先生成一个包含所有 key 的 list。

Python2.2 引入了 iterator,相关文档参见 What’s New in Python 2.2 - PEP 234: Iterators。总结起来有以下改进:

  1. for x in C 会默认调用 iter(C),如果 C 没有定义 __iter__ 方法,会使用 __getitem__
  2. dict(以及其它一些内置类型)现在实现了 __iter__ 方法,所以可以 for x in some_dict 了。这样遍历 dict 比原来快很多,因为不需要生成一个 key list;
  3. for in 默认调用 iter() 的好处在于,用户可以对遍历过程应该返回什么,如何返回有更强的控制。另一方面就是拓宽了 for in 的使用范围,因为并不是所有我们希望遍历的 object 都拥有有意义的 index。比如之前 MyClass 的例子,index 就没有含义,所以用 __getitem__ 实现并不合适。


这篇文章主要是讲历史。下一篇文章详细讲 iterator 要怎么用,最后一篇对三种实现遍历的方式进行比较。

参考资料:
https://en.wikipedia.org/wiki/Iterator
http://effbot.org/zone/python-for-statement.htm
https://docs.python.org/3/whatsnew/2.2.html#pep-234-iterators


top