使用 Riak 和 Nginx 搭建的静态文件服务器

在搭建网站时,如果更新不多会用静态文件服务器,但如果要扩容,处理巨大的流量是件比较复杂的事情。负载均衡、磁盘冗余、容量规划、横向扩展、缩 减,工程师需要考虑的问题非常多。在这里,我们将使用基于 GitHub pages 中表现出色的 Riak,在不编程的情况下,搭建既可以维持稳定延迟,又可快速扩展的静态文件服务器网站。

概要

大家一定都还记得初次搭建 Web 服务器的时候,先安装 Apache httpd,将写入了 hello 的 index.html 放到指定文件夹,然后在浏览器里打开它。或者也有不少读者可能记得另一种方法,即用 FTP 客户端将 HTML 文件上传到 Geocities。

静态文件服务器是创建 Web 网站的基础。看上去非常简单,但其实相当深奥。比如说,很多人可能有过这种经历,等 HTTP 下载 Linux 的 ISO 镜像等了一个晚上,最终通过 tarball 1 下载的时候却发生硬盘故障,或因为超负荷服务 器宕机了。实际上,Web 网站的服务器需要使用到负载均衡、磁盘冗余、容量规划、横向扩展、网络消耗等众多技术。

1 使用 tar 命令打包的文件。工程师俚语。

其实这些问题,除去节省网络消耗外,只要使用两个软件就可以解决了。那就是 Nginx 和 Riak。通过 Nginx 完成负载均衡,而磁盘冗余、容量规划、横向扩展,都可以通过 Riak 实现。Riak 容易被认为是单纯的 KVS(Key-value Store),并被拿来和 RDB 比较,但其接口为 HTTP,并且可将数据以记录为粒度进行冗余化,只要增加机器就可以实现横向扩展。既可以作为数据库处理较大数据的记录,也可以当作存储几千到几百万级别 小数据的对象数据库来使用。

本连载会介绍一种方法,使用基于 GitHub pages 中表现出色的 Riak,在不编程的情况下,搭建既维持稳定延迟,又快速扩展的静态文件服务器网站。

网站设计

我们假设要搭建静态文件服务器的网站的域名为 foo.example.com。想要在这个网站中实现的是:

  1. 存储大量静态文件。
  2. 通过 foo.example.com/path/to/the/file 这样的 URL 定位每一个静态文件。
  3. 静态文件的大小最多也不超过几 MB,平均为数百 KB。
  4. 静态文件的个数在 1000~1 000 000 000 之间,起初先小规模启动。
  5. 所有图片都会被以相同的概率随机被读取。
  6. 不会特意地去删除图片(简单起见)。
  7. 响应时间的要求为,99% 的请求的响应时间控制在 500 ms 以内。
  8. 争取无服务停止时间。

以上内容,特别是第 4 条的横向扩展和第

8 条的停止时间,如果采用普通的文件服务器及 Apache 的 HA(High Availability)架构,实现起来会比较困难。即便导入了 Nginx 这样的负载均衡器,需要自行设计及操作数据库分片,而第 8 条的不停服务维护,以及磁盘故障冗余的建立和负载均衡的分发设定也都很复杂。所以,在这样的分工前提下,依靠功能本身更为丰富的 Riak 即可以简化系统整体。

图 1 foo.example.com 的架构例子

Riak是基于2006年由Amazon公开的Dynamo 设计出来的。它的主要特性为,通过 Consistent Hashing 进行水平分布、扩容,通过复制实现冗余,通过稳定的响应提供高可用性等。除此之外,它通过使用 JavaScript 执行 MapReduce,具有与除 Join 之外的 SQL 同等的表现能力。

Riak 具有 HTTP 接口,在 Riak 中存储的数 据可以通过浏览器原样展示出来。因此,如果这样就在互联网上公开的话,不仅通过 HTTP 的 GET 方法可以访问 Riak,PUT 和 DELETE 方法也可以不经过认证执行了。为了防范任意用户对数据的删除、更新,首先,使用 Riak,有必要通过 NAT(Network Address Translation)来构建一个确保不能直接通过互联网访问 Riak 的网络。

因为大部分的负载均衡器都能设置为只处理特定的 HTTP 方法,拒绝其他方法,如果读者自身比较熟悉的话,使用那个也没问题。这里,我们使用非常流行且高速的 Nginx 来作为例子。其实在 Nginx 之外,还有很多的免费 / 商业的 负载均衡器,例如 HAProxy、Varnish、LVS 和 BigIP 等,读者只要使用自己常用的就行了。

小贴士

在使用云服务的情况下,因为环境的不同,使用方式也略有不同。如果是 AWS 的 EC2,使用 Marketplace 的 AMI 比较好。在 EC2 环境下,所有的节点都分配了一个全球 IP 地址,最好使用一开始就已经利用 IPSec 限制访问的官方 AMI。若选择 IDC Frontier,最好使用 cloud selftype 官方 API 或 VPC(Virtual Private Cloud)。使用 NTT 通信的 Cloudn 或 Nifty 云等以 CloudStack 为基础的平台的话,可以让虚拟机一开始就被隔离到了私有网络中。

把 Nginx 设置为只允许 GET 方法,PUT、DELETE 等其他方法都拒绝。对于实际的文件更新,这里为简单起见,假设不通过负载均衡器,而是使用 curl 直接访问 Riak 集群来进行维护。

理论上来说,除了后面介绍的 Riak 集群,根据使用场景的不同,下面这些内容也是会用到的:

  • 用来上传文件的页面(包含认证及上传的表单)。
  • 监视 Riak 集群的系统(Zabbix 等)。
  • 针对系统扩展的部署系 统(Puppet、Chef 等)。
  • 获取、管理访问日志以及切断非法访问。
  • 为了降低负载使用的 CDN(Contents Delivery Network)。

验证操作的软件环境如下所示:

  • Ubuntu Linux LTS 12.04/x86_64
  • Riak 1.3.1
  • Nginx - 1.1.19(Ubuntu 的 apt 版本)
  • curl

大部分硬件都能运行上面的软件需要的环境。并且,Riak 几乎支持所有的 Unix 系平台。运行 Riak 没有什么特殊的最低硬件条件,比如:

  • 8 核的 x86 CPU
  • 内存 16~32 GB
  • 硬盘 8 TB
  • 1 GB 的网卡

  • 32 核的 CPU
  • 内存 64~128 GB
  • 硬盘 72 TB
  • 10 GB 的网卡

上述配置都是可以的。磁盘方面,需要使用 Raid0 配套 LVM、ZFS 这样的目录管理系统。并且,这次使用 Bitcask 作为后端,如果静态文件数量太多的话,内存的消耗量在纸上计算出来会比较好。说到磁盘大小,可以通过需部署文件的大小乘以个数来预估:

每台的磁盘大小 ≥ 文件的平均大小 × 每台的文件数 × 3 + 余量(20%)

这样估算是可以。(此数值受故障率及网络组成影响,不能一概而论。想必读者都知道运行的稳定性及运行成本之间是相互制约的。)事实上,这需要综合考 虑硬件的 MTBF(Mean Time Between Failures)和 MTR(Mean Time to Recovery),以及网络带宽来进行总体设计,以便发挥最高的性能。

Riak 的准备与启动

下面介绍实际搭建网站的步骤。Riak 运行所需的最低环境要 求为 64 位 CPU、4GB RAM、多个 HDD、高速网络。虽然 Basho 推荐 Riak 台数最少为 5 台,但这边为了便于验证操作,只以 3 台(10.0.1.11,10.0.1.12,10.0.1.13)作为例子。并且准备好一台 Nginx 的机器(10.0.1.10)。此处读者可依据自身的环境替换为其他配置。

截至 2013 年 4 月 Riak 的最新版本号为 1.3.1。如果是 Ubuntu Linux 12.04 或者 APT 环境,可以使用 Basho 提供的 apt-line 来进行安装。

$ sudo curl http://apt.basho.com/gpg/
basho.apt.key | sudo apt-key add -
$ sudo bash -c "echo deb http://apt.
basho.com $ (lsb_release -sc) main >
/etc/apt/sources.list.d/basho.list"
$ sudo apt-get update
$ sudo apt-get install riak

这样,Riak 就安装好了。如果在不能访 问网络的环境下使用的话,可以从 Basho 的网站下载,并使用 dpkg 命令进行安装。

# sudo dpkg -i riak_1.3.1-1.amd64.deb

这样,这台服务器上就装好了。

Riak 在大多环境下都不需要优化参数(这里所指的是为了获得最优参数不断地进行性能测试,但在 Linux 环境下为了获得最佳性能,需要变更多个参数)。详细请参见参数的设定。

# sudo dpkg -i riak_1.3.1-1.amd64.deb

接下来,为了增大进程能打开的最大 FD 数量,需要对 /etc/security/limits.conf 进行编辑。

riak      hard      nprc      100000
riak      soft      nprc      100000

另外,因为会影响硬盘的 I/O 性能,内核的 I/O 调度器需要设置为 deadline。sda 部分,使用的是 /etc/riak/app.config 中设定的数据目录所在的设备。

# sudo echo deadline > /sys/block/sda/
queue/scheduler
$ cat /sys/blok/sda/queue/scheduler

根据环境需要对 /etc/riak/app.config/etc/riak/vm.args进行设定。需要设定为接受 HTTP 请求要绑定的 IP 地址和 Erlang 通信库需要使用到的 IP 地址(默认情况下会已设置为 127.0.0.1 的本地回环地址,从外部无法使用)。

app.config 的第 33 行(riak_core 区块的 http 项目)可以设置 HTTP 的 IP,将其设置为自己 Riak 服务器的 IP 地址。

{http, [ {"10.0.1.11", 8098} ]},

另外,数据目录原来应是 /var/lib/riak,如果已经定下数据用的分区路径,就要调整这个地方。是在默认 bitcask 区块的 data_root 项目外。

接下来,设置 /etc/riak/vm.args

## Name of the riak node
-name riak@10.0.1.11
+zdbbl 131072

这样做是为了在 Erlang 的通信库里指明 IP 地址。第二个 +zdbbl 是 Erlang/OPT 的通信库的发送队列的大小(KB 单位),默认是 1024 KB,最大可设置为 2 GB。

通过这些步骤,设置已基本完成,接下来在所有服务器上启动 Riak 进程。

[10.0.1.11]
$ sudo riak start

[10.0.1.12]
$ sudo riak start

[10.0.1.13]
$ sudo riak start

这样 3 台服务器上就完成了启动,但其相互识别为不同的 Riak 集群,所以需要使用 cluster 命令识别为同一集群。从思路上来说,在图 1 中显示的一台服务器 10.0.1.11 的集群与另两台汇合成了一个集群。

[10.0.1.12]
$ riak-admin cluster join riak@10.0.1.11

[10.0.1.13]
$ riak-admin cluster join riak@10.0.1.11

[10.0.1.11]
$ riak-admin cluster plan
$ riak-admin cluster commit

cluster plan 命令是用来规划 vnode 数据管理单位的配置命令。没有任何数据的状态下,最初构建集群时,并不会感觉到有太大的必要性,但在积累到一定数据无法停止系统时,用来控制再配置时就很有用了。执行 cluster commit 的话,就会实施 plan 里计划的再构建。具体来说,随着时间的转移,会实现 vnode 的 转移。通过执行以下命令:

$ riak-admin transfers

这样就可以观察到转移的具体情况。如果希望调整再配置的速度,就通过执行如下命令:

$ riak-admin transfer-limit
$ riak-admin transfer-limit 1

即可确认和调整再配置的带宽。transfer-limit 的默认值为 2,这是可以同时连接的数量(等于可同时移动的 vnode),所以会成为整体的再配置所需要的时间、网络带宽以及机器的负荷之间相互制约的结果。在再配置的过程中这个值也是可以变更的,所以可以边观察 transfers 的情况及服务器的负荷边进行值的调整。

这样,服务器就可以启动并且顺利作为集群运行了。确认的方法有多种。首先,使用 Riak 的运用命令的方法如下:

$ curl -X PUT http://10.0.1.11:8098/
buckets/spam/keys/ham -d 'egg'
$ curl http://10.0.1.12:8098/buckets/
spam/keys/ham
egg
$ curl -X DELETE http://10.0.1.13:8098/
buckets/spam/keys/ham
$ curl http://10.0.1.11:8098/buckets/
spam/keys/ham
not found

对 Bucket 名为 spam、key 名为 ham 的记录,可对到 3 台机器的访问进行确认。

初试文件的上传

首先,在不使用 Nginx 的情况下,上传样本图片(图2)。

图 2 因为 Riak 有 http 接口,所以可以使用浏览器确认数据

$ curl -X PUT http://10.0.1.11:8098/
buckets/static/keys/Lenna.jpg \
  -H "Content-type: image/jpeg"
  --data-binary @Lenna.jpg
$ gnome-open http://10.0.1.11:8098/
buckets/static/keys/Lenna.jpg

如果使用非 gnome 窗口管理器,或其他 OS,可以用浏览器打开这个 URL,应该就能确认上传的图片了。

Nginx 的设置

Riak 的基本功能的可用性已经得到确认,接下来进入到 Nginx 的设置阶段。设置 Nginx 需要完成以下两点:

  1. 将 GET 的请求平均转发给 Riak 集群。
  2. 拒绝 PUT、POST 等和更新相关的请求。

首先在 10.0.1.10 的机器上安装 Nginx。

[10.0.1.10]
$ sudo apt-get install nginx

为了设置上面的内容,编辑 /etc/nginx/site-enabled/default

首先将 HttpUpstreamModule的设定记述为 upstream backend,即可指定分配 HTTP 请求的 Riak 服务器。

upstream backend {
      server 10.0.1.11:8098;
      server 10.0.1.12:8098;
      server 10.0.1.13:8098;
      keepalive 4096;
}

如上,将IP地址和端口号罗列就可以了。另外,keepalive的作用是,以 HTTP KeepAlive 保持 4096 条 TCP 连接。

接下来,将GET以外的请求,都设定为不可通过,通过使用 limit_except 记述到 server 区块。若网站的 FQDN 是 foo.example.com,那就这样配置:

server {
   listen   80;
   server_name foo.example.com;

   location / {
       limit_except GET {
            deny   all;
       }
       proxy_pass http://backend;
   }
}

上述的意思是“监听 80 端口”,这个服务器的 FQDN 是 foo.example,接受方法只有 GET,向 proxy_pass 指定的服务器通过负载均衡进行传输。

这样,就可以访问 http://foo.example.com/buckets/static/keys/Lenna.jpg 所指定的图像了。实际的应用程序中,会更宽松地进行路径指定。如果需要在 Riak 的 Key 或 Bucket 中包含“ /”字符,可以在 Nginx 里重写 URL,把编码后的文字作为 bucket 或 Key 的名字就可以了。

使用 basho_bench 进行压力测试,增加服务器进行扩展

接下来对搭建的静态服务器性能进行实际评估。评估 HTTP 服务器性能,使用 ab(apache bench)比较方便,这里为了减少磁盘缓存的影响,最好对图片进行随机访问。所以,这次主要介绍 Basho 为评估 Riak 性能所开发的 basho_bench。

准备

首先,向 Riak 上传 100 万张图片作为静态文件样本。

实际上不必使用完全不同的图片,文件名使用自动命名就行了。这里为了方便 basho_bench 使用,在 Riak 上使用的是连续数字的文件名 1.jpg,……,1048576.jpg。图片的大小平均为 25 KB,复制 2 份,总 计 75GB 的文件被上传到集群中。

我们准备了一个简单的脚本来上传文件。其实什么语言都可以,这里挑选了 Ruby(见图 3)。

n = 1024 * 1024
(0..n).each do |i|
  url = "http://10.0.1.11:8098/buckets/static/keys/#{i}.jpg"
  print "#{i}/#{n} th image to #{url}\n"
  print `curl -X PUT #{url} -H "content-type: image/jpeg" --data-binary @lenna.jpg`
end

图 3 用不同名字上传 1,048,576 个相同图片的 ruby 脚本

同时,开始 basho_bench 的准备工作。我们准备了另一台服务器来承担负载,并安装好 Git 和 Erlang。

$ git clone git://github.com/basho/
basho_bench.git
$ cd basho_bench
$ make

这样就生成好了名为 basho_bench 的可执行文件。basho_bench 的配置文件如下:

{mode, max}.
{duration, 30}.
{concurrent, 64}.
{driver, basho_bench_driver_http_raw}.

{key_generator, {int_to_str, {uniform_int,
1048576}}}.

{http_raw_ips, ["10.0.1.10"]}.
{http_raw_prot, 80}.
{http_raw_path, "/buckets/static/keys"}.
{http_raw_params, ".jpg"}.

{operations, [{get, 1}]}.

这里不对每个项目都详细说明,只要理解以下内容即可,对安装了 Nginx 的服务器,使用 http://10.0.1.10/buckets/static/keys/{i}.jpg(i 是随机从 1~1 048 576 中挑选出来的)地址以 64 的并发,连续访问 30 分钟。

还有,为了降低读取负载,把读取访问数设置为 1。若读取和写入经常发生冲突也可以设定为超过半数。但我们这边考虑的访问是 Write once、Read many,所以没有必要这样做。

$ curl -X PUT http://10.0.1.11:8098/
buckets/static/props \
  -H "content-type: application/json"
-d '{"props":{"r":1}}'

这样,就在名为 static 的 bucket 里设置 了 r=1

实际使用的硬件环境,为如下配置的虚拟机:

  • Intel Xeon CPU,2.40 GHz,4 核
  • 内存 8 GB
  • 硬盘数据区域 128 GB

实验

执行 basho_bench 的基准测试,只要执行如下脚本:

$ ./basho_bench httpraw.config

如果要马上在命令行上显示图像,就执行:

$ ./priv/gp_throughput.sh -t dumb
$ ./priv/gp_latencies.sh -t dumb

这会在终端上用文本绘制出图像。这个命令在没有 Xforward 的服务器上很有用。

这次评估,通过在 2 到 8 台 Riak 之间进行变化,来测定相应的吞吐量和延迟。可以想象的是,随着 Riak 运行台数的增加,其负荷也能更加分散,吞吐会线性上升,但可以限制在一定数值下。同时,在实际超过 6 台左右时,每一台的数据量将低于物理存储量,所以性能会更好。

结果

首先,从吞吐量来看,机器数量在两台时是 700 qps,8 台时是 2700 qps,表明随着机器台数的增加,吞吐量可以稳定地进行扩展(见图 4)。

图 4 台数变化过程中的 HTTP GET 的吞吐量

事实上,因为 lenna.png 的大小约为 25 KB, 所以 basho_bench 通过 Nginx,获得了 67.5 MB/s 的数据。

因为是静态文件,处理这样规模的流量还是使用 CDN(Contents Delivery Network)比较稳妥。

图 5 也表明了延迟的状况。不同台数的机器承担相同负荷,可以看出台数少的时候,最长延迟情况很差。8 台的时候,99% 的请求在 400 ms 以内就返回了。或者说,随着台数的增加,延迟和吞吐量都有提升,可以认为负载均衡的目的已经达成。

图 5 台数变化过程中的 HTTP GET 的延迟

另外,在实验进行过程中,我费心于调整 Nginx,设置 KeepAlive,设置 OS,并多次往集群中增加 Riak 服务器,结果测试到 6 台时发现错误,再次从两台开始测试。这样的操作反反复复进行了很多次。但是就算这样,100 万个文件没有丢失过,可以顺畅地进行增加、删除服务器的操作。或者说,2、4、6、8 台测试的过程中,Riak 整个系统没有一次停止过。这样看来,相信大家也能明白不间断地运行 Riak 是多么地容易了。

GitHub pages

本文只介绍了最简单的方法。关于实际的服务 Github pages 是如何运作的,在 Erlang Factory 2012 上 Github 的工程师 Jesse Newland 的演讲中有详细说明。实际上,Github pages 的静态内容都缓存在名为 Fastly 的 CDN 上(图 6)。并且,因为有些路径包含“ /”字符,可以使用 Webmachine 架设一个简单的 HTTP 服务器,把路径和 Riak 的数据一一对应变换,对静态内容进行简单的版本管理。

$ dig basho.github.io
…省略…
;; QUESTION SECTION:

;basho.github.io.          IN   A

;; ANSWER SECTION:

basho.githum.io.    3600   IN   CNAME    github.map.fastly.net.

github.map.fastly.net.     30   IN       A       103.245.222.133

图 6 Github Pages 使用 Fastly 作为 CDN

使用了 Riak,GitHub Pages 现在可以维持着快速响应的同时进行扩展。

其他途径

Nginx 上也有标准的静态文件传送功能,利用这项功能,会十分方便。使用 RAID 提高磁盘的可靠性,再加上一定的缓存功能,也可以进行高速的静态文件传送。同样,把 NFS 上的数据 mount 到多台机器上,并用 Apache 或 Nginx 做文件服务器也是可以的。

或者,也可以使用 3 月公开的 OSS 版 Riak CS。它是和 Amazon S3 兼容的分布式存储方案,由于后端使用了 Riak,所以具有了与 Riak 一样的容错性,并可使用 S3 相关的工具。它所支持的最大对象可以达到 5TB。

总结

本文介绍了使用 Riak 和 Nginx 搭建支持横向扩展静态文件服务器的方法。因为使用了 Riak,数量和负载两方面都能实现扩展和分流。并且,可以利用 Riak 保持稳定延迟,利用 Nginx 的简单配置实现负载均衡。

虽然这次只使用了 8 台服务器,但还是有一些商业应用使用了 50 到 100 台服务器,甚至扩展到更多。请一定挑战一下更大的集群。还有,这次数据量只固定到了 25GB,随着服务器台数的增加,总的数据量也可以相应增加。可以根据系统需求调整硬盘大小或服务器台数。

Erlong/OTP 和 Riak 由于自身设计方面的原因,读取性能和吞吐并不太高。但在实际使用中,比起使用最耗资源的高性能数据库和内存数据库,选择 CDN 服务商提供的静态文件发布服务,会即高效又廉价。高峰时的读取请求委托给 CDN,自己保存原始数据,应付缓存未命中时所使用的数据存储 Riak,应该也相当划算。

虽然现在不能像以前那样,个人有机会运营文本、图片的网站,但当突然有数百万个文件出现在眼前时,本文描述的基本思考方法能对读者有所帮助的话,那就再好不过了。

参考文献

[1] 官网是 http://www.nginx.org

[2] 参见 http://github.com/basho/riak

[3] 参见 http://pages.github.com

[4] Giuseppe DeCandia 等 . Dynamo: Amazon' s highly available key-value store. In SOSP' 07

[5] Jesse Newland, Rewriting GitHub Pages with Riak Core, Riak KV, and Webmachine. In Erlang Factory SF2012.

发表评论