QEMU+librbd调试环境搭建

本次环境搭建在云主机上完成。如在物理机上搭建,可能需要修改虚拟机xml配置文件。

前提

ceph环境已搭建完毕,并可以创建rbd卷。

安装qemu及libvirt

 

apt-get update
apt-get install qemu qemu-block-extra libvirt-daemon-system libvirt-daemon libvirt-clients

 

其中qemu-block-extra包是为了给qemu提供rbd协议存储后端扩展支持。

下载虚拟机镜像

wget https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img  ## 这个是最精简的虚拟机镜像,只有14M

 

创建虚拟机使用的rbd卷(虚拟机系统盘)

qemu-img convert -f qcow2 -O raw cirros-0.4.0-x86_64-disk.img rbd:rbd/vol1
# 注意这条命令会在转换镜像格式的同时在ceph rbd池里创建rbd卷vol1,因此vol1不能已存在
# 如有需要可以resize扩容rbd卷,扩容卷之后还需要扩容文件系统,建议单独挂载一块rbd卷到虚拟机用于测试

 

准备虚拟机xml配置文件

保存成xxx.xml,比如libvirt.xml:

<domain type="qemu">  <!-- 注意这里的type,如果是物理机上启动虚拟机,需要改为kvm  -->
  <uuid>5d1289be-50e1-47b7-86de-1de0ff16a9d4</uuid>  <!-- 虚拟机uuid  -->
  <name>ceph</name>    <!-- 虚拟机名称  -->
  <memory>524288</memory>   <!-- 虚拟机内存大小,这里是配置的512M  -->
  <vcpu>1</vcpu>   <!-- 虚拟机CPU数量  -->
  <os>
    <type>hvm</type>
    <boot dev="hd"/>
  </os>
  <features>
    <acpi/>
    <apic/>
  </features>
  <clock offset="utc">
    <timer name="pit" tickpolicy="delay"/>
    <timer name="rtc" tickpolicy="catchup"/>
  </clock>
  <cpu mode="host-model" match="exact"/>
  <devices>       <!-- 虚拟机磁盘配置,一般vda是系统盘  -->
      <disk type="network" device="disk">
      <driver type="raw" cache="none"/>
      <source protocol="rbd" name="rbd/vol1">      <!-- 一般需要修改name,也就是$pool/$volume  -->
        <host name="192.168.0.2" port="6789"/>        <!-- mon地址  -->
      </source>
      <target bus="virtio" dev="vda"/>         <!-- 虚拟机内设备  -->
    </disk>
    <serial type="file">
      <source path="/var/log/libvirt/qemu/ceph-console.log"/>        <!-- 把虚拟机控制台输出到文件,可选  -->
    </serial>
    <serial type="pty"/>
    <input type="tablet" bus="usb"/>
    <graphics type="vnc" autoport="yes" keymap="en-us" listen="0.0.0.0"/>      <!-- 虚拟机VNC监听地址,一般不需要修改  -->
  </devices>
</domain>

 

之后执行virsh define libvirt.xml(定义虚拟机并持久化虚拟机配置到libvirt),virsh list

–all (查看所有状态虚拟机,包含关机状态),virsh start ceph(启动虚拟机),virsh destroy/shutdown ceph(强制/正常关机),virsh undefine ceph(清理虚拟机)。

 

挂载卷到虚拟机上

准备挂载卷的xml配置,基本上就是从虚拟机配置里面摘出来的磁盘配置:

<disk type="network" device="disk">
  <driver type="raw" cache="none"/>
  <source protocol="rbd" name="rbd/vol2">   <!-- 卷名称要改下 -->
    <host name="192.168.0.2" port="6789"/>
  </source>
  <target bus="virtio" dev="vdb"/>   <!-- 主要是这里的虚拟机设备要改下,不能是虚拟机xml配置文件里面已有的 -->
</disk>

 

之后执行virsh attach-device ceph vdb.xml,虚拟机里面sudo fdisk -l即可看到,注意,virsh destroy之后再start虚拟机,动态挂载的卷会消失,可以在attach-device命令后加上–config参数进行持久化,或者直接把这段xml放到libvirt.xml里面(<devices></devices>段里面即可)再启动虚拟机。

卸载磁盘设备执行virsh detach-device ceph vdb.xml即可。

 

ceph-client socket

如果你在ceph.conf里面配置了admin_socket,并且相关目录的权限也放开(虚拟机对应的qemu进程是libvirt-qemu用户组),

[client]
admin_socket = /var/run/ceph/$cluster-$type.$id.$pid.$cctid.asok
log_file = /var/log/ceph/qemu/qemu-guest-$pid.log

那么你就可以看到相应的ceph-client socket生成。

$ ll /var/run/ceph/
total 0
srwxrwxr-x 1 libvirt-qemu libvirt-qemu 0 Aug 21 11:30 ceph-client.admin.70781.94809655383520.asok
srwxrwxrwx 1 ceph         ceph         0 Aug 17 13:51 ceph-mgr.ceph1.asok
srwxrwxrwx 1 ceph         ceph         0 Aug 17 16:06 ceph-mon.ceph1.asok
srwxrwxrwx 1 ceph         ceph         0 Aug 17 13:51 ceph-osd.0.asok
srwxrwxrwx 1 ceph         ceph         0 Aug 17 13:51 ceph-osd.1.asok
srwxrwxrwx 1 ceph         ceph         0 Aug 17 13:51 ceph-osd.2.asok
$ ceph --admin-daemon /var/run/ceph/ceph-client.admin.70781.94809655383520.asok perf dump | grep librbd-fad56b8b4567-rbd-vol1 -A20                   
    "librbd-fad56b8b4567-rbd-vol1": {
        "rd": 1227,
        "rd_bytes": 25809408,
        "rd_latency": {
            "avgcount": 1227,
            "sum": 3.044197946,
            "avgtime": 0.002481008
        },
        "wr": 65,
        "wr_bytes": 159744,
        "wr_latency": {
            "avgcount": 65,
            "sum": 40.068453646,
            "avgtime": 0.616437748
        },
        "discard": 0,
        "discard_bytes": 0,
        "discard_latency": {
            "avgcount": 0,
            "sum": 0.000000000,
            "avgtime": 0.000000000

 

通过qemu调试librbd

注意:需要先手工编译安装debug版本librbd,qemu使用上面提到的apt-get方式安装,未编译,因此调试时看不到相应源码,如有需要可以自行编译,或者安装debug包。

虚拟机启动后,可以用gdb调试qemu进程,qemu进程通过调用librbd.so来进行rbd卷的IO读写:

## 首先找到qemu进程pid
ps -ef | grep qemu | grep mon_host
libvirt+   70781       1  6 11:30 ?        00:15:18 /usr/bin/qemu-system-x86_64 -name guest=ceph,debug-threads=on -S -object secret,id=masterKey0,format=raw,file=/var/lib/libvirt/qemu/domain-6-ceph/master-key.aes -machine pc-i440fx-2.8,accel=tcg,usb=off,dump-guest-core=off -cpu Broadwell,+vme,+ss,+osxsave,+f16c,+rdrand,+hypervisor,+arat,+tsc_adjust,+xsaveopt,+pdpe1gb,+abm -m 512 -realtime mlock=off -smp 1,sockets=1,cores=1,threads=1 -uuid 5d1289be-50e1-47b7-86de-1de0ff16a9d4 -no-user-config -nodefaults -chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-6-ceph/monitor.sock,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc,driftfix=slew -global kvm-pit.lost_tick_policy=delay -no-shutdown -boot strict=on -device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2 -drive file=rbd:rbd/vol1:auth_supported=none:mon_host=192.168.0.2\:6789,format=raw,if=none,id=drive-virtio-disk0,cache=none -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x3,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1 -add-fd set=0,fd=27 -chardev file,id=charserial0,path=/dev/fdset/0,append=on -device isa-serial,chardev=charserial0,id=serial0 -chardev pty,id=charserial1 -device isa-serial,chardev=charserial1,id=serial1 -device usb-tablet,id=input0,bus=usb.0,port=1 -vnc 0.0.0.0:0 -k en-us -device cirrus-vga,id=video0,bus=pci.0,addr=0x2 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x4 -msg timestamp=on
 
## 之后用gdb挂载到pid
$ gdb -p 70781
GNU gdb (Debian 7.12-6) 7.12.0.20161007-git
......
Attaching to process 70781
0x00007f1cb4f51741 in __GI_ppoll (fds=0x563a98ec8690, nfds=8, timeout=<optimized out>, sigmask=0x0) at ../sysdeps/unix/sysv/linux/ppoll.c:39
39      ../sysdeps/unix/sysv/linux/ppoll.c: No such file or directory.
(gdb)b librbd::io::ImageRequest<librbd::ImageCtx>::create_write_request     ### 添加librbd断点
(gdb) c
Continuing.       #### 虚拟机里面执行IO操作
[Switching to Thread 0x7f1c84ff9700 (LWP 70800)]
Thread 20 "CPU 0/TCG" hit Breakpoint 3, librbd::io::ImageRequest<librbd::ImageCtx>::create_write_request(librbd::ImageCtx&, librbd::io::AioCompletion*, std::vector<std::pair<unsigned long, unsigned long>, std::allocator<std::pair<unsigned long, unsigned long> > >&&, ceph::buffer::list&&, int, ZTracer::Trace const&) (
    image_ctx=..., aio_comp=aio_comp@entry=0x7f1c417c9400,
    image_extents=image_extents@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71f9f>,
    bl=bl@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71fb7>, op_flags=op_flags@entry=0, parent_trace=...)
    at /mnt/ceph/src/librbd/io/ImageRequest.cc:96
96      ImageRequest<I>* ImageRequest<I>::create_write_request(
(gdb) bt
#0  librbd::io::ImageRequest<librbd::ImageCtx>::create_write_request(librbd::ImageCtx&, librbd::io::AioCompletion*, std::vector<std::pair<unsigned long, unsigned long>, std::allocator<std::pair<unsigned long, unsigned long> > >&&, ceph::buffer::list&&, int, ZTracer::Trace const&) (image_ctx=...,
    aio_comp=aio_comp@entry=0x7f1c417c9400, image_extents=image_extents@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71f9f>,
    bl=bl@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71fb7>, op_flags=op_flags@entry=0, parent_trace=...)
    at /mnt/ceph/src/librbd/io/ImageRequest.cc:96
#1  0x00007f1ca1318f59 in librbd::io::ImageRequestWQ<librbd::ImageCtx>::aio_write(librbd::io::AioCompletion*, unsigned long, unsigned long, ceph::buffer::list&&, int, bool) (this=0x563a97ee5090, c=0x7f1c417c9400, off=off@entry=43303936, len=len@entry=1024,
    bl=bl@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1dbf9f7, DIE 0x1e87755>, op_flags=op_flags@entry=0, native_async=true)
    at /mnt/ceph/src/librbd/io/ImageRequestWQ.cc:264
#2  0x00007f1ca1228310 in rbd_aio_write (image=<optimized out>, off=43303936, len=1024, buf=<optimized out>, c=<optimized out>)
    at /mnt/ceph/src/librbd/librbd.cc:3536
#3  0x00007f1ca172a33a in ?? () from /usr/lib/x86_64-linux-gnu/qemu/block-rbd.so
#4  0x00007f1ca172a426 in ?? () from /usr/lib/x86_64-linux-gnu/qemu/block-rbd.so
#5  0x0000563a965aac3c in ?? ()
#6  0x0000563a965abed0 in ?? ()
#7  0x0000563a965acba7 in bdrv_co_pwritev ()
#8  0x0000563a9656e469 in ?? ()
#9  0x0000563a965aab21 in ?? ()
#10 0x0000563a965abed0 in ?? ()
#11 0x0000563a965acba7 in bdrv_co_pwritev ()
#12 0x0000563a9659e90d in blk_co_pwritev ()
#13 0x0000563a9659ea2b in ?? ()
#14 0x0000563a9661752a in ?? ()
#15 0x00007f1cb4eb6000 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#16 0x00007ffda22a0ff0 in ?? ()
#17 0x0000000000000000 in ?? ()
(gdb) l
91                                       std::move(read_result), op_flags,
92                                       parent_trace);
93      }
94
95      template <typename I>
96      ImageRequest<I>* ImageRequest<I>::create_write_request(
97          I &image_ctx, AioCompletion *aio_comp, Extents &&image_extents,
98          bufferlist &&bl, int op_flags, const ZTracer::Trace &parent_trace) {
99        return new ImageWriteRequest<I>(image_ctx, aio_comp, std::move(image_extents),
100                                       std::move(bl), op_flags, parent_trace);
(gdb) l
101     }
102
103     template <typename I>
104     ImageRequest<I>* ImageRequest<I>::create_discard_request(
105         I &image_ctx, AioCompletion *aio_comp, uint64_t off, uint64_t len,
106         bool skip_partial_discard, const ZTracer::Trace &parent_trace) {
107       return new ImageDiscardRequest<I>(image_ctx, aio_comp, off, len,
108                                         skip_partial_discard, parent_trace);
109     }
110

 

安装qemu debug symbols

 

### 添加debug symbols源
cat <<EOF | sudo tee /etc/apt/sources.list.d/dbgsym.list
> deb http://debug.mirrors.debian.org/debian-debug/ stretch-debug main
> EOF
deb http://debug.mirrors.debian.org/debian-debug/ stretch-debug main
### 更新源
$ apt update -y
$ apt install qemu-system-x86-dbgsym qemu-block-extra-dbgsym qemu-system-common-dbgsym qemu-utils-dbgsym -y

之后再用gdb调试就可以看到全部的调用栈信息及源码位置了:

Thread 37 "CPU 0/TCG" hit Breakpoint 1, librbd::io::ImageRequest<librbd::ImageCtx>::create_write_request(librbd::ImageCtx&, librbd::io::AioCompletion*, std::vector<std::pair<unsigned long, unsigned long>, std::allocator<std::pair<unsigned long, unsigned long> > >&&, ceph::buffer::list&&, int, ZTracer::Trace const&) (
    image_ctx=..., aio_comp=aio_comp@entry=0x7facc4278200,
    image_extents=image_extents@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71f9f>,
    bl=bl@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71fb7>, op_flags=op_flags@entry=0, parent_trace=...)
    at /mnt/ceph/src/librbd/io/ImageRequest.cc:96
96      ImageRequest<I>* ImageRequest<I>::create_write_request(
(gdb) bt
#0  librbd::io::ImageRequest<librbd::ImageCtx>::create_write_request(librbd::ImageCtx&, librbd::io::AioCompletion*, std::vector<std::pair<unsigned long, unsigned long>, std::allocator<std::pair<unsigned long, unsigned long> > >&&, ceph::buffer::list&&, int, ZTracer::Trace const&) (image_ctx=...,
    aio_comp=aio_comp@entry=0x7facc4278200, image_extents=image_extents@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71f9f>,
    bl=bl@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1c9367c, DIE 0x1d71fb7>, op_flags=op_flags@entry=0, parent_trace=...)
    at /mnt/ceph/src/librbd/io/ImageRequest.cc:96
#1  0x00007fad47504f59 in librbd::io::ImageRequestWQ<librbd::ImageCtx>::aio_write(librbd::io::AioCompletion*, unsigned long, unsigned long, ceph::buffer::list&&, int, bool) (this=0x55cbc3898890, c=0x7facc4278200, off=off@entry=17248256, len=len@entry=1024,
    bl=bl@entry=<unknown type in /usr/local/lib/librbd.so.1, CU 0x1dbf9f7, DIE 0x1e87755>, op_flags=op_flags@entry=0, native_async=true)
    at /mnt/ceph/src/librbd/io/ImageRequestWQ.cc:264
#2  0x00007fad47414310 in rbd_aio_write (image=<optimized out>, off=off@entry=17248256, len=len@entry=1024,
    buf=buf@entry=0x7facc4279000 " Opts: (null)\nAug 22 04:20:32 cirros kern.info kernel: [    4.859555] EXT4-fs (sda1): re-mounted. Opts: data=ordered\nAug 22 04:20:32 cirros kern.notice kernel: [    5.703121] random: dd urandom read w"..., c=<optimized out>) at /mnt/ceph/src/librbd/librbd.cc:3536
#3  0x00007fad4791633a in rbd_start_aio (bs=<optimized out>, off=17248256, qiov=<optimized out>, size=1024, cb=<optimized out>, opaque=<optimized out>,
    cmd=RBD_AIO_WRITE) at ./block/rbd.c:697
#4  0x00007fad47916426 in qemu_rbd_aio_writev (bs=<optimized out>, sector_num=<optimized out>, qiov=<optimized out>, nb_sectors=<optimized out>, cb=<optimized out>,
    opaque=<optimized out>) at ./block/rbd.c:746
#5  0x000055cbc23b7c3c in bdrv_driver_pwritev (bs=bs@entry=0x55cbc36c9890, offset=offset@entry=17248256, bytes=bytes@entry=1024, qiov=qiov@entry=0x7facc4277c60,
    flags=flags@entry=0) at ./block/io.c:901
#6  0x000055cbc23b8ed0 in bdrv_aligned_pwritev (bs=bs@entry=0x55cbc36c9890, req=req@entry=0x7facf82fbbc0, offset=offset@entry=17248256, bytes=bytes@entry=1024,
    align=align@entry=512, qiov=qiov@entry=0x7facc4277c60, flags=0) at ./block/io.c:1360
#7  0x000055cbc23b9ba7 in bdrv_co_pwritev (child=<optimized out>, offset=<optimized out>, offset@entry=17248256, bytes=bytes@entry=1024,
    qiov=qiov@entry=0x7facc4277c60, flags=flags@entry=0) at ./block/io.c:1610
#8  0x000055cbc237b469 in raw_co_pwritev (bs=0x55cbc36c35e0, offset=17248256, bytes=1024, qiov=<optimized out>, flags=<optimized out>) at ./block/raw_bsd.c:243
#9  0x000055cbc23b7b21 in bdrv_driver_pwritev (bs=bs@entry=0x55cbc36c35e0, offset=offset@entry=17248256, bytes=bytes@entry=1024, qiov=qiov@entry=0x7facc4277c60,
    flags=flags@entry=0) at ./block/io.c:875
#10 0x000055cbc23b8ed0 in bdrv_aligned_pwritev (bs=bs@entry=0x55cbc36c35e0, req=req@entry=0x7facf82fbe90, offset=offset@entry=17248256, bytes=bytes@entry=1024,
    align=align@entry=1, qiov=qiov@entry=0x7facc4277c60, flags=0) at ./block/io.c:1360
#11 0x000055cbc23b9ba7 in bdrv_co_pwritev (child=<optimized out>, offset=<optimized out>, offset@entry=17248256, bytes=bytes@entry=1024,
    qiov=qiov@entry=0x7facc4277c60, flags=0) at ./block/io.c:1610
#12 0x000055cbc23ab90d in blk_co_pwritev (blk=0x55cbc36bd690, offset=17248256, bytes=1024, qiov=0x7facc4277c60, flags=<optimized out>) at ./block/block-backend.c:848
#13 0x000055cbc23aba2b in blk_aio_write_entry (opaque=0x7facc574eb50) at ./block/block-backend.c:1036
#14 0x000055cbc242452a in coroutine_trampoline (i0=<optimized out>, i1=<optimized out>) at ./util/coroutine-ucontext.c:79
#15 0x00007fad5b0a2000 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#16 0x00007facf9ff98c0 in ?? ()
#17 0x0000000000000000 in ?? ()

 

从gdb启动qemu进程

如果需要用gdb直接启动qemu进程,可以使用qemu命令行方式启动虚拟机:

$ gdb qemu-system-x86_64
GNU gdb (Debian 7.12-6) 7.12.0.20161007-git
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from qemu-system-x86_64...(no debugging symbols found)...done.
(gdb) set args -m 512 -smp 1 -drive format=raw,file=rbd:rbd/vol1:auth_supported=none:mon_host=192.168.0.2\\:6789 -vnc :0
(gdb) b main

 

注意事项:

这里的虚拟机配置没有配置网络(如果需要网络,则需要先配置网桥),如果要连接到虚拟机内部,可以使用VNC客户端连接,下载地址:https://www.realvnc.com/en/connect/download/viewer/

vnc端口号查看:virsh vncdisplay ceph,:0表示5900,VNC客户端比较智能,会自动把0映射到5900,也即你在客户端里面输入:192.168.0.2和192.168.0.2:0和192.168.0.2:5900是一样的效果。

也可以通过ps -ef|grep qemu | grep vnc查看,如-vnc 0.0.0.0:0,表示vnc server监听端口5900。

公有云云主机,由于有安全组限制,需要打开对应的VNC server端口才能连接。

或者通过xshell/secureCRT客户端的端口映射转发来映射过去(secureCRT如下图),之后在VNC客户端输入127.0.0.1:5900即可访问虚拟机。

QEMU/KVM USB设备直通/透传虚拟机

英文应该是叫qemu kvm usb device passthrough,有些人翻译成直通,有些人叫透传,总之就是passthrough。

下面的内容是综合整理了网上的教程和咨询前华为同事的结果,以及自己试验的一些结论。

准备工作:

  1. 确认宿主机BIOS里面打开了VT-d/VT-x/VT-i等所有硬件虚拟化支持开关
  2. 打开Linux操作系统的iommu开关,在grub启动命令行里面配置,Intel CPU和AMD CPU配置参数有区别:Intel CPU: intel_iommu=on;AMD CPU: amd_iommu=on
  3. 重启服务器,检查iommu配置是否生效(dmesg | grep -i iommu,输出“Intel-IOMMU: enabled”表示生效)

grub配置iommu参考资料:https://www.jianshu.com/p/035287ba9acb 《CentOS7 minimal kvm iommu 辅助虚拟化 vt-x (用于pci透传)》

在宿主机上通过lsusb命令获取USB设备信息(yum install usbutils -y安装该工具):

字段信息解释:Bus:001,Device:032,vendor id:0781,product id:5581

libvirt配置修改:

在虚拟机的libvirt xml配置文件的<devices></devices>段内添加如下配置:

vendor id和product id就是lsusb获取的需要直通的usb设备信息,之后启动虚拟机,正常情况下就可以在虚拟机里看到usb设备了。

注意事项:

有些usb设备不在windows设备管理器的“通用串行总线控制器”里面(U盘一般属于这个),比如我试过的USB无线网卡,是属于“网络适配器”,U盾则属于“DVD/CD-ROM驱动器”,而加密狗设备,则属于“人体学输入设备”(我用的加密狗是这个,不同的加密狗可能有区别)。要区分设备类型,可以用lsusb -t命令查看(比如我用的加密狗设备信息如下):

我就是通过Class=Human Interface Device,才想到它属于“人体学输入设备”的。

工行U盾直通效果:

另外nova里面(Mitaka版本)支持PCI设备的直通,但是usb设备好像还不支持,还没仔细研究过代码。

因此先临时用libvirt管理这台虚拟机了。注意如果手工修改了libvirt的xml配置,通过nova对虚拟机做操作,如reboot、stop/start、rebuild、resize等等会重置虚拟机的xml文件,相关usb配置都会丢失。(我是通过复制一份xml,修改掉uuid,让nova管理不了我这台虚拟机来解决的,用的系统盘、虚拟网卡还是nova创建的,nova看到的虚拟机永远是关机状态的就好了,它是用来为我手工管理的虚拟机占坑用的)。

 

OpenStack Trove&manila的网络依赖

这俩项目架构都差不多,

Trove是trove-api(接收用户请求)、 trove-taskmanager(用户管理操作逻辑适配层)、 trove-conductor(通过RPC接收数据库操作) trove-guestagent(运行在虚拟机里面,实际管理数据库实例,具有多种类型数据库driver以驱动不同数据库类型)。

https://docs.openstack.org/trove/latest/install/get_started.html

Manila是manila-api(接收用户请求)、manila-data(处理备份、迁移等数据相关逻辑)、manila-scheduler(调度文件共享服务节点,也即share service节点)、manila-share(文件共享服务节点,提供实际的共享文件服务,可以运行在物理机上或者虚拟机里)。

https://docs.openstack.org/manila/pike/install/get-started-with-shared-file-systems.html

这两个服务有一个共同点,多个子服务同时运行在物理机和虚拟机里面,这种场景下,就得考虑物理网络到虚拟网络的连通性问题,否则服务之间不能互通,肯定没法正常运行。2种服务的解决方案也比较类似,都是通过L2或者L3来打通物理和虚拟网络:

https://wiki.openstack.org/wiki/Manila/Networking

L2方式下,需要使用FLAT网络,所有物理机和虚拟机都在一个2层下,业务和管理数据都在一个平面,性能好,但是不安全,大规模环境下也存在网络广播风暴问题。

L3方式下,也有两种方法,虚拟路由和物理路由。虚拟路由模式下,服务提供节点(数据库的guestagent节点和共享文件服务节点)需要跟物理机上的管理服务互通,以便接收用户管理操作请求,但实际的业务面数据(客户端虚拟机到服务节点虚拟机,如读写数据库、读写共享文件)仍然是走的同一个私有网络(同一个network的subnet)。如果服务节点(提供数据库或共享文件服务的虚拟机)上有2个port,可以配置为1个租户私有网port,用来提供业务面的网络数据服务,另一个配置为service port(也即FLAT模式网络),用来提供跟物理机网络互通的控制面网络数据服务。如果服务节点上只有一个port,那就需要两个虚拟路由器,一个是租户私有网的,一个是服务网络,二者之间要通过一个interface来打通。

https://www.jianshu.com/p/d04f829e3330

http://ju.outofmemory.cn/entry/113174

我们用的是VLAN网络模式,这种模式下需要用到物理路由器,来打通各个VLAN的子网,这样就可以做到物理网络和虚拟网络(分属不同VLAN,可避免广播风暴问题),这种方案在中小规模私有云下,非常稳定可靠,性能也更好,也更接近传统IDC的网络模型,对传统企业的IT运维人员比较友好。

 

Gerrit删除一个review提交记录

举例:我想删除这个review提交,http://10.0.30.120/#/c/3018/,也即gerrit数据库change_id=3018的提交记录

目标效果:gerrit web页面上看不到这个提交,历史记录也没有它,上面的链接也打不开,但是如果是已经merge的提交,则git库中仍然还是有这个commit的。

网上找了好久,自己把数据库里面相关的几个表都清理了(删除了所有change_id=3018的记录),结果gerrit web页面上还是能看到它,只不过打开就报错了,唯一能想到的就是缓存问题了。

最后找到一篇这个:https://stackoverflow.com/questions/29575600/fully-delete-abandoned-commit-from-gerrit-db-and-query

关键是重建gerrit索引这一步: java -jar path/to/gerrit.war reindex -d path/to/gerrit-site-dir

kube-apiserver RestFul API route创建流程分析

看完《kubernetes权威指南》和《Go程序设计语言》两本书之后,终于可以进入实际的代码分析阶段,根据之前熟悉其他开源项目源码(如libvirt、OpenStack等)的经验,首先从接口/API开始分析。

k8s开发环境搭建:使用kubeasz快速搭建k8s集群all-in-one开发测试环境

Golang调试环境搭建:kubernetes源码调试体验

k8s源码版本:v1.10

分析API:GET 127.0.0.1:8080/api/v1/nodes/10.0.90.22(10.0.90.22是我环境中的一个node,8080为kubectl proxy API代理端口)

分析目标:搞清楚用户发送请求到这个API之后kube-apiserver的处理流程

参考资料:http://www.wklken.me/posts/2017/09/23/source-apiserver-04.html(还有前面几篇,这里面讲的是老版本的,有很多代码已经改了,但是主要流程值得参考)

源码流程很绕(至少目前我是这么认为,可能是因为我刚开始看源码),需要多看,多动手调试,看很多遍,调很多遍,整天琢磨它,应该都能看明白。

route注册({path} to {handler})

要搞清楚route注册流程,就必须先把go-restful框架的用法搞明白,官方Readme文档有说明,也有示例代码。这里给出上面提到的参考资料:http://www.wklken.me/posts/2017/09/23/source-apiserver-01.html

我们只需要记住,go-restful框架中route注册需要经过如下几个步骤:

  1. 创建一个container(默认使用default)
  2. 创建一个web service
  3. 创建handler(也就是API的请求处理函数)
  4. 把API path和handler绑定到web service
  5. 把web service(可多个,但root Path不能相同)绑定到container
  6. 启动包含container(可多个)的http server

我这边试验的一个简单的示例代码:

 

我们下面所有的流程都以这个为基础进行分析。

kube-apiserver的main入口:

这里用到了github.com/spf13/cobra这个包来解析启动参数并启动可执行程序。

具体注册流程就不一步一步的分析了,直接根据断点的bt输出跟着代码查看吧:

NewLegacyRESTStorage这个方法(注意是LegacyRESTStorageProvider类型的方法),返回了3个参数,restStorage, apiGroupInfo, nil,最后一个是错误信息可忽略,第一个对我们流程分析没啥影响(应该是),中间这个apiGroupInfo是重点,InstallLegacyAPIGroup就是注册/api这个path的,apiPrefix这个参数是 DefaultLegacyAPIPrefix = "/api" ,apiGroupInfo.PrioritizedVersions目前就”v1″一个版本。

注意这里的getAPIGroupVersion方法,它把apiGroupInfo封装到了apiGroupVersion结构体里面,具体是apiGroupVersion.Storage,下面会用到:

 

paths变量就是上面NewLegacyRESTStorage里restStorageMap map的key,也即”pods”、”nodes”等path。a.registerResourceHandlers()就是注册各个path的handler,也即restStorageMap的value。

下面分析handler具体是什么方法?以restfulGetResource为例:

也即r.Get是最终的实际请求处理函数,它是怎么来的?

r就是getter也就是storage.(rest.Getter),上面提到过参数storage是restStorageMap的value,path是key,针对我们分析的API对应”nodes”: nodeStorage.Node(storage == nodeStorage.Node)。rest.Getter是一个go-restful里面定义的接口,接口也是一种类型,根据Go语言的接口类型定义,接口即约定,可以在任何地方(包)为任何数据类型(int、string、struct等等)实现接口(也即实现接口约定的具有指定参数类型和返回类型的函数),我们看下这个接口的定义:

再看下storage也即nodeStorage.Node有没有实现它,很郁闷,没找到,但是不要灰心,想想看,struct是可以嵌套的(类似父子类的继承关系),并且支持匿名成员,使用匿名成员可以省略中间struct的名称(尤其是匿名成员根本没有嵌套的中间struct变量名可用)(如果没印象了,可以翻看一下《Go程序设计语言》),看看Node的嵌套struct里面有没有实现接口,

对,最终我们在genericregistry.Store这个struct定义的包文件里面找到了它的Get接口实现方法:

下面继续找e.Storage.Get,其实根据接口的定义,我已经找到了真正的后端实现了,k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/storage/etcd3.(*store).Get,为啥是etcd3?需要分析etcd存储后端(k8s元数据存储后端)类型注册(默认是etcd3后端)过程:

fff但是怎么反推回来handler?也就是e.Storage.Get,最简单的办法当然是加断点调试,调试结果在下面的请求处理部分有贴出来,这里就不贴了。但是看了之后还是有疑问,怎么从e.Storage.Get走到etcd3的Get方法的?

NewCacherFromConfig返回的Cache结构体指针,结构体实现了storage.Interface定义的各个接口,

所以e.Storage.Get就是k8s.io/apiserver/pkg/storage/cacher.go:Get方法,它里面又调用了etcd3后端的Get方法:

c.storage==config.Storage由NewRawStorage生成,根据上面的调试结果可以确定它最终调用到k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/storage/etcd3.newStore,所以c.storage.Get调用的就是:

这个Get就是最终处理用户发送的Get API请求的位置。

 

请求处理(HTTP RestFul request to {handler})

这部分就不多说了,直接看调用栈吧:

 

未解决的问题

  • 上面代码分析过程中没搞清楚的两个Go语言知识点,都是关于接口的
  • container到http server,这个应该不复杂,不在本次代码分析的目标之内,就没关注
  • 各种filter注册和使用,同上,没关注
  • etcd部署及使用,etcd在k8s中的使用,这部分属于扩展知识,有时间再看下

跟OpenStack API处理流程的比较

最大的差异当然还是语言上的,python和Go还是有点不太一样的,所以用到的RestFul HTTP Server的框架也不一样,route定义和注册流程也差别比较大,当然还是习惯问题,如果整天看这些代码,用这些框架,也就不会有刚开始看代码时很强烈的差异感了。犹记得当年刚开始看OpenStack nova-api route注册转发流程也是一脸懵逼好久好久。。。

其他方面就是交互流程不一样,当然Get方法差不多,都是接收用户API请求,然后分发到具体的处理方法(route到controller或handler),之后controller或handler从后端数据库(MySQL或etcd)查询用户请求的数据,最后封装返回给用户,再由controller或handler获取请求数据之前还会对用户请求进行多次filter过程,把不合法或不合适的请求过滤掉,只允许合法合适(如未被限流)的请求被controller或handler处理并正常返回用户。

而Create方法,则差异比较大,OpenStack一般是用消息队列进行消息传递,从API服务把具体执行的动作RPC到其他服务(如调度服务、计算节点管理服务等),动作执行过程中或者执行完毕也会通过RPC更新操作状态到数据库(当然最开始是直接访问数据库,后面为了安全改为经过RPC)。而k8s则是完全通过etcd来完成各个组件之间的异步交互,通过watch各自关系的key来实现消息传递和异步调用,操作状态更新也是通过更新etcd来实现。

 

nova、cinder的snapshot与ceph相关交互

其实这两块代码已经看过很多遍了,但是经常忘记,而且经常有同事问起来,每次都要翻一遍代码,因此决定整理下相关交互流程,后续备查。(注:仍然是基于Mitaka版本分析源码,Queens版本粗略看了下改动不大)

先说nova部分,nova的snapshot主要是针对系统盘的(root disk),相关命令行是:

从nova源码来看,backup和image(snapshot)的区别只是多了个rotation的概念,多出的这个概念主要是用来限制备份数量的(超过这个数量的backup会被滚动删除,肯定是先删除最老的),你如果把rotation设置的很大,那它就跟image没什么区别了,nova后端的代码也是一套(api入口不一样,但是到了nova compute manager那层就没什么区别了)

从上面源码可见二者调用的具体实现没有区别。

_snapshot_instance调用了libvirt driver的snapshot方法,这里面区分了live和cold的snapshot类型,并且还区分了direct snapshot和外部快照,ceph后端是用的direct snapshot,也即通过ceph的rbd image相关api来做快照。

可以看出经过了创建临时snapshot(还在nova系统盘的pool)、在glance pool中clone snapshot出新rbd卷(跨pool clone卷)、flatten(clone的新卷与snapshot解除关联)、删除临时快照(清理临时资源)、glance pool中的rbd image创建snapshot,此snapshot就是生成的云主机(虚拟机)系统盘的快照(新的镜像,或者叫自定义镜像、捕获镜像、镜像模板等,总之就是nova image-create生成的东西,可以用glance image-list看到),也就是说glance中的image(不管是管理员上传的image还是nova image-create制作的image,都是snap)对应的是rbd里面的一个snap而不是实际的卷,这样创建新的云主机(虚拟机)的时候,系统盘直接从snap clone一个rbd卷就好了,由于支持COW,因此实际clone过程中数据copy量极少、创建系统盘卷速度特别快(这也是glance镜像在有云主机使用的情况下不能删除的原因)。

rbd snapshot的原理可以参考前同事的一篇文章:http://www.sysnote.org/2016/02/28/ceph-rbd-snap/

direct+live snapshot场景下,创建临时snapshot过程中,由于云主机一直运行中,因此可能有部分数据还在内存的磁盘缓存中,没有刷新到磁盘,所以还是有一定概率导致制作的系统盘快照是损坏的。

上面是ceph后端的流程,本地存储后端的snapshot流程可参考之前的文章:Mitaka Nova在线快照数据丢失问题及解决方法

nova这边其实还有一种需要跟cinder(ceph)交互的功能,boot-from-volume,从卷启动云主机(虚拟机),这种情况下cinder list里面看到的volume是bootable的,不过这种功能在ceph后端场景下不常用,就不介绍了。

接下来是cinder部分,涉及的命令行应该有create、backup-create、snapshot-create这几个(还有没有其它的不确定,估计应该没了):

先看create,创建卷,支持多种参数,比如创建裸卷、从snapshot创建卷、从已有的volume创建卷等。

上面忽略了很多taskflow,直接到了cinder.volume.flows.manager.create_volume.CreateVolumeFromSpecTask#execute,cinder里面用到的taskflow一般都是linear类型的,顺序执行,只要一个一个看过去就行了,一般都包含一个参数解析的task,如cinder.volume.flows.manager.create_volume.ExtractVolumeSpecTask,解析出来的参数传递给下一个task使用,最后run起来,正常执行execute,有异常的话就执行revert方法。关于OpenStack的taskflow介绍:https://docs.openstack.org/taskflow/latest/

创建卷的api文档(v2版本,v3也类似):https://developer.openstack.org/api-ref/block-storage/v2/index.html#create-volume

跟snapshot相关的主要是_create_from_snapshot和_create_from_source_volume,先看第一个:

共3步,从snapshot clone新卷、flatten、resize,后面两步不是必须步骤。配置项rbd_flatten_volume_from_snapshot,

从snapshot创建卷的时候是否flatten,默认是False,不flatten。

再看_create_from_source_volume,它调用的是create_cloned_volume,

这个流程比较多,毕竟要先做一个snapshot,然后再clone新卷,相当于包含了从snapshot创建卷的流程。配置项rbd_max_clone_depth,

默认最大clone深度是5层,达到5层就flatten。

再看下backup操作(其实这个操作跟rbd snapshot没啥大关系),cinder.backup.drivers.ceph.CephBackupDriver#_backup_rbd这里是最终执行的方法,就不具体分析了,主要是有个增量备份过程:

先用’rbd export-diff’导出增量部分,再用’rbd import-diff’导入。参考:https://ceph.com/geen-categorie/incremental-snapshots-with-rbd/

第一次备份的话是走全量备份过程:

 

最后看下snapshot create,这个流程跟create创建卷流程类似,直接看最终rbd调用就行了:

非常简单,就创建snap,然后protect。

 

 

写给儿子们<18>

今天是一个特别的日子,哥哥幼儿园毕业典礼,弟弟一周岁生日

今天是一个不特别的日子,妈妈不在家,弟弟也不在

本应该一家人开心幸福的参加哥哥的毕业典礼,然后再给弟弟过生日,可惜并没有。。。

 

从某种程度上说,是爸爸自己亲手造成这一切后果的,爸爸内心充满愧疚和自责

曾经是爸爸最不愿发生的事情却实实在在的发生了,弟弟成了留守儿童。。。

妈妈说了很多次,哭着说爸爸太狠心,怎么舍得让弟弟一个人留在老家,即如此为何当初还要生下他?爸爸当时还嘴硬,还狡辩,还强词夺理,说弟弟还小,还不懂事,在家呆一段时间没啥问题,过段时间还会回来的

其实爸爸也很难受,但爸爸还能说什么?让弟弟回来谁来带?妈妈不在家,爷爷连哥哥的饭都做不好(他真的已经尽力了),而爸爸又经常加班。。。

一年来,工作上的困难爸爸都预料到了,实际上虽说很多困难、问题,也都一一化解或规避了,虽说进展不很大,但也比预估的不差太多

一年前决定创业的时候,爸爸想法很简单,只要一家人在一起,没什么好担心的

可是现在,一家人不在一起了,爸爸就不能不担心了

但生活上或者家庭方面的问题,已经完全偏离了爸爸的预期,爸爸感觉生活已经有点失控了

可以说,爸爸与奋斗的目标已经南辕北辙,妈妈、哥哥、弟弟过上好日子是爸爸奋斗的目标和动力来源

最近爸爸的耳边时不时的会想起妈妈哭着对爸爸说的那个词:“妻离子散”

妈妈去外地工作,弟弟回了老家,只有哥哥和爸爸还在家里

是的,爸爸目前的生活状态用这个词来形容真的是很贴切,多么可悲。。。

每当想到这个词,爸爸都有一种生无可恋的挫败感,这么努力的工作,却导致这种下场,所有的努力不但化作泡影,反而还结出了恶果,可悲可叹可笑。。。

爸爸最近也在思考,为啥会出现这种状况?爸爸究竟错在哪里?

想到的一个原因,可能是妈妈和爸爸的心态发生了变化,当爸爸决定出来创业的时候,已经想好了要过苦日子,而妈妈则感觉到咱们家的经济基础已经不稳固了,需要她也努力工作,以防家里没有收入来源,所以她去了外地工作,因为那是她看起来最好、最有前途的选择了

如果爸爸工作稳定,爸爸想妈妈可能还不会下这么大的决心把弟弟放老家,离开哥哥和爸爸去外地工作

因此这一切的问题根源,都可能在爸爸去年的那个出来创业决定

而现在,爸爸后悔了

爸爸不是后悔在工作不稳定、公司发展不如意、收入下降等等问题上

而是在家庭问题上,爸爸真的感觉后悔了。。。

爸爸想恢复到一年前的状态,也决定要这么做

钱,爸爸需要,一家人在一起快乐生活,爸爸更需要

而这一年来,你们妈妈流的泪,比之前10年都要多

而这一切的根源,都在爸爸

爸爸还有什么好说的呢?无话可说。。。

所有人的眼光爸爸都不在乎,而妈妈的眼泪,弟弟的哭声,哥哥想妈妈想弟弟的梦呓,爸爸在乎、爸爸不能不在乎、爸爸必须在乎

爸爸一直希望,替你们走的路,不会变成给你们挖的坑

钱,我们赚不来,也不赚了,只希望我们一家人永远不分开,这比什么都重要

希望一切都还来得及,一切都还回的来

 

kubernetes源码调试体验

源码是k8s的release-1.10分支,为啥没用master?因为我的虚拟机里面安装的golang版本是1.9.4的,不满足最新版的要求,也懒得更新了。

编译环境是CentOS 7.2 x86_64。

总的来说,Go的调试与C比较接近,调试工具和调试命令都很像,尤其是都可以用gdb调试,但我这次没有用gdb,而是用了网上说的更适合Go的delve

要调试k8s,首先得编译出来debug版本的二进制程序,正常编译肯定是make all就好了,debug版本的要去掉编译优化选项并打开调试选项,看了下k8s的Makefile:

可以看到 81 # make all GOGCFLAGS="-N -l" 这行和下面的几行注释,提到了-N -l这两个编译选项,分别是指禁用编译优化和禁用内联优化,最终目的是可以在调试代码的时候单步执行可以看到实际对应的每行源码,具体支持的选项列表可以通过如下命令查看:

 

要编译首先得下载源码, git clone https://github.com/kubernetes/kubernetes.git ,之后切到你想要编译的分支,这里以release-1.10为例, git checkout origin/release-1.10 -b release-1.10 ,根据官方文档准备编译环境:https://github.com/kubernetes/community/blob/master/contributors/devel/development.md#building-kubernetes-on-a-local-osshell-environment

我这里选的是Linux本地编译,不是Docker编译。etcd和go的安装就不多说了。接下来就是编译了,命令上面已经说过了 make all GOGCFLAGS="-N -l" ,在kubernetes目录(源码根目录)下执行就好了。然后就是等待编译结束,第一次会比较慢,编译好之后会把编译好的二进制文件放到kubernetes目录下的 _output/bin/ ,对应源码则是在 _output/local/go/src/ ,进入目录可以看出, _output/local/go/src/k8s.io/ 下面的kubernetes目录其实是一个软链接,链接到kubernetes源码根目录。

接下来是调试过程,首先要安装delve,安装也比较简单,我是用的 go get github.com/derekparker/delve/cmd/dlv 直接下载编译的,自动编译好的二进制文件放在 ~/go/bin/ 目录下(也就是是$GOPATH/bin目录),接下来要么你把这个dlv工具copy到系统变量$PATH里任何目录下(比如/usr/local/bin),要么也可以建个软链接过去,或者把它所在的目录也加到系统变量$PATH下,比如在dlv工具所在目录执行: export PATH=$PATH:`pwd` (注意这种方式没有持久化,退出shell窗口之后就失效了,需要再次执行),之后就可以在任何地方运行dlv命令了。

首先看下dlv用法,目前看起来比较常用的是attach、exec、debug三个子命令:

简单调试下kube-apiserver:

这里我没有给kube-apiserver传参数,执行c之后就异常退出了,我们这次调试的目标就是分析为啥退出?首先在入口的main函数处设置断点(可以不设置):

之后根据错误日志”–etcd-servers must be specified”找到报错的代码位置处继续设置断点(注意源文件路径,是相对路径,相对于/root/k8s/kubernetes/_output/local/go/src/目录的,其中kubernetes是git clone的源码根目录):

bt命令可以看到整个调用栈:

这里简单调试下,主要是为后续深入分析k8s代码做准备,有了调试工具,所有代码流程都可以分析清楚,只需要在你关心的代码或者函数哪里加上断点,之后等代码执行过来执行bt命令就能看到整个调用栈了,非常省事省力快速有效,拿到调用栈之后就是一步一步的分析源码了,这样可以保证分析流程不出错(尤其是在大型项目里,很多同名函数、回调函数,很容易绕晕,调用栈基本就相当于是分析代码流程的指南针了)。

dlv的attach和debug,也试了下,也比较简单,只是使用场景不太一样而已,attach是挂载到运行中的进程进行debug,debug是直接针对go源码调试的,命令执行起来之后跟exec调试过程就没有区别了。这里就不贴执行流程了。

 

超级账本fabric项目试玩

最近跟着这本书学习区块链相关入门知识,

区块链技术指南pdf下载:https://legacy.gitbook.com/download/pdf/book/yeasy/blockchain_guide

该pdf已出版纸质书,《区块链原理、设计与应用》:https://item.jd.com/12159265.html

总体来说这本书对想了解区块链技术的初学者非常有价值,可以了解到区块链相关技术的来龙去脉、基本原理、知识体系、相关开源项目等等。看完超级账本项目fabric之后,感觉其整体框架跟同为分布式系统的OpenStack、k8s也比较类似(当然只是从框架来看,从底层实现来讲还是有很大差异的),主要差异在于其数据存储方式上,共识机制方面,OpenStack、k8s都是以传统数据库或etcd作为基础的,类似区块链中共识算法的多个提案者+一个确认者的场景。都是由多个服务构成,比如nova服务有nova-api、nova-compute、nova-scheduler、nova-conductor等服务,k8s有kublet、kube-apiserver、kube-proxy、kube-scheduler等服务,而fabric则有peer、ca、order这几个服务,他们也都有对应的客户端(命令行、sdk)用来发送请求给服务端。

fabric具体能干啥就不提了,还是看书吧,我还是从已知的IaaS、PaaS等云计算技术来对比,总体来说它可以用高级编程语言编写链码,然后上传到fabric区块链网络的某个节点里(具体来说是某个peer节点的docker容器里,我这里还有个疑问没有找到答案,一个链码只能跑在一个peer节点的一个容器里面吗?我理解应该是可以跑在多个peer节点的,从而实现分布式应用,不然就没有意义了),链码可以实现各种各样的功能(实现智能合约、账本管理等),链码用到的持久化数据都在区块链的块里存储,区块相当于是一个分布式数据库(或者专业名词叫分布式账本),从而实现应用的分布式、高可用、高可靠,以及数据的不可篡改,相比较而言,利用IaaS、PaaS等云计算技术也可以实现类似的功能或架构,尤其是k8s的微服务架构,也能支持分布式应用,但其数据却通常还是集中式存储的,并且容易篡改(当然其适用场景也不是为了解决这个问题,我这里只是随意对比下),也可以用分布式数据库来存数据,但仍然不像区块链那样,数据是用链表+区块来存储的。

要说fabric和k8s唯一的关联,我理解就是他们都是把链码或者说应用跑在docker里面的。fabric通过gRPC协议调用链码接口(接口比较固定),而k8s的服务则一般是通过HTTP RestFul API来调用服务(当然也有其他服务使用其他协议,如tcp或者专有协议)。

代码跑在哪里、数据存在哪里其实都不是关键,关键是怎么样让代码跑的愉快,跑的稳定,跑的没压力,还有就是让数据存的可靠,存的准确,存的安全。去中心化的问题,也是相对的,如果你的应用跑在全世界各地,数据也做到分布式存储到世界各地,我理解这也算是去中心化的。

不胡扯了,下面讲下怎么搭fabric测试环境,我这也是现学现卖,跟着官网文档学的,跑了一个example。我这里用的是fabric-1.1版本,操作系统是CentOS-7.2 x86_64。

环境准备:https://hyperledger-fabric.readthedocs.io/en/release-1.1/prereqs.html

主要是两个部分,一个是golang环境,一个是docker环境,docker包含docker daemon和docker-compose两部分。

安装比较简单,yum install golang docker docker-compose,完事儿,其他依赖如curl一般操作系统都是自带有的,当然下面还要用到git clone代码,也可以提前装上。

docker最好配置下国内的镜像源,否则下镜像要等死。修改docker daemon的配置,之后重启docker服务,systemctl restart docker。

上面的几个registry-mirrors不保证可用,最好自己找好用的替换。

之后就是下载代码了,需要下载两个项目,一个是fabric本身,一个是fabric-example,当然我是为了跑example示例才下载的第二个,如果你想手工搭fabric环境,可以不用下载第二个。

再之后就是准备fabric可执行文件了,脚本在fabric项目目录下的scripts目录下:

fabric/scripts/bootstrap.sh,直接cd到这个目录下,./bootstrap.sh执行就好了,因为我们已经checkout到1.1版本的release分支了,默认就是跑的1.1版本。

相关可执行文件会下载到scripts目录下的bin目录(get-docker-images.sh是代码库自带的,其他几个二进制文件是新下载的):

看下bootstrap脚本,可以看到是用curl命令下载的文件,每个文件几十M,国外网站比较慢,建议加上代理或者找国内的镜像(我是挂的代理)。

之后还要把这几个可执行文件所在目录加到PATH环境变量里(后面部署环境跑example要用到,具体来说是byfn.sh里面要用),让它们在任何目录下都可以被调用,当然也可以加上软链接,或者直接copy到/usr/local/bin之类的默认已加入环境变量的路径下,我这里是用的添加环境变量方式:export PATH=$PATH:/root/fabric/scripts/bin/

之后就是参考官方上手文档跑example了:https://hyperledger-fabric.readthedocs.io/en/release-1.1/build_network.html

文档里说是执行3步(文档里面有-m参数,看了下byfn.sh,里面已经没用这个参数了,不过貌似不影响执行,被忽略了):

每一步都有一堆输出,参考官网文档就行了,这里不贴了。

主要就是启动了一坨docker容器(在执行./byfn.sh up命令过程中通过docker ps -a命令查看):

通过镜像名就可以看出来容器跑的什么服务。

仔细看下byfn.sh就可以分析出fabric是怎么部署的(至少开发测试环境可以这么部署):

部署环境主要就是跑了这句: IMAGE_TAG=$IMAGETAG docker-compose -f $COMPOSE_FILE up -d 2>&1

然后就是在cli那个容器里跑example: docker exec cli scripts/script.sh $CHANNEL_NAME $CLI_DELAY $LANGUAGE $CLI_TIMEOUT

跑example过程中会有一坨日志在屏幕上输出。

因此部署环境就是通过docker-compose -f $COMPOSE_FILE up -d来搞定的,IMAGETAG可以通过byfn.sh的-i命令指定,默认是latest。COMPOSE_FILE可以通过-f命令指定,默认是docker-compose-cli.yaml,我们看下这个yaml文件:

可以看出里面定义了一个容器网络,networks: byfn,几个volumes与容器名称相同,在base/docker-compose-base.yaml文件里面有用到,6个service包含1个order节点、4个peer节点、一个cli节点(依赖其他5个service,这个容易理解,客户端必须要等服务端启动才能执行命令),每个service各包含一个容器实例,容器名称与service名称一样(就是上面贴出来的docker ps -a命令的看到的容器列表)。

除了cli这个service之外,其它几个都继续使用了base/docker-compose-base.yaml这个compose配置,这里就不贴了,而这个yaml里的peer服务又依赖了base/peer-base.yaml。总之就是定义了一坨docker容器,然后利用docker-compose编排功能把容器跑起来。

关于docker-compose,可以参考官方文档:

networks、volumes等配置文件中关键字的意义可以参考上面的文档,简单来说部分是给docker实例准备的各种参数,与k8s的service配置文件比较类似,都是为编排服务里各种容器的。这里networks默认就是一个Linux bridge,volumes就是一个临时目录mount到docker容器里给容器用来保存临时数据。

配置文件里端口映射、命令行、环境变量啥的就不说了。

可以看出,整个集群或者说区块链网络就是通过docker compose的编排功能实现的。比手工部署跑起来简单方便多了。

 

使用kubeasz快速搭建k8s集群all-in-one开发测试环境

最近在为VisionStack产品开发容器服务,提起容器服务就不得不考虑kubernetes项目(简称k8s),当今主流开源的容器编排框架,因此就想搭建一个k8s环境调研下它的功能、使用、相关概念等内容,在搭建环境过程中,首先是参考官网文档提到的minikube工具,用它来进行测试环境的搭建,结果折腾了一下午,也没有成功,遇到的最大问题是国内的防火墙屏蔽了google相关网站的访问,而k8s是google主推的,大部分资源都在google服务器上(如环境部署过程中用到的各种docker镜像都需要从gcr.io上下载,这个是google云提供的docker镜像库网站),如果镜像都无法下载,那肯定环境也就无法搭建成功了。而另外一个工具kubeadm,由于比较复杂,另外估计很可能也会遇到类似问题,也就没有尝试。

正准备放弃的时候,发了个朋友圈吐槽这个事情,正好有朋友也遇到过这个问题,他推荐了另外一个工具:kubeasz,专门针对国内网络环境开发的k8s安装部署工具。下面的安装部署过程就是基于它来完成的,并且是一次成功,对搭建开发测试环境来说非常简单易用,因此这里强烈推荐下(这个故事告诉我们,朋友圈真的是万能的)。下面的安装过程也主要是参考它给的官方文档,略有删减。

项目地址:https://github.com/gjmzj/kubeasz

项目用途:使用Ansible脚本安装K8S集群,介绍组件交互原理,方便直接,不受国内网络环境影响。

快速指南

以下为快速体验k8s集群的测试、开发环境all-in-one部署,国内环境下觉得比官方的minikube方便、简单很多。

1.基础系统配置

  • 推荐内存2G/硬盘20G以上
  • 最小化安装Ubuntu 16.04 server或者CentOS 7 Minimal
  • 配置基础网络、更新源、SSH登陆等

2.安装依赖工具

我实验过程中使用的是CentOS 7.2系统。

Ubuntu 16.04 请执行以下脚本:

CentOS 7 请执行以下脚本:

3.ansible安装及准备

4.安装kubernetes集群

如果执行成功,k8s集群就安装好了。详细分步讲解请查看项目目录 /docs 下相关文档

5.验证安装

6.安装主要组件

7.dashboard安装及登录

  • 登陆 dashboard可以查看和管理集群,更多内容请查阅dashboard文档
  • 本文档基于 dashboard 1.8.3版本,k8s版本v1.10.0。

7.1部署dashboard

如果之前已按照本项目部署dashboard1.6.3,先删除旧版本:kubectl delete -f /etc/ansible/manifests/dashboard/1.6.3/

1.8.3配置文件参考官方文档

  • 增加了通过api-server方式访问dashboard
  • 增加了NodePort方式暴露服务,这样集群外部可以使用 https://NodeIP:NodePort (注意是https不是http,区别于1.6.3版本) 直接访问 dashboard,生产环境建议关闭该访问途径。

安装部署

7.2验证部署结果

7.3登录dashboard

访问 https://x.x.x.x:8443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy (该URL具体使用kubectl cluster-info|grep dashboard查看) ,先使用admin、test1234账号密码通过http认证,之后选择令牌方式登录,令牌通过命令行kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')获取(token:那行),复制冒号后的全部字符并粘贴到dashboard‘输入令牌’处即可。admin登陆拥有所有权限,比如删除某个部署;使用 readonly登陆只有查看权限,尝试删除某个部署会提示错误 forbidden: User \"readonly\" cannot delete services/proxy in the namespace \"kube-system\"

8.清理集群

以上步骤创建的K8S开发测试环境请尽情折腾,碰到错误尽量通过查看日志、上网搜索、提交issues等方式解决;当然如果是彻底奔溃了,可以清理集群后重新创建。

一步清理:ansible-playbook 99.clean.yml

总结

上述流程大部分都是由工具自动完成的,并且所需要的资源也已经全部下载到本地,因此安装过程其实很快,如果你也有类似需求,可以试试这个工具。

参考资料