标签 Linux 下的文章

搭建自己的ngrok服务

在国内开发微信公众号企业号以及做前端开发的朋友想必对ngrok都不陌生吧,就目前来看,ngrok可是最佳的在内网调试微信服务的tunnel工 具。记得今年春节前,ngrok.com提供的服务还一切正常呢,但春节后似乎就一切不正常了。ngrok.com无法访问,ngrok虽然能连上 ngrok.com提供的服务,但微信端因为无法访问ngrok.com,导致消息一直无法发送到我们的服务地址上,比如xxxx.ngrok.com。 这一切都表明,ngork被墙了。没有了ngrok tunnel,一切开始变得困难且没有效率起来。内网到外部主机部署和调试是一件慢的让人想骂街的事情。

ngrok不能少。ngrok以及其服务端ngrokd都是开源的,之前我也知道通过源码可以自搭建ngrok服务。请求搜索引擎后,发现国内有个朋友已经搭建了一个www.tunnel.mobi的ngrok公共服务,与ngrok.com类似,我也实验了一下。

编写一个ngrok.cfg,内容如下:

server_addr: "tunnel.mobi:44433"
trust_host_root_certs: true

用ngrok最新客户端1.7版本执行如下命令:

$ngrok -subdomain tonybaiexample -config=ngrok.cfg 80

可以顺利建立一个tunnel,用于本机向外部提供"tonybaiexample.tunnel.mobi"服务。

Tunnel Status                 online
Version                       1.7/1.7
Forwarding                    http://tonybaiexample.tunnel.mobi -> 127.0.0.1:80
Forwarding                    https://tonybaiexample.tunnel.mobi -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        0
Avg Conn Time                 0.00ms

而且国内的ngrok服务显然要远远快于ngrok.com提供的服务,消息瞬间即达。

但这是在公网上直接访问的结果。放在公司内部,我看到的却是另外一个结果:

Tunnel Status                 reconnecting
Version                       1.7/
Web Interface                 127.0.0.1:4040
# Conn                        0
Avg Conn Time                 0.00ms

我们无法从内网建立tunnel,意味着依旧不方便和低效,因为很多基础服务都在内网部署,内外网之间的交互十分不便。但内网连不上tunnel.mobi也是个事实,且无法知道原因,因为看不到server端的连接错误日志。

于是我决定自建一个ngrok服务。

一、准备工作

搭建ngrok服务需要在公网有一台vps,去年年末曾经在Amazon申请了一个体验主机EC2,有公网IP一个,这次就打算用这个主机作为ngrokd服务端。

需要一个自己的域名。已有域名的,可以建立一个子域名,用于关联ngrok服务,这样也不会干扰原先域名提供的服务。(不用域名的方式也许可以,但我没有试验过。)

搭建的参考资料主要来自下面三个:
1) ngrok的官方SELFHOST指南:https://github.com/inconshreveable/ngrok/blob/master/docs/SELFHOSTING.md
2) 国外一哥们的博客:http://www.svenbit.com/2014/09/run-ngrok-on-your-own-server/
3) "海运的博客"中的一篇文章:http://www.haiyun.me/archives/1012.html

二、实操步骤

我的AWS EC2实例安装的是Ubuntu Server 14.04 x86_64,并安装了golang 1.4(go version go1.4 linux/amd64)。Golang是编译ngrokd和ngrok所必须的,建议直接从golang官方下载对应平台的二进制安装包(国内可以从 golangtc.com上下载,速度慢些罢了)。

1、下载ngrok源码

(GOPATH=~/goproj)
$ mkdir ~/goproj/src/github.com/inconshreveable
$ git clone https://github.com/inconshreveable/ngrok.git
$ export GOPATH=~/goproj/src/github.com/inconshreveable/ngrok

2、生成自签名证书

使用ngrok.com官方服务时,我们使用的是官方的SSL证书。自建ngrokd服务,我们需要生成自己的证书,并提供携带该证书的ngrok客户端。

证书生成过程需要一个NGROK_BASE_DOMAIN。 以ngrok官方随机生成的地址693c358d.ngrok.com为例,其NGROK_BASE_DOMAIN就是"ngrok.com",如果你要 提供服务的地址为"example.tunnel.tonybai.com",那NGROK_BASE_DOMAIN就应该 是"tunnel.tonybai.com"。

我们这里以NGROK_BASE_DOMAIN="tunnel.tonybai.com"为例,生成证书的命令如下:

$ cd ~/goproj/src/github.com/inconshreveable/ngrok
$ openssl genrsa -out rootCA.key 2048
$ openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=
tunnel.tonybai.com" -days 5000 -out rootCA.pem
$ openssl genrsa -out device.key 2048
$ openssl req -new -key device.key -subj "/CN=
tunnel.tonybai.com" -out device.csr
$ openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000

执行完以上命令,在ngrok目录下就会新生成6个文件:

-rw-rw-r– 1 ubuntu ubuntu 1001 Mar 14 02:22 device.crt
-rw-rw-r– 1 ubuntu ubuntu  903 Mar 14 02:22 device.csr
-rw-rw-r– 1 ubuntu ubuntu 1679 Mar 14 02:22 device.key
-rw-rw-r– 1 ubuntu ubuntu 1679 Mar 14 02:21 rootCA.key
-rw-rw-r– 1 ubuntu ubuntu 1119 Mar 14 02:21 rootCA.pem
-rw-rw-r– 1 ubuntu ubuntu   17 Mar 14 02:22 rootCA.srl

ngrok通过bindata将ngrok源码目录下的assets目录(资源文件)打包到可执行文件(ngrokd和ngrok)中 去,assets/client/tls和assets/server/tls下分别存放着用于ngrok和ngrokd的默认证书文件,我们需要将它们替换成我们自己生成的:(因此这一步务必放在编译可执行文件之前)

cp rootCA.pem assets/client/tls/ngrokroot.crt
cp device.crt assets/server/tls/snakeoil.crt
cp device.key assets/server/tls/snakeoil.key

3、编译ngrokd和ngrok

在ngrok目录下执行如下命令,编译ngrokd:

$ make release-server

不过在我的AWS上,出现如下错误:

GOOS="" GOARCH="" go get github.com/jteeuwen/go-bindata/go-bindata
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/client/assets/assets_release.go \
        assets/client/…
make: bin/go-bindata: Command not found
make: *** [client-assets] Error 127

go-bindata被安装到了$GOBIN下了,go编译器找不到了。修正方法是将$GOBIN/go-bindata拷贝到当前ngrok/bin下。

$ cp /home/ubuntu/.bin/go14/bin/go-bindata ./bin

再次执行make release-server。

~/goproj/src/github.com/inconshreveable/ngrok$ make release-server
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/client/assets/assets_release.go \
        assets/client/…
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/server/assets/assets_release.go \
        assets/server/…
go get -tags 'release' -d -v ngrok/…
code.google.com/p/log4go (download)
go: missing Mercurial command. See http://golang.org/s/gogetcmd
package code.google.com/p/log4go: exec: "hg": executable file not found in $PATH
github.com/gorilla/websocket (download)
github.com/inconshreveable/go-update (download)
github.com/kardianos/osext (download)
github.com/kr/binarydist (download)
github.com/inconshreveable/go-vhost (download)
github.com/inconshreveable/mousetrap (download)
github.com/nsf/termbox-go (download)
github.com/mattn/go-runewidth (download)
github.com/rcrowley/go-metrics (download)
Fetching https://gopkg.in/yaml.v1?go-get=1
Parsing meta tags from https://gopkg.in/yaml.v1?go-get=1 (status code 200)
get "gopkg.in/yaml.v1": found meta tag main.metaImport{Prefix:"gopkg.in/yaml.v1", VCS:"git", RepoRoot:"https://gopkg.in/yaml.v1"} at https://gopkg.in/yaml.v1?go-get=1
gopkg.in/yaml.v1 (download)
make: *** [deps] Error 1

又出错!提示找不到hg,原来是aws上没有安装hg。install hg后(sudo apt-get install mercurial),再编译。

$ make release-server
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/client/assets/assets_release.go \
        assets/client/…
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/server/assets/assets_release.go \
        assets/server/…
go get -tags 'release' -d -v ngrok/…
code.google.com/p/log4go (download)
go install -tags 'release' ngrok/main/ngrokd

同样编译ngrok:

$ make release-client
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/client/assets/assets_release.go \
        assets/client/…
bin/go-bindata -nomemcopy -pkg=assets -tags=release \
        -debug=false \
        -o=src/ngrok/server/assets/assets_release.go \
        assets/server/…
go get -tags 'release' -d -v ngrok/…
go install -tags 'release' ngrok/main/ngrok

AWS上ngrokd和ngrok被安装到了$GOBIN下。

三、调试

1、启动ngrokd

$ ngrokd -domain="tunnel.tonybai.com" -httpAddr=":8080" -httpsAddr=":8081"
[03/14/15 04:47:24] [INFO] [registry] [tun] No affinity cache specified
[03/14/15 04:47:24] [INFO] [metrics] Reporting every 30 seconds
[03/14/15 04:47:24] [INFO] Listening for public http connections on [::]:8080
[03/14/15 04:47:24] [INFO] Listening for public https connections on [::]:8081
[03/14/15 04:47:24] [INFO] Listening for control and proxy connections on [::]:4443

… …

2、公网连接ngrokd

将生成的ngrok下载到自己的电脑上。

创建一个配置文件ngrok.cfg,内容如下:

server_addr: "tunnel.tonybai.com:4443"
trust_host_root_certs: false

执行ngrok:
$ ngrok -subdomain example -config=ngrok.cfg 80

Tunnel Status                 reconnecting
Version                       1.7/
Web Interface                 127.0.0.1:4040
# Conn                        0
Avg Conn Time                 0.00ms

连接失败。此刻我的电脑是在公网上。查看ngrokd的日志,没有发现连接到达Server端。试着在本地ping tunnel.tonybai.com这个地址,发现地址不通。难道是DNS设置的问题。之前我只是设置了"*.tunnel.tonybai.com"的A地址,并未设置"tunnel.tonybai.com"。于是到DNS管理页面,添加了"tunnel.tonybai.com"的A记录。

待DNS记录刷新OK后,再次启动ngrok:

Tunnel Status online
Version 1.7/1.7
Forwarding http://epower.tunnel.tonybai.com:8080 -> 127.0.0.1:80
Forwarding https://epower.tunnel.tonybai.com:8080 -> 127.0.0.1:80
Web Interface 127.0.0.1:4040
# Conn 0
Avg Conn Time 0.00ms

这回连接成功了!

3、内网连接ngrokd

将ngrok拷贝到内网的一台PC上,这台PC设置了公司的代理。

按照同样的步骤启动ngrok:

$ ngrok -subdomain example -config=ngrok.cfg 80

Tunnel Status                 reconnecting
Version                       1.7/
Web Interface                 127.0.0.1:4040
# Conn                        0
Avg Conn Time                 0.00ms

不巧,怎么又失败了!从Server端来看,还是没有收到客户端的连接,显然是连接没有打通公司内网。从我自己的squid代理服务器来看,似乎只有443端口的请求被公司代理服务器允许通过,4443则无法出去。

1426301143.558 9294 10.10.126.101 TCP_MISS/000 366772 CONNECT api.equinox.io:443 – DEFAULT_PARENT/proxy.xxx.com -   通过了
1426301144.441 27 10.10.126.101 TCP_MISS/000 1185 CONNECT tunnel.tonybai.com:4443 – DEFAULT_PARENT/proxy.xxx.com -  似乎没有通过

只能修改server监听端口了。将-tunnelAddr由4443改为443(注意AWS上需要修改防火墙的端口规则,这个是实时生效的,无需重启实例):

$ sudo ngrokd -domain="tunnel.tonybai.com" -httpAddr=":8080" -httpsAddr=":8081" -tunnelAddr=":443"
[03/14/15 04:47:24] [INFO] [registry] [tun] No affinity cache specified
[03/14/15 04:47:24] [INFO] [metrics] Reporting every 30 seconds
[03/14/15 04:47:24] [INFO] Listening for public http connections on [::]:8080
[03/14/15 04:47:24] [INFO] Listening for public https connections on [::]:8081
[03/14/15 04:47:24] [INFO] Listening for control and proxy connections on [::]:443

… …

将ngrok.cfg中的地址改为443:

server_addr: "tunnel.tonybai.com:443"

再次执行ngrok客户端:

Tunnel Status                 online
Version                       1.7/1.7
Forwarding                    http://epower.tunnel.tonybai.com:8080 -> 127.0.0.1:80
Forwarding                    https://epower.tunnel.tonybai.com:8080 -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        0
Avg Conn Time                 0.00ms

这回成功连上了。

4、80端口

是否大功告成了呢?我们看看ngrok的结果,总感觉哪里不对呢?噢,转发的地址怎么是8080端口呢?为何不是80?微信公众号/企业号可只是支持80端口啊!

我们还需要修改一下Server端的参数,将-httpAddr从8080改为80。

$ sudo ngrokd -domain="tunnel.tonybai.com" -httpAddr=":80" -httpsAddr=":8081" -tunnelAddr=":443"

这回再用ngrok连接一下:
Tunnel Status                 online
Version                       1.7/1.7
Forwarding                    http://epower.tunnel.tonybai.com -> 127.0.0.1:80
Forwarding                    https://epower.tunnel.tonybai.com -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        0
Avg Conn Time                 0.00ms

这回与我们的需求匹配上了。

5、测试

在内网的PC上建立一个简单的http server 程序:hello

//hello.go
package main

import "net/http"

func main() {
    http.HandleFunc("/", hello)
    http.ListenAndServe(":80", nil)
}

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello!"))
}

$ go build -o hello hello.go
$ sudo ./hello

通过公网浏览器访问一下“http://epower.tunnel.tonybai.com”这个地址,如果你看到浏览器返回"hello!"字样,那么你的ngrokd服务就搭建成功了!

四、注意事项

客户端ngrok.cfg中server_addr后的值必须严格与-domain以及证书中的NGROK_BASE_DOMAIN相同,否则Server端就会出现如下错误日志:

[03/13/15 09:55:46] [INFO] [tun:15dd7522] New connection from 54.149.100.42:38252
[03/13/15 09:55:46] [DEBG] [tun:15dd7522] Waiting to read message
[03/13/15 09:55:46] [WARN] [tun:15dd7522] Failed to read message: remote error: bad certificate
[03/13/15 09:55:46] [DEBG] [tun:15dd7522] Closing

Golang跨平台交叉编译

近期在某本书上看到Go跨平台交叉编译的强大功能,于是想自己测试一下。以下记录了测试过程以及一些结论,希望能给大家带来帮助。

我的Linux环境如下:

uname -a
Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ go version
go version go1.3.1 linux/amd64

跨平台交叉编译涉及两个重要的环境变量:GOOS和GOARCH,分别代表Target Host OS和Target Host ARCH,如果没有显式设置这些环境变量,我们通过go env可以看到go编译器眼中这两个环境变量的当前值:

$ go env
GOARCH="amd64"
GOOS="linux"

GOHOSTARCH="amd64"
GOHOSTOS="linux"

… …

这里还有两个变量GOHOSTOS和GOHOSTARCH,分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的,因此默认情况下,这两组环境变量的值都是来自当前主机的信息。

现在我们就来交叉编译一下:在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序,样例程序如下:

//testport.go
package main

import (
        "fmt"
        "os/exec"
        "bytes"
)

func main() {
        cmd := exec.Command("uname", "-a")
        var out bytes.Buffer
        cmd.Stdout = &out

        err := cmd.Run()
        if err != nil {
                fmt.Println("Err when executing uname command")
                return
        }

        fmt.Println("I am running on", out.String())
}

在Linux/amd64下编译运行:

$ go build -o testport_linux testport.go
$ testport_linux
I am running on Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

接下来,我们来尝试在Linux/amd64上编译一个可以运行在darwin/amd64上的程序。我只需修改GOOS和GOARCH两个标识目标主机OS和ARCH的环境变量:

$ GOOS=darwin GOARCH=amd64 go build -o testport_darwin testport.go
go build runtime: darwin/amd64 must be bootstrapped using make.bash

编译器报错了!提示darwin/amd64必须通过make.bash重新装载。显然,通过安装包安装到linux/amd64下的Go编译器还无法直接交叉编译出darwin/amd64下可以运行的程序,我们需要做一些准备工作。我们找找make.bash在哪里!

我们到Go的$GOROOT路径下去找make.bash,Go的安装路径下的组织很简约,扫一眼便知make.sh大概在$GOROOT/src下,打开make.sh,我们在文件头处看到如下一些内容:

# Environment variables that control make.bash:
#
# GOROOT_FINAL: The expected final Go root, baked into binaries.
# The default is the location of the Go tree during the build.
#
# GOHOSTARCH: The architecture for host tools (compilers and
# binaries).  Binaries of this type must be executable on the current
# system, so the only common reason to set this is to set
# GOHOSTARCH=386 on an amd64 machine.
#
# GOARCH: The target architecture for installed packages and tools.
#
# GOOS: The target operating system for installed packages and tools.

… …

make.bash头并未简要说明文件的用途,但名为make.xx的文件想必是用来构建Go编译工具的。这里提到几个环境变量可以控制 make.bash的行为,显然GOARCH和GOOS更能引起我们的兴趣。我们再回过头来输出testport.go编译过程的详细信息:

$ go build -x -o testport_linux testport.go
WORK=/tmp/go-build286732099
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a

我们发现Go实际上用的是$GOROOT/pkg/tool/linux_amd64下的6g(编译器)和6l(链接器)来完成整个编译过程的,看到6g 和6l所在目录名为linux_amd64,我们可以大胆猜测编译darwin/amd64 go程序应该使用的是$GOROOT/pkg/tool/darwin_amd64下的工具。不过在我在$GOROOT/pkg/tool下没有发现 darwin_amd64目录,也就是说我们通过安装包安装的Go仅自带了for linux_amd64的编译工具,要想交叉编译出for darwin_amd64的程序,我们需要通过make.bash来手工编译出这些工具。

tonybai@ubuntu-Server-14:/usr/local/go/pkg$ ls
linux_amd64  linux_amd64_race  obj  tool

tonybai@ubuntu-Server-14:/usr/local/go/pkg/tool$ ls
linux_amd64

根据前面make.bash的用法说明,我们来尝试构建一下:

cd $GOROOT/src
sudo GOOS=darwin GOARCH=amd64 ./make.bash

# Building C bootstrap tool.
cmd/dist

# Building compilers and Go bootstrap tool for host, linux/amd64.
… …
cmd/cc
cmd/gc
cmd/6l
cmd/6a
cmd/6c
cmd/6g
pkg/runtime
… …
cmd/go
pkg/runtime (darwin/amd64)

# Building packages and commands for host, linux/amd64.
runtime
… …
text/scanner

# Building packages and commands for darwin/amd64.
runtime
errors
… …
testing/quick
text/scanner


Installed Go for darwin/amd64 in /usr/local/go
Installed commands in /usr/local/go/bin

编译后,我们再来试试编译for darwin_amd64的程序:

$ GOOS=darwin GOARCH=amd64 go build -x -o testport_darwin testport.go
WORK=/tmp/go-build972764136
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_darwin -L $WORK -extld=gcc $WORK/command-line-arguments.a

将文件copy到我的Mac Air下执行:

$chmod +x testport_darwin
$testport_darwin
I am running on Darwin TonydeMacBook-Air.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64

编译虽然成功了,但从-x输出的详细编译过程来看,Go编译连接使用的工具依旧是linux_amd64下的6g和6l,为什么没有使用darwin_amd64下的6g和6l呢?原来$GOROOT/pkg/tool/darwin_amd64下根本就没有6g和6l:

/usr/local/go/pkg/tool/darwin_amd64$ ls
addr2line  cgo  fix  nm  objdump  pack  yac
c

但查看一下pkg/tool/linux_amd64/下程序的更新时间:

/usr/local/go/pkg/tool/linux_amd64$ ls -l
… …
-rwxr-xr-x 1 root root 2482877 10月 20 15:12 6g
-rwxr-xr-x 1 root root 1186445 10月 20 15:12 6l
… …

我们发现6g和6l都是被刚才的make.bash新编译出来的,我们可以得出结论:新6g和新6l目前既可以编译本地程序(linux/amd64),也可以编译darwin/amd64下的程序了,例如重新编译testport_linux依旧ok:

$ go build -x -o testport_linux testport.go
WORK=/tmp/go-build636762567
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a

如果我们还想给Go编译器加上交叉编译windows/amd64程序的功能,我们再执行一次make.bash:

sudo GOOS=windows GOARCH=amd64 ./make.bash

编译成功后,我们来编译一下Windows程序:

$ GOOS=windows GOARCH=amd64 go build -x -o testport_windows.exe testport.go
WORK=/tmp/go-build626615350
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_windows.exe -L $WORK -extld=gcc $WORK/command-line-arguments.a

把testport_windows.exe扔到Windows上执行,结果:

Err when executing uname command

显然Windows下没有uname命令,提示执行出错。

至此,我的Go编译器具备了在Linux下编译windows/amd64和darwin/amd64的能力。如果你还想增加其他平台的能力,就像上面那样操作执行make.bash即可。

如果在go源文件中有与C语言的交互代码,那么交叉编译功能是否还能奏效呢?毕竟C在各个平台上的运行库、链接库等都是不同的。我们先来看看这个例子,我们使用之前在《探讨docker容器对共享内存的支持情况》一文中的一个例子:

//testport_cgoenabled.go
package main

//#include <stdio.h>
//#include <sys/types.h>
//#include <sys/mman.h>
//#include <fcntl.h>
//
//#define SHMSZ     27
//
//int shm_rd()
//{
//      char c;
//      char *shm = NULL;
//      char *s = NULL;
//      int fd;
//      if ((fd = open("./shm.txt", O_RDONLY)) == -1)  {
//              return -1;
//      }
//
//      shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);
//      if (!shm) {
//              return -2;
//      }
//
//      close(fd);
//      s = shm;
//      int i = 0;
//      for (i = 0; i < SHMSZ – 1; i++) {
//              printf("%c ", *(s + i));
//      }
//      printf("\n");
//
//      return 0;
//}
import "C"

import "fmt"

func main() {
        i := C.shm_rd()
        if i != 0 {
                fmt.Println("Mmap Share Memory Read Error:", i)
                return
        }
        fmt.Println("Mmap Share Memory Read Ok")
}

我们先编译出一个本地可运行的程序:

$ go build -x -o testport_cgoenabled_linux testport_cgoenabled.go
WORK=/tmp/go-build977176241
mkdir -p $WORK/command-line-arguments/_obj/
cd /home/tonybai/Test/Go/porting
CGO_LDFLAGS="-g" "-O2" /usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ — -I $WORK/command-line-arguments/_obj/ testport_cgoenabled.go
/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_defun.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_defun.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -print-libgcc-file-name
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_main.o -c $WORK/command-line-arguments/_obj/_cgo_main.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_export.o -c $WORK/command-line-arguments/_obj/_cgo_export.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -c $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_cgo_.o $WORK/command-line-arguments/_obj/_cgo_main.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2
/usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ -dynimport $WORK/command-line-arguments/_obj/_cgo_.o -dynout $WORK/command-line-arguments/_obj/_cgo_import.c
/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_import.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_import.c
gcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_all.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2 -Wl,-r -nostdlib /usr/lib/gcc/x86_64-linux-gnu/4.8/libgcc.a
/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -D _/home/tonybai/Test/Go/porting -I $WORK -pack $WORK/command-line-arguments/_obj/_cgo_gotypes.go $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo1.go
pack r $WORK/command-line-arguments.a $WORK/command-line-arguments/_obj/_cgo_import.6 $WORK/command-line-arguments/_obj/_cgo_defun.6 $WORK/command-line-arguments/_obj/_all.o # internal
cd .
/usr/local/go/pkg/tool/linux_amd64/6l -o testport_cgoenabled_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a

输出了好多日志!不过可以看出Go编译器先调用CGO对Go源码中的C代码进行了编译,然后才是常规的Go编译,最后通过6l链接在一起。Cgo似乎直接使用了Gcc。我们再来试试跨平台编译:

$ GOOS=darwin GOARCH=amd64 go build -x -o testport_cgoenabled_darwin testport_cgoenabled.go
WORK=/tmp/go-build124869433
can't load package: no buildable Go source files in /home/tonybai/Test/Go/porting

当我们编译for Darwin/amd64平台的程序时,Go无法像之前那样的顺利完成编译,而是提示错误。从网上给出的资料来看,如果Go源码中包含C互操作代码,那么 目前依旧无法实现交叉编译,因为cgo会直接使用各个平台的本地c编译器去编译Go文件中的C代码。默认情况下,make.bash会置 CGO_ENABLED=0。

如果你非要将CGO_ENABLED设置为1去编译go的话,至少我得到了如下错误,导致无法编译通过:

$ sudo CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./make.bash –no-clean
… …
# Building packages and commands for darwin/amd64.
… …
37: error: 'AI_MASK' undeclared (first use in this function)

 

探讨Docker容器中修改系统变量的方法

探讨完Docker对共享内存状态持久化的支持状况后,我将遗留产品build到一个pre-production image中,测试启动是否OK。很显然,我过于乐观了,Docker之路并不平坦。我收到了shmget报出的EINVAL错误码,提示参数非法。 shmget的manual对EINVAL错误码的说明如下:

EINVAL:
A  new  segment  was  to  be  created  and size < SHMMIN or size > SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment.

显然我们要创建的shared memory的size很可能大于SHMMAX这个系统变量了。那么一个从base image创建出的容器中的系统变量到底是什么值呢?我们来查看一下,我们基于"centos:centos6"启动一个Docker容器,并检查其中的 系统变量值设置:

$ sudo docker run -it "centos:centos6" /bin/bash
bash-4.1# cat /proc/sys/kernel/shmmax
33554432
bash-4.1# sysctl -a|grep shmmax
kernel.shmmax = 33554432

可以看出默认情况下,当前容器中root账号看到的shmmax值我33554432, 我的程序要创建的shm size的确要大于这个值,报出EINVAL错误也就无可厚非了。我尝试按照物理机上的方法临时修改一下该值:

bash-4.1# echo 68719476736 > /proc/sys/kernel/shmmax
bash: /proc/sys/kernel/shmmax: Read-only file system

/proc/sys/kernel/shmmax居然是只读的,无法修改。

我又尝试修改/etc/sysctl.conf这个持久化系统变量的地方,但打开/etc/sysctl.conf文件,我发现我又错了,这 个文件中shmmax的值如下:

# Controls the maximum shared segment size, in bytes
kernel.shmmax = 68719476736

/etc/sysctl.conf文件 中的系统变量shmmax的值是68719476736,而系统当前的实际值则是33554432,难道是/etc /sysctl.conf中的值没有生效,于是我手工重新加载一次该文件:

-bash-4.1# sysctl -p
error: "Read-only file system" setting key "net.ipv4.ip_forward"
error: "Read-only file system" setting key "net.ipv4.conf.default.rp_filter"
error: "Read-only file system" setting key "net.ipv4.conf.default.accept_source_route"
error: "Read-only file system" setting key "kernel.sysrq"
error: "Read-only file system" setting key "kernel.core_uses_pid"
error: "net.ipv4.tcp_syncookies" is an unknown key
error: "net.bridge.bridge-nf-call-ip6tables" is an unknown key
error: "net.bridge.bridge-nf-call-iptables" is an unknown key
error: "net.bridge.bridge-nf-call-arptables" is an unknown key
error: "Read-only file system" setting key "kernel.msgmnb"
error: "Read-only file system" setting key "kernel.msgmax"
error: "Read-only file system" setting key "kernel.shmmax"
error: "Read-only file system" setting key "kernel.shmall"

我得到了和之前类似的错误结果:只读文件系统,无法修改。于是乎两个问题萦绕在我的面前:
1、为什么容器内当前系统变量值与sysctl.conf中的不一致?
2、为什么无法修改当前系统变量值?

在翻阅了Stackoverflow, github docker issues后,我得到了的答案如下:

1、Docker的base image做的很精简,甚至都没有init进程,原本在OS启动时执行生效系统变量的过程(sysctl -p)也给省略了,导致这些系统变量依旧保留着kernel默认值。以CentOs为例,在linux kernel boot后,init都会执行/etc/rc.d/rc.sysinit,后者会加载/etc/sysctl.conf中的系统变量值。下面是 CentOs5.6中的rc.sysinit代码摘录:

… …
# Configure kernel parameters
update_boot_stage RCkernelparam
sysctl -e -p /etc/sysctl.conf >/dev/null 2>&1
… …

2、Docker容器中的系统变量在non-priviledged模式下目前(我使用的时docker 1.2.0版本)就无法修改,这 和resolv.conf、hosts等文件映射到宿主机对应的文件有不同。

$ mount -l
…. ….
/dev/mapper/ubuntu–Server–14–vg-root on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/mapper/ubuntu–Server–14–vg-root on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/mapper/ubuntu–Server–14–vg-root on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)
… …

那么我们该如何修改系统变量值来满足遗留产品的需求呢?

一、使用–privileged选项

我们使用–privileged这个特权选项来启动一个基于centos:centos6的新容器,看看是否能对shmmax这样的系统变量值 进行修改:

$ sudo docker run -it –privileged  "centos:centos6" /bin/bash
bash-4.1# cat /proc/sys/kernel/shmmax
33554432
bash-4.1# echo 68719476736 > /proc/sys/kernel/shmmax
bash-4.1# cat /proc/sys/kernel/shmmax
68719476736

bash-4.1# sysctl -p
net.ipv4.ip_forward = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
… …
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296

可以看出,通过–privileged选项,容器获得了额外的特权,并且可以对系统变量的值进行修改了。不过这样的修改是不能保存在容器里的, 我们stop 容器,再重启该容器就能看出来:

$ sudo docker start 3e22d65a7845
$ sudo docker attach 3e22d65a7845
bash-4.1# cat /proc/sys/kernel/shmmax
33554432

shmmax的值在容器重启后又变回了原先的那个默认值。不过重启后的容器依旧具有privileged的特权,我们还可以重新手工执行命令对系 统变量进行修改:

bash-4.1# echo 68719476736 > /proc/sys/kernel/shmmax
bash-4.1# cat /proc/sys/kernel/shmmax
68719476736

但即便这样,也无法满足我们的需求,我们总不能每次都在容器中手工执行系统变量值修改的操作吧。privileged选项的能力能否带到 image中呢?答案是目前还不能,我们无法在build image时通过privileged选项修改系统变量值。

这样一来,我们能做的只有把产品启动与系统变量值修改放在一个脚本中了,并将该脚本作为docker 容器的cmd命令来执行,比如我们构建一个Dockerfile:

FROM centos:centos6
MAINTAINER Tony Bai <bigwhite.cn@gmail.com>
RUN yum install python-setuptools -y
RUN easy_install supervisor
RUN mkdir -p /var/log/supervisor
COPY ./supervisord.conf /etc/supervisord.conf
COPY ./start.sh /bin/start.sh
RUN chmod +x /bin/start.sh
CMD ["/bin/start.sh]

//start.sh
sysctl -p
/usr/bin/supervisord

这样,start.sh在supervisord启动前将系统变量值重新加载,而supervisord后续启动的程序就可以看到这些新系统变量 的值了。不过别忘了利用这个image启动容器时要加上–priviledged选项,否则容器启动就会失败。

二、使用phusion/baseimage

前面说过/etc/sysctl.conf中的值没有生效是因为docker官方提供的centos:centos6把init进程的初始化过程给精 简掉了。phusion/baseimage是目前docker registery上仅次于ubuntu和centos两个之后的base image,其提供了/sbin/my_init这个init进程,用于在container充当init进程的角色。那么my_init是否可以用于执行sysctl -p呢?我们试验一下:

我们先pull这个base image下来:sudo docker pull phusion/baseimage。pull成功后,我们先基于“phusion/baseimage”启动一个容器做一些explore工作:

$ sudo docker run -i -t "phusion/baseimage"
*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh…
No SSH host key available. Generating one…
Creating SSH2 RSA key; this may take some time …
Creating SSH2 DSA key; this may take some time …
Creating SSH2 ECDSA key; this may take some time …
Creating SSH2 ED25519 key; this may take some time …
invoke-rc.d: policy-rc.d denied execution of restart.
*** Running /etc/rc.local…
*** Booting runit daemon…
*** Runit started as PID 100

通过nsenter进去,查看一下/sbin/my_init的源码,我们发现这是一个python脚本,不过从头到尾浏览一遍,没有发现sysctl加载/etc/sysctl.conf系统变量的操作。

不过,phusion文档中说my_init可以在初始化过程中执行/etc/my_init.d下的脚本。那是不是我们将一个执行sysctl -p的脚本放入/etc/my_init.d下就可以实现我们的目的了呢?试试。

我们编写一个脚本:load_sys_varibles.sh

#!/bin/sh
sysctl -p > init.txt

下面是制作image的Dockerfile:

FROM phusion/baseimage:latest
MAINTAINER Tony Bai <bigwhite.cn@gmail.com>
RUN echo "kernel.shmmax = 68719476736" >> /etc/sysctl.conf
RUN mkdir -p /etc/my_init.d
ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh
RUN chmod +x /etc/my_init.d/load_sys_varibles.sh
CMD ["/sbin/my_init"]

phusion/baseimage是基于ubuntu的OS,其sysctl.conf默认情况下没啥内容,所以我们在Dockerfile中向这个文件写入我们需要的系统变量值。构建image并启动容器:

$ sudo docker build -t "myphusion:v1" ./
Sending build context to Docker daemon 13.12 MB
Sending build context to Docker daemon
Step 0 : FROM phusion/baseimage:latest
 —> cf39b476aeec
Step 1 : MAINTAINER Tony Bai <bigwhite.cn@gmail.com>
 —> Using cache
 —> d0e9b51a3e4f
Step 2 : RUN echo "kernel.shmmax = 68719476736" >> /etc/sysctl.conf
 —> Using cache
 —> 2c800687cc83
Step 3 : RUN mkdir -p /etc/my_init.d
 —> Using cache
 —> fe366eea5eb4
Step 4 : ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh
 —> a641bb595fb9
Removing intermediate container c381b9f001c2
Step 5 : RUN chmod +x /etc/my_init.d/load_sys_varibles.sh
 —> Running in 764866552f25
 —> eae3d7f1eac5
Removing intermediate container 764866552f25
Step 6 : CMD ["/sbin/my_init"]
 —> Running in 9ab8d0b717a7
 —> 8be4e7b6b174
Removing intermediate container 9ab8d0b717a7
Successfully built 8be4e7b6b174

$ sudo docker run -it "myphusion:v1"
*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh…
No SSH host key available. Generating one…
Creating SSH2 RSA key; this may take some time …
Creating SSH2 DSA key; this may take some time …
Creating SSH2 ECDSA key; this may take some time …
Creating SSH2 ED25519 key; this may take some time …
invoke-rc.d: policy-rc.d denied execution of restart.
*** Running /etc/my_init.d/load_sys_varibles.sh…
sysctl: setting key "kernel.shmmax": Read-only file system
*** /etc/my_init.d/load_sys_varibles.sh failed with status 255

*** Killing all processes…

唉,还是老问题!即便是在my_init中执行,依旧无法逾越Read-only file system,查看Phusion/baseimage的Dockerfile才知道,它也是From ubuntu:14.04的,根不变,上层再怎么折腾也没用。

换一种容器run方法吧,加上–privileged:

$ sudo docker run -it –privileged  "myphusion:v1"
*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh…
No SSH host key available. Generating one…
Creating SSH2 RSA key; this may take some time …
Creating SSH2 DSA key; this may take some time …
Creating SSH2 ECDSA key; this may take some time …
Creating SSH2 ED25519 key; this may take some time …
invoke-rc.d: policy-rc.d denied execution of restart.
*** Running /etc/my_init.d/load_sys_varibles.sh…
*** Running /etc/rc.local…
*** Booting runit daemon…
*** Runit started as PID 102

这回灵光了。enter到容器里看看设置的值是否生效了:

root@9e399f46372a:~#cat /proc/sys/kernel/shmmax
68719476736

结果如预期。这样来看phusion/baseimage算是为sysctl -p加载系统变量值提供了一个便利,但依旧无法脱离–privileged,且依旧无法在image中持久化这个设置。

在Docker github的issue中有人提出建议在Dockerfile中加入类似RUNP这样的带有特权的指令语法,但不知何时才能在Docker中加入这一功能。

总而言之,基于目前docker官网提供的base image,我们很难找到特别理想的修改系统变量值的方法,除非自己制作base image,这个还没尝试过,待后续继续研究。




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


文章

评论

  • 正在加载...

分类

标签

归档











更多