nginx-proxy 自动反向代理

docker 使得部署应用非常的方便,真正的让在单机部署多个应用成为一键化操作。然而,如果单机中有多个应用需要对外提供服务,用 docker 有时候感觉并不是那么优雅。

如果在单机中需要部署多个应用,而这些应用需要占用同一个端口,比如多个 web 应用同时需要 80/443 端口,或者希望对于来自同一个域名的流量进行 load balance,希望这些流量可以被分发到不同的容器上。这些常见的场景,目前操作起来并不方便。

对于上述场景,常见的做法就是在本地部署一个 nginx,每当有 docker 容器更新的时候,便修改 nginx 的配置文件,使满足需求。

方法很简单,但是繁琐,如果有工具可以把这些操作自动化,那么在使用 docker 的时候,是不是可以节省很多精力。在 daocloud hub 里,的确有这么一样工具,即是 nginx-proxy。这是一个会自动获取容器配置并做好反向代理和负责均衡的 nginx。

部署 nginx-proxy

首先需要把共享使用的端口预留给 proxy,比如 80、443 端口,这样,就可以让打在这两个端口上的流量由 proxy 来分配。其次,因为 proxy 需要获取 docker 容器的配置进行对 nginx 的配置文件修改,因此,需要把 docker.sock 映射到容器中。

docker 启动命令

docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro daocloud.io/daocloud/nginx-proxy

建议使用 compose 来配置,yml文件如下:

proxy:
  image: daocloud.io/daocloud/nginx-proxy:0.3.6
  privileged: false
  restart: always
  ports:
  - 443:443
  - 80:80
  volumes:
  - /var/run/docker.sock:/tmp/docker.sock:ro

反向代理

对于配置好 proxy 的主机,每当在主机上启动一个容器的时候,proxy 都会自动检查该容器是否含有 VIRTUAL_HOST 这个环境变量,当检查到某个刚启动的容器含有这个环境变量时,便会自动修改配置文件,使得这个来自 环境变量描述的域名 的流量会被打在这个容器上。

那么,在启动容器的时候,只需要添加一个环境变量,比如:

hypo_blog:
  image: daocloud.io/cmss/hypo-blog:master-796038f
  privileged: false
  restart: always
  ports:
  - '4000'
  environment:
  - VIRTUAL_HOST=blog.ihypo.net

负载均衡

当有多个容器含有相同的 VIRTUAL_HOST 的时候,proxy 便会自动做好负载均衡,如果使用 daocloud 的话:

只需要在容器界面调整容器个数,proxy 会自动做好 load balance。

如果进入 proxy 容器中查看的话,也可以看到生成的配置文件:

upstream blog.ihypo.net {
		   # dao_hypo_blog_2
		   server 192.168.0.6:4000;
		   # dao_hypo_blog_1
		   server 192.168.0.4:4000;
}

其他

nginx-proxy 的使用非常简单,还支持多域名,多端口,https 等,可以参考镜像的 README

参考

https://github.com/jwilder/nginx-proxy

http://jasonwilder.com/blog/2014/03/25/automated-nginx-reverse-proxy-for-docker/

2016/08/19 12:28 pm posted in  Docker

为 nginx 配置 https

为了给全站加上https,需要先获得一个经过CA签名的证书,恰好startssl提供免费的证书,所以我就有了折腾https的机会。

首先,要在本地生成一个证书:

openssl req -newkey rsa:2048 -keyout yourname.key -out yourname.csr

一定要记好生成key的密码,在启动nginx的时候需要用到,执行完之后就获得了一个csr文件,把这个csr文件上传到startssl,就可以获得被startssl签名的证书:

然后把自己生成的key和在startssl,把这两个文件copy服务器上

cp 1_api.updev.cn_bundle.crt /usr/local/nginx/conf/server.crt
cp api.key /usr/local/nginx/conf/server.key

在nginx的server里配置:

ssl on;
ssl_certificate /usr/local/nginx/conf/server.crt;
ssl_certificate_key /usr/local/nginx/conf/server.key;

并把之前监听80端口改为监听443端口,之后重启nginx,这时会提示输入Enter PEM pass phrase,需要输入之前生成key文件时设置的密码,完成后,nginx重启成功。

这时便完成了https的配置。

然而,在重启nginx的时候却需要输入两次密码,比较蛋疼,而且使用daocloud或七牛的https时,带密码的key是不被支持的,所以我们可以生成一个不带密码的key,这样可以避免每次重启nginx都需要输入密码。而且也可以使用daocloud和七牛的https功能。

我们可以使用 openssl rsa -in server.key -out server.key.unsecure命令来生成一个不带密码的key,然后把server.key.unsecure按照之前的配置就可以了。

而使用daocloud的https时,需要提供startssl签名的crt文件(nginx版)和不带密码的key,这样就可以了。

2016/07/20 12:29 pm posted in  坏笔记不如好记性

一个微信红包问题

在V2上看到一个问题:
将 100 元的红包分为 10 份,每份最低 5 元最高 15 元,金额精确到 0.01 元。那么如果算法没问题的话,每个人的期望值自然是平均值,即 100 元 /10 人=10 元 /人。

说下我大体的思路,我们可以把总金额设定为sum,人数为n,那么,要满足这个条件可以:
5n<=sum<=15n
也就是说,如果随机一个金额的话:
5(n-1)<=sum-randNum<=15(n-1)
化简一下:
sum-15(n-1)<=randNum<=sum - 5(n-1)

def comp(sum=100, n=10): 
    result = [] 
    while n > 1: 
        min = sum - 15 * (n - 1) if sum - 15 * (n - 1) >= 5 else 5
        max = sum - 5 * (n-1) if sum - 5 * (n-1) <= 15 else 15 
        rand = random.uniform(min, max) 
        rand = round(rand, 2) 
        result.append(rand) 
        sum -= rand 
        n -= 1 
        result.append(round(sum, 2)) 
    return result 
2016/07/14 12:29 pm posted in  坏笔记不如好记性

flask如何优雅的处理异常

如果使用flask编写RESTful风格的程序,不可避免的就是异常的处理问题。flask默认的异常处理方式实在不敢恭维,如果想达到能友好的返回错误信息,自己还要多做不少工作。

什么算优雅的返回错误信息呢?举个例子,下面的代码是获取User token的一段代码,如果密码验证失败将会向前端反馈错误信息,而反馈是通过抛出异常:

def get_user_token(self, data):
  valid(get_token, data)

  with get_session() as session:
  if "username" in data:
      user = session.query(User).filter_by(user_name=data['username']).first()
  elif "email" in data:
      user = session.query(User).filter_by(email=data['email']).first()
  else:
    user = None
    if not user or not user.valid_passwd(data['password']):
      raise APIException("get_token_error", "can't get the token with a wrong user password", 400)

  expiration = data['expiration'] if "expiration" in data else 86400
  return {
    "token": user.generate_auth_token(expiration=expiration),
    "id": user.id,
  }

那么可以在上层逻辑捕获这个异常,并把异常的信息包装一下优雅的反馈给前端,比如这样处理:

try:
  get_user_token(data)
except APIExcepting as e:
  return json.dumps({
    "id": e.error_id,
    "message": e.message,
    "code": e.code,
  })

当然,这只是最简单的包装,而且是http状态码写在了response的body里,而最好应该直接返回相应状态码的response。

每个下层逻辑的异常由上层来处理是很常规的办法,但是有的异常是可以直接抛到最上层的,比如参数错误,密码错误等,这些异常差的只是一个友好的显示,如果有办法对这些异常自动封装一个友好的反馈显示,那么就省不少事情。

而利用flask是可以实现这一点的,flask有一个叫errorhandler的东西,它可以捕捉特定的异常,然后根据这个异常进行自定义操作。那么,我们就可以创建一个可以直接抛到最上层的异常,由errorhandler监听捕捉这个异常,把捕捉到的异常封装成友好的response反馈出去就可以了。

首先是写一个可以被抛到最上层的异常:

class APIException(Base):
    def __init__(self, error_id, message, code=500):
        super(Base, self).__init__()
        self.raw_message = message
        self.error_id = error_id
        self.code = code
        self.message = message

    def to_dict(self):
        result = {
            "id": self.error_id,
            "code": self.code,
            "message": self.message,
        }
        return result

这个异常使用起来也很简单raise APIException(error_id, message, status_code)就可以了,而to_dict方法可以构建一个符合REST风格的错误信息返回body。下面就差一个错误处理句柄来把APIEception的实例封装成友好的response了。

@app.errorhandler(APIException)
def handle_api_exception(error):
    from flask import jsonify
    response = jsonify(error.to_dict())
    response.status_code = error.code
    return response

这样就有了一个处理APIException的句柄,当有这个异常抛出的话,它便会返回一个友好的body,而且状态码也会是在raise时制定的状态码。

2016/05/15 12:29 pm posted in  Python 黑魔法

KMP算法next数组活用之前后缀问题

确切的说这是一篇解题报告(SDUTOJ 2784:http://acm.sdut.edu.cn/sdutoj/problem.php?action=showproblem&problemid=2784)。

前置技能 KMP字符串匹配算法: KMP字符串匹配算法

正如题目中描述:

你的任务是找出一个字串s,s既是String的前缀,又是String的后缀,并且s也出现在String的中间部分(既不是前缀,也不是后缀),s的长度越长越好。

很明显这是一道KMP算法周边题目,说是KMP算法是因为需要计算next数组,说是周边是因为这并不是一道匹配类型的题目。

正如《KMP字符串匹配算法》所说,next数组的目的是保存的匹配过的 S 串中还有多少直接等于 T 串的。但实际上,next中的数值的含义在不同的使用场景还有很多种理解。

比如求前后缀的题目中,我们就可以把next中的数组看做在next[i](next[i]!=0 and next[i] != -1)出,存在重复的字段String[0,next[i]]。

而对于这个题目,对于长度为len-1的字符串,那么next数组的长度为len。

如果在next数组中,如果next[len]为0,这可以说明不存在后缀和前缀相同的情况。如果next[len]不为0,这说明前缀和后缀相同,但是还不能说明中间存在这一循环体。

如果next[len]不为0,而且存在和next[len]一样的值,也就是说在在字符串中间出现了循环题,也就可以断定存在符合题目要求的字符串,而长度就是next[len](也就是后缀和前缀共同的长度)。

但如果串是一个循环串,比如像"papapapa"这种,那么next是递增的。因此不能用上段的方式判断十分满足题目要求。

因此需要判断出去最短循环节之后时候还是循环串。而除去最短循环串,便是String[0,next[len]]。因此,在该串的基础上进行判断,也就是判断next[next[i]]十分还是一个有效的值(非0和非-1),如果有效,说明依然存在字段重复。

因此这个题目就非常清楚了:只需要寻找next[i] == next[len]和判断 next[next[len]] != 0 且 next[next[len]] != -1。

把KMP相关算法封装,这个题目的核心代码就变成了:

/**
 * SDUTOJ 2748
 * Created by hypo on 15-11-15.
 */

class TestString{
    private String str;
    private int[] next;

    //初始化 顺便获得next数组
    public TestString(String str) {
        this.str = str;
        next = getNext();
    }

    //获取next数组
    private int[] getNext(){
        char[] chars = this.str.toCharArray();
        int len = chars.length;
        int[] next = new int[len + 1];
        int k = -1, i = 0;
        next[0] = -1;

        while(i < len){
            if(k == -1 || chars[k] == chars[i]){
                i++;
                k++;
                next[i] = k;
            }else{
                k = next[k];
            }
        }
        return next;
    }

    //获得结果 null表示不符合题目要求
    public String getAns(){

        int len = this.str.length();

        if(next[len] == 0){
            return null;
        }

        for(int i = 0;i < len;i++){
            if(this.next[i] == next[len]){
                return this.str.substring(0,next[len]);
            }
        }

        if (next[next[len]] != 0 && next[next[len]] != -1)
        {
            return this.str.substring(0,next[next[len]]);
        }

        return null;
    }
}
2015/11/17 01:22 am posted in  算法&数据结构