标签 Kernel 下的文章

Ubuntu Server 14.04安装docker

近期在研究docker这一轻量级容器引擎,研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从 Ubuntu搬到了Mac Air上,对Mac OS X的一切均不甚熟悉,给docker研究带来了不便,于是打算在VirtualBox中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程,主要为了备忘,如果能给其他人带来帮助,我会甚感欣慰。

docker官方对ubuntu的支持是蛮好的。docker对Linux内核版本有要求,要>=3.8,Ubuntu Server目前最新版本14.04.1恰符合这一要求,其kernel version = 3.13.0-32。

一、VirtualBox安装Ubuntu Server 14.04.1

VirtualBox安装Ubuntu OS做过了不止一遍,即便是换成最新的14.04.1 Server版,差别也没有太多,无非是按照安装提示,逐步Next。这里给Ubuntu Server 14.04分配了1G Memory, 32G动态硬盘空间。

【配置源】

  默认情况下,/etc/apt/sources.list中只有一组源:cn.archive.ubuntu.com/ubuntu。这个国外源的下载速度显然无法满足我的要求,于是我把我常用的sohu源加入sources.list中,并且放在前面:

  deb http://mirrors.sohu.com/ubuntu/ trusty main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted

  deb-src http://mirros.sohu.com/ubuntu/ trusty main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-security main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted

  公司采用代理访问外网,于是还得在/etc/apt/apt.conf中加上代理的设置,否则无法更新源,也就无法安装第三方软件:

  Acquire::http::Proxy "http://username:passwd@proxyhost:proxyport";

 【乱码处理】

  由于安装时候选择了中国区域(locale zh_CN.UTF-8),因此在VirtualBox的窗口中直接执行命令的提示信息可能是乱码。对于Server,我们一般是不会直接通过其主机显示 器登录使用的,都是通过终端访问,但在未安装和开启ssh服务和未配置端口转发前,我们只能先凑合这个窗口了。可先将/etc/default /locale中的LANGUAGE由"zh_CN:zh"改为"en_US:en", logout后重新登录就可以看到非乱码的英文提示信息了。

【安装VirtualBox增强组件】

  Ubuntu Server默认是不安装图形桌面的,只有一个命令行窗口,连鼠标都无法使用。因此增强组件安装的意义没有桌面系统那么强烈。我能想到的只有“共享目录”这一个功能有些用处。

  安装方法也不难,按下面步骤逐步操作即可:

  sudo apt-get install build-essential linux-headers-$(uname -r) dkms gcc g++
  sudo mnt /dev/cdrom /mnt
  cd /mnt
  sudo bash ./VBoxLinuxAdditions.run

  如果结果都是"done",重启后就ok了。

【安装ssh服务】

    ssh服务由openssh-server提供:
    sudo apt-get openssh-server
   
   安装成功后,ssh server服务就会自动启动起来。

   不过我们还是需要修改一些配置,比如允许Root登录:打开/etc/ssh/sshd_config,将PermitRootLogin后面的内容改为yes。
   
【设置端口转发】

  前面说过,对于Server,我们更多是在其他主机上通过ssh或telnet远程访问该Server并执行各种操作。由于这里是VirtualBox安 装的虚拟机,其他主机无法看到这台Server,我们需要设置端口转发将外部访问的数据转发给这个内部虚拟Server。

  我们通过VirtualBox软件提供的图形界面即可完成这个操作:
    1、“设置”这个虚拟机
    2、在“网络”标签中,点击“端口转发”按钮,进入端口转发规则添加窗口。
    3、添加一条规则:
          名称:ssh-rules
          协议:TCP
          主机IP、子系统IP可以为空。
          主机端口:2222
          子系统端口:22
   4、配置结束

    配置结束后,我们在宿主机上netstat -an|grep 2222,可以看到VirtualBox增加了该端口2222的监听。

  现在我们就可以在其他机器上通过ssh -l tonybai 宿主机ip -p 2222的方式登录到我们新安装的这台虚拟Server了。

  
二、安装docker

docker目前的最新版本号是1.2.0,但14.04源中的docker还是正式稳定版1.0之前的版本,显然这是无法满足我的要求的。我们只能另外添加docker源来安装最新版docker。

  【安装docker】

    我们在/etc/apt/sources.list中加入下面这个源:
       deb http://mirror.yandex.ru/mirrors/docker/ docker main
  
    执行apt-get update。

    sudo apt-get install lxc-docker

正在读取软件包列表… 完成
正在分析软件包的依赖关系树      
正在读取状态信息… 完成      
将会安装下列额外的软件包:
  aufs-tools cgroup-lite git git-man liberror-perl lxc-docker-1.2.0
建议安装的软件包:
  git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk
  gitweb git-arch git-bzr git-cvs git-mediawiki git-svn
下列新软件包将被安装:
  aufs-tools cgroup-lite git git-man liberror-perl lxc-docker lxc-docker-1.2.0
升级了 0 个软件包,新安装了 7 个软件包,要卸载 0 个软件包,有 59 个软件包未被升级。
需要下载 7,477 kB 的软件包。
解压缩后会消耗掉 35.4 MB 的额外空间。
您希望继续执行吗? [Y/n] y

  这个源里的docker居然是最新版。于是安装之。安装后,我们执行docker version来确认一下安装是否成功。

  tonybai@ubuntu-Server-14:~$ docker version
Client version: 1.2.0
Client API version: 1.14
Go version (client): go1.3.1
Git commit (client): fa7b24f
OS/Arch (client): linux/amd64
2014/09/26 13:56:53 Get http:///var/run/docker.sock/v1.14/version: dial unix /var/run/docker.sock: permission denied

  【为docker设置http代理】

    在公司内使用代理才能访问到外网,于是我们也需要为docker命令设置代理以使其顺利执行命令。

    我们安装的docker实际上分为两部分,docker命令行和docker daemon。两者是C/S结构,docker命令行将用户的请求转发给docker daemon,后者会真正与外部通信完成各种操作。

    于是我们可以这样为docker daemon设置http_proxy:
    sudo service docker stop
    sudo http_proxy='http://user:passwd@proxyhost:port' docker -d &

    这样设置启动后,我们可以通过下面命令测试设置是否ok:

      sudo docker search ubuntu

    如果你看到下面信息,说明设置成功了:

    tonybai@ubuntu-Server-14:~$ sudo docker search ubuntu
[info] GET /v1.14/images/search?term=ubuntu
[b36518a9] +job search(ubuntu)
[b36518a9] -job search(ubuntu) = OK (0)
NAME                                             DESCRIPTION                                     STARS                                   OFFICIAL   AUTOMATED
ubuntu                                           Official Ubuntu base image                      709                                     [OK]      
dockerfile/ubuntu                                Trusted automated Ubuntu (http://www.ubunt…   24                                                 [OK]
crashsystems/gitlab-docker                       A trusted, regularly updated build of GitL…   20                                                 [OK]
ubuntu-upstart                                   Upstart is an event-based replacement for …   13                                      [OK]      

… ….

 

为阻塞型函数调用添加超时机制

我们产品中的一个子模块在进行Oracle实时数据库查询时,常常因数据库性能波动或异常而被阻塞在OCI API的调用上,为此我们付出了“惨痛”的代价。说来说去还是我们的程序设计的不够完善,在此类阻塞型函数调用方面缺少微小粒度的超时机制。

调用阻塞多发生在I/O操作(磁盘、网络、低速设备)、第三方API调用等方面。对于文件/网络I/O操作,我们可利用在非阻塞文件描述符上select /poll的超时机制来替代针对阻塞型文件描述符的系统调用;但在第三方API方面,多数时候是无法用select/poll来进行超时的,我们可以选择 另外一种方法:利用setjmp和longjmp的非局部跳转机制来为特定阻塞调用添加超时机制。其原理大致是:利用定时器(alarm、setitimer)设置超时时间,在SIGALRM的handler中利用longjmp跳到阻塞型调用之前,达到超时跳出阻塞型函数调用的效果。同时这种方法通用性更好些。

这个机制实现起来并不难,但有些细节还是要考虑周全,否则很容易出错。我们的产品是需要运行在LinuxSolaris两个平台下的,因此机制的实现还要考虑移植性的问题。下面简要说说在实现这一机制过程中出现的一些问题与解决方法。

一、第一版

考虑到阻塞型函数的原型各不相同,且我们的产品中对阻塞调用有重试次数的要求,因此打算将这个机制包装成一个,大致是这个模样:

#define add_timeout_to_func(func, n, interval, ret, …) \…

其中func是函数名;n是重试的次数;interval是超时的时间,单位是秒;ret是函数成功调用后的返回值,若失败,也是这个宏的返回值。

我们可以像下面这样使用这个宏:

/* example.c */
int
main()
{
    #define MAXLINE 1024
    char line[MAXLINE];

    int ret = 0;
    int try_times = 3;
    int interval = 1000;
    add_timeout_to_func(read, try_times, interval, ret, STDIN_FILENO, line, MAXLINE);
    if (ret == E_CALL_TIMEOUT) {
        printf("invoke read timeouts for 3 times\n");
        return -1;
    } else if (ret == 0) {
        printf("invoke read ok\n");
        return 0;
    } else {
        printf("add_timeout_to_func error = %d\n", ret);
    }
}

add_timeout_to_func中为阻塞型函数添加的超时机制是利用setjmp/longjmp与信号的处理函数合作完成的。

/* timeout_wrapper.h */
 

#include <setjmp.h>
#include <stdarg.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

extern volatile int invoke_count;
extern jmp_buf invoke_env;

void timeout_signal_handler(int sig);
typedef void (*sighandler_t)(int);
#define E_CALL_TIMEOUT (-9)

#define add_timeout_to_func(func, n, interval, ret, ...) \
    { \
        invoke_count = 0; \
        sighandler_t h = signal(SIGALRM, timeout_signal_handler); \
        if (h == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigjmp(invoke_env) != 0) { \
            if (invoke_count >= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        alarm(interval);\
        ret = func(__VA_ARGS__);\
        alarm(0); \
err:\
        signal(SIGALRM, h);\
end:\
        ;\
    }

/* timeout_wrapper.c */
#include "timeout_wrapper.h"

volatile int invoke_count = 0;
jmp_buf invoke_env;

void
timeout_signal_handler(int sig)
{
    invoke_count++;
    longjmp(invoke_env, 1);
}

编译运行这个程序,分别在Solaris、Linux下运行,遗憾的是两个平台下都以失败告终。

先说一下在Linux下的情况。在Linux下,程序居然不响应第二次SIGALRM信号了。通过strace也可以看出,当alarm被第二次调用后, 系统便阻塞在了read上,没有实现为read增加超时机制的目的。原因何在呢?我在《The Linux Programming Interface》一书中找到了原因。原因大致是这样的,我们按照代码的执行流程来分析:

* add_timeout_to_func宏首先设置了信号的handler,保存了env信息(setjmp),调用alarm设置定时器,然后阻塞在read调用上;
* 1s后,定时器信号SIGALRM产生,中断发生,代码进入信号处理程序,即timeout_signal_handler; Linux上的实现是当进入处理程序时,内核会自动屏蔽对应的信号(SIGALRM)以及此时act.sa_mask字段中的所有信号;在离开 handler后,内核取消这些信号的屏蔽。
* 问题在于我们是通过longjmp调用离开handler的,longjmp对应的invoke_env是否在setjmp时保存了这些被屏蔽的信号呢? 答案是:在Linux上没有。这样longjmp跳到setjmp后也就无法恢复对SIGALRM的屏蔽;当再次产生SIGALRM信号时,程序将无法处 理,也就一直阻塞在read调用上了。

解决方法:将setjmp/longjmp替换为sigsetjmp和siglongjmp,后面这组调用在sigsetjmp时保存了屏蔽信号,这样在 siglongjmp返回时可以恢复到handler之前的信号屏蔽集合,也就是说SIGALRM恢复自由了。在Solaris 下,setjmp/longjmp是可以恢复被屏蔽的信号的。

再说说在Solaris下的情况。在Solaris下,程序在第二次SIGALRM到来之际,居然退出了,终端上显示:“闹钟信号”。这是因为在 Solaris下,通过signal函数设置信号的处理handler仅是一次性的。在应对完一次信号处理后,信号的handler被自动恢复到之前的处 理策略设置,对于SIGALRM来说,也就是程序退出。解决办法:通过多次调用signal设置handler或通过sigaction来长效设置 handler。考虑到移植性和简单性,我们选择了sigaction。在Linux平台下,signal函数底层就是用sigaction实现的,是简洁版的sigaction,因此它的设置不是一次性的,而是长效的。

二、第二版

综上问题的修改,我们有了第二版代码。

/* timeout_wrapper.h */

extern volatile int invoke_count;
extern sigjmp_buf invoke_env;

void timeout_signal_handler(int sig);
typedef void sigfunc(int sig);
sigfunc *my_signal(int signo, sigfunc* func);
#define E_CALL_TIMEOUT (-9)

#define add_timeout_to_func(func, n, interval, ret, …) \
    { \
        invoke_count = 0; \
        sigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \
        if (sf == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigsetjmp(invoke_env, SIGALRM) != 0) { \
            if (invoke_count >= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        alarm(interval); \
        ret = func(__VA_ARGS__);\
        alarm(0); \
err:\
        my_signal(SIGALRM, sf); \
end:\
        ;\
    }

/* timeout_wrapper.c */

volatile int invoke_count = 0;
sigjmp_buf invoke_env;

void
timeout_signal_handler(int sig)
{
    invoke_count++;
    siglongjmp(invoke_env, 1);
}

sigfunc *
my_signal(int signo, sigfunc *func)
{
    struct sigaction act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
#endif
    } else {
#ifdef SA_RESTART
        act.sa_flags |= SA_RESTART;
#endif
    }
    if (sigaction(signo, &act, &oact) < 0)
        return SIG_ERR;
    return oact.sa_handler;
}

这里从《Unix高级环境编程》中借了一段代码,就是那段my_signal的实现。这样修改后,程序在Linux和Solaris下工作都蛮好的。但目前唯一的缺点就是超时时间粒度太大,alarm仅支持秒级定时器,我们至少要支持毫秒级,接下来我们要换掉alarm。

三、第三版

setitimer与alarm是同出一门,共享一个定时器的。不同的是setitimer可以支持到微秒级的粒度,因此我们就用setitimer替换alarm,第三版仅改动了add_timeout_to_func这个宏:

#define add_timeout_to_func(func, n, interval, ret, …) \
    { \
        invoke_count = 0; \
        sigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \
        if (sf == SIG_ERR) { \
            ret = errno; \
            goto end; \
        }  \
\
        if (sigsetjmp(invoke_env, SIGALRM) != 0) { \
            if (invoke_count >= n) { \
                ret = E_CALL_TIMEOUT; \
                goto err; \
            } \
        } \
\
        struct itimerval tick;  \
        struct itimerval oldtick;  \
        tick.it_value.tv_sec = interval/1000; \
        tick.it_value.tv_usec = (interval%1000) * 1000; \
        tick.it_interval.tv_sec = interval/1000; \
        tick.it_interval.tv_usec = (interval%1000) * 1000; \
\
        if (setitimer(ITIMER_REAL, &tick, &oldtick) < 0) { \
            ret = errno; \
            goto err; \
        } \
\
        ret = func(__VA_ARGS__);\
        setitimer(ITIMER_REAL, &oldtick, NULL); \
err:\
        my_signal(SIGALRM, sf); \
end:\
        ;\
    }

至此,一个为阻塞型函数调用添加的超时机制的雏形基本实现完毕了,但要放在产品代码里还需要更细致的打磨。至少目前只是在单进程单线程中跑过,而且要求每个函数中只能调用add_timeout_to_func一次,否则就会有编译错误。

以上完整代码我都放到github上的experiments repository中了,有兴趣的朋友可以下载细看。

关于编程语言学习的一些体会

Learn at least one new language every year.
                                              — Andy Hunt and Dave Thomas

自己一直是“每年学习一门新语言”的忠实拥趸,曾先后认真地学习了HaskellCommon LispPythonGo等语言,对PrologScalaErlangLuaPHP也有一定了解。但几年下来,只有Python一门语言算 是真正被留在我的大脑里,用在了工作中。其他那几门语言留下来的只是一些思想了。这似乎符合了Andy Hunt和Dave Thomas在《程序员修炼之道》中对于这一实践目的的阐述:“学会用多种方式解决问题,扩展我们的视野,避免思路僵化和停滞不前”^_^。

即便是残存的思想,其实也并不深刻。要真正会运用新思维并非那么简单。一门编程语言从入门到精通,至少要经历学语法、做实践、用idioms(写出地道的代码)三个阶段。这让我深刻的感悟到:不以使用为目的的语言学习,都是在浪费生命

有精力多学习些语言自然很好,我迫切期待能拥有一个像“七龙珠”中孙悟空那样的“精神时光屋”呢。但现实中,人的精力是有限的,而我们要面对的计算机科学领域中的知识、技能以及问题却似乎是无限的。因此在“每年至少学习一门新语言”这一实践上,建议不要过于教条。 从编程语言自身来看,范型(Paradigm)是影响语言思维差异的主要因素,而编程语言的范型有限,主流的也就那么几种:命令式(过程式)、函数式、逻 辑式、面向对象等。每种范型的背后都有几种、十几种甚至几十种语言,我们其实没有必要都去学。从拓展视野的角度去说,从每种主流范式中找到一两门典型的语 言去学习就可以了。比如命令式的,我们可以选择C;函数式我们选择Haskell;逻辑式的选择Prolog;面向对象的选择Java等。

即便是从每个范型中挑出一门,你要付出的精力依旧不少,我们还要考虑其实用性:要以使用为目的。如果能将其用在工作中,天天与你相伴,被他人接受,自然最 好;退而求其次,你能找到一两个开源项目,并参与其中也是可以的,至少可以让你保持手热;如果这两点都无法做到,仅仅是凭借个人的热情与坚持,那是不会持 久的,若干时间后,你就会对其生疏,可能连基本的"Hello World"语法都记不得了。不过这个年头,思想也不能不要。在有剩余精力的前提下,挑选些牛人们极力“鼓吹”的语言,吸收一下其思想精华,说不定哪天就 能用得上,让自己和大家都感觉你很NB,抬高一下自己的身价^_^。记住:编程语言也是要拼爹的系出名门的语言(诸如Go、Dart等)自然得到更多的青睐、使用和推广,出位的几率也就高出许多,尤其是在目前新编程语言百花齐放的阶段。因此在选择有思想的新语言时,最好在这些名门之后中做优选。

这个时代喜欢“专家”,因此我们在一两门语言上务必要做到“精专”,这是会给你带来黄油和面包的语言。要专到什么程度呢?我有一个同事,什么问题都用C解决。他甚至为此写了个不小的基础框架,所有业务问题的Code放在框架中被回调即可,即便是这个问题用Python实现只需几行代码。

计算机科学的研究核心是什么?我想肯定不是编程语言,就好比社会科学研究的核心不是人类语言一样。我比较欣赏这样的观点:作为程序员而言,最重要的是去创造,而不是研究。我们应更多的利用已经掌握的语言解决现实中的问题。做 编程语言研究的人可能要了解各种语言的特点与实现方式,但对于大多数的程序员来说,其实我们只需要关注问题域:做底层平台开发的,关注机器模型、通信原理 以及OS原理和实现细节;做算法的,很荣幸,那才是正统的程序设计的核心;前端攻城师则更多关注用户的体验。而在这些解决实际问题的过程中,我们更多采用 的是“制式”的编程语言。即做平台开发的,一般用C,C++等系统编程语言,更多的考虑的是性能;做前端开发的,PHP/JavaScript不可或缺。 我们要考虑的是如何利用这些制式的编程语言去解决问题,而在这些制式语言上,我们要做到精通。

从新兴语言中借鉴新思想,然后在旧语言中实现新语言的特性,其实更多是在旧语言中实现了某 种语法糖,你爱吃,不代表其他人也理解也爱吃,还容易被人误认为是“炫技”。如果你是技术负责人,且经过评估,新语言十分适合这个问题域,那莫不入直接引 入这门语言,让大家都能使用到这门语言的新思想、新特性。

辩证的说,任何一种编程语言都有其利与弊,比如Haskell,纯函数式语言,变量不能改变,无状态,对并行处理具有天然的适应性,但在处理基本IO时却要编写难于理解的monad;而在命令式语言中,这种IO处理简直简单的不得了。

关于函数式语言,个人感觉未来若干年内仍难以大行其道,建议还是跟上命令式语言的演化主线吧。

跨越问题域学习语言,通常收获不大。一个做平台服务端,用惯了C的资深程序员,让他去学PHP写前端代码,估计是无法迸发出任何火花的。

以上是自己这些年关于编程语言学习的一些体会,比较零散,但希望能有帮助。




这里是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


文章

评论

  • 正在加载...

分类

标签

归档











更多