存档

作者存档

TensorFlow Note (1) – 环境搭建

2017年3月17日 没有评论

由于 tensorflow 采用 python 开发,是一个 python 的 library, 因此借助于
pip, 在各个平台上安装 tensorflow 都非常方便, 不过要想取得更高的性能,
需要安装 gpu 的版本(包括安装 CUDA 相关包等).

安装 CUDA

CUDA 是 NVIDIA 提供的并行计算平台和编程模型, 应用程序可以利用 CUDA, 进行
高性能的并行计算.

CUDNN (DNN means Deep Neural Network) 是 Nvidia 基于 GPU 开发的一套深度学习
库.

目前, 官方提供了 Linux, MacOS, Windows 平台的各种格式的 CUDA 安装包, 这里的
安装以 ubuntu 16.04为例.

验证支持

https://developer.nvidia.com/cuda-gpus 下查看显卡是否支持CUDA.

安装 CUDA

CUDA 的下载页面 下载相应的格式(这里为 deb 格式), 然后安装:

$ sudo dpkg -i cuda-repo-ubuntu1604-8-0-local_8.0.44-1_amd64.deb 
$ sudo apt-get update
$ sudo apt-get install cuda

安装 CUDNN

CUDNN 的下载页面 注册并下载(一般下载最新版本的):

$ tar xvzf cudnn-8.0-linux-x64-v5.1.tgz 
$ sudo cp -P cuda/include/cudnn.h /usr/local/cuda/include/
$ sudo cp -P cuda/lib64/libcudnn* /usr/local/cuda/lib64
$ sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*

安装 TensorFlow

使用 python 的虚拟环境来安装 tensorflow:

$ virtualenv --system-site-packages ~/Develop/ML/tensorflow
$ source ~/Develop/ML/tensorflow/bin/activate
$ pip install tensorflow-gpu

验证:

>>> import tensorflow as tf
>>> hello = tf.constant('Hello, TensorFlow!'); sess = tf.Session(); print(sess.run(hello))
Hello, TensorFlow!
>>> a = tf.constant(10); b = tf.constant(32); print(sess.run(a + b))
42

OpenStack Magnum 初体验

2017年1月9日 没有评论

OpenStack 的容器历史

自容器技术变为目前云计算最火热和最前沿的技术以来,OpenStack 作为一个云计算管理平台(亦或是云计算生态系统), 怎么将容器管理纳入到其内,成为了近两年 OpenStack 发展的一个热门方向。

OpenStack 的容器整合之路先后经历了以下几种历程:

os_container.png

目前 MagnumMurano 是 OpenStack 中血统最正的容器项目。

可以通过 gitstatus 的分析结果看一下 magnum 的开发活跃度

os_magnum_commits_by_year.png

os_magnum_month_of_year.png

由上面的分析可以看到,magnum 目前开发非常活跃。

Magnum 介绍

Magnum 利用 Keystone, Nova, Heat, Neutron 等已有的 OpenStack组件,整合容器的 集群管理系统如 kubernetes, Mesos 等, 为用户更加简单灵活,多租户的容器服务.

在 Magnum 中的设计中,容器和虚拟机一样为一等公民, 有自己独立的逻辑实现,而不是像 nova docker driver 那样仅仅是 nova 的后端实现。

目前 Magnum 主要为用户提供以下功能:

  • 容器管理集群的部署: 支持 Kubernetes, Docker Swarm, Mesos 这三种容器管理集群类型的部署
  • 容器管理集群的管理: 对容器管理集群进行弹性扩容, 修改配置,提供访问容器管理集群的认证信息

Magnum 的主要概念:

  • Bay: 表示一个集群,比如一个 Mesos 集群,新版本中已经改为 cluster
  • BayModel: 集群的 flavor, 类似于虚拟机的 flavor, BayModel 定义 Docker 集群的一些规格, 比如节点使用的 k8s 或者 mesos 镜像, 网络参数等. 新版本中已经改为 ClusterTemplate
  • Cluser: 同 bay
  • ClusterTemplate: 同 BayModel
  • Node: 指 bay 中的某个节点,即 nova 中的虚拟机, 容器在该 node 里面运行.
  • COE: Container Orchestration Engine, 容器集群管理后端,目前支持的有 k8s, Swarm 和 Mesos. magnum 的主要工作就是将用户的请求转发到 COE, 完成容器的管理。

以下是 k8s 中的一些概念扫盲(下文中的演示部分使用 k8s 来举例)

  • Pod: k8s 的部署调度单元, 一般多个容器组成一个 Pod, Pod 里的容器运行相同的应用, 属于同一 Pod的容器运行在同一个Minion(Host)上,作为一个统一管理单元,共享相同的 volume 和 network namespace/IP和Port空间。详细信息参考 k8s 中的介绍。在新版本中 Pod 的相关管理 命令已经从 magnum 中移除, 要管理 Pod 需要使用 k8s 提供的 kubectl 工具。
  • Replication controller: 用来保证用户定义数量的 Pod 副本在任何时候都是处于 正常运行状态的。也即他的职责是用来管理 Pod 的伸缩
  • Service: 可以理解为是pod网关,因为pod在运行中可能被删除或者ip发生变化,service可以 保证pod的动态变化对访问端是透明的。

800px-Magnum_architecture.png

安装

可以采用 devstack 或者手动安装的方式安装,下面是使用 devstack 安装 magnum 部分的相关配置

# magnum 需要 heat 来编排调度 node
enable_plugin heat http://git.trystack.cn/openstack/heat.git
ENABLED_SERVICES+=,h-eng,h-api,h-api-cfn,h-api-cw
 
# magnum 需要负载均衡组件来做集群管理,特别的,这里使用 v2 的 lbaas 服务
enable_plugin neutron-lbaas http://git.trystack.cn/openstack/neutron-lbaas.git
enable_plugin octavia http://git.trystack.cn/openstack/octavia
disable_service q-lbaas
ENABLED_SERVICES+=,q-lbaasv2
ENABLED_SERVICES+=,octavia,o-cw,o-hk,o-hm,o-api
 
# 启用 magnum 和 magnum-u 插件
enable_plugin magnum http://git.trystack.cn/openstack/magnum.git
enable_plugin magnum-ui http://git.trystack.cn/openstack/magnum-ui.git

安装完成后,可以通过以下命令确认 magnum 服务正常运行。

$ magnum service-list
+----+-----------------------+------------------+-------+----------+-----------------+---------------------------+---------------------------+
| id | host                  | binary           | state | disabled | disabled_reason | created_at                | updated_at                |
+----+-----------------------+------------------+-------+----------+-----------------+---------------------------+---------------------------+
| 1  | localhost.localdomain | magnum-conductor | up    |          | -               | 2017-01-03T10:48:14+00:00 | 2017-01-04T06:45:33+00:00 |
+----+-----------------------+------------------+-------+----------+-----------------+---------------------------+---------------------------+

创建容器管理集群

在容器的世界中,容器是运行在操作系统之上的特殊的 “应用程序”, 由在此操作系统上运行的容器管理程序 k8s 或者 mesos 管理. 在 magnum 中,这个操作系统(和管理程序)是 nova 中的虚拟机。不难理解这样使用 带来的好处,可以复用 nova,glance,cinder,neutron 等等的功能。反正容器本身只需要一个运行时的操作 系统,不管这个操作系统实在虚拟机还是物理机上。

创建容器管理集群的规格

类似于虚拟机的使用,在创建容器管理集群的时候,需要有一个规格,也即上面说过的 ClusterTemplate, 根据此规格的定义来创建容器管理集群。

$ nova keypair-add --pub-key ~/.ssh/id_rsa.pub testkey
$ magnum cluster-template-create \
  --name k8s-cluster-template \          <-- 规格的名字
  --image fedora-atomic-latest \         <-- 运行 node 的镜像
  --keypair testkey \                    <-- 导入一个密钥到 k8s node 上, 方便后续的访问
  --external-network public \            <-- 给每个 node 分配浮动 IP时用到的外部网络
  --dns-nameserver 192.168.2.64 \        <-- node 里面的 dns 配置, 我这里选用一台内网的地址,是为了防止集群初始化的时候访问 google 的服务失败,在这里做翻墙处理 :)
  --flavor m1.small \                    <-- node 虚拟机的规格
  --docker-volume-size 5 \               <-- 给 docker 预留的 volume 的大小
  --network-driver flannel \             <-- 容器使用的网络后端,这里选用 flannel
  --coe kubernetes                       <-- 容器的管理后端,这里选择 k8s

创建 k8s 集群

$ magnum cluster-create --name k8s-cluster --cluster-template k8s-cluster-template --node-count 2
 
$ magnum cluster-list # 等到 status 变为 CREATE_COMPLETE 说明集群创建成功。
+--------------------------------------+-------------+---------+------------+--------------+-----------------+
| uuid                                 | name        | keypair | node_count | master_count | status          |
+--------------------------------------+-------------+---------+------------+--------------+-----------------+
| dd483a45-2b7a-4294-b918-680e1399a7cc | k8s-cluster | testkey | 2          | 1            | CREATE_COMPLETE |
+--------------------------------------+-------------+---------+------------+--------------+-----------------+
 
$ magnum cluster-show k8s-cluster
+---------------------+------------------------------------------------------------+
| Property            | Value                                                      |
+---------------------+------------------------------------------------------------+
| status              | CREATE_COMPLETE                                            |
| cluster_template_id | 8538544b-c809-478f-a98d-e0c35bfeef9c                       |
| node_addresses      | ['192.168.5.136', '192.168.5.134']                         |        <-- k8s 的 minion node
| uuid                | dd483a45-2b7a-4294-b918-680e1399a7cc                       |
| stack_id            | b9e815a8-a746-49b0-a307-755c17415324                       |
| status_reason       | Stack CREATE completed successfully                        |
| created_at          | 2017-01-04T09:07:45+00:00                                  |
| updated_at          | 2017-01-04T09:10:05+00:00                                  |
| coe_version         | v1.2.0                                                     |
| keypair             | testkey                                                    |
| api_address         | https://192.168.5.137:6443                                 |        <-- k8s api endpoint
| master_addresses    | ['192.168.5.137']                                          |        <-- k8s 的master node
| create_timeout      | 60                                                         |
| node_count          | 2                                                          |
| discovery_url       | https://discovery.etcd.io/6a6004c6a9730be2e0896e412caea0a9 |
| master_count        | 1                                                          |
| container_version   | 1.9.1                                                      |
| name                | k8s-cluster                                                |
+---------------------+------------------------------------------------------------+

由于 magnum 后端使用 heat 来做集群的编排,所以也可以通过 heat 来查看相关状态:

$ heat stack-list
+--------------------------------------+--------------------------+-----------------+----------------------+--------------+
| id                                   | stack_name               | stack_status    | creation_time        | updated_time |
+--------------------------------------+--------------------------+-----------------+----------------------+--------------+
| 5fb85534-54fe-407d-ac2d-f3c319024735 | k8s-cluster-644272c22n6h | CREATE_COMPLETE | 2017-01-05T02:28:43Z | None         |
+--------------------------------------+--------------------------+-----------------+----------------------+--------------+

在 horizon 上也可以看到用到的资源的拓扑图:

os_magnum_ui.png

配置集群的访问方式

注入 k8s 的访问密钥到 k8s 集群中,这里利用 magnum 提供的 cluster-config 命令, 该命令会生成三个 *.pem证书文件和一个 k8s 客户端的配置文件 config, 并将相应的公钥注入到 k8s 中. 将这四个文件拷贝到任何想访问 k8s 集群的终端即可。

# Set kubectl to use the correct certs
$ magnum cluster-config k8s-cluster
$ ls *.pem config
ca.pem  cert.pem  config  key.pem

使用容器管理集群

严格来讲上面已经完成了容器管理集群的部署,照着 k8s 的文档就可以开始使用容器了。

安装配置 k8s 客户端

$ wget https://github.com/kubernetes/kubernetes/releases/download/v1.0.1/kubernetes.tar.gz
$ tar -xvzf kubernetes.tar.gz
$ sudo cp -a kubernetes/platforms/linux/amd64/kubectl /usr/bin/kubectl

拷贝 k8s 的证书文件到当前目录下,再导入 k8s 的配置文件即可开始使用,例如下面的实例:

$ export KUBECONFIG=config
$ kubectl cluster-info
Kubernetes master is running at https://192.168.5.131:6443
KubeUI is running at https://192.168.5.131:6443/api/v1/proxy/namespaces/kube-system/services/kube-ui

创建容器集群

我们以 k8s 源码目录 kubernetes/examples/redis 下的 redis 集群为例子。

$ kubectl create -f redis-master.yaml
$ kubectl create -f redis-sentinel-service.yaml
$ kubectl create -f redis-controller.yaml
$ kubectl create -f redis-sentinel-controller.yaml

创建完成之后可以看到所有资源的状态都就绪了。

$ kubectl get pods,rc,services
NAME                   READY     STATUS    RESTARTS   AGE
redis-master           2/2       Running   0          2h
redis-ptvgd            1/1       Running   0          1h
redis-sentinel-6wiha   1/1       Running   0          3m
CONTROLLER       CONTAINER(S)   IMAGE(S)              SELECTOR              REPLICAS
redis            redis          kubernetes/redis:v1   name=redis            2
redis-sentinel   sentinel       kubernetes/redis:v1   redis-sentinel=true   2
NAME             LABELS                                    SELECTOR              IP(S)          PORT(S)
kubernetes       component=apiserver,provider=kubernetes   <none>                10.254.0.1     443/TCP
redis-sentinel   name=sentinel,role=service                redis-sentinel=true   10.254.38.28   26379/TCP

验证我们创建的redis集群:

首先进入容器中设置一个 key:

$ kubectl exec redis-master -ti /bin/bash
root@redis-master:/data# redis-cli  
127.0.0.1:6379> set var "hello world"
OK

找另外一台机器作为 redis 客户端

dunrong@localhost$ kubectl port-forward -p redis-master 6380:6379                <-- 将 k8s 中 redis 的服务端口(6379) 端口映射到本地6380
I0110 17:56:55.657227    2903 portforward.go:225] Forwarding from 127.0.0.1:6380 -> 6379
I0110 17:56:55.657335    2903 portforward.go:225] Forwarding from [::1]:6380 -> 6379

然后用 redis-cli 访问:

dunrong@localhost$ redis-cli -p 6380
127.0.0.1:6380> GET var
"hello world"
127.0.0.1:6380>

that’s it!

小结

从上面的介绍和 demo 来看,目前用 magnum 作为容器的方案是完全可行的。 但由于容器在 OpenStack 中还属于快速开发阶段,在我看来他的作用目前仅仅是一个 k8s, mesos 之类的的容器管理集群的部署脚本,且在使用中存在各种各样的 bug。如果用户具有 一定的 OpenStack 和容器技术的经验,那么使用 OpenStack 在生产中管理部署容器是可行的。

tips

在 k8s 初始化的时候,默认情况下件会到 google的镜像仓库去下载一些 docker 镜像, 由于 你懂的 的原因,在大中华区这步肯定会失败。解决方案无外乎是让整个集群都能 科学上网 , 我这里使用一种最简单的方式。在创建 ClusterTemplate 的时候,指定 DNS 为另外一台机器,并这台机器上安装 dnsmasq, 将 gcr 的域名请求做本地解决即可。

分类: Docker, OpenStack 标签: ,

lxde 桌面屏幕截图配置

2016年10月12日 1 条评论

在 Mac 或者 Windows 下使用微信截图工具很方便,hack 一个到 lxde.

这里选择 scrot 作为后端截图的引擎

修改 $HOME/.config/openbox/lxde-rc.xml 文件, 增加以下一个配置项:

<keybind key="Print">
  <action name="Execute">
    <execute>scrot -s '%F-%H%M%S.png' -e 'mv $f ~/Desktop/'</execute>
  </action>
</keybind>

表示当按下 PrtScn 的时候,调用 scrot 选择区域截图, 截图完成之后将截图按照 year-month-day-时分秒.png 的格式保存到桌面上

稍后,让 openbox 重载配置文件即可

$ openbox --reconfigure

如果习惯 QQ 或者 Wechat 快捷键截图,只需修改按键绑定到 Ctrl-Shift-A 即可:

<keybind key="C-A-A">
分类: Tips 标签:

加速 SSH 连接的 tips

2016年5月18日 没有评论

SSH 登陆服务器的时候有很多流程,例如:

  • 不必要的将所有支持的登陆方式都尝试一遍知道某一种登陆成功.
  • 某些场景下不必要的 DNS 反向解析验证.

客户端指定验证方式加速连接

openssh 有基于 GSSAPI, host, 公钥/私钥, challenge-response, 密码等认证方式.
默认情况下, 客户端会使用一定的顺序尝试认证,直到某一种认证通过. 实际上, 在实际使用中,
我们一般只会使用秘钥和密码这两种认证方式,其他方式完全没有必要. 并且还会浪费时间.

可以在 ssh 客户端用户的配置文件中禁用其他配置.

# file: ~/.ssh/config
PreferredAuthentications publickey,password

上面的配置表示客户端仅支持(秘钥)和password(密码)认证. 并且优先尝试秘钥认证.

关闭 UseDNS

如果 UseDNS 选项被打开, 在客户端发起连接的时候, SSH 服务器会做一个 DNS 的反向查询验证,
根据客户端的 IP 地址进行 DNS PTR 反向查询, 用查询出到的客户端的主机名进行 DNS 正向 A 记录查询,
验证和原始地址是否一致. 一般来讲, 我们的客户端的 IP 都是动态的, 不存在 PTR 记录,
SSH 服务器做这个验证只会浪费时间, 如下的 debug 信息:

May 18 06:22:51 localhost sshd[4969]: debug3: mm_answer_pwnamallow
May 18 06:22:51 localhost sshd[4969]: debug3: Trying to reverse map address 25.0.0.1.  <-- 这里浪费了 15 秒, 很恼人
May 18 06:23:06 localhost sshd[4969]: debug2: parse_server_config: config reprocess config len 757
May 18 06:23:06 localhost sshd[4969]: debug3: mm_answer_pwnamallow: sending MONITOR_ANS_PWNAM: 1

所以一般推荐关闭这个选项, 修改 /etc/ssh/sshd_config 中 UseDNS 为 no:

UseDNS no

PS: 稍新的 openssh 版本改选项默认已经关闭

pam 认证相关

有时候登录很慢, 查询日志: 通常都会看到类似 pam 的问题:

May 18 00:00:26 m8x CROND[14936]: pam_systemd(crond:session): Failed to release session:

在支持 systemd 的系统上, 一般这种情况是处理用户登录的 systemd-logind 服务造成的, 重启该服务即可.

$ sudo systemctl restart systemd-logind.service
分类: Tips 标签:

使用 s3fs 挂载对象存储到本地磁盘

2016年5月10日 没有评论

虽然对象存储的协议和使用场景与传统的文件系统存储(传统的 NAS 等)存在较大的差异, 但是
某些场景下, 将对象存储当作普通的文件系统更为方便, 比如不用再为了一些基本的文件操作而
专门使用第三方客户端等.

以下介绍通过 s3fs 来将 S3 兼容对象存储作为文件系统使用的方法.

PS1: 对于 windows 用户来讲, 类似的工具很多, 比如使用 CloudBerry 提供的 CloudBerry Drive.

PS2: 对于非 S3 兼容的对象存储产品, 由于各大厂商都是模拟重新实现 S3 协议, 所以可以找到基于 s3fs
的工具, 比如阿里云对象存储的 ossfs, 用法和 s3fs 一样

安装

Linux

由于大部分发行版都没有在软件仓库中收录改包, 所以在 Linux 需要自行编译.

  • 安装开发依赖包
$ sudo apt-get install -y automake autotools-dev g++ git libcurl4-gnutls-dev libfuse-dev libssl-dev libxml2-dev make pkg-config # for ubuntu
$ sudo yum install -y automake fuse-devel gcc-c++ git libcurl-devel libxml2-devel make openssl-devel # for fedora
  • 下载编译 s3fs
$ git clone https://github.com/s3fs-fuse/s3fs-fuse.git
$ cd s3fs-fuse
$ ./autogen.sh
$ ./configure # 如果报错, 查看具体错误类型安装相应的依赖
$ make -j3
$ sudo make install

OSX

Mac OS 由于有 Homebrew 的存在, 安装很方便:

$ brew install s3fs

使用

设置密码文件

echo {AccessKey}:{SecretKey} > $HOME/.passwd-s3fs && chmod 600 $HOME/.passwd-s3fs

挂载

$ s3fs {BucketName} {MountPoint} -o host=http://obs.eayun.com:9090 -o umask=0022
  • obs.eayun.com 是一个自己搭建的兼容 S3 的对象存储服务, 如果使用的是 S3, 则该选项不需要使用
  • umask 是挂载的权限, 和 liunx 下的 umask 的一致, 这个参数很重要, 否则挂载用户无法读写某些对象

示例:

$ mkdir mnt
$ s3fs tmp mnt -o host=http://obs.eayun.com:9090 -o umask=0022
$ df -h mnt
文件系统        容量  已用  可用 已用% 挂载点
s3fs            256T     0  256T    0% /home/dunrong/mnt

之后就可以像使用一般文件系统一样使用 bucket 了

$ cp Kvm-forum-2013-COLO.pdf vm.sh mnt/  # <-- 复制本地文件到对象存储
$ s3cmd ls s3://tmp  # <-- 在对象存储中查看是否成功
2016-05-09 08:04   2440042   s3://tmp/Kvm-forum-2013-COLO.pdf
2016-05-09 08:04       586   s3://tmp/vm.sh
$ du -sh mnt/  # <-- 查看使用量的大小
2.4M	mnt/
$ ls -l mnt/  # <-- 查看挂载点上是否存在上传的对象
总用量 2384
-rwxr-xr-x 1 dunrong dunrong 2440042  59 16:04 Kvm-forum-2013-COLO.pdf
-rwxr-xr-x 1 dunrong dunrong     586  59 16:04 vm.sh
$ rm mnt/vm.sh  # <-- 删除对象
rm:是否删除普通文件 "mnt/vm.sh"? y
$ ls -l mnt/
总用量 2383
-rwxr-xr-x 1 dunrong dunrong 2440042  59 16:04 Kvm-forum-2013-COLO.pdf
$ s3cmd ls s3://tmp  # <-- 在对象存储中查看是否删除成功
2016-05-09 08:04   2440042   s3://tmp/Kvm-forum-2013-COLO.pdf

优缺点

优点

  • 可以像传统文件系统一样读写对象存储, 比如复制删除查看文件等操作
  • 在不同设备(windows, linux, macos) 之间共享 bucket 较为方便

缺点

虽然 s3fs 基于 FUSE, 但是底层的实现协议还是对象存储的协议, 很多基于文件的操作无法实现,
或者实现起来性能很差. 所以和传统的 NAS 等文件系统存储比较, 存在很多缺点.

  • 由于原生 S3 协议不支持基于文件的读写, 追加文件和随机写文件非常慢
  • 由于 S3 协议没有所谓的元数据服务器, 列出文件, 查看文件夹大小等操作非常慢
分类: Storage 标签:

Ceph 深入解析(2) — Ceph Admin Socket

2015年9月1日 1 条评论

介绍

在 Ceph 每个组件(e.g. Monitor, OSD, CephFS) 运行的时候,会创建一个基于 UNIX socket 的服务,该服务通过这个 socket 提供一个和该组件交互的接口. 用户可以 通过 ceph daemon {socket-file} command 查询/配置一些常用的参数.

使用

使用下列格式访问 admin socket:

$ ceph daemon [socket-file] [command]

其中, socket-file 是该组件的 unix socket 的路径, command 是具体的查询指令

一般可以通过 help 得到改组件的 admin socket 支持的命令

$ ceph daemon [socket-file] help

下面举几个例子:

  1. 查询 monitor 的 quorum 状态:
$ ceph daemon /var/run/ceph/ceph-mon.ceph1.asok quorum_status
{ "election_epoch": 304,
  "quorum": [
        0,
        1,
        2],
  "quorum_names": [
        "ceph1",
        "ceph2",
        "ceph3"],
  "quorum_leader_name": "ceph1",
  "monmap": { "epoch": 3,
      "fsid": "8e92b691-a67a-4e26-969a-4e4f18ed3fa0",
      "modified": "2015-06-17 19:35:00.946699",
      "created": "0.000000",
      "mons": [
            { "rank": 0,
              "name": "ceph1",
              "addr": "192.168.3.137:6789\/0"},
            { "rank": 1,
              "name": "ceph2",
              "addr": "192.168.3.138:6789\/0"},
            { "rank": 2,
              "name": "ceph3",
              "addr": "192.168.3.139:6789\/0"}]}}
  1. 查询本节点的 OSD 状态
$ ceph daemon /var/run/ceph/ceph-osd.1.asok status
{ "cluster_fsid": "8e92b691-a67a-4e26-969a-4e4f18ed3fa0",
  "osd_fsid": "05ee7ec8-2309-47b1-81bf-f6ef49d0f87f",
  "whoami": 1,
  "state": "active",
  "oldest_map": 1,
  "newest_map": 280,
  "num_pgs": 699}

实现代码解析

流程

bool AdminSocket::init(const std::string &path)
{
    // 创建一个管道,该管道用来让父进程关闭该子进程,看下面 entry 的处理
    err = create_shutdown_pipe(&pipe_rd, &pipe_wr);
 
    // 创建 Unix socket, 在该 socket 上监听客户端的连接
    err = bind_and_listen(path, &sock_fd);
 
    // 注册通用的命令 hook
    m_version_hook = new VersionHook;
    register_command("0", "0", m_version_hook, "");
    register_command("version", "version", m_version_hook, "get ceph version");
    register_command("git_version", "git_version", m_version_hook, "get git sha1");
    m_help_hook = new HelpHook(this);
    register_command("help", "help", m_help_hook, "list available commands");
    m_getdescs_hook = new GetdescsHook(this);
    register_command("get_command_descriptions", "get_command_descriptions",
                     m_getdescs_hook, "list available commands");
 
    // 进入新线程中执行 AdminSocket::entry()
    create();
}
 
void* AdminSocket::entry()
{
    while (true) {
        // 使用 poll 来轮训 socket, 注意,该线程轮训 Unix socket 和与父进程通信的
        // 管道
        struct pollfd fds[2];
        memset(fds, 0, sizeof(fds));
        fds[0].fd = m_sock_fd;
        fds[0].events = POLLIN | POLLRDBAND;
        fds[1].fd = m_shutdown_rd_fd;
        fds[1].events = POLLIN | POLLRDBAND;
 
        int ret = poll(fds, 2, -1);
        if (fds[0].revents & POLLIN) {
            // 处理这个请求
            do_accept();
        }
        // 如果进程推出,会通过 pipe 的写端发送一个字节
        if (fds[1].revents & POLLIN) {
            // Parent wants us to shut down
            return PFL_SUCCESS;
        }
    }
}
 
bool AdminSocket::do_accept()
{
    int connection_fd = accept(m_sock_fd, (struct sockaddr*) &address,
                               &address_length);
    char cmd[1024];
    int pos = 0;
 
    // 读取客户端发送的请求到 cmd
    while (1) {
        int ret = safe_read(connection_fd, &cmd[pos], 1);
        // new protocol: null or \n terminated string
        if (cmd[pos] == '\n' || cmd[pos] == '\0') {
            cmd[pos] = '\0';
            c = cmd;
            break;
        }
        pos++;
    }
 
    // 通过得到的 cmd 从所有注册的 hooks(AdminSocket::m_hooks)中找到匹配的
    // hook
    p = m_hooks.find(match);
 
    bufferlist out;
    // 调用 hook 的 AdminSocketHook::call 方法处理改请求,将结果填充到 out
    // 中
    bool success = p->second->call(match, cmdmap, format, out);
 
    // 发送请求结果
    uint32_t len = htonl(out.length());
    int ret = safe_write(connection_fd, &len, sizeof(len));
    if (out.write_fd(connection_fd) >= 0)
        rval = true;
}

分类: Storage 标签:

Ceph 深入解析(1) — Ceph 的消息处理架构

2015年8月14日 2 条评论

Ceph 的消息处理主要关联到以下几个类:

消息处理架构

架构上采用 Publish/subscribe(发布/订阅) 的设计模式.

模块说明

class Messenger

该类作为消息的发布者, 各个 Dispatcher 子类作为消息的订阅者, Messenger 收到消息之后,通过 Pipe 读取消息,然后转给 Dispatcher 处理

class SimpleMessenger

Messenger 接口的实现

class Dispatcher

该类是订阅者的基类,具体的订阅后端继承该类,初始化的时候通过 Messenger::add_dispatcher_tail/head 注册到 Messenger::dispatchers. 收到消息后,通知改类处理

class Accepter

监听 peer 的请求, 有新请求时, 调用 SimpleMessenger::add_accept_pipe() 创建新的 Pipe 到 SimpleMessenger::pipes 来处理该请求

class Pipe

用于消息的读取和发送,该类主要有两个组件,Pipe::Reader 和 Pipe::Writer, 分别用来处理 消息的读取和发送. 这两个类都是 class Thread 的子类,意味这每次处理消息都会有两个 线程被分别创建.

消息被 Pipe::Reader 读取后,该线程会通知注册到 Messenger::dispatchers 中的某一个 Dispatcher(如 Monitor) 处理, 处理完成之后将回复的消息放到 SimpleMessenger::Pipe::out_q 中,供 Pipe::Writer 来处理发送

class DispatchQueue

该类用来缓存收到的消息, 然后唤醒 DispatchQueue::dispatch_thread 线程找到后端的 Dispatch 处理消息

深入解析

流程

下面的代码涉及到的订阅子类以 Monitor 为例:

初始化

int main(int argc, char *argv[])
{
    // 创建一个 Messenger 对象,由于 Messenger 是抽象类,不能直接实例化,提供了一个
    // ::create 的方法来创建子类,目前 Ceph 所有模块使用 SimpleMessenger
    Messenger *messenger = Messenger::create(g_ceph_context,
                                             entity_name_t::MON(rank),
                                             "mon",
                                             0);
 
    /**
     * 执行 socket() -> bind() -> listen() 等一系列动作, 执行流程如下:
     SimpleMessenger::bind()
         --> Accepter::bind()
             socket() -> bind() -> listen()
    */
    err = messenger->bind(ipaddr);
 
    // 创建一个 Dispatch 的子类对象, 这里是 Monitor
    mon = new Monitor(g_ceph_context, g_conf->name.get_id(), store, 
                      messenger, &monmap);
 
    // 启动 Reaper 线程
    messenger->start();
 
    /**
     * a). 初始化 Monitor 模块
     * b). 通过 SimpleMessenger::add_dispatcher_tail() 注册自己到
     * SimpleMessenger::dispatchers 中, 流程如下:
     * Messenger::add_dispatcher_tail()
     *      --> ready()
     *        --> dispatch_queue.start()(新 DispatchQueue 线程)
              --> Accepter::start()(启动start线程)
     *            --> accept
     *                --> SimpleMessenger::add_accept_pipe
     *                    --> Pipe::start_reader
     *                        --> Pipe::reader()
     * 在 ready() 中: 通过 Messenger::reader(),
     * 1) DispatchQueue 线程会被启动,用于缓存收到的消息消息
     * 2) Accepter 线程启动,开始监听新的连接请求.
     */
    mon->init();
 
    // 进入 mainloop, 等待退出
    messenger->wait();
    return 0;
}

消息处理

收到连接请求

请求的监听和处理由 SimpleMessenger::ready –> Accepter::entry 实现

void SimpleMessenger::ready()
{
    // 启动 DispatchQueue 线程
    dispatch_queue.start();
 
    lock.Lock();
    // 启动 Accepter 线程监听客户端连接, 见下面的 Accepter::entry
    if (did_bind)
        accepter.start();
    lock.Unlock();
}
 
void *Accepter::entry()
{
    struct pollfd pfd;
    // listen_sd 是 Accepter::bind() 中创建绑定的 socket
    pfd.fd = listen_sd;
    pfd.events = POLLIN | POLLERR | POLLNVAL | POLLHUP;
    while (!done) {
        int r = poll(&pfd, 1, -1);
        if (pfd.revents & (POLLERR | POLLNVAL | POLLHUP))
            break;
        if (done) break;
        entity_addr_t addr;
        socklen_t slen = sizeof(addr.ss_addr());
        int sd = ::accept(listen_sd, (sockaddr*)&addr.ss_addr(), &slen);
        if (sd >= 0) {
            // 调用 SimpleMessenger::add_accept_pipe() 处理这个连接
            msgr->add_accept_pipe(sd);
        } 
    }
    return 0;
}

随后创建 Pipe() 开始消息的处理

Pipe *SimpleMessenger::add_accept_pipe(int sd)
{
    lock.Lock();
    Pipe *p = new Pipe(this, Pipe::STATE_ACCEPTING, NULL);
    p->sd = sd;
    p->pipe_lock.Lock();
    // 
    /**
     * 调用 Pipe::start_reader() 开始读取消息, 将会创建一个读线程开始处理.
     * Pipe::start_reader() --> Pipe::reader
     */
    p->start_reader();
    p->pipe_lock.Unlock();
    pipes.insert(p);
    accepting_pipes.insert(p);
    lock.Unlock();
    return p;
}

创建消息读取和发送线程

处理消息由 Pipe::start_reader() –> Pipe::reader() 开始,此时已经是在 Reader 线程中. 首先会调用 accept() 做一些简答的处理然后创建 Writer() 线程,等待发送回复 消息. 然后读取消息, 读取完成之后, 将收到的消息封装在 Message 中,交由 dispatch_queue() 处理.

dispatch_queue() 找到注册者,将消息转交给它处理,处理完成唤醒 Writer() 线程发送回复消息.

void Pipe::reader()
{
    /**
     * Pipe::accept() 会调用 Pipe::start_writer() 创建 wirter 线程, 进入 writer 线程
     * 后,会 cond.Wait() 等待被激活,激活的流程看下面的说明. Writer 线程的创建见后后面
     * Pipe::accept() 的分析
     */
    if (state == STATE_ACCEPTING) {
        accept();
    }
 
    while (state != STATE_CLOSED &&
           state != STATE_CONNECTING) {
        // 读取消息类型,某些消息会马上激活 writer 线程先处理
        if (tcp_read((char*)&tag, 1) < 0) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_KEEPALIVE) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_KEEPALIVE2) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_KEEPALIVE2_ACK) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_ACK) {
            continue;
        }
        else if (tag == CEPH_MSGR_TAG_MSG) {
            // 收到 MSG 消息
            Message *m = 0;
            // 将消息读取到 new 到的 Message 对象
            int r = read_message(&m, auth_handler.get());
 
            // 先激活 writer 线程 ACK 这个消息
            cond.Signal();  // wake up writer, to ack this
 
            // 如果该次请求是可以延迟处理的请求,将 msg 放到 Pipe::DelayedDelivery::delay_queue, 
            // 后面通过相关模块再处理
            // 注意,一般来讲收到的消息分为三类:
            // 1. 直接可以在 reader 线程中处理,如上面的 CEPH_MSGR_TAG_ACK
            // 2. 正常处理, 需要将消息放入 DispatchQueue 中,由后端注册的消息处理,然后唤醒发送线程发送
            // 3. 延迟发送, 下面的这种消息, 由定时时间决定什么时候发送
            if (delay_thread) {
                utime_t release;
                if (rand() % 10000 < msgr->cct->_conf->ms_inject_delay_probability * 10000.0) {
                    release = m->get_recv_stamp();
                    release += msgr->cct->_conf->ms_inject_delay_max * (double)(rand() % 10000) / 10000.0;
                    lsubdout(msgr->cct, ms, 1) << "queue_received will delay until " << release << " on " << m << " " << *m << dendl;
                }
                delay_thread->queue(release, m);
            } else {
                // 正常处理的消息,放到 Pipe::DispatchQueue *in_q 中, 以下是整个消息的流程
                // DispatchQueue::enqueue()
                //     --> mqueue.enqueue() -> cond.Signal()(激活唤醒 DispatchQueue::dispatch_thread 线程)
                //         --> DispatchQueue::dispatch_thread::entry() 该线程得到唤醒
                //             --> Messenger::ms_deliver_XXX
                //                 --> 具体的 Dispatch 实例, 如 Monitor::ms_dispatch()
                //                     --> Messenger::send_message()
                //                         --> SimpleMessenger::submit_message()
                //                             --> Pipe::_send()
                //                                 --> Pipe::out_q[].push_back(m) -> cond.Signal 激活 writer 线程
                //                                     --> ::sendmsg() // 发送到 socket
                in_q->enqueue(m, m->get_priority(), conn_id);
            }
        } 
 
        else if (tag == CEPH_MSGR_TAG_CLOSE) {
            cond.Signal();
            break;
        }
        else {
            ldout(msgr->cct,0) << "reader bad tag " << (int)tag << dendl;
            pipe_lock.Lock();
            fault(true);
        }
    }
}

Pipe::accept() 做一些简单的协议检查和认证处理,之后创建 Writer() 线程: Pipe::start_writer() –> Pipe::Writer

int Pipe::accept()
{
    ldout(msgr->cct,10) << "accept" << dendl;
    // 检查自己和对方的协议版本等信息是否一致等操作
    // ......
 
    while (1) {
        // 协议检查等操作
        // ......
 
        /**
         * 通知注册者有新的 accept 请求过来,如果 Dispatcher 的子类有实现
         * Dispatcher::ms_handle_accept(),则会调用该方法处理
         */
        msgr->dispatch_queue.queue_accept(connection_state.get());
 
        // 发送 reply 和认证相关的消息
        // ......
 
        if (state != STATE_CLOSED) {
            /**
             * 前面的协议检查,认证等都完成之后,开始创建 Writer() 线程等待注册者
             * 处理完消息之后发送
             * 
             */
            start_writer();
        }
        ldout(msgr->cct,20) << "accept done" << dendl;
 
        /**
         * 如果该消息是延迟发送的消息, 且相关的发送线程没有启动,启动之
         * Pipe::maybe_start_delay_thread()
         *     --> Pipe::DelayedDelivery::entry()
         */
        maybe_start_delay_thread();
        return 0;   // success.
    }
}

随后 Writer 线程等待被唤醒发送回复消息

void Pipe::writer()
{
    while (state != STATE_CLOSED) {// && state != STATE_WAIT) {
        if (state != STATE_CONNECTING && state != STATE_WAIT && state != STATE_STANDBY &&
            (is_queued() || in_seq > in_seq_acked)) {
 
            // 对 keepalive, keepalive2, ack 包的处理
            // ......
 
            // 从 Pipe::out_q 中得到一个取出包准备发送
            Message *m = _get_next_outgoing();
            if (m) {
                // 对包进行一些加密处理
                m->encode(features, !msgr->cct->_conf->ms_nocrc);
 
                // 包头
                ceph_msg_header& header = m->get_header();
                ceph_msg_footer& footer = m->get_footer();
 
                // 取出要发送的二进制数据
                bufferlist blist = m->get_payload();
                blist.append(m->get_middle());
                blist.append(m->get_data());
 
                // 发送包: Pipe::write_message() --> Pipe::do_sendmsg --> ::sendmsg()
                ldout(msgr->cct,20) << "writer sending " << m->get_seq() << " " << m << dendl;
                int rc = write_message(header, footer, blist);
                m->put();
            }
            continue;
        }
 
        // 等待被 Reader 或者 Dispatcher 唤醒
        ldout(msgr->cct,20) << "writer sleeping" << dendl;
        cond.Wait(pipe_lock);
    }
}

消息的处理

Reader 线程将消息交给 dispatch_queue 处理,流程如下:

Pipe::reader() –> Pipe::in_q->enqueue()

void DispatchQueue::enqueue(Message *m, int priority, uint64_t id)
{
    Mutex::Locker l(lock);
    ldout(cct,20) << "queue " << m << " prio " << priority << dendl;
    add_arrival(m);
    // 将消息按优先级放入 DispatchQueue::mqueue 中
    if (priority >= CEPH_MSG_PRIO_LOW) {
        mqueue.enqueue_strict(
            id, priority, QueueItem(m));
    } else {
        mqueue.enqueue(
            id, priority, m->get_cost(), QueueItem(m));
    }
    // 唤醒 DispatchQueue::entry() 处理消息
    cond.Signal();
}
 
void DispatchQueue::entry()
{
    while (true) {
        while (!mqueue.empty()) {
            QueueItem qitem = mqueue.dequeue();
            Message *m = qitem.get_message();
            /**
             * 交给 Messenger::ms_deliver_dispatch() 处理,后者会找到
             * Monitor/OSD 等的 ms_deliver_dispatch() 开始对消息的逻辑处理
             * Messenger::ms_deliver_dispatch()
             *     --> Monitor::ms_dispatch()
             */
            msgr->ms_deliver_dispatch(m);
        }
        if (stop)
            break;
 
        // 等待被 DispatchQueue::enqueue() 唤醒
        cond.Wait(lock);
    }
    lock.Unlock();
}

下面简单看一下在订阅者的模块中消息是怎样被放入 Pipe::out_q 中的:

Messenger::ms_deliver_dispatch()
    --> Monitor::ms_dispatch()
        --> Monitor::_ms_dispatch
            --> Monitor::dispatch
                --> Monitor::handle_mon_get_map
                    --> Monitor::send_latest_monmap
                        --> SimpleMessenger::send_message()
                            --> SimpleMessenger::_send_message()
                                --> SimpleMessenger::submit_message()
                                    --> Pipe::_send()
bool Monitor::_ms_dispatch(Message *m)
{
    ret = dispatch(s, m, src_is_mon);
 
    if (s) {
        s->put();
    }
 
    return ret;
}
 
bool Monitor::dispatch(MonSession *s, Message *m, const bool src_is_mon)
{
    switch (m->get_type()) {
    case CEPH_MSG_MON_GET_MAP:
        handle_mon_get_map(static_cast<MMonGetMap*>(m));
        break;
    // ......
    default:
        ret = false;
    }
    return ret;
}
 
void Monitor::handle_mon_get_map(MMonGetMap *m)
{
    send_latest_monmap(m->get_connection().get());
    m->put();
}
 
void Monitor::send_latest_monmap(Connection *con)
{
    bufferlist bl;
    monmap->encode(bl, con->get_features());
    /**
     * SimpleMessenger::send_message()
     *     --> SimpleMessenger::_send_message()
     *         --> SimpleMessenger::submit_message()
     *             --> Pipe::_send()
     */
    messenger->send_message(new MMonMap(bl), con);
}
 
void Pipe::_send(Message *m)
{
    assert(pipe_lock.is_locked());
    out_q[m->get_priority()].push_back(m);
    // 唤醒 Writer 线程
    cond.Signal();
}

总结

由上面的所有分析,除了订阅者/发布者设计模式,对网络包的处理上采用的是古老的 生产者消费者问题 线程模型,每次新的请求就会有创建一对收/发线程用来处理消息的接受 发送,如果有大规模的请求,线程的上下文切换会带来大量的开销,性能可能产生瓶颈。

不过在较新的 Ceph 版本中,新增加了两种新的消息模型: AsyncMessenger 和 XioMessenger 让 Ceph 消息处理得到改善.

相关的评测以后带来

分类: Storage 标签:

RadosGW 初体验

2015年8月3日 没有评论

介绍

Ceph 的最底层模块是对象存储,本质上就是一个 Rados, 如果不考虑用户体验,利用 rados 就可以使用 Ceph 对象存储. 例如:

$ rados -p test_pool get SI7W1FUI43H3I3ED9CWX test.data
  • test_pool: pool 名
  • SI7W1FUI43H3I3ED9CWX: object 名
  • test.data 保存到这里

和 RBD 以及 CephMDS 一样,RadosGW 提供了对 Radow 对象存储访问的更友好支持. 它允许用户通过 Restful API 的方式进行使用 RadowGW 。

从技术架构上讲, 和 RBD 一样, 它位于 Librados 之上, 而且正如它的名字所暗示的, 它是 Rados 的一个网关,通过 HTTP RestFul API, 对外提供对象存储的服务. 为了用户使用的方便性和兼容性,还提供了兼容 S3 和 Swfit 的接口.

以上,使得可以基于 RadosGW 构建云存储.

环境描述

假设已经有一个安装好的 Ceph 集群. 在一台新的节点上配置 RadowGW

节点信息如下:

主机名字
ceph.test.com
IP 地址
192.168.3.140

NOTE: 所有节点的操作系统都是 centos 7.

安装部署

安装配置 Apache

  1. 安装
$ sudo yum install -y httpd
  1. 配置: 编辑 /etc/httpd/conf/httpd.conf
# 设置 ServerName
ServerName ceph.test.com
 
# 添加下面内容
<IfModule !proxy_fcgi_module>
    LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
</IfModule>
 
# 修改监听参数: 192.168.3.140 为本机 IP
Listen 192.168.3.140:80
  1. 重启 Apache
$ sudo systemctl restart httpd

安装配置 RadosGW

  1. 安装 RadosGW
$ sudo yum install -y radosgw-agent
  1. 创建用于 RadosGW 的用户等信息(在 ceph admin 节点执行)
$ sudo ceph-authtool --create-keyring /etc/ceph/ceph.client.radosgw.keyring
$ sudo chmod +r /etc/ceph/ceph.client.radosgw.keyring
$ sudo ceph-authtool /etc/ceph/ceph.client.radosgw.keyring -n client.radosgw.gateway --gen-key
$ sudo ceph-authtool -n client.radosgw.gateway --cap osd 'allow rwx' --cap mon 'allow rwx' /etc/ceph/ceph.client.radosgw.keyring
$ sudo ceph -k /etc/ceph/ceph.client.admin.keyring auth add client.radosgw.gateway -i /etc/ceph/ceph.client.radosgw.keyring

将这个 keyring 和 admin.keyring 复制到 ceph.test.com

$ scp /etc/ceph/ceph.client.radosgw.keyring root@ceph.test.com:/etc/ceph
$ scp /etc/ceph/ceph.client.admin.keyring root@ceph.test.com:/etc/ceph
  1. 创建 RadosGW 需要的 pool(在 ceph admin 节点)
$ ceph osd pool create .rgw 64 64
$ ceph osd pool create .rgw.root 64 64 
$ ceph osd pool create .rgw.control 64 64 
$ ceph osd pool create .rgw.gc 64 64
$ ceph osd pool create .rgw.buckets 64 64 
$ ceph osd pool create .rgw.buckets.index 64 64 
$ ceph osd pool create .log 64 64 
$ ceph osd pool create .intent-log 64 64 
$ ceph osd pool create .usage 64 64 
$ ceph osd pool create .users 64 64 
$ ceph osd pool create .users.email 64 64
$ ceph osd pool create .users.swift 64 64
$ ceph osd pool create .users.uid 64 64
  1. 修改并 gateway.client 的配置到各个节点.

将下列内容添加到 ceph 节点的各个配置文件(包括 RadosGW 节点)

[client.radosgw.gateway]
host = ceph
keyring = /etc/ceph/ceph.client.radosgw.keyring
rgw socket path = /var/run/ceph/ceph.radosgw.gateway.fastcgi.sock
log file = /var/log/radosgw/client.radosgw.gateway.log
rgw print continue = false
  1. 创建 RadosGW 运行目录
$ sudo mkdir -p /var/lib/ceph/radosgw/ceph-radosgw.gateway
$ sudo chown apache:apache /var/log/radosgw/client.radosgw.gateway.log
  1. 启动 RadosGW
$ /etc/init.d/ceph-radosgw start
  1. 将 RadosGW 的配置写入 Apache 中 a) 创建文件 /etc/httpd/conf.d/rgw.conf, 内容为:
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/html
 
ErrorLog /var/log/httpd/rgw_error.log
CustomLog /var/log/httpd/rgw_access.log combined
 
# LogLevel debug
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
SetEnv proxy-nokeepalive 1
ProxyPass / unix:///var/run/ceph/ceph.radosgw.gateway.fastcgi.sock|fcgi://localhost:9000/
</VirtualHost>

b) 重启 Apache

$ sudo systemctl restart httpd

测试

创建用户

为了完成测试,分别创建一个用于 S3 和 Swift 的用户:

  1. 创建 S3 用户:
$ sudo radosgw-admin user create --uid="testuser" --display-name="First User"
{ "user_id": "testuser",
  ... ...
  "keys": [
        { "user": "testuser",
          "access_key": "EZU2MX4CJCIZILATWQSK",
          "secret_key": "4z18K+f7MZQop2Z99PxN2KKGSX9rb6KBd9ioW0D\/"},
  ... ...
  "temp_url_keys": []}
  1. 创建 Swift 用户: a) 创建 Swift 用户
$ sudo radosgw-admin subuser create --uid=testuser --subuser=testuser:swift
{ "user_id": "testuser",
  ... ...
  "keys": [
        { "user": "testuser",
          "access_key": "EZU2MX4CJCIZILATWQSK",
          "secret_key": "4z18K+f7MZQop2Z99PxN2KKGSX9rb6KBd9ioW0D\/"},
        { "user": "testuser:swift",
          "access_key": "SI7W1FUI43H3I3ED9CWX",
          "secret_key": ""}],
  ... ...
  "temp_url_keys": []}

b) 设置 Swift 用户的密钥:

$ sudo radosgw-admin subuser create --uid=testuser --subuser=testuser:swift
{ "user_id": "testuser",
  ... ...
  "keys": [
        { "user": "testuser",
          "access_key": "EZU2MX4CJCIZILATWQSK",
          "secret_key": "4z18K+f7MZQop2Z99PxN2KKGSX9rb6KBd9ioW0D\/"},
        { "user": "testuser:swift",
          "access_key": "SI7W1FUI43H3I3ED9CWX",
          "secret_key": ""}],
  "swift_keys": [
        { "user": "testuser:swift",
          "secret_key": "yTXiN+2y1Uf6j+CXioZwhqzwCPhOgqVblm2iShj+"}],
  ... ...
  "temp_url_keys": []}

测试

下面的例子使用 s3cmd 测试用 S3 接口连接使用 RadosGW:

NOTE: s3cmd的安装配置请参考 s3cmd 的相关文档

$ s3cmd mb s3://my-bucket # 创建一个 bucket
Bucket 's3://my-bucket/' created
$ s3cmd ls
2015-08-03 10:11  s3://my-bucket
$ cat test.sh 
source openrc
python foo.py $@
$ s3cmd put test.sh s3://my-bucket/my-dir/test.sh # 将 test.sh 上传到 my-bucket/my-dir 下
test.sh -> s3://my-bucket/my-dir/test.sh  [1 of 1]
 31 of 31   100% in    0s   196.90 B/s  done
$ s3cmd ls s3://my-bucket # 查看目录
                       DIR   s3://my-bucket/my-dir/
$ s3cmd get s3://my-bucket/my-dir/test.sh test1.sh # 下载 test.sh 这个对象
s3://my-bucket/my-dir/test.sh -> test1.sh  [1 of 1]
s3://my-bucket/my-dir/test.sh -> test1.sh  [1 of 1]
 31 of 31   100% in    0s     5.83 kB/s  done
$ cat test1.sh
source openrc
python foo.py $@

分类: Storage 标签: ,

Cinder 的私有卷类型功能的使用

2015年7月9日 没有评论

数据库升级

如果是在之前部署的 Juno 上升级的,通过 backport 改功能来实现的,就使用下列指令 升级一下数据库。

[root@localhost ~]# cinder-manage db sync

使用

下面以一个例子来说明一下怎么使用该功能. 使用的后端存储是 lvm 和 ceph 集群里面的 pool。

基本配置

用户 租户 卷类型 后端卷类型
demo1 tenant1 volume_type1 rbd1
demo2 tenant2 volume_type2 rbd2

创建租户

为了比较,这里创建两个租户 demo1 和 demo2 和两个用户 demo1(属于 tenant1), demo2(属于 tenant2):

创建两个租户 tenant1 和 tenant2:

[root@localhost ~]# keystone tenant-create --name tenant1
+-------------+----------------------------------+
|   Property  |              Value               |
+-------------+----------------------------------+
| description |                                  |
|   enabled   |               True               |
|      id     | 38132b7bf32b434398862971c567bca3 |
|     name    |             tenant1              |
+-------------+----------------------------------+
[root@localhost ~]# keystone tenant-create --name tenant2
+-------------+----------------------------------+
|   Property  |              Value               |
+-------------+----------------------------------+
| description |                                  |
|   enabled   |               True               |
|      id     | 3fb70cdab54c4322a277f9acaa360e71 |
|     name    |             tenant2              |
+-------------+----------------------------------+

再分别创建两个用户 demo1 和 demo2:

[root@localhost ~]# keystone user-create --name demo1 --tenant tenant1 --pass demo1
+----------+----------------------------------+
| Property |              Value               |
+----------+----------------------------------+
|  email   |                                  |
| enabled  |               True               |
|    id    | 2e69d91577e949509ce20069786526c5 |
|   name   |              demo1               |
| tenantId | 38132b7bf32b434398862971c567bca3 |
| username |              demo1               |
+----------+----------------------------------+
[root@localhost ~]# keystone user-create --name demo2 --tenant tenant2 --pass demo2
+----------+----------------------------------+
| Property |              Value               |
+----------+----------------------------------+
|  email   |                                  |
| enabled  |               True               |
|    id    | 7687d7a33e49464cb7cedfbf20e5781c |
|   name   |              demo2               |
| tenantId | 3fb70cdab54c4322a277f9acaa360e71 |
| username |              demo2               |
+----------+----------------------------------+

创建 rbd pool

创建 pool rbd1, rbd2

[root@localhost ~]# ceph osd pool create rbd1 128
pool 'rbd1' created
[root@localhost ~]# ceph osd pool create rbd2 128
pool 'rbd2' created

查看创建的 pool

[root@localhost ~]# rados lspools
data
metadata
rbd
images
rbd1
rbd2

增加 cinder 的卷后端

编辑 cinder 的配置文件(一般为 /etc/cinder/cinder.conf):

enabled_backends=lvm,rbd2,rbd1
[rbd1]
volume_backend_name=rbd1
volume_driver=cinder.volume.drivers.rbd.RBDDriver
rbd_pool=rbd1
rbd_max_clone_depth=5
rbd_user=admin
rbd_flatten_volume_from_snapshot=False
rbd_ceph_conf=/etc/ceph/ceph.conf
rbd_secret_uuid=7d65f135-d7ba-4d87-a083-910ff8cf4eb2
 
[rbd2]
volume_backend_name=rbd2
volume_driver=cinder.volume.drivers.rbd.RBDDriver
rbd_pool=rbd2
rbd_max_clone_depth=5
rbd_user=admin
rbd_flatten_volume_from_snapshot=False
rbd_ceph_conf=/etc/ceph/ceph.conf
rbd_secret_uuid=7d65f135-d7ba-4d87-a083-910ff8cf4eb2

重启 cinder 服务

[root@localhost ~]# systemctl restart openstack-cinder-scheduler
[root@localhost ~]# systemctl restart openstack-cinder-volume
[root@localhost ~]# systemctl restart openstack-cinder-api

创建私有卷类型

注意使用的 cinderclient 必须是 V2 版本,如果是 RestAPI,也必须访问 V2 的cinder,可以使用下列指令设置命令行的版本号:

export OS_VOLUME_API_VERSION=2

创建两种私有的卷类型: volume_type1, volume_type

[root@localhost ~]# cinder type-create volume_type1 --is-public false
+--------------------------------------+--------------+-------------+-----------+
|                  ID                  |     Name     | Description | Is_Public |
+--------------------------------------+--------------+-------------+-----------+
| 71fa330c-5401-47ea-9e2d-96466ec7d3bd | volume_type1 |             |   False   |
+--------------------------------------+--------------+-------------+-----------+
[root@localhost ~]# cinder type-create volume_type2 --is-public false
+--------------------------------------+--------------+-------------+-----------+
|                  ID                  |     Name     | Description | Is_Public |
+--------------------------------------+--------------+-------------+-----------+
| f4030afb-3300-47c0-97e4-debfcacbcf82 | volume_type2 |             |   False   |
+--------------------------------------+--------------+-------------+-----------+
[root@localhost ~]# cinder type-list --all
+--------------------------------------+--------------+-------------+-----------+
|                  ID                  |     Name     | Description | Is_Public |
+--------------------------------------+--------------+-------------+-----------+
| 71fa330c-5401-47ea-9e2d-96466ec7d3bd | volume_type1 |             |   False   |
| f4030afb-3300-47c0-97e4-debfcacbcf82 | volume_type2 |             |   False   |
+--------------------------------------+--------------+-------------+-----------+

将 volume_type1 和 volume_type2 的后端存储分别设置为 rbd1 和 rbd2:

注意: 查看卷的类型只能需要加 –all 参数

[root@localhost ~]# cinder type-key 71fa330c-5401-47ea-9e2d-96466ec7d3bd set volume_backend_name=rbd1
[root@localhost ~]# cinder type-key f4030afb-3300-47c0-97e4-debfcacbcf82 set volume_backend_name=rbd2
[root@localhost ~]# . keystonerc_admin; cinder type-list --all
+--------------------------------------+--------------+-------------+-----------+
|                  ID                  |     Name     | Description | Is_Public |
+--------------------------------------+--------------+-------------+-----------+
| 71fa330c-5401-47ea-9e2d-96466ec7d3bd | volume_type1 |             |   False   |
| f4030afb-3300-47c0-97e4-debfcacbcf82 | volume_type2 |             |   False   |
+--------------------------------------+--------------+-------------+-----------+

将卷类型指定给租户

将卷 volume_type1 和 volume_type2 分别指定给 tenant1 和 tenant2, 注意这里 只能使用 UUID,不能使用名字

[root@localhost ~]# cinder type-access-add --volume-type 71fa330c-5401-47ea-9e2d-96466ec7d3bd --project-id 38132b7bf32b434398862971c567bca3
[root@localhost ~]# cinder type-access-add --volume-type f4030afb-3300-47c0-97e4-debfcacbcf82 --project-id 3fb70cdab54c4322a277f9acaa360e71
[root@localhost ~]# cinder type-access-list --volume-type 71fa330c-5401-47ea-9e2d-96466ec7d3bd
+--------------------------------------+----------------------------------+
|            Volume_type_ID            |            Project_ID            |
+--------------------------------------+----------------------------------+
| 71fa330c-5401-47ea-9e2d-96466ec7d3bd | 38132b7bf32b434398862971c567bca3 |
+--------------------------------------+----------------------------------+
[root@localhost ~]# cinder type-access-list --volume-type f4030afb-3300-47c0-97e4-debfcacbcf82
+--------------------------------------+----------------------------------+
|            Volume_type_ID            |            Project_ID            |
+--------------------------------------+----------------------------------+
| f4030afb-3300-47c0-97e4-debfcacbcf82 | 3fb70cdab54c4322a277f9acaa360e71 |
+--------------------------------------+----------------------------------+

创建卷

使用 demo1 创建类型为 volume_type1 的卷

[root@localhost ~]# . keystonerc_demo1; cinder create 1 --volume-type volume_type1
+---------------------------------------+--------------------------------------+
|                Property               |                Value                 |
+---------------------------------------+--------------------------------------+
|           availability_zone           |                 nova                 |
|                   id                  | 4e4fad1f-26aa-4690-9268-6628d01138e8 |
|                  name                 |                 None                 |
|      os-vol-tenant-attr:tenant_id     |   38132b7bf32b434398862971c567bca3   |
|                  size                 |                  1                   |
|                 status                |               creating               |
|                user_id                |   2e69d91577e949509ce20069786526c5   |
|              volume_type              |             volume_type1             |
+---------------------------------------+--------------------------------------+
[root@localhost ~]# rbd -p rbd1 ls
volume-4e4fad1f-26aa-4690-9268-6628d01138e8

使用 demo2 创建类型为 volume_type2 的卷

[root@localhost ~]# . keystonerc_demo2; cinder create 1 --volume-type volume_type2
+---------------------------------------+--------------------------------------+
|                Property               |                Value                 |
+---------------------------------------+--------------------------------------+
|           availability_zone           |                 nova                 |
|                   id                  | 3366b9d9-7437-45d1-bbef-bc59127785e6 |
|                  name                 |                 None                 |
|      os-vol-tenant-attr:tenant_id     |   3fb70cdab54c4322a277f9acaa360e71   |
|                 status                |               creating               |
|                user_id                |   7687d7a33e49464cb7cedfbf20e5781c   |
|              volume_type              |             volume_type2             |
+---------------------------------------+--------------------------------------+
[root@localhost ~]# rbd -p rbd2 ls
volume-3366b9d9-7437-45d1-bbef-bc59127785e6

分别使用 demo1, demo2 创建默认类型的卷

[root@localhost ~]# . keystonerc_demo1; cinder create 1
+---------------------------------------+--------------------------------------+
|                Property               |                Value                 |
+---------------------------------------+--------------------------------------+
|           availability_zone           |                 nova                 |
|                   id                  | 6d2811e9-3ca6-4993-801a-ff59783ef891 |
|                  name                 |                 None                 |
|      os-vol-tenant-attr:tenant_id     |   38132b7bf32b434398862971c567bca3   |
|                  size                 |                  1                   |
|                 status                |               creating               |
|                user_id                |   2e69d91577e949509ce20069786526c5   |
|              volume_type              |             volume_type1             |
+---------------------------------------+--------------------------------------+
[root@localhost ~]# rbd -p rbd1 ls
volume-4e4fad1f-26aa-4690-9268-6628d01138e8
volume-6d2811e9-3ca6-4993-801a-ff59783ef891
[root@localhost ~]# . keystonerc_demo2; cinder create 1
+---------------------------------------+--------------------------------------+
|                Property               |                Value                 |
+---------------------------------------+--------------------------------------+
|           availability_zone           |                 nova                 |
|                   id                  | ae3466e2-3ed3-4b6a-be9b-0dd485f99da0 |
|                  name                 |                 None                 |
|      os-vol-tenant-attr:tenant_id     |   3fb70cdab54c4322a277f9acaa360e71   |
|                  size                 |                  1                   |
|                 status                |               creating               |
|                user_id                |   7687d7a33e49464cb7cedfbf20e5781c   |
|              volume_type              |             volume_type2             |
+---------------------------------------+--------------------------------------+
[root@localhost ~]# rbd -p rbd2 ls
volume-3366b9d9-7437-45d1-bbef-bc59127785e6
volume-ae3466e2-3ed3-4b6a-be9b-0dd485f99da0

测试分别使用 demo1 创建 volume_type2, 使用 demo2 创建 volume_type1 的卷

[root@localhost ~]# . keystonerc_demo1; cinder create 1 --volume-type volume_type2
ERROR: Not Found (HTTP 404) (Request-ID: req-c749eb4e-d4dd-4bfb-aa62-26f8b8a4f986)
[root@localhost ~]# . keystonerc_demo2; cinder create 1 --volume-type volume_type1
ERROR: Not Found (HTTP 404) (Request-ID: req-255b8249-f260-4b36-a2c0-0a5aa28e74e3)

查看卷类型

可以看到,除了管理员用户,用户只能看到各自租户的卷类型,不能看到其他租户的卷类型

[root@localhost ~]# . keystonerc_demo1; cinder type-list
+--------------------------------------+--------------+-------------+-----------+
|                  ID                  |     Name     | Description | Is_Public |
+--------------------------------------+--------------+-------------+-----------+
| 71fa330c-5401-47ea-9e2d-96466ec7d3bd | volume_type1 |             |   False   |
+--------------------------------------+--------------+-------------+-----------+
[root@localhost ~]# . keystonerc_demo2; cinder type-list
+--------------------------------------+--------------+-------------+-----------+
|                  ID                  |     Name     | Description | Is_Public |
+--------------------------------------+--------------+-------------+-----------+
| f4030afb-3300-47c0-97e4-debfcacbcf82 | volume_type2 |             |   False   |
+--------------------------------------+--------------+-------------+-----------+

查看卷

可以看到,除了管理员用户,用户只能看到各自租户的卷,不能看到其他租户的卷

[root@localhost ~]# . keystonerc_demo1; cinder list
+--------------------------------------+-----------+------+------+--------------+----------+-------------+
|                  ID                  |   Status  | Name | Size | Volume Type  | Bootable | Attached to |
+--------------------------------------+-----------+------+------+--------------+----------+-------------+
| 4e4fad1f-26aa-4690-9268-6628d01138e8 | available | None |  1   | volume_type1 |  false   |             |
| 6d2811e9-3ca6-4993-801a-ff59783ef891 | available | None |  1   | volume_type1 |  false   |             |
+--------------------------------------+-----------+------+------+--------------+----------+-------------+
[root@localhost ~]# . keystonerc_demo2; cinder list
+--------------------------------------+-----------+------+------+--------------+----------+-------------+
|                  ID                  |   Status  | Name | Size | Volume Type  | Bootable | Attached to |
+--------------------------------------+-----------+------+------+--------------+----------+-------------+
| 3366b9d9-7437-45d1-bbef-bc59127785e6 | available | None |  1   | volume_type2 |  false   |             |
| ae3466e2-3ed3-4b6a-be9b-0dd485f99da0 | available | None |  1   | volume_type2 |  false   |             |
+--------------------------------------+-----------+------+------+--------------+----------+-------------+

分类: OpenStack 标签:

Cinder Private Volume Type(租户卷类型) 在大规模部署中带来的好处

2015年7月9日 没有评论

问题的提出

在 OpenStack 部署中,一般将不同存储类型,不同性能规格的存储资源形成一个大的集群,对外提供云硬盘的服务,这个大的集群统一由 cinder 来管理。但是在实践中,往往会遇到一些问题。

我们知道所有的计算节点可以按照 Zone 来划分,把计算机点按机房(机柜)的位置纳入到一个 Zone 中,如果其中一个机房(机柜)因为某种原因出问题了,不会影响其它机房/柜的虚拟机和网络。 而且, 同属于一个 Zone 的云主机在发生租户内部的流量的时候,就不会超出这个 Zone 的网络,租户网络的性能得到最大的利用和提升。

不过对于存储的访问,如果没有类似于这种隔离的技术,就会造成存储网络不可控,存储流量在各个存储交换机之间乱跑。

另一方面,根据不同的要求往往划分为多种不同的类型的云硬盘:例如:

  • 根据不同的后端存储
  • 创建不同性能的云硬盘
  • 为了开发新技术,创建一些测试类型的存储卷

但是所有的存储类型对租户都是可见的,用户创建磁盘的时候可以任意选择云硬盘类型,哪怕这些类型并不希望被某些用户使用。更糟糕的是,用户创建基于新的云硬盘启动的云主机的时候,这个新的云硬盘的类型是随机的!

情况在 Kilo 版本带来的名为 Private Type(私有卷) 的功能得到改善。该功能允许创建租户私有的云硬盘类型,这些类型的云硬盘指定为只能被某些租户访问。

解决方式

综上,对之前提到的两个问题,就有了比较好的解决方案。下面以使用一个 Ceph 作为存储后端为例说明:

  • 首先,从物理层面上,将一个 Zone 内的计算节点的存储网络和适当的 Ceph 节点的存储网络放在同一个网络设备下(比如同一个交换机)。
  • 然后,创建一个 Ceph 集群的资源池,指定资源池包含上面的 Ceph 节点。
  • 最后,根据上面的资源池创建对应的的云硬盘类型,并设置改类型为私有的,将它的访问权限赋予适当的租户。

至此,没有被赋予访问权限的租户将看不到这些存储。完成了实际上的存储隔离。

需要注意的地方

注意:即使是 Kilo 版本的 OpenStack,也没有完善该功能, 需要对创建云硬盘部分进行改动,确保在 nova 创建基于云硬盘启动的云主机的时候,只能创建具有权限的云主机。

参考

具体改功能的用法请参考 这里 的干货

分类: OpenStack 标签: