for j:=0;j<4 ;j++ { } 与for _,v :=range persons {} 区别

有问必答sheepbao 回复了问题 • 4 人关注 • 3 个回复 • 1182 次浏览 • 2016-10-12 11:44 • 来自相关话题

Beego能否在静态文件的header上动态增加no cache?

有问必答astaxie 回复了问题 • 2 人关注 • 1 个回复 • 1078 次浏览 • 2016-10-12 10:20 • 来自相关话题

beego框架 个人博客系统

有问必答朋也 回复了问题 • 6 人关注 • 3 个回复 • 2113 次浏览 • 2016-10-12 09:56 • 来自相关话题

如果用go重构samba为分布式的软件,会有前景吗?

有问必答leoliu 回复了问题 • 2 人关注 • 1 个回复 • 896 次浏览 • 2016-10-12 09:52 • 来自相关话题

请问go的windows gui库walk中提及的syso文件是否哪位兄弟有过比较深入的研究?

技术讨论astaxie 回复了问题 • 3 人关注 • 1 个回复 • 1235 次浏览 • 2016-10-12 09:45 • 来自相关话题

10/12 每日早报

文章分享astaxie 发表了文章 • 0 个评论 • 547 次浏览 • 2016-10-12 08:06 • 来自相关话题

10.12 每日早报

新闻:

1.百度云计算品牌升级,面向企业正式启用“百度云”品牌

2.FreeBSD 11.0正式发布,支持RISC-V指令集和NUMA内存调度

3.三菱汽车正式并入雷诺日产联盟... 查看全部

10.12 每日早报


新闻:


1.百度云计算品牌升级,面向企业正式启用“百度云”品牌


2.FreeBSD 11.0正式发布,支持RISC-V指令集和NUMA内存调度


3.三菱汽车正式并入雷诺日产联盟,所有业务将由后者接管


4.中通快递在美提交招股书,拟最高融资15亿美元


5.伊朗打车软件Snapp完成2000万欧元A轮融资,南非手机企业MTN独家领投


6.空间再造平台共享际获A轮超4亿元融资,主打空间运营+内容运营


7.美国梅西百货计划在2017年在中国上线独立电商网站


资源:


15个开源的顶级人工智能工具
https://news.cnblogs.com/n/554933/


二手车行业用户搜索报告
http://www.goerxon.com/media/report-1.pdf


注:上述内容来源于互联网,由EGO整理

杭州/物联网门锁公司招聘Golang工程师

招聘应聘astaxie 回复了问题 • 5 人关注 • 1 个回复 • 1165 次浏览 • 2016-10-11 23:59 • 来自相关话题

bee run时可以配置排除某些目录吗

有问必答astaxie 回复了问题 • 2 人关注 • 1 个回复 • 846 次浏览 • 2016-10-11 23:37 • 来自相关话题

go怎么基于http显示上传和下载进度?

有问必答philosophia14 回复了问题 • 5 人关注 • 3 个回复 • 1935 次浏览 • 2016-10-11 22:34 • 来自相关话题

[译] fasthttp 文档手册

文章分享davidcai1993 发表了文章 • 2 个评论 • 1843 次浏览 • 2016-10-11 22:03 • 来自相关话题

fasthttp 文档手册

貌似文章有最大长度限制,完整全文地址:https://github.com/DavidC... 查看全部

fasthttp 文档手册


貌似文章有最大长度限制,完整全文地址:https://github.com/DavidCai1993/my-blog/issues/35


常量


const (
CompressNoCompression = flate.NoCompression
CompressBestSpeed = flate.BestSpeed
CompressBestCompression = flate.BestCompression
CompressDefaultCompression = flate.DefaultCompression
)

所支持的压缩级别。


const (
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1

StatusOK = 200 // RFC 7231, 6.3.1
StatusCreated = 201 // RFC 7231, 6.3.2
StatusAccepted = 202 // RFC 7231, 6.3.3
StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
StatusNoContent = 204 // RFC 7231, 6.3.5
StatusResetContent = 205 // RFC 7231, 6.3.6
StatusPartialContent = 206 // RFC 7233, 4.1
StatusMultiStatus = 207 // RFC 4918, 11.1
StatusAlreadyReported = 208 // RFC 5842, 7.1
StatusIMUsed = 226 // RFC 3229, 10.4.1

StatusMultipleChoices = 300 // RFC 7231, 6.4.1
StatusMovedPermanently = 301 // RFC 7231, 6.4.2
StatusFound = 302 // RFC 7231, 6.4.3
StatusSeeOther = 303 // RFC 7231, 6.4.4
StatusNotModified = 304 // RFC 7232, 4.1
StatusUseProxy = 305 // RFC 7231, 6.4.5

StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7
StatusPermanentRedirect = 308 // RFC 7538, 3

StatusBadRequest = 400 // RFC 7231, 6.5.1
StatusUnauthorized = 401 // RFC 7235, 3.1
StatusPaymentRequired = 402 // RFC 7231, 6.5.2
StatusForbidden = 403 // RFC 7231, 6.5.3
StatusNotFound = 404 // RFC 7231, 6.5.4
StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5
StatusNotAcceptable = 406 // RFC 7231, 6.5.6
StatusProxyAuthRequired = 407 // RFC 7235, 3.2
StatusRequestTimeout = 408 // RFC 7231, 6.5.7
StatusConflict = 409 // RFC 7231, 6.5.8
StatusGone = 410 // RFC 7231, 6.5.9
StatusLengthRequired = 411 // RFC 7231, 6.5.10
StatusPreconditionFailed = 412 // RFC 7232, 4.2
StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11
StatusRequestURITooLong = 414 // RFC 7231, 6.5.12
StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13
StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4
StatusExpectationFailed = 417 // RFC 7231, 6.5.14
StatusTeapot = 418 // RFC 7168, 2.3.3
StatusUnprocessableEntity = 422 // RFC 4918, 11.2
StatusLocked = 423 // RFC 4918, 11.3
StatusFailedDependency = 424 // RFC 4918, 11.4
StatusUpgradeRequired = 426 // RFC 7231, 6.5.15
StatusPreconditionRequired = 428 // RFC 6585, 3
StatusTooManyRequests = 429 // RFC 6585, 4
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3

StatusInternalServerError = 500 // RFC 7231, 6.6.1
StatusNotImplemented = 501 // RFC 7231, 6.6.2
StatusBadGateway = 502 // RFC 7231, 6.6.3
StatusServiceUnavailable = 503 // RFC 7231, 6.6.4
StatusGatewayTimeout = 504 // RFC 7231, 6.6.5
StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
StatusInsufficientStorage = 507 // RFC 4918, 11.5
StatusLoopDetected = 508 // RFC 5842, 7.2
StatusNotExtended = 510 // RFC 2774, 7
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
)

与 net/http 相同的 HTTP 状态吗。


const DefaultConcurrency = 256 * 1024

DefaultConcurrency 为默认情况下(没有设置 Server.Concurrency 时)服务器可以接受的最大并发请求数。


const DefaultDNSCacheDuration = time.Minute

DefaultDNSCacheDuration 是由 Dial* 函数族缓存处理过的 TCP 地址的持续时间。


const DefaultDialTimeout = 3 * time.Second

DefaultDialTimeout 是由 DialDialDualStack 使用的用于建立 TCP 连接的超时时间。


const DefaultMaxConnsPerHost = 512

DefaultMaxConnsPerHost 是 http 客户端在默认情况下(如果没有设置 Client.MaxConnsPerHost)单个 host 可以建立的最大并发连接数。


const DefaultMaxIdleConnDuration = 10 * time.Second

DefaultMaxIdleConnDuration 是在空闲的 keep-alive 连接被关闭前默认的持续时间。


const DefaultMaxPendingRequests = 1024

DefaultMaxPendingRequestsPipelineClient.MaxPendingRequests 的默认值。


const DefaultMaxRequestBodySize = 4 * 1024 * 1024

DefaultMaxRequestBodySize 是服务器默认可读的最大请求体大小。


更多详情请参阅 Server.MaxRequestBodySize


const FSCompressedFileSuffix = ".fasthttp.gz"

FSCompressedFileSuffix 是当需要使用新文件名存储被压缩后的文件时, FS 在原始文件名上添加的前缀。更多详情请参阅 FS.Compress


const FSHandlerCacheDuration = 10 * time.Second

FSHandlerCacheDuration 是由 FS 所打开的非活跃文件句柄的默认失效时间。


变量


var (
// ErrNoFreeConns 在当特定的 host 没有可用的连接时返回。
//
// 如果你看到了这个错误,你可以选择调高每个 host 可用的连接数。
ErrNoFreeConns = errors.New("no free connections available to host")

// ErrTimeout 在调用超时时返回。
ErrTimeout = errors.New("timeout")

// ErrConnectionClosed 会在当服务端在返回第一个相应字节前被关闭时,
// 于客户端方法中返回。
//
// 如果你看到了这个错误,你可以在服务端关闭连接前通过 `'Connection: close'` 相应头
// 来修复这个错误,或者在客户端发送请求前添加 `'Connection: close'` 请求头。
ErrConnectionClosed = errors.New("the server closed connection before returning the first response byte. " +
"Make sure the server returns 'Connection: close' response header before closing the connection")
)

var (
// CookieExpireDelete 可以会被支持于 `Cookie.Expire` 中,用于为指定
// cookie 添加过期。
CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)

// CookieExpireUnlimited 用于表明该 cookie 不会过期。
CookieExpireUnlimited = zeroTime
)

var (
// ErrPerIPConnLimit 会在任一 ip 连接数超过 Server.MaxConnsPerIP 时
// 由 ServeConn 返回。
ErrPerIPConnLimit = errors.New("too many connections per ip")

// ErrConcurrencyLimit 会在并发连接数超过 Server.Concurrency 时由
// ServeConn 返回。
ErrConcurrencyLimit = errors.New("canot serve the connection because Server.Concurrency concurrent connections are served")

// ErrKeepaliveTimeout 会在连接的时长超过 MaxKeepaliveDuration 时
// 由 ServeConn 返回。
ErrKeepaliveTimeout = errors.New("exceeded MaxKeepaliveDuration")
)

var ErrBodyTooLarge = errors.New("body size exceeds the given limit")

ErrBodyTooLarge 会在请求体或者响应体超过指定限制时返回。


var ErrDialTimeout = errors.New("dialing to the given TCP address timed out")

ErrDialTimeout 会在 TCP 握手超时时触发。


var ErrMissingFile = errors.New("there is no uploaded file associated with the given key")

ErrMissingFile 会在没有与指定的 multipart 表单键相关联的被上传文件时由 FormFile 返回。


var ErrNoArgValue = errors.New("no Args value for the given key")

ErrNoArgValue 会在指定 Args 键缺少值时返回。


var ErrNoMultipartForm = errors.New("request has no multipart/form-data Content-Type")

ErrNoMultipartForm 意味着请求的 Content-Type 不是 'multipart/form-data'


var ErrPipelineOverflow = errors.New("pipelined requests' queue has been overflown. Increase MaxConns and/or MaxPendingRequests")

ErrPipelineOverflow 会在请求的队列溢出时,由 PipelineClient.Do* 函数族返回。


func AppendBytesStr


func AppendBytesStr(dst []byte, src string) []byte

AppendBytesStrdst 追加 src ,并且返回追加后的 dst


这个函数与 append(dst, src...) 的性能没有差别。目前它仅用于向后兼容。


这个函数已经弃用并且可能很快被移除。


func AppendGunzipBytes


func AppendGunzipBytes(dst, src []byte) ([]byte, error)

AppendGunzipBytesdst 追加 gunzip 压缩后的 src ,并且返回追加后的 dst


func AppendGzipBytes


func AppendGzipBytes(dst, src []byte) []byte

AppendGzipBytesdst 追加 gzip 压缩后的 src ,并且返回追加后的 dst


func AppendGzipBytesLevel


func AppendGzipBytesLevel(dst, src []byte, level int) []byte

AppendGzipBytesLeveldst 追加指定级别的 gzip 压缩后的 src ,并且返回追加后的 dst


支持的压缩级别有:



  • CompressNoCompression

  • CompressBestSpeed

  • CompressBestCompression

  • CompressDefaultCompression


func AppendHTMLEscape


func AppendHTMLEscape(dst []byte, s string) []byte

AppendHTMLEscapedst 追加 HTML 转义后的 src ,并且返回追加后的 dst


func AppendHTMLEscapeBytes


func AppendHTMLEscapeBytes(dst, s []byte) []byte

AppendHTMLEscapeBytesdst 追加 HTML 转义后的 src ,并且返回追加后的 dst


func AppendHTTPDate


func AppendHTTPDate(dst []byte, date time.Time) []byte

AppendHTTPDatedst 追加符合 HTTP-compliant (RFC1123) 表示的时间 ,并且返回追加后的 dst


func AppendIPv4


func AppendIPv4(dst []byte, ip net.IP) []byte

AppendIPv4dst 追加表示 ip v4 的字符串 ,并且返回追加后的 dst


func AppendNormalizedHeaderKey


func AppendNormalizedHeaderKey(dst []byte, key string) []byte

AppendNormalizedHeaderKeydst 追加标准化后的 HTTP 头键(名),并且返回追加后的 dst


标准化后的头键由一个大写字母开头。在 - 后的第一个字母也为大写。其他的所有字母则都为小写。例子:



  • coNTENT-TYPe -> Content-Type

  • HOST -> Host

  • foo-bar-baz -> Foo-Bar-Baz


func AppendNormalizedHeaderKeyBytes


func AppendNormalizedHeaderKeyBytes(dst, key []byte) []byte

AppendNormalizedHeaderKeyBytesdst 追加标准化后的 HTTP 头键(名),并且返回追加后的 dst


标准化后的头键由一个大写字母开头。在 - 后的第一个字母也为大写。其他的所有字母则都为小写。例子:



  • coNTENT-TYPe -> Content-Type

  • HOST -> Host

  • foo-bar-baz -> Foo-Bar-Baz


func AppendQuotedArg


func AppendQuotedArg(dst, src []byte) []byte

AppendQuotedArgdst 追加经过 url 加密的 src ,并且返回追加后的 dst


func AppendUint


func AppendUint(dst []byte, n int) []byte

AppendUintdst 追加 n,并且返回追加后的 dst


func Dial


func Dial(addr string) (net.Conn, error)

Dial 使用 tcp4 连接指定的 TCP 地址 addr


net.Dial 相比,该函数有以下这些额外的特性:



  • 它通过以 DefaultDNSCacheDuration 持续时间缓存解析后的 TCP 地址来减少 DNS 解析器的负载。

  • 它通过轮询来连接所有被解析后的 TCP 连接,直至第一个连接被建立。这在当其中的某一个 TCP 地址临时性不可用时相当有用。

  • DefaultDialTimeout 秒之后若连接还没有被建立,它会返回 ErrDialTimeout ,可以使用 DialTimeout 来自定义这个超时。


addr 参数必须包含端口,例如:



  • foobar.baz:443

  • foo.bar:80

  • aaa.com:8080


func DialDualStack


func DialDualStack(addr string) (net.Conn, error)

DialDualStack 使用 tcp4 和 tcp6 连接指定的 TCP 地址 addr


net.Dial 相比,该函数有以下这些额外的特性:



  • 它通过以 DefaultDNSCacheDuration 持续时间缓存解析后的 TCP 地址来减少 DNS 解析器的负载。

  • 它通过轮询来连接所有被解析后的 TCP 连接,直至第一个连接被建立。这在当其中的某一个 TCP 地址临时性不可用时相当有用。

  • DefaultDialTimeout 秒之后若连接还没有被建立,它会返回 ErrDialTimeout ,可以使用 DialTimeout 来自定义这个超时。


addr 参数必须包含端口,例如:



  • foobar.baz:443

  • foo.bar:80

  • aaa.com:8080


func DialDualStackTimeout


func DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error)

DialDualStackTimeout 使用 tcp4 和 tcp6 连接指定的 TCP 地址 addr ,并且会在指定时间后超时。


net.Dial 相比,该函数有以下这些额外的特性:



  • 它通过以 DefaultDNSCacheDuration 持续时间缓存解析后的 TCP 地址来减少 DNS 解析器的负载。

  • 它通过轮询来连接所有被解析后的 TCP 连接,直至第一个连接被建立。这在当其中的某一个 TCP 地址临时性不可用时相当有用。

  • DefaultDialTimeout 秒之后若连接还没有被建立,它会返回 ErrDialTimeout ,可以使用 DialTimeout 来自定义这个超时。


addr 参数必须包含端口,例如:



  • foobar.baz:443

  • foo.bar:80

  • aaa.com:8080


func DialTimeout


func DialTimeout(addr string, timeout time.Duration) (net.Conn, error)

DialTimeout 使用 tcp4 和 tcp6 连接指定的 TCP 地址 addr ,并且会在指定时间后超时。


net.Dial 相比,该函数有以下这些额外的特性:



  • 它通过以 DefaultDNSCacheDuration 持续时间缓存解析后的 TCP 地址来减少 DNS 解析器的负载。

  • 它通过轮询来连接所有被解析后的 TCP 连接,直至第一个连接被建立。这在当其中的某一个 TCP 地址临时性不可用时相当有用。

  • DefaultDialTimeout 秒之后若连接还没有被建立,它会返回 ErrDialTimeout ,可以使用 DialTimeout 来自定义这个超时。


addr 参数必须包含端口,例如:



  • foobar.baz:443

  • foo.bar:80

  • aaa.com:8080


func Do


func Do(req *Request, resp *Response) error

Do 发出指定的 http 请求,在得到响应后并且填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func DoDeadline


func DoDeadline(req *Request, resp *Response, deadline time.Time) error

DoDeadline 发出指定的 http 请求,并且在指定的 deadline 之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func DoTimeout


func DoTimeout(req *Request, resp *Response, timeout time.Duration) error

DoTimeout 发出指定的 http 请求,并且在指定的超时之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func EqualBytesStr


func EqualBytesStr(b []byte, s string) bool

EqualBytesStr,在 string(b) == s 时返回 true


这个函数与 string(b) == s 的性能没有差别。目前它仅用于向后兼容。


这个函数已经弃用并且可能很快被移除。


func FileLastModified


func FileLastModified(path string) (time.Time, error)

FileLastModified 返回文件的最后修改时间。


func Get


func Get(dst []byte, url string) (statusCode int, body []byte, err error)

Getdst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


func GetDeadline


func GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error)

GetDeadlinedst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


若在指定的 deadline 之前没能获取到响应,那么会返回 ErrTimeout


func GetTimeout


func GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error)

GetTimeoutdst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


若在指定的超时之前没能获取到响应,那么会返回 ErrTimeout


func ListenAndServe


func ListenAndServe(addr string, handler RequestHandler) error

ListenAndServe 使用指定的 handler 处理来自指定 TCP 地址 addr 的 HTTP 请求。


例子:


// 这个服务器会监听所有来自该地址的请求
listenAddr := "127.0.0.1:80"

// 当每个请求到来时,这个函数都将被调用。
// RequestCtx 提供了很多有用的处理 http 请求的方法。更多详情请参阅 RequestCtx 说明。
requestHandler := func(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Hello, world! Requested path is %q", ctx.Path())
}

// 使用默认设置启动服务器。
// 创建服务器实例。
//
// ListenAndServe 只返回一个错误,所以它通常是永久阻塞的。
if err := fasthttp.ListenAndServe(listenAddr, requestHandler); err != nil {
log.Fatalf("error in ListenAndServe: %s", err)
}

func ListenAndServeTLS


func ListenAndServeTLS(addr, certFile, keyFile string, handler RequestHandler) error

ListenAndServeTLS 使用指定的 handler 处理来自指定 TCP 地址 addr 的 HTTPS 请求。


certFilekeyFile 是 TLS 证书和密钥文件的路径。


func ListenAndServeTLSEmbed


func ListenAndServeTLSEmbed(addr string, certData, keyData []byte, handler RequestHandler) error

ListenAndServeTLSEmbed 使用指定的 handler 处理来自指定 TCP 地址 addr 的 HTTPS 请求。


certDatakeyData 必须包含合法的 TLS 证书和密钥数据。


func ListenAndServeUNIX


func ListenAndServeUNIX(addr string, mode os.FileMode, handler RequestHandler) error

ListenAndServeUNIX 使用指定的 handler 处理来自指定 UNIX 地址 addr 的 HTTP 请求。


这个函数会在开始接受请求前删除所有 addr 下的文件。


该函数会为制定 UNIX 地址 addr 设置参数中指定的 mode


func NewStreamReader


func NewStreamReader(sw StreamWriter) io.ReadCloser

NewStreamReader 返回一个 reader ,用于获取所有由 sw 生成的数据。


返回的 reader 可以被传递至 Response.SetBodyStream


在返回的 reader 中所有的数据都被读取完毕之后,必须调用 Close 。否则可能会造成 goroutine 泄露。


更多详情可参阅 Response.SetBodyStreamWriter


func ParseByteRange


func ParseByteRange(byteRange []byte, contentLength int) (startPos, endPos int, err error)

ParseByteRange 用于解释 'Range: bytes=...' 头的值。


依据的规范是 https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35


func ParseHTTPDate


func ParseHTTPDate(date []byte) (time.Time, error)

ParseHTTPDate 用于解释符合 HTTP-compliant (RFC1123) 规范的时间。


func ParseIPv4


func ParseIPv4(dst net.IP, ipStr []byte) (net.IP, error)

ParseIPv4 解释 ipStr 提供的 ip 地址,并填充 dst ,然后返回填充后的 dst


func ParseUfloat


func ParseUfloat(buf []byte) (float64, error)

ParseUfloat 解释 buf 提供的无符号浮点数。


func ParseUint


func ParseUint(buf []byte) (int, error)

ParseUint 解释 buf 提供的无符号整型数。


func Post


func Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error)

Post 使用指定 POST 参数向指定 url 发出 POST 请求。


请求体会追加值 dst ,并且通过 body 返回。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


dstnil ,那么新的 body 缓冲会被分配。


如果 postArgsnil ,则发送空 POST 请求体。


func ReleaseArgs


func ReleaseArgs(a *Args)

ReleaseArgs 向池中释放通过 AquireArgs 取得的对象。


不要试图访问释放的 Args 对象,可能会产生数据竞争。


func ReleaseByteBuffer


func ReleaseByteBuffer(b *ByteBuffer)

ReleaseByteBuffer 返回池中释放指定字节缓冲。


在释放回池之后, ByteBuffer.B 不能再被访问,可能会产生数据竞争。


func ReleaseCookie


func ReleaseCookie(c *Cookie)

ReleaseCookie 向池中释放由 AcquireCookie 返回的对象。


不要试图访问释放的 Cookie 对象,可能会产生数据竞争。


func ReleaseRequest


func ReleaseRequest(req *Request)

ReleaseRequest 向池中释放由 AcquireRequest 返回的对象。


在释放回池之后,禁止再访问 req 对象以及它的任何成员。


func ReleaseResponse


func ReleaseResponse(resp *Response)

ReleaseResponse 向池中释放由 AcquireResponse 返回的对象。


在释放回池之后,禁止再访问 resp 对象以及它的任何成员。


func ReleaseURI


func ReleaseURI(u *URI)

ReleaseURI 向池中释放由 AcquireURI 返回的对象。


不要试图访问释放的 URI 对象,可能会产生数据竞争。


func SaveMultipartFile


func SaveMultipartFile(fh *multipart.FileHeader, path string) error

SaveMultipartFile 在指定的 path 下保存文件 fh


func Serve


func Serve(ln net.Listener, handler RequestHandler) error

Serve 使用指定的 handler 来处理来自 listener 的连接。


listener 返回永久性的错误之前, Serve 都会一直保持阻塞。


例子:


// 创建一个接受请求的 listener
//
// 你不仅可以创建 TCP listener - 任意的 net.Listener 都可以。
// 例如 UNIX Socket 或 TLS listener 。

ln, err := net.Listen("tcp4", "127.0.0.1:8080")
if err != nil {
log.Fatalf("error in net.Listen: %s", err)
}

// 当每个请求到来时,这个函数都将被调用。
// RequestCtx 提供了很多有用的处理 http 请求的方法。更多详情请参阅 RequestCtx 说明。
requestHandler := func(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Hello, world! Requested path is %q", ctx.Path())
}

// 使用默认设置启动服务器。
// 创建服务器实例。
//
// Serve 在 ln.Close() 或发生错误时返回,所以它通常是永久阻塞的。
if err := fasthttp.Serve(ln, requestHandler); err != nil {
log.Fatalf("error in Serve: %s", err)
}

func ServeConn


func ServeConn(c net.Conn, handler RequestHandler) error

ServeConn 使用指定的 handler 处理来自指定连接的 HTTP 请求。


如果所有来自 c 的请求都被成功处理,ServeConn 会返回 nil 。否则返回一个非空错误。


连接 c 必须立刻将所有数据通过 Write() 发送至客户端,否则请求的处理可能会被挂起。


ServeConn 在返回之前会关闭 c


func ServeFile


func ServeFile(ctx *RequestCtx, path string)

ServeFile 返回来自指定 path 的压缩后文件内容的 HTTP 响应。


在以下情况下,HTTP 响应可能会包含未压缩文件内容:



  • 缺少 'Accept-Encoding: gzip' 请求头。

  • 没有对文件目录的写权限。


如果 path 指向一个目录,那么目录的内容会被返回。


如果你不需要响应压缩后的文件内容,请使用 ServeFileUncompressed


更多详情可参阅 RequestCtx.SendFile


func ServeFileBytes


func ServeFileBytes(ctx *RequestCtx, path []byte)

ServeFileBytes 返回来自指定 path 的压缩后文件内容的 HTTP 响应。


在以下情况下,HTTP 响应可能会包含未压缩文件内容:



  • 缺少 'Accept-Encoding: gzip' 请求头。

  • 没有对文件目录的写权限。


如果 path 指向一个目录,那么目录的内容会被返回。


如果你不需要响应压缩后的文件内容,请使用 ServeFileUncompressed


更多详情可参阅 RequestCtx.SendFile


func ServeFileBytesUncompressed


func ServeFileBytesUncompressed(ctx *RequestCtx, path []byte)

ServeFileBytesUncompressed 返回来自指定 path 文件内容的 HTTP 响应。


如果 path 指向一个目录,那么目录的内容会被返回。


若需要处理压缩后的文件,请使用 ServeFileBytes


更多详情可参阅 RequestCtx.SendFileBytes


func ServeFileUncompressed


func ServeFileUncompressed(ctx *RequestCtx, path string)

ServeFileUncompressed 返回来自指定 path 文件内容的 HTTP 响应。


如果 path 指向一个目录,那么目录的内容会被返回。


若需要处理压缩后的文件,请使用 ServeFile


更多详情可参阅 RequestCtx.SendFile


func ServeTLS


func ServeTLS(ln net.Listener, certFile, keyFile string, handler RequestHandler) error

ServeTLS 使用指定的 handler 来处理来自指定 net.Listener 的 HTTPS 请求。


certFilekeyFile 是 TLS 证书和密钥文件的路径。


func ServeTLSEmbed


func ServeTLSEmbed(ln net.Listener, certData, keyData []byte, handler RequestHandler) error

ServeTLSEmbed 使用指定的 handler 来处理来自指定 net.Listener 的 HTTPS 请求。


certDatakeyData 必须包含合法的 TLS 证书和密钥数据。


func StatusMessage


func StatusMessage(statusCode int) string

StatusMessage 根据指定的状态码返回 HTTP 状态信息。


func WriteGunzip


func WriteGunzip(w io.Writer, p []byte) (int, error)

WriteGunzipw 写入经 gunzip 压缩的 p ,并且返回未压缩的字节数。


func WriteGzip


func WriteGzip(w io.Writer, p []byte) (int, error)

WriteGunzipw 写入经 gzip 压缩的 p ,并且返回未压缩的字节数。


func WriteGzipLevel


func WriteGzipLevel(w io.Writer, p []byte, level int) (int, error)

WriteGunzipw 写入经指定级别 gzip 压缩的 p ,并且返回未压缩的字节数。


支持的压缩级别有:



  • CompressNoCompression

  • CompressBestSpeed

  • CompressBestCompression

  • CompressDefaultCompression


func WriteInflate


func WriteInflate(w io.Writer, p []byte) (int, error)

WriteGunzipw 写入压缩后的 p ,并且返回未压缩的字节数。


func WriteMultipartForm


func WriteMultipartForm(w io.Writer, f *multipart.Form, boundary string) error

WriteMultipartForm 使用指定的 w 写入指定的表单 f


type Args


type Args struct {
// 包含被过滤或未导出的属性
}

Args 代表查询字符串参数。


拷贝 Args 实例是禁止的。你需要使用 CopyTo() 函数或创建一个新实例。


Args 实例必须不能在并发执行的 goroutine 间使用。


func AcquireArgs


func AcquireArgs() *Args

AcquireArgs 从池中返回一个空的 Args 对象。


返回的 Args 实例在不再需要时可以通过 ReleaseArgs 释放回池。这可以降低垃圾回收负载。


func (*Args) Add


func (a *Args) Add(key, value string)

Add 添加 'key=value' 参数。


同一个 key 可以添加多个值。


func (*Args) AddBytesK


func (a *Args) AddBytesK(key []byte, value string)

AddBytesK 添加 'key=value' 参数。


同一个 key 可以添加多个值。


func (*Args) AddBytesKV


func (a *Args) AddBytesKV(key, value []byte)

AddBytesKV 添加 'key=value' 参数。


同一个 key 可以添加多个值。


func (*Args) AddBytesV


func (a *Args) AddBytesV(key string, value []byte)

AddBytesV 添加 'key=value' 参数。


同一个 key 可以添加多个值。


func (*Args) AppendBytes


func (a *Args) AppendBytes(dst []byte) []byte

AppendBytesdst 追加查询字符串,并返回 dst


func (*Args) CopyTo


func (a *Args) CopyTo(dst *Args)

CopyTo 将所有的参数复制至 dst


func (*Args) Del


func (a *Args) Del(key string)

Del 删除键为指定 key 的参数。


func (*Args) DelBytes


func (a *Args) DelBytes(key []byte)

Del 删除键为指定 key 的参数。


func (*Args) GetUfloat


func (a *Args) GetUfloat(key string) (float64, error)

GetUfloat 返回指定 key 的无符号浮点数值。


func (*Args) GetUfloatOrZero


func (a *Args) GetUfloatOrZero(key string) float64

GetUfloatOrZero 返回指定 key 的无符号浮点数值。


当出错时返回 0


func (*Args) GetUint


func (a *Args) GetUint(key string) (int, error)

GetUint 返回指定 key 的无符号整型数值。


func (*Args) GetUintOrZero


func (a *Args) GetUintOrZero(key string) int

GetUintOrZero 返回指定 key 的无符号整型数值。


当出错时返回 0


func (*Args) Has


func (a *Args) Has(key string) bool

Has 在当 Args 中存在指定 key 时返回 true


func (*Args) HasBytes


func (a *Args) HasBytes(key []byte) bool

HasBytes 在当 Args 中存在指定 key 时返回 true


func (*Args) Len


func (a *Args) Len() int

Len 查询参数的数量。


func (*Args) Parse


func (a *Args) Parse(s string)

Parse 解析包含查询参数的字符串。


func (*Args) ParseBytes


func (a *Args) ParseBytes(b []byte)

ParseBytes 解析包含查询参数的 b


func (*Args) Peek


func (a *Args) Peek(key string) []byte

Peek 返回查询参数中指定 key 的值。


func (*Args) PeekBytes


func (a *Args) PeekBytes(key []byte) []byte

PeekBytes 返回查询参数中指定 key 的值。


func (*Args) PeekMulti


func (a *Args) PeekMulti(key string) [][]byte

PeekMulti 返回查询参数中指定 key 的所有值。


func (*Args) PeekMultiBytes


func (a *Args) PeekMultiBytes(key []byte) [][]byte

PeekMultiBytes 返回查询参数中指定 key 的所有值。


func (*Args) QueryString


func (a *Args) QueryString() []byte

QueryString 返回查询参数的字符串表示。


在下个 Args 方法调用之前,返回值都是合法的。


func (*Args) Reset


func (a *Args) Reset()

Reset 清除所有查询参数。


func (*Args) Set


func (a *Args) Set(key, value string)

Set 设置 'key=value' 参数。


func (*Args) SetBytesK


func (a *Args) SetBytesK(key []byte, value string)

SetBytesK 设置 'key=value' 参数。


func (*Args) SetBytesKV


func (a *Args) SetBytesKV(key, value []byte)

SetBytesKV 设置 'key=value' 参数。


func (*Args) SetBytesV


func (a *Args) SetBytesV(key string, value []byte)

SetBytesV 设置 'key=value' 参数。


func (*Args) SetUint


func (a *Args) SetUint(key string, value int)

SetUint 为指定 key 设置无符号整数值。


func (*Args) SetUintBytes


func (a *Args) SetUintBytes(key []byte, value int)

SetUintBytes 为指定 key 设置无符号整数值。


func (*Args) String


func (a *Args) String() string

String 返回查询参数的字符串表示。


func (*Args) VisitAll


func (a *Args) VisitAll(f func(key, value []byte))

VisitAll 对每一个存在的参数调用 f


f 在返回后必须不能保留对键和值的引用。若要在返回后扔需要存储它们,请存储它们的副本。


func (*Args) WriteTo


func (a *Args) WriteTo(w io.Writer) (int64, error)

WriteTow 写入查询字符串。


WriteTo 实现了 io.WriterTo 接口。


type Client


type Client struct {

// 客户端名字。在 User-Agent 请求头中会被使用到。
//
// 如果未被设置,则会使用默认客户端名。
Name string

// 建立到指定 host 的新连接后的回调函数。
//
// 如果未被设置,则会使用默认 Dial 函数。
Dial DialFunc

// 若被设为 true ,则会试图连接 ipv4 和 ipv6 的地址。
//
// 这个选项仅在使用默认 TCP dialer 时有效,
// 例如:Dial 为空。
//
// 默认情况下客户端仅会连接 ipv4 地址,
// 因为 ipv6 在世界上的大多数网络中都仍然不可用 :)
DialDualStack bool

// HTTPS 连接的 TLS 配置。
// 如果未被设置,则使用默认的 TLS 配置。
TLSConfig *tls.Config

// 每个 host 可以被建立的最大连接数。
//
// 如果未被设置,则使用默认的 DefaultMaxConnsPerHost 。
MaxConnsPerHost int

// 在这个时间间隔后,空闲的 keep-alive 连接会被关闭。
// 默认值为 DefaultMaxIdleConnDuration 。
MaxIdleConnDuration time.Duration

// 每个连接响应读取时的缓冲大小。
// 这个值也限制了最大头大小。
//
// 默认值为 0 。
ReadBufferSize int

// 每个连接请求写入时的缓冲大小。
//
// 默认值为 0 。
WriteBufferSize int

// 完整的响应读取(包含响应体)可用的最大时间。
//
// 默认为无限制。
ReadTimeout time.Duration

// 完整的请求写入(包含请求体)可用的最大时间。
//
// 默认为无限制。
WriteTimeout time.Duration

// 相应体的最大大小。
//
// 当该值大于 0 ,且相应体超过它时,客户端返回 ErrBodyTooLarge 。
// 默认为无限制。
MaxResponseBodySize int

DisableHeaderNamesNormalizing bool

// 包含被过滤或未导出的属性
}

Client 实现了 HTTP 客户端。


不允许按值拷贝 Client ,应该创建一个新的实例。


在多个运行的 goroutine 间调用 Client 方法是安全的。


func (*Client) Do


func (c *Client) Do(req *Request, resp *Response) error

Do 发出指定的 http 请求,在得到响应后并且填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


如果 respnil ,那么响应会被忽略。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*Client) DoDeadline


func (c *Client) DoDeadline(req *Request, resp *Response, deadline time.Time) error

DoDeadline 发出指定的 http 请求,并且在指定的 deadline 之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*Client) DoTimeout


func (c *Client) DoTimeout(req *Request, resp *Response, timeout time.Duration) error

DoTimeout 发出指定的 http 请求,并且在指定的超时之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*Client) Get


func (c *Client) Get(dst []byte, url string) (statusCode int, body []byte, err error)

Getdst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


func (*Client) GetDeadline


func (c *Client) GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error)

GetDeadlinedst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


若在指定的 deadline 之前没能获取到响应,那么会返回 ErrTimeout


func (*Client) GetTimeout


func (c *Client) GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error)

GetTimeoutdst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


若在指定的超时之前没能获取到响应,那么会返回 ErrTimeout


func (*Client) Post


func (c *Client) Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error)

Post 使用指定 POST 参数向指定 url 发出 POST 请求。


请求体会追加值 dst ,并且通过 body 返回。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


dstnil ,那么新的 body 缓冲会被分配。


如果 postArgsnil ,则发送空 POST 请求体。


type Cookie


type Cookie struct {
// 包含被过滤或未导出的属性
}

Cookie 代表 HTTP 相应的 cookie 。


不允许按值拷贝 Cookie ,应该创建一个新的实例。


在多个运行的 goroutine 间使用 Cookie 实例是禁止的。


func AcquireCookie


func AcquireCookie() *Cookie

AcquireCookie 从池中返回一个空的 Cookie 对象。


返回的 Cookie 实例在不再需要时可以通过 ReleaseCookie 释放回池。这可以降低垃圾回收负载。


func (*Cookie) AppendBytes


func (c *Cookie) AppendBytes(dst []byte) []byte

AppendBytesdst 追加 cookie ,并且返回追加后的 dst


func (*Cookie) Cookie


func (c *Cookie) Cookie() []byte

Cookie 返回 cookie 的表示。


直到下次调用 Cookie 方法前,返回值都是合法的。


func (*Cookie) CopyTo


func (c *Cookie) CopyTo(src *Cookie)

CopyTo 拷贝 src cookie 至 c


func (*Cookie) Domain


func (c *Cookie) Domain() []byte

Domain 返回 cookie 的 domain 值。


直到下次调用会改变 Cookie 的方法前,返回值都是合法的。


func (*Cookie) Expire


func (c *Cookie) Expire() time.Time

Expire 返回 cookie 的过期时间。


若没设置过期,则返回 CookieExpireUnlimited


func (*Cookie) HTTPOnly


func (c *Cookie) HTTPOnly() bool

HTTPOnly 在 cookie 为 http only 时返回 true


func (*Cookie) Key


func (c *Cookie) Key() []byte

Key 返回 cookie 名字。


直到下次调用会改变 Cookie 的方法前,返回值都是合法的。


func (*Cookie) Parse


func (c *Cookie) Parse(src string) error

Parse 解析 Set-Cookie 头。


func (*Cookie) ParseBytes


func (c *Cookie) ParseBytes(src []byte) error

ParseBytes 解析 Set-Cookie 头。


func (*Cookie) Path


func (c *Cookie) Path() []byte

Path 返回 cookie path 。


func (*Cookie) Reset


func (c *Cookie) Reset()

Reset 清空该 cookie 。


func (*Cookie) Secure


func (c *Cookie) Secure() bool

Secure 在当 cookie 为 secure 时返回 true


func (*Cookie) SetDomain


func (c *Cookie) SetDomain(domain string)

SetDomain 设置 cookie 的 domain 。


func (*Cookie) SetDomainBytes


func (c *Cookie) SetDomainBytes(domain []byte)

SetDomainBytes 设置 cookie 的 domain 。


func (*Cookie) SetExpire


func (c *Cookie) SetExpire(expire time.Time)

SetExpire 设置 cookie 的过期时间。


若要使该 cookie 在客户端过期,则将值设置为 CookieExpireDelete


默认情况下 cookie 的寿命由浏览器会话限制。


func (*Cookie) SetHTTPOnly


func (c *Cookie) SetHTTPOnly(httpOnly bool)

SetHTTPOnly 将 cookie 的 httpOnly 标识设置为指定值。


func (*Cookie) SetKey


func (c *Cookie) SetKey(key string)

SetKey 设置 cookie 名。


func (*Cookie) SetKeyBytes


func (c *Cookie) SetKeyBytes(key []byte)

SetKeyBytes 设置 cookie 名。


func (*Cookie) SetPath


func (c *Cookie) SetPath(path string)

SetPath 设置 cookie 路径。


func (*Cookie) SetPathBytes


func (c *Cookie) SetPathBytes(path []byte)

SetPathBytes 设置 cookie 路径。


func (*Cookie) SetSecure


func (c *Cookie) SetSecure(secure bool)

SetSecure 将 cookie 的 secure 标识设置为指定值。


func (*Cookie) SetValue


func (c *Cookie) SetValue(value string)

SetValue 设置 cookie 的值。


func (*Cookie) SetValueBytes


func (c *Cookie) SetValueBytes(value []byte)

SetValueBytes 设置 cookie 的值。


func (*Cookie) String


func (c *Cookie) String() string

String 返回 cookie 的字符串表示。


func (*Cookie) Value


func (c *Cookie) Value() []byte

Value 返回 cookie 的值。


直到下次调用会改变 Cookie 的方法前,返回值都是合法的。


func (*Cookie) WriteTo


func (c *Cookie) WriteTo(w io.Writer) (int64, error)

WriteTo 将 cookie 的字符串表示写入 w


WriteTo 实现了 io.WriterTo 接口。


type DialFunc


type DialFunc func(addr string) (net.Conn, error)

DialFunc 必须建立到 addr 的连接。


没有必要为 HTTPS 建立到 TLS(SSL)的连接。若 HostClient.IsTLS 被设置,则客户端会自动转换连接至 TLS 。


TCP address passed to DialFunc always contains host and port. Example TCP addr values:
传递至 DialFunc 的 TCP 地址总是包含 host 和端口。例子:



  • foobar.com:80

  • foobar.com:443

  • foobar.com:8080


type FS


type FS struct {

// 用于响应文件的根目录
Root string

// 目录中的索引文件名。
//
// 例子:
//
// * index.html
// * index.htm
// * my-super-index.xml
//
// 默认为空。
IndexNames []string

GenerateIndexPages bool

// 若设为 true ,则压缩响应。
//
// 服务器会通过缓存来最小化 CPU 的使用。
// 新的缓存文件名字会添加 `CompressedFileSuffix` 前缀。
// 所以建议使服务器对 Root 目录以及子目录有写权限。
Compress bool

// 若被设为 true ,则启用字节范围请求
//
// 默认为 false 。
AcceptByteRange bool

// 重写路径函数。
//
// 默认为不改变请求路径。
PathRewrite PathRewriteFunc

// 非活跃的文件句柄的过期时间间隔。
//
// 默认为 `FSHandlerCacheDuration` 。
CacheDuration time.Duration

// 为缓存的压缩文件添加的前缀。
//
// 这个值仅在 Compress 被设置时才有效。
//
// 默认为 FSCompressedFileSuffix 。
CompressedFileSuffix string

// 包含被过滤或未导出的属性
}

FS 代表了通过本地文件系统来响应静态文件 HTTP 请求的设置。


不允许复制 FS 值,应该创建新的 FS 值。


例子:


fs := &fasthttp.FS{
// 响应静态文件请求的目录
Root: "/var/www/static-site",

// 生成索引
GenerateIndexPages: true,

// 开启压缩,用于节省带宽
Compress: true,
}

// 创建响应静态文件的 handler
h := fs.NewRequestHandler()

// 启动服务器
if err := fasthttp.ListenAndServe(":8080", h); err != nil {
log.Fatalf("error in ListenAndServe: %s", err)
}

func (*FS) NewRequestHandler


func (fs *FS) NewRequestHandler() RequestHandler

NewRequestHandler 通过指定的 FS 设置返回新的请求 handler 。


返回的 handler 根据 FS.CacheDuration 来缓存请求的文件句柄。若 FS.Root 目录包含大量文件,请确保你的程序通过 'ulimit -n' 来保证有足够的“可打开文件”。


不需要对单个 FS 实例创建多个请求 handler ,只需重用即可。


type HijackHandler


type HijackHandler func(c net.Conn)

HijackHandler 必须处理拦截的连接 c


HijackHandler 返回后连接 c 会被自动关闭。


HijackHandler 返回后连接 c 必须不可再被使用。


type HostClient


type HostClient struct {

// 以逗号分隔的上游 HTTP 服务器 host 地址列表,通过轮询传递给 Dial
//
// 如果默认的 dialer 被使用,每一个地址都需要包含端口。
// 例子:
//
// - foobar.com:80
// - foobar.com:443
// - foobar.com:8080
Addr string

// 客户端名,用于 User-Agent 请求头。
Name string

// 建立到指定 host 的新连接后的回调函数。
//
// 如果未被设置,则会使用默认 Dial 函数。
Dial DialFunc

// 若被设为 true ,则会试图连接 ipv4 和 ipv6 的地址。
//
// 这个选项仅在使用默认 TCP dialer 时有效,
// 例如:Dial 为空。
//
// 默认情况下客户端仅会连接 ipv4 地址,
// 因为 ipv6 在世界上的大多数网络中都仍然不可用 :)
DialDualStack bool

// 是否使用 TLS 。
IsTLS bool

// 可选的 TLS 配置。
TLSConfig *tls.Config

// 每个 host 可以被建立的最大连接数。
//
// 如果未被设置,则使用默认的 DefaultMaxConnsPerHost 。
MaxConns int

// 在这个时间间隔后, keep-alive 连接会被关闭。
// 默认值为无限制。
MaxConnDuration time.Duration

// 在这个时间间隔后,空闲的 keep-alive 连接会被关闭。
// 默认值为 DefaultMaxIdleConnDuration 。
MaxIdleConnDuration time.Duration

// 每个连接响应读取时的缓冲大小。
// 这个值也限制了最大头大小。
//
// 默认值为 0 。
ReadBufferSize int

// 每个连接请求写入时的缓冲大小。
//
// 默认值为 0 。
WriteBufferSize int

// 完整的响应读取(包含响应体)可用的最大时间。
//
// 默认为无限制。
ReadTimeout time.Duration

// 完整的请求写入(包含请求体)可用的最大时间。
//
// 默认为无限制。
WriteTimeout time.Duration

// 相应体的最大大小。
//
// 当该值大于 0 ,且相应体超过它时,客户端返回 ErrBodyTooLarge 。
// 默认为无限制。
MaxResponseBodySize int

DisableHeaderNamesNormalizing bool

// 包含被过滤或未导出的属性
}

HostClient 均衡地向列于 Addr 中的 host 发起请求。


禁止拷贝 HostClient 实例。应使用创建新的实例。


在多个运行的 goroutine 间执行 HostClient 方法是安全的。


例子:


package main

import (
"log"

"github.com/valyala/fasthttp"
)

func main() {
// 准备一个客户端,用于通过监听于 localhost:8080 的 HTTP 代理获取网页
c := &fasthttp.HostClient{
Addr: "localhost:8080",
}

// 使用本地代理获取谷歌页面。
statusCode, body, err := c.Get(nil, "http://google.com/foo/bar")
if err != nil {
log.Fatalf("Error when loading google page through local proxy: %s", err)
}
if statusCode != fasthttp.StatusOK {
log.Fatalf("Unexpected status code: %d. Expecting %d", statusCode, fasthttp.StatusOK)
}
useResponseBody(body)

// 通过本地代理获取 foobar 页面。重用 body 缓冲。
statusCode, body, err = c.Get(body, "http://foobar.com/google/com")
if err != nil {
log.Fatalf("Error when loading foobar page through local proxy: %s", err)
}
if statusCode != fasthttp.StatusOK {
log.Fatalf("Unexpected status code: %d. Expecting %d", statusCode, fasthttp.StatusOK)
}
useResponseBody(body)
}

func useResponseBody(body []byte) {
// 处理 body
}

func (*HostClient) Do


func (c *HostClient) Do(req *Request, resp *Response) error

Do 发出指定的 http 请求,在得到响应后并且填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*HostClient) DoDeadline


func (c *HostClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error

DoDeadline 发出指定的 http 请求,并且在指定的 deadline 之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*HostClient) DoTimeout


func (c *HostClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error

DoTimeout 发出指定的 http 请求,并且在指定的超时之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*HostClient) Get


func (c *HostClient) Get(dst []byte, url string) (statusCode int, body []byte, err error)

Getdst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


func (*HostClient) GetDeadline


func (c *HostClient) GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error)

GetDeadlinedst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


若在指定的 deadline 之前没能获取到响应,那么会返回 ErrTimeout


func (*HostClient) GetTimeout


func (c *HostClient) GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error)

GetTimeoutdst 追加 url 信息,并且通过 body 返回它。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


如果 dstnil ,那么则会分配一个新的 body 缓冲。


若在指定的超时之前没能获取到响应,那么会返回 ErrTimeout


func (*HostClient) LastUseTime


func (c *HostClient) LastUseTime() time.Time

LastUseTime 返回客户端最后被使用的时间。


func (*HostClient) PendingRequests


func (c *HostClient) PendingRequests() int

PendingRequests 返回正在执行的请求数。


func (*HostClient) Post


func (c *HostClient) Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error)

Post 使用指定 POST 参数向指定 url 发出 POST 请求。


请求体会追加值 dst ,并且通过 body 返回。


这个函数会跟随重定向。若要手动操作重定向,请使用 Do*


dstnil ,那么新的 body 缓冲会被分配。


如果 postArgsnil ,则发送空 POST 请求体。


type Logger


type Logger interface {
// Printf 必须与 log.Printf 有相同的语义。
Printf(format string, args ...interface{})
}

Logger 被用于记录格式化信息日志。


type PathRewriteFunc


type PathRewriteFunc func(ctx *RequestCtx) []byte

PathRewriteFunc 必须返回基于 ctx.Path() 的新请求路径。


该函数用于在 FS 中转义当前请求路径至相对于 FS.Root 的相对路径。


处于安全原因,返回的路径中不允许包含 '/../' 子字符串。


func NewPathPrefixStripper


func NewPathPrefixStripper(prefixSize int) PathRewriteFunc

NewPathPrefixStripper 返回重写路径函数,返回移除的前缀大小。


例子:



  • prefixSize = 0, 原路径: "/foo/bar", 结果: "/foo/bar"

  • prefixSize = 3, 原路径: "/foo/bar", 结果: "o/bar"

  • prefixSize = 7, 原路径: "/foo/bar", 结果: "r"


返回的路径重写函数可能会被 FS.PathRewrite 使用。


func NewPathSlashesStripper


func NewPathSlashesStripper(slashesCount int) PathRewriteFunc

NewPathSlashesStripper 返回重写路径函数,返回移除的路径分隔符数量。


例子:



  • slashesCount = 0, 原路径: "/foo/bar", 结果: "/foo/bar"

  • slashesCount = 1, 原路径: "/foo/bar", 结果: "/bar"

  • slashesCount = 2, 原路径: "/foo/bar", 结果: ""


返回的路径重写函数可能会被 FS.PathRewrite 使用。


type PipelineClient


type PipelineClient struct {

// 连接的 host 的地址
Addr string

// 连接至 Addr 的最大并发数。
//
// 默认为单连接。
MaxConns int

// 单个连接至 Addr 的最大等待管道请求数量。
//
// 默认为 DefaultMaxPendingRequests 。
MaxPendingRequests int

// 在批量发送管道请求至服务器前的最大延时。
//
// 默认为无延时。
MaxBatchDelay time.Duration

/// 建立到指定 host 的新连接后的回调函数。
//
// 如果未被设置,则会使用默认 Dial 函数。
Dial DialFunc

// 若被设为 true ,则会试图连接 ipv4 和 ipv6 的地址。
//
// 这个选项仅在使用默认 TCP dialer 时有效,
// 例如:Dial 为空。
//
// 默认情况下客户端仅会连接 ipv4 地址,
// 因为 ipv6 在世界上的大多数网络中都仍然不可用 :)
DialDualStack bool

// 是否使用 TLS 。
IsTLS bool

// 可选的 TLS 配置。
TLSConfig *tls.Config

// 在这个时间间隔后,空闲的 keep-alive 连接会被关闭。
// 默认值为 DefaultMaxIdleConnDuration 。
MaxIdleConnDuration time.Duration

// 每个连接响应读取时的缓冲大小。
// 这个值也限制了最大头大小。
//
// 默认值为 0 。
ReadBufferSize int

// 每个连接请求写入时的缓冲大小。
//
// 默认值为 0 。
WriteBufferSize int

// 完整的响应读取(包含响应体)可用的最大时间。
//
// 默认为无限制。
ReadTimeout time.Duration

// 完整的请求写入(包含请求体)可用的最大时间。
//
// 默认为无限制。
WriteTimeout time.Duration

// 用于记录客户端错误的日志记录器。
//
// 默认为标准 log 库。
Logger Logger

// 包含被过滤或未导出的属性
}

PipelineClient 通过一个指定的并发连接限制数,来发送请求。


这个客户端可能被用于高负载的 RPC 系统。更多详情参阅 https://en.wikipedia.org/wiki/HTTP_pipelining


禁止拷贝 PipelineClient 实例。应该创建新实例。


在运行的 goroutine 间调用 PipelineClient 方法是安全的。


func (*PipelineClient) Do


func (c *PipelineClient) Do(req *Request, resp *Response) error

Do 发出指定的 http 请求,在得到响应后并且填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*PipelineClient) DoDeadline


func (c *PipelineClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error

DoDeadline 发出指定的 http 请求,并且在指定的 deadline 之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*PipelineClient) DoTimeout


func (c *PipelineClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error

DoTimeout 发出指定的 http 请求,并且在指定的超时之前得到响应后填充指定的 http 响应对象。


请求必须至少包含一个非空的 RequestURI (包含协议和 host)或非空的 Host 头 + RequestURI。


客户端以以下顺序确定待请求的服务端:



  • 如果 RequestURI 包含完整的带有协议和 host 的 url ,则从 RequestURI 中取得。

  • 否则就从 Host 头中取得。


这个函数不会跟随重定向。若要跟随重定向,请使用 Get*


如果 respnil ,那么响应会被忽略。


如果向指定请求 host 的所有 DefaultMaxConnsPerHost 数量的连接都被占用,那么会返回 ErrNoFreeConns


在有性能要求的代码中,推荐通过 AcquireRequestAcquireResponse 来获取 reqresp


func (*PipelineClient) PendingRequests


func (c *PipelineClient) PendingRequests() int

PendingRequests 返回正在执行的请求数。


type Request


type Request struct {

// 请求头
//
// 按值拷贝 Header 是禁止的。应使用指针。
Header RequestHeader

// 包含被过滤或未导出的属性
}

Request 代表一个 HTTP 请求。


禁止拷贝 Request 实例。应该创建新实例或使用 CopyTo


Request 实例必须不能再多个运行的 goroutine 间使用。


func AcquireRequest


func AcquireRequest() *Request

AcquireRequest 从请求池中返回一个空的 Request 实例。


返回的 Request 实例在不再需要时可以通过 ReleaseRequest 释放回池。这可以降低垃圾回收负载。


func (*Request) AppendBody


func (req *Request) AppendBody(p []byte)

AppendBody 追加 p 至请求体。


在函数返回后重用 p 是安全的。


func (*Request) AppendBodyString

Golang In PingCAP

文章分享qiuyesuifeng 发表了文章 • 3 个评论 • 848 次浏览 • 2016-10-11 17:15 • 来自相关话题

随着 Golang 在后端领域越来越流行,有越来越多的公司选择 Golang 作为主力开发语言。本次 GopherChina Beijing 2016 大会上,看到 Golang 在各家公司从人工智能到自动运维,从 Web 应用到基础架构都发挥着越来越多... 查看全部

随着 Golang 在后端领域越来越流行,有越来越多的公司选择 Golang 作为主力开发语言。本次 GopherChina Beijing 2016 大会上,看到 Golang 在各家公司从人工智能到自动运维,从 Web 应用到基础架构都发挥着越来越多的作用。可以说 Golang 在这几年间,获得了长足的进步。
PingCAP 是一家由几名 Golang 粉丝创建的数据库公司。在我们的日常工作中,除了对性能有苛刻要求的最底层存储引擎外,大部分都是使用 Golang,算是 Golang 的重度用户。我们从 Golang 语言以及社区中收益颇多,TiDB 在短短半年的时间内,从无到有,从默默无闻到广泛关注,已经成长为 Golang 社区的明星项目。我们在这个过程中也积累了不少工程实践经验,这里想和大家分享一下。


Why Golang?


网上已经有无数的文章描述 Golang 的优点,所以没有必要一一列举。我们选择 Golang 并不是因为跟风或者是我们是 Golang 的粉丝,而是经过理性的分析和讨论,认为 Golang 最适合我们的业务场景。


开发效率高


作为技术创业公司,我们期望维护一个精英技术团队,人数不多,但是交付速度快、代码质量高。这样我们需要一门高效的语言,Golang 在这方面令我们非常满意。Golang 易于上手,有过其他语言经验的人,很容易转到 Golang。超强的表达能力、完备的标准库以及大量成熟的第三方库,使得我们可以专心于核心业务。自动内存管理,避免了 c/c++ 中的指针乱飞的情况,易于写出正确的程序。从15年6月写下第一行代码开始,到15年9月我们已经完成第一版的产品,并且达到可开源的要求。开源后我们从社区中获得了不少有价值的反馈以及大量的第三方 Contributor。从 GopherChina 大会上,我们注意到除了大公司处理海量并发时会采用 Golang 外,越来越多的创业型公司也在使用 Golang,我想这和 Golang 的易于上手、开发效率高有很大关系。


并发友好


对于一个分布式数据库,相比较延迟而言吞吐量是一个更关键指标。当然这里并不是说延迟可以无限大,而是在保证延迟相对较低的情况下,尽可能的提高吞吐。TiDB 的设计目标是能响应海量的用户请求,我们期望有一种低成本的方式同时处理多个用户连接。同时数据库内部的一些逻辑也要求在处理用户请求的同时,还有大量的后台线程在做自己的工作。
Golang 在这方面有天然的优势,甚至可以说 Golang 就是一门为了并发而生语言。goroutine 和 channel 使得编写并发的程序变得相当容易且自然,很多情况下完全不需要考虑锁机制以及由此带来的各种问题。单个 Go 应用也能有效的利用多个 CPU 核,并行执行的性能好。与此同时,Golang 运行的性能虽然不如 C/C++,但是还没有数量级的差别,可以满足对延迟的要求。


部署简单


我们把系统部署简单易用作为 TiDB 的一个重要的设计目标。我想部署和维护过其他分布式系统(比如 Hbase)的同学,对这一点一定深有感触。
Golang 编译生成的是一个静态链接的可执行文件,除了 glibc 外没有其他外部依赖。这让部署变得很方便。目标机器上只需要一个基础的系统和必要的管理、监控工具,完全不需要操心应用所需的各种包、库的依赖关系,大大减轻了维护的负担。


Good Practice


在使用 Golang 的过程中,我们也获得了一些很好的实践经验,包括语言使用上的,以及工程上的经验。


重视单元测试


Golang 带有一个简单好用的单元测试框架,包括功能测试和性能测试。每个模块都能以非常简单的方式进行测试,以验证功能的正确性,并且避免后续被别人改错。在做 Code Review 时,我们强制要求所有的改动必须有 test case,否则 PR 会被拒绝。对于性能关键的模块,我们会加上 bench test,每次改动后会观察性能的变化。


重视 CI


数据库是一个复杂的系统,单靠单元测试无法保证系统的正确性,我们需要大量的集成测试。受益于 MySQL 的生态,我们可以获得大量可以直接用的测试资源,包括各种 ORM 自带的测试、MySQL 自带的测试、各种 MySQL 应用的测试。
TiDB 除了在提交 PR 时会做最基本的测试之外,还有十几个集成测试随时待命。我们在内部搭建了 jenkins 系统,每次代码有变动,都会自动构建这十几个测试集。如果有任何一个 Fail 了,相关人员必须停下手中的工作,马上去 Fix。另外 jenkins 也可以作为性能监测工具,每次提交后都会记录下运行时间,可以和历史记录中的时间作比较,如果运行时间突然变长,需要立即解决。


重视代码质量


代码是技术型公司最重要的产品,而且我们又是一家以开源方式运作的技术公司,代码的质量相当于公司的招牌,我们在这方面花了很大的力气。
我们制定了严格的 Code Review 制度。任何 PR 都需要有至少两个maintainer 看过,并且认为改动 OK,给出 LGTM 后,才能合并进主干。这两个做 review 工作的人要保证看过、理解每一行代码,并且要到能独立修改。否则提交 PR 的人需要给 reviewer 进行详细的介绍,直到讲懂为止。
另外我们还利用一些第三方工具来检测代码质量。比如 GoReportCard,这个工具会分析代码中的潜在问题,如赋值过的变量在作用域内没有被使用、函数过长、switch 分支过长、typo。项目在这里面的排名在一定程度上反映了代码的质量。目前 TiDB 的代码质量被评为A+级别。


一切自动化


Go 自带完善的工具链,大大提高了团队协作的一致性。比如 gofmt 自动排版 Go 代码,很大程度上杜绝了不同人写的代码排版风格不一致的问题。把编辑器配置成在编辑存档的时候自动运行 gofmt,这样在编写代码的时候可以随意摆放位置,存档的时候自动变成正确排版的代码。此外还有 golint, govet 等非常有用的工具。TiDB 将 golint、govet 的检查加入 Makefile,每次构建时,都会自动测试,这样可以防止一些低级的错误被提交。


善于利用 Pprof


在系统性能调优或者是死锁监测方面,一个 Inspector 机制能极大的提高效率。幸运的是 Golang 自带 profile 工具,简单的几行代码就能方便地提供一个 HTTP 界面,展现当前系统的所有状态。目前在开发过程中,我们会默认打开 pprof,这个机制也不止一次地帮助我们发现系统中的问题。


那些年我们踩过的坑


Golang 是一门很好的语言,但并不是一门完美无缺的语言,我们在实践中也踩过不少坑。


interface{} 的性能问题


数据库中有大量的数据类型,所以我们需要一个统一的结构来处理所有的类型。我们最初的方案是选择 interface{},这也是 Golang 中比较自由的选择。所有的数据类型都可以赋值给 interface{},所有的数据类型相关的函数也都以 interface{} 作为参数,然后在内部用 switch 语句判断类型,这样程序写起来比较简单。但是很快我们发现大量的 type assert 拖慢了我们的程序,比如下面这段代码:
var val interface{}
val = int64(100)
经过我们测试,把一个整数赋值给一个 interface{} 类型的变量,会触发一次内存分配,通常要耗时几十到上百纳秒。在运行 SQL 语句时,会有大量的类似操作,对性能的损耗严重。为了解决这个问题,我们调研了其他数据库的解决方案,最终采用自定的数据包装类型 Datum 取代 interface{},这个 Datum 需要能存放各种类型, 实现 value 对 value 赋值。同时为了减少空间占用, Datum 内部的属性会在多种数据类型之间重用。上面的代码重构后变成:
var d Datum
d.SetInt64(100)
重构后,在我们的 bench 结果中,表达式计算相关操作的性能,提升 10 倍以上。


包依赖问题


Golang 的包依赖问题一直被人诟病,可以说到目前为止,也没有完美的解决方案。


Golang 中隐藏的一些 Bug


相比 C/C++/Java/Python 等语言,Golang 算是一门年轻的语言,还是存在一些 bug。上周我们遇到一个诡异的问题,调用 atomic.AddInt64 时,在64位系统上 OK, 但是在 i386 系统上,会导致 crash。我们通过内部的 CI 发现问题后,经过研究发现这是 Golang 的一个 bug,对于 32 位系统,需要自己来保证内存对齐。


Conclusion


相比 C++/Java/Python 等语言,Golang 不支持许多高级的语言特性,但从工程的角度讲,Go 的设计是非常优秀的:规范足够简单灵活,有其他语言基础的程序员都能迅速上手。
TiDB 设计之初,我们定了一个原则: Make it run. Make it right. Make it fast. Golang 很好的满足了我们的原则。高效的开发使得我们很快能做出能 run 的产品,自动的 GC 以及内置的测试框架有利于我们写出正确的程序,方便的 Profile 工具帮助我们进行系统调优。
除此之外,Golang 还有一个成熟友好的社区,Gopher 们在从社区获得收益的同时,很愿意向社区做贡献,大量高质量的第三方库就是最明显的体现。在平时开发遇到 Golang 相关的问题时,很容易借鉴到别人的经验,节省了我们大量的时间。


最近整个 TiDB 团队都在做稳定性和性能相关的事情,也在积极地和国外优秀的开源团队交流协作,在工程和实践方面,有蛮多可以借鉴的经验,等我们11月份忙完 GA 版本的发布之后,会和大家进一步分享。另外非常感谢谢大对整个 golang 社区的贡献,让 PingCAP 从社区中汲取了很多的养分和鼓励,希望大家一起加油,共同推动社区的发展。

【译】优化Go的模式

文章分享mnhkahn 发表了文章 • 2 个评论 • 656 次浏览 • 2016-10-11 17:07 • 来自相关话题

之前写过一篇文章《为什么SignalFx metric proxy通过Go语言开发》,这篇文章将会关注以我们的ingest服务为例,来讲述我们是如何优化Go代码的。

SingalFx基于流分析和时间报警序列,例如应用程序指标,可以为时间序列... 查看全部

之前写过一篇文章《为什么SignalFx metric proxy通过Go语言开发》,这篇文章将会关注以我们的ingest服务为例,来讲述我们是如何优化Go代码的。


SingalFx基于流分析和时间报警序列,例如应用程序指标,可以为时间序列数据的现代应用开发的高级监控平台(“我的应用程序收到了多少请求?”),还有系统级指标(“我的Linux服务器使用了多少网络流量?”)。我们用户流量很大并且粒度很高,每次用户的流量都要先通过我们的ingest服务才能访问其它的SignalFx服务。


第一步:启用pprof


啥是pprof?


pprof是Go语言内置的标准方法用来调试Go程序性能。可以通过HTTP的方式调用pprof包,它能提取出来应用程序的CPU和内存数据,此外还有运行的代码行数和内容信息。


如何启用pprof?


你可以通过在你的应用增加一行代码 import _ "net/http/pprof",然后启动你的应用服务器,pprof就算是启动了。还有一种方式,就是我们在做SignalFx的时候,为了在外部控制pprof,我们附加了一些处理程序,可以用过路由设置暴露出去,代码如下:


import "github.com/gorilla/mux"
import "net/http/pprof"
var handler *mux.Router
// ...
handler.PathPrefix("/debug/pprof/profile").HandlerFunc(pprof.Profile)
handler.PathPrefix("/debug/pprof/heap").HandlerFunc(pprof.Heap)

第二步:找到可以优化的代码


要执行什么?


curl http://ingest58:6060/debug/pprof/profile > /tmp/ingest.profile
go tool pprof ingest /tmp/ingest.profile
(pprof) top7

这是干嘛的?


Go语言包含了一个本地的pprof工具来可视化输出pprof的结果。我们配置的路由/debug/pprof/profile可以收集30秒数据。我上面的操作,第一步是保存输出到本地文件,然后运行保存后的文件。值得一提的是,最后一个参数可以直接输入一个URL来取代文件(译者注:go tool pprof ingest http://ingest58:6060/debug/pprof/profile)。 命令top7可以展示消耗CPU最好的7个函数。


结果


12910ms of 24020ms total (53.75%)
Dropped 481 nodes (cum <= 120.10ms)
Showing top 30 nodes out of 275 (cum >= 160ms)
flat flat% sum% cum cum%
1110ms 4.62% 4.62% 2360ms 9.83% runtime.mallocgc
940ms 3.91% 8.53% 1450ms 6.04% runtime.scanobject
830ms 3.46% 11.99% 830ms 3.46% runtime.futex
800ms 3.33% 15.32% 800ms 3.33% runtime.mSpan_Sweep.func1
750ms 3.12% 18.44% 750ms 3.12% runtime.cmpbody
720ms 3.00% 21.44% 720ms 3.00% runtime.xchg
580ms 2.41% 23.86% 580ms 2.41% runtime._ExternalCode

为啥是这个结果


我们可以发现,这些函数我们都没有直接调用过。然而,mallocgcsacnobject还有mSpan_Sweep全部都会导致是垃圾回收的时候CPU占用高。我们可以深入了解这些函数,而不是去优化Go语言的垃圾回收器本身,更好的优化办法是我们来优化我们代码里面使用Go语言的垃圾回收器的方法。在这个例子中,我们可以优化的是减少在堆上面创建对象。


第三步:探究GC的原因


执行啥?


curl http://ingest58:6060/debug/pprof/heap > /tmp/heap.profile
go tool pprof -alloc_objects /tmp/ingest /tmp/heap.profile
(pprof) top3

做了啥?


可以注意到这次下载的URL和之前的有点像,但是是以/heap结尾的。这个将会给我们提供机器上面堆的使用总结的数据。我再一次保存成文件用户后面的比较。参数-alloc_objects将会可视化应用程序在执行过程中分配的对象数量。


结果


4964437929 of 7534904879 total (65.89%)
Dropped 541 nodes (cum <= 37674524)
Showing top 10 nodes out of 133 (cum >= 321426216)
flat flat% sum% cum cum%
853721355 11.33% 11.33% 859078341 11.40% github.com/signalfuse/sfxgo/ingest/tsidcache/tsiddiskcache.(*DiskKey).EncodeOld
702927011 9.33% 20.66% 702927011 9.33% reflect.unsafe_New
624715067 8.29% 28.95% 624715067 8.29% github.com/signalfuse/sfxgo/ingest/bus/rawbus.(*Partitioner).Partition

啥意思?


可以看出,11.33%的对象分配都发生在对象DiskKey的函数EncodeOld里面,我们预期也是这个结果。然而,没有料到的是Partition函数占用了全部内存分配的8.29%,因为这个函数只是一些基本的计算,我得着重研究一下这个问题。


第四步:找到为什么partitioner使用如此多内存的原因


执行啥?


(pprof) list Partitioner.*Partition

做了啥?


这个命令可以打印出来我关注的源代码行,还有就是函数内部哪些代码引起了堆的内存申请。这是pprof里面许多命令的其中一个。另一个非常有用的是查看调用方和被调用方。可以通过help命令查看完整的帮助并且都试一试。


结果


Total: 11323262665
ROUTINE ======================== github.com/signalfuse/sfxgo/ingest/bus/rawbus.(*Partitioner).Partition in /opt/jenkins/workspace/ingest/gopath/src/github.com/signalfuse/sfxgo/ingest/bus/rawbus/partitioner.go
927405893 927405893 (flat, cum) 8.19% of Total
. . 64: if ringSize == 0 {
. . 65: return 0, ErrUnsetRingSize
. . 66: }
. . 67: var b [8]byte
. . 68: binary.LittleEndian.PutUint64(b[:], uint64(message.Key.(*partitionPickingKey).tsid))
239971917 239971917 69: logherd.Debug2(log, "key", message.Key, "numP", numPartitions, "Partitioning")
. . 70: murmHash := murmur3.Sum32(b[:])
. . 71:
. . 72: // 34026 => 66
. . 73: setBits := uint(16)
. . 74: setSize := uint32(1 << setBits)
. . 75: shortHash := murmHash & (setSize - 1)
. . 76: smallIndex := int32(shortHash) * int32(k.ringSize) / int32(setSize)
687433976 687433976 77: logherd.Debug3(log, "smallIndex", smallIndex, "murmHash", murmHash, "shortHash", shortHash, "Sending to partition")
. . 78: return smallIndex, nil
. . 79:}
. . 80:

啥意思?


这个可以表示debug日志是引起变量从栈逃逸到堆的原因。因为调试日志并不是直接需要的,我能够直接删掉这些行。但是首先,还是让我们来确认这个假设。logherd.Debug2函数看起来封装了如下所示,如果日志级别debug没有符合条件,WithField对象并不会调用。


// Debug2 to logger 2 key/value pairs and message.  Intended to save the mem alloc that WithField creates
func Debug2(l *logrus.Logger, key string, val interface{}, key2 string, val2 interface{}, msg string) {
if l.Level >= logrus.DebugLevel {
l.WithField(key, val).WithField(key2, val2).Debug(msg)
}
}

从pprof检测看起来是传递整数到Debug2函数引起的内存分配,让我们进一步确认。


第五步:找到日志语句引起内存分配的原因


执行什么:


go build -gcflags='-m' . 2>&1 | grep partitioner.go

这个用来干啥?


通过-m参数编译可以让编译器打印内容到stderr。这包括编译器是否能够在栈上面分配内存还是一定得将变量放到堆上面申请。如果编译器不能决定一个变量是否在外部继续被调用,他会被Go语言放到堆上面。


结果


./partitioner.go:63: &k.ringSize escapes to heap
./partitioner.go:62: leaking param: k
./partitioner.go:70: message.Key escapes to heap
./partitioner.go:62: leaking param content: message
./partitioner.go:70: numPartitions escapes to heap
./partitioner.go:77: smallIndex escapes to heap
./partitioner.go:77: murmHash escapes to heap
./partitioner.go:77: shortHash escapes to heap
./partitioner.go:68: (*Partitioner).Partition b does not escape
./partitioner.go:71: (*Partitioner).Partition b does not escape

注意第77行,smallIndexmurmHash还有shortHash全部逃逸到了堆上面。编译器为短生命周期的变量在堆上面申请了空间,导致我们在对上创建了很多我们并不需要的对象。


第六步:对partition函数压测


写什么?


func BenchmarkPartition(b *testing.B) {

r := rand.New(rand.NewSource(0))

k := partitionPickingKey{}

msg := sarama.ProducerMessage {

Key: &k,

}

p := Partitioner{

ringSize: 1024,

ringName: "quantizer.ring",

}

num_partitions := int32(1024)

for i := 0; i < b.N; i++ {

k.tsid = r.Int63()

part, err := p.Partition(&msg, num_partitions)

if err != nil {

panic("Error benchmarking")

}

if part < 0 || part >= num_partitions {

panic("Bench failure")

}

}

}

压测只是简单的创建了B.N个对象,并且在返回的时候做了一个基本的检查来确认对象不会被简单的优化掉。我们推荐当程序员在优化代码之前编写压测代码来确保你在朝着正确的方向进行。


第七步:对partition函数压测内存分配


执行啥?


go test -v -bench . -run=_NONE_ -benchmem BenchmarkPartition

做了啥?


压测会按照正则匹配符合“.”条件的函数,-benchmen将会追踪每次循环的堆使用平均情况。通过传递参数-run=_NONE_,我可以节约一些时间,这样测试只会运行有“NONE”字符串的单元测试。换句话说,不下运行任何一个单元测试,只运行全部的压力测试。


结果


PASS

BenchmarkPartition-8 10000000 202 ns/op 64 B/op 4 allocs/op

意味着啥?


每一次循环消耗平均202ns,最重要的是,每个操作有4次对象分配。


第八步:删掉日志语句


咋写?


@@ -66,7 +65,6 @@ func (k *Partitioner) Partition(message *sarama.ProducerMessage, numPartitions i

}

var b [8]byte

binary.LittleEndian.PutUint64(b[:], uint64(message.Key.(*partitionPickingKey).tsid))

- logherd.Debug2(log, "key", message.Key, "numP", numPartitions, "Partitioning")

murmHash := murmur3.Sum32(b[:])

// 34026 => 66

@@ -74,7 +72,6 @@ func (k *Partitioner) Partition(message *sarama.ProducerMessage, numPartitions i

setSize := uint32(1 << setBits)

shortHash := murmHash & (setSize - 1)

smallIndex := int32(shortHash) * int32(k.ringSize) / int32(setSize)

- logherd.Debug3(log, "smallIndex", smallIndex, "murmHash", murmHash, "shortHash", shortHash, "Sending to partition")

return smallIndex, nil

}

干了什么?


我的修复方式是删除日志代码。测试期间/调试期间,我增加了这些调试代码,但是一直没有删掉它们。这种情况下,删掉这些代码最简单。


第九步:重新编译评估是否变量逃逸到了堆


如何执行?


go build -gcflags='-m' . 2>&1 | grep partitioner.go

结果


./partitioner.go:62: &k.ringSize escapes to heap

./partitioner.go:61: leaking param: k

./partitioner.go:61: (*Partitioner).Partition message does not escape

./partitioner.go:67: (*Partitioner).Partition b does not escape

./partitioner.go:68: (*Partitioner).Partition b does not escape

意味着什么?


可以发现smallIndexmurmHashshortHash变量不在有逃逸到堆的消息。


第十步:重新压测评估每个操作的内存分配情况


如何执行?


go test -v -bench . -run=_NONE_ -benchmem BenchmarkPartition

结果


PASS

BenchmarkPartition-8 30000000 40.5 ns/op 0 B/op 0 allocs/op

ok github.com/signalfuse/sfxgo/ingest/bus/rawbus 1.267s

啥意思?


注意到每个操作只消耗40ns,更重要的是,每个操作不再有内存分配。因为我是准备来优化堆,这对我来说很重要。


结束语


pprof是非常有用的工具来剖析Go代码的性能问题。通过结合Go语言内置的压测工具,你能够得到关于代码改变引起的变化的真正的数字。不幸的是,性能衰退会随着时间而攀升。下一步,读者可以练习,保存benchmark的结果到数据库,这样你可以在每一次代码提交之后查看代码的性能。

Excelize - Golang 操作 Office Excel 文档类库

开源程序astaxie 发表了文章 • 5 个评论 • 1460 次浏览 • 2016-10-11 16:13 • 来自相关话题


Excelize 是 Golang 编写的一个用来操作 Office Excel 文档类库,基于微软的 Office Open XML 标准。可以使用它来读取、写入 XLSX 文件。相比较其他的开源类库,Excelize 支持写入带有图表的文档,并且在保存后不会丢失图表样式。


安装


go get github.com/Luxurioust/excelize

创建 XLSX


package main

import (
"fmt"
"github.com/Luxurioust/excelize"
)

func main() {
xlsx, err := excelize.CreateFile()
if err != nil {
fmt.Println(err)
}
xlsx.NewSheet(2, "Sheet2")
xlsx.NewSheet(3, "Sheet3")
xlsx.SetCellInt("Sheet2", "A23", 10)
xlsx.SetCellStr("Sheet3", "B20", "Hello")
err = xlsx.WriteTo("/home/Workbook.xlsx")
if err != nil {
fmt.Println(err)
}
}

修改已有文档


package main

import (
"fmt"
"github.com/Luxurioust/excelize"
)

func main() {
xlsx, err := excelize.OpenFile("/home/Workbook.xlsx")
if err != nil {
fmt.Println(err)
}
xlsx.SetCellInt("Sheet2", "B2", 100)
xlsx.SetCellStr("Sheet2", "C11", "Hello")
xlsx.NewSheet(3, "TestSheet")
xlsx.SetCellInt("Sheet3", "A23", 10)
xlsx.SetCellStr("Sheet3", "b230", "World")
xlsx.SetActiveSheet(2)
err = xlsx.Save()
if err != nil {
fmt.Println(err)
}
}

读取 XLSX 单元格


package main

import (
"fmt"
"github.com/Luxurioust/excelize"
)

func main() {
xlsx, err := excelize.OpenFile("/home/Workbook.xlsx")
if err != nil {
fmt.Println(err)
}
cell := xlsx.GetCellValue("Sheet2", "D11")
fmt.Println(cell)
}

aurora - 基于 Web UI 的 Beanstalk 消息队列服务器管理工具

开源程序astaxie 发表了文章 • 1 个评论 • 689 次浏览 • 2016-10-11 16:11 • 来自相关话题

GitHub: github.com/Luxurioust... 查看全部


GitHub: github.com/Luxurioust/aurora


aurora 是一个 Golang 编写的基于 Web 的 Beanstalk 消息队列服务器管理工具,单文件无需依赖其他组件,支持管理本地和远程多个队列服务器。


特点



  • 跨平台支持 macOS/Linux/Windows 32/64-bit

  • 单文件简单易部署

  • 不依赖其他组件

  • 支持读取配置文件方式启动 + 登陆用户认证

  • 定时刷新 Beanstalk 队列服务器状态

  • 对每个 Tube 的 ready/delayed/buried 状态进行管理

  • 支持批量清空 Tube 中的 Job

  • 支持 Job 文本高亮显示

  • 支持 Job 模糊搜索

  • 自定义队列服务器状态监控项

[QingCloud Insight 2016] How do we build TiDB

文章分享qiuyesuifeng 发表了文章 • 0 个评论 • 546 次浏览 • 2016-10-11 15:17 • 来自相关话题

首先我们聊聊 Database 的历史,在已经有这么多种数据库的背景下我们为什么要创建另外一个数据库;以及说一下现在方案遇到的困境,说一下 Google Spanner 和 F1,TiKV 和 TiDB,说一下架构的事情,在这里我们会重点聊一下 TiKV... 查看全部

首先我们聊聊 Database 的历史,在已经有这么多种数据库的背景下我们为什么要创建另外一个数据库;以及说一下现在方案遇到的困境,说一下 Google Spanner 和 F1,TiKV 和 TiDB,说一下架构的事情,在这里我们会重点聊一下 TiKV。因为我们产品的很多特性是 TiKV 提供的,比如说跨数据中心的复制,Transaction,auto-scale。


再聊一下为什么 TiKV 用 Raft 能实现所有这些重要的特性,以及 scale,MVCC 和事务模型。东西非常多,我今天不太可能把里面的技术细节都描述得特别细,因为几乎每一个话题都可以找到一篇或者是多篇论文。但讲完之后我还在这边,所以详细的技术问题大家可以单独来找我聊。


后面再说一下我们现在遇到的窘境,就是大家常规遇到的分布式方案有哪些问题,比如 MySQL Sharding。我们创建了无数 MySQL Proxy,比如官方的 MySQL proxy,Youtube 的 Vitess,淘宝的 Cobar、TDDL,以及基于 Cobar 的 MyCAT,金山的 Kingshard,360 的 Atlas,京东的 JProxy,我在豌豆荚也写了一个。可以说,随便一个大公司都会造一个MySQL Sharding的方案。


为什么我们要创建另外一个数据库?


昨天晚上我还跟一个同学聊到,基于 MySQL 的方案它的天花板在哪里,它的天花板特别明显。有一个思路是能不能通过 MySQL 的 server 把 InnoDB 变成一个分布式数据库,听起来这个方案很完美,但是很快就会遇到天花板。因为 MySQL 生成的执行计划是个单机的,它认为整个计划的 cost 也是单机的,我读取一行和读取下一行之间的开销是很小的,比如迭代 next row 可以立刻拿到下一行。实际上在一个分布式系统里面,这是不一定的。


另外,你把数据都拿回来计算这个太慢了,很多时候我们需要把我们的 expression 或者计算过程等等运算推下去,向上返回一个最终的计算结果,这个一定要用分布式的 plan,前面控制执行计划的节点,它必须要理解下面是分布式的东西,才能生成最好的 plan,这样才能实现最高的执行效率。


比如说你做一个 sum,你是一条条拿回来加,还是让一堆机器一起算,最后给我一个结果。 例如我有 100 亿条数据分布在 10 台机器上,并行在这 10 台 机器我可能只拿到 10 个结果,如果把所有的数据每一条都拿回来,这就太慢了,完全丧失了分布式的价值。聊到 MySQL 想实现分布式,另外一个实现分布式的方案是什么,就是 Proxy。但是 Proxy 本身的天花板在那里,就是它不支持分布式的 transaction,它不支持跨节点的 join,它无法理解复杂的 plan,一个复杂的 plan 打到 Proxy 上面,Proxy 就傻了,我到底应该往哪一个节点上转发呢,如果我涉及到 subquery sql 怎么办?所以这个天花板是瞬间会到,在传统模型下面的修改,很快会达不到我们的要求。


另外一个很重要的是,MySQL 支持的复制方式是半同步或者是异步,但是半同步可以降级成异步,也就是说任何时候数据出了问题你不敢切换,因为有可能是异步复制,有一部分数据还没有同步过来,这时候切换数据就不一致了。前一阵子出现过某公司突然不能支付了这种事件,今年有很多这种类似的 case,所以微博上大家都在说“说好的异地多活呢?”……


为什么传统的方案在这上面解决起来特别的困难,天花板马上到了,基本上不可能解决这个问题。另外是多数据中心的复制和数据中心的容灾,MySQL 在这上面是做不好的。
屏幕快照 2016-08-03 下午3.49.40.png-109.8kB
在前面三十年基本上是关系数据库的时代,那个时代创建了很多伟大的公司,比如说 IBM、Oracle、微软也有自己的数据库,早期还有一个公司叫 Sybase,有一部分特别老的程序员同学在当年的教程里面还可以找到这些东西,但是现在基本上看不到了。
另外是 NoSQL。NoSQL 也是一度非常火,像 Cassandra,MongoDB 等等,这些都属于在互联网快速发展的时候创建这些能够 scale 的方案,但 Redis scale 出来比较晚,所以很多时候大家把 Redis 当成一个 Cache,现在慢慢大家把它当成存储不那么重要的数据的数据库。因为它有了 scale 支持以后,大家会把更多的数据放在里面。
然后到了 2015,严格来讲是到 2014 年到 2015 年之间,Raft 论文发表以后,真正的 NewSQL 的理论基础终于完成了。我觉得 NewSQL 这个理论基础,最重要的划时代的几篇论文,一个是谷歌的 Spanner,是在 2013 年初发布的,再就是 Raft 是在 2014 年上半年发布的。这几篇相当于打下了分布式数据库 NewSQL 的理论基础,这个模型是非常重要的,如果没有模型在上面是堆不起来东西的。说到现在,大家可能对于模型还是可以理解的,但是对于它的实现难度很难想象。


前面我大概提到了我们为什么需要另外一个数据库,说到 Scalability 数据的伸缩,然后我们讲到需要 SQL,比如你给我一个纯粹的 key-velue 系统的 API,比如我要查找年龄在 10 岁到 20 岁之间的 email 要满足一个什么要求的。如果只有 KV 的 API 这是会写死人的,要写很多代码,但是实际上用 SQL 写一句话就可以了,而且 SQL 的优化器对整个数据的分布是知道的,它可以很快理解你这个 SQL,然后会得到一个最优的 plan,他得到这个最优的 plan 基本上等价于一个真正理解 KV 每一步操作的人写出来的程序。通常情况下,SQL 的优化器是为了更加了解或者做出更好的选择。


另外一个就是 ACID 的事务,这是传统数据库必须要提供的基础。以前你不提供 ACID 就不能叫数据库,但是近些年大家写一个内存的 map 也可以叫自己是数据库。大家写一个 append-only 文件,我们也可以叫只读数据库,数据库的概念比以前极大的泛化了。


另外就是高可用和自动恢复,他们的概念是什么呢?有些人会有一些误解,因为今天还有朋友在现场问到,出了故障,比如说一个机房挂掉以后我应该怎么做切换,怎么操作。这个实际上相当于还是上一代的概念,还需要人去干预,这种不算是高可用。


未来的高可用一定是系统出了问题马上可以自动恢复,马上可以变成可用。比如说一个机房挂掉了,十秒钟不能支付,十秒钟之后系统自动恢复了变得可以支付,即使这个数据中心再也不起来我整个系统仍然是可以支付的。Auto-Failover 的重要性就在这里。大家不希望在睡觉的时候被一个报警给拉起来,我相信大家以后具备这样一个能力,5 分钟以内的报警不用理会,挂掉一个机房,又挂掉一个机房,这种连续报警才会理。我们内部开玩笑说,希望大家都能睡个好觉,很重要的事情就是这个。


说完应用层的事情,现在很有很多业务,在应用层自己去分片,比如说我按照 user ID 在代码里面分片,还有一部分是更高级一点我会用到一致性哈希。问题在于它的复杂度,到一定程度之后我自动的分库,自动的分表,我觉得下一代数据库是不需要理解这些东西的,不需要了解什么叫做分库,不需要了解什么叫做分表,因为系统是全部自动搞定的。同时复杂度,如果一个应用不支持事务,那么在应用层去做,通常的做法是引入一个外部队列,引入大量的程序机制和状态转换,A 状态的时候允许转换到 B 状态,B 状态允许转换到 C 状态。


举一个简单的例子,比如说在京东上买东西,先下订单,支付状态之后这个商品才能出库,如果不是支付状态一定不能出库,每一步都有严格的流程。


Google Spanner / F1


说一下 Google 的 Spanner 和 F1,这是我非常喜欢的论文,也是我最近几年看过很多遍的论文。Google Spanner 已经强大到什么程度呢?Google Spanner 是全球分布的数据库,在国内目前普遍做法叫做同城两地三中心,它们的差别是什么呢?以 Google 的数据来讲,谷歌比较高的级别是他们有 7 个副本,通常是美国保存 3 个副本,再在另外 2 个国家可以保存 2 个副本,这样的好处是万一美国两个数据中心出了问题,那整个系统还能继续可用,这个概念就是比如美国 3 个副本全挂了,整个数据都还在,这个数据安全级别比很多国家的安全级别还要高,这是 Google 目前做到的,这是全球分布的好处。


现在国内主流的做法是两地三中心,但现在基本上都不能自动切换。大家可以看到很多号称实现了两地三中心或者异地多活,但是一出现问题都说不好意思这段时间我不能提供服务了。大家无数次的见到这种 case,我就不列举了。


Spanner 现在也提供一部分 SQL 特性。在以前,大部分 SQL 特性是在 F1 里面提供的,现在 Spanner 也在逐步丰富它的功能,Google 是全球第一个做到这个规模或者是做到这个级别的数据库。事务支持里面 Google 有点黑科技(其实也没有那么黑),就是它有 GPS 时钟和原子钟。大家知道在分布式系统里面,比如说数千台机器,两个事务启动先后顺序,这个顺序怎么界定(事务外部一致性)。这个时候 Google 内部使用了 GPS 时钟和原子钟,正常情况下它会使用一个 GPS 时钟的一个集群,就是说我拿的一个时间戳,并不是从一个 GPS 上来拿的时间戳,因为大家知道所有的硬件都会有误差。如果这时候我从一个上拿到的 GPS 本身有点问题,那么你拿到的这个时钟是不精确的。而 Google 它实际上是在一批 GPS 时钟上去拿了能够满足 majority 的精度,再用时间的算法,得到一个比较精确的时间。同时大家知道 GPS 也不太安全,因为它是美国军方的,对于 Google 来讲要实现比国家安全级别更高的数据库,而 GPS 是可能受到干扰的,因为 GPS 信号是可以调整的,这在军事用途上面很典型的,大家知道导弹的制导需要依赖 GPS,如果调整了 GPS 精度,那么导弹精度就废了。所以他们还用原子钟去校正 GPS,如果 GPS 突然跳跃了,原子钟上是可以检测到 GPS 跳跃的,这部分相对有一点黑科技,但是从原理上来讲还是比较简单,比较好理解的。


最开始它 Spanner 最大的用户就是 Google 的 Adwords,这是 Google 最赚钱的业务,Google 就是靠广告生存的,我们一直觉得 Google 是科技公司,但是他的钱是从广告那来的,所以一定程度来讲 Google 是一个广告公司。Google 内部的方向先有了 Big table ,然后有了 MegaStore ,MegaStore 的下一代是 Spanner ,F1 是在 Spanner 上面构建的。


TiDB and TiKV


TiKV 和 TiDB 基本上对应 Google Spanner 和 Google F1,用 Open Source 方式重建。目前这两个项目都开放在 GitHub 上面,两个项目都比较火爆,TiDB 是更早一点开源的, 目前 TiDB 在 GitHub 上 有 4300 多个 Star,每天都在增长。
另外,对于现在的社会来讲,我们觉得 Infrastructure 领域闭源的东西是没有任何生存机会的。没有任何一家公司,愿意把自己的身家性命压在一个闭源的项目上。举一个很典型的例子,在美国有一个数据库叫 FoundationDB,去年被苹果收购了。 FoundationDB 之前和用户签的合约都是一年的合约。比如说,我给你服务周期是一年,现在我被另外一个公司收购了,我今年服务到期之后,我是满足合约的。但是其他公司再也不能找它服务了,因为它现在不叫 FoundationDB 了,它叫 Apple了,你不能找 Apple 给你提供一个 enterprise service。
8.png-81.6kB
TiDB 和 TiKV 为什么是两个项目,因为它和 Google 的内部架构对比差不多是这样的:TiKV 对应的是 Spanner,TiDB 对应的是 F1 。F1 里面更强调上层的分布式的 SQL 层到底怎么做,分布式的 Plan 应该怎么做,分布式的 Plan 应该怎么去做优化。同时 TiDB 有一点做的比较好的是,它兼容了 MySQL 协议,当你出现了一个新型的数据库的时候,用户使用它是有成本的。大家都知道作为开发很讨厌的一个事情就是,我要每个语言都写一个 Driver,比如说你要支持 C++,你要支持 Java,你要支持 Go 等等,这个太累了,而且用户还得改他的程序,所以我们选择了一个更加好的东西兼容 MySQL 协议,让用户可以不用改。一会我会用一个视频来演示一下,为什么一行代码不改就可以用,用户就能体会到 TiDB 带来的所有的好处。
9.png-134kB
这个图实际上是整个协议栈或者是整个软件栈的实现。大家可以看到整个系统是高度分层的,从最底下开始是 RocksDB ,然后再上面用 Raft 构建一层可以被复制的 RocksDB,在这一层的时候它还没有 Transaction,但是整个系统现在的状态是所有写入的数据一定要保证它复制到了足够多的副本。也就是说只要我写进来的数据一定有足够多的副本去 cover 它,这样才比较安全,在一个比较安全的 Key-value store 上面, 再去构建它的多版本,再去构建它的分布式事务,然后在分布式事务构建完成之后,就可以轻松的加上 SQL 层,再轻松的加上 MySQL 协议的支持。然后,这两天我比较好奇,自己写了 MongoDB 协议的支持,然后我们可以用 MongoDB 的客户端来玩,就是说协议这一层是高度可插拔的。TiDB 上可以在上面构建一个 MongoDB 的协议,相当于这个是构建一个 SQL 的协议,可以构建一个 NoSQL 的协议。这一点主要是用来验证 TiKV 在模型上面的支持能力。
10.png-117.2kB
这是整个 TiKV 的架构图,从这个看来,整个集群里面有很多 Node,比如这里画了四个 Node,分别对应了四个机器。每一个 Node 上可以有多个 Store,每个 Store 里面又会有很多小的 Region,就是说一小片数据,就是一个 Region 。从全局来看所有的数据被划分成很多小片,每个小片默认配置是 64M,它已经足够小,可以很轻松的从一个节点移到另外一个节点,Region 1 有三个副本,它分别在 Node1、Node 2 和 Node4 上面, 类似的Region 2,Region 3 也是有三个副本。每个 Region 的所有副本组成一个 Raft Group, 整个系统可以看到很多这样的 Raft groups。


Raft 细节我不展开了,大家有兴趣可以找我私聊或者看一下相应的资料。


因为整个系统里面我们可以看到上一张图里面有很多 Raft group 给我们,不同 Raft group 之间的通讯都是有开销的。所以我们有一个类似于 MySQL 的 group commit 机制 ,你发消息的时候实际上可以 share 同一个 connection , 然后 pipeline + batch 发送, 很大程度上可以省掉大量 syscall 的开销。


另外,其实在一定程度上后面我们在支持压缩的时候,也有非常大的帮助,就是可以减少数据的传输。对于整个系统而言,可能有数百万的 Region,它的大小可以调整,比如说 64M、128M、256M,这个实际上依赖于整个系统里面当前的状况。


比如说我们曾经在有一个用户的机房里面做过测试,这个测试有一个香港机房和新加坡的机房。结果我们在做复制的时候,新加坡的机房大于 256M 就复制不过去,因为机房很不稳定,必须要保证数据切的足够小,这样才能复制过去。


如果一个 Region 太大以后我们会自动做 SPLIT,这是非常好玩的过程,有点像细胞的分裂。


然后 TiKV 的 Raft 实现,是从 etcd 里面 port 过来的,为什么要从 etcd 里面 port 过来呢?首先 TiKV 的 Raft 实现是用 Rust 写的。作为第一个做到生产级别的 Raft 实现,所以我们从 etcd 里面把它用 Go 语言写的 port 到这边。
12.png-315.9kB
这个是 Raft 官网上面列出来的 TiKV 在里面的状态,大家可以看到 TiKV 把所有 Raft 的 feature 都实现了。 比如说 Leader Election、Membership Changes,这个是非常重要的,整个系统的 scale 过程高度依赖 Membership Changes,后面我用一个图来讲这个过程。后面这个是 Log Compaction,这个用户不太关心。
13.png-695.5kB
这是很典型的细胞分裂的图,实际上 Region 的分裂过程和这个是类似的。


我们看一下扩容是怎么做的。
14.png-150.8kB
比如说以现在的系统假设,我们刚开始说只有三个节点,有 Region1 分别是在 1 、2、4,我用虚线连接起来代表它是 一个 Raft group ,大家可以看到整个系统里面有三个 Raft group,在每一个 Node 上面数据的分布是比较均匀的,在这个假设每一个 Region 是 64M ,相当于只有一个 Node 上面负载比其他的稍微大一点点。


这是一个在线的视频。默认的时候,我们都是推荐 3 个副本或者 5 个副本的配置。Raft 本身有一个特点,如果一个 leader down 掉之后,其它的节点会选一个新的 leader,那么这个新的 leader 会把它还没有 commit 但已经 reply 过去的 log 做一个 commit ,然后会再做 apply,这个有点偏 Raft 协议,细节我不讲了。


复制数据的小的 Region,它实际上是跨多个数据中心做的复制。这里面最重要的一点是永远不丢失数据,无论如何我保证我的复制一定是复制到 majority,任何时候我只要对外提供服务,允许外面写入数据一定要复制到 majority。很重要的一点就是恢复的过程一定要是自动化的,我前面已经强调过,如果不能自动化恢复,那么中间的宕机时间或者对外不可服务的时间,便不是由整个系统决定的,这是相对回到了几十年前的状态。


MVCC


MVCC 我稍微仔细讲一下这一块。MVCC 的好处,它很好支持 Lock-free 的 snapshot read ,一会儿我有一个图会展示 MVCC 是怎么做的。isolation level 就不讲了,MySQL 里面的级别是可以调的,我们的 TiKV 有 SI,还有 SI+lock,默认是支持 SI 的这种隔离级别,然后你写一个 select for update 语句,这个会自动的调整到 SI 加上 lock 这个隔离级别。这个隔离级别基本上和 SSI 是一致的。还有一个就是 GC 的问题,如果你的系统里面的数据产生了很多版本,你需要把这个比较老的数据给 GC 掉,比如说正常情况下我们是不删除数据的, 你写入一行,然后再写入一行,不断去 update 同一行的时候,每一次 update 会产生新的版本,新的版本就会在系统里存在,所以我们需要一个 GC 的模块把比较老的数据给 GC 掉,实际上这个 GC 不是 Go 里面的GC,不是 Java 的 GC,而是数据的 GC。
19.png-138.5kB
这是一个数据版本,大家可以看到我们的数据分成两块,一个是 meta,一个是 data。meta 相对于描述我的数据当前有多少个版本。大家可以看到绿色的部分,比如说我们的 meta key 是 A,keyA 有三个版本,是 A1、A2、A3,我们把 key 自己和 version 拼到一起。那我们用 A1、A2、A3 分别描述 A 的三个版本,那么就是 version 1/2/3。meta 里面描述,就是我的整个 key 相对应哪个版本,我想找到那个版本。比如说我现在要读取 key A 的版本 10,但显然现在版本 10 是没有的,那么小于版本 10 最大的版本是 3,所以这时我就能读取到 3,这是它的隔离级别决定的。关于 data,我刚才已经讲过了。


分布式事务模型


接下来是分布式事务模型,其实是基于 Google Percolator,这是 Google 在 2006 发表的一篇论文,是 Google 在做内部增量处理的时候发现了这个方法,本质上还是二阶段提交的。这使用的是一个乐观锁,比如说我提供一个 transaction ,我去改一个东西,改的时候是发布在本地的,并没有马上 commit 到数据存储那一端,这个模型就是说,我修改的东西我马上去 Lock 住,这个基本就是一个悲观锁。但如果到最后一刻我才提交出去,那么锁住的这一小段的时间,这个时候实现的是乐观锁。乐观锁的好处就是当你冲突很小的时候可以得到非常好的性能,因为冲突特别小,所以我本地修改通常都是有效的,所以我不需要去 Lock ,不需要去 roll back 。本质上分布式事务就是 2PC 或者是 2+xPC,基本上没有 1PC,除非你在别人的级别上做弱化。比如说我允许你读到当前最新的版本,也允许你读到前面的版本,书里面把这个叫做幻读。如果你调到这个程度是比较容易做 1PC 的,这个实际上还是依赖用户设定的隔离级别的,如果用户需要更高的隔离级别,这个 1PC 就不太好做了。
这是一个路由,正常来讲,大家可能会好奇一个 SQL 语句怎么最后会落到存储层,然后能很好的运行,最后怎么能映射到 KV 上面,又怎么能路由到正确的节点,因为整个系统可能有上千个节点,你怎么能正确路由到那一个的节点。我们在 TiDB 有一个 TiKV driver , 另外 TiKV 对外使用的是 Google Protocol Buffer 来作为通讯的编码格式。


Placement Driver


来说一下 Placement Driver 。Placement Driver 是什么呢?整个系统里面有一个节点,它会时刻知道现在整个系统的状态。比如说每个机器的负载,每个机器的容量,是否有新加的机器,新加机器的容量到底是怎么样的,是不是可以把一部分数据挪过去,是不是也是一样下线, 如果一个节点在十分钟之内无法被其他节点探测到,我认为它已经挂了,不管它实际上是不是真的挂了,但是我也认为它挂了。因为这个时候是有风险的,如果这个机器万一真的挂了,意味着你现在机器的副本数只有两个,有一部分数据的副本数只有两个。那么现在你必须马上要在系统里面重新选一台机器出来,它上面有足够的空间,让我现在只有两个副本的数据重新再做一份新的复制,系统始终维持在三个副本。整个系统里面如果机器挂掉了,副本数少了,这个时候应该会被自动发现,马上补充新的副本,这样会维持整个系统的副本数。这是很重要的 ,为了避免数据丢失,必须维持足够的副本数,因为副本数每少一个,你的风险就会再增加。这就是 Placement Driver 做的事情。
同时,Placement Driver 还会根据性能负载,不断去 move 这个 data 。比如说你这边负载已经很高了,一个磁盘假设有 100G,现在已经用了 80G,另外一个机器上也是 100G,但是他只用了 20G,所以这上面还可以有几十 G 的数据,比如 40G 的数据,你可以 move 过去,这样可以保证系统有很好的负载,不会出现一个磁盘巨忙无比,数据已经多的装不下了,另外一个上面还没有东西,这是 Placement Driver 要做的东西。


Raft 协议还提供一个很高级的特性叫 leader transfer。leader transfer 就是说在我不移动数据的时候,我把我的 leadership 给你,相当于从这个角度来讲,我把流量分给你,因为我是 leader,所以数据会到我这来,但我现在把 leader 给你,我让你来当 leader,原来打给我的请求会被打给你,这样我的负载就降下来。这就可以很好的动态调整整个系统的负载,同时又不搬移数据。不搬移数据的好处就是,不会形成一个抖动。


MySQL Sharding


MySQL Sharding 我前面已经提到了它的各种天花板,MySQL Sharding 的方案很典型的就是解决基本问题以后,业务稍微复杂一点,你在 sharding 这一层根本搞不定。它永远需要一个 sharding key,你必须要告诉我的 proxy,我的数据要到哪里找,对用户来说是极不友好的,比如我现在是一个单机的,现在我要切入到一个分布式的环境,这时我必须要改我的代码,我必须要知道我这个 key ,我的 row 应该往哪里 Sharding。如果是用 ORM ,这个基本上就没法做这个事情了。有很多 ORM 它本身假设我后面只有一个 MySQL。但 TiDB 就可以很好的支持,因为我所有的角色都是对的,我不需要关注 Sharding、分库、分表这类的事情。


这里面有一个很重要的问题没有提,我怎么做 DDL。如果这个表非常大的话,比如说我们有一百亿吧,横跨了四台机器,这个时候你要给它做一个新的 Index,就是我要添加一个新的索引,这个时候你必须要不影响任何现有的业务,实际上这是多阶段提交的算法,这个是 Google 和 F1 一起发出来那篇论文。


简单来讲是这样的,先把状态标记成 delete only ,delete only 是什么意思呢?因为在分布式系统里面,所有的系统对于 schema 的视野不是一致的,比如说我现在改了一个值,有一部分人发现这个值被改了,但是还有一部分人还没有开始访问这个,所以根本不知道它被改了。然后在一个分布系统里,你也不可能实时通知到所有人在同一时刻发现它改变了。比如说从有索引到没有索引,你不能一步切过去,因为有的人认为它有索引,所以他给它建了一个索引,但是另外一个机器他认为它没有索引,所以他就把数据给删了,索引就留在里面了。这样遇到一个问题,我通过索引找的时候告诉我有, 实际数据却没有了,这个时候一致性出了问题。比如说我 count 一个 email 等于多少的,我通过 email 建了一个索引,我认为它是在,但是 UID 再转过去的时候可能已经不存在了。


比如说我先标记成 delete only,我删除它的时候不管它现在有没有索引,我都会尝试删除索引,所以我的数据是干净的。如果我删除掉的话,我不管结果是什么样的,我尝试去删一下,可能这个索引还没 build 出来,但是我仍然删除,如果数据没有了,索引一定没有了,所以这可以很好的保持它的一致性。后面再类似于前面,先标记成 write only 这种方式, 连续再迭代这个状态,就可以迭代到一个最终可以对外公开的状态。比如说当我迭代到一定程度的时候,我可以从后台 build index ,比如说我一百亿,正在操作的 index 会马上 build,但是还有很多没有 build index ,这个时候后台不断的跑 map-reduce 去 build index ,直到整个都 build 完成之后,再对外 public ,就是说我这个索引已经可用了,你可以直接拿索引来找,这个是非常经典的。在这个 Online, Asynchronous Schema Change in F1 paper 之前,大家都不知道这事该怎么做。


Proxy Sharding 的方案不支持分布式事务,更不用说跨数据中心的一致性事务了。 TiKV 很好的支持 transaction,刚才提到的 Raft 除了增加副本之外,还有 leader transfer,这是一个传统的方案都无法提供的特性。以及它带来的好处,当我瞬间平衡整个系统负载的时候,对外是透明的, 做 leader transfer 的时候并不需要移动数据, 只是个简单的 leader transfer 消息。


然后说一下如果大家想参与我们项目的话是怎样的过程,因为整个系统是完全开源的,如果大家想参与其中任何一部分都可以,比如说我想参与到分布式 KV,可以直接贡献到 TiKV。TiKV 需要写 Rust,如果大家对这块特别有激情可以体验写 Rust 的感觉 。


TiDB 是用 Go 写的,Go 在中国的群众基础是非常多的,目前也有很多人在贡献。整个 TiDB 和TiKV 是高度协作的项目,因为 TiDB 目前还用到了 etcd,我们在和 CoreOS 在密切的合作,也特别感谢 CoreOS 帮我们做了很多的支持,我们也为 CoreOS 的 etcd 提了一些 patch。同时,TiKV 使用 RocksDB ,所以我们也为 RocksDB 提了一些 patch 和 test,我们也非常感谢 Facebook RocksDB team 对我们项目的支持。


另外一个是 PD,就是我们前面提的 Placement Driver,它负责监控整个系统。这部分的算法比较好玩,大家如果有兴趣的话,可以去自己控制整个集群的调度,它和 k8s 或者是 Mesos 的调度算法是不一样的,因为它调度的维度实际上比那个要更多。比如说磁盘的容量,你的 leader 的数量,你的网络当前的使用情况,你的 IO 的负载和 CPU 的负载都可以放进去。同时你还可以让它调度不要跨一个机房里面建多个副本。


原文链接