docker-compose:一招解决多容器问题

1,410 阅读10分钟

        不知不觉,docker相关的文章已连载好几篇了,今天我们来介绍下docker三剑客之compose,用一个实操型的案例来为该系列文章收尾。

       之所以要以compose来做该系列文章的收尾的原因很简单,因为docker-compose确实给我们在实际的工作中带来了很大的便捷性。根据我所在的小组定位,旨在开发一些基础组件给各个业务系统使用,前段时间一系列的升级给不少domain集成带来不小的工作量,无论是本地联调还是集成测试。在没有下沉到mesh之前绝大部分组件不可避免的要各个业务方来一起来升级发布。幸好,我们从VM时代演进下来的一套体系,大部分业务系统都是nginx + service + mysql + redis作为标配,这就非常适合用compose来一键拉起开发/测试环境。

docker compose简介

         Docker-Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排。在实际环境中,一个应用往往由许多服务构成,往往需要运行多个容器。多个容器协同工作需要一个有效的工具来管理他们,定义这些容器如何相互关联,docker compose 应运而生。Docker-Compose将所管理的容器分为三层,分别是工程(project),服务(service)以及容器(container),一个工程当中可包含多个服务,每个服务中定义了一个或者多个容器运行的镜像,参数,依赖。Docker-Compose允许用户通过一个单独的docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

Compose模板文件是一个定义服务、网络和卷的YAML文件。Compose模板文件默认路径是当前目录下的docker-compose.yml,可以使用.yml或.yaml作为文件扩展名。Docker-Compose标准模板文件包含version、services、networks 三大部分,最关键的是services和networks两个部分,例如本文实例最后的compose文件长这样:

version: '3' 
services:   
  nginx:     
    container_name: demo_nginx     
    image: nginx:latest     
    restart: always     
    depends_on:       
      - api     
      ports:       
        - 80:80       
        - 443:443     
    volumes:     
      - ./nginx/conf.d:/etc/nginx/conf.d   
database:     
  container_name: database     
  healthcheck:       
    test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost","--password=123456"]       
    timeout: 20s       
    retries: 10     
  build: ./mysql     
  environment:       
    MYSQL_DATABASE: t_order       
    MYSQL_ROOT_PASSWORD: 123456       
    MYSQL_ROOT_HOST: '%'     
  ports:     
    - 3306:3306   
api:     
  build: ./api     
  ports:       
    - 10001     
  depends_on:       
    database:         
      condition: service_healthy

简单说明:

  • version:指定当前使用的一个版本(一般是2或者3),例中我们使用version 3

  • services:定义服务,例中我们定义了nginx,api,mysql三个服务

  • container_name: 容器名,可不指定让docker生成一个. 当容器需要有多个副本的时候不要去指定他,这会导致scale up 失败

  • image: 指定服务的镜像名或者ID

  • build:服务除了可以基于指定的镜像,还可以基于一份Dockerfile,在使用up启动时执行构建任务,构建标签是build,可以指定Dockerfile所在文件夹的路径。Compose将会利用Dockerfile自动构建镜像,然后使用镜像启动服务容器。

  • depend_on: 定义服务的依赖关系,这会决定启动先后顺序,但这仅仅是服务启动开始的顺序,并保证被依赖服务启动完毕,后面例子会有详细的解释.

  • ports: 指定容器的端口,以及宿主机的映射端口的映射.

  • volumes: 定义数据卷.

  • envrionment: 定义环境变量.

  • healthcheck/condition: 定义健康检查和依赖条件,保证依赖的服务启动完毕.

当然,除了例子中用到的这些并不是compose的全部能力,其他像net/host/links/dns等等在Dockfile里介绍过的几乎都可以使用,具体可以去翻阅官方文档.

有了docker-compose.yaml, compose命令就可以上场了!

docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
选项包括:
-f --file FILE指定Compose模板文件,默认为docker-compose.yml
-p --project-name NAME 指定项目名称,默认使用当前所在目录为项目名
--verbose  输出更多调试信息
-v,-version 打印版本并退出
--log-level LEVEL 定义日志等级(DEBUG, INFO, WARNING, ERROR, CRITICAL)

1)docker-compose up [options] [--scale SERVICE=NUM...] [SERVICE...]

用途:很常用的命令,用来启动服务
选项包括:
-d 在后台运行服务容器
-no-color 不是有颜色来区分不同的服务的控制输出
-no-deps 不启动服务所链接的容器
--force-recreate 
强制重新创建容器,不能与-no-recreate同时使用
–no-recreate 
如果容器已经存在,则不重新创建,不能与–force-recreate同时使用
–no-build 不自动构建缺失的服务镜像
–build 在启动容器前构建服务镜像
–abort-on-container-exit 
停止所有容器,如果任何一个容器被停止,不能与-d同时使用
-t, –timeout TIMEOUT 
停止容器时候的超时(默认为10秒)
–remove-orphans 
删除服务中没有在compose文件中定义的容器

3)docker-compose ps [options] [SERVICE...]

列出项目中所有的容器

4)docker-compose stop [options] [SERVICE...]

用途:停止正在运行的容器,可以通过docker-compose start 再次启动
选项包括-t, –timeout TIMEOUT 停止容器时候的超时(默认为10秒)
docker-compose stop

5)docker-compose down [options]

用途:停止和删除容器、网络、卷、镜像。
选项包括:–rmi type,删除镜像,类型必须是:all,删除compose文件中定义的所有镜像;local,删除镜像名为空的镜像
-v, –volumes,删除已经在compose文件中定义的和匿名的附在容器上的数据卷
–remove-orphans,删除服务中没有在compose中定义的容器

6)docker-compose logs [options] [SERVICE...]

用途:查看服务容器的输出。默认情况下,docker-compose将对不同的服务输出使用不同的颜色来区分。
可以通过–no-color来关闭颜色。
-f 跟踪日志输出

7)docker-compose bulid [--build-arg key=val...] [SERVICE...]

用途:构建(重新构建)项目中的服务容器。
选项包括:–compress 通过gzip压缩构建上下环境
–force-rm 删除构建过程中的临时容器
–no-cache 构建镜像过程中不使用缓存
–pull 始终尝试通过拉取操作来获取更新版本的镜像
-m, –memory MEM为构建的容器设置内存大小
–build-arg key=val为服务设置build-time变量

8)docker-compose pull [options] [SERVICE...]

用途:拉取服务依赖的镜像。
选项包括:
–ignore-pull-failures,忽略拉取镜像过程中的错误
–parallel,多个镜像同时拉取
–quiet,拉取镜像过程中不打印进度信息

9)docker-compose restart [options] [SERVICE...]

重启项目中的服务。
选项包括:
-t, –timeout TIMEOUT,指定重启前停止容器的超时(默认为10秒)

10)docker-compose rm [options] [SERVICE...]

删除所有(停止状态的)服务容器。
选项包括:–f, –force,强制直接删除,包括非停止状态的容器
-v,删除容器所挂载的数据卷

另外,start/run/scale/pause/kill/config/create/exec等命令不一一列举了,有兴趣或者需要用到的时候去查阅吧!

docker compose项目实战

       虽然本文的初衷是说利用compose在多个不同业务系统(每个系统都有类似的体系)之间来回联调和测试如何提高效率,但为了更加深刻,我们从零开始手把手来介绍如何利用docker compose来一键拉起开发环境。我们从下面最简单也是最典型的一个微服务出发,前置一个nginx,然后有一个api服务(有些功能需要多副本才能验证,所以我们需要三个副本),存储一个mysql容器(Redis也是类似的,而且比mysql更简单一些,所以这里就没有纳入进来).

1) nginx

稍微了解nginx都知道其配置是在/etc/nginx下,所以我们利用volume先搞出配置

# 1. 启动同时把配置文件所在的目录作为挂载了一个volume
docker run -dit --name nginx_test -p 8080:80 -v /etc/nginx nginx
# 2. 启动一个容器把配置文件搞下来
docker run --rm --volumes-from nginx_test -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /etc/nginx/conf.d

然后我们根们上面整体的图知道nginx需要监听80端口并转发到10001端的api服务,所以修改刚刚搞下来的default配置文件:

 server {     
    listen 80;     
    charset utf-8;     
    access_log off;     
    location / {         
        proxy_pass http://api:10001;         
        proxy_set_header Host $host:$server_port;         
        proxy_set_header X-Forwarded-Host $server_name;         
        proxy_set_header X-Real-IP $remote_addr;         
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;     
    }     

    location /static {        
        access_log   off;         
        expires      30d;         
        alias /app/static;     
    } 
}

        这个常规的简单配置就多废话了,主要是要注意proxy_pass写的容器服务名,原因是nginx和api服务不是同一个容器无法用localhost,而docker-compose可以通过服务名做容器之间的通信。

2)api

api是我们的微服务,也是业务系统的主体,这里假设是一个价格查询服务,给下单等上游系统提供API。我们从start.spring.io开始创建:

a) 添加一个coffee查询服务

model:

@Entity@Table(name = "T_COFFEE")
@Builder
@Data
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper=false)
public class Coffee extends BaseEntity implements Serializable {    
    private String name;    
    private long price;
}

@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {    
    @Id    
    @GeneratedValue(strategy = GenerationType.IDENTITY)    
    private Long id;    

    @Column(updatable = false)    
    @CreationTimestamp    
    private Date createTime;    

    @UpdateTimestamp    
    private Date updateTime;
}

service & repository:

@Service
public class CoffeeService {    
    @Autowired    
    private CoffeeRepository coffeeRepository;    
    
    public List<Coffee> list(){        
        return coffeeRepository.findAll();    
    }
}

public interface CoffeeRepository extends JpaRepository<Coffee, Long> {}

应用启动&API

@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
@RestController
public class PriceApiApplication {  
    public static void main(String[] args) {    
        SpringApplication.run(PriceApiApplication.class, args);  
    }  
    @Autowired  
    private CoffeeService coffeeService;  
    
    @RequestMapping("coffee/list")  
    public List<Coffee> getAllCoffee(){    
        return coffeeService.list();  
    }
}

b) application.yml

server:  
  port: 10001
spring:  
  datasource:    
    url: jdbc:mysql://database:3306/t_order    
    username: root    
    password: 123456    
    driver-class-name: com.mysql.jdbc.Driver  
  jpa:    
    hibernate:      
      ddl-auto: none    
      show-sql: true    
    database-platform: org.hibernate.dialect.MySQLDialect    
    database: mysql

这里也要注意一下,数据库url使用的mysql容器的服务名,而不是localhost或者某个ip

c) Dockfile

 FROM java:8 
 VOLUME /tmp ADD build/libs/api-service-0.0.1-SNAPSHOT.jar price-api.jar 
 EXPOSE 10001 ENTRYPOINT ["java", "-jar", "price-api.jar"]

3)mysql

mysql容器有一个小的要求,就是我们必须启动容器之后需要自动做好数据库和表的创建,以及初始数据的插入。所以我们不能直接使用官方的镜像,而是需要基于官方镜像把我们初始化要执行的sql脚本注入到/docker-entrypoint-initdb.d。

Dockfile

 FROM mysql:8 
 ENV AUTO_RUN_DIR /docker-entrypoint-initdb.d 
 ENV INSTALL_DB_SQL init_database.sql 
 COPY ./$INSTALL_DB_SQL $AUTO_RUN_DIR/ 
 EXPOSE 3306 
 RUN chmod a+x $AUTO_RUN_DIR/$INSTALL_DB_SQL

而init_database.sql就是我们要自动执行的脚本,虽然这里只有一个表来演示

CREATE DATABASE IF NOT EXISTS t_order default charset utf8 COLLATE utf8_general_ci; 
USE t_order; 
create table if not exists t_coffee(     
  id bigint auto_increment,     
  create_time timestamp,     
  update_time timestamp,     
  name varchar(255),     
  price bigint,     
  primary key (id) 
); 
insert into t_coffee (name, price, create_time, update_time) 
values ('espresso', 2000, now(), now()); 
insert into t_coffee (name, price, create_time, update_time) 
values ('latte', 3200, now(), now()); 
insert into t_coffee (name, price, create_time, update_time) 
values ('capuccino', 2800, now(), now()); 
insert into t_coffee (name, price, create_time, update_time) 
values ('mocha', 2500, now(), now());

4**)docker-compose.yml**

需要解释三点:

1)api不能指定container_name,避免扩容到3个副本时候容器名冲突

2)api 只定义host端口,映射的容器端口随机

3)为了保证在api启动时,Mysql已经启动好并且执行了sql脚本,我们需要加入condition和healthcheck

现在,我们可以用docker-compose up -d来启动:

省略了第一次pull mysql和nginx镜像部分日志,总之一切正常:

$ curl http://localhost/coffee/list
[{"id":1,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"espresso","price":2000},{"id":2,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"latte","price":3200},{"id":3,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"capuccino","price":2800},{"id":4,"createTime":"2021-12-29T16:57:42.000+00:00","updateTime":"2021-12-29T16:57:42.000+00:00","name":"mocha","price":2500}]

最后,我们用docker-compose up --scale api=3 -d来做一下扩容

有了这个成功运行的案例,我们就可以作为模板套用到其他各个业务里,同样的模式一键拉起本地开发环境,或者拉起一台测试机不费吹灰之力!

最后的最后,再讲一个提高效率的小窍门,我在个人本地电脑实操demo时候发现好几个网络问题,一是gradle下载jar很慢,二是docker拉mysql:8镜像很慢,大中午的实在是等不起,所以可以通过添加国内mirror来加速:

gradle.build:

mac docker engine: