“ 在失控边缘冲杀为,最终解脱”
CMD与EntryPoint实战
EntryPoint 与 CMD都是docker 镜像制作中的一条命令,它们在概念上可能有些相似,但在使用中,两者是有明显的区别的。比如,执行一个没有调用EntryPoint、CMD的容器会返回错误,这两条命令一般都作为容器启动的入口命令~
🎨 覆盖
编写Dockerfile时,一旦使用了EntryPoint、CMD命令都会覆盖之前默认的EntryPoint、CMD命令集。我们在Docker镜像运行时,也可以通过携带 “命令+参数”,覆盖CMD;如果设置了EntryPoint,这些命令集又会被当成参数,喂给EntryPoint作为参数。或是携带 ”--entrypoint“用于覆盖EntryPoint中的命令。
如果你只希望Docker容器在运行时,只执行一个具体的程序,建议使用EntryPoint。
🎨 Shell 与 EXEC模式
ENTRYPOINT 与 CMD指令支持两种不同的写法: shell 、 EXEC
CMD指令写法:
# EXEC FORM
CMD ["excutable","param1","param2"]
# 用于给 EntryPoint传输参数
CMD ["param1","param2"]
# shell FORM
CMD command param1 param2
ENTRYPOINT指令写法:
# EXEC FORM
ENTRYPOINT ["exutable","param1","param2"]
# Shell FORM
ENTRYPOINT command param1 param2
使用shell表示法,言外之意。这些命令终将是喂给 shell程序来执行的!
即 —— docker使用: /bin/sh -c 的语法调用
使用EXEC语法,不会启动 /bin/sh,而是直接运行命令,该命令PID=1
使用docker ps就可以看到实际运行的命令模式~
因此,无论你选择CMD或是ENTRYPOINT,都最后选用 ”EXEC表示法”。
🎨 组合模式
组合使用ENTRYPOINT 与 CMD时,ENTRYPOINT作为默认的运行命令,CMD指定运行参数。当ENTRYPOINT与CMD同时存在时,docker会把CMD中的命令都拼接在ENTRYPOINT之后,并最终执行命令~
实战步骤
💎 多次覆盖
我们创建一个Dockerfile文件,指定多个CMD,如下:
构建镜像,查看运行结果,我们发现三条命令中,只打印了一句~
我们继续创建第二个 dockerfile2,指定多个EntryPoint:
构建、运行镜像:
💎 参数覆盖
我们通过指定后面启动的参数,可以覆盖CMD的指令,但却无法对ENTRYPOINT进行覆盖~
当我们指定 --entrypoint时,才会覆盖dockerfile中的ENTRYPOINT默认命令:
💎 Shell vs EXEC
我们编写Dockerfile,让容器启动时,自动运行ping命令~
编译运行镜像,进入镜像之中,查看进程ID,我们可以看到PID为1的进程为 /bin/sh。
我们新建Dockerfile4,并采用EXEC的模式:
编译运行镜像后,我们进入到容器中查看进程PID,此时发现PID为1的进程变为PING~
使用entrypoint指令也是同样的结果,这里也就不再过多实验~
💎 组合
我们新建Dockerfile5,同时设置ENTRYPOINT、CMD:
此时,我们编译镜像+运行镜像看看是什么效果:
其中CMD中的内容,作为ENTRYPOINT的参数,添加到了后面~
又因为,我们可以在docker run启动时,通过命令行替换CMD中的内容,所以,我们又可以进行如下的执行:
Dockerfile搭建Mysql主从集群
build功能
在 docker-cmpose.yml文件中,使用build选项 编译镜像:
# 方式一
services:
frontend:
image: awesome/webapp
build: ./webapp
# 注解:
该镜像的构建,是由 "awesome"的子目录“webapp"决定。
如果该文件(webapp) 缺少dockerfile 就会发生报错
# 方式二
backend:
image: awesome/database
build:
context: ./backend
dockerfile: ./backend.dockerfile
# 注解:
"./backend"作为镜像构建的上下文,这个"./backend"需要是一个子目录
"./backend.dockerfile" 与之是同级目录,其路径与"./backend"是相关的都是子目录
Mysql主从同步原理
什么是Mysql的主从同步?
所谓主从 —— 即一个主节点,多个从节点的模式。在Mysql中,主节点的Mysql服务器上的数据可以通过一定的方式,“同步复制”给其他从节点,从而保证主、从节点数据的一致性。
Mysql默认采用异步复制的方式,这样的好处在于,从节点不用频繁地找主节点更新拷贝新数据,数据的更新可以放在连接上。
为什么需要Mysql主从同步?
🎯 读写分离,性能提升: 让主库负责写,从库负责读。这样当主库进行写触发锁机制时,因为有从库的存在,也不会停止提供读服务。
🎯 数据实时备份: 主数据库实时保存,当主节点突然宕机、挂掉,主库可以去从库哪里找到历史数据~
🎯 高可用: 某个节点发送故障,仍然有其他节点提供服务~不会致使整个服务瘫痪~
主从同步架构
在主从复制中存在3个线程用来执行这个过程,一个是"binlog dump thread",该线程位于master节点上,另外两个线程分别为 I/O 、SQL线程,它们分别存在于从节点上。
同步过程:
🎃 当master接收到一个写请求(增\删除\改),这些操作都会被记录进 binlog 之中.
🎃 master节点会为每一个 slave节点(前提是,slave节点连接到了master节点上),分别创建一个 线程(binlog dump thread),并将binlog中的内容通过线程发送给各slave节点。
🎃 binlog dump thread线程会 "互斥地"(加锁)读取master节点上的binlog日志,并将该日志信息发送到 slave节点的 I/O上
🎃 slave节点的 I/O接收到binlog日志信息后,会将其存放到本节点上的 relaylog中~
🎃 slave启用SQL THREAD,前去读取relaylog中的内容,将其具体解析成执行的SQL,并执行这些SQL,实现某种意义上的还原~
从而实现一种 主从节点数据上,一致性的现象。
Binlog
Binlog本质上虽然是一个二进制文件,但其内部存储的是一个一个的事件~ 所谓的事件就是指使用数据库过程中产生的各个SQL 指令: INSERT、UPDATE、DELETE等等。
主库每提交一次”事务“(即,一组持有原子性、持久性、隔离性、一致性语句的逻辑),都会把数据进行变更,记录到一个 二进制文件之中 —— binlog。
参数值 | 含义 | 缺点 |
Statement | 记录原始的SQL语句 | SQL中包含了每次执行结果不一致的函数、触发器时,同步数据时会造成不一致 |
row | 记录了数据被更改的具体值 | 每条数据的更改被详细记录,如整表删除,alter表等操作涉及的数据行都会记录,ROW格式会产生大量日志。 |
mixed | 混合模式以上两种格式的混合版 | 无法对误操作数据进行单独恢复。 |
主从同步方式
💰 全同步方式:
当主库处理执行完一个事务之后,要求所有的从库也必须执行完该事务,才可以继续返回处理客户端的请求。这样虽然能够极大程度上保障主从节点数据的一致性,但却带来的是请求处理性能的损耗,从库宕机也会对主库产生影响。
💰 异步同步方式:
这是Mysql默认采取的主从同步方式。主库在执行完客户端提交的事务之后,立即返回结果给客户端,并不关心从库,是否已经同步完成 新增数据信息~
所以,这必然会在某一个周期时间内,主从库数据会产生不一致的问题。而且,一旦主库宕机挂掉,此时的binlog可能还没有发送新提交事务的信息,就会产生数据丢失问题。由此,异步同步方式虽然性能上比 同步方式下更优,但是数据安全、一致性问题上显得欠缺~
💰 半同步方式:
基于异步同步方式的缺陷,mysql在5.5版本退出,半同步复制。其本质就是对传统异步复制的改进。在Master事务的commit提交之前,必须确保slave收到一个relay log并且响应ACK,简单来说,就是新增了一个从库的 反馈机制。
在Mysql5.7版本中,又新增一个参数: "rpl_semi_sync_master_wait_for_slave_count"。这个参数是用来干嘛的呢 ? 你可以把它类似于一种投票机制,默认设置为1。也就是说,一旦一个从库进行了响应,那么就可以告诉主节点,可以返回给客户端了。 当这个参数设置得越大,也就说明需要从库进行确认的个数越多,更大程度上地提升数据一致性的强度,但也会增加咱们主库等待ACK响应的时延~
不过,半同步方式也存在一系列的问题:
⌛ 性能较低: 异步复制一旦客户端进行commit提交,立马就能得到返回响应。但,半同步复制则需要等待至少一个库发送 确认收到relaylog后,才能进行返回客户端。
⌛ 主库等待的最大时长可以进行配置的,一旦超过了配置的时间,半同步复制就会演变为异步复制,异步复制的问题也会显现~
⌛ 半同步复制还会存在幻读问题!!
所谓幻读,其本质就是一种 “不可重复读“问题的一种~
主要针对的是在执行了Insert 插入语句后,可能导致的事务前后查询数据 不一致的问题
💰 增强半同步方式:
看这个方式的名字也就知晓,这是半同步复制的一种改进,原理上几乎与半同步复制一样,但解决了其遗留的幻读问题!
其核心在于:
主库配置了新的参数 "rpl_semi_sync_mater_wait_point=AFTER_SYNC"。现在的主库不再将写入binlog中的内容,立即同步给 ”存储引擎“,而是直到收到Slave的relay log的ACK后,才能进行提交存储引擎,完成向客户端的请求反馈、处理。
💰 组复制:
Mysql官方在5.7.17版本中,正式推出组复制(Mysql Group Replication) —— MGR。
由若干个节点共同组织成一个组,一个事务的提交,必须经由绝大多数组内节点的确认 —— (N / 2 + 1)。例如,如果是由3个Mysql服务器共同组成的组复制,在事务提交的过程中,至少需要2个节点决议,是否通过这个提交决议~
引入组复制,根本上是为解决传统异步复制、半同步复制存在的数据不一致性的问题。
不过,MGR的解决方案是也有一定的局限,如仅由Innodb表能够支持,并且对表的结构也有一顶的要求……
Mysql主从形式
👑 一主一从(多):
👑 双主复制:
每个master都是对端master的slave,任何一方做变更,都会复制应用到另一方。
👑 级联复制:
级联复制下,部分slave的数据同步不跟随主节点,而是连接的从节点。通过增加replication层,可以缓解主节点replication产生的性能损耗,对数据一致性也没什么负面影响,但数据同步时的时延性会增加~
Mysql主从集群搭建
搭建步骤:
⛳ 创建主库,并在主库中创建单独的Mysql用户,用于数据的同步,授予该用户所有权限。
⛳ 创建从库,配置从库的数据,同步到主库。
⛳ 启动从库,开始同步。
配置Mysql文件 + Dockerfile:
咱们选择的主从模式是一主多从,创建一个主节点两个从节点:
创建主库的配置sql脚本:
# 创建新用户
CREATE USER 'root'@'%' IDENTIFIED BY 'root';
# 让"root"授予访问 slave1 slave2 的所有权限
grant replication slave1,replication slave2 on *.* to 'root'@'%';
flush privileges;
创建从库的配置sql脚本:
change master to master_host='mysql-server';
# 填写主节点的用户信息
master_user='root',master_password='root',master_port=3306;
start slave;
进入"mysql-cluster-master"目录下,创建并配置主库Dockerfile文件 —— "Dockerfile-master":
这里本质上就是将Mysql初始化库的启动命令更换为了咱们写的sql语句。并且进行了时间同步,可以看到Linux系统中有许多地区、城市的时间信息~
FROM mysql:5.7
RUN ls /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY ./master/master.sql /docker-entrypoint-initdb.d
还有Dockerfile-slave:
FROM mysql:5.7
RUN ls /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY ./slave/slave.sql /docker-entrypoint-initdb.d
编写docker-compose.yml 统一编排容器
我们进入到 "/root/mysql-cluster/",创建.yml文件:
version: "3.6"
services:
mysql-master:
build:
context: ./
dockerfile: ./master/Dockerfile-master
image: mysqlmaster:v1.0
restart: always
container_name: mysql-master
volumes:
- ./mastervarlib:/var/lib/mysql
ports:
- 8080:3306
environment:
MYSQL_ROOT_PASSWORD: root
privileged: true
command: ['--server-id=1',
'--log-bin=master-bin',
'--binlog-ignore-db=mysql',
'--binlog_cache_size=256M',
'--binlog_format=mixed',
'--lower_case_table_names=1',
'--character-set-server=utf8',
'--collation-server=utf8_general_ci']
mysql_slave:
build:
context: ./
dockerfile: ./slave/Dockerfile-slave
image: mysqlslave:v1.0
restart: always
container_name: mysql-slave
volumes:
- ./slavevarlib:/var/lib/mysql
ports:
- 8081:3306
environment:
- MYSQL_ROOT_PASSWORD=root
privileged: true
command: ['--server-id=2',
'--relay-log=slave-relay',
'--lower_case_table_names=1',
'--character-set-server=utf8',
'--collation-server=utf8_general_ci']
depends_on:
- mysql-master
mysql_slave2:
# build:
# context: ./
# dockerfile: ./slave/Dockerfile-slave
image: mysqlslave:v1.0
restart: always
container_name: mysql-slave2
volumes:
- ./slavevarlib2:/var/lib/mysql
ports:
- 8082:3306
environment:
- MYSQL_ROOT_PASSWORD=root
privileged: true
command: ['--server-id=3',
'--relay-log=slave-relay',
'--lower_case_table_names=1',
'--character-set-server=utf8',
'--collation-server=utf8_general_ci']
depends_on:
- mysql-master
我们使用 "docker compose config" 检查.yml是否编写规范~
没问题后,我们就可以选择构建镜像了:
启动服务进行测试,这些容器能够正常启动~
连接上主库,查看数据库~
查看数据库角色、同步状态、连接上的从库信息等~
SHOW MASTER STATUS\G
SHOW SLAVE STATUS\G
master:
slave:
在主库上创建数据:
查看从库上数据的同步写入~
我们可以瞧见,最终完成了同步~
Dockerfile搭建Redis主从集群
修改redis.conf文件
我们可以在Windows上先下载redis7.x的源码: Download | Redis
亦或是在linux机器下,使用wget命令:
https://codeload.github.com/redis/redis/tar.gz/refs/tags/7.2.4
准备目录,将下载的redis源码放在新建目录中:
找到redis中的配置文件模板,完成以下redis.conf内容的修改,把该文件继续放在新建目录("/data/wgzzs/rediscluster/redis"):
编写docker-file
在"/data/wgzzs/rediscluster/redis"编写Dockerfile-redis文件,用于构建自己的redis镜像~
通过"docker build -t"来构建镜像:
启动一个容器测试看看能否正常运行~
清理容器资源,之后会启动redis集群,需要的服务资源也会增多~
进行容器编排 —— 编写docker-compose.yml
我们需要启动多个容器而不再是一个了!又因为咱们的redis容器需要自定义所以采用了dockerfile编写,现在需要启动多个容器则需要用到 —— 容器编排~
“docker compose config”检查编写符合规范~
执行完成构建镜像:
值得注意的是:当我们设置.context时,就是以这个路径去寻找相对路径下的文件
启动服务,查看服务状态:
查看redis07打印的日志,是否正确 —— 哈希槽已经被各个节点支配~
我们再次进入redis01 ~ redis07任意一个容器中,检查容器功能是否正常~
# 进入容器
docker exec -it redis01 bash
# -c 插入key值时,会自动重定向 映射的slots分片
/redis/redis-cli -c -a 123456
查看集群节点详情信息~
我们可以插入一些 {Key,Value}~
完成资源释放,咱们使用Dockerfile搭建的Redis集群也就得到了一个圆满的成功~
Dockerfile结合Docker-Compose搭建C++微服务
构建C++微服务
之前我们在容器中搭建的C++程序,一旦运行完成后就会自动退出~我们现在打算使用Dockerfile构建一个长时间运行的 C++服务器。
这个服务器的功能很简单,就是一问一答,返回一个nginx经典的html~
构建相应目录:
“/data/wgzzs/webcpp/cppweb”中,编写源代码main.cpp:
这只是一个简短的C-S客户端模型,用来给客户端返回前端编写的html文件(这个文件我们借用的是Nginx页面)~
#include <iostream>
#include <cstring>
#include <cassert>
#include <fstream>
#include <string>
#include <netinet/in.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
using namespace std;
struct pthread_data
{
struct sockaddr_in client_addr;
int clientfd;
};
void InitNet(struct sockaddr_in &local, int &sock_fd)
{
// init addr
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_addr.s_addr = htons(INADDR_ANY);
local.sin_port = htons(8011);
// socket()
int opt = 1;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
// reuse addr
assert(sock_fd >= 0);
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// bind()
if (bind(sock_fd, (struct sockaddr *)&local, sizeof local) < 0)
{
perror("bind error");
cout << strerror(errno) << endl;
}
// listen()
assert(listen(sock_fd, 5) == 0);
cout << "WebCpp-Server Created..." << endl;
}
void readfile(string *body)
{
ifstream ifs("index.html", std::ios::binary);
if (ifs.is_open() == false)
{
perror("readfile");
return;
}
size_t fsize = 0;
ifs.seekg(0, ifs.end);
fsize = ifs.tellg();
ifs.seekg(0, ifs.beg);
if (fsize > 0)
{
body->resize(fsize);
ifs.read(&(*body)[0], fsize);
}
ifs.close();
}
void *serverHandler(void *args)
{
struct pthread_data *pdata = (struct pthread_data *)args;
int conn_fd = pdata->clientfd;
std::cout << "Handler Task: " << pdata->clientfd << endl;
char request[1024];
int len = recv(conn_fd, request, 1024, 0);
assert(len >= 0);
// cout << "Message From Cli: " << request << endl;
// char header[128] = "HTTP/1.1 200 ok\r\n";
// char body[128] = "connection:close\r\n";
// char blank_row[4] = "\r\n";
char response_head[128] = "HTTP/1.1 200 ok\r\nconnection:close\r\n\r\n";
// 首行+报头
int s = send(conn_fd, response_head, strlen(response_head), 0);
assert(s > 0);
// 正文
string body;
readfile(&body);
if(body.size() > 0) assert(send(conn_fd, body.c_str(), body.size(), 0) > 0);
close(conn_fd);
return nullptr;
}
int main()
{
int sock_fd;
struct sockaddr_in local;
InitNet(local, sock_fd);
// 作为监听
while (1)
{
struct sockaddr_in client;
int client_len = sizeof(client);
int conn_fd = accept(sock_fd, (struct sockaddr *)&client, (socklen_t *)&client_len);
// 分配线程去 完成任务
struct pthread_data data;
data.client_addr = client;
data.clientfd = conn_fd;
assert(data.clientfd >= 1);
cout << "Get a new Link:" << data.clientfd << "~~" << endl;
pthread_t pt;
pthread_create(&pt, NULL, serverHandler, (void *)&data);
}
return 0;
}
启动服务器,我们来看看效果~
服务器访问效果:
结果是,我们的程序能够正确地执行,达到我们想要的效果~
编写C++ Dockerfile应用
我们现在要做的,就是将这份程序制作成镜像,并让它在咱们的容器中运行。
进入 “/data/wgzzs/webcpp/cppweb” 目录,编写Dockerfile~
进入 "/data/wgzzs/webcpp/nginx目录,编写bit.conf,用于使用nginx作为负载均衡器的功能~
我们还需要继续编写nginx的Dockerfile:
C++微服务容器编排
进入"/data/wgzzs/webcpp"目录,编写docker-compose.yml文件:
version: "3.6"
services:
mywebcpp1:
image: mywebcpp:v1.0
build:
context: ./cppweb
mywebcpp2:
image: mywebcpp:v1.0
mywebcpp3:
image: mywebcpp:v1.0
web:
image: mynginx:v1.0
build:
context: ./nginx
ports:
- 8112:80
depends_on:
mywebcpp1:
condition: service_started
mywebcpp2:
condition: service_started
mywebcpp3:
condition: service_started
构建镜像:
启动服务,从nginx的日志来看,启动是没有任何问题的~
访问nginx服务器6次,我们看看nginx是否能够将连接打在其他cpp服务器中~
我们都可以得知,连接请求被负载到了不同的服务器容器中~
清理资源,完成咱们最后的收尾工作~
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~