开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

FastApi+Vue+LayUI实现前后端分离 前言 项目

发表于 2021-11-17

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

前言

在前面的Api开发中,我们使用FastApi已经可以很好的实现。但是实际使用中,我们通常建议前后端项目分离。今天我们就使用FastApi+Vue+LayUI做一个前后端分离的Demo。

项目设计

后端

后端我们采用FastApi在新的test视图中,定义一个路由,并将其注册到app中,并且在test视图中定义一个接口,实现模拟从数据库读取数据供前端调用渲染。

代码

test.py

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
python复制代码from fastapi import FastAPI,Depends,Header,HTTPException,APIRouter
from fastapi.param_functions import Body
from starlette.requests import Request
from starlette.templating import Jinja2Templates
from starlette import status
import uvicorn
from deta import Deta
from fastapi.responses import StreamingResponse
from fastapi.responses import JSONResponse

# 实例化路由器
router = APIRouter()
templates = Jinja2Templates('templates')

# 注意,视图这里使用router来声明请求方式&URI
@router.get('/info')
def user_list():
# vue的响应数据
items = [
{'id':'1','name':'phyger'},
{'id':'2','name':'fly'},
{'id':'3','name':'enheng'},
]
return JSONResponse(content=items)

@router.get('/')
def welcome():
return "这里是测试路由"

'''
实际上,这里的home.html也是需要前端服务去向用户渲染的,
但是我们为了方便演示,未启动前端服务器,直接将前端代码写在了home.html中,
实际上,当用户请求/check的时候,前端代码会去请求/info接口获取数据,
从而实现前端页面的数据渲染。
'''

@router.get('/check')
def home(request:Request):
return templates.TemplateResponse(name='home.html',context={'request':request,})

前端

前端我们直接导入Vue、LayUI、Axios的JS和CSS的CDN资源,在Vue实例的mount阶段,使用axios调用后端接口拿到数据,使用LayUI的样式对table元素进行美化。

代码

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@next"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- 引入 layui.css -->
<link rel="stylesheet" href="https://www.layuicdn.com/layui/css/layui.css"/>

<!-- 引入 layui.js -->
<script src="https://www.layuicdn.com/layui/layui.js" type="text/javascript" charset="utf-8"></script>
<title>Home</title>
</head>
<body>
<div id="app">
<table class="layui-table">

<tr v-for="p in infos">
<td>[[ p.id ]]</td>
<td>[[ p.name ]]</td>
</tr>

</table>
</div>
<table id="test" class="layui-table"></table>


<script type="text/javascript">
const Vapp = Vue.createApp({
data() {
return {
infos: [{id:1,name:'phyger'}],
info: "hello vue..."
}
},
mounted() {
this.showinfo();
},
methods: {
showinfo(){
axios.get('/test/info')
.then(response=>{
this.infos=response.data;
console.log(response);
console.log(this.infos);

})
,err=>{
console.log(err);
};
},
},
})
Vapp.config.compilerOptions.delimiters = ['[[', ']]']
Vapp.mount('#app')
</script>
</body>

</html>

运行项目

启动 FastApi 后端服务器,访问 /test/check 接口。

效果

Q&A

Q:为什么在请求/info 接口总会出现一个 Temporary Redirect 重定向呢?

image

A:原因是因为我们在 FastApi 接口定义的时候,uri 的格式不规范导致,uri 的结尾不需要/,如果你接口增加了/,我们使用浏览器访问 uri,浏览器会忽略结尾的/,FastApi 会在内部进行查重定向,将浏览器不带/的请求重定向到我们定义的带/的视图函数上。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker入门笔记 1、Docker概述 2、虚拟化技术和

发表于 2021-11-17

1、Docker概述

1.1 基本介绍

Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。

Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

Docker 从 17.03 版本之后分为 CE(Community Edition: 社区版) 和 EE(Enterprise Edition: 企业版),我们用社区版就可以了。官网:docs.docker.com/

2.2 应用场景

  • Web 应用的自动化打包和发布。
  • 自动化测试和持续集成、发布。
  • 在服务型环境中部署和调整数据库或其他的后台应用。
  • 从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。

2.3 Docker的优势

Docker 是一个用于开发,交付和运行应用程序的开放平台。Docker 使您能够将应用程序与基础架构分开,从而可以快速交付软件。借助 Docker,您可以与管理应用程序相同的方式来管理基础架构。通过利用 Docker 的方法来快速交付,测试和部署代码,您可以大大减少编写代码和在生产环境中运行代码之间的延迟。

1、快速,一致地交付您的应用程序。Docker 允许开发人员使用您提供的应用程序或服务的本地容器在标准化环境中工作,从而简化了开发的生命周期。

容器非常适合持续集成和持续交付(CI / CD)工作流程,请考虑以下示例方案:

您的开发人员在本地编写代码,并使用 Docker 容器与同事共享他们的工作。
他们使用 Docker 将其应用程序推送到测试环境中,并执行自动或手动测试。
当开发人员发现错误时,他们可以在开发环境中对其进行修复,然后将其重新部署到测试环境中,以进行测试和验证。
测试完成后,将修补程序推送给生产环境,就像将更新的镜像推送到生产环境一样简单。
2、响应式部署和扩展
Docker 是基于容器的平台,允许高度可移植的工作负载。Docker 容器可以在开发人员的本机上,数据中心的物理或虚拟机上,云服务上或混合环境中运行。

Docker 的可移植性和轻量级的特性,还可以使您轻松地完成动态管理的工作负担,并根据业务需求指示,实时扩展或拆除应用程序和服务。

3、在同一硬件上运行更多工作负载
Docker 轻巧快速。它为基于虚拟机管理程序的虚拟机提供了可行、经济、高效的替代方案,因此您可以利用更多的计算能力来实现业务目标。Docker 非常适合于高密度环境以及中小型部署,而您可以用更少的资源做更多的事情。

2、虚拟化技术和容器技术

虚拟化技术特点:1.资源占用多 2.冗余步骤多 3.启动很慢

容器化技术:容器化技术不是模拟的一个完整的操作系统

比较Docker和虚拟机的不同:
1.传统虚拟机,虚拟出硬件,运行一个完整的操作系统,然后在这个系统上安装和运行软件。

2.Docker容器内的应用直接运行在宿主机的内容,容器是没有自己的内核的,也没有虚拟硬件。

3.每个容器都是相互隔离的,每个容器都有属于自己的文件系统,互不影响。

容器化带来的好处:

  • 应用更快的交付和部署
  • 更便捷的升级和扩缩容
  • 更简单的系统运维
  • 更高效的计算资源利用

3、Docker的基本组成

image.png

镜像images:
docker镜像就好比是一个模板,可以通过这个模板来创建容器服务,tomcat镜像==>run==>tomcat01容器,通过这个镜像可以创建多个容器(最终服务运行或者项目运行就是在容器中)
容器container
Docker利用容器技术,独立运行一个或者一个组应用,通过镜像来创建的
启动,停止,删除,基本命令
目前就可以把这个容器理解为就是一个简易的linux系统
仓库repository
仓库就是存放镜像的地方
仓库分为共有仓库私有仓库
Docker Hub(默认是国外的)

4、Docker的安装

查看系统的内核:
uname -r

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码[root@iZuf682v6un2qg2cl6bgijZ ~]# uname -r
3.10.0-1160.31.1.el7.x86_64

# 查看系统配置 cat /etc/os-release

NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

Docker安装步骤

  1. 卸载旧版本
1
2
3
4
5
6
7
8
shell复制代码yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
  1. 下载需要的安装包
1
shell复制代码yum install -y yum-utils
  1. 设置镜像的仓库
1
2
3
4
5
6
7
8
shell复制代码yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo #国外的地址

# 设置阿里云的Docker镜像仓库
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo #国外的地址
  1. 更新yum软件包索引
1
shell复制代码yum makecache fast
  1. 安装docker相关的配置
    docker-ce 是社区版,docker-ee 企业版
1
shell复制代码yum install docker-ce docker-ce-cli containerd.io
  1. 启动Docker
1
2
3
4
shell复制代码systemctl start docker 
# 查看当前版本号,是否启动成功
docker version
# 设置开机自启动 systemctl enable docker

image.png

5、Docker的卸载

1
2
3
4
shell复制代码# 1. 卸载依赖 
yum remove docker-ce docker-ce-cli containerd.io
# 2. 删除资源 . /var/lib/docker是docker的默认工作路径
rm -rf /var/lib/docker

6、配置阿里云镜像加速

  1. 进入阿里云官网,搜索容器服务

image.png

  1. 依次执行官方的这四条命令
1
2
3
4
5
6
7
8
shell复制代码sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://axvfsf7e.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

7、Docker容器运行流程

image.png

8、底层原理

Docker是一个Client-Server结构的系统,Docker的守护进程运行在主机上,通过Socker从客户端访问!Docker Server接收到Docker-Client的指令,就会执行这个指令!

image.png

Docker为什么比VM Ware快?

1、Docker比虚拟机更少的抽象层

2、docker利用宿主机的内核,VM需要的是Guest OS

image.png
Docker新建一个容器的时候,不需要像虚拟机一样重新加载一个操作系统内核,直接利用宿主机的操作系统,而虚拟机是需要加载Guest OS。Docker和VM的对比如下:

image.png

9、Docker常用命令

9.1 基础命令

1
2
3
4
shell复制代码docker version # 查看docker的版本信息
docker info # 查看docker的系统信息,包括镜像和容器的数量
docker 命令 --help # 帮助命令
docker COMMAND --help

命令的帮助文档地址:docs.docker.com/engine/refe…

9.2 镜像命令

  1. docker images 查看本地主机的所有镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码[root@iZuf682v6un2qg2cl6bgijZ ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 04661cdce581 7 days ago 141MB
tomcat 9.0 43e421a14aec 3 weeks ago 680MB
tomcat latest b0e0b0a92cf9 3 weeks ago 680MB
mysql 5.7 938b57d64674 4 weeks ago 448MB
mysql latest ecac195d15af 4 weeks ago 516MB
redis latest 7faaec683238 5 weeks ago 113MB
hello-world latest feb5d9fea6a5 7 weeks ago 13.3kB
centos latest 5d0da3dc9764 2 months ago 231MB
portainer/portainer latest 580c0e4e98b0 8 months ago 79.1MB

#解释:
1.REPOSITORY 镜像的仓库源
2.TAG 镜像的标签
3.IMAGE ID 镜像的id
4.CREATED 镜像的创建时间
5.SIZE 镜像的大小

-a/--all 列出所有镜像
-q/--quiet 只显示镜像的id
  1. docker search 搜索镜像
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
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker search mysql
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
mysql MySQL is a widely used, open-source relation… 10308 [OK]
mariadb MariaDB is a community-developed fork of MyS… 3819 [OK]
mysql/mysql-server Optimized MySQL Server Docker images. Create… 754 [OK]
percona Percona Server is a fork of the MySQL relati… 517 [OK]
centos/mysql-57-centos7 MySQL 5.7 SQL database server 86
mysql/mysql-cluster Experimental MySQL Cluster Docker images. Cr… 79
centurylink/mysql Image containing mysql. Optimized to be link… 60 [OK]


#可选参数

Search the Docker Hub for images

Options:
-f, --filter filter Filter output based on conditions provided
--format string Pretty-print search using a Go template
--limit int Max number of search results (default 25)
--no-trunc Don't truncate output


#搜索收藏数大于3000的镜像
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker search mysql --filter=STARS=3000
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
mysql MySQL is a widely used, open-source relation… 10308 [OK]
mariadb MariaDB is a community-developed fordockerk of MyS… 3819 [OK]
  1. docker pull 镜像名[:tag] 下载镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker pull mysql
Using default tag: latest #如果不写tag默认就是latest
latest: Pulling from library/mysql
6ec7b7d162b2: Pull complete #分层下载,docker image的核心-联合文件系统
fedd960d3481: Pull complete
7ab947313861: Pull complete
64f92f19e638: Pull complete
3e80b17bff96: Pull complete
014e976799f9: Pull complete
59ae84fee1b3: Pull complete
ffe10de703ea: Pull complete
657af6d90c83: Pull complete
98bfb480322c: Pull complete
6aa3859c4789: Pull complete
1ed875d851ef: Pull complete
Digest: sha256:78800e6d3f1b230e35275145e657b82c3fb02a27b2d8e76aac2f5e90c1c30873 #签名
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest #下载来源的真实地址 #docker pull mysql等价于docker pull docker.io/library/mysql:latest

指定版本下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker pull mysql:5.7
5.7: Pulling from library/mysql
6ec7b7d162b2: Already exists
fedd960d3481: Already exists
7ab947313861: Already exists
64f92f19e638: Already exists
3e80b17bff96: Already exists
014e976799f9: Already exists
59ae84fee1b3: Already exists
7d1da2a18e2e: Pull complete
301a28b700b9: Pull complete
529dc8dbeaf3: Pull complete
bc9d021dc13f: Pull complete
Digest: sha256:c3a567d3e3ad8b05dfce401ed08f0f6bf3f3b64cc17694979d5f2e5d78e10173
Status: Downloaded newer image for mysql:5.7
docker.io/library/mysql:5.7
  1. docker rmi 删除镜像
1
2
3
4
5
6
shell复制代码#1.删除指定的镜像id
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker rmi -f 镜像id
#2.删除多个镜像id
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker rmi -f 镜像id 镜像id 镜像id
#3.删除全部的镜像id
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker rmi -f $(docker images -aq)

9.3 容器命令

如拉取一个centos容器
docker pull centos

运行容器

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码docker run [可选参数] image

#参数说明
--name="名字" 指定容器名字
-d 后台方式运行
-it 使用交互方式运行,进入容器查看内容
-p 指定容器的端口
(
-p ip:主机端口:容器端口 配置主机端口映射到容器端口
-p 主机端口:容器端口
-p 容器端口
)
-P 随机指定端口(大写的P)

进入容器

1
2
3
shell复制代码[root@iZuf682v6un2qg2cl6bgijZ ~]# docker run -it centos /bin/bash
[root@2cbad4f854bb /]# ls
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var

退出容器

1
2
3
4
5
shell复制代码#exit 停止并退出容器(后台方式运行则仅退出) 
#Ctrl+P+Q 不停止容器退出
[root@bd1b8900c547 /]# exit
exit
[root@iZwz99sm8v95sckz8bd2c4Z ~]#

列出运行过的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码#docker ps 
# 列出当前正在运行的容器
-a # 列出所有容器的运行记录
-n=? # 显示最近创建的n个容器
-q # 只显示容器的编号


[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bca129320bb5 centos "/bin/bash" 4 minutes ago Exited (0) 3 minutes ago optimistic_shtern
bd1b8900c547 centos "/bin/bash" 6 minutes ago Exited (0) 5 minutes ago cool_tesla
cf6adbf1b506 bf756fb1ae65 "/hello" 5 hours ago Exited (0) 5 hours ago optimistic_darwin

删除容器

1
2
3
shell复制代码docker rm 容器id                 #删除指定的容器,不能删除正在运行的容器,强制删除使用 rm -f
docker rm -f $(docker ps -aq) #删除所有的容器
docker ps -a -q|xargs docker rm #删除所有的容器

启动和停止容器

1
2
3
4
shell复制代码docker start 容器id          #启动容器
docker restart 容器id #重启容器
docker stop 容器id #停止当前运行的容器
docker kill 容器id #强制停止当前容器

9.4 其他常用命令

1. 日志的查看

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
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker logs --help

Usage: docker logs [OPTIONS] CONTAINER

Fetch the logs of a container

Options:
--details Show extra details provided to logs
-f, --follow Follow log output
--since string Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)
-n, --tail string Number of lines to show from the end of the logs (default "all")
-t, --timestamps Show timestamps
--until string Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)

常用:
docker logs -tf 容器id
docker logs --tail number 容器id #num为要显示的日志条数


#docker容器后台运行,必须要有一个前台的进程,否则会自动停止
#编写shell脚本循环执行,使得centos容器保持运行状态
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -d centos /bin/sh -c "while true;do echo hi;sleep 5;done"
c703b5b1911ff84d584390263a35707b6024816e1f46542b61918a6327a570dc
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c703b5b1911f centos "/bin/sh -c 'while t…" 13 seconds ago Up 10 seconds pedantic_banach
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker logs -tf --tail 10 c703b5b1911f
2020-12-27T03:34:07.255599560Z hi
2020-12-27T03:34:12.257641517Z hi
2020-12-27T03:34:17.259706294Z hi
2020-12-27T03:34:22.261693707Z hi
2020-12-27T03:34:27.262609289Z hi
2020-12-27T03:34:32.267862677Z hi
2020-12-27T03:34:37.270382873Z hi
2020-12-27T03:34:42.272414182Z hi
2020-12-27T03:34:47.274823243Z hi
2020-12-27T03:34:52.277419274Z hi

2. 查看容器中进程信息

1
2
3
4
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker top c703b5b1911f
UID PID PPID C STIME TTY TIME CMD
root 11156 11135 0 11:31 ? 00:00:00 /bin/sh -c while true;do echo hi;sleep 5;done
root 11886 11156 0 11:43 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 5

3. 查看容器的元数据

1
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker inspect 容器id

4. 进入当前正在运行的容器
因为通常我们的容器都是使用后台方式来运行的,有时需要进入容器修改配置

方式一:

1
2
3
4
5
6
7
8
9
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it c703b5b1911f /bin/bash
[root@c703b5b1911f /]# ls
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@c703b5b1911f /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 03:31 ? 00:00:00 /bin/sh -c while true;do echo hi;sleep 5;done
root 279 0 0 03:54 pts/0 00:00:00 /bin/bash
root 315 1 0 03:56 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 5
root 316 279 0 03:56 pts/0 00:00:00 ps -ef

方式二:

1
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker attach c703b5b1911f

docker exec 进入容器后开启一个新的终端,可以在里面操作

docker attach 进入容器正在执行的终端,不会启动新的进程

拷贝容器的文件到主机中

docker cp 容器id:容器内路径 目的主机路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it c703b5b1911f /bin/bash
[root@c703b5b1911f /]# cd home
[root@c703b5b1911f home]# ls
#touch 新建文件
[root@c703b5b1911f home]# touch test.java
[root@c703b5b1911f home]# ls
test.java
[root@c703b5b1911f home]# exit
exit
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c703b5b1911f centos "/bin/sh -c 'while t…" 35 minutes ago Up 35 minutes pedantic_banach
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker cp c703b5b1911f:/home/test.java /home
[root@iZwz99sm8v95sckz8bd2c4Z ~]# ls /home
hai pan test.java

image.png

10、图形化管理工具Portaniner

Portaniner是Docker的图形化管理工具,类似的工具还有Rancher(CI/CD再用)

下载运行Portaniner镜像并运行,设置本机映射端口为8088

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码[root@localhost conf]# docker run -d -p 8088:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --privileged=true portainer/portainer
Unable to find image 'portainer/portainer:latest' locally
latest: Pulling from portainer/portainer
94cfa856b2b1: Pull complete
49d59ee0881a: Pull complete
a2300fd28637: Pull complete
Digest: sha256:fb45b43738646048a0a0cc74fcee2865b69efde857e710126084ee5de9be0f3f
Status: Downloaded newer image for portainer/portainer:latest
8c525a0137be22965bd1e3944da622a2c4248f8ad20883f4b3ea4f8a6b11e163
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7789d4505a00 portainer/portainer "/portainer" 6 seconds ago Up 5 seconds 0.0.0.0:8088->9000/tcp quirky_sinoussi

第一次登录设置admin用户的密码

image.png
如果是阿里云服务器记得设置安全组,选择连接本地的Docker,整体界面预览如下图:

image.png

11、Docker镜像详解

11.1 什么是镜像

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需要的所有内容,包括代码,运行时(一个程序在运行或者在被执行的依赖)、库,环境变量和配置文件。

11.2 Docker镜像加载原理

Docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统是UnionFS联合文件系统。

image.png

image.png

11.3 分层理解

image.png

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker image inspect nginx:latest
[
{
"Id": "sha256:ae2feff98a0cc5095d97c6c283dcd33090770c76d63877caa99aefbbe4343bdd",
"RepoTags": [
"nginx:latest"
],
"RepoDigests": [
"nginx@sha256:4cf620a5c81390ee209398ecc18e5fb9dd0f5155cd82adcbae532fec94006fb9"
],
"Parent": "",
"Comment": "",
"Created": "2020-12-15T20:21:00.007674532Z",
"Container": "4cc5da85f27ca0d200407f0593422676a3bab482227daee044d797d1798c96c9",
"ContainerConfig": {
"Hostname": "4cc5da85f27c",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.19.6",
"NJS_VERSION=0.5.0",
"PKG_RELEASE=1~buster"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"nginx\" \"-g\" \"daemon off;\"]"
],
"Image": "sha256:13bffe371b56f4aeed88218ec17d0c6f653a83b49bd3e211fc8cfa2ca5d7a3d3",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": [
"/docker-entrypoint.sh"
],
"OnBuild": null,
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGQUIT"
},
"DockerVersion": "19.03.12",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.19.6",
"NJS_VERSION=0.5.0",
"PKG_RELEASE=1~buster"
],
"Cmd": [
"nginx",
"-g",
"daemon off;"
],
"Image": "sha256:13bffe371b56f4aeed88218ec17d0c6f653a83b49bd3e211fc8cfa2ca5d7a3d3",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": [
"/docker-entrypoint.sh"
],
"OnBuild": null,
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGQUIT"
},
"Architecture": "amd64",
"Os": "linux",
"Size": 132935043,
"VirtualSize": 132935043,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/cb791e78a08db7091bf2ce1d78603f1758f52199e57f1805156fe30e39067aae/diff:/var/lib/docker/overlay2/1e73a72b25af68ee9abf4eb443f778d31226e12e9af428fcc14c7b044c83b258/diff:/var/lib/docker/overlay2/88c9c01762f2af8327db65d0b0d4a64785e87c9c2ab76c62e7d03619db03a985/diff:/var/lib/docker/overlay2/7304ab112ac4a9cb91fc6f74730be28fecbe19f042e92d321aa9181424cc4b2e/diff",
"MergedDir": "/var/lib/docker/overlay2/48b288740bbb2b07b41ed43a4d17a005c46b08d3357d2960b5ef7db4b2de6618/merged",
"UpperDir": "/var/lib/docker/overlay2/48b288740bbb2b07b41ed43a4d17a005c46b08d3357d2960b5ef7db4b2de6618/diff",
"WorkDir": "/var/lib/docker/overlay2/48b288740bbb2b07b41ed43a4d17a005c46b08d3357d2960b5ef7db4b2de6618/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:87c8a1d8f54f3aa4e05569e8919397b65056aa71cdf48b7f061432c98475eee9",
"sha256:5c4e5adc71a82a96f02632433de31c998c5a9e2fccdcbaee780ae83158fac4fa",
"sha256:7d2b207c26790f693ab1942bbe26af8e2b6a14248969e542416155a912fec30d",
"sha256:2c7498eef94aef8c40d106f3e42f7da62b3eee8fd36012bf7379becc4cd639a2",
"sha256:4eaf0ea085df254fd5d2beba4e2c11db70a620dfa411a8ad44149e26428caee4"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]

这里指示了分层信息:

1
2
3
4
5
6
7
8
9
10
shell复制代码        "RootFS": {
"Type": "layers",
"Layers": [
"sha256:87c8a1d8f54f3aa4e05569e8919397b65056aa71cdf48b7f061432c98475eee9",
"sha256:5c4e5adc71a82a96f02632433de31c998c5a9e2fccdcbaee780ae83158fac4fa",
"sha256:7d2b207c26790f693ab1942bbe26af8e2b6a14248969e542416155a912fec30d",
"sha256:2c7498eef94aef8c40d106f3e42f7da62b3eee8fd36012bf7379becc4cd639a2",
"sha256:4eaf0ea085df254fd5d2beba4e2c11db70a620dfa411a8ad44149e26428caee4"
]
},

image.png

image.png

image.png

11.4 提交镜像

1
2
3
shell复制代码使用docker commit 命令提交容器成为一个新的版本

docker commit -m=“提交的描述信息” -a="作者" 容器id 目标镜像名:[TAG]

由于默认的Tomcat镜像的webapps文件夹中没有任何内容,需要从webapps.dist中拷贝文件到webapps文件夹。下面自行制作镜像:就是从webapps.dist中拷贝文件到webapps文件夹下,并提交该镜像作为一个新的镜像。使得该镜像默认的webapps文件夹下就有文件。具体命令如下:

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
45
46
47
48
49
50
51
52
shell复制代码#1.复制文件夹
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -it tomcat /bin/bash
root@2a3bf3eaa2e4:/usr/local/tomcat# cd webapps
root@2a3bf3eaa2e4:/usr/local/tomcat/webapps# ls
root@2a3bf3eaa2e4:/usr/local/tomcat/webapps# cd ../
root@2a3bf3eaa2e4:/usr/local/tomcat# cp -r webapps.dist/* webapps
root@2a3bf3eaa2e4:/usr/local/tomcat# cd webapps
root@2a3bf3eaa2e4:/usr/local/tomcat/webapps# ls
ROOT docs examples host-manager manager
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2a3bf3eaa2e4 tomcat "/bin/bash" 4 minutes ago Up 4 minutes 8080/tcp competent_torvalds
7789d4505a00 portainer/portainer "/portainer" 24 hours ago Up 24 hours 0.0.0.0:8088->9000/tcp quirky_sinoussi
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it 2a3bf3eaa2e4 /bin/bash
root@2a3bf3eaa2e4:/usr/local/tomcat# cd webapps
root@2a3bf3eaa2e4:/usr/local/tomcat/webapps# ls
ROOT docs examples host-manager manager
root@2a3bf3eaa2e4:/usr/local/tomcat/webapps# cd ../
root@2a3bf3eaa2e4:/usr/local/tomcat# read escape sequence
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2a3bf3eaa2e4 tomcat "/bin/bash" 8 minutes ago Up 8 minutes 8080/tcp competent_torvalds
7789d4505a00 portainer/portainer "/portainer" 24 hours ago Up 24 hours 0.0.0.0:8088->9000/tcp quirky_sinoussi
#2.提交镜像作为一个新的镜像

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker commit -m="add webapps" -a="Ethan" 2a3bf3eaa2e4 mytomcat:1.0
sha256:f189aac861de51087af5bc88a5f1de02d9574e7ee2d163c647dd7503a2d3982b
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mytomcat 1.0 f189aac861de 7 seconds ago 653MB
mysql 5.7 f07dfa83b528 6 days ago 448MB
tomcat latest feba8d001e3f 10 days ago 649MB
nginx latest ae2feff98a0c 12 days ago 133MB
centos latest 300e315adb2f 2 weeks ago 209MB
portainer/portainer latest 62771b0b9b09 5 months ago 79.1MB
elasticsearch 7.6.2 f29a1ee41030 9 months ago 791MB

#3.运行容器

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -it mytomcat:1.0 /bin/bash
root@1645774d4605:/usr/local/tomcat# cd webapps
root@1645774d4605:/usr/local/tomcat/webapps# ls
ROOT docs examples host-manager manager
wz99sm8v95sckz8bd2c4Z ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mytomcat 1.0 f189aac861de 7 seconds ago 653MB
mysql 5.7 f07dfa83b528 6 days ago 448MB
tomcat latest feba8d001e3f 10 days ago 649MB
nginx latest ae2feff98a0c 12 days ago 133MB
centos latest 300e315adb2f 2 weeks ago 209MB
portainer/portainer latest 62771b0b9b09 5 months ago 79.1MB
elasticsearch 7.6.2 f29a1ee41030 9 months ago 791MB

12、常用容器部署

12.1 Nginx部署

(1)搜索并下载镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker search nginx
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
nginx Official build of Nginx. 14207 [OK]
jwilder/nginx-proxy Automated Nginx reverse proxy for docker con… 1932 [OK]
richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable of… 797 [OK]
linuxserver/nginx An Nginx container, brought to you by LinuxS… 137
jc21/nginx-proxy-manager Docker container for managing Nginx proxy ho… 123
tiangolo/nginx-rtmp Docker image with Nginx using the nginx-rtmp… 107 [OK]
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
6ec7b7d162b2: Already exists
cb420a90068e: Pull complete
2766c0bf2b07: Pull complete
e05167b6a99d: Pull complete
70ac9d795e79: Pull complete
Digest: sha256:4cf620a5c81390ee209398ecc18e5fb9dd0f5155cd82adcbae532fec94006fb9
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker images;
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql 5.7 f07dfa83b528 5 days ago 448MB
nginx latest ae2feff98a0c 11 days ago 133MB
centos latest 300e315adb2f 2 weeks ago 209MB

可以到dockerhub官网查看Nginx的详细版本信息 :hub.docker.com/_/nginx

(2)运行测试

1
2
3
4
5
shell复制代码docker run -d --name nginx01 -p 3334:80 nginx

-d 后台运行
--name 给容器命名
-p 3334:80 将宿主机的端口3334映射到该容器的80端口

运行结果:

1
2
3
4
5
6
7
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it nginx01 /bin/bash
Error: No such container: nginx01
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -d --name nginx01 -p 3334:80 nginx
20c896637ff5de8be835797109d62ee2465e28d9d716be5a8d550ef7d547fcf5
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
20c896637ff5 nginx "/docker-entrypoint.…" 7 seconds ago Up 5 seconds 0.0.0.0:3334->80/tcp nginx01

image.png

(3) 配置文件
进入容器,自定义配置文件

1
2
3
4
5
6
7
8
9
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it nginx01 /bin/bash
root@20c896637ff5:/# whereis nginx
nginx: /usr/sbin/nginx /usr/lib/nginx /etc/nginx /usr/share/nginx
root@20c896637ff5:/# cd /etc/nginx
root@20c896637ff5:/etc/nginx# ls
conf.d fastcgi_params koi-utf koi-win mime.types modules nginx.conf scgi_params uwsgi_params win-utf
root@20c896637ff5:/# cd /etc/nginx
root@20c896637ff5:/etc/nginx# ls
conf.d fastcgi_params koi-utf koi-win mime.types modules nginx.conf scgi_params uwsgi_params win-utf

(4) 访问测试

本地主机访问测试,curl命令发起请求,如果使用阿里云服务器需要设置安全组。

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
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
20c896637ff5 nginx "/docker-entrypoint.…" 7 minutes ago Up 7 minutes 0.0.0.0:3334->80/tcp nginx01
[root@iZwz99sm8v95sckz8bd2c4Z ~]# curl localhost:3334
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

12.2 Tomcat部署

(1)下载并运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker pull tomcat
Using default tag: latest
latest: Pulling from library/tomcat
6c33745f49b4: Pull complete
ef072fc32a84: Pull complete
c0afb8e68e0b: Pull complete
d599c07d28e6: Pull complete
e8a829023b97: Pull complete
d04be46a31d1: Pull complete
db6007c69c35: Pull complete
e4ad4c894bce: Pull complete
248895fda357: Pull complete
277059b4cba2: Pull complete
Digest: sha256:57dae7dfb9b62a413cde65334c8a18893795cac70afc3be589c8336d8244655d
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -d -p 3335:8080 --name tomcat01 tomcat
7136295a6082cb0f805b025a1471bde02ead4864be3e2c9dcd337b1dde0a3113

(2) 进入容器
1.容器中的命令是少了

2.阿里云镜像默认下载的是最小的镜像,保证最小的运行环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it tomcat01 /bin/bash
root@7136295a6082:/usr/local/tomcat# ls
BUILDING.txt CONTRIBUTING.md LICENSE NOTICE README.md RELEASE-NOTES RUNNING.txt bin conf lib logs native-jni-lib temp webapps webapps.dist work
root@7136295a6082:/usr/local/tomcat# cd webapps.dist
root@7136295a6082:/usr/local/tomcat/webapps.dist# ls
ROOT docs examples host-manager manager
root@7136295a6082:/usr/local/tomcat/webapps.dist# cd ROOT
root@7136295a6082:/usr/local/tomcat/webapps.dist/ROOT# ls
RELEASE-NOTES.txt WEB-INF asf-logo-wide.svg bg-button.png bg-middle.png bg-nav.png bg-upper.png favicon.ico index.jsp tomcat.css tomcat.svg
root@7136295a6082:/usr/local/tomcat/webapps.dist/ROOT# cd ../../
root@7136295a6082:/usr/local/tomcat# cd webapps
root@7136295a6082:/usr/local/tomcat/webapps# ls
root@7136295a6082:/usr/local/tomcat/webapps# cp -r /usr/local/tomcat/webapps.dist/* /usr/local/tomcat/webapps/
root@7136295a6082:/usr/local/tomcat/webapps# ls
ROOT docs examples host-manager manager
root@7136295a6082:/usr/local/tomcat/webapps# exit
exit

(3)访问测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码[root@iZwz99sm8v95sckz8bd2c4Z ~]# curl localhost:3335

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Apache Tomcat/9.0.41</title>
<link href="favicon.ico" rel="icon" type="image/x-icon" />
<link href="tomcat.css" rel="stylesheet" type="text/css" />
</head>

<body>
<div id="wrapper">
<div id="navigation" class="curved container">
<span id="nav-home"><a href="https://tomcat.apache.org/">Home</a></span>
<span id="nav-hosts"><a href="/docs/">Documentation</a></span>
<span id="nav-config"><a href="/docs/config/">Configuration</a></span>
<span id="nav-examples"><a href="/examples/">Examples</a></span>
<span id="nav-wiki"><a href="https://wiki.apache.org/tomcat/FrontPage">Wiki</a></span>
<span id="nav-lists"><a href="https://tomcat.apache.org/lists.html">Mailing Lists</a></span>
<span id="nav-help"><a href="https://tomcat.apache.org/findhelp.html">Find Help</a></span>
<br class="separator" />
</div>

12.3 ElasticSearch部署

添加 ’-e ES_JAVA_OPTS=”-Xms128m -Xmx512m” ‘ 配置ElasticSearch的虚拟机占用的内存大小。

docker stats 查看资源占用情况

1
2
3
4
5
6
7
8
9
shell复制代码$ docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms128m -Xmx512m" elasticsearch:7.6.2


[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -d --name elasticsearch01 -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms128m -Xmx512m" elasticsearch:7.6.2
3b8cd4991814896c523ee67b84ce198e32bd82b1a62d512b198138a58ca946f1
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3b8cd4991814 elasticsearch:7.6.2 "/usr/local/bin/dock…" 10 seconds ago Up 6 seconds 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp elasticsearch01
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker stats

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

netty(八)初识Netty-channel 一、chan

发表于 2021-11-17

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」。

一、channel

1.1 channel的主要方法

1)close() 可以用来关闭 channel

2)closeFuture() 用来处理 channel 的关闭,有如下两种方式

sync 方法作用是同步等待 channel 关闭
而 addListener 方法是异步等待 channel 关闭

3)pipeline() 方法添加处理器

4)write() 方法将数据写入

5)writeAndFlush() 方法将数据写入并刷出

1.2 什么是channelFuture?

我们看下面一段客户端代码,也是前面文章使用的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup(1))
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
System.out.println("init...");
ch.pipeline().addLast(new StringEncoder());
}
})
.channel(NioSocketChannel.class)
.connect("localhost", 8080)
.sync()
.channel();

channel.writeAndFlush("ccc");
Thread.sleep(1000);
channel.writeAndFlush("ccc");
}

主要看到调用connect()方法除,此处返回的其实是一个ChannelFuture 对象,通过channel()方法可以获得channel对象。

1
2
3
arduino复制代码    public ChannelFuture connect(String inetHost, int inetPort) {
return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
1
2
3
4
csharp复制代码public interface ChannelFuture extends Future<Void> {
Channel channel();
... ...
}

需要注意的是,这个connect方法是一个异步的方法,调用过后实际并没有建立连接,所以我们得到的ChannelFuture对象中并不能立刻获得正确的channel。

通过下面的例子看一下现象,启动一个服务端,端口8080,这里不提供服务端代码了,使用前面的就行。启动我们写好的测试客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csharp复制代码public class ChannelFutureTest {

public static void main(String[] args) throws Exception {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) {

}
})
.connect("localhost", 8080);

System.out.println(channelFuture.channel());

//同步等待连接
channelFuture.sync();
System.out.println(channelFuture.channel());
}
}

结果:

1
2
bash复制代码[id: 0x7aa12c28]
[id: 0x7aa12c28, L:/127.0.0.1:52286 - R:localhost/127.0.0.1:8080]

如上结果所示,首先打印只有一个id地址,当执行sync()方法,此处会同步阻塞等待连接,如果一直无法连接会抛出超时异常。当成功建立连接后,会继续执行,并打印出如上结果最后一行的内容。

除使用sync()这个同步方法以外,还有一种异步的方式:

1
2
3
4
arduino复制代码        // 异步
channelFuture.addListener((ChannelFutureListener) future -> {
System.out.println(future.channel());
});

结果:

1
2
bash复制代码[id: 0xd9d474f1]
[id: 0xd9d474f1, L:/127.0.0.1:59564 - R:localhost/127.0.0.1:8080]

ChannelFutureListener 会在连接建立时被调用(其中 operationComplete 方法),这里是一个函数式接口调用。

1.3 什么是CloseFuture?

我们同通过一段代码演示一下,此处涉及到channel的close方法,和CloseFuture的close方法。关闭是为了释放占用的资源。

看如下一段代码:

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
java复制代码public class ChannelFutureTest {

public static void main(String[] args) throws Exception {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("localhost", 8080);

// 同步等待连接
Channel channel = channelFuture.sync().channel();

new Thread(()->{
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
System.out.println("关闭channel");
// close 异步操作 1s 之后
channel.close();
break;
}
channel.writeAndFlush(line);
}
}, "input").start();

System.out.println("处理channel关闭后的操作");
}

如上代码所示,我们的客户端,允许用户手动输入q进行关闭程序,否则就发送内容到服务端。

但是按照如上代码直接运行客户端,发现System.out.println(“处理channel关闭后的操作”);这条命令直接执行了,因为我们的主要关闭业务逻辑是启用子线程实现的。

也就是说我们在子线程,即channel还没有关闭就执行了代码,这样可能导致我们的业务逻辑存在问题。

所以我们需要在channel关闭后才进行打印,真实场景中就是channel关闭后进行剩余业务操作。

我们需要在 System.out.println(“处理channel关闭后的操作”); 之前增加以下的代码:

1
2
3
4
ini复制代码// 获取closefuture
ChannelFuture closeFuture = channel.closeFuture();
//同步阻塞
closeFuture.sync();

上述代码会获取到一个closeFuture对象,sync方法会同步阻塞在此,直到子线程当中的channel真正关闭了,才会继续执行代码。

输入1、2、3、q,直接看结果:

1
2
3
4
5
6
css复制代码1
2
3
q
关闭channel
处理channel关闭后的操作

与channelFuture相同,closeFuture除了有sync方法进行同步阻塞,仍然也可以使用异步方式进行监听channel是否关闭的状态。

将 System.out.println(“处理channel关闭后的操作”); 放在以下代码:

1
arduino复制代码        closeFuture.addListener((ChannelFutureListener) future -> System.out.println("处理channel关闭后的操作"); );

输入1、2、3、q,看结果:

1
2
3
4
5
6
css复制代码1
2
3
q
关闭channel
处理channel关闭后的操作

提供一个NioEventLoopGroup专门用于关闭。

1
ini复制代码NioEventLoopGroup group = new NioEventLoopGroup();

通过上面的代码我们已经能够成功监测到channel的关闭了,但是相信实践过朋友们会发现我们channel虽然关闭了,但是整个程序仍然在运行,整体的资源没有做到全部释放,这是应为EventLoopGroup当中的线程没有停止,这里需要引入一个方法:

shutdownGracefully()

这个方式是EventLoopGroup当中的方法。我们需要做以下操作,为了大家看,我把所有的客户端内容全放在以下代码中了:

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
45
46
47
48
scss复制代码public class ChannelFutureTest {

public static void main(String[] args) throws Exception {
// 将group提出来,不能匿名方式,为了后面调动shutdownGracefully()方法
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("localhost", 8080);

// 同步等待连接
Channel channel = channelFuture.sync().channel();

new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
System.out.println("关闭channel");
// close 异步操作 1s 之后
channel.close();
break;
}
channel.writeAndFlush(line);
}
}, "input").start();


// 处理channel关闭后的操作
// 获取 CloseFuture 对象, 1) 同步处理关闭, 2) 异步处理关闭
ChannelFuture closeFuture = channel.closeFuture();

//同步
//closeFuture.sync();

//异步 - EventLoopGroup线程未关闭
//closeFuture.addListener((ChannelFutureListener) future -> System.out.println("处理channel关闭后的操作"));

//异步 - EventLoopGroup线程优雅关闭
closeFuture.addListener((ChannelFutureListener) future -> group.shutdownGracefully());
}
}

结果:

1
2
3
4
5
6
7
css复制代码1
2
3
q
关闭channel

Process finished with exit code 0

关于channel的介绍就这么多,有帮助的话点个赞吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

性能优化 Go Ballast 让内存控制更加丝滑

发表于 2021-11-17

关于 Go GC 优化的手段你知道的有哪些?比较常见的是通过调整 GC 的步调,以调整 GC 的触发频率。

  • 设置 GOGC
  • 设置 debug.SetGCPercent()

这两种方式的原理和效果都是一样的,GOGC 默认值是 100,也就是下次 GC 触发的 heap 的大小是这次 GC 之后的 heap 的一倍。

我们都知道 GO 的 GC 是标记-清除方式,当 GC 会触发时全量遍历变量进行标记,当标记结束后执行清除,把标记为白色的对象执行垃圾回收。值得注意的是,这里的回收仅仅是标记内存可以返回给操作系统,并不是立即回收,这就是你看到 Go 应用 RSS 一直居高不下的原因。在整个垃圾回收过程中会暂停整个 Go 程序(STW),Go 垃圾回收的耗时还是主要取决于标记花费的时间的长短,清除过程是非常快的。

设置 GOGC 的弊端

1. GOGC 设置比率的方式不精确

设置 GOGC 基本上我们比较常用的 Go GC 调优的方式,大部分情况下其实我们并不需要调整 GOGC 就可以,一方面是不涉及内存密集型的程序本身对内存敏感程度太低,另外就是 GOGC 这种设置比率的方式不精确,我们很难精确的控制我们想要的触发的垃圾回收的阈值。

2. GOGC 设置过小

GOGC 设置的非常小,会频繁触发 GC 导致太多无效的 CPU 浪费,反应到程序的表现就会特别明显。举个例子,对于 API 接口来说,导致的结果的就是接口周期性的耗时变化。这个时候你抓取 CPU profile 来看,大部分的耗时都集中在 GC 的相关处理上。

图片

如上图,这是一次 prometheus 的查询操作,我们看到大部分的 CPU 都消耗在 GC 的操作上。这也是生产环境遇到的,由于 GOGC 设置的过小,导致过多的消耗都耗费在 GC 上。

3. 对某些程序本身占用内存就低,容易触发 GC

对 API 接口耗时比较敏感的业务,如果 GOGC 置默认值的时候,也可能也会遇到接口的周期性的耗时波动。这是为什么呢?

因为这种接口本身占用内存比较低,每次 GC 之后本身占的内存比较低,如果按照上次 GC 后的 heap 的一倍的 GC 步调来设置 GOGC 的话,这个阈值其实是很容易就能够触发,于是就很容出现接口因为 GC 的触发导致额外的消耗。

4. GOGC 设置很大,有的时候又容易触发 OOM

那如何调整呢?是不是把 GOGC 设置的越大越好呢?这样确实能够降低 GC 的触发频率,但是这个值需要设置特别大才有效果。这样带来的问题,GOGC 设置的过大,如果这些接口突然接受到一大波流量,由于长时间无法触发 GC 可能导致 OOM。

由此,GOGC 对于某些场景并不是很友好,那有没有能够精确控制内存,让其在 10G 的倍数时准确控制 GC 呢?

GO 内存 ballast

这就需要 Go ballast 出场了。什么是 Go ballast,其实很简单就是初始化一个生命周期贯穿整个 Go 应用生命周期的超大 slice。

1
2
3
4
5
6
7
go复制代码func main() {
  ballast := make([]byte, 10*1024*1024*1024) // 10G 
  
  // do something
  
  runtime.KeepAlive(ballast)
}

上面的代码就初始化了一个 ballast,利用 runtime.KeepAlive 来保证 ballast 不会被 GC 给回收掉。

利用这个特性,就能保证 GC 在 10G 的一倍时才能被触发,这样就能够比较精准控制 GO GC 的触发时机。

这里你可能有一个疑问,这里初始化一个 10G 的数组,不就占用了 10 G 的物理内存呢? 答案其实是不会的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package main

import (
    "runtime"
    "math"
    "time"
)

func main() {
    ballast := make([]byte, 10*1024*1024*1024)

    <-time.After(time.Duration(math.MaxInt64))
    runtime.KeepAlive(ballast)
}
1
2
3
4
css复制代码$ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 5 | numfmt --header --from-unit=1024 --to=iec --field 6 | column -t | egrep "[t]est|[P]I"

%MEM  COMMAND   PID    MAJFL      MINFL  RSS    VSZ
0.1   test      12859  0          1.6K   344M   11530184

这个结果是在 CentOS Linux release 7.9 验证的,我们看到占用的 RSS 真实的物理内存只有 344M,但是 VSZ 虚拟内存确实有 10G 的占用。

延伸一点,当怀疑我们的接口的耗时是由于 GC 的频繁触发引起的,我们需要怎么确定呢?首先你会想到周期性的抓取 pprof 的来分析,这种方案其实也可以,但是太麻烦了。其实可以根据 GC 的触发时间绘制这个曲线图,GC 的触发时间可以利用 runtime.Memstats 的 LastGC 来获取。

生产环境验证

  • 绿线 调整前 GOGC = 30
  • 黄线 调整后 GOGC 默认值,ballast = 50G

图片

图片

这张图相同的流量压力下,ballast 的表现明显偏好

图片

结论

本篇文章只是简单的阐述了 Go ballast 的使用,Go ballast 是官方比较认可的方案,具体可以参见 issue 23044[1]。很多开源程序,如 tidb[2],cortex[3] 都实现了 go ballast,如果你的程序饱受 GOGC 的问题影响或者周期性的耗时不稳定,不妨尝试下 go ballast。

当然强烈推荐你看下twitch.tv 这篇文章[4],相信让你会对 GOGC 以及 ballast 的运用理解的更加透彻。

参考资料

[1]

issue 23044: github.com/golang/go/i…

[2]

tidb: github.com/pingcap/tid…

[3]

cortex: github.com/cortexproje…

[4]

twitch.tv 这篇文章: blog.twitch.tv/en/2019/04/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【力扣-二叉树】8、二叉树的所有路径(257) 257 二

发表于 2021-11-17

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

257. 二叉树的所有路径

题目描述

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

示例 1:

1
2
css复制代码输入: root = [1,2,3,null,5]
输出: ["1->2->5","1->3"]

示例 2:

1
2
css复制代码输入: root = [1]
输出: ["1"]

解析

求解路径问题

  • 递归法
    • 使用前序遍历
    • 确定递归函数的参数与返回值
    • 确定递归终止的条件
    • 单层递归的逻辑实现
  • 迭代法
    • 使用前序遍历
    • 定义存放节点的栈
    • 定义存放路径的栈
    • 迭代更新栈中的节点与路径

递归法

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
c++复制代码class Solution
{

public:
vector<string> binaryTreePaths(TreeNode *root)
{

vector<string> result;
vector<int> path;
if (root == NULL)
{
return result;
}
traversal(root, path, result);

return result;
}

private:
// 1、确定递归函数的参数与返回值
// 参数:传入的根节点,记录的路径path ,存放结果集的result
void traversal(TreeNode *cur, vector<int> &path, vector<string> &result)
{
// 将当前节点存入到path中
path.push_back(cur->val);
// 2、确定递归的终止条件
// 左右孩子节点为空则说明当前节点为叶子节点,可以终止本次遍历
if (cur->left == NULL && cur->right == NULL)
{
string strPath;
// 先加入前n-1个节点,这里不包括path中的最后一个节点
for (int i = 0; i < path.size() - 1; i++)
{
strPath += to_string(path[i]);
strPath += "->";
}

// 加入每个路径的最后一个节点
strPath += to_string(path[path.size() - 1]);
result.push_back(strPath);
return;
}

// 3、单层递归的逻辑
// 采用前序遍历,所以先处理中间节点,中间节点就是要记录的路径中的节点,使用path存放
// 然后是递归与回溯过程
// 节点为空的时候就不进行递归与回溯了

if (cur->left)
{
traversal(cur->left, path, result);
path.pop_back(); // 回溯
}
if (cur->right)
{
traversal(cur->right, path, result);
path.pop_back(); // 回溯
}
}
};

迭代法

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
c++复制代码// 迭代法
// 前序遍历
// 定义两个栈
// 一个栈用来存放 遍历路径中的节点
// 一个栈用来存放 遍历的路径
class Solution
{
public:
vector<string> binaryTreePaths(TreeNode *root)
{
// 存放节点的栈
stack<TreeNode *> nodePath;
// 存放路径的栈
stack<string> strPath;
// 结果数组
vector<string> result;
if (root == NULL)
{
return result;
}

// 将根节点入栈
nodePath.push(root);
// 根节点作为路径的起点
strPath.push(to_string(root->val));

// 迭代
while (!nodePath.empty())
{
// 取出当前节点(中)
TreeNode *node = nodePath.top();
nodePath.pop();

// 取出节点对应的路径
string path = strPath.top();
strPath.pop();

// 遇到叶子节点,将当前节点的路径存入到结果集中
if (node->left == NULL && node->right == NULL)
{
result.push_back(path);
}
// 左节点
if (node->left)
{
nodePath.push(node->left);
strPath.push(path + "->" + to_string(node->left->val));
}
// 右节点
if (node->right)
{
nodePath.push(node->right);
strPath.push(path + "->" + to_string(node->right->val));
}
}
return result;
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

多线程环境下变量可见性-volatile关键字

发表于 2021-11-17

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

多线程可见性问题分析

多个线程共用一个变量,有一个线程修改这个值,另一个线程读取这个值。如果不使用volatile,会导致读取线程一直读取到旧的值。这个问题本质是多线程的可见性问题。

主内存 及 CPU 缓存模型

计算机中CPU会从主内存中取数据指令执行计算操作,如果每次数据都从主内存中取,会因为CPU的速度远超过内存速度,频繁读取会降低性能,所以现代CPU会有自己的CPU缓存。

一个 CPU 有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。

因为 L1 和 L2 缓存是每个物理核私有的,所以,当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,保证CPU计算效率非常高。

img

因为每个CPU有自己的本地缓存,每次计算的时候都是读自己的缓存。修改数据的线程也是被另一个CPU在自己的本地缓存中不断修改,两个都是在各自本地的高速缓存中读取和计算,所以多线程并发运行时就可能出现这个可见性问题。

所以多线程可见性问题最本质的问题其实是可以深究到CPU层面。

总线加锁机制和MESI缓存一致性协议

总线加锁机制是指 CPU处理数据时,通过锁定系统总线或者时内存总线,让其他CPU不具备访问内存的访问权限,进而来保证缓存的一致性。

总线加锁机制,效率太差,目前已经淘汰了。

目前比较流程的是 MESI 协议,缓存一致性协议。MESI通过修改后将数据强刷回主内存,以及嗅探探查主内存数据是否修改,若修改则将自己的缓存无效机制,来保证缓存一致性。

MESI是四个单词的首字母缩写,Modified修改,Exclusive独占,Shared共享,Invalid无效。

Java 内存模型

Java内存模型是基于CPU缓存模型来建立的。Java内存模型是标准化的,是为了屏蔽底层不同计算机的区别。

  • read 从主存读取
  • load 将主存读取到的值写入工作内存
  • use 从工作内存读取数据来计算
  • assign 将计算好的值重新赋值到工作内存中
  • store 将工作内存数据写入主存
  • write 将store过去的变量赋值给主存中的变量

img

因为工作内存不是每次都访问主内存,所以出现可见性问题。

volatile

volatile是什么?

在多线程并发编程可能产生三类问题,分别是可见性,原子性,有序性。

可见性:当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

有序性:在并发时,程序的执行可能会出现乱序。编译器和指令器有时候为了提高代码运行效率高,会将指令重排序。

原子性:一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。例如多个线程对共享变量 int i = 0 进行并发 i++ 时。如果保证原子性则 i = 2 ,如果不是原子性 则 i 不一定是几,可能为 1。

volatile 可以保证可见性,有序性,但无法保证原子性。

volatile 如何使用

volatile 使用简单,只要在共享变量声明时,添加volatile关键字即可。

1
2
3
4
5
java复制代码public class VolatileDemo {

public volatile static int i = 0;

}

voltaile 如何保证可见性

volatile 会保证修改后的工作内存中的数据立即刷会到主内存中,且会让其他工作内存中这个数据的缓存过期掉。进而保证可见性。

对于volatile修饰的变量,执行写操作,JVM会发送一条lock前缀指令给CPU,CPU计算完毕后立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地的缓存是否被别人修改。

CPU发现其它线程修改了某个缓存的数据,CPU就会将本地缓存的数据过期,然后这个CPU上执行的线程读取那个变量的时候就会从主内存重新加载最新的数据。

volatile 如何保证有序性

编译器执行器可能对代码进行重排序,遵循happens-before原则。

happens-before原则其中有一个原则就是

volatile变量原则:对一个变量的写操作先行发生于后面对这个变量的读操作。

volatile的变量 会在读和写之前会插入内存屏障,禁止内存屏障前后代码的重排序。

volatile 为什么无法保证原子性

因为两个线程同时完成运算,同时执行 assign ,store 和 write 写回主内存,运算后的这个需要写回的结果跟工作内存中的变量值无关,所以无法保证原子性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

干货分享:细说双 11 直播背后的压测保障技术 直播的架构

发表于 2021-11-17

简介: 阿里云 PTS 站在双 11 巨人的肩膀上,是阿里全链路压测的延伸。PTS 通过伸缩弹性,轻松发起用户百万级别的流量,免去机器、人力成本;PTS 对流量的控制,能够实时脉冲,精准控制; 是应对视频直播快速攀升的流量脉冲的优秀方案。

作者:子矜

“今年 1 月到现在,淘宝直播的用户超过了 5 亿,到 8 月份流量也增长了 59%,在最核心的商家 GMV 上增长了 55%。双 11 是从 10 月 20 日晚开始的,我们希望淘宝直播作为主场去承接这件事情。”日前,淘宝事业群直播事业部负责人程道放在接受 21 世纪经济报道记者采访时透露,过去一年的直播可谓热闹,今年会更加专业化。

如此大的用户体量下,直播类应用给后端服务带来了一些什么不一样的挑战呢?我们今天来介绍一些直播的架构,以及针对这个架构,给我们的应用架构带来的挑战。

直播的架构

我们通常看到的有下面几种直播:

  1. 单人直播,例如淘宝直播,通常伴随着秒杀,弹幕,送火箭等业务逻辑;
  1. 多人同时直播,例如连麦,会议;
  1. 录播,对于部分直播场景如培训、会议等,需要将现场直播视频保存以进行传播、留存等使用,有对直播进行录制的需求。这种往往对实时性要求不高。

而当用户观看直播时,如果服务接入了 CDN,若接入 CDN,播放端选择就近的 CDN 节点进行拉流播放,此时拉流压力在 CDN;若未接入 CDN,播放端从直播源站进行拉流。

下图是一个比较常见的视频流的架构和两条数据走向:

  1. 视频流推拉逻辑,如蓝色线所示
  1. 常规的业务逻辑,如黄色线所示

可以看到有四个主要模块:

  1. 推流端:主要的作用在于采集主播的音视频数据推送到流媒体服务端;
  1. 流媒体服务端:主要作用在于把推流端传递过来的数据转换成指定格式,同时推送到播放端方便不同播放端用户观看,当然目前云产商也流媒体服务端的一整套解决方案;
  1. 业务服务端:主要处理一些常见的业务逻辑,如秒杀,弹幕等等;
  1. 播放端:播放端简而言之就是拉取音视频进行播放,把相应的内容呈现给用户。

四个关键的模块的协议其实就是流媒体传输协议。大部分直播的结构都采取上图的格式,较大的区别是是否引入 CDN。一般来说,我们建议客户引入 CDN 来减少直播流量对服务器的冲击。四个模块之间的协议并不着重强调一致性。

接下来,我们沿着这个架构来讨论一下,在这其中比较脆弱的风险有哪些,以及我们如何提早通过压测来排查这些风险点。

直播中的挑战

挑战一:视频流给流媒体服务端的压力

在这个推拉的逻辑中,由于涉及视频的流量较大,经过的路线较长,对流媒体服务器都会造成冲击;通场的做法是引入 CDN,当用户开始收看视频的时候,会先就近去 CDN 拉取流,如果这个时候视频内容还没有缓存在 CDN 的时候,CDN 就回源到流媒体服务端。

但是,风险就存在在瞬间大量用户同时收看 CDN,CDN 大量回源的时候;这种脉冲流量,会给流服务端带来不可预计的效果。

**我们通常通过压测来提前验证链路的有效性,甚至可以通过压测,提前把视频在 CDN 预热。**然而,传统的 HTTP 请求协议是无法支持这种场景的,因为:

  1. 即使开源软件 srs_bench,以及 JMeter 都提供了一些插件来使用。但是这些开源软件需要用户对视频协议有比较深入的理解,使用门槛会略微偏高;
  1. 视频压测本身对带宽的要求非常高,这就意味压测机器成本比较高;
  1. 视频压测需要考虑到地域对传输质量的影响。

针对以上问题,PTS 加入了 RTMP/HLS 协议,并且结合压测场景做了抽象,让用户可以界面化的使用不同协议的压测。

除此以外,PTS 还提供丰富的编排模式,可以方便自如的对场景进行编排;更重要的是,还可以利用 PTS 全国定制的模式,模拟客户从不同的地方发起请求,更快捷的探测出问题。

挑战二:低延时的互动协议

和传统的大促不一样,直播往往追求和线下客户的互动。例如弹幕,评论,聊天,秒杀等等。主持人聊的 热火朝天,用户毫无反应,这就是一次失败的直播了。而普通的 HTTP 请求无法满足对时效的需求;因此,通常这些功能用WebSocket来实现的。因为 HTTP 是一种无状态的、无连接的协议,WebSocket 则通过服务端/客户端建立长链,来保证消息的实时性、以及降低性能开销。

每建立一个 WebSocket 连接时,在握手阶段都会发起 HTTP 请求。通过 HTTP 协议协定好 WebSocket 支持的版本号、协议的字版本号、原始地址,主机地址等内容给服务端。报文的关键地方在于 Upgrade 的首部,用于告诉服务端把当前的 HTTP 请求升级到 WebSocket 协议,如果服务端支持,则返回的状态码必须是 101:

1
2
3
4
makefile复制代码HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:xxxxxxxxxxxxxxxxxxxx

有了上述返回,Websocket 连接才建立成功,接下来就是完全按照 Websocket 协议进行了数据传输了。

针对 WebSocket 的通信过程,JMeter 提供了插件来模拟整个过程,但是它也需要用户理解协议的玩法,使用起来相对晦涩。PTS 通过抽象业务含义,用户通过场景配置和施压配置,仅需要配置压测 url 等基本配置、出参设置、检查点设置等几个简单参数,就能够把复杂协议玩起来。

除了在直播中使用,Websocket 也广泛应用于在线游戏、股票基金、体育实况更新、聊天室、弹幕、在线教育等实时性要求非常高的场景。

挑战三: 高并发的脉冲流量

不同于普通应用,直播类应用的使用时间段非常的集中,因此在这短短几小时之间,会涌入大量的用户,一次大 V 的直播通常就会造成百万级的用户登录,故直播系统对应脉冲流量的能力要求也变得很高。而且在抢货的时候,和传统的秒杀不同,往往是主播进行到某个时间突然发起秒杀的–这个时间往往无法非常精确–同时脉冲流量对系统的要求极高,很多平时不会出现的问题,例如懒加载,jit 预热,冷热数据切换等传统大流量不会出现的问题,都会出现。

这两点特性,要求压测工具能够瞬间发起大流量。这除了需要较多的机器引擎,还需要对流量的有精准控制–满足流量快速攀升的诉求。

而这两点,正是阿里云 PTS 的强项。阿里云 PTS 站在双 11 巨人的肩膀上,是阿里全链路压测的延伸。PTS 通过伸缩弹性,轻松发起用户百万级别的流量,免去机器、人力成本;PTS 对流量的控制,能够实时脉冲,精准控制; 是应对视频直播快速攀升的流量脉冲的优秀方案。

最后

PTS 针对视频、直播行业的变化,对 PTS 支持的协议做了全面升级。它不光支持传统的 HTTP 请求,更是引入了 HTTP 2、流媒体、MQTT 等多种协议,让用户可以 Test Anywhere!

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

精通 zookeeper 第一章 zk 集群是如何启动的

发表于 2021-11-17

zookeeper server 是如何启动的

我们使用 zkServer.sh start 进行启动 zk 服务器,查看这个脚本的代码发现实际上调用的是 QuorumPeerMain 的 main 方法

我们讲解 zk 集群模式的工作原理,先看下图是 server 启动弄的整体架构组件工作原理图,然后我大概解释一下各大核心组件

集群是如何启动的.jpg

启动时候运行的核心组件

上图我们可以看到三种颜色的组件图

  • 黄色:监听了 2181 端口,大概应该知道这个端口就是负责与客户端通信的,围绕这一块的核心组件是 NioServerCnxnFactory
  • 蓝色:监听了 3888 端口,这一系列组件主要是用于集群选举用的,选举成为 leader 还是 follower 还是 observer,围绕这一块的核心组件为负责通信的 QuirumCnxManager,负责选举的 FastLeaderElection
  • 紫色:监听了 2888 端口,这一系列组件主要是用于选举完成后确认好了各个 server 所处的角色之后,leader 和 follower 需要进行数据交互,围绕这一块的核心组件为 Leader(Follower) 组件

同时我们可以看到这一系列的组件都是包含在 QuorumPeer 这个中心组件之中,可以说这就代表了一个 zk server,可以想象成各大组件就是一个人的各个器官,从而组成了 QuorumPeer 这个完整的人

在上图完整的画出了 Leader 的核心组件,Follower 的这些组件跟 Leader 是一样的,不同的是 Follower 在选举完成后创建的不是 Leader 组件而是创建的 Follower 组件

ZKDatabase 代表了一个内存数据库,写入到 zk 的数据会被全部记录在内存中,同时重启之后也会将磁盘的数据恢复到内存中

整体流程梳理

启动 zk server 创建一个 NioServerCnxnFactory 用于跟客户端进行网络通信

创建一个 ZKDatabase 通过使用 FileTxnSnapLog 加载磁盘快照文件 + proposal 日志文件恢复出来一个内存数据库

创建一个 QuirumCnxManager 负责集群之间的投票信息传递,各个节点会发送给对方当前机器的 zxid、epoch、myid 值给对方

创建一个 FastLeaderElection 读取归档各个节点的投票数据,最终决策出来谁做 Leader、Follower、Observer

最后根据当前节点所属角色创建 Leader、Follower、Observer,他们会另外绑定一个端口用于集群之间的数据同步,针对每个连接创建一个线程处理,同时为了避免连接重复绑定创建,只允许 server id 大的向 server id 小的发起连接,小的发往大的会比强制关闭

思考

从 zk 服务器的启动我们可以看到,对于网络调用的处理,什么情况下适合用 BIO,什么情况适合用 NIO,以及不同的端口各成体系,逻辑独立,易于拓展、维护、同时容错性也更高

与客户端通信的时候,可能存在大量客户端的连接和高并发的读请求默认最大连接客户端数量为 60 个可以调整大小,如果采用 BIO 的话需要为每个客户端创建一个线程,就算客户端没有请求也会占用线程资源

集群之间的通信采用的是 BIO,为每个客户端之间的连接创建一个线程来处理,因为集群结点相对较少

集群之间的通信选举使用 3888 端口,leader 和 follower 信息同步采用 2888 端口,二者互相隔离就算某一方有错误也不会影响到对方

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文讲清,MySQL中的二级索引

发表于 2021-11-17

主键索引是InnoDB存储引擎默认给我们创建的一套索引结构,我们表里的数据也是直接放在主键索引里,作为叶子节点的数据页。

但我们在开发的过程中,往往会根据业务需要在不同的字段上建立索引,这些索引就是二级索引,今天我们就给大家讲讲二级所有的原理。

比如,你给name字段加了一个索引,你插入数据的时候,就会重新搞一棵B+树,B+树的叶子节点,也是数据页,但是这个数据页里仅仅放了主键字段和name字段。

叶子节点的数据页的name值,跟主键索引一样的,都是按照大小排序的。同一个数据页里的name字段值都是大于上一个数据页里的name字段值。

name字段的B+树也会构建多层索引页,这个索引页里放的是下一层的页号和最小name字段值。就像这样:

)

假设你要根据name字段来搜索数据,比如:select * from user where name=‘xxx’,过程与主键索引一样的。从name索引的根节点开始找,一层一层的向下找,一直找到叶子节点,定位到name字段值对应的主键值。

但此时叶子节点的数据页没有完整所有字段,就需要根据主键到主键索引里去查找,从主键索引的根节点一路找到叶子节点,就可以找到这行数据的所有字段了,这个过程就叫回表。

二级索引,可以对多个字段建立联合索引,比如,name + age + sex

此时联合索引与单个字段的索引原理是一样的,只不过叶子节点的数据页里放的是id + name + age + sex,然后默认按照name排序,name一样就按age排序,age一样就按sex排序。

每个name + age +sex的索引页里,放的就是下层节点的页号和最小的name + age + sex值。当你用name + age + sex搜索的时候,就会走name + age + sex联合索引这棵树,再回表查询。

)

以上就是innoDB二级索引的原理了,有没有感觉也不过如此?

索引的利弊

随着我们不停的在表里插入数据,就会不停的在数据页里插入数据,然后一个数据页放满了就会分裂成多个数据页,这个时候就需要索引页去指向各个数据页。

如果数据页太多了,那么索引页里的数据页指针也就会太多了,索引页也必然会放满的,此时索引页也会分裂成多个,再形成更上层的索引页。

这个过程跟主键索引是一模一样的,所以你如果搞懂了主键索引,二级索引也很简单的。

索引的好处是显而易见的,查找数据的时候不需要全表扫描,性能是很高的。

但索引也有其缺点,如果用的不好,反而对会有副作用。

首先,要创建索引,就要占用存储空间。我们每创建一个索引,MySQL就会搞出一个B+树,每棵B+树都要占用很多的磁盘空间啊,所以搞太多索引,也是很耗费磁盘空间的。

其次,你在进行增删改查的时候,每次都需要维护各个索引的数据有序性,因为每个B+树都要求页内是按照值大小来排序的,页之间也是有序的。所以你不停的增删改查,各个索引的数据页要不停的分裂、增加新的索引页,如果你一个表里搞太多索引,增删改的性能就会比较差

所以综合上面两个原因,我们不建议给一张表搞太多索引的。

联合索引查询原理

之所以要讲联合索引的查询原理,是想带着读者们更清晰的理解索引的工作原理,我们平时设计索引也大多是设计的联合索引。

假如有一个索引KEY(class, name, course),对学生班级、姓名、科目名称建立的联合索引。联合索引的示意图如下:

)

每个数据页都包含了联合索引的三个字段值和主键值,数据页内部也是按照顺序来排序的。

首先按照班级值来排序,如果一样则按照学生姓名来排序,如果一样,则按照科目名称来排序,所以数据页内部都是按照这三个字的值来排序的。

数据页内部与数据页之间也是有序的,数据页内部组成单向链表,数据页之间组成双向链表。

图中索引页分别指向两个数据页,索引页放的是数据页里最小的那个数据值。

假如我们要执行语句:select * from student where class=’1班‘ and student_name=’张强’ and course_name=’数学’。

查询时先到索引页里去找,索引页里有多个数据页的最小值记录,此时直接在索引页里基于二分查找方法来找就可以了,先根据班级名来找1班这个值对应的数据页,直接可以定位到所在的数据页。

)

然后就可以找到索引指向的那个数据页就可以了,在数据页内部是一个单向链表, 你也是基于二分查找就可以了,先按1班这个值查询,你发现有几条数据都是1班,然后按照张强这个学生姓名查找,发现也有多条数据,接着按照科目名称来二分查找。

很快就定位到一条数据了,对应的就是图中的id=127的数据。

)

然后根据主键id=127回表查找完整的字段,在主键索引开始二分查找迅速定位到各层级的索引页,再逐步向下定位到id=127的那条数据,就可以拿到所有字段的值了。

上面的过程就是联合索引的查找过程。对于联合索引,就是一次安装各个字段来进行二分查找,先定位到第一个字段对应的值在哪个页,如果第一个字段值一样,就按第二个字段值来查找,以此类推,就找到最终的数据了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RowVersion字段从SqlServer到Postgre

发表于 2021-11-17

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。

SQL Server 有个列是rowversion,之前是timestamp,因此这两个关键字在SQL server中是同义词,不过目前timestamp处在废弃阶段,因此我们最好使用rowversion来代替它。而在数据库迁移时,因为使用到该类型,因此要考虑怎么迁移它。

  • 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文作者:由webmote 原创,首发于 【CSDN】
  • 📢作者格言: 生活在于折腾,当你不折腾生活时,生活就开始折腾你,让我们一起加油!💪💪💪
  1. SQL Server中 RowVersion的含义

Timestamp/rowversion 是一个EF Core属性,在每次插入或更新数据行时,数据库会自动为其生成新值。

因此此属性也被视为并发标记,这确保了在你查询行后,如果正在更新的行发生了更改,则会出现异常。可以参考之前的数据库乐观锁介绍。

对于 SQL Server,通常使用 byte [] 属性,该属性将设置为数据库中的 ROWVERSION 列。

代码如下:

1
2
3
4
5
6
csharp复制代码public class Blog {
public int BlogId {get; set; }
public string Url { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}

当然也可以是在 OnModelCreating 内设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion();
}
}

public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public byte[] Timestamp { get; set; }
}

注意: 如果需要映射到ulong,需要使用转换函数HasConversion:

1
2
3
scss复制代码modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion().HasConversion<int>();

在SQL Server里,RowVersion是一种自增的只用于定义数据表的列类型,其值占用的大小是固定的8个字节,是SQL Server数据库自动生成的、数据库级别唯一的、二进制数字,使用binary(8)存储。

1.1 递增原理介绍

每个数据库都有一个自增的计数器,该计数器是Database RowVersion,在用户对有RowVersion 字段的数据表执行插入或修改命令时,该计数器就会增加。

可以使用全局变量 @@DBTS 进行查询其值, 该值在整个数据库中是唯一的、递增的,不可回滚的。

1
sql复制代码select @@DBTS;

当然对于一个数据表,最多有一个RowVersion 字段。

1.2 RowVersion字段的特性

  1. 每个数据库只有一个计数器,因此所有拥有RowVersion字段的数据表,其值都是不同的;
  2. 数据库的RowVersion 只会递增,不会回滚;
  3. 由数据库自动赋值(插入,修改),不能显式赋值;
  1. PostgreSQL 有无Timestamp/RowVersion

PostgreSQL中具有Timestamp类型,其是日期时间字段,并不能直接转为SQL Server的RowVersion/Timestamp类型。

与RowVersion行为最为接近的列类型,是PostgreSQL中用于MVCC管理的 xmin隐藏列,这个列每个表系统都会自动建立,因此无需增加。当然还有其他隐藏列,比如xmax,xmin表示插入该表的事务号,xmax表示删除该表的事务号。

唯一的瑕疵是使用xmin作为行版本标识不能区别同一个事务内的两次、多次修改。当然事务内的第一次修改对其他事务不可见,唯一能看见它的只有修改这一行的事务自己。

在EF中我们可以采用下列实体定义,以便支持xmin列。

1
2
3
4
csharp复制代码[Timestamp]
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[Column("xmin", TypeName = "xid")]
public uint Rowversion { get; set; }

当然,也可以在OnConfiguring里写语句。

1
2
csharp复制代码builder.Entity<EmailAddressValidation>()
.Property(e => e.RowVersion).IsRowVersion().HasColumnName("xmin").HasConversion<int>();

检查数据库数据,OK,是那么的回事了。

image.png

  1. 小结

rowversion字段还是挺有意思的,你学废了吗?

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…309310311…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%