前言

9 月的时候,为了后面开发准备学习一些新玩意儿,按照我的习惯,学习新东西一般比较倾向于做一个 mini project ,结合自己的需求,于是选了这个 DDNS 分线路解析作为学习的 mini project

学到的新玩意儿

  • viper 读取配置文件

之前是用 ini 包做的解析,使用 viper 可以读取 JSON 、 TOML 、YAML、 INI 在内的多种格式的配置文件,甚至是 Java 的 properties 文件

  • gin 静态文件打包

通过 Go 的 embed 包,可以把所有文件(比如前端文件和静态资源)打包进一个二进制文件运行,方便部署的同时可以规避同时打开文件数量过多的问题(公司有个老项目,java 写的,没有打成一个 jar 包,访问量大了时候经常出问题)

  • Gracefully Shutdown

Gin 可以在等待处理完当前的连接之后再关闭,这个机制叫做 Gracefully Shutdown ,需要用到 Go 的 channel

  • template

顾名思义,是“模板”,可以在不通过请求接口的情况下,拿到后端对象的数据,由于是在服务器端渲染的,客户端直接拿到的是渲染好的数据

  • xorm

和名字一样,是 orm ,在对象有大量属性的时候,手写 sql 显然是不现实的,加上公司生产环境有 pgsql 和 Oracle 两种数据库,不可能写两种 sql ,所以需要掌握 orm 的使用

  • cron

之前给定时,但是写的多少有点问题,这次也一并探究一下应该怎么写

  • gin metrics

在已经用了 gin 的情况下再用 net/http 包和 VictoriaMetrics/metrics 结合的方法就不大好了,可以用 gin metrics 导出 metrics 给 Prometheus 监控

  • swagger

接口文档,各个语言都有自己的实现,和前端联调的时候很方便

实践

viper 配置文件

和 ini 不一样, 需要先去建个模型,而不是直接拿数据了,这里我们拿出 CocoaSyncer 的一点代码简单看下,由于我们是从 yaml 配置文件里面拿数据,所以需要建立这样的模型,后面的 jsonyamlxorm 分别定义了 json 序列化,yaml 解析,xorm 建表时候的一些参数

读取配置文件的时候,定义配置文件的文件名、后缀、搜索路径,使用 viper.Unmarshal() 方法即可

type CocoaBasic struct {
// 平台相关
APIPort int `json:"apiPort" yaml:"APIPort" xorm:"'apiPort' comment('CocoaSyncer API 服务端口')"` // CocoaSyncer API 服务端口
NodeName string `json:"nodeName" yaml:"NodeName" xorm:"'nodeName' comment('CocoaSyncer 节点友好名称(WEB 展示及记忆用)')"` // CocoaSyncer 节点友好名称(WEB 展示及记忆用)
}

// read config.yaml
func readConfig(debugMode bool) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
// 校验配置文件
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalln("没有找到配置文件,请检查 ./config.yaml 是否存在!")
} else {
log.Fatalln("配置文件校验失败,请检查 ./config.yaml 是否存在语法错误")
}
}
viper.Unmarshal(&CocoaBasic)
}

gin 静态文件打包

Gracefully Shutdown

监听中断信号即可,请看以下代码片段


go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}

}()

// 等待中断信号以优雅地关闭服务器(设置 15 秒的超时时间)
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("等待最长15秒处理完剩余连接后关闭服务")

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("服务器关闭超时,强制退出,原因:", err)
}
log.Println("服务成功关闭,mafumayumayumayu~~~~")

template

服务端渲染需要前后端一起完成

后端需要把 struct 转 map[string]interface{} ,在返回 html 的时候一并返回

func ShowStatus(context *gin.Context) {
thisCocoa := &model.CocoaBasic{ConfigImported: true}
// thisCocoa.ConfigImported = true
subFunction.CocoaDataEngine.Where("configImported=true").Get(thisCocoa)

// 屏蔽掉不应该在此的输出
thisCocoa.DataBaseType = "*****"
thisCocoa.Dsn = "*****"
thisCocoa.CocoaSecret = "*****"
thisCocoa.CloudPlatformInfo = nil
thisCocoa.OtherCocoaSyncer = nil
thisCocoa.CocoaManagedService = nil

// 转 map[string]interface{} 给前端
thisCocoaJson, _ := json.Marshal(&thisCocoa)
var thisCocoaMap map[string]interface{}
_ = json.Unmarshal(thisCocoaJson, &thisCocoaMap)

// tmplate 用法:https://www.liwenzhou.com/posts/Go/template/
context.HTML(http.StatusOK, "status.tmpl", thisCocoaMap)
}

前端用花括号 .变量名 的方式来取

    <title>CocoaSyncer - {{.nodeName}}</title>
</head>
<body style="background-image: url(&quot;/static/sakura.png&quot;);">
<div><center>
<p>
<span>CocoaSyncer - {{.nodeName}} 已正常运行<br>Powered By Luckykeeper &lt; luckykeeper@luckykeeper.site |
<a href="http://luckykeeper.site" target="_blank">http://luckykeeper.site</a> &gt;</span>
</p>
<p>
<span>本节点信息如下:</span>
</p>
<p>
<table>
<tr>
<td class="CocoaProperties">数据最近一次更新时间</td>
<td class="CocoaValues">{{.updatedAt}}</td>
</tr>
</table>
</body>

效果可以看线上的这个例子:https://cocoasyncer.luckykeeper.site:44443/v1/status

xorm

通过 DSN 传连接信息,Oracle 需要取消引号,避免大小写导致的 SQL 问题,使用 Engine.Dialect().SetQuotePolicy(1) 即可(使用引号会限定大小写)


// 初始化数据库
func InitializeDatabase(databaseType, dsnInfo string, debugMode bool) {
// 防止空指针问题,这样声明 err
// 见:https://stackoverflow.com/questions/56396386/xorm-example-not-working-runtime-error-invalid-memory-address-or-nil-pointer
var err error
CocoaDataEngine, err = xorm.NewEngine(databaseType, dsnInfo)
if err != nil {
log.Fatalln("数据库初始化失败:", err)
}

// 取消大小写敏感
// https://pkg.go.dev/xorm.io/xorm@v1.0.1/dialects#QuotePolicy
// https://www.cnblogs.com/bartggg/p/13066944.html
CocoaDataEngine.Dialect().SetQuotePolicy(1)

// 使用缓存
cacher := caches.NewLRUCacher(caches.NewMemoryStore(), 1000)
CocoaDataEngine.SetDefaultCacher(cacher)

// 不需要 Close ,orm 会自己判断
// defer CocoaDataEngine.Close()

if debugMode {
CocoaDataEngine.ShowSQL(true)
}
err = CocoaDataEngine.Ping()
if err != nil {
log.Panicln("数据库连接失败!检查数据库信息是否正确,程序返回原因为:", err)
} else {
CocoaDataEngine.SetConnMaxLifetime(time.Second * 60) // 最大连接存活时间
CocoaDataEngine.SetMaxOpenConns(100) // 最大连接数
CocoaDataEngine.SetMaxIdleConns(3) // 最大空闲连接数
log.Println("连接到远程数据库成功!")
}
// log.Println("同步数据表中……")
// err = CocoaDataEngine.Sync(new(model.CocoaBasic))
// if err != nil {
// log.Fatalln("同步数据表失败:", err)
// }
// log.Println("数据库同步成功!")
}

cron

cron 的方法需要在 Gin 启动之前完成初始化,建议在 Init 阶段做这个事情

gin metrics

非常简单,首先在路由组添加一个 handler ,然后把业务数据一并导出即可

	handler.PrometheusExporter(router)

func PrometheusExporter(router *gin.Engine) {
// get global Monitor object
monitor := ginmetrics.GetMonitor()

// +optional set metric path, default /debug/metrics
monitor.SetMetricPath("/metrics")
// +optional set slow time, default 5s
monitor.SetSlowTime(5)
// +optional set request duration, default {0.1, 0.3, 1.2, 5, 10}
// used to p95, p99
monitor.SetDuration([]float64{0.1, 0.3, 1.2, 5, 10})

// cocoaSyncer 相关信息
thisCocoa := model.CocoaBasic{ConfigImported: true}
subFunction.CocoaDataEngine.Get(&thisCocoa)

cocoaSyncerStatus := &ginmetrics.Metric{
// 注意中间不能有空格
Type: ginmetrics.Gauge,
Name: "cocoaSyncerStatus",
Description: "cocoaSyncer运行状态",
Labels: []string{"cocoaSyncerStatus"},
}

// Add metric to global monitor object
_ = ginmetrics.GetMonitor().AddMetric(cocoaSyncerStatus)

_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"NodeNumber"}, float64(len(thisCocoa.OtherCocoaSyncer)))
_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerManagedService"}, float64(len(thisCocoa.CocoaManagedService)))
_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerCloudPlatformInfo"}, float64(len(thisCocoa.CloudPlatformInfo)))

nodeOnlineCount := 0
nodeOfflineCount := 0
for _, node := range thisCocoa.OtherCocoaSyncer {
if node.StatusCode == 200 {
nodeOnlineCount++
} else {
nodeOfflineCount++
}
}

_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerNodeOnlineCount"}, float64(nodeOnlineCount))
_ = ginmetrics.GetMonitor().GetMetric("cocoaSyncerStatus").SetGaugeValue([]string{"CocoaSyncerNodeOfflineCount"}, float64(nodeOfflineCount))

// set middleware for gin
monitor.Use(router)
}

swagger

使用这个:swaggo/gin-swagger

初始化:swag -i /router/router.go

我是比较习惯在路由组初始化这个,然后把它直接写到路由里面

然后参考他的写法在 handler 的方法前面写上注释,生成文档即可

// programatically set swagger info
docs.SwaggerInfo.Title = "CocoaSyncer Swagger Doc Center"
docs.SwaggerInfo.Description = "CocoaSyncer 在线文档 - V1"
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Host = strings.Split(thisCocoa.NodeAddress, "://")[1]
docs.SwaggerInfo.BasePath = "/v1"
docs.SwaggerInfo.Schemes = []string{"http", "https"}

if devMode {
// Swagger - V1
v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

业务逻辑

阿里云DNS提供了分线路解析的接口,只要把不同的线路解析到不同的 IP ,即可实现基于线路的 DDNS 和分流,同时节点之间做心跳,不通超过一段时间就认为是故障,正常的一方把本来是对端线路的解析改到自己头上即可。不过这样的场景很少,大家应该很少有这个需求,没有找到类似的项目,于是就自己做了一个

结语

通过这个 mini project ,学会了不少东西,可以说是给后面的 K8S 开发打下了基础,这两天我写的第一个 K8S 程序已经和前端大佬联调好了,已经发版上线啦,回头来给大家分享一下第一次 K8S 开发的一点体会