网易传媒在2021年的时候就开始尝试将线上的一些核心集群使用Go语言进行重构,笔者有幸主导了本次Go语言重构,在本次重构中,为了减少各业务不必要的调研时间,我们成立了一个攻坚小组,就业务用到的各种依赖进行了统一调研、封装、测试,给各业务提供了统一的Go语言基础框架,极大的增加了业务开发效率,避免了重复研究。
之前笔者也就Go语言的一些特性、优势及问题进行了阐述,说明了传媒使用Go语言进行业务重构的一些目的、收益和重构过程中遇到的一些坑,感兴趣的读者可以参考笔者以前撰写的文章。
作为Go语言的通用框架,ngo除了在传媒内部大量使用,也在网易集团内部其他BU使用,这些BU也作为了参与者,贡献了自己的一份力量,能让ngo支持更多的特性。
在本文中,笔者将对ngo建设的目的做一些阐述,对一些我们开发应用要用到的基础服务做详细阐述,希望能给读者带来一些收益。
笔者主要调研了以下几个Go开源框架
Beego(https://beego.vip/)
go-zero(https://go-zero.dev/)
jupiter(https://jupiter.douyu.com/)
go-spring(https://go-spring.com/)
以上的Go开源框架我们都做了一些调研和验证,最后都没有直接使用这些开源框架,主要有以下几个原因:
缺乏大量业务所需库,比如kafka、redis、rpc、xxlJob等,如果在其基础上开发不如从零选择更适合的库。
对集团内部的一些基础服务需要做集成。
HTTP Server的性能不理想。
大部分库无法注入回调函数,也就难以增加无感的监控。
若干模块如ORM不够好用。
在传媒技术团队中推广Go语言的过程中,急需一个通用基础框架提供给业务开发使用,这个基础框架要包含业务开发常用到的库,并且能够无感知的自动监控数据上报,实现一些自动化能力,Ngo框架在这样的背景下诞生了。
ngo主要的目标包括
提供比原有Java框架更高性能和更低资源占用率。
为业务提供所需全部工具库。
接入监控,业务无需开发即可自动上传监控数据。
自动加载配置和初始化程序环境,开发者能直接使用各种库。
与线上的健康检查、运维接口等运行环境匹配,无需用户手动开发配置。
ngo目前已经支持了Redis、Kafka、memcache等绝大部分的中间件和数据库客户端的SDK,也支持了监控、配置中心、openID、xxljob等基础服务。
以下笔者将对最基础的服务作一些介绍。
一个可运行的应用,除了业务逻辑本身之外,还要能够监控应用的当前状态,比如:应用是否在线、是否健康、启动完成后需要做哪些动作、停止之前需要做哪些动作等等。
ngo为应用提供了统一的健康检查入口(类似于SpringBoot的Actuator),应用通过这些健康检查接口,可以查看应用当前的健康状态。
ngo预制了以下四个检查入口
/health/online:流量灰度中容器上线时调用,允许服务开始接受请求。
/health/offline:流量灰度中容器下线时调用,停止接收请求。
/health/check:提供k8s liveness探针,展示当前进程存活状态,如果检查失败,POD将会重启。
/health/status:提供k8s readiness探针,表明当前服务状态,是否能提供服务,如果检查失败,EndPoint将会被删除,不会对外提供服务。
下线的接口,当K8S调用offline接口时,ngo首先会检查当前的请求是不是已经全部处理完成了,如果全部处理完成,则返回200,否则,ngo将会返回400,默认情况下,K8S会继续探测,直到返回200,这样也就实现了业务的优雅停服,不会因为应用的停服导致大量请求失败或在处理的请求被强制中断。
func (s *Server) offlineHandler(c *gin.Context) {
atomic.StoreInt32(&s.active, 0) //active置为0,表示应用不可用
if s.requestsFinished() { //当前的处理请求是否已经完成
c.String(http.StatusOK, "ok")
log.Info("Server offline requested!")
} else {
c.String(http.StatusBadRequest, "bad")
log.Info("Server offline failed!")
}
}
应用上线接口的预置逻辑,当K8S调用online接口时,ngo将框架的预置变量active置为1,并返回200,表示应用上线完成,可以接收流量。
func (s *Server) onlineHandler(c *gin.Context) {atomic.StoreInt32(&s.active, 1) //active置为1,表示应用可接收流量
c.String(http.StatusOK, "ok")
log.Info("Server online requested!")
}
应用check接口的预置逻辑,当调用check时,会调用healthz接口,判断是否正常,接下来将检查server服务的健康状态。
func (s *Server) checkHandler(c *gin.Context) {ss := Get()
if !ss.Healthz() { //查看http://localhost/healthz接口的状态
return
}
if s.healthy != nil && !s.healthy() { //查看当前Server的状态
c.String(http.StatusForbidden, "error")
return
}
c.String(http.StatusOK, "ok")
log.Info("Server check requested!")
}
应用status接口的预置逻辑比较简单,就是查看当前active的状态是不是为1。
func (s *Server) statusHandler(c *gin.Context) {if atomic.LoadInt32(&s.active) == 1 {
c.String(http.StatusOK, "ok")
} else {
c.String(http.StatusForbidden, "error")
}
}
ngo预置了应用的一些健康检查基本接口,同时,框架也支持业务增加自己的一些特殊逻辑,让我们看看ngo框架支持的一些扩展点。
ngo支持在应用启动前或停止时增加预置逻辑,也支持增加自己的check逻辑。
app.PreStart():启动前执行的函数,使用方可以在这里执行一些自定义的初始化。
app.AfterStop():关闭后执行的函数,使用方可以在这里执行一些自定义的资源释放操作。
具体代码如下
func main() {
app := ngo.Init()
s := http.Get()
....
app.PreStart = func() error { //应用启动前执行的业务逻辑
return nil
}
app.AfterStop = func() error { //应用停止时执行的业务逻辑
return nil
}
admin.Get().SetHealthyFn(func() bool {return true}) //应用增加的check业务逻辑
app.Start()
...
}
当应用接收到操作系统的shutdown signal时,将执行停止逻辑,具体代码如下
func (a *application) waitSignals()
{signals.Shutdown(func(grace bool) {
if grace {
a.GracefulStop()
} else {
a.Stop()
}
})
}
ngo通过监听syscall.SIGQUIT、syscall.SIGINT、syscall.SIGTERM任意一个信号进行关服,服务在stop时,会关闭内置组件http server、pprof server、redis client、kafka client、httplib、 tracer、 xxljob。
笔者已将ngo框架对健康检查的逻辑进行了阐述,大家可以看到,一个健壮应用的健康检查逻辑是很复杂的,如果做到无损、平滑的启动停止都需要做很多的工作,接入ngo后,这些逻辑都已经被封装,大大降低了开发的工作量。
随着业务复杂度的增加,开发者会根据业务划分出多个微服务,一个对外接口请求可能就需要多个微服务协同完成,如果一个服务出现问题,就可能导致接口异常。面对这种情况,我门需要追踪整个链路,看具体发生了什么,问题出在哪里。分布式追踪系统就是我们排查系统问题和系统性能的重要工具。
市场上大部分产品都支持opentracing规范,例如jaeger、zipkin、skywalking以及传媒自研的optimus,同时也存在非opentracing规范的产品,比如pinpoint。
ngo的tracing模块整合了这两类产品,提供了一个统一的接口,支持多种tracing方案,用户可以无需更改代码快速切换。
核心API如下
type Tracer interface {
Enabled() bool
Type() string
StartSpanFromCarrier(ctx context.Context, op string, carrier interface{}) (Span, context.Context)
StartSpanFromContext(ctx context.Context, op string) (Span, context.Context)
Inject(ctx context.Context, carrier interface{})
SetSamplingRate(rate int)
Stop()
}
type Span interface {
SetTag(key string, value interface{}) Span
Finish()
GetTraceId() string
}
func SpanFromContext(ctx context.Context) Span {
v := ctx.Value(contextKey)
if v != nil {
return v.(Span)
}
return nil
}
func ContextWithSpan(ctx context.Context, span Span) context.Context {
return context.WithValue(ctx, contextKey, span)
}
func GetTraceId(ctx context.Context) string {
span := SpanFromContext(ctx)
if span == nil {
return ""
}
return span.GetTraceId()
}
整体采集链路如下
同时提供ngo内部组件的采集支持,包含
httpserver
db
redis
httpclient
这里拿httpserver举例,实现为gin的middleware
func ServerTraceMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if tracing.Enabled() && !strings.HasPrefix(c.Request.RequestURI, "/health") {
// 生成根span和新ctx 新ctx 包含span信息,需要向下传递
span, ctx := tracing.StartSpanFromCarrier(c.Request.Context(), "server", c.Request.Header)
tracing.SpanKind.Set(span, "server")
tracing.SpanType.Set(span, "HTTP_SERVER")
tracing.HttpServerRequestUrl.Set(span, c.FullPath())
tracing.HttpServerRequestHost.Set(span, c.Request.Host)
tracing.HttpServerRequestMethod.Set(span, c.Request.Method)
if remoteIp, remotePortStr, err := net.SplitHostPort(c.Request.RemoteAddr); err == nil {
tracing.HttpServerPeerHost.Set(span, remoteIp)
if remotePort, err := strconv.Atoi(remotePortStr); err == nil {
tracing.HttpServerPeerPort.Set(span, uint16(remotePort))
}
}
tracing.HttpServerRequestPath.Set(span, c.Request.URL.Path)
tracing.HttpServerRequestSize.Set(span, c.Request.ContentLength)
c.Request = c.Request.WithContext(ctx)
defer func() {
code := c.Writer.Status()
tracing.HttpServerResponseStatus.Set(span, uint16(code))
tracing.HttpServerResponseSize.Set(span, c.Writer.Size())
span.Finish()
}()
}
c.Next()
}
}
Apollo的接入有两种方式
方式一:直接使用外部sdk(官方sdk/第三方sdk),通过调用sdk api完成apollo的接入。
方式二:使用ngo配置模块的apollo数据源。
ngo 配置模块提供读取配置的统一API,支持从本地文件、远程数据源两种方式读取配置,并且支持监听配置变化,发送事件。本地文件支持父子文件include方式接入,远程数据源包含携程的apollo、etcd、网易配置中心,并且提供了数据源扩展接口,方便用户对接其他数据源。
接入方式也比较简单,ngo对外提供了-c和-w两个参数负责配置模块的初始化,其中
-c 参数指定数据源,格式为{schema}://{addr},schema为数据源类型,不指定默认为本地文件file。
-w 参数为bool类型,默认为false,表示不监听配置变化,如果设置为true,则监听配置变化,并且发送事件。
apollo数据源配置示例
-c apollo://host:port?appId=xx&cluster=xxx&namespaceNames=xx.yaml,xx.yaml -w true
apollo数据源实现了config datasource的接口,并且我们支持apollo多namespace的properties和yaml两种格式的读取
type DataSource interface {ReadConfig() ([]byte, error)
IsConfigChanged() <-chan struct{}
io.Closer
}
方式二相对方式一有明显的优势
在使用层面,用户不需要理解第三方api,直接使用全局内置的配置模块api即可。
在设计层面,ngo的配置模块可以快速的增加其他数据源,用户在切换数据源时不需要修改代码。
ngo提供了OpenIdClient的支持,OpenIdClient提供了获取token,根据token获取用户信息等接口,用户可以很方便的对接openid来完成自己的认证登录,不过一般情况系统会很少直接使用openid的token来作为前后端的传输凭证,大部分系统都会自己维护登录状态,有的用session、有的用token。
这里ngo提供了jwt+openid的登录模式,jwt用于对外认证,openid负责提供登录入口,大体流程如下
认证会以middeware方式进行集成
httpServer:
middlewares:
jwtAuth:
enabled: false -- 这里默认为false,需要改成true开启认证
authHeader: Authorization --默认认证头
tokenType: Bearer --默认value前缀
accessTokenExpiresIn: 3600 --默认访问token有效时间,单位s
refreshTokenExpiresIn: 7200 --默认刷新token有效时间,单位s
encryption: HS256 --默认加密方式
oidc: -- https://login.netease.com/sitemgnt/create/ 建立新站点,会生成id和secret
clientId: xxxxxxxxx
clientSecret: xxxxxxxxx
encryption: HS256
routePathPrefix: "" --内置接口前缀,默认为空
ignorePaths:
- /xxx --忽略路径,这里为前缀匹配
并且提供内置接口,用户可以使用配置中的routePathPrefix来添加和系统格式匹配的通用前缀。
获取token, detail=true 会调oidc获取用户信息
GET /auth/access-token?code=&redirectUri=&detail=true|false
刷新token,用户可以在临近失效时进行token刷新,防止使用过程中失效跳出登录页
GET /auth/refresh-token?refreshToken=
Header
Authorization: Bearer {accessToken}
考虑到有些业务要根据用户去后台做二次验证,这里提供了扩展方法,用户可以实现Authenticator,然后调用server的AddAdminAuthHandler添加进去,同时该接口也可以自定义响应信息,给用户提供了足够的灵活性
// AddAdminAuthHandler 扩展处理方法
// param auth openid通过后的回调,用来验证业务后台用户逻辑,返回的第二个参数需要存入token的信息,返回的信息尽量少,否则token很长
// param gtRsp 获取token和刷新token后回调的方法,用来自定义响应报文格式
// param unauthRsp token验证失败的回调方法,用来自定义响应报文格式
func (s *Server) AddAdminAuthHandler(auth Authenticator, gtRsp GenTokenResponse, unauthRsp UnauthenticatedResponse) *Server
当然,如果不想使用内置接口,可以自己添加路由,使用系统内置函数即可。
笔者将先介绍下最常使用的服务,更多的服务,可以从Git中获取最新的代码及说明。
ngo的git地址如下:
https://github.com/NetEase-Media/ngo
--End--