libraco

libraco 查看完整档案

武汉编辑  |  填写毕业院校  |  填写所在公司/组织 sha256.cc 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

libraco 收藏了文章 · 2020-07-20

Ceph实现DockerSwarm集群共享存储的尝试

一、背景与结论

在一个四节点的Docker Swarm集群上,尝试使用Ceph作为Docker集群的共享存储,解决有状态服务在分布式环境下的数据存储问题。

经过尝试,成功使用CephFS实现了多个Docker节点之间的存储共享。但同时注意到,对于小规模Docker集群并且运维成本有限的场景,Ceph这样的分布式对象存储系统仍然显得有点重了,本文的4节点docker swarm集群上,在Ceph集群与Portainer-Agent(docker运维工具)部署之后,容器数量达到了30个。因此最终决定采用其他方案实现小规模docker集群的共享存储,比如NFS。

但Ceph作为一个高可用高性能的分布式对象/块/文件存储系统,在其他场景还是有使用价值的。

本文主要记述Ceph的搭建过程以及如何用cephfs实现docker swarm集群的共享存储。其他诸如原理机制,运维资料等请参考社区文档。

Ceph对外提供了三种存储接口,分别是对象存储 RGW(rados gateway)、块存储 RBD(rados block device) 和文件存储 CephFS。

RGW是RestAPI,docker的数据卷无法直接使用。从性能角度考虑,应该是RBD最快,但本文使用的Ceph版本与使用RBD做共享存储所需要的中间件RexRay貌似存在版本不兼容问题,最终没能成功。。。

最终本文使用了CephFS来实现docker swarm集群的共享存储。

二、版本与相关资料

本文搭建Ceph使用了目前社区文档推荐的安装工具cephadm,安装的版本为octopus。四个节点的操作系统均为CentOS7

  • 社区文档入口:https://docs.ceph.com/docs/master/
  • 中文社区文档入口:http://docs.ceph.org.cn/
注意,编写本文时,中文社区文档并非最新版本,比如社区文档中目前建议使用的安装工具cephadm在中文社区文档中就没有相关资料。

三、Ceph角色规划

NoiphostnameOSD盘Ceph角色
1172.17.13.1manager01.xxx.com/dev/sdbmon,osd,ceph-mds,mgr
2172.17.13.2manager02.xxx.com/dev/sdbmon,osd,ceph-mds
3172.17.13.3worker01.xxx.com/dev/sdbmon,osd,ceph-mds
4172.17.13.4worker02.xxx.com/dev/sdbmon,osd,ceph-mds,mgr,cephadm,NTP服务器

Ceph用于存储数据的服务OSD需要使用干净的磁盘,要求如下:

  1. 设备没有分区
  2. 设备不得具有任何LVM状态
  3. 设备没有挂载
  4. 设备不包含任何文件系统
  5. 设备不包含ceph bluestore osd
  6. 设备必须大于5G

也就是给节点增加一块新磁盘之后,只要linux能识别到即可,不要做分区/格式化/mount等操作。

四、各节点环境准备工作

4.1 防火墙与Selinux

提前将Ceph相关防火墙规则设置好。或直接关闭防火墙。

firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --permanent --zone=public --add-service=ceph
firewall-cmd --permanent --zone=public --add-service=ceph-mon
firewall-cmd --reload
firewall-cmd --zone=public --list-services

firewall-cmd --zone=public --add-port=3300/tcp --permanent
firewall-cmd --zone=public --add-port=3300/udp --permanent
firewall-cmd --zone=public --add-port=6789/tcp --permanent
firewall-cmd --zone=public --add-port=6800-7300/tcp --permanent
firewall-cmd --zone=public --add-port=8443/tcp --permanent
firewall-cmd --reload
firewall-cmd --zone=public --list-ports

Ceph集群在使用中最好禁用Selinux。在各个节点上以root用户vi /etc/selinux/config,修改:

#SELINUX=enforcing
SELINUX=disabled

然后执行setenforce 0

4.2 各节点添加epel-release资源包

从阿里云镜像获取/etc/yum.repos.d/epel.repo,如下:

wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo

4.3 各节点安装依赖库

填坑的过程中尝试过不同的搭建方案,这里有些依赖库可能不是必须的。。。

yum -y install python3 yum-utils
yum install yum-plugin-priorities -y
yum install gcc python-setuptools python-devel -y
easy_install pip

4.4 安装Docker CE

本文的环境已经提前安装了docker ce,docker compose,并创建了docker swarm集群。

每个节点都需要先安装好Docker,具体方法参考官方文档:https://docs.docker.com/engine/install/centos/
注意最好使用国内的dokcer安装yum镜像。

yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

安装完成之后,添加docker镜像仓库地址,vi /etc/docker/daemon.json,添加:

  "registry-mirrors": [
          "https://docker.mirrors.ustc.edu.cn",
          "https://dockerhub.azk8s.cn",
          "https://reg-mirror.qiniu.com",
          "https://hub-mirror.c.163.com",
          "https://mirror.ccs.tencentyun.com",
          "https://registry.docker-cn.com"
  ],

然后重启docker服务:

systemctl daemon-reload
systemctl restart docker

然后提前为每个节点拉取Ceph相关镜像:

docker pull ceph/ceph:v15
docker pull ceph/ceph-grafana
docker pull prom/prometheus:v2.18.1
docker pull prom/alertmanager:v0.20.0
docker pull prom/node-exporter:v0.18.1

4.5 安装NTP并同步各节点时间

各节点安装ntp服务

yum install ntp ntpdate ntp-doc -y

在管理节点worker02.xxx.com上安装ntp服务,vi /etc/ntp.conf:

...
restrict 127.0.0.1 
restrict ::1
restrict 172.17.13.0 mask 255.255.255.0

# Hosts on local network are less restricted.
#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap

# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst
server 127.127.1.0
...

配置了NTP服务的管理节点需要防火墙配置端口:

firewall-cmd --zone=public --add-port=123/udp --permanent
firewall-cmd --reload
firewall-cmd --zone=public --list-ports

在其他节点设置ntp服务器为刚刚设定的ntp服务器,vi /etc/ntp.conf:

…
# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
server worker02.xxx.com
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst
…

重启各个节点的ntp服务,并设置开机启动:

systemctl enable ntpd
systemctl restart ntpd
systemctl status ntpd

# Centos7默认安装了chronyd并且默认enable,这里需要关闭,否则重启以后会导致ntpd不能启动
systemctl disable chronyd

管理节点从外网同步时间,其他节点从管理节点同步时间:

# 管理节点
ntpdate -u ntp1.aliyun.com

# 其他节点
ntpdate -u worker02.xxx.com

4.6 更新SSH服务

各节点默认已安装,可以更新最新版本,并确认服务状态

yum install openssh-server -y
service sshd status

4.7 确保短主机名可以ping通

在每个节点以root身份vi /etc/hosts,添加:

172.17.13.1  manager01
172.17.13.2  manager02
172.17.13.3  worker01
172.17.13.4  worker02
Ceph默认使用短域名,社区文档在环境检查中要求能以短域名ping通,但本文环境都是用的FQDN,这里即使设置短域名到hosts文件,后续依然有些许小问题。

五、Ceph集群搭建

5.1 cephadm节点安装cephadm

以下操作均在cephadm节点上执行。(本文规划将worker02.xxx.com作为cephadm节点)

cd ~
mkdir cephadmin
cd cephadmin
curl --silent --remote-name --location https://hub.fastgit.org/ceph/ceph/raw/octopus/src/cephadm/cephadm
chmod +x cephadm
ll -h
官方地址https://github.com/ceph/ceph/raw/octopus/src/cephadm/cephadm总是下载失败,因此使用了国内某镜像地址。

5.2 初始化Ceph集群

以下操作均在cephadm节点上执行。

生成ceph的yum源文件并将其替换为使用阿里云yum源:

./cephadm add-repo --release octopus
cat /etc/yum.repos.d/ceph.repo

sed -i 's#download.ceph.com#mirrors.aliyun.com/ceph#' /etc/yum.repos.d/ceph.repo
cat /etc/yum.repos.d/ceph.repo
yum list | grep ceph
注意版本是octopus

安装cephadm:

./cephadm install
which cephadm

引导ceph集群:

mkdir -p /etc/ceph
cephadm bootstrap --mon-ip 172.17.13.4 --allow-fqdn-hostname

成功后会出现如下信息:

...
INFO:cephadm:Ceph Dashboard is now available at:

         URL: https://worker02.xxx.com:8443/
        User: admin
    Password: 3y44vf60ms

INFO:cephadm:You can access the Ceph CLI with:

    sudo /usr/sbin/cephadm shell --fsid fbd10774-c8cf-11ea-8bcc-00505683571d -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring

INFO:cephadm:Please consider enabling telemetry to help improve Ceph:

    ceph telemetry on

For more information see:

    https://docs.ceph.com/docs/master/mgr/telemetry/

INFO:cephadm:Bootstrap complete.

尝试登录Ceph CLI:

[root@worker02 ~]# cephadm shell --fsid fbd10774-c8cf-11ea-8bcc-00505683571d -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring
INFO:cephadm:Using recent ceph image ceph/ceph:v15
[ceph: root@worker02 /]# exit
exit

在浏览器中访问https://worker02.xxx.com:8443/,打开 ceph ui, 第一次登陆要求更改默认密码

域名访问前先配置浏览器所在节点的hosts文件

5.3 安装ceph-common并验证集群

以下操作均在cephadm节点上执行。

安装 ceph 工具包, 其中包括 ceph, rbd, mount.ceph 等命令:

cephadm install ceph-common

验证集群状态:

# 查看 ceph 集群所有组件运行状态
ceph orch ps

# 查看指定组件运行状态
ceph orch ps --daemon-type mon

# 查看集群当前状态
ceph status
ceph -s

5.4 向集群添加主机

将之前cephadm bootstrap初始化集群命令所生成的ceph密钥拷贝到其他节点root用户的"~/.ssh"目录。注意要输入其他节点root用户密码。

ssh-copy-id -f -i /etc/ceph/ceph.pub root@manager01.xxx.com
ssh-copy-id -f -i /etc/ceph/ceph.pub root@manager02.xxx.com
ssh-copy-id -f -i /etc/ceph/ceph.pub root@worker01.xxx.com

将其他节点加入集群

ceph orch host add manager01.xxx.com
ceph orch host add manager02.xxx.com
ceph orch host add worker01.xxx.com

ceph orch host ls

查看集群当前服务分布(此时应该有4个crash,4个mon,两个mgr)

ceph orch ps

5.5 部署OSD

检查每个节点是否有一块尚未分区的新磁盘,例如这里的sdb:

[root@worker01 ~]# lsblk 
NAME            MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
fd0               2:0    1     4K  0 disk 
sda               8:0    0   300G  0 disk 
├─sda1            8:1    0   500M  0 part /boot
└─sda2            8:2    0 299.5G  0 part 
  ├─centos-root 253:0    0 295.5G  0 lvm  /
  └─centos-swap 253:1    0     4G  0 lvm  [SWAP]
sdb               8:16   0   300G  0 disk 
sr0              11:0    1  1024M  0 rom 
对于虚拟机,在虚拟机控制台直接给运行中的虚拟机添加新磁盘之后,每个节点执行以下命令就可以刷出磁盘信息,不用重启:
echo "- - -" > /sys/class/scsi_host/host0/scan
echo "- - -" > /sys/class/scsi_host/host1/scan
echo "- - -" > /sys/class/scsi_host/host2/scan

将新磁盘设备加入集群

ceph orch daemon add osd manager01.xxx.com:/dev/sdb
ceph orch daemon add osd manager02.xxx.com:/dev/sdb
ceph orch daemon add osd worker01.xxx.com:/dev/sdb
ceph orch daemon add osd worker02.xxx.com:/dev/sdb

# 查看设备信息
ceph orch device ls
# 查看挂载好的osd信息
ceph osd df

5.6 其他节点安装ceph-common

为了在其他节点也能够直接访问Ceph集群,我们需要在其他节点上也安装ceph-common。

cephadm以外的其他节点执行:

mkdir ~/cephadmin
mkdir /etc/ceph

从cephadm节点以拷贝ceph.repo,cephadm,ceph集群配置文件,ceph客户端管理员密钥到其他节点:

scp /etc/yum.repos.d/ceph.repo root@manager01.xxx.com:/etc/yum.repos.d
scp /etc/yum.repos.d/ceph.repo root@manager02.xxx.com:/etc/yum.repos.d
scp /etc/yum.repos.d/ceph.repo root@worker01.xxx.com:/etc/yum.repos.d

scp ~/cephadmin/cephadm root@manager01.xxx.com:~/cephadmin/
scp ~/cephadmin/cephadm root@manager02.xxx.com:~/cephadmin/
scp ~/cephadmin/cephadm root@worker01.xxx.com:~/cephadmin/

scp /etc/ceph/ceph.conf root@manager01.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.conf root@manager02.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.conf root@worker01.xxx.com:/etc/ceph/

scp /etc/ceph/ceph.client.admin.keyring root@manager01.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.client.admin.keyring root@manager02.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.client.admin.keyring root@worker01.xxx.com:/etc/ceph/

其他节点执行:

cd ~/cephadmin
./cephadm install ceph-common

ceph -s

5.7 健康检查的错误

在执行ceph -s或者ceph health时,可能会发现如下的错误:

Module 'cephadm' has failed: auth get failed: failed to find client.crash.worker02 in keyring retval: -2

推测是Ceph对长域名支持不足的原因。通过ceph auth ls命令可以查看ceph集群所有的用户的密钥,能查到对应的长域名client.crash.worker02.xxx.com用户但没有短域名用户。通过以下命令创建对应的短域名用户:

# 手动添加用户client.crash.xxx,任意节点执行
ceph auth add client.crash.worker02 mgr 'profile crash' mon 'profile crash'
ceph auth add client.crash.manager01 mgr 'profile crash' mon 'profile crash'
ceph auth add client.crash.manager02 mgr 'profile crash' mon 'profile crash'
ceph auth add client.crash.worker01 mgr 'profile crash' mon 'profile crash'

# 查看新增用户是否成功
ceph auth ls

# 重启ceph集群,各个节点上都需要执行
systemctl restart ceph.target

# 也可以用下面的命令停止再重启ceph集群,各个节点上都需要执行
systemctl stop ceph.target
systemctl stop ceph\*.service ceph\*.target
ps -ef | grep ceph
docker ps -a
systemctl restart ceph.target

# 重启后重新检查状态
ceph -s

六、部署cephfs服务

6.1 创建cephfs

任意节点上执行:

# 创建一个用于cephfs数据存储的池,相关参数自行参阅社区文档,一言难尽。。。
ceph osd pool create cephfs_data 64 64
# 创建一个用于cephfs元数据存储的池
ceph osd pool create cephfs_metadata 32 32
# 创建一个新的fs服务,名为cephfs
ceph fs new cephfs cephfs_metadata cephfs_data
# 查看集群当前的fs服务
ceph fs ls
# 设置cephfs最大mds服务数量
ceph fs set cephfs max_mds 4
# 部署4个mds服务
ceph orch apply mds cephfs --placement="4 manager01.xxx.com manager02.xxx.com worker01.xxx.com worker02.xxx.com"
# 查看mds服务是否部署成功
ceph orch ps --daemon-type mds
本文在这里遇到一个问题,mds服务一直不能启动,查看ceph health发现一个1 filesystem is online with fewer MDS than max_mds的警告,应该是ceph fs set cephfs max_mds 4没有生效。后来重启了整个集群就好了。
# 在所有节点上执行
systemctl restart ceph.target

# 重启之后检查相关服务
ceph orch ps --daemon-type mds
ceph osd lspools
ceph fs ls

6.2 创建cephfs访问用户

创建用户,用于客户端访问CephFs

ceph auth get-or-create client.cephfs mon 'allow r' mds 'allow r, allow rw path=/' osd 'allow rw pool=cephfs_data' -o ceph.client.cephfs.keyring

查看输出的ceph.client.cephfs.keyring密钥文件,或使用下面的命令查看密钥:

ceph auth get-key client.cephfs

6.3 挂载cephfs到各节点本地目录

在各个节点执行:

mkdir /mnt/cephfs/
mount -t ceph manager01.xxx.com:6789,manager02.xxx.com:6789,worker01.xxx.com:6789,worker02.xxx.com:6789:/ /mnt/cephfs/ -o name=cephfs,secret=<cephfs访问用户的密钥>
manager01.xxx.com:6789,manager02.xxx.com:6789,worker01.xxx.com:6789,worker02.xxx.com:6789 是所有mon服务

编辑各个节点的/etc/fstab文件,实现开机自动挂载,添加以下内容:

manager01.xxx.com:6789,manager02.xxx.com:6789,worker01.xxx.com:6789,worker02.xxx.com:6789:/     /mnt/cephfs    ceph    name=cephfs,secretfile=<cephfs访问用户的密钥>,noatime,_netdev    0       2
相关参数请自行查阅linux下fstab的配置资料。

6.4 使用cephfs实现docker共享存储

将挂载到本地的cephfs作为本地磁盘使用即可。例如本文中,可以在docker run命令中,或者docker compose编排文件中,使用volume本地挂载到/mnt/cephfs下。

示例,注意volumes:

...
  portainer:
    image: portainer/portainer
    ...
    volumes:
      - /mnt/cephfs/docker/portainer:/data
    ...
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]
...
至此,使用cephfs实现docker共享存储的尝试已经OK。

七、部署RBD

Ceph的RBD对外提供的是块存储,但块存储直接作为磁盘挂载到各节点的话,并不能实现不同节点之间的数据共享。因此需要配合中间件Rexray,以及docker插件rexray/rbd来实现共享存储。

但本文并未成功,原因后叙。

7.1 初始化rbd pool并创建RBD的image

在任意节点上执行:

ceph osd pool create rbd01 32
rbd pool init rbd01

# 在rbd01上创建一个300G的image
rbd create img01 --size 307200 --pool rbd01
rbd ls rbd01
rbd info --image img01 -p rbd01

此时可以在各个节点上挂载RBD的img01,但是并不能实现存储共享。

# 向linux系统内核载入rbd模块
modprobe rbd

# 将img01映射到系统内核
rbd map img01 --pool rbd01 --id admin
# 失败,需要禁用当前系统内核不支持的feature
rbd feature disable img01 --pool rbd01 exclusive-lock, object-map, fast-diff, deep-flatten
# 重新映射
rbd map img01 --pool rbd01 --id admin

# 映射成功后,会返回映射目录"/dev/rbd0",对该目录进行格式化:
[root@worker01 ~]# mkfs.xfs /dev/rbd0
meta-data=/dev/rbd0              isize=512    agcount=17, agsize=4914176 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=0, sparse=0
data     =                       bsize=4096   blocks=78643200, imaxpct=25
         =                       sunit=1024   swidth=1024 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal log           bsize=4096   blocks=38400, version=2
         =                       sectsz=512   sunit=8 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0

# mount到本地
mkdir /mnt/data01
mount /dev/rbd0 /mnt/data01

# 查看本地磁盘信息
df -hT

# 卸除挂载并删除image
umount /mnt/data01
rbd unmap img01 --pool rbd01 --id admin
rbd rm --image img01 -p rbd01 

7.2 安装rexray与rexray/rbd

任意某个节点下载rexray:

curl -sSL https://rexray.io/install | sh

编辑配置文件vi /etc/rexray/config.yml,内容如下:

rexray:
  logLevel:        debug
libstorage:
  logging:
    level:         debug
    httpRequests:  true
    httpResponses: true
libstorage:
  service: rbd
rbd:
  defaultPool: rbd01

尝试启动Rexray服务,这里一直失败:

[root@manager01 rexray]# rexray start
...
error: service startup failed: agent: mod init failed: error initializing instance ID cache
日志中除了最后的错误,并无其他有用的信息。在github对应项目的issue中有类似问题,解决方法是ceph配置文件/etc/ceph/ceph.conf中添加mon_host配置。但本文使用的ceph版本octopus的配置文件中已经明确配置了mon_host,且格式与issue所述不同。

最终也没有找到原因,怀疑是Ceph版本太新,与rexray不兼容所致。所以这里的尝试到此为止,后续安装docker插件与创建rbd数据卷并未进行。

如果Rexray服务能成功启动,那么后续还需要在每个节点安装docker插件rexray/rbd:

docker plugin install rexray/rbd RBD_DEFAULTPOOL=rbd01 LINUX_VOLUME_FILEMODE=0777

最后在docker swarm上创建rbd驱动的数据卷:

docker volume create -d <rexrayHost>:5011/rexray/rbd <数据卷名称>

如果成功,docker run或编排文件中的volume使用事先创建好的数据卷即可实现存储共享。

查看原文

libraco 赞了文章 · 2020-07-20

Ceph实现DockerSwarm集群共享存储的尝试

一、背景与结论

在一个四节点的Docker Swarm集群上,尝试使用Ceph作为Docker集群的共享存储,解决有状态服务在分布式环境下的数据存储问题。

经过尝试,成功使用CephFS实现了多个Docker节点之间的存储共享。但同时注意到,对于小规模Docker集群并且运维成本有限的场景,Ceph这样的分布式对象存储系统仍然显得有点重了,本文的4节点docker swarm集群上,在Ceph集群与Portainer-Agent(docker运维工具)部署之后,容器数量达到了30个。因此最终决定采用其他方案实现小规模docker集群的共享存储,比如NFS。

但Ceph作为一个高可用高性能的分布式对象/块/文件存储系统,在其他场景还是有使用价值的。

本文主要记述Ceph的搭建过程以及如何用cephfs实现docker swarm集群的共享存储。其他诸如原理机制,运维资料等请参考社区文档。

Ceph对外提供了三种存储接口,分别是对象存储 RGW(rados gateway)、块存储 RBD(rados block device) 和文件存储 CephFS。

RGW是RestAPI,docker的数据卷无法直接使用。从性能角度考虑,应该是RBD最快,但本文使用的Ceph版本与使用RBD做共享存储所需要的中间件RexRay貌似存在版本不兼容问题,最终没能成功。。。

最终本文使用了CephFS来实现docker swarm集群的共享存储。

二、版本与相关资料

本文搭建Ceph使用了目前社区文档推荐的安装工具cephadm,安装的版本为octopus。四个节点的操作系统均为CentOS7

  • 社区文档入口:https://docs.ceph.com/docs/master/
  • 中文社区文档入口:http://docs.ceph.org.cn/
注意,编写本文时,中文社区文档并非最新版本,比如社区文档中目前建议使用的安装工具cephadm在中文社区文档中就没有相关资料。

三、Ceph角色规划

NoiphostnameOSD盘Ceph角色
1172.17.13.1manager01.xxx.com/dev/sdbmon,osd,ceph-mds,mgr
2172.17.13.2manager02.xxx.com/dev/sdbmon,osd,ceph-mds
3172.17.13.3worker01.xxx.com/dev/sdbmon,osd,ceph-mds
4172.17.13.4worker02.xxx.com/dev/sdbmon,osd,ceph-mds,mgr,cephadm,NTP服务器

Ceph用于存储数据的服务OSD需要使用干净的磁盘,要求如下:

  1. 设备没有分区
  2. 设备不得具有任何LVM状态
  3. 设备没有挂载
  4. 设备不包含任何文件系统
  5. 设备不包含ceph bluestore osd
  6. 设备必须大于5G

也就是给节点增加一块新磁盘之后,只要linux能识别到即可,不要做分区/格式化/mount等操作。

四、各节点环境准备工作

4.1 防火墙与Selinux

提前将Ceph相关防火墙规则设置好。或直接关闭防火墙。

firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --permanent --zone=public --add-service=ceph
firewall-cmd --permanent --zone=public --add-service=ceph-mon
firewall-cmd --reload
firewall-cmd --zone=public --list-services

firewall-cmd --zone=public --add-port=3300/tcp --permanent
firewall-cmd --zone=public --add-port=3300/udp --permanent
firewall-cmd --zone=public --add-port=6789/tcp --permanent
firewall-cmd --zone=public --add-port=6800-7300/tcp --permanent
firewall-cmd --zone=public --add-port=8443/tcp --permanent
firewall-cmd --reload
firewall-cmd --zone=public --list-ports

Ceph集群在使用中最好禁用Selinux。在各个节点上以root用户vi /etc/selinux/config,修改:

#SELINUX=enforcing
SELINUX=disabled

然后执行setenforce 0

4.2 各节点添加epel-release资源包

从阿里云镜像获取/etc/yum.repos.d/epel.repo,如下:

wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo

4.3 各节点安装依赖库

填坑的过程中尝试过不同的搭建方案,这里有些依赖库可能不是必须的。。。

yum -y install python3 yum-utils
yum install yum-plugin-priorities -y
yum install gcc python-setuptools python-devel -y
easy_install pip

4.4 安装Docker CE

本文的环境已经提前安装了docker ce,docker compose,并创建了docker swarm集群。

每个节点都需要先安装好Docker,具体方法参考官方文档:https://docs.docker.com/engine/install/centos/
注意最好使用国内的dokcer安装yum镜像。

yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

安装完成之后,添加docker镜像仓库地址,vi /etc/docker/daemon.json,添加:

  "registry-mirrors": [
          "https://docker.mirrors.ustc.edu.cn",
          "https://dockerhub.azk8s.cn",
          "https://reg-mirror.qiniu.com",
          "https://hub-mirror.c.163.com",
          "https://mirror.ccs.tencentyun.com",
          "https://registry.docker-cn.com"
  ],

然后重启docker服务:

systemctl daemon-reload
systemctl restart docker

然后提前为每个节点拉取Ceph相关镜像:

docker pull ceph/ceph:v15
docker pull ceph/ceph-grafana
docker pull prom/prometheus:v2.18.1
docker pull prom/alertmanager:v0.20.0
docker pull prom/node-exporter:v0.18.1

4.5 安装NTP并同步各节点时间

各节点安装ntp服务

yum install ntp ntpdate ntp-doc -y

在管理节点worker02.xxx.com上安装ntp服务,vi /etc/ntp.conf:

...
restrict 127.0.0.1 
restrict ::1
restrict 172.17.13.0 mask 255.255.255.0

# Hosts on local network are less restricted.
#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap

# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst
server 127.127.1.0
...

配置了NTP服务的管理节点需要防火墙配置端口:

firewall-cmd --zone=public --add-port=123/udp --permanent
firewall-cmd --reload
firewall-cmd --zone=public --list-ports

在其他节点设置ntp服务器为刚刚设定的ntp服务器,vi /etc/ntp.conf:

…
# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
server worker02.xxx.com
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst
…

重启各个节点的ntp服务,并设置开机启动:

systemctl enable ntpd
systemctl restart ntpd
systemctl status ntpd

# Centos7默认安装了chronyd并且默认enable,这里需要关闭,否则重启以后会导致ntpd不能启动
systemctl disable chronyd

管理节点从外网同步时间,其他节点从管理节点同步时间:

# 管理节点
ntpdate -u ntp1.aliyun.com

# 其他节点
ntpdate -u worker02.xxx.com

4.6 更新SSH服务

各节点默认已安装,可以更新最新版本,并确认服务状态

yum install openssh-server -y
service sshd status

4.7 确保短主机名可以ping通

在每个节点以root身份vi /etc/hosts,添加:

172.17.13.1  manager01
172.17.13.2  manager02
172.17.13.3  worker01
172.17.13.4  worker02
Ceph默认使用短域名,社区文档在环境检查中要求能以短域名ping通,但本文环境都是用的FQDN,这里即使设置短域名到hosts文件,后续依然有些许小问题。

五、Ceph集群搭建

5.1 cephadm节点安装cephadm

以下操作均在cephadm节点上执行。(本文规划将worker02.xxx.com作为cephadm节点)

cd ~
mkdir cephadmin
cd cephadmin
curl --silent --remote-name --location https://hub.fastgit.org/ceph/ceph/raw/octopus/src/cephadm/cephadm
chmod +x cephadm
ll -h
官方地址https://github.com/ceph/ceph/raw/octopus/src/cephadm/cephadm总是下载失败,因此使用了国内某镜像地址。

5.2 初始化Ceph集群

以下操作均在cephadm节点上执行。

生成ceph的yum源文件并将其替换为使用阿里云yum源:

./cephadm add-repo --release octopus
cat /etc/yum.repos.d/ceph.repo

sed -i 's#download.ceph.com#mirrors.aliyun.com/ceph#' /etc/yum.repos.d/ceph.repo
cat /etc/yum.repos.d/ceph.repo
yum list | grep ceph
注意版本是octopus

安装cephadm:

./cephadm install
which cephadm

引导ceph集群:

mkdir -p /etc/ceph
cephadm bootstrap --mon-ip 172.17.13.4 --allow-fqdn-hostname

成功后会出现如下信息:

...
INFO:cephadm:Ceph Dashboard is now available at:

         URL: https://worker02.xxx.com:8443/
        User: admin
    Password: 3y44vf60ms

INFO:cephadm:You can access the Ceph CLI with:

    sudo /usr/sbin/cephadm shell --fsid fbd10774-c8cf-11ea-8bcc-00505683571d -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring

INFO:cephadm:Please consider enabling telemetry to help improve Ceph:

    ceph telemetry on

For more information see:

    https://docs.ceph.com/docs/master/mgr/telemetry/

INFO:cephadm:Bootstrap complete.

尝试登录Ceph CLI:

[root@worker02 ~]# cephadm shell --fsid fbd10774-c8cf-11ea-8bcc-00505683571d -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring
INFO:cephadm:Using recent ceph image ceph/ceph:v15
[ceph: root@worker02 /]# exit
exit

在浏览器中访问https://worker02.xxx.com:8443/,打开 ceph ui, 第一次登陆要求更改默认密码

域名访问前先配置浏览器所在节点的hosts文件

5.3 安装ceph-common并验证集群

以下操作均在cephadm节点上执行。

安装 ceph 工具包, 其中包括 ceph, rbd, mount.ceph 等命令:

cephadm install ceph-common

验证集群状态:

# 查看 ceph 集群所有组件运行状态
ceph orch ps

# 查看指定组件运行状态
ceph orch ps --daemon-type mon

# 查看集群当前状态
ceph status
ceph -s

5.4 向集群添加主机

将之前cephadm bootstrap初始化集群命令所生成的ceph密钥拷贝到其他节点root用户的"~/.ssh"目录。注意要输入其他节点root用户密码。

ssh-copy-id -f -i /etc/ceph/ceph.pub root@manager01.xxx.com
ssh-copy-id -f -i /etc/ceph/ceph.pub root@manager02.xxx.com
ssh-copy-id -f -i /etc/ceph/ceph.pub root@worker01.xxx.com

将其他节点加入集群

ceph orch host add manager01.xxx.com
ceph orch host add manager02.xxx.com
ceph orch host add worker01.xxx.com

ceph orch host ls

查看集群当前服务分布(此时应该有4个crash,4个mon,两个mgr)

ceph orch ps

5.5 部署OSD

检查每个节点是否有一块尚未分区的新磁盘,例如这里的sdb:

[root@worker01 ~]# lsblk 
NAME            MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
fd0               2:0    1     4K  0 disk 
sda               8:0    0   300G  0 disk 
├─sda1            8:1    0   500M  0 part /boot
└─sda2            8:2    0 299.5G  0 part 
  ├─centos-root 253:0    0 295.5G  0 lvm  /
  └─centos-swap 253:1    0     4G  0 lvm  [SWAP]
sdb               8:16   0   300G  0 disk 
sr0              11:0    1  1024M  0 rom 
对于虚拟机,在虚拟机控制台直接给运行中的虚拟机添加新磁盘之后,每个节点执行以下命令就可以刷出磁盘信息,不用重启:
echo "- - -" > /sys/class/scsi_host/host0/scan
echo "- - -" > /sys/class/scsi_host/host1/scan
echo "- - -" > /sys/class/scsi_host/host2/scan

将新磁盘设备加入集群

ceph orch daemon add osd manager01.xxx.com:/dev/sdb
ceph orch daemon add osd manager02.xxx.com:/dev/sdb
ceph orch daemon add osd worker01.xxx.com:/dev/sdb
ceph orch daemon add osd worker02.xxx.com:/dev/sdb

# 查看设备信息
ceph orch device ls
# 查看挂载好的osd信息
ceph osd df

5.6 其他节点安装ceph-common

为了在其他节点也能够直接访问Ceph集群,我们需要在其他节点上也安装ceph-common。

cephadm以外的其他节点执行:

mkdir ~/cephadmin
mkdir /etc/ceph

从cephadm节点以拷贝ceph.repo,cephadm,ceph集群配置文件,ceph客户端管理员密钥到其他节点:

scp /etc/yum.repos.d/ceph.repo root@manager01.xxx.com:/etc/yum.repos.d
scp /etc/yum.repos.d/ceph.repo root@manager02.xxx.com:/etc/yum.repos.d
scp /etc/yum.repos.d/ceph.repo root@worker01.xxx.com:/etc/yum.repos.d

scp ~/cephadmin/cephadm root@manager01.xxx.com:~/cephadmin/
scp ~/cephadmin/cephadm root@manager02.xxx.com:~/cephadmin/
scp ~/cephadmin/cephadm root@worker01.xxx.com:~/cephadmin/

scp /etc/ceph/ceph.conf root@manager01.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.conf root@manager02.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.conf root@worker01.xxx.com:/etc/ceph/

scp /etc/ceph/ceph.client.admin.keyring root@manager01.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.client.admin.keyring root@manager02.xxx.com:/etc/ceph/
scp /etc/ceph/ceph.client.admin.keyring root@worker01.xxx.com:/etc/ceph/

其他节点执行:

cd ~/cephadmin
./cephadm install ceph-common

ceph -s

5.7 健康检查的错误

在执行ceph -s或者ceph health时,可能会发现如下的错误:

Module 'cephadm' has failed: auth get failed: failed to find client.crash.worker02 in keyring retval: -2

推测是Ceph对长域名支持不足的原因。通过ceph auth ls命令可以查看ceph集群所有的用户的密钥,能查到对应的长域名client.crash.worker02.xxx.com用户但没有短域名用户。通过以下命令创建对应的短域名用户:

# 手动添加用户client.crash.xxx,任意节点执行
ceph auth add client.crash.worker02 mgr 'profile crash' mon 'profile crash'
ceph auth add client.crash.manager01 mgr 'profile crash' mon 'profile crash'
ceph auth add client.crash.manager02 mgr 'profile crash' mon 'profile crash'
ceph auth add client.crash.worker01 mgr 'profile crash' mon 'profile crash'

# 查看新增用户是否成功
ceph auth ls

# 重启ceph集群,各个节点上都需要执行
systemctl restart ceph.target

# 也可以用下面的命令停止再重启ceph集群,各个节点上都需要执行
systemctl stop ceph.target
systemctl stop ceph\*.service ceph\*.target
ps -ef | grep ceph
docker ps -a
systemctl restart ceph.target

# 重启后重新检查状态
ceph -s

六、部署cephfs服务

6.1 创建cephfs

任意节点上执行:

# 创建一个用于cephfs数据存储的池,相关参数自行参阅社区文档,一言难尽。。。
ceph osd pool create cephfs_data 64 64
# 创建一个用于cephfs元数据存储的池
ceph osd pool create cephfs_metadata 32 32
# 创建一个新的fs服务,名为cephfs
ceph fs new cephfs cephfs_metadata cephfs_data
# 查看集群当前的fs服务
ceph fs ls
# 设置cephfs最大mds服务数量
ceph fs set cephfs max_mds 4
# 部署4个mds服务
ceph orch apply mds cephfs --placement="4 manager01.xxx.com manager02.xxx.com worker01.xxx.com worker02.xxx.com"
# 查看mds服务是否部署成功
ceph orch ps --daemon-type mds
本文在这里遇到一个问题,mds服务一直不能启动,查看ceph health发现一个1 filesystem is online with fewer MDS than max_mds的警告,应该是ceph fs set cephfs max_mds 4没有生效。后来重启了整个集群就好了。
# 在所有节点上执行
systemctl restart ceph.target

# 重启之后检查相关服务
ceph orch ps --daemon-type mds
ceph osd lspools
ceph fs ls

6.2 创建cephfs访问用户

创建用户,用于客户端访问CephFs

ceph auth get-or-create client.cephfs mon 'allow r' mds 'allow r, allow rw path=/' osd 'allow rw pool=cephfs_data' -o ceph.client.cephfs.keyring

查看输出的ceph.client.cephfs.keyring密钥文件,或使用下面的命令查看密钥:

ceph auth get-key client.cephfs

6.3 挂载cephfs到各节点本地目录

在各个节点执行:

mkdir /mnt/cephfs/
mount -t ceph manager01.xxx.com:6789,manager02.xxx.com:6789,worker01.xxx.com:6789,worker02.xxx.com:6789:/ /mnt/cephfs/ -o name=cephfs,secret=<cephfs访问用户的密钥>
manager01.xxx.com:6789,manager02.xxx.com:6789,worker01.xxx.com:6789,worker02.xxx.com:6789 是所有mon服务

编辑各个节点的/etc/fstab文件,实现开机自动挂载,添加以下内容:

manager01.xxx.com:6789,manager02.xxx.com:6789,worker01.xxx.com:6789,worker02.xxx.com:6789:/     /mnt/cephfs    ceph    name=cephfs,secretfile=<cephfs访问用户的密钥>,noatime,_netdev    0       2
相关参数请自行查阅linux下fstab的配置资料。

6.4 使用cephfs实现docker共享存储

将挂载到本地的cephfs作为本地磁盘使用即可。例如本文中,可以在docker run命令中,或者docker compose编排文件中,使用volume本地挂载到/mnt/cephfs下。

示例,注意volumes:

...
  portainer:
    image: portainer/portainer
    ...
    volumes:
      - /mnt/cephfs/docker/portainer:/data
    ...
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]
...
至此,使用cephfs实现docker共享存储的尝试已经OK。

七、部署RBD

Ceph的RBD对外提供的是块存储,但块存储直接作为磁盘挂载到各节点的话,并不能实现不同节点之间的数据共享。因此需要配合中间件Rexray,以及docker插件rexray/rbd来实现共享存储。

但本文并未成功,原因后叙。

7.1 初始化rbd pool并创建RBD的image

在任意节点上执行:

ceph osd pool create rbd01 32
rbd pool init rbd01

# 在rbd01上创建一个300G的image
rbd create img01 --size 307200 --pool rbd01
rbd ls rbd01
rbd info --image img01 -p rbd01

此时可以在各个节点上挂载RBD的img01,但是并不能实现存储共享。

# 向linux系统内核载入rbd模块
modprobe rbd

# 将img01映射到系统内核
rbd map img01 --pool rbd01 --id admin
# 失败,需要禁用当前系统内核不支持的feature
rbd feature disable img01 --pool rbd01 exclusive-lock, object-map, fast-diff, deep-flatten
# 重新映射
rbd map img01 --pool rbd01 --id admin

# 映射成功后,会返回映射目录"/dev/rbd0",对该目录进行格式化:
[root@worker01 ~]# mkfs.xfs /dev/rbd0
meta-data=/dev/rbd0              isize=512    agcount=17, agsize=4914176 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=0, sparse=0
data     =                       bsize=4096   blocks=78643200, imaxpct=25
         =                       sunit=1024   swidth=1024 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal log           bsize=4096   blocks=38400, version=2
         =                       sectsz=512   sunit=8 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0

# mount到本地
mkdir /mnt/data01
mount /dev/rbd0 /mnt/data01

# 查看本地磁盘信息
df -hT

# 卸除挂载并删除image
umount /mnt/data01
rbd unmap img01 --pool rbd01 --id admin
rbd rm --image img01 -p rbd01 

7.2 安装rexray与rexray/rbd

任意某个节点下载rexray:

curl -sSL https://rexray.io/install | sh

编辑配置文件vi /etc/rexray/config.yml,内容如下:

rexray:
  logLevel:        debug
libstorage:
  logging:
    level:         debug
    httpRequests:  true
    httpResponses: true
libstorage:
  service: rbd
rbd:
  defaultPool: rbd01

尝试启动Rexray服务,这里一直失败:

[root@manager01 rexray]# rexray start
...
error: service startup failed: agent: mod init failed: error initializing instance ID cache
日志中除了最后的错误,并无其他有用的信息。在github对应项目的issue中有类似问题,解决方法是ceph配置文件/etc/ceph/ceph.conf中添加mon_host配置。但本文使用的ceph版本octopus的配置文件中已经明确配置了mon_host,且格式与issue所述不同。

最终也没有找到原因,怀疑是Ceph版本太新,与rexray不兼容所致。所以这里的尝试到此为止,后续安装docker插件与创建rbd数据卷并未进行。

如果Rexray服务能成功启动,那么后续还需要在每个节点安装docker插件rexray/rbd:

docker plugin install rexray/rbd RBD_DEFAULTPOOL=rbd01 LINUX_VOLUME_FILEMODE=0777

最后在docker swarm上创建rbd驱动的数据卷:

docker volume create -d <rexrayHost>:5011/rexray/rbd <数据卷名称>

如果成功,docker run或编排文件中的volume使用事先创建好的数据卷即可实现存储共享。

查看原文

赞 4 收藏 3 评论 0

libraco 赞了文章 · 2019-07-26

如何优化Python占用的内存

概述

如果程序处理的数据比较多、比较复杂,那么在程序运行的时候,会占用大量的内存,当内存占用到达一定的数值,程序就有可能被操作系统终止,特别是在限制程序所使用的内存大小的场景,更容易发生问题。下面我就给出几个优化Python占用内存的几个方法。

说明:以下代码运行在Python3。

举个栗子

我们举个简单的场景,使用Python存储一个三维坐标数据,x,y,z。

Dict

使用Python内置的数据结构Dict来实现上述例子的需求很简单。

>>> ob = {'x':1, 'y':2, 'z':3}
>>> x = ob['x']
>>> ob['y'] = y

查看以下ob这个对象占用的内存大小:

>>> print(sys.getsizeof(ob))
240

简单的三个整数,占用的内存还真不少,想象以下,如果有大量的这样的数据要存储,会占用更大的内存。

数据量占用内存大小
1 000 000240 Mb
10 000 0002.40 Gb
100 000 00024 Gb

Class

对于喜欢面向对象编程的程序员来说,更喜欢把数据包在一个class里。使用class使用同样需求:

class Point:
    #
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

>>> ob = Point(1,2,3)

class的数据结构和Dict区别就很大了,我们来看看这种情况下占用内存的情况:

字段占用内存
PyGC_Head24
PyObject_HEAD16
_weakref_8
_dict_8
TOTAL56

关于 __weakref__(弱引用)可以查看这个文档, 对象的__dict__中存储了一些self.xxx的一些东西。从Python 3.3开始,key使用了共享内存存储, 减少了RAM中实例跟踪的大小。

>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 
56 112
数据量占用内存
1 000 000168 Mb
10 000 0001.68 Gb
100 000 00016.8 Gb

可以看到内存占用量,class比dict少了一些,但这远远不够。

_slots_

从class的内存占用分布上,我们可以发现,通过消除__dict__和_weakref__,可以显着减少RAM中类实例的大小,我们可以通过使用__slots__来达到这个目的。

class Point:
    __slots__ = 'x', 'y', 'z'

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
64

可以看到内存占用显著的减少了

字段内存占用
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL64
数据量占用内存
1 000 00064Mb
10 000 000640Mb
100 000 0006.4Gb

默认情况下,Python的新式类和经典类的实例都有一个dict来存储实例的属性。这在一般情况下还不错,而且非常灵活,乃至在程序中可以随意设置新的属性。但是,对一些在”编译”前就知道有几个固定属性的小class来说,这个dict就有点浪费内存了。

当需要创建大量实例的时候,这个问题变得尤为突出。一种解决方法是在新式类中定义一个__slots__属性。

__slots__声明中包含若干实例变量,并为每个实例预留恰好足够的空间来保存每个变量;这样Python就不会再使用dict,从而节省空间。

那么用slot就是非非常那个有必要吗?使用__slots__也是有副作用的:

  1. 每个继承的子类都要重新定义一遍__slots__
  2. 实例只能包含哪些在__slots__定义的属性,这对写程序的灵活性有影响,比如你由于某个原因新网给instance设置一个新的属性,比如instance.a = 1, 但是由于a不在__slots__里面就直接报错了,你得不断地去修改__slots__或者用其他方法迂回的解决
  3. 实例不能有弱引用(weakref)目标,否则要记得把__weakref__放进__slots__

最后,namedlistattrs提供了自动创建带__slot__的类,感兴趣的可以试试看。

Tuple

Python还有一个内置类型元组,用于表示不可变数据结构。 元组是固定的结构或记录,但没有字段名称。 对于字段访问,使用字段索引。 在创建元组实例时,元组字段一次性与值对象关联:

>>> ob = (1,2,3)
>>> x = ob[0]
>>> ob[1] = y # ERROR

元组的示例很简洁:

>>> print(sys.getsizeof(ob))
72

可以看只比__slot__多8byte:

字段占用内存(bytes)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
TOTAL72

Namedtuple

通过namedtuple我们也可以实现通过key值来访问tuple里的元素:

Point = namedtuple('Point', ('x', 'y', 'z'))

它创建了一个元组的子类,其中定义了用于按名称访问字段的描述符。 对于我们的例子,它看起来像这样:

class Point(tuple):
     #
     @property
     def _get_x(self):
         return self[0]
     @property
     def _get_y(self):
         return self[1]
     @property
     def _get_y(self):
         return self[2]
     #
     def __new__(cls, x, y, z):
         return tuple.__new__(cls, (x, y, z))

此类的所有实例都具有与元组相同的内存占用。 大量实例会留下稍大的内存占用:

数据量内存占用
1 000 00072 Mb
10 000 000720 Mb
100 000 0007.2 Gb

Recordclass

python的第三方库recordclassd提供了一个数据结构recordclass.mutabletuple,它几乎和内置tuple数据结构一致,但是占用更少的内存。

 >>> Point = recordclass('Point', ('x', 'y', 'z'))
 >>> ob = Point(1, 2, 3)

实例化以后,只少了PyGC_Head:

字段占用内存
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL48

到此,我们可以看到,和__slot__比,又进一步缩小了内存占用:

数据量内存占用
1 000 00048 Mb
10 000 000480 Mb
100 000 0004.8 Gb

Dataobject

recordclass提供了另外一个解决方法:在内存中使用与__slots__类相同的存储结构,但不参与循环垃圾收集机制。通过recordclass.make_dataclass可以创建出这样的实例:

>>> Point = make_dataclass('Point', ('x', 'y', 'z'))

另外一个方法是继承自dataobject

class Point(dataobject):
    x:int
    y:int
    z:int

以这种方式创建的类将创建不参与循环垃圾收集机制的实例。 内存中实例的结构与__slots__的情况相同,但没有PyGC_Head:

字段内存占用(bytes)
PyObject_HEAD16
x8
y8
y8
TOTAL40
>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
40

要访问这些字段,还使用特殊描述符通过其从对象开头的偏移量来访问字段,这些对象位于类字典中:

mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>,
              .......................................
              'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>,
              'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>,
              'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>})
数据量内存占用
1 000 00040 Mb
10 000 000400 Mb
100 000 0004.0 Gb

Cython

有一种方法基于Cython的使用。 它的优点是字段可以采用C语言原子类型的值。例如:

cdef class Python:
    cdef public int x, y, z

 def __init__(self, x, y, z):
      self.x = x
      self.y = y
      self.z = z

这种情况下,占用的内存更小:

>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
32

内存结构分布如下:

字段内存占用(bytes)
PyObject_HEAD16
x4
y4
y4
пусто4
TOTAL32
数据量内存占用
1 000 00032 Mb
10 000 000320 Mb
100 000 0003.2 Gb

但是,从Python代码访问时,每次都会执行从int到Python对象的转换,反之亦然。

Numpy

在纯Python的环境中,使用Numpy能带来更好的效果,例如:

>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])

创建初始值是0的数组:

 >>> points = numpy.zeros(N, dtype=Point)
数据量内存占用
1 000 00012 Mb
10 000 000120 Mb
100 000 0001.2 Gb

最后

可以看出,在Python性能优化这方面,还是有很多事情可以做的。Python提供了方便的同时,也需要暂用较多的资源。在不通的场景下,我需要选择不同的处理方法,以便带来更好的性能体验.

更多有趣的文章,请点击我的博客

查看原文

赞 8 收藏 5 评论 0

libraco 赞了文章 · 2019-06-20

Go defer 会有性能损耗,尽量不要用?

image

原文地址:Go defer 会有性能损耗,尽量不要用?

上个月在 @polaris @轩脉刃 的全栈技术群里看到一个小伙伴问 “说 defer 在栈退出时执行,会有性能损耗,尽量不要用,这个怎么解?”

恰好前段时间写了一篇 《深入理解 Go defer》 去详细剖析 defer 关键字。那么这一次简单结合前文对这个问题进行探讨一波,希望对你有所帮助,但在此之前希望你花几分钟,自己思考一下答案,再继续往下看。

测试

func DoDefer(key, value string) {
    defer func(key, value string) {
        _ = key + value
    }(key, value)
}

func DoNotDefer(key, value string) {
    _ = key + value
}

基准测试:

func BenchmarkDoDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        DoDefer("煎鱼", "https://github.com/EDDYCJY/blog")
    }
}

func BenchmarkDoNotDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        DoNotDefer("煎鱼", "https://github.com/EDDYCJY/blog")
    }
}

输出结果:

$ go test -bench=. -benchmem -run=none
goos: darwin
goarch: amd64
pkg: github.com/EDDYCJY/awesomeDefer
BenchmarkDoDefer-4          20000000            91.4 ns/op          48 B/op           1 allocs/op
BenchmarkDoNotDefer-4       30000000            41.6 ns/op          48 B/op           1 allocs/op
PASS
ok      github.com/EDDYCJY/awesomeDefer    3.234s

从结果上来,使用 defer 后的函数开销确实比没使用高了不少,这损耗用到哪里去了呢?

想一下

$ go tool compile -S main.go 
"".main STEXT size=163 args=0x0 locals=0x40
    ...
    0x0059 00089 (main.go:6)    MOVQ    AX, 16(SP)
    0x005e 00094 (main.go:6)    MOVQ    $1, 24(SP)
    0x0067 00103 (main.go:6)    MOVQ    $1, 32(SP)
    0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)
    0x0075 00117 (main.go:6)    TESTL    AX, AX
    0x0077 00119 (main.go:6)    JNE    137
    0x0079 00121 (main.go:7)    XCHGL    AX, AX
    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)
    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP
    0x0084 00132 (main.go:7)    ADDQ    $64, SP
    0x0088 00136 (main.go:7)    RET
    0x0089 00137 (main.go:6)    XCHGL    AX, AX
    0x008a 00138 (main.go:6)    CALL    runtime.deferreturn(SB)
    0x008f 00143 (main.go:6)    MOVQ    56(SP), BP
    0x0094 00148 (main.go:6)    ADDQ    $64, SP
    0x0098 00152 (main.go:6)    RET
    ...

我们在前文提到 defer 关键字其实涉及了一系列的连锁调用,内部 runtime 函数的调用就至少多了三步,分别是 runtime.deferproc 一次和 runtime.deferreturn 两次。

而这还只是在运行时的显式动作,另外编译器做的事也不少,例如:

  • deferproc 阶段(注册延迟调用),还得获取/传入目标函数地址、函数参数等等。
  • deferreturn 阶段,需要在函数调用结尾处插入该方法的调用,同时若有被 defer 的函数,还需要使用 runtime·jmpdefer 进行跳转以便于后续调用。

这一些动作途中还要涉及最小单元 _defer 的获取/生成, deferrecover 链表的逻辑处理和消耗等动作。

Q&A

最后讨论的时候有提到 “问题指的是本来就是用来执行 close() 一些操作的,然后说尽量不能用,例子就把 defer db.close() 前面的 defer 删去了” 这个疑问。

这是一个比较类似 “教科书” 式的说法,在一些入门教程中会潜移默化的告诉你在资源控制后加个 defer 延迟关闭一下。例如:

resp, err := http.Get(...)
if err != nil {
    return err
}
defer resp.Body.Close()

但是一定得这么写吗?其实并不,很多人给出的理由都是 “怕你忘记” 这种说辞,这没有毛病。但需要认清场景,假设我的应用场景如下:

resp, err := http.Get(...)
if err != nil {
    return err
}
defer resp.Body.Close()
// do something
time.Sleep(time.Second * 60)

嗯,一个请求当然没问题,流量、并发一下子大了呢,那可能就是个灾难了。你想想为什么?从常见的 defer + close 的使用组合来讲,用之前建议先看清楚应用场景,在保证无异常的情况下确保尽早关闭才是首选。如果只是小范围调用很快就返回的话,偷个懒直接一套组合拳出去也未尝不可。

结论

一个 defer 关键字实际上包含了不少的动作和处理,和你单纯调用一个函数一条指令是没法比的。而与对照物相比,它确确实实是有性能损耗,目前延迟调用的全部开销大约在 50ns,但 defer 所提供的作用远远大于此,你从全局来看,它的损耗非常小,并且官方还不断地在优化中。

因此,对于 “Go defer 会有性能损耗,尽量不能用?” 这个问题,我认为该用就用,应该及时关闭就不要延迟,在 hot paths 用时一定要想清楚场景

补充

补充上柴大的回复:“不是性能问题,defer 最大的功能是 Panic 后依然有效。如果没有 defer,Panic 后就会导致 unlock 丢失,从而导致死锁了”,非常经典。

查看原文

赞 37 收藏 16 评论 1

libraco 赞了文章 · 2019-06-20

python中使用ctypes调用so传参设置

问题

近日在做一组声纹聚类时,使用了另一团队同学开发的声纹距离算法。该算法对外提供的是一组so包,需要使用方自己去使用。在python中调用纯so包一般使用ctypes类库,用起来看起来简单但也有不少细节容易犯错。本次使用过程中,就遇到传参的问题。

目标so库中对外export的函数是大致如下的三个函数:

    void* create_handler();
    int extract_feature(void* hander);
    bool destroy(void* handler); 

这三个函数使用起来倒也简单,顺序使用就可以了。但发现写成如下形式的python代码后,执行会直接segment fault。

    import sys
    import ctypes
    
    so = ctypes.CDLL("./lib/libbase.so")
    p = so.create_handler()
    feature = so.extract_feature(p)
    so.destroy(p)

解决

这段代码中p是int类型,由void*自动转来,在ctyeps中这种转型本身是没问题的。segment fault发生在extract_feature函数调用中,问题应当出在参数上,回传的handler已经不是原来的pointer了,导致访问指针出错。

查阅ctypes的文档后,发现ctypes可以声明so库中函数的参数,返回类型。试了试,显式声明后问题得到了解决,证明我们的猜想是对的,确实指针发生了变化。修改后代码如下:

    import sys
    import ctypes
    
    so = ctypes.CDLL("./lib/libbase.so")
    so.create_handler.restype=ctypes.c_void_p
    so.extract_feature.argtypes=[ctypes.c_void_p]
    so.destroy.argtypes=[ctypes.c_void_p]
    
    p = so.create_handler()
    feature = so.extract_feature(p)
    so.destroy(p)

结论:

ctypes中传递指针类型参数需要显式声明c函数的参数,返回类型。
查看原文

赞 4 收藏 2 评论 0

libraco 赞了文章 · 2019-05-17

对比学习:Golang VS Python3

Golang和Python都是目前在各自领域最流行的开发语言之一。

Golang其高效而又友好的语法,赢得了很多后端开发人员的青睐,最适用于高并发网络编程的语言之一。

Python不用说,TIOBE排行榜的前十常驻居民,现在已经稳定在前五了。在机器学习、AI、数据分析领域成为必学语言。

两门编程语言在语法上都有各自的特点,而且都易学易用

本文对比这两门语言目的不是争谁优谁略,只是为了对比学习,适合掌握Python想学Go或者掌握Go想学Python的同学们参考。

Go和Python,一个是静态语言一个是动态语言,从各个方面来看,都有根本性的差异,所以,文中很多内容不进行深入的比较了,我们只从程序员最直观的语法面做对比。

为了便于阅读,文中涉及代码都采用尽量简单的语句呈现

字符编码

Python

Python中默认的编码格式是 ASCII 格式,程序文件中如果包含中文字符(包括注释部分)需要在文件开头加上 # -*- coding: UTF-8 -*- 或者 #coding=utf-8 就行了

Golang

原生支持Unicode

保留字(关键字)

Python

30个关键字

and    exec    not
assert    finally    or
break    for    pass
class    from    print
continue global    raise
def    if    return
del    import    try
elif    in    while
else    is    with
except    lambda    yield

Golang

25个关键字

break    default    func    interface    select
case    defer    go    map    struct
chan    else    goto    package    switch
const    fallthrough    if    range    type
continue    for    import    return    var

注释

Python

# 单行注释

'''
多行注释
多行注释
'''

"""
多行注释
多行注释
"""

Golang

//单行注释

/*
多行注释
多行注释
*/

变量赋值

Python

Python是动态语言,所以在定义变量的时候不需要申明类型,直接使用即可。
Python会根据值判断类型。

name = "Zeta" # 字符串变量
age = 38 # 整数
income = 1.23 # 浮点数

多变量赋值

a,b = 1,2 # a=1; b=2
c = d = 3 # c=3; d=3

Golang

Go是静态语言,是强类型的,但是Go语言也允许在赋值变量时确定类型。

因此Go有多种申明变量的方式

// 1. 完整的申明并赋值
var a int
a = 1

// 2. 声明变量类型同时赋值
var a int = 1

// 3. 不声明类型,赋值时确定
var a = 1

// 4. 不用 var 关键字申明变量并赋值后确定类型
a := 1

注意,Go中的new关键字并不是声明变量,而是返回该类型的指针

a := new(int) //这时候a是一个*int指针变量

标准数据类型

Python 的标准数据类型有:

  • Boolean(布尔值)
  • Number(数字)
  • String(字符串)
  • List(列表)
  • Tuple(元组)
  • Set(集合)
  • Dictionary(字典)

Golang

  • boolean(布尔值)
  • numeric(数字)
  • string(字符串)
  • 数组(数组)
  • slice(切片:不定长数组)
  • map(字典)
  • struct(结构体)
  • pointer(指针)
  • function(函数)
  • interface(接口)
  • channel(通道)

总结

Python中的List列表对应Go语言中的Slice切片

Python中的Dictionary字典对应Go语言中的map

有一些值得注意的地方:

  • Go是支持函数编程的语言,所以在Go语言中函数是一个类型
  • Go语言不是面向对象的语言,没有定义类的关键字Class,要实现OOP风格编程,是通过struct、interface类型实现的
  • Python中的元组和集合在Go中都没有
  • channel是Go里独有的类型,多线程之间的通信就靠它

数据类型转换

Python

Python类型转换非常简单,用类型名作为函数名即可。

int(n)            # 将数字n转换为一个整数
float(n)          # 将数字n转换到一个浮点数
str(o)            # 将对象 obj 转换为字符串
tuple(s)          # 将序列 s 转换为一个元组
list(s)           # 将序列 s 转换为一个列表
set(s)            # 将序列 s 转换为一个集合

Golang

Go语言的基础类型转换和Python差不多,也是用类型名作为函数名

i := 1024
f := float32(i)
i = float32(f)

另外,Python中可以直接转换数字字符串和数字:

s = "123"
i = 456
print(int(s), str(i))

但是Go是不可以的。

Go语言的字符串处理很不同,string()只能用于[]byte类型转换成字符串,其他基础类型的转换需要用strconv包,另外,其他类型转换成为string类型除了用strconv包,还可以用fmt.Sprintf函数:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    s := "123"
    i, _ := strconv.Atoi(s)
    println(i)

    s2 := fmt.Sprintf("%d", 456)
    println(s2)
}

Go中的interface类型是不能直接转换成其他类型的,需要使用到断言

package main

func main() {
var itf interface{} = 1
i, ok := itf.(string)
println("值:", i, "; 断言结果", ok)

j, ok := itf.(int)
println("值:", j, "; 断言结果", ok)
}

输出为:

值:  ; 断言结果 false
值: 1 ; 断言结果 true

条件语句

Python

Python传统的判断语句如下

if name == 'zeta':          # 判断变量是否为 zeta 
    print('Welcome boss')   # 并输出欢迎信息
else:
    print('Hi, ' + name)  

Python不支持三元表达式,但是可以用一种类似的替代办法

title = "boss"
name = "zeta" if title == "boss" else "chow"
print(name)

逻辑与用 and ,逻辑或用 or

Golang

Go的if的语法类似Java,但是表达式不需要使用()

if a > b{
    println("a > b")
} else {
    println("a <= b")
}

Go同样没有三元表达式,并且也没有什么替代方法。

另外,Go允许在if的表达式里定义变量,定义并赋值的表达式与判断的表达式用;隔开,常见的情况是获取函数返回error,然后判断error是否为空:

if err := foo(); err != nil {
    println("发生一些错误")
} 

与Python不同,逻辑与用 &&, 逻辑或用||

循环语句

Python

Python中有whilefor两种循环,都可以使用break跳出循环和continue立即进入下一轮循环,另外,Python的循环语句还可以用else执行循环全部完毕后的代码,break跳出后不会执行else的代码

while 条件循环,

count = 0
while (count < 9):
    print('The count is:', count)
    count = count + 1
    if count == 5:
        break   # 可以比较以下break和不break的区别
        pass
else:
    print('loop over')

for 遍历循环,循环遍历所有序列对象的子项

names = ['zeta', 'chow',  'world']
for n in names:
    print('Hello, ' + n)
    if n == 'world':
        break
        pass
else:
    print('Good night!')

for循环中也可以用else,(注释掉代码中的break试试看。)

Golang

Go语言只有一个循环语句for,但是根据不同的表达式,for有不同的表现

for 前置表达式; 条件表达式; 后置表达式 {
    //...
}

前置表达式 在每轮循环前运行,可以用于声明变量或调用函数返回;
条件表达式 满足该表达式则执行下一轮循环,否则退出循环;
后置表达式 在循环完成后执行

经典的用法:

for i := 0; i < 10; i++ {
    println(i)
}

我们可以忽略掉前置和后置表达式

sum := 1
for sum < 10 {
    sum += sum
}

设置可以忽略掉全部表达式,也就是无限循环

for {
    print(".")
}

Go的for循环同样可以使用 break退出循环和continue立即进行下一轮循环。

for除了配合表达式循环,同样也可以用于遍历循环,需要用到range关键字

names := []string{"zeta", "chow", "world"}
for i, n := range names {
    println(i,"Hello, " + n)
}

函数

Python

def关键字定义函数,并且在Python中,作为脚本语言,调用函数必须在定义函数之后。

def foo(name):
    print("hello, "+name)
    pass

foo("zeta")

默认参数 Python定义函数参数时,可以设置默认值,调用时如果没有传递该参数,函数内将使用默认值,默认值参数必须放在无默认值参数后面。

def foo(name="zeta"):
    print("hello, "+name)
    pass

foo()

关键字参数 一般函数传递参数时,必须按照参数定于的顺序传递,但是Python中,允许使用关键字参数,这样通过指定参数明,可以不按照函数定义参数的顺序传递参数。

def foo(age, name="zeta"):
    print("hello, "+name+"; age="+str(age))
    pass

foo(name="chow", age=18)

不定长参数,Python支持不定长参数,用*定义参数名,调用时多个参数将作为一个元祖传递到函数内

def foo(*names):
    for n in names:
        print("hello, "+n)
    pass

foo("zeta", "chow", "world")

return 返回函数结果。

Golang

Go用func定义函数,没有默认值参数、没有关键字参数,但是有很多其他特征。

func main() {
    println(foo(18, "zeta"))
}

func foo(age int, name string) (r string) {
    r = fmt.Sprintf("myname is %s , age %d", name, age)
    return 
}

函数的定义和调用没有顺序的限制。

Go的函数不仅可以定义函数返回值类型,还可以申明返回值变量,当定义了返回值变量时,函数内的return语句可以不需要带返回值,函数会默认使用返回值变量返回。

可变参数

使用…类型定义可变参数,函数内获得的参数实际是该类型slice对象

func main() {
    println(foo(18, “zeta”, “chow”, “world”))
}

func foo(age int, names …string) (r string) {
    for _, n := range names {
        r += fmt.Sprintf(“myname is %s , age %d \n”, n, age)
    }

    return
}

defer句

defer语句后面指定一个函数,该函数会延迟到本函数return后再执行。

defer语句在Go语言中非常有用,详细可以查阅本专栏的另一篇文章《Golang研学:如何掌握并用好defer(延迟执行)

func foo() {
    defer fmt.Println("defer run")
    fmt.Println("Hello world")
    return
}

运行结果:

Hello world
defer run

另外,在Go语言中函数也是类型,可以作为参数传递给别的函数

func main() {
    n := foo(func(i int, j int) int {
        return i + j
    })
    println(n)
}

func foo(af func(int, int) int) int {
    return af(1, 2)
}

上面这个例子直接在参数定义时使用函数类型,看上去有点混乱

再看来看一个清晰并完整的例子,说明全在注释里。

package main

type math func(int, int) int //定义一个函数类型,两个int参数,一个int返回值

//定义一个函数add,这个函数两个int参数一个int返回值,与math类型相符
func add(i int, j int) int {
    return i + j
}

//再定义一个multiply,这个函数同样符合math类型
func multiply(i, j int) int {
    return i * j
}

//foo函数,需要一个math类型的参数,用math类型的函数计算第2和第3个参数数字,并返回计算结果
//稍后在main中我们将add函数和multiply分别作为参数传递给它
func foo(m math, n1, n2 int) int {
    return m(1, 2)
}

func main() {
    //传递add函数和两个数字,计算相加结果
    n := foo(add, 1, 2)
    println(n)

    //传递multply和两个数字,计算相乘结果
    n = foo(multiply, 1, 2)
    println(n)
}

结果

3
2

模块

Python

  • 模块是一个.py文件
  • 模块在第一次被导入时执行
  • 一个下划线定义保护级变量和函数,两个下划线定义私有变量和函数
  • 导入模块习惯性在脚本顶部,但是不强制

Golang

  • 与文件和文件名无关,每一个文件第一行用package定义包名,相同包名为一个包
  • 包中的变量第一次引用时初始化,如果包中包含init函数,也会在第一次引用时执行(变量初始化后)
  • 保重首写字母大写的函数和变量为共有,小写字母为私有,Golang不是面向对象的,所以不存在保护级。
  • 导入模块必须写在package之后,其他代码之前。

导入包

Python

在Python中,使用import导入模块。

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 导入模块
import support
 
support.print_func(“Runoob”)

还可以使用from import导入模块指定部分

from modname import name1[, name2[, ... nameN]]

为导入的包设置别名用 as关键字

import datetime as dt

Golang

也是使用import导入包,导入包指定的是包的路径,包名默认为路径中的最后部分

import "net/url" //导入url包

多个包可以用()组合导入

import (
    "fmt"
    "net/url"
)

为导入的包设置别名, 直接在导入包时,直接在报名前面添加别名,用空格隔开

import (
    f "fmt"
  u "net/url"
)

错误和异常

Python

Python中用经典的 try/except 捕获异常

try:
<语句>        #运行别的代码
except <异常名称>:
<语句>        #
except <异常名称>,<数据>:
<语句>        #如果引发了指定名称的异常,获得附加的数据

还提供了 elsefinally

如果没发生异常的执行else语句块,finally块的代码无论是否捕获异常都会执行

Python内建了很全面的异常类型名称,同时能自定义异常类型

Golang

Golang里没有用经典的 try/except捕获异常。

Golang提供两种错误处理方式

  1. 函数返回error类型对象判断错误
  2. panic异常

一般情况下在Go里只使用error类型判断错误,Go官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断error是否存在。

error是一个内置的接口类型

type error interface {
    Error() string
}

通常,使用error异常处理类似这样:

package main

import "fmt"

func foo(i int, j int) (r int, err error) {
    if j == 0 {
        err = fmt.Errorf("参数2不能为 %d", j) //给err变量赋值一个error对象
        return //返回r和err,因为定义了返回值变量名,所以不需要在这里写返回变量
    }

    return i / j, err //如果没有赋值error给err变量,err是nil
}

func main() {
    //传递add函数和两个数字,计算相加结果
    n, err := foo(100, 0)
    if err != nil { //判断返回的err变量是否为nil,如果不是,说明函数调用出错,打印错误内容
        println(err.Error())
    } else {
        println(n)
    }
}

panic可以手工调用,但是Golang官方建议尽量不要使用panic,每一个异常都应该用error对象捕获。

Go语言在一些情况下会触发内建的panic,例如 0 除、数组越界等,修改一下上面的例子,我们让函数引起0除panic

package main

func foo(i int, j int) (r int) {
    return i / j
}

func main() {
    //传递add函数和两个数字,计算相加结果
    n := foo(100, 0)
    println(n)
}

运行后会出现

panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.foo(...)
        /lang.go:4
main.main()
        /lang.go:9 +0x12
exit status 2

手工panic可以这样:

func foo(i int, j int) (r int) {
    if j == 0 {
        panic("panic说明: j为0")
    }
    return i / j
}

运行后,可以看到,错误消息的第一句:

panic: panic说明: j为0

面向对象

Python

Python完全支持面向对象的。

Golang

尽管Go语言允许面向对象的风格编程,但是本身并不是面向对象的

官方FAQ原文

Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

多线程

Python

  1. 使用thread模块中的start_new_thread()函数
  2. 使用threading模块创建线程

Golang

用关键 go创建协程goroutine

go关键字后指定函数,将会开启一个协程运行该函数。

package main

import (
    "fmt"
    "time"
)

func foo() {
    for i := 0; i < 5; i++ {
        fmt.Println("loop in foo:", i)
        time.Sleep(1 * time.Second)
    }
}

func main() {
    go foo()

    for i := 0; i < 5; i++ {
        fmt.Println("loop in main:", i)
        time.Sleep(1 * time.Second)
    }
    time.Sleep(6 * time.Second)
}

Go语言中,协程之间的通信是通过channel实现的:

package main

import (
    "fmt"
    "time"
)

//接受一个chan类型的参数c
func foo(c chan int) { 
    time.Sleep(1 * time.Second) //等待1秒
    c <- 1024                   //向c中写入数字
}

func main() {
    c := make(chan int) //创建chan变量c
    go foo(c)           //在子写成中运行函数foo,并传递变量c
    fmt.Println("wait chan 'c' for 1 second")
    fmt.Println(<-c) //取出chan 'c'的值(取值时,如果c中无值,主县城会阻塞等待)
}

总结

Python和Go分别在动态语言和静态语言中都是最易学易用的编程语言之一。

它们并不存在取代关系,而是各自在其领域发挥自己的作用。

Python的语法简单直观,除了程序员爱不释手外也非常适合于其他领域从业者使用。

Go兼具语法简单和运行高效的有点,在多线程处理方面很优秀,非常适合已经掌握一定编程基础和一门主流语言的同学学习,不过,Go是不支持面向对象的,对于大多数支持面向对象语言的使用者在学习Go语言的时候,需要谨记并且转换编程思路。

后记

文中还有许多应该涉及的知识却没有能够详细说明,它是不完整的,甚至难免会有一些失误。

如果,您觉得本文对您有所帮助,希望能够得到您的点赞支持,如果文中有错误的地方,也希望不吝赐教,通过评论或者公众号和大家一起交流学习。

晓代码

查看原文

赞 28 收藏 17 评论 1

libraco 提出了问题 · 2019-04-25

C语言里整型比较的一个诡异之处

以下代码定义了一个整型int_min,值为0x80000000。在计算机补码表示法下,0x80000000是int可表示的最小的数。
然后取int_min的负数,根据补码运算规则,-int_min的位模式和int_min相同,也等于0x80000000。
神奇的事情来了,-int_min = int_min < 0 竟然不成立。

#include <limits.h>
#include <assert.h>

void test_int_min() {
    int int_min = 0x80000000;
    assert(int_min == INT_MIN);     // True
    assert(int_min < 0);            // True
    assert(int_min == -int_min);    // True
    assert(-int_min < 0);           // False
}

在so上面找到了一个类似问题 Why is 0 < -0x80000000?,猜测-int_min和0比较的时候自动转换成了unsigned类型,于是对-int_min进行强制类型转换,没想到依然是False。

assert((int)-int_min < 0);      // False

再考虑到可能是编译器的问题,但是不论windows+mingw64+gcc8.1还是Kali+gcc8.2编译运行结果都是一样,用gbd单步调试得到的结果也是-int_min < 0

clipboard.png

编译是用的最简单的命令gcc test_int_min.c -o test_int_min,没有额外编译参数。

补充1:完成操作流程。

PS E:\Repository\csapp\tests> Get-WmiObject -Class Win32_OperatingSystem | Select-Object -ExpandProperty Caption
Microsoft Windows 10 教育版
PS E:\Repository\csapp\tests> gcc --version
gcc.exe (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

PS E:\Repository\csapp\tests> cat t.c
#include <limits.h>
#include <assert.h>

void test_int_min() {
    int int_min = 0x80000000;
    assert(int_min == INT_MIN);     // True
    assert(int_min < 0);            // True
    assert(int_min == -int_min);    // True
    assert(-int_min < 0);           // False
}


int main(int argc, char const *argv[])
{
    test_int_min();
    return 0;
}
PS E:\Repository\csapp\tests> gcc t.c -o t.exe
PS E:\Repository\csapp\tests> .\t.exe
Assertion failed!

Program: E:\Repository\csapp\tests\t.exe
File: t.c, Line 9

Expression: -int_min < 0
PS E:\Repository\csapp\tests> bash
kali@A003657-PC01:/mnt/e/Repository/csapp/tests$ cat /etc/issue
Kali GNU/Linux Rolling \n \l

kali@A003657-PC01:/mnt/e/Repository/csapp/tests$ gcc --version
gcc (Debian 8.2.0-13) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

kali@A003657-PC01:/mnt/e/Repository/csapp/tests$ gcc t.c -o t
kali@A003657-PC01:/mnt/e/Repository/csapp/tests$ ./t
t: t.c:9: test_int_min: Assertion `-int_min < 0' failed.
Aborted (core dumped)
kali@A003657-PC01:/mnt/e/Repository/csapp/tests$

补充2:在centos7+gcc4.8环境下能得到预期结果。

clipboard.png

有人能解释一下吗?

关注 2 回答 1

libraco 提出了问题 · 2019-04-24

CSAPP一道移位运算题

**2.81 编写C表达式产生如下位模式,其中a(k)表示符号a重复k次。假设一个w位的数据类型。
代码可以包含对参数j和k的引用,它们分别表示j和k的值,**但是不能使用表示w的参数**。

A. 1(w-k)0(k)
B. 0(w-k-j)1(k)0(j)

这是CSAPP上面的一道课后习题,这里只考虑A题支,我看了很多答案,他们是这样写的:

int A(int k)
{
    return -1 << k; 
}

//or

int A(int k)
{
    return ~((1<<k) - 1);
}

这些解答在0 <= k < w的时候是正确的,但是当k = w时,参考下面移位运算的说明,有A(w) = -1

clipboard.png

1(w-k)0(k) = 0(w) = 0,二者并不相等。在不使用w变量的条件下真的可以得到正确的结果吗?

补充题目要求:

clipboard.png

关注 2 回答 1

libraco 赞了回答 · 2019-02-14

解决Git pull如何忽略指定文件

你可以在服务器上修改你的config,这样你在git pull 的时候就服务器上的配置文件就不会被覆盖了。
(ps :建议使用rsync实现代码上线部署。)

关注 3 回答 1

libraco 提出了问题 · 2019-01-26

Vue + element-ui 打包生成的js文件这么大正常吗?

package.json 文件内容:

{
  "name": "wingspage",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "axios": "^0.18.0",
    "element-ui": "^2.4.11",
    "lodash": "^4.17.11",
    "vue": "^2.5.17",
    "vue-router": "^3.0.1"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.2.0",
    "@vue/cli-plugin-eslint": "^3.2.0",
    "@vue/cli-service": "^3.2.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^5.8.0",
    "eslint-plugin-vue": "^5.0.0-0",
    "vue-template-compiler": "^2.5.17"
  }
}

npm run build打包输出:

> wingspage@0.1.0 build D:\GOPATH\src\WingsGo\page
> vue-cli-service build


-  Building for production...

 WARNING  Compiled with 2 warnings                                                                                                                         14:55:14
 warning

asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  js/chunk-vendors.40fc56d8.js (746 KiB)

 warning

entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  app (959 KiB)
      css/chunk-vendors.9c8e3772.css
      js/chunk-vendors.40fc56d8.js
      css/app.d188071d.css
      js/app.314fa8ec.js


  File                                    Size              Gzipped

  dist\js\chunk-vendors.40fc56d8.js       745.90 kb         191.94 kb
  dist\js\app.314fa8ec.js                 18.14 kb          6.09 kb
  dist\js\chunk-3853daff.7c85ec3e.js      0.68 kb           0.44 kb
  dist\js\chunk-26ad2470.57300bb8.js      0.68 kb           0.44 kb
  dist\css\chunk-vendors.9c8e3772.css     194.50 kb         29.45 kb
  dist\css\app.d188071d.css               0.04 kb           0.06 kb
  dist\css\chunk-26ad2470.739f8c92.css    0.00 kb           0.02 kb
  dist\css\chunk-3853daff.739f8c92.css    0.00 kb           0.02 kb

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html

页面功能逻辑很简单,引用组件也不多,由于js太大,打开页面比较慢(相比jquery的同等实现),前端不太熟,是不是我哪里配置不对?

关注 3 回答 4

认证与成就

  • 获得 31 次点赞
  • 获得 186 枚徽章 获得 19 枚金徽章, 获得 76 枚银徽章, 获得 91 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • weiboRobot

    一个自动保存微博并展示的小工具

注册于 2013-03-23
个人主页被 1.2k 人浏览