个人笔记

专注互联网

蓝灯局域网共享

蓝灯

蓝灯 github https://github.com/getlantern/lantern,其他页面

  1. https://getlantern.org/
  2. http://s3.amazonaws.com/urtuz53txrmk9/index.html

下载For windows版本,安装,我的版本是3.7.2,默认绑定的127.0.0.1主机,使用了三个端口,一个用于ui,一个用于socks5代理,一个用于http代理。

windows下会自动设置系统代理为lantern的http代理,也可以用SwitchyOmega之类的工具只代理浏览器

具体使用的端口可以从配置文件【C:\Users\xxx\AppData\Roaming\Lantern\setting.yaml】文件中找到,key分别是addr、uiAddr、socksAddr。(不同的版本,配置文件可能有出入,端口也有出入

另一种方法是在打开lantern的时候自动打开的浏览器页面查看,具体是点击左上角的按钮 - 设置 - 高级设置(addr、socksAddr),uiAddr从浏览器地址栏就可以知道。

默认情况下,可以上网,免费账户每个月500M高速流量

共享

为了共享给局域网使用,特别是版本欠缺的iphone,由于默认绑定的127.0.0.1,局域网其他主机是无法访问socks5和http代理的,最简单直接的办法就是让其绑定0.0.0.0,google了网上教程,发现都无效,可能新版本都修复了那些漏洞,毕竟任意让你共享,收费版就玩不下去了。

直接日它不行那就取巧,将local的代理forward(中继)到一个绑定了0.0.0.0的其他端口

具体使用的工具是privoxy,一个成熟稳定的代理工具,支持Windows,我下载的版本是3.0.10

安装之后,找到【c:\Program files(x86)\Privoxy】的config.txt,将listen-address绑定的IP从127.0.0.1改成0.0.0.0,另外增加forward配置,将本机的lantern http代理forward到本机的8118端口

listen-address  0.0.0.0:8118
forward / 127.0.0.1:49763

我尝试过用【forward-socks5(t) / 127.0.0.1:49764 .】来重定向socks5代理,发现不行,只好重定向http代理了。

Golang持续集成,并生成docker镜像

本文代码请参考https://github.com/qjw/git-notify,部分脚本参考drone的实现

将讲述两种ci,分别是dronetravis-ci

编译

.PHONY: build

ifneq ($(shell uname), Darwin)
EXTLDFLAGS = -extldflags "-static" $(null)
else
EXTLDFLAGS =
endif

PACKAGES = $(shell go list ./... | grep -v /vendor/)

deps: deps_backend

deps_backend:
go get -u github.com/kardianos/govendor
go get -u github.com/jteeuwen/go-bindata/...
govendor sync

gen: gen_template

gen_template:
go generate github.com/qjw/git-notify/templates/

test:
go test -cover $(PACKAGES)

build: build_static

build_static:
CGO_ENABLED=0 GOOS=linux go install -a -installsuffix cgo \
-ldflags '${EXTLDFLAGS}' github.com/qjw/git-notify
mkdir -p release
cp $(GOPATH)/bin/git-notify release/
  1. makefile中的项目路径需要根据实际情况修改
  2. 依赖的二进制工具(deps_backend)也因项目而异
  3. 上面的govendor sync是用来根据vendor/vendor.json来同步依赖库,若使用其他,需要酌情修改
  4. git-notify有模板资源,并且依赖<github.com/jteeuwen/go-bindata>打包到代码中,所以需要go template,根据需要作删减

有了这个,运行下面命令即可在项目主目录/release生成目标执行文件。为了生成的docker镜像更小,这里使用静态编译,若无此需求,可以普通编译,以减少目标文件尺寸

make deps gen test build

Dockerfile

编译成功之后,就可以运行下列命令生成docker镜像

docker build -t git-notify:0.1 .

FROM scratch
ADD release/git-notify /
CMD ["/git-notify"]

drone

workspace:
base: /go
path: src/github.com/qjw/git-notify

pipeline:
test:
image: golang:1.8
environment:
- GO15VENDOREXPERIMENT=1
commands:
- make deps gen
- make test

compile:
image: golang:1.8
environment:
- GO15VENDOREXPERIMENT=1
- GOPATH=/go
commands:
- export PATH=$PATH:$GOPATH/bin
- make build

docker:
image: plugins/docker
username: ${EMAIL}
password: ${PASSWORD}
email: ${EMAIL}
repo: hub.c.163.com/${USERNAME}/test
registry: hub.c.163.com
tags: latest

travis-ci

sudo: required
language: go
go:
- 1.8

services:
- docker

branches:
only:
- master

install:
- make deps gen
- make test

script:
- make build

after_script:
- docker build -t qjw/git-notify .
- docker login -u ${EMAIL} -p ${PASSWORD} hub.c.163.com
- docker tag qjw/git-notify hub.c.163.com/${USERNAME}/git-notify
- docker push hub.c.163.com/${USERNAME}/git-notify

参考

  1. https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/

Golang单元测试

简单用法

记住下面的这些原则:

  1. 文件名必须是_test.go结尾的,这样在执行go test的时候才会执行到相应的代码
  2. 你必须import testing这个包
  3. 所有的测试用例函数必须是Test开头
  4. 测试用例会按照源代码中写的顺序依次执行
  5. 测试函数TestXxx()的参数是testing.T,我们可以使用该类型来记录错误或者是测试状态
  6. 测试格式:func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如Testintdiv是错误的函数名。
  7. .T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log方法用来记录测试的信息。

例如 fibonacci.go

package lib

//斐波那契数列
//求出第n个数的值
func Fibonacci(n int64) int64 {
if n < 2 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}

fibonacci_test.go

package lib

import (
"testing"
)

func TestFibonacci(t *testing.T) {
r := Fibonacci(10)
if r != 55 {
t.Errorf("Fibonacci(10) failed. Got %d, expected 55.", r)
}
}

go test lib

构造析构

// package level initialization of database connections
func init() {
// init database connections
}

// 该函数由 go 语言的 test 框架调用
func TestLastUnit(t *testing.T) {
// 测试结束时,清理数据
defer func(userID string) {
}(userID)
}

如果待测试的功能模块涉及到文件操作,临时文件是一个不错的解决方案,go语言的 ioutil 包提供了 TempDir 和 TempFile 方法,供我们使用。

Package

在写单元测试时,一般情况下,我们将功能代码和测试代码放到同一个目录下,仅以后缀 _test 进行区分。

对于复杂的大型项目,功能依赖比较多时,通常在跟目录下再增加一个 test 文件夹,不同的测试放到不同的子目录下面,如下图所示:

覆盖率

在go语言的测试覆盖率统计时,go test通过参数covermode的设定可以对覆盖率统计模式作如下三种设定。

  1. set 缺省模式, 只记录语句是否被执行过
  2. count 记录语句被执行的次数
  3. atomic 记录语句被执行的次数,并保证在并发执行时的正确性

其他选项

  1. -cover 允许代码分析
  2. -coverprofile 输出结果文件
go test -cover -coverprofile=cover.out -covermode=count -o /tmp/testgo test
go tool cover -func=cover.out
# 用html直观展示
go tool cover -html=cover.out

httptest

针对模拟网络访问,标准库了提供了一个httptest包,可以让我们模拟http的网络调用,下面举个例子了解使用。

package test

import (
"io"
"net/http"
)

// e.g. http.HandleFunc("/health-check", HealthCheckHandler)
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
// A very simple health check.
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")

// In the future we could report back on the status of our DB, or our cache
// (e.g. Redis) by performing a simple PING, and include them in the response.
io.WriteString(w, `{"alive": true}`)
}

package test

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestHealthCheckHandler(t *testing.T) {
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
// pass 'nil' as the third parameter.
req, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
t.Fatal(err)
}

// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
rr := httptest.NewRecorder()
handler := http.HandlerFunc(HealthCheckHandler)

// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
// directly and pass in our Request and ResponseRecorder.
handler.ServeHTTP(rr, req)

// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}

// Check the response body is what we expect.
expected := `{"alive1": true}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}

stretchr/testify

和其他的单元测试相比,go提供的默认方案灵活但繁琐,https://github.com/stretchr/testify作了适当的包装简化

go get github.com/stretchr/testify

testify又分成几个模块

assert

  1. Prints friendly, easy to read failure descriptions
  2. Allows for very readable code
  3. Optionally annotate each assertion with a message
package yours

import (
"testing"
"github.com/stretchr/testify/assert"
)

func TestSomething(t *testing.T) {

// assert equality
assert.Equal(t, 123, 123, "they should be equal")

// assert inequality
assert.NotEqual(t, 123, 456, "they should not be equal")

// assert for nil (good for errors)
assert.Nil(t, object)

// assert for not nil (good when you expect something)
if assert.NotNil(t, object) {

// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal(t, "Something", object.Value)

}
}

require

The require package provides same global functions as the assert package, but instead of returning a boolean result they terminate current test.

mock

当某些接口依赖其他接口时,可以通过mock模拟依赖的接口并做出预期的输出

package test

type Random interface {
Random(limit int) int
}

type Calculator interface {
Random() int
}

func newCalculator(rnd Random) Calculator {
return calc{
rnd: rnd,
}
}

type calc struct {
rnd Random
}

func (c calc) Random() int {
return c.rnd.Random(100)
}

Calculator 接口创建依赖于Random接口,为了测试前者,我们用mock模拟一个Random接口,见randomMock

package test

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
)

type randomMock struct {
mock.Mock
}

func (o randomMock) Random(limit int) int {
args := o.Called(limit)
return args.Int(0)
}

func TestRandom(t *testing.T) {
rnd := new(randomMock)
rnd.On("Random", 100).Return(7)
calc := newCalculator(rnd)
assert.Equal(t, 7, calc.Random())
}

mockery

mockery provides the ability to easily generate mocks for golang interfaces. It removes the boilerplate coding required to use mocks.

接上面的例子,运行下面的命令

go get github.com/vektra/mockery/.../

king@king:~/code/go/src/test$ mockery -name=Random
Generating mock for: Random

就自动生成mocks/Random.go

├── main.go
├── main_test.go
├── mocks
│   └── Random.go
// Code generated by mockery v1.0.0
package mocks

import mock "github.com/stretchr/testify/mock"

// Random is an autogenerated mock type for the Random type
type Random struct {
mock.Mock
}

// Random provides a mock function with given fields: limit
func (_m *Random) Random(limit int) int {
ret := _m.Called(limit)

var r0 int
if rf, ok := ret.Get(0).(func(int) int); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int)
}

return r0
}

对应的main_test.go修改为

package test

import (
"github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/mock"
"testing"
"test/mocks"
)

//type RandomMock struct {
// mock.Mock
//}
//
//func (o RandomMock) Random(limit int) int {
// args := o.Called(limit)
// return args.Int(0)
//}

func TestRandom(t *testing.T) {
rnd := new(mocks.Random)
rnd.On("Random", 100).Return(7)
calc := newCalculator(rnd)
assert.Equal(t, 7, calc.Random())
}

另参考https://github.com/jaytaylor/mockery-example

Goblin

Minimal and Beautiful Go testing framework https://github.com/franela/goblin

Goblin是一个小巧的测试框架,可以配合testing使用,内建了assert的一些方法

package test

import (
"testing"
. "github.com/franela/goblin"
)

func Test(t *testing.T) {
g := Goblin(t)
g.Describe("Numbers", func() {
g.It("Should add two numbers ", func() {
g.Assert(1+1).Equal(2)
})
g.It("Should match equal numbers", func() {
g.Assert(2).Equal(4)
})
g.It("Should substract two numbers")
})
}

搭建gitlab测试环境

准备docker

mkdir -p /mnt/volumes/gitlab
cd /mnt/volumes/gitlab
mkdir config logs data
sudo docker run -it --rm \
--hostname localhost \
--publish 8080:80 --publish 2222:22 \
--name gitlab \
--volume /mnt/volumes/gitlab/config:/etc/gitlab \
--volume /mnt/volumes/gitlab/logs:/var/log/gitlab \
--volume /mnt/volumes/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce:latest

–hostname
指定容器中绑定的域名,会在创建镜像仓库的时候使用到,这里绑定localhost
–publish
端口映射,冒号前面是宿主机端口,后面是容器expose出的端口
–volume
volume 映射,冒号前面是宿主机的一个文件路径,后面是容器中的文件路径

配置

启动之后就可以使用地址http://127.0.0.1:8080访问本地的gitlab,

启动之后需要设置初始的密码,以及注册新用户

完了创建group和project

添加公钥,以便免密clone,从~/.ssh/isa.pub文件中获取

然后就可以clone代码了

指定git clone 的端口

由于docker将22端口重定向到了主机的2222,所以需要修改~/.ssh/config文件,添加

Host localhost
Port 2222

参考

  1. http://www.jianshu.com/p/05e3bb375f64
  2. https://stackoverflow.com/questions/3596260/git-remote-add-with-other-ssh-port

git rebase 合并commit

对git基本上是记住一些常用的功能,其他用到了去查手册,这里备注一个我不常用但挺实用的功能

一句话,将若干commit合并成一个。

本地多个commit合并

# 本地新建10个commit
for ((i=1;i<10;i++));do echo $i > a;git add .;git commit -m commit$i;done;

log如下

* a69d8fb - (HEAD -> master) commit9 (2 分钟前) <King>
* 6b24d3a - commit8 (2 分钟前) <King>
* 98aeb92 - commit7 (2 分钟前) <King>
* 8b2120b - commit6 (2 分钟前) <King>
* 8660ff5 - commit5 (2 分钟前) <King>
* 38b1be3 - commit4 (2 分钟前) <King>
* 07be2fa - commit3 (2 分钟前) <King>
* 139aabd - commit2 (2 分钟前) <King>
* f570edb - commit1 (2 分钟前) <King>

king@king:/tmp/git/work$ git rebase -i f570edb
# 将后面的提交的pick都改成s,保存确认commit的描述
pick 139aabd commit2
s 07be2fa commit3
s 38b1be3 commit4
s 8660ff5 commit5
s 8b2120b commit6
s 98aeb92 commit7
s 6b24d3a commit8
s a69d8fb commit9
king@king:/tmp/git/work$ git log
commit c040772af1dd96cd3311112193e39cbfd7a93e28
Author: King <qiujinwu456@gmail.com>
Date: Mon Apr 17 16:17:11 2017 +0800
commit2
commit3
commit4
commit5
commit6
commit7
commit8
commit9
commit f570edbf5bbe3ba661c36916c36ac12227e1cdd1
Author: King <qiujinwu456@gmail.com>
Date: Mon Apr 17 16:17:11 2017 +0800
commit1

可以看到最新的9个已经merge成一个,但是第一个和第二个还是分开了(搞不懂为啥要这么设计,将第一个pick改成s,或者其他操作都不行

如果一定要合并,可以参考http://stackoverflow.com/questions/2563632/how-can-i-merge-two-commits-into-one

# 清掉第一条commit,但是保留代码
git reset --soft "HEAD^"
# 复用上一次的commit
git commit --amend

多人合作

新建三个目录

king@king:/tmp/git$ tree . -d 2
.
├── origin
├── work
└── work1

cd /tmp/git/work
git init .
for ((i=1;i<10;i++));do echo $i > a;git add .;git commit -m commit$i;done;
git remote add origin /tmp/git/origin/
cd ../origin
git init --bare
cd -
git push origin master
cd ../work1
git clone /tmp/git/origin/ .

到目前为止,work和work1共享了origin仓库

接下来让别人push一些修改到master,自己也做了一些commit

cd /tmp/git/work
for ((i=1;i<10;i++));do echo $i > b;git add .;git commit -m local$i;done;
cd ../work1
git config user.name "another king"
for ((i=1;i<10;i++));do echo $i > c;git add .;git commit -m remote$i;done;
git push origin master
cd -

在work pull远程代码之后,log是这样的

git pull origin master

*   3d5b7d2 - (HEAD -> master) Merge branch 'master' of /tmp/git/origin (19 秒钟前) <King>
|\
| * c83b1e9 - (origin/master) remote9 (40 秒钟前) <another king>
| * 0c6e331 - remote8 (40 秒钟前) <another king>
| * 17a8fcd - remote7 (40 秒钟前) <another king>
| * c685ee7 - remote6 (40 秒钟前) <another king>
| * 8868d35 - remote5 (40 秒钟前) <another king>
| * 3b724d2 - remote4 (40 秒钟前) <another king>
| * d82b1bd - remote3 (40 秒钟前) <another king>
| * e9865f1 - remote2 (40 秒钟前) <another king>
| * 2c86a3b - remote1 (40 秒钟前) <another king>
* | 1384e0c - local9 (40 秒钟前) <King>
* | ae7c46e - local8 (40 秒钟前) <King>
* | bbea9bb - local7 (40 秒钟前) <King>
* | 88e2fbb - local6 (40 秒钟前) <King>
* | 60b7ac3 - local5 (40 秒钟前) <King>
* | c5494bd - local4 (40 秒钟前) <King>
* | 5dfe9a9 - local3 (40 秒钟前) <King>
* | 18ecbb4 - local2 (40 秒钟前) <King>
* | 76662a5 - local1 (40 秒钟前) <King>
|/
* 8383510 - commit9 (48 秒钟前) <King>
* 0a15467 - commit8 (48 秒钟前) <King>
* 3f76d47 - commit7 (48 秒钟前) <King>
* 3d469ec - commit6 (48 秒钟前) <King>
* cdcf488 - commit5 (48 秒钟前) <King>
* cab099e - commit4 (48 秒钟前) <King>
* 77a35fe - commit3 (48 秒钟前) <King>
* 5b8b08e - commit2 (48 秒钟前) <King>
* f1d3575 - commit1 (49 秒钟前) <King>

这里有个问题,别人的改动插入到我的改动之中,若改成rebase,就不一样

king@king:/tmp/git/work$ git pull --rebase origin master
remote: 对象计数中: 26, 完成.
remote: 压缩对象中: 100% (18/18), 完成.
remote: Total 26 (delta 0), reused 0 (delta 0)
展开对象中: 100% (26/26), 完成.
来自 /tmp/git/origin
* branch master -> FETCH_HEAD
4245282..ff5ceb9 master -> origin/master
首先,回退分支以便在上面重放您的工作...
应用:local1
应用:local2
应用:local3
应用:local4
应用:local5
应用:local6
应用:local7
应用:local8
应用:local9

生成后的log如下

* d6f1150 - (HEAD -> master) local9 (29 秒钟前) <King>
* 3ea2ae7 - local8 (29 秒钟前) <King>
* a1dcc9b - local7 (29 秒钟前) <King>
* 65488ec - local6 (29 秒钟前) <King>
* 417f852 - local5 (29 秒钟前) <King>
* e460dbd - local4 (29 秒钟前) <King>
* 7a0e9a5 - local3 (29 秒钟前) <King>
* cc58778 - local2 (29 秒钟前) <King>
* e69d861 - local1 (29 秒钟前) <King>
* ff5ceb9 - (origin/master) remote9 (54 秒钟前) <another king>
* dc8997b - remote8 (54 秒钟前) <another king>
* ef231f5 - remote7 (54 秒钟前) <another king>
* e7af01a - remote6 (54 秒钟前) <another king>
* 3ed0382 - remote5 (54 秒钟前) <another king>
* 1dce6c2 - remote4 (54 秒钟前) <another king>
* f726941 - remote3 (54 秒钟前) <another king>
* 13b51a6 - remote2 (54 秒钟前) <another king>
* 949d038 - remote1 (54 秒钟前) <another king>
* 4245282 - commit9 (60 秒钟前) <King>
* 6713be4 - commit8 (60 秒钟前) <King>
* d5c11c8 - commit7 (60 秒钟前) <King>
* 3e07d30 - commit6 (60 秒钟前) <King>
* 7265b3d - commit5 (60 秒钟前) <King>
* 0deb807 - commit4 (60 秒钟前) <King>
* 9b232a4 - commit3 (60 秒钟前) <King>
* bfd3ad1 - commit2 (60 秒钟前) <King>
* 108010e - commit1 (60 秒钟前) <King>

原理很简单

–rebase,这里表示把你的本地当前分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到”.git/rebase”目录中),然后把本地当前分支更新 为最新的”origin”分支,最后把保存的这些补丁应用到本地当前分支上。

参考http://blog.csdn.net/hudashi/article/details/7664631/

开发分支合并

大部分情况下,不会直接基于master分支,而是自己拉一个分支下来,然后commit,最后创建一个pull request。

cd /tmp/git/work
git init .
for ((i=1;i<10;i++));do echo $i > a;git add .;git commit -m commit$i;done;
git remote add origin /tmp/git/origin/
cd ../origin
git init --bare
cd -
git push origin master
cd ../work1
git clone /tmp/git/origin/ .

到目前为止,work和work1共享了origin仓库

接下来让别人push一些修改到master,自己开个新分支也做了一些commit。最后切回master,并且拉取别人提交到master的commit

cd /tmp/git/work
git checkout -b tmp
for ((i=1;i<10;i++));do echo $i > b;git add .;git commit -m local$i;done;
cd ../work1
git config user.name "another king"
for ((i=1;i<10;i++));do echo $i > c;git add .;git commit -m remote$i;done;
git push origin master
cd -
git checkout master
git pull origin master

一般情况是直接git merge master将别人的commit合并到临时分支

git checkout tmp
git merge master

和之前的直接pull一样,别人的提交穿插在自己的commit中间

*   36d5aa7 - (HEAD -> tmp) Merge branch 'master' into tmp (29 秒钟前) <King>
|\
| * f5c0dc8 - (origin/master, master) remote9 (2 分钟前) <another king>
| * 45ec76f - remote8 (2 分钟前) <another king>
| * 095dc1b - remote7 (2 分钟前) <another king>
| * 7999947 - remote6 (2 分钟前) <another king>
| * 889f88c - remote5 (2 分钟前) <another king>
| * 303ab0c - remote4 (2 分钟前) <another king>
| * cff4cb8 - remote3 (2 分钟前) <another king>
| * c4995a0 - remote2 (2 分钟前) <another king>
| * 56bf566 - remote1 (2 分钟前) <another king>
* | 589f098 - local9 (2 分钟前) <King>
* | e575680 - local8 (2 分钟前) <King>
* | 2a14876 - local7 (2 分钟前) <King>
* | fa8458b - local6 (2 分钟前) <King>
* | a4f6c50 - local5 (2 分钟前) <King>
* | 6118659 - local4 (2 分钟前) <King>
* | b2bfba8 - local3 (2 分钟前) <King>
* | 0f422b5 - local2 (2 分钟前) <King>
* | 3be57eb - local1 (2 分钟前) <King>
|/
* 491590d - commit9 (3 分钟前) <King>
* 918800c - commit8 (3 分钟前) <King>
* 0dfe536 - commit7 (3 分钟前) <King>
* 17521f8 - commit6 (3 分钟前) <King>
* 9db0b79 - commit5 (3 分钟前) <King>
* 58cbf4e - commit4 (3 分钟前) <King>
* 1c4abaa - commit3 (3 分钟前) <King>
* b973520 - commit2 (3 分钟前) <King>
* 9718254 - commit1 (3 分钟前) <King>

利用rebase,同样可以做的很干净

king@king:/tmp/git/work$ git checkout  tmp 
切换到分支 'tmp'
king@king:/tmp/git/work$ git rebase master
首先,回退分支以便在上面重放您的工作...
应用:local1
应用:local2
应用:local3
应用:local4
应用:local5
应用:local6
应用:local7
应用:local8
应用:local9

用docker构建c编译环境2

可以编译了,最终需要运行,而且很有必要编译和运行是同一个环境

下面以一个libevent程序来说明问题

main.c

#include <sys/types.h>  
#include <sys/stat.h>
#include <time.h>
#ifdef _EVENT_HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#include <stdio.h>
#include <event2/event.h>
#include <event2/event_struct.h>
#include <event2/util.h>

struct timeval lasttime;
int event_is_persistent;

static void timeout_cb(evutil_socket_t fd, short event, void *arg)
{
printf("timeout\n");
}

int main(int argc, char **argv)
{
struct event timeout; //创建事件
struct timeval tv;
struct event_base *base; //创建事件"总管"的指针
int flags;
//事件标志,超时事件可不设EV_TIMEOUT,因为在添加事件时可设置

event_is_persistent = 1;
flags = EV_PERSIST;
/* Initalize the event library */
base = event_base_new(); //创建事件"总管"
/* Initalize one event */
event_assign(&timeout, base, -1, flags, timeout_cb, (void*) &timeout);
evutil_timerclear(&tv);
tv.tv_sec = 1;
event_add(&timeout, &tv); //添加事件,同时设置超时时间
evutil_gettimeofday(&lasttime, NULL);
event_base_dispatch(base); //循环监视事件,事件标志的条件发生,就调用回调函数
return (0);
}

Dockerfile

FROM king_release:0.1

MAINTAINER qiujinwu@gmail.com

RUN mkdir -p /app
WORKDIR /app

COPY sources.list /etc/apt
COPY run.sh /

RUN apt-get update \
&& apt-get install build-essential -y \
&& apt-get install gdb -y \
&& apt-get install libevent-dev -y \
&& apt-get clean -y \
&& apt-get autoclean

ENTRYPOINT ["/run.sh"]

Dockerfile2

FROM ubuntu:16.04

MAINTAINER qiujinwu@gmail.com

COPY sources.list /etc/apt

RUN apt-get update \
&& apt-get install libevent-2.0-5 -y \
&& apt-get clean -y \
&& apt-get autoclean

Dockerfile3

FROM king_release:0.1
MAINTAINER qiujinwu@gmail.com
ADD a.out /
CMD ["/a.out"]

# 编译运行时基础镜像
docker build -f $PWD/Dockerfile2 -t king_release:0.1 ~/tmp/dockerfile/
# 编译 打包编译服务镜像
docker build -t king_build:0.1 ~/tmp/dockerfile/
# 编译程序
./b.sh + main.c -levent
# 编译最终运行的镜像
docker build -f $PWD/Dockerfile3 -t a.out:0.1 ~/tmp/dockerfile/

编译后如下

king@king:~/tmp/dockerfile$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
a.out 0.1 bd79dd4a7add 2 minutes ago 172 MB
king_build 0.1 e0d3c8b3e691 23 minutes ago 479 MB
king_release 0.1 cc28cd7564c0 33 minutes ago 172 MB

运行程序

king@king:~/tmp/dockerfile$ docker run -it --rm a.out:0.1
timeout
timeout
timeout
timeout

停止容器和删除镜像,无需删除容器

# 停止镜像
docker ps | grep a.out | awk '{print $1}' | xargs -i docker stop {}
# 删除镜像
docker images | grep a.out | awk '{print $3}' | xargs -i docker rmi {}

裁剪

上面的运行时还有100多M,还是挺大的,利用静态编译可以压缩到更小。

king@king:~/tmp/dockerfile$ ./b.sh + main.c -levent
king@king:~/tmp/dockerfile$ ./b.sh + main.c -levent -static -o b.out
king@king:~/tmp/dockerfile$ ll
总用量 1148
-rwxr-xr-x 1 root root 9024 4月 13 09:48 a.out*
-rwxr-xr-x 1 root root 1112800 4月 13 09:49 b.out*

可以看到,静态连接的可执行程序大了非常多,但是他没有那些系统库/三方库的依赖,所以可以很方便的使用精简版的docker来运行

Dockerfile4

FROM scratch
ADD b.out /
CMD ["/b.out"]

docker build -f $PWD/Dockerfile4 -t b.out:0.1 ~/tmp/dockerfile/
king@king:~/tmp/dockerfile$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
b.out 0.1 1176fc755f3d 2 minutes ago 1.11 MB
a.out 0.1 bd79dd4a7add 32 minutes ago 172 MB
king_build 0.1 e0d3c8b3e691 53 minutes ago 479 MB
king_release 0.1 cc28cd7564c0 About an hour ago 172 MB

可以看到打出来的docker镜像非常非常小

king@king:~/tmp/dockerfile$ docker run -it --rm b.out:0.1
timeout
timeout
timeout
timeout
(venv) king@king:~/tmp/dockerfile$ ldd a.out 
linux-vdso.so.1 => (0x00007ffdd8157000)
libevent-2.0.so.5 => /usr/lib/x86_64-linux-gnu/libevent-2.0.so.5 (0x00007f85e9e7d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f85e9ab4000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f85e9896000)
/lib64/ld-linux-x86-64.so.2 (0x000055b917112000)
(venv) king@king:~/tmp/dockerfile$ ldd b.out
不是动态可执行文件
(venv) king@king:~/tmp/dockerfile$ file b.out
b.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked,
for GNU/Linux 2.6.32, BuildID[sha1]=eb066746936748bf4438aa6473cfcd101759dd95, not stripped

常见问题

  1. 部分设备只安装了动态库,没有匹配的静态库,导致静态编译失败
  2. 静态编译时存在一个顺序问题,如果在链接时,并未发现引用就不会链接任何符号,而右边的对象如果又依赖这些库,就会出现链接失败,如下

还是上面的代码main.c

king@king:~/tmp/a$ gcc main.c -c
king@king:~/tmp/a$ gcc -levent -static main.o
main.o:在函数‘main’中:
main.c:(.text+0x5e):对‘event_base_new’未定义的引用
main.c:(.text+0x9b):对‘event_assign’未定义的引用
main.c:(.text+0xd8):对‘event_add’未定义的引用
main.c:(.text+0xf6):对‘event_base_dispatch’未定义的引用
collect2: error: ld returned 1 exit status
king@king:~/tmp/a$ gcc main.o -levent -static

用docker构建c编译环境

Dockerfile

king@king:~/tmp/dockerfile$ 
FROM ubuntu:16.04

MAINTAINER qiujinwu@gmail.com

RUN mkdir -p /app
WORKDIR /app

COPY sources.list /etc/apt
COPY run.sh /

RUN apt-get update \
&& apt-get install build-essential -y \
&& apt-get install gdb -y \
&& apt-get clean -y \
&& apt-get autoclean

ENTRYPOINT ["/run.sh"]

Docker 入口脚本

用于支持gcc/g++/make命令,也可以直接进入shell

#!/bin/bash
#for arg in "$@"
#do
# echo "arg: ${arg}"
#done

declare -r g_red="\e[0;31;44m"
declare -r g_green="\e[0;32m"
declare -r g_blue="\e[0;34m"
declare -r g_end="\e[0m"

_err(){
echo -e "${g_red}${@}${g_end}"
return 1
}

do_make(){
make "${@}"
}

do_gcc(){
gcc "${@}"
}

do_gplus(){
g++ "${@}"
}

if [ "${#}" -lt 1 ]
then
/bin/bash
elif [ "${1}" == "make" -o "${1}" == "m" ]
then
shift 1
do_make "${@}"
elif [ "${1}" == "gcc" -o "${1}" == "g" ]
then
shift 1
do_gcc "${@}"
elif [ "${1}" == "g++" -o "${1}" == "+" ]
then
shift 1
do_gplus "${@}"
else
_err "unsupport command"
fi

apt源

king@king:~/tmp/dockerfile$ cat sources.list 

deb http://mirrors.aliyun.com/ubuntu/ xenial main restricted
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted
deb http://mirrors.aliyun.com/ubuntu/ xenial universe
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates universe
deb http://mirrors.aliyun.com/ubuntu/ xenial multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted
deb http://mirrors.aliyun.com/ubuntu/ xenial-security universe
deb http://mirrors.aliyun.com/ubuntu/ xenial-security multiverse

编译和测试

#/bin/bash
# 编译镜像
docker build -t king:0.1 ~/tmp/dockerfile/
# 检查是否创建成功
docker images | grep king
# 运行
docker run -it --rm -v "$PWD":/app king:0.1 gcc main.c -v

运行

#!/bin/bash
docker run -it --rm -v "$PWD":/app king:0.1 "${@}"

运行,必须在当前目录运行

king@king:~/tmp/dockerfile$ ./b.sh g main.c 
king@king:~/tmp/dockerfile$ ./b.sh g main.c -g2
king@king:~/tmp/dockerfile$ ./b.sh
root@c780882cbd3f:/app# gdb a.out
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.

SSH远程自动执行交互任务

ssh远程到其他主机是比较安全和方便的办法,例如[ssh king@1.2.3.4],若要自动执行某些命令(并立即返回结果,退出ssh),可以[ssh king@1.2.3.4 ls]

若要执行一些交互式的任务,比方登录一个mysql客户端,并且需要持续的使用(而不是简单地做个sql查询),那么上面的办法就不行了

#!/bin/bash
ssh -t kingqiu@1.2.3.4 "mysql -h192.168.0.1 -uuser -ppassword --default-character-set=utf8mb4 test"

参考

  1. https://superuser.com/questions/646196/ssh-and-execute-interactive-command

Flask 处理websocket

最近要用flask弄一个websocket服务,由于ws的特殊性,若用正常的flask写法,就会阻塞当前线程,生产环境一般用Gunicorn,没有测试过有无作异步处理,但是若用flask内置的容器,就会出问题。

之前学习Golang的时候,了解到gorilla/websocket这个工程。在Go里面写ws逻辑很简单,直接在回调里面一个for循环就好,因为go运行时在底层会自动将goruntime的请求异步化,也就是用同步的代码写异步请求,而不是nodejs的那种回调满天飞的代码,很是赏心悦目。

Flask运行需要一个WSGI 容器来提供服务,flask就内置了wsgi容器,不过性能一般,供测试足够了。

一些生产环境下的容器如

  1. Gunicorn
  2. Tornado
  3. Gevent

不同的容器使用的服务器模型不一样,比如

  1. 多进程模型,来一请求就开一进程
  2. 多线程模型,来一请求就开一线程
  3. 进程池或者线程池,若池为空就排队,进程调度通常又通过外部的调度程序,例如nginx,或者直接用操作系统来调度(父进程bind,然后fork),多线程调度通常用一个队列来分配任务,或者利用操作系统来调度
  4. 两者混合使用,有多个进程的池,每个进程有一个线程池

在大部分情况下,如果一个任务(请求)阻塞特别长时间,可能导致整个线程不可用,进而导致拒绝服务。在大部分阻塞特别长的情况下,后台只是简单地等待IO,这种情况下可以用全异步实现最大的并发。

写个例子

import time
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
time.sleep(1000)
return 'Hello, World!'


@app.route('/a')
def hello_world1():
return 'a'

if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)

直接用python运行【python main.py】,访问http://127.0.0.1:5000/a成功,访问http://127.0.0.1:5000/在那等待,再访问http://127.0.0.1:5000/a也一并卡住了,这里可能只有一个线程在提供服务,而且sleep把这个线程阻塞掉了

换个容器

import time
from gevent import monkey
monkey.patch_all()
from flask import Flask
from gevent import wsgi
app = Flask(__name__)

@app.route('/')
def hello_world():
time.sleep(1000)
return 'Hello, World!'


@app.route('/a')
def hello_world1():
return 'a'

if __name__ == '__main__':
server = wsgi.WSGIServer(('0.0.0.0', 5000), app)
server.serve_forever()

写个脚本,运行100个wget去请求http://127.0.0.1:5000/,然后再访问http://127.0.0.1:5000/a,结果成功

#!/bin/bash
for ((i=0;i<100;i++));do
(
wget http://127.0.0.1:5000
)&
done

Sentry

安装Sentry

推荐用docker来安装,参考https://hub.docker.com/_/sentry/

docker pull sentry
docker pull redis
docker pull postgres

运行redis

docker run -d --name sentry-redis redis

运行progress

docker run -d --name sentry-postgres -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=sentry postgres

使用sentry命令行生成一个密钥,用于在sentry容器之间授权

king@king:~$ docker run --rm sentry config generate-secret-key
fc=k^xur0()hb7cqi81z@&b!d9cy6l_-j#&36)kk28u^%ldx*l

如果数据库是第一次使用(新的),用下面的命令创建表结构(初始化)

docker run -it --rm -e SENTRY_SECRET_KEY='<secret-key>' \
--link sentry-postgres:postgres --link sentry-redis:redis sentry upgrade

在这个过程中会提示创建用户

Would you like to create a user account now? [Y/n]: Y
Email: email@gmail.com
Password:
Repeat for confirmation:
Should this user be a superuser? [y/N]: Y
User created: email@gmail.com

运行服务器(Web)

注意-p 8080:9000

docker run -d --name my-sentry -e SENTRY_SECRET_KEY='<secret-key>' \
-p 8080:9000 \
--link sentry-redis:redis --link sentry-postgres:postgres sentry

sentry还需要 celery beat 和 celery workers干活,所以再启动两个容器

docker run -d --name sentry-cron -e SENTRY_SECRET_KEY='<secret-key>' \
--link sentry-postgres:postgres --link sentry-redis:redis sentry run cron
docker run -d --name sentry-worker-1 -e SENTRY_SECRET_KEY='<secret-key>' \
--link sentry-postgres:postgres --link sentry-redis:redis sentry run worker

Go Client

sentry client for go https://docs.sentry.io/clients/go/,只需要简单的封裝就可以适配gin

package main

import (
"github.com/getsentry/raven-go"
"net/http"
"fmt"
"github.com/gin-gonic/gin"
"runtime/debug"
"errors"
)

func Middleware(dsn string) gin.HandlerFunc {
if dsn == "" {
panic("Error: No DSN detected!\n")
}
raven.SetDSN(dsn)
var client = raven.DefaultClient

return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {

flags := map[string]string{
"endpoint": c.Request.RequestURI,
}

debug.PrintStack()
rvalStr := fmt.Sprint(err)
packet := raven.NewPacket(rvalStr, raven.NewException(
errors.New(rvalStr),
raven.NewStacktrace(2, 3, nil)),
)
client.Capture(packet, flags)
c.Writer.WriteHeader(http.StatusInternalServerError)

//const size = 1 << 12
//buf := make([]byte, size)
//n := runtime.Stack(buf, false)
//client.CaptureMessage(fmt.Sprintf("%v\nStacktrace:\n%s", err, buf[:n]),nil)
//c.Writer.WriteHeader(http.StatusInternalServerError)
}
}()
c.Next()
}
}

参考

  1. https://github.com/dz0ny/martini-sentry
  2. https://github.com/gin-gonic/gin-sentry
package main

import (
"github.com/gin-gonic/gin"
"github.com/getsentry/raven-go"
"errors"
"fmt"
)

func main() {
r := gin.Default()

r.Use(Middleware("http://5138362834d22ecbd@localhost:8080/2"))
// r.Use(gin.Recovery())

r.GET("/ping", func(c *gin.Context) {
raven.CaptureErrorAndWait(errors.New("this is a string error"), nil)
panic(fmt.Sprintf("this is a panic"))
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run(":12345")
}

Python client

# -*- coding: utf-8 -*-
from raven.contrib.flask import Sentry
from flask import Flask, session, redirect, url_for, escape, request
from flask_script import Manager

sentry = Sentry()

def create_app():
app = Flask(__name__)
sentry.init_app(app, dsn='http://5138362834d22ecbd@localhost:8080/2')
return app

app = create_app()
sentry.captureMessage('hello, world!.shit');

Pip修改默认源

Pip默认的源速度很慢,而且时而访问不了,可以修改到国内的源

pip install -i http://pypi.douban.com/simple/ \
--trusted-host pypi.douban.com itsdangerous

其他比如http://mirrors.aliyun.com/pypi/simple/

设置全局默认

king@KingUbuntu64:~$ mkdir -p ~/.config/pip/
king@KingUbuntu64:~$ vi ~/.config/pip/pip.conf

如果是Windows,路径是【%APPDATA%\pip\pip.ini】

[global]
timeout = 60
index-url = http://pypi.douban.com/simple
[install]
trusted-host=pypi.douban.com

不用docker

先准备redis,mysql或者postgres

安装sentry

apt-get 安装的依赖可能有所差异

apt-get install libpq-dev libffi-dev python-mysqldb libmysqlclient-dev \
libssl-dev libjpeg9 libjpeg9-dev libpng16-16 libpng3 libpng16-dev
pip install sentry pymysql MySQL-python

生成初始化配置

(venv) king@kingqiu:~/proj$ sentry init .
(venv) king@kingqiu:~/proj$ ls
config.yml sentry.conf.py

修改配置

数据库

默认是postgres

DATABASES = {
'default': {
'ENGINE': 'sentry.db.postgres',
'NAME': 'sentry',
'USER': 'postgres',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}

改成mysql,其中的【NAME】表示数据库名,需要事先新建该数据库

DATABASES = {
'default': {
# You can swap out the engine for MySQL easily by changing this value
# to ``django.db.backends.mysql`` or to PostgreSQL with
# ``django.db.backends.postgresql_psycopg2``
'ENGINE': 'django.db.backends.mysql',
'NAME': 'sentry',
'USER': 'username',
'PASSWORD': 'password',
'PORT': '3306',
'HOST': 'localhost'
}
}

Redis

# See https://docs.sentry.io/on-premise/server/queue/ for more
# information on configuring your queue broker and workers. Sentry relies
# on a Python framework called Celery to manage queues.

BROKER_URL = 'redis://localhost:6379'

IP/端口

SENTRY_WEB_HOST = '0.0.0.0'
SENTRY_WEB_PORT = 9000
SENTRY_WEB_OPTIONS = {
# 'workers': 3, # the number of web workers
# 'protocol': 'uwsgi', # Enable uwsgi protocol instead of http
}

运行

初始化

创建表神马的,config后面的.表示路径,自行修改。

在这个过程中,可以一次性创建超级用户

sentry --config=. upgrade

创建新用户

sentry --config=. createsuperuser
sentry --config=. createuser

启动

启动Web服务

sentry --config=. start

启动Wokers

sentry --config=. celery worker -B

使用OpenID登陆

在upgrade时,注册一个owner权限的用户,并且用此账户登陆

首次登陆会弹出Welcome to Sentry,其中Root URL会用作oauth回调路径,所以慎重填写

使用通用的OpenID登陆依赖一个sentry-auth-openid插件。

直接安装的sentry,按照指引即可,若使用docker,可以先调试确认

sudo docker run --rm -it --name my-sentry -e SENTRY_SECRET_KEY='$KEY' \
-p 9000:9000 \
--link sentry-redis:redis --link sentry-postgres:postgres sentry /bin/bash
# 安装插件
pip install https://github.com/TableflippersAnonymous/sentry-auth-openid/archive/master.zip
# 安装vi以便编辑配置
apt update && apt install vim -y

vi /etc/sentry/sentry.conf.py 添加以下内容

OPENID_AUTHORIZE_URL = "http://server:5556/dex/auth"
OPENID_TOKEN_URL = "http://server:5556/dex/token"
OPENID_CLIENT_ID = "example-app"
OPENID_CLIENT_SECRET = "ZXhhbXBsZS1hcHAtc2VjcmV0"

openid服务器,可以用https://github.com/coreos/dex 测试用着

启用

登陆之后,左边菜单MANAGE -> Auth,完整的URL http://localhost:9000/organizations/sentry/auth/

然后点击OPENID 按钮会跳转到oidc服务器进行验证,验证OK保存,下次就自动开启了openid登陆