kubernetes元数据存储之kine(二)

kine中的存储Driver最终需要对接Log接口,以SQLite为例,分析一下,kine如何基于关系数据库实现的mvccdb。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/logstructured.go

type Log interface {
    Start(ctx context.Context) error
    CurrentRevision(ctx context.Context) (int64, error)
    List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeletes bool) (int64, []*server.Event, error)
    After(ctx context.Context, prefix string, revision, limit int64) (int64, []*server.Event, error)
    Watch(ctx context.Context, prefix string) <-chan []*server.Event
    Count(ctx context.Context, prefix string) (int64, int64, error)
    Append(ctx context.Context, event *server.Event) (int64, error)
    DbSize(ctx context.Context) (int64, error)
}

表设计和字段映射

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/sqlite/sqlite.go
CREATE TABLE IF NOT EXISTS kine
(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name INTEGER,
    created INTEGER,
    deleted INTEGER,
    create_revision INTEGER,
    prev_revision INTEGER,
    lease INTEGER,
    value BLOB,
    old_value BLOB
)

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/types.go

type KeyValue struct {
    Key            string
    CreateRevision int64
    ModRevision    int64
    Value          []byte
    Lease          int64
}

type Event struct {
    Delete bool
    Create bool
    KV     *KeyValue
    PrevKV *KeyValue
}
Event 表字段
KV.ModRevision id
KV.Key name
KV.Create created
KV.Delete deleted
KV.CreateRevision create_revision
PrevKV.ModRevision prev_revision
KV.Lease lease
KV.Value value
PrevKV.Value old_value
  • 主键自增id为该记录KeyValue的Revision。
  • KeyValue创建记录created=1、deleted=0、create_revision=0、prev_revision=0。
  • KeyValue更新记录create_revison为created=1记录id,prev_revision为前记录id。
  • KeyValue删除记录deleted=1,create_revison=创建记录id,prev_revision=前记录id。
  • value字段和old_value字段记录,当前版本和上一版本KeyValue值内容。

MVCC实现

查询Key

获取当前版本KeyValue即:查询name为key,最新且deleted=0的记录。

SELECT *
FROM kine AS kv
JOIN (
   SELECT MAX(mkv.id) AS id FROM kine AS mkv WHERE mkv.name LIKE ? GROUP BY mkv.name
) AS maxkv ON maxkv.id = kv.id
WHERE
kv.deleted = 0

写入Key

乐观无锁、无事务并发安全写入:name和prev_revision建立联合唯一索引。

所有Log.Append执行写入Event均携带PrevKV信息,PrevKV.ModRevision作为新记录的prev_revision字段,因为kine_name_prev_revision_uindex联合唯一索引限制并发将冲突报错,仅有一条写入成功。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/sqlite/sqlite.go

`CREATE UNIQUE INDEX IF NOT EXISTS kine_name_prev_revision_uindex ON kine (name, prev_revision)`

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/logstructured.go

func (l *LogStructured) Update(ctx context.Context, key string, value []byte, revision, lease int64) (revRet int64, kvRet *server.KeyValue, updateRet bool, errRet error) {
	...
    rev, event, err := l.get(ctx, key, "", 1, 0, false)
	...
    updateEvent := &server.Event{
        KV: &server.KeyValue{
            Key:            key,
            CreateRevision: event.KV.CreateRevision,
            Value:          value,
            Lease:          lease,
        },
        PrevKV: event.KV,
    }

    rev, err = l.log.Append(ctx, updateEvent)
   ...
}

获取全局Revision

表最大id作为全局Revision。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/generic/generic.go

SELECT MAX(rkv.id) AS id
FROM kine AS rkv

获取全局CompactRevision

内置keycompact_rev_key记录已压缩版本。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/drivers/generic/generic.go

SELECT MAX(crkv.prev_revision) AS prev_revision
FROM kine AS crkv
WHERE crkv.name = 'compact_rev_key'

获取Key列表

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/sqllog/sql.go

func (s *SQLLog) List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeleted bool) (int64, []*server.Event, error) {
  	...
	//获取Key列表
    if revision == 0 {
        rows, err = s.d.ListCurrent(ctx, prefix, limit, includeDeleted)
    } else {
        rows, err = s.d.List(ctx, prefix, startKey, limit, revision, includeDeleted)
    }
    if err != nil {
        return 0, nil, err
    }

	//处理版本已压缩返回
    if revision > 0 && len(result) == 0 {
        // a zero length result won't have the compact revision so get it manually
        compact, err = s.d.GetCompactRevision(ctx)
        if err != nil {
            return 0, nil, err
        }
    }

    if revision > 0 && revision < compact {
        return rev, result, server.ErrCompacted
    }
	...
}
SELECT *
FROM (
	SELECT (rev), (compactRev), columns
	FROM kine AS kv
	JOIN (
		SELECT MAX(mkv.id) AS id
		FROM kine AS mkv
		WHERE
			mkv.name LIKE [?keyPrefix]
		GROUP BY mkv.name) AS maxkv
		ON maxkv.id = kv.id
	WHERE
		kv.deleted = 0 OR
		[?includeDeleted]
) AS lkv
ORDER BY lkv.theid ASC
LIMIT [?limit]

Watch实现

Watch gRPC接口复用etcd的代码,重新实现Handle函数。

SQLLog实现Watch内部,通过读写事件和定时轮询(默认一秒)实现Watch事件推送。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/watch.go

func (s *KVServerBridge) Watch(ws etcdserverpb.Watch_WatchServer) error {
	w := watcher{
		server:  ws,
		backend: s.limited.backend,
		watches: map[int64]func(){},
	}
	...
	for {
		...
		if msg.GetCreateRequest() != nil {
			w.Start(ws.Context(), msg.GetCreateRequest())
		} else if msg.GetCancelRequest() != nil {
			...
		}
	}
}
...

func (w *watcher) Start(ctx context.Context, r *etcdserverpb.WatchCreateRequest) {
	...
	go func() {
		...
		for events := range w.backend.Watch(ctx, key, r.StartRevision) {
			...
		}
		...
	}()
}
//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/sqllog/sql.go

func (s *SQLLog) Watch(ctx context.Context, prefix string) <-chan []*server.Event {
    //broadcastern对象内部回调startWatch
    values, err := s.broadcaster.Subscribe(ctx, s.startWatch)
	...
    return res
}

func (s *SQLLog) startWatch() (chan interface{}, error) {
    pollStart, err := s.d.GetCompactRevision(s.ctx)
   ...
    go s.compactor(compactInterval)
    go s.poll(c, pollStart)
    return c, nil
}

func (s *SQLLog) poll(result chan interface{}, pollStart int64) {
	...
    wait := time.NewTicker(time.Second)
    defer wait.Stop()
    defer close(result)

    for {
        if waitForMore {
            select {
            case <-s.ctx.Done():
                return
            case check := <-s.notify:
                if check <= last {
                    continue
                }
            case <-wait.C:
            }
        }
		...
	}
}
阅读全文

kubernetes元数据存储之kine(一)

Kine是k3s-io为实现轻量化部署kubernetes而实现的一个支持kubernetes api-server直接读写,数据持久化到SQLite、MySQL、Postgres等后端的中间件。

Kubernetes api-server使用etcd作为后端存储集群的元数据,并且其controller机制强依赖etcd的watch机制。

Kine实现对etcd的替代,完整实现了kubernetes api-server用到的etcd接口。

透过kine这个轻量的中间件,既可以深入部分etcd原理,又可以了解kubernetes核心controller机制背后的支撑。

Etcd到Kine

所谓知己知彼,要替换etcd,首先就要了解etcd的实现本身。

如图,etcd核心分三个模块:

  • gRPC KV Server 基于gRPC的接口层
  • Raft 实现强一致的Raft日志层
  • mvccdb 基于boltdb的mvcc存储层

Etcd的读写事务请求,客户端通过gRPC发送给etcd server,etcd server形成raft日志提交给raft集群,最后etcd server apply日志,持久化数据到boltdb。

Etcd实现可靠和高性能watch,一方面基于gRPC/http2网络,一方面是数据存储的mvcc机制。

Kine实现了部分etcd相同接口,gRPC Server部分复用了etcd的gRPC代码,没有实现Raft日志模块,但是抽象了Backend层对接后端不同存储Driver,存储Driver需要实现支持mvcc的读写。

Kine Server

kine复用了etcd的go.etcd.io/etcd/api/v3/etcdserverpb所有代码实现etcd gRPC Server。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/server.go

type KVServerBridge struct {
    limited *LimitedServer
}

...

func (k *KVServerBridge) Register(server *grpc.Server) {
    ...
    etcdserverpb.RegisterWatchServer(server, k)
    etcdserverpb.RegisterKVServer(server, k)
}

KVServerBridge 实现了gRPC功能接口函数。

//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/kv.go

func (k *KVServerBridge) Range(ctx context.Context, r *etcdserverpb.RangeRequest) (*etcdserverpb.RangeResponse, error) {
   ...
    resp, err := k.limited.Range(ctx, r)
    if err != nil {
        logrus.Errorf("error while range on %s %s: %v", r.Key, r.RangeEnd, err)
        return nil, err
    }
    ...
    return rangeResponse, nil
}

LimitedServer对接Backend存储接口


// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/limited.go

type LimitedServer struct {
    backend Backend
    scheme  string
}

func (l *LimitedServer) Range(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
    if len(r.RangeEnd) == 0 {
        return l.get(ctx, r)
    }
    return l.list(ctx, r)
}

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/types.go

type Backend interface {
    Start(ctx context.Context) error
    Get(ctx context.Context, key, rangeEnd string, limit, revision int64) (int64, *KeyValue, error)
    Create(ctx context.Context, key string, value []byte, lease int64) (int64, error)
    Delete(ctx context.Context, key string, revision int64) (int64, *KeyValue, bool, error)
    List(ctx context.Context, prefix, startKey string, limit, revision int64) (int64, []*KeyValue, error)
    Count(ctx context.Context, prefix string) (int64, int64, error)
    Update(ctx context.Context, key string, value []byte, revision, lease int64) (int64, *KeyValue, bool, error)
    Watch(ctx context.Context, key string, revision int64) <-chan []*Event
    DbSize(ctx context.Context) (int64, error)
}


//https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/get.go

func (l *LimitedServer) get(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
    if r.Limit != 0 && len(r.RangeEnd) != 0 {
        return nil, fmt.Errorf("invalid combination of rangeEnd and limit, limit should be 0 got %d", r.Limit)
    }
    rev, kv, err := l.backend.Get(ctx, string(r.Key), string(r.RangeEnd), r.Limit, r.Revision)
   ...
    return resp, nil
}

Kine Backend

kine在pkg/logstructuredpkg/drivers中实现对接各种后端存储。

LogStructured实现Server的Backend接口,通过Log接口和底层Driver对接。


// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/logstructured/logstructured.go

type Log interface {
    Start(ctx context.Context) error
    CurrentRevision(ctx context.Context) (int64, error)
    List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeletes bool) (int64, []*server.Event, error)
    After(ctx context.Context, prefix string, revision, limit int64) (int64, []*server.Event, error)
    Watch(ctx context.Context, prefix string) <-chan []*server.Event
    Count(ctx context.Context, prefix string) (int64, int64, error)
    Append(ctx context.Context, event *server.Event) (int64, error)
    DbSize(ctx context.Context) (int64, error)
}

type LogStructured struct {
    log Log
}

func (l *LogStructured) Update(ctx context.Context, key string, value []byte, revision, lease int64) (revRet int64, kvRet *server.KeyValue, updateRet bool, errRet error) {
   ...
    rev, err = l.log.Append(ctx, updateEvent)
    ...
    return rev, updateEvent.KV, true, err
}

Backend最终向Driver通过Log.Append使用Event结构存储数据。

// https://github.com/k3s-io/kine/blob/v0.9.8/pkg/server/types.go

type KeyValue struct {
    Key            string
    CreateRevision int64
    ModRevision    int64
    Value          []byte
    Lease          int64
}


type Event struct {
    Delete bool
    Create bool
    KV     *KeyValue
    PrevKV *KeyValue
}

kine中EventKeyValue设计了和Etcd中keyIndex和mvccpb.KeyValue类似的可以记录版本信息的数据结构。存储Driver在执行数据存储动作时候,需要对接相关字段。

阅读全文

etcd原理之mvcc机制

什么是MVCC

MVCC 机制正是基于多版本技术实现的一种乐观锁机制,它乐观地认为数据不会发生冲突,但是当事务提交时,具备检测数据是否冲突的能力。

在 MVCC 数据库中,你更新一个 key-value数据的时候,它并不会直接覆盖原数据,而是新增一个版本来存储新的数据,每个数据都有一个版本号。

当你指定版本号读取数据时,它实际上访问的是版本号生成那个时间点的快照数据。当你删除数据的时候,它实际也是新增一条带删除标识的数据记录。

ETCD的实现

首先etcd在内存中使用b-tree维护了key与版本号revision的关系树treeIndex,再以revision作为boltdb的key存放value,boltdb内部同样使用b-tree作为查找结构。

get/put操作key首先在treeindex中查找/构建对应的版本revision,然后用revision作为key到boltdb中读写value。

treeIndex中每一个叶子节点为一个keyIndex结构。

type keyIndex struct {
   key         []byte //用户的key名称,比如我们案例中的"hello"
   modified    revision //最后一次修改key时的etcd版本号,比如我们案例中的刚写入hello为world1时的,版本号为2
   generations []generation //generations 表示一个 key 从创建到删除的过程,每代对应 key 的一个生命周期的开始与结束,每代中包含对key的多次修改的版本号列表
}

type generation struct {
   ver     int64    //表示此key的修改次数
   created revision //表示generation结构创建时的版本号
   revs    []revision //每次修改key时的revision追加到此数组
}

type revision struct {
   main int64    // 一个全局递增的主版本号,随put/txn/delete事务递增,一个事务内的key main版本号是一致的
   sub int64    // 一个事务内的子版本号,从0开始随事务内put/delete操作递增
}

boltdb中存储的Value为mvccpb.KeyValue结构。

type KeyValue struct {
    Key []byte
    CreateRevision int64 // 表示此 key 创建时的版本号
    ModRevision int64 // 表示 key 最后一次修改时的版本号
    Version int64 // 表示此 key 的修改次数
    Value []byte
    Lease int64
}

ETCD MVCC读写

更新key

初始化一个新集群,全局版本号默认为 1。

执行下面的 txn 事务,它包含两次 put、一次 get 操作,那么按照我们上面介绍的原理,全局版本号随读写事务自增,因此是 main 为 2,sub 随事务内的 put/delete 操作递增,因此 key hello 的 revison 为{2,0},key world 的 revision 为{2,1}。

$ etcdctl txn -i
compares:

success requests (get,put,del):
put hello 1
get hello
put world 2

第一次创建 hello key,此时 keyIndex 索引为空,etcd会根据当前的全局版本号(空集群启动时默认为 1)自增,生成put hello操作对应的版本号revision{2,0},这就是boltdb的key。

boltdb的value是mvccpb.KeyValue结构体,由key、value、create_revision、mod_revision、version、lease 组成。

{
    "key": "aGVsbG8=",
    "create_version": 2,
    "mod_version": 2,
    "version": 1,
    "value": "Mg=="
}

因为 key hello是首次创建,treeIndex会生成 key hello对应的keyIndex对象。

{
	key: "hello"
	modified: <2,0>
	generations:
	[
		{ver:1,created:<2,0>,revisions: [<2,0>]}
	]
}

再次发起一个put hello为world2修改操作时,key hello对应的keyIndex的结果如下面所示,keyIndex.modified字段更新为 <3,0>,generation的revision数组追加最新的版本号<3,0>,ver修改为2。

{
	key:  "hello"
	modified: <3,0>
	generations:
	[
		{ver:2,created:<2,0>,revisions: [<2,0>,<3,0>]}
	]
}

读取key

在读事务中,它首先需要根据 key从treeIndex 模块获取版本号,未带版本号读,默认是读取最新的数据。

treeIndex从b-tree中,根据key查找到keyIndex对象后,匹配有效的generation,返回generation的revisions数组中最后一个版本号{2,0}给读事务,读事务根据此版本号为key,从boltdb中查询此key的value信息。

删除key

与更新key生成botldb key相比,删除key生成的boltdb key 版本号{4,0,t}追加了删除标识(tombstone, 简写t),treeIndex会给此 key hello 对应的keyIndex对象,追加一个空的generation对象,表示此索引对应的key被删除了。

{
	key:     "hello"
	modified: <4,0>
	generations:
	[
		{ver:3,created:<2,0>,revisions: [<2,0>,<3,0>,<4,0>(t)]},
		{empty}
	]
}
阅读全文

Dapper构建工具

Build in Docker

开发协作中,源码可以通过Git共享协作,但是开发环境一致性,是大家往往容易忽略的问题。项目一开始就实现开发环境统一,可以规避很多团队成员重复搭建开发环境问题,以及帮助新同学快速参与到项目开发中。

虽然Python、Java等语言本身和平台无关,但是项目中难免出现异构多语言、脚本工具等情况。随着项目迭代,项目除核心代码之外,开发构建会出现各种各样的周边依赖,比如bash、make、gcc、curl等等,复杂的项目编译还会依赖特定的库。

Docker镜像是管理这些依赖的很好载体,团队可以共享同一个构建镜像,编译项目时候使用该镜像启动一个容器,将代码放到这个容器中构建,保证每次构建的环境一致性。

Rancher的dapper工具,帮助我们管理构建镜像和一键启动构建容器,在Docker Cli和Dockerfile之上,添加其他辅助功能,帮助我们实现Build in Docker。

Dapperfile

Dapper基于Dockerfile定义了Dockerfile.dapper,用户通过Dockerfile.dapper管理构建镜像依赖,dapper使用该文件一键执行: docker build、 docker run、docker cp 等等命令。

  • docker build 完成在本地构建镜像的生成。
  • docker run 启动一个构建容器,可以依照参数向容器中注入代码,环境变量,构建命令等等。
  • docker cp 可以实现从构建容器中将产物从容器中拷贝到本地。

可以理解为dapper是docker cli的一个封装,一键快速实现多个动作。

Dapper复用了Dockerfile的ENV指令,扩展了特定ENV key,实现docker run 和docker cp的参数注入。

  • DAPPER_RUN_ARGS 执行docker run 时候额外的参数。
  • DAPPER_ENV 获取环境中变量注入到容器中。
  • DAPPER_SOURCE 容器中代码路径。
  • DAPPER_OUTPUT 构建产物目录注入到docker cp中,从容器中复制产物到本地。

另外dapper复用了Dockerfile的ARG指令,扩展ARG从环境中获取ARG key相同的环境变量,注入到docker build –build-args中,动态影响构建镜像的生成。

Dapper示例

以Kine项目的Dockerfile.dapper为例:

FROM golang:1.19-alpine3.16 AS dapper

ARG ARCH=amd64

RUN apk -U add bash coreutils git gcc musl-dev docker-cli vim less file curl wget ca-certificates
RUN GOPROXY=direct go install golang.org/x/tools/cmd/goimports@gopls/v0.9.5
RUN rm -rf /go/src /go/pkg

RUN if [ "${ARCH}" == "amd64" ]; then \
    curl -sL https://raw.githubusercontent.com/golangci/golangci-lint/v1.50.0/install.sh | sh -s;  \
    fi

ENV DAPPER_RUN_ARGS --privileged -v kine-cache:/go/src/github.com/k3s-io/kine/.cache
ENV DAPPER_ENV ARCH REPO TAG DRONE_TAG IMAGE_NAME CROSS SKIP_VALIDATE
ENV DAPPER_SOURCE /go/src/github.com/k3s-io/kine/
ENV DAPPER_OUTPUT ./bin ./dist
ENV DAPPER_DOCKER_SOCKET true
ENV HOME ${DAPPER_SOURCE}
WORKDIR ${DAPPER_SOURCE}

ENTRYPOINT ["./scripts/entry"]
CMD ["ci"]
  • ARG ARCH=amd64 默认指定生成构建镜像时为amd64的环境架构,后面判断ARCH非amd64则不安装golangci相关工具。
  • DAPPER_SOURCE 指定将当前目录代码文件,放到容器中的/go/src/github.com/k3s-io/kine/目录。
  • DAPPER_OUTPUT 声明产物在./bin ./dist目录,dapper工作目录是相对当前目录,所以cp产物出来时候,就到当前目录的./bin ./dist目录。
  • DAPPER_DOCKER_SOCKET 挂载本地的docker socket到构建容器中,可以实现在容器中执行docker命令,比如我们在容器中执行docker build 将产物打包成镜像。

Dapper源码解析

# https://github.com/rancher/dapper/blob/5e204736a984c5ccae817af5618c3c310283f0e4/file/file.go#L112

func (d *Dapperfile) Run(commandArgs []string) error {
	// 执行docker build 生成构建镜像
	// 执行之前解析Dockerfile,获取所有ARG环境变量并注入到docker build --build-arg命令中
	tag, err := d.build(nil, true)
	if err != nil {
		return err
	}


	//生成docker run命令
	//通过docker inspect image 获取所有构建镜像的env信息,解析所有DAPPER_*相关的env信息
	//使用DAPPER_*相关的env信息,生成docker run 命令
	logrus.Debugf("Running build in %s", tag)
	name, args := d.runArgs(tag, "", commandArgs)
	defer func() {
		if d.Keep {
			logrus.Infof("Keeping build container %s", name)
		} else {
			logrus.Debugf("Deleting temp container %s", name)
			if _, err := d.execWithOutput("rm", "-fv", name); err != nil {
				logrus.Debugf("Error deleting temp container: %s", err)
			}
		}
	}()

	//执行docker run 命令
	if err := d.run(args...); err != nil {
		return err
	}

	//通过DAPPER_*相关的env信息,生成docker cp命令并执行
	source := d.env.Source()
	output := d.env.Output()
	if !d.IsBind() && !d.NoOut {
		for _, i := range output {
			p := i
			if !strings.HasPrefix(p, "/") {
				p = path.Join(source, i)
			}
			targetDir := path.Dir(i)
			if err := os.MkdirAll(targetDir, 0755); err != nil {
				return err
			}
			logrus.Infof("docker cp %s %s", p, targetDir)
			if err := d.exec("cp", name+":"+p, targetDir); err != nil {
				logrus.Debugf("Error copying back '%s': %s", i, err)
			}
		}
	}
	return nil
}
阅读全文

科学上网

因为公司之前网络自带梯子,自己一直也没搞代理服务器就也没有在机器上配置梯子,最近换了办公地网络没有梯子了。一开始觉得自己能接受,查阅外网的文档慢点就慢点吧,结果真让自己怀疑人生了,时不时问身边人,你可以打开github吗?是我们网又坏了吗?还有就是自己chrome浏览器的数据不能同步很不方便。于是,决定给机器配下梯子!

关于撕我们所处的环境为什么有GFW这种东西,在github上看到一句话“We don’t dismantle the wall, we just find a hole to bypass then instead. Don’t ask us how to bypass, we don’t know. And don’t ask us what a wall is, it’s not a continuous vertical brick or stone structure that encloses or divides an area of land.”。

原理

如图,代理服务器我们需要找一个IP没有被GFW Block掉的境外节点服务器在上面搭建一个shadowsocks协议的代理服务。代理服务器软件有很多python的,go的等等,这里安利一个go项目gost。服务端代理服务器和本地client通信协议一定是shadowsocks,其它协议不行吗?当然可以!但是GFW能认出来呀,然后把你Block掉。shadowsocks协议现在也很敏感了,个人用不要被认为有sale行为就好。 服务端是shdowscoks协议,但是我们的浏览器和其他软件支持的代理协议一般有socks4,socks5,http,https等协议,不能直接用,所以我们要在本地启动一个进程和服务端以shadowsocks协议通信,然后该进程再向本地开放socks5,https等代理协议,我们的浏览器和其他软件就可以直接用了。这个进程程序就是我们看到的很多shadowsocks客户端程序,它就是两边握手byte by byte的转发。 最后,我们在浏览器里使用设置一下浏览器的代理就好了,chrome/firefox代理设置管理,这里安利SwitchyOmega,linux/mac终端走代理可以设置http_proxy/https_proxy环境变量。

shadowsocks客户端方案

本地shdowsocks客户端,网上有很多,大致分两种,CLI形式和GUI形式,GUI的看上去方便,但是linux涉及图形的软件就是各种问题,下了一个QT的试了一下果然各种库问题。果断转CLI,这里再次安利gost。 有一个问题就是随着开机启动需要我们自己解决,linux下配置自启动大家各种脚本写init等等。但是经验告诉我,这么随意的改home目录以外的东西,我这mint又用不过半年了,还有就是发现脚本启动gost用一段时间gost有不响应的情况(可能和网有关系挂掉了)简单的脚本不能重启gost,写复杂好费事。然后想到了docker,机器本来就装了docker,gost放到一个守护容器里面让docker帮我管理就好了,gost是挂掉还是什么docker帮我重启。实践发现确实比我自己手动脚本启动好使,目前一周了没出现不响应的情况。

docker run -d --name gost --restart unless-stopped --log-opt max-size=10m -p 1080:1080 \
ginuerzh/gost -L socks5://:1080 -F ss://aes-256-cfb:password@xx.xx.xx.xx:port

chrome代理设置

chrome代理管理用的SwitchyOmega,值得一提实践中的一个问题就是,我们访问国内网站不想走代理绕一圈回来,访问国外网站要走代理。不能来回手点设置那些网站走代理那些网站不走代理,很烦。解决方法就是设置Switchomega的自动切换规则,参考的这个wiki,自动更新GFW的block list挺好的。

总结

花了一个多小时时间,看了几个项目,找到了自己觉得比较适合自己linuxmint的方案,分享在这里。

阅读全文