编程我只用CPP 发布的文章

一、概述

docker默认存在/var/lib/docker目录下,一般情况下这个目录都没有单独挂载,都是放在根目录下的,目录较小。

为了避免占用太多/var目录空间,并且方便管理,可以把存储目录放到其他的文件夹,例如/data/docker。

二、步骤

创建想要修改的目录,假设是/data/docker,首先创建文件夹并赋予权限。

> mkdir /data/docker
> chgrp -R docker /data/docker

停掉docker,修改docker的systemd服务文件,位于/usr/lib/systemd/system/docker.service,修改ExecStart一行:

> systemctl restart docker
> sed -i 's#ExecStart=/usr/bin/dockerd#ExecStart=/usr/bin/dockerd --graph /data/docker#g' /usr/lib/systemd/system/docker.service

重启docker:

> systemctl daemon-reload
> systemctl start docker 

验证是否修改成功:

> docker info | grep "Root"
Docker Root Dir: /data/docker
> ll /data/docker/
total 48
drwx------ 2 root root 4096 Dec 22 22:39 builder
drwx--x--x 3 root root 4096 Dec 22 22:39 containerd
drwx------ 2 root root 4096 Dec 22 22:39 containers
drwx------ 3 root root 4096 Dec 22 22:39 image
drwxr-x--- 3 root root 4096 Dec 22 22:39 network
drwx------ 3 root root 4096 Dec 22 22:39 overlay2
drwx------ 4 root root 4096 Dec 22 22:39 plugins
drwx------ 2 root root 4096 Dec 22 22:39 runtimes
drwx------ 2 root root 4096 Dec 22 22:39 swarm
drwx------ 2 root root 4096 Dec 22 22:39 tmp
drwx------ 2 root root 4096 Dec 22 22:39 trust
drwx------ 2 root root 4096 Dec 22 22:39 volumes

来源:力扣(LeetCode)

链接:113. 路径总和 II

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

说明:叶子节点是指没有子节点的节点。

示例:

给定如下二叉树,以及目标和 sum = 22:

          5
         / \
        4   8
       /   / \
      11  13  4
     /  \    / \
    7    2  5   1

返回:

[
   [5,4,11,2],
   [5,8,4,5]
]

二、题解

说明:

此题是112. 路径总和Ⅰ的升级版(题解参考:),相对于第一题来说,有以下两点不通:

  1. 题Ⅰ只需要判断存在即可,该题需要找到所有可能的结果。
  2. 找到所有结果的同时,该题还需要保存所有的路径。

算法:

递归+深搜遍历所有的树节点,每遍历一个节点除了计算sum是否满足条件以外,还要记录下该节点。当遇到满足条件的叶节点,把当前路径加到结果中去。需要利用到两个辅助的数组。

代码:

class Solution {
public:
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
        dfs(root, sum);
        return m_ans;
    }
private:
    void dfs(TreeNode *root, int sum) {
        if(root == NULL) {
            return;
        }

        // 节点添加到临时的数组里面去
        m_vec.push_back(root->val);
        sum -= root->val;

        if (root->left == NULL && root->right == NULL && sum == 0) {
            // 当前临时数组加入到结果数组中
            m_ans.push_back(m_vec);
        }

        // 遍历左节点
        if (root->left) {
            dfs(root->left, sum);
        }
        
        // 遍历右节点
        if (root->right) {
            dfs(root->right, sum);
        }
        
        // 最关键的一步:回溯状态
        // 函数入口处把当前节点添加到临时数组里面去了,这里退出的时候要删掉这一个节点
        // 避免当前节点还存在于临时数组中,影响后续的遍历结果
        m_vec.pop_back();
    }

    vector<vector<int>> m_ans; // 保存所有的结果
    vector<int> m_vec; // 临时保存路径的数组
};

复杂度分析:

  • 时间复杂度:访问每个节点一次,时间复杂度为O(N) ,其中N是节点个数。
  • 空间复杂度:需要用到两个数组,其中一个是保存结果的数组可不算入空间占用,另外一个保存路径的临时数组最坏情况下(树是一条线,即类似链表的时候)占用O(N) ,平均和最优情况下(树是平衡树)占用O(log(N))。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/maximum-subarray/

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

  • 输入:[-2,1,-3,4,-1,2,1,-5,4]
  • 输出:6
  • 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶:

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

分治法并不能算是最优解,但是可以锻炼对该算法的掌握情况。本题不考虑此算法。

二、题解

2.1 贪心算法

算法:

  1. 遍历数组,使用sum变量计算每一个节点时的最大和,用ans表示当前所有区间的最大子序和。
  2. 运行到第i个节点的时候,如果sum > 0,说明对当前节点有增益效果,sum继续加上当前节点。
  3. 如果sum <= 0,说明之前的序列对当前节点来说没有增益效果,之前的和可以舍弃。sum从当前节点开始重新计算。
  4. 然后比较ans和sum的大小,较大的赋值给sum。

图例:

图片来源于解答:画解算法:53. 最大子序和 , © 著作权归作者所有 。

以数组[-2, 3, -1, 1, -3]为例,初始化时设置sum为0,ans为第一个元素的值:

访问第一个元素-2,当前sum为0,更新sum为当前节点的值-2,sum和ans对比ans还是-2:

image2781361f9b1cc7d6.png

访问第二个元素3,因为sum = -2,如果加上当前节点,会使得连续和变成1(还不如不加,不加是3)。因此重新计算sum,设置sum为当前节点值3,继续往前计算:

imagefd03c8e73879b133.png

访问第三个元素-1,此时sum > 0,对-1有增益效果,sum加上-1等于2,ans不变:

image1f06514c3fa63f36.png

第四个元素:

image8742e9aae039ff80.png

到第五个元素时,sum = 3,加上-3之后等于0,ans依旧等于3:

imagedaf7eae811ef6e59.png

然后到此结束,最大连续和是3。

代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int ans, sum, i;
        ans = nums.size() > 0 ? nums[0] : 0;

        for (i = 0, sum = 0; i < nums.size(); i++) {
            if (sum <= 0) {
                sum = nums[i];
            } else {
                sum += nums[i];
            }

            ans = max(ans, sum);
        }
        return ans;
    }
};

image.png

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2.2 动态规划

算法:

其实动态规划的思路和上面的贪心算法差不多,关键的结果是动态规划的状态方程:假设dp[i]是第i个节点时候的最大连续和,那怎么计算它的值呢?

其实还是和上面的贪心算法一样,只要dp[i - 1]加上当前节点的和大于当前节点,那么dp[i]就等于和值。否则dp[i]就应该设置为当前节点值,所以它的状态转移方程是:

dp[0] = nums[0];
dp[i] = max(dp[i - 1] + nums[i], nums[i])

代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int i, sum;
        vector<int> dp(nums.size());

        if (nums.size() == 0) {
            return 0;
        }

        dp[0] = nums[0];
        sum = dp[0];

        for (i = 1; i < nums.size(); i++) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            sum = max(dp[i], sum);
        }

        return sum;
    }
};

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n),整个状态中只用到了dp[i]和dp[i - 1],优化成O(1)。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/diameter-of-binary-tree

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。

示例 :

给定二叉树

          1
         / \
        2   3
       / \     
      4   5    

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

注意:两结点之间的路径长度是以它们之间边的数目表示。

二、题解

本题最重要的一个点是要理清题意,求的是最大直径,是树内任意两点间的距离的最大值,不是求根节点到任意节点间距离的最大值(即深度)。

想法:

任意一条路径可以被写成两个箭头(不同方向),每个箭头代表一条从某些点向下遍历到孩子节点的路径。

假设我们知道对于每个节点最长箭头距离分别为L,R,那么最优路径经过L + R + 1个节点。

算法:

按照常用方法计算一个节点的深度:max(depth of node.left, depth of node.right) + 1。在计算的同时,经过这个节点的路径长度为1 + (depth of node.left) + (depth of node.right)。搜索每个节点并记录这些路径经过的点数最大值,期望长度是结果-1

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int diameterOfBinaryTree(TreeNode* root) {
        unsigned int diameter = 1;
        depth(root, diameter);
        return diameter - 1;
    }
private:
    int depth(TreeNode *node, unsigned int &diameter) {
        unsigned int lDepth, rDepth;
        if (node == NULL) {
            return 0;
        }
        lDepth = depth(node->left, diameter);
        rDepth = depth(node->right, diameter);

        diameter = max(diameter, lDepth + rDepth + 1);

        return max(lDepth, rDepth) + 1;
    }
};

复杂度分析:

  • 时间复杂度:O(N),每个节点只访问一次。
  • 空间复杂度:O(N),深度优先搜索的栈开销。

2020-03-10添加

为了完成leetcode每日1题打卡计划,使用golang重新提交了一次。

计算深度和直径的方式和上面略有不同,原理还是一样的:

func depth(root *TreeNode, diameter *int) int {
    if root == nil {
        return 0
    }

    // 左子节点的深度
    lDepth := depth(root.Left, diameter) + 1
    // 右子节点的深度
    rDepth := depth(root.Right, diameter) + 1

    // 当前节点的直径
    curDiameter := lDepth + rDepth - 1
    // 更新最大的直径
    if *diameter < curDiameter {
        *diameter = curDiameter
    }

    // 返回最大深度
    if lDepth < rDepth {
        return rDepth
    } else {
        return lDepth
    }
}

func diameterOfBinaryTree(root *TreeNode) int {
    var diameter int
    diameter = 0
    depth(root, &diameter)
    return diameter
}

nginx中只有if关键字,并不支持else语法,if的使用方法为:

if ($xxx = xxx) {
    xxx
}

和代码不同的是:if条件语句判断相等时只要一个等号,不能是==

虽然不支持else,但是可以使用以下的方法来模拟实现else:

server {
    server_name *.maqian.io;
    listen 80;
    
    location / {
        set $is_matched 0;
        if ($host = a.maqian.io) {
            proxy_pass http://127.0.0.1:1001/;
            set $is_matched 1;
        }
        
        if ($host = b.maqian.io) {
            proxy_pass http://127.0.0.1:1002/;
            set $is_matched 1;
        }
        # 没有匹配到,跳转到默认页面
        if ($is_matched = 0) {
            proxy_pass http://127.0.0.1;
        }
        
        # xxx
        # xxx
        # xxx
    }
}

setuid、setgid、以及黏着位

setuid的作用是以该命令拥有者的权限去执行,比如修改密码的passwd命令,执行passwd时会拥有root权限,不然就修改不了/etc/passwd文件了。

而setgid的意思是以命令所有组的权限去执行,它们的标志位是s,出现在x的地方,例如-rwsr-xr-x

手动添加这一位的方式:

> touch ls
> chmod u+s ls # UID权限设置
> ll ls
-rwSrw-r--. 1 ma ma 0 Nov  3 00:50 ls
> chmod g+s ls # GID权限设置
> ll ls
-rwSrwSr--. 1 ma ma 0 Nov  3 00:50 ls

对于以下程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    // 打印实际用户ID和有效用户ID
    printf("uid: %u euid: %d\n", getuid(), geteuid());
    return 0;
}

编译后加上UID权限,分别使用不同的身份运行:

> make setuid
gcc setuid.c -o debug/setuid
> ll debug/setuid 
-rwxrwxr-x. 1 ma ma 8552 Nov  3 02:24 debug/setuid
> chmod u+s debug/setuid # 加上UID权限位
> ll debug/setuid 
-rwsrwxr-x. 1 ma ma 8552 Nov  3 02:24 debug/setuid 
> sudo ./debug/setuid 
[sudo] password for ma: 
uid: 0 euid: 1000

可以看到,不管是以当前用户还是root用户运行,实际用户id都是一样的,都是其所有者。

二、黏着位(sticky bit)

关于黏着位找了很多资料也没有找到明确的描述,网上众说纷纭也没有清晰的描述出它的作用,最后还是在《UNIX环境高级编程》中找到一些更明确的解释:

The S_ISVTX bit has an interesting history. Onversions of the UNIX System that predated demand paging, this bit was known as the sticky bit.If it was set for an executable program file, then the first time the program was executed, a copy of the program’s text was saved in the swap area when the process terminated. (The text portion of a program is the machine instructions.) The program would then load into memory morequickly the next time it was executed, because the swap area was handled as a contiguous file, as compared to the possibly random location of data blocks in a normal UNIX file system. The sticky bit was often set for common application programs, such as the text editor and the passes of the C compiler. Naturally,therewas a limit to the number of sticky files that could be contained in the swap area beforerunning out of swap space, but it was a useful technique. The name sticky came about because the text portion of the file stuck around in the swap area until the system was rebooted. Later versions of the UNIX System referred to this as the saved-text bit; hence the constant S_ISVTX.With today’s newer UNIX systems, most of which have a virtual memory system and a faster file system, the need for this technique has disappeared.

在早期的unix系统中,如果一个程序被设置了黏着位,那么当它第一次执行结束之后,程序的正文段会被写入到交换空间中,以此加快后续使用的加载速度。因为交换空间是顺序存放,而磁盘上是随机的。它通常被设置成经常被使用的公用程序例如文本编辑器、编译器等,会一直持续到系统重启。

在后续的unix系统都设计了新的更快速的文件系统,所以这种用法逐渐消失。而对于这一黏着位“现在的用途”的描述是:

On contemporary systems, the use of the sticky bit has been extended. The Single UNIX Specification allows the sticky bit to be set for a directory. If the bit is set for a directory, a file in the directory can be removed or renamed only if the user has write permission for the directory and meets one of the following criteria:

  1. Owns the file
  2. Owns the directory
  3. Is the superuser

The directories /tmp and /var/tmp are typical candidates for the sticky bit—they are directories in which any user can typically create files. The permissions for these two directories are often read, write, and execute for everyone (user, group, and other). But users should not be able to delete or rename files owned by others.

现在的系统里面,黏着位已经被扩展了,它被用于目录权限上,如果一个目录设置了这一位,这个目录下的文件就只能被满足以下条件的用户重命名或者删除:

  1. 所有者
  2. 当前目录苏有这
  3. 超级用户

目录/tmp和/var/tmp是设置粘住位的候选者—这两个目录是任何用户都可在其中创建文件的目录。这两个目录对任一用户 (用户、组和其他)的许可权通常都是读、写和执行。但是用户不应能删除或更名属于其他人的文件,为此在这两个目录的文件方式中都设置了粘住位

手动给目录添加sticky位:

> mkdir 123
> chmod +t 123
> ll -d 123
drwxrwxr-t. 2 ma ma 6 Nov  3 02:18 123

加上这一个权限之后目录的颜色也有变化:

image

linux c获取文件路径和文件名

linux提供了两个函数分别用来获取文件所在的目录和文件名:

char *dirname(char *path);
char *basename(char *path);

它们被包含在头文件libgen.h中,dirname取得的路径名不包含最后的/basename取自于最后一个下划线后的内容。

以下是几个示例:

路径dirnamebasename
/usr/lib/usrlib
/usr//usr
usr.usr
...
.....

通过这两个函数我们就可以写出属于我们自己的dirname和basename命令了:

#include <libgen.h>
#include <stdio.h>

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Usage: %s path ...\n", argv[0]);
        return 0;
    }

    for (int i = 1; i < argc; i++) {
        printf("%s\n", dirname(argv[i])); // basename直接替换这里的函数即可
    }

    return 0;
}

编译运行:

> gcc dirname.c -o dirname
> ./dirname /usr/local /data/
/usr
/data

实际上linux也内置了dirnamebasename两个命令:

> basename /usr/lib/libDeployPkg.so.0 
libDeployPkg.so.0
> dirname /usr/local/ /data
/usr
/

一、原理

linux支持多进程间共享打开文件,即同一时刻允许多个进程同时打开同个文件,每个进程之间的读写操作互不影响。为了实现这一个机制,linux内核使用了三种数据结构来表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

1.1 内核数据结构

每个进程的进程表中有一个记录项,包含了当前进程所有打开的文件描述符,它包含了一个指向文件表项的指针和文件描述符标志。内核中,为所有打开的文件维持一张表,它包含了以下内容:

  • 当前文件打开的状态:以何种方式打开的该文件,只读、只写或是可读可写等。
  • 当前文件的偏移量:当前文件指针所处的位置。
  • 指向该文件节点表的指针:节点包含了当前文件的属性信息。

每个文件的信息被封装在一个v节点表项中,包含了当前文件的文件名、所有者以及inode等信息。

三者之间的状态关系为:

1.2 多进程共享同一个文件

对于多个进程打开的同一个文件,其状态关系为:

正因为每个文件描述符都有一个属于自己的文件表项,所以每个进程间的文件指针偏移相互独立,互相读写不干扰。但是打开同一个文件的时候v节点指针都指向同一个节点:

  • 每次完成write后,文件表项的当前文件指针偏移量也会立马加上写入的字节数。
  • 如果打开文件的时候加了O_APPEND参数,每次写入数据前会先把偏移量设置到文件末尾。
  • 通过lseek函数只修改当前文件偏移量,不进行任何I/O操作。

有一个要注意的是,每次fork进程后,子进程会复制父进程的文件描述符,两者相互独立。

二、dup和dup2

dup和dup2都可以用来复制一个现有的文件描述符,其用法如下:

#include <unistd.h>

int dup(int fd);
int dup2(int fd1, int fd2);

dup函数直接把复制后的文件描述符返回,返回的一定是当前文件描述符表中的最小数值。

对于dup2,可以通过fd2表示新描述符的值,如果fd2已经打开,系统会先关闭。如果fd1等于fd2,则直接返回不关闭。

复制过后的文件描述符共享一个文件表项,共享后的状态如下:

我们可以通过一个程序来验证这一个结论:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    char buff[6] = { 0 };
    int fd_1, fd_2;

    fd_1 = open("data.txt", O_RDONLY);
    if (fd_1 == -1) {
        perror("open file error");
        return -1;
    }

    fd_2 = dup(fd_1);
    if (fd_2 == -1) {
        perror("dup error");
        return -1;
    }

    if (read(fd_1, buff, 5) == -1) {
        perror("read error at fd_1");
        return -1;
    }
    printf("fd_1 read: %s\n", buff);

    if (read(fd_2, buff, 5) == -1) {
        perror("read error at fd_2");
    }
    printf("fd_2 read: %s\n", buff);

    close(fd_1);
    close(fd_2);

    return 0;
}

上面的代码中通过fd_1打开文件data.txtfd_2复制fd_1,两个文件描述符文件从文件中读取5个字节数据并打印出来。

我们编译代码执行:

# 先写十个字节数据到文件
> echo "HelloWorld" > data.txt
> mkdir debug
# 编译
> gcc dup.c -o debug/dup
# 执行
>  ./debug/dup 
fd_1 read: Hello
fd_2 read: World

可以看到,fd_2读取的数据是从第5个字节开始,即从fd_1读完偏移处开始,两者确实共享了同一个文件表项。

nginx访问限频

一、并发访问限制

ngx_http_limit_conn_module是一个默认安装的内置模块,被用来限制在某一个关键字维度上的最大并发数量,通常情况下,这个维度被设置为访问者的IP。在计算的一个连接当前的并发数量时,不是一连接就会被计数,而是当所有请求头都被读完才计数。它的示例配置为:

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    # ...
    server {
        # ...
        location /download/ {
            limit_conn addr 1;
        }
    }
}

以上配置通过limit_conn_zone指令定义了一个名为addr的并发限制器,它以$binary_remote_addr(即访问的IP地址)作为key,分配一块10m的空间来保存所有的连接数量。

而后的对应的server段中,使用这个addr,限制同一时刻最多只能有1个连接。所以整个配置的意思就是:限制单个IP同一时刻最多有3个访问连接。

相关的参考文档:Module ngx_http_limit_conn_module

1.1 案例一:针对IP的并发数量限制

当前有一个站点d2.maqian.co,希望同一时刻同一IP最多只能3个并发访问:

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m
    server {
        listen       80;
        server_name  d2.maqian.co;
        limit_conn   addr 3;
        limit_rate   1m; # 限制访问速率1M/s
    
        location / {
            root   html;
            index  index.html index.htm;
        }
        
        access_log logs/access.log main;
        error_log logs/error.log;
    }
}

网站的根目录下有个文件master.zip,大小8.8M,在另一台客户端上使用ab命令执行并发测试:

ab -c 5 -n 10 http://d2.maqian.co/master.zip # 同一时刻5个并发连接,一共10个连接

测试过程中Nginx的访问日志:

> cat access.log
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:19 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:27 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:27 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:17:45:27 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"

可以看到,除了3个连接的的返回值为200以外,其他的都返回状态码503。

为什么三个200访问的日志在最后呢?

因为下载的文件内容是8.8M,配置中有限制最大下载速率为1M/s,所以它大概需要9秒才能下载完成,状态码是在连接返回完成才打印出来的,并且可以看到503状态码和200状态码的日志间隔差不多刚好8-9s。

那为什么要限制下载速率呢?

当前在内网环境下,下载速度非常快,8.8M的文件几乎1S内就能下载完成,连接很难并发,即使并发了,10个连接也很有可能有多个成功了。

1.2 针对服务端同一时刻的访问频率限制

和上面同样的环境,我们不限制单个IP的并发访问数量,而希望同一时刻服务器最多处理10个连接:

http {
    limit_conn_zone $server_name zone=web_server:10m;
    server {
        listen       80;
        server_name  d2.maqian.co;
        limit_conn web_server 10;
        limit_conn_log_level info;    
        limit_rate 1m;
    
        location / {
            root   html;
            index  index.html index.htm;
        }
    
        access_log logs/access.log main;
        error_log logs/error.log;
    }
}

执行命令测试:

ab -c 11 -n 15 http://d2.maqian.co/master.zip # 同时产生11个连接,一共访问15次

nginx访问日志:

192.168.123.101 - - [20/Oct/2018:18:23:49 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:49 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:49 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:49 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:49 +0800] GET "d2.maqian.co/master.zip" 503 537 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"
192.168.123.101 - - [20/Oct/2018:18:23:58 +0800] GET "d2.maqian.co/master.zip" 200 9204205 "-" "ApacheBench/2.3" "-"

限制了同一时刻服务端只能有10个连接之后,现象也和上面的一样,15个连接有5个返回错误,但是不同的是这些连接可以同时来自于同一个IP。

1.3 多关键字配合使用

上面的基于IP和服务端访问限制可以同时使用,并且不只是这两个字段可以配合使用,其他的变量也都可以同时使用,具体的变量可以参考:Alphabetical index of variables

例如我们可以同时限制单个IP并发连接数为3,并且同时访问服务端的连接数为100:

http {
    limit_conn_zone $server_name zone=web_server:10m;
    limit_conn_zone $binary_remote_addr zone=addr:10m
    server {
        listen       80;
        server_name  d2.maqian.co;
        limit_conn web_server 100; # 同一时刻最多100个连接访问服务端
        limit_conn addr 3; # 同一时刻同一IP最多3个连接访问
        limit_conn_log_level info;    
        limit_rate 1m;
    
        location / {
            root   html;
            index  index.html index.htm;
        }
    
        access_log logs/access.log main;
        error_log logs/error.log;
    }
}

二、限制访问频率

ngx_http_limit_req_module模块用于限制每个IP访问某个关键字维度的请求速率,其参数用法如下:

limit_req_zone key zone=name:size rate=rate;

以上的配置创建一个速率限制器,限制单个IP在key这个维度上的访问速率。和上面一样,这个key也通常被设置成访问者的IP。使用时在对应的server段内设置:

limit_req zone=name [burst=number] [nodelay];

burst表示令牌数量,连接满了之后,给接下来的连接发放令牌进行等待。令牌数量超出后,可以选择继续等待令牌或者直接返回错误状态。

这里的逻辑可以看成去银行办业务:人多的时候需要等号,number可以堪称最大的等号数量,rate可以堪称银行的窗口个数。银行同一时刻处理rate个客户的请求,并且同时允许number个客户排队,超出后,根据nodelay是否被设置来判断该连接是应该被丢弃还是等待。

参考文档:Module ngx_http_limit_req_module

2.1 限制每秒只处理同一个用户的一个请求

http {
    # 以请求IP作为KEY,设置访问频率为1秒1次请求
    limit_req_zone $binary_remote_addr zone=addr:10m rate=1r/s;
    server {
        listen       80;
        server_name  d2.maqian.co;
        # 设置队列为5,最多有5个连接等待,超出的不继续等待
        limit_req zone=addr burst=5 nodelay;
        limit_rate 1m;
    
        location / {
            root   html;
            index  index.html index.htm;
        }

        access_log logs/access.log main;
        error_log logs/error.log;
    }
}

为了避免大文件下载耗时,这里不再和上面一样下载大文件,使用小文件测试:

ab -c 6 -n 100 http://d2.maqian.co # 6个并发连接,访问100次

得到日志后,复制到当前目录下,分别分析200和503响应的次数:

> grep "503" access.log | wc -l
94
> grep "200" access.log | wc -l
6

nginx第一秒处理第一个请求,同时给接下来的5个请求排队,剩下的都直接返回503,所以返回200的次数为6,503的次数为94。

ab返回的结果也能看到成功和失败的数量: