Like的世界

个人总结与随想

基于Python multiprocessing的Actor模型

| Comments

虽然基于Gevent的Actor基于Python 3.5异步的Actor都支持并发(concurrent)计算(仅运行于单进程中),但是不支持并行(parallel)计算,即无法利用多核。

Python内置的multiprocessing模块不仅支持并行计算,而且与Gevent接口相似。所以,模仿Gevent的Actor实现multiprocessing的Actor并不困难。

multiprocessing的Actor实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from multiprocessing import Process, Queue

try:
    from Queue import Empty
except ImportError:
    from queue import Empty


class Actor(Process):
    def __init__(self, receive_timeout=None):
        Process.__init__(self)
        self.inbox = Queue()
        self.receive_timeout = receive_timeout

    def send(self, message):
        self.inbox.put_nowait(message)

    def receive(self, message):
        raise NotImplemented()

    def handle_timeout(self):
        pass

    def run(self):
        self.running = True
        while self.running:
            try:
                message = self.inbox.get(True, self.receive_timeout)
            except Empty:
                self.handle_timeout()
            else:
                self.receive(message)

基于message的扩展

将并行Actor扩展为发布-订阅者模式,基本与Gevent的实现一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from message import observable

from actor import Actor


@observable
class Publisher(Actor):
    def __init__(self, subject, receive_timeout=None):
        self.subject = subject
        Actor.__init__(self, receive_timeout)

    def subcribe(self, observer):
        self.sub(self.subject, observer.send)

    def publish(self, message):
        self.pub(self.subject, message)

基于Publisher实现Ping-Pong,与Gevent的实现差异也不大。

不同的是它实际启动3个进程。除主进程外,每个actor分别运行于独立进程,从而实现多核计算。主进程监督2个actor进程运行,如启动、停止以及异常处理等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import time

from publisher import Publisher


class Pinger(Publisher):
    def receive(self, message):
        print(message)
        time.sleep(2)
        self.publish('ping')

    def handle_timeout(self):
        print('pinger timeout')


class Ponger(Publisher):
    def receive(self, message):
        print(message)
        time.sleep(2)
        self.publish('ping')

    def handle_timeout(self):
        print('ponger timeout')


ping = Pinger('evt.ping', 1)
pong = Ponger('evt.pong', 1)

ping.subcribe(pong)
pong.subcribe(ping)
ping.start()
pong.start()

ping.publish('start')

pong.join()
ping.join()

基于Python 3.5异步的Actor模型

| Comments

Python 3.5异步模型

Python 3.5推出了async/await语法,在语法层面简化了异步编程。官方库asyncio是应用async/await的途径。

Ubuntu 16.04默认安装Python 3.5,或者通过pyenv安装它。

异步Actor的实现

基于asyncio,可以实现async actor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import asyncio


class Actor(object):
    def __init__(self):
        self.inbox = asyncio.Queue()

    def send(self, message):
        self.inbox.put_nowait(message)

    async def receive(self, message):
        raise NotImplemented()

    async def run(self):
        self.running = True

        while self.running:
            message = await self.inbox.get()
            await self.receive(message)

上述代码的关键是通过asyncio.Queue异步接收消息,并异步处理接收到的消息。

通过这个类,实现Ping-Pong示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import asyncio

from actor import Actor


class Pinger(Actor):
    async def receive(self, message):
        print(message)
        pong.send('ping')
        await asyncio.sleep(3)


class Ponger(Actor):
    async def receive(self, message):
        print(message)
        ping.send('pong')
        await asyncio.sleep(3)


ping = Pinger()
pong = Ponger()

ping.send('start')

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait((ping.run(), pong.run())))
loop.close()

该示例代码中,actor之间同步发送消息(asyncio.Queue.put_nowait),由于运行在单线程上,并不存在竞争。

接收消息超时(timeout)

某些应用场景需要周期性激活Actor,当Actor没有收到任何消息时。

基于上述代码,利用asyncio.wait_for的超时功能来实现接收消息超时。如此,进一步加强Actor的并发能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import asyncio


class Actor(object):
    def __init__(self, receive_timeout=None):
        self.inbox = asyncio.Queue()
        self.receive_timeout = receive_timeout

    def send(self, message):
        self.inbox.put_nowait(message)

    async def receive(self, message):
        raise NotImplemented()

    async def handle_timeout(self):
        pass

    async def run(self):
        self.running = True

        while self.running:
            try:
                message = await asyncio.wait_for(self.inbox.get(),
                                                 self.receive_timeout)
            except asyncio.TimeoutError:
                await self.handle_timeout()
            else:
                await self.receive(message)

基于message的扩展

由于message仅支持Python 2,而且Google Code已经停止服务。

基于原代码基础上,我在GitHub创建python-message,并扩展支持Python 3.

新版本message,也可以通过pip安装:

1
pip install https://github.com/likema/python-message/archive/master.zip

在此基础上,将异步Actor扩展为发布-订阅者模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from message import observable

from actor import Actor


@observable
class Publisher(Actor):
    def __init__(self, subject, receive_timeout=None):
        self.subject = subject
        Actor.__init__(self, receive_timeout)

    def subcribe(self, observer):
        self.sub(self.subject, observer.send)

    def publish(self, message):
        self.pub(self.subject, message)

基于Publisher实现Ping-Pong,从而解耦发送者与接收者,且支持发送者发送1条消息时,多个接收者接收同1条消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import asyncio

from publisher import Publisher


class Pinger(Publisher):
    async def receive(self, message):
        print(message)
        await asyncio.sleep(3)
        self.publish('ping')

    async def handle_timeout(self):
        print('Pinger timeout')


class Ponger(Publisher):
    async def receive(self, message):
        print(message)
        await asyncio.sleep(3)
        self.publish('pong')

    async def handle_timeout(self):
        print('Ponger timeout')


ping = Pinger('evt.ping', 1)
pong = Ponger('evt.pong', 1)

ping.subcribe(pong)
pong.subcribe(ping)
ping.publish('start')

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait((ping.run(), pong.run())))
loop.close()

存在的问题

相比Gevent实现的Actor,异步Actor并不透明支持所有的I/O函数,它仅支持基于asyncio实现的库,如aiohttp

参考

nvm简介——Debian/Ubuntu中管理多版本Node.js

| Comments

nvm是管理Node.js版本的工具,它支持在多个Node.js版本间切换。

一、安装nvm

1
2
3
git clone https://github.com/creationix/nvm.git ~/.nvm
cd ~/.nvm
git checkout `git describe --abbrev=0 --tags

激活nvm

1
. $NVM_DIR/nvm.sh

为了每次登录后自动激活nvm,需要将NMV_DIRnvm.sh和补齐加入bash的~/.bashrc(或zsh的~/.zshrc)

1
2
3
export NVM_DIR=~/.nvm
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
[ -r $NVM_DIR/bash_completion ] && . $NVM_DIR/bash_completion

二、nvm常用命令

列表可安装的Node.js版本

1
nvm ls-remote

除了Node.js官方版本,还支持io.js

安装指定版本的Node.js

1
nvm install 6.2.1

它会自动下载指定版本的Node.js二进制包(不需要编译源码),安装在~/.nvm/versions/node

卸载指定版本的Node.js

1
nvm uninstall 6.2.1

设置shell的Node.js版本

1
nvm use 6.2.1

它将Node.js指定版本的bin路径加入PATH.

还原环境变量PATH

1
nvm deactivate

迁移npm至新版本的Node.js

1
nvm install node --reinstall-packages-from=node

1
nvm install v6.2.1 --reinstall-packages-from=5.0

.nvmrc

它存储在工程根目录中,用于记录该工程依赖的Node.js版本

1
echo 6.2.1 > .nvmrc

进入工程目录(当前目录),运行

1
nvm use

将根据.nvmrc指定shell的Nodejs版本

三、升级nvm

1
2
3
cd $NVM_DIR
git fetch origin
git checkout `git describe --abbrev=0 --tags`

升级完成后,需要重新激活nvm

1
. $NVM_DIR/nvm.sh

pyenv简介——Debian/Ubuntu中管理多版本Python

| Comments

pyenv是管理Python版本的工具,它支持在多个Python版本间切换。

一、安装pyenv

1
git clone https://github.com/yyuu/pyenv.git ~/.pyenv

PYENV_ROOTpyenv init加入bash的~/.bashrc(或zsh的~/.zshrc)

1
2
3
echo 'export PATH=~/.pyenv/bin:$PATH' >> ~/.bashrc
echo 'export PYENV_ROOT=~/.pyenv' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc

二、pyenv常用命令

列表可安装的Python版本

1
pyenv install -l

除了Python官方版本,还支持

  • anaconda
  • ironpython
  • jython
  • miniconda
  • pypy
  • stackless

安装指定版本的Python

1
2
pyenv install 3.5.1
pyenv rehash

它会自动下载并编译指定版本的Python源码,这需要系统安装:

1
sudo apt-get install -y build-essential zlib1g-dev libssl-dev

还可选择安装:

1
sudo apt-get install libsqlite3-dev libbz2-dev  libreadline-dev

安装完成后:

  • 源码(如~/Python-3.5.1.tar.gz)缓存在.pyenv/cache目录中,在安装完后可手动删除。
  • Python版本安装在~/.pyenv/versions目录中。

卸载指定版本的Python

1
pyenv unstall 3.5.1

设置shell的Python版本

1
pyenv shell 3.5.1

等同于

1
export PYENV_VERSION=3.5.1

清除PYENV_VERSION

1
pyenv shell --unset

三、安装pyenv-virtualenv

pyenv-virtual是pyenv的插件,它支持管理多个virtualenv

1
2
git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile

创建virtualenv

1
pyenv virtualenv 3.5.1 aiohttp-virtual-env
  • 创建aiohttp-virtual-env之前,须先安装Python 3.5.1(通过系统或pyenv安装)。
  • aiohttp-virtual-env存储在~/.pyenv/versions/3.5.1/envs目录中,且在~/.pyenv/versions目录中建立同名符号链接。

删除virtualenv

1
pyenv uninstall aiohttp-virtual-env

列表virtualenv

1
pyenv virtualenvs

激活/禁用virtualenv

1
2
pyenv activate aiohttp-virtual-env
pyenv deactivate

扩展Python Gevent的Actor模型

| Comments

什么是Actor模型?

Actor模型中文版)是一种基于消息传递(message-passing)的并发(concurrent)计算模型。

它与OOP异同:

  • 它推崇“一切皆为Actor”,而OOP推崇“一切皆为Object”
  • 表面上,Actor通过发送消息与其他Actor通信,OOP的Object通过发送消息与其他Object通信。实际上,前者为发送结构化的数据,而后者为调用对方的方法。
  • 它的发送者与已经发送的消息解耦,它允许进行异步通信,从而实现发送者与接收者并发执行。而OOP的方法调用者(发送者)与方法被调用者(接收者)通常顺序执行,而且调用者与被调用者通常具有较强的耦合。
  • 它的消息接收者是通过地址区分的,有时也被称作“邮件地址”。而OOP的Object通过引用(地址)来区分。
  • 它着重消息传递,而OOP着重于类与对象。

Gevent的Actor实现

gevent中文版)是一个基于libev的并发库,它为各种并发和网络相关的任务提供了整洁的API。

Actors中文版)章节已介绍了如何基于Greenlet和 Queue实现

该实现存在的问题:发送者与接收者紧耦合,发送者持有接收者的对象引用。

解决办法

在此基础上,我利用message库将其扩展为发布-订阅者模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from gevent.queue import Queue
from gevent import Greenlet
from message import observable


class Actor(Greenlet):
    def __init__(self):
        self.inbox = Queue()
        Greenlet.__init__(self)

    def send(self, message):
        self.inbox.put(message)

    def receive(self, message):
        raise NotImplemented()

    def _run(self):
        self.running = True

        while self.running:
            message = self.inbox.get()
            self.receive(message)


@observable
class Publisher(Actor):
    def __init__(self, subject):
        self.subject = subject
        Actor.__init__(self)

    def subcribe(self, observer):
        self.sub(self.subject, observer.send)

    def publish(self, message):
        self.pub(self.subject, message)

如此,不仅将发送者与接收者解耦,而且支持发送者发送1条消息时,多个接收者接收同1条消息。

类似Ping-Pong的示例,Pinger对象订阅了Ponger对象的evt.pong事件,Ponger对象订阅Pinger对象的evt.ping事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import gevent


class Pinger(Publisher):
    def receive(self, message):
        print(message)
        self.publish('ping')
        gevent.sleep(1)


class Ponger(Publisher):
    def receive(self, message):
        print(message)
        self.publish('pong')
        gevent.sleep(1)


ping = Pinger('evt.ping')
pong = Ponger('evt.pong')

ping.subcribe(pong)
pong.subcribe(ping)
ping.start()
pong.start()

ping.publish('start')
gevent.joinall([ping, pong])

接收消息超时(timeout)

某些应用场景需要周期性激活Actor,当Actor没有收到任何消息时。

基于上述代码,利用gevent.queue.get的超时功能来实现接收消息超时。如此,进一步加强Actor的并发能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from gevent.queue import Queue, Empty
from gevent import Greenlet
from message import observable


class Actor(Greenlet):
    def __init__(self, receive_timeout=None):
        self.inbox = Queue()
        self.receive_timeout = receive_timeout
        Greenlet.__init__(self)

    def send(self, message):
        self.inbox.put(message)

    def receive(self, message):
        raise NotImplemented()

    def handle_timeout(self):
        pass

    def _run(self):
        self.running = True

        while self.running:
            try:
                message = self.inbox.get(True, self.receive_timeout)
            except Empty:
                self.handle_timeout()
            else:
                self.receive(message)


@observable
class Publisher(Actor):
    def __init__(self, subject, receive_timeout=None):
        self.subject = subject
        Actor.__init__(self, receive_timeout)

    def subcribe(self, observer):
        self.sub(self.subject, observer.send)

    def publish(self, message):
        self.pub(self.subject, message)

SSH翻墙集群

| Comments

SSH动态代理是国内较为常见的翻墙方法。正如SSH隧道简介所说,它存在不少有优点。

然而在实际使用中,它存在如下缺点:

  • 与PPTP等VPN协议相比,它的连接不稳定。前者应该具备协议级断线重传机制。
  • 基于廉价VPS,导致它的连接不稳定。而且廉价VPS容易掉线,有时需要用户自己找在线客户修复,进一步延长了掉线时间。
  • 由于上述缺点,不适合小型公司多人使用。

在大概2年前,我摸索出SSH动态代理集群的办法。并将之部署于我所服务的公司,成功负载了20-30人日常翻墙学习与工作的需求。

原理

SSH动态代理,即为SOCK5代理,所以我们需要的是SOCK5集群。

若搜索socks 5 load balance不难发现一些有用信息:

What is the best way to load balance multiple sock5 proxys on seperate VM’s in the same datacenter?

我将分别介绍3种方法搭建SOCK5集群:

  1. 利用第三方模块nginx_tcp_proxy_module
  2. Nginx 1.9开始支持TCP Load Balancing
  3. HAProxy

关于SSH动态代理的配置方法,请参看AutoSSH简介

nginx_tcp_proxy_module的配置方法

Ubuntu的Nginx并没有将nginx_tcp_proxy_module编译进去。为了简化安装,我基于Ubuntu的Nginx包,做了Nginx的PPA:

  • 升级Nginx版本
  • 加入nginx_tcp_proxy_module

添加我的PPA

1
2
sudo add-apt-repository ppa:likemartinma/net
sudo apt-get -y update

若未安装nginx,则

1
sudo apt-get install -y nginx

若已安装nginx,则

1
sudo apt-get -y upgrade

在/etc/nginx/nginx.conf中,增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tcp {
    access_log /var/log/nginx/tcp_access.log;

    upstream ssh_cluster {
        # simple round-robin
        server 127.0.0.1:12345;
        server 127.0.0.1:12346;
        server 127.0.0.1:12347;

        check interval=3000 rise=2 fall=5 timeout=1000;
    }

    server {
        listen 9999;
        proxy_pass ssh_cluster;
    }
}

为了查看集群的状态,在/etc/nginx/sites-enabled/default的中,增加如下内容:

1
2
3
4
5
6
7
server {
    ...

    location /status {
        tcp_check_status;
    }
}

重启Nginx:

1
service nginx restart

如此,访问http://<cluster IP>/status将能查看集群的详细状态。

Nginx 1.9的配置方法

Ubuntu 15.10之前的官方Nginx版本都小于1.9,须通过ppa:nginx/development升级nginx。

添加ppa:nginx/development

1
2
sudo add-apt-repository ppa:nginx/development
sudo apt-get -y update

若未安装nginx,则

1
sudo apt-get install -y nginx

若已安装nginx,则

1
sudo apt-get -y upgrade

在/etc/nginx/nginx.conf中,增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
stream {
    upstream ssh_cluster {
        least_conn;
        server 127.0.0.1:12345;
        server 127.0.0.1:12346;
        server 127.0.0.1:12347;
    }

    server {
        listen 9999;
        proxy_pass ssh_cluster;
    }
}

重启Nginx:

1
service nginx restart

HAProxy的配置方法

安装haproxy

1
sudo apt-get install -y haproxy

在/etc/haproxy/haproxy.cfg中,增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
frontend socks5
    mode tcp
    bind *:9999
    default_backend ssh_cluster

backend ssh_cluster
    mode tcp
    balance roundrobin
    server vps1 127.0.0.1:12345 weight 1 check inter 30000
    server vps2 127.0.0.1:12346 weight 1 check inter 30000
    server vps3 127.0.0.1:12347 weight 1 check inter 30000

为了查看集群的状态,在/etc/haproxy/haproxy.cfg中,增加如下内容:

1
2
3
4
5
listen stats :9090
    balance
    mode http
    stats enable
    stats auth admin:admin

默认安装,haproxy处于不活动状态,须要激活它。

在/etc/default/haproxy中,修改如下行:

1
ENABLED=1

最后,启动haproxy:

1
service haproxy start

如此,访问http://<cluster IP>:9090/haproxy?stats将能查看集群的详细状态。

总结

  • nginx_tcp_proxy_module有简单的集群状态页面。
  • nginx 1.9没有集群状态查页面,仅能通过错误日志/var/log/ngnix/error.log来查看掉线的集群节点。
  • haproxy不仅有完善的集群状态页面,而且不需要任何PPA,应该是最佳选择。
  • 上述3种方法都缺乏认证机制,只能部署于家庭或企业内网。当然也可以部署于个人电脑,事实上,我就是这样使用的。

AutoSSH简介

| Comments

autossh (Automatically restart SSH sessions and tunnels),它在运行的时候启动一个SSH进程,并监控该进程的健康状况;当SSH进程崩溃或停止通信时,它将重启动SSH进程。

命令选项

1
autossh [-V] [-M port[:echo_port]] [-f] [SSH_OPTIONS]
  • -M port[:echo_port] 指定监控端口(和echo端口,默认为前者加1)。

    • 若希望使远程标准inetd的echo服务(默认端口为7),则指定echo_port,仅需服务监听地址为localhost。
    • 若port设置为0,则将禁用监控功能。仅在ssh退出后重启它。
  • -f 使autossh在后台运行。

另外,autossh还提供了一组环境变量来控制其行为, 这里仅介绍几个有代表性的,其可以man autossh

  • AUTOSSH_FIRST_POLL 指定首次论询测试时间。
  • AUTOSSH_POLL 指定连接论询时间,默认600。若该值小于两次网络超时(默认15秒),则网络超将被调整为该值的1/2
  • AUTOSSH_GATETIME 指定等待SSH连接成功建立的时间,默认30秒,超时表示首次运行失败,将退出autossh。若设为0,则禁用该功能,通常用于启动时运行autossh。
  • AUTOSSH_MAXLIFETIME 指autossh最长运行时间,达到该时间,autossh将退出,并杀死SSH进程。
  • AUTOSSH_MAXSTART 指定SSH最大启动次数。默认-1,表示无限制。

Ubuntu配置方法

基于Ubuntu 12.04或14.04,以SSH动态代理(即SSH翻墙)为例。init script和Upstart都可以将autossh变成服务,然Upstart的respawn容错能力更强,它能在服务进程掉线,重新启动该服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
# autossh

description "autossh daemon"

start on (net-device-up IFACE=eth0 or net-device-up IFACE=wlan0)
stop on (net-device-down IFACE=eth0 and net-device-down IFACE=wlan0)

respawn

setuid like
setgid like

exec /usr/bin/autossh -M64000 -q -N -D localhost:12348 sshproxy
  • setuidsetgid为了让autossh运行在指定的用户和用户组上。
  • start on表示当eth0或wlan0激活时,启动autossh,stop on反之。其目的为避免系统启动或网络掉线时,频繁尝试启动autossh。

更好的办法

最近OpenSSH都支持选项ServerAliveIntervalServerAliveCountMax,实际为建立在SSH协议上的心跳测试。当测试失败后,SSH客户端进程将退出。通过Upstart的respawn功能重启SSH客户端进程,也能达到autossh目的。

仍以SSH动态代理为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# sshproxy

description "ssh proxy"

start on (net-device-up IFACE=eth0 or net-device-up IFACE=wlan0)
stop on (net-device-down IFACE=eth0 and net-device-down IFACE=wlan0)

respawn

setuid like
setgid like

exec /usr/bin/ssh \
    -oServerAliveInterval=300 \
    -oServerAliveCountMax=2 \
    -q -N -D localhost:12348 sshproxy

SSH隧道简介

| Comments

SSH的功能非常强大,日常除了用于命令行远程登录服务器。它还具有神奇的隧道(tunnel)功能(也被称为SSH代理),可用于加密访问本地或远程主机的服务。

通常,SSH代理具有3种方式:

SSH(正向)代理

通过参数-L [bind_address:]port:remote_host:remote_port,将指定本地(客户端)端口转发至远程端口上。

SSH Proxy

如上图, hosta无法直接访问hostb,但它能直接SSH登录gateway;如此通过gateway,将hosta的端口X转发至hostb的端口Y上。相当于端口X和端口Y之间建立了加密隧道。

一般来说,端口Y为hostb上某服务的监听端口。当建立隧道后,hosta将监听端口X。应用程序访问hosta的端口X,等同于访问hostb的端口Y。对于应用程序,hostb端口Y对应的服务就如同运行在hosta上。

日常工作中,客户的网络常由于信息安全而被网关(或防火墙)隔离。当我们的软件在客户网络中某服务器发生问题时,我们常需奔赴客户现场进行调试。若客户存在某机器安装了SSH服务器,且能被外部访问。就可以利用SSH正向代理的方法,快速简便的登录被隔离的服务器并进行应用调试。

SSH反向代理

通过参数-R [bind_address:]port:remote_host:remote_port,将指定远程端口转发至本地(客户端)端口上。

SSH Reverse Proxy

如上图,hosta在防火墙内,无法被hostb直接访问。但它能直接SSH登录hostb;如此通过hostb,将hostb的端口X转发至hosta的端口Y上。该方法与SSH正向代理类似,所不同的是该隧道的访问方向是从服务端(hostb)至客户端(hosta),故被称为反向代理。

其应用场景也与SSH正向代理类似,所不同的是若客户不存在可供外部访问的SSH服务器时,我们可以在外网建设一个SSH服务器给客户的被隔离服务器来建立隧道。如此,我们可以访问自己的SSH服务器对应端口来调试客户服务器的应用。

更进一步,客户内网甚至不能访问外网,此时可利用客户内网一台笔记本(或台式机,它可以访问目标服务器)USB接上3G/4G手机来达到访问外部SSH服务器的目的。

SSH Reverse Proxy Mobile

SSH动态代理

通过参数-D [bind_address:]port,利用远程服务器为访问出口,在本地建立SOCKS 4/5代理服务器。

可以形象描绘为将本地应用的端口(SOCKS客户端端口),动态转发至远程。

SSH Dynamic Proxy

该功能广为人知的应用场景为翻墙。如上图,在国外租用VPS(hostb),客户端(hosta)通过SSH动态代理端口X(SOCKS 4/5的端口)便可以访问被GFW封锁的网络。

这种翻墙最大的优势在于

  • 低成本:国外廉价低配置VPS基本满足个人翻墙需求。
  • 服务端0配置:服务端只需要安装SSH服务端。
  • 客户端配置简单:客户端需要安装SSH客户端,以及一条命令。
  • 加密隧道:保证网络访问的数据安全。

创建通过SSH访问的chroot

| Comments

我的工作环境是Ubuntu,然而经常需要CentOS来编译或测试。一般存在3种解决办法:

  • 创建VirtualBox或KVM虚拟机:
    • 优点:部署容易,且可以运行各种应用(如Oracle)。
    • 缺点:运行速度相对LXC和chroot慢,特别是I/O.
  • 创建CentOS的LXC:
    • 优点:运行速度快,且具有独立IP,可以通过SSH访问。
    • 缺点: 需要修改启动脚本。
  • 创建CentOS的chroot:
    • 优点:不需要修改任何配置。
    • 缺点:无法直接通过SSH访问,且需要root权限才能运行chroot。

创建CentOS的chroot

具体参看Rinse简介

创建SSH用户(或组)

在当前OS环境中,创建用户centos6.

1
sudo useradd -m -s /bin/bash centos6

获取centos6的uid和gid

1
2
id -u centos6
id -g centos6

在chroot环境中创建同名用户,且保持uid和gid相同。

1
2
3
sudo chroot /var/lib/centos-6 /bin/bash
groupadd -g <gid> centos6
useradd -s /bin/bash -m -u <uid> -g centos6 centos6

配置sshd

将下述内容追加至/etc/ssh/sshd_config

1
2
Match group centos6
  ChrootDirectory /var/lib/centos-6

确保/var/lib/centos-6的每一级目录的属主为root,且其他用户或组没有写权限。

然后,重启ssh

1
service ssh restart

这样保证centos6组的用户登录ssh时,chroot至/var/lib/centos-6目录中。

配置dev, proc和sysfs

chroot环境中rpm安装和卸载的前/后置脚本依赖dev, proc和sysfs,否则可能将造成安装和卸载错误。

在/etc/fstab中,增加proc和sysfs的挂载选项

1
2
3
proc /var/lib/centos-6/proc proc  defaults      0 0
none /var/lib/centos-6/sys  sysfs defaults      0 0
/dev /var/lib/centos-6/dev  none  defaults,bind 0 0

配置ssh密钥登录

chroot的ssh密钥登录与常规情况的唯一区别在于公钥应存放在当前OS环境(而非chroot环境)的~/.ssh/authorized_keys,因为sshd在执行chroot之前需要检查公钥是否正确。

因此,本例中应该存放在当前OS的/home/centos-6/.ssh/authorized_keys.

Rinse简介——Debian/Ubuntu中创建RPM安装环境

| Comments

Rinse是一个Debian环境中创建RPM发行版本(如CentOS,Scientific Linux和openSUSE)的工具。你可以利用它轻松创建各种RPM发行版本的chroot环境。

以下基于Ubuntu 12.04 amd64,主要以创建CentOS 6 x86_64为例。

安装Rinse

1
sudo apt-get install -y rinse

创建CentOS 6

1
sudo rinse --distribution centos-6 --arch amd64 --directory centos-6

运行该命令将创建CentOS 6 amd64于当前工作目录的centos-6目录中。其中,

  • –distribution指定发行版本,类似还可以centos-{4,5}, fedora-core-{4,5,6,7,8,9}和opensuse-{10.1,10.2,10.3,11.0,11.1,12.1}等。可以下述命令获取:
1
rinse --list-distributions

具体对应于/etc/rinse/*.packages的模板名,它们主要包含RPM包列表。换一句话说,你根据需要定制自己的模板。另一方面,你也可以通过–pkgs-dir指定不同于/etc/rinse的模板目录。

  • –arch指定架构,amd64表示64位架构,i386表示32位架构。缺省为i386.
  • –directory指定为安装目录,安装结束后便可以chroot该目录了。

另外,需要额外安装某些包,可以通过指定–add-pkg-list来完成。

配置RPM缓存

rinse默认使用/var/rinse/cache作为缓存目录,它大大缩短了重复运行同样命令的时间。具体通过:

  • –cache 0指禁用缓存,缺省为1
  • –cache-dir指定不同于/var/rinse/cache作为缓存目录。
  • –clean-cache指清楚缓存

定制安装后执行脚本

–after-post-install, –before-post-install和–post-install顾名思义,需要指出的是–post-install默认执行/usr/lib/rinse//post-install.sh.

如何提高安装速度?

通过修改/etc/rinse/rinse.conf中对应发行版的镜像地址可以加速安装,如CentOS 6 x86_64的镜像地址可以修改为

http://centos.ustc.edu.cn/centos/6/os/x86_64/CentOS/

也可以通过–config指定不同于/etc/rinse/rinse.conf的配置文件。

若内网存在HTTP cache服务器(如Squid),还可以设置环境变量http_proxy来缓存rpm以及加速安装,如:

1
sudo http_proxy=http://<http proxy address> rinse --distribution centos-6 --arch amd64 --directory centos-6