huandu

huandu 查看完整档案

填写现居城市华中科技大学  |  电子信息工程 编辑滴滴  |  架构师 编辑填写个人主网站
编辑

我是 NPC

个人动态

huandu 赞了文章 · 2016-10-27

PHP 混合 Go 协程并发

想法很简单。通过设置 runtime.GOMAXPROCS(1) 让 golang 的进程变成单线程执行的。类似python用gevent的效果。然后通过调度多个协程实现异步I/O并发。php作为一个子函数跑在go的进程内,php需要yield到其他协程时,通过回调到golang函数来实现。从php里调用go提供的子函数时,go保证保存php的当前上下文。当协程执行权让渡回来的时候,把原来的php上下文恢复。关键的代码在:

// 保存当前协程上的php上下文
    oldServerCtx := engine.ServerContextGet()
    fmt.Println(oldServerCtx)
    defer engine.ServerContextSet(oldServerCtx)
    oldExecutorCtx := engine.ExecutorContextGet()
    fmt.Println(oldExecutorCtx)
    defer engine.ExecutorContextSet(oldExecutorCtx)
    oldCoreCtx := engine.CoreContextGet()
    fmt.Println(oldCoreCtx)
    defer engine.CoreContextSet(oldCoreCtx)

// 放弃全局的锁,使得其他的协程可以开始执行php
    engineLock.Unlock()
    defer engineLock.Lock()

ServerContextGet 这几个函数是我加的,获得的是php的(EG/SG/PG)这三个全局context(参见:http://www.cnblogs.com/chance...)。修改过的github.com/deuill/go-php的源代码在:https://github.com/taowen/go-...

完整的php/go混合协程的demo:

package main

import (
    "fmt"
    "github.com/deuill/go-php/engine"
    "os"
    "runtime"
    "time"
    "sync"
)

type TestObj struct{}

func newTestObj(args []interface{}) interface{} {
    return &TestObj{}
}
var engineLock *sync.Mutex

func (self *TestObj) Hello() {
    oldServerCtx := engine.ServerContextGet()
    fmt.Println(oldServerCtx)
    defer engine.ServerContextSet(oldServerCtx)
    oldExecutorCtx := engine.ExecutorContextGet()
    fmt.Println(oldExecutorCtx)
    defer engine.ExecutorContextSet(oldExecutorCtx)
    oldCoreCtx := engine.CoreContextGet()
    fmt.Println(oldCoreCtx)
    defer engine.CoreContextSet(oldCoreCtx)
    engineLock.Unlock()
    defer engineLock.Lock()
    time.Sleep(time.Second)
    fmt.Println("sleep done")
}

func main() {
    runtime.GOMAXPROCS(1)
    theEngine, err := engine.New()
    engineLock = &sync.Mutex{}
    if err != nil {
        fmt.Println(err)
    }
    _, err = theEngine.Define("TestObj", newTestObj)
    wg := &sync.WaitGroup{}
    wg.Add(2)
    before := time.Now()
    fmt.Println("1")
    go func() {
        engineLock.Lock()
        defer engineLock.Unlock()
        context1, err := theEngine.NewContext()
        if err != nil {
            fmt.Println(err)
        }
        context1.Output = os.Stdout
        if err != nil {
            fmt.Println(err)
        }
        fmt.Println("1 enter")
        _, err = context1.Eval("$testObj = new TestObj(); $testObj->Hello();")
        fmt.Println("1 back")
        if err != nil {
            fmt.Println(err)
        }
        //theEngine.DestroyContext(context1)
        fmt.Println("1 done")
        wg.Done()
    }()
    fmt.Println("2")
    go func() {
        engineLock.Lock()
        defer engineLock.Unlock()
        context2, err := theEngine.NewContext()
        if err != nil {
            fmt.Println(err)
        }
        if err != nil {
            fmt.Println(err)
        }
        context2.Output = os.Stdout
        fmt.Println("2 enter")
        _, err = context2.Eval("$testObj = new TestObj(); $testObj->Hello();")
        fmt.Println("2 back")
        if err != nil {
            fmt.Println(err)
        }
        //theEngine.DestroyContext(context2)
        fmt.Println("2 done")
        wg.Done()
    }()
    wg.Wait()
    after := time.Now()
    fmt.Println(after.Sub(before))
}

执行结果是

1
2
2 enter
{0x2cf2930 {<nil> <nil> <nil> 0 <nil> <nil> <nil> <nil> 0 0 0 [0 0 0 0 0] <nil> <nil> <nil> <nil> <nil> <nil> <nil> 0 0 <nil> 1000 [0 0 0 0]} {{<nil> <nil> 0 16 0x7f682e819780 0 [0 0 0 0 0 0 0] <nil>} 0 1 [0 0 0] <nil> <nil>} 0 0 0 [0 0 0 0 0 0] {0 0 0 0 0 0 0 0 0 0 0 {0 0} {0 0} {0 0} [0 0 0]} 0x2a00270 0x2a00f60 <nil> 8388608 0 1 [0 0 0] 0 {8 7 2 [0 0 0 0] 0 0x29f4520 0x29f4520 0x29f4470 0x29f4420 <nil> 1 0 0 [0 0 0 0 0]} <nil> {0 [0 0 0 0 0 0 0] <nil> <nil> <nil> <nil>} 0 [0 0 0 0 0 0 0]}
{0x7ffd30bac588 {[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] 2 0 0 [0 0]} 0x7f682f01b928 {[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] 1 0 0 [0 0]} 0x7f682f01b948 [<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>] 0x7f682f01ba60 0x7f682f01b960 0x7f682f167168 0x7f682f01ba88 {64 63 5 [0 0 0 0] 0 0x7f682f1972d8 0x7f682f1972d8 0x7f682f1993f8 0x7f682f1970c8 0x7f682e862d10 0 0 1 [0 0 0 0 0]} {8 0 0 [0 0 0 0] 0 <nil> <nil> <nil> 0x7f682f016a00 <nil> 0 0 1 [0 0 0 0 0]} 0x7ffd30bac590 22527 0 0 [0 0 0 0] 0x7f682f197640 0x29f4f80 0x29f4fd0 0x29f5070 <nil> 0x2cf2950 0x7f682f1989c0 14 0 1 [0 0 0] <nil> <nil> 0 1 [0 0 0 0 0 0] {8 0 0 [0 0 0 0] 1 <nil> <nil> <nil> 0x7f682f016a00 0x7f682e883140 0 0 1 [0 0 0 0 0]} {8 0 0 [0 0 0 0] 0 <nil> <nil> <nil> 0x7f682f016a00 0x7f682e8831d0 1 0 0 [0 0 0 0 0]} 0x7f682f167088 0 [0 0 0 0] <nil> <nil> {0 0 <nil>} {0 0 <nil> <nil> 0 [0 0 0 0 0 0 0]} {0 0 <nil> <nil> 0 [0 0 0 0 0 0 0]} 0 [0 0 0 0] <nil> 0 0 0x29fb2e0 <nil> <nil> {0x7f682f187030 2 1024 -1 [0 0 0 0]} <nil> <nil> <nil> [{0x7f682e915050 [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] 0 0 149 8 8 8} {0x7f682e915050 [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] 0 0 149 8 8 8} {0x7f682e915050 [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] 0 0 149 8 8 8}] 0x7f682f167168 <nil> {0 [0 0 0 0] <nil> 0 [0 0 0 0] 0 0 [0 0 0 0] <nil> 0 [0 0 0 0] <nil>} 1 [0 0 0 0 0 0 0] <nil> 0x7f682f01bde8 895 [0 0 0 0 0 0] [<nil> <nil> <nil> <nil>]}
{1 [0 0 0 0 0 0 0] 0 0 0 [0 0 0 0 0 0] <nil> 0x29ff9a0 17 134217728 -1 0 0 0 1 [0 0 0 0] 1024 0 0 1 [0 0 0 0 0] 0x2a00870 <nil> 0x2a010a0 0x7f682ecc58b0 <nil> 0x7f682ecc5c23 <nil> <nil> <nil> 2097152 <nil> <nil> 0x2a00180 0x2a00230 <nil> <nil> <nil> {0x7f682ec91aa8 0x7f682ec91aa8} 0x2a00910 {0 0 0 [0 0 0 0] 0 <nil> <nil> <nil> <nil> <nil> 0 0 0 [0 0 0 0 0]} 0 0 0 [0 0 0] {0x2b6dc10 0x2b6dc10 1 8 <nil> 1 [0 0 0 0 0 0 0] <nil>} [0x7f682f197330 0x7f682f197040 0x7f682f197410 <nil> <nil> 0x7f682f1974f0] 0 1 1 [0 0 0 0 0] 0x7f682ec9544b 0x7f682ec9544b 0 0 [0 0 0 0 0 0] 0 [0 0 0 0 0 0 0 0] 1 1 1 1 1 0 1 [0] 0 [0 0 0 0] <nil> <nil> 0 [0 0 0 0] 0x2cf27c0 <nil> 0 0 [0 0 0 0 0 0] 64 1000 0 [0 0 0 0 0 0 0] 0x7f682ecc6270 300 0x2a009b0 1 [0 0 0 0 0 0 0] <nil> 0 [0 0 0 0 0 0 0]}
1 enter
{0x7f6818000aa0 {<nil> <nil> <nil> 0 <nil> <nil> <nil> <nil> 0 0 0 [0 0 0 0 0] <nil> <nil> <nil> <nil> <nil> <nil> <nil> 0 0 <nil> 1000 [0 0 0 0]} {{<nil> <nil> 0 16 0x7f682e819780 0 [0 0 0 0 0 0 0] <nil>} 0 1 [0 0 0] <nil> <nil>} 0 0 0 [0 0 0 0 0 0] {0 0 0 0 0 0 0 0 0 0 0 {0 0} {0 0} {0 0} [0 0 0]} 0x2a00270 0x2a00f60 <nil> 8388608 0 1 [0 0 0] 0 {8 7 2 [0 0 0 0] 0 0x29f4520 0x29f4520 0x29f4470 0x29f4420 <nil> 1 0 0 [0 0 0 0 0]} <nil> {0 [0 0 0 0 0 0 0] <nil> <nil> <nil> <nil>} 0 [0 0 0 0 0 0 0]}
{0x7f682a4cccd8 {[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] 2 0 0 [0 0]} 0x7f682f01b928 {[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] 1 0 0 [0 0]} 0x7f682f01b948 [<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>] 0x7f682f01ba60 0x7f682f01b960 0x7f682802f110 0x7f682f01ba88 {64 63 5 [0 0 0 0] 0 0x7f682f197a00 0x7f682f197a00 0x7f682f198368 0x7f682f198fa0 0x7f682e862d10 0 0 1 [0 0 0 0 0]} {8 0 0 [0 0 0 0] 0 <nil> <nil> <nil> 0x7f682f016a00 <nil> 0 0 1 [0 0 0 0 0]} 0x7f682a4ccce0 22527 0 0 [0 0 0 0] 0x7f682f197d28 0x29f4f80 0x29f4fd0 0x29f5070 <nil> 0x2cf2950 0x7f682f1983e8 14 0 1 [0 0 0] <nil> <nil> 0 1 [0 0 0 0 0 0] {8 0 0 [0 0 0 0] 1 <nil> <nil> <nil> 0x7f682f016a00 0x7f682e883140 0 0 1 [0 0 0 0 0]} {8 0 0 [0 0 0 0] 0 <nil> <nil> <nil> 0x7f682f016a00 0x7f682e8831d0 1 0 0 [0 0 0 0 0]} 0x7f682802f030 0 [0 0 0 0] <nil> <nil> {0 0 <nil>} {0 0 <nil> <nil> 0 [0 0 0 0 0 0 0]} {0 0 <nil> <nil> 0 [0 0 0 0 0 0 0]} 0 [0 0 0 0] <nil> 0 0 0x29fb2e0 <nil> <nil> {0x7f682804efd8 2 1024 -1 [0 0 0 0]} <nil> <nil> <nil> [{0x7f682e915050 [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] 0 0 149 8 8 8} {0x7f682e915050 [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] 0 0 149 8 8 8} {0x7f682e915050 [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] 0 0 149 8 8 8}] 0x7f682802f110 <nil> {0 [0 0 0 0] <nil> 0 [0 0 0 0] 0 0 [0 0 0 0] <nil> 0 [0 0 0 0] <nil>} 1 [0 0 0 0 0 0 0] <nil> 0x7f682f01bde8 895 [0 0 0 0 0 0] [<nil> <nil> <nil> <nil>]}
{1 [0 0 0 0 0 0 0] 0 0 0 [0 0 0 0 0 0] <nil> 0x29ff9a0 17 134217728 -1 0 0 0 1 [0 0 0 0] 1024 0 0 1 [0 0 0 0 0] 0x2a00870 <nil> 0x2a010a0 0x7f682ecc58b0 <nil> 0x7f682ecc5c23 <nil> <nil> <nil> 2097152 <nil> <nil> 0x2a00180 0x2a00230 <nil> <nil> <nil> {0x7f682ec91aa8 0x7f682ec91aa8} 0x2a00910 {0 0 0 [0 0 0 0] 0 <nil> <nil> <nil> <nil> <nil> 0 0 0 [0 0 0 0 0]} 0 0 0 [0 0 0] {0x2b6dc10 0x2b6dc10 1 8 <nil> 1 [0 0 0 0 0 0 0] <nil>} [0x7f682f197a58 0x7f682f198ce0 0x7f682f197b38 <nil> <nil> 0x7f682f197c18] 0 1 1 [0 0 0 0 0] 0x7f682ec9544b 0x7f682ec9544b 0 0 [0 0 0 0 0 0] 0 [0 0 0 0 0 0 0 0] 1 1 1 1 1 0 1 [0] 0 [0 0 0 0] <nil> <nil> 0 [0 0 0 0] 0x2cf27c0 <nil> 0 0 [0 0 0 0 0 0] 64 1000 0 [0 0 0 0 0 0 0] 0x7f682ecc6270 300 0x2a009b0 1 [0 0 0 0 0 0 0] <nil> 0 [0 0 0 0 0 0 0]}
sleep done
1 back
1 done
sleep done
2 back
2 done
1.00099211s

可以看到两个sleep 1s,最终只用了1.00099211s。说明协程是并发的。

一些性能指标。走http调用后端,在i7-6700k上,用ab -n 100 -c 4 可以跑出这样的结果

Requests per second:    3183.70 [#/sec] (mean)
Time per request:       1.256 [ms] (mean)
Time per request:       0.314 [ms] (mean, across all concurrent requests)

如果不用http调用后端,直接php=>go返回"hello",则可以达到

Requests per second:    10073.54 [#/sec] (mean)
Time per request:       0.397 [ms] (mean)
Time per request:       0.099 [ms] (mean, across all concurrent requests)

这些指标只说明了协程切换的成本。实际的收益取决于后端的http服务的延迟,如果耗时很长,通过协程并发则可以收益明显。

这个实验说明了可以用golang实现一个代替nginx+php-fpm的应用服务器。并且提供了一条从php向golang迁移的平滑迁移路径。在一个应用里混合PHP和Go两种语言。

并且可以通过提供golang函数给php调用的方式实现I/O的异步化。像libcurl这样的扩展自身是支持异步回调的,只是php是同步的所以只给php暴露了同步的execute。有了Golang之后,可以把execute变成对异步execute+callback的包装,从而实现基于协程的调度。

参考资料:

查看原文

赞 7 收藏 37 评论 5

huandu 赞了文章 · 2015-11-09

应该对什么告警?

告警的本质

没有多少系统的告警是设计得当的。良好的告警设计是一项非常困难的工作。如何知道你收到的告警是糟糕的?多少次你收到了告警之后,立即就关掉了的?是不是成天被这些然而并没有什么卵用的东西给淹没?最常见的告警设置:cpu使用率超过90%,然后告警。这种设置在大部分场合下是没有办法提供高质量的告警的。

高质量的告警应该是这样的:每次收到之后你可以立即评估影响的范围,并且每一个告警需要你做出分级响应。所谓每个告警都应该是,actionable的。

告警的实质可以用下图表明:

图片描述

服务器的设计应该是以这样的无人值守为目的的。假设所有的运维全部放假了,服务也能7*24自动运转。

图片描述

告警的实质就是“把人当服务用”。在一些事情还没有办法做到程序化执行的时候,用告警通知人的方式去干预系统达到修正的目的。一次告警就像一次服务调用一样。如果告警了,但是收到告警的人并不需要做任何处理,那么这就是一种DDoS攻击,攻击的是运维的幸福生活。

很多时候,告警通知人去干的事情是真的可以被自动化掉的。比如服务器挂了,换一台上来。在小一点的系统里,可能就是停机一会,人工来处理换一台冷备的机器上去。大一点的系统,因为服务器多了,天天都挂可不行,必须是热备的,系统自动切换到备机。再大一点的系统,因为切换实在太频繁了,故障机的退库,备机的保有都变成了一种管理负担,那么可以和其他的运维流程打通变成完全自动化的系统。只是因为业务处理不同阶段,选择不同的实现策略而已。业务量小,拿血肉当机器用,有的时候更经济而已。当然对于那个被当成机器人来用的哥们来说,生活确实有点不公平。

告警对象

告警对象可以分为两种:

  • 业务规则监控
  • 系统可靠性监控

对于业务规则监控可以举一个游戏的例子。比如DNF的游戏角色在一定装备的情况下,单次打击的伤害输出应该是有一个上限,如果超过了就说明有作弊的情况。又比如斗地主游戏里一个人的连胜场次是有一定上限的,每天的胜率是有一定上限,如果超出平均值太多就可能是作弊。业务规则监控的不是硬件,也不是软件是否工作正常。而是软件是否按照业务规则实现的,是否有漏洞。也可以理解为对“正确性”的监控。

系统可靠性监控是最常见的监控形式,比如发现是不是服务器挂掉了,服务是不是过载了等等。对于大部分后台服务,系统可以抽象建模成这个样子:

图片描述

对于这样的系统可以采集什么指标?

  • 请求数,请求到达速率
  • 正常响应数,正常响应占比
  • 错误响应数,错误响应占比
  • 响应延时
  • 队列长度,排队时间

实际的情况是,几乎任何系统都不是孤立运行的。而是这样的:

图片描述

一个DB会依赖于底层的cpu,内存,磁盘等资源。一个Http服务会依赖于底层的DB服务。一个应用会依赖于数个底层的RPC服务。于是又多了几个指标

  • 资源A的调用量(比如CPU使用率)
  • 资源B的调用量(比如内存分配和释放)
  • 资源C的调用量(比如网络发送包量)
  • ...

这种层次结构,一般来说简单来说可以分为四层:

  • 产品策略和营销:它们决定了根本的请求到达的速率
  • 应用层(更粗俗一点可以叫web层):最上层的胶水
  • 服务层:db,各种RPC服务,以及层层嵌套的服务
  • 硬件层:cpu,内存,磁盘,网络

图片描述

因为这样的一个依赖层次。上一层对下一层的资源消耗量变成了下一层的请求数。比如Http服务消耗了多少DB的资源,就对应了DB服务需要处理多少请求数。DB繁忙与否取决于Http服务请求,Http服务请求繁忙与否取决于多少人打开客户端,多少人打开客户端又取决于产品策略和营销活动。这种层次结构决定了单纯跟踪一个指标,比如绝对请求数,很难说明这一层的服务是否出现了故障。

有这么多层次,每层又有很多指标可以采集。那么应该采集什么指标,用什么告警策略去告警呢?最前面已经提到了告警必须是actionable的,但是实际情况下只有这种纲领性要求仍然是不好操作的。至少可以提几点不应该做的事情:

  • 不应该用采集的难度决定你使用什么指标去告警。很多情况下cpu使用率可能是最好采集的,但是未必是最值得告警的。
  • 不要给运维他们想要的告警,而是要做“真正”想要的告警。大部分情况下,人们告诉你的是一个解决方案。运维告诉你它需要对db进程的cpu使用率超过x%的时候告警,它给你的是一个他认为最优的解决方案。但是他真正想要的是知道db服务是否有异常,cpu使用率超过x%未必是最好的告诉你服务是否出现异常的指标。

盲目地采集那些容易获取的指标,并随意地设定阈值告警是大部分糟糕的告警质量的根源。

监控的指标和策略

那到底应该采集什么指标呢?我认为大部分的系统可靠性监控不外乎三个目标:

  • is the work getting done?系统是否在持续完成其设定的工作。
  • is the user having good experience?用户体验是否好。
  • where is the problem/bottleneck?问题或者瓶颈在哪里。

其中最核心最关键的是第一个问题,is the work getting done。对于数据库来说,我们可以采集:

  • cpu 使用率
  • 网络带宽大小
  • db请求数
  • db响应数
  • db错误响应数
  • db请求延迟

显然要回答一个db是否完成了其指定的工作,更应该关注的指标是这两个:

  • db请求数的绝对量
  • db正确响应相对请求数的占比

这两个指标相对于采集什么cpu使用率更能说明问题。不仅仅是db,各个层次的服务都可以用请求量和正确响应占比来反映其工作状况。比如http请求数(对比http正确响应数),比如app打开次数(对比服务端记录的在线人数)等等。

为什么cpu使用率不能说明问题?大部分时候,我们并不关心cpu本身,而关心使用cpu为资源的服务。所以cpu使用率只是一种资源的请求数而已。与请求数相关的一个概念是saturation(上限),当上限达到的时候,处理开始排队,延迟开始变长,错误率开始升高。那么cpu使用率是不是能够说明上限呢?cpu使用率的上限以100%记,那么90%开始告警不是很合理吗?毕竟cpu 100%了几乎可以等同于db无法正常处理请求了。

这种利用底层资源调用量,评估其是否达到上限的做法有两个根本缺陷:

  • 你无法知道上层服务可以把底层资源利用到什么程度
  • 底层资源的 saturation 未必可以容易度量

具体来说,db是不是可以真的100%利用cpu是位置的。假如请求里锁,或者sleep,那么也许cpu永远也无法达到100%。90%可能就是极限了。而且现代的cpu是多核的,如果请求处理只能利用单核,处理在多个核之间跳跃,对于一个核来说永远也不会一直保持100%。

对于cpu可能其上限真的有一个100%的值。但是对于很多非硬件的服务,比如你是一个登陆服务,依赖于一个db。那么这个db每秒可以处理的不同sql组合数是很难度量的,绝非和磁盘一样有一个mb/s的极限绝对值可以做为对比。

而且度量底层资源的使用还有一个缺陷是你无法枚举出所有依赖的资源的。所以与其这么绕弯子地通过底层资源来间接监控上层服务是否正常,还不如直接测量work是不是getting done呢。

对于第二个问题,is the user having good experience?可以采集的指标为

  • 平均排队时间,平均总响应延迟
  • 99/95/90 percentile的排队时间,99/95/90 percentile的响应延迟

这里的用户不一定是指人或者玩家,可能是上一层的服务调用方,另外一个系统。

第三个问题就是所谓的故障定位。要是人工来做的话,最常见的做法是收到了告警,然后登陆CRT,开始敲各种命令查找原因。对于系统来说,最合适的做法不是出了问题再去执行一堆命令,而是:

  1. 每个层次都对自己做告警
  2. 顶层服务出了告警触发自动定位程序
  3. 按照服务的依赖关系和大致的时间范围,定位到告警之间的关联,从而找到出问题或者瓶颈的地方

当然实际情况是很复杂的。很多原因和结果是互为因果的。两个告警是两个现象,还是一个原因一个现象实际上很难说得清楚。

从告警算法的角度来讲,对成功请求率,或者平均响应延迟做告警是非常容易的。静态阈值大家看不起,觉得简单。但是大部分告警用静态阈值就可以解决问题。

理论与现实

那告警要不要高难度的算法?我的观点是采集到了正确的指标,是不需要复杂算法的,就是静态阈值都可以搞得定。但是至少有三种场合需要算法:

  • 无法直接采集到错误数:需要对错误日志的自动分类
  • 无法直接采集到请求成功率:需要对请求数或响应数的绝对值做异常检测
  • 只有总数,无法采集到其中的每个细分构成项的占比:需要对参与的factor进行算法拟合

其实这三项都是一个主题的,当你无法直接获取到告警所需的指标的时候,事情会变得复杂很多。有一个比喻是:最近NASA宣布的地球孪生兄弟Kepler 452b。如果我们的探测器可以跑到1400光年之外,发现他将是非常容易的事情。正式因为直接获得数据非常困难,所以科学家才需要根据行星阻挡恒星时引起的亮度变化(所谓掩星法)来发现这些遥远的星球。

采集所需的指标的困难可能是几方面的因素。一种原因是采集本身是非常消耗资源的事情。比如获取每个mysql查询所消耗的cpu。跟踪每个请求处理过程是不可能的。这个时候就需要算法的帮助了,可以仔细看一下vividcortex的视频:http://www.youtube.com/watch?v=szAfGjwLO8k

更多情况是采集指标困难是D/O分离造成的沟通问题,运维需要的指标需要开发去埋点,而开发埋点的地方又需要运维去做告警。很多时候退而求其次就会造成,有什么指标就用什么指标的状况。比如虽然没有请求响应的错误数,但是错误基本上都会有错误日志记录,根据错误日志滚动的快慢可以大致知道是不是出了问题。这就引入了一个非常困难的日志分类问题,什么日志代表了正常,什么日志代表了异常,异常又非了哪些类型?这个方面算法做得好的是summo logic公司:https://www.sumologic.com/ 。为什么这种opsdev(嘲讽devops那)公司如此热衷于算法?对于他们来说好处是显而易见的,客户需要做的改动越少,接入成本越低,客户面就越广。但是拿机器算法去挖掘海量日志真的是回答:is the work getting done?的最佳手段?显然不是。这就是大炮打蚊子。日志的存在是用于解决问题,而不是有了海量日志了,如何用好“它们”变成了问题本身。

第三类情况是没有办法采集到请求成功率,只能对绝对的处理成功的量。只有这类数据要告警,就无法做简单的静态阈值了。对于延迟,一般可以定一个业务上可以接受的延迟上限。对于成功率,也可以定一个可接受的成功率上限。但是对于绝对的处理量,是没有办法简单地比较一个静态阈值就可以判断是正常还是异常的。

在讨论如何实现之前,再强调两点:

  • 处理成功的量不是度量is work getting done的最佳指标。费事费力去搞算法,不如直接把成功率指标给采集了。
  • 处理成功的量,还取决于请求数。而请求数根本上是取决于上层服务了。你是一个dba,发现db的每秒处理的请求数陡降了。这说明是db故障了?还是app故障了?都有可能……最最上层是产品和营销。你发现一个业务的注册量相对前几天变少了,这个是不是说明注册服务出问题了?也需是产品太烂了,游戏根本没有人来玩。也可能是营销手段的营销,不送金币了,玩家没积极性了。

异常检测

只有请求数,没有参考的上限值(saturation),也没有成功率,没有失败率,怎么检测异常?

bVdeIB

上图的黄线是昨天的值,绿线是今天的值,大部分服务监控的曲线图都长这样。可以得出四个思路:

  • 曲线平滑:故障一般是对近期趋势的一个破坏,视觉上来说就是不平滑
  • 绝对值的时间周期性:两条曲线几乎重合
  • 波动的时间周期性:假设两个曲线不重合,在相同时间点的波动趋势和振幅也是类似的
  • 有一个长度可观的坑:当曲线开始回升到历史范围的时候,一般可以确认这个时间段是真的故障了

从这四种直觉展开,可以得出各种或复杂或简单的算法。下面要讲的算法都是非常简单的,无需很高深的数学知识。

基于曲线的平滑性的检测

这种检测的根据是在一个最近的时间窗口,比如1个小时。曲线会遵循某种趋势,而新的数据点打破了这种趋势,使得曲线不光滑了。也就是说,这种检测利用的是时间序列的temporal dependency,T对于T-1有很强的趋势依赖性。业务逻辑上来说,8:00 有很多人登陆,8:01 也有很多人来登陆的概率是很高的,因为吸引人来登陆的因素是有很强的惯性的。但是7.1很多人来登陆,8.1也有很多人来登陆的惯性就要差很多。

基于近期趋势做告警,就需要对曲线的趋势进行拟合。拟合有两种方式,moving average 或者 regression。这两种拟合方式有不同的bias(倾向)。

statistical-anomaly-detection-for-database-monitoring-33-638.jpg?cb=1425415561

这就是一种moving average的算法图,叫做exponentially weighted moving average。它的计算非常简单

图片描述

x是实际值,s是ewma计算出来的平均值。也就是下一点的平均值是由上一点的平均值,加上当前点的实际值修正而来。这个修正的比例,就取决月这个alpha的decay factor的大小。视觉上来说就是ewma曲线是否紧跟实际曲线,也就是平滑程度。

statistical-anomaly-detection-for-database-monitoring-36-638.jpg?cb=1425415561

有了平均值之后可以计算方差,方差乘以一定的倍数可以得出对于振幅的容忍范围。比较实际的值是否超出了这个范围就可以知道是否可以告警了。超出了上界,可能是突然用户量突然激增了。超出了下届,可能是营销活动结束了,用户快速离开,也可能是光纤断了,玩家掉线了。想要了解更多关于ewma的算法细节:关注Baron Schwartz(http://www.slideshare.net/vividcortex/statistical-anomaly-detection-fo...

moving average认为曲线是趋向于历史的,如果曲线的势头是上升,那么它认为下一个点应该是开始下降的。regression认为曲线是趋向于未来的,如果曲线的势头是上升,那么它认为下一个点应该是保持这个上升势头。还有更复杂的模型是综合了moving average和regression的。无论是哪种算法,用过去10分钟预测下10分钟是不可能精确的。如果这种预测可以精确,那么股神早就诞生了。使用moving average,可能会掩盖故障产生的下降(因为其bias是下降)。如果使用regression,那么又有可能把没有上升得那么快当成故障了(因为其bias是上升)。

这种基于近期趋势计算方差的算法还有一个缺陷是当前面几个点振动很大的时候,方差值会被搞大。后面的故障就被掩盖了,使得连续的故障点无法被检测到。其实也就是算法对于什么是正常是没有概念的,它认为过去的历史就是正常。如果过去几分钟处于故障中,那么故障的曲线就是正常。

实际使用中发现这种基于曲线平滑度的算法的优点有

  • 依赖的数据少,只需要近期的历史,不依赖于周期性
  • 非常敏感,历史如果波动很小,方差就很小,容忍的波动范围也会非常小

缺点也是显著的

  • 过于敏感,容易误报。因为方差会随着异常点的引入而变大,所以很难使用连续三点才告警这样的策略
  • 业务曲线可能自身有规律性的陡增和陡降

最佳的使用方式是不用一根曲线做告警。结合几条相关的曲线,如果同时出现平滑度破坏的情况,而且与业务规律的趋势相背离(比如在线人数降低,登陆请求数增高)则可以认定为业务出现故障。

基于绝对值的时间周期性

view

上图中不同的颜色代表了不同日期的曲线。很多监控曲线都有这样以一天为周期的周期性(早上4点最低,晚上11点最高之类的)。一种利用时间周期性的最简单的算法

min(14 days history) * 0.6

对历史14天的曲线取最小值。怎么个取最小值的方法?对于12:05分,有14天对应的点,取最小值。对于12:06分,有14天对应的点,取最小值。这样可以得出一条一天的曲线。然后对这个曲线整体乘以0.6。如果几天的曲线低于这条参考线则告警。

这其实是一种静态阈值告警的升级版,动态阈值告警。过去静态阈值是一个根据历史经验拍脑袋的产物。用这个算法,其实是把同时间点的历史值做为依据,计算出一个最不可能的下界。同时阈值不是唯一的一个,而是每个时间点有一个。如果1分钟一个点,一天中就有1440个下界阈值。

实际使用中0.6当然还是要酌情调整的。而且一个严重的问题是如果14天历史中有停机发布或者故障,那么最小值会受到影响。也就是说不能把历史当成正常,而是要把历史剔除掉异常值之后再进行计算。一个务实的近似的做法是取第二小的值。

为了让告警更加精确,可以累积计算实际曲线和参考曲线的差值之和。也就是相对于参考曲线下跌的面积。这个面积超过一定的值则告警。对于深度下跌,则累积几个点就可以告警。对于浅度下跌,那么多累几个点也可以告警出来。翻译成人话就是,一下在跌了很多,则很有可能是故障了。或者连续好久都偏离正常值,那么也很有可能是出问题了。

优点:

  • 计算简单
  • 可以确保发现大的故障,出了告警一定是大问题,可以直接打电话

缺点:

  • 依赖周期性的历史数据,计算量大,而且无法对新接入的曲线告警
  • 非常不敏感,小波动无法发现

基于振幅的时间周期性

figure7.png

有些时候曲线是有周期性,但是两个周期的曲线相叠加是不重合的。比如上图这样的,曲线整体的趋势是网上的。两个周期的曲线一叠加,一个会比另外一个高出一头。对于这种情况,利用绝对值告警就会有问题。

比如今天是10.1日,放假第一天。过去14天的历史曲线必然会比今天的曲线低很多。那么今天出了一个小故障,曲线下跌了,相对于过去14天的曲线仍然是高很多的。这样的故障如何能够检测得出来?一个直觉的说法是,两个曲线虽然不一样高,但是“长得差不多”。那么怎么利用这种“长得差不多”呢?那就是振幅了。

与其用x(t)的值,不如用x(t) - x(t-1)的值,也就是把绝对值变成变化速度。可以直接利用这个速度值,也可以是 x(t) - x(t-1) 再除以 x(t-1),也就是一个速度相对于绝对值的比率。比如t时刻的在线900人,t-1时刻的在线是1000人,那么可以计算出掉线人数是10%。这个掉线比率在历史同时刻是高还是低?那么就和前面一样处理了。

实际使用中有两个技巧:可以是x(t) - x(t-1),也可以是x(t) - x(t-5)等值。跨度越大,越可以检测出一些缓慢下降的情况。

另外一个技巧是可以计算x(t) -x(t-2),以及x(t+1) - x(t-1),如果两个值都异常则认为是真的异常,可以避免一个点的数据缺陷问题。

优点:

  • 比绝对值要敏感
  • 利用了时间周期性,规避了业务曲线自身的周期性陡降

缺点:

  • 要求原曲线是光滑的
  • 周期性陡降的时间点必须重合,否则误警
  • 按百分比计算容易在低峰时期误警
  • 陡降不一定代表故障,由上层服务波动引起的冲高再回落的情况时有发生

这种异常告警算法是比较优秀的。缺点也很多。所以可以进行一些修补凑合用。为了避免低峰时期,基于振幅百分比容易误警,可以加入绝对振幅的下限。业务上来说,就是小波动如果相对比率大,但是绝对影响范围小也是没关系的。对于冲高回落的问题,可以判断一下冲高的情况,对于冲高之后屏蔽一段时间。

基于曲线回升的异常判断

图片描述

图片描述

当我们看见图2的时候比图1更确认是故障了。为什么?因为图2中有一个明显的回升。算法其实和人眼一样。如果多等几个时间点,发现曲线回升了可以更很准确地判断“曾经”有一个故障。但是这种基于回升的异常检测是没有多少“告警”意义上的机制的。告警的作用就是让人参与干预,去帮助曲线回升。如果曲线已经开始回升,再告警不是事后诸葛了吗?

这种检测的意义在于机器复制告警的确认。当我们需要统计误警率,漏警率的时候。用另外一种视角的算法重新跑一遍可以统计出很多原算法的问题。同时也可以用半自动化的方式建立一个历史故障的样本库。这个样本库可以变成更复杂的机器学习算法的训练集。

总结

Key take away

  • 高质量的告警是actionable的
  • 不应该用采集的难度决定你使用什么指标去告警
  • 不要别人做什么告警,你就做什么,要做“真正”有用的告警:特别是cpu使用率告警
  • is work getting done:请求数 + 成功率
  • is the user having good experience:响应延迟
  • 只要采集对了指标,大部分时候告警不需要复杂算法
  • 基于算法的异常检测:算法不难,实在必要也是可以做到的
查看原文

赞 20 收藏 52 评论 4

huandu 赞了文章 · 2015-11-09

淘宝的消息中间件(2013)

这是一篇老帖子了,我是来学习的。原文在这里:http://jm-blog.aliapp.com/?p=3483。问题是图片都没有了。我费了很大功夫才把原文的图片给找回来。排版之后重新发布一遍。
淘宝的notify是一个非常有特色的消息中间件。它用创新地方式解决了分布式事务的问题,用相对较低的成本,实现了跨micro service的最终一致性。这种把最终一致性用application queue而不是database replication queue的方式来实现,把IT技术层面的跨业务的事务变成一个业务层面的单据传递的概念,非常值得推广。

消息中间件——分布式消息的广播员

综述
消息中间件是一种由消息传送机制或消息队列模式组成的最典型的中间件技术。通过消息中间件,应用程序或组件之间可以进行可靠的异步通讯来降低系统之间的耦合度,从而提高整个系统的可扩展性和可用性。

3.1、Notify

Notify是淘宝自主研发的一套消息服务引擎,是支撑双11最为核心的系统之一,在淘宝和支付宝的核心交易场景中都有大量使用。消息系统的核心作用就是三点:解耦,异步和并行。下面让我以一个实际的例子来说明一下解耦异步和并行分别所代表的具体意义吧:

假设我们有这么一个应用场景,为了完成一个用户注册淘宝的操作,可能需要将用户信息写入到用户库中,然后通知给红包中心给用户发新手红包,然后还需要通知支付宝给用户准备对应的支付宝账号,进行合法性验证,告知sns系统给用户导入新的用户等10步操作。
那么针对这个场景,一个最简单的设计方法就是串行的执行整个流程,如图3-1所示:

riyq2e.jpg

这种方式的最大问题是,随着后端流程越来越多,每步流程都需要额外的耗费很多时间,从而会导致用户更长的等待延迟。自然的,我们可以采用并行的方式来完成业务,能够极大的减少延迟,如图3-2所示。

Rva6Vj.jpg

但并行以后又会有一个新的问题出现了,在用户注册这一步,系统并行的发起了4个请求,那么这四个请求中,如果通知SNS这一步需要的时间很长,比如需要10秒钟的话,那么就算是发新手包,准备支付宝账号,进行合法性验证这几个步骤的速度再快,用户也仍然需要等待10秒以后才能完成用户注册过程。因为只有当所有的后续操作全部完成的时候,用户的注册过程才算真正的“完成”了。用户的信息状态才是完整的。而如果这时候发生了更严重的事故,比如发新手红包的所有服务器因为业务逻辑bug导致down机,那么因为用户的注册过程还没有完全完成,业务流程也就是失败的了。这样明显是不符合实际的需要的,随着下游步骤的逐渐增多,那么用户等待的时间就会越来越长,并且更加严重的是,随着下游系统越来越多,整个系统出错的概率也就越来越大。

通过业务分析我们能够得知,用户的实际的核心流程其实只有一个,就是用户注册。而后续的准备支付宝,通知sns等操作虽然必须要完成,但却是不需要让用户等待的。
这种模式有个专业的名词,就叫最终一致。为了达到最终一致,我们引入了MQ系统。业务流程如下:

主流程如图3-3所示:

3aeq2a.jpg
图3-3-用户注册流程-引入MQ系统-主流程

异步流程如图3-4所示:
yMNJBn.jpg
图3-4-用户注册流程-引入MQ系统-异步流程

核心原理

Notify在设计思路上与传统的MQ有一定的不同,他的核心设计理念是

  1. 为了消息堆积而设计系统
  2. 无单点,可自由扩展的设计

下面就请随我一起,进入到我们的消息系统内部来看看他设计的核心原理

为了消息堆积而设计系统在市面上的大部分MQ产品,大部分的核心场景就是点对点的消息传输通道,然后非常激进的使用内存来提升整体的系统性能,这样做虽然标称的tps都能达到很高,但这种设计的思路是很难符合大规模分布式场景的实际需要的。

在实际的分布式场景中,这样的系统会存在着较大的应用场景瓶颈,在后端有大量消费者的前提下,消费者出现问题是个非常常见的情况,而消息系统则必须能够在后端消费不稳定的情况下,仍然能够保证用户写入的正常并且TPS不降,是个非常考验消息系统能力的实际场景。

也因为如此,在Notify的整体设计中,我们最优先考虑的就是消息堆积问题,在目前的设计中我们使用了持久化磁盘的方式,在每次用户发消息到Notify的时候都将消息先落盘,然后再异步的进行消息投递,而没有采用激进的使用内存的方案来加快投递速度。

这种方式,虽然系统性能在峰值时比目前市面的MQ效率要差一些,但是作为整个业务逻辑的核心单元,稳定,安全可靠是系统的核心诉求。

无单点,可自由扩展的设计

BfmYbm.jpg

图3-5展示了组成Notify整个生态体系的有五个核心的部分。

  • 发送消息的集群这主要是业务方的机器,这些APP的机器上是没有任何状态信息的,可以随着用户请求量的增加而随时增加或减少业务发送方的机器数量,从而扩大或缩小集群能力。
  • 配置服务器集群(Config server)这个集群的主要目的是动态的感知应用集群,消息集群机器上线与下线的过程,并及时广播给其他集群。如当业务接受消息的机器下线时,config server会感知到机器下线,从而将该机器从目标用户组内踢出,并通知给notify server,notify server 在获取通知后,就可以将已经下线的机器从自己的投递目标列表中删除,这样就可以实现机器的自动上下线扩容了。
  • 消息服务器(Notify Server)消息服务器,也就是真正承载消息发送与消息接收的服务器,也是一个集群,应用发送消息时可以随机选择一台机器进行消息发送,任意一台server 挂掉,系统都可以正常运行。当需要增加处理能力时,只需要简单地增加notify Server就可以了
  • 存储(Storage)Notify的存储集群有多种不同的实现方式,以满足不同应用的实际存储需求。针对消息安全性要求高的应用,我们会选择使用多份落盘的方式存储消息数据,而对于要求吞吐量而不要求消息安全的场景,我们则可以使用内存存储模型的存储。自然的,所有存储也被设计成了随机无状态写入存储模型以保障可以自由扩展。
  • 消息接收集群业务方用于处理消息的服务器组,上下线机器时候也能够动态的由config server 感知机器上下线的时机,从而可以实现机器自动扩展。

3.2、Notify双11准备与优化

在双11的整个准备过程中,Notify都承载了非常巨大的压力,因为我们的核心假定就是后端系统一定会挂,而我们需要能够承载整个交易高峰内的所有消息都会堆积在数据库内的实际场景。
在多次压测中,我们的系统表现还是非常稳定的,以60w/s的写入量堆积4.5亿消息的时候,整个系统表现非常淡定可靠。在真正的大促到来时,我们的后端系统响应效率好于预期,所以我们很轻松的就满足了用户所有消息投递请求,比较好的满足了用户的实际需要。

3.3、MetaQ

METAQ是一款完全的队列模型消息中间件,服务器使用Java语言编写,可在多种软硬件平台上部署。客户端支持Java、C++编程语言,已于2012年3月对外开源,开源地址是:http://metaq.taobao.org/。MetaQ大约经历了下面3个阶段

在2011年1月份发布了MetaQ 1.0版本,从Apache Kafka衍生而来,在内部主要用于日志传输。
在2012年9月份发布了MetaQ 2.0版本,解决了分区数受限问题,在数据库binlog同步方面得到了广泛的应用。
在2013年7月份发布了MetaQ 3.0版本,MetaQ开始广泛应用于订单处理,cache同步、流计算、IM实时消息、binlog同步等领域。MetaQ3.0版本已经开源,参见这里
综上,MetaQ借鉴了Kafka的思想,并结合互联网应用场景对性能的要求,对数据的存储结构进行了全新设计。在功能层面,增加了更适合大型互联网特色的功能点。

MetaQ简介

maiyai.jpg
图3-6-MetaQ整体结构

如图3-6所示,MetaQ对外提供的是一个队列服务,内部实现也是完全的队列模型,这里的队列是持久化的磁盘队列,具有非常高的可靠性,并且充分利用了操作系统cache来提高性能。

  • 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。
  • Producer、Consumer、队列都可以分布式。
  • Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费* * 这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合。
  • 能够保证严格的消息顺序
  • 提供丰富的消息拉取模式
  • 高效的订阅者水平扩展能力
  • 实时的消息订阅机制
  • 亿级消息堆积能力

MetaQ存储结构

MetaQ的存储结构是根据阿里大规模互联网应用需求,完全重新设计的一套存储结构,使用这套存储结构可以支持上万的队列模型,并且可以支持消息查询、分布式事务、定时队列等功能,如图3-7所示。

feArQf.jpg
图3-7-MetaQ存储体系

MetaQ单机上万队列

MetaQ内部大部分功能都靠队列来驱动,那么必须支持足够多的队列,才能更好的满足业务需求,如图所示,MetaQ可以在单机支持上万队列,这里的队列全部为持久化磁盘方式,从而对IO性能提出了挑战。MetaQ是这样解决的

Message全部写入到一个独立的队列,完全的顺序写
Message在文件的位置信息写入到另外的文件,串行方式写。
通过以上方式,既做到数据可靠,又可以支持更多的队列,如图3-8所示。

NbiUZf.jpg
图3-8-MetaQ单机上万队列

MetaQ与Notify区别

  • Notify侧重于交易消息,分布式事务消息方面。
  • MetaQ侧重于顺序消息场景,例如binlog同步。以及主动拉消息场景,例如流计算等。

3.4、MetaQ双11准备与优化

Notify 交易消息转 MetaQ 方案改进

MetaQ 交易集群主要是 Notify 交易消息的一个镜像,旧有的方案是通过 Notify-Client 订阅 Notify 交易消息,然后再转投到 MetaQ 集群,这个方案的缺点:1. 通过消息订阅的方式给 Notify 集群带来比较大的压力 2. 一旦 MetaQ 集群处理不及时会给 Notify 造成消息的堆积,从而带来连锁不良效应。新的方案则是从Notify DB直接拉取binlog到MetaQ,它带来的优势: 1. 解放 NotifyServer 集群的压力;2. 通过 binlog 批量处理可以提升系统的吞吐量。

交易集群低延迟优化

天猫直播间,旨在通过实时获取活动当天的交易数据,通过实时流计算的方式,及时、准确的展示各业务数据。它的特点就是数据全而准确、实时性要求较高。而在全链路压测过程中发现从 Notify Mysql binlog 获取数据时,出现较大的延迟,最严重的延迟高达4h+,这显然是不合系统需求的。基于这些问题,我们在 Notify Mysql 端做了很多的优化:
Mysql 数据库实例扩容,从而提高集群的整体吞吐量;
对 binlog 的存放位置进行优化,将数据存储以及 binlog 存储进行分离,从而发挥 DB 的最大写性能;
由于 MySQL 的 binlog 操作存在锁操作,优化了 MySQL 生成 binlog 的配置,保证了拉 binlog 无延时。

针对不同集群运行参数调优

根据业务的特点,对不同集群的运行参数调优,如批量拉取大小,刷盘方式,数据有效期等等;同时对io调度、虚拟内存等参数进行调优,以提供更为高效的堆积。

监控与实时告警

任何一个值得信赖的系统,最低限度是需要做到及时发现并处理异常,在第一时间排除故障发生的可能,从而提高产品的可用性。在双十一活动之前,我们实现了由 MetaQ 系统内部,根据集群状态,各消息的业务数据指标进行监控统计并主动告警,同时还能通过 Diamond 做到动态调整,从而提高监控的及时性以及灵活性。

回顾双十一活动当日,淘宝消息写入总量112亿,消息投递总量220亿,支付宝消息写入总量24亿,消息投递总量24亿。其中实时直播间集群消息写入峰值为13.1w,消息投递峰值为27.8w。

从总体上看,我们的前期准备还是比较充分的,MetaQ 各集群在高峰期表现稳定,全天表现很平稳,个别订阅组对消息进行重溯,部分消息有少量的堆积,但都没有对系统造成影响,效果还是非常好的。75%的交易在聚石塔上完成,实时直播间交易统计延迟在1s左右,加减库存做到零超卖。

小结

目前分布式消息中间件产品已经服务于整个集团,支持了阿里集团各个公司的500多个业务应用系统。每日处理海量消息超350亿次,保证所有交易数据高可靠,高性能,分布式事务处理,是中间件团队最老牌的中间件产品之一。

补充阅读:Notify实现分布式事务的方式

资料来源:http://club.alibabatech.org/resource_detail.htm?topicId=61

图片描述

图片描述

图片描述

图片描述

图片描述

图片描述

图片描述

图片描述

业务操作和消息存储都在本地事务域进行,不存在跨资源的事务。
提交/回滚消息有可能失败,系统会处于短暂的不一致状态
Broker会主动发送Check消息,确认消息是否提交或回滚
最终一致
将分布式事务分解在两个本地事务中
客户端需要付出的代价
实现CheckMessageListener接口

延伸阅读:MetaQ在双十二彩票中的运用

原帖链接:http://jm-blog.aliapp.com/?p=3405&utm_source=tuicool

双十二大促是淘宝集市的年终促销活动,活动当天扫描首页二维码赠送一注彩票的活动更是让大家“玩”了一把。面对瞬间的数倍于往常的峰值,如何让用户有一个良好的体验,如何保证系统的稳定运行,让我们来揭秘这一切。

归纳一下系统需要做到如下几点:

  • RT足够短
  • 压力分布均匀
  • 复杂逻辑分离,异步话

系统结构图
Mjiuau.jpg

大体分为两个部分:活动系统,彩票系统,他们之间通过消息驱动。
活动系统里面只更新一个彩票分配的状态,数据更新成功就返回给用户,逻辑足够简单能保证RT非常短。彩票系统负责真正的彩票出票和更新用户状态等一些耗时操作。
用户在首页扫描二维码发起对活动系统的请求,活动系统更新彩票分配状态,产生一条消息,立即返回并。彩票系统收到这个消息完成后续出票等一些的业务操作。整个过程是通过MetaQ提供的消息服务驱动完成。活动的时候会有大量的请求涌进活动系统,会产生大量的消息,预估qps达到24w;并且消息不可丢失,确保用户都能领到一注彩票;活动系统,彩票系统业务复杂度相差较大,处理能力也相差较大,可能会出现大量的堆积;为了让用户有个较好的体验,消息的消费也需要足够的及时。综上对MetaQ有这么一些挑战:

  • 高吞吐量
  • 数据可靠
  • 高效堆积
  • 消息投递足够低延迟

下面介绍一下MetaQ如何做到这些的。

MetaQ简介

METAQ是一款完全的队列模型消息中间件,服务器使用Java语言编写,可在多种软硬件平台上部署。客户端支持Java、C++编程语言,已于2012年3月对外开源,开源地址。MetaQ的设计目标是高吞吐量,高效堆积。完全的队列模型还提供了顺序消息,消息回溯等一些特性。

2Yr6ru.jpg

基本概念

  • Topic 消息主题
  • Partition 分区,代表一个消费队列 (一个Topic可以划分为多个分区,分区数越多并行度越大,支持的Qps越高)
  • Group 消费分组,代表一个消费集群

MetaQ对外提供的是一个队列服务,内部实现也是完全的队列模型,这里的队列是持久化的磁盘队列,具有非常高的可靠性,并且充分利用了操作系统cache来提高性能。这些特性都源于存储层的设计。

MetaQ存储结构

MetaQ的存储结构是根据阿里大规模互联网应用需求,完全重新设计的一套存储结构,使用这套存储结构可以支持上万的队列模型,并且可以支持消息查询、分布式事务、定时队列等功能,如图3所示。

NZvmuu.jpg

存储层可以大致分为数据文件(CommitLog)和索引文件两部分。数据文件保存了所有的消息的内容,索引文件保存了消息所在数据文件的偏移量。消息数据不区分Topic,顺序的append到CommitLog,索引文件按照Topic-Partition维度组织,不同分区的消息append到不同索引队列里面。

消息写入
客户端发送一条消息,数据首先会写到文件缓存中,同时派发一个写索引请求;异步的构建消息索引。

消息读取
客户端读取消息,根据索引的位置找到需要读取的消息的物理位置和消息长度,从CommitLog中读取数据,通过文件缓存来加速消息的读取。通常热数据都在缓存中,无需IO操作;非热数据,会触发缺页中断,数据从磁盘加载到文件缓存中,直接写到socket缓冲区,避免数据进入Java堆。

数据刷盘
刷盘后数据最终持久化到磁盘。刷盘方式分为两种方式:同步刷盘,数据写入缓存后立即刷盘,确保数据落盘后返回客户端,MetaQ在同步刷盘过程中也做了一定优化避免过多的性能损失;异步刷盘,数据批量定时的刷到磁盘。

数据清理
消息数据按照文件有效期定时做清理。

数据复制
数据可靠性要求很高的应用,可通过数据复制保证数据的可靠。MetaQ提供两种数据同步的方式:同步双写,数写入到主机后会同时写到备机,主备都写成功才返回客户端成功,主备间数据无延迟,MetaQ有一套自己的主备间高效数据复制方案;异步复制,数据写到主机后返回客户端成功,主备间异步同步,主备间存在一定的延迟。

MetaQ单机上万队列
b6fU32.jpg

MetaQ的大部分功能都是靠队列来驱动,以文件的方式存储,通过内存映射对数据进行操作。所有的消息都是顺序的写到数据文件(CommitLog),完全顺序的写入,避免随机IO;消息索引按照Topic和Partition的维度区分串行的写到索引文件。通过这种这种组织方式可以实现单机上万队列,如图4所示。

数据流图
3Yjii2.jpg

消息数据首先写入到Java堆,然后在写入到文件缓存,在根据具体的刷盘策略Flush到磁盘;消费消息的时候如果是热数据则直接从系统缓存写到socket发到远端;如果非热数据则由系统产生缺页中断将数据从磁盘加载到系统缓存在写到socket,数据不进应用程序内存空间。内存的管理,页面的换入换出都是由OS来管理的。同时消息的拉取也是批量的,一次处理多条数据,尽量减少往返的时间。
MetaQ性能依赖于系统内存分配,磁盘IO的有效利用,所以我们也对操作系统内存分配,脏页会写策略,以及IO调度算法做了一些调优,让资源的分配耗时趋于平稳,堆积的时候保持较高的吞吐量。

负载均衡

发送端负载
默认发送端通过轮询的方式向broker写消息,如图6所示;也可以自行指定消息发到哪里。
VBBV3m.jpg

消费端负载
默认是消费集群机器均分所有的消费队列。余下的部分由靠前的消费者消费,如图7所示。消费端负载均很也可以定制,如同机房优先等。

另推荐:http://www.infoq.com/cn/news/2014/03/interview-alibaba-wangjingyu?utm_...

查看原文

赞 21 收藏 155 评论 4

huandu 回答了问题 · 2015-10-10

解决flip.js 的原理是什么?

貌似不难,从 flip.js 源码来看,就是个简单的替换式密码而已。

// 密码表
var str = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}';

// 生成替换密码表,规则很简单,头尾字符相替换。
str.split('').forEach(function(ch, i) {
  map[str[i]] = str[str.length - 1 - i];
});

关注 4 回答 2

huandu 回答了问题 · 2015-10-10

Centos系统宕机,没有任何征兆

通过这些信息看不出是什么问题,服务器负载非常轻,没什么引发问题的先兆出现。

比较怀疑是你的机器本身的故障,这应该是一个云服务器吧,说不定是服务器网络层出了什么问题,导致管理后台和 ssh 都无法连接,这个最好去找你的服务提供商来定位。

关注 2 回答 1

huandu 回答了问题 · 2015-07-07

解决mongodb 副本集能否方便的扩展为分片的方式而不用停机?

可以。

官方的教程详见这里:Convert a Replica Set to a Replicated Sharded Cluster

能平滑迁移的前提是需要有 config server,并且一切请求走 mongos。鉴于题主已经有了副本集(replica-set),那么前提条件是已经具备了,所以平滑扩展应该没有问题。

关注 2 回答 1

huandu 回答了问题 · 2015-07-07

解决Cocoapods 0.37 没有生成workspace文件

你需要在 Podfile 最开始加上 use_frameworks!。现在的状况是 pod install 报错了,没有走完所有的流程,所以没有生成 workspace 文件。

关注 2 回答 1

huandu 回答了问题 · 2015-06-09

解决android udp接收

DatagramSocket(int aPort, InetAddress addr) 这里的 addr 只能绑定本机的 IP,你给的 IP 恐怕不是 Android 机器自己的 IP 吧。

如果 addr 不是本机 IP,new DatagramSocket(PORT,address) 会抛异常,从而导致 dataSocket == null,后面你在 dataSocket.receive 调用时没有捕捉 NullPointerException,所以直接挂掉了。

关注 2 回答 2

huandu 回答了问题 · 2015-06-08

解决gradle wrapper 如何加入版本控制?

这个 ./gradlew 可以让任何一个没有安装 gradle 的机器自动下载指定版本的 gradle,跟题主想象中的不太一样,这个 ./gradlew 其实很笨,只认 ~/.gradle 里面的文件,不认全局的 gradle 命令,所以会“重复”下载。

不过不用担心,同一台机器,如果通过 ./gradlew 下载了一次,那么未来相同版本的 gradle 就不会再次被下载了。

需要放入版本管理的是项目目录的 gradle/ 目录,里面是 gradlew 的具体实现。

gradle 貌似没有类似 python 的机制,不写 dependencies 就不会自动分析源码里面 import 哪些库。这其实也可以理解,毕竟通过 import 无法得知 jar 包的确切名字(比如发现了 import a.b.c.d 这种代码,我如何知道包名叫 a.b 还是 a.b.c?),更无法获得包的版本信息。

关注 3 回答 1

认证与成就

  • 获得 417 次点赞
  • 获得 18 枚徽章 获得 1 枚金徽章, 获得 10 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2012-09-27
个人主页被 2.9k 人浏览