Golang

Gin源码简要分析

概述 #

通过日常对gin场景出发,深入源码,总结介绍gin的核心设计。包含:Engine / HandlerFunc / RouterGroup(Router) / Context。在日常使用中常见的就以上概念,汇总如下:

概念 解释 应用意义
Engine 引擎 web server的基础支持,也是服务的入口 和 根级数据结构
RouterGroup(Router) 路由 用于支持gin,REST路由绑定和路由匹配的基础,源于radix-tree数据结构支持
HandlerFunc 处理函数 逻辑处理器和中间件实现的函数签名
Context 上下文 封装了请求和响应的操作,为HandlerFunc的定义和中间件模式提供支持

从DEMO开始 #

type barForm struct {
    Foo string  `form:"foo" binding:"required"`
    Bar int     `form:"bar" binding:"required"`
}

func (fooHdl FooHdl) Bar(c *gin.Context) {
    var bform = new(barForm)
    if err := c.ShouldBind(bform); err != nil {
        // true: parse form error
        return
    }

    // handle biz logic and generate response structure
    // c (gin.Context) methods could called to support process-controling

    c.JSON(http.StatusOK, resp)
    // c.String() alse repsonse to client
}

// mountRouters .
func mountRouters(engi *gin.Engine) {
    // use middlewares
    engi.Use(gin.Logger())
    engi.Use(gin.Recovery())
    
    // mount routers
    group := engi.Group("/v1")
    {
        fooHdl := demohtp.New()
        group.GET("/foo", fooHdl.Bar)
        group.GET("/echo", fooHdl.Echo)
        // subGroup := group.Group("/subg")
        // subGroup.GET("/hdl1", fooHdl.SubGroupHdl1) // 最终路由:"targetURI = /v1/subg/hdl1"
    }
}

func main() {
    engi := gin.New()

    mountRouters(engi)

    if err := engi.Run(":8080"); err != nil {
        log.Fatalf("engi exit with err=%v", err)
    }
}

通过上述的代码就简单开启了一个gin server,其中就包括了常见的:路由注册,中间件注册,路由分组,服务启动。核心概念也就是刚刚在上文提到的那四个概念。概览流程如下图:

...

一次gRPC使用不当导致goroutine泄漏排查记录

由于保留必要的“罪证”,因此某些异常只能通过文字来描述了~

背景 #

昨晚上10点左右,前端童鞋反映开发环境接口响应超时,但过了几分钟后又恢复了,于是有了这一篇文章。

其实很久以前就出现了内存占用异常的情况~,只是占用并不高也就是50MB左右,加上当时还忙着写业务需求就没有急着加上pprof来检查。

首先通过运维平台(k8s based)直观发现了该pod数量从1变成了2, 再结合新增pod的启动时间,我发现该时间正好是前端童鞋反映状况的时间节点,稍后我检查了下该服务的资源限制如下图:

那么前端童鞋反映的问题就很明显了,由于某种原因导致了pod内存超限,触发了运维平台对于内存超限的“容忍机制”。表现为: 新增一个pod用于缓解服务压力,老服务由于无法申请更多内存会导致崩溃或其他异常(无法响应客户端请求),这与反映的情况一致。

pprof排查 #

知道了服务内存异常,想要具体定位的话,这时候就需要pprof上场了。

如果你需要重启服务才能开启pprof的话,那么只能等待复现了。这里我在开发环境和测试环境一直开启了pprof,因此可以直接检查。个人觉得,这样还可以帮助开发和测试,完成最初的性能分析😼。

内存检查 #

go tool pprof --http=:8080 https://service.host.com/debug/pprof/heap

这个命令是在本地打开一个web服务,直接可视化该服务的内存占用情况。也可以使用:

go tool pprof https://service.host.com/debug/pprof/heap 使用交互模式来分析。通过这个步骤定位到了 grpc相关的包内存占用异常分为两个部分:

50MB+ google.golang.org/grpc/internal/transport.newBufWriter
50MB+ bufio.NewReaderSize
http2 相关库的占用也比较多

这一切都指向了我们使用的gRPC,可是为啥使用gRPC会用到这么“多”内存呢?接着分析

goroutine检查 #

打开一看 https://service.host.com/debug/pprof/一看,goroutine和heap居“高”(4000+)不下,虽然对于动辄10W+的别人家的服务来说,这点根本不算事,但在我们这种小作坊里可就算异常了。点开看goroutine查看详情,有四个部分的goroutine分别有900个左右,这里就算初步定位了“gRPC客户端使用了较多的goroutine,但是却没有正确的结束掉”,如下图(这是解决后的截的图):

pprof总结 #

服务中使用的gRPC客户端出了某些故障,导致了goroutine泄漏,引发了OOM(Out Of Memory)。如下图:

代码排查 #

上一步已经定位到是gRPC客户端的问题,那么就可以直接从代码上手了。我心里已经有一个“嫌疑犯”了,如下:

var (
    defaultHandler *handler
    timeout        = 5 * time.Second
    // _              pb.UserServiceClient = &handler{}
)

// Init of usersvc.handler
func Init(rpcAddr string) error {
    // ... 略去不表
}

type handler struct {
    // rpc configs
    rpcAddr          string
    client           pb.UserServiceClient
    lastGrpcReqError error
}

func (h *handler) connectRPC() {
    if h.client != nil && h.lastGrpcReqError != nil {
        // 这里判断的本意是:如果客户端初始化失败,
        // 或者期间因为异常情况,导致客户端与服务端连接中断的情况下尝试重连。
        // 
        // 但是忽略了gRPC实现中,对于客户端的处理:
        // 1. grpc.Dail 是异步的
        // 2. grpc 有自己的重连机制
        // 
        // 这一部分我还没有看完,就不乱发表看法了。
        conn, err := grpc.Dial(h.rpcAddr, grpc.WithInsecure())
        if err != nil {
            logger.Std.Errorf("could not dial: %s with err: %v", h.rpcAddr, err)
            return
        }

        logger.Std.Infof("usersvc.client.connectRPC called")
        h.client = pb.NewUserServiceClient(conn)
    }
}

// QueryBasicInfoByID based default Handler .
func QueryBasicInfoByID(in *pb.ByIDForm) (*pb.BasicInfoResponse, error) {
    defaultHandler.connectRPC()
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    resp, err := defaultHandler.client.QueryBasicInfoByID(ctx, in)
    defaultHandler.lastGrpcReqError = err
    return resp, err
}

抛开本意不谈,这样的写法也是不OK的。。。因为

...

介绍一下snowflake和rc4

snowflake是twitter公司开源的生成唯一ID的网络服务,具有很强的伸缩性,这里只取用生成唯一ID的算法部分。 rc4(Rivest Cipher 4)是一种流加密算法,密钥长度可变,它的加解密使用相同的密钥,因此也属于对称加密算法。

为啥要介绍这两种算法? #

其一,snowflake可以生成唯一ID,而相比与UUIDsnowflake生成的ID更加“好用”,这个放在后面解释。 其二,UUIDsnowflake虽然可以生成唯一ID,但是无法适用于所有场景,譬如说“生成推广码”。生成推广码的时候,希望尽可能短而精,很明显唯一ID都不太短。

snowflake #

snowflake的唯一ID是一个64bit的int型数据,相较于UUID来说耗费空间更小,可以更方便的作为数据库主键来索引和排序。

snowflake

生成过程: #
  • 置0不用
  • timestamp(41bits)精确到ms。
  • machine-id(10bits)该部分其实由datacenterId和workerId两部分组成,这两部分是在配置文件中指明的。datacenterId(5bits)方便搭建多个生成uid的service,并保证uid不重复。workerId(5bits)是实际server机器的代号,最大到32,同一个datacenter下的workerId是不能重复的。
  • sequence-id(12bits),该id可以表示4096个数字,它是在time相同的情况下,递增该值直到为0,即一个循环结束,此时便只能等到下一个ms到来,一般情况下4096/ms的请求是不太可能出现的,所以足够使用了。
优势和缺陷: #
  • 速度快,无依赖,原理和实现简单,也可以根据自己的需求做算法调整
  • 依赖机器时间,如果时间回拨可能导致重复的ID

rc4 #

RC4加密算法也是一种基于密钥流的加密算法。

首先,rc4根据明文和密钥生成的密钥流,其长度和明文的长度是相等的,也就是说明文的长度是500字节,那么密钥流也是500字节,这也是我们用来生成推广码的原因之一了;其次,rc4是是对称加密完全可以通过密文得到明文,也就是说在生成码的时候把必要信息放在明文中,在使用密文的时候可以不用查库也能得到相关的信息,譬如用户ID,这是原因之二。

使用场景 #

现在需要生成一种码,短小易记,且唯一,但并不需要大量。

上述的snowflake和UUID都很容易实现唯一,但是短小就不符合要求了。因为并不需要大量生成这种码,因此我们考虑用自增ID + RC4来实现:

package main

import (
	"crypto/rc4"
	"encoding/hex"
	"fmt"
	"log"
)

func main() {
	cipher, err := rc4.NewCipher([]byte("thisiskey"))
	if err != nil {
		log.Fatalf("wrong with NewCipher: %v", err)
	}

	c := map[string]bool{}
	for i := 0; i < 1000; i++ {
		src := []byte(fmt.Sprintf("%7d", i))
		dst := make([]byte, len(src))
		cipher.XORKeyStream(dst, src)
		s := toString(dst) // 密文是不可读的字节流,这里采用hex编码
		println(s)         // 形如:a09def6b6e4797
		c[s] = true
	}

	println(len(c)) # 1000
}

func toString(src []byte) string {
	return hex.EncodeToString(src)
}

参考 #

go-watcher-热重载轮子

Golang编写的热重载工具,自定义命令,支持监视文件及路径配置,环境变量配置。这是一个重复的轮子~

安装使用 #

go install github.com/yeqown/go-watcher/cmd/go-watcher

命令行 #

➜  go-watcher git:(master) ✗ ./go-watcher -h   
NAME:
   go-watcher - A new cli application

USAGE:
   go-watcher [global options] command [command options] [arguments...]

VERSION:
   2.0.0

AUTHOR:
   [email protected]

COMMANDS:
     init     generate a config file to specified postion
     run      execute a command, and watch the files, if any change to these files, the command will reload
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

配置文件 #

watcher:                   # 监视器配置
  duration: 2000           # 文件修改时间间隔,只有高于这个间隔才回触发重载
  included_filetypes:      # 监视的文件扩展类型
  - .go                    # 
  excluded_regexps:        # 不被监视更改的文件正则表达式
  - ^.gitignore$
  - '*.yml$'
  - '*.txt$'
additional_paths: []       # 除了当前文件夹需要额外监视的文件夹
excluded_paths:            # 不需要监视的文件名,若为相对路径,只能对于当前路径生效
- vendor
- .git
envs:                      # 额外的环境变量
- GOROOT=/path/to/your/goroot
- GOPATH=/path/to/your/gopath

使用范例日志 #

➜  go-watcher git:(master) ✗ ./package/osx/go-watcher run -e "make" -c ./config.yml
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/cmd) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/cmd/go-watcher) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/command) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/log) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata/exclude) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/internal/testdata/testdata_inner) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/resources) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils/testdata) is under watching
[INFO] directory (/Users/yeqown/Projects/opensource/go-watcher/utils/testdata/testdata_inner) is under watching
rm -fr package
go build -o package/osx/go-watcher cmd/go-watcher/main.go
GOOS=linux GOARCH=amd64 go build -o package/linux/go-watcher cmd/go-watcher/main.go
mkdir -p package/archived
tar -zcvf package/archived/go-watcher.osx.tar.gz package/osx
a package/osx
a package/osx/go-watcher
tar -zcvf package/archived/go-watcher.linux.tar.gz package/linux
a package/linux
a package/linux/go-watcher
[INFO] command executed done!
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx/go-watcher) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux/go-watcher) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived/go-watcher.linux.tar.gz) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/VERSION) is skipped, not target filetype
[INFO] [/Users/yeqown/Projects/opensource/go-watcher/cmd/go-watcher/main.go] changed
rm -fr package
mkdir -p package/osx
mkdir -p package/linux
echo "2.0.0" > VERSION
cp VERSION package/osx
cp VERSION package/linux
go build -o package/osx/go-watcher cmd/go-watcher/main.go
GOOS=linux GOARCH=amd64 go build -o package/linux/go-watcher cmd/go-watcher/main.go
mkdir -p package/archived
tar -zcvf package/archived/go-watcher.osx.tar.gz package/osx
a package/osx
a package/osx/go-watcher
a package/osx/VERSION
tar -zcvf package/archived/go-watcher.linux.tar.gz package/linux
a package/linux
a package/linux/go-watcher[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/linux) is skipped, not target filetype

a package/linux/VERSION
[INFO] command executed done!
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/osx) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package/archived) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/VERSION) is skipped, not target filetype
[INFO] (/Users/yeqown/Projects/opensource/go-watcher/package) is skipped, not target filetype
^C[INFO] quit signal captured!
[INFO] go-watcher exited
➜  go-watcher git:(master)

goswagger入门手册

本文旨在记录使用goswagger过程中遇到的一些问题(只在生成文档方面,不涉及其他功能):

  • 如何在go1.11+以上(支持Go Module)版本中的应用swagger
  • 一些注解上的注意事项
  • 如何在团队中管理API文档(主要涵盖了:swagger-ui的部署和使用)

关于swagger #

swagger涵盖WebAPI的一整套工具:API设计,API实现(代码生成),API文档,API测试及API规范。更多信息请参见官网

准备工作 #

  • 一个Golang web项目,并软连接到GOPATH/src下。【毕竟是支持Gomodule的项目,还放在GOPATH下就不科学了😄】
  • 安装swagger工具. 参见安装
  • 环境:
    ➜  swagger-demo git:(master) ✗ go version
    go version go1.11.5 darwin/amd64
    ➜  swagger-demo git:(master) ✗ swagger version
    version: v0.18.0
    commit: 6b23bb61413826ce42c3b14a37bf5870caf91e0b
    

编写注释 #

元信息包含了这个应用的基本信息。一般新建一个doc.go放在你的API根目录下;还有一定要注意这句话:

You give it a main file and it will parse all the files that are reachable by that main package to produce a swagger specification.To use you can add a go:generate comment to your main file。

...

QRCode Generator based Golang

项目地址:yeqown/go-qrcode 同类项目:skip2/go-qrcode 纠错算法和bitset使用了该库,后续可能会考虑自己实现一遍

go-qrcode #

示例 #

link to CODE

package main

import (
	"fmt"

	qrcode "github.com/yeqown/go-qrcode"
)

func main() {
	qrc, err := qrcode.New("https://github.com/yeqown/go-qrcode")
	if err != nil {
		fmt.Printf("could not generate QRCode: %v", err)
	}

	// save file
	if err := qrc.Save("../testdata/repo-qrcode.jpeg"); err != nil {
		fmt.Printf("could not save image: %v", err)
	}
}

生成结果如图:

QR Code 基本原理 #

1 数据分析(data analysis): #

分析输入数据,根据数据决定要使用的QR码版本、容错级别和编码模式。低版本的QR码无法编码过长的数据,含有非数字字母字符的数据要使用扩展字符编码模式。

2 编码数据(data encoding): #

根据选择的编码模式,将输入的字符串转换成比特流,插入模式标识码(mode indicator)和终止标识符(terminator),把比特流切分成八比特的字节,加入填充字节来满足标准的数据字码数要求。

3 计算纠错码(error correction coding): #

对步骤二产生的比特流计算纠错码,附在比特流之后。高版本的编码方式可能需要将数据流切分成块(block)再分别进行纠错码计算。

...

go-get遇到🧱的解决方法

解决方法有两种,在网上也很好找到:

1. 最简单的,从¥“github.com/golang”找到对应的包并下载到 $GOPATH/src/golang.org/x/ 下
2. 第二种,就是翻过🧱了。

1 问题:翻了🧱,还是没办法直接使用go get来下载呢? #

先说原因,因为go get并没有走你的代理啊!!!!那么如何设置代理呢?

export http_proxy=http://ip:port
go get golang.org/xxx

其他设置代理的方式,自行参见 参考

2 如果你的代理不支持http || https协议,可咋整? #

那么想办法支持http或者把http再代理到你可以使用的协议(socks5~),那么可以使用cow

cow 推荐使用方式:

go get 下载安装(因为刚开始图简单,使用程序的时候运行报错了,go get 安装方式并不会有这样的困扰)。 配置的时候也很简单,编辑配置文件 ~/.cow/rc,配置http socks5代理服务和监听代理端口。

listen = http://127.0.0.1:7777
proxy = socks5://127.0.0.1:1080

3 运行 #

配置完成之后就可以直接运行了

    ./cow
    # 另开一个Terminal
    export http_proxy=http://ip:port
    go get golang.org/xxx

写在后面 #

知道了go get无法翻🧱的原因之后,就可以发挥自己的想象力来解决问题了,这样解决还是挺繁琐的。虽然cow可以配置开机启动,但对于一个懒癌晚期)的人来说(如果不是因为升级Go到了1.11,go-module机制让我无法开心的玩耍,我也不会去考虑为啥翻🧱了go get还是不能用,明明有代理却还有使用另外一个代理~。

友情提示:vscode-go 也可以设置proxy哦

参考: #

etcd与service-registration-discovery

声明:本文对etcd的原理,实现细节,性能等均不考虑,仅将etcd作为一个分布式的K-V存储组件。本文提价代码均在: github.com/yeqown/server-common/tree/master/framework/etcd

一个核心 #

etcd, 分布式Key-Value存储工具。详细资料由此去

两个对象 #

  • 服务提供者(在测试环境中,我定义为单独的服务实例),也就是服务的提供者,需要向其他服务暴露自己的ip和端口,方便调用。
  • 服务调用者(同样地,在测试环境中我定义为反向代理网关程序),也就是服务的调用者,需要获取到 可使用 地服务地址并调用。

关于服务注册与发现 #

就具体场景而言:我们的生产环境中使用了一个代理网关服务器,用于转发移动端和PC端的API请求,并完成其他功能。所有的服务实例配置都是硬编码在网关程序中,顶多就是抽离出来成了一个配置文件。这样做的缺点很明显:“非动态”。也就意味着,一旦有服务Down掉,那么用户访问则可能异常,甚至导致整个服务的崩溃;其次,需要对服务进行扩容的情况下,则需要先进行服务部署再更新网关程序,步骤繁琐且容易出错。

那么如果我们设计成为如下图的样子: etcd-service-regisration-discovery 对于新添加的服务实例,只需要启动新的服务,并注册到etcd相应的路径下就行了。

注册:对于同一组服务,配置一个统一的前缀(如图上的"/specServer"),不同实例使用ID加以区分。

将现行服务改造成为上述模式需要解决的问题: #

  • etcd 配置安装
  • 网关程序改造(监听etcd的节点夹子/prefix;适配动态的服务实例调用)
  • 服务实例改造(注册服务实例到etcd;心跳更新;其他配套设施,异常退出删除注册信息)

etcd安装配置在github.com已经非常详细了。在这里贴一下我在本地测试时候启动的脚本(这部分是从etcd-demo获取到的,做了针对端口的改动):

#!/bin/bash

# For each machine
TOKEN=token-01
CLUSTER_STATE=new
NAME_1=machine1
NAME_2=machine2
NAME_3=machine3
HOST_1=127.0.0.1
HOST_2=127.0.0.1
HOST_3=127.0.0.1
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2381,${NAME_3}=http://${HOST_3}:2382

# For machine 1
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
etcd --data-dir=machine1.etcd --name ${THIS_NAME} \
	--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
	--advertise-client-urls http://${THIS_IP}:2377 --listen-client-urls http://${THIS_IP}:2377 \
	--initial-cluster ${CLUSTER} \
	--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN} &

# For machine 2
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
etcd --data-dir=machine2.etcd --name ${THIS_NAME} \
	--initial-advertise-peer-urls http://${THIS_IP}:2381 --listen-peer-urls http://${THIS_IP}:2381 \
	--advertise-client-urls http://${THIS_IP}:2378 --listen-client-urls http://${THIS_IP}:2378 \
	--initial-cluster ${CLUSTER} \
	--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN} & 

# For machine 3
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
etcd --data-dir=machine3.etcd --name ${THIS_NAME} \
	--initial-advertise-peer-urls http://${THIS_IP}:2382 --listen-peer-urls http://${THIS_IP}:2382 \
	--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
	--initial-cluster ${CLUSTER} \
	--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN} &

对于程序的改造,鉴于服务较多且etcd操作流程大体一致,便简单包装了一下,项目地址见文首位置。

...

Golang适用的DTO工具

DTO (Data Transfer Object) 是Java中的概念,起到数据封装和隔离的作用。在使用Golang开发Web应用的过程中,也会有类似的需求。先贴项目地址 github.com/yeqown/server-common/tree/master/dbs/tools

举个例子 #

现在有一个用户数据结构如下,

type UserModel struct {
    ID          int64   `gorm:"column:id"`
    Name        string  `gorm:"column:name"`
    Password    string  `gorm:"column:password"`
}

// 问题1: 现在要求是想要JSON格式返回用户数据,并且不希望其中包含有Password字段 // 解决1:

type UserModel struct {
    ID          int64   `gorm:"column:id" json:"id"` 
    Name        string  `gorm:"column:name" json:"name"`
    Password    string  `gorm:"column:password" json:"-"`
}

// 问题2: 同样是JSON数据格式,并且希望额外返回用户的身份标示Ident(假设必须要跟用户数据放在一起) // 解决2: (这也是我的场景)

type UserDTO struct {
    ID          int64   `json:"id"` 
    Name        string  `json:"name"`
    Password    string  `json:"-"`
    Ident       string  `json:"ident"`
}

func LoadUserDTOFromModel(data *UserMolde) *UserDTO {
    ident := genUserIdent(data)
    return &{
        ID          data.ID,
        Name        data.Name,
        Ident       ident,
    }
}

背景和需求 #

一般来说我的项目结构如下:其中models和services也就是分开定义Data struct(UserModel)和Object(UserDTO)的文件夹。

Web项目结构举例

其实DTO的过程对于我来说,就是基于Data Struct生成一个新的Struct结构,并附带一个func LoadDTOTypeFromModel(data *ModelType) *DTOType。在这个过程中,其实除了个别Object结构体需要额外处理以外,大部分都是新换一个tag~。因此这部分工作步骤都是类似的,那么为什么不用一个工具来避免这部分重复的工作呢~?

思路 #

先说一下思路:

...

api-gateway中实现基于权重的轮询调度

背景和目标 #

背景 #

项目需要在现有项目的基础上实现权限系统,但为了低耦合,选择实现了一个基于ne7ermore/gRBAC的auth-server,用于实现权限,角色,用户的管理,以及提供鉴权服务。在开发环境对接没有问题,正常的鉴权访问。到了线上部署的时候,才发现:

  1. 线上某服务部署在多台机器上;
  2. 目前的api-gateway并不支持同一服务配置多个node;

想的办法有:

序号 描述 优点 缺点
1 api-gateway通过url来转发请求,之前是配置IP加端口 api-gateway改动小 影响web和APP升级
2 api-gateway能支持多台机器,并进行调度 api-gateway功能更强大,把以后要做的事情提前做好基础 好像没啥缺点,只是费点时间支持下多节点配置,并调度

如果没说清,请看下图:

api-gateway-changing

目标 #

那么,目标也就明确了,需要实现api-gateway中实现基于权重的调度。为啥要基于权重?其一是仿照nginx基于权重的负载均衡,其二是服务器性能差异。

轮询调度算法介绍 #

轮询调度算法: #

轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。该算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。

假设有一组服务器N台,S = {S1, S2, …, Sn},一个指示变量i表示上一次选择的服务器ID。变量i被初始化为N-1。其算法如下:

j = i;
do {
  j = (j + 1) mod n;
  i = j;
  return Si;
} while (j != i);
return NULL;

平滑加权轮询调度算法#

上述的轮询调度算法,并没有考虑服务器性能的差异,实际生产环境中,每一台服务器配置和安装的业务并不一定相同,处理能力不完全一样。因此需要根据服务器能力,分配不同的权值,以免服务的超负荷和过分闲余。

...

访问量 访客数