2023年3月

前言

记录下我安装
sealos
的踩坑历程,全网基本没有什么类似的可靠资料,也许是因为太小众了吧,希望能帮助到搜索到此文的人.

sealos是什么

Sealos 是以 kubernetes 为内核的云操作系统发行版, 单机操作系统如同 linux 发行版本可以在上面安装和使用各种单机应用,如 PPT,Word,Excel 等。 云操作系统只需要把这些单机应用替换成各种云应用,如数据库,对象存储,消息队列等,就很容易理解了,这些应用都是分布式高可用的。 Sealos 就是能支撑运行各种分布式应用的云操作系统。有了 Sealos 就拥有了一朵云。
主要资料参考这里
介绍 | sealos
这里不做赘述

资料

架构

  • 本身资料中没有画,要么从代码中提炼
  • 阅读代码,了解设计模式和代码架构,了解基础操作和实现

安装

官方操作

4.0版本的sealos

# 安装前必读

1.目前只支持root用户,不支持非root和sudo
2.目前只支持在集群内的节点执行安装命令
3.提前卸载掉已安装的docker
4.3.0版本的k8s离线包无法使用4.0版本的sealos安装
5.run命令时如果密码有特殊字符,请加英文单引号
6.离线安装示例:
4.0离线安装示例:

---
# 镜像打包, 在有外网的机器上执行

sealos pull labring/kubernetes:v1.24.0
sealos pull labring/calico:v3.22.1
sealos save -o kubernetes.tar labring/kubernetes:v1.24.0
sealos save -o calico.tar labring/calico:v3.22.1

---
# 加载镜像, 内网机器执行

sealos load -i kubernetes.tar
sealos load -i calico.tar

主机

主机 用途
10.55.10.107 计划作为sealos的安装机,以及master节点
10.55.10.106 node节点1
10.55.10.97 node节点2

可以选择打通免密,方便定位问题

ssh-keygen -t rsa
cat id_rsa.pub >> authorized_keys
vim authorized_keys # 添加秘钥
vim /etc/ssh/sshd_config # 修改允许root登录 PermitRootLogin yes

systemctl restart sshd

前置检查和文件准备

# 主机只有挂载的/data01磁盘支持overlay,所以注定了没法向上面官方文档给出的那么简单的就能安装完成
[root@test-d-010055010107 data01]# xfs_info  /data01
meta-data=/dev/vdb               isize=512    agcount=4, agsize=5242880 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=0 spinodes=0
data     =                       bsize=4096   blocks=20971520, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal               bsize=4096   blocks=10240, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0


lsmod | grep -e ip_vs -e nf_conntrack_ipv4

# 文件准备,从远处pull下来,然后save成镜像包
ctr image import kubernetes.tar
ctr image import calico.tar
ctr images export calico.tar docker.io/labring/calico:v3.22.1

wget https://github.com/labring/sealos/releases/download/v4.1.4/sealos_4.1.4_linux_amd64.tar.gz \
   && tar zxvf sealos_4.1.4_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin

# sealos_4.1.4 和 sealos_4.1.7 在Global Flags地方有区别,并且4.1.4有bug无法完成当前主机集群的正常部署,需要使用4.1.7版本

单机安装

# 遇到文件格式问题,需要指定主目录
[root@test-d-010055010107 data01]# ./sealos run
Error: kernel does not support overlay fs: overlay: the backing xfs filesystem is formatted without d_type support, which leads to incorrect behavior. Reformat the filesystem with ftype=1 to enable d_type support. Running without d_type is not supported.: driver not supported
kernel does not support overlay fs: overlay: the backing xfs filesystem is formatted without d_type support, which leads to incorrect behavior. Reformat the filesystem with ftype=1 to enable d_type support. Running without d_type is not supported.: driver not supported

# 加载镜像包有问题,需要指定镜像解包格式
[root@test-d-010055010107 data01]# sealos --root /data01/ --runroot /data01/ load -i kubernetes.tar
Error: loading index: open /var/tmp/oci1097864579/index.json: no such file or directory
loading index: open /var/tmp/oci1097864579/index.json: no such file or directory

# 常用命令
mkdir /data01/sealos
sealos --debug --root /data01/sealos --runroot /data01/sealos/docker load -i calico.tar -t docker-archive
sealos --debug --root /data01/sealos --runroot /data01/sealos/docker load -i new-kubernetes.tar -t oci-archive
sealos load --help
sealos --debug --root /data01/sealos --runroot /data01/sealos/docker run localhost/labring/kuberentes:v1.24 --single # 通过镜像名有问题,这里直接用镜像id
sealos --debug --root /data01/sealos --runroot /data01/sealos/docker run 133c6a0a0d5f --single

# 重置安装
sealos --debug --root /data01/sealos --runroot /data01/sealos/docker reset

# 简化命令
alias s="sealos --debug --root /data01/sealos --runroot /data01/sealos/docker "

s run 133c6a0a0d5f --single

[root@test-d-010055010107 sealos]# s images
REPOSITORY                     TAG       IMAGE ID       CREATED        SIZE
docker.io/labring/kubernetes   v1.24     133c6a0a0d5f   10 days ago    635 MB
docker.io/labring/helm         v3.8.2    1123e8b4b455   7 months ago   45.1 MB
docker.io/labring/calico       v3.22.1   29516dc98b4b   9 months ago   546 MB

# sealos version must >= v4.1.0
s reset
s run 133c6a0a0d5f 1123e8b4b455 29516dc98b4b --single

# 手动执行image-cri-shim启动,还是有问题,查看有报错
/usr/bin/image-cri-shim -f /etc/image-cri-shim.yaml

fatal failed to setup image_shim, cri/shim: failed to register image service: falling using CRI v1 image API, please using other cri support v1 CRI API
fatal failed to setup image_shim, cri/shim: failed to register image service: falling using CRI v1alpha2 image API, please using other cri support v1alpha2 CRI API

# 排查containerd,看到有报错信息
[root@test-d-010055010107 sealos]# systemctl status containerd -l
● containerd.service - containerd container runtime
   Loaded: loaded (/etc/systemd/system/containerd.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2023-03-27 17:49:48 CST; 16h ago
     Docs: https://containerd.io
 Main PID: 7077 (containerd)
   Memory: 13.9M
   CGroup: /system.slice/containerd.service
           └─7077 /usr/bin/containerd

Mar 27 17:49:48 test-d-010055010107 systemd[1]: Starting containerd container runtime...
Mar 27 17:49:48 test-d-010055010107 containerd[7077]: time="2023-03-27T17:49:48.229104592+08:00" level=warning msg="failed to load plugin io.containerd.snapshotter.v1.overlayfs" error="/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support"
Mar 27 17:49:48 test-d-010055010107 containerd[7077]: time="2023-03-27T17:49:48.229191393+08:00" level=warning msg="failed to load plugin io.containerd.snapshotter.v1.devmapper" error="devmapper not configured"
Mar 27 17:49:48 test-d-010055010107 containerd[7077]: time="2023-03-27T17:49:48.229403283+08:00" level=warning msg="could not use snapshotter overlayfs in metadata plugin" error="/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support"
Mar 27 17:49:48 test-d-010055010107 containerd[7077]: time="2023-03-27T17:49:48.229420619+08:00" level=warning msg="could not use snapshotter devmapper in metadata plugin" error="devmapper not configured"
Mar 27 17:49:48 test-d-010055010107 containerd[7077]: time="2023-03-27T17:49:48.238313538+08:00" level=warning msg="failed to load plugin io.containerd.grpc.v1.cri" error="failed to create CRI service: failed to find snapshotter \"overlayfs\""
Mar 27 17:49:48 test-d-010055010107 systemd[1]: Started containerd container runtime.


# 怀疑是 containerd 没有安装成功,尝试安装crictl命令来看看
tar zxvf crictl-v1.25.0-linux-amd6.tar.gz  -C /usr/local/bin

# 查看信息,确定是这个问题,尝试修复
[root@test-d-010055010107 sealos]# crictl info
E0328 10:07:11.802780   10291 remote_runtime.go:948] "Status from runtime service failed" err="rpc error: code = Unimplemented desc = unknown service runtime.v1alpha2.RuntimeService"
FATA[0000] getting status of runtime: rpc error: code = Unimplemented desc = unknown service runtime.v1alpha2.RuntimeService

# 查看containerd关于overlayfs的配置,以及修改目录
cp -r /var/lib/container* /data01/
vim /etc/containerd/config.toml 修改 root = "/data01/containerd"

# 顺利启动containerd和image-cri-shim
systemctl restart containerd
systemctl restart image-cri-shim

# 遇到了 /root/.sealos/default/etc/admin.conf 找不到的问题,看着issue需要升级到4.1.7版本,问题解决但又然后发现重复安装有问题,无法继续上次安装
s reset # 重新开始

# 但是安装出来的containerd还是在/var/lib/containerd,需要找到改变此路径的方法,翻阅文档猜测指定criData环境变量可能有用

# 改变命令
s run 133c6a0a0d5f --single --env criData=/data01/containerd

# 的确有用,会把containerd安装到/data01/containerd,但是/root/.sealos/default/Clusterfile中显示的criData还是/var/lib/containerd

# 成功安装

# 但是节点一直未就绪
[root@test-d-010055010107 sealos]# kubectl get node
NAME                             STATUS     ROLES           AGE     VERSION
test-d-010055010107   NotReady   control-plane   8m56s   v1.24.0

KubeletNotReady              container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized

[root@test-d-010055010107 sealos]# crictl ps -a
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD
5d3572591a876       77b49675beae1       12 minutes ago      Running             kube-proxy                0                   dc61529f47415       kube-proxy-vjjqv
9559b3a7d80ec       aebe758cef4cd       12 minutes ago      Running             etcd                      0                   1a1846fb97f25       etcd-test-d-010055010107
00a5f23d7d227       529072250ccc6       12 minutes ago      Running             kube-apiserver            0                   b65e60cdc8996       kube-apiserver-test-d-010055010107
91b737d89b72e       e3ed7dee73e93       12 minutes ago      Running             kube-scheduler            0                   e682c3fb7cc11       kube-scheduler-test-d-010055010107
dd3a2ea10b7c7       88784fb4ac2f6       12 minutes ago      Running             kube-controller-manager   0                   d3177bd65479c       kube-controller-manager-test-d-010055010107

[root@test-d-010055010107 sealos]# kubectl get pod -A
NAMESPACE     NAME                                                     READY   STATUS    RESTARTS   AGE
kube-system   coredns-6d4b75cb6d-qfnf5                                 0/1     Pending   0          3h24m
kube-system   coredns-6d4b75cb6d-xzjz5                                 0/1     Pending   0          3h24m
kube-system   etcd-test-d-010055010107                      1/1     Running   0          3h24m
kube-system   kube-apiserver-test-d-010055010107            1/1     Running   0          3h24m
kube-system   kube-controller-manager-test-d-010055010107   1/1     Running   0          3h24m
kube-system   kube-proxy-vjjqv                                         1/1     Running   0          3h24m
kube-system   kube-scheduler-test-d-010055010107            1/1     Running   0          3h24m

[root@test-d-010055010107 sealos]# journalctl -xeu kubelet
Mar 28 11:43:40 test-d-010055010107 kubelet[20385]: E0328 11:43:40.678552   20385 kubelet.go:2344] "Container runtime network not ready" networkReady="NetworkReady=f
Mar 28 11:43:45 test-d-010055010107 kubelet[20385]: E0328 11:43:45.679314   20385 kubelet.go:2344] "Container runtime network not ready" networkReady="NetworkReady=f

# 看issue上是说没有安装calico导致的,重新安装
s reset # 并不会删除/root/.sealos
s run 133c6a0a0d5f 1123e8b4b455 29516dc98b4b --single --env criData=/data01/containerd

# 看着一切正常
[root@test-d-010055010107 sealos]# kubectl get pod -A
NAMESPACE         NAME                                                     READY   STATUS    RESTARTS   AGE
calico-system     calico-kube-controllers-6b44b54755-qsmkl                 0/1     Pending   0          115s
calico-system     calico-node-7grz7                                        1/1     Running   0          115s
calico-system     calico-typha-6f9598cfd9-2sr27                            1/1     Running   0          115s
kube-system       coredns-6d4b75cb6d-6fncr                                 1/1     Running   0          2m2s
kube-system       coredns-6d4b75cb6d-b8czk                                 1/1     Running   0          2m2s
kube-system       etcd-test-d-010055010107                      1/1     Running   1          2m16s
kube-system       kube-apiserver-test-d-010055010107            1/1     Running   1          2m18s
kube-system       kube-controller-manager-test-d-010055010107   1/1     Running   1          2m16s
kube-system       kube-proxy-wnp2g                                         1/1     Running   0          2m3s
kube-system       kube-scheduler-test-d-010055010107            1/1     Running   1          2m16s
tigera-operator   tigera-operator-d7957f5cc-5wfc4                          1/1     Running   0          2m2s
[root@test-d-010055010107 sealos]#
[root@test-d-010055010107 sealos]#
[root@test-d-010055010107 sealos]# kubectl get node
NAME                             STATUS   ROLES           AGE     VERSION
test-d-010055010107   Ready    control-plane   2m25s   v1.24.0

集群安装

有了单机安装的经验,该踩的坑都踩了,直接开始安装集群

# 尝试集群安装
alias s="sealos --debug --root /data01/sealos --runroot /data01/sealos/docker "
s run 133c6a0a0d5f 1123e8b4b455 29516dc98b4b -e defaultVIP=10.55.10.108 -e criData=/data01/containerd  --masters 10.55.10.107 --nodes 10.55.10.97,10.55.10.106 --passwd 112233
passwd 112233

[root@test-d-010055010107 ~]# kubectl get node -o wide
NAME                             STATUS   ROLES           AGE   VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION               CONTAINER-RUNTIME
test-d-010055010097   Ready    <none>          65s   v1.24.0   10.55.10.97    <none>        CentOS Linux 7 (Core)   3.10.0-693.11.6.el7.x86_64   containerd://1.7.0
test-d-010055010106   Ready    <none>          76s   v1.24.0   10.55.10.106   <none>        CentOS Linux 7 (Core)   3.10.0-693.11.6.el7.x86_64   containerd://1.7.0
test-d-010055010107   Ready    control-plane   95s   v1.24.0   10.55.10.107   <none>        CentOS Linux 7 (Core)   3.10.0-693.11.6.el7.x86_64   containerd://1.7.0

# 看着没啥问题

解决问题用到的参考连接

感想

  • 版本变化多,命令参数有改动,bug隐藏的深
  • 需要耐心抽丝剥茧的排查遇到的问题,可以提前安装些k8s定位问题依赖的命令如
    ctr/crictl
  • 也加入了官方的钉钉群,但基本不答复问题和咨询
  • 关注issue,也是唯一有价值的参考资料了
  • 禁止转载

Admin.NET 是一套基于Furion/.NET 6实现的通用管理平台,模块插件式开发,框架包含了常规的权限管理、字典等管理模块,以及一些Vue3的Demo案例,框架前后端分离。后端基于基于Furion/.NET 6实现,底层集成SqlSugar;前端则是采用Vue-Next-Admin的前端框架,整体是一套非常不错的框架。本人比较喜欢研究一些技术框架,最近对该框架进行了一些研究分析,结合我自己开发框架的思路,对其前后端进行一定的修改调整,本篇随笔记录一些对该框架的相关修改内容。

Admin.NET官网的的地址:
https://gitee.com/zuohuaijun/Admin.NET
,Vue-Next-Admin的官网地址:
https://lyt-top.gitee.io/vue-next-admin-doc-preview/
,有兴趣可以分别到官网上进行预览了解。

1、API及对象接口的处理

一般的前端,为了访问后端接口,以及转换对象,都需要构建后端接口的API代理类,以及相关的对象接口定义,Admin.NET的前端这部分内容放在
api-services
目录 下,包含了apis和models两个目录

不过由于它们可能使用基于类似
generator-swagger-2-ts
插件的方式进行前端代码的生成,因此代码显得非常臃肿,一个简单的API需要来回的封装接口进行调用,以
字典API
为例,每个API的类代码都显得很臃肿,接近1000行代码,这个和我们实际的API调用不太匹配,我们一般只需要简单的调用就可以做到了,太多的代码不利于阅读和维护。

在我的随笔《
基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理
》中介绍过前端的API调用过程场景,如下所示。

前端一般根据框架后端的接口进行前端JS端的类的封装处理,引入了ES6类的概念实现业务基类接口的统一封装,简化代码。

一般我们在基类BaseApi中创建一些常用API的调用处理,那么常用的业务类继承BaseApi,就会具有相关的接口了,如下所示继承关系。

这样我们代码就会变得简洁很多,维护阅读都非常方便。

我们遵循Admin.NET的目录结构,如下所示放置Api接口和业务对象接口类。

根据是否具有常规接口的后台接口定义,我们创建两个不同的基类BaseNormal 和
BaseApi ,
这样我们便于实际的业务类Api的封装抽象。

如下是常规的基类,不具有任何基类接口,只是为了方便构造一些参数

/**
* 此类作为普通API的基类,不继承常规的通用CRUD方法,如文件操作,服务器信息等类
*/exportclassBaseNormal {/**
* 服务器请求的起始路径, 类似 '
http://localhost:***/ protected basePath =serveConfig.basePath;/**
* Api路径。子类通过构造函数修改, 其中api转义为具体的路径,如'/api/test'
*/ protected apiPath = '/api/test';/**
* 请求完整路径(除了方法名),类似 `
http://localhost:**\/api/test`*/ protected baseUrl = this.basePath + this.apiPath;// /**
* 定义一个axios变量,便于子类访问
*/ protected axiosInstance =axiosInstance;/**
* 构造函数,接受Api路径,如'/api/test'
*/constructor(apiPath:string) {//构造函数 this.apiPath =apiPath;this.baseUrl = this.basePath + this.apiPath;
}
}

下面是一个具有数据访问CRUD的操作接口,如下所示。

/**
* 服务器请求基础类
*/exportclass BaseApi<EntityType = any, AddType = any, UpdateType = any>extends BaseNormal {/**
* 分页获取列表
*/page= async (data: object | null) =>{const url = this.baseUrl + `/page`;return await this.axiosInstance.get<UnifyResult<SqlSugarPagedList<EntityType>>>(url, { params: data })
}
/**
* 获取列表
*/list= async (data: object | null) =>{const url = this.baseUrl + `/list`;return await this.axiosInstance.get<UnifyResult<Array<EntityType>>>(url, { params: data })
}
/**
* 新增记录
*/add= async (data: AddType) =>{const url = this.baseUrl + `/add`;return await this.axiosInstance.post<UnifyResult<void>>(url, data)
}
/**
* 更新记录
*/update= async (data: UpdateType) =>{const url = this.baseUrl + `/update`;return await this.axiosInstance.post<UnifyResult<void>>(url, data)
}
/**
* 删除记录
*/delete= async (data: object) =>{const url = this.baseUrl + `/delete`;return await this.axiosInstance.post<UnifyResult<void>>(url, data)
}
/** 批量删除*/batchDelete= async (data: object) =>{const url = this.baseUrl + `/BatchDelete`;return await this.axiosInstance.post<UnifyResult<void>>(url, data)
}
}

根据接口返回的内容,其中
UnifyResult
对象接口是统一接口返回的处理对象,我们在types目录中定义即可,而
SqlSugarPagedList
则是Admin.NET分页返回的结果集合,这些基础类接口也是定义types目录中即可。

而对于对应后端业务类对象接口的定义,我们倾向于把它按业务区分,一个业务类对应的放在一个独立的文件中定义即可,如下所示。

一般包含一个标准的对象接口,增加对象、修改对象、查询对象等接口对象。

业务API代理类的定义,这是根据这些模型的信息进行简单的声明即可,如下对于菜单,如果不考虑除了增删改查的其他额外的接口,那么只需要简单的继承BaseApi即可。

import { BaseApi } from './base-api';
import { SysMenu, UpdateMenuInput, AddMenuInput, MenuOutput }
from '/@/api/models';/**
* 菜单管理Api
*/ class SysMenuApi extends BaseApi<SysMenu, AddMenuInput, UpdateMenuInput>{

.............
/*其他接口定义*/}

export
default new SysMenuApi('/api/sysMenu');

对于没有标准CRUD接口的非常规API接口,我们可以让它继承NormalApi即可。

import { BaseNormal} from './base-api';
import { ConstOutput }
from '/@/api/models';/**
* 系统常量服务 管理Api
*/ classSysConstApi extends BaseNormal {/**
* 获取所有常量列表
*/list= async () =>{const url = this.baseUrl + `/list`;return await this.axiosInstance.get<UnifyResult<Array<ConstOutput>>>(url, { params: null})
}
}

export
default new SysConstApi('/api/sysConst');

有了这些内容我们就可以在实际业务视图中进行API接口的调用了。

对于原先的Admin.NET的业务接口调用,他们需要先引入一个工厂类,然后构造处理才能调用接口,如下定义:

import { getAPI } from '/@/utils/axios-utils';
import { SysMenuApi }
from '/@/api-services/api';

原先的Admin.NET视图组件中的实际的调用代码如下所示。

//查询操作
const handleQuery = async () =>{
state.loading
= true;var res = awaitgetAPI(SysMenuApi).apiSysMenuListGet(state.queryParams.title, state.queryParams.type);
state.menuData
= res.data.result ??[];
state.loading
= false;
};

由于他们是采用Swagger的接口生成,因此默认接口名称都带有api的前缀,Get或者Post的后缀,感觉不是那么易读。

而对于我们重构过的处理逻辑,定义代码如下所示。

import { SysMenu } from '/@/api/models';
import menuApi
from '/@/api/apis/sys-menu-api'

实际视图或者组件中的调用代码如下所示。

//查询操作
const handleQuery = async () =>{
state.loading
= true;var res = awaitmenuApi.list(state.queryParams);
state.menuData
= res.data.result ??[];
state.loading
= false;
};

实际调用代码简单只是一点点,但是Api的定义代码,从上千行调用代码则锐减到仅仅几行代码就可以了,减少了大量重复的累赘接口定义,以及很多模型接口重复定义操作(例如对于分页返回的对象,他们每次都生成一遍重读的类型,而这里则是使用泛型基于
SqlSugarPagedList的方式进行简化
)。

2、基于代码生成工具的生成

有些人说他们虽然代码多了一点,贵在能够根据接口自动生成前端代码呀,确实能自动生成代码是非常不错的一件事情,可以极大提高效率。

那么我们也根据接口的通用性,来构建代码生成的相关规则即可。由于这些接口的生成,大多数情况下,都是以数据库表和字段的规则进行生成的,因此我把它整合在代码生成工具的功能上生成即可。

最后我们把生成的Api部分代码放在目录中

视图代码放在views目录里面对应的目录即可,如下是测试生成的页面,包括有index.vue 页面,以及edit.vue,以及import.vue的页面。

其中index是主页面查询及列表展示内容,edit.vue是新增和编辑界面内容,而import.vue这是导入界面内容。

目录文件如下图所示。

自动生成的index.vue页面代码,根据预定义的模板进行生成,经过多次的校准,已经比较完美的根据数据库表字段及备注信息,生成视图代码了。

生成的页面,进行一定的微调即可用于实际的生产业务中了。

该测试页面添加完成后,在后端创建一个菜单指向它即可,编译运行界面效果如下所示。

我改变了一下常规的界面功能,增加了导入、导出、批量删除的操作入口。

默认进行折叠,展开则列出所有条件,如下界面所示。

导入界面是改进了ele-Import插件,得到界面效果如下所示。

导出则是利用xlsx的插件进行导出Excel文件。

如果需要了解代码生成,可以下载
Database2Sharp代码生成工具
进行了解。

Spdlog是一个快速且可扩展的C++日志库,它支持多线程和异步日志记录。在本文中,我们将分析Spdlog日志库的核心代码,探究其实现原理和代码结构。

Spdlog的基本架构

上一篇文章介绍了spdlog的五个主要组件,其中最重要是Logger、Sink和Formatter其中,Logger负责日志的记录和管理,Sink负责将日志输出到不同的目标(比如控制台、文件、网络等),Formatter负责将日志格式化为字符串。我们会在下面详细的介绍下它们。

Logger

Logger是Spdlog日志库的核心组件,它负责记录和管理日志。Logger的定义如下:

class logger {
public:
    explicit logger(std::string logger_name, sinks_init_list sinks);

    template<typename T>
    void log(level::level_enum lvl, const T &msg);
    
		template<typename... Args>
    void log(source_loc loc, level::level_enum lvl, format_string_t<Args...> fmt, Args &&... args);
    
    template<typename... Args>
    void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&... args);
    
    // ...
    template<typename... Args>
    void trace(format_string_t<Args...> fmt, Args &&... args);
    
    template<typename... Args>
    void debug(format_string_t<Args...> fmt, Args &&... args);

    template<typename... Args>
    void info(format_string_t<Args...> fmt, Args &&... args);

    // ....

private:
    std::string name_;
    std::vector<sink_ptr> sinks_;
    // ...
};

Logger主要包含一个名称和一组Sink,它还提供了log()方法用于记录日志。当调用log()方法时,Logger会将日志消息传递给每个Sink,并由Sink将日志输出到目标。Logger还提供了其他一些方法,比如设置日志级别、添加和删除Sink等。

Logger 是个日志包装器,包含了日志名称和一组Sink,它提供了输出不同级别日志的方法,通过不同Sink的组合可以输出到一个或多个不同输出路径(文件,控制台,网络等)。

日志名全局唯一

每个
logger
都有一个名称,并且是全局唯一的,通过上一篇提到的
register
组件注册到全局的 map里,代码如下,
registry

loggers_
字段通过名字记录了所有的
logger
实例。

class registry
{
public:
    // ..... 
    // 注册日志
	  void register_logger(std::shared_ptr<logger> new_logger);

 private:
  // ....
	std::unordered_map<std::string, std::shared_ptr<logger>> loggers_;
}

registry
提供
register_logger
接口注册日志。这里值得注意点是:注册时候如果发现已经存在则会抛异常,
throw_if_exists_
会检查是否已经存在同名日志实例,存在则通过
throw_spdlog_ex
抛出异常。

SPDLOG_INLINE void registry::throw_if_exists_(const std::string &logger_name)
{
    if (loggers_.find(logger_name) != loggers_.end())
    {
        throw_spdlog_ex("logger with name '" + logger_name + "' already exists");
    }
}

SPDLOG_INLINE void registry::register_logger_(std::shared_ptr<logger> new_logger)
{
    auto logger_name = new_logger->name();
    throw_if_exists_(logger_name);
    loggers_[logger_name] = std::move(new_logger);
}

日志输出控制

  • 提供不同级别日志的输出接口


logger
类中,Spdlog 提供了不同级别日志的输出接口,包括
trace()

debug()

info()

warn()

error()

critical()
等。下面是
logger
类中提供的不同级别日志输出接口的代码示例:

template<typename... Args>
void trace(format_string_t<Args...> fmt, Args &&... args);

template<typename... Args>
void debug(format_string_t<Args...> fmt, Args &&... args);

template<typename... Args>
void info(format_string_t<Args...> fmt, Args &&... args);

template<typename... Args>
void warn(format_string_t<Args...> fmt, Args &&... args);

template<typename... Args>
void error(format_string_t<Args...> fmt, Args &&... args);

template<typename... Args>
void critical(format_string_t<Args...> fmt, Args &&... args);

使用这些接口,可以根据不同的日志级别输出不同的日志信息,比如
logger.info("This is an info message.");
将输出一条信息级别为
info
的日志。

  • 日志输出级别控制

logger
提供了
set_level
接口来设置日志级别,这个级别可以是枚举类型
level
中的任何一个,比如
spdlog::set_level(spdlog::level::trace);
将设置日志级别为
trace
,这样所有级别的日志都会被记录下来。如果想要只记录
info
级别及以上的日志,则可以使用
spdlog::set_level(spdlog::level::info);

如果想要在运行时动态地设置日志级别,可以使用
set_level()
方法,例如
logger->set_level(spdlog::level::trace);
将设置当前
logger
的日志级别为
trace

注意:如果想要关闭日志,则可以将日志级别设置为
off
,例如
spdlog::set_level(spdlog::level::off);

enum class level
{
    trace,
    debug,
    info,
    warn,
    err,
    critical,
    off
};
  • 日志刷新控制

logger
提供了一些控制日志刷新的方法,最重要的方法是flush()。当调用flush()方法时,Logger会将所有挂起的日志消息刷新到Sink中。Logger还提供了set_pattern()方法,用于设置日志格式化模式。这个方法可以用于修改日志的输出格式,例如添加时间戳、线程ID等信息。

void flush();
  • 日志格式设置

logger
提供了设置日志格式的方法
set_pattern
通过此方法可以设置包含的 skin的日志格式,具体的格式信息可以参考上一篇。

// 日志格式设置
void set_pattern(std::string pattern, pattern_time_type time_type = pattern_time_type::local);

通过格式字符串 pattern,会生成
formatter
实例,调用 skin的
set_formatter
接口设置日志格式。

SPDLOG_INLINE void logger::set_pattern(std::string pattern, pattern_time_type time_type)
{
    auto new_formatter = details::make_unique<pattern_formatter>(std::move(pattern), time_type);
    set_formatter(std::move(new_formatter));
}

// set formatting for the sinks in this logger.
// each sink will get a separate instance of the formatter object.
SPDLOG_INLINE void logger::set_formatter(std::unique_ptr<formatter> f)
{
    for (auto it = sinks_.begin(); it != sinks_.end(); ++it)
    {
        if (std::next(it) == sinks_.end())
        {
            // last element - we can be move it.
            (*it)->set_formatter(std::move(f));
            break; // to prevent clang-tidy warning
        }
        else
        {
            (*it)->set_formatter(f->clone());
        }
    }
}

【问题描述】

开发有天碰到一个很奇怪的问题,他的场景是这样子的:
通过Canal来订阅MySQL的binlog, 当捕获到有数据变化时,回到数据库,反查该数据的明细,然后做进一步处理。
有一次,他碰到一个诡异的现象:

1.  Canal收到消息,有一条主键id=31019319的数据插入
2.  11:19:51.081, 应用程序去反查数据库,11:19:51.084查询完毕,发现id=31019319的数据为空
3.  过几分钟后,开发去手工查数据库,发现id=31010319的数据是存在的,每次插入的时候,我们会在数据库记录插入时间,发现插入的时间是11:19:51.059。

让开发感到困惑的是11:19:51.059写入的数据,11:19:51.081去查询,应该是能查到数据的呀。我们首先排除了读写分离,主从分离等场景,Canal订阅和数据库查询都是在Master上,所以这个问题就变得非常诡异了。

【问题分析】

因为中间夹杂着Canal, 而Canal是通过binlog读取的,这个问题我们可以简化为:当我们在master插入一条数据,该数据在master还没落库,但是在Slave却能查到。我们尝试重现这种场景。因为我们是采用GTID模式,GTID也就是全局事务编号,我们通过跟踪GTID来调试问题。

我们创建一个测试表如下:

CREATE TABLE `gtid_debug` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

此时,在Master和Slave上,分别收集到的GTID信息如下:

角色 @@global_gtid_executed @@port
Master be7945f1-3613-11ec-8353-98039ba5775a:
1-16
3306
Slave be7945f1-3613-11ec-8353-98039ba5775a:
1-16
3307

我们在Master上开启gdb调试,在函数ReplSemiSyncMaster::commitTrx上设置断点。

步骤1:

在Master上,开启Session1, 插入一条数据:

insert into gtid_debug(name)values('test1'); 

此时会hit到断点。

步骤2:

在Slave上,开启Session2, 查看GTID:

角色 @@global_gtid_executed @@port
Slave be7945f1-3613-11ec-8353-98039ba5775a:
1-17
3307

也就是说,事务在Slave上,开始走字了。
我们进行如下查询:可以看到,在Slave这条记录能被查询到。

slave>select * from test.gtid_debug;
| ID   | NAME  |
| ---- | ----- |
| 1    | test1 |

步骤3:

在Master上,我们开启Session3, 查看GTID, 这个session也会被断点中断,我们继续执行下一步,直到查询结果返回。注意,此时Session1还停留在断点上,未提交成功。

角色 @@global_gtid_executed @@port
Master be7945f1-3613-11ec-8353-98039ba5775a:
1-16
3306

并进行如下查询,返回结果为空:

master>select * from test.gtid_debug;
Empty set 

所以我们重现了问题,也就是说,在Master插入数据,事务还没有提交,但在Slave就能查到了。 Slave跑的比Master还快。

【原因分析】

重现了问题后,我们对问题进行分析,并查看了相应代码,发现是半同步复制的模式导致,半同步复制有两种模式: After_Sync(5.7版本默认)模式和After_Commit(5.6版本默认)模式。我们线上的版本是5.7,所以采用的是After_Sync模式。

从上图可以看到,一个事务在半同步模式下提交,无论是after_sync还是after_commit,都要经历4个阶段:

1. InnoDB Redo File Prepare Write
2. Binlog File Flush & Sync
3. InnoDB Redo File Commit (同时释放事务持有的锁)
4. Send binlog to Slave

After_Commit模式的四个阶段顺序为: 1->2->3->4, 而after_sync模式的顺序为1->2->4->3.

在5.7默认的after_sync模式下,确实存在先发送binlog到Slave, 然后再进行事务提交的场景。这时候大家会问了,为啥5.7把半同步复制改为after_sync模式了?这主要是因为after_commit机制存在数据丢失的风险。我们可以设想一下,在3->4的T1时间段,新数据对其它Session已经可见,突然Master挂了,MySQL进行主从切换,这时事务在Master上完成,如在Slave上不存在,切换后,业务会发现之前能查到的数据又没了。

而在after_sync模式下,其执行的顺序为1->2->4->3. 也就是说Master在收到Slave的应答之后,才Commit事务。在3->4的T1时间段内,因事务还未Commit,新数据对其它Session还不可见,所以看上去像比Slave跑的更慢。具体可以参考网上关于这两种模式的讨论。

【解决建议】

我们分析清楚问题之后,解决的方法就比较简单了。不建议改为after_commit模式,虽然改为after_commit模式,可以保证事务在Master落地后,Canal才会读到消息,但存在主从切换事务丢失的风险。我们的解决方法,是在Canal消息处理时,延后1秒再处理。这样解决方法比较合理。因为一般来讲,业务对消息的实时性不是特别高。

这份报告是机票预订系统的总体设计,老师评分90分,应该是图画的好。

image-20230328170849320

1.引言

1.1编写目的

由前面的需求分析,得出了系统的基本需求,要实现整个系统,需要对用户的需求进行设计,概要设计主要是利用比较抽象的语言对整个系统进行概括,确定对系统的物理配置,确定整个系统的处理流程和系统的数据结构,接口设计,实现对系统的初步设计。目的在推动软件工程的规范化,使设计人员遵循统一的概要设计书写规范,节省制作文档的时间,降低系统实现的风险,做到系统设计资料的规范性与全面性,以利于系统的实现、测试、维护、版本升级等。

1.2背景

本项目的名称:机票预订系统。

随着人们物质需求的提高,科技全球化的发展,乘坐飞机成为多数人生活、旅行中不可缺少的一部分。而飞机的航班的数量和业务量庞大,仅仅靠传统的记账式管理是不可行的。机票预订系统应运而生,逐渐成为信息化建设的重要组成部分。机票预订系统为机场的管理员提供所有乘客的详细信息,以及飞机航班的详细情况,对飞机购票和航班信息两大功能进行合理操纵并登记。

1.3 定义

开发(develop ):除了单纯的开发活动外,还包括维护活动。

项目(project ):向顾客交付的最终的全部产品,包括程序及各种文档,以及开发活动所需资源经费等各种信息。

项目开发计划(project development plan):把项目与过程联系起来的计划方案。

产品生命周期(product life cycle):产品从构思到不可在使用的持续时间。

1.4参考文献

张海藩:《软件工程导论》第五版 清华大学出版社 肖刚等:《实用软件文档写作》清华大学出版社 李涛、刘凯奎、王永皎:《Visual C# SQL Server 数据库开发与实例 》清华大学出版社

2.总体设计

2.1需求规定

主要输入输出项目:

航班信息:(航班号、飞机号、登机口、起飞时间、最近一天航班的日期和余票额)

旅客信息:(姓名、性别、身份证号码、旅行时间、旅行始发地和目的地)

订票:(目标航班、订票数额、座位号)

退票:(日期、航班)

2.2运行环境

数据库管理系统软件:Mysql

运行软件:Java eclipse/Idea

运行平台:Windows XP/ Windows 7/ Windows 10/ Windows 11

系统类型:64 位操作系统 基于 x64 的处理器

CPU:1.6GHz 以上

内存:256M 以上

分辨率:最佳为1024*768像素

2.3基本设计概念和处理过程

录入模块:可以录入航班情况,全部数据可以只放在内存中,最好存储在文件中。管理员登录系统后,输入需要录入航班信息。系统对航班信息进行处理,将航班信息有顺序地写到文件中。

航班查询模块:根据旅客提出的终点站名输出下列信息:航班号、飞机号及星期几飞行、最近一天航班的日期和余票额。旅客登录系统后,输入需求的终点站名进行航班查询。系统对旅客查询的航班订单进行处理,系统将显示适合要求的航班信息。

订票模块:根据客户提供的要求(航班、订票数额),查询该航班的票额情况,若有余票,则为客户办理订票手续,输入座位号;若无客户需求的座位号,则重新询问客户要求。客户进入订票系统后,输入航班和订票数额。系统对客户输入的航班进行查询找到目标航班,查询该航班的票额情况并与客户输入订票数额进行比对,最后为客户处理要求。

退票模块:根据客户提供的情况(日期、航班),为客户办理退票手续,然后查询该航班是否有人排队候补,首先询问排在第一的客户,若所剩余票额能满足他的要求,则为他办理订票手续,否则依次询问其他排队候补的客户。

登录模块:分为管理员和客户登录两种,管理员拥有客户的权限以及航班管理和客户信息管理的权限。客户只有客户权限。

系统流程图:

image-20230324234621067

3. 数据库设计

列名 数据类型 可否为空 说明 主键
User_Name CHAR(10) NOT NULL 姓名
User_No INT(11) NOT NULL 联系方式
User_ID CHAR(18) NOT NULL 证件号码
User_Type BOOLEAN NOT NULL 是否为学生

​ 3.1乘客信息表

列名 数据类型 可否为空 说明 主键
Plane_No INT(15) NOT NULL 航班编号
Plane_Origin CHAR(10) NOT NULL 起飞地点
Plane_Destination CHAR(10) NOT NULL 目的地
Plane_Departure CHAR(10) NOT NULL 出发时间
Plane_Arrival CHAR(10) NOT NULL 到达时间

​ 3.2航班信息表

列名 数据类型 可否为空 说明 主键
Plane_No INT(15) NOT NULL 航班编号
ticket_Level CHAR(4) NOT NULL 机舱等级
ticket_Price INT(5) NOT NULL 价格
ticket_Status CHAR(5) NOT NULL 购票状态

​ 3.3机票信息表

4。接口设计

4.1用户接口

在用户界面部分,根据需求分析的结果,用户需要一个用户友善界面。在界面设计上,应做到简单明了,易于操作,并且要注意到界面的布局,应突出的显示重要以及出错信息。外观上也要做到合理化。总的来说,系统的用户界面应作到可靠性、简单性、易学习和使用。

4.2外部接口

在输入方面,对于键盘、鼠标的输入。在输出方面,打印机的连接及使用。在网络传输部分,在网络硬件部分,实现高速传输。

4.3内部接口

内部接口方面,各模块之间采用函数调用、参数传递、返回值的方式进行信息传递。具体参数的结构将在下面数据结构设计的内容中说明。接口传递的信息将是以数据结构封装了的数据,以参数传递或返回值的形式在各模块间传输。

5.运行设计

5.1运行控制

​ 管理员先对有合作的航空公司的数据库进行连接,然后把数据写在文件上。用户登录系统后进行查询相关的航班信息,信息从文件读取形成航班信息表给用户。用户进行订票功能,从查询出来的航班信息进行选择,进入支付页面,支付成功就把相关信息写在数据库里。用户进行退票功能,系统从数据库中查找用户支付成功的信息返回给用户。用户对已经支付成功的机票进入退票页面,退票成功信息返回给用户,并且从数据库中删除之前用户支付成功的机票信息。

5.2 运行模块的组合

image-20230324234705974

5.3 运行时间

整个机票预定系统总体占用内存较小,运行速度很快,不管是管理员还是用户,体验感不错,反应迅速。

6.系统出错设计

6.1出错信息

在用户使用错误的数据或访问没有权限的数据后,系统给出提示:“对不起,你非法使用数据,没有权限!”而且用户的密码管理可以允许用户修改自己的密码,不允许用户的匿名登录。

6.2补救措施

由于数据在数据库中已经有备份,故在系统出错后可以依靠数据库的恢复功能,并且依靠日志文件使系统再启动,就算系统崩溃用户数据也不会丢失或遭到破坏。但有可能占用更多的数据存储空间,权衡措施由用户来决定。