分类 Go 下的文章

id和code的转换(Base62)

id.png

版本加密解密的实现

package lib

import (
    "fmt"
    "strconv"
)

func Id2Code(id int, version byte) string {
    var code string = ""
    if version == '1' {
        number := id
        for {
            remain := number % 10000000
            str := Base62Encode(remain)
            code = str + code
            number = number / 10000000
            if number == 0 {
                break
            }
        }
        code = string(version) + code
    }
    return code
}

func Code2Id(code string) int {
    version := code[0]
    code = code[1:]
    if version == '1' {
        var buffer string = ""
        for i := len(code); i > 0; i -= 4 {
            start := i - 4
            if start < 0 {
                start = 0
            }
            seg := code[start:i]
            segId := Base62Decode(seg)
            // 大于7位非法
            if segId >= 100000000 {
                return 0
            }
            buffer += fmt.Sprintf("%07d", Base62Decode(seg))
        }
        result, _ := strconv.Atoi(buffer)
        return result
    }
    return 0
}

Base62加密解密的实现

package lib

import (
    "math"
    "bytes"
)

const dictLength = 62

var dict []byte = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}

func Base62Encode(id int) string {
    result := make([]byte, 0)
    number := id
    for number > 0 {
        round := number / dictLength
        remain := number % dictLength
        result = append([]byte{dict[remain]}, result...)
        number = round
    }
    return string(result)
}

func Base62Decode(code string) int {
    var result int = 0
    codeLength := len(code)
    for i, c := range []byte(code) {
        result += bytes.IndexByte(dict, c) * int(math.Pow(dictLength, float64(codeLength - 1 - i)))
    }
    return result
}

建一个socks5代理集群

背景

由于在线工具中部分工具有翻墙的需求,而又没有找到一个稳定的翻墙方案(支持集群),在此背景下产生了这个项目。

架构图

arch.png

功能点

  1. 多账户支持, socks5密码验证
  2. 可用性,集群保证
  3. 流量计算,阈值限制
  4. Custom DNS, 可进行域名白名单的控制
  5. 安全,防止暴力破解socks5密码

表设计

CREATE TABLE `pre_accounts` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `accountname` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `password` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `used` int(10) unsigned NOT NULL DEFAULT '0',
  `max` int(10) unsigned NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_accountname` (`accountname`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Socks5的基础知识

这里就不重复写了,链接到 一个简单的Golang实现的Socks5 Proxy

功能点的实现

多账户支持, socks5密码验证

实现 github.com/armon/go-socks5/credentials.go CredentialStore 接口,进行校验;注意:每次proxy都会进行验证,如果请求量比较大,自行选择缓存方式

可用性,集群保证

每台socks5 server心跳上报到etcd,通过阈值,判断socks5的存活

流量计算,阈值限制

github.com/armon/go-socks5/request.go:358 修改这个函数的调用函数

安全,防止暴力破解socks5密码

使用 ratelimit 来保证,如果错误次数过多,则直接将IP加入黑名单;原理可以看之前写过的一篇文章 使用 redis 做限流

记一次内存泄露的debug过程

在压测 代码在线运行 工具的时候,发现当并发比较高的时候程序占用的内存会飙升,而且在中断压测之后,内存占用并没有回落。

第一个能想到的办法就是去看代码,但是大多数时候,自己写的代码,很难review出太多的问题;于是就借助golang的pprof来定位问题。

在程序中嵌入 pprof

package main

import (
    "tool.lu/sandbox-server/app"
    "net/http"
    _ "net/http/pprof"
    "strconv"
    "runtime"
)

func main() {
    debug()
    server := app.NewApp()
    server.Run(":9090")
}

func debug() {
    go func() {
        // 这边是由于通过pprof发现问题之后,加的一段debug代码;后面会讲到
        http.HandleFunc("/go", func(w http.ResponseWriter, r *http.Request) {
            num := strconv.FormatInt(int64(runtime.NumGoroutine()), 10)
            w.Write([]byte(num))
        })
        http.ListenAndServe("localhost:6060", nil)
    }()
}

通过 go tool 工具,查看内存分配最多的 top 5

go tool pprof http://localhost:6060/debug/pprof/heap
top 5

Screenshot 2016-02-07 at 17.55.39.png

查看代码,发现是 goroutine, ioPipe 的问题,一定是使用姿势出了问题:

Screenshot 2016-02-07 at 17.56.40.png

于是便有了上面的那段代码,curl http://localhost:6060/go,查看当前 go routine 的数量;于是猜测是因为 ioPipe 没有正确的关闭,引起 go routine 大量的产生,但是没有退出,耗费大量的内存;于是在异常退出前,主动关闭 ioPipe 的Reader,至此问题解决。

压测验证

本机

wrk -t5 -c20 -d10000s -s post.lua http://tool.lu

服务器

Screenshot 2016-02-07 at 16.24.24.png

curl http://localhost:6060/go

总结

这是一个很小的bug,由于写代码的时候不仔细,return之前没有关闭资源造成,但却要花费不少的力气去解决;对语言自己提供的工具链需要熟悉在熟悉,这样不管在解决问题或者避免问题的时候,都能节省很多的时间。

[开源] kaka 咔咔

项目地址:https://github.com/xiaozi/kaka
下载地址:https://github.com/xiaozi/kaka/releases

依赖

  1. nsq
  2. casperjs
  3. phantomjs

安装

  1. 将 .env.example 拷贝为 .env
  2. 修改 .env, 填写信息
  3. 运行
./kaka

用法

只需要将消息塞到 nsq 的 topic 中就可以了,topic 是你在 .env 文件里面设置的

消息使用 json 格式,结构如下:

{
    "url": "http://tool.lu/",
    "target": "/data/screenshots/WrTSV5zbkHPCqU6t.png",
    "path": "screenshots/WrTSV5zbkHPCqU6t.png",
    "device": "mac"
}

url: (必须) 需要截图的url

target: (必须) 截图在服务器上的保存的绝对路径

path: (可选) 上传到七牛云的路径,不填则不上传

device: (可选) 目前只支持 “mac" 这个值

FAQ

  1. 使用casperjs截出优雅的图片
  2. 多种网络环境的处理

在每台机器上都配置一个kaka,然后让他们从不同channel的订阅

  1. 不想上传到七牛云

消息中的path留空就好了

开发依赖

go get -u github.com/joho/godotenv
go get -u github.com/qiniu/api.v7
go get -u github.com/bitly/go-nsq

golang中map的排序

在实现 golang 中发布订阅模式的时候,需要按照优先级排序回调函数;golang 中的 map 是无序的,需要手动取出 key,并对key进行排序,下面是排序一块的代码段:

[
    1 => [func1, func2]
    0 => [func5, func6]
    2 => [func3, func4]
]

...

[func5, func6, func1, func2, func3, func4]
func (ed *eventdispatcher) SortListeners(event string) {
    ed.sorted[event] = nil
    // 发布订阅模式中函数执行的优先级
    priorities := make([]int, 0)
    for priority, _ := range ed.listeners[event] {
        priorities = append(priorities, priority)
    }
    // 对优先级的数字进行排序
    sort.Ints(priorities)
    sorted := make([]func(interface {}) interface {}, 0)
    // 按照优先级顺序合并 map
    for _, priority := range priorities {
        sorted = append(sorted, ed.listeners[event][priority]...)
    }
    ed.sorted[event] = sorted
}

PopClip插件开发

配置文件

Actions 里面一个 dict 是一个图标,由于 PopClip 不支持直接执行可执行文件,所以要使用 shell 来执行一下。

自己给定的两个图标的颜色是没有关系的,PopClip 会自动修改图标的颜色。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Actions</key>
  <array>
    <dict>
      <key>After</key>
      <string>copy-result</string>
      <key>Image File</key>
      <string>id.png</string>
      <key>Regular Expression</key>
      <string>(?s)1\w+$</string>
      <key>Title</key>
      <string>Url2Id</string>
      <key>Shell Script File</key>
      <string>url2id.sh</string>
    </dict>
    <dict>
      <key>After</key>
      <string>copy-result</string>
      <key>Image File</key>
      <string>url.png</string>
      <key>Regular Expression</key>
      <string>(?s)\d+$</string>
      <key>Title</key>
      <string>Id2Url</string>
      <key>Shell Script File</key>
      <string>id2url.sh</string>
    </dict>
  </array>
  <key>Apps</key>
  <array>
    <dict>
      <key>Link</key>
      <string>http://tool.lu/</string>
      <key>Name</key>
      <string>在线工具</string>
    </dict>
  </array>
  <key>Credits</key>
  <array>
    <dict>
      <key>Link</key>
      <string>mailto:245565986@qq.com</string>
      <key>Name</key>
      <string>xiaozi</string>
    </dict>
  </array>
  <key>Extension Description</key>
  <string>Convert ids for mogujie.</string>
  <key>Extension Identifier</key>
  <string>lu.tool.popclip.extension.id-converter</string>
  <key>Extension Image File</key>
  <string>id.png</string>
  <key>Extension Name</key>
  <string>Id Converter</string>
  <key>Version</key>
  <integer>1</integer>
</dict>
</plist>

代码

PopClip 操作的文本是直接放在环境变量 POPCLIP_TEXT 里面的,所以下面的代码可以当做是 go 的一个插件模板

package main

import (
    "fmt"
    "os"
)

func main() {
    text := os.Getenv("POPCLIP_TEXT")
    fmt.Print(text)
}
go build -o IdConverter .

发布

mv IdConverter/ IdConverter.popclipext
zip -r IdConverter.popclipext.zip IdConverter.popclipext
mv IdConverter.popclipext.zip IdConverter.popclipextz

实时的 CPU 占用显示

预览

QQ20141108-1.png

流程

  1. golang 分析 vmstat 1 -n
  2. publish 到 redis
  3. subscribe redis 然后通过 SSE push 到 浏览器

代码

performance.go

go get gopkg.in/redis.v2
package main

import (
    "bufio"
    "fmt"
    "log"
    "io"
    "os/exec"
    "strings"
    "strconv"
    "gopkg.in/redis.v2"
)

var client *redis.Client

func tail(stream io.Reader) {
    scanner := bufio.NewScanner(stream)
    scanner.Scan() // 跳过header1
    scanner.Scan() // 跳过header2
    for scanner.Scan() {
        text := scanner.Text()
        segments := strings.Fields(text)
        if ide, err := strconv.ParseInt(segments[14], 10, 32); err == nil {
            used := 100 - ide
            log.Println(used)
            pub := client.Publish("performance", fmt.Sprintf("%d", used))
            if err := pub.Err(); err != nil {
                log.Println(err)
            }
        }
    }
    if err := scanner.Err(); err != nil {
        // ...
    }
}

func main () {
    client = redis.NewClient(&redis.Options{
        Network: "tcp",
        Addr: "127.0.0.1:6379",
    })
    outReader, outWriter := io.Pipe()
    cmd := exec.Command("vmstat", "1", "-n")
    cmd.Stdout = outWriter
    go tail(outReader)
    cmd.Run()
}

SSE 部分请自行实现 (提示:github上搜一下),使用nginx进行反向代理。

html部分

SmoothieChart
js/widget/performance/main.js

(function ($, SmoothieChart) {
    var chart = new SmoothieChart({
        minValue:0,
        maxValue:100,
        grid:{
            fillStyle:'#FFFFFF',
            strokeStyle:'#CCCCCC',
            sharpLines:true
        },
        labels: {
            fillStyle:'#333333',
        }
    }),
    canvas = document.getElementById('performance-widget'),
    series = new TimeSeries();

    chart.addTimeSeries(series, {lineWidth:2,strokeStyle:'#009A61',fillStyle:'rgba(0,154,97,.1)'});
    chart.streamTo(canvas, 500);

    if (!!window.EventSource) {
        var source = new EventSource('//your/sse/sever/and/path');
        source.addEventListener('performance', function (e) {
            series.append(+new Date(), +e.data);
        }, false);
    }

})(jQuery, SmoothieChart);

PS: Chrome的开发工具暂时无法看到浏览器返回的值,调试的话可以访问:chrome://view-http-cache/http(s)://your/sse/sever/and/path

访问可视化

网站的统计

由于 tool.lu 的流量还不是很大,所以我把每次的访问记录都存到了MySQL(如果流量大,这么做是作死的节奏)

主要流程图

design1.jpg

design2.jpg

使用canal做异步处理,主要是因为

  1. ip => city的映射,可能要调用第三方接口比较耗时
  2. 网站代码处不需要写2份数据
  3. 装x

其中cannal分发的数据处理是用的java,本打算sse也用java的netty来实现了,惭愧,尝试未果后就放弃了,最后用golang实现的。

这样做不会太耗性能,而且每秒钟往客户端传输一次数据,但是由于 vps 的内存有限,java 又比较吃内存,所以上线之后就直接下线了。