go汇编入门

lrita 发表了文章 • 0 个评论 • 112 次浏览 • 1 天前 • 来自相关话题

免费可以测试的vps服务器资源?

xiaodu2017 回复了问题 • 7 人关注 • 12 个回复 • 1217 次浏览 • 2 天前 • 来自相关话题

GRABC beego框架的RABC插件

codyi 发表了文章 • 13 个评论 • 206 次浏览 • 2 天前 • 来自相关话题

最近写的一个权限管理插件,欢迎大家来吐槽~~

GRABC

GRABC 是一个beego权限管理插件,插件分为路由、权限、角色。将路由分配给权限,权限授给角色,角色授给用户~~

安装

查看全部
					

最近写的一个权限管理插件,欢迎大家来吐槽~~


GRABC


GRABC 是一个beego权限管理插件,插件分为路由、权限、角色。将路由分配给权限,权限授给角色,角色授给用户~~


安装


go get github.com/codyi/grabc

配置


第一步:在你项目中的数据库中导入rabc.sql,生成对应数据表


第二步:在项目中引入grabc库(可以在项目中的main.go或router.go中引入)


//引入grabc库
import "github.com/codyi/grabc"

引入之后,在引入的router.go或main.go中添加如下配置


func init() {
//将路由注册到grabc,用于反射出对应的网址
grabc.RegisterController(& controllers.SiteController{})
grabc.RegisterController(&controllers.UserController{})
//注册用户系统模型到grabc,用于用户ID和grabc插件绑定
//注意:注册的这个用户模型,需要实现IUserModel中的方法
grabc.RegisterUserModel(&models.User{})
//增加忽律权限检查的页面
grabc.AppendIgnoreRoute("site", "login")
//403页面地址注册到grabc中,用于grabc插件禁止权限的页面跳转
grabc.Http_403("/site/nopermission")
//设置模板,为了让grabc更具有通用性,可以设置模板
//目前设置模板只支持传入模板的内容
grabc.SetLayout(libs.Grabc_layout, nil)
}

添加好上面的配置之后,剩下就是在controller中增加权限判了,个人建议做一个BaseController,然后每个controller都继承这个base,然后在BaseController中的Prepare方法中增加grabc的权限检查~~


//注册当前登录的用户,注意:user需要继承IUserIdentify接口


grabc.RegisterIdentify(user)

if !grabc.CheckAccess(this.controllerName, this.actionName) {
this.redirect(this.URLFor("SiteController.NoPermission"))
}

到此grabc的功能都加完了,是不是很简单~~~


注意:增加完权限判断之后,会发现很多页面都不能访问了,那么就在忽律权限中增加如下配置


grabc.AppendIgnoreRoute("*", "*")

以上配置将会忽律所有的权限检查,这时候需要去/route/index中增加路由,然后添加权限,角色和用户分配,都配置好之后,就可以将grabc.AppendIgnoreRoute("*", "*")代码删掉,然后重启项目~~权限起作用了


接口说明


IUserModel接口

//用于定义用户model
type IUserModel interface {
//用户列表返回可用用户的id和姓名
//参数:pageIndex 分页的页数
//参数:pageCount 每页显示的用户数量
//返回值:userList [用户ID]用户姓名,用户列表展示
//返回值:totalNum 全部的用户数目,用于计算分页的数量
//返回值:err
UserList(pageIndex, pageCount int) (userList map[int]string, totalNum int, err error)
//根据用户ID获取用户姓名
FindNameById(id int) string
}

IUserIdentify接口
type IUserIdentify interface {
GetId() int //返回当前登录用户的ID
}

注意


grabc对注册的控制器会进行反射,然后获取每个controller的名称和controller内的公共方法,由于每个controller都继承了beego.Controller,在获取controller下的方法名称时,会将beego.Controller继承的方法也会获取到,所以目前还不能区分出方法名到底是beego和用户自己定义的,所以grabc将beego继承的方法都进行了忽律,如果在route扫描中,没有找到自定义的方法,可以在controller中增加如下方法,进行方法返回~~


func (this *SiteController) RABCMethods() []string {
return []string{"Get", "Post"}
}

grabc的详细例子:github.com/codyi/grabc_example


Image text
Image text
Image text
Image text

关于too many open files的一点深究

changjixiong 发表了文章 • 2 个评论 • 152 次浏览 • 3 天前 • 来自相关话题

今天在gocn上看到一个问题https://gocn.io/question/1391,题主问为啥运行到打开1000多个文件的时候就会报错,提示too many op... 查看全部

今天在gocn上看到一个问题https://gocn.io/question/1391,题主问为啥运行到打开1000多个文件的时候就会报错,提示too many open files。关于这段代码本身的错误,我已经在回帖中回复了,这里不再赘述。这里我主要想对相关问题稍微做一点深入的讨论,毕竟关于文件和文件描述符相关的问题,几乎是每个程序员都会碰到的问题,然而却很少人真的会去搞清楚问题的背后究竟是什么原理。


假设有这样一段代码


func main() {
input := ""
fmt.Scanln(&input)
for i := 0; ; i++ {
_, err := os.OpenFile("data.txt", os.O_RDWR|os.O_CREATE, os.ModePerm)

if err != nil {
fmt.Println("OpenFile i:", i, err)
return
}
}
}

程序运行起来,在键入回车后会输出:


OpenFile i: 1021 open data.txt: too many open files

如果在网上搜索too many open files,会找到无数的文章,里面会提到如何将系统设定的最大打开文件数从1024修改为一个很大的数字。


不过这里会有一个问题,既然最大限制是1024,为啥这里1021就提示错误了?


在回答这个问题之前,再次运行一下上面那段程序,这次不做任何输入,另外开一个终端找到这个程序的进程ID假设是9527,运行


ls /proc/9527/fd

会看到结果 0 1 2,这3个就是大家不能更熟悉的stdin、stdout、stderr,所以程序在初始化以后就已经打开了3个文件,那么在1024的限制了还能打开1021个,i从0开始计数,于是当i计数到1021的时候,就是在尝试打开第1025个文件,所以失败了。这下是不是将标准输入输出与文件的知识关联起来更好理解了?


至于如何将1024的限制修改为更大,这个网上有太多太多的文章,这里就不在赘述。

当go get遇上gitlab

yhf_szb 发表了文章 • 0 个评论 • 250 次浏览 • 2017-11-30 23:15 • 来自相关话题

前言

go get命令可以说是golang开发者最常用的命令了,通过它我们可以轻松获得各种开源仓库中的包,并且比较方便的在不同的开发机快速部署开发环境。

此处应有版本依赖... 查看全部

前言


go get命令可以说是golang开发者最常用的命令了,通过它我们可以轻松获得各种开源仓库中的包,并且比较方便的在不同的开发机快速部署开发环境。



此处应有版本依赖的问题,但听说新版的go会处理。



但作为企业行为,不是所有的代码包都适合放在公开的网站上,而开源的又适用于中小型企业的自建git仓库工具中,gitlab无疑是耀眼的一个,如果配合docker,一键部署简直不要太舒服。


自建仓库的go get


其实golang在设计的时候是可以支持go get获取自建仓库的,详细原理网上很多,不罗嗦,简单讲,当执行go get your-web.com/your-project的时候,go其实会提交一个HTTP GET 到网址https://you-web.com/your-project?go-get=1,此时如果这个网址能在headmeta标签中返回以下格式的内容时,就可以告诉go客户端应该再到哪里获取仓库。



注意,默认情况下go会且仅会从https的网址获取数据!



<html>
<head>
<meta content="szyhf/go-dicache git https://github.com/szyhf/go-dicache" name="go-import">
</head>
</html>

其中meta标签中的name是必填项,内容必须是go-import,而contat的格式为导入路径 VCS类型 仓库路径,例如,上述代码的含义就是从https://github.com/szyhf/go-dicache下载git仓库并放到导入路径为szyhf/go-dicache的$GOPATH中。



至于域名怎么能访问,怎么输出这个meta,相信对于各位童鞋来说肯定是不是什么problem,跳过



更多说明可以看这里go get命令


遇上gitlab


那么对于自建仓库的gitlab,应该怎么实现这个功能呢?其实gitlab很早就支持了go get,例如你的gitlab网站部署在gitlab.hello.com,你要get的项目是gitlab.hello.com/foo/bar,那么直接执行go get gitlab.hello.com/foo/bar就可以了,gitlab会自动在返回的网页中设置合适的meta标签的。


但实际使用的时候,我们知道,很多时候我们之所以用自建的gitlab,是因为这个仓库见不得光,说白了,我们自己git clone的时候还需要输入一下密码,go get显然也绕不过这个问题。


而默认情况下,gitlab返回的meta标签中的url是https类型的,而实际上更多时候,我们都是通过ssh的方式实现获取仓库,因此,我们需要对gitlab做一定的改造。


当前笔者使用的gitlab版本是9.3.6,对go get的支持是用过ruby-rails中的Middleware的方式实现的,很传统,如果懂ruby的话可以试试直接改,文件是gitlab/embedded/service/gitlab-rails/lib/gitlab/middleware/go.rb,此处不多说。



主要考虑要改源代码不是很优雅,特别是要处理gitlab的升级的时候。



此处给一个不需要懂ruby的非侵入式方案,因为公司的gitlab是搭配nginx使用的,所以在处理对gitlab的请求的时候,加入以下配置,可以达到一样的效果:


if ($http_user_agent ~* "go") {
return 200 "<!DOCTYPE html><head><meta content='$host$uri git ssh://git@$host:$uri.git' name='go-import'></head></html>";
}

简单解释一下,来自go get的HTTP请求中,User Agent都是以go作为开头的,而且go也不会跟现在任何主流浏览器冲突,所以当发现$http_user_agentgo开头的时候,直接返回一个固定的字符串,字符串中注意仓库路径的拼接要加上ssh://,要不go1.8以下的版本无法识别。



上述是我第一次的方案,go get gitlab.hello.com/foo/bar成功,顺利按照预期工作。



然后工作了一阵子之后忽然又出现了新的问题,subpackage


原因很简单,当我们在go get某个项目时,如果这个项目依赖于gitlab.hello.com/foo/bar/you/hu包,那么go get实际提交的请求会变成https://gitlab.hello.com/foo/bar/you/hu,而实际上并不存在这个仓库,如果按方案1的实现逻辑,会尝试下载git@gitlab.hello.com/foo/bar/you/hu.git


很遗憾,这个仓库并不存在,真正存在的是gitlab.hello.com/foo/bar.git,那么应该怎么处理呢?结合nginx的正则表达式重定位的功能,更新的配置如下:


location ~* ^/[^/]+/[^/]+$ {
if ($http_user_agent ~* '^go.*') {
return 200 "<!DOCTYPE html><head><meta content='$host$uri git ssh://git@$host:$uri.git' name='go-import'></head></html>";
}
proxy_cache off;
proxy_pass http://gitlab-workhorse;
}
location ~* ^/(?<holder>[^/]+)/(?<project>[^/]+)/.*$ {
set $goRedirect 'https://$host/$holder/$project?$args';
if ($http_user_agent ~* '^go.*') {
return 301 $goRedirect;
}
proxy_cache off;
proxy_pass http://gitlab-workhorse;
}

其中proxy_cache off;proxy_pass http://gitlab-workhorse;是gitlab官方文档中给出的设置。



其实很容易理解。



主要来解释一下两个location,首先:


~*表示开始不区分大小写地匹配后边给出的正则。


正则^/[^/]+/[^/]+$是为了匹配形如/foo/bar的路径结构,如果匹配成功,继续检查User-Agent,如果符合go,则按第一个方案返回结果,如果不符合,则按一般的gitlab请求进行处理。


正则^/(?<holder>[^/]+)/(?<project>[^/]+)/.*$是为了匹配形如/foo/bar/you/hu/hu的结构,其中的小括号表示对其第一二个斜杠之间的字符串进行捕捉,并赋值给变量$holder$project,然后判定User-Agent,如果符合go,则将请求重定位给/foo/bar,也就会再交给第一个正则处理,最后获得一致的结果。


显然第二个方案比第一个方案复杂了不少,但也都是很标准的nginx配置逻辑,未必优雅,但还是很实用的。



一般来说小型企业的代码库并不会有很高的访问频率,哪怕proxy稍微慢一点,影响也不大。



Docker


如果使用docker版并使用了docker-compose,配置文件中的选项可以参考如下:


environment:
GITLAB_OMNIBUS_CONFIG: |
nginx['custom_gitlab_server_config'] = "location ~* ^/[^/]+/[^/]+$$ {\n if ($$http_user_agent ~* '^go.*') {\n return 200 \"<!DOCTYPE html><html><head><meta content='$$host$$uri git ssh://git@$$host:$$uri.git' name='go-import'></head></html>\";\n }\n proxy_cache off;\n proxy_pass http://gitlab-workhorse;\n}\nlocation ~* ^/(?<holder>[^/]+)/(?<project>[^/]+)/.*$$ {\n set $$goRedirect 'https://$$host/$$holder/$$project?$$args';\n if ($$http_user_agent ~* '^go.*') {\n return 301 $$goRedirect;\n }\n proxy_cache off;\n proxy_pass http://gitlab-workhorse;\n}"


使用\$\$可以防止参数被当成环境变量使用。



小结


代理的思维方式可以解决很多问题。


公众号广告=。=


原文链接
公众号二维码

【社区福利】使用优惠价格购买《Go Web 编程》

zhaoyun4122 回复了问题 • 6 人关注 • 6 个回复 • 956 次浏览 • 2017-11-30 16:37 • 来自相关话题

Go 迷思之 Named 和 Unnamed Types

hxzqlh 发表了文章 • 0 个评论 • 200 次浏览 • 2017-11-28 09:54 • 来自相关话题

始发于微信公众号 查看全部

始发于微信公众号 Go 迷思之 Named 和 Unnamed Types


先来热身一下,下面的代码能编译吗?为什么?


package main

type stack []uintptr

func callers() stack {
return make([]uintptr, 20)
}

func main() {
callers()
}

(此处省略一分钟冥思苦想状....)



好啦,不用多想了,当然可以编译。


但是……这个问题重要吗?


是的,很重要。


如果上面这份代码不能编译,那意味着你无法写这样的代码:


type stack []uintptr
var st stack = make([]uintptr, 20)

而我们知道,这样的代码几乎无处不在。


再来,下面的代码能通过编译吗?


type T int

func F(t T) {}

func main() {
var q int
F(q)
}

结合你平时写的代码,再思考一分钟……


Ops, it couldn't。


稍微改动如下,它能通过编译吗?


type T []int

func F(t T) {}

func main() {
var q []int
F(q)
}

Yes, it does.


Surprised?! How could this happen?


Read The Fxxking Manual


言归正传,先来看下这又臭又长的 《Go 规范手册》 是怎么解释 Types 的。



A type determines a set of values together with operations and methods specific to those values. A type may be denoted by a type name, if it has one, or specified using a type literal, which composes a type from existing types.


Named instances of the boolean, numeric, and string types are predeclared. Other named types are introduced with type declarations. Composite types—array, struct, pointer, function, interface, slice, map, and channel types—may be constructed using type literals.


Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.



Named vs Unnamed Type


Named types 有两类:



  • 内置的类型,比如 int, int64, float, string, bool,

  • 用 type 关键字声明的类型,比如 type Foo string


Unamed types:基于已有的 named types 声明出的组合类型,uname types 在 Go 里俯拾皆是。比如 struct{}、[]string、interface{}、map[string]bool、[20]float32……


Named types 可以作为方法的接受者, unnamed type 却不能。比如:


type Map map[string]string

// ok
func (m Map) Set(key string, value string){
m[key] = value
}

// invalid receiver type map[string]string (map[string]string is an unnamed type)
func (m map[string]string) Set(key string, value string){
m[key] = value
}

Underlying Type


每种类型 T 都有一个底层类型:如果 T 是预声明类型或者 类型字面量(笔者注:type literal 翻译成类型字面量,地道不?) ,它的底层类型就是 T 本身,否则,T 的底层类型是其类型声明中引用的类型的底层类型。


type (
B1 string
B2 B1
B3 []B1
B4 B3
)

string, B1 和 B2 的底层类型是 string.



B2 引用了 B1,那么 B2 的底层类型其实是 B1 的底层类型,而 B1 又引用了 string,那么 B1 的底层类型其实是 string 的底层类型,很明显,string 的底层类型就是string,最终 B2 的底层类型是 string。



[]B1, B3, 和 B4 的底层类型是 []B1.



[]B1 是类型字面量,因此它的底层类型就是它本身。



所有基于相同 unnamed types 声明的变量的类型都相同,而对于 named types 变量而言,即使它们的底层类型相同,它们也是不同类型。


// x 是 unnamed types
var x struct{ I int }

// x 和 x2 类型相同
var x2 struct{ I int }

// y 是 named type
type Foo struct{ I int }
var y Foo

// y 和 z 类型不同
type Bar struct{ I int }
var z Bar

Assignability


不同类型的变量之间是不能赋值的。


type MyInt int
var i int = 2
var i2 MyInt = 4
i = i2 // error: cannot use i2 (type MyInt) as type int in assignment

你不能把 i2 赋值给 i,因为它们的类型不同,虽然它们的底层类型都是 int。


对于那些拥有相同底层类型的变量而言,还需要理解另外一个重要概念:可赋值性。在 Assignability 的六大准则中,其中有一条:



x's type V and T have identical underlying types and at least one of V or T is not a defined type.



也就是说底层类型相同的两个变量可以赋值的条件是:至少有一个不是 named type。


x  = y   // ok
y = x // ok
x = x2 // ok
y = z // error: cannot use y (type Foo) as type Bar in assignment

现在,你知道“为什么开头那两份代码为什么一个能编译另一个不能”了吧。


Type Embedding


当你使用 type 声明了一个新类型,它不会继承原有类型的方法集。


package main

type User struct {
Name string
}

func (u *User) SetName(name string) {
u.Name = name
}

type Employee User

func main(){
employee := new(Employee)
employee.SetName("Jack").
// error employee.SetName undefined (type *Employee has no field or method SetName)
}

作为一个小技巧,你可以将原有类型作为一个匿名字段内嵌到 struct 当中来继承它的方法,这样的 struct 在 Go 代码中太常见不过了。


比如:


package main

type User struct {
Name string
}

func (u *User) SetName(name string) {
u.Name = name
}

type Employee struct {
User // annonymous field
Title string
}

func main(){
employee := new(Employee)
employee.SetName("Jack")
}

Last But Not Least


Go 里面关于类型 Types 的一些规定有时候让初学者丈二和尚摸不着头脑,而 Types 几乎是任何一门编程语言的基石,如果你不能理解 Go 里面最基本的概念之一:Types,相信我,你将不可能在这门语言上走远。


6年過去了才反應過來Rob Pike这个梗啊

h12 发表了文章 • 3 个评论 • 331 次浏览 • 2017-11-27 12:58 • 来自相关话题

突然意識到Go“反射定律”這篇blog的題目,是借用光學“反射定律”这个术语,然鵝6年已然逝去~

深邃,推薦再次細讀:

...

golang echo 代码详解之 log 篇

Laily 发表了文章 • 1 个评论 • 209 次浏览 • 2017-11-26 16:38 • 来自相关话题

一、echo 自带的 log 库

1. log 结构

echo 框架的 log 结构体是 echo.Echo 结构体的一个属性

查看全部
					

一、echo 自带的 log 库


1. log 结构


echo 框架的 log 结构体是 echo.Echo 结构体的一个属性


    type Echo struct {
...
Logger Logger
}

而 logger 是个这样的接口


type (
// Logger defines the logging interface.
Logger interface {
Output() io.Writer
SetOutput(w io.Writer)
Prefix() string
SetPrefix(p string)
Level() log.Lvl
SetLevel(v log.Lvl)
Print(i ...interface{})
Printf(format string, args ...interface{})
Printj(j log.JSON)
Debug(i ...interface{})
Debugf(format string, args ...interface{})
Debugj(j log.JSON)
Info(i ...interface{})
Infof(format string, args ...interface{})
Infoj(j log.JSON)
Warn(i ...interface{})
Warnf(format string, args ...interface{})
Warnj(j log.JSON)
Error(i ...interface{})
Errorf(format string, args ...interface{})
Errorj(j log.JSON)
Fatal(i ...interface{})
Fatalj(j log.JSON)
Fatalf(format string, args ...interface{})
Panic(i ...interface{})
Panicj(j log.JSON)
Panicf(format string, args ...interface{})
}
)

一般的 log 也都实现了这些方法,所以我们可以使用自己的 log 包替换这个。而作者是使用的 github.com/labstack/gommon/log 这个包。


到这里,自定义 log 级别,输出位置都一目了然了。


2. 默认的 log


在生成 echo.Echo 实例的时候,会初始化一个默认的 log。


// 初始化一个 Echo 实例
func New() (e *Echo) {
e = &Echo{
...
Logger: log.New("echo"),
}
...
e.Logger.SetLevel(log.ERROR) // 默认日志级别
...
return
}

// log.New() 方法是这样的
func New(prefix string) (l *Logger) {
l = &Logger{
level: INFO,
prefix: prefix,
template: l.newTemplate(defaultHeader),
color: color.New(), // 这个是让不同级别的日志在控制台显示不用颜色的。
bufferPool: sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 256))
},
},
}
l.initLevels() // 同样是处理颜色
l.SetOutput(output()) // 默认是 os.Stdout
return
}

这里的 template 是 github.com/valyala/fasttemplate 包的对象,是一个简单的模版引擎,用来控制 log 的输出样式。


默认的样式是这样的


    defaultHeader = `{"time":"${time_rfc3339_nano}","level":"${level}","prefix":"${prefix}",` +
`"file":"${short_file}","line":"${line}"}`

这里的很多配置都和官方 log 的配置类似,也显示了文件名和行号。这里默认支持的时间格式只有两种


time_rfc3339 // "2006-01-02T15:04:05Z07:00"
time_rfc3339_nano // "2006-01-02T15:04:05.999999999Z07:00"

需要更深层次的定制的话就需要修改或者替换 log 包了。


二、 log 中间件


通过这样的方法来注册 log 中间件,这个中间件主要用来针对 http 请求打印日志。


// 使用默认的配置
e.Use(middleware.Logger())

// 自定义配置
// 自定义配置只支持 Format 和 Output 两个属性
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "method=${method}, uri=${uri}, status=${status}\n",
Output os.Stdout,
}))

默认的配置是这样的


    DefaultLoggerConfig = LoggerConfig{
...
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}",` +
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
`"bytes_out":${bytes_out}}` + "\n",
Output: os.Stdout,
colorer: color.New(),
}

自定义配置支持下面这些字段


- time_unix
- time_unix_nano
- time_rfc3339
- time_rfc3339_nano
// 时间上多了两个 unix 时间戳类型

- id (Request ID)
- remote_ip
- uri
- host
- method
- path
- referer
- user_agent
- status
// 常规的 http 请求内容

- latency (In nanoseconds)
- latency_human (Human readable)
// 这个可以算作处理日志花的时间

- bytes_in (Bytes received)
- bytes_out (Bytes sent)
// request 请求和 response 响应的大小

- header:<NAME>
- query:<NAME>
- form:<NAME>
- cookie:<NAME>
// 这几个可以拿到具体内容,分别用下面的方法取得
// tag 就是上面的字段
// c.Request().Header.Get(tag[7:])
// c.QueryParam(tag[6:])
// c.FormValue(tag[5:])
// c.Cookie(tag[7:]

整个日志中间件在这里 https://github.com/labstack/echo/blob/master/middleware/logger.go ,不符合也可以根据需要重新实现一个。


三、后记


之前翻译了 echo 的中文文档 http://go-echo.org,发现文档有很多地方没有说清楚,就萌生了边看代码边补充使用文档的想法。拖了很久终于开工了,拿最简单的 log 开篇,后面会陆续更新关于 echo 其他模块的介绍。


原文地址:laily.net

OK Log设计思路

cdh0805010118 发表了文章 • 4 个评论 • 325 次浏览 • 2017-11-17 16:17 • 来自相关话题

OK Log 姊妹篇

设计

在这个文档中,我们首先在顶层设计上描述这个系统。然后,我们再引入约束和不变量来确定问题域。我们会一步步地提出一... 查看全部

OK Log 姊妹篇


设计


在这个文档中,我们首先在顶层设计上描述这个系统。然后,我们再引入约束和不变量来确定问题域。我们会一步步地提出一个具体的解决方案,描述框架中的关键组件和组件之间的行为。


生产者与消费者


我们有一个大且动态地生产者集,它们会生产大量的日志记录流。这些记录应该可供消费者查找到的。


     +-----------+
P -> | |
P -> | ? | -> C
P -> | |
+-----------+

生产者主要关心日志被消费的速度尽可能地快。如果这个速度没有控制好,有一些策略可以提供,包括:背压策略(ps: 流速控制), 例如:事件日志、缓冲和数据丢弃(例如:应用程序日志)。在这些情况下,接收日志记录流的组件需要优化顺序写操作。


消费者主要关心尽快地响应用户端的日志查询,保证尽可能快的日志持久化。因为我们定义了查询必须带时间边界条件,我们要确保我们可以通过时间分隔数据文件,来解决grep问题。所以存储在磁盘上的最终数据格式,应该是一个按照时间划分的数据文件格式,且这些文件内的数据是由所有生产者的日志记录流全局归并得到的。如下图所示:


     +-------------------+
P -> | R |
P -> | R ? R R R | -> C
P -> | R |
+-------------------+

设计细节


我们有上千个有序的生产者。(一个生产者是由一个应用进程,和一个forward代理构成)。我们的日志系统有必要比要服务的生产系统小得多。因此我们会有多个ingest节点,每个ingest节点需要处理来自多个生产者的写请求。


我们也想要服务于有大量日志产生的生产系统。因此,我们不会对数据量做还原性假设。我们假设即使是最小工作集的日志数据,对单个节点的存储可能也是太大的。因此,消费者将必须通过查询多个节点获取结果。这意味着最终的时间分区的数据集将是分布式的,并且是复制的。


producers --> forwarders --> ingester ---> **storage** <--- querying  <--- consumer

+---+ +---+
P -> F -> | I | | Q | --.
P -> F -> | | +---+ |
+---+ +---+ '->
+---+ ? | Q | ----> C
P -> F -> | I | +---+ .->
P -> F -> | | +---+ |
P -> F -> | | | Q | --'
+---+ +---+

现在我们引入分布式,这意味着我们必须解决协同问题。


协同


协同是分布式系统的死亡之吻。(协同主要是解决分布式数据的一致性问题)。我们的日志系统是无协同的。让我们看看每个阶段需要什么。


生产者,更准确地说,forwarders,需要能够连接任何一个ingest节点,并且发送日志记录。这些日志记录直接持久化到ingester所在的磁盘上,并尽可能地减少中间处理过程。如果ingester节点挂掉了,它的forwarders应该非常简单地连接其他ingester节点和恢复日志传输。(根据系统配置,在传输期间,它们可以提供背压,缓冲和丢弃日志记录)言外之意,forwarders节点不需要知道哪个ingest是ok的。任何ingester节点也必须是这样。


有一个优化点是,高负载的ingesters节点可以把负载(连接数)转移到其他的ingesters节点。有三种方式:、



  • ingesters节点通过gossip协议传递负载信息给其他的ingesters节点,这些负载信息包括:连接数、IOps(I/O per second)等。

  • 然后高负载ingesters节点可以拒绝新连接请求,这样forwarders会重定向到其他比较轻量级负载的ingesters节点上。

  • 满负载的ingesters节点,如果需要的话,甚至可以中断已经存在的连接。但是这个要十分注意,避免错误的拒绝合理的服务请求。


例如:在一个特定时间内,不应该有许多ingesters节点拒绝连接。也就是说日志系统不能同时有N个节点拒绝forwarders节点日志传输请求。这个可以在系统中进行参数配置。


consumers需要能够在没有任何时间分区和副本分配等条件的情况下进行查询。没有这些已知条件,这意味着用户的一个查询总是要分散到每个query节点上,然后聚合和去重。query节点可能会在任何时刻挂掉,启动或者所在磁盘数据空。因此查询操作必须优雅地管理部分结果。


另一个优化点是,consumers能够执行读修复。一个查询应该返回每一个匹配的N个备份数据记录,这个N是复制因子。任何日志记录少于N个备份都是需要读修复的。一个新的日志记录段会被创建并且会复制到集群中。更进一步地优化,独立的进程能够执行时空范围内的顺序查询,如果发现查询结果存在不一致,可以立即进行读修复。


在ingest层和query层之间的数据传输也需要注意。理想情况下,任何ingest节点应该能够把段传送到任何查询节点上。我们必须优雅地从传输失败中恢复。例如:在事务任何阶段的网络分区。


让我们现在观察怎么样从ingest层把数据安全地传送到query层。


ingest段


ingesters节点从N个forwarders节点接收了N个独立的日志记录流。每个日志记录以带有ULID的字符串开头。每个日志记录有一个合理精度的时间错是非常重要的,它创建了一个全局有序,且唯一的ID。但是时钟全局同步是不重要的,或者说记录是严格线性增长的。如果在一个很小的时间窗口内日志记录同时到达出现了ID乱序,只要这个顺序是稳定的,也没有什么大问题。


到达的日志记录被写到一个活跃段中,在磁盘上这个活跃段是一个文件。


          +---+
P -> F -> | I | -> Active: R R R...
P -> F -> | |
P -> F -> | |
+---+

一旦这个段文件达到了B个字节,或者这个段活跃了S秒,那么这个活跃段就会被flush到磁盘上。(ps: 时间限制或者size大小)


          +---+
P -> F -> | I | -> Active: R R R...
P -> F -> | | Flushed: R R R R R R R R R
P -> F -> | | Flushed: R R R R R R R R
+---+

这个ingester从每个forwarder连接中顺序消费日志记录。当当前的日志记录成功写入到活跃的段中后,下一个日志记录将会被消费。并且这个活跃段在flush后立即同步复制备份。这是默认的持久化模式,暂定为fast。


Producers选择性地连接一个独立的端口上,其处理程序将在写入每个记录后同步活跃的段。者提供了更强的持久化,但是以牺牲吞吐量为代价。这是一个独立的耐用模式,暂时定为持久化。(ps: 这段话翻译有点怪怪的,下面是原文)


Producers can optionally connect to a separate port, whose handler will sync the active segment after each record is written. This provides stronger durability, at the expense of throughput. This is a separate durability mode, tentatively called durable.


第三个更高级的持久化模式,暂定为混合模式。forwarders一次写入整个段文件到ingester节点中。每一个段文件只有在存储节点成功复制后才能被确认。然后这个forwarder节点才可以发送下一个完整的段。


ingesters节点提供了一个api,用于服务已flushed的段文件。



  • Get /next ---- 返回最老的flushed段,并将其标记为挂起

  • POST /commit?id=ID ---- 删除一个挂起的段

  • POST /failed?id=ID ---- 返回一个已flushed的挂起段


ps: 上面的ID是指:ingest节点的ID


段状态由文件的扩展名控制,我们利用文件系统进行原子重命名操作。这些状态包括:.active、.flushed或者.pending, 并且每个连接的forwarder节点每次只有一个活跃段。


          +---+                     
P -> F -> | I | Active +---+
P -> F -> | | Active | Q | --.
| | Flushed +---+ |
+---+ +---+ '->
+---+ ? | Q | ----> C
P -> F -> | I | Active +---+ .->
P -> F -> | | Active +---+ |
P -> F -> | | Active | Q | --'
| | Flushed +---+
| | Flushed
+---+

观察到,ingester节点是有状态的,因此它们需要一个优雅地关闭进程。有三点:



  • 首先,它们应该中断链接和关闭监听者

  • 然后,它们应该等待所有flushed段被消费

  • 最后,它们才可以完成关闭操作


消费段


这个ingesters节点充当一个队列,将记录缓冲到称为段的组中。虽然这些段有缓冲区保护,但是如果发生断电故障,这内存中的段数据没有写入到磁盘文件中。所以我们需要尽快地将段数据传送到query层,存储到磁盘文件中。在这里,我们从Prometheus的手册中看到,我们使用了拉模式。query节点从ingester节点中拉取已经flushed段,而不是ingester节点把flushed段推送到query节点上。这能够使这个设计模型提高其吞吐量。为了接受一个更高的ingest速率,更加更多的ingest节点,用更快的磁盘。如果ingest节点正在备份,增加更多的查询节点一共它们使用。


query节点消费分为三个阶段:



  • 第一个阶段是读阶段。每一个query节点定期地通过GET /next, 从每一个intest节点获取最老的flushed段。(算法可以是随机选取、轮询或者更复杂的算法,目前方案采用的是随机选取)。query节点接收的段逐条读取,然后再归并到一个新的段文件中。这个过程是重复的,query节点从ingest层消费多个活跃段,然后归并它们到一个新的段中。一旦这个新段达到B个字节或者S秒,这个活跃段将被写入到磁盘文件上然后关闭。

  • 第二个阶段是复制阶段。复制意味着写这个新的段到N个独立的query节点上。(N是复制因子)。这是我们仅仅通过POST方法发送这个段到N个随机存储节点的复制端点。一旦我们把新段复制到了N个节点后,这个段就被确认复制完成。

  • 第三个阶段是提交阶段。这个query节点通过POST /commit方法,提交来自所有ingest节点的原始段。如果这个新的段因为任何原因复制失败,这个query节点通过POST /failed方法,把所有的原始段全部改为失败状态。无论哪种情况,这三个阶段都完成了,这个query节点又可以开始循环随机获取ingest节点的活跃段了。


下面是query节点三个阶段的事务图:


Q1        I1  I2  I3
-- -- -- --
|-Next--->| | |
|-Next------->| |
|-Next----------->|
|<-S1-----| | |
|<-S2---------| |
|<-S3-------------|
|
|--.
| | S1∪S2∪S3 = S4 Q2 Q3
|<-' -- --
|-S4------------------>| |
|-S4---------------------->|
|<-OK------------------| |
|<-OK----------------------|
|
| I1 I2 I3
| -- -- --
|-Commit->| | |
|-Commit----->| |
|-Commit--------->|
|<-OK-----| | |
|<-OK---------| |
|<-OK-------------|

让我们现在考虑每一个阶段的失败处理



  • 对于第一个阶段:读阶段失败。挂起的段一直到超时都处于闲置状态。对于另一个query节点,ingest节点的活跃段是可以获取的。如果原来的query节点永远挂掉了,这是没有任何问题的。如果原始的query节点又活过来了,它有可能仍然会消费已经被其他query节点消费和复制的段。在这种情况下,重复的记录将会写入到query层,并且一个或者多个会提交失败。如果这个发生了 ,这也ok:记录超过了复制因子,但是它会在读时刻去重,并且最终会重新合并。因此提交失败应该被注意,但是也能够被安全地忽略。

  • 对于第二个阶段:复制阶段。错误的处理流程也是相似的。假设这个query节点没有活过来,挂起的ingest段将会超时并且被其他query节点重试。如果这个query节点活过来了,复制将会继续进行而不会失败,并且一个或者多个最终提交将将失败

  • 对于第三个阶段:commit阶段。如果ingest节点等待query节点commit发生超时,则处在pending阶段的一个或者多个ingest节点,会再次flushed到段中。和上面一样,记录将会重复,在读取时进行数据去重,然后合并。


节点失败


如果一个ingest节点永久挂掉,在其上的所有段记录都会丢失。为了防止这种事情的发生,客户端应该使用混合模式。在段文件被复制到存储层之前,ingest节点都不会继续写操作。


如果一个存储节点永久挂掉,只要有N-1个其他节点存在都是安全的。但是必须要进行读修复,把该节点丢失的所有段文件全部重新写入到新的存储节点上。一个特别的时空追踪进行会执行这个修复操作。它理论上可以从最开始进行读修复,但是这是不必要的,它只需要修复挂掉的段文件就ok了。


查询索引


所有的查询都是带时间边界的,所有段都是按照时间顺序写入。但是增加一个索引对找个时间范围内的匹配段也是非常必要的。不管查询节点以任何理由写入一个段,它都需要首先读取这个段的第一个ULID和最后一个ULID。然后更新内存索引,使这个段携带时间边界。在这里,一个线段树是一个非常好的数据结构。


另一种方法是,把每一个段文件命名为FROM-TO,FROM表示该段中ULID的最小值,TO表示该段中ULID的最大值。然后给定一个带时间边界的查询,返回所有与时间边界有叠加的段文件列表。给定两个范围(A, B)和(C, D),如果A<=B, C<=D以及A<=C的话。(A, B)是查询的时间边界条件,(C, D)是一个给定的段文件。然后进行范围叠加,如果B>=C的话,结果就是FROM C TO B的段结果


A--B         B >= C?
C--D yes

A--B B >= C?
C--D no

A-----B B >= C?
C-D yes

A-B B >= C?
C----D yes

这就给了我们两种方法带时间边界的查询设计方法


合并


合并有两个目的:



  • 记录去重

  • 段去叠加


在上面三个阶段出现有失败的情况,例如:网络故障(在分布式协同里,叫脑裂),会出现日志记录重复。但是段会定期透明地叠加。


在一个给定的查询节点,考虑到三个段文件的叠加。如下图所示:


t0             t1
+-------+ |
| A | |
+-------+ |
| +---------+ |
| | B | |
| +---------+ |
| +---------+
| | C |
| +---------+

合并分为三步:



  • 首先在内存中把这些重叠的段归并成一个新的聚合段。

  • 在归并期间,通过ULID来进行日志记录去重和丢弃。

  • 最后,合并再把新的聚合段分割成多个size的段,生成新的不重叠的段文件列表


t0             t1
+-------+-------+
| | |
| D | E |
| | |
+-------+-------+

合并减少了查询搜索段的数量。在理想情况下,每次都会且只映射到一个段。这是通过减少读数量来提高查询性能。


观察到合并能改善查询性能,而且也不会影响正确性和空间利用率。在上述合并处理过程中同时使用压缩算法进行合并后的数据压缩。合适的压缩可以使得日志记录段能够在磁盘保留更长的时间(ps: 因为可以使用的空间更多了,磁盘也没那么快达到设置的上限),但是会消耗衡更多的CPU。它也可能会使UNIX/LINUX上的grep服务无法使用,但是这可能是不重要的。


由于日志记录是可以单独寻址的,因此查询过程中的日志记录去重会在每个记录上进行。映射到段的记录可以在每个节点完全独立优化,无需协同。


合并的调度和耦合性也是一个非常重要的性能考虑点。在合并期间,单个合并groutine会按照顺序执行每个合并任务。它每秒最多进行一次合并。更多的性能分析和实际研究是非常必要的。


查询


每个查询节点提供一个GET /query的api服务。用户可以使用任意的query节点提供的查询服务。系统受到用户的查询请求后,会在query层的每一个节点上进行查询。然后每个节点返回响应的数据,在query层进行数据归并和去重,并最终返回给用户。


真正的查询工作是由每个查询节点独立完成的。这里分为三步:



  • 首先匹配查询时间边界条件的段文件被标记。(时间边界条件匹配)

  • 对于第一步获取的所有段,都有一个reader进行段文件查找匹配的日志记录,获取日志记录列表

  • 最后对获取到的日志记录列表通过归并Reader进行归并,排序,并返回给查询节点。


这个pipeline是由很多的io.ReaderClosers构建的,主要开销在读取操作。这个HTTP响应会返回给查询节点,最后返回给用户。


注意一点,这里的每个段reader都是一个goroutine,并且reading/filtering是并发的。当前读取段文件列表还进行goroutine数量的限制。(ps: 有多少个段文件,就会生成相应数量的goroutine)。这个是应该要优化的。


用户查询请求包括四个字段:



  • FROM, TO time.Time - 查询的时间边界

  • Q字符串 - 对于grep来说,空字符串是匹配所有的记录

  • Regex布尔值 - 如果是true,则进行正则表达式匹配

  • StatsOnly布尔值 - 如果是true,只返回统计结果


用户查询结果响应有以下几个字段:



  • NodeCount整型 - 查询节点参与的数量

  • SegmentCount整型 - 参与读的段文件数量

  • Size整型 - 响应结果中段文件的size

  • io.Reader的数据对象 - 归并且排序后的数据流


StatsOnly可以用来探索和迭代查询,直到它被缩小到一个可用的结果集


组件模型


下面是日志管理系统的各个组件设计草案


进程


forward


  • ./my_application | forward ingest.mycorp.local:7651

  • 应该接受多个ingest节点host:ports的段拉取

  • 应该包含DNS解析到单个实例的特性

  • 应该包含在连接断掉后进行容错的特性

  • 能够有选择fast, durable和chunked写的特性

  • Post-MVP: 更复杂的HTTP? forward/ingest协议;


ingest


  • 可以接收来自多个forwarders节点的写请求

  • 每条日志记录以\n符号分割

  • 每条日志记录的前缀必须是ULID开头

  • 把日志记录追加到活跃段中

  • 当活跃段达到时间限制或者size时,需要flush到磁盘上

  • 为存储层的所有节点提供轮询的段api服务

  • ingest节点之间通过Gossip协议共享负载统计数据

  • Post-MVP: 负载扩展/脱落;分段到存储层的流传输


store


  • 轮询ingest层的所有flush段

  • 把ingest段归并到一起

  • 复制归并后的段到其他存储节点上

  • 为客户端提供查询API服务

  • 在某个时刻执行合并操作

  • Post-MVP:来自ingest层的流式段合并;提供更高级的查询条件


Libraries


Ingest日志


  • 在ingest层的段Abstraction

  • 主要操作包括:创建活跃段,flush、pending标记,和提交

  • (I've got a reasonable prototype for this one) (ps: 不明白)

  • 请注意,这实际上是一个磁盘备份队列,有时间期限的持久化存储


Store日志


  • 在storage层的段Abstraction

  • 操作包括段收集、归并、复制和合并

  • 注意这个是长期持久化存储


集群



  • 来之各个节点之间的信息的Abstraction

  • 大量的数据共享通信是不必要的,只需要获取节点身份和健康检查信息就足够了

  • HashiCorp's memberlist fits the bill (ps:不明白)

结合 Go 读 APUE-基本文件I/O

suc 回复了问题 • 4 人关注 • 3 个回复 • 530 次浏览 • 2017-11-17 15:29 • 来自相关话题

OK Log

cdh0805010118 发表了文章 • 2 个评论 • 334 次浏览 • 2017-11-16 16:28 • 来自相关话题

OK Log姊妹篇


OK Log


OK Log是一个用于大规模集群的分布式且无协的日志管理系统。我是从一些最基本的原则考虑这个系统的设计的。下面介绍的就是这个原型的思路来源。


绪论


过去的一两年时间,我受邀参加很多关于微服务、Go和Go kit的演讲和研讨会议。选一个微服务架构,意味着要对很多考虑点进行技术选型。如果可能的话,对一些新兴的中等规模系统,我愿意给出一些技术指南。开源社区的项目是非常丰富的。



  • 服务编排? 有Kubernetesnomad、DC/OS、ECS等,有很多服务编排工具,都是很好的选择。(ps:目前docker和Kubernetes深度合作了,Mesos可能要被边缘化了)。

  • 服务发现? ConsulEtcdZookeeper动态服务发现等工具,也有静态注册和服务发现工具Linkerd

  • 分布式调用链跟踪?ZipkinJaeger、Appdash、Lightstep等,它还在爆发式增长.

  • 监控工具?Prometheus, 它是目前最强的监控工具、InfluxDB等,它们结合Grafana工具使用

  • 日志?我陷入了沉思....


很明确的答案似乎是Elastic和ELK技术栈。确实,它很有特点、且入手很容易。但是Elastic被很多人认为,对于中等规模的集群,都很难操作。同时我相信,在全文、基于文档搜索时,Lucene或许不是最好的的数据存储格式。最终,我了解了很多使用Elastic的朋友,由于操作的难度很高,他们中的大多数都不怎么乐意使用它。几乎很少有人使用更高级的特性。


更美好的事物


我认为,对于日志管理系统,应该有一个更好的答案。我问了一些同事,他们正在着手的解决方案。一些同事实际上采用了Kafka消息队列解决日志系统管理,特别是对于高QOS和持久化日志要求。但是它的操作也相当难,且最终设计成的日志管理系统,和我感兴趣要解决的问题也不相同。其他人通过数据仓库HBase来解决。但是管理一个Hadoop集群需要更加专业化的只是和非凡的努力。对于这些方案的选择,我认为具体化的或者比较重的系统设计都是一个好的建议。


我还在Twitter上提出了这个问题。Heka似乎是最接近我需要的,但是因为作者前期设计错误,导致了16年年底遇到了无法修复的性能问题,已经放弃了Heka的维护,这是一件非常糟糕的事情。Ekanite提供了端到端的解决方案,但是它的系统日志协议与微服务的工作负载有很明显的不匹配。对于日志传送和注解有非常好的工具,例如:FluentdLogstash,但是它们只能解决部分问题;它们不能处理存储和日志查询。委托解决方案的工具,有SplunkLoggly,如果你的日志是低容量,且不介意把日志上传到云端,这两个工具都是很好的选择,但是它们很快变得昂贵,且无法再本地和开放源代码框中打勾。(ps: 这句话不是很明白)。


Prometheus日志


我意识到我需要的是Prometheus日志的设计原则。什么意思呢?Prometheus好的地方有什么呢?我的观点:



  • 独立运行:它既是开源的、又可以在本地部署

  • 云原生的工作负载:动态的、容器化的和微服务的水平扩展. (ps: 链接中的解释我是非常满意的,是不是就是Serverless)

  • 容易操作:本地存储、没有集群、拉模式

  • 完善的系统:不需要独立的TSDB(时间序列数据库)、web UI等,容易使用

  • 系统扩容:90%的用户承认使用很小的成本,就可以获取比较高的满意度


那Prometheus日志是什么样子的呢?我希望冬天把这个日志管理系统设计完成,我认为这是非常有趣的,同时我也可以学到很多的知识。首先我需要思考得更加深入。


设计


高层次目标


首先,像Prometheus一样,系统应该是开源的,且支持本地部署。更重要的是,它应该很容易部署和水平扩展。它应该更加关注容器化的微服务工作负载。同时他应该是一个完善的,端到端的系统,有forwarders、ingesters、storages和query四个特性。


这个日志管理系统关注点:



  • 微服务的应用程序日志,包括:debug、info、warn等各种级别日志。这个是典型的高容量、低QOS日志,但是对延时(查询时间)有较高的要求。

  • 我们也想服务于事件日志,包括:审计跟踪和点击跟踪等等。这是典型的低容量,搞QOS,但是对延时(查询时间)没有较高的要求。

  • 最后,它应该有一个统一的日志消费者,管理来自黑盒的日志输出,例如:mysql服务。也就是说,我们不会控制日志格式。
    我相信这样的系统可以服务于所有的需求,同时扩展性也非常好。


心里有了这些目标,我们就需要开始增加一些约束,有了边界才能使问题更加容易处理,关注点更加集中。


问题约束


宝贵的经验告诉我,数据系统应该更多地关注数据传输,同时增加数据的价值。这就是说:



  • 它是一个数据运输系统,解决更多的机械问题,黑盒运输

  • 它也应该是一个应用系统,提供商业价值,对拓扑和性能要求不需要参与


如果尝试用一个方案解决这两个问题,会造成竞争和一定的妥协。所以我比较感兴趣数据传输系统,旨在解决低吞吐率和延时问题。我们可以使用其他的工具,在系统外部增加数据的商业价值。例如:上下文context可以在ingest之前发生。或者,解析日志再聚合可以在ETLs(数据仓库技术)中完成。然后再使用更加丰富的查询功能的数据系统将其结果视图化。


考虑到这一点,基于时间边界的grep查询接口是完全可接受的。对于新用户,他们经常想要一个熟悉的接口来帮助他们调试-“我想要grep我的日志”,这是非常有用的。构建ETLs(数据仓库技术)到更复杂的系统中是完全足够的。总之,这个日志管理系统是一个基本的、底层系统,它可以和其他工具搭配使用,至于搭配什么样的工具,主要看你自己的需求。(ps: 类似于系统插件化)


分布式系统


去年在旧金山的Prometheus见面会上,Julius Volz观察到日志数据比监控数据要大几个数量级。Prometheus安装的大多数节点日志已经超出了ingest和单节点容量限制。因此,与Prometheus相同的日志系统必须是一个分布式系统。这个复杂度是根本性的、不可避免的。那好,我们就着手解决它。


无协同


到目前为止,我们最重要的目标是系统容易操作。并且从Prometheus中,我们学习到它应该是平滑水平扩展的,从测试环境到生产环境,没有重大结构变化。在简单和复杂的系统设计中做出合适的权衡是非常痛苦的。但是我强烈建议无协同系统。无协同意味着放弃了其他软件系统的一些优秀特性,如:Elastic、Kafka和Canssandra等。没有master选举、没有节点分配、没有分区表、没有分布式索引、没有vnodes。承认暂停、分袂和死节点是这个设计的一部分。某种意义上来说,这些会使系统设计更加困难,我们使用很少的技术做支撑,所以需要花更多的时间做前期设计。但是另一方面,它更容易实现,因为无协同组件往往更简单,更容易实现。


我们可以看到,如果我们能够设计一个组件模型是无协同的,那么我会充分思考系统的设计


写的重要性


在我们开始设计之前,一个重要的观察:在日志管理系统中,写需求是强烈的,读需求可以等待。(ps: 因为写阻塞,会影响业务系统的性能)。所以,我认为最重要的运行时挑战是写高吞吐量。理想情况下,它无限接近到硬件速度————这也有助于日志管理系统的节点容量扩展。


首先日志系统的总体设计。Agents把日志记录从容器forward到ingesters中。这个ingesters应该执行快速的序列化写操作,把日志记录写入到一个活跃的段文件中,这个ingesters的任务就完成了。让存储节点更关心读操作的优化。
ingesters


Ingestion


由于我们对ingestion和querying有不同的性能要求,那么分离这些组件是非常有意义的。它们是细粒度的节点安装。我们也可以通过一个编译过的二进制文件安装,这样更加容易。


在写操作的时候,每条日志记录被ingester赋予一个全局唯一的ID。这使得很多特性的实现变成了可能,例如:多次相同日志记录的去重。唯一的ID生成有很多方式,例如:UUID或者ULID, 还有twitter提出的64位byte[毫秒数:业务线:机房:机器:预留:毫秒内序列号],这些都非常好。有一点非常重要,每条日志记录有个合理精度的时间戳去创建一个全局唯一的ID;但是有一点不重要的,时钟是全局同步的,或者这些日志记录是严格的线性递增。同时,我认为如果在同一个相同的时间窗口内出现了乱序,只要顺序是稳定的,这也是ok的。ULIDs曾明能够在50ns内生成ID有序,它可以很好的工作。


为了满足日志持久化要求,这里有不同的持久化模式。



  • 如果我们主要关心吞吐量,例如:应用程序的日志,我们可以使用fast模式。写入一个文件描述符而不需要直接同步;

  • 对于事件日志,有一个持久化模式,我们定期地同步活跃段到磁盘上;

  • 最后一种批处理模式:许多客户端同时写整个段文件,只有当它完全复制到其他节点时才被确认。(这个是从Kafka获取的灵感)


这样,我们的日志管理系统的组件模型慢慢的变成了下面这幅图:
Ingesters


我们可以对协同的讨论思考得更多一点。如果我们编排它,以至于任何ingest节点都能够服务于任何forwarder传送的日志记录,这样我们避免了forwarders需要知道超出ingesters的地址信息。forwarders是在ingesters池中任意拿一个进行连接,并且能够实现反压(backpressure)、缓冲和重连逻辑等。依赖于其他下游服务。有了这些限制,ingesters可以不受约束。到目前为止这种方式还挺好的。


日志复制


日志记录写入磁盘是不安全的,一旦落到磁盘,日志记录就需要被复制备份。我们知道数据需要多节点存储。


来自Prometheus的日志设计思路,我们把典型的日志复制有push模式改为了pull模式。准确地说,一个集群中的所有ingest和存储节点,需要通过gossip协议通信。所有的存储节点定期地和随机地消费来自所有ingest节点的段文件。消费后的段需要合并,同时合并之后的文件达到一定的size后,就会复制到其他存储节点上进行备份。只有成功复制之后,原始段从ingest节点中确认和清除。


replication图解


实际上,我们把每一个ingest节点变成了一个后端磁盘队列。并且每一个存储节点获得了整个日志的子集,并且密度由复制因子决定。


为什么要把ingest节点数据移动到存储节点上呢?



  • 在较小的场景中,低频的读操作和写操作负载可能这是没有什么作用。例如:本地测试,我们会提供ingest+存储的混合节点

  • 在一些较大的场景中,I/O可能是最主要的瓶颈,并且ingest工作负载(顺序写)与存储工作负载(半随机读和写)是竞态的。隔离是很聪明的做法。


在复制事务期间,任何失败(或者超时)都会造成事务的中断,并且ingester段将会在后面重新消费。这回造成重复日志记录,但是这是ok的。因为查询时结果通过ULIDs是去重的。最终,我们至少交付了一次。这种复制形式是事务的,但是没有协同。


弹性


注意到,ingest层实际上是一个分布式的,磁盘存储的日志记录队列。我们能够扩展ingesters来处理我们的写容量。同时我们也能扩展存储层来处理我们的复制因子,设置日志有效期,以及读容量要求。


增加节点到每一层,就像让他们加入集群并开始工作一样简单。有一个优化,ingest节点能够通过gossip协议扩散负载信息,并通过增减来平衡节点负载。存储节点自动地开始消费来自多个ingest节点的段共同平分的份额。只要ingest段size小于存储段size,就可以立即平衡写负载。磁盘利用率在保留时间范围内保持平衡。所有这些都没有明确的成员注册,芈月空间声明或者任何形式的公式。集群的增长或者削减都是无协同的。


合并


存储节点最终累计不同size和时间范围的段文件。合并是对日志记录的清洗、合并和重新分割的过程,目的是统一数据格式存储和优化查询。


compaction


合并能够merge段的叠加,如上图所示,合并小的、序列化的段。在每一个阶段,它可以炒作统一的段文件数据格式一步步进化,这就是我们想要的时间边界查询。同时,合并agent能够用于强制保留期。观察数据集保持不变,只有磁盘上的布局优化。合并的影响是透明的,本地的,所以无协同。


查询


查询字面上是时间边界grep。我们分散查询到所有的查询节点上,然后聚集数据,返回合并后且日志去重的记录给用户。每一个ULIDs日志记录为去重的日志记录提供可排序的身份ID。通过从较少的节点读取,可以提交效率吗?Yes。but that would involve prior knowledge about segment location/allocation, which requires some form of coördination. We deliberately make the read path dumb, and pay some costs of inefficiency, to keep it coördination-free., (ps: 这段话不明白)


原型设计


实现


在几个朋友的帮助下,我逐渐详细地描述了系统设计。这给我带来了很大的乐趣。设计无协同的分布式系统是人生中一个非常大的乐趣。经过几周时间的努力,我开始说服自己,设计方案是可行的。经过整个假期,我开始了一个设计代码实现。经过一周或者更久,我有了一个看似正确且有用的原型。然后开始花时间进行压力测试。


验证


现在我将会描述验证的过程,并且通过连续的系统负载测试来分析系统性能。这个测试环境是由DigitalOcean提供的,在此感谢他们!


我创建了8个forwarder节点集合,3个ingester节点和3个存储节点。我开始从一些基本的正确性和crash测试入手,很快就被来自每个组件的垃圾日志所淹没。重现状态时非常困难的,或者从日志垃圾邮件中得出有意义的结论。我最终删除了一大堆日志语句,并添加了很多指标。构建Prometheus表达式和图表是建立洞察力更有效的方法。最后,我仅仅在启动时记录一些运行参数,并清除错误,如:写入文件失败。我非常清楚地意识到这里的坑。


吞吐量


我想要第一件要优化的是吞吐量。为了满足我自己的好奇心,我再Twitter上做了一个调查。我对集群中的每个节点日志吞吐量非常感兴趣。这个测试结果范围值很大,从1KBps到25MBps之间变化。5MB/sec/node对于80%~90%的方案是一个比较好的目标。让我们看看典型的测试用例。


DigitalOcean磁盘显然可以达到250MBps的持续写入,在云服务中这是表现非常好的。在我自己的测试环境中,磁盘写入测试要少一些,它在150MBps上下浮动。如果我们系统设计得正确,那么150MBps就是我们的I/O性能瓶颈。因为我们每个节点的写速度控制在5MB/sec/node,则单个ingest节点能够处理写操作不阻塞的集群节点大小范围:150/5=30 ~ 250/5=50个节点。这个范围间的集群因子都是合理的。因为我们有3个ingest节点,所以写操作的速度是150MBps*3个节点=450MBps的聚合速度。


优化forwarding


这个forwarder不过是netcat而已。基本地


    // client connection, forwarder就类似于下面的作用,传送数据
conn, _ := net.Dial("tcp", ingesterAddress)
s:=bufio.NewScanner(os.Stdin)
for s.Scan() {
fmt.Fprintf(conn, "%s\n", s.Text()) // 往tcp链路中向服务端发送终端产生的数据
}

Go's bufio.Scanner在这里非常形象;产生数据后,通过tcp链路传送数据。~~whatever limits I hit, they weren’t imposed by the scanner. ~~,(ps: 不明白)。我用一些低效率地方式来生成日志记录。我观察到CPU一路飘高,吞吐率远低于预期。性能分析暴露了两个问题:




  1. 我在热循环中使用了一个time.Ticker。每一个日志行带有一个ticker


    hz:=time.Second / recordsPerSecond
    for range time.Tick(hz) {
    // 传送一条日志记录
    }

    这里有一个问题,如果你想要每秒记录1000条日志时,则每1ms阻塞一次是资源浪费的。我推荐采用批量传送日志记录,如下所示:


    var (
    recordsPerCycle = 1
    timePerCycle = time.Second / recordsPerSecond
    )
    for timePerCycle < 50*time.Millisecond {
    recordsPerCycle *= 2
    timePerCycle *= 2
    }
    for range time.Tick(timePerCycle) {
    // 每次记录recordsPerCycle条日志记录
    }


  2. 我利用随机数据在热循环中构建每一行日志,大量消耗CPU。在程序开始时,预先计算一大组固定的随机日志可以解决这个问题。通过这些变化,我可以很轻松地从每个进程推送大量的MBps,而且负载可以忽略不计。这个第二点翻译感觉很有问题, 原文如下:


Also, I was building each log line, of random data, within the hot loop, and burning lots of CPU in math.Rand to do it. Precomputing a large, fixed set of random log lines at program start solved that one. With those changes, I could easily push plenty of MBps second from each process with negligible load.


我为每个forward节点创建了1-8个forwarders,共为8个ingest节点设置了8-64个forwarders。每个进程将每秒处理100-1000条日志记录,每条日志记录有100-8000个bytes,每秒生成能力高达512MB。非常高的性能。


优化ingestion


在开头我很担心每一条记录一个全局唯一ULID造成的性能问题。但是多亏Tomás Senart写的优秀库ULID,这些代价实际上非常低,每ULID消耗50ns,则1s可以生成1000000000/50=2千万个ULID。因为我们不需要任何数据加密协议。下面是测试性能数据:


BenchmarkNew/WithoutEntropy-8    30.0 ns/op  534.06 MB/s  1 alloc/op
BenchmarkNew/WithEntropy-8 65.8 ns/op 243.01 MB/s 1 alloc/op
BenchmarkNew/WithCryptoEntropy-8 771 ns/op 20.73 MB/s 1 alloc/op

我最初能够将每个ingest实例推送到30MBps,但是事情变得很棘手。初始化性能分析揭露了在bytes.FieldsFunc和系统调用方法中,CPU不成比例的过度消耗。



  • 对于bytes.FieldFunc方法,我对比了ULIDs,前者表现令人意想不到的差劲。切换到固定大小偏移分割-ULID给了我们这个能力,改进并提升性能,但不是ingest率。

  • 对于系统调用syscalls,我怀疑是文件系统的竞争导致的。为了验证我的怀疑,我抽象了文件系统,并提供了一个无操作的实现。即使扩展到几百兆的ingest节点写入,这性能表现也非常好。事实证明,在最初的设计中,我将所有传入的连接多路复用到相同的活跃段文件中。假设将部分段传送到存储节点会是一个瓶颈,所以我的想法是优化(最小化)每个ingest节点产生的活跃部分数量


但是性能分析揭示,多路复用会花费很大的代价。因此,我分离它,把每个连接写入到自己的段文件中。这是最主要的改善。


活跃的段文件达到一定的时间或者size就会写入到存储节点,并关闭段文件。这个时间限制保证了系统日志的实时性,文件大小保持复制可管理特性。



  • 如果根据时间窗口关闭段文件,日志记录大小可能会比较低,而且事情也简单。我们能够基于想要的日志记录延时,选择你想要的时间窗口。(ps:但是这可能会造成日志持久化不实时,当日志产生后查询不到结果)。3s是默认值

  • 如果根据size大小关闭段文件,我们会有高容量,但是如果日志记录产生速度过慢,则会造成段文件在一定时间内达不到size阈值,造成上面一样的结果,不实时。


所以我们必须要进行时间和size的权衡,文件应该足够大,以便分摊复制开销,并有效利用典型的SSD,但又足够小,可以快速传输,并有效地组成存储层段文件。我选择16MB作为默认的ingest段文件大小,同时选择128MB作为存储节点段文件的默认大小。但是这些选择没有必要是统一的。他们可能会极大地影响系统吞吐量,应该要在特定的环境中进行验证。


随着ingest节点积极地消费和保存记录,下一步我们讨论存储节点。


优化复制


日志复制工作起来要像这样:一旦一个ingest节点的段,由于时间窗口或者size关闭了,那么存储节点则可以存储该段了。存储节点随机轮询ingest节点,并且获取最老可用的段(ps: 偏移位置)。它们会在内存中就合并好这些段到一个聚合的段中。这都是在一个循环中做的,直到这个聚合段达到了设置的时间窗口或者size。然后它们会复制这个聚合段到N个随机的存储节点上,N是由你的复制因子决定的。一旦复制确认完成,这个ingest节点的段将被确认和删除。


在实践中这个复制机制工作得很好。甚至当ingestion真的不平衡时,存储节点也会消化掉要复制的日志段。


ingest&store nodes


存储节点通过状态机consume段文件:gather、replicate和commit。



  • gather状态要求尽快地调整并消费这些段文件(当然,我明白轮询很糟糕,订阅方式比较好。未来我会实现这个功能)

  • replicate状态主要是采用POST方式发送聚合的段到其他存储节点。用足够大的段来分摊这个POST的成本也是很重要的。(我们也可以通过一直选择本节点作为目标节点之一来进行优化。这也是未来的工作)


这里成功的标准是看ingest节点数在队列上的深度。。换句话说,就是存储节点的消费日志的速率与ingest节点接收日志的比率。


store nodes/ingest nodes


这个比率保持在1附近上下浮动是最好的。既不会饥饿,也不会撑死。大于1,则表示存储节点的消费速度大于ingest节点的接收速度;~~maybe chewing through a backlog? ~~ (ps: 不明白)。小于1,则表示存储节点消费得不够快,最好是添加存储节点。


最初的这些设计证明是稳定可靠的。当然还有很多可以优化的地方,但是还没有发现重大问题。我很满意整个系统处理日志的速率。但是它能查询吗?


优化querying


这个是真正的考验。如果查询速度太慢,大多数用户将无法忍受。初始的查询性能测试完全达不到我的预期。现在这部分仍然还有很多可以改善的地方。


初始的grep设计师非常简单的。我们通过读取多条日志记录,然后通过多路归并算法找到匹配的段日志记录。并且我们在这个归并的输出上附加上了时间范围和查询表达式过滤的条件查询。整体查询框架图如下:


querying


Tomás注意到,在一个完美紧凑的系统中,不会有重叠的段。在这种情况下,不需要过多消耗CPU来完成全局的归并。我们可以首先读取不重叠的段,然后序列化地读取重叠段先进行一次归并。(ps: 可以这样理解,先把不重叠的段不合并,重叠的段先局部合并,然后再一次次的局部合并,最后在做一次整体合并)因此,我实现了一个MultiReader,它由普通的文件reader或者归并reader组成。具体取决于段的重叠。


优化后的querying


利用这种方式,在某些例子中可以提高50%的速率。


然后我们再收集了一些性能分析数据,显示在系统调用syscalls、日志记录的过滤管道和正则表达式匹配时的CPU消耗。我们认为显示的文件mmaping可能会提高读性能,所以我们设计的一个mmaping文件系统抽象的原型。但是和直接的文件系统做性能对比,我们没有获取显著的性能改善。


direct


mmaping


同时我也对比了过滤日志记录从磁盘读取日志的时间开销。对比图如下所示:



  • 在直接的文件系统中,读取时间开销主要在系统调用;

  • 在mmap文件系统中,读取时间开销主要在memmove上。


direct


mmaping


我发现页面缓存是非常有效的


经过一两天的思考,我意识到操作的顺序是归并然后过滤


merge


上面这幅图,有一部分CPU消耗在了不相关的段归并上。由于归并建立在一个全局有序的事务上,所以它被约束在一个CPU核上来完成。如果我们先过滤,然后再归并过滤后的段。这使得前者可以并发执行,充分利用多核特性。


优化后的查询


做了这些改变后,查询速度比之前高两倍了。CPU利用率更高了。但是阻塞分析揭示了在每个段的goroutine写入到io.Pipe中需要花费大量的等待时间。如果我们可以缓冲pipe,我们会有更好的性能提升。但不幸的是,io.Pipe没有给你一个可以缓冲的内存空间。同时,幸运的是,我们找到了djherbis/nio包,它提供了一个具有相同功能的缓冲Pipe。用一个适中的1MB缓冲区与直接用io.Pipe进行速度对比,提高了2倍多,太惊人了!!!


满意后,我们开始把注意力放在过滤时附加的正则表达式时间消耗。切换到bytes.Contains后有了合理的改善。事实证明各个点优化也是非常nice的。因此,我给查询时间定义了这个flag,只有在需要的时候才选择加入正则表达式匹配。(进一步优化,可能会使用PCRE(ps: 这个perl写的正则表达式库比其他库的速度要提高3倍)正则表达式,这是未来一段时间的工作)。


此时,我们意识到,我们当前的工作集(大约21G)超过了存储节点(8G)。如果我们可以获取更多的内存,我可以一次性加载整个工作集到页面缓存中,并希望借此解决其他的低效率问题。我们启动了一个32G DigitalOcean droplet,包括更多的CPU核来协助并发过滤。没有其他的变化就给了我们两倍的提速。


之前调优Cassandra的经验给了我们更多的想法。我们调整了I/O调度和readahead设置,这给了我们另外20%的改进。尽管在这一点上,我们已经非常接近仅仅基于节点上的内存总线。我们在4.6s内可靠的查询了20G日志记录。读取吞吐量为4.47GBps。这里可能还有额外的工作来优化磁盘访问,但是这似乎完全可以达到初始设置的标准。


版本一


现在大家使用的就是版本一!OK Log。哪还有什么工作要留给未来呢?


未来工作


我们能够,而且应该做类似于Cassandra的一些读修复。即查询结果在所有存储节点的数据都是相同的。存储节点数据不一致的日志记录目前还不能被检测、批处理和重写到存储节点中。未来合并会把他们最终存储到合适的位置。这个是issue 6


```Cassandra读修复
Cassandra读修复


客户端读取某个对象的时候,触发对该对象的一致性检查:


读取Key A的数据时,系统会读取Key A的所有数据副本,如果发现有不一致,则进行一致性修复。



  1. 如果读一致性要求为ONE,会立即返回离客户端最近的一份数据副本。然后会在后台执行Read Repair。这意味着第一次读取到的数据可能不是最新的数据;

  2. 如果读一致性要求为QUORUM,则会在读取超过半数的一致性的副本后返回一份副本给客户端,剩余节点的一致性检查和修复则在后台执行;
    3.如果读一致性要求高(ALL),则只有Read Repair完成后才能返回一致性的一份数据副本给客户端。可见,该机制有利于减少最终一致的时间窗口。


相关地,在数据丢失之前,整个系统能够容忍N-1个存储节点挂掉,但是如果一个存储节点挂掉了,没有其他修复进程拉起和修复数据的话,我们会进行服务降级。一个修复进程监听每个存储节点,当节点挂掉后会立即启动进程修复,它会顺序地查询每条日志记录,触发对尚未完全复制的日志记录进行读取修复。这个是issue 11


添加一个ingest节点对系统来说是无感知的。仅仅只有新的客户端会连接它,存储节点消费它,小事情。但是ingest节点会通过gossip协议传送各自的瞬时负荷给通信的对方。例如:连接的客户端数量,吞吐量等。这里有三种处理情况:



  • 有些高负载的服务器可以在连接时向新客户端提供一些轻量级的服务。客户端,例如:forward节点也会根据ingest节点的负载情况合理的传送日志记录。

  • 高负荷的ingest节点会在一段时间内拒绝新连接请求。

  • 如果需要的话,再高负载的ingest节点,也可以既拒绝新连接请求,也可以启动现有连接。


以上三种策略都是可以的,考虑到forward节点会重连集群中的其他ingest节点。有一点需要关心的是:响应慢,绝不要超过X%的ingest节点拒绝连接。这个ingest节点的负载均衡在issue 2有讨论。


相似地,增加一个新的存储节点也工作得很好。它会开始consume来之ingest节点的共享成比例段文件。在保留时间的窗口内,这将是平等的。但在该窗口过去之前,新的存储节点和其他存储节点相比,前者获取的共享比例段比较小。作为一种优化,它会通过gossip协议告诉其他存储节点它的当前数据集。当复制时,存储节点偏向于具有较小总体数据集大小的节点。新的存储节点将会获得较多的数据复制请求;老的存储节点将会接收较少的数据复制请求。这种逐渐重新平衡策略反应了我的观点,即尽可能地不要移动数据,静态存放。在发生大的拓扑变化之后,我观察到由分段/分片/节点重新平衡引起的大量中断。我认为这是可行的,但是它是否真的可行还有待观察。存储层的负载均衡在issue 3有讨论。


失败模式是经过深思熟虑的,但是没有经验证实它。我有兴趣构建一个带有故障注入的分布式验证框架,类似于简化的Jepsen风格测试工具。另外,建立一种方法来验证总体系统的吞吐量(MBps)和延时(ingest to query)。这个测试工作在issue 14中讨论。


在早期的设计过程中,我观察到队列理论在这里比较适用。我真的很喜欢用Adrian Cockcroft的微服务响应时间分布式的分析方法对系统进行建模。我开始着手这个工作,但是我没有太多时间去跟这个事情。这个模型在issue 9中讨论。


这真的只是一个开头;还有很多其他工作量比较小的事情要做。问题清单可能还需要一两个月才能列出来。


总结


这个系统对一些人是有用的吗?我不知道,或许吧。如果有人多听说,请试一试,或者可以邀请我和你一起在线讨论。如果没有,也好,这个系统设计对我也是一种很好的锻炼,是一个很享受的过程体验,并从中学到了很多知识。对我来说这就足够了。

Teleport v2.5发布,支持限制包大小与自定义包协议

henrylee2cn 发表了文章 • 0 个评论 • 329 次浏览 • 2017-11-15 21:32 • 来自相关话题

Teleport v2.5(简称tp v2.5)今日发布啦!它是一个通用、高效、灵活的TCP Socket框架。可用于Peer-Peer对等通信、... 查看全部

Teleport v2.5(简称tp v2.5)今日发布啦!它是一个通用、高效、灵活的TCP Socket框架。可用于Peer-Peer对等通信、RPC、长连接网关、微服务、推送服务,游戏服务等领域。这次升级新增了自定义通信协议、包大小限制等一些新特性,并作了一系列深度优化。


tp v2.5 特性变化:



  • 【新增】支持设置读取包的大小限制(如果超出则断开连接)

  • 【新增】支持定制通信协议

  • 【升级】支持插件机制,可以自定义认证、心跳、微服务注册中心、统计信息插件等

  • 【优化】无论服务器或客户端,均支持优雅重启、优雅关闭

  • 支持实现反向代理功能

  • 【优化】日志信息详尽,支持打印输入、输出消息的详细信息(状态码、消息头、消息体)

  • 服务器和客户端之间对等通信,两者API方法基本一致

  • 底层通信数据包包含HeaderBody两部分

  • 支持单独定制HeaderBody编码类型,例如JSON Protobuf string

  • Body支持gzip压缩

  • Header包含状态码及其描述文本

  • 支持推、拉、回复等通信模式

  • 支持设置慢操作报警阈值

  • 底层连接使用I/O缓冲区

  • 端点间通信使用I/O多路复用技术


teleport




tp v2.5 升级详情:


一、增加对自定义通信协议的支持,通过实现socket.Protocol接口来定制:


// Protocol socket communication protocol
type Protocol interface {
// WritePacket writes header and body to the connection.
WritePacket(
packet *Packet,
destWriter *utils.BufioWriter,
tmpCodecWriterGetter func(string) (*TmpCodecWriter, error),
isActiveClosed func() bool,
) error

// ReadPacket reads header and body from the connection.
ReadPacket(
packet *Packet,
bodyAdapter func() interface{},
srcReader *utils.BufioReader,
codecReaderGetter func(byte) (*CodecReader, error),
isActiveClosed func() bool,
checkReadLimit func(int64) error,
) error
}

然后,可以通过以下任意方法指定自己的通信协议:


func SetDefaultProtocol(socket.Protocol)
func (*Peer) ServeConn(conn net.Conn, protocol ...socket.Protocol) Session
func (*Peer) DialContext(ctx context.Context, addr string, protocol ...socket.Protocol) (Session, error)
func (*Peer) Dial(addr string, protocol ...socket.Protocol) (Session, error)
func (*Peer) Listen(protocol ...socket.Protocol) error

二、新增限制通信包大小


在读取包时可以限制包的大小,如果超出最大值则会主动断开连接。全局设置函数:


func SetReadLimit(maxPacketSize int64)

三、升级插件接口



  1. 插件返回值由以前的error改为tp.Xerror,从而用户可以灵活地在插件中定义错误码和错误描述;

  2. 增加更多、更细、更合理的插件位置

  3. 插件执行出错时的日志格式更加清晰整洁


// Interfaces about plugin.
type (
Plugin interface {
Name() string
}
PostRegPlugin interface {
Plugin
PostReg(*Handler) Xerror
}
PostDialPlugin interface {
Plugin
PostDial(PreSession) Xerror
}
PostAcceptPlugin interface {
Plugin
PostAccept(PreSession) Xerror
}
PreWritePullPlugin interface {
Plugin
PreWritePull(WriteCtx) Xerror
}
PostWritePullPlugin interface {
Plugin
PostWritePull(WriteCtx) Xerror
}
PreWriteReplyPlugin interface {
Plugin
PreWriteReply(WriteCtx) Xerror
}
PostWriteReplyPlugin interface {
Plugin
PostWriteReply(WriteCtx) Xerror
}
PreWritePushPlugin interface {
Plugin
PreWritePush(WriteCtx) Xerror
}
PostWritePushPlugin interface {
Plugin
PostWritePush(WriteCtx) Xerror
}
PreReadHeaderPlugin interface {
Plugin
PreReadHeader(ReadCtx) Xerror
}

PostReadPullHeaderPlugin interface {
Plugin
PostReadPullHeader(ReadCtx) Xerror
}
PreReadPullBodyPlugin interface {
Plugin
PreReadPullBody(ReadCtx) Xerror
}
PostReadPullBodyPlugin interface {
Plugin
PostReadPullBody(ReadCtx) Xerror
}

PostReadPushHeaderPlugin interface {
Plugin
PostReadPushHeader(ReadCtx) Xerror
}
PreReadPushBodyPlugin interface {
Plugin
PreReadPushBody(ReadCtx) Xerror
}
PostReadPushBodyPlugin interface {
Plugin
PostReadPushBody(ReadCtx) Xerror
}

PostReadReplyHeaderPlugin interface {
Plugin
PostReadReplyHeader(ReadCtx) Xerror
}
PreReadReplyBodyPlugin interface {
Plugin
PreReadReplyBody(ReadCtx) Xerror
}
PostReadReplyBodyPlugin interface {
Plugin
PostReadReplyBody(ReadCtx) Xerror
}

PostDisconnectPlugin interface {
Plugin
PostDisconnect(PostSession) Xerror
}
)

四、更多细节优化



  1. 运行日志中打印增加包序号seq,便于debug

  2. 当收到不支持的包类型时,断开连接并打印包详情

  3. tp.PullCmd增加func (c *PullCmd) Result() (interface{}, Xerror)方法,便于使用Session.GoPull方法进行并发请求

  4. 升级平滑重启与关闭功能

  5. 增加对并发资源的控制,防止内存资源耗尽

  6. 一些代码块的细节优化


Teleport项目地址:
https://github.com/henrylee2cn/teleport

结合 Go 读 APUE-文件共享

zhaohu 发表了文章 • 0 个评论 • 230 次浏览 • 2017-11-10 21:26 • 来自相关话题

在公众号 "别捉急" 上 同步了文章,并且可以点击原文链接阅读:查看全部


在公众号 "别捉急" 上 同步了文章,并且可以点击原文链接阅读:传送门



文件共享


UNIX 系统支持在不同进程间共享打开文件, 知识点:内核用于所有 I/O 的数据结构、原子操作。


概念性的 I/O 数据结构


内核用于所有 I/O 的数据结构,只是个概念性的,不一定适用,有个大体的轮廓就 OK。



  • 进程表 (process table entry) 中的记录

  • 文件表项 (file table entry)

  • v节点表项 (v-node table entry)


打开文件的内核数据结构


这是一个 打开文件的内核数据结构 图。打开文件 这个操作是一个进程, 每个进程在进程表中都有一个记录,而 打开文件进程记录 中包含一张打开文件描述符表, 包括:



  • 文件描述符标志

  • 指向一个文件表项的指针


文件描述符表用 Go 抽象如下表示:


type fd struct {
flags int
pointer *FileTableEntry
}

代码中 flags 的类型是随便定义的(实际我没查),由图中看出 pointer 指向文件表项 (file table entry), 内核为所有打开文件维持一张文件表, 每个文件表项包括:



  • 文件状态标志 (读、写、添写、同步和非阻塞)

  • 当前文件偏移量

  • 指向该文件 v 节点表项的指针


文件表项用 Go 抽象如下表示:


type FileTableEntry struct {
status int
offset int
pointer *VNodeTableEntry
}

由图中看出 pointer 指向v节点表项 (v-node table entry), 每个打开文件都有一个 v 节点结构如下所示:



  • 文件类型和对此文件进行各种操作函数的指针,统称为 v节点信息

  • 该文件的 i 节点: 文件所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等


V 节点表项和 i 节点用 Go 抽象如下:


type VNodeTableEntry struct {
information *Information
vData *INode
}

type INode struct {
owner *Owner
length int
vNodeTableEntry *VNodeTableEntry
}

通过这种方式,来加深对 内核通用 I/O 数据结构 的理解。


如果两个独立进程各自打开同一个文件,则三者关系如下所示:


两个独立进程打开同一个文件


原子操作


一般而言,原子操作 (atomic operation) 指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。


函数 dup 和 dup2


下面两个函数都可用来复制一个现有的文件描述符。


#include <unistd.h>

int dup(int fd);

int dup2(int fd, int fd2);

上面函数中的参数:



  • fd 表示要复制的文件描述符

  • fd2 表示复制后的文件描述符


dup2 函数是可以指定复制后的文件描述符,而 dup 是返回当前可用文件描述符中的最小数值。


调用 dup(1) 函数后,进程表项,文件表,v 节点表,之间的关系图如下:
dup(1) 后的内核数据结构


对于传入的参数 fd2, 已经被打开了,会先关闭。知道了这个点,就明白了,下面的操作中会先调用 close(fd2)


很不爽了,没找到相应的 Go 的源码。复制一个描述符的另一种方法是使用 fcntl函数, dup2(fd, fd2) 等效于 close(fd2)fcntl(fd, F_DUPFD, fd2), 但不完全等效,因为 dup2(fd, fd2) 是个原子操作。


目前我先知道 fcntl函数 可以改变已经打开文件的属性,就可以啦。

不再傻傻分不清:atoi, itoa, iota

Xanthus 发表了文章 • 0 个评论 • 242 次浏览 • 2017-11-09 21:01 • 来自相关话题

atoi

Array to Integer 字符数组(字符串)转化为整数。golang标准库与C++标准库均有

itoa

Integer to Array 整数转化为字符串。golang标准库与C++标准... 查看全部

atoi


Array to Integer
字符数组(字符串)转化为整数。golang标准库与C++标准库均有


itoa


Integer to Array
整数转化为字符串。golang标准库与C++标准库均有


iota


希腊字母。golang中定义常量经常用的iota关键字,C艹中用于Store increasing sequence

emmmmm, 都是递增