老齐教室

剖析Web技术栈(四)

作者:Leonardo Giordani

翻译:老齐

与本文相关书籍推荐:《跟老齐学Python:Django实战》


4 Web server

4.1 基本原理

我们给Web server的一般标签是:用于执行任务的软件,nginx和Apache是两个常用的web server,这两个开源项目目前在市场上处于领先地位,它们使用不同的技术方法,都实现了我们在上一节中讨论的所有特性(以及更多特性)。

4.2 实施

为了测试nginx,又要避免与操作系统中其他软件包冲突,我们可以使用Docker。Docker对于模拟多机环境很有用,对于实际的生产环境,也能选择Docker(例如,AWS ECS与Docker容器配合使用)。

即将运行的基本配置非常简单,一个容器将包含Flask代码并使用Gunicorn运行框架,而另一个容器将运行nginx。Gunicorn将在内部端口8000上提供HTTP,这个端口不会被Docker公开,因此无法从浏览器访问。但是nignx将公开端口80,这是传统的HTTP端口。

在文件wsgi.py的同一目录中,创建一个Dockerfile

1
2
3
4
5
6
7
8
FROM python:3.6

ADD app /app
ADD wsgi.py /

WORKDIR .
RUN pip install flask gunicorn
EXPOSE 8000

从Python Docker开始,添加app目录和wsgi.py文件,并安装Gunicorn,然后在同一目录中名为nginx.conf的文件中为nginx创建一个配置

1
2
3
4
5
6
7
8
server {
listen 80;
server_name localhost;

location / {
proxy_pass http://application:8000/;
}
}

这样就定义了一个服务器,它监听端口80,并将以/开头的所有URL连接到端口8000上名为application的服务器,该服务器是运行Gunicorn的容器。

最后,创建一个描述容器配置的文件docker compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: "3.7"
services:
application:
build:
context: .
dockerfile: Dockerfile
command: gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
expose:
- 8000

nginx:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8080:80
depends_on:
- application

如你所见,我们在nginx配置文件中提到的名称application不是一个魔法字符串,而是我们在Docker Compose配置中分配给Gunicorn容器的名称。

要创建这个基础设施,我们需要通过pip install Docker Compose在我们的虚拟环境中安装Docker Compose。我还用项目名创建了一个名为.env的文件。

1
COMPOSE_PROJECT_NAME=service

此时,你可以使用Docker Compose up -d运行Docker Compose。

1
2
3
4
$ docker-compose up -d
Creating network "service_default" with the default driver
Creating service_application_1 ... done
Creating service_nginx_1 ... done

如果一切正常,打开浏览器并访问localhost应该会显示Flask提供的HTML页面。

通过docker compose日志,我们可以检查服务正在做什么。我们可以在名为application的服务日志中识别Gunicorn的输出。

1
2
3
4
5
6
7
8
$ docker-compose logs application
Attaching to service_application_1
application_1 | [2020-02-14 08:35:42 +0000] [1] [INFO] Starting gunicorn 20.0.4
application_1 | [2020-02-14 08:35:42 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
application_1 | [2020-02-14 08:35:42 +0000] [1] [INFO] Using worker: sync
application_1 | [2020-02-14 08:35:42 +0000] [8] [INFO] Booting worker with pid: 8
application_1 | [2020-02-14 08:35:42 +0000] [9] [INFO] Booting worker with pid: 9
application_1 | [2020-02-14 08:35:42 +0000] [10] [INFO] Booting worker with pid: 10

现在我们最感兴趣的是名为nginx的服务,所以我们使用docker compose logs -f nginx实时跟踪日志。刷新你用浏览器访问的localhost页面,容器应该输出如下内容:

1
2
3
$ docker-compose logs -f nginx
Attaching to service_nginx_1
nginx_1 | 192.168.192.1 - - [14/Feb/2020:08:42:20 +0000] "GET / HTTP/1.1" 200 13 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0" "-"

这是nginx的标准日志格式。它显示客户机的IP地址(192.168.192.1)、连接时间戳、HTTP请求和响应状态代码(200),以及客户端的其他信息。

现在让我们增加服务的数量,以查看负载平衡机制的作用。为此,我们首先需要更改nginx的日志格式,以显示对请求做出响应的机器的IP地址。更改“nginx.conf”文件,添加 log_formataccess_log选项。

1
2
3
4
5
6
7
8
9
10
11
12
log_format upstreamlog '[$time_local] $host to: $upstream_addr: $request $status';

server {
listen 80;
server_name localhost;

location / {
proxy_pass http://application:8000;
}

access_log /var/log/nginx/access.log upstreamlog;
}

变量$upstream_addr是nginx代理的服务器的IP地址。现在运行docker compose down停止所有容器,然后通过docker compose up -d --scale application=3重新启动。

1
2
3
4
5
6
7
8
9
10
11
12
$ docker-compose down
Stopping service_nginx_1 ... done
Stopping service_application_1 ... done
Removing service_nginx_1 ... done
Removing service_application_1 ... done
Removing network service_default
$ docker-compose up -d --scale application=3
Creating network "service_default" with the default driver
Creating service_application_1 ... done
Creating service_application_2 ... done
Creating service_application_3 ... done
Creating service_nginx_1 ... done

如你所见,Docker Compose为application启动了3个容器,如果你打开日期,可以看到如下内容。

1
2
3
$ docker-compose logs -f nginx
Attaching to service_nginx_1
nginx_1 | [14/Feb/2020:09:00:16 +0000] localhost to: 192.168.240.4:8000: GET / HTTP/1.1 200

你可以在这里找到to:192.168.240.4:8000,这是其中一个应用所在容器的IP地址。如果你现在多次访问该页面,应该会注意到上游地址的更改,例如:

1
2
3
4
5
6
7
$ docker-compose logs -f nginx
Attaching to service_nginx_1
nginx_1 | [14/Feb/2020:09:00:16 +0000] localhost to: 192.168.240.4:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:09:00:17 +0000] localhost to: 192.168.240.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:09:00:17 +0000] localhost to: 192.168.240.3:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:09:00:17 +0000] localhost to: 192.168.240.4:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:09:00:17 +0000] localhost to: 192.168.240.2:8000: GET / HTTP/1.1 200

这表明nginx正在执行负载平衡,但说实话,这是通过Docker的DNS进行的,而不是通过web服务器执行的显式操作。我们可以通过访问nginx容器并运行dig application来验证这一点(你需要运行apt updateapt install dnsutils来安装dig)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@99c2f348140e:/# dig application

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> application
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7221
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;application. IN A

;; ANSWER SECTION:
application. 600 IN A 192.168.240.2
application. 600 IN A 192.168.240.4
application. 600 IN A 192.168.240.3

;; Query time: 1 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Fri Feb 14 09:57:24 UTC 2020
;; MSG SIZE rcvd: 110

要查看nginx执行的负载平衡,我们可以显式地定义两个服务并为它们分配不同的权重。运行docker compose down并将nginx配置更改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upstream app {
server application1:8000 weight=3;
server application2:8000;
}

log_format upstreamlog '[$time_local] $host to: $upstream_addr: $request $status';

server {
listen 80;
server_name localhost;

location / {
proxy_pass http://app;
}

access_log /var/log/nginx/access.log upstreamlog;
}

我们在这里定义了一个upstream结构。它列出了两种不同的服务:application1application2。其中第一种服务的权重为3。这意味着:每4个请求中,有3个请求将被路由到第一种服务,1个被路由到第二种服务。现在nginx不仅仅依赖于DNS,而是有意识地在两种不同的服务之间进行选择。

我们在Docker Compose配置文件中相应地定义服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
version: "3"
services:
application1:
build:
context: .
dockerfile: Dockerfile
command: gunicorn --workers 6 --bind 0.0.0.0:8000 wsgi
expose:
- 8000

application2:
build:
context: .
dockerfile: Dockerfile
command: gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
expose:
- 8000

nginx:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 80:80
depends_on:
- application1
- application2

我基本上重复了application的定义,但是第一种服务现在运行6个工作线,只是为了显示两者之间可能的区别。现在运行docker-compose up -ddocker-compose logs -f nginx。如果多次刷新浏览器上的页面,你将看到如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker-compose logs -f nginx
Attaching to service_nginx_1
nginx_1 | [14/Feb/2020:11:03:25 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:25 +0000] localhost to: 172.18.0.2:8000: GET /favicon.ico HTTP/1.1 404
nginx_1 | [14/Feb/2020:11:03:30 +0000] localhost to: 172.18.0.3:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:31 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:32 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:33 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:33 +0000] localhost to: 172.18.0.3:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:34 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:34 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:35 +0000] localhost to: 172.18.0.2:8000: GET / HTTP/1.1 200
nginx_1 | [14/Feb/2020:11:03:35 +0000] localhost to: 172.18.0.3:8000: GET / HTTP/1.1 200

你可以清楚地看到172.18.0.2(application1)172.18.0.3(application2)之间的负载平衡。

我不会在这里展示反向代理或HTTPS的例子,以免这篇文章过长,你可以在下一节中找到有关这类内容的资源。

4.3 参考资料

这些资源提供了关于本节讨论的主题的更详细的信息。

  • Docker Compose official documentation
  • nginx documentation: in particular the sections about log_format and upstream directives
  • How to configure logging in nginx
  • How to configure load balancing in nginx
  • Setting up an HTTPS Server with nginx and how to created self-signed certificates
  • How to create a reverse proxy with nginx, the documentation of the location directive and some insights on the location choosing algorithms (one of the most complex parts of nginx)
  • The source code of this example is available here

4.4问题

现在,我们可以说任务完成了。我们在多线程Web框架前面加了一个用于生产的Web服务器,我们可以专注于编写Python代码,而不是处理HTTP头信息。

使用Web服务器允许我们扩展基础设施,只需在其后面添加新实例,而不会中断服务。HTTP并发服务器运行框架的多个实例,框架本身使HTTP抽象化,将其映射到我们的高级语言。

云基础设施

在互联网的早期,公司都要有自己的服务器,而系统管理员则直接在光秃秃的操作系统上运行所有东西,不用说,这是复杂、昂贵和容易失败的。

现在“云”是一个好东西,很多网站都部署到云上,而且也有很多组件供我们使用。

(完毕)

阅读链接

  • 剖析Web技术栈(一)
  • 剖析Web技术栈(二)
  • 剖析Web技术栈(三)

原文链接:https://www.thedigitalcatonline.com/blog/2020/02/16/dissecting-a-web-stack/

搜索技术问答的公众号:老齐教室

在公众号中回复:老齐,可查看所有文章、书籍、课程。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能