web-server

uwsgi 的使用和性能优化配置

更新:建议使用 gunicorn

假设我们编写了如下的 flask 应用,要用 uwsgi 部署,希望性能越高越好,那么下面是一份还说得过去的配置。

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "world"

if __name__ == "__main__":
    app.run()

对应的 uwsgi 配置

[uwsgi]
wsgi-file=app.py  # 应用的主文件
callable=app  # 应用中的 flask 实例
chdir=/opt/app  # chdir 到给定目录
env= XXX=XXX  # 额外的环境变量

# 以下三者任选其一
http=0.0.0.0:5000  # 如果直接暴露 uwsgi 的话用这个
http-socekt=0.0.0.0:5001  # 如果用 nginx 反向代理的话,用这个
socket=:3031  # 在 3031 使用 uwsgi 协议,nginx 中使用 uwsgi_pass 更高效

chmod-socket = 664

pidfile=xxx  # pid 文件路径
venv=xxx  # 虚拟环境路径
logto = /var/log/www.log

# 并发设置
workers = 2  # 一般为 CPU 核数 * 2
threads = 2  # 线程比进程开销更小一点。如果没有使用 threads 那么 thread 直接不工作的,必须使用 enable_threads。
max-requests = 100000  # 处理过多少个请求后重启进程,目的是防止内存泄露
master = true  # 使用 max-requests 必须采用这个选项
listen = 65536  # 每个进程排队的请求数量,默认为 100 太小了。并发数 = procsses * threads * listen
buffer-size = 65536  # header 的 buffer 大小,默认是 4 k
thunder-lock = true  # 避免惊群效应
uid=www-data
gid=www-data
harakiri=30  # 所有进程在 30s 没有响应后傻屌
log-slow=3000  # 记录满于 3000 毫秒的请求
# lazy-apps  # 不使用 prefork,而是在需要时才启动进程

# 监控设置
stats = 127.0.0.1:9191  # 可以使用 uwsgi top 监控
python-autoreload=1  # 自动重载,开发时非常方便

# 静态文件
check-static = /var/static  # 尝试从该目录下加载静态文件
static-map = /static=/var/static  # 把对应目录映射
route = /static/(.*)\.png static:/var/www/images/pngs/$1/highres.png  # 使用高级路由模式
offload-threads = 10  # 让 uwsgi 启动额外的进程处理

参考

  1. https://blog.zengrong.net/post/2568.html
  2. https://stackoverflow.com/questions/34255044/why-use-uwsgi-max-requests-option/34255744
  3. https://blog.csdn.net/apple9005/article/details/76232852、
  4. https://mhl.xyz/Python/uwsgi.html
  5. https://stackoverflow.com/questions/34824487/when-is-thunder-lock-beneficial

使用 caddy 部署网站

caddy 支持自动的 SSL 证书获取,这个非常方便,个人站的话,没必要用 nginx 了。Caddy 2 是最新的版本,并且和 1 不太兼容,本文讨论的是 Caddy 2.

caddy 的配置可以用的自己的语法:Caddyfile(注意必须大写), 不过新版本都支持用 json 了。相比于 nginx 的功能丰富但是又显得有点复杂的配置文件来说,caddy 的配置比较少,也就比较简单。

使用 Caddy 来部署一个 PHP 应用

Caddyfile 是分区的,每个地址对应一个区,可以用大括号包围起来,还有一个全局的配置区。

# 全局配置
{
  email [email protected]
}

yifei.me {
  encode gzip
  root * /var/www/html
  php_fastcgi unix//run/php/php-fpm.sock
  file_server
}

www.yifei.me {
  redir https://yifei.me{uri}
}

安装 PHP

sudo apt -y install php-fpm php-mysql php-xml

增加一个反向代理的 Python/Node/Java 应用

假设我们现在在端口 5002 部署了一个另外一个应用,然后想通过 super-cool.yifei.me 来访问,这时候只需要在 Caddyfile 中增加如下配置就可以了。

super-cool.yifei.me {
  reverse_proxy localhost:5002
}

参考

  1. Caddy Official Docs

如何使用 letsencrypt

letsencrypt 现在终于支持通配符证书了。

certbot 比较坑爹的一点是 renew 时候使用的是和创建证书相同的参数,而且不能更改,也就是最好在创建证书的时候就选择使用 webroot 的方式。

install certbot

see ~/.dotfiles/installs/install_certbot.sh

create new cert

sudo certbot certonly –webroot -w /opt/spider/nginx/html/ -d shujutuzi.com -d www.shujutuzi.com

sudo certbot certonly –standalone –agree-tos –email [email protected] –domain g.yifei.me –preferred-challenges http –non-interactive

the cert is placed at /etc/letsencrypt/live/shujutuzi.com/

there will be four certs:

  • cert.pem: server certificate only.
  • chain.pem: root and intermediate certificates only.
  • fullchain.pem: combination of server, root and intermediate certificates (replaces cert.pem and chain.pem).
  • privkey.pem: private key (do not share this with anyone!).

install the cert

https://gist.github.com/cecilemuller/a26737699a7e70a7093d4dc115915de8

auto renew

create a cron job to run renew peroidcally

cerbot renew –pre-hook “/opt/nginx/sbin/nginx -s stop” –post-hook “/opt/nginx/sbin/nginx -s start” –quiet

optionally, you could generate a Strong Diffie-Hellman Group

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Third, change you defautl server settings:

server {
    listen 443 ssl;
    server_name example.com www.example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

// optional

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_dhparam /etc/ssl/certs/dhparam.pem;
        ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:50m;
        ssl_stapling on;
        ssl_stapling_verify on;
        add_header Strict-Transport-Security max-age=15768000;
}

Side Notes:

what is a pem file?

pem container format, may contain one or many certs, short for Privacy Enhanced Main
key just the private key file of pem format
cert, cer, crt just pem file with different extendsion, used on windows

去掉 SSL 证书的密码

openssl rsa -in futurestudiowithpass.key -out futurestudio.key

生成自签名证书

参考

  1. https://futurestud.io/tutorials/how-to-remove-pem-password-from-ssl-certificate
  2. https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl

Python 的 wsgi 协议

wsgi 协议

值得注意的是,wsgi 实际上定义了一个同步的模型,也就是每一个客户请求会调用一个同步的函数,这样也就无法发挥异步的特性。

两个最简单的例子

其中实现 simple_app 函数也就是实现了 wsgi 协议。需要注意的有一下三点:

  1. environ 字典中包含的变量
  2. start_response 的参数
  3. simple_app 的调用次序和返回值
HELLO_WORLD = b"Hello world!\n"

def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = "200 OK"
    response_headers = [("Content-type", "text/plain")]
    start_response(status, response_headers)
    return [HELLO_WORLD]

class AppClass:
    """Produce the same output, but using a class
    (Note: "AppClass" is the "application" here, so calling it
    returns an instance of "AppClass", which is then the iterable
    return value of the "application callable" as required by
    the spec.
    If we wanted to use *instances* of "AppClass" as application
    objects instead, we would have to implement a "__call__"
    method, which would be invoked to execute the application,
    and we would need to create an instance for use by the
    server or gateway.
    """
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response
    def __iter__(self):
        status = "200 OK"
        response_headers = [("Content-type", "text/plain")]
        self.start(status, response_headers)
        yield HELLO_WORLD

而对于 server/gateway 来说,每接收到一个 http 客户端,都会调用一次这个 application callable

import os, sys
enc, esc = sys.getfilesystemencoding(), "surrogateescape"
def unicode_to_wsgi(u):
    # Convert an environment variable to a WSGI "bytes-as-unicode" string
    return u.encode(enc, esc).decode("iso-8859-1")
def wsgi_to_bytes(s):
    return s.encode("iso-8859-1")
def run_with_cgi(application):
    environ = {k: unicode_to_wsgi(v) for k,v in os.environ.items()}
    environ["wsgi.input"]        = sys.stdin.buffer
    environ["wsgi.errors"]       = sys.stderr
    environ["wsgi.version"]      = (1, 0)
    environ["wsgi.multithread"]  = False
    environ["wsgi.multiprocess"] = True
    environ["wsgi.run_once"]     = True
if environ.get("HTTPS", "off") in ("on", "1"):
        environ["wsgi.url_scheme"] = "https"
    else:
        environ["wsgi.url_scheme"] = "http"
headers_set = []
    headers_sent = []
def write(data):
        out = sys.stdout.buffer
if not headers_set:
             raise AssertionError("write() before start_response()")
elif not headers_sent:
             # Before the first output, send the stored headers
             status, response_headers = headers_sent[:] = headers_set
             out.write(wsgi_to_bytes("Status: %s\r\n" % status))
             for header in response_headers:
                 out.write(wsgi_to_bytes("%s: %s\r\n" % header))
             out.write(wsgi_to_bytes("\r\n"))
out.write(data)
        out.flush()
def start_response(status, response_headers, exc_info=None):
        if exc_info:
            try:
                if headers_sent:
                    # Re-raise original exception if headers sent
                    raise exc_info[1].with_traceback(exc_info[2])
            finally:
                exc_info = None     # avoid dangling circular ref
        elif headers_set:
            raise AssertionError("Headers already set!")
headers_set[:] = [status, response_headers]
# Note: error checking on the headers should happen here,
        # *after* the headers are set.  That way, if an error
        # occurs, start_response can only be re-called with
        # exc_info set.
return write
result = application(environ, start_response)
    try:
        for data in result:
            if data:    # don"t send headers until body appears
                write(data)
        if not headers_sent:
            write("")   # send headers now if body was empty
    finally:
        if hasattr(result, "close"):
            result.close()

参考资料

  1. https://bottlepy.org/docs/dev/async.html
  2. http://uwsgi-docs-cn.readthedocs.io/zh_CN/latest/WSGIquickstart.html
  3. https://www.digitalocean.com/community/tutorials/how-to-deploy-python-wsgi-applications-using-uwsgi-web-server-with-nginx