【Docker进阶】基于Docker部署Web应用

以下文章为转载,该篇讲述如何使用Docker技术在线上单机模式下部署一个Web应用,很经典。

在这里插入图片描述

在这里插入图片描述

一、web初体验

  首先,什么是Docker?根据官网描述,Docker是一个软件/容器平台,使用了虚拟化技术(cgroups,namespaces)来实现操作系统的资源隔离和限制。

  对于开发人员来说,容器技术为应用的部署提供了沙盒环境,我们可以在独立的容器运行和管理应用程序进程,Docker提供的抽象层使得开发人员之间可以保持开发环境相对的一致,避免了冲突。

1、Welcome to nginx!

下面体验下nginx镜像运行容器并作端口转发:


使用下面的shell命令安装Docker

$ curl -sSL https://get.docker.com/ | sh

安装成功后,使用下面的命令应该能显示Docker的版本信息,说明Docker已经被安装了

$ docker -v
Docker version 17.04.0-ce, build 4845c56

接着我们使用Docker创建一个nginx的容器:

$ docker run -d --name=web -p 80:80 nginx:latest

这条命令表示Docker基于nginx:alpine这个Docker镜像,创建一个名称为web的容器,并把容器内部的80端口与宿主机上的80端口做映射,使得通过宿主机80端口的流量转发到容器内部的80端口上。

使用docker ps命令,可以列出正在运行的容器,可以看到,刚才基于nginx镜像创建的容器已经处于运行状态了:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                         NAMES
a89d281829f9        nginx:latest        "nginx -g 'daemon ..."   8 minutes ago       Up 8 minutes        0.0.0.0:80->80/tcp, 443/tcp   web

现在访问宿主机地址的80端口,看到nginx的欢迎页面。
在这里插入图片描述

Docker容器本质上是一个运行的进程以及它需要的一些依赖,而Docker镜像则是定义这个容器的一个"模版"。

使用docker images能看到目前的镜像:

$ docker images
REPOSITORY
nginx                 latest              bedece1f06cc        10 minutes ago         54.3MB

了解到这个事实之后,我们使用下面的命令进入刚才创建的容器内部

$ docker exec -i -t web bash

现在处于的是容器内部的根文件系统(rootfs),它跟宿主机以及其他容器的环境是隔离开的,看起来这个容器就是一个独立的操作系统环境一样。使用ps命令可以看到容器内正在运行的进程:

$ ps -l
PID   USER     TIME   COMMAND
    1 root       0:00 nginx: master process nginx -g daemon off;
    5 nginx      0:00 nginx: worker process
   23 root       0:00 ps -l

使用exit命令可以从容器中退出,回到宿主机的环境:

$ exit

2、通信与端口转发

使用docker inspect命令我们可以看到关于这个容器的更多详细信息:

$ docker inspect web

结果是用json格式表示的容器相关信息,拉到下面的Networks一列可以看到这个容器的网络环境信息:

"Networks": {
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "716496983db3eef8257dae57f4e0084c8242d8f5277da8a35b5ce265ccb4b3e5",
    "EndpointID": "e3ab409f152e87594fe2f07e32cea2577983b352f3bba8cc99de6092682d6774",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02"
  }
}

  内容显示了这个容器使用了bridge桥接的方式通信,它是docker容器默认使用的网络驱动(使用docker network ls可以看到所有的驱动),从上面可以看到这个容器的IP地址为172.17.0.2,网关地址为172.17.0.1。

现在回想刚才的例子,访问宿主机的80端口,宿主机是怎么跟容器打交道,实现转发通信的呢?

要解决这个问题,我们首先要知道,docker在启动的时候会在宿主机上创建一块名为docker0的网卡,可以用ifconfig查看:

$ ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:a4:e4:10:80
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1414 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1778 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:181802 (181.8 KB)  TX bytes:142440 (142.4 KB)

  这个网卡的ip地址为172.17.0.1,看到这里你是否想起了刚才我们创建的容器使用的网关地址即为172.17.0.1?我们是否可以大胆地猜测,docker容器就是通过这张名为docker0的网卡进行通信呢?确实如此,以单机环境为例,Docker Daemon启动时会创建一块名为docker0的虚拟网卡,在Docker初始化时系统会分配一个IP地址绑定在这个网卡上,docker0的角色就是一个宿主机与容器间的网桥,作为一个二层交换机,负责数据包的转发。当使用docker创建一个容器时,如果使用了bridge模式,docker会创建一个vet对,一端绑定到docker0上,而另一端则作为容器的eth0虚拟网卡。

使用ifconfig也可以看到这个veth对的存在:

veth8231e5b Link encap:Ethernet  HWaddr 16:e8:f2:1d:e1:4d
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:29 errors:0 dropped:0 overruns:0 frame:0
          TX packets:29 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:4033 (4.0 KB)  TX bytes:3741 (3.7 KB)

我找了一张图,可以很好地表示veth对的存在方式:
在这里插入图片描述
  而真正实现端口转发的魔法的是nat规则。如果容器使用-p指定映射的端口时,docker会通过iptables创建一条nat规则,把宿主机打到映射端口的数据包通过转发到docker0的网关,docker0再通过广播找到对应ip的目标容器,把数据包转发到容器的端口上。反过来,如果docker要跟外部的网络进行通信,也是通过docker0和iptables的nat进行转发,再由宿主机的物理网卡进行处理,使得外部可以不知道容器的存在。

使用iptables -t nat命令可以看到添加的nat规则:

$ iptables -t nat -xvL
Chain PREROUTING (policy ACCEPT 376 packets, 21292 bytes)
    pkts      bytes target     prot opt in     out     source               destination
   78606  4609864 DOCKER     all  --  any    any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 376 packets, 21292 bytes)
    pkts      bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 286 packets, 21190 bytes)
    pkts      bytes target     prot opt in     out     source               destination
       0        0 DOCKER     all  --  any    any     anywhere            !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 290 packets, 21430 bytes)
    pkts      bytes target     prot opt in     out     source               destination
       0        0 MASQUERADE  all  --  any    !docker0  172.17.0.0/16        anywhere
       0        0 MASQUERADE  tcp  --  any    any     172.17.0.2           172.17.0.2           tcp dpt:http

Chain DOCKER (2 references)
    pkts      bytes target     prot opt in     out     source               destination
       0        0 RETURN     all  --  docker0 any     anywhere             anywhere
       4      240 DNAT       tcp  --  !docker0 any     anywhere             anywhere             tcp dpt:http to:172.17.0.2:80

从上面的最后一行可以观察到流量转发到了172.17.0.2的80端口上,这个地址就是刚才创建容器使用的IP地址。

现在知道在刚才的例子中宿主机是怎么跟容器通信了吧,那么容器跟容器之间通信呢?类似地,也是通过这个docker0交换机进行广播和转发。

在这里插入图片描述在这里插入图片描述

关于容器的通信,具体跳:Docker网络模式与容器间的通信


二、实例–统计该应用的访问次数

开始进入正题,写一个Web应用。

  一般情况下,如果你要编写一个Web项目,你会做什么呢?反正对于我来说,如果我要写一个python web项目的话,我会先用virtualenv建立一个隔离环境,进入环境内,使用pip安装Django,最后用django-admin startproject创建一个项目,搞定。

  但是如果用容器化的方式思考,我们大可直接借助于容器的隔离性优势,更好地控制环境和版本的隔离,通常情况下你都不需要再关心用pyenv,virtualenv这种方式来初始化python环境的了,一切交给docker来完成吧。

甚至把安装django这个步骤也省了,直接通过一句命令来拉取一个安装了django的Python环境的镜像。

1、雏形

$ docker pull django

现在通过这个镜像运行django容器,同时进入容器Shell环境:

$ docker run -it --name=app -p 8080:8000 django bash

在/usr/src这个目录下新建一个app目录,然后用django-admin命令新建一个django项目:

$ cd /usr/src
$ mkdir app
$ cd app
$ django-admin startproject django_app

然后使用下面的命令,在容器8000端口上运行这个应用:

$ python manage.py makemigartions
$ python manage.py migrate
$ python manage.py runserver 0.0.0.0:8000 &

由于之前已经将容器的8000端口与宿主机的8080端口做了映射,因此我们可以通过访问宿主机的8080端口访问这个应用。

$ exit #退出容器
$ curl -L http://127.0.0.1:8080/

注意了,对这个容器的所有修改仅仅只对这个容器有效,不会影响到镜像和基于镜像创建的其他容器,当这个容器被销毁之后,所做的修改也就随之销毁。

下面新建一个应用ping,作用是统计该应用的访问次数,每次访问页面将次数累加1,返回响应次数给前端页面,并把访问次数存到数据库中。

使用redis作为ping的数据库,与之前类似,拉取redis的镜像,运行容器。

$ docker pull redis
$ docker run -d --name=redis -p 6379 redis

由于django容器需要与redis容器通信的话首先要知道它的ip地址,但是像刚才那样,每次都手工获取容器的ip地址显然是一件繁琐的事情,于是我们需要修改容器的启动方式,加入—link参数,建立django容器与redis容器之间的联系。

删除掉之前的容器,现在重新修改django容器的启动方式:

$ docker run -it --name=app -p 8080:8000 -v /code:/usr/src/app --link=redis:db django bash

这次加入了两个参数:

-v /code:/usr/src/app 表示把宿主机上的/code目录挂载到容器内的/usr/src/app目录,可以通过直接管理宿主机上的挂载目录来管理容器内部的挂载目录。

–link=redis:db 表示把redis容器以db别名与该容器建立关系,在该容器内以db作为主机名表示了redis容器的主机地址。

现在进入到django容器,通过ping命令确认django容器能访问到redis容器:

$ ping db
PING db (192.168.32.12): 56 data bytes
64 bytes from 192.168.32.12: icmp_seq=0 ttl=64 time=0.463 ms
64 bytes from 192.168.32.12: icmp_seq=1 ttl=64 time=0.086 ms

像之前一样,建立一个项目,接着使用django-admin新建一个应用:

$ django-admin startapp ping

编写ping的视图,添加到项目的urls.py:

from django.shortcuts import render
from django.http import HttpResponse
import redis

rds = redis.StrictRedis('db', 6379)

def ping(request):
    rds.incr('count', 1)
    cnt = rds.get('count')
    cnt = b'0' if cnt is None else cnt
    return HttpResponse(cnt.decode())

''' urls.py
from pingtest.views import ping

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', ping)
]
'''

别忘了安装redis的python驱动:

$ pip install redis

运行django应用,访问应用的根地址,如无意外便能看到随着页面刷新累加的数字。

$ curl http://127.0.0.1/
3243
$ curl http://127.0.0.1/
3244

2、Dockerfile的应用

  你或许会想,每次创建一个容器都要手工做这么多操作,好麻烦,有没有更方便的方式地来构建容器,不需要做那么多额外的环境和依赖安装呢?

  仔细一想,其实我们创建的容器都是建立在基础镜像上的,那么有没有办法,把修改好的容器作为基础镜像,以后需要创建容器的时候都使用这个新的镜像呢?当然可以,使用docker commit [CONTAINER]的方式可以将改动的容器导出为一个Docker镜像。

  当然,更灵活的方式是编写一个Dockerfile来构建镜像,正如Docker镜像是定义Docker容器的模版,Dockerfile则是定义Docker镜像的文件。下面我们来编写一个Dockerfile,以定义出刚才我们进行改动后的容器导出的镜像。

下面加入supervisor和gunicorn以更好地监控和部署应用进程:

gunicorn的配置文件:

import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'meinheld.gmeinheld.MeinheldWorker'
user = 'root'
loglevel = 'warning'
reload = True
accesslog = '-' #to supervisord's stdout
#errorlog

supervisord的配置文件:

[supervisord]
nodaemon=true
logfile_maxbytes=10MB
loglevel=debug

[program:ping]
command=gunicorn -c /etc/gunicorn_conf.py django_app.wsgi:application
directory=/usr/src/app
user=root
process_name=root
numprocs=1
autostart=true
autorestart=true
redirect_stderr=True

  以supervisord作为web应用容器的启动进程,supervisord来管理gunicorn的进程。这里说明一下的是,由于使用docker logs命令来打印容器的日志时默认是从启动进程(supervisord)的stdout和stderr里收集的,而gunicorn又作为supervisord的派生进程存在,因此要正确配置gunicorn和supervisord的日志选项,才能从docker logs中看到有用的信息。

把上面所做的修改混杂在一起,终于得出了第一个Dockerfile:

FROM django:latest

COPY ./app /usr/src/app

COPY supervisord.conf /etc/supervisord.conf

COPY gunicorn_conf.py /etc/gunicorn_conf.py

RUN apt-get update && \
    apt-get install -y supervisor && \
    rm -rf /var/lib/apt/lists/*

RUN pip install meinheld && \
    pip install gunicorn && \
    cd /usr/src/app && \
    pip install -r requirement.txt && \
    python manage.py makemigrations && \
    python manage.py migrate

WORKDIR /usr/src/app

CMD supervisord -c /etc/supervisord.conf

上面的Dockerfile的说明如下:

FROM指令制定了该镜像的基础镜像为django:latest。

三行COPY指令分别将宿主机的代码文件和配置文件复制到容器环境的对应位置。

接着两行RUN指令,一条指令安装supervisor,另一条指令安装python的依赖以及初始化django应用。

最后运行supervisord,配置为刚才复制的supervisor的配置文件。

上面每一条指令都会由docker容器执行然后提交为一个镜像,叠在原来的镜像层的上方,最后得到一个拥有许多镜像层叠加的最终镜像。

完成Dockerfile的编写后,只需要用docker build命令就能构建出一个新的镜像:

docker build -t test/app .

接着就可以根据这个镜像来创建和运行容器了:

$ docker run -d --name=app -p 8080:8000 -v /code:/usr/src/app --link=redis:db test/app

目前为止,项目的应用结构图如下:

在这里插入图片描述

3、高可用

现在,如果Redis这个节点出现故障的话会怎么样?(高可用

答案是,整个服务都会不可用了,更糟糕的是,数据备份和恢复同步成为了更棘手的问题。

很明显,我们不能只依赖一个节点,还要通过建立主从节点防止数据的丢失。再创建两个redis容器,通过slaveof指令为Redis建立两个副本。

$ docker run -d --name=redis_slave_1 -p 6380:6379 --link=redis:master redis redis-server --slaveof master 6379
$ docker run -d --name=redis_slave_2 -p 6381:6379 --link=redis:master redis redis-server --slaveof master 6379

现在写入到Redis主节点的数据都会在从节点上备份一份数据。
在这里插入图片描述

现在看起来好多了,然而当Redis master挂掉之后,服务仍然会变的不可用,所以当master宕机时还需要通过选举的方式把新的master节点推上去(故障迁移),Redis Sentinel正是一个合适的方式,我们建立Sentinel集群来监控Redis master节点,当master节点不可用了,再由Sentinel集群根据投票选举出slave节点作为新的master。

下面为Sentinel编写Dockerfile,在redis镜像的基础上作改动:

FROM redis:latest

COPY run-sentinel.sh /run-sentinel.sh

COPY sentinel.conf /etc/sentinel.conf

RUN chmod +x /run-sentinel.sh

ENTRYPOINT ["/run-sentinel.sh"]

Sentinel的配置文件:

port 26379

dir /tmp

sentinel monitor master redis-master 6379 2

sentinel down-after-milliseconds master 30000

sentinel parallel-syncs master 1

sentinel failover-timeout master 180000

run-sentinel.sh:

#!/bin/bash

exec redis-server /etc/sentinel.conf --sentinel

构建出Sentinel的镜像文件,容器运行的方式类似于redis:

$ docker run -d --name=sentinel_1 --link=redis:redis-master [build_sentinel_image]
$ docker run -d --name=sentinel_2 --link=redis:redis-master [build_sentinel_image]
$ docker run -d --name=sentinel_3 --link=redis:redis-master [build_sentinel_image]

这下Sentinel的容器也搭建起来了,应用的结构图如下:
在这里插入图片描述

简单验证一下当redis主节点挂掉后sentinel怎么处理:

$ docker pause redis-master
$ docker logs -f --tail=100 sentinel_1
1:X 17 Apr 14:32:51.633 # +sdown master master 192.168.32.12 6379
1:X 17 Apr 14:32:52.006 # +new-epoch 1
1:X 17 Apr 14:32:52.007 # +vote-for-leader 35ff9e1686f3425f4cbe5680a741e366a1863cae 1
1:X 17 Apr 14:32:52.711 # +odown master master 192.168.32.12 6379 #quorum 3/2
1:X 17 Apr 14:32:52.711 # Next failover delay: I will not start a failover before Mon Apr 17 14:33:02 2017
1:X 17 Apr 14:32:53.221 # +config-update-from sentinel 35ff9e1686f3425f4cbe5680a741e366a1863cae 192.168.32.7 26379 @ master 192.168.32.12 6379
1:X 17 Apr 14:32:53.221 # +switch-master master 192.168.32.12 6379 192.168.32.5 6379
1:X 17 Apr 14:32:53.221 * +slave slave 192.168.32.6:6379 192.168.32.6 6379 @ master 192.168.32.5 6379
1:X 17 Apr 14:32:53.221 * +slave slave 192.168.32.12:6379 192.168.32.12 6379 @ master 192.168.32.5 6379
$ docker exec -it sentinel_1 redis-cli -p 26379 info Sentinel


# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=master,status=ok,address=192.168.32.5:6379,slaves=2,sentinels=3

修改代码用Sentinel获取redis实例:

sentinel = Sentinel([('sentinel', 26379)])
rds = sentinel.master_for('master')

4、负载均衡

下面再来考虑这种情况:
在这里插入图片描述
假设我们对django_app容器进行伸缩,扩展出三个一模一样的django应用容器,这时候怎么办,该访问哪个?显然,这时候需要一个负载均衡的工具作为web应用的前端,做反向代理。

nginx是一个非常流行的web服务器,用它完成这个当然没问题,这里不说了。

下面说一说个人尝试过的两种选择:

LVS(Linux Virtual Server)作为最外层的服务,负责对系统到来的请求做负载均衡,转发到后端的服务器(Real Server)上,DR(Direct Route)算法是指对请求报文的数据链路层进行修改mac地址的方式,转发到后端的一台服务器上,后端的服务器集群只需要配置和负载均衡服务器一样的虚拟IP(VIP),请求就会落到对应mac地址的服务器上,跟NAT模式相比,DR模式不需要修改目的IP地址,因此在返回响应时,服务器可以直接将报文发送给客户端,而无须转发回负载均衡服务器,因此这种模式也叫做三角传输模式。

在这里插入图片描述
Haproxy是一个基于TCP/HTTP的负载均衡工具,在负载均衡上有许多精细的控制。下面简单地使用Haproxy来完成上面的负载均衡和转发。

首先把haproxy的官方镜像下载下来:

$ docker pull haproxy

这类的镜像的Dockerfile都可以在Docker Hub上找到。

这次同样选择编写Dockerfile的方式构建自定的haproxy镜像:

FROM haproxy:latest

COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

暂时只需要把配置文件复制到配置目录就可以了,因为通过看haproxy的Dockerfile可以看到最后有这么一行,于是乎偷个懒~

CMD ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]

haproxy的配置文件如下:

global
    log 127.0.0.1 local0
    maxconn 4096
    daemon
    nbproc 4

defaults
    log 127.0.0.1 local3
    mode http
    option dontlognull
    option redispatch
    retries 2
    maxconn 2000
    balance roundrobin
    timeout connect 5000ms
    timeout client 5000ms
    timeout server 5000ms

frontend main
    bind *:6301
    default_backend webserver

backend webserver
    server app1 app1:8000 check inter 2000 rise 2 fall 5
    server app2 app2:8000 check inter 2000 rise 2 fall 5
    server app3 app3:8000 check inter 2000 rise 2 fall 5

这里的app即web应用容器的主机名,运行haproxy容器时用link连接三个web应用容器,绑定到宿主机的80端口。

docker run -d --name=lb -p 80:6301 --link app1:app1 --link app2:app2 --link app3:app3  [build_haproxy_image]

这时候访问宿主机的80端口后,haproxy就会接管请求,用roundrobin方式轮询代理到后端的三个容器上,实现健康检测和负载均衡。
在这里插入图片描述


5、引入etcd实现动态维护

现在又有一个问题了,每次我们想增加或者减少web应用的数量时,都要修改haproxy的配置并重启haproxy,十分的不方便。

理想的方式是haproxy能自动检测到后端服务器的运行状况并相应调整配置,好在这种方式不难,我们可以使用etcd作为后端服务器的服务发现工具,把服务器的信息写入到etcd的数据库中,再由confd来间隔一段时间去访问etcd的api,将服务器的信息写入到模版配置中,并更新haproxy的文件以及重启haproxy进程。

按官方的说法,etcd是一个可靠的分布式的KV存储系统,而confd则是使用模版和数据管理应用配置的一个工具,关于他俩我还没太多了解,所以不多说,下面把他们集成到上面的应用中。

创建一个etcd的容器:

docker run -d \
-e CLIENT_URLS=http://0.0.0.0:2379 \
-e PEER_URLS=http://0.0.0.0:2380 \
-p 2379:2379 \
-p 2380:2380 \
-p 4001:4001 \
-p 7001:7001 \
-v /etc/ssl/certs/:/etc/ssl/certs/ \
elcolio/etcd \
-name etcd \
-initial-cluster-token=etcd-cluster-1 \
-initial-cluster="etcd=http://etcd:2380"\
-initial-cluster-state=new \
-advertise-client-urls=http://etcd:2379 \
-initial-advertise-peer-urls http://etcd:2380

confd的处理比较简单,把confd的二进制文件和配置文件集成到之前haproxy的Dockerfile中:

FROM haproxy:latest

COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

COPY confd .

RUN chmod +x confd

COPY haproxy.toml /etc/confd/conf.d/

COPY haproxy.tmpl /etc/confd/templates/

COPY boot.sh .

COPY watcher.sh .

CMD ["./boot.sh"]

通过之前haproxy的配置文件创建出新的模版文件,修改backend的配置,加入模版指令,表示confd从etcd的前缀为/app/servers的所有key中获取键值对,作为server的key的value,逐条追加到配置文件中去:

backend webserver
    {{range gets "/app/servers/*"}}
    server {{base .Key}} {{.Value}} check inter 2000 rise 2 fall 5
    {{end}}

下面是confd的配置文件:

[template]
src = "haproxy.tmpl"
dest = "/usr/local/etc/haproxy/haproxy.cfg"
keys = [
    "/app/servers"
]
owner = "root"
mode = "0644"
reload_cmd = "kill -HUP 1"

confd会把数据填入上面的模版文件,并把配置更新到haproxy配置的目标路径,再使用reload_cmd指定的命令重启haproxy。

修改后的haproxy镜像最后通过boot.sh启动进程:

#!/bin/bash

./watcher.sh &

exec /docker-entrypoint.sh haproxy -f /usr/local/etc/haproxy/haproxy.cfg

watcher.sh启动了confd间隔一段时间去访问etcd的地址,检查是否有更新:

./confd -interval 10 -node http://etcd:2379 -config-file /etc/confd/conf.d/haproxy.toml

启动haproxy时建立与etcd容器间的连接:

$ docker run -d --name=lb -p 80:6301 --link app1:app1 --link app2:app2 --link app3:app3  --link=etcd:etcd [build_haproxy_image]

下面通过调用etcd的api在/app/servers上新建一个服务器节点:

$ docker exec -it etcd etcdctl set /app/servers/app1 172.17.0.5:8000

观察haproxy容器的日志,可以看到配置被更新了:

$ docker logs -f lb
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Backend set to etcd
<7>haproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -f /usr/local/etc/haproxy/haproxy.cfg -Ds
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Starting confd
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Backend nodes set to http://etcd:2379
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO /usr/local/etc/haproxy/haproxy.cfg has md5sum 8e6fc297a13fbb556426f55e130cecf4 should be f5e8a4b8fbea0b20da3796334bac1ddb
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Target config /usr/local/etc/haproxy/haproxy.cfg out of sync
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Target config /usr/local/etc/haproxy/haproxy.cfg has been updated
<5>haproxy-systemd-wrapper: re-executing on SIGHUP.
<7>haproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -f /usr/local/etc/haproxy/haproxy.cfg -Ds -sf 15 16 17 18

最终的应用结构图如下:
在这里插入图片描述
运行在机器上的服务时刻有可能有意外发生,因此我们需要一个服务来监控机器的运行情况和容器的资源占用。netdata是服务器的一个实时监测工具,利用它可以直观简洁地了解到服务器的运行情况。
在这里插入图片描述
当docker镜像和容器数量增多的情况下,手工去运行和定义docker容器以及其相关依赖无疑是非常繁琐和易错的工作。Docker Compose是由Docker官方提供的一个容器编排和部署工具,我们只需要定义好docker容器的配置文件,用compose的一条命令即可自动分析出容器的启动顺序和依赖,快速的部署和启动容器。

下面编写好compose的文件:

version: '2.1'
services:
  haproxy:
    container_name: lb
    build: ./builds/haproxy
    ports:
     - 80:6301
    restart: always
    links:
    - ping-app
    - etcd:etcd
    - netdata:netdata

  ping-app:
    build: .
    restart: always
    volumes:
    - ./app:/usr/src/app
    links:
    - sentinel
    - redis-master:db

  redis-master:
    image: redis:latest
    ports:
     - 6379:6379
    restart: always

  redis-slave:
    image: redis:latest
    command: redis-server --slaveof master 6379
    restart: always
    links:
    - redis-master:master
 
  sentinel:
    build: ./builds/sentinel
    restart: always
    links:
    - redis-master:redis-master
    - redis-slave

  etcd:
    image: elcolio/etcd
    command: -name etcd -initial-cluster-token=etcd-cluster-1 -initial-cluster="etcd=http://etcd:2380" -initial-cluster-state=new -advertise-client-urls=http://etcd:2379 -initial-advertise-peer-urls http://etcd:2380
    environment:
     - CLIENT_URLS=http://0.0.0.0:2379
     - PEER_URLS=http://0.0.0.0:2380
    ports:
     - 2379:2379
     - 2380:2380
     - 4001:4001
     - 7001:7001
    volumes:
     - /etc/ssl/certs/:/etc/ssl/certs/

  netdata:
    image: titpetric/netdata
    restart: always
    cap_add:
     - SYS_PTRACE
    volumes:
     - /proc:/host/proc:ro
     - /sys:/host/sys:ro

只需几条命令,就能启动和伸缩容器:

docker-compose -f docker-compose.yml up -d
docker-compose -f docker-compose.yml scale redis-slave=2
docker-compose -f docker-compose.yml scale sentinel=3
docker-compose -f docker-compose.yml scale ping-app=3

再通过一个脚本把web应用注册到etcd中去:

APP_SERVERS=$(docker-compose -f docker-compose.yml ps ping-app | awk '{print $1}' |sed '1,2d'|xargs docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' | xargs)
APP_SERVER_PORT=8000
INDEX=1
for ETCD_NODE in ${APP_SERVERS//\s/};
do
    docker-compose -f docker-compose.yml exec etcd etcdctl set /app/servers/app$INDEX $ETCD_NODE:$APP_SERVER_PORT
    INDEX=$(expr $INDEX + 1)
done

就这样,一个基于Docker构建并具有良好可用性的web应用就完成了。


原文作者:知乎在这里插入图片描述
很优秀的一篇文章,融会贯通好多东西,值得细品

  • 10
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一个 `RSS` 阅读器的 Web 版本,基于 `Python`、`Django` 和 `MySQL`。 ![预览](http://tonghs-cdn-static.qiniudn.com/new_rss_style_20150213-1.png) ## 部署 有三种部署方式: 1. [下载 docker 镜像](#docker_image) 2. [使用 dockerfile 编译 docker 镜像](#dockerfile) 3. [手动部署](#manually) <a name='docker_image'></a> ### 1. docker image 地址:[https://registry.hub.docker.com/u/tonghuashuai/rss-deploy/](https://registry.hub.docker.com/u/tonghuashuai/rss-deploy/) ``` shell docker pull tonghuashuai/rss-deploy:0.1 ``` <a name='dockerfile'></a> ### 2. dockerfile dockerfile及相关文件:[https://github.com/tonghuashuai/OnlyRSSWeb/tree/master/dockerfile](https://github.com/tonghuashuai/OnlyRSSWeb/tree/master/dockerfile) ``` shell cd DOCKERFILE DIR docker build -t rss-demo . ``` <a name='manually'></a> ### 3. 手动部署Web 端和后台脚本两部分。 #### 相关与依赖: * django * feedparser * uwsgi * nginx * mysql #### 后台脚本: 用于定时获取订阅内容,可配合任务计划工作。 -------- 该资源内项目源码是个人的毕设,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! <项目介绍> 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 --------
第01章 课程介绍 1-1 导学.mp4 1-2 课程介绍.mp4 第02章 实战-“云存储”系统原型 2-1 “云存储”系统原型之简单文件上传服务架构说明.mp4 2-2 编码实战:“云存储”系统之实现上传接口.mp4 2-3 编码实战:“云存储”系统之保存文件元信息.mp4 2-4 编码实战:“云存储‘系统之实现单个文件查询信息接口.mp4 2-5 编码实战:“云存储”系统之实现文件下载接口.mp4 2-6 编码实战:“云存储”系统之实现文件修改接口+小结.mp4 第03章 “云存储”系统之基于MySQL实现的文件数据库 3-1 MySQL基础知识.mp4 3-2 MySQL主从数据同步演示.mp4 3-3 文件表的设计及创建.mp4 3-4 编码实战:“云存储”系统之持久化元数据到文件表.mp4 3-5 编码实战:“云存储”系统之从文件表中获取元数据.mp4 3-6 Docker入门基础文档.mp4 3-6 本章小结.mp4 3-7 Ubuntu中通过Docker安装配置MySQL主从节点.mp4 第04章 “云存储”系统之基于用户系统实现的资源隔离及鉴权 4-1 帐号系统介绍与用户表设计.mp4 4-2 编码实战:“云存储”系统之实现用户注册接口.mp4 4-3 编码实战:“云存储”系统之实现用户登录接口.mp4 4-4 编码实战:“云存储”系统之实现用户信息查询接口.mp4 4-5 接口梳理小结.mp4 4-6 编码实战:“云存储”系统之快速实现访问鉴权接口+小结.mp4 4-7 关于静态资源访问404的问题【补漏.mp4 第05章 “云存储”系统之基于Hash计算实现秒传 5-1 Hash算法对比及秒传原理.mp4 5-2 用户文件表设计与创建.mp4 5-3 编码实战:“云存储”系统之升级改造上传接口.mp4 5-4 编码实战:“云存储”系统之基于用户查询文件Hash信息.mp4 5-5 编码实战:“云存储”系统之实现秒传功能接口+小结.mp4 第06章 “云存储”系统之基于Redis实现分块上传及断点续传 6-1_分块上传与断点续传原理.mp4 6-2_编码实战:Go实现Redis连接池(存储分块信息).mp4 6-3_编码实战:实现初始化分块上传接口.mp4 6-4_编码实战:实现分块上传接口.mp4 6-5_编码实战:实现分块合并接口.mp4 6-6_分块上传场景测试+小结.mp4 6-7_文件断点下载原理.mp4 第07章 “云存储”系统之基于Ceph实现私有云存储服务 7-1_Ceph是什么.mp4 7-2_Ceph集群介绍及兼容亚马逊S3接口详解.mp4 7-3_编码实战:Go访问管理Ceph集群.mp4 7-4_编码实战:Go实现Ceph的文件上传下载+小结.mp4 7-5_Ubuntu下通过Docker快速搭建Ceph测试集群(单机部署).mp4 7-6_Centos7下Docker部署Ceph集群(nautilus最新版,多机部署).mp4 第08章 “云存储”系统之基于阿里云OSS实现海量数据上云 8-1_阿里云对象存储OSS简介.mp4 8-2_阿里云对象存储OSS特点.mp4 8-3_阿里云对象存储OSS专业术语.mp4 8-4_阿里云对象存储OSS控制台管理.mp4 8-5_编码实战:OSS上传文件.mp4 8-6_编码实战:OSS下载文件.mp4 8-7_编码实战:OSS对象生命周期管理等常用功能.mp4 8-8_阿里云OSS本章小结.mp4 第09章 “云存储”系统之基于RabbitMQ实现异步存储 9-1_Ubuntu下通过Docker安装RabbitMQ.mp4 9-2_关于任务的同步与异步.mp4 9-3_RabbitMQ简介.mp4 9-4_RabbitMQ工作原理和转发模式.mp4 9-5_Docker安装RabbitMQ及UI管理.mp4 9-6_编码实战_实现异步转移的MQ生产者.mp4 9-7_编码实战_实现异步转移的MQ消费者.mp4 9-8_编码实战_异步转移文件测试+小结.mp4 第10章 “云存储”系统之架构微服务化 10-1_基于Docker部署服务注册发现中心consul集群.mp4 10-2_微服务基础概念与原理.mp4 10-3_云存储系统之微服务架构(1).mp4 10-4_云存储系统之微服务架构(2).mp4 10-5_Web框架Gin基础介绍.mp4 10-6_编码实战_基于Gin改造用户service(1).mp4 10-7_编码实战_基于Gin改造用户service(2.mp4 10-8_gRPC与Protobuf基础原理.mp4 10-9_RPC框架go-micro基础介绍.mp4 10-10_编码实战_改造账号系统service.mp4 10-11_编码实战_改造api网关service.mp4 10-12_编码实战_改造文件上传service.mp4 10-13_综合测试演示+小结.mp4 第11章 “云存储”系统之k8s&Docker;容器化实战 11-1_Ubuntu18下通过kubeadm单机安装k8s(v1.14.1)集群.mp4 11-2_Ubuntu18下安装k8s(v1.14.1)可视化管理工具.mp4 11-3_DockerDocker-Compose基础概念.mp4 11-4_基于容器的微服务反向代理利器Traefik.mp4 11-5_基于Docker-compose与Traefik的容器部署演示.mp4 11-6_Kubernetes基础原理.mp4 11-7_基于Kubernetes的容器部署演示.mp4 第12章 “云存储”系统之持续集成部署 12-1_ubuntu下离线安装harbor1.6.mp4 12-2_持续构建之基础概念.mp4 12-3_基于gitlab+jenkins+harbor的自动化部署配置演示.mp4 第13章 课程总结 13-1_课程总结之章节重点及技能树温习.mp4
# 项目需求 ## 项目背景 对于刚学习springboot的同学,最好的就是拿一个项目练练手。在编码过程中遇到的问题并解决,这都是宝贵的经验。 用springboot开发的博客系统,简单而且实用,适合做练手项目。 ## 功能需求 ### 界面需求 #### 主页 - 博客汇总,以列表加图片的形式展示 - 能够以分类的方式查看文章 - 能够以时间列表的方式归档文章 - 个人介绍,github地址 - 搜索框,能够搜索文章 #### 后台管理 - 管理主页,记录最新文章,最新留言,最近日志等 - 最近日志记录登录IP,地址,操作等 - 记录一天的访问量 - 发布文章 - 使用markdown编辑器,支持插入代码,插入图片等功能 - 能够给文章添加缩略图。 - 可将文章存为草稿或者发布 - 文章可选择分类和标签,自定义url - 文章可控制是否允许评论 - 文章管理 - 以列表形式展示文章信息 - 在可选操作中增加删除,预览,编辑功能 - 支持分页显示 - 增加搜索功能,可根据文章名文章信息 - 分类管理 - 可以新增、删除、修改分类 - 文件管理 - 支持文件上传 - 支持删除已上传的文件 - 友情联机 - 支持增加友情链接 - 支持删除友情链接 - 系统设置 - 支持修改密码 - 支持备份数据库 - 支持黑名单配置 ### 非界面需求 - 日志记录,记录来访IP名单 - 每天定时备份数据库 ### 安装部署需求 - 可以使用docker方式部署,也可支持-jar方式 - 使用springboot自带方式打包 ## 非功能性需求 ### 性能需求 - 首页响应的时间不超过1秒钟 - 文章页响应时间不超过1秒钟 # 项目设计 ## 总体设计 - 本项目用到的技术和框架 - 项目构建: maven - web框架:spring boot - 数据库ORM:mybatis - 数据库连接池:Druid - 分页插件:PageHelper - 数据库:mysql - 缓存NOSQL:redis - 前段模板:thymeleaf - 文章展示:使用commonmark,将markdown转成html页面 - 本项目的关键点 - 采用springboot开发,数据库使用连接池加orm框架的模式,对于系统的关键业务使用redis缓存,加快响应速度 - 整体系统采用门户网站+后台管理的方式搭建,门户主要展示博客内容,后台管理主要用于编辑文章,上传附件,控制黑名单登录等。 - 环境 工具 | 名称 | ------- | -------| 开发工具 | IDEA 语言 | JDK1.8, JS, HTML 数据库 | mysql5.6 缓存NOSQL | redis 项目构建|Maven 运行环境|阿里云Centos7 ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。
使用Docker部署web应用有许多好处。首先,Docker提供了持续集成的优势,通过使用Docker容器,开发人员可以快速构建、测试和部署应用程序,提高了开发效率。其次,Docker可以实现版本控制,使得应用程序的管理更加灵活和可靠。同时,Docker还具有很强的可移植性,即使在不同的环境中部署应用程序,也可以保持一致的运行效果。此外,Docker容器之间具有隔离性,不同的容器互相独立,这样可以确保应用程序之间的安全性和稳定性。最后,Docker还提供了安全性方面的保障,可以限制容器的访问权限,防止潜在的安全风险。总之,通过使用Docker部署web应用,可以提高开发效率、简化管理、增强可移植性、加强隔离性和提供额外的安全保障。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Docker 入门 到部署Web 程序- (阿里面试常用的docker命令和优点)](https://blog.csdn.net/diaopai5230/article/details/101216678)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [docker入门(利用docker部署web应用)](https://blog.csdn.net/q610376681/article/details/90483576)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [undefined](undefined)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值