Go 中如何准确地判断和识别各种网络错误

Go 自带的网络标准库可能让很多第一次使用它的人感慨,这个库让网络编程的门槛低到了令人发指的地步。然而,封装层次与开发人员的可控性往往是矛盾的。Go 的网络库封装程度算是一个不错的折衷,绝大部分时候,我们只需要调用 Dial, Read, Write Close 几个基本操作就可以了。

但是,网络是复杂的。我们有时候需要细致的处理网络中的各种错误,根据不同的错误进行不同的处理。比如我们遇到一个网络错误时,需要区分这个错误是因为无法解析 host ip, 还是 TCP 无法建立连接,亦或是读写超时。一开始的时候,我们的写法可能是这样的:

    errString := err.Error()
    fmt.Println(errString)
    switch {
    case strings.Contains(errString, "timeout"):
        fmt.Println("Timeout")
    case strings.Contains(errString, "no such host"):
        fmt.Println("Unknown host")
    case strings.Contains(errString, "connection refused"):
        fmt.Println("Connection refused")
    default:
        fmt.Printf("Unknown error:%s", errString)
    }

这种根据错误信息进行字符串匹配进行判断的方法有非常明显的局限性:该错误信息依赖于操作系统,不同的操作系统对于同一错误返回的字符串信息可能是不同的。因此,这种判断网络错误类型的方法是不可靠的。那么有没有一种准确而可靠的判断各种网络错误的方式呢?答案是肯定的。

我们知道在 Go 中,error 是一个内建的 interface 类型:

type error interface {
        Error() string
}

要准确判断不同的错误类型,我们只需要类型断言出其错误类型即可。

在 Go 的网络标准库中,错误类型被统一封装为 net.Errorinterface 类型:

type Error interface {
        error
        Timeout() bool   // Is the error a timeout?
        Temporary() bool // Is the error temporary?
}

net.Error 类型的具体 concrete 类型又被封装为 net.OpError 类型:

type OpError struct {
        // Op is the operation which caused the error, such as
        // "dial", "read" or "write".
        Op string

        // Net is the network type on which this error occurred,
        // such as "tcp" or "udp6".
        Net string

        // For operations involving a remote network connection, like
        // Dial, Read, or Write, Source is the corresponding local
        // network address.
        Source Addr

        // Addr is the network address for which this error occurred.
        // For local operations, like Listen or SetDeadline, Addr is
        // the address of the local endpoint being manipulated.
        // For operations involving a remote network connection, like
        // Dial, Read, or Write, Addr is the remote address of that
        // connection.
        Addr Addr

        // Err is the error that occurred during the operation.
        Err error
}

其中,net.OpError.Err 可能是以下几种类型:

*os.SyscallError 错误比较特殊,与具体操作系统调用有关:

type SyscallError struct {
        Syscall string
        Err     error
}

对于我们关心的网络错误,SyscallError.Err 一般为 sys.Errno 类型,与网络错误相关的常用值有:

  • syscall.ECONNREFUSED
  • syscall.ETIMEDOUT

看到这里,你可能忍不住要吐槽 Go 这种错误嵌套处理了,事实上,官方也意识到了这种错误处理的问题,在 Go 2中,可能会出现新的错误和异常处理方式,可以参见 GopherChina 2018 keynote 点评: RETHINKING ERRORS FOR GO 2.

当前阶段,我们依然要直面这种错误处理方式。为了方便大家理解 Go 网络标准库中处理错误的方式,我们把上面的错误嵌套整理了一张关系图:

明白了网络标准库中处理错误的逻辑,判断和识别各种类型的网络错误就非常简单了:对网络错误进行类型断言。以我们团队主要关心的 DNS 解析错误、TCP 无法建立连接、读写超时为例,判断逻辑可以是这样:

func isCaredNetError(err error) bool {
    netErr, ok := err.(net.Error)
    if !ok {
        return false
    }

    if netErr.Timeout() {
        log.Println("timeout")
        return true
    }

    opErr, ok := netErr.(*net.OpError)
    if !ok {
        return false
    }

    switch t := opErr.Err.(type) {
    case *net.DNSError:
        log.Printf("net.DNSError:%+v", t)
        return true
    case *os.SyscallError:
        log.Printf("os.SyscallError:%+v", t)
        if errno, ok := t.Err.(syscall.Errno); ok {
            switch errno {
            case syscall.ECONNREFUSED:
                log.Println("connect refused")
                return true
            case syscall.ETIMEDOUT:
                log.Println("timeout")
                return true
            }
        }
    }

    return false
}

这种错误判定方式除了能解决最开始提到的可靠性和准确性问题,也具有良好的普适性。即基于 net 的其他标准库,如 net/http 也支持这种错误判断方式。

扩展阅读

微信小程序文件上传二三事

这段时间陆陆续续上了好几个微信小程序,功能上都会用到文件上传功能(头像上传、证件照上传等)。在APP上传文件到云端的正确姿势中,我们介绍了我们认为安全的上传流程:

即将密钥保存在服务器,客户端每次向服务器申请一个一次性的signature,然后使用该signature作为凭证来上传文件。一般情况下,向阿里云OSS上传内容,又拍云作为灾备。

随着大家安全意识的增强,这种上传流程几乎已经成为标准姿势。但是,把这个流程在应用到微信小程序却有很多细节需要调整。这里把踩过的坑记录一下,希望能让有需要的同学少走弯路。

微信小程序无法直接读取文件内容进行上传

在我们第一版的上传流程方案中,我们的cds 签名发放服务只实现了阿里云 PutObject 接口的signature发放. PutObject 上传是直接将需要上传的内容以二进流的方式 PUT 到云储存。

但是,微信小程序提供的文件上传API wx.uploadFile 要求文件通过 filePath 提供:

另一方面,微信小程序的 JS API 当前还比较封闭,无法根据 filePath 读取到文件内容,因此也无法通过 wx.request 直接发起网络请求的方式来实现文件上传。

考虑到 wx.uploadFile 本质上是一个 multipart/form-data 网络请求的封装,因此我们只需要实现一个与之对应的签名发放方式接口。阿里云OSS对应的上传接口是 PostObject, 又拍云对应的是其 FORM API. 以阿里云OSS为例,cds 服务生成signature 代码如下:

func GetDefaultOSSPolicyBase64Str(bucket, key string) string {
    policy := map[string]interface{}{
        "expiration": time.Now().AddDate(3, 0, 0).Format("2006-01-02T15:04:05.999Z"),
        "conditions": []interface{}{
            map[string]string{
                "bucket": bucket,
            },
            []string{"starts-with", "$key", key},
        },
    }
    data, _ := json.Marshal(&policy)
    return base64.StdEncoding.EncodeToString(data)
}

func GetOSSPostSignature(secret string, policyBase64 string) string {
    h := hmac.New(sha1.New, []byte(secret))
    io.WriteString(h, policyBase64)
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

小程序端代码如下:

//使用说明
/**
 * 1、引入该文件:const uploadFile = require('../../common/uploadAliyun.js');
 * 2、调用如下:
 * uploadImg: function () {
        const params = {
            _success: this._success
        }
        uploadFile.chooseImg(params);
    },
    _success: function(imgUrl){
        this.setData({
            cover_url: imgUrl,
        })
    },
*/

const uploadFile = {
    _fail: function(desc) {
        wx.showToast({
            icon: "none",
            title: desc
        })
    },
    _success: function() {},
    chooseImg: function(sendData) {
        //先存储传递过来的回调函数
        this._success = sendData._success;
        var that = this;
        wx.chooseImage({
            count: 1,
            sizeType: ['original', 'compressed'],
            sourceType: ["album", "carmera"],
            success: function (res) {
                that.getSign(res.tempFilePaths[0]);
            },
            fail: function (err) {
                wx.showToast({
                    icon: "none",
                    title: "选择图片失败" + err
                })
            }
        })
    },
    //获取阿里上传图片签名
    getSign: function (path) {
        var that = this;
        wx.request({
            url: 'https://somewhere/v2/cds/apply_upload_signature',
            method: 'POST',
            data: {
                "content_type": "image/jpeg",
                "signature_type": "oss_post",
                "business": "xiaochengxu",
                "file_ext": '.jpeg',
                "count": '1'
            },
            success: function (res) {
                let getData = res.data.data[0];
                that.startUpload(getData, path);
            },
            fail: function (err) {
                that._fail("获取签名失败" + JSON.stringify(err))
            }
        })
    },
    //拿到签名后开始上传
    startUpload: function (getData, path) {
        var that = this;
        this.uploadAliYun({
            filePath: path,
            dir: 'wxImg/',
            access_key_id: getData.oss_ext_param.access_key_id,
            policy_base64: getData.oss_ext_param.policy_base64,
            signature: getData.signature,
            upload_url: getData.upload_url,
            object_key: getData.oss_ext_param.object_key,
            content_url: getData.content_url.origin 
        })
    },
    uploadAliYun: function(params) {
        var that = this;
        // if (!params.filePath || params.filePath.length < 9) {
        if (!params.filePath) {
            wx.showModal({
                title: '图片错误',
                content: '请重试',
                showCancel: false,
            })
            return;
        }
        const aliyunFileKey = params.dir + params.filePath.replace('wxfile://', '');

        const aliyunServerURL = params.upload_url;
        const accessid = params.access_key_id;
        const policyBase64 = params.policy_base64;
        const signature = params.signature;
        wx.uploadFile({
            url: aliyunServerURL,
            filePath: params.filePath,
            name: 'file',
            formData: {
                'key': params.object_key,
                'policy': policyBase64,
                'OSSAccessKeyId': accessid,
                'Signature': signature
            },
            success: function (res) {
                if (res.statusCode != 204) {
                    that._fail("上传图片失败");
                    return;
                }
                that._success(params.content_url);
            },
            fail: function (err) {
                that._fail(JSON.stringify(err));
            },
        })
    }
}


module.exports = uploadFile;

使用阿里云OSS域名上传失败

解决签名问题后,发现使用阿里云OSS提供的上传域名无法上传成功,在微信后台尝试添加合法域名的时候,惊奇的发现阿里云OSS的域名直接被微信小程序封禁了:

显然是两个神仙在打架,作为草民只能见招拆招。解决办法就是在阿里云OSS -> bucket -> 域名管理 绑定用户域名:

此外,由于微信小程序已经升级为uploadFile的链接必须是https, 因此还需要在绑定用户域名后设置 证书托管

他山之石,可以攻玉

既然微信能够封禁用阿里云OSS的上传域名,那么微信也可以封禁你自定义的域名。根据以往经验(对天发誓,我们不是有意为之,我们也是受害者……),微信封禁域名一般都是一锅端,即发现一个子域名存在违规内容,那么整个域名都会被封禁。因此,一方面要从技术角度对上传的内容及时检查是否合规(如黄图扫描),另一方面提前做好域名规划,将业务接口域名与自定义的文件上传域名分开,这样即使上传域名被一锅端了,不至于是业务完全不可用。

容器环境下 go 服务性能诊断方案设计与实现

背景

业务上量以后,对程序进行 profiling 性能诊断对很多后端程序员来说就是家常便饭。一个趁手的工具往往能让这个事情做起来事半功倍。

在这方面,go 有着天然的优势:继承 Google’s pprof C++ profiler 的衣钵,从出生就有 go tool pprof 工具。并且,标准库里面提供 runtime/pprofnet/http/pprof 两个package, 使得 profiling 可编程化。

在非容器环境下,我们的研发同学喜欢使用 net/http/pprof 来提供http接口供 go tool pprof 工具进行 profiling:

import _ "net/http/pprof"

func main(){
    ...
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    ...
}

获取 CPU profile 数据:

go tool pprof http://localhost:6060/debug/pprof/profile

但是,当架构逐步演进为微服务架构并使用k8s等容器化技术进行部署以后,这种这种方式面临的问题也越来越多:

  1. 我们生产环境使用k8s进行容器编排和部署。service类型是 NodePort. 因此研发同学无法直接对某个 service 的特定 pod 进行 profiling. 之前的解决方式是:
    1. 如果要诊断的问题是这个service普遍存在的问题,则直接进行 profiling。
    2. 如果要诊断的问题只出现在这个service的某个特定的pod上,则由运维同学定位到该pod所处的宿主机后登录到该容器中进行profiling。耗时耗力,效率低。
  2. 架构微服务化以后,服务数量呈量级增加。以前那种出现问题再去诊断服务现场的方式越来越难满足频率和数量越来越多的profiling需求(很多情况下,我们才做好profiling的准备,问题可能已经过去了……)。我们迫切的需要一种能够在程序出问题时,自动对程序进行profiling的方案,最大可能获取程序现场数据。
  3. 同时,我们希望这种自动profiling机制对程序性能影响尽可能小,并且可以与现有告警系统集成,直接将诊断结果通知到程序的owner.

方案设计与实现

  • 我们使用 heapster 对k8s的容器集群进行监控。并将监控到的时序数据写入influxDB进行持久化。
  • gopprof 是我们容器环境下对其他 go 服务进行性能诊断的核心服务:
    • 通过对influxDB中的监控数据分析,对于异常的pod自动进行 profiling. 当前设置的策略是如果该pod在两个1分钟分析周期内,资源使用率都超过设定的阈值0.8,则触发profiling。
    • gopprof 作为一个服务部署在k8s集群中主要是使其可以通过内网IP直接访问pod的 http profile接口,已实现对特定pod的profiling:
    go tool pprof http://POD_LAN_IP:NodePort/debug/pprof/profile
    
    • gopprof 完成profiling后,会自动生成 profile svg 调用关系图,并将profile 数据和调用关系图上传云存储,并向服务的owner推送诊断结果通知:

    • 由于 gopprof 依赖工具 go tool pprofgraphivz, 因此gopprof的基础镜像需要预装这两个工具。参考Dockerfile
    # base image contains golang env and graphivz
    
    FROM ubuntu:16.04
    
    MAINTAINER Daniel liudan@codoon.com
    
    RUN apt-get update
    RUN apt-get install wget -y
    RUN wget -O go.tar.gz https://dl.google.com/go/go1.9.2.linux-amd64.tar.gz && \
        tar -C /usr/local -xzf go.tar.gz && \
        rm go.tar.gz
    
    ENV PATH=$PATH:/usr/local/go/bin
    
    RUN go version
    
    RUN apt-get install graphviz -y
    
    • gopprof 向研发同学提供了对特定pod以及特定一组pod进行手动profiling的的接口。在解放运维同学生产力的同时,也让研发同学在出现难以复现的问题时,能够有更大可能性获取到程序现场。
    • 在高可用方面,当前只支持部署一个 gopprof pod, 服务可用性依赖于k8s的的auto restart. 后期如果有这方面的需求,可能会修改为依赖于etcd支持多个gopprof pod部署。

小结

gopprof 服务已经在我们内部落地试运行了一段时间,整个上达到了我们的设计预期,并辅助我们发现和解决了一些之前没有意识到的性能问题。由于有一些内部代码依赖,暂时还无法开源出来。但是整个方案所依赖的组件都是通用的,因此你也可以很容易的实现这个方案。如果你对我们实现中的一些细节感兴趣,欢迎评论和留言。

Service Mesh 及其主流开源实现解析

什么是 Service mesh

Service Mesh 直译过来是 服务网格,目的是解决系统架构微服务化后的服务间通信和治理问题。服务网格由 sidecar 节点组成。在介绍 service mesh 之前,我们先来看一下什么是 sidecar.

Sidecar 在软件系统架构中特指边车模式。这个模式的灵感来源于我们生活中的边三轮:即在两轮摩托车的旁边添加一个边车的方式扩展现有的服务和功能。在绝地求生吃鸡游戏中,摩托车是无敌的,应该也与这个模式有关吧😅 这个模式的精髓在于实现了数据面(业务逻辑)控制面的解耦:原来两轮摩托车的驾驶者集中注意力跑赛道,边车上的领航员专注周围信息和地图,专注导航。

具体到微服务架构中,即给每一个微服务实例(也可以是每个宿主机host)同步部署一个 sidecar proxy:

该 sidecar proxy 负责接管对应服务的入流量和出流量。并将微服务架构中以前有公共库、framework实现的熔断、限流、降级、服务发现、调用链分布式跟踪以及立体监控等功能从服务中抽离到该 proxy 中:

当该 sidecar 在微服务中大量部署时,这些 sidecar 节点自然就形成了一个网格:

这就是我们说的 service mesh 了。对 service mesh 有了一个感性认识后,我们看一下 Linkerd 和 Conduit 的作者 William Morgan 在What’s a service mesh? And why do I need one? 中是如何诠释什么是 Service Mesh:

A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.

Service Mesh 这个服务网络专注于处理服务和服务间的通讯。其主要负责构造一个稳定可靠的服务通讯的基础设施,并让整个架构更为的先进和 Cloud Native。在工程中,Service Mesh 基本来说是一组轻量级的与应用逻辑服务部署在一起的服务代理,并且对于应用服务是透明的。

Service Mesh的特点

  • 是一个基础设施
  • 轻量级网络代理,应用程序间通讯的中间层
  • 应用程序无感知,对应用程序透明无侵入
  • 解耦应用程序的重试/超时、监控、追踪和服务发现等控制层面的东西

Service Mesh 有哪些开源实现

Service Mesh 的概念从2016年提出至今,已经发展到了第二代。

第一代 service mesh 以 LinkerdEnvoy 为代表。

Linkerd 使用Scala编写,是业界第一个开源的service mesh方案。作者 William Morgan 是 service mesh 的布道师和践行者。Envoy 基于C++ 11编写,无论是理论上还是实际上,后者性能都比 Linkderd 更好。这两个开源实现都是以 sidecar 为核心,绝大部分关注点都是如何做好proxy,并完成一些通用控制面的功能。 但是,当你在容器中大量部署 sidecar 以后,如何管理和控制这些 sidecar 本身就是一个不小的挑战。于是,第二代 Service Mesh 应运而生。

第二代service mesh主要改进集中在更加强大的控制面功能(与之对应的 sidecar proxy 被称之为数据面),典型代表有 IstioConduit

Istio 解析

Istio 是 Google 和 IBM 两位巨人联合 Lyft 的合作开源项目。是当前最主流的service mesh方案,也是事实上的第二代 service mesh 标准。

Google 和 IBM 之所以要带上小弟 Lyft 一起玩耍是因为他们不想从头开始做数据面的组件,于是在 Istio 中,直接把 Lyft 家的 Envoy 拿来做 sidecar. 除了sidecar, Istio中的控制面组件都是使用Go编写。Istio架构如下图所示:

对于一个仅提供服务与服务之间连接功能的基础设施来说,Istio的架构算不上简单。但是架构中的各个组件的理念的确非常先进和超前。

  • Envoy: 扮演sidecar的功能,协调服务网格中所有服务的出入站流量,并提供服务发现、负载均衡、限流熔断等能力,还可以收集大量与流量相关的性能指标。
  • Pilot: 负责部署在service mesh中的Envoy实例的生命周期管理。本质上是负责流量管理和控制,是将流量和基础设施扩展解耦,这是Istio的核心。感性上,可以把Pilot看做是管理sidecar的sidecar, 但是这个特殊的sidacar并不承载任何业务流量。Pilot让运维人员通过Pilot指定它们希望流量遵循什么规则,而不是哪些特定的pod/VM应该接收流量。有了 Pilot 这个组件,我们可以非常容易的实现 A/B 测试和金丝雀Canary测试:

  • Mixer: Mixer在应用程序代码和基础架构后端之间提供通用中介层。它的设计将策略决策移出应用层,用运维人员能够控制的配置取而代之。应用程序代码不再将应用程序代码与特定后端集成在一起,而是与Mixer进行相当简单的集成,然后Mixer负责与后端系统连接。也就是说,Mixer可以认为是其他后端基础设施(如数据库、监控、日志、配额等)的sidecar proxy:

  • Istio-Auth: 提供强大的服务间认证和终端用户认证,使用交互TLS,内置身份和证书管理。可以升级服务网格中的未加密流量,并为运维人员提供基于服务身份而不是网络控制来执行策略的能力。Istio的未来版本将增加细粒度的访问控制和审计,以使用各种访问控制机制(包括基于属性和角色的访问控制以及授权钩子)来控制和监视访问您的服务,API或资源的人员。

Istio 的很多设计理念的确非常吸引人,又有 Google 和 IBM 两个巨人加持,理论上这条赛道上的其他选手都可以直接退赛回家了。但是 Istio 发布的前几个版本都在可用性和易用性上都差强人意。此外,service mesh 布道师、 Linkerd 作者 William Morgan 也心有不甘。因此, William Morgan一方面在2017年7月11日,Linkerd 发布版本 1.1.1,宣布和 Istio 项目集成,一方面夜以继日的开发Conduit.

Conduit 解析

Conduit 各方面的设计理念与 Istio 非常类似。但是作者抛弃了 Linkerd, 使用Rust重新编写了sidecar, 叫做 Conduit Data Plane, 控制面则由Go编写的 Conduit Control Plane接管:

从Conduit的架构看,作者号称Conduit吸取了很多 Linkerd 的 Scala 的教训,比 Linkerd 更快,还轻,更简单,控制面功能更强可信度还是挺高的。与Istio比较,个人其实更喜欢Conduit的架构,一方面是它足够简单,另一方面对于要解决的问题足够聚焦。

nginMesh 凑热闹?

Service Mesh 最基础的功能毕竟是 sidecar proxy. 提到 proxy 怎么能够少了 nginx? 我想nginx自己也是这么想的吧😂 毫不意外,nginx也推出了其 service mesh 的开源实现:nginMesh.

不过,与 William Morgan 的死磕策略不同,nginMesh 从一开始就没有想过要做一套完整的第二代Service Mesh 开源方案,而是直接宣布兼容Istio, 作为Istio的 sidecar proxy. 由于 nginx 在反向代理方面广泛的使用,以及运维技术的相对成熟,nginMesh在sidecar proxy领域应该会有一席之地。

反思

对于大规模部署微服务(微服务数>1000)、内部服务异构程度高(交互协议/开发语言类型>5)的场景,使用service mesh是合适的。但是,可能大部分开发者面临的微服务和内部架构异构复杂度是没有这么高的。在这种情况下,使用service mesh就是一个case by case的问题了。

理论上,service mesh 实现了业务逻辑和控制的解耦。但是这并不是免费的。由于网络中多了一跳,增加了性能和延迟的开销。另一方面,由于每个服务都需要sidecar, 这会给本来就复杂的分布式系统更加复杂,尤其是在实施初期,运维对service mesh本身把控能力不足的情况下,往往会使整个系统更加难以管理。

本质上,service mesh 就是一个成规模的sidecar proxy集群。那么如果我们想渐进的改善我们的微服务架构的话,其实有针对性的部署配置gateway就可以了。该gateway的粒度可粗可细,粗可到整个api总入口,细可到每个服务实例。并且 Gateway 只负责进入的请求,不像 Sidecar 还需要负责对外的请求。因为 Gateway 可以把一组服务给聚合起来,所以服务对外的请求可以交给对方服务的 Gateway。于是,我们只需要用一个只负责进入请求的 Gateway 来简化需要同时负责进出请求的 Sidecar 的复杂度。

小结:service mesh不是银弹。对于大规模部署、异构复杂的微服务架构是不错的方案。对于中小规模的微服务架构,不妨尝试一下更简单可控的gateway, 在确定gateway已经无法解决当前问题后,再尝试渐进的完全service mesh化。

扩展阅读

Dropbox Bandaid 微服务反向代理/Sevice Mesh 代理解析

随着微服务架构以及的广泛普及,很多公司都会使用或者自行开发自己的API Gateway, 甚至在内部服务也会应用Service Mesh.

不久前,看到了一篇Dropbox公司介绍其内部服务代理Bandaid的文章: Meet Bandaid, the Dropbox service proxy. 不得不说设计细节一贯独角兽风格,非常有收获,对于改进我们自己设计的proxy 也有一定参考意义。这里做一个简单的读后笔记。英语好的同学可以直接阅读原文。(插一句,Dropbox不久前在美股上市了,在中美贸易战中为数不多逆势上涨的股票之一,玩美股的同学可以关注一下。再插一句:股市有风险,投资须谨慎。)

Bandaid 诞生的背景

Bandaid 是有公司内部的反向代理服务演变而来,使用Golang实现。反向代理有很多成熟的解决方案,之所以选择自行开发主要有以下几个原因:

  • 更好的与与内部的基础设施集成
  • 可以复用公司内部基础库(更好的与内部代码集成)
  • 减少对外部的依赖,团队可以灵活的按需开发
  • 更适合公司内某些特殊使用场景

上面的大部分因素根我们进行微服务组件开发时候的考量基本一致。这也是我们当初没有使用Go kit 这种工具套装进行架构微服务改造的顾虑,当然,那个时候还没有这些工具链。

Bandaid 的特性

  • 支持多种负载均衡策略 (round-robin, least N random choices, absolute least connection, pinning peer)
  • 支持 https to http
  • 上下游支持 http2
  • 路由改写
  • 缓存请求与响应
  • host级别的逻辑隔离
  • 配置热加载
  • 服务发现
  • 路由信息统计
  • 支持gRPC代理
  • 支持HTTP/gRPC健康检查
  • 流量支持按权重分配和金丝雀测试

丰富的负载均衡策略,以及对HTTP/2和gRPC的支持实例亮点。要知道,nginx 从 1.13.10才开始支持gRPC.

另一方面,从支持的特性看,如果要求不是特别多,直接用来作为Service Mesh也是相当不错的。而且由于是使用Go开发,对于本来就在使用Go作为技术栈的团队来说,无论是使用还是二次开发,门槛和学习成本都是很低的。

Bandaid 设计解析

整体架构

请求队列设计

接收的请求按照LIFO 后入先出的方式进行处理。这个设计有点反直觉,但却是合理的:

  • 绝大部分情况下,队列应该是空或者接近空的状态。因此LIFO的策略并不会恶化队列最大等待时间。
  • 根据业务类型,可以配置队列的优先级和长度。可以非常方便的实现服务限流、服务降级和熔断器。
  • Bandaid 采用总是接收TCP连接,并将连接交由用户态管理的策略。结合LIFO有一个很大的好处:
    • 与内核态管理连接比较,如果客户端发送请求后意外关闭了TCP连接,Bandaid 是无法马上获取到该错误的,需要等到读取完该请求,然后处理请求后开始写response时才会触发错误,发现这个连接其实已经关闭。因此,处理这类请求是在无谓的消耗服务器资源。而采用LIFO和用户态管理连接的话,Bandaid 可以根据配置的超时策略,一定程度上drop掉这类请求,减少处理这种「dead request」的数量。

Worker 设计

Worker采取固定大小的工作池设计,一方面可以精确的控制并发数量,另一方面,也避免频繁创建worker的开销。不过文中也承认,池的大小在设置的时候需要结合业务考虑清楚,否则可能不能充分利用服务器资源,错误的触发服务降级。

worker在处理队列中的请求时,支持按照优先和权重处理。因此,可以非常容易的实现金丝雀发布和逻辑上的upstream隔离。

负载均衡策略

  • RR, 均匀撒胡椒面。优点是足够简单,缺点是没有考虑不同后端服务实例的数量和接口处理时长差异,会导致大量Bandaid 服务资源被极少数的慢服务和接口消耗掉。
  • least N random choices: 先随机选择N个候选upstream host, 然后选择连接数最少的host (认为是当前负载最低的)作为最终目标host.

这种方式在在大部分时候可以工作得很好,但对于那种快速失败的小服务会失效。因为这种服务有很大概率被选中,但是并不意味着其当前负载较低。缓解该策略的方式是absolute least connection.

  • absolute least connection: 从全局host中选择连接数最少的host作为目标host.

  • pinning peer: 将worker与host绑定。这种方式可以避免慢服务过量消耗Bandaid资源的问题,但是请求调度不够灵活。

总结

从 Bandaid 的设计看,无论是作为 reverse proxy 还是 service mesh 都有不错的潜力。不过 Dropbox 团队当前还没有公开Bandaid性能测试数据,代码也还没有开源。因此,猴急的同学可能还需要等一段时间。