一般来说Web应用程序的开发者不太关心网络限速的问题。所以通常写的程序逻辑基本认为用户提交上来的数据速率越快越好;用户下载文件时,下载越快越好。但现实情况是服务器的带宽不是无限的,通常我们并不希望某一个用户的极速下载导致其它用户感觉此Web应用程序不可用。这样就带来了网络速率的需求。我在实际工作中大概总结出好几种限速办法,在这里记录以备忘。

ngx_http_core_module限制下载速率

最简单是直接使用ngx_http_core_module中的limit_ratelimit_rate_after指令,如下

location /flv/ {
    alias /www/flv/;
    limit_rate_after 500k;
    limit_rate       50k;
}

limit_rate可限制响应传输至浏览器客户端的速率,limit_rate_after表示浏览器客户端下载多少后才可以执行限速(使下载小文件不受限,下载大文件才限速)。

limit_rate还有一种配合后端被代理服务器的用户,如下

location /download/ {
    proxy_pass http://127.0.0.1:8080/download/; # proxied server return "X-Accel-Limit-Rate" response header
}

后端被代理服务器可返回X-Accel-Limit-Rate响应头,nginx将根据这个响应头设置的值进行限速。这样就可以灵活控制限速的逻辑(比如有些用户下载不限速,有些用户下载限速,而且限速的数值也可根据不同用户身份而不同)

nginx-upload-module限制上传速率

location /upload {
        # 转到后台处理URL,表示Nginx接收完上传的文件后,然后交给后端处理的地址
        upload_pass @backend;

        # 临时保存路径, 可以使用散列
        # 上传模块接收到的文件临时存放的路径, 1 表示方式,该方式是需要在/tmp/nginx_upload下创建以0到9为目录名称的目录,上传时候会进行一个散列处理。
        upload_store /tmp/nginx_upload;

        # 上传文件的权限,rw表示读写 r只读
        upload_store_access user:rw group:rw all:rw;

        set $upload_field_name "file";
        # upload_resumable on;

        # 这里写入http报头,pass到后台页面后能获取这里set的报头字段
        upload_set_form_field "${upload_field_name}_name" $upload_file_name;
        upload_set_form_field "${upload_field_name}_content_type" $upload_content_type;
        upload_set_form_field "${upload_field_name}_path" $upload_tmp_path;

        # Upload模块自动生成的一些信息,如文件大小与文件md5值
        upload_aggregate_form_field "${upload_field_name}_md5" $upload_file_md5;
        upload_aggregate_form_field "${upload_field_name}_size" $upload_file_size;

        # 允许的字段,允许全部可以 "^.*$"
        upload_pass_form_field "^.*$";
        # upload_pass_form_field "^submit$|^description$";

        # 每秒字节速度控制,0表示不受控制,默认0, 128K
        upload_limit_rate 0;

        # 如果pass页面是以下状态码,就删除此次上传的临时文件
        upload_cleanup 400 404 499 500-505;

        # 打开开关,意思就是把前端脚本请求的参数会传给后端的脚本语言,比如:http://192.168.1.251:9000/upload/?k=23,后台可以通过POST['k']来访问。
        upload_pass_args on;
    }

    location @backend {
        proxy_pass http://127.0.0.1:8080/process_upload;
    }

upload_limit_rate即可对上传速率进行限制。

ngx_stream_proxy_module限制上传下载速率

server {
    listen 81;
    proxy_pass 127.0.0.1:8081;
    proxy_download_rate 200k;
    proxy_upload_rate 200k;
}

使用ngx_stream_proxy_module的好处时只要是tcp或udp协议且使用nginx作反向代理,都可以限速。proxy_download_rate可限制下载速率,proxy_upload_rate可限制上传速率。

Java使用Guava的RateLimiter进行限速

上面说的全是使用nginx配置的方式进行限速,当有很特殊需求时,我们也可以使用程序来限速,如Java可使用GuavaRateLimiter进行限速。

RateLimiter 从概念上来讲,速率限制器会在可配置的速率下分配许可证。如果必要的话,每个acquire() 会阻塞当前线程直到许可证可用后获取该许可证。一旦获取到许可证,不需要再释放许可证。

RateLimiter使用的是一种叫令牌桶的流控算法,RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。(这个跟nginx的ngx_http_limit_req_module中用到的leaky bucket是一个意思)

RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率。与Semaphore 相比,Semaphore 限制了并发访问的数量而不是使用速率。

RateLimiter几个关键的方法

  • static RateLimiter create(double permitsPerSecond) 根据指定的稳定吞吐率创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询)
  • static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 根据指定的稳定吞吐率和预热期来创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少个请求量),在这段预热时间内,RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)
  • double acquire(int permits) 从RateLimiter获取指定许可数,该方法会被阻塞直到获取到请求
  • void setRate(double permitsPerSecond) 动态更新RateLimite的稳定速率,参数permitsPerSecond 由构造RateLimiter的工厂方法提供。
  • boolean tryAcquire(int permits) 从RateLimiter 获取许可数,如果该许可数可以在无延迟下的情况下立即获取得到的话
  • boolean tryAcquire(int permits, long timeout, TimeUnit unit) 从RateLimiter 获取指定许可数如果该许可数可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可数的话,那么立即返回false (无需等待)

使用示例

限制写入response的速率不超过200kB/s

RateLimiter limiter = RateLimiter.create(1024*200);
while(....){
  byte[] bytes = ...
  limiter.acquire(bytes.length);
  response.getWriter().write(bytes);
}