Like的世界

个人总结与随想

在Docker容器中创建GlusterFS

| Comments

本文展示如何在一台Linux物理机/虚拟机上,创建GlusterFS集群。目的在于测试和学习GlusterFS,而非将GlusterFs应用生产环境。

下载节点容器镜像

1
docker pull gluster/gluster-centos

创建元数据目录

1
mkdir -p /etc/glusterfs{1,2,3} /var/lib/glusterd{1,2,3} /var/log/glusterfs{1,2,3}

创建节点容器实例

1
2
3
4
5
6
7
8
9
10
for i in `seq 1 3`; do
        mkdir -p $etc$i $var$i $log$i
        docker run -d --privileged=true --name gluster$i --hostname=gluster$i \
                -v /etc/glusterfs$i:/etc/glusterfs:z \
                -v /var/lib/glusterd$i:/var/lib/glusterd:z \
                -v /var/log/glusterfs$i:/var/log/glusterfs:z \
                -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
                -v /dev:/dev \
                gluster/gluster-centos
done

组建集群

1
docker exec -ti gluster1 /bin/bash
1
2
gluster peer probe <gluster2 ip addr>
gluster peer probe <gluster3 ip addr>

创建卷

冗余卷 (replica)

1
gluster peer volume create v1 replica 3 172.17.0.{2,3,4}:/mnt/v1 force

条带卷 (stripe)

1
gluster peer volume create v2 strip 3 172.17.0.{2,3,4}:/mnt/v2 force

纠删码卷 (disperse)

1
gluster peer volume create e3 disperse 3 redundancy 1 172.17.0.{2,3,4}:/mnt/v3 force
  • 由于这些容器实例的/mnt与/在同一个分区,故需要指定force参数。
  • 创建卷时,目录/mnt/v{1,2,3}将被gluster自动创建,前提是父目录 (/mnt)已存在

挂载卷

通过FUSE挂载

1
mount -t glusterfs 172.17.0.2:/v1 /mnt/glusterfs/

脚本化

为了便于测试,我将上述诸多过程归纳成脚本:gluster_docker

Gluster Web Interface

gluster-web-interface是一个管理GlusterFS的Web应用应用,它基于Ruby on Rails实现。

为了简化其安装,我创建其docker镜像docker-gluster-web-interface

1
docker run -it -p 3000:3000 like/gluster-web-interface

Shadowsocks简介

| Comments

Shadowsocks(中文名: 影梭) 是一款开源的安全SOCKS 5代理,它主要用于在大陆翻墙。

原理

与SSH动态代理相似,客户端呈现为SOCKS 5代理服务,客户端与服务器之间采用加密通信。服务器部署于GFW之外,从而实现代理翻墙服务。

特点

  • 使用自行设计的协议加密通信,支持多种加密算法:AES、Blowfish、IDEA、RC4等。除创建TCP连接外无需握手,每次请求只转发1个连接,因此使用起来网速较快,在移动设备上较省电。
  • 通过异步I/O和事件驱动实现,响应速度快。
  • 客户端支持主流操作系统平台:Windows、Linux、OS X、Android、iOS和OpenWrt

shadowsocks-libev简介

网络中普遍采用Python版本的shadowsocks,该版本看似安装简单,却存在如下缺点:

  • 没有Linux操作系统原生安装包(如:RPM和DEB)。
  • 没有操作系统的服务脚本(如init.d和upstart)。
  • Python程序占用内存较多,运行效率不佳。

shadowsocks-libev是Shadowsocks在嵌入式和低端设备的轻量级实现:

  • 纯C实现,不仅占用内存极小,且运行效率更快。
  • 在几乎所有Linux平台都存在原生安装包,且具有对应平台的服务启动脚本。

下面基于Ubuntu 14.04/16.04介绍它的安装和配置。

安装shadowsocks-libev

虽然shadowsocks-libev可以编译出DEB,但是默认没有提供Ubuntu的包仓库。为了方便安装,我将它构建于我的PPA中。

客户端和服务端的安装方法相同:

1
2
3
sudo add-apt-repository ppa:likemartinma/net
sudo apt-get update
sudo apt-get install shadowsocks-libev

配置shadowsocks-libev服务端

编辑/etc/shadowsocks-libev/config.json:

1
2
3
4
5
6
{
    "server_port": 8388,
    "password": "<共享密码>",
    "timeout": 60,
    "method": "aes-256-cfb"
}
  • server_port为服务端监听端口
  • password为客户端和服务端预设的共享密码,它最好由安全密码生成器生成(如LastPassKeePass),且长度不小于6个字符。
  • timeout为连接超时时间。
  • method为加密算法,aes-256-cfb的安全性较好。

配置完成后,需重启:

1
sudo service shadowsocks-libev restart

配置shadowsocks-libev客户端

创建/etc/shadowsocks-libev/client.json (文件名可修改):

1
2
3
4
5
6
7
{
    "server": "<服务端地址>",
    "server_port": "<服务端端口>",
    "local_port": "22357",
    "password": "<共享密码>",
    "method": "aes-256-cfb"
}

Ubuntu 14.04

安装包没有提供系统服务脚本,故须自己创建Upstart脚本/etc/init/ss-local.conf (文件名可修改):

1
2
3
4
5
6
7
8
9
10
11
12
13
# ss-local

description "shadowsocks client"

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 nobody
setgid nogroup

exec ss-local -c /etc/shadowsocks-libev/client.json

为了client.json的安全:

1
2
3
sudo chown -R root:nogroup /etc/shadowsocks-libev
sudo chmod 0750 /etc/shadowsocks-libev
sudo chmod 0640 /etc/shadowsocks-libev/client.json

启动客户端

1
sudo start ss-local

最后,通过修改/etc/default/shadowsocks-libev的START=no禁止在客户机启动服务端程序——它在客户机没有作用。

1
sudo service shadowsocks-libev stop

Ubuntu 16.04

安装包提供了systemd的服务模板/lib/systemd/system/shadowsocks-libev-local@.service:

1
2
sudo systemctl enable shadowsocks-libev-local@client
sudo systemctl start shadowsocks-libev-local@client

注意,@client与client.json的基本名必须一致。

最后,禁用并停止服务端程序:

1
2
sudo systemctl disable shadowsocks-libev
sudo systemctl stop shadowsocks-libev

集群化

类似SSH集群,多个shadowsocks也可以构建SOCKS 5集群,具体请参考《SSH翻墙集群》的“HAProxy的配置方法”。

基于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/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 uninstall 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 -)"' >> ~/.bashrc

创建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

四、配置Upstart脚本

若python程序须要通过Upstart启动,则其Upstart脚本可以类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# service name

description "service description ..."

respawn

setuid <username>
setgid <group>

env PYENV_ROOT=/home/<username>/.pyenv
env PATH=/home/<username>/.pyenv/bin:/sbin:/usr/sbin:/bin:/usr/bin
env PYENV_VERSION=<python version or virtualenv name>

chdir <app dir>

script
        eval "$(pyenv init -)"
        exec ./<app>
end script
# vim: ts=4 sw=4 sts=4 ft=upstart

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

description "service description ..."

respawn

setuid <username>
setgid <group>

env PYENV_ROOT=/home/<username>/.pyenv
env PATH=/home/<username>/.pyenv/shims:/home/<username>/.pyenv/bin:/sbin:/usr/sbin:/bin:/usr/bin
env PYENV_VERSION=<python version or virtualenv name>

chdir <app dir>

exec ./<app>
# vim: ts=4 sw=4 sts=4 ft=upstart
  • username为服务运行的用户名,通常为PYENV_ROOT所属用户。
  • group为服务运行的组名,通常为PYENV_ROOT所属组。
  • PYENV_VERSION为Python版本号或virtualenv的名字。
  • app dir为Python程序的目录。
  • app为Python程序或启动脚本。

扩展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客户端,以及一条命令。
  • 加密隧道:保证网络访问的数据安全。