Post

认识微服务

Docker

环境配置与基本了解

Docker方面自己以前在部署AutoBangumi这个项目的时候接触过,也是在文档和朋友的帮助下成功的跑了起来,只知道按照他给的配置文件和命令一顿操作,对其是知其然而不知其所以然,为此需要进行更加系统的学习。
首先是了解了Docker最基本的三元素——镜像、容器、仓库,Docker本身只是一个容器的运行载体,而镜像是容器的模板,容器示例由Docker通过镜像文件创建,一个容器运行一种服务,一个镜像可以包含多个容器,仓库则是存放各种镜像的地方。
我在WSL上配置了阿里云镜像站的stable仓库,安装好了docker-ce,在docker run hello-world时遇到了context deadline exceeded报错的问题,总之就是镜像拉不下来,想起来早在去年六七月份国内的docker容器镜像站停用曾引起过一阵讨论,包括阿里云容器仓库的加速器也已经只对云服务器用户开放使用了,我打算用在WSL中配置网络代理的方式来解决。
了解到Clash For Windows也可以在Linux上运行,我尝试了在WSL上装CFW,配完后发现图形界面并不像Windows上的有系统代理按钮,改用Clash的Linux版本,发现还是不行,在网上搜索相关经验知道了WSL和主机网络是互通的,但由于NAT我们的代理IP配127.0.0.1是不会有效果的,直接配置本机IP即可,最后成功连上代理后却发现命令行仍然是走不了代理的,在/etc/docker/daemon.json中配置代理IP后终于成功拉取下来了run hello-world所需的镜像。

镜像常用命令

1
2
3
4
5
6
7
8
9
10
11
12
// 列出本地主机上的镜像
docker images -a:列出本地所有镜像(包括历史映像层) -q:只显示镜像ID
// 远程库查找镜像
docker search 镜像名 --limit N:只列出N个镜像,默认25个
// 下载镜像
docker pull 镜像名[:TAG] 没有TAG默认最新版本,等价于:latest
// 查看镜像/容器/数据卷所占的空间
docker system df
// 删除镜像
docker rmi -f:强制删除 镜像ID或镜像名
docker rmi -f 镜像名1:TAG 镜像名2:TAG ... 删除多个
docker rmi -f ${docker images -qa} 删除全部

容器常用命令

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 新建+启动容器
docker run [OPTIONS] IMAGE [COMMAND][ARG...]
例:docker run -it --name=myMysql mysql bash
// 常用OPTIONS
--name:为容器指定一个名称 -i:交互模式运行容器 -t:为容器重新分配一个伪输入终端,一般-it联合使用 -P:随机分配端口映射 -p:指定端口映射

// 列出所有容器
docker ps -a:列出当前正在运行的和历史上运行过的容器 -l:显示最近创建的容器 -n:显示最近n个创建的容器 -q:只显示容器编号

// 退出容器
exit:容器会停止 ctrl+p+q:容器不停止
// 启动容器
docker start 容器ID或容器名
// 重启容器
docker restart 容器ID或容器名
// 停止容器
docker stop 容器ID或容器名
// 强制停止容器
docker kill 容器ID或容器名
// 删除已停止的容器
docker rm 容器ID
docker rm -f ${docker ps -a -q} 删除所有容器
docker ps -a -q | xargs docker rm 删除所有容器

// 启动守护式容器
docker run -d 容器名 有一些容器如ubuntu后台运行必须有一个前台进程

// 查看容器日志
docker logs 容器ID或容器名
// 查看容器内运行进程
docker top 容器ID或容器名
// 查看容器内部细节
docker inspect 容器ID或容器名

// 重新进入正在运行的容器
docker exec -it 容器ID bash 在容器中打开新的终端,并且可以启动新的进程,exit不会导致容器的停止,推荐使用
docker attach 容器ID 直接进入容器启动命令的终端,不会启动新的进程,exit会导致容器的停止

// 容器内文件拷贝
docker cp 容器ID:容器内路径 目的主机路径
// 容器导出
docker export 容器ID > tar文件名
// 容器导入
cat tar文件 | docker import - 自定义镜像用户/自定义镜像名:自定义镜像版本号

镜像进阶

镜像:
Docker中镜像是从base镜像一层一层叠加生成的。当容器启动时,一个新的可写层将被加载到镜像的顶部,这一层通常被称为容器层,容器层之下的都叫镜像层,所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。
Docker Hub中拉取的Ubuntu镜像是轻量级的,只包含了必要核心功能,以vim为例,是不支持该命令的。我们在拉取的镜像中安装vim,再通过docker commit -m=”提交的描述信息” -a=”作者” 容器ID 要创建的目标镜像名:[tag]命令生成新的镜像,那么这个镜像就相当于是升级版的Ubuntu了。
镜像上传阿里云在阿里云官网有一套完整的命令和说明,后续个人项目的环境就打算配置一套在阿里云上,对于私有库的搭建主要是了解相关的使用方法和常规配置仓库有没有什么区别,因为后续在工作中接触的Docker相关大概也是自建私有库进行存储的。

数据卷:
卷就是目录或者文件,容器数据卷就是将容器目录和主机目录做映射,将容器中的重要数据备份+持久化到本地主机目录。数据卷能够绕过Union File System提供一些持续存储或共享数据的特性,完全独立于容器的生存周期,因此Docker不会在容器删除时删除其挂载的数据卷。
要给运行的容器挂载数据卷,需要通过–privileged=true -v 宿主机绝对路径目录:容器内目录[rw | ro] 镜像名这样的OPTIONS来挂载。加上ro的话容器自己只能读不能写,但宿主机的修改可以同步到容器。
Docker中服务安装基本就是拉取镜像并运行容器的过程,但之中会出现例如tomcat目录下webapps为空,而运行所需实际内容变更到了webapps.list文件夹这中问题,需要我们手动进行修改。
而Mysql安装出现的问题就是Docker上默认字符集编码不支持我们对中文数据进行操作,且若是要对Mysql容器进行删除,我们在实际运行容器时要挂载/log、/data、/conf的容器数据卷以保证数据不丢失。挂载好后在/conf下创建my.cnf配置UTF8字符集后即可正常处理中文数据。Redis容器同样也面临着这样的问题,因此要挂载数据卷后配置redis.conf。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// mysql安装过程,以mysql5.7为例
docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 常规安装
docker run -d -p 3306:3306 --privileged=true -v /home/kurisu/mysql/log:/var/log/mysql -v /home/kurisu/mysql/data:/var/lib/mysql -v /home/kurisu/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql mysql:5.7 挂载数据卷安装

// 在conf/my.cnf下新增内容解决中文数据无法操作的问题,并开启自动补全
[client]
default-character-set = utf8
[mysqld]
collation_server = utf8_general_ci
character-set-server = utf8
[mysql]
auto-rehash

// Redis安装过程
首先在宿主机上拷贝一份redis.conf模板
修改该模板,注释掉bind 127.0.0.1允许外网连接,修改daemonize yes为daemonize no
// 挂载数据卷,读取宿主机上修改好的配置文件启动
docker run -p 6379:6379 -privileged=true -v /home/kurisu/redis/redis.conf:/etc/redis/redis.conf -v /home/kurisu/redis/data:/data -d redis:6.0.8 --name redis redis-server /etc/redis/redis.conf

Dockerfile简介

Dockerfile是用来构建镜像的文本文件,由一条条构建镜像所需的指令合参数构成的脚本组成。Dockerfile将镜像逐步增强反复commit的过程列清单一次性搞定,通过编写Dockerfile文件、docker build命令构建镜像、docker run运行镜像三步完成一个完整的镜像安装运行过程。
Dockerfile中每条保留字指令都必须大写字母并后面跟随至少一个参数,指令从上往下依次顺序执行,用#表示注释,每条指令都会创建一个新镜像层并提交。大致流程是Docker从基础镜像运行一个容器,执行一条指令并对容器做修改,执行类似docker commit的操作提交一个新的镜像层,docker再基于刚提交的镜像运行一个新容器,执行dockerfile中的下一条指令重复直至所有指令完成。

Dockerfile保留字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM # 基础镜像,指定一个已存在的镜像作为模板,第一条必须是FROM
MAINTAINER # 镜像维护者的姓名和邮箱地址

RUN apt install ... # 构建时要执行的命令,等同于终端中操作的Shell命令,此为shell格式写法
RUN["可执行文件", "参数1", "参数2"] # 例如RUN["./test.php", "dev", "offline"]等价于RUN ./test.php dev offline,此为exec格式写法

EXPOSE # 当前容器对外暴露出的端口
WORKDIR # 指定在创建容器后,终端默认登陆的工作目录
USER # 指定镜像该以什么用户去运行,默认root
ENV # 配置运行时环境变量,如ENV MY_PATH /usr/mytest,后续指令可直接引用$MY_PATH。
VOLUME # 容器数据卷,用于数据保存和持久化工作

ADD # 将宿主机下的文件拷贝进镜像且自动处理URL和解压tar压缩包
COPY # 类似ADD,拷贝文件和目录到镜像中,从源路径复制复制到新的一层镜像中的目标路径中

CMD # 指定容器启动后要做的事,格式和RUN相似,注意只有最后一个会生效,且CMD会被docker run之后的参数替换
ENTRYPOINT # 也是用来指定一个容器启动要运行的命令,类似于CMD指令,但不会被docker run后面的命令覆盖,而且这些命令行参数会被当作参数传入ENTRYPOINT指定的程序
# ENTRYPOINT可以和CMD一起使用,一般是变参使用CMD,这里的CMD等于是在给ENTRYPOINT传参,两个命令组合会变成<ENTRYPOINT>"<CMD>"

这里给到一个给原版Centos系统加装vim、ifconfig、java8的案例

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
FROM centos
MAINTAINER LJY<LJY@qq.com>

ENV MYPATH /usr/local
WORKDIR $MYPATH

# 安装vim编辑器
RUN yum -y install vim
# 安装ifconfig命令
RUN yum -y install net-tools
# 安装java8及lib库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
# 解压添加到镜像中
ADD jdk-8u171-linux-x64.tar.gz /usr/local/java/
# 配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_171
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH

EXPOSE 80

# 虽然说只有最后一个CMD会生效,但这是针对docker run时来谈的,构建过程中,注入echo的指令仍然会生效
CMD echo $MY_PATH
CMD echo "success----------ok"
CMD /bin/bash

微服务部署

依然是将项目打包成jar包,将jar包部署到Docker中,遵循使用Dockerfile进行镜像构建并运行的过程。(之后部署自己的项目时补全具体过程和注意事项)

1

Docker network

当Docker启动后,会产生一个名为docker0的虚拟网桥。
Dockere network用于容器的互联和通信以及端口映射,容器IP变动的时候可以直接通过服务名直接网络通信而不受到影响,这些作用主要体现于容器之间的调用规划。

网络模式:
bridge是为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,默认为该模式。其在内核层连同了其他的物理或虚拟网卡,将所有容器和本地主机放在同一个物理网络中。
host模式容器不会虚拟出自己的网卡、配置自己的IP等,直接采用宿主机的IP和端口。
none模式是容器有独立的Network namespace但没有对其进行任何网络设置。
container模式是新创建的容器不会创建自己的网卡和配置自己的IP,而是和一个指定的容器共享IP、端口范围等。

Docker容器内部的IP是会发生改变的,对于一个容器,其关闭后启动其他容器,该容器原本的IP可能就会分配给其他容器。

SpringCloud

项目部署进行了从单体->集群->分布式的演变,集群就是将相同的副本部署在多台机器上,而分布式微服务则是拆分各个服务到多台机器上。
对各服务的访问,均由统一的网关进行转发;而网关为什么知道该把请求转发给哪台服务器呢,这是因为每个服务都会经由注册中心进行注册,注册中心还可以作为配置中心,我们将微服务的配置保存到配置中心中,进行统一管理,主动推送配置的变更而无需下线换包;处理请求时可能涉及到各个微服务之间的远程调用(RPC);多个微服务对数据库的操作需要用到分布式事务保证操作的ACID属性;同时为了防止一个服务请求卡死导致影响到其他所有的服务,还需要引入服务熔断机制来对请求进行快速失败处理。
这就是微服务中需要新考虑到的问题,均有对应的技术来进行解决。

Nacos

注册中心

Nacos作为注册中心和配置中心,是一个动态服务发现、配置管理和服务管理的平台,服务发现主要依赖于spring-cloud-starter-alibaba-nacos-discovery依赖,需要我们在yml文件中对服务分别配置Nacos地址。

1
2
3
4
spring: 
    cloud: 
        nacos: 
            server-addr: 127.0.0.1:8848

最基本的服务发现依赖于EnableDiscoveryClient注解,可以通过DiscoveryClient和NacosServiceDiscovery两个类查看各个服务实例的参数,后者仅适用于Nacos,前者都可以使用。
通过这样的最基本的服务发现功能,我们可以实现一个最基本的远程调用,以用户下单->远程查询商品->返回商品数据->返回订单数据这样的过程为例。

1
2
3
4
5
6
7
8
// 远程调用部分
private Product getProductFromRemote (Long productId) {
    List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
    ServiceInstance instance = instances.get(0);
    String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product" + productId;
    Product product = restTemplate.getForObject(url, Product.class);
    return null;
}

以上远程调用只调用了固定的服务,我们可以引入负载均衡实现自动对各个服务的均衡调用。负载均衡基于spring-cloud-starter-loadbalancer依赖,可以通过LoadBalancerClient类和@LoadBalanced注解进行服务调用的自动负载均衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LoadBalancerClient类
private Product getProductFromRemote (Long productId) {
    ServiceInstance instance = loadBalancerClient.choose("service-product"); //自动选择要调用的服务
    String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product" + productId;
    Product product = restTemplate.getForObject(url, Product.class);
    return null;
}

// @LoadBalanced注解
@LoadBalanced
@Bean
RestTemplate restTemplate() {
    return new RestTemplate();
}

private Product getProductFromRemote (Long productId) {
    String url = "http://service-product/" + productId;
    Product product = restTemplate.getForObject(url, Product.class); //打上注解后会自动负载均衡地替换url中的地址
    return null;
}

配置中心

基于spring-cloud-starter-alibaba-nacos-config依赖,可以让各个微服务直接读取nacos上编写的配置文件。
我们可以通过Value注解获取配置+RefreshScope注解实现配置动态刷新,除此之外通过ConfigurationProperties注解批量绑定在Nacos下实现无感自动刷新,配置NacosConfigManager配置监听可以达到一样的效果,但主要是为了和传统监听器一样在配置文件变更时自定义一些额外的功能。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// RefreshScope注解
@RefreshScope
public class OrderController {
    ...
    @Value("${order.timeout}")
    String orderTimeout;
    ...
}

// ConfigurationProperties注解
@Componet
@ConfigurationProperties(prefix = "order")
@Data
public class OrderProperties {
    // 字段会根据驼峰命名法自动映射配置中心中的配置文件
    String timeout;
    String autoConfirm;
}

// NacosConfigManager监听
@Bean
ApplicationRunner applicationrunner(NacosConfigManager nacosConfigManager) {
    return args -> {
        ConfigService configService = nacosConfigManager.getConfigService();
        configService.addListener(
            "service-order.properties",
            "DEFAULT_GROUP",
            new Listener() {
                @Override
                public Executor getExecutor() {
                    return Executors.newFixedThreadPool(4); //监听器工作在线程池中
                }
                @Override
                public void receiveConfigInfo(String configInfo) {
                    //....监听配置要进行的额外操作
                }
            }
        );
        System.out.println("========");
    }
}

如果Nacos和本地编写了相同的配置项,配置中心的配置更优先。

Nacos依靠命名空间Namespace区分多套环境,一个命名空间下可以有多个分组,一个分组下又可以有多个数据集。

OpenFeign

我们之前用到的RestTemplate是编程式的REST客户端,整个远程调用流程需要手动进行编码,而声明式的REST客户端不需要,这就是OpenFeign的作用。通过OpenFeign,我们只需要注解驱动指定远程地址、指定请求方式、指定携带数据、指定返回结果就可以了,基于spring-cloud-starter-openfeign依赖。
OpenFeign也支持第三方接口的调用,只需要在注解属性中写明详细URL地址即可。

1
2
3
4
5
6
7
8
9
10
11
12
@FeignClient(value = "service-product")
public interface ProductFeignClient {
    // MVC同款注解,在这里是代表发送请求的方式
    @GetMapping("/product/{id}")
    Product getProductById(@PathVariable("id") Long id, @RequestParam("token") String token);
}

// FeignClient注入
@Autowired
ProductFeignClient productFeignClient;
// 方法调用
Product product = productFeignClient.getProductById(pruductId);

使用OpenFeign进行的远程调用自带负载均衡,通过配置文件可以修改超时控制、重试等相关参数。

This post is licensed under CC BY 4.0 by the author.

Trending Tags