分类 技术志 下的文章

Hello, ROS

ROS,全称是Robot Operating System,字面译为“机器人操作系统”。不过ROS并非是一个真正意义上的操作系统,而仅仅是一套用于机器人操作和控制软件开发的开发框架(framework),包括各种库和工具。

ROS在2007年诞生于斯坦福大学人工智能实验室Stanford Artificial Intelligence Laboratory,简称SAIL;2008年至2013年,ROS的开发和推广由Willow Garage公司(该公司2014年已关门大吉)主导。2013年8月,ROS的管理权转移给了Open Source Robotics Foundation。截至目前,ROS已经成为全世界使用和支持最为广泛的机器人开发框架之一。

一、ROS简介

ROS推出的初衷旨在降低机器人类软件开发的门槛,提高复用率,避免机器人软件开发人员的重复劳动,快速搭建机器人原型。因此,它采用了当时流行的面向服务SOA的软件架构,最大程度上降低内部耦合,并且易于被第三方扩展。采用C++作为主要开发语言,提高ROS的可移植性,让ROS可以很方便地移植到其他各种CPU体系和OS上。

ROS最初的是针对单机家用移动智能机器人而设计的,因此ROS1版本在以下几方面尚存不足:

  • 鲁棒性

ROS1版本运行时仅有一个master node,一旦master node发生crash,整个robot将无法正常工作。

  • 安全性

ROS1内部完全不设防,Node间通信完全是信赖的。任何Node都可以轻易得到其他node的各种topic数据、参数以及访问相关关键service。

  • 实时性

在ROS1的设计约束下,ROS内部各个节点间产生的实时数据通过master建立的内部网络在各个node间传递。一旦数据量很大,数据可能因内部网络通信性能问题而导致延迟,致使机器人工作异常。这也是ROS在工业机器人领域并未受到“热烈欢迎”的重要原因之一。

为了解决上述这些问题,ROS启动了ROS2的设计和实现。ROS2的第一个alpha版本发布于2015年,最新一个版本是今年七月份发布的beta2版本,ROS2的1.0版本计划将于今年年末正式发布。不过对于ROS2,笔者了解也不多,感兴趣的童鞋可以移步其wiki观看详情。

二、ROS1安装

在深入ROS1之前,我们先来安装一个ROS1。我们首先需要选择一个ROS1的发布版。ROS的发布模式与Ubuntu极其相似:每逢偶数年份发布一个长期支持版(LTS),support 5年;每逢奇数年份发布一个支持2年的版本。

并且ROS的发布版与Ubuntu发布版有着“神同步”:

2014:     ROS Indigo Igloo  对应  Ubuntu 14.04 LTS
2016:     ROS Kinetic Kame 对应 Ubuntu 16.04 LTS

ROS主要基于Ubuntu这款OS进行开发和测试,所以官方建议ROS尽量与Ubuntu一并使用,当然其他linux distribution也可以安装ROS,但正确性和稳定性ROS不能给予明确的保证。目前ROS1发布版的最新版本为:ROS Lunar Loggerhead,但官方推荐使用Ubuntu 16.04 + ROS Kinetic Kame组合;不过由于KK版本发布也就一年出头,市面上更多组织采用的可能还是Ubuntu 14.04 + ROS Indigo Igloo组合。

这里以Ubuntu 16.04+ ROS Kinetic Kame简单说明一下ROS1的安装过程:

1、获取source list并update源

# sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
# apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116
Executing: /tmp/tmp.gJDpQgL6qG/gpg.1.sh --keyserver
hkp://ha.pool.sks-keyservers.net:80
--recv-key
421C365BD9FF1F717815A3895523BAEEB01FA116
gpg: requesting key B01FA116 from hkp server ha.pool.sks-keyservers.net
gpg: key B01FA116: public key "ROS Builder <rosbuild@ros.org>" imported
gpg: Total number processed: 1
gpg:               imported: 1

如果需要代理,可以用:
apt-key adv --keyserver-options http-proxy=<myProxy>  --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116

# apt-get update

2、安装kk版本

ROS有几个release配置供你选择安装:ROS-Base、Desktop Install和Desktop-Full Install,Desktop-Full Install是官方推荐的选项,也是安装最全的选项,它包含了ROS, rqt, rviz, robot-generic libraries, 2D/3D simulators, navigation and 2D/3D perception等package:

# apt-get install ros-kinetic-desktop-full

这个过程需要好长一段时间(依你的网络情况而定),因为ROS超级庞大,有大约2G的安装文件要下载安装。

3、初始化ROS依赖

在使用ROS之前,我们还得先初始化ROS的一些依赖,ROS为你提供了“一键式”的初始化命令:

# rosdep init
Wrote /etc/ros/rosdep/sources.list.d/20-default.list
Recommended: please run

    rosdep update

# rosdep update
reading in sources list data from /etc/ros/rosdep/sources.list.d
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/osx-homebrew.yaml
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml
... ...
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/ruby.yaml
Hit https://raw.githubusercontent.com/ros/rosdistro/master/releases/fuerte.yaml
Query rosdistro index https://raw.githubusercontent.com/ros/rosdistro/master/index.yaml
Add distro "groovy"
Add distro "hydro"
Add distro "indigo"
Add distro "jade"
Add distro "kinetic"
Add distro "lunar"
updated cache in /home/tonybai/.ros/rosdep/sources.cache
... ...

到这里,我们可以看到ROS被安装到/opt/ros/kinetic下面了:

# tree -L 1  /opt/ros/kinetic
/opt/ros/kinetic
├── bin
├── env.sh
├── etc
├── include
├── lib
├── setup.bash
├── setup.sh
├── _setup_util.py
├── setup.zsh
└── share

5 directories, 5 files

4、设置环境变量

ROS提供了设置环境变量的脚本:/opt/ros/kinetic/setup.bash,我们将其加入到.bashrc中,这样每次用户登录后就可以使用下面这些ROS专属环境变量了:

# echo "source /opt/ros/kinetic/setup.bash" >> ~/.bashrc
# source ~/.bashrc

# env|grep ROS
ROS_ROOT=/opt/ros/kinetic/share/ros
ROS_PACKAGE_PATH=/opt/ros/kinetic/share
ROS_MASTER_URI=http://localhost:11311
ROSLISP_PACKAGE_DIRECTORIES=
ROS_DISTRO=kinetic
ROS_ETC_DIR=/opt/ros/kinetic/etc/ros

5、安装一些用于ROS package构建的工具依赖

ROS的用户会创建自己的ROS package,为了方便构建这些user package,我们需要安装以下一些工具:

# apt-get install python-rosinstall python-rosinstall-generator python-wstool build-essential

6、验证安装结果

完成以上操作后,ROS kk版本就安装OK了,我们来验证一下安装结果是否正确。我们来尝试启动一下ROS的master node:

# roscore
... logging to /root/.ros/log/fc6a002e-75cf-11e7-b053-00163e1001d7/roslaunch-myhost-7609.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://myhost:43606/
ros_comm version 1.12.7

SUMMARY
========

PARAMETERS
 * /rosdistro: kinetic
 * /rosversion: 1.12.7

NODES

auto-starting new master
process[master]: started with pid [7620]
ROS_MASTER_URI=http://myhost:11311/

setting /run_id to fc6a002e-75cf-11e7-b053-00163e1001d7
process[rosout-1]: started with pid [7633]
started core service [/rosout]

如果你看到上面这些roscore的输出,那么基本就证明你的ROS1安装成功了!

三、ROS架构

ROS安装完毕后,我们来对ROS做进一步的探索!先来看看ROS1的架构。

ROS文档中将ROS架构分为三个级别:Filesystem level、Computation Graph level和Community level。对于一个framework来说,从字面意义上理解这三个level还是有些晦涩的。Community level先不说,我们可以通过对照来理解Filesystem level和Computation Graph level,实质上它们一个对应的是ROS的静态结构,一个对应的则是ROS的运行时结构。

1、ROS Filesystem level

我们这里借用《Effective Robotics Programming with ROS 3rd》中的图来整体看一下ROS Filesystem的概念:

img{512x368}

ROS实质上是由一系列的packages组成的,在packages的基础上,ROS通过metapackage来聚合一组packages以形成一个逻辑package。基于metapackage和package概念,ROS为开发者提供了在package之间跳转、文件拷贝、包查找、执行等功能的”类FileSystem”命令集合,比如:roscd、rosls、roscp、rosrun、roscat、rospack等。下面是一些命令使用的例子:

// 切换到ros安装目录
root@myhost:~# roscd
root@myhost:/opt/ros/kinetic#

// 切换到turtlesim包目录
root@myhost:~# roscd turtlesim
root@myhost:/opt/ros/kinetic/share/turtlesim#

// list turtlesim包内的文件
root@myhost:~# rosls turtlesim
cmake  images  msg  package.xml  srv

// 查找turtlesim包的路径
root@myhost:~# rospack find turtlesim
/opt/ros/kinetic/share/turtlesim

// 执行包turtlesim下的turtlesim_node
root@myhost:~# rosrun turtlesim turtlesim_node

// 查看包turtlesim的package.xml内容
root@myhost:~# roscat turtlesim package.xml

<?xml version="1.0"?>
<package>
  <name>turtlesim</name>
  <version>0.7.1</version>
  <description>
    turtlesim is a tool made for teaching ROS and ROS packages.
  </description>
... ...
</package>

ROS安装后,其所有package均存储在$ROS_PACKAGE_PATH下面,初始情况下即为/opt/ros/kinetic/share:

root@myhost:/opt/ros/kinetic# ls share
actionlib                         eigen_stl_containers          laser_pipeline         rosbag_migration_rule  roswtf                 shape_msgs
actionlib_msgs                    executive_smach               librviz_tutorial       rosbag_storage         rqt_action             simulators
actionlib_tutorials               filters                       map_msgs               ros_base               rqt_bag                smach
... ...

每个package下的结构都类似,以turtlesim包为例:

root@myhost:/opt/ros/kinetic/share# ls -F turtlesim
cmake/  images/  msg/  package.xml  srv/

至此,上面图片中package中的结构似乎与上面看到的turtlesim package中的结构对应上了。每个package下面都至少有一个package.xml作为package的manifests,msg、srv是功能性配置,分别定义了package用到的message和提供的service的结构。这里并没有代码,只是一些配置信息。

而对应的包的可执行文件则在/opt/ros/kinetic/lib下,还是以turtlesim package为例,当我们执行下面命令时:

# rosrun turtlesim turtlesim_node

rosrun首先会到$ROS_PACKAGE_PATH下找是否有package.xml中name为”turtlesim”的package(与目录的名字无关)。如果有,rosrun会到/opt/ros/kinetic/lib/turtlesim下查找是否有turtlesim_node这个二进制可执行文件。存在,则启动之;否则报错。

root@myhost:/opt/ros/kinetic/lib/turtlesim# ls
draw_square  mimic  turtlesim_node  turtle_teleop_key

root@myhost:/opt/ros/kinetic/lib/turtlesim# rosrun turtlesim turtlesim_node
[ INFO] [1501549501.410816841]: Starting turtlesim with node name /turtlesim
[ INFO] [1501549501.428589492]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000]

还有一种package:metapackage。metapackage在目录结构上与普通package无异,但package.xml尾部多了metapackage标签,我们以ros_core/package.xml为例:

<package>
  <name>ros_core</name>
  <version>1.3.1</version>

  <buildtool_depend>catkin</buildtool_depend>

  <run_depend>catkin</run_depend>
  <run_depend>cmake_modules</run_depend>
  <run_depend>common_msgs</run_depend>
  <run_depend>gencpp</run_depend>
  <run_depend>geneus</run_depend>
  <run_depend>genlisp</run_depend>
  <run_depend>genmsg</run_depend>
  <run_depend>gennodejs</run_depend>
  <run_depend>genpy</run_depend>
  <run_depend>message_generation</run_depend>
  <run_depend>message_runtime</run_depend>
  <run_depend>ros</run_depend>
  <run_depend>ros_comm</run_depend>
  <run_depend>rosbag_migration_rule</run_depend>
  <run_depend>rosconsole_bridge</run_depend>
  <run_depend>roscpp_core</run_depend>
  <run_depend>rosgraph_msgs</run_depend>
  <run_depend>roslisp</run_depend>
  <run_depend>rospack</run_depend>
  <run_depend>std_msgs</run_depend>
  <run_depend>std_srvs</run_depend>

  <export>
    <metapackage/>
  </export>
</package>

这种包称为metapackage,它的实质是一组package的集合。

2、ROS Computation Graph level

说完了ROS的静态结构,我们再来看看ROS整体的运行时结构,即ROS Computation Graph level:

img{512x368}

ROS在运行时层面是由一个master和一组node组成的,master的作用就是名字注册和查找,建立node与topic间联系以及服务发现之用。node间的通信方式可以是:

  • 服务srv调用
  • topic的发布和订阅

我们通过rosnode命令可以操作node,比如查看当前ROS中node信息:

# rosnode list
/rosout
/turtlesim

/rosout node是一个由roscore命令启动的特殊node,它相当于整个ROS运行环境的stdout/stderr,将所有node发往/rosout topic的消息汇聚在一起。

每个ROS运行时环境有且仅有一个ros master,ros master通过执行roscore命令启动,这也是一个ROS运行环境启动最先应该执行的命令:

# roscore
... logging to /home/tonybai/.ros/log/ee13b88e-7666-11e7-af90-4ccc6a7061a6/roslaunch-tonybai-myhost-26158.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://tonybai-myhost:36180/
ros_comm version 1.12.7

SUMMARY
========

PARAMETERS
 * /rosdistro: kinetic
 * /rosversion: 1.12.7

NODES

auto-starting new master
process[master]: started with pid [26169]
ROS_MASTER_URI=http://tonybai-myhost:11311/

setting /run_id to ee13b88e-7666-11e7-af90-4ccc6a7061a6
process[rosout-1]: started with pid [26182]
started core service [/rosout]

roscore位于/opt/ros/kinetic/bin下,它实际上是一个python脚本,它调用位于/opt/ros/kinetic/lib/python2.7/dist-packages/roslaunch下的roslaunch lib,并依据launch配置文件/opt/ros/kinetic/etc/ros/roscore.xml启动对应的核心node:

// /opt/ros/kinetic/etc/ros/roscore.xml
<!--
  ROS Core Stack definition

  Before making any modifications to this file, please read:

http://ros.org/wiki/roscore

  -->
<launch>
  <group ns="/">
    <param name="rosversion" command="rosversion roslaunch" />
    <param name="rosdistro" command="rosversion -d" />
    <node pkg="rosout" type="rosout" name="rosout" respawn="true"/>
  </group>
</launch>

roscore会自动启动master,master对应的是一个metapackage: ros。ros package的package.xml如下:

<package>
  <name>ros</name>
  <version>1.13.5</version>
  <description>ROS packaging system</description>
  <maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
  ... ...

  <buildtool_depend>catkin</buildtool_depend>

  <run_depend>catkin</run_depend> <!-- only for backward compatibility with rosbuild -->
  <run_depend>mk</run_depend>
  <run_depend>rosbuild</run_depend>
  <run_depend>roslang</run_depend>
  <run_depend>roslib</run_depend>
  <run_depend>rosbash</run_depend>
  <run_depend>rosboost_cfg</run_depend>
  <run_depend>rosclean</run_depend>
  <run_depend>roscreate</run_depend>
  <run_depend>rosmake</run_depend>
  <run_depend>rosunit</run_depend>

  <export>
    <metapackage/>
  </export>
</package>

ROS的运行时当前目录为~/.ros,在这个目录下你会看到ros的一些运行时输出信息:

$ tree  -L 1 ~/.ros
/home/tonybai/.ros
├── log/
├── roscore-11311.pid
├── rosdep/
├── rospack_cache_00988404638878154258
├── rospack_cache_04359245844500407984
├── rospack_cache_05251971726343818934
├── rospack_cache_11134725904490598093
├── rosstack_cache_00988404638878154258
├── rosstack_cache_04359245844500407984
├── rosstack_cache_05251971726343818934
└── rosstack_cache_11134725904490598093

2 directories, 9 files

roscore还会启动一个Parameter Server,用于各个节点保存或读取parameters,通过rosparam可以查看相关param信息,比如当前param的list:

$ rosparam list
/background_b
/background_g
/background_r
/rosdistro
/roslaunch/uris/host_tonybai_myhost__36180
/rosversion
/run_id

我们可以通过ros提供的rqt_graph命令查看node之间以及node与topic之间的订阅和发布关系,如下图:

img{512x368}

3、ROS的“分布式”源码结构

安装过程中ROS的“庞大”,与ROS在github上源码库的“渺小”形成鲜明对比。其实我们安装的ROS与这份源码库并非一一对应的:ROS的源码结构也是“分布式”的,ROS源码实质上是一系列package源码的组合。当前版本的ROS发布版采用bloom工具进行release的。以kk版本为例,bloom读取一份rosdistro库的kk版本distribution.yaml文件(这份文件比较庞大),即ROS发布文件,并根据文件中的描述信息,下载对应的package源码并编译构建的:

// kinetic/distribution.yaml)
%YAML 1.1
# ROS distribution file
# see REP 143: http://ros.org/reps/rep-0143.html
---
release_platforms:
  debian:
  - jessie
  fedora:
  - '23'
  - '24'
  ubuntu:
  - xenial
repositories:
  abb:
    doc:
      type: git
      url: https://github.com/ros-industrial/abb.git
      version: kinetic-devel
    release:
      packages:
      - abb
      - abb_driver
      - abb_resources
      ... ...
      tags:
        release: release/kinetic/{package}/{version}
      url: https://github.com/ros-industrial-release/abb-release.git
      version: 1.3.0-1
    source:
      type: git
      url: https://github.com/ros-industrial/abb.git
      version: kinetic
    status: developed
  abb_experimental:
    doc:
      type: git
      url: https://github.com/ros-industrial/abb_experimental.git
      version: kinetic-devel
    status: developed
... ...
type: distribution
version: 2

鉴于ROS这种分布式的相对松散的源码组织结构,对ROS的裁剪则相对简单,只需挑选你自己需要的第三方包即可。

四、启动你的第一个ROS“机器人”

ROS虽然号称机器人开发框架,但拥有一个实体版机器人并不是进行ROS开发的必要条件。ROS的一大优势就是可以利用各种仿真工具进行机器人操作和控制逻辑的仿真和调试。常见的仿真器主要有三个:TurtlesimRviz+arbotixGazebo。Turtlesim是一个QT开发的2D轨迹显示界面,只能显示运动轨迹;arbotix是含有一个差速驱动机器人的控制器,结合rviz使用,用于机器人运动及topic数据的3D显示,但不包含物理学引擎;Gazebo是全功能的3D物理模拟器,要用这个模拟器,需要掂量掂量你的主机的内存和显卡是否够用。

本文是入门文章,我们就从turtlesim开始。假设此时roscore已经启动了。

我们来启动一下turtlesim_node:

# rosrun turtlesim turtlesim_node
[ INFO] [1501549501.410816841]: Starting turtlesim with node name /turtlesim
[ INFO] [1501549501.428589492]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000]

这时你的desktop会出现一个新的窗口,如下图:

img{512x368}

不过此时小海龟一动不动!如果要让它移动,我们需要告诉他如何移动!

我们启动另外一个node – turtle_teleop_key:

# rosrun turtlesim turtle_teleop_key
Reading from keyboard
---------------------------
Use arrow keys to move the turtle.

通过turtle_teleop_key,我们可以使用方向键控制小海龟的移动了:

img{512x368}

其原理在于:turtle_teleop_key将方向键产生的数据转换为位置信息后,发布到topic: /turtle1/cmd_vel上;turtlesim_node由于subscribe了该topic,因此将接收到新的位置数据,这样小海龟就会移动到新的位置上去:

img{512x368}

turtlesim node启动后还启动了一个service: spawn,调用该服务我们可以在窗口上创建出一个新的小海龟:

# rosservice call /spawn 2 2 0.2 ""
name: turtle2

img{512x368}

可以看到,通过service调用或向topic发布数据,我们可以自由控制小海龟。下面的是一个稍微复杂的控制指令,其结果就是让小海龟1进行持续的转圈动作:

rostopic pub /turtle1/cmd_vel geometry_msgs/Twist -r 1 -- '[2.0, 0.0, 0.0]' '[0.0, 0.0, -1.8]'

img{512x368}

五、创建你的第一个ROS package

现在我们来创建第一个ROS package!

1、初始化ros workspace

我们要添加自己的ROS package,一般不会直接在ROS的安装目录下创建,因此我们需要创建自己的workspace,并在后续将其加入到ROS_PACKAGE_PATH中,以使得ros的文件系统命令也能适用于我们自己的workspace路径。

# mkdir -p ~/myros_ws/src
# cd ~/myros_ws/src
# catkin_init_workspace
Creating symlink "/home/tonybai/myros_ws/src/CMakeLists.txt" pointing to "/opt/ros/kinetic/share/catkin/cmake/toplevel.cmake"

$ tree ~/myros_ws/
/home/tonybai/myros_ws/
└── src
    └── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake

1 directory, 1 file

2、创建Package

我们来创建一个我们自己的package – chattingsim:

# cd ~/myros_ws/src
# catkin_create_pkg chattingsim std_msgs rospy roscpp
Created file chattingsim/package.xml
Created file chattingsim/CMakeLists.txt
Created folder chattingsim/include/chattingsim
Created folder chattingsim/src
Successfully created files in /home/tonybai/myros_ws/src/chattingsim. Please adjust the values in package.xml.

# tree chattingsim/
chattingsim/
├── CMakeLists.txt
├── include
│   └── chattingsim
├── package.xml
└── src

3 directories, 2 files

虽然目前我们的chattingsim package并没有任何有意义的代码,但不妨碍我们先来编译一下myros_ws这个workspace:

# cd ~/myros_ws/
# catkin_make

# catkin_make
Base path: /home/tonybai/myros_ws
Source space: /home/tonybai/myros_ws/src
Build space: /home/tonybai/myros_ws/build
Devel space: /home/tonybai/myros_ws/devel
Install space: /home/tonybai/myros_ws/install
####
#### Running command: "cmake /home/tonybai/myros_ws/src -DCATKIN_DEVEL_PREFIX=/home/tonybai/myros_ws/devel -DCMAKE_INSTALL_PREFIX=/home/tonybai/myros_ws/install -G Unix Makefiles" in "/home/tonybai/myros_ws/build"
####
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
... ...
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found gtest sources under '/usr/src/gtest': gtests will be built
-- Using Python nosetests: /usr/bin/nosetests-2.7
-- catkin 0.7.6
-- BUILD_SHARED_LIBS is on
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- ~~  traversing 1 packages in topological order:
-- ~~  - chattingsim
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- +++ processing catkin package: 'chattingsim'
-- ==> add_subdirectory(chattingsim)
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tonybai/myros_ws/build
####
#### Running command: "make -j4 -l4" in "/home/tonybai/myros_ws/build"
####

catkin_make后,myros_ws下面又增加了不少目录和文件:

~/myros_ws$ tree -L 2
.
├── build
│   ├── catkin
│   ├── catkin_generated
│   ├── CATKIN_IGNORE
│   ├── catkin_make.cache
│   ├── chattingsim
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── cmake_install.cmake
│   ├── CTestTestfile.cmake
│   ├── gtest
│   ├── Makefile
│   └── test_results
├── devel
│   ├── env.sh
│   ├── lib
│   ├── setup.bash
│   ├── setup.sh
│   ├── _setup_util.py
│   ├── setup.zsh
│   └── share
└── src
    ├── chattingsim
    └── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake

12 directories, 12 files

我们看到~/myros_ws/devel目录下的结构与/opt/ros/kinetic下的非常相似,我们将其加入到ROS_PACKAGE_PATH:

# cd ~/myros_ws/devel
# source ./setup.bash
# echo $ROS_PACKAGE_PATH
/home/tonybai/myros_ws/src:/opt/ros/kinetic/share

3、添加talker和listener

chattingsim package的架子已经搭好,接下来我们开始“填肉”。这里我们直接使用ros tutorials中写好的两个源文件talker.cpplistener.cpp,我们把这两个文件放在~/myros_ws/src/chattingsim/src下面。

在build之前,我们需要修改一下chattingsim的CMakeLists.txt:

cmake_minimum_required(VERSION 2.8.3)
project(chattingsim)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  genmsg
)

generate_messages(DEPENDENCIES std_msgs)

include_directories(
  include ${catkin_INCLUDE_DIRS}
)

add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker chattingsim_generate_messages_cpp)

add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(listener chattingsim_generate_messages_cpp)

构建chattingsim package:

~/myros_ws# catkin_make
Base path: /home/tonybai/myros_ws
Source space: /home/tonybai/myros_ws/src
Build space: /home/tonybai/myros_ws/build
Devel space: /home/tonybai/myros_ws/devel
Install space: /home/tonybai/myros_ws/install
####
#### Running command: "make cmake_check_build_system" in "/home/tonybai/myros_ws/build"
####
####
#### Running command: "make -j4 -l4" in "/home/tonybai/myros_ws/build"
####
[  0%] Built target std_msgs_generate_messages_eus
[  0%] Built target std_msgs_generate_messages_cpp
[  0%] Built target std_msgs_generate_messages_lisp
[  0%] Built target std_msgs_generate_messages_py
[  0%] Built target std_msgs_generate_messages_nodejs
[  0%] Built target chattingsim_generate_messages_cpp
[  0%] Built target chattingsim_generate_messages_lisp
[ 14%] Generating EusLisp manifest code for chattingsim
[ 28%] Building CXX object chattingsim/CMakeFiles/talker.dir/src/talker.cpp.o
[ 28%] Built target chattingsim_generate_messages_nodejs
[ 42%] Generating Python msg __init__.py for chattingsim
[ 57%] Generating Python srv __init__.py for chattingsim
[ 71%] Building CXX object chattingsim/CMakeFiles/listener.dir/src/listener.cpp.o
[ 71%] Built target chattingsim_generate_messages_py
[ 71%] Built target chattingsim_generate_messages_eus
[ 71%] Built target chattingsim_generate_messages
[ 85%] Linking CXX executable /home/tonybai/myros_ws/devel/lib/chattingsim/talker
[ 85%] Built target talker
[100%] Linking CXX executable /home/tonybai/myros_ws/devel/lib/chattingsim/listener
[100%] Built target listener

4、启动chattingsim的talker node和listener node

在两个terminal窗口分别启动listener node和talker node:

# rosrun chattingsim listener
[ INFO] [1501577165.148477238]: I heard: [hello world 3]
[ INFO] [1501577165.248349227]: I heard: [hello world 4]
[ INFO] [1501577165.348301478]: I heard: [hello world 5]
[ INFO] [1501577165.448340592]: I heard: [hello world 6]
[ INFO] [1501577165.548433696]: I heard: [hello world 7]
[ INFO] [1501577165.648466054]: I heard: [hello world 8]
[ INFO] [1501577165.748424131]: I heard: [hello world 9]
[ INFO] [1501577165.848457076]: I heard: [hello world 10]
[ INFO] [1501577165.948449431]: I heard: [hello world 11]
[ INFO] [1501577166.048470110]: I heard: [hello world 12]
[ INFO] [1501577166.148340964]: I heard: [hello world 13]

# rosrun chattingsim talker
[ INFO] [1501577164.847745179]: hello world 0
[ INFO] [1501577164.947898377]: hello world 1
[ INFO] [1501577165.047889213]: hello world 2
[ INFO] [1501577165.147882701]: hello world 3
[ INFO] [1501577165.247923700]: hello world 4
[ INFO] [1501577165.347918242]: hello world 5
[ INFO] [1501577165.447917169]: hello world 6
[ INFO] [1501577165.547916593]: hello world 7
[ INFO] [1501577165.647920474]: hello world 8
[ INFO] [1501577165.747930882]: hello world 9
[ INFO] [1501577165.847917356]: hello world 10
[ INFO] [1501577165.947918365]: hello world 11
[ INFO] [1501577166.047918187]: hello world 12
[ INFO] [1501577166.147919712]: hello world 13
^C[ INFO] [1501577166.247984284]: hello world 14

至此,基于我们第一个package: chattingsim而创建的node工作正常!

六、小结

如果说人工智能的算法是大脑,实体的机械部件构成四肢,那么ROS则提供了大脑与各肢体间提供了神经传递的机制。之前ROS在国内发展的不瘟不火,随着Baidu Apollo项目将ros作为Apollo-platform支持的一部分,更多人会去了解ROS,ROS在国内的发展势也许会走得更顺畅一些。ROSCon 2017也即将于下月在加拿大温哥华召开,ROS2对ROS1的安全性和实时性的加强也势必会让ROS有更多用武之地,值得期待。

注:ros wiki 资料非常详尽,ros tutorial是学习ros的起点,几乎不用任何其他书籍。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

解决Kubernetes 1.6.4 Dashboard无法访问的问题

前一段时间将之前采用kubeadm安装的Kubernetes 1.5.1环境升级到了1.6.4版本,升级过程较为顺利。由于该k8s cluster是一个测试环境,当时并没有过于关注,就忙别的事情了。最近项目组打算在这个环境下做一些事情,而当我们重新“捡起”这个环境时,发现Kubernetes Dashboard无法访问了。

Kubernetes的dashboard可以有很多种访问方式,比如:可以通过暴露nodeport的方式(无身份验证,不安全)、可以通过访问apiserver的api服务的方式等。我们的Dashboard通过APIServer进行访问:

https://apiserver_ip:secure_port/ui

正常情况下通过浏览器访问:https://apiserver_ip:secure_port/ui,浏览器会弹出身份验证对话框,待输入正确的用户名和密码后,便可成功进入Dashboard了。但当前,我们得到的结果却是:

User "system:anonymous" cannot proxy services in the namespace "kube-system".

而访问apiserver(https://apiserver_ip:secure_port/)得到的结果如下:

User "system:anonymous" cannot get  at the cluster scope.

一、问题原因分析

k8s 1.6.x版本与1.5.x版本的一个很大不同在于1.6.x版本启用了RBACAuthorization mode(授权模型),这点在K8s master init的日志中可以得到证实:

# kubeadm init --apiserver-advertise-address xx.xx.xx
... ...
[init] Using Kubernetes version: v1.6.4
[init] Using Authorization mode: RBAC
[preflight] Running pre-flight checks
[preflight] Starting the kubelet service
[certificates] Generated CA certificate and key.
[certificates] Generated API server certificate and key
.... ...
[apiconfig] Created RBAC rules
[addons] Created essential addon: kube-proxy
[addons] Created essential addon: kube-dns

Your Kubernetes master has initialized successfully!
... ...

《Kubernetes集群的安全配置》一文中我们提到过Kubernetes API server的访问方法:

Authentication(身份验证) -> Authorization(授权)-> Admission Control(入口条件控制)

只不过在Kubernetes 1.5.x及以前的版本中,Authorization的环节都采用了默认的配置,即”AlwaysAllow”,对访问APIServer并不产生什么影响:

# kube-apiserver -h
... ...
--authorization-mode="AlwaysAllow": Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: AlwaysAllow,AlwaysDeny,ABAC,Webhook,RBAC
... ...

但K8s 1.6.x版本中,–authorization-mode的值发生了变化:

# cat /etc/kubernetes/manifests/kube-apiserver.yaml

spec:
  containers:
  - command:
    - kube-apiserver
    - --allow-privileged=true
    ... ...
    - --basic-auth-file=/etc/kubernetes/basic_auth_file
    - --authorization-mode=RBAC
    ... ...

注:这里我们依旧通过basic auth方式进行apiserver的Authentication,而不是用客户端数字证书校验等其他方式。

显然问题的原因就在于这里RBAC授权方式的使用,让我们无法正常访问Dashboard了。

二、Kubernetes RBAC Authorization简介

RBAC Authorization的基本概念是Role和RoleBinding。Role是一些permission的集合;而RoleBinding则是将Role授权给某些User、某些Group或某些ServiceAccount。K8s官方博客《RBAC Support in Kubernetes》一文的中的配图对此做了很生动的诠释:

img{512x368}

从上图中我们可以看到:

Role: pod-reader 拥有Pod的get和list permissions;
RoleBinding: pod-reader 将Role: pod-reader授权给右边的User、Group和ServiceAccount。

和Role和RoleBinding对应的是,K8s还有ClusterRole和ClusterRoleBinding的概念,它们不同之处在于:ClusterRole和ClusterRoleBinding是针对整个Cluster范围内有效的,无论用户或资源所在的namespace是什么;而Role和RoleBinding的作用范围是局限在某个k8s namespace中的。

Kubernetes 1.6.4安装时内建了许多Role/ClusterRole和RoleBinds/ClusterRoleBindings:

# kubectl get role -n kube-system
NAME                                        AGE
extension-apiserver-authentication-reader   50d
system:controller:bootstrap-signer          50d
system:controller:token-cleaner             50d

# kubectl get rolebinding -n kube-system
NAME                                 AGE
system:controller:bootstrap-signer   50d
system:controller:token-cleaner      50d

# kubectl get clusterrole
NAME                                           AGE
admin                                          50d
cluster-admin                                  50d
edit                                           50d
system:auth-delegator                          50d
system:basic-user                              50d
system:controller:attachdetach-controller      50d
... ...
system:discovery                               50d
system:heapster                                50d
system:kube-aggregator                         50d
system:kube-controller-manager                 50d
system:kube-dns                                50d
system:kube-scheduler                          50d
system:node                                    50d
system:node-bootstrapper                       50d
system:node-problem-detector                   50d
system:node-proxier                            50d
system:persistent-volume-provisioner           50d
view                                           50d
weave-net                                      50d

# kubectl get clusterrolebinding
NAME                                           AGE
cluster-admin                                  50d
kubeadm:kubelet-bootstrap                      50d
kubeadm:node-proxier                           50d
kubernetes-dashboard                           50d
system:basic-user                              50d
system:controller:attachdetach-controller      50d
... ...
system:controller:statefulset-controller       50d
system:controller:ttl-controller               50d
system:discovery                               50d
system:kube-controller-manager                 50d
system:kube-dns                                50d
system:kube-scheduler                          50d
system:node                                    50d
system:node-proxier                            50d
weave-net                                      50d

三、Dashboard的role和rolebinding

Kubernetes 1.6.x启用RBAC后,诸多周边插件也都推出了适合K8s 1.6.x的manifest描述文件,比如:weave-net等。Dashboard的manifest文件中也增加了关于rolebinding的描述,我当初用的是1.6.1版本,文件内容摘录如下:

// kubernetes-dashboard.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
  labels:
    k8s-app: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard
  namespace: kube-system
... ...

我们看到在kubernetes-dashboard.yaml中,描述文件新建了一个ClusterRoleBinding:kubernetes-dashboard。该binding将ClusterRole: cluster-admin授权给了一个ServiceAccount: kubernetes-dashboard。我们看看ClusterRole: cluster-admin都包含了哪些permission:

# kubectl get clusterrole/cluster-admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: 2017-05-30T14:06:39Z
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: cluster-admin
  resourceVersion: "11"
  selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolescluster-admin
  uid: 331c79dc-4541-11e7-bc9a-12584ec3a8c9
rules:
- apiGroups:
  - '*'
  resources:
  - '*'
  verbs:
  - '*'
- nonResourceURLs:
  - '*'
  verbs:
  - '*'

可以看到,在rules设定中,cluster-admin似乎拥有了“无限”权限。不过注意:这里仅仅授权给了一个service account,并没有授权给user或group。并且这里的kubernetes-dashboard是dashboard访问apiserver时使用的(下图右侧流程),并不是user访问APIServer时使用的。

img{512x368}

我们需要给登录dashboard或者说apiserver的user(图左侧)进行授权。

四、为user: admin进行授权

我们的kube-apiserver的启动参数中包含:

    - --basic-auth-file=/etc/kubernetes/basic_auth_file

也就是说我们访问apiserver使用的是basic auth的身份验证方式,而user恰为admin。而从本文开头的错误现象来看,admin这个user并未得到足够的授权。这里我们要做的就是给admin选择一个合适的clusterrole。但kubectl并不支持查看user的信息,初始的clusterrolebinding又那么多,一一查看十分麻烦。我们知道cluster-admin这个clusterrole是全权限的,我们就来将admin这个user与clusterrole: cluster-admin bind到一起:

# kubectl create clusterrolebinding login-on-dashboard-with-cluster-admin --clusterrole=cluster-admin --user=admin
clusterrolebinding "login-on-dashboard-with-cluster-admin" created

# kubectl get clusterrolebinding/login-on-dashboard-with-cluster-admin -o yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: 2017-07-20T08:57:07Z
  name: login-on-dashboard-with-cluster-admin
  resourceVersion: "5363564"
  selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolebindingslogin-on-dashboard-with-cluster-admin
  uid: 686a3f36-6d29-11e7-8f69-00163e1001d7
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: admin

binding后,我们再来访问一下dashboard UI,不出意外的话,熟悉的dashboard界面就会出现在你的眼前。

注:Kubernetes API Server新增了–anonymous-auth选项,允许匿名请求访问secure port。没有被其他authentication方法拒绝的请求即Anonymous requests, 这样的匿名请求的username为”system:anonymous”, 归属的组为”system:unauthenticated”。并且该选线是默认的。这样一来,当采用chrome浏览器访问dashboard UI时很可能无法弹出用户名、密码输入对话框,导致后续authorization失败。为了保证用户名、密码输入对话框的弹出,需要将–anonymous-auth设置为false:

// /etc/kubernetes/manifests/kube-apiserver.yaml
    - --anonymous-auth=false

用curl测试结果如下:

$curl -u admin:YOUR_PASSWORD -k https://apiserver_ip:secure_port/
{
  "paths": [
    "/api",
    "/api/v1",
    "/apis",
    "/apis/apps",
    "/apis/apps/v1beta1",
    "/apis/authentication.k8s.io",
    "/apis/authentication.k8s.io/v1",
    "/apis/authentication.k8s.io/v1beta1",
    "/apis/authorization.k8s.io",
    "/apis/authorization.k8s.io/v1",
    "/apis/authorization.k8s.io/v1beta1",
    "/apis/autoscaling",
    "/apis/autoscaling/v1",
    "/apis/autoscaling/v2alpha1",
    "/apis/batch",
    "/apis/batch/v1",
    "/apis/batch/v2alpha1",
    "/apis/certificates.k8s.io",
    "/apis/certificates.k8s.io/v1beta1",
    "/apis/extensions",
    "/apis/extensions/v1beta1",
    "/apis/policy",
    "/apis/policy/v1beta1",
    "/apis/rbac.authorization.k8s.io",
    "/apis/rbac.authorization.k8s.io/v1alpha1",
    "/apis/rbac.authorization.k8s.io/v1beta1",
    "/apis/settings.k8s.io",
    "/apis/settings.k8s.io/v1alpha1",
    "/apis/storage.k8s.io",
    "/apis/storage.k8s.io/v1",
    "/apis/storage.k8s.io/v1beta1",
    "/healthz",
    "/healthz/ping",
    "/healthz/poststarthook/bootstrap-controller",
    "/healthz/poststarthook/ca-registration",
    "/healthz/poststarthook/extensions/third-party-resources",
    "/healthz/poststarthook/rbac/bootstrap-roles",
    "/logs",
    "/metrics",
    "/swaggerapi/",
    "/ui/",
    "/version"
  ]
}


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

Go 1.9中值得关注的几个变化

Go语言在2016年当选tiobe index的年度编程语言。

img{512x368}

转眼间6个月过去了,Go在tiobe index排行榜上继续强势攀升,在最新公布的TIBOE INDEX 7月份的排行榜上,Go挺进Top10:

img{512x368}

还有不到一个月,Go 1.9版本也要正式Release了(计划8月份发布),当前Go 1.9的最新版本是go1.9beta2,本篇的实验环境也是基于该版本的,估计与final go 1.9版本不会有太大差异了。在今年的GopherChina大会上,我曾提到:Go已经演进到1.9,接下来是Go 1.10还是Go 2? 现在答案已经揭晓:Go 1.10。估计Go core team认为Go 1还有很多待改善和优化的地方,或者说Go2的大改时机依旧未到。Go team的tech lead Russ Cox将在今年的GopherCon大会上做一个题为”The Future of Go”的主题演讲,期待从Russ的口中能够得到一些关于Go未来的信息。

言归正传,我们还是来看看Go 1.9究竟有哪些值得我们关注的变化,虽然我个人觉得Go1.9的变动的幅度并不是很大^0^。

一、Type alias

Go 1.9依然属于Go1系,因此继续遵守Go1兼容性承诺。这一点在我的“值得关注的几个变化”系列文章中几乎每次都要提到。

不过Go 1.9在语言语法层面上新增了一个“颇具争议”的语法: Type Alias。关于type alias的proposal最初由Go语言之父之一的Robert Griesemer提出,并计划于Go 1.8加入Go语言。但由于Go 1.8的type alias实现过于匆忙,测试不够充分,在临近Go 1.8发布的时候发现了无法短时间解决的问题,因此Go team决定将type alias的实现从Go 1.8中回退

Go 1.9 dev cycle伊始,type alias就重新被纳入。这次Russ Cox亲自撰写文章《Codebase Refactoring (with help from Go)》为type alias的加入做铺垫,并开启新的discussion对之前Go 1.8的general alias语法形式做进一步优化,最终1.9仅仅选择了type alias,而不需要像Go 1.8中general alias那样引入新的操作符(=>)。这样,结合Go已实现的interchangeable constant、function、variable,外加type alias,Go终于在语言层面实现了对“Gradual code repair(渐进式代码重构)”理念的初步支持。

注:由于type alias的加入,在做Go 1.9相关的代码试验之前,最好先升级一下你本地编辑器/IDE插件(比如:vim-govscode-go)以及各种tools的版本。

官方对type alias的定义非常简单:

An alias declaration binds an identifier to the given type.

我们怎么来理解新增的type alias和传统的type definition的区别呢?

type T1 T2  // 传统的type defintion

vs.

type T1 = T2 //新增的type alias

把握住一点:传统的type definition创造了一个“新类型”,而type alias并没有创造出“新类型”。如果我们有一个名为“孙悟空”的类型,那么我们可以写出如下有意思的代码:

type  超级赛亚人  孙悟空
type  卡卡罗特 = 孙悟空

这时,我们拥有了两个类型:孙悟空超级赛亚人。我们以孙悟空这个类型为蓝本定义一个超级赛亚人类型;而当我们用到卡卡罗特这个alias时,实际用的就是孙悟空这个类型,因为卡卡罗特就是孙悟空,孙悟空就是卡卡罗特。

我们用几个小例子再来仔细对比一下:

1、赋值

Go强调“显式类型转换”,因此采用传统type definition定义的新类型在其变量被赋值时需对右侧变量进行显式转型,否则编译器就会报错。

//github.com/bigwhite/experiments/go19-examples/typealias/typedefinitions-assignment.go
package main

// type definitions
type MyInt int
type MyInt1 MyInt

func main() {
    var i int = 5
    var mi MyInt = 6
    var mi1 MyInt1 = 7

    mi = MyInt(i)  // ok
    mi1 = MyInt1(i) // ok
    mi1 = MyInt1(mi) // ok

    mi = i   //Error: cannot use i (type int) as type MyInt in assignment
    mi1 = i  //Error: cannot use i (type int) as type MyInt1 in assignment
    mi1 = mi //Error: cannot use mi (type MyInt) as type MyInt1 in assignment
}

而type alias并未创造新类型,只是源类型的“别名”,在类型信息上与源类型一致,因此可以直接赋值:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-assignment.go
package main

import "fmt"

// type alias
type MyInt = int
type MyInt1 = MyInt

func main() {
    var i int = 5
    var mi MyInt = 6
    var mi1 MyInt1 = 7

    mi = i // ok
    mi1 = i // ok
    mi1 = mi // ok

    fmt.Println(i, mi, mi1)
}

2、类型方法

Go1中通过type definition定义的新类型,新类型不会“继承”源类型的method set

// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-method.go
package main

// type definitions
type MyInt int
type MyInt1 MyInt

func (i *MyInt) Increase(a int) {
    *i = *i + MyInt(a)
}

func main() {
    var mi MyInt = 6
    var mi1 MyInt1 = 7
    mi.Increase(5)
    mi1.Increase(5) // Error: mi1.Increase undefined (type MyInt1 has no field or method Increase)
}

但是通过type alias方式得到的类型别名却拥有着源类型的method set(因为本就是一个类型),并且通过alias type定义的method也会反映到源类型当中:

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-method1.go
package main

type Foo struct{}
type Bar = Foo

func (f *Foo) Method1() {
}

func (b *Bar) Method2() {
}

func main() {
    var b Bar
    b.Method1() // ok

    var f Foo
    f.Method2() // ok
}

同样对于源类型为非本地类型的,我们也无法通过type alias为其增加新method:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-method.go
package main

type MyInt = int

func (i *MyInt) Increase(a int) { // Error: cannot define new methods on non-local type int
    *i = *i + MyInt(a)
}

func main() {
    var mi MyInt = 6
    mi.Increase(5)
}

3、类型embedding

有了上面关于类型方法的结果,其实我们也可以直接知道在类型embedding中type definition和type alias的差异。

// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-embedding.go
package main

type Foo struct{}
type Bar Foo

type SuperFoo struct {
    Bar
}

func (f *Foo) Method1() {
}

func main() {
    var s SuperFoo
    s.Method1() //Error: s.Method1 undefined (type SuperFoo has no field or method Method1)
}

vs.

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-embedding.go

package main

type Foo struct{}
type Bar = Foo

type SuperFoo struct {
    Bar
}

func (f *Foo) Method1() {
}

func main() {
    var s SuperFoo
    s.Method1() // ok
}

通过type alias得到的alias Bar在被嵌入到其他类型中,其依然携带着源类型Foo的method set

4、接口类型

接口类型的identical的定义决定了无论采用哪种方法,下面的赋值都成立:

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-interface.go
package main

type MyInterface interface{
    Foo()
}

type MyInterface1 MyInterface
type MyInterface2 = MyInterface

type MyInt int

func (i *MyInt)Foo() {

}

func main() {
    var i MyInterface = new(MyInt)
    var i1 MyInterface1 = i // ok
    var i2 MyInterface2 = i1 // ok

    print(i, i1, i2)
}

5、exported type alias

前面说过type alias和源类型几乎是一样的,type alias有一个特性:可以通过声明exported type alias将package内的unexported type导出:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-export.go
package main

import (
    "fmt"

    "github.com/bigwhite/experiments/go19-examples/typealias/mylib"
)

func main() {
    f := &mylib.Foo{5, "Hello"}
    f.String()            // ok
    fmt.Println(f.A, f.B) // ok

    // Error:  f.anotherMethod undefined (cannot refer to unexported field
    // or method mylib.(*foo).anotherMethod)
    f.anotherMethod()
}

而mylib包的代码如下:

package mylib

import "fmt"

type foo struct {
    A int
    B string
}

type Foo = foo

func (f *foo) String() {
    fmt.Println(f.A, f.B)
}

func (f *foo) anotherMethod() {
}

二、Parallel Complication(并行编译)

Go 1.8版本的gc compiler的编译性能虽然照比Go 1.5刚自举时已经提升了一大截儿,但依然有提升的空间,虽然Go team没有再像Go 1.6时对改进compiler性能那么关注。

在Go 1.9中,在原先的支持包级别的并行编译的基础上又实现了包函数级别的并行编译,以更为充分地利用多核资源。默认情况下并行编译是enabled,可以通过GO19CONCURRENTCOMPILATION=0关闭。

在aliyun ECS一个4核的vm上,我们对比了一下并行编译和关闭并行的差别:

# time GO19CONCURRENTCOMPILATION=0 go1.9beta2 build -a std

real    0m16.762s
user    0m28.856s
sys    0m4.960s

# time go1.9beta2 build -a std

real    0m13.335s
user    0m29.272s
sys    0m4.812s

可以看到开启并行编译后,gc的编译性能约提升20%(realtime)。

在我的Mac 两核pc上的对比结果如下:

$time GO19CONCURRENTCOMPILATION=0 go build -a std

real    0m16.631s
user    0m36.401s
sys    0m8.607s

$time  go build -a std

real    0m14.445s
user    0m36.366s
sys    0m7.601s

提升大约13%。

三、”./…”不再匹配vendor目录

自从Go 1.5引入vendor机制以来,Go的包依赖问题有所改善,但在vendor机制的细节方面依然有很多提供的空间。

比如:我们在go test ./…时,我们期望仅执行我们自己代码的test,但Go 1.9之前的版本会匹配repo下的vendor目录,并将vendor目录下的所有包的test全部执行一遍,以下面的repo结构为例:

$tree vendor-matching/
vendor-matching/
├── foo.go
├── foo_test.go
└── vendor
    └── mylib
        ├── mylib.go
        └── mylib_test.go

如果我们使用go 1.8版本,则go test ./…输出如下:

$go test ./...
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching    0.008s
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching/vendor/mylib    0.009s

我们看到,go test将vendor下的包的test一并执行了。关于这点,gophers们在go repo上提了很多issue,但go team最初并没有理会这个问题,只是告知用下面的解决方法:

$go test $(go list ./... | grep -v /vendor/)

不过在社区的强烈要求下,Go team终于妥协了,并承诺在Go 1.9中fix该issue。这样在Go 1.9中,你会看到如下结果:

$go test ./...
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching    0.008s

这种不再匹配vendor目录的行为不仅仅局限于go test,而是适用于所有官方的go tools。

四、GC性能

GC在Go 1.9中依旧继续优化和改善,大多数程序使用1.9编译后都能得到一定程度的性能提升。1.9 release note中尤其提到了大内存对象分配性能的显著提升。

在”go runtime metrics“搭建一文中曾经对比过几个版本的GC,从我的这个个例的图中来看,Go 1.9与Go 1.8在GC延迟方面的指标性能相差不大:

img{512x368}

五、其他

下面是Go 1.9的一些零零碎碎的改进,这里也挑我个人感兴趣的说说。

1、Go 1.9的新安装方式

go 1.9的安装增加了一种新方式,至少beta版支持,即通过go get&download安装:

# go get golang.org/x/build/version/go1.9beta2

# which go1.9beta2
/root/.bin/go18/bin/go1.9beta2
# go1.9beta2 version
go1.9beta2: not downloaded. Run 'go1.9beta2 download' to install to /root/sdk/go1.9beta2

# go1.9beta2 download
Downloaded 0.0% (15208 / 94833343 bytes) ...
Downloaded 4.6% (4356956 / 94833343 bytes) ...
Downloaded 34.7% (32897884 / 94833343 bytes) ...
Downloaded 62.6% (59407196 / 94833343 bytes) ...
Downloaded 84.6% (80182108 / 94833343 bytes) ...
Downloaded 100.0% (94833343 / 94833343 bytes)
Unpacking /root/sdk/go1.9beta2/go1.9beta2.linux-amd64.tar.gz ...
Success. You may now run 'go1.9beta2'

# go1.9beta2 version
go version go1.9beta2 linux/amd64

# go1.9beta2 env GOROOT
/root/sdk/go1.9beta2

go1.9 env输出支持json格式:

# go1.9beta2 env -json
{
    "CC": "gcc",
    "CGO_CFLAGS": "-g -O2",
    "CGO_CPPFLAGS": "",
    "CGO_CXXFLAGS": "-g -O2",
    "CGO_ENABLED": "1",
    "CGO_FFLAGS": "-g -O2",
    "CGO_LDFLAGS": "-g -O2",
    "CXX": "g++",
    "GCCGO": "gccgo",
    "GOARCH": "amd64",
    "GOBIN": "/root/.bin/go18/bin",
    "GOEXE": "",
    "GOGCCFLAGS": "-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build750457963=/tmp/go-build -gno-record-gcc-switches",
    "GOHOSTARCH": "amd64",
    "GOHOSTOS": "linux",
    "GOOS": "linux",
    "GOPATH": "/root/go",
    "GORACE": "",
    "GOROOT": "/root/sdk/go1.9beta2",
    "GOTOOLDIR": "/root/sdk/go1.9beta2/pkg/tool/linux_amd64",
    "PKG_CONFIG": "pkg-config"
}

2、go doc支持查看struct field的doc了

我们使用Go 1.8查看net/http包中struct Response的某个字段Status:

# go doc net/http.Response.Status
doc: no method Response.Status in package net/http
exit status 1

Go 1.8的go doc会报错! 我们再来看看Go 1.9:

# go1.9beta2 doc net/http.Response.Status
struct Response {
    Status string  // e.g. "200 OK"
}

# go1.9beta2 doc net/http.Request.Method
struct Request {
    // Method specifies the HTTP method (GET, POST, PUT, etc.).
    // For client requests an empty string means GET.
    Method string
}

3、核心库的变化

a) 增加monotonic clock支持

在2017年new year之夜,欧美知名CDN服务商CloudflareDNS出现大规模故障,导致欧美很多网站无法正常被访问。之后,Cloudflare工程师分析了问题原因,罪魁祸首就在于golang time.Now().Sub对时间的度量仅使用了wall clock,而没有使用monotonic clock,导致返回负值。而引发异常的事件则是新年夜际授时组织在全时间范围内添加的那个闰秒(leap second)。一般来说,wall clock仅用来告知时间,mnontonic clock才是用来度量时间流逝的。为了从根本上解决问题,Go 1.9在time包中实现了用monotonic clock来度量time流逝,这以后不会出现时间的“负流逝”问题了。这个改动不会影响到gopher对timer包的方法层面上的使用。

b) 增加math/bits包

在一些算法编程中,经常涉及到对bit位的操作。Go 1.9提供了高性能math/bits package应对这个问题。关于bits操作以及算法,可以看看经典著作《Hacker’s Delight》。这里就不举例了。

c) 提供了一个支持并发的Map类型

Go原生的map不是goroutine-safe的,尽管在之前的版本中陆续加入了对map并发的检测和提醒,但gopher一旦需要并发map时,还需要自行去实现。在Go 1.9中,标准库提供了一个支持并发的Map类型:sync.Map。sync.Map的用法比较简单,这里简单对比一下builtin map和sync.Map在并发环境下的性能:

我们自定义一个简陋的支持并发的类型:MyMap,来与sync.Map做对比:

// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark.go
package mapbench

import "sync"

type MyMap struct {
    sync.Mutex
    m map[int]int
}

var myMap *MyMap
var syncMap *sync.Map

func init() {
    myMap = &MyMap{
        m: make(map[int]int, 100),
    }

    syncMap = &sync.Map{}
}

func builtinMapStore(k, v int) {
    myMap.Lock()
    defer myMap.Unlock()
    myMap.m[k] = v
}

func builtinMapLookup(k int) int {
    myMap.Lock()
    defer myMap.Unlock()
    if v, ok := myMap.m[k]; !ok {
        return -1
    } else {
        return v
    }
}

func builtinMapDelete(k int) {
    myMap.Lock()
    defer myMap.Unlock()
    if _, ok := myMap.m[k]; !ok {
        return
    } else {
        delete(myMap.m, k)
    }
}

func syncMapStore(k, v int) {
    syncMap.Store(k, v)
}

func syncMapLookup(k int) int {
    v, ok := syncMap.Load(k)
    if !ok {
        return -1
    }

    return v.(int)
}

func syncMapDelete(k int) {
    syncMap.Delete(k)
}

针对上面代码,我们写一些并发的benchmark test,用伪随机数作为key:

// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark_test.go
package mapbench

import "testing"

func BenchmarkBuiltinMapStoreParalell(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            k := r.Intn(100000000)
            builtinMapStore(k, k)
        }
    })
}

func BenchmarkSyncMapStoreParalell(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            k := r.Intn(100000000)
            syncMapStore(k, k)
        }
    })
}
... ...

我们执行一下benchmark:

$go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map
BenchmarkBuiltinMapStoreParalell-4         3000000           515 ns/op
BenchmarkSyncMapStoreParalell-4            2000000           754 ns/op
BenchmarkBuiltinMapLookupParalell-4        5000000           396 ns/op
BenchmarkSyncMapLookupParalell-4          20000000            60.5 ns/op
BenchmarkBuiltinMapDeleteParalell-4        5000000           392 ns/op
BenchmarkSyncMapDeleteParalell-4          30000000            59.9 ns/op
PASS
ok      github.com/bigwhite/experiments/go19-examples/benchmark-for-map    20.550s

可以看出,除了store,lookup和delete两个操作,sync.Map都比我自定义的粗糙的MyMap要快好多倍,似乎sync.Map对read做了特殊的优化(粗略看了一下代码:在map read这块,sync.Map使用了无锁机制,这应该就是快的原因了)。

d) 支持profiler labels

通用的profiler有时并不能完全满足需求,我们时常需要沿着“业务相关”的执行路径去Profile。Go 1.9在runtime/pprof包、go tool pprof工具增加了对label的支持。Go team成员rakyll有一篇文章“Profiler labels in go”详细介绍了profiler labels的用法,可以参考,这里不赘述了。

六、后记

正在写这篇文章之际,Russ Cox已经在GopherCon 2017大会上做了”The Future of Go”的演讲,并announce Go2大幕的开启,虽然只是号召全世界的gopher们一起help and plan go2的设计和开发。同时,该演讲的文字版已经在Go官网发布了,文章名为《Toward Go 2》,显然这又是Go语言演化史上的一个里程碑的时刻,值得每个gopher为之庆贺。不过Go2这枚靴子真正落地还需要一段时间,甚至很长时间。当下,我们还是要继续使用和改善Go1,就让我们从Go 1.9开始吧^0^。

本文涉及的demo代码可以在这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:


如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多