[译] fasthttp 文档手册

davidcai1993 发表了文章 • 2 个评论 • 1220 次浏览 • 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 个评论 • 662 次浏览 • 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 个评论 • 488 次浏览 • 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的结果到数据库,这样你可以在每一次代码提交之后查看代码的性能。

[QingCloud Insight 2016] How do we build TiDB

qiuyesuifeng 发表了文章 • 0 个评论 • 385 次浏览 • 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 的负载都可以放进去。同时你还可以让它调度不要跨一个机房里面建多个副本。


原文链接

[CDAS 2016] 分布式数据库模式与反模式

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

TiDB 是一个 OLTP 的数据库,我们主要 focus 在大数据的关系型数据库的存储和可扩展性,还有关系的模型,以及在线交易型数据库上的应用。所以,今天整个数据库的模式和反模式,我都会围绕着如何在一个海量的并发,海量的数据存... 查看全部


TiDB 是一个 OLTP 的数据库,我们主要 focus 在大数据的关系型数据库的存储和可扩展性,还有关系的模型,以及在线交易型数据库上的应用。所以,今天整个数据库的模式和反模式,我都会围绕着如何在一个海量的并发,海量的数据存储的容量上,去做在线实时的数据库业务的一些模式来讲。并从数据库的开发者角度,来为大家分享怎样写出更加适合数据库的一些程序。



基础软件的发展趋势


先简单介绍一下,现在我认为的一些基础软件上的发展趋势。


开源


第一点,开源是一个非常大的趋势。大家可以看到一些比较著名的基础软件,基本都是开源的,比如 Docker,比如 k8s。甚至在互联网公司里面用的非常多的软件,像 MySQL、Hadoop 等这种新一代的大数据处理的数据库等基础软件,也大多是开源的。其实这背后的逻辑非常简单:在未来其实你很难去将你所有的技术软件都用闭源, 因为开源会慢慢组成一个生态,而并不是被某一个公司绑定住。比如国家经常说去 IOE,为什么?很大的原因就是基本上你的业务是被基础软件绑死的,这个其实是不太好的一个事情。而且现在跟过去二十年前不一样,无论是开源软件的质量,还是社区的迭代速度,都已经是今非昔比,所以基本上开源再也不是低质低量的代名词,在互联网公司已经被验证很多次了。


分布式


第二,分布式会渐渐成为主流的趋势。这是为什么?这个其实也很好理解,因为随着数据量越来越大,大家可以看到,随着现在的硬件发展,我感觉摩尔定律有渐渐失效的趋势。所以单个节点的计算资源或者计算能力,它的增长速度是远比数据的增长速度要慢的。在这种情况下,你要完成业务,存储数据,要应对这么大的并发,只有一种办法就是横向的扩展。横向的扩展,分布式基本是唯一的出路。scale-up 和 scale-out 这两个选择其实我是坚定的站在 scale-out 这边。当然传统的关系数据库都会说我现在用的 Oracle,IBM DB2,他们现在还是在走 scale-up 的路线,但是未来我觉得 scale-out 的方向会渐渐成为主流。


碎片化


第三,就是整个基础软件碎片化。现在看上去会越来越严重。但是回想在十年前、二十年前,大家在写程序的时候,我上面一层业务,下面一层数据库。但是现在你会发现,随着可以给你选择的东西越来越多,可以给你在开源社区里面能用到的组件越来越多,业务越来越复杂,你会发现,像缓存有一个单独的软件,比如 redis,队列又有很多可以选择的,比如说 zeromq,rabbitmq,celery 各种各样的队列;数据库有 NoSQL、HBase,关系型数据库有 MySQL、PG 等各种各样的基础软件都可以选。但是就没有一个非常好东西能够完全解决自己的问题。所以这是一个碎片化的现状。


微服务


第四,是微服务的模式兴起。其实这个也是最近两年在软件架构领域非常火的一个概念。这个概念的背后思想,其实也是跟当年的 SOA 是一脉相承的。就是说一个大的软件项目,其实是非常难去 handle 复杂度的,当你业务变得越来越大以后,维护成本和开发成本会随着项目的代码量呈指数级别上升的。所以现在比较流行的就是,把各个业务之间拆的非常细,然后互相之间尽量做到无状态,整个系统的复杂度可以控制,是由很多比较简单的小的组件组合在一起,来对外提供服务的。
这个服务看上去非常美妙,一会儿会说有什么问题。最典型的问题就是,当你的上层业务都拆成无状态的小服务以后,你会发现原有的逻辑需要有状态的存储服务的时候你是没法拆的。我所有的业务都分成一小块,每一小块都是自己的数据库或者数据存储。比如说一个简单的 case,我每一个小部分都需要依赖同一个用户信息服务,这个信息服务会变成整个系统的一个状态集中的点,如果这个点没有办法做弹性扩展或者容量扩展的话,就会变成整个系统很致命的单点。


所以现在整个基础软件的现状,特别在互联网行业是非常典型的几个大的趋势。我觉得大概传统行业跟互联网行业整合,应该在三到五年,这么一个时间。所以互联网行业遇到的今天,可能就是传统行业,或者其他的行业会遇到的明天。所以,通过现在整个互联网里面,在数据存储、数据架构方面的一些比较新的思想,我们就能知道如何去做这个程序的设计,应对明天数据的量级。


现有存储系统的痛点


其实今天主要的内容是讲存储系统,存储系统现在有哪些痛点?其实我觉得在座的各位应该也都能切身的体会到。


弹性扩展


首先,大数据量级下你如何实现弹性扩展?因为我们今天主要讨论的是 OLTP ,是在线的存储服务,并不是离线分析的服务。所以在线的存储服务,它其实要做到的可用性、一致性,是要比离线的分析业务强得多的。但是在这种情况下,你们怎样做到业务无感知的弹性扩展,你的数据怎么很好的满足现有的高并发、大吞吐,还有数据容量的方案。


可用性


第二,在分布式的存储系统下,你的应用的可用性到底是如何去定义,如何去保证?其实这个也很好理解,因为在大规模的分布式系统里面,任何一个节点,任何一个数据中心或者支架都有可能出现硬件的故障,软件的故障,各种各样的故障,但这个时候你很多业务是并没有办法停止,或者并没有办法去容忍 down time 的。所以在一个新的环境之下,你如何对你系统的可用性做定义和保证,这是一个新的课题。一会儿我会讲到最新的研究方向和成果。


可维护性


第三,对于大规模的分布式数据库来说它的可维护性,这个怎么办?可维护性跟单机的系统是明显不同的,因为单机的数据库,或者传统的单点的数据库,它其实做到主从,甚至做到一主多从,我去维护 master ,别让它挂掉,这个维护性主要就是维护单点。在一个大规模的分布式系统上,你去做这个事情是非常麻烦的。可以简单说一个案例,就是 Google 的 Spanner。Spanner 是 Google 内部的一个大规模分布式系统,整个谷歌内部只部署了一套,在生产环节中只部署了一套。这一套系统上有上万甚至上数十万的物理节点。但是整个数据库的维护团队,其实只有很小的一组人。想像一下,上十万台的物理节点,如果你要真正换一块盘、做一次数据恢复或者人工运维的话,这是根本不可能做到的事情。但是对于一个分布式系统来说,它的可维护性或者说它的维护应该是转嫁给数据库自己。


开发复杂度


还有,就是对于一个分布式数据库来说,它在开发业务的时候复杂度是怎么样的。大家其实可能接触的比较多的,像 Hbase、Cassandra、Bigtable 等这种开源的实现,像 NoSQL 数据库它其实并没有一个很好的 cross-row transaction 的 support。另外,对于很多的 NoSQL 数据库并没有一个很好的 SQL 的 interface,这会让你写程序变得非常麻烦。比如说对于一些很普通的业务,一个表,我需要去 select from table,然后有一个 fliter 比如一个条件大于 10,小于 100,这么简单的逻辑,如果在 HBase 上去做的话,你要写十行、二十行、三十行;如果你在一个关系的数据库,或者支持 SQL 的数据库,其实一行就搞定了。其实这个对于很多互联网公司来说,在过去的几年之内基本上已经完成了这种从 RDBMS 到 NoSQL 的改造,但是这个改造的成本和代价是非常非常高的。比如我原来的业务可能在很早以前是用 MySQL 已经写的稳定运行好久了,但是随着并发、容量、可扩展性的要求,我需要迁移 Bigtable、Hbase、Cassandra、MongoDB 这种 NoSQL 数据库上,这时基本上就要面临代码的完整重写。这个要放在互联网公司还可以,因为它们有这样的技术能力去保证迁移的过程。反正我花这么多钱,招这么牛的工程师,你要帮我搞定这个事情。但是对于传统的行业,或者传统的机构来说,这个基本上是不可能的事情。你不可能让他把原来用 Oracle 用SQL 的代码改成 NoSQL 的 code。
因为 NoSQL 很少有跨行事务,首先你要做一个转账,你如果不是一个很强的工程师,你这个程序基本写不对,这是一个很大的问题。这也是为什么一直以来像这种 NoSQL 的东西并没有很好的在传统行业中去使用的一个最核心的原因,就是代价实在太大。


存储系统的扩展模型


所以其实在去讲这些具体到底该怎么解决,或者未来数据库会是什么样的之前,我想简单讲一下扩展的模型。对于一个关系型数据库也好,对于存储的系统本身也好,它的扩展模型有哪些。


Sharding 模式


第一种模式是 Sharding 模式。如果在座的各位有运维过线上的 MySQL 的话,对这个模型会非常熟悉。最简单的就是分库、分表加中间件,就是说我不同的业务可能用不同的库,不同的表。当一个单表太大的时候,我通过一些 Cobar、Mycat 等这样的数据库中间件来去把它分发到具体的数据库的实例上。这种模型是目前用的最普遍的模型,它其实也解决了很大部分的问题。为什么这十年在关系型数据库上并没有很好的扩展方案,但是大家看上去这种业务还没有出现死掉的情况,就是因为后面有各种各样 Sharding 的中间件或者分库分表这种策略在硬扛着。像这种中间件 Sharding 第一个优势就是实现非常简单。你并不需要对数据库内做任何的改造,你也并不需要去比如说从你原来的 SQL 代码转到 NoSQL 的代码。
但是它也有自己的缺点。首先,对你的业务层有很强的侵入性。这是没有办法的,比如你想用一个中间件,你就需要给它指定一个 Sharding key。另外,原来比如你的业务有一些 join ,有一些跨表跨行的事务,像这种事务你必须得改掉,因为很多中间件并没有办法支持这个跨 shard 的分布式 join。
第二个比较大的缺陷是它的分片基本是固定的,自动化程度、扩展性都非常差,你必须得有一个专职的 DBA 团队给你的 MySQL 或者 PG 的 Sharding 的集群去做运维。我之前在豌豆荚做过一段时间 MySQL cluster 的分片的维护工作。当时我记得是一个 16 个节点的 MySQL 的集群,我们需要扩展到 32 个节点的规模,整整提前演练了一个月,最后上线了一个礼拜。上线那个礼拜,晚上基本上没有办法睡觉,所以非常痛苦。
再说一个 Google 的事情,Google 在刚才我说的 Spanner 和 F1 这两个数据库没有上线之前,Google 的广告系统的业务是由 100 多个节点的 MySQL 的集群对外提供服务的。如 Google 这么牛的公司,在维护一百多个节点的 MySQL Sharding 的数据库的时候,都已经非常痛苦,宁可重新去写一个数据库,也不想去维护这个 datebase cluster。其实大家可以看到,像这种 Sharding 的方案,它比较大的问题就是它的维护代价或者维护集群的复杂度,并不是随着节点数呈线性增长,而是随着节点的增加非线性的增长上去。比如你维护 2 个节点的还好,维护 4 个节点的也还可以,但是你维护 16 个、64 个、128 个基本就是不可能的事情。
第三就是一些复杂的查询优化,并没有办法在中间件这一层,去帮你产生一个足够优化的执行计划,因此,对于一些复杂查询来说,Sharding 的方案是没法做的。所以对你的业务层有很高的要求。


Region Base 模型


第二种扩展模型是 Region Base。这张图是我项目里面扒出来的图。
分布式数据库模式与反模式 图1.png-296.9kB
它整个思路有点像 Bigtable,它相当于把底下的存储层分开,数据在最底层存储上已经没有表、行这样结构的划分,每一块数据都由一个固定的 size 比如 64 M、128 M 连续的 Key-value pairs 组成。其实这个模型背后最早的系统应该是谷歌在 06 年发表的 Bigtable 这篇论文里面去描述的。这个模型有什么好处呢?一是它能真正实现这种弹性的扩展。第二个,它是一个真正高度去中心化。去中心化这个事情,对于一个大的 Cluster 来说是一个非常重要的特性。
还有一个优势,在 KV 层实现真正具有一定的自动 Failover 的能力。 Failover指的是什么呢?比如说在一个集群比较大的情况下,或者你是一个 cluster,你任何一个节点,任何一个数据损坏,如果能做到业务端的透明,你就真正实现了 Auto-Failover 的能力。其实在一些对一致性要求不那么高的业务里面,Auto-Failover 就是指, 比如在最简单的一个 MySQL 组从的模型里,当你的组挂掉了以后,我监控的程序自动把 slave 提上来,这也是一种 Failover 的方式。但是这个一致性或者说数据的正确性并不能做到很好的保证。你怎么做到一致性的 Auto-Failover,其实背后需要做非常非常多的工作。
这是 Region 模型的一些优势。但是它的劣势也同样明显,这种模型的实现非常复杂。我一会儿会说到背后的关键技术和理论,但是它比起写中间件真的复杂太多了。你要写一个能用的 MySQL 或者 PG 的中间件,可能只需要一两个工程师,花一两周的时间就能写出一个能用的数据库中间件;但是你如果按照这个模型做一个弹性扩展的数据库的话,你的工作量就会是数量级的增加。
第二个劣势就是它业务层的兼容性。像 Region Base 的模型,最典型的分布式存储系统就是 HBase。HBase 它对外的编程接口和 SQL 是千差万别,因为它是一个 Key Value 的数据库。你的业务层的代码兼容性都得改,这个对于一些没有这么强开发能力的用户来说,是很难去使用的,或者它说没有 SQL 对于用户端这么友好。


可用性级别


我一会儿会讲一下,刚才我们由 Region Base 这个模型往上去思考的一些东西,在此之前先说一些可用性。高可用。其实说到高可用这个词,大多数的架构师都对它非常熟悉。我的系统是高可用的,任何一个节点故障业务层都不受影响,但是真的不受影响吗?我经过很多的思考得到的一个经验就是主从的模型是不可能保证同时满足强一致性和高可用性的。可能这一点很多人觉得,我主从,我主挂了,从再提起来就好,为什么不能保护这个一致性呢?就是因为在一个集群的环境下,有一种故障叫脑裂。脑裂是什么情况?整个集群是全网络联通的,但是出现一种情况,就是我只是在集群内部分成了两个互不联通的一个子集。这两个子集又可以对外提供服务,其实这个并不是非常少见的状况,经常会发生。像这种情况,你贸然把 slave 提起来,相当于原来的 master 并没有完全的被 shutdown,这个时候两边可能都会有读写的情况,造成数据非常严重的不一致,当然这个比较极端了。所以你会发现阿里或者说淘宝,年年都在说我们有异地多活。但是去年甚至前几个月,杭州阿里的数据中心光纤被挖断,支付宝并没有直接切到重复层,而是宁可停止服务,完全不动,也不敢把 slave 数据中心提起来。所以其实任何基于主从模型的异地多活方案都是不行的。这个问题有没有办法解决呢?其实也是有的。
还是说到 Google,我认为它才是全世界最大的数据库公司,因为它有全世界最大的数据量。你从来没有听说过 Google 哪一个业务因为哪一个数据中心光纤挖断,哪一个磁盘坏了而对外终止服务的,几乎完全没有。因为 Google 的存储系统大多完全抛弃了基于主从的一致性模型。它的所有数据都不是通过主从做复制的,而是通过类似 Raft 或者 Paxos 这种分布式选举的算法做数据的同步。这个算法的细节不展开了,总体来说是一个解决在数据的一致性跟自动的数据恢复方面的一个算法。同时,它的 latency 会比多节点强同步的主从平均表现要好的一个分布式选举的算法。
在 Google 内部其实一直用的 Paxos,它最新的 Spanner 数据库是用 Paxos 做的 replication 。在社区里面,跟 Paxos 等价的一个算法就是 Raft。Raft 这个算法的性能以及可靠性都是跟 Paxos 等价的实现。这个算法就不展开了。我认为这才是新一代的强一致的数据库应该使用的数据库复制模型。


分布式事务


说到事务。对于一个数据库来说,我要做传统的关系型数据库业务,事务在一个分布式环境下,并不像单机的数据库有这么多的方法,这么多的优化。其实在分布式事务这个领域只有一种方法,并且这么多年了从分布式事务开始到现在,在这个方法上并没有什么突破,基本只有一条出路就是两阶段提交。其实可以看一下 Google 的系统。对于我们做分布式系统的公司来说,Google 就是给大家带路的角色。Google 最新的数据库系统上它使用的分布式事务的方法仍然是两阶段提交。其实还有没有什么优化的路呢?其实也是有的。两阶段提交最大的问题是什么呢?一个是延迟。因为第一阶段先要把数据发过去,第二阶段要收到所有参与的节点的 response 之后你才能去 commit 。这个过程,相当于你走了很多次网络的 roundtrip,latency 也会变得非常高。所以其实优化的方向也是有的,但是你的 latency 没法优化,只能通过吞吐做优化,就是 throughput 。比如说我在一万个并发的情况下,每个用户的 latency 是 100 毫秒,但是一百万并发,一千万并发的时候,我每个用户的 latency 还可以是 100 毫秒,这在传统的单点关系型数据库上,是没有办法实现的。第二就是去中心化的事务管理器。另外没有什么东西是银弹,是包治百病的,你要根据你的业务的特性去选择合适的一致性算法。


NewSQL


其实刚刚这些 pattern 会发展出一个新的类别,我们能不能把关系数据库上的一些 SQL、Transaction 跟 NoSQL 跟刚才我说到的 Region Base 的可扩展的模型融合起来。这个思想应该是在 2013 年左右的时候,学术界提出来比较多的东西,NewSQL。NewSQL 首先要解决 Scalability 的问题, 刚给我们说过 scalability 是一个未来的数据库必须要有的功能,第二个就是 SQL,SQL 对于业务开发者来说是很好的编程的接口。第三,ACID Transaction,我希望我的数据库实现转帐和存钱这种强一致性级别的业务。第四,就是整个 cluster 可以支持无穷大的数据规模,同时任何数据节点的宕机、损坏都需要集群自己去做监控,不需要 DBA 的介入。


案例:Google Spanner / F1


有没有这样的系统?其实有的。刚才一直提到 Google 的 Spanner 系统。Spanner 系统是在 2012 年底于 OSDI 的会议上发布了论文; F1 这篇论文在 2013 年的 VLDB 发布的,去描述了整个 Google 内部的分布式关系型数据库的实现。首先,根据 Spanner 的论文 Spanner 和 F1 在生产环境只有一个部署,上万物理节点遍布在全球各种数据中心内,通过 Paxos 进行日志复制。第二,整个架构是无状态的 SQL 层架在一个 NoSQL 的基础之上。第三,它对外提供的是一个跨行事务的语义,这个跨行事务是透明的跨行事务,我不需要对我的业务层做修改,它是通过 一个硬件支持的 Truetime API,GPS 时钟和原子钟去实现事务的隔离级别。这个系统是最早用来支撑 Google 的在线广告业务,在线广告业务大家知道,其实对于扣费、广告计费、点击记录,广告活动等一致性的级别要求非常高。首先这个钱不能多扣,也不能少扣,实时性要求非常高。而且业务逻辑相当复杂,这个时候需要一个 SQL 数据库,需要一个支持事务的数据库,需要一个支持 Auto-Failover 的数据库,所以就有了 Google Spanner/F1 这个系统。


典型业务场景


最典型的业务场景是什么呢?对于这种高吞吐、大容量的数据量级对于 NoSQL 系统来说,典型的应用的 Pattern 就是高吞吐大容量,还有就是 Workload 相对比较分散的情况。比较典型的反例是秒杀,秒杀这个场景其实非常不适合这种 NewSQL。另外就是一致性、可用性和 latency 之间怎么做取舍。在这种分布式数据库上面,第一个要丢弃的就是延迟,在这么大规模的量级上做强一致性,延迟是首先是不能保证的。你去看谷歌的 F1 和 Spanner 的论文里面,它提到它们的 latency 基本都是 10 毫秒、20 毫秒、甚至50 毫秒、100 毫秒的量级,但是它并不会太关心 latency 。因为它必须保证通过 Paxos 去做跨机房、多机房的复制,光速你肯定没法超越,所以这个时间是省不了的,它整个系统是面向吞吐设计的,而不是面向低延迟设计的,但是它要求非常强的一致性。


MySQL Sharding


一个典型的场景就是替换 MySQL Sharding 的业务。它的业务典型的特点,就是高吞吐,海量并发的小事务,并不是特别大的 transection,模型也相对简单,没有复杂的 join 的程序。但是痛点刚才提到了非常明显,首先就是对于 MySQL Sharding 方案 scale 能力很差,对于表结构变更的方案并没有太多;再者,cross shard transaction,目前 Sharding 的方案并没有办法很好支持,但 NewSQL 面向的场景和 MySQL Sharding 面向的场景是非常的像的。


Cross datacenter HA


第二种场景是 Cross datacenter HA(跨数据中心多活),这种场景简直是数据开发者追求的圣杯。因为像 Oracle、DB2文,它在最关键,最核心的业务上,比如说像银行这种业务,它必须要求实现这种跨数据中心的高可用。但是在一个分布式的数据库上,或者说是 Open-source solution 里,有没有人有办法实现这个 Cross datacenter HA 呢?完全没有。目前来说并没有任何一个数据库能去解决这个问题。因为刚才提到了主从的模型根本是不可靠的。像这种业务的数据极端的重要,任何一点数据的丢失都不能容忍。另外即使整个数据中心宕机也不能影响线上的业务,很典型的像支付宝,这种涉及钱相关的,甚至有些比如你的银行卡,你肯定不能容忍说你吃完饭,告诉你不好意思数据中心挂了,你的钱我刷不了。但是并没有开源的数据库能解决这个问题,如果你真的自己做同步复制的方案的话,特别两地三中心的情况,你请求的延迟是取决于离你最远的数据中心,所以大部分业务来说延迟过大。 另外就是这些系统重度依赖人工运维。我认为一旦任何系统有人工的介入一定是会出错的。因为人是最难自动化的一个因素。


反模式


滥用传统关系模型


对于这种大规模的分布式 NewSQL 来说,比较大的反模式就是,大量的使用存储过程 foreign key,视图等操作。因为首先存储过程是没有办法 scale 的。另外,大表与大表的 JOIN ,在线上的 OLTP 数据库上最大的开销并不是优化器的开销,而是网络通信的开销。比如两个表去做一个笛卡儿积,算起来可能并不是这么慢,但是要把数据一条一条在网络中传输代价是非常大的,这种情况下用 OLAP 的数据库是比较适合。


没有利用好并发


刚才我一直强调对于新型的 OLTP 或者分布式关系型数据库来说,并发或者说吞吐才是应该追求的优化点。在架构设计时,不应该把对延迟非常敏感的业务去使用分布式数据库来实现。所以其实在延迟跟吞吐之间,数据库永远去选择吞吐。所以写程序会遇到的一个很典型的问题,就是我的查询是一行一行,上下之间有这种强依赖关系的模式。其实在这种模式下,对于分布式关系型数据库来说是非常不合适的。所以你应该用并发的思想去做,比如说我的请求,我可以把吞吐打到整个集群上,我的 Database,我的 cluster 会自动 balance 一些 workload,如果一行行查询之间是有依赖的话,那么每一条查询之间的 latency 是会叠加起来。所以这个其实并不是一个太好的 pattern 。


不均匀的设计


数据库其实经常会被大家滥用。比如说我看到很多传统行业数据库使用的方法,其实真的是非常痛苦。无论什么业务我全都直接上 Oracle,不管这个业务是否真的需要 Database 来去支持;比如有人用 MySQL 做计数器,有些有 MySQL 做队列或者做秒杀,这个并不是太 scale 的东西。特别像秒杀的业务,特别高的并发打到同一个 key 上,workload 没办法去分拆到好多个节点来帮你去处理,所以整个分布式的集群就会退化成一个单点,这是非常不合理的。第二,当你的索引设计的不太好的时候,涉及到的过多全表扫描是非常不划算的。刚刚也说过,在一个分布式集群里面,最大的开销是网络的开销,你做全表扫描的时候并不是在一台机器上,而是在好多机器上做全表扫描,同时经过很大的网络传输,当你的索引设计不合理的时候会出现这样的问题。还有是过多的无效索引,会导致整个系统在写入的时候有延迟也好或者吞吐也好,会变得更慢。主要注意的就是不要存在任何可以把你的系统退化成单点的机会。


错误的一致性模型


还有一种最容易犯的问题,就是在没有好好思考你的业务适合的场景情况下,就去使用很强的一致性模型。因为当你要求一个非常高的强一致性的时候,你的分布式事务的 latency 一定比不这么强的一致性的业务要高得多的。其实,很多业务并不需要那么强的一致性,我的数据库虽然给你了这个能力,但如果你去滥用的话,你会说这个数据库好慢,或者为什么我这个请求 100 毫秒才给我返回。但其实很多场景下,你并不是这么强的依赖强一致性 。这个跟数据库本身提不提供这个机制是没有关系的。作为一个 Database 开发者来说,我还是需要给你能实现这个功能的机制,但是不要滥用。
另外根据你业务的冲突的频繁程度选择不同的锁策略。你知道这个业务就是一个很高的冲突的场景,比如说可能像类似发工资,一个集团的账号发到成千上万的小账号。如果你是一个乐观事务的话,可能涉及到的冲突就会很大。因为所有的事务的发起者都要去修改这个公共账号,这种情况下一般可以使用悲观事务,因为你已经预知到你业务的 conflict 级别会很高。但当我知道我的业务的冲突很小,我要追求整个系统的吞吐,我可能会选用乐观的事务的模型。


Q&A


问:对于秒杀这个场景,什么样的数据库设计是合适的?



黄东旭:其实秒杀这个业务是一个比较系统的工程,并不是一个简单的我用什么数据库就可以做的东西。我之前给一些朋友做秒杀的架构设计的时候,从最上层,甚至你可能从 JS 这边就要去做排队,一层一层的排队。比如在缓存这边我可以用 redis,我可以用一些缓存的数据库再去进行二级的排队,因为每一次排队你都会丢掉很多,最后到底下的队列,在数据库这一层,比如库存就十个,你只要在前面放十个进来就行了。就是说,你在上层把流量用排队的方式做更好。最典型的例子就是大家看 12306 卖火车票的时候经常会有排队。基本没有什么数据库能去应对,像淘宝,像京东这种秒杀的业务,它从上到下是整个系统的过程。谢谢。



问:分布式的 OLTP 数据库,历史数据怎么清理,如果后续还需要分析数据的话。



黄东旭:其实在谷歌 Spanner 包括像 TiDB 、TiKV 这个系统之上,是通过一种 MVCC 的技术,每一条数据都会有一个版本号。比如说你不停的 update,它可能会产生好多版本。我背后会有一个 GC 的逻辑,去选定一个 safe point,在这个 safe point 之前的所有数据全都删掉也好,还是挪到冷的存储上也好,我们是通过这种方式实现的。另外随着数据量的膨胀,整个系统设计的时候,我们是通过 Raft 算法,比如说这个数据慢慢长大,我会把它切成两块,再慢慢 Auto-balance 到其它的节点上,然后新的那一块会再长大......这相当于一个细胞一样,一个细胞分裂成两个细胞,成千上万的细胞会均匀的分布在整个集群里面。如果你要删数据可以直接删,如果你不想删的话,你的数据可以全都留下,你自己随便往集群里增加新的节点。这个集群很大,它会自动帮你把数据均匀的分到这儿。所有的历史数据是通过 GC 的模块把历史的版本拿出来,丢到冷的存储上。
在 Google 几乎是完全不删数据的,因为存储的成本是很低的,但是数据未来说不定什么时候就用了。大概是这样,我需要有一个办法能把它存下来。



问:当前做的交易要使用历史的参考数据,可能量不是很大,有这样的例子吗?



黄东旭:当然有。像 MVCC 在访问历史,它其实提供了一个 Lock-free 的 Snapshot Read, 它在读历史版本的时候是不会读线上的正在进行的读写事务,所以它是有一个无所读的机制,这也是为什么它要去采用每一个数据都要有版本号的机制去实现。它是有这个需求,而且在实现的时候也非常漂亮,不会阻塞其他的请求。



问:TiDB 是怎么考虑用 MySQL 或者 PG 的?当时是怎么考虑的?



黄东旭:首先我们没有用 MySQL 或者 PG,刚才说的 TiDB 这个模型是谷歌的 Spanner 跟 F1 的模型。用传统的单机数据库做改造的话,有一个比较大的误区,整个 SQL 的查询优化器跟存储的数据结构,并不知道你底层是分布式的存储,它还是假设你生成 SQL 的方案都是单机的。比如最简单的例子,我需要去 count ,如果是一个单机的 MySQL 优化器,没有建索引的话会一行一行把数据拉过来计一个数,这样算下去;但是如果对一个分布式系统来说,我只要把 count 这个逻辑推到所有存储表的数据节点上,算法再 reduce 回来就可以,更像一个 Mapreduce 这种分布式框架的 SQL 优化器。如果你没有从底到上完整的去实现 Database 的话,你很难对分布式的场景或者分布式数据存储的这些性质来去对数据库做优化。没有办法,这是一条很难的道路,但是我们也得去下走。



原文链接

云时代数据库的核心特点

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

引言

最近几年,随着云计算相关技术的发展,各种不同类型的云层出不穷,服务越来越多不同类型的企业业务,传统企业也渐渐开始探索上云的道路。在云上,作为业务最核心的数据库,相比之前的传统方案会有哪些变化呢?在正式... 查看全部

引言


最近几年,随着云计算相关技术的发展,各种不同类型的云层出不穷,服务越来越多不同类型的企业业务,传统企业也渐渐开始探索上云的道路。在云上,作为业务最核心的数据库,相比之前的传统方案会有哪些变化呢?在正式聊云时代的数据库特点之前,我们需要了解一下目前云时代架构发生的变化。


畅想一下,未来的服务都跑在云端,任何的服务资源都可以像水电煤一样按需选购。从 IaaS 层的容器/虚拟机,到 PaaS 层的数据库,缓存和计算单元,再到 SaaS 层的不同类型的应用,我们只需要根据自身业务特点进行资源选配,再也不用担心应用服务支撑不住高速的业务增长,因为在云上一切都是弹性伸缩的。有了可靠的基础软件架构,我们就可以把更多精力放到新业务的探索,新模式的创新,就有可能产生更多不一样的新场景,从而催生更强大能力的云端服务,这是一件多么 cool 的事情。


当然,理想要一步一步实现,未来的基础软件栈到底会怎样呢?社区在这方面正在进行积极地探索,其中最有代表性的就是基于容器(以 Docker 为代表)的虚拟化技术和微服务(Microservice)。


在云时代,一切都应该是可伸缩的,使用 k8s(Kubernetes)在保证资源平衡的前提下,通过 Docker 部署我们依托于容器的微服务模块,我们不用关心服务到底跑在哪里,只需要关心我们需要多少服务资源。Docker 提供了极大的便利性,一次构建,到处运行,我们可以很好地解决开发、测试和上线的环境一致性问题。(如果不能很好地保证测试和实际上线环境的一致性,则很有可能需要花费远超过开发的时间去发现和修复问题。)k8s 更是在 Docker 构建的基础上增加了更多的云特性,包括 Docker 的升级,高可用和弹性伸缩等等。 关于 Docker/k8s 相关的讨论已经很多了,因为时间关系,关于具体的细节就不再展开。我们只需要了解,有了它,可以很轻松地解决服务的安装和部署。


下面再聊聊微服务,微服务将一个服务拆分成相对独立的更小的子服务单元,不同的子服务单元之间通过统一的接口(HTTP/RPC 等)进行数据交互。


相比于传统的解决方案,这种架构有很多的优点。



  • 更好的开发效率和可维护性。微服务将一个单独的服务进行更细力度的拆分,每一个子服务单元专注于更小的功能模块,可以更好地根据业务建立对应的数据模型,降低复杂度,使得开发变得更轻松,维护和部署变得更加友好.

  • 更好的可扩展性。每个不同的子服务单元相互独立,彼此之间没有任何依赖,所以可以根据业务的具体需要,灵活地部署多个子服务单元进行水平扩展。

  • 更强的容错性。当其中一个子服务出现故障的时候,可以通过辅助的负载均衡工具,自动路由到其他的子服务,不会影响整体服务的可用性.


当然,微服务也不是一个银弹,相对来说,这种方案会使整体系统的设计更加复杂,同时也加大了网络的延迟,对整个系统测试的复杂度也会更高。


Docker 提供的隔离型和可移植性,与微服务是一种天然的契合,微服务将整个软件进行拆分和解耦,而通过 Docker/k8s 可以很自然地做到独立的部署,高可用和容错性,似乎一切都可以完美地运转起来。但是真的是这样么?我们是不是忽略了什么?


是的,我们在讨论前面的问题的时候忽略了一个很重要的东西:状态。


从整个技术发展的角度来看,微服务是一个非常有意义的探索。每个人都期望着每个微服务的子服务都是无状态的,这样我可以自由地启停和伸缩,没有任何的心智负担,但是现实的业务情况是什么样的呢?比如一个电商网站,用户正在下单购买一件商品,此时平台是通过订单子服务的 A 应用来提供服务的,突然,因为机器故障,订单子服务的 A 应用不可用了,改由订单子服务的 B 应用提供服务,那么它是必须要知道刚才用户的订单信息的,否则正在访问自己订单页面的用户会发现自己的订单信息突然不见了。虽然我们尽量想把子服务设计成无状态的,但是很多时候状态都是不可避免的,我们不得不通过存储层保存状态,业界最主要的还是各种数据库,包括 RDBMS 和 NoSQL,比如使用 MySQL、MongoDB、HBase、Cassandra 等,特别是有些场景还要考虑数据一致性问题的时候,更加重了对存储层的依赖。


由此可见,云计算时代系统的架构发生了巨大的变化,这一方面为用户提供了更优秀的特性,另一方面也对云计算的组件提出了更高的要求。数据库作为云计算最基础的组件之一,也需要适应这种架构的变化。(这里我们主要关注 SQL 数据库,云时代的数据库以下简称云数据库。)


那么云数据库主要有一些什么样的特点呢?我认为主要有以下几点。


弹性伸缩


传统的数据库方案,常见的会选用 Oracle,MySQL,PostgreSQL。在云时代,数据量的规模有爆发性的增长,传统的数据库很容易遇到单机的存储瓶颈,不得不选用一些集群方案,常见的比如 Oracle RAC、 MySQL Sharding 等,而这些集群方案或多或少都有一些不令人满意的地方。


比如说,Oracle RAC 通过共享存储的硬件方案解决集群问题,这种方式基本上只能通过停机换用更大的共享内存硬件来解决扩容问题,RAC 节点过多会带来更多的并发问题,同样也会带来更高的成本。


以 MySQL Sharding 为代表的数据分片方案,很多时候不得不提前对数据量进行规划,把扩容作为很重要的一个计划来做,从 DBA 到运维到测试到开发人员,很早之前就要做相关的准备工作,真正扩容的时候,为了保证数据安全,经常会选择停服务来保证没有新的数据写入,新的分片数据同步后还要做数据的一致性校验。当然业界大公司有足够雄厚的技术实力,可以采用更复杂的方案,将扩容停机时间尽量缩短(但是很难缩减到 0),但是对于大部分中小互联网公司和传统企业,依然无法避免较长时间的停服务。


在云时代,理想中所有的资源都是根据用户业务需求按需分配的,服务器资源,应用容器资源,当然也包括数据库资源。添加或者减少新的数据库资源,完全就像日常吃饭那样稀疏平常,甚至用户基本感知不到。比如作为一个电商用户,在双 11 促销活动之前,可以通过增加数据库节点的方式,扩大更多的资源池,用来部署相应的容器服务,当活动结束之后,再将多余的资源移除去支持其他的服务,这样可以极大地提高资源的利用率,同样可以弹性地支撑各种峰值业务。


高可用


传统的 MySQL 方案,数据复制的时候默认采用异步的方式,对于一个写入的请求,主库写入成功后就会返回成功信息给客户端,但是这个时候数据可能还没有同步给从库,一旦主库这个时候挂掉了,启动从库的时候就会有丢失数据的风险。当然,也有人会选择半同步的复制方式,这种方式在正常情况下是同步的,但是在遇到数据压力比较大的时候,依然会退化为异步的方式,所以本质上来说,同样有丢失数据的风险。其他也有一些多主的同步方案,比如在应用层做数据同步,但是这种方式一是需要应用层的配合,二是在对网络超时的处理非常复杂,增加心智负担。


在云时代,因为所有的数据库资源都是分布式存储的,每个数据库节点出现问题都是很正常的事情,所以就必须有一种可以实现数据一致性的数据复制方式来保证服务的高可用,业界给出的答案就是:Paxos/Raft(关于 Paxos 和 Raft 的实现细节我们不在这里展开)。PingCAP 在做的 TiDB 就是选择了 Raft 协议,Raft 协议看起来更像是一个多副本的自适应的主从复制协议,对于每次写请求,Raft 都会保证大多数写成功才会返回客户端,即使 Raft Group的Leader 挂掉了,在一个有限的时间范围内,会很快地选出一个新的 Leader 出来,继续提供服务。同样,对于一个 3 副本的 Raft Group,只要 2 个写入成功,就可以保证成功,而大多数情况下,最先写入成功的往往是与 Leader 网络情况最好的那个副本,所以这种 Majority 写的方式,可以很自然地选择速度最快的副本进行数据同步复制。另外,Raft 协议本身支持 Config Change,增加一个新的节点,可以很容易地做副本数据分布的变更,而不需要停止任何服务。


同样,在云时代,数据库的 DDL 操作也会是一个非常有趣的事情。以一个常见的 Add Column 操作为例,在表规模已经很大的情况下,在传统的实现方案中,比较有参考意义的是,通过一些工具,创建类似表级别的触发器,将原表的数据同步到一个新的临时表中,当数据追平的时候,再进行一个锁表操作,将临时表命名为原表,这样一个 Add Column 操作就完成了。但是在云时代,分布式的数据存储方式决定了这种方案很难实现,因为每个数据库节点很难保证 Schema 状态变更的一致性,而且当数据规模增长到几十亿,几百亿甚至更多的时候,很短的阻塞时间都有可能会导致很大的负载压力变化,所以 DDL 操作必须是保证无阻塞的在线操作。值得欣慰的是,Google 的 F1 给我们提供了很好的实现参考,TiDB 即是根据 F1 的启发进行的研发,感兴趣的同学可以看下相关的内容。


易用透明

我们可以将云数据库想象成一个提供无限大容量的数据库,传统数据库遇到单机数据存储瓶颈的问题将不复存在。已有的程序基本上不怎么需要修改已有的代码,就可以很自然地接入到云数据库中来获得无限 Scale 的能力。增减数据库节点,或者节点的故障恢复,对于应用层来说完全透明。另外,云数据库的监控、运维、部署、备份等等操作都可以在云端通过高效的自动化工具来自动完成,极大地降低了运维成本。


多租户

云数据库本身应该是可以弹性伸缩的,所以很自然的,从资源利用率的角度来考虑,多个不同用户的数据库服务底层会跑在一个共享的云数据库中。因此多租户技术会成为云数据库的标配。但是这里面就有一个不得不面对的问题,如何做到不同用户的隔离性?用户数据隔离是相对比较容易的,比如还是以电商用户(这里说的是电商企业,不是顾客客户)为例,每个用户都有一个唯一的 ID,这样在云数据库的底层存储中,可以保证每个用户数据都带有自己 ID 前缀,用户登陆进来的时候可以根据这个前缀规则,获取他对应的数据,同时他看不到其他用户的数据。


在一个真实的多租户环境下面,纯粹的数据隔离往往是不够的,你还需要做到资源公平性的隔离。比如有的用户写一个 SQL,这个 SQL 没有做优化,主要做的事情是一个全表描扫,这个表的数据量特别特别大,这样他会吃掉很多的 CPU、Memory、IO 等资源,导致其他用户很轻量级的 SQL 操作都可能会变得很慢,影响到其他用户实际的体验。那么针对这种情况怎么做隔离?与此类似的还有,网络带宽怎么做隔离?大家都是跑在一个云数据库上面的,如果一个用户存放的数据特别大,他把带宽都吃掉了,别人就显得非常慢了。


还有一种情况,如果我本身作为一个租户,内部又怎么做隔离,大家知道 MySQL 可以建很多 Database,不同的 Database 给不同的团队来用,那么他们之间内部隔离又怎么做,这个问题就进一步更加复杂了。


目前来讲没有特别好的方法,在一个分布式的环境下面去做很好的隔离,有两个方向可以考虑:


第一种是最简单也是有效的方法,制定一些规则,把某些用户特别大的数据库表迁移到独享的服务器节点上面,这样就不会影响其他用户的服务,但是这里面就涉及到定制化的事情了,本身理念其实与云数据库并不相符。


第二种就是依靠统计信息,做资源隔离和调度,但是这里面对技术的要求就比较高了。因为云数据库是分布式的,所以一般的统计都要横跨很多的机器,因为网络原因,不可能做到完全准确的统计,所有统计都是有延迟的。比如说对于某个用户,现在统计到的流量是 1 个 G,他可能突然就有一次峰值的网络访问,可能下一次统计消耗的流量是 5 个 G(这里面只是举例说明问题),如果你给他流量限制是 1 个 G,中间统计的间隔是多少比较合适,如果间隔比较小,那么这个对整个系统的压力就比较大,可能影响正常的用户 SQL 访问,另外本身这个流量限制的系统也是很复杂的系统。


调度算法一直是整个分布式系统领域很困难的一个问题,如何做到隔离性和公平调度也是未来云数据库非常有挑战的一个事情。


低成本

低成本应该是云时代基础设施最明显的特点。首先,云数据库的高可用和容错能力,使得我们不再需要昂贵的硬件设备,只需要普通的 X86 服务器就可以提供服务。然后,受益于 Docker 的虚拟化技术,使得不同类型的应用容器可以跑在同一个物理机上,这样可以极大地提高资源的利用率。其次,多租户的支持,使得不同的用户可以共用一套底层的数据库存储系统,在数据库层面再一次提高了资源的利用效率。再次,云数据库的自动化运维工具,降低了整个核心数据库的运维成本。最后,云数据库资源是按需分配的,用户完全可以根据自身的业务特点,选购合适的服务资源。


高吞吐


云数据库虽然可以做到弹性扩容,但是本身是分布式存储的,虽然可以通过 Batch Write、Pipeline 和 Router Cache 等方式加快访问 SQL 请求的数据,但是相对传统单机的数据库来说,在数据访问链路上至少也要多走一次网络,所以大部分并发量不大的小数据量请求,都会比单机延迟要高一些。也就是说,当没有足够高的并发 SQL 访问的话,其实不能完全体现云数据库的性能优势,所以这也是我们在选用云数据库的时候需要认识到的问题,云数据库更多的是追求高吞吐,而不是低延迟。当并发大到一定规模,云数据库高吞吐特性就显现出来了,即使在很高的并发下,依然可以维持相当稳定的延迟,而不会像单机数据库那样,延迟线性增长。当然,延迟的问题,在合理的架构设计方案下,可以通过缓存的方式得到极大的缓解。


数据安全

云数据库的物理服务器分布在多个机房,这就为跨数据库中心的数据安全提供了最基础的硬件支持。谈到金融业务,大家耳熟能详的可能就是两地三中心,比如北京有两个机房,上海有一个。未来一切服务都跑在云上,金融类的业务当然也不例外。相比其他业务,金融类业务对数据安全要求就要高得多。当然,每个公司内部都有核心的业务,所以如果上云的话,也会有同样的强烈需要。这样,对云数据库来说,数据的一致性、分布式事务、跨数据中心的数据安全等更高端的需求有可能会日益强烈。常见的数据备份也有可能会被其他新的模式所取代或者弱化,比如基于 Paxos/Raft 的多副本方案,本身就保证了会有多份备份。


自动负载平衡

对于云数据库来说,负载平衡是一个很重要的问题,它直接决定了整个云数据库系统性能的好坏,如果一个数据库节点的数据访问过热的话,就需要考虑把数据迁移到其他的数据库节点来分担负载,不然就很容易出现性能瓶颈。整个负载平衡是一个动态的过程,调度算法需要保证资源配比的最大平衡,还有保证数据迁移的过程对系统整体的负载影响最小。这在未来也是云数据库需要解决的一个核心问题。


小结

从目前已有的 SQL 数据库实现方案来看,NewSQL 应该是最贴近于云数据库理念的实现。NewSQL 本身具有 SQL、ACID 和 Scale 的能力,天然就具备了云数据库的一些特点。但是,从 NewSQL 到云数据库,依然有很多需要挑战的难题,比如多租户、性能等。


上面提到的一些云数据库的特点,也是 PingCAP 目前在着力实现的部分,TiDB 作为国内第一个 NewSQL 的开源项目,在与社区的共同努力下,我们在上月底刚刚发布了 Beta 版本,欢迎各位上 GitHub 了解我们。


随着整个社区技术水平的发展和云时代新的业务需求的驱动,除了 PingCAP 的 TiDB,相信会有更多的团队在这方面进行探索, 期待早日看到云数据库成熟的那一天。


Q&A

问:由于客户数据环境复杂多样,在迁移到云端的时候怎么怎么做规划,以便后期统一运维管理?或者说,怎么把用户 SQL Server 或者 MongoDB 逐渐迁移到 TiDB 之类的分布式数据库?

崔秋:因为每个业务场景都不太相同,所以在选用云端服务的时候,首先要了解自身业务和云服务具体的优缺点。
如果你的业务本身比较简单,比如你之前用的 MongoDB,现在很多云服务厂商都会提供云端的 MongoDB 服务。这个时候你就要根据业务特点来做判断,如果 MongoDB 本身容量不大,远期的业务数据不会增长过快的话,这个时候其实你可以直接使用 MongoDB 的服务的。但是如果你本身的数据量比较大,或者数据增长比较快的话,就可能要考虑数据的扩容问题,MongoDB 在这方面做的不是太好。
你可以考虑 SQL 数据库的集群方案。比如 TiDB,它本身是支持弹性扩容,高并发高吞吐和跨数据库中心数据安全的,另外有一点明显的好处是 TiDB 兼容 MySQL 协议,所以如果你的应用程序是使用 MySQL,就基本上可以无缝地迁移到 TiDB,这方面是非常方便的。后续我们会提供常用的数据库迁移工具,帮用户把数据从 MongoDB/SQL Server 等平滑迁移到 TiDB 上面。
还是那个原则,不要为了上云而上云,一定要了解清楚自己的业务特点,云数据库会帮助你提供很多特性,如果真的很适用你的业务的话,就可以考虑。


问:但从产品的角度来看,云厂商提供的 RDS 产品是 Copy 客户数据库的思路,或者说是为了支持不同的数据库而支持。请问这种局面以后会有什么改变吗?

崔秋:现在确实蛮多云数据库服务其实就是在传统的 RDS 上面包了一层自动化的监控,运维和部署工具,就卖给用户提供服务了,但是实际上本身解决的仅仅是自动化管控的问题,云服务提供的数据库特性还是单机的 RDS,或者 RDS Sharing 的特性。如果本身底层的数据库满足不了你的需求的话,云服务也是满足不了的。
如果你需要不停服务的弹性扩容,单机的 RDS 服务肯定是搞不定的,RDS Sharing 也很难帮助你做到,这就对底层的数据库有了更高的要求,当然这方面是 TiDB 的强项了。
现在很多云上的 RDS 产品还远远没有达到理想中的云数据库的要求,不过随着社区的发展和业务需求的推动,我个人觉得,这方面最近几年会有更多的变化。如果对于这方面感兴趣的话,可以关注下 TiDB。


问:从 Oracle 分流数据到 TiDB、Oracle 增量修改、Update 的记录,如何同步到 TiDB?有没有工具推荐,比如类似 Ogg?


崔秋:目前 TiDB 还没有相应的工具。如果真的需要在线从 Oracle 这边分流的话,可以考虑使用 Oracle 的触发器,将数据的变化记录下来,然后转化为 SQL,同步到 TiDB,当然这需要一定开发的工作量。


原文链接

基于 Raft 构建弹性伸缩的存储系统的一些实践

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

最近几年来,越来越多的文章介绍了 Raft 或者 Paxos 这样的分布式一致性算法,且主要集中在算法细节和日志同步方面的应用。但是呢,这些算法的潜力并不仅限于此,基于这样的分布式一致性算法构建一个完整的可弹性伸缩的高可用的大规模存储系统,是一个很新的课... 查看全部

最近几年来,越来越多的文章介绍了 Raft 或者 Paxos 这样的分布式一致性算法,且主要集中在算法细节和日志同步方面的应用。但是呢,这些算法的潜力并不仅限于此,基于这样的分布式一致性算法构建一个完整的可弹性伸缩的高可用的大规模存储系统,是一个很新的课题,我结合我们这一年多以来在 TiKV 这样一个大规模分布式数据库上的实践,谈谈其中的一些设计和挑战。


本次分享的主要内容是如何使用 Raft 来构建一个可以「弹性伸缩」存储。其实最近这两年也有很多的文章开始关注类似 Paxos 或者 Raft 这类的分布式一致性算法,但是主要内容还是在介绍算法本身和日志复制,但是对于如何基于这样的分布式一致性算法构建一个大规模的存储系统介绍得并不多,我们目前在以 Raft 为基础去构建一个大规模的分布式数据库 TiKV ,在这方面积累了一些第一手的经验,今天和大家聊聊类似系统的设计,本次分享的内容不会涉及很多 Raft 算法的细节,大家有个 Paxos 或者 Raft 的概念,知道它们是干什么的就好。


先聊聊 Scale


其实一个分布式存储的核心无非两点,一个是 Sharding 策略,一个是元信息存储,如何在 Sharding 的过程中保持业务的透明及一致性是一个拥有「弹性伸缩」能力的存储系统的关键。如果一个存储系统,只有静态的数据 Sharding 策略是很难进行业务透明的弹性扩展的,比如各种 MySQL 的静态路由中间件(如 Cobar)或者 Twemproxy 这样的 Redis 中间件等,这些系统都很难无缝地进行 Scale。


Sharding 的几种策略


在集群中的每一个物理节点都存储若干个 Sharding 单元,数据移动和均衡的单位都是 Sharding 单元。策略主要分两种,一种是 Range 另外一种是 Hash。针对不同类型的系统可以选择不同的策略,比如 HDFS 的Datanode 的数据分布就是一个很典型的例子:


WechatIMG12.png-50.3kB


首先是 Range


Range 的想法比较简单粗暴,首先假设整个数据库系统的 key 都是可排序的,这点其实还是蛮普遍的,比如 HBase 中 key 是按照字节序排序,MySQL 可以按照自增 ID 排序,其实对于一些存储引擎来说,排序其实是天然的,比如 LSM-Tree 或者 BTree 都是天然有序的。Range 的策略就是一段连续的 key 作为一个 Sharding 单元:


屏幕快照 2016-10-13 下午4.40.22.png-180.9kB


例如上图中,整个 key 的空间被划分成 (minKey, maxKey),每一个 Sharding 单元(Chunk)是一段连续的 key。按照 Range 的 Sharding 策略的好处是临近的数据大概率在一起(例如共同前缀),可以很好的支持 range scan 这样的操作,比如 HBase 的 Region 就是典型的 Range 策略。


但是这种策略对于压力比较大的顺序写是不太友好的,比如日志类型的写入 load,写入热点永远在于最后一个 Region,因为一般来说日志的 key 基本都和时间戳有关,而时间显然是单调递增的。但是对于关系型数据库来说,经常性的需要表扫描(或者索引扫描),基本上都会选用 Range 的 Sharding 策略。


另外一种策略是 Hash


与 Range 相对的,Sharding 的策略是将 key 经过一个 Hash 函数,用得到的值来决定 Sharding ID,这样的好处是,每一个 key 的分布几乎是随机的,所以分布是均匀的分布,所以对于写压力比较大、同时读基本上是随机读的系统来说更加友好,因为写的压力可以均匀的分散到集群中,但是显然的,对于 range scan 这样的操作几乎没法做。


屏幕快照 2016-10-19 下午6.14.37.png-135.4kB


比较典型的 Hash Sharding 策略的系统如:Cassandra 的一致性 Hash,Redis Cluster 和 Codis 的 Pre-sharding 策略,Twemproxy 有采用一致性 Hash 的配置。


当然这两种策略并不是孤立的,可以灵活组合,比如可以建立多级的 Sharding 策略,最上层用 Hash ,每一个 Hash Sharding 中,数据有序的存储。


在做动态扩展的时候,对于 Range 模型的系统会稍微好做一些,简单来说是采用分裂,比如原本我有一个 [1, 100) 的 Range Region,现在我要分裂,逻辑上我只需要简单的将这个 region 选取某个分裂点,如分裂成 [1,50), [50, 100) 即可,然后将这两个 Region 移动到不同的机器上,负载就可以均摊开。


但是对于 Hash 的方案来说,做一次 re-hash 的代价是挺高的,原因也是显而易见,比如现在的系统有三个节点,现在我添加一个新的物理节点,此时我的 hash 模的 n 就会从 3 变成 4,对于已有系统的抖动是很大,尽管可以通过 ketama hash 这样的一致性 hash 算法尽量的降低对已有系统的抖动,但是很难彻底的避免。


Sharding 与高可用方案结合


选择好了 sharding 的策略,那剩下的就是和高可用方案结合,不同的复制方案达到的可用性及一致性级别是不同的。很多中间件只是简单的做了 sharding 的策略,但是并没有规定每个分片上的数据的复制方案,比如 redis 中间件 twemproxy 和 codis,MySQL 中间件 cobar 等,只是在中间层进行路由,并未假设底层各个存储节点上的复制方案。但是,在一个大规模存储系统上,这是一个很重要的事情,由于支持弹性伸缩的系统一般来说整个系统的分片数量,数据分片的具体分布都是不固定的,系统会根据负载和容量进行自动均衡和扩展,人工手动维护主从关系,数据故障恢复等操作在数据量及分片数量巨大的情况下几乎是不可能完成的任务。选择一个高度自动化的高可用方案是非常重要的。


在 TiKV 中,我们选择了按 range 的 sharding 策略,每一个 range 分片我们称之为 region,因为我们需要对 scan 的支持,而且存储的数据基本是有关系表结构的,我们希望同一个表的数据尽量的在一起。另外在 TiKV 中每一个 region 采用 Raft 算法在多个物理节点上保证数据的一致性和高可用。


图4.png-46kB


从社区的多个 Raft 实现来看,比如 Etcd / LogCabin / Consul 基本都是单一 raft group 的实现,并不能用于存储海量的数据,所以他们主要的应用场景是配置管理,很难直接用来存储大量的数据,毕竟单个 raft group 的参与节点越多,性能越差,但是如果不能横向的添加物理节点的话,整个系统没有办法 scale。


scale 的办法说来也很简单,采用多 raft group,这就很自然的和上面所说的 sharding 策略结合起来了,也就是每一个分片作为一个 raft group,这是 TiKV 能够存储海量数据的基础。但是管理动态分裂的多 raft group 的复杂程度比单 group 要复杂得多,目前 TiKV 是我已知的开源项目中实现 multiple raft group 的仅有的两个项目之一。


正如之前提到过的我们采用的是按照 key range 划分的 region,当某一个 region 变得过大的时候(目前是 64M),这个 region 就会分裂成两个新的 region,这里的分裂会发生在这个 region 所处的所有物理节点上,新产生的 region 会组成新的 raft group。


总结


构建一个健壮的分布式系统是一个很复杂的工程,上面提到了在 TiKV 在实践中的一些关键的设计和思想,希望能抛砖引玉。因为 TiKV 也是一个开源的实现,作为 TiDB 的核心存储组件,最近也刚发布了 Beta 版本,代码面前没有秘密,有兴趣深入了解的同学也可以直接阅读源码和我们的文档,谢谢大家。


Q&A


Q1:如何在这个 region 的各个副本上保证分裂这个操作安全的被执行?


其实这个问题比较简单,就是将 split region 这个操作作为一个 raft log,走一遍 raft 状态机,当这个 log 成功 apply 的时候,即可以认为这个操作被安全的复制了(因为 raft 算法干得就是这个事情)。确保 split log 操作被 accept 后,对新的 region 在走一次 raft 的选举流程(也可以沿用原来的 leader,新 region 的其他节点直接发心跳)。split 的过程是加上网络隔离,可能会产生很复杂的 case,比如一个复杂的例子:


a, b 两个节点,a 是 leader, 发起一个分裂 region 1 [a, d) -> region 1 [a, b) + region 2 [b, d), region 2的 heartbeart 先发到 b,但这时候 region 2 分裂成了 region 2 [b, c) + region 3 [c, d),给 b 发送的 snapshot 是最新的 region 2 的 snapshot [b, c),region 1的 split log 到了 b,b 的老 region 1 也分裂成了 region 1 [a, b) + region 2 [b,d), 这之后 a 给 b 发送的最新的 region 2 的 snapshot [b, c) 到了,region 2 被 apply 之后,b 节点的 region 2 必须没有 [c, d) 区间的数据。


Q2:如何做到透明?


在这方面,raft 做得比 paxos 好,raft 很清晰的提供了 configuration change 的流程,configuration change 流程用于应对 raft gourp 安全的动态添加节点和移除节点,有了这个算法,在数据库中 rebalance 的流程其实能很好的总结为:


对一个 region: add replica / transfer leadership / remove local replica


这三个流程都是标准的 raft 的 configuration change 的流程,TiKV 的实现和 raft 的 paper 的实现有点不一样的是:


config change 的 log 被 apply 后,才会发起 config change 操作,一次一个 group 只能处理一个 config change 操作,避免 disjoint majority,不过这点在 diego 的论文里提到过。


主要是出于正确性没问题的情况下,工程实现比较简单的考虑。
另外这几个过程要做到业务层透明,也需要客户端及元信息管理模块的配合。毕竟当一个 region 的 leader 被转移走后,客户端对这个 region 的读写请求要发到新的 leader 节点上。


客户端这里指的是 TiKV 的 client sdk,下面简称 client , client 对数据的读写流程是这样的:首先 client 会本地缓存一份数据的路由表,这个路由表形如:


{startKey1, endKey1}   ->     {Region1, NodeA}
{startKey2, endKey2} -> {Region2, NodeB}
{startKey3, endKey3} -> {Region3, NodeC}

client 根据用户访问的 key,查到这个 key 属于哪个区间,这个区间是哪个 region,leader 现在在哪个物理节点上,然后客户端查到后直接将这个请求发到这个具体的 node 上,刚才说过了,此时 leader 可能已经被 transfer 到了其他节点,此时客户端会收到一个 region stale 的错误,客户端会向元信息管理服务请求然后更新自己的路由表缓存。


这里可以看到,路由表是一个很重要的模块,它需要存储所有的 region 分布的信息,同时还必须准确,另外这个模块需要高可用。另一方面,刚才提到的数据 rebalance 工作,需要有一个拥有全局视角的调度器,这个调度器需要知道哪个 node 容量不够了,哪个 node 的压力比较大,哪个 node region leader 比较多?以动态的调整 regions 在各个 node 中的分布,因为每个 node 是几乎无状态的,它们无法自主的完成数据迁移工作,需要依靠这个调度器发起数据迁移的操作(raft config change)。


大家应该也注意到了,这个调度器的角色很自然的能和路由表融合成一个模块,在 Google Spanner 的论文中,这个模块的名字叫 Placement Driver, 我们在 TiKV 中沿用了这个名称,简称 pd,pd 主要的工作就是上面提到的两项:1. 路由表 2. 调度器。
Spanner 的论文中并没有过多的介绍 pd 的设计,但是设计一个大规模的分布式存储系统的一个核心思想是一定要假设任何模块都是会 crash 的,模块之间互相持有状态是一件很危险的事情,因为一旦 crash,standby 要立刻启动起来,但是这个新实例状态不一定和之前 crash 的实例一致,这时候就要小心会不会引发问题.
比如一个简单的 case :因为 pd 的路由表是存储在 etcd 上的,但是 region 的分裂是由 node 自行决定的 ( node 才能第一时间知道自己的某个 region 大小是不是超过阈值),这个 split 事件如果主动的从 node push 到 pd ,如果 pd 接收到这个事件,但是在持久化到 etcd 前宕机,新启动的 pd 并不知道这个 event 的存在,路由表的信息就可能错误。


我们的做法是将 pd 设计成彻底无状态的,只有彻底无状态才能避免各种因为无法持久化状态引发的问题。


每个 node 会定期的将自己机器上的 region 信息通过心跳发送给 pd, pd 通过各个 node 通过心跳传上来的 region 信息建立一个全局的路由表。这样即使 pd 挂掉,新的 pd 启动起来后,只需要等待几个心跳时间,就又可以拥有全局的路由信息,另外 etcd 可以作为缓存加速这一过程,也就是新的 pd 启动后,先从 etcd 上拉取一遍路由信息,然后等待几个心跳,就可以对外提供服务。


但是这里有一个问题,细心的朋友也可能注意到了,如果集群出现局部分区,可能某些 node 的信息是错误的,比如一些 region 在分区之后重新发起了选举和分裂,但是被隔离的另外一批 node 还将老的信息通过心跳传递给 pd,可能对于某个 region 两个 node 都说自己是 leader 到底该信谁的?


在这里,TiKV 使用了一个 epoch 的机制,用两个逻辑时钟来标记,一个是 raft 的 config change version,另一个是 region version,每次 config change 都会自增 config version,每次 region change(比如split、merge)都会更新 region version. pd 比较的 epoch 的策略是取这两个的最大值,先比较 region version, 如果 region version 相等则比较 config version 拥有更大 version 的节点,一定拥有更新的信息。


原文链接

开源数据库的现状

qiuyesuifeng 发表了文章 • 0 个评论 • 514 次浏览 • 2016-10-11 14:57 • 来自相关话题

数据库作为业务的核心,是整个基础软件栈非常重要的一环。近几年的开源社区,新的思想和方案层出不穷,我将总结一下近几年一些主流的开源数据库方案,及其背后的设计思想以及适用场景。本人才疏学浅如有遗漏或者错误请见谅。本次分享聚焦于数据库即结构化数据存储 OLTP... 查看全部

数据库作为业务的核心,是整个基础软件栈非常重要的一环。近几年的开源社区,新的思想和方案层出不穷,我将总结一下近几年一些主流的开源数据库方案,及其背后的设计思想以及适用场景。本人才疏学浅如有遗漏或者错误请见谅。本次分享聚焦于数据库即结构化数据存储 OLTP 及 NoSQL 领域,不会涉及 OLAP、对象存储以及分布式文件系统。


开源 RDBMS 与互联网的崛起
很长时间以来,关系型数据库一直是大公司的专利,市场被 Oracle / DB2 等企业数据库牢牢把持。但是随着互联网的崛起和开源社区的发展,上世纪九十年代 MySQL 1.0 的发布,标志着在关系型数据库的领域,社区终于有了可选择的方案。


✦MySQL


第一个介绍的单机 RDBMS 就是 MySQL。相信大多数朋友都已经对 MySQL 非常熟悉,基本上 MySQL 的成长史就是互联网的成长史。我接触的第一个 MySQL 版本是 MySQL 4.0,后来的 MySQL 5.5 更是经典——基本上所有的互联网公司都在使用。MySQL 也普及了「可插拔」引擎这一概念,即针对不同的业务场景选用不同的存储引擎,这也是 MySQL tuning 的一个重要方式。比如对于有事务需求的场景使用 InnoDB;对于并发读取的场景 MyISAM 可能比较合适;但是现在我推荐绝大多数情况还是使用 InnoDB,毕竟 MySQL 5.6 后它已经成为了官方的默认引擎。MySQL 适用于几乎所有需要持久化结构化数据的场景, 大多数朋友应该都知道,我就不赘述了。


另外值得一提的是 MySQL 5.6 中引入了多线程复制和 GTID,使得故障恢复和主从的运维变得比较方便。另外,MySQL 5.7(目前处于 GA 版本) 发布了一个重大更新,主要是在读写性能和复制性能上有了长足的进步:在 5.6 版本中实现了 SCHEMA 级别的并行复制。不过意义不大,倒是 MariaDB 的多线程并行复制大放异彩,有不少人因为这个特性选择 MariaDB。另外,MySQL 5.7 MTS 支持两种模式,一种是和 5.6 一样,另一种则是基于 binlog group commit 实现的多线程复制,也就是 MASTER 上同时提交的 binlog 在 SLAVE 端也可以同时被 apply,实现并行复制。如果有单机数据库技术选型的朋友,基本上只需要考虑 MySQL 5.7 或者 MariaDB 就好了,而且 MySQL 5.6 和 5.7 由 Oracle 接手后,性能和稳定性都有了明显的提升。


✦PostgreSQL


PostgreSQL 的历史也非常悠久,其前身是 UCB 的 Ingres,主持这个项目的 Michael Stronebraker 于 2015 年获得图灵奖。后来该项目更名为 Post-Ingres,基于 BSD license 下开源。 1995 年几个 UCB 的学生为 Post-Ingres 开发了 SQL 的接口,正式发布了 PostgreSQL95,随后一步步在开源社区中成长起来。和 MySQL 一样,PostgreSQL 也是一个单机的关系型数据库,但是与 MySQL 方便用户过度扩展的 SQL 文法不一样的是,PostgreSQL 的 SQL 支持非常强大,不管是内置类型、JSON 支持、GIS 类型以及对于复杂查询的支持,PL/SQL 等都比 MySQL 强大得多,而且从代码质量上来看,PostgreSQL 的代码质量是优于 MySQL 的。另外,相对于 MySQL 5.7 以前的版本, PostgreSQL 的 SQL 优化器比 MySQL 强大很多,几乎所有稍微复杂的查询 PostgreSQL 的表现都优于 MySQL。


从近几年的趋势上来看,PostgreSQL 的势头也很强劲,我认为 PostgreSQL 的不足之处在于没有 MySQL 那样强大的社区和群众基础。MySQL 经过那么多年的发展,积累了很多的运维工具和最佳实践,但是 PostgreSQL 作为后起之秀,拥有更优秀的设计和更丰富的功能。PostgreSQL 9 以后的版本也足够稳定,在做新项目技术选型的时候,是一个很好的选择。另外也有很多新的数据库项目是基于 PostgreSQL 源码的基础上进行二次开发,比如 Greenplum 等。


我认为,单机数据库的时代很快就会过去。摩尔定律带来的硬件红利总是有上限的,现代业务的数据规模、流量以及现代的数据科学对于数据库的要求,单机已经很难满足。比如,网卡磁盘 IO 和 CPU 总有瓶颈,线上敏感的业务系统可能还得承担 SPOF(单点故障) 的风险,主从复制模型在主挂掉时到底切还是不切?切了以后数据如何恢复?如果只是出现主从机器网络分区问题呢?甚至是监控环境出现网络分区问题呢?这些都是单机数据库面临的巨大挑战。所以我的观点是,无论单机性能多棒(很多令人乍舌的评测数据都是针对特定场景的优化,另外甚至有些都是本机不走网络,而大多数情况数据库出现的第一个瓶颈其实是网卡和并发连接……),随着互联网的蓬勃发展和移动互联网的出现,数据库系统迎来了第一次分布式的洗礼。


分布式时代:NoSQL 的复兴和模型简化的力量
在介绍 NoSQL 之前,我想提两个公司,一个是 Google,另一个是 Amazon。


✦Google


Google 应该是第一个将分布式存储技术应用到大规模生产环境的公司,同时也是在分布式系统上积累最深的公司,可以说目前工业界的分布式系统的工程实践及思想大都来源于 Google。比如 2003 年的 GFS 开创了分布式文件系统,2006 年的 Bigtable 论文开创了分布式键值系统,直接催生的就是 Hadoop 的生态;至于 2012 年发表论文的 Spanner 和 F1 更是一个指明未来关系型数据库发展方向的里程碑式的项目,这个我们后续会说。


✦Amazon


另一个公司是 Amazon。2007 年发表的 Dynamo 的论文 尝试引入了最终一致性的概念, WRN 的模型及向量时钟的应用,同时将一致性 HASH、merkle tree 等当时一些很新潮的技术整合起来,正式标志着 NoSQL 的诞生。NoSQL——对后来业界的影响非常也是很大,包括后来的 Cassandra、RiakDB、Voldemort 等数据库都是基于 Dynamo 的设计发展起来的。


✦新思潮


另外这个时期(2006 年前后持续至今)一个比较重要的思潮就是数据库(持久化)和缓存开始有明确的分离——我觉得这个趋势是从 memcached 开始的。随着业务的并发越来越高,对于低延迟的要求也越来越高;另外一个原因是随着内存越来越便宜,基于内存的存储方案渐渐开始普及。当然内存缓存方案也经历了一个从单机到分布式的过程,但是这个过程相比关系型数据库的进化要快得多。这是因为 NoSQL 的另外一个重要的标志——数据模型的变化——大多 NoSQL 都抛弃了关系模型,选择更简单的键值或者文档类型进行存储。数据结构和查询接口都相对简单,没有了 SQL 的包袱,实现的难度会降低很多。另外 NoSQL 的设计几乎都选择牺牲掉复杂 SQL 的支持及 ACID 事务换取弹性扩展能力,也是从当时互联网的实际情况出发:业务模型简单、爆发性增长带来的海量并发及数据总量爆炸、历史包袱小、工程师强悍,等等。其中最重要的还是业务模型相对简单。


✦嵌入式存储引擎


在开始介绍具体的开源的完整方案前,我想介绍一下嵌入式存储引擎们。


随着 NoSQL 的发展,不仅仅缓存和持久化存储开始细分,再往后的存储引擎也开始分化并走上前台。之前很难想象一个存储引擎独立于数据库直接对外提供服务,就像你不会直接拿着 InnoDB 或者 MyISAM 甚至一个 B-tree 出来用一样(当然,bdb 这样鼎鼎大名的除外)。人们基于这些开源的存储引擎进行进一步的封装,比如加上网络协议层、加上复制机制等等,一步步构建出完整的风格各异的 NoSQL 产品。


这里我挑选几个比较著名的存储引擎介绍一下。


✦TC


我最早接触的是 Tokyo Cabinet(TC)。TC 相信很多人也都听说过,TC 是由日本最大的社交网站 Mixi 开发并开源的一个混合 Key-Value 存储引擎,其中包括 HASH Table 和 B+ Tree 的实现。但是这个引擎的一个缺陷是随着数据量的膨胀,性能的下降会非常明显,而且现在也基本不怎么维护了,所以入坑请慎重。与于 TC 配合使用的 Tokyo Tyrant(TT) 是一个网络库,为 TC 提供网络的接口使其变成一个数据库服务,TT + TC 应该是比较早的 NoSQL 的一个尝试。


✦LevelDB


在 2011 年,Google 开源了 Bigtable 的底层存储引擎: LevelDB。LevelDB 是一个使用 C++ 开发的嵌入式的 Key-Value 存储引擎,数据结构采用了 LSM-Tree,具体 LSM-Tree 的算法分析可以很容易在网上搜索到,我就不赘述了。其特点是,对于写入极其友好,LSM 的设计避免了大量的随机写入;对于特定的读也能达到不错的性能(热数据在内存中);另外 LSM-Tree 和 B-tree 一样是支持有序 Scan 的;而且 LevelDB 是出自 Jeff Dean 之手,他的事迹做分布式系统的朋友一定都知道,不知道的可以去 Google 搜一下。


LevelDB 拥有极好的写性能,线程安全,BatcTCh Write 和 Snapshot 等特性,使其很容易的在上层构建 MVCC 系统或者事务模型,这对于数据库来说非常重要。另外值得一说的是,Facebook 维护了一个活跃的 LevelDB 的分支,名为 RocksDB。RocksDB 在 LevelDB 上做了很多的改进,比如多线程 Compactor、分层自定义压缩、多 MemTable 等。另外 RocksDB 对外暴露了很多 ConfigurationConfigration ,可以根据不同业务的形态进行调优;同时 Facebook 在内部正在用 RocksDB 来实现一个全新的 MySQL 存储引擎:MyRocks,值得关注。RocksDB 的社区响应速度很快也很友好,实际上 PingCAP 也是 RocksDB 的社区贡献者。我建议新的项目如果在 LevelDB 和 RocksDB 之间纠结的话,请果断选择 RocksDB。


✦B-tree 家族


当然,除了 LSM-Tree 外,B-tree 的家族也还是有很多不错的引擎。首先大多数传统的单机数据库的存储引擎都选择了 B+Tree,B+Tree 对磁盘的读比较友好,第三方存储引擎比较著名的纯 B+Tree 实现是 LMDB。首先 LMDB 选择在内存映像文件 (mmap) 实现 B+Tree,而且同时使用了 Copy-On-Write 实现了 MVCC 实现并发事务无锁读的能力,对于高并发读的场景比较友好;同时因为使用的是 mmap 所以拥有跨进程读取的能力。不过因为我并没有在生产环境中使用过 LMDB ,所以并不能给出 LMDB 的一些缺陷,见谅。


✦混合引擎


还有一部分的存储引擎选择了多种引擎混合,比如最著名的应该是 WiredTiger,大概是 2014 年去年被 MongoDB 收购,现在成为了 MongoDB 的默认存储引擎。WiredTiger 内部有 LSM-Tree 和 B-tree 两种实现,对外提供相同的一套接口,根据业务的情况可自由选择。另外一些特殊数据结构的存储引擎在某些特殊场合下非常抢眼,比如极高压缩比 TokuDB,采用了名为分形树的数据结构,在维持一个可接受的读写压力的情况下,能拥有 10 倍以上的压缩率。


✦NoSQL


说完了几个比较著名的存储引擎,我们来讲讲比较著名的 NoSQL。在我的定义中,NoSQL 是 Not Only SQL 的缩写,所以可能包含的范围有内存数据库,持久化数据库等。总之就是和单机的关系型数据库不一样的结构化数据存储系统。


我们先从缓存开始。


✦memcached


前面提到了 memcached 应该是第一个大规模在业界使用的缓存数据库,memcached 的实现极其简单,相当于将内存用作大的 HASH Table,只能在上面进行 get/set/ 计数器等操作,在此之上用 libevent 封装了一层网络层和文本协议(也有简单的二进制协议),虽然支持一些 CAS 的操作,但是总体上来看,还是非常简单的。但是 memcached 的内存利用率并不太高,这是这个因为 memcached 为了避免频繁申请内存导致的内存碎片的问题,采用了自己实现的 slab allocator 的方式。即内存的分配都是一块一块的,最终存储在固定长度的 chunk 上,内存最小的分配单元是 chunk,另外 libevent 的性能也并没有优化到极致。但是这些缺点并不妨碍 memcached 成为当时的开源缓存事实标准。(另外,八卦一下,memcached 的作者 Brad Fitzpatrick 现在在 Google,大家如果用 Golang 的话,Go 的官方 HTTP 包就是这哥们写的,是个很高产的工程师)。


✦Redis


如果我没记错的话,在 2009 年前后,一位意大利的工程师 Antirez ,开源了 Redis。从此彻底颠覆了缓存的市场,到现在大多数缓存的业务都已用上 Redis,memcached 基本退出了历史舞台。Redis 最大的特点是拥有丰富的数据结构支持,不仅仅是简单的 Key-Value,还包括队列、集合、Sorted Set 等等,提供了非常丰富的表达力,而且 Redis 还提供 sub/pub 等超出数据库范畴的便捷功能,使得几乎一夜之间大家纷纷投入 Redis 的怀抱。


✦Twemproxy


但是随着 Redis 渐渐的普及,而且越用越狠,另外内存也越来越便宜,人们开始寻求扩展单机 Redis 的方案,最早的尝试是 twitter 开源的 twemproxy。twemproxy 是一个 Redis 中间件,基本只有最简单的数据路由功能,并没有动态的伸缩能力,但是还是受到了很多公司的追捧,因为确实没其他替代方案。随后的 Redis Cluster 也是难产了好久,时隔好几年,中间出了 7 个 RC 版本,最后才发布;2014 年底,我们开源了 Codis,解决了 Redis 中间件的数据弹性伸缩问题,目前广泛应用于国内各大互联网公司中,这个在网上也有很多文章介绍,我也就不展开了。所以在缓存上面,开源社区现在倒是非常统一,就是 Redis 及其极其周边的扩展方案。


✦MongoDB


在 NoSQL 的大家庭中,MongoDB 其实是一个异类,大多 NoSQL 舍弃掉 SQL 是为了追求更极致的性能和可扩展能力,而 MongoDB 主动选择了文档作为对外的接口,非常像 JSON 的格式。Schema-less 的特性对于很多轻量级业务和快速变更的了互联网业务意义很大,而且 MongoDB 的易用性很好,基本做到了开箱即用,开发者不需要费心研究数据的表结构,只需要往里存就好了,这确实笼络了一大批开发者。


尽管 MongoDB 早期的版本各种不稳定,性能也不太好(早期的 Mongo 并没有存储引擎,直接使用了 mmap 文件),集群模式还全是问题(比如至今还未解决的 Cluster 同步带宽占用过多的问题),但是因为确实太方便了,在早期的项目快速迭代中,Mongo 是一个不错的选择。但是这也正是它的问题,我不止一次听到当项目变得庞大或者「严肃」的时候,团队最后还是回归了关系型数据库。Anyway,在 2014 年底 MongoDB 收购了 WiredTiger 后,在 2.8 版本中正式亮相,同时 3.0 版本后更是作为默认存储引擎提供,性能和稳定性有了非常大的提升。


但是,从另一方面讲,Schema-less 到底对软件工程是好事还是坏事这个问题还是有待商榷。我个人是站在 Schema 这边的,不过在一些小项目或者需要快速开发的项目中使用 Mongo 确实能提升很多的开发效率,这是毋庸置疑的。


✦HBase


说到 NoSQL 不得不提的是 HBase,HBase 作为 Hadoop 旗下的重要产品,Google Bigtable 的正统开源实现,是不是有一种钦定的感觉 :)。提到 HBase 就不得不提一下 Bigtable, Bigtable 是 Google 内部广泛使用的分布式数据库,接口也不是简单的 Key-Value,按照论文的说法叫:multi-dimensional sorted map,也就是 Value 是按照列划分的。Bigtable 构建在 GFS 之上,弥补了分布式文件系统对于海量、小的、结构化数据的插入、更新以及、随机读请求的缺陷。


HBase 就是这么一个系统的实现,底层依赖 HDFS。HBase 本身并不实际存储数据,持久化的日志和 SST file (HBase 也是 LSM-Tree 的结构) 直接存储在 HDFS 上,Region Server (RS) 维护了 MemTable 以提供快速的查询,写入都是写日志,后台进行 Compact,避免了直接随机读写 HDFS。数据通过 Region 在逻辑上进行分割,负载均衡通过调节各个 Region Server 负责的 Region 区间实现。当某 Region 太大时,这个 Region 会分裂,后续可能由不同的 RS 负责,但是前面提到了,HBase 本身并不存储数据,这里的 Region 仅是逻辑上的,数据还是以文件的形式存储在 HDFS 上,所以 HBase 并不关心 Replication 、水平扩展和数据的分布,统统交给 HDFS 解决。


和 Bigtable 一样,HBase 提供行级的一致性,严格来说在 CAP 理论中它是一个 CP 的系统,但遗憾的是并没有更进一步提供 ACID 的跨行事务。HBase 的好处就不用说了,显而易见,通过扩展 RS 可以几乎线性提升系统的吞吐,及 HDFS 本身就具有的水平扩展能力。


但是缺点仍然是有的。首先,Hadoop 的软件栈是 Java,JVM 的 GC Tuning 是一个非常烦人的事情,即使已经调得很好了,平均延迟也得几十毫秒;另外在架构设计上,HBase 本身并不存储数据,所以可能造成客户端请求的 RS 并不知道数据到底存在哪台 HDFS DataNode 上,凭空多了一次 RPC;第三,HBase 和 Bigtable 一样,并不支持跨行事务,在 Google 内部不停的有团队基于 Bigtable 来做分布式事务的支持,比如 MegaStore、Percolator。后来 Jeff Dean 有次接受采访也提到非常后悔没有在 Bigtable 中加入跨行事务,不过还好这个遗憾在 Spanner 中得到了弥补,这个一会儿说。总体来说,HBase 还是一个非常健壮且久经考验的系统,但是需要你有对于 Java 和 Hadoop 比较深入的了解后,才能玩转,这也是 Hadoop 生态的一个问题,易用性真是不是太好,而且社区演进速度相对缓慢,也是因为历史包袱过重的缘故吧。


✦Cassandra


提到 Cassandra (C),虽然也是 Dynamo 的开源实现,但就没有这种钦定的感觉了。C 确实命途多舛,最早 2008 由 Facebook 开发并开源,早期的 C* 几乎全是 bug,Facebook 后来索性也不再维护转过头搞 HBase 去了,一个烂摊子直接丢给社区。还好 DataStax 把这个项目捡起来商业化,搞了两年,终于渐渐开始流行起来。


C 不能简单的归纳为读快写慢,或者读慢写快,因为采用了 qourm 的模型,调整复制的副本数以及读的数量,可以达到不同的效果,对于一致性不是特别高的场景,可以选择只从一个节点读取数据,达到最高的读性能。另外 C 并不依赖分布式文件系统,数据直接存储在磁盘上,各个存储节点之间自己维护复制关系,减少了一层 RPC 调用,延迟上对比 HBase 还是有一定优势的。


不过即使使用 qourm 的模型也并不代表 C 是一个强一致的系统。C 并不帮你解决冲突,即使你 W(写的副本数) + R(读请求的副本数) > N(节点总数),C 也没办法帮你决定哪些副本拥有更新的版本,因为每个数据的版本是一个 NTP 的时间戳或者客户端自行提供,每台机器可能都有误差,所以有可能并不准确,这也就是为什么 C 是一个 AP 的系统。不过 C* 一个比较友好的地方是提供了 CQL,一个简单的 SQL 方言,比起 HBase 在易用性上有明显优势。


即使作为一个 AP 系统,C 已经挺快了,但是人们追求更高性能的脚步还是不会停止。应该是今年年初,ScyllaDB 的发布就是典型的证明,ScyllaDB 是一个兼容 C 的 NoSQL 数据库,不一样的是,ScyllaDB 完全用 C++ 开发,同时使用了类似 DPDK 这样的黑科技,具体我就不展开了,有兴趣可以到 Scylla 的官网去看看。BTW, 国内的蘑菇街第一时间使用了 ScyllaDB,同时在 Scylla 的官网上 share 了他们的方案,性能还是很不错的。


中间件与分库分表
NoSQL 就先介绍到这里,接下来我想说的是一些在基于单机关系型数据库之上的中间件和分库分表方案。


这些技术确实历史悠久,而且也是没有办法的选择。关系型数据库不比 Redis,并不是简单的写一个类似 Twemproxy 的中间件就搞定了。数据库的中间件需要考虑很多,比如解析 SQL,解析出 sharding key,然后根据 sharding key 分发请求,再合并;另外数据库有事务,在中间件这层还需要维护 Session 及事务状态,而且大多数方案并没有办法支持跨 shard 的事务。这就不可避免的导致了业务使用起来会比较麻烦,需要重写代码,而且会增加逻辑的复杂度,更别提动态的扩容缩容和自动的故障恢复了。在集群规模越来越大的情况下,运维和 DDL 的复杂度是指数级上升的。


✦中间件项目盘点


数据库中间件最早的项目大概是 MySQL Proxy,用于实现读写分离。后来国人在这个领域有过很多著名项目,比如阿里的 Cobar 和 TDDL(并未完全开源);后来社区基于 Cobar 改进的 MyCAT、360 开源的 Atlas 等,都属于这一类中间件产品;在中间件这个方案上基本走到头的开源项目应该是 Youtube 的 Vitess。Vitess 基本上是一个集大成的中间件产品,内置了热数据缓存、水平动态分片、读写分离等等,但是代价也是整个项目非常复杂,另外文档也不太好。大概 1 年多以前,我们尝试搭建起完整的 Vitess 集群,但是并未成功,可见其复杂度。


另外一个值得一提的是 Postgres-XC 这个项目,Postgres-XC 的野心还是很大的,整体的架构有点像早期版本的 OceanBase,由一个中央节点来处理协调分布式事务 / 解决冲突,数据分散在各个存储节点上,应该是目前 PostgreSQL 社区最好的分布式扩展方案。其他的就不提了。


未来在哪里?NewSQL!
一句话,NewSQL 就是未来。


2012 年 Google 在 OSDI 上发表了 Spanner 的论文,2013 年在 SIGMOD 发表了 F1 的论文。这两篇论文让业界第一次看到了关系模型和 NoSQL 的扩展性在超庞大集群规模上融合的可能性。在此之前,大家普遍认为这个是不可能的,即使是 Google 也经历了 Megastore 这样的失败。


✦Spanner 综述


但是 Spanner 的创新之处在于通过硬件(GPS 时钟 + 原子钟)来解决时钟同步的问题。在分布式系统里,时钟是最让人头痛的问题,刚才提到了 C* 为什么不是一个强 C 的系统,正是因为时钟的问题。而 Spanner 的厉害之处在于即使两个数据中心隔得非常远,不需要有通信(因为通信的代价太大,最快也就是光速)就能保证 TrueTime API 的时钟误差在一个很小的范围内(10ms)。另外 Spanner 沿用了很多 Bigtable 的设计,比如 Tablet / Directory 等,同时在 Replica 这层使用 Paxos 复制,并未完全依赖底层的分布式文件系统。但是 Spanner 的设计底层仍然沿用了 Colossus,不过论文里也说是可以未来改进的点。


Google 的内部的数据库存储业务,大多是 3~5 副本,重要一点的 7 副本,遍布全球各大洲的数据中心,由于普遍使用了 Paxos,延迟是可以缩短到一个可以接受的范围(Google 的风格一向是追求吞吐的水平扩展而不是低延迟,从悲观锁的选择也能看得出来,因为跨数据中心复制是必选的,延迟不可能低,对于低延迟的场景,业务层自己解决或者依赖缓存)。另外由 Paxos 带来的 Auto-Failover 能力,更是能让整个集群即使数据中心瘫痪,业务层都是透明无感知的。另外 F1 构建在 Spanner 之上,对外提供了更丰富的 SQL 语法支持,F1 更像一个分布式 MPP SQL——F1 本身并不存储数据,而是将客户端的 SQL 翻译成类似 MapReduce 的任务,调用 Spanner 来完成请求。


其实 Spanner 和 F1 除了 TrueTime 整个系统并没有用什么全新的算法,其意义在于这是近些年来第一个 NewSQL 在生产环境中提供服务的分布式系统技术。


Spanner 和 F1 有以下几个重点:



  1. 完整的 SQL 支持,ACID 事务;

  2. 弹性伸缩能力;

  3. 自动的故障转移和故障恢复,多机房异地灾备。


NewSQL 特性确实非常诱人,在 Google 内部,大量的业务已经从原来的 Bigtable 切换到 Spanner 之上。我相信未来几年,整个业界的趋势也是如此,就像当年的 Hadoop 一样,Google 的基础软件的技术趋势是走在社区前面的。


✦社区反应


Spanner 的论文发表之后,当然也有社区的追随者开始实现(比如我们 :D ),第一个团队是在纽约的 CockroachDB。CockroachDB 的团队的组成还是非常豪华的,早期团队由是 Google 的分布式文件系统 Colossus 团队的成员组成;技术上来说,Cockroach 的设计和 Spanner 很像,不一样的地方是没有选择 TrueTime 而是 HLC (Hybrid logical clock),也就是 NTP + 逻辑时钟来代替 TrueTime 时间戳;另外 Cockroach 选用了 Raft 代替 Paxos 实现复制和自动容灾,底层存储依赖 RocksDB 实现,整个项目使用 Go 语言开发,对外接口选用 PostgreSQL 的 SQL 子集。


✦TiDB


目前从全球范围来看,另一个朝着 Spanner / F1 的开源实现这个目标上走的产品是 TiDB(终于谈到我们的产品了)。TiDB 本质上是一个更加正统的 Spanner 和 F1 实现,并不像 CockroachDB 那样选择将 SQL 和 Key-Value 融合,而是像 Spanner 和 F1 一样选择分离,这样分层的思想也是贯穿整个 TiDB 项目始终的。对于测试、滚动升级以及各层的复杂度控制会比较有优势;另外 TiDB 选择了 MySQL 协议和语法的兼容,MySQL 社区的 ORM 框架和运维工具,直接可以应用在 TiDB 上。


和 F1 一样,TiDB 是一个无状态的 MPP SQL Layer,整个系统的底层是依赖 TiKV 来提供分布式存储和分布式事务的支持。TiKV 的分布式事务模型采用的是 Google Percolator 的模型,但是在此之上做了很多优化。Percolator 的优点是去中心化程度非常高,整个集群不需要一个独立的事务管理模块,事务提交状态这些信息其实是均匀分散在系统的各个 Key 的 meta 中,整个模型唯一依赖的是一个授时服务器。在我们的系统上,极限情况这个授时服务器每秒能分配 400w 以上个单调递增的时间戳,大多数情况基本够用了(毕竟有 Google 量级的场景并不多见);同时在 TiKV 中,这个授时服务本身是高可用的,也不存在单点故障的问题。


TiKV 和 CockroachDB 一样也是选择了 Raft 作为整个数据库的基础;不一样的是,TiKV 整体采用 Rust 语言开发,作为一个没有 GC 和 Runtime 的语言,在性能上可以挖掘的潜力会更大。


✦关于未来


我觉得未来的数据库会有几个趋势,也是 TiDB 项目追求的目标:


●数据库会随着业务云化,未来一切的业务都会跑在云端,不管是私有云、公有云还是混合云,运维团队接触的可能再也不是真实的物理机,而是一个个隔离的容器或者「计算资源」。这对数据库也是一个挑战,因为数据库天生就是有状态的,数据总是要存储在物理的磁盘上,而移动数据的代价比移动容器的代价可能大很多。


●多租户技术会成为标配,一个大数据库承载一切的业务,数据在底层打通,上层通过权限,容器等技术进行隔离;但是数据的打通和扩展会变得异常简单,结合第一点提到的云化,业务层可以再也不用关心物理机的容量和拓扑,只需要认为底层是一个无穷大的数据库平台即可,不用再担心单机容量和负载均衡等问题。


●OLAP 和 OLTP 会进一步细分,底层存储也许会共享一套,但是 SQL 优化器这层的实现一定是千差万别的。对于用户而言,如果能使用同一套标准的语法和规则来进行数据的读写和分析,会有更好的体验。


●在未来分布式数据库系统上,主从日志同步这样落后的备份方式会被 Multi-Paxos / Raft 这样更强的分布式一致性算法替代,人工的数据库运维在管理大规模数据库集群时是不可能的,所有的故障恢复和高可用都会是高度自动化的。


Q
&
A
问:HANA 等内存数据库怎么保证系统掉电而处理结果不丢?传统数据库也用缓存,可是 HANA 用的内存太大。


黄东旭:没用过 HANA,但是直观感觉这类内存数据库的可用性可能通过集中方式保证:
●写入会先写 WAL;




  • 写入可能会通过主从或者 paxos 之类的算法做同步和冗余复制还有 HANA 本身就是内存数据库,会尽可能把数据放到内存里,这样查询才能快呀。



问:对于传统创业公司如何弥补 NoSQL 的技术短板?快速的引入 NoSQL 提高效率?


黄东旭:选用 NoSQL 主要注意两点:



  1. 做好业务的调研,估计并发量,数据量,数据的结构看看适不适合;

  2. 对各种 NoSQL 擅长和不擅长的地方都尽可能了解。
    不要盲目相信关系型数据库,也不要盲目相信 NoSQL,没有银弹的。


问:有多个条件 比如年龄 20 到 30 或年龄 35 到 40 并且加入购物车或下单 这种数据怎么存储?


黄东旭:购物车这种场景是典型的 OLTP 的场景,可以选用关系型数据库 MySQL PostgreSQL 什么的,如果对于扩展性的数据跨机房有要求的话,可以调研一下 NewSQL,比如我们的 TiDB。


问:多纬度查询应该选择哪种数据库?


黄东旭:多纬度查询可以说是一个 OLAP 的场景,可以选用 Greenplum 或者 Vertica 之类的分析性数据库。


问:想知道为什么需要这些开源的数据库,既然已经有了 MySQL、DB2、Oracle 这些成熟的数据库,成本考虑,还是传统数据库满足不了需求?


黄东旭:对,传统数据库的扩展性是有问题的,在海量并发和数据量的场景下很难支持业务。所以可以看到比较大的互联网公司基本都有自己的分布式数据库方案。


问:未来可能不再需要数据仓库吗?


黄东旭:大家可以想想数据仓库的定义,如果是还需要离线的从线上库倒腾数据到数据仓库上,这样很难做到实时查询,而且空间的利用率也低,我认为是目前并没有太好的方案的情况下的折衷……
如果有一个更好的数据库能解决数据仓库的场景,为什么还需要一个独立的数据仓库?


原文链接

分布式数据库 TiDB 过去现在和未来

qiuyesuifeng 发表了文章 • 0 个评论 • 2533 次浏览 • 2016-10-11 14:50 • 来自相关话题

近年来,随着移动互联网、物联网、人工智能等技术的兴起,我们已经进入了一个信息爆炸的大数据时代,需要处理和分析的数据越来越多,与此同时,PC Server 的造价不断降低,促使 Hadoop、Spark 等分布式分析计算框架得到广泛的使用,可以肯定的是,在... 查看全部

近年来,随着移动互联网、物联网、人工智能等技术的兴起,我们已经进入了一个信息爆炸的大数据时代,需要处理和分析的数据越来越多,与此同时,PC Server 的造价不断降低,促使 Hadoop、Spark 等分布式分析计算框架得到广泛的使用,可以肯定的是,在未来,分布式一定是主流的数据处理技术。TiDB 是 PingCAP 团队开发的一款开源的 NewSQL 数据库,整个设计参考 Google Spanner 和 F1,目标是构建一个面向高并发、高吞吐的在线海量数据存储的关系型数据库,提供透明的跨行事务及兼容 MySQL 的 SQL 语法支持,支持无缝的水平伸缩以及跨数据中心多活等特性。目前项目社区相当活跃(5000+ Stars),已经成为在国际上有影响力的顶级开源项目。


为什么要做这么一个数据库?现在的方案有什么问题吗?其实一直以来,在关系型数据库这一层几乎没有一种优雅的分布式方案。但是作为存储架构的核心,关系型数据库的水平扩展是不可避免的,目前大多数方案基本都需要对业务层有很强的侵入性,比如采用 NoSQL 替换原有关系型数据库,但是这样就需要涉及大规模的业务重构,相当于将数据库层的复杂度转嫁给业务层;另外一种方案是通过中间件,或者分库分表,但是这种方案仍然很难做到透明和数据的均匀分布,也无法支持一致性的跨节点事务和 JOIN 操作,而且随着集群规模的增大,维护和扩展的复杂度呈指数级上升。另外经常被人忽略的是,两种方案都没有很好的解决高可用的问题,跨机房多活、故障恢复、扩容经常都需要繁重的人工介入。直到 Google 发布 Spanner 和 F1 ,这才出现了一个真正在 Google 这样的业务规模上验证过的分布式关系型数据库。作为 Google Spanner 和 F1 的开源实现,TiDB 和 TiKV 完美地解决了现有的这些问题。下面简单介绍一下我们是如何解决这些问题的。


TiDB 包含三个子项目:一个是 TiDB,一个是 TiKV,还有 PD (placement driver)。TiDB 是一个无状态的 SQL 层,客户端可以任意连接到一个 TiDB Server 实例上,看到的存储层是完全一致的。TiDB 负责解析用户的 SQL 请求,生成查询计划,交给底层的 TiKV 去执行,TiDB 和 TiKV 通过 Protobuf RPC 进行通信;而 TiKV 负责实际的数据存储,支持全局分布式事务,提供对上层透明的水平扩展。另外一个组件 PD 负责存储元信息,如 Region 具体分布在哪台物理节点上,每个 TiKV Node 每隔一定的时间会将自己的状态信息和本机的 Region 分布情况上报给 PD,PD 会根据需要进行 Region 数据的移动和发起添加副本指令等操作,可以说是集群的大脑,整个 TiDB 项目的架构大致就是如此。
水平扩展
我们先来看看 TiKV 是如何实现无缝的水平扩展的。


从以上 TiKV 的架构图中可以看出,TiKV 将数据逻辑上分为很多小的 Region,每一个 Region 是一系列连续按照字节排序的 Key-value 对。和很多传统的中间件方案不同的是,TiKV 并不关心上层的 Schema 结构,也不知道上层每个 Column 的类型,就是一个很纯粹的 Key-value 数据库。 数据移动的单位是 Region,每个 Region 都会在不同的机器上维护副本(默认 3 副本)。当一个 Region 的数据增长到一定程度的时候,这个 Region 就会像细胞分裂一样会分裂成两个新的 Region,然后新的 Region 可能会被移动到不同的机器上,用这种方式来进行负载均衡以及容量的扩展。


在 TiKV 中,每个 Region 的不同副本之间通过 Raft 协议进行强一致保证,我们在 TiKV 的上层实现了两阶段提交(2PC)来支持跨 Region 的透明分布式事务。整个模型正是 Raft 的作者 Diego Ongaro 博士于 2014 年其博士论文中描述的理想的 Large-scale storagesystem 的模型:


高可用与跨数据中心多活
其实如果抛开数据库层面来谈高可用并不是太复杂,因为业务逻辑服务是比较容易设计成无状态的(一般来说,状态都会持久化到存储层)。现在很多比较前卫的互联网公司,已经在业务的最上层通过动态 DNS + LVS 、F5、HAProxy 等负载均衡工具配合 Docker 以及调度器(K8S 或者 Mesos)实现按需的动态伸缩。如果不考虑存储层,即使整个数据中心宕机,业务层也能做到透明的 Failover,也就是多起几个容器的事情,难点无非是控制好瞬时流量等问题,这从整个模型看是比较完善的。


但是到数据库这层就不那么美好了,传统的关系型数据库的容灾主要依赖主从(master-slave)模型,比如 MySQL 的半同步复制,Oracle 的 DataGuard 都属于主从复制的范畴。但是主从模型有两个很大的问题,即数据的一致性和故障的自动切换。在主从模型中,即使是热备,在极端情况下也并不能保证 Failover 时数据的一致性。网络环境错综复杂,比如出现集群脑裂的状态(网络部分隔离),监控程序自动的将副本提升 master,但是原来的 master 可能仍然在处理部分客户端的请求,这种情况下,数据就可能发生不一致。这也就是为什么很多一致性敏感的业务即使主库挂掉了,宁可暂停服务也不敢自动切换热备,这样业务的中断时间就不可控了。再者,如果面对一个非常庞大的集群,可能上百个物理节点甚至更多,机器故障是常态,如果每一次故障都需要人工介入,整个系统的维护代价是非常惊人的。


那有没有办法能在强一致的前提下做到安全的故障恢复和故障转移呢?答案是有的。Spanner 的做法是利用 Multi-Paxos 进行同步,这类分布式选举算法拥有很强的容错能力,即使发生节点宕机,网络隔离,整个系统都不会发生数据丢失或者不一致的状态。而且在保证安全的同时,所有的 Failover 工作都是自动的。自动化是一个能弹性水平扩展的系统的重要条件,因为一切需要人工介入的工作都是无法弹性水平扩展的。但是长期以来,由于 Paxos 的实现极其复杂,社区并没有可以在生产环境中使用的实现。


在 TiKV 中我们采用了 Raft 算法来作为和 Spanner 中的 Multi-Paxos。Raft 是 Diego Ongaro 在 2014 年发布的一个全新的先进的分布式一致性算法,在正确性和性能上和 Multi-Paxos 等价,但是对工程实现的复杂度控制很好。目前 TiKV 的 Raft 实现是和 CoreOS 一起合作开发的。CoreOS 是知名的 Raft 项目 etcd 背后的公司。抛弃传统的主从模型,而选用 Raft 这样的分布式一致性算法为 TiDB 在稳定性和健壮性带来了质的飞跃。可以说,整个 TiDB 系统都是构建在 Raft 这个基石之上的。


另外,得益于 Raft 算法的特性,TiDB 可以在保证延迟处于可接受范围内的情况下,能够真正支持跨数据中心多活,这也是 TiDB 的一个重要特点。现在很多对于强一致性要求极端严格的业务,比如金融、支付、计费等,两地三中心的部署渐渐成为共识,但是在多数据中心容灾这个事情上很难找到一个很好的方案。在传统的方案中,如果要做到强一致的热备,需要所有数据中心的副本同步写成功,一个写入的延迟取决于所有数据中心尤其是最远的数据中心的延迟。而在 TiDB 上,由于 Raft 协议只需要同步复制到「大多数」的数据中心即可保证数据的强一致性,所以写入的延迟取决于「大多数」数据中心的延迟。一般来说,对于两地三中心的部署,一定会有一个同城双机房,这时候的延迟较于异地机房的延迟就会小很多了。
完整的面向分布式存储的 SQL Layer
TiDB 实现了完整的 SQL 解析器和优化器。传统的中间件方案只做简单的路由和转发到底层的数据库上,并不能很好地支持 JOIN 或者透明的事务,也就很难完整地兼容原有的业务,对业务层有比较大的侵入性。另外传统的单机数据库的查询优化器并没有考虑到底层存储可能是一个分布式存储,很多优化技巧和手段都难以利用分布式的计算能力和优势。出于以上几点原因,TiDB 选择了重新实现完整的 SQL 解析和 MPP SQL 优化器等组件以支持客户端透明的复杂查询,以及分布式事务。
另外 TiDB 支持完整的 MySQL 语法和网络协议兼容。这样,TiDB 可以直接使用 MySQL 社区的海量测试用例,大量的已有 DBA 工具,更使得那些使用 MySQL 的客户的迁移和兼容的工作成本降到最低。这很好的替代了传统的分库分表和中间件等过渡方案。而且 TiDB 还能完美兼容 MySQL 的 binlog, 可以和现有的 MySQL 互为备份,进一步降低了早期的测试成本。
产品规划
分布式执行引擎


TiDB 目前内置一个分布式执行框架,采用和 HBase Coprocessor 类似的方案。这套框架可以利用多台 TiKV 并行处理数据,再将结果汇集到单台 TiDB 来做最终的处理。在处理 OLTP 请求以及中小规模的 OLAP 请求时,这套框架用起来得心应手。但是当处理大型的 OLAP 请求(例如超大表之间的 Join)时,就显得力不从心了。后续我们计划引入一套新的分布式执行引擎,来专门处理复杂 Query。


Spark 集成


Spark 是当前最火的大数据分析工具,在业界得到广泛的应用。TiDB 可以通过 JDBC 接口和 Spark 对接,原有的基于 MySQL 的 Spark 作业可以无缝迁移到 TiDB 上。这样用户可以同时享有 TiDB 海量存储的能力以及 Spark 强大的通用计算能力,从数据中挖掘出更大的价值。这也让 TiDB 同时支持了 OLTP 和 OLAP 两种生态。


Kubernetes


另外一个很重要的事情就是将 TiDB 的整个部署与 Kubernetes 整合。我们认为未来的数据库一定会和云深度结合以提供无缝的扩展及部署体验,而第一步就是和容器调度器的深度整合,我们在内部的测试版本中已经成功的将 TiDB 的部署和扩展在 Kubernetes 上实现,相信很快就会在社区中亮相。


社区情况 / 如何进行贡献


目前 TiDB 项目的社区贡献者达近百人,其中包括京东和华为的大量代码及 Use case 贡献;作为一个国际化开源项目,CoreOS 及 Facebook 等公司也加入进来一起合作开发,因此也非常感谢 Facebook 的 RocksDB team 和 CoreOS 的 etcd team 的大力支持。同时,TiKV 作为 Rust 社区的顶级项目,得到了 Rust 语言官方的大力支持,项目被收录到 Rust Weekly 作为常驻栏目向 Rust 社区通报每周进展。TiDB 作为一个开放的开源项目,欢迎一切感兴趣的开发者的加入,在 Github 的 TiDB 项目首页有 How to contribute 的文档,Welcome aboard and happy hacking!


原文链接

提问的智慧

astaxie 发表了文章 • 1 个评论 • 376 次浏览 • 2016-10-11 10:28 • 来自相关话题

提问的智慧

艾瑞克.史蒂文.雷蒙德(Eric Steven Raymond)

查看全部

提问的智慧


艾瑞克.史蒂文.雷蒙德(Eric Steven Raymond)


https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md


这是一篇长文,看完需要十几分钟的时间。如果之前没有认真看过并且思考过,这十几分钟会改变你的职业生涯。这文章可能会出现一些让人不适的词语或者过时的例子,但我认为这不会影响它要表达的内容,而你需要好好琢磨作者的思想。


《提问的智慧》是一个敲门砖,它把黑客间的礼仪和准则明白地写下来,让我们了解到一个事实,为什么那些看起来很牛的人几乎从不提问。其实他们也有问题,但是通常在提问之前就自己解决了。不是因为他们本来就牛,而是解决问题的经历让他们成为牛人。最终,你只会看到网络上多了一篇文章:关于解决XXX问题的方案。


要不要花十几分钟改变自己的人生,决定权在自己。

10.11 每日早报

astaxie 发表了文章 • 0 个评论 • 408 次浏览 • 2016-10-11 09:52 • 来自相关话题

10.11 每日早报

新闻:

1.哔哩哔哩正式上线VIP会员制度“大会员”,单月价格25元

2.Mozilla为Firefox Sync引入设备管理功能,可远程取消设备同步

3.京东发起公益活动,在... 查看全部

10.11 每日早报


新闻:


1.哔哩哔哩正式上线VIP会员制度“大会员”,单月价格25元


2.Mozilla为Firefox Sync引入设备管理功能,可远程取消设备同步


3.京东发起公益活动,在北京和天津推出快递小哥上门收旧衣服活动


4.花椒直播获3亿元A轮融资,360投资6000万元


5.福田汽车牵手百度,聚焦无人驾驶超级卡车和车联网


6.ofo共享单车完成1.3亿美元C轮融资,宣布向非校园的城市用户开放


7.电子合同签署云平台契约锁获得900万元天使轮融资,上海泛微网络领投


资源:


2016上半年度中国快递市场研究报告
http://www.bigdata-research.cn/content/201610/354.html


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

三十分钟 Docker 新手入门

astaxie 发表了文章 • 0 个评论 • 571 次浏览 • 2016-10-10 23:35 • 来自相关话题

简介

如果您是 Docker 新手请您花大约三十分钟的时间来了解 Docker 相关的知识和内容。 Docker 与 Linux 息息相关,因此在阅读本文档之前请您确保以下条件:

  1. 对 Linux 的命令行操作有... 查看全部

简介


如果您是 Docker 新手请您花大约三十分钟的时间来了解 Docker 相关的知识和内容。 Docker 与 Linux 息息相关,因此在阅读本文档之前请您确保以下条件:



  1. 对 Linux 的命令行操作有一定了解,并且懂得一些基础命令。

  2. 对 Linux 服务管理有一定的了解。

  3. 当阅读完本文之后您可以了解什么是 Docker、使用它有什么好处、以及 Docker 具体的使用方法。


为什么选择 Docker?



  1. 相对于虚拟机来说 Docker 有镜像管理。

  2. 相对于虚拟机来说更强大的迁移能力。

  3. 云计算的未来,再也不用受到环境 API 的限制。


安装 Docker


Docker 的安装十分简单方便,如果您有 Linux 虚拟机 VPS 可以直接参考 Docker 极速下载 运行如下脚本来安装 Docker:


user$ curl -sSL https://get.daocloud.io/docker | sh

在 Docker 安装完成以后,国内的特殊网络环境会导致在构建 Docker 镜像或者是抓取(pull)来自 Docker Hub 上的镜像时会遇到连接问题,这时我们就需要使用 DaoCloud 加速器服务



  1. 注册 DaoCloud 账号,打开控制台进入加速服务。

  2. 在网页的最下方有详细的使用步骤请参考。


如果您使用的是 Windows 或 Mac 请您下载安装 Boot2Docker。


这里我推荐使用 Ubuntu Server 操作系统,无论是在裸机上还是虚拟机上。


Hello World


这部分的代码来自 Docker 用户指南,但是与用户指南不同的是我们会描述更多的执行过程。


案例 1 启动一个容器


root# docker run ubuntu:14.04 /bin/echo 'Hello world'
Hello world


  1. 注意操作 Docker 是需要权限的,这里使用 root 用户操作,当然您也可以把 docker 加入自己的用户。

  2. run 作为 Docker 的子命令来控制新建容器并且运行。Docker 命令虽然比较多,但是命令是分级来执行的,多参照 help 就会习惯。

  3. ubuntu:14.04 代表镜像的名字和版本号,托管在 Docker Hub 上,如果本地没有抓取过那么执行命令的时候会自动从 Docker Hub 抓取。

  4. /bin/echo 为在容器内执行的程序(应用)。

  5. 'Hello world' 为程序执行的参数。



提示:Docker 在命令执行完毕后不会销毁容器,但是状态为变为 Exited。



从这句命令中我们可以看到 Docker 可以根据情况判断镜像存在的情况,在后文中会介绍镜像的管理。同时 ubuntu:14.04 将会被载入到缓存中,如果后面的镜像构建依赖于它并不会花费额外的网络带宽抓取,十分方便。


案例 2-1 以交互模式启动一个容器


root# docker run -t -i ubuntu:14.04 /bin/bash
root@af8bae53bdd3:/#

与上面的案例不同的是这条命令带有 -t 和 -i 选项,这两个选项在这指的是:


-i, --interactive=false Keep STDIN open even if not attached
-t, --tty=false Allocate a pseudo-TTY

上面来自 docker help run 的输出,help 命令是获取文档帮助信息最简短而高效的途径。本次启动的进程是 bash,运行 bash 之后,容器会在交互的模式下启动。当 bash 退出后就会停止运行。这就是 Docker 运行程序的最简单方式。



提示:此程序运行完成之后,容器不会被销毁,但是状态为 Exited。此外对于以交互模式启动的容器可以先按下 Ctrl+P 然后按下 Ctrl+Q 这样的按键顺序脱离(detach)一个容器,脱离后的容器状态仍为 Up 并且程序会继续在后台运行,这时可以使用 attach 命令重新附到一个已经脱离的程序。


案例 2-2 查看容器内的文件以及容器本身



在上一个例子中如果容器没有关闭通过如下命令可以看出:


root@33d90ffaf1ac:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@33d90ffaf1ac:/# pwd
/
root@33d90ffaf1ac:/#

Docker 中整个容器是一个 Linux 环境,ubuntu 镜像的默认用户为 root,默认工作目录为根目录。


root@33d90ffaf1ac:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 18172 3104 ? Ss 08:50 0:00 /bin/bash
root 16 0.0 0.0 15572 2200 ? R+ 09:04 0:00 ps aux

容器中的进程相当简洁,只有正在运行的两个程序没有其他任何进程。


并且 PID 号码是独立存在的,与宿主机完全没有关系。


案例 3 查看后台运行容器的日志信息


root# sudo docker run -t -i -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
8b8aad0aa7670441f99ce88fbac021bfb9cb124e7de4417a00ed3c0ccc6cb203

在这个案例中加入了新的选项 -d,这个选项中可以让 Docker 后台运行程序。


如果我们查看运行的结果:


root# docker logs 8b8aad0aa767
hello world
hello world
hello world

从上面的命令来看,使用 logs 能看到程序的输出 log 过程,这样对服务的调试是非常有帮助的。如果容器没有自己设定的名字很难快速准确的调度容器。


案例 4 快速准确的调度容器--给容器起名字


root# docker run -t -i -d --name helloubuntu ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
8b8aad0aa7670441f99ce88fbac021bfb9cb124e7de4417a00ed3c0ccc6cb203

上面的命令跟之前案例中的命令对比多了一个 --name 选项给调度容器带来了很多方便,用户可以自己设定容器的名字,当然如果未指定名字系统会自动起一个随机的名字给容器。那么我们查看 logs 的时候就可以通过命令 docker logs helloubuntu 来查看日志信息了。


注意:容器名必须以英文字母和数字开头,并且只能包含英文数字、下划线 _、小数点 .、和减号 -。


案例 5 列出所有正在运行的容器和它们的基本信息


root# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
53d90769be11 ubuntu:14.04 [...] 2 min ago Up 2 min helloubuntu
8b8aad0aa767 ubuntu:14.04 [...] 14 min ago Up 14 min dreamy_fermat
deaaa8e60c9f ubuntu:14.04 [...] 14 min ago Up 1 sec distracted_darwin
33d90ffaf1ac ubuntu:14.04 [...] 26 min ago Exited (0) tender_franklin


注意:由于文档宽度关系,样例中有些内容被缩写以方便展示。



# docker ps -a 这个命令中我们可以看到容器的 ID、使用的镜像、启动的命令、创建的时间、当前状态、端口映射状态和名字。


案例 6 容器管理


以下管理命令都可以通过传入容器的 ID 或者名字来管理指定的容器:



  • stop 停止一个正在运行的容器。

  • start 运行一个正在停止的容器。

  • restart 重启一个容器。

  • rm 删除一个容器。


传入多个 ID 或者名字可以操作多个容器。例如:docker rm name1 name2 可以同时删除两个容器。



提示:如果要删除所有容器 docker rm $(docker ps -q -a)。



阶段总结


当您看到这里的时候可以说对 Docker 已经有了初步的操作能力,包括运行容器中的程序、查看容器内容、运行容器、停止容器、查看容器、重启容器和删除容器。但是距离在 Docker 上运行自己的业务或者组织开发还是有一定距离的。接下来我们开始进阶学习,重点研究一下 Docker 操作的组成要素。


Docker 组成要素


如果将轮船拉着集装箱运行在大海上,与 Docker 运行容器里面的程序类比:



  • Linux 相当于大海,有不同的海洋气候各有不同。

  • Docker 相当于能行驶在各种大海上的轮船,容器相当于各种规格的集装箱。

  • Docker 内的系统相当于货物的包装。

  • 目标程序则相当于货物。


当您看完上面的描述之后可以了解到每一种角色不同的作用以及所处的位置有所了解。


当然理论上的 Docker 只是一个轮船图纸,必须得有一个守护进程才能运行。


镜像也是一样,它只是集装箱的图纸,需要在 Docker 中运行容器。


通过 Docker 运行您的 Web 应用,Step By Step


代码文件请参考: DaoCloud 搭建静态博客 以及 整个研究过程参考


Step 1:准备自己的互联网应用 - 参考文件中的 static 这个文件为编译好的 golang 应用程序。


Step 2:准备 Linux RootFS - 参考文件中的 root.fs 为打包好的 BusyBox。


Step 3:准备 Dockerfile:


# 从一个空镜像中开始创建,没有任何依赖。
FROM scratch
MAINTAINER DaoCloud <example@daocloud.io>

# 给 Docker 文件系统中添加根目录,也是 Linux 的一些基础目录。
ADD ./rootfs.tar /

# 给镜像添加工作目录 /app
RUN mkdir -p /app

# 设定默认工作路径
WORKDIR /app

# 复制应用进入到镜像中
COPY ./static /app

# 复制应用依赖的静态文件目录
COPY ./public /public

# 对外开放的服务接口为 8080
EXPOSE 8080

# 容器运行时默认调用的启动命令
CMD ["/app/static"]

Step4:构建镜像 - sudo docker build -t [标签] . 注意:最后有一个点 . 表示构建时的当前目录。


Step5:运行镜像 - 这一步就不赘述了,请参阅最开始的一个章节。


这里重点讲解一下第三步中 Dockerfile 的格式。参照 Dockerfile 中的注释。


关于 Dockerfile 内部的指令的教程网上虽然很多,但是坑也不少。首先要注意,构建用的依赖文件都要放到同一个目录中,为了安全和可移植性尽量不要用到目录之外的文件。


COPY 或者 ADD 的目的地址比如说上面的 /app/ 与 /public 意义不同,第一个是复制文件到 app 下,第二个是复制目录到根名字叫 public。


为了更详细的介绍 Dockerfile 添加一个额外的案例:


FROM node:slim

MAINTAINER DaoCloud <example@daocloud.io>

RUN apt-get update \
&& apt-get install -y git ssh-client ca-certificates --no-install-recommends \
&& rm -r /var/lib/apt/lists/*

RUN echo "Asia/Shanghai" > /etc/timezone \
&& dpkg-reconfigure -f noninteractive tzdata

RUN npm install hexo@3.0.0 -g

RUN mkdir /hexo
WORKDIR /hexo

EXPOSE 4000

CMD ["/bin/bash"]

在这个 Dockerfile 中:


FROM node:slim 表示构建的时候依赖于 Node.js 系统,并标明了标签为 slim,但是没标明也不需要标明的是 node 依赖于 Debian,这是用户自己需要了解的,下一步就开始运行,当开始构建时是可以运行 Dockerfile 中的脚本的。


第一个 RUN:后面标记的是需要让 Debian 更新源并且安装一系列的软件,最后清空无用缓存。


第二个 RUN:设定时区。


第三个 RUN:安装 Hexo 博客系统。


最后新建目录设定工作目录开放 4000 端口上文中已经提过。


在补充的这个例子中我们可以清楚的看到构建自己的镜像可以依赖于其他镜像构建环境基本上都已经准备好了,非常方便。第二个例子是构建 Hexo 博客环境的做法。


管理 Docker 镜像


拿来主义:pull(抓取)


第一种是直接抓取比如说下载一个 docker 管理用的 dockerui 工具,我们可以直接告诉 docker pull 回来:


root# docker pull dockerui/dockerui

也可以通过运行的方式来抓回镜像:


root# docker run -d -p 9000:9000 --privileged \
-v /var/run/docker.sock:/var/run/docker.sock dockerui/dockerui

这样用直接访问 http://localhost:9000/ 的方法来管理 docker 了。


查看自己所有的镜像


root# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
hexo3 latest 66f7fc371b44 9 days ago 261.5 MB
node slim 84326d4c0101 11 days ago 164.5 MB
dockerui/dockerui latest 9ac79962b9b0 2 weeks ago 5.422 MB
busybox latest 8c2e06607696 7 weeks ago 2.433 MB
garland/butterfly latest 114f9c134231 3 months ago 394.5 MB
progrium/busybox latest 8cee90767cfe 4 months ago 4.789 MB

搜索需要的镜像


推荐到 Docker Hub Registry 上搜索。


但是也可以通过命令 docker search 关键词 进行搜索。


清空所有当前镜像


使用命令:docker rmi $(docker images -q)


如果当前已经没有镜像了命令会报错找不到镜像。



警告:请谨慎清空所有镜像。



构建镜像的缓存


在构建镜像时 Dockerfile 中的指令会进入缓存,如果构建时可以使用缓存,docker 可以做到快速完成构建。


Docker 数据卷的挂载与外部服务访问


首先要注意的一点:容器被删除后里面的数据会被删除,因此要注意挂载数据卷使数据持久化。


因此这里介绍一下挂载宿主机中的目录作为数据卷到 Docker 中的方法:


root# docker run -it -d -p 4000:4000 -v /root/blog/:/hexo --name hexo hexo3
这是上一个案例中运行 Hexo 博客的方法,注意挂载描述在 -v 之后,容器中就可以访问到宿主机中的持久化位置了。



注意,在数据库应用中最需要根据配置文件将数据库持久化的位置放到宿主机中。



对于开发更加详细的意见可以产考 使用 Docker 做开发的建议团队工作流


为了使 Hexo 博客可以被宿主机以外的设备访问,这里使用 -p 参数来发布 Docker 的端口到宿主机中。


Docker 学习建议:



  1. 在详细实践完成本文之后如果您有精力,并且英文阅读能力还不错请您移步到 Docker官方文档 继续更深入的学习。

  2. Docker 只是一种非常实用的工具,不要以 Docker 为目的去学习 Docker,重要的不是 Docker 而是您用 Docker 做什么。


总结


由于篇幅有限新手教程就到这里,希望您在这半个小时到一个小时中能有一次非常完美的 Docker 学习体验,在接下来的学习中您还可以继续从 Docker 官方的文档中了解更多的 Docker 相关的信息,尤其是 Docker 容器与容器之间的问题解决,以及更多更加丰富的命令参数使用,比如环境变量的控制。


相信做完这部分实践之后您已经了解了 Docker 能够做什么,如何进行基础方面的使用。在下一篇中我们会 根据您现有的基础介绍如何使用 DaoCloud 各项服务。


在接下来的学习中,如果需要请您学习使用版本控制工具 Git,及其相关基础知识。