CodeCloud

CodeCloud 查看完整档案

北京编辑吉林大学  |  计算机应用技术 编辑某公司  |  程序员 编辑 github.com 编辑
编辑

一个程序猿,喜欢学习技术,喜欢交流技术

个人动态

CodeCloud 赞了文章 · 2020-11-30

gracehttp: 优雅重启 Go 程序(热启动 - Zero Downtime)

看完此篇你会知道,如何优雅的使用 HTTP Server

问题背景

http 应用程序重启时,如果我们直接 kill -9 使程序退出,然后在启动,会有以下几个问题:

  1. 旧的请求未处理完,如果服务端进程直接退出,会造成客户端链接中断(收到 RST);
  2. 新请求打过来,服务还没重启完毕,造成 connection refused
  3. 即使是要退出程序,直接 kill -9 仍然会让正在处理的请求中断;
  4. 面对海量请求,如何对链接数进行限制,并进行过载保护;
  5. 避免 open too many files 错误;

这些问题会造成不好的客户体验,严重的甚至影响客户业务。所以,我们需要以一种优雅的方式重启/关闭我们的应用,来达到热启动的效果,即:Zero Downtime

(Tips:名词解释)
热启动:新老程序(进程)无缝替换,同时可以保持对client的服务。让client端感觉不到你的服务挂掉了;
Zero Downtime: 0 宕机时间,即不间断的服务;

解决问题

Github: gracehttp

平滑启动

一般情况下,我们是退出旧版本,再启动新版本,总会有时间间隔,时间间隔内的请求怎么办?而且旧版本正在处理请求怎么办?
那么,针对这些问题,在升级应用过程中,我们需要达到如下目的:

  • 旧版本为退出之前,需要先启动新版本;
  • 旧版本继续处理完已经接受的请求,并且不再接受新请求;
  • 新版本接受并处理新请求的方式;

这样,我们就能实现 Zero Downtime 的升级效果。

实现原理

首先,我们需要用到以下基本知识:
1.linux 信号处理机制:在程序中,通过拦截 signal,并针对 signal 做出不同处理;
2.子进程继承父进程的资源:一切皆文件,子进程会继承父进程的资源句柄,网络端口也是文件;
3.通过给子进程重启标识(比如:重启时带着 -continue 参数),来实现子进程的初始化处理;

重启时,我们可以在程序中捕获 HUP 信号(通过 kill -HUP pid 可以触发),然后开启新进程,退出旧进程。信号处理代码示例如下:

package gracehttp

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

var sig chan os.Signal
var notifySignals []os.Signal

func init() {
    sig = make(chan os.Signal)
    notifySignals = append(notifySignals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGQUIT)
    signal.Notify(sig, notifySignals...) // 注册需要拦截的信号
}

// 捕获系统信号,并处理
func handleSignals() {
    capturedSig := <-sig
    srvLog.Info(fmt.Sprintf("Received SIG. [PID:%d, SIG:%v]", syscall.Getpid(), capturedSig))
    switch capturedSig {
    case syscall.SIGHUP: // 重启信号
        startNewProcess() // 开启新进程
        shutdown() // 退出旧进程
    case syscall.SIGINT:
        fallthrough
    case syscall.SIGTERM:
        fallthrough
    case syscall.SIGTSTP:
        fallthrough
    case syscall.SIGQUIT:
        shutdown()
    }
}

startNewProcessshutdown 具体实现可以参考 Github

过载保护

通过限制 HTTP Serveraccept 数量实现链接数的限制,来达到如果并发量达到了最大值,客户端超时时间内可以等待,但不会消耗服务端文件句柄数(我们知道 Linux 系统对用户可以打开的最大文件数有限制,网络请求也是文件操作)

实现原理

  • 利用 channel 的缓冲机制实现,每个请求都会获取缓冲区的一个单元大小,知道缓冲区满了,后边的请求就会阻塞;
  • 如果客户端请求被阻塞,达到了客户端设置的超时时间,这时候链接会断开,那我们利用 goselect 机制,退出阻塞,并返回,不再进行 accept

处理代码如下:

package gracehttp

// about limit @see: "golang.org/x/net/netutil"

import (
    "net"
    "sync"
    "time"
)

type Listener struct {
    *net.TCPListener
    sem       chan struct{}
    closeOnce sync.Once     // ensures the done chan is only closed once
    done      chan struct{} // no values sent; closed when Close is called
}

func newListener(tl *net.TCPListener, n int) net.Listener {
    return &Listener{
        TCPListener: tl,
        sem:         make(chan struct{}, n),
        done:        make(chan struct{}),
    }
}

func (l *Listener) Fd() (uintptr, error) {
    file, err := l.TCPListener.File()
    if err != nil {
        return 0, err
    }
    return file.Fd(), nil
}

// override
func (l *Listener) Accept() (net.Conn, error) {
    acquired := l.acquire()
    tc, err := l.AcceptTCP()
    if err != nil {
        if acquired {
            l.release()
        }
        return nil, err
    }
    tc.SetKeepAlive(true)
    tc.SetKeepAlivePeriod(time.Minute)

    return &ListenerConn{Conn: tc, release: l.release}, nil
}

// override
func (l *Listener) Close() error {
    err := l.TCPListener.Close()
    l.closeOnce.Do(func() { close(l.done) })
    return err
}

// acquire acquires the limiting semaphore. Returns true if successfully
// accquired, false if the listener is closed and the semaphore is not
// acquired.
func (l *Listener) acquire() bool {
    select {
    case <-l.done:
        return false
    case l.sem <- struct{}{}:
        return true
    }
}

func (l *Listener) release() { <-l.sem }

type ListenerConn struct {
    net.Conn
    releaseOnce sync.Once
    release     func()
}

func (l *ListenerConn) Close() error {
    err := l.Conn.Close()
    l.releaseOnce.Do(l.release)
    return err
}

参考:grace-http:listener.go

gracehttp

现在我们把这个功能做得更优美有点,并提供一个开箱即用的代码库。
地址:Github-gracehttp

支持功能

  1. 平滑重启(Zero-Downtime);
  2. 平滑关闭;
  3. Server 添加(支持 HTTPHTTPS);
  4. 自定义日志组件;
  5. 支持单个端口 server 链接数限流,默认值为:C100K。超过该限制之后,链接阻塞进入等待,但是不消耗系统文件句柄,避免发生雪崩,压坏服务。

使用指南

添加服务

    import "fevin/gracehttp"
    
    ....

    // http
    srv1 := &http.Server{
        Addr:    ":80",
        Handler: sc,
    }
    gracehttp.AddServer(srv1, false, "", "")

    // https

    srv2 := &http.Server{
        Addr:    ":443",
        Handler: sc,
    }
    gracehttp.AddServer(srv2, true, "../config/https.crt", "../config/https.key")

    gracehttp.Run() // 此方法会阻塞,直到进程收到退出信号,或者 panic

如上所示,只需创建好 Server 对象,调用 gracehttp.AddServer 添加即可。

退出或者重启服务

  • 重启:kill -HUP pid
  • 退出:kill -QUIT pid

添加自定义日志组件

    gracehttp.SetErrorLogCallback(logger.LogConfigLoadError)

此处提供了三个 Set* 方法,分别对应不同的日志等级:

  • SetInfoLogCallback
  • SetNoticeLogCallback
  • SetErrorLogCallback

最后

实际中,很多情况会用到这种方式,不妨点个 star 吧!
欢迎一起来完善这个小项目,共同贡献代码。

查看原文

赞 13 收藏 8 评论 0

CodeCloud 赞了文章 · 2020-11-26

新书《Go语言编程之旅:一起用Go做项目》出版啦!

最早从我在 Segmentfault 开始写技术文章起,不知不觉近三年过去了,咨询和催我出书和读者逐年递增,在 2019 年算是达到一个高峰。当然,综合考虑下我也是一直拒绝的,觉得火候还不够。

直至 2019.09 月,polaris 主动找到了我,说有事情想找我商量,本着 “如果你在纠结一件事情做还是不做,不如先做了看看结果,至少不会后悔” 的想法,更何况是长期被 Ping,因此我一口答应下来,故事自此开始了。

image

本书定位

本书不直接介绍 Go 语言的语法基础,内容将面向项目实践,同时会针对核心细节进行分析。而在实际项目迭代中,常常会出现或多或少的事故,因此本书也针对 Go 语言的大杀器(分析工具)以及常见问题进行了全面讲解。

本书适合已经大致学习了 Go 语言的基础语法后,想要跨越到下一个阶段的开发人员,可以填补该阶段的空白和进一步拓展你的思维方向。

读者定位

  • 基本了解 Go 语言的语法和使用方式的开发人员。
  • 想要进行 Go 相关项目实践和进一步摸索的开发人员。
  • 希望熟悉 Go 常用分析工具的开发人员。

本书大纲

本书针对常见的项目类型,主要细分为 5 + 1 板块,分别是命令行、HTTP、RPC、Websocket 应用、进程内缓存以及 Go 中的大杀器。

同时我们在项目开发、细节分析、运行时分析等方方面面都进行了较深入的介绍和说明,能够为 Go 语言开发者提供相对完整的项目实践经验,而如果深入阅读第六章的章节,更能够为未来各类问题出现时的问题排查提供一份强大的知识板块。

如下为本书的思维导图概览:

image

如何阅读这本书

常规的列目录未免太无趣。我想不如说说从我个人的角度,所看到读者们在近 3 年来是如何阅读/实践我的实践系列文章的,其面向的读者群体是完全一致的。希望能够从另外一个角度告诉你,应当如何阅读这本书,尽可能的效益最大化。

首先,图书,买来要读,而与实战结合的图书,势必需要实践,实践最常见又分为脑内思考和上机实践:

image

而在持续的交流中,可以发现至少会延伸出以下几类深入层次的不同:

image

  • 第一层:只阅读,留有印象,需要时再唤醒,也行。
  • 第二层:阅读并实践,实打实的完成项目实践,收获丰满。
  • 第三层:实践的过程中,一定会遇到或大或小的问题,有的人会放弃,这就是分叉点。但有的读者会持续排查,其提升了个人能力(排错能力很重要)。
  • 第四层:实践完毕后,有自己的想法,认为某某地方还可以这样,也可以再实现更多的功能,举一反三,进一步拓展,并对项目提 issues 或进行 pr。
  • 第五层:完成整体项目后,抽离业务代码,标准化框架,实现框架的应用脚手架,并有的读者会进一步开源。
  • 第六层:形成脚手架后,在自己业务组开始落地,实际在项目中使用,由业务学习转化为企业实践。
  • 第七层:在内部落地实践稳妥后,开始在其它业务组开始推广该框架脚手架,进一步标准化,拓展思路。

通过上图中 “七层金字塔” 的理解,我们不难发现其对于实践项目的理解和应用已经不再是单单这个项目,而是有了更深远的意义,抽象一下,对照着著名的 “学习效率金字塔” 来看:

image

在单纯的 “阅读” 时,其基本处于 “被动学习” 的阶段,而单进入阅读并实践时,已经转为了 “主动学习”,且绝大部分的读者做完实践后,表示 “嗯,实践完,挺好的,有所得”。

这时候就会进入到一个新的阶段(分叉点),绝大部分读者在做完后,会纠结 ”接下来要做什么“:

image

有部分读者会停滞,也有部分读者会转入 “转教别人/立即应用” 的阶段,也就是普遍的在企业内部进行标准化的使用,又或是开源项目,据此得到更一步的深入实践和提高,更大的吸收差距也在于此。

当然,这一切都要基于前面的 “1”,你得先买了书,读了书,接着就是你的选择和创建机遇的能力了,不同的路线效益自然不一样。

广告时间

在《Go语言编程之旅:一起用Go做项目》写作中后期,作为 2020 年的煎鱼,我回顾了 2018、2019 年的煎鱼所写的文章,在现在看来发现多多少少都有些瑕疵。再对比本书,在同类主题下,写出的内容更具知识结构化和实战意义,且能做出更优的选题抉择,确实变化了。

因此我也在这里正式向你推荐本书,希望能够给所有 Go 语言爱好者带来更大的技术价值和切切实实的项目实践经验。

后续有任何问题或建议也欢迎随时来交流。

image

关于写书

有关注我的小伙伴应该会发现,我之前突然退了很多个微信群,并且停止了博客的更新,也较少在社区里冒泡了。其实本质上是为了给写书让路,希望尽可能的把业余时间都聚焦在写书上。

这时候又会有另外一个问题,那就是写书,是一件非常长耗时的事情,没有任何的外界反馈,因此我严格做了一系列的 todolist 和时间节点的管理,围绕着自己的生活作息设置了一系列闹钟作为信号量提醒自己。

基本是吃饭、睡前构思结构、想灵感,下班回到家一坐下就开始写内容。当然,我也经常走火入魔一想到好的灵感就激动的掏出手机记在工具上,免得第二天大脑重置后丢失了数据,那就很可惜。

最终在长期的坚持下自然而然也就完成了这本书的写作。

感谢你们

非常感谢 polaris,在艰难的情况下依旧完成了本书的编写。感谢博文视点的编辑安娜,基本从不催更。感谢曹大、无闻、杨文、傲飞、大彬、晓东的推荐词或 Review.

我还记得当时曹大的书出版时,因为种种原因,我还立下过 ”绝不写书” 的 flag,和晓东在深圳湾一号吃自助餐时立过 “绝对不会放弃,一定会写完” 的 flag,果然计划赶不上变化,flag 该折折。

当然,最该感谢的还是我司的研发负责人,当年把我从个小角落里筛了出来,否则也不会有这一切的开端了。

查看原文

赞 24 收藏 1 评论 12

CodeCloud 赞了文章 · 2020-10-12

OpenWrite 创始人 DD

OpenWrite 创始人 DD,必须了解下:https://t.1yb.co/6hk1

程序猿DD 是谁?
• 个人博客介绍:http://blog.didispace.com/abo...
• 思否社区采访:https://segmentfault.com/a/11...
• 开源中国采访:https://gitee.com/gitee-stars/9

「程序猿 DD」星球,是一个汇聚互联网技术人的社群。少讲大道理,只会分享、讨论以及职场心得。

免费预约百人拼团活动:https://t.1yb.co/6hk1

「程序猿 DD」已经复活,第二春的输出内容很丰富,你不来了解一下?

https://t.1yb.co/6hk1

「程序猿 DD」星球,是一个汇聚互联网技术人的社群,深度分享、讨论以及职场心得。

随着这半年的建设,我们的核心内容已经非常丰富,并还在持续产出中:

2 大固定系列专栏:

「#技术人」系列(每周三):分享技术高手行走江湖的绝技以及职场心得;
「#社会人」系列(每周六):分享踏入社会切身相关需要知道知识与思维;

4 大专题系列专栏:

「#技术资源」系列分享对技术提升很多有用的工具、网站等;
「#技术问答」用心回答对技术问题或者技能提升的干货方法;
「#职业规划」系列分享对职场提升很多有用的回答、建议等;
「#轻享」系列分享行业朋友们的所见所思所得:思维方法等;

10月份我们打算再搞一次,百人拼团活动,免费预约赶紧上车:https://t.1yb.co/6hk1

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 1 收藏 0 评论 0

CodeCloud 赞了文章 · 2020-10-12

业务压力一大就宕机?一文带你搞懂限流熔断!

“在分布式应用中,最常见的问题是什么呢?”

“一个分布式应用部署上去后,还要关注什么?”

image

“这服务的远程调用依赖似乎有点多...”

前言

《微服务的战争:级联故障和雪崩》中有提到,在一个分布式应用中,最常见,最有危险性之一的点就是级联故障所造成的雪崩,而其对应的解决方案为根据特定的规则/规律进行流量控制和熔断降级,避免请求发生堆积,保护自身应用,也防止服务提供方进一步过载。

image

简单来讲就是,要控制访问量的流量,要防各类调用的强/弱依赖,才能保护好应用程序的底线。

诉求,期望

  1. 诉求:作为一个业务,肯定是希望自家的应用程序,能够全年无休,最低限度也要有个 4 个 9,一出突发性大流量,在资源贫乏的窗口期,就马上能够自动恢复。
  2. 期望:万丈高楼平地起,我们需要对应用程序进行流量控制、熔断降级。确保在特定的规则下,系统能够进行容错,只处理自己力所能及的请求。若有更一步诉求,再自动扩缩容,提高系统资源上限。

解决方案

要如何解决这个问题呢,可以关注到问题的核心点是 “系统没有任何的保护的情况下”,因此核心就是让系统,让你的应用程序有流量控制的保护。一般含以下几个方面:

  • 来自端控制:在业务/流量网关处内置流量控制、熔断降级的外部插件,起到端控制的效果。
  • 来自集群/节点控制:在统一框架中内建流量控制、熔断降级的处理逻辑,起到集群/节点控制的效果。
  • 来自 Mesh 控制:通过 ServiceMesh 来实现流量控制、熔断降级。侵入性小,能带来多种控制模式,但有利有弊。

以上的多种方式均可与内部的治理平台打通,且流量控制、熔断降级是不止面试应用程序的,就看资源埋点上如何设计、注入。常见有如下几种角度:

  • 资源的调用关系:例如远程调用,像是面向 HTTP、SQL、Redis、RPC 等调用均,另外针对文件句柄控制也可以。
  • 运行指标:例如 QPS、线程池、系统负载等。

流量控制

在资源不变的情况下,系统所能提供的处理能力是有限的。而系统所面对的请求所到来的时间和量级往往是随机且不可控的。因此就会存在可能出现突发性流量,而在系统没有任何的保护的情况下,系统就会在数分钟内无法提供正常服务,常见过程为先是出现调用延迟,接着持续出现饱和度上升,最终假死。

image

流量控制一般常见的有两种方式,分别是:基于 QPS、基于并发隔离。

基于 QPS

最常用的流量控制场景,就是基于 QPS 来做流控,在一定的时间窗口内按照特定的规则达到所设定的阈值则进行调控:

image

案例

在本文中借助 sentinel-golang 来实现案例所需的诉求,代码如下:

import (
    ...
    sentinel "github.com/alibaba/sentinel-golang/api"
    "github.com/alibaba/sentinel-golang/core/base"
    "github.com/alibaba/sentinel-golang/core/flow"
    "github.com/alibaba/sentinel-golang/util"
)

func main() {
    _ = sentinel.InitDefault()
    _, _ = flow.LoadRules([]*flow.Rule{
        {
            Resource:               "控制吃煎鱼的速度",
            Threshold:              60,
            ControlBehavior:        flow.Reject,
        },
    })

    ...
    e, b := sentinel.Entry("控制吃煎鱼的速度", sentinel.WithTrafficType(base.Inbound))
    if b != nil {
        // Blocked
    } else {
        // Passed
        e.Exit()
    }
}

总的来讲,上述规则结果就是 1s 内允许通过 60 个请求,超出的请求的处理策略为直接拒绝(Reject)。

首先我们初始化了 Sentinel 并定义资源(Resource)为 “控制吃煎鱼的速度”。其 Threshold 配置为 3,也就是 QPS 的阈值为 3,统计窗口未设置默认值为 1s,ControlBehavior 控制的行为为直接拒绝。

而在满足阈值条件后,常见的处理策略还有匀速排队(Throttling),匀速排队方式会严格控制请求通过的间隔时间,也就是让请求以均匀的速度通过。

基于并发隔离

基于资源访问的并发协程数来控制对资源的访问数量,主要是控制对资源访问的最大协程数,避免因为资源的异常导致协程耗尽。

image

这类情况,Go 语言在设计上常常可以使用协程池来进行控制,但设计总是赶不上计划的,且不同场景情况可能不同,因此作为一个日常功能也是非常有存在的必要性。

熔断降级

在分布式应用中,随着不断地业务拆分,远程调用逐渐变得越来越多。且在微服务盛兴的情况下,一个小业务拆出七八个服务的也常有。

此时就会出现一个经典的问题,那就是客户端的一个普通调用,很有可能就要经过好几个服务,而一个服务又有可能远程调用外部 HTTP、SQL、Redis、RPC 等,调用链会特别的长。

若其中一个调用流程出现了问题,且没有进行调控,就会出现级联故障,最终导致系统雪崩:

image

服务 D 所依赖的外部接口出现了故障,而他并没有做任何的控制,因此扩散到了所有调用到他的服务,自然也就包含服务 B,因此最终出现系统雪崩。

这种最经典的是出现在默认 Go http client 调用没有设置 Timeout,从而只要出现一次故障,就足矣让记住这类 “坑”,毕竟崩的 ”慢“,错误日志还多。(via: 《微服务的战争:级联故障和雪崩》)

目的和措施

为了解决上述问题所带来的灾难,在分布式应用中常需要对服务依赖进行熔断降级。在存在问题时,暂时切断内部调用,避免局部不稳定因素导致整个分布式系统的雪崩。

而熔断降级作为保护服务自身的手段,通常是在客户端进行规则配置和熔断识别:

image

常见的有三种熔断降级措施:慢调用比例策略、错误比例策略、错误计数策略。

慢调用比例

在所设定的时间窗口内,慢调用的比例大于所设置的阈值,则对接下来访问的请求进行自动熔断。

错误比例

在所设定的时间窗口内,调用的访问错误比例大于所设置的阈值,则对接下来访问的请求进行自动熔断。

错误计数

在所设定的时间窗口内,调用的访问错误次数大于所设置的阈值,则对接下来访问的请求进行自动熔断。

实践案例

知道流量控制、熔断降级的基本概念和功能后,在现实环境中应该如何结合项目进行使用呢。最常见的场景是可针对业务服务的 HTTP 路由进行流量控制,以 HTTP 路由作为资源埋点,这样子就可以实现接口级的调控了。

image

还可以增强其功能特性,针对参数也进行多重匹配。常会有这种限流诉求:针对 HTTP GET /eddycjy/info 且 language 为 go 的情况下进行限流。另外还可以针对 HTTP 调用封装统一方法,进行默认的熔断注入,实现多重保障。

而结合系统负载、服务 QPS 等,可以对限流熔断的规则数据源进行实时调控,再结合 Watch 机制,就能够比较平滑的实现自适应限流熔断的接入。

总结

在分布式应用中,限流熔断是非常重要的一环,越早开始做越有益处。但需要注意的是,不同公司的业务模型多多少少有些不一样,所针对的匹配维度多少有些不同,因此需要提前进行业务调研。

且在做业务的限流熔断时,注意把度量指标的打点做上,这样子后续就能够结合 Prometheus+Grafana+Alertmanager 做一系列的趋势图,熔断告警,自动扩缩容等相关工作了,会是一个很好的助力。

我的公众号

分享 Go 语言、微服务架构和奇怪的系统设计,欢迎大家关注我的公众号和我进行交流和沟通,二维码:

image

最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

查看原文

赞 7 收藏 4 评论 0

CodeCloud 赞了文章 · 2020-08-21

重磅丨科技教育公司“好未来”正式对外开源高性能PHP框架Fend

好未来是一家以智慧教育和开放平台为主体,以素质教育和课外辅导为载体,在全球范围内服务公办教育,助力民办教育,探索未来教育新模式的科技教育公司。

截至目前,好未来集团已围绕教育场景需求,累计研发包括图像、语音、数据挖掘、自然语言处理等8大类型、100多项AI能力,打造10余项教育场景应用AI解决方案。

在技术不断提升的道路上,好未来技术线提出坚持“大中台、小前台”的技术战略,统一基础服务设施建设,推进公司技术组件落地,增强企业技术人才内生,不断提升企业的技术实力及技术影响力。

除此之外,好未来内部坚持开源共享,通过“开放、共享、合力开发”的模式,推动开源文化氛围的形成与技术组织变革,为中台建设提供了另外一种抓手。同时通过开源文化的建设,促进整个教育生态技术共享,提升教育科技实力,更好的为“科技与爱让教育更美好”的愿景奠定基础。

近期由“好未来”技术团队开源的高性能PHP框架Fend PHP正式上线!该框架单机QPS可达到4000个,好未来内部目前超过30个团队项目在使用该PHP框架!

前言

PHP是一款简单方便的语言,而行业开源框架为了后续灵活 而变得过于繁重

Fend框架是一款很有历史的框架、初代发布后一直在好未来坊间传播使用、衍生出大量分支版本

这是一款很有意思的框架、普通的框架内隐藏着大型互联网经验的精华、也同时存在大量历史痕迹

2019年7月 我们对Fend进行整理、封装、推广、目前在好未来内部有大量的用户在使用、维护

2020年7月 开源、以此共建交流

我们崇尚 脚踏实地、仰望星空 精神 欢迎小伙伴一起参与开源共建

设计方向

Fend 框架是一款以企业快速实现业务为主要目标的框架,但与复杂的行业流行框架追求不同:

  • 简单实用:追求快速上手,扩展功能一步到位、大量降低功能的复杂度、框架更注重简单实用实现
  • 单层内核:追求一个函数能实现的功能绝不继承封装,不追求框架自身功能的继承可复用
  • 内聚归类:高度集中归类功能,降低底层复杂度,减少底层组件关注度、更多时间在业务
  • 持续积累:持续积累大型互联网线上运营经验,持续探索企业实用技巧,深度来自于积累而非AOP带来的灵活性
  • 内核设计:高内聚简单内核,放开业务自封装空间,留下更多空间给业务
  • 开源心态:开放公开,接受任何符合价值观源码奉献、但有严格代码审核

功能简介

  • Swoole/FPM 双引擎平滑切换(协程版本还在整理稍晚放出)
  • 统一使用 Composer Autoload PSR4
  • 请求Debug 模式,请求网址wxdebug=1可查看debug模式查看异常分析性能
  • 协程模式下对变量域做了更好的封装,降低协程使用难度
  • 支持压测使用灰度影子库
  • 高速map映射路由 + FastRouter正则路由
  • 符合大数据挖掘设计的Trace日志,方便ELK分析、ClickHouse、HBase、实时预警
  • throw new Exception方式处理业务异常、能够快速发现异常

性能压测

目前是在KVM虚拟机上压测、后续会找一台阿里云进行压测

FPM性能

服务器配置

  • CPU 4 核 Xeon 2.2
  • 内存 12G
  • KVM + CentOS 7.6
  • FPM 开启进程数 500

QPS 5331 (分析:fpm空跑hello 1w、引入composer autoload 后 7000、开启日志trace 6000、框架内echo 5000)

Swoole 1.10.x 性能

服务器配置

  • CPU 4 核 Xeon 2.2
  • 内存 12G
  • KVM + CentOS 7.6
  • FPM 开启进程数 500

QPS 24000、协程版本稍晚放出

发行版本介绍

Fend有两个版本

  • Tag版本为 1.2.x FPM/Swoole 1.10.x 平滑切换版本
  • Tag版本为 1.3.x FPM/Swoole 4.5.x Coroutine 协程 平滑切换版本 此版本还在调整

以下为1.2.x版本安装

FPM Engine Start

master is 1.2.x version

composer create-project fend/fend-skeleton:~1.2.0 project_name
复制代码

Ref nginx.conf to configure Nginx and http://127.0.0.1/ on browser

Swoole Engine Start

composer create-project fend/fend-skeleton:~1.2.0 project_name

# swoole start ( /bin/fend depend on composer require symfony/console )
php /bin/fend Swoole -c app/Config/Swoole.php start
php /bin/start.php -c app/Config/Swoole.php start
复制代码

browser http://127.0.0.1:9572/

1.3.0协程版本 安装

composer create-project fend/fend-skeleton:~1.3.0
复制代码

软件作者贡献列表

image

(其他贡献者、请详见文档鸣谢)

合作伙伴

好未来教育集团90%在线业务在使用本框架

  • xiaohouai.png

    xiaohouai.png

共建规则

欢迎挑战组件功能、允许同类功能同时发布竞争、以 性能好 + 实用及实现简单 + 功能实用 评判

联系我们

issue: github.com/tal-tech/fe…

加群请加微信:

contactus.png

也许你还想看

DStack--基于flutter的混合开发框架

WebRTC源码分析——视频流水线建立(上)

"考试"背后的科学:教育测量中的理论与模型(IRT篇)

查看原文

赞 14 收藏 6 评论 1

CodeCloud 赞了文章 · 2020-06-28

Go 每日一库之 cli

## 简介

cli是一个用于构建命令行程序的库。我们之前也介绍过一个用于构建命令行程序的库cobra。在功能上来说两者差不多,cobra的优势是提供了一个脚手架,方便开发。cli非常简洁,所有的初始化操作就是创建一个cli.App结构的对象。通过为对象的字段赋值来添加相应的功能。

cli与我们上一篇文章介绍的negroni是同一个作者urfave

快速使用

cli需要搭配 Go Modules 使用。创建目录并初始化:

$ mkdir cli && cd cli
$ go mod init github.com/darjun/go-daily-lib/cli

安装cli库,有v1v2两个版本。如果没有特殊需求,一般安装v2版本:

$ go get -u github.com/urfave/cli/v2

使用:

package main

import (
  "fmt"
  "log"
  "os"

  "github.com/urfave/cli/v2"
)

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "hello world example",
    Action: func(c *cli.Context) error {
      fmt.Println("hello world")
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

使用非常简单,理论上创建一个cli.App结构的对象,然后调用其Run()方法,传入命令行的参数即可。一个空白的cli应用程序如下:

func main() {
  (&cli.App{}).Run(os.Args)
}

但是这个空白程序没有什么用处。我们的hello world程序,设置了Name/Usage/ActionNameUsage都显示在帮助中,Action是调用该命令行程序时实际执行的函数,需要的信息可以从参数cli.Context获取。

编译、运行(环境:Win10 + Git Bash):

$ go build -o hello
$ ./hello
hello world

除了这些,cli为我们额外生成了帮助信息:

$ ./hello --help
NAME:
   hello - hello world example

USAGE:
   hello [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)

参数

通过cli.Context的相关方法我们可以获取传给命令行的参数信息:

  • NArg():返回参数个数;
  • Args():返回cli.Args对象,调用其Get(i)获取位置i上的参数。

示例:

func main() {
  app := &cli.App{
    Name:  "arguments",
    Usage: "arguments example",
    Action: func(c *cli.Context) error {
      for i := 0; i < c.NArg(); i++ {
        fmt.Printf("%d: %s\n", i+1, c.Args().Get(i))
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

这里只是简单输出:

$ go run main.go hello world
1: hello
2: world

选项

一个好用的命令行程序怎么会少了选项呢?cli设置和获取选项非常简单。在cli.App{}结构初始化时,设置字段Flags即可添加选项。Flags字段是[]cli.Flag类型,cli.Flag实际上是接口类型。cli为常见类型都实现了对应的XxxFlag,如BoolFlag/DurationFlag/StringFlag等。它们有一些共用的字段,Name/Value/Usage(名称/默认值/释义)。看示例:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:  "lang",
        Value: "english",
        Usage: "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if c.String("lang") == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

上面是一个打招呼的命令行程序,可通过选项lang指定语言,默认为英语。设置选项为非english的值,使用汉语。如果有参数,使用第一个参数作为人名,否则使用world。注意选项是通过c.Type(name)来获取的,Type为选项类型,name为选项名。编译、运行:

$ go build -o flags

# 默认调用
$ ./flags
hello world

# 设置非英语
$ ./flags --lang chinese
你好 world

# 传入参数作为人名
$ ./flags --lang chinese dj
你好 dj

我们可以通过./flags --help来查看选项:

$ ./flags --help
NAME:
   flags - A new cli application

USAGE:
   flags [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --lang value  language for the greeting (default: "english")
   --help, -h    show help (default: false)

存入变量

除了通过c.Type(name)来获取选项的值,我们还可以将选项存到某个预先定义好的变量中。只需要设置Destination字段为变量的地址即可:

func main() {
  var language string

  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:        "lang",
        Value:       "english",
        Usage:       "language for the greeting",
        Destination: &language,
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if language == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

与上面的程序效果是一样的。

占位值

cli可以在Usage字段中为选项设置占位值,占位值通过反引号 ` 包围。只有第一个生效,其他的维持不变。占位值有助于生成易于理解的帮助信息:

func main() {
  app := & cli.App{
    Flags : []cli.Flag {
      &cli.StringFlag{
        Name:"config",
        Usage: "Load configuration from `FILE`",
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

设置占位值之后,帮助信息中,该占位值会显示在对应的选项后面,对短选项也是有效的:

$ go build -o placeholder
$ ./placeholder --help
NAME:
   placeholder - A new cli application

USAGE:
   placeholder [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --config FILE  Load configuration from FILE
   --help, -h     show help (default: false)

别名

选项可以设置多个别名,设置对应选项的Aliases字段即可:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Aliases: []string{"language", "l"},
        Value:   "english",
        Usage:   "language for the greeting",
      },
    },
    Action: func(c *cli.Context) error {
      name := "world"
      if c.NArg() > 0 {
        name = c.Args().Get(0)
      }

      if c.String("lang") == "english" {
        fmt.Println("hello", name)
      } else {
        fmt.Println("你好", name)
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

使用--lang chinese--language chinese-l chinese效果是一样的。如果通过不同的名称指定同一个选项,会报错:

$ go build -o aliase
$ ./aliase --lang chinese
你好 world
$ ./aliase --language chinese
你好 world
$ ./aliase -l chinese
你好 world
$ ./aliase -l chinese --lang chinese
Cannot use two forms of the same flag: l lang

环境变量

除了通过执行程序时手动指定命令行选项,我们还可以读取指定的环境变量作为选项的值。只需要将环境变量的名字设置到选项对象的EnvVars字段即可。可以指定多个环境变量名字,cli会依次查找,第一个有值的环境变量会被使用。

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:    "lang",
        Value:   "english",
        Usage:   "language for the greeting",
        EnvVars: []string{"APP_LANG", "SYSTEM_LANG"},
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("你好")
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

编译、运行:

$ go build -o env
$ APP_LANG=chinese ./env
你好

文件

cli还支持从文件中读取选项的值,设置选项对象的FilePath字段为文件路径:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:     "lang",
        Value:    "english",
        Usage:    "language for the greeting",
        FilePath: "./lang.txt",
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("你好")
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

main.go同级目录创建一个lang.txt,输入内容chinese。然后编译运行程序:

$ go build -o file
$ ./file
你好

cli还支持从YAML/JSON/TOML等配置文件中读取选项值,这里就不一一介绍了。

选项优先级

上面我们介绍了几种设置选项值的方式,如果同时有多个方式生效,按照下面的优先级从高到低设置:

  • 用户指定的命令行选项值;
  • 环境变量;
  • 配置文件;
  • 选项的默认值。

组合短选项

我们时常会遇到有多个短选项的情况。例如 linux 命令ls -a -l,可以简写为ls -alcli也支持短选项合写,只需要设置cli.AppUseShortOptionHandling字段为true即可:

func main() {
  app := &cli.App{
    UseShortOptionHandling: true,
    Commands: []*cli.Command{
      {
        Name:  "short",
        Usage: "complete a task on the list",
        Flags: []cli.Flag{
          &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}},
          &cli.BoolFlag{Name: "option", Aliases: []string{"o"}},
          &cli.BoolFlag{Name: "message", Aliases: []string{"m"}},
        },
        Action: func(c *cli.Context) error {
          fmt.Println("serve:", c.Bool("serve"))
          fmt.Println("option:", c.Bool("option"))
          fmt.Println("message:", c.Bool("message"))
          return nil
        },
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

编译运行:

$ go build -o short
$ ./short short -som "some message"
serve: true
option: true
message: true

需要特别注意一点,设置UseShortOptionHandlingtrue之后,我们不能再通过-指定选项了,这样会产生歧义。例如-langcli不知道应该解释为l/a/n/g 4 个选项还是lang 1 个。--还是有效的。

必要选项

如果将选项的Required字段设置为true,那么该选项就是必要选项。必要选项必须指定,否则会报错:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.StringFlag{
        Name:     "lang",
        Value:    "english",
        Usage:    "language for the greeting",
        Required: true,
      },
    },
    Action: func(c *cli.Context) error {
      if c.String("lang") == "english" {
        fmt.Println("hello")
      } else {
        fmt.Println("你好")
      }
      return nil
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

不指定选项lang运行:

$ ./required
2020/06/23 22:11:32 Required flag "lang" not set

帮助文本中的默认值

默认情况下,帮助文本中选项的默认值显示为Value字段值。有些时候,Value并不是实际的默认值。这时,我们可以通过DefaultText设置:

func main() {
  app := &cli.App{
    Flags: []cli.Flag{
      &cli.IntFlag{
        Name:     "port",
        Value:    0,
        Usage:    "Use a randomized port",
        DefaultText :"random",
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

上面代码逻辑中,如果Value设置为 0 就随机一个端口,这时帮助信息中default: 0就容易产生误解了。通过DefaultText可以避免这种情况:

$ go build -o default-text
$ ./default-text --help
NAME:
   default-text - A new cli application

USAGE:
   default-text [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --port value  Use a randomized port (default: random)
   --help, -h    show help (default: false)

子命令

子命令使命令行程序有更好的组织性。git有大量的命令,很多以某个命令下的子命令存在。例如git remote命令下有add/rename/remove等子命令,git submodule下有add/status/init/update等子命令。

cli通过设置cli.AppCommands字段添加命令,设置各个命令的SubCommands字段,即可添加子命令。非常方便!

func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:    "add",
        Aliases: []string{"a"},
        Usage:   "add a task to the list",
        Action: func(c *cli.Context) error {
          fmt.Println("added task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "complete",
        Aliases: []string{"c"},
        Usage:   "complete a task on the list",
        Action: func(c *cli.Context) error {
          fmt.Println("completed task: ", c.Args().First())
          return nil
        },
      },
      {
        Name:    "template",
        Aliases: []string{"t"},
        Usage:   "options for task templates",
        Subcommands: []*cli.Command{
          {
            Name:  "add",
            Usage: "add a new template",
            Action: func(c *cli.Context) error {
              fmt.Println("new task template: ", c.Args().First())
              return nil
            },
          },
          {
            Name:  "remove",
            Usage: "remove an existing template",
            Action: func(c *cli.Context) error {
              fmt.Println("removed task template: ", c.Args().First())
              return nil
            },
          },
        },
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

上面定义了 3 个命令add/complete/templatetemplate命令定义了 2 个子命令add/remove。编译、运行:

$ go build -o subcommand
$ ./subcommand add dating
added task:  dating
$ ./subcommand complete dating
completed task:  dating
$ ./subcommand template add alarm
new task template:  alarm
$ ./subcommand template remove alarm
removed task template:  alarm

注意一点,子命令默认不显示在帮助信息中,需要显式调用子命令所属命令的帮助(./subcommand template --help):

$ ./subcommand --help
NAME:
   subcommand - A new cli application

USAGE:
   subcommand [global options] command [command options] [arguments...]

COMMANDS:
   add, a       add a task to the list
   complete, c  complete a task on the list
   template, t  options for task templates
   help, h      Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)

$ ./subcommand template --help
NAME:
   subcommand template - options for task templates

USAGE:
   subcommand template command [command options] [arguments...]

COMMANDS:
   add      add a new template
   remove   remove an existing template
   help, h  Shows a list of commands or help for one command

OPTIONS:
   --help, -h  show help (default: false)

分类

在子命令数量很多的时候,可以设置Category字段为它们分类,在帮助信息中会将相同分类的命令放在一起展示:

func main() {
  app := &cli.App{
    Commands: []*cli.Command{
      {
        Name:  "noop",
        Usage: "Usage for noop",
      },
      {
        Name:     "add",
        Category: "template",
        Usage:    "Usage for add",
      },
      {
        Name:     "remove",
        Category: "template",
        Usage:    "Usage for remove",
      },
    },
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

编译、运行:

$ go build -o categories
$ ./categories --help
NAME:
   categories - A new cli application

USAGE:
   categories [global options] command [command options] [arguments...]

COMMANDS:
   noop     Usage for noop
   help, h  Shows a list of commands or help for one command
   template:
     add     Usage for add
     remove  Usage for remove

GLOBAL OPTIONS:
   --help, -h  show help (default: false)

看上面的COMMANDS部分。

自定义帮助信息

cli中所有的帮助信息文本都可以自定义,整个应用的帮助信息模板通过AppHelpTemplate指定。命令的帮助信息模板通过CommandHelpTemplate设置,子命令的帮助信息模板通过SubcommandHelpTemplate设置。甚至可以通过覆盖cli.HelpPrinter这个函数自己实现帮助信息输出。下面程序在默认的帮助信息后添加个人网站和微信信息:

func main() {
  cli.AppHelpTemplate = fmt.Sprintf(`%s

WEBSITE: http://darjun.github.io

WECHAT: GoUpUp`, cli.AppHelpTemplate)

  (&cli.App{}).Run(os.Args)
}

编译运行:

$ go build -o help
$ ./help --help
NAME:
   help - A new cli application

USAGE:
   help [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h  show help (default: false)


WEBSITE: http://darjun.github.io

WECHAT: GoUpUp

我们还可以改写整个模板:

func main() {
  cli.AppHelpTemplate = `NAME:
  {{.Name}} - {{.Usage}}
USAGE:
  {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
  {{if len .Authors}}
AUTHOR:
  {{range .Authors}}{{ . }}{{end}}
  {{end}}{{if .Commands}}
COMMANDS:
 {{range .Commands}}{{if not .HideHelp}}   {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
  {{range .VisibleFlags}}{{.}}
  {{end}}{{end}}{{if .Copyright }}
COPYRIGHT:
  {{.Copyright}}
  {{end}}{{if .Version}}
VERSION:
  {{.Version}}
{{end}}
 `

  app := &cli.App{
    Authors: []*cli.Author{
      {
        Name:  "dj",
        Email: "darjun@126.com",
      },
    },
  }
  app.Run(os.Args)
}

{{.XXX}}其中XXX对应cli.App{}结构中设置的字段,例如上面Authors

$ ./help --help
NAME:
  help - A new cli application
USAGE:
  help [global options] command [command options] [arguments...]

AUTHOR:
  dj <darjun@126.com>

COMMANDS:
    help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
  --help, -h  show help (default: false)

注意观察AUTHOR部分。

通过覆盖HelpPrinter,我们能自己输出帮助信息:

func main() {
  cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
    fmt.Println("Simple help!")
  }

  (&cli.App{}).Run(os.Args)
}

编译、运行:

$ ./help --help
Simple help!

内置选项

帮助选项

默认情况下,帮助选项为--help/-h。我们可以通过cli.HelpFlag字段设置:

func main() {
  cli.HelpFlag = &cli.BoolFlag{
    Name:    "haaaaalp",
    Aliases: []string{"halp"},
    Usage:   "HALP",
  }

  (&cli.App{}).Run(os.Args)
}

查看帮助:

$ go run main.go --halp
NAME:
   main.exe - A new cli application

USAGE:
   main.exe [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --haaaaalp, --halp  HALP (default: false)

版本选项

默认版本选项-v/--version输出应用的版本信息。我们可以通过cli.VersionFlag设置版本选项 :

func main() {
  cli.VersionFlag = &cli.BoolFlag{
    Name:    "print-version",
    Aliases: []string{"V"},
    Usage:   "print only the version",
  }

  app := &cli.App{
    Name:    "version",
    Version: "v1.0.0",
  }
  app.Run(os.Args)
}

这样就可以通过指定--print-version/-V输出版本信息了。运行:

$ go run main.go --print-version
version version v1.0.0

$ go run main.go -V
version version v1.0.0

我们还可以通过设置cli.VersionPrinter字段控制版本信息的输出内容:

const (
  Revision = "0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b"
)

func main() {
  cli.VersionPrinter = func(c *cli.Context) {
    fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision)
  }

  app := &cli.App{
    Name:    "version",
    Version: "v1.0.0",
  }
  app.Run(os.Args)
}

上面程序同时输出版本号和git提交的 SHA 值:

$ go run main.go -v
version=v1.0.0 revision=0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b

总结

cli非常灵活,只需要设置cli.App的字段值即可实现相应的功能,不需要额外记忆函数、方法。另外cli还支持 Bash 自动补全的功能,对 zsh 的支持也比较好,感兴趣可自行探索。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. cli GitHub:https://github.com/urfave/cli
  2. Go 每日一库之 cobra:https://darjun.github.io/2020/01/17/godailylib/cobra/
  3. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

查看原文

赞 5 收藏 2 评论 0

CodeCloud 赞了文章 · 2020-06-09

为什么容器内存占用居高不下,频频 OOM

最近我在回顾思考(写 PPT),整理了现状,发现了这个问题存在多时,经过一番波折,最终确定了元凶和相对可行的解决方案,因此也在这里分享一下排查历程。

时间线:

  • 在上 Kubernetes 的前半年,只是用 Kubernetes,开发没有权限,业务服务极少,忙着写新业务,风平浪静。
  • 在上 Kubernetes 的后半年,业务服务较少,偶尔会阶段性被运维唤醒,问之 “为什么你们的服务内存占用这么高,赶紧查”。此时大家还在为新业务冲刺,猜测也许是业务代码问题,但没有调整代码去尝试解决。
  • 在上 Kubernetes 的第二年,业务服务逐渐增多,普遍增加了容器限额 Limits,出现了好几个业务服务是内存小怪兽,因此如果不限制的话,服务过度占用会导致驱逐,因此反馈语也就变成了:“为什么你们的服务内存占用这么高,老被 OOM Kill,赶紧查”。据闻也有几个业务大佬有去排查(因为 OOM 反馈),似乎没得出最终解决方案。

不禁让我们思考,为什么个别 Go 业务服务,Memory 总是提示这么高,经常达到容器限额,以至于被动 OOM Kill,是不是有什么安全隐患?

现象

内存居高不下

发现个别业务服务内存占用挺高,触发告警,且通过 Grafana 发现在凌晨(没有什么流量)的情况下,内存占用量依然拉平,没有打算下降的样子,高峰更是不得了,像是个内存炸弹:

image

并且我所观测的这个服务,早年还只是 100MB。现在随着业务迭代和上升,目前已经稳步 4GB,容器限额 Limits 纷纷给它开道,但我想总不能是无休止的增加资源吧,这是一个大问题。

进入重启怪圈

有的业务服务,业务量小,自然也就没有调整容器限额,因此得不到内存资源,又超过额度,就会进入疯狂的重启怪圈:

image

重启将近 300 次,非常不正常了,更不用提所接受到的告警通知。

排查

猜想一:频繁申请重复对象

出现问题的个别业务服务都有几个特点,那就是基本为图片处理类的功能,例如:图片解压缩、批量生成二维码、PDF 生成等,因此就怀疑是否在量大时频繁申请重复对象,而 Go 本身又没有及时释放内存,因此导致持续占用。

sync.Pool

基本上想解决 “频繁申请重复对象”,我们大多会采用多级内存池的方式,也可以用最常见的 sync.Pool,这里可参考全成所借述的《Go 夜读》上关于 sync.Pool 的分享,关于这类情况的场景:

当多个 goroutine 都需要创建同⼀个对象的时候,如果 goroutine 数过多,导致对象的创建数⽬剧增,进⽽导致 GC 压⼒增大。形成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒降低-并发更⼤”这样的恶性循环。

验证场景

在描述中关注到几个关键字,分别是并发大,Goroutine 数过多,GC 压力增大,GC 缓慢。也就是需要满足上述几个硬性条件,才可以认为是符合猜想的。

通过拉取 PProf goroutine,可得知 Goroutine 数并不高:

image

另外在凌晨长达 6 小时,没有什么流量的情况下,也不符合并发大,Goroutine 数过多的情况,若要更进一步确认,可通过 Grafana 落实其量的高低。

从结论上来讲,我认为与其没有特别直接的关系,但猜想其所对应的业务功能到导致的间接关系应当存在。

猜想二:不知名内存泄露

内存居高不下,其中一个反应就是猜测是否存在泄露,而我们的容器中目前只跑着一个 Go 进程,因此首要看看该 Go 应用是否有问题。这时候可以借助 PProf heap(可以使用 base -diff):

image

显然其提示的内存使用不高,那会不会是 PProf 出现了 BUG 呢。接下通过命令也可确定 Go 进程的 RSS 并不高,但 VSZ 却相对 “高” 的惊人,我在 19 年针对此写过一篇《Go 应用内存占用太多,让排查?(VSZ篇)》 ,这次 VSZ 过高也给我留下了一个念想。

从结论上来讲,也不像 Go 进程内存泄露的问题,因此也将其排除。

猜想三:madvise 策略变更

  • 在 Go1.12 以前,Go Runtime 在 Linux 上使用的是 MADV_DONTNEED 策略,可以让 RSS 下降的比较快,就是效率差点。
  • 在 Go1.12 及以后,Go Runtime 专门针对其进行了优化,使用了更为高效的 MADV_FREE 策略。但这样子所带来的副作用就是 RSS 不会立刻下降,要等到系统有内存压力了才会释放占用,RSS 才会下降。

查看容器的 Linux 内核版本:

$ uname -a
Linux xxx-xxx-99bd5776f-k9t8z 3.10.0-693.2.2.el7.x86_64 

MADV_FREE 的策略改变,需要 Linux 内核在 4.5 及以上(详细可见 go/issues/23687),显然不符合,因此也将其从猜测中排除。

猜想四:监控/判别条件有问题

会不会是 Grafana 的图表错了,Kubernetes OOM Kill 的判别标准也错了呢,显然不大可能,毕竟我们拥抱云,阿里云 Kubernetes 也运行了好几年。

image

但在这次怀疑中,我了解到 OOM 的判断标准是 container_memory_working_set_bytes 指标,因此有了下一步猜想。

猜想五:容器环境的机制

既然不是业务代码影响,也不是 Go Runtime 的直接影响,那是否与环境本身有关呢,我们可以得知容器 OOM 的判别标准是 container_memory_working_set_bytes(当前工作集)。

而 container_memory_working_set_bytes 是由 cadvisor 提供的,对应下述指标:

image

从结论上来讲,Memory 换算过来是 4GB+,石锤。接下来的问题就是 Memory 是怎么计算出来的呢,显然和 RSS 不对标。

原因

cadvisor/issues/638 可得知 container_memory_working_set_bytes 指标的组成实际上是 RSS + Cache。而 Cache 高的情况,常见于进程有大量文件 IO,占用 Cache 可能就会比较高,猜测也与 Go 版本、Linux 内核版本的 Cache 释放、回收方式有较大关系。

image

而各业务模块常见功能,如:

  • 批量图片解压缩。
  • 批量二维码生成。
  • 批量上传渲染后图片。
  • 批量 PDF 生成。
  • ...

只要是涉及有大量文件 IO 的服务,基本上是这个问题的老常客了,写这类服务基本写一个中一个,因为这是一个混合问题,像其它单纯操作为主的业务服务就很 “正常”,不会出现内存居高不下。

解决方案

在本场景中 cadvisor 所提供的判别标准 container_memory_working_set_bytes 是不可变更的,也就是无法把判别标准改为 RSS,因此我们只能考虑掌握主动权。

首先是做好做多级内存池管理,可以缓解这个问题的症状。但这存在难度,从另外一个角度来看,你怎么知道什么时候在哪个集群上会突然出现这类型的服务,何况开发人员的预期情况参差不齐,写多级内存池写出 BUG 也是有可能的。

让业务服务无限重启,也是不现实的,被动重启,没有控制,且告警,存在风险

因此为了掌握主动权,可以在部署环境可以配合脚本做 “手动” HPA,当容器内存指标超过约定限制后,起一个新的容器替换,再将原先的容器给释放掉,就可以在预期内替换且业务稳定了。

image

总结

虽然这问题时间跨度比较长,整体来讲都是阶段性排查,本质上可以说是对 Kubernetes 的不熟悉有关。但综合来讲这个问题涉及范围比较大,因为内存居高不下的可能性有很多种,要一个个排查,开发权限有限,费时费力。

基本排查思路就是:

  1. 怀疑业务代码(PProf)。
  2. 怀疑其它代码(PProf)。
  3. 怀疑 Go Runtime 。
  4. 怀疑工具。
  5. 怀疑环境。

非常感谢在这大段时间内被我咨询的各位大佬们,感觉就是隔了一层纱,捅穿了就很快就定位到了,大家如果有其它解决方案也欢迎随时沟通。

我的博客

原文地址:为什么容器内存占用居高不下,频频 OOM

我的公众号

image

查看原文

赞 5 收藏 0 评论 0

CodeCloud 发布了文章 · 2020-05-10

Hackintosh (黑苹果) 折腾

ASRock-Z370-Pro4-9900K-Hackintosh

系统版本

MacOS Catalina

EFI 文件 GitHub 链接

EFI 引导文件可以去 git clone https://github.com/wujunze/ASRock-Z370-Pro4-9900K-Hackintosh

详细配置

硬件品牌型号
处理器英特尔 Core i9-9900K 3.6GHz 八核心十六线程
主板华擎 Z370 Pro4
内存芝奇 DDR4 3200MHz 8GB x 2
硬盘三星 970 Pro MLC 1T
显卡蓝宝石 白金 满血版 RX560 4G
显示器LG 27UL850 27英寸 4K Type-C IPS
蓝牙和无线网卡博通 BCM943602CS

部分截图

boot menu
loading
install one
选择语言
install end
htop one

安装系统

1.制作苹果系统 安装U盘

1.1 U盘 16G 格式化为 GUID Mac日志格式

1.2 执行命令 制作启动U盘
sudo /Users/wujunze/Desktop/Install\ macOS\ Catalina.app/Contents/Resources/createinstallmedia --volume /Volumes/MacInstall

1.3 U 盘制作 OK
U 盘制作 OK

2. copy 引导文件 给 U盘用

2.1 挂载 EFI 分区
diskutil list
sudo diskutil mount disk4s1
diskutil list

2.2 把你主板对应的 EFI 文件 copy 到 EFI 分区
EFI

2.3 此时的 U 盘 应该是这样的

通过 EFI 引导 MacOS U盘镜像安装系统

选择 U盘里面的 UEFI 启动项
UEFI 启动项
U盘引导 MacOS 安装

进入安装引导界面 按回车键

安装界面

Clover 引导

loading
进入安装界面
Mac 安装工具

格式化磁盘
显示磁盘
抹掉磁盘
系统盘格式化成功
安装系统
安装系统

安装
安装系统

安装完成
选择语言

进入系统
进入系统

系统概览
系统概览

存储空间
disk info

通过 硬盘 EFI 引导系统

重复第一步的 挂载 EFI 分区步骤
然后把U盘里面的 EFI 引导文件 copy 到硬盘的 EFI 分区即可

去 APP store 下载一个 Xcode
APP store

恭喜你的 Hackintosh 已经安装完成了

折腾黑苹果是一个很有趣的过程 可以深入了解系统启动 系统引导 赶快动手折腾起来吧

欢迎评论区留言交流 或者加入 Hackintosh 交流小组 加微信1017109588 暗号Hackintosh

笔者才疏学浅,仓促成文,如有不当之处,还请大家斧正.
查看原文

赞 1 收藏 0 评论 0

CodeCloud 赞了文章 · 2020-05-07

什么,秒杀系统也有这么多种!

前言

本文结构很简单:

5张图送你5种秒杀系统,再加点骚操作,再顺带些点心里话🤷‍♀️。

一个简单的秒杀系统

实现原理: 通过redis原子操作减库存

图一

优点缺点
简单好用考验redis服务能力
是否公平
公平
先到先得

我们称这类秒杀系统为:

简单秒杀系统

如果刚开始QPS并不高,redis完全抗的下来的情况,完全可以依赖这个「简单秒杀系统」。

一个够用的秒杀系统

实现原理: 服务内存限流算法 + redis原子操作减库存

图二

优点缺点
简单好用-
是否公平
不是很公平
相对的先到先得

我们称这类秒杀系统为:

够用秒杀系统

性能再好点的秒杀系统

实现原理: 服务本地内存原子操作减库存

服务本地内存的库存怎么来的?

活动开始前分配好每台机器的库存,推送到机器上。

图三

优点缺点
高性能不支持动态伸缩容(活动进行期间),因为库存是活动开始前分配好的
释放redis压力-
是否公平
不是很公平
不是绝对的先到先得

我们称这类秒杀系统为:

预备库存秒杀系统

支持动态伸缩容的秒杀系统

实现原理: 服务本地协程Coroutine定时redis原子操作减部分库存到本地内存 + 服务本地内存原子操作减库存

图四

优点缺点
高性能支持动态伸缩容(活动进行期间)
释放redis压力-
具备通用性-
是否公平
不是很公平,但是好了点
几乎先到先得

我们称这类秒杀系统为:

实时预备库存秒杀系统

公平的秒杀系统

实现原理: 服务本地Goroutine定时同步是否售罄到本地内存 + 队列 + 排队成功轮训(或主动Push)结果

图五

优点缺点
高性能开发成本高(需主动通知或轮训排队结果)
真公平-
具备通用性-
是否公平
很公平
绝对的先到先得

我们称这类秒杀系统为:

公平排队秒杀系统

骚操作

上面的秒杀系统还不够完美吗?

答案:是的。

还有什么优化的空间?

答案:静态化获取秒杀活动信息的接口。

静态化是什么意思?

答案:比如获取秒杀活动信息是通过接口 https://seckill.skrshop.tech/v1/acticity/get 获取的。现在呢,我们需要通过https://static-api.skrshop.tech/seckill/v1/acticity/get 这个接口获取。有什么区别呢?看下面:

服务名接口数据存储位置
秒杀服务https://seckill.skrshop.tech/...秒杀服务内存或redis等
接口静态化服务https://static-api.skrshop.te...CDN、本地文件

以前是这样

变成了这样

结果:可以通过接口https://static-api.skrshop.tech/seckill/v1/acticity/get就获取到了秒杀活动信息,流量都分摊到了cdn,秒杀服务自身没了这部分的负载。

小声点说:“秒杀结果我也敢推CDN😏😏😏。”
备注:
之后我们会分享`如何用Golang设计一个好用的「接口静态化服务」`。

总结

上面我们得到了如下几类秒杀系统

秒杀系统
简单秒杀系统
够用秒杀系统
预备库存秒杀系统
实时预备库存秒杀系统
公平排队秒杀系统

我想说的是里面没有最好的方案,也没有最坏的方案,只有适合你的。

先到先得来说,一定要看你们的产品对外宣传,切勿上来就追逐绝对的先到先得。其实你看所有的方案,相对而言都是“先到先得”,比如,活动开始一个小时了你再来抢,那相对于准时的用户自然抢不过,对吧。

又如预备库存秒杀系统,虽然不支持动态伸缩容。但是如果你的环境满足如下任意条件,就完全够用了。

  • 秒杀场景结束时间之快,通常几秒就结束了,真实活动可能会发生如下情况:

    • 服务压力大还没挂:根本就来不及动态伸缩容
    • 服务压力大已经挂了:可以先暂停活动,服务起来&扩容结束,用剩余库存重新推送
  • 运维自身不具备动态伸缩容的能力

所以:

合适好用就行,切勿过度设计。

最后

这次算是把老本都吐露出来了,真是慌得一匹。


SkrShop历史分享:https://github.com/skr-shop/m...

3911642037-d2bb08d8702e7c91_articlex.jpg

查看原文

赞 59 收藏 39 评论 2

CodeCloud 赞了文章 · 2020-04-12

Linux基础:用tcpdump抓包

简介

网络数据包截获分析工具。支持针对网络层、协议、主机、网络或端口的过滤。并提供and、or、not等逻辑语句帮助去除无用的信息。

tcpdump - dump traffic on a network

例子

不指定任何参数

监听第一块网卡上经过的数据包。主机上可能有不止一块网卡,所以经常需要指定网卡。

tcpdump

监听特定网卡

tcpdump -i en0

监听特定主机

例子:监听本机跟主机182.254.38.55之间往来的通信包。

备注:出、入的包都会被监听。

tcpdump host 182.254.38.55

特定来源、目标地址的通信

特定来源

tcpdump src host hostname

特定目标地址

tcpdump dst host hostname

如果不指定srcdst,那么来源 或者目标 是hostname的通信都会被监听

tcpdump host hostname

特定端口

tcpdump port 3000

监听TCP/UDP

服务器上不同服务分别用了TCP、UDP作为传输层,假如只想监听TCP的数据包

tcpdump tcp

来源主机+端口+TCP

监听来自主机123.207.116.169在端口22上的TCP数据包

tcpdump tcp port 22 and src host 123.207.116.169

监听特定主机之间的通信

tcpdump ip host 210.27.48.1 and 210.27.48.2

210.27.48.1除了和210.27.48.2之外的主机之间的通信

tcpdump ip host 210.27.48.1 and ! 210.27.48.2

稍微详细点的例子

tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap
(1)tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型
(2)-i eth1 : 只抓经过接口eth1的包
(3)-t : 不显示时间戳
(4)-s 0 : 抓取数据包时默认抓取长度为68字节。加上-S 0 后可以抓到完整的数据包
(5)-c 100 : 只抓取100个数据包
(6)dst port ! 22 : 不抓取目标端口是22的数据包
(7)src net 192.168.1.0/24 : 数据包的源网络地址为192.168.1.0/24
(8)-w ./target.cap : 保存成cap文件,方便用ethereal(即wireshark)分析

抓http包

TODO

限制抓包的数量

如下,抓到1000个包后,自动退出

tcpdump -c 1000

保存到本地

备注:tcpdump默认会将输出写到缓冲区,只有缓冲区内容达到一定的大小,或者tcpdump退出时,才会将输出写到本地磁盘

tcpdump -n -vvv -c 1000 -w /tmp/tcpdump_save.cap

也可以加上-U强制立即写到本地磁盘(一般不建议,性能相对较差)

实战例子

先看下面一个比较常见的部署方式,在服务器上部署了nodejs server,监听3000端口。nginx反向代理监听80端口,并将请求转发给nodejs server(127.0.0.1:3000)。

浏览器 -> nginx反向代理 -> nodejs server

问题:假设用户(183.14.132.117)访问浏览器,发现请求没有返回,该怎么排查呢?

步骤一:查看请求是否到达nodejs server -> 可通过日志查看。

步骤二:查看nginx是否将请求转发给nodejs server。

tcpdump port 8383 

这时你会发现没有任何输出,即使nodejs server已经收到了请求。因为nginx转发到的地址是127.0.0.1,用的不是默认的interface,此时需要显示指定interface

tcpdump port 8383 -i lo

备注:配置nginx,让nginx带上请求侧的host,不然nodejs server无法获取 src host,也就是说,下面的监听是无效的,因为此时对于nodejs server来说,src host 都是 127.0.0.1

tcpdump port 8383 -i lo and src host 183.14.132.117

步骤三:查看请求是否达到服务器

tcpdump -n tcp port 8383 -i lo and src host 183.14.132.117

相关链接

tcpdump 很详细的
http://blog.chinaunix.net/uid...

http://www.cnblogs.com/ggjuch...
Linux tcpdump命令详解

Tcpdump usage examples(推荐)
http://www.rationallyparanoid...

使用TCPDUMP抓取HTTP状态头信息
http://blog.sina.com.cn/s/blo...

查看原文

赞 10 收藏 19 评论 0

认证与成就

  • SegmentFault 讲师
  • 获得 693 次点赞
  • 获得 49 枚徽章 获得 1 枚金徽章, 获得 10 枚银徽章, 获得 38 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • nginx-http-echo-module

    A simple Nginx echo module is used to study and demonstration

  • panda

    Develop a simple PHP extension to learn PHP extension development and the PHP kernel (PHP7)

  • php-cli-color

    Simple and easy to use the PHP command-line output of color

  • wujunzeblog

    我的博客,基于typecho开发的

注册于 2015-09-23
个人主页被 9.5k 人浏览